diff options
Diffstat (limited to 'app/assets/javascripts/work_items/components/work_item_notes.vue')
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_notes.vue | 258 |
1 files changed, 180 insertions, 78 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue index 02b94c5331c..092b90a5731 100644 --- a/app/assets/javascripts/work_items/components/work_item_notes.vue +++ b/app/assets/javascripts/work_items/components/work_item_notes.vue @@ -1,21 +1,36 @@ <script> import { GlSkeletonLoader, GlModal } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; -import { s__, __ } from '~/locale'; +import { uniqueId } from 'lodash'; +import { __ } from '~/locale'; +import { scrollToTargetOnResize } from '~/lib/utils/resize_observer'; import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants'; import SystemNote from '~/work_items/components/notes/system_note.vue'; -import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; -import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants'; +import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; +import { + i18n, + DEFAULT_PAGE_SIZE_NOTES, + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, +} from '~/work_items/constants'; import { ASC, DESC } from '~/notes/constants'; -import { getWorkItemNotesQuery } from '~/work_items/utils'; +import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils'; +import { + updateCacheAfterCreatingNote, + updateCacheAfterDeletingNote, +} from '~/work_items/graphql/cache_utils'; +import { getLocationHash } from '~/lib/utils/url_utility'; import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue'; +import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue'; +import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql'; +import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql'; +import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql'; import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation.graphql'; +import workItemNotesByIidQuery from '../graphql/notes/work_item_notes_by_iid.query.graphql'; import WorkItemAddNote from './notes/work_item_add_note.vue'; export default { - i18n: { - ACTIVITY_LABEL: s__('WorkItem|Activity'), - }, loader: { repeat: 10, width: 1000, @@ -24,21 +39,19 @@ export default { components: { GlSkeletonLoader, GlModal, - ActivityFilter, SystemNote, WorkItemAddNote, WorkItemDiscussion, + WorkItemNotesActivityHeader, + WorkItemHistoryOnlyFilterNote, }, + inject: ['fullPath'], props: { workItemId: { type: String, required: true, }, - queryVariables: { - type: Object, - required: true, - }, - fullPath: { + workItemIid: { type: String, required: true, }, @@ -46,18 +59,33 @@ export default { type: String, required: true, }, - fetchByIid: { + isModal: { type: Boolean, required: false, default: false, }, + assignees: { + type: Array, + required: false, + default: () => [], + }, + canSetWorkItemMetadata: { + type: Boolean, + required: false, + default: false, + }, + reportAbusePath: { + type: String, + required: true, + }, }, data() { return { isLoadingMore: false, - perPage: DEFAULT_PAGE_SIZE_NOTES, sortOrder: ASC, noteToDelete: null, + discussionFilter: WORK_ITEM_NOTES_FILTER_ALL_NOTES, + addNoteKey: uniqueId(`work-item-add-note-${this.workItemId}`), }; }, computed: { @@ -73,70 +101,135 @@ export default { hasNextPage() { return this.pageInfo?.hasNextPage; }, - showLoadingMoreSkeleton() { - return this.isLoadingMore && !this.changeNotesSortOrderAfterLoading; - }, - disableActivityFilter() { + disableActivityFilterSort() { return this.initialLoading || this.isLoadingMore; }, formAtTop() { return this.sortOrder === DESC; }, + markdownPreviewPath() { + return markdownPreviewPath(this.fullPath, this.workItemIid); + }, + autocompleteDataSources() { + return autocompleteDataSources(this.fullPath, this.workItemIid); + }, workItemCommentFormProps() { return { - queryVariables: this.queryVariables, fullPath: this.fullPath, workItemId: this.workItemId, - fetchByIid: this.fetchByIid, + workItemIid: this.workItemIid, workItemType: this.workItemType, sortOrder: this.sortOrder, + isNewDiscussion: true, + markdownPreviewPath: this.markdownPreviewPath, + autocompleteDataSources: this.autocompleteDataSources, }; }, notesArray() { const notes = this.workItemNotes?.nodes || []; + const visibleNotes = notes.filter((note) => { + const isSystemNote = this.isSystemNote(note); + + if (this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS && isSystemNote) { + return false; + } + + if (this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_HISTORY && !isSystemNote) { + return false; + } + + return true; + }); + if (this.sortOrder === DESC) { - return [...notes].reverse(); + return [...visibleNotes].reverse(); } - return notes; + return visibleNotes; + }, + commentsDisabled() { + return this.discussionFilter === WORK_ITEM_NOTES_FILTER_ONLY_HISTORY; + }, + targetNoteHash() { + return getLocationHash(); }, }, apollo: { workItemNotes: { - query() { - return getWorkItemNotesQuery(this.fetchByIid); - }, + query: workItemNotesByIidQuery, context: { isSingleRequest: true, }, variables() { return { - ...this.queryVariables, + fullPath: this.fullPath, + iid: this.workItemIid, after: this.after, pageSize: DEFAULT_PAGE_SIZE_NOTES, }; }, update(data) { - const workItemWidgets = this.fetchByIid - ? data.workspace?.workItems?.nodes[0]?.widgets - : data.workItem?.widgets; - const discussionNodes = - workItemWidgets.find((widget) => widget.type === 'NOTES')?.discussions || []; - return discussionNodes; + const widgets = data.workspace?.workItems?.nodes[0]?.widgets; + return widgets?.find((widget) => widget.type === 'NOTES')?.discussions || []; }, skip() { - return !this.queryVariables.id && !this.queryVariables.iid; + return !this.workItemIid; }, error() { this.$emit('error', i18n.fetchError); }, result() { - this.updateSortingOrderIfApplicable(); - if (this.hasNextPage) { this.fetchMoreNotes(); + } else if (this.targetNoteHash) { + if (this.isModal) { + this.$emit('has-notes'); + } else { + scrollToTargetOnResize(); + } } }, + subscribeToMore: [ + { + document: workItemNoteCreatedSubscription, + updateQuery(previousResult, { subscriptionData }) { + return updateCacheAfterCreatingNote(previousResult, subscriptionData); + }, + variables() { + return { + noteableId: this.workItemId, + }; + }, + skip() { + return !this.workItemId || this.hasNextPage; + }, + }, + { + document: workItemNoteDeletedSubscription, + updateQuery(previousResult, { subscriptionData }) { + return updateCacheAfterDeletingNote(previousResult, subscriptionData); + }, + variables() { + return { + noteableId: this.workItemId, + }; + }, + skip() { + return !this.workItemId || this.hasNextPage; + }, + }, + { + document: workItemNoteUpdatedSubscription, + variables() { + return { + noteableId: this.workItemId, + }; + }, + skip() { + return !this.workItemId; + }, + }, + ], }, }, methods: { @@ -148,30 +241,25 @@ export default { isSystemNote(note) { return note.notes.nodes[0].system; }, - updateSortingOrderIfApplicable() { - // when the sort order is DESC in local storage and there is only a single page, call - // changeSortOrder manually - if ( - this.changeNotesSortOrderAfterLoading && - this.perPage === DEFAULT_PAGE_SIZE_NOTES && - !this.hasNextPage - ) { - this.changeNotesSortOrder(DESC); - } - }, changeNotesSortOrder(direction) { this.sortOrder = direction; }, + filterDiscussions(filterValue) { + this.discussionFilter = filterValue; + }, + updateKey() { + this.addNoteKey = uniqueId(`work-item-add-note-${this.workItemId}`); + }, + reportAbuse(isOpen, reply = {}) { + this.$emit('openReportAbuse', reply); + }, async fetchMoreNotes() { this.isLoadingMore = true; - // copied from discussions batch logic - every fetchMore call has a higher - // amount of page size than the previous one with the limit being 100 - this.perPage = Math.min(Math.round(this.perPage * 1.5), 100); await this.$apollo.queries.workItemNotes .fetchMore({ variables: { - ...this.queryVariables, - pageSize: this.perPage, + fullPath: this.fullPath, + iid: this.workItemIid, after: this.pageInfo?.endCursor, }, }) @@ -223,17 +311,14 @@ export default { <template> <div class="gl-border-t gl-mt-5 work-item-notes"> - <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"> - <label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label> - <activity-filter - class="gl-min-h-5 gl-pb-3" - :loading="disableActivityFilter" - :sort-order="sortOrder" - :work-item-type="workItemType" - @changeSortOrder="changeNotesSortOrder" - @updateSavedSortOrder="changeNotesSortOrder" - /> - </div> + <work-item-notes-activity-header + :sort-order="sortOrder" + :disable-activity-filter-sort="disableActivityFilterSort" + :work-item-type="workItemType" + :discussion-filter="discussionFilter" + @changeSort="changeNotesSortOrder" + @changeFilter="filterDiscussions" + /> <div v-if="initialLoading" class="gl-mt-5"> <gl-skeleton-loader v-for="index in $options.loader.repeat" @@ -248,13 +333,17 @@ export default { </div> <div v-else class="issuable-discussion gl-mb-5 gl-clearfix!"> <template v-if="!initialLoading"> - <ul class="notes main-notes-list timeline gl-clearfix!"> - <work-item-add-note - v-if="formAtTop" - v-bind="workItemCommentFormProps" - @error="$emit('error', $event)" - /> - + <div v-if="formAtTop && !commentsDisabled" class="js-comment-form"> + <ul class="notes notes-form timeline"> + <work-item-add-note + v-bind="workItemCommentFormProps" + :key="addNoteKey" + @cancelEditing="updateKey" + @error="$emit('error', $event)" + /> + </ul> + </div> + <ul class="notes main-notes-list timeline"> <template v-for="discussion in notesArray"> <system-note v-if="isSystemNote(discussion)" @@ -265,26 +354,39 @@ export default { <work-item-discussion :key="getDiscussionKey(discussion)" :discussion="discussion.notes.nodes" - :query-variables="queryVariables" - :full-path="fullPath" :work-item-id="workItemId" - :fetch-by-iid="fetchByIid" + :work-item-iid="workItemIid" :work-item-type="workItemType" + :is-modal="isModal" + :autocomplete-data-sources="autocompleteDataSources" + :markdown-preview-path="markdownPreviewPath" + :assignees="assignees" + :can-set-work-item-metadata="canSetWorkItemMetadata" @deleteNote="showDeleteNoteModal($event, discussion)" + @reportAbuse="reportAbuse(true, $event)" @error="$emit('error', $event)" /> </template> </template> - <work-item-add-note - v-if="!formAtTop" - v-bind="workItemCommentFormProps" - @error="$emit('error', $event)" + <work-item-history-only-filter-note + v-if="commentsDisabled" + @changeFilter="filterDiscussions" /> </ul> + <div v-if="!formAtTop && !commentsDisabled" class="js-comment-form"> + <ul class="notes notes-form timeline"> + <work-item-add-note + v-bind="workItemCommentFormProps" + :key="addNoteKey" + @cancelEditing="updateKey" + @error="$emit('error', $event)" + /> + </ul> + </div> </template> - <template v-if="showLoadingMoreSkeleton"> + <template v-if="isLoadingMore"> <gl-skeleton-loader v-for="index in $options.loader.repeat" :key="index" |