diff options
Diffstat (limited to 'app/assets')
7 files changed, 346 insertions, 80 deletions
diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue index a7331cc1fd3..1b7782c6860 100644 --- a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue @@ -68,9 +68,8 @@ export default { variables() { return this.queryVariables; }, - update({ project: { jobs: { nodes = [], pageInfo = {}, count = 0 } = {} } }) { + update({ project: { jobs: { nodes = [], pageInfo = {} } = {} } }) { this.pageInfo = pageInfo; - this.count = count; return nodes .map(mapArchivesToJobNodes) .map(mapBooleansToJobNodes) @@ -93,7 +92,6 @@ export default { data() { return { jobArtifacts: [], - count: 0, pageInfo: {}, expandedJobs: [], pagination: INITIAL_PAGINATION_STATE, @@ -110,7 +108,9 @@ export default { }; }, showPagination() { - return this.count > JOBS_PER_PAGE; + const { hasNextPage, hasPreviousPage } = this.pageInfo; + + return hasNextPage || hasPreviousPage; }, prevPage() { return Number(this.pageInfo.hasPreviousPage); diff --git a/app/assets/javascripts/work_items/components/notes/activity_filter.vue b/app/assets/javascripts/work_items/components/notes/activity_filter.vue index 71784d3a807..6d5535797ef 100644 --- a/app/assets/javascripts/work_items/components/notes/activity_filter.vue +++ b/app/assets/javascripts/work_items/components/notes/activity_filter.vue @@ -1,18 +1,35 @@ <script> import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { s__ } from '~/locale'; import Tracking from '~/tracking'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import { ASC, DESC } from '~/notes/constants'; -import { TRACKING_CATEGORY_SHOW, WORK_ITEM_NOTES_SORT_ORDER_KEY } from '~/work_items/constants'; +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, + TRACKING_CATEGORY_SHOW, + WORK_ITEM_NOTES_FILTER_KEY, +} from '~/work_items/constants'; -const SORT_OPTIONS = [ - { key: DESC, text: __('Newest first'), dataid: 'js-newest-first' }, - { key: ASC, text: __('Oldest first'), dataid: 'js-oldest-first' }, +const filterOptions = [ + { + key: WORK_ITEM_NOTES_FILTER_ALL_NOTES, + text: s__('WorkItem|All activity'), + }, + { + key: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + text: s__('WorkItem|Comments only'), + testid: 'comments-activity', + }, + { + key: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, + text: s__('WorkItem|History only'), + testid: 'history-activity', + }, ]; export default { - SORT_OPTIONS, + filterOptions, components: { GlDropdown, GlDropdownItem, @@ -20,11 +37,6 @@ export default { }, mixins: [Tracking.mixin()], props: { - sortOrder: { - type: String, - default: ASC, - required: false, - }, loading: { type: Boolean, default: false, @@ -34,80 +46,74 @@ export default { type: String, required: true, }, - }, - data() { - return { - persistSortOrder: true, - }; + discussionFilter: { + type: String, + default: WORK_ITEM_NOTES_FILTER_ALL_NOTES, + required: false, + }, }, computed: { tracking() { return { category: TRACKING_CATEGORY_SHOW, - label: 'item_track_notes_sorting', + label: 'item_track_notes_filtering', property: `type_${this.workItemType}`, }; }, - selectedSortOption() { - const isSortOptionValid = this.sortOrder === ASC || this.sortOrder === DESC; - return isSortOptionValid ? SORT_OPTIONS.find(({ key }) => this.sortOrder === key) : ASC; - }, getDropdownSelectedText() { return this.selectedSortOption.text; }, + selectedSortOption() { + return ( + filterOptions.find(({ key }) => this.discussionFilter === key) || + WORK_ITEM_NOTES_FILTER_ALL_NOTES + ); + }, }, methods: { - setDiscussionSortDirection(direction) { - this.$emit('updateSavedSortOrder', direction); + setDiscussionFilterOption(filterValue) { + this.$emit('changeFilter', filterValue); }, - fetchSortedDiscussions(direction) { - if (this.isSortDropdownItemActive(direction)) { + fetchFilteredDiscussions(filterValue) { + if (this.isSortDropdownItemActive(filterValue)) { return; } - this.track('notes_sort_order_changed'); - this.$emit('changeSortOrder', direction); + this.track('work_item_notes_filter_changed'); + this.$emit('changeFilter', filterValue); }, - isSortDropdownItemActive(sortDir) { - return sortDir === this.sortOrder; + isSortDropdownItemActive(discussionFilter) { + return discussionFilter === this.discussionFilter; }, }, - WORK_ITEM_NOTES_SORT_ORDER_KEY, + WORK_ITEM_NOTES_FILTER_KEY, }; </script> <template> - <div - id="discussion-preferences" - data-testid="discussion-preferences" - class="gl-display-inline-block gl-vertical-align-bottom gl-w-full gl-sm-w-auto" - > + <div class="gl-display-inline-block gl-vertical-align-bottom"> <local-storage-sync - :value="sortOrder" - :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY" - :persist="persistSortOrder" + :value="discussionFilter" + :storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY" as-string - @input="setDiscussionSortDirection" + @input="setDiscussionFilterOption" /> <gl-dropdown - :id="`discussion-preferences-dropdown-${workItemType}`" class="gl-xs-w-full" size="small" :text="getDropdownSelectedText" :disabled="loading" right > - <div id="discussion-sort"> - <gl-dropdown-item - v-for="{ text, key, dataid } in $options.SORT_OPTIONS" - :key="text" - :data-testid="dataid" - is-check-item - :is-checked="isSortDropdownItemActive(key)" - @click="fetchSortedDiscussions(key)" - > - {{ text }} - </gl-dropdown-item> - </div> + <gl-dropdown-item + v-for="{ text, key, testid } in $options.filterOptions" + :key="text" + :data-testid="testid" + is-check-item + :is-checked="isSortDropdownItemActive(key)" + @click="fetchFilteredDiscussions(key)" + > + {{ text }} + </gl-dropdown-item> </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/work_items/components/notes/activity_sort.vue b/app/assets/javascripts/work_items/components/notes/activity_sort.vue new file mode 100644 index 00000000000..bfbb2b65346 --- /dev/null +++ b/app/assets/javascripts/work_items/components/notes/activity_sort.vue @@ -0,0 +1,99 @@ +<script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { __ } from '~/locale'; +import Tracking from '~/tracking'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { ASC, DESC } from '~/notes/constants'; +import { TRACKING_CATEGORY_SHOW, WORK_ITEM_NOTES_SORT_ORDER_KEY } from '~/work_items/constants'; + +const sortOptions = [ + { key: DESC, text: __('Newest first'), testid: 'newest-first' }, + { key: ASC, text: __('Oldest first') }, +]; + +export default { + sortOptions, + components: { + GlDropdown, + GlDropdownItem, + LocalStorageSync, + }, + mixins: [Tracking.mixin()], + props: { + sortOrder: { + type: String, + default: ASC, + required: false, + }, + loading: { + type: Boolean, + default: false, + required: false, + }, + workItemType: { + type: String, + required: true, + }, + }, + computed: { + tracking() { + return { + category: TRACKING_CATEGORY_SHOW, + label: 'item_track_notes_sorting', + property: `type_${this.workItemType}`, + }; + }, + selectedSortOption() { + return sortOptions.find(({ key }) => this.sortOrder === key) || ASC; + }, + getDropdownSelectedText() { + return this.selectedSortOption.text; + }, + }, + methods: { + setDiscussionSortDirection(direction) { + this.$emit('changeSort', direction); + }, + fetchSortedDiscussions(direction) { + if (this.isSortDropdownItemActive(direction)) { + return; + } + this.track('work_item_notes_sort_order_changed'); + this.$emit('changeSort', direction); + }, + isSortDropdownItemActive(sortDir) { + return sortDir === this.sortOrder; + }, + }, + WORK_ITEM_NOTES_SORT_ORDER_KEY, +}; +</script> + +<template> + <div class="gl-display-inline-block gl-vertical-align-bottom"> + <local-storage-sync + :value="sortOrder" + :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY" + as-string + @input="setDiscussionSortDirection" + /> + <gl-dropdown + class="gl-xs-w-full" + size="small" + :text="getDropdownSelectedText" + :disabled="loading" + right + > + <gl-dropdown-item + v-for="{ text, key, testid } in $options.sortOptions" + :key="text" + :data-testid="testid" + is-check-item + :is-checked="isSortDropdownItemActive(key)" + @click="fetchSortedDiscussions(key)" + > + {{ text }} + </gl-dropdown-item> + </gl-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue new file mode 100644 index 00000000000..07e25312f87 --- /dev/null +++ b/app/assets/javascripts/work_items/components/notes/work_item_history_only_filter_note.vue @@ -0,0 +1,61 @@ +<script> +import { GlButton, GlIcon, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +import { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, +} from '~/work_items/constants'; + +export default { + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + i18n: { + information: s__( + "WorkItem|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options.", + ), + }, + components: { + GlButton, + GlIcon, + GlSprintf, + }, + methods: { + selectFilter(value) { + this.$emit('changeFilter', value); + }, + }, +}; +</script> + +<template> + <li class="timeline-entry note note-wrapper discussion-filter-note"> + <div class="timeline-icon gl-display-none gl-lg-display-flex"> + <gl-icon name="comment" /> + </div> + <div class="timeline-content gl-pl-8"> + <gl-sprintf :message="$options.i18n.information"> + <template #bold="{ content }"> + <b>{{ content }}</b> + </template> + </gl-sprintf> + + <div class="discussion-filter-actions"> + <gl-button + class="gl-mr-2 gl-mt-3" + data-testid="show-all-activity" + @click="selectFilter($options.WORK_ITEM_NOTES_FILTER_ALL_NOTES)" + > + {{ __('Show all activity') }} + </gl-button> + <gl-button + class="gl-mt-3" + data-testid="show-comments-only" + @click="selectFilter($options.WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS)" + > + {{ __('Show comments only') }} + </gl-button> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue new file mode 100644 index 00000000000..e700d5353e2 --- /dev/null +++ b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue @@ -0,0 +1,67 @@ +<script> +import ActivitySort from '~/work_items/components/notes/activity_sort.vue'; +import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; +import { s__ } from '~/locale'; +import { ASC } from '~/notes/constants'; +import { WORK_ITEM_NOTES_FILTER_ALL_NOTES } from '~/work_items/constants'; + +export default { + i18n: { + activityLabel: s__('WorkItem|Activity'), + }, + components: { + ActivitySort, + ActivityFilter, + }, + props: { + disableActivityFilterSort: { + type: Boolean, + required: true, + }, + sortOrder: { + type: String, + default: ASC, + required: false, + }, + workItemType: { + type: String, + required: true, + }, + discussionFilter: { + type: String, + default: WORK_ITEM_NOTES_FILTER_ALL_NOTES, + required: false, + }, + }, + methods: { + changeNotesSortOrder(direction) { + this.$emit('changeSort', direction); + }, + filterDiscussions(filterValue) { + this.$emit('changeFilter', filterValue); + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-pb-3 gl-align-items-center" + > + <h3 class="gl-font-base gl-m-0">{{ $options.i18n.activityLabel }}</h3> + <div class="gl-display-flex gl-gap-3"> + <activity-filter + :loading="disableActivityFilterSort" + :work-item-type="workItemType" + :discussion-filter="discussionFilter" + @changeFilter="filterDiscussions" + /> + <activity-sort + :loading="disableActivityFilterSort" + :sort-order="sortOrder" + :work-item-type="workItemType" + @changeSort="changeNotesSortOrder" + /> + </div> + </div> +</template> 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 aa6dd9b5184..331d0b5e8d0 100644 --- a/app/assets/javascripts/work_items/components/work_item_notes.vue +++ b/app/assets/javascripts/work_items/components/work_item_notes.vue @@ -1,11 +1,17 @@ <script> import { GlSkeletonLoader, GlModal } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; -import { s__, __ } from '~/locale'; +import { __ } from '~/locale'; 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 { @@ -13,6 +19,7 @@ import { updateCacheAfterDeletingNote, } from '~/work_items/graphql/cache_utils'; 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'; @@ -20,9 +27,6 @@ import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation import WorkItemAddNote from './notes/work_item_add_note.vue'; export default { - i18n: { - ACTIVITY_LABEL: s__('WorkItem|Activity'), - }, loader: { repeat: 10, width: 1000, @@ -31,10 +35,11 @@ export default { components: { GlSkeletonLoader, GlModal, - ActivityFilter, SystemNote, WorkItemAddNote, WorkItemDiscussion, + WorkItemNotesActivityHeader, + WorkItemHistoryOnlyFilterNote, }, props: { workItemId: { @@ -65,6 +70,7 @@ export default { perPage: DEFAULT_PAGE_SIZE_NOTES, sortOrder: ASC, noteToDelete: null, + discussionFilter: WORK_ITEM_NOTES_FILTER_ALL_NOTES, }; }, computed: { @@ -83,7 +89,7 @@ export default { showLoadingMoreSkeleton() { return this.isLoadingMore && !this.changeNotesSortOrderAfterLoading; }, - disableActivityFilter() { + disableActivityFilterSort() { return this.initialLoading || this.isLoadingMore; }, formAtTop() { @@ -102,10 +108,27 @@ export default { 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; }, }, apollo: { @@ -210,6 +233,9 @@ export default { changeNotesSortOrder(direction) { this.sortOrder = direction; }, + filterDiscussions(filterValue) { + this.discussionFilter = filterValue; + }, async fetchMoreNotes() { this.isLoadingMore = true; // copied from discussions batch logic - every fetchMore call has a higher @@ -271,17 +297,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" @@ -298,7 +321,7 @@ export default { <template v-if="!initialLoading"> <ul class="notes main-notes-list timeline gl-clearfix!"> <work-item-add-note - v-if="formAtTop" + v-if="formAtTop && !commentsDisabled" v-bind="workItemCommentFormProps" @error="$emit('error', $event)" /> @@ -325,10 +348,14 @@ export default { </template> <work-item-add-note - v-if="!formAtTop" + v-if="!formAtTop && !commentsDisabled" v-bind="workItemCommentFormProps" @error="$emit('error', $event)" /> + <work-item-history-only-filter-note + v-if="commentsDisabled" + @changeFilter="filterDiscussions" + /> </ul> </template> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index 81f9bf04bc8..b372f2d6f7b 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -176,3 +176,9 @@ export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10; export const DEFAULT_PAGE_SIZE_NOTES = 30; export const WORK_ITEM_NOTES_SORT_ORDER_KEY = 'sort_direction_work_item'; + +export const WORK_ITEM_NOTES_FILTER_ALL_NOTES = 'ALL_NOTES'; +export const WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS = 'ONLY_COMMENTS'; +export const WORK_ITEM_NOTES_FILTER_ONLY_HISTORY = 'ONLY_HISTORY'; + +export const WORK_ITEM_NOTES_FILTER_KEY = 'filter_key_work_item'; |