diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-21 07:08:36 +0000 |
commit | 48aff82709769b098321c738f3444b9bdaa694c6 (patch) | |
tree | e00c7c43e2d9b603a5a6af576b1685e400410dee /app/assets/javascripts/notes | |
parent | 879f5329ee916a948223f8f43d77fba4da6cd028 (diff) | |
download | gitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz |
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/notes')
21 files changed, 289 insertions, 157 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 54fcf41ca50..cfdadbceaf6 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -371,6 +371,7 @@ export default { :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" :add-spacing-classes="false" + :textarea-value="note" > <textarea id="note-body" @@ -380,7 +381,8 @@ export default { dir="auto" :disabled="isSubmitting" name="note[note]" - class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area qa-comment-input" + class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area" + data-qa-selector="comment_field" data-supports-quick-actions="true" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files here…')" @@ -425,7 +427,8 @@ export default { > <gl-button :disabled="isSubmitButtonDisabled" - class="js-comment-button js-comment-submit-button qa-comment-button" + class="js-comment-button js-comment-submit-button" + data-qa-selector="comment_button" type="submit" category="primary" variant="success" @@ -439,7 +442,8 @@ export default { name="button" category="primary" variant="success" - class="note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown" + class="note-type-toggle js-note-new-discussion dropdown-toggle" + data-qa-selector="note_dropdown" data-display="static" data-toggle="dropdown" icon="chevron-down" @@ -468,7 +472,10 @@ export default { </li> <li class="divider droplab-item-ignore"></li> <li :class="{ 'droplab-item-selected': noteType === 'discussion' }"> - <button class="qa-discussion-option" @click.prevent="setNoteType('discussion')"> + <button + data-qa-selector="discussion_menu_item" + @click.prevent="setNoteType('discussion')" + > <i aria-hidden="true" class="fa fa-check icon"></i> <div class="description"> <strong>{{ __('Start thread') }}</strong> diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue index 8e6c01ba63f..ee39a529345 100644 --- a/app/assets/javascripts/notes/components/diff_discussion_header.vue +++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue @@ -1,7 +1,7 @@ <script> -/* eslint-disable vue/no-v-html */ import { mapActions } from 'vuex'; import { escape } from 'lodash'; +import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import { s__, __, sprintf } from '~/locale'; import { truncateSha } from '~/lib/utils/text_utility'; @@ -17,6 +17,9 @@ export default { noteEditedText, noteHeader, }, + directives: { + SafeHtml, + }, props: { discussion: { type: Object, @@ -113,7 +116,7 @@ export default { :expanded="discussion.expanded" @toggleHandler="toggleDiscussionHandler" > - <span v-html="headerText"></span> + <span v-safe-html="headerText"></span> </note-header> <note-edited-text v-if="discussion.resolved" diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index c01cd8f8037..a4271852563 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -76,7 +76,7 @@ export default { :discussion-path="discussion.discussion_path" :diff-file="discussion.diff_file" :can-current-user-fork="false" - :expanded="!discussion.diff_file.viewer.collapsed" + :expanded="!discussion.diff_file.viewer.automaticallyCollapsed" /> <div v-if="isTextFile" class="diff-content"> <table class="code js-syntax-highlight" :class="$options.userColorSchemeClass"> diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index c6fab271376..2427a3f98ad 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -1,6 +1,7 @@ <script> import { mapGetters, mapActions } from 'vuex'; -import { GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon, GlButton, GlButtonGroup } from '@gitlab/ui'; +import { __ } from '~/locale'; import discussionNavigation from '../mixins/discussion_navigation'; export default { @@ -9,6 +10,8 @@ export default { }, components: { GlIcon, + GlButton, + GlButtonGroup, }, mixins: [discussionNavigation], computed: { @@ -34,6 +37,12 @@ export default { allExpanded() { return this.toggeableDiscussions.every(discussion => discussion.expanded); }, + lineResolveClass() { + return this.allResolved ? 'line-resolve-btn is-active' : 'line-resolve-text'; + }, + toggleThreadsLabel() { + return this.allExpanded ? __('Collapse all threads') : __('Expand all threads'); + }, }, methods: { ...mapActions(['setExpandDiscussions']), @@ -51,59 +60,49 @@ export default { <div v-if="resolvableDiscussionsCount > 0" ref="discussionCounter" - class="line-resolve-all-container full-width-mobile" + class="line-resolve-all-container full-width-mobile gl-display-flex d-sm-flex" > - <div class="full-width-mobile d-flex d-sm-flex"> - <div class="line-resolve-all"> - <span - :class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }" - > - <template v-if="allResolved"> - <gl-icon name="check-circle-filled" /> - {{ __('All threads resolved') }} - </template> - <template v-else> - {{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }} - </template> - </span> - </div> - <div - v-if="resolveAllDiscussionsIssuePath && !allResolved" - class="btn-group btn-group-sm" - role="group" - > - <a - v-gl-tooltip - :href="resolveAllDiscussionsIssuePath" - :title="s__('Resolve all threads in new issue')" - class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" - > - <gl-icon name="issue-new" /> - </a> - </div> - <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group"> - <button - v-gl-tooltip - :title="__('Jump to next unresolved thread')" - class="btn btn-default discussion-next-btn" - data-track-event="click_button" - data-track-label="mr_next_unresolved_thread" - data-track-property="click_next_unresolved_thread_top" - @click="jumpToNextDiscussion" - > - <gl-icon name="comment-next" /> - </button> - </div> - <div class="btn-group btn-group-sm" role="group"> - <button - v-gl-tooltip - :title="__('Toggle all threads')" - class="btn btn-default toggle-all-discussions-btn" - @click="handleExpandDiscussions" - > - <gl-icon :name="allExpanded ? 'angle-up' : 'angle-down'" /> - </button> - </div> + <div class="line-resolve-all"> + <span :class="lineResolveClass"> + <template v-if="allResolved"> + <gl-icon name="check-circle-filled" /> + {{ __('All threads resolved') }} + </template> + <template v-else> + {{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }} + </template> + </span> </div> + <gl-button-group> + <gl-button + v-if="resolveAllDiscussionsIssuePath && !allResolved" + v-gl-tooltip + :href="resolveAllDiscussionsIssuePath" + :title="s__('Resolve all threads in new issue')" + :aria-label="s__('Resolve all threads in new issue')" + class="new-issue-for-discussion discussion-create-issue-btn" + icon="issue-new" + /> + <gl-button + v-if="isLoggedIn && !allResolved" + v-gl-tooltip + :title="__('Jump to next unresolved thread')" + :aria-label="__('Jump to next unresolved thread')" + class="discussion-next-btn" + data-track-event="click_button" + data-track-label="mr_next_unresolved_thread" + data-track-property="click_next_unresolved_thread_top" + icon="comment-next" + @click="jumpToNextDiscussion" + /> + <gl-button + v-gl-tooltip + :title="toggleThreadsLabel" + :aria-label="toggleThreadsLabel" + class="toggle-all-discussions-btn" + :icon="allExpanded ? 'angle-up' : 'angle-down'" + @click="handleExpandDiscussions" + /> + </gl-button-group> </div> </template> diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 989ce9ff144..e4b191b55a7 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -1,11 +1,11 @@ <script> -import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; -import { GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui'; import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility'; import { DISCUSSION_FILTERS_DEFAULT_VALUE, HISTORY_ONLY_FILTER_VALUE, + COMMENTS_ONLY_FILTER_VALUE, DISCUSSION_TAB_LABEL, DISCUSSION_FILTER_TYPES, NOTE_UNDERSCORE, @@ -14,7 +14,9 @@ import notesEventHub from '../event_hub'; export default { components: { - GlIcon, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, }, props: { filters: { @@ -37,7 +39,7 @@ export default { }; }, computed: { - ...mapGetters(['getNotesDataByProp']), + ...mapGetters(['getNotesDataByProp', 'timelineEnabled']), currentFilter() { if (!this.currentValue) return this.filters[0]; return this.filters.find(filter => filter.value === this.currentValue); @@ -62,14 +64,20 @@ export default { window.removeEventListener('hashchange', this.handleLocationHash); }, methods: { - ...mapActions(['filterDiscussion', 'setCommentsDisabled', 'setTargetNoteHash']), + ...mapActions([ + 'filterDiscussion', + 'setCommentsDisabled', + 'setTargetNoteHash', + 'setTimelineView', + ]), selectFilter(value, persistFilter = true) { const filter = parseInt(value, 10); - // close dropdown - this.toggleDropdown(); - if (filter === this.currentValue) return; + + if (this.timelineEnabled && filter !== COMMENTS_ONLY_FILTER_VALUE) { + this.setTimelineView(false); + } this.currentValue = filter; this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), @@ -78,9 +86,6 @@ export default { }); this.toggleCommentsForm(); }, - toggleDropdown() { - $(this.$refs.dropdownToggle).dropdown('toggle'); - }, toggleCommentsForm() { this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE); }, @@ -92,7 +97,6 @@ export default { if (/^note_/.test(hash) && this.currentValue !== DISCUSSION_FILTERS_DEFAULT_VALUE) { this.selectFilter(this.defaultValue, false); - this.toggleDropdown(); // close dropdown this.setTargetNoteHash(hash); } }, @@ -109,43 +113,24 @@ export default { </script> <template> - <div + <gl-dropdown v-if="displayFilters" - class="discussion-filter-container js-discussion-filter-container d-inline-block align-bottom full-width-mobile" + id="discussion-filter-dropdown" + class="gl-mr-3 full-width-mobile discussion-filter-container js-discussion-filter-container qa-discussion-filter" + :text="currentFilter.title" > - <button - id="discussion-filter-dropdown" - ref="dropdownToggle" - class="btn btn-sm qa-discussion-filter" - data-toggle="dropdown" - aria-expanded="false" - > - {{ currentFilter.title }} <gl-icon name="chevron-down" /> - </button> - <div - ref="dropdownMenu" - class="dropdown-menu dropdown-menu-selectable dropdown-menu-right" - aria-labelledby="discussion-filter-dropdown" - > - <div class="dropdown-content"> - <ul> - <li - v-for="filter in filters" - :key="filter.value" - :data-filter-type="filterType(filter.value)" - > - <button - :class="{ 'is-active': filter.value === currentValue }" - class="qa-filter-options" - type="button" - @click="selectFilter(filter.value)" - > - {{ filter.title }} - </button> - <div v-if="filter.value === defaultValue" class="dropdown-divider"></div> - </li> - </ul> - </div> + <div v-for="filter in filters" :key="filter.value" class="dropdown-item-wrapper"> + <gl-dropdown-item + :is-check-item="true" + :is-checked="filter.value === currentValue" + :class="{ 'is-active': filter.value === currentValue }" + :data-filter-type="filterType(filter.value)" + class="qa-filter-options" + @click.prevent="selectFilter(filter.value)" + > + {{ filter.title }} + </gl-dropdown-item> + <gl-dropdown-divider v-if="filter.value === defaultValue" /> </div> - </div> + </gl-dropdown> </template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index a8057276f1a..c2f40b2d21a 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -160,7 +160,7 @@ export default { }); }, displayMemberBadgeText() { - return sprintf(__('This user is a %{access} of the %{name} project.'), { + return sprintf(__('This user has the %{access} role in the %{name} project.'), { access: this.accessLevel.toLowerCase(), name: this.projectName, }); @@ -275,7 +275,8 @@ export default { v-gl-tooltip type="button" title="Edit comment" - class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button" + class="note-action-button js-note-edit btn btn-transparent" + data-qa-selector="note_edit_button" @click="onEdit" > <gl-icon name="pencil" class="link-highlight" /> diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 314fa762768..65b89b94eaa 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -45,7 +45,7 @@ export default { }, }, computed: { - ...mapGetters(['getDiscussion']), + ...mapGetters(['getDiscussion', 'suggestionsCount']), discussion() { if (!this.note.isDraft) return {}; @@ -125,6 +125,7 @@ export default { <suggestions v-if="hasSuggestion && !isEditing" :suggestions="note.suggestions" + :suggestions-count="suggestionsCount" :batch-suggestions-info="batchSuggestionsInfo" :note-html="note.note_html" :line-type="lineType" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 88b4461cf38..4b3f23e742d 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -328,6 +328,7 @@ export default { :add-spacing-classes="false" :help-page-path="helpPagePath" :show-suggest-popover="showSuggestPopover" + :textarea-value="updatedNoteBody" @handleSuggestDismissed="() => $emit('handleSuggestDismissed')" > <textarea @@ -337,7 +338,8 @@ export default { v-model="updatedNoteBody" :data-supports-quick-actions="!isEditing" name="note[note]" - class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form qa-reply-input" + class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form" + data-qa-selector="reply_field" dir="auto" :aria-label="__('Description')" :placeholder="__('Write a comment or drag your files here…')" @@ -376,7 +378,8 @@ export default { <button :disabled="isDisabled" type="button" - class="btn btn-success qa-start-review" + class="btn btn-success" + data-qa-selector="start_review_button" @click="handleAddToReview" > <template v-if="hasDrafts">{{ __('Add to review') }}</template> @@ -385,7 +388,8 @@ export default { <button :disabled="isDisabled" type="button" - class="btn qa-comment-now js-comment-button" + class="btn js-comment-button" + data-qa-selector="comment_now_button" @click="handleUpdate()" > {{ __('Add comment now') }} @@ -404,7 +408,8 @@ export default { <button :disabled="isDisabled" type="button" - class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button" + class="js-vue-issue-save btn btn-success js-comment-button" + data-qa-selector="reply_comment_button" @click="handleUpdate()" > {{ saveButtonTitle }} diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index fb18be9386e..9eaa4e422d5 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -73,6 +73,7 @@ export default { 'userCanReply', 'discussionTabCounter', 'sortDirection', + 'timelineEnabled', ]), sortDirDesc() { return this.sortDirection === constants.DESC; @@ -95,7 +96,7 @@ export default { return this.discussions; }, canReply() { - return this.userCanReply && !this.commentsDisabled; + return this.userCanReply && !this.commentsDisabled && !this.timelineEnabled; }, slotKeys() { return this.sortDirDesc ? ['form', 'comments'] : ['comments', 'form']; @@ -252,7 +253,7 @@ export default { <ordered-layout :slot-keys="slotKeys"> <template #form> <comment-form - v-if="!commentsDisabled" + v-if="!(commentsDisabled || timelineEnabled)" class="js-comment-form" :noteable-type="noteableType" /> diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue index 60b531d7597..c279a7107c7 100644 --- a/app/assets/javascripts/notes/components/sort_discussion.vue +++ b/app/assets/javascripts/notes/components/sort_discussion.vue @@ -1,6 +1,5 @@ -gs <script> -import { GlIcon } from '@gitlab/ui'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { mapActions, mapGetters } from 'vuex'; import { __ } from '~/locale'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; @@ -15,12 +14,13 @@ const SORT_OPTIONS = [ export default { SORT_OPTIONS, components: { - GlIcon, + GlDropdown, + GlDropdownItem, LocalStorageSync, }, mixins: [Tracking.mixin()], computed: { - ...mapGetters(['sortDirection', 'noteableType']), + ...mapGetters(['sortDirection', 'persistSortOrder', 'noteableType']), selectedOption() { return SORT_OPTIONS.find(({ key }) => this.sortDirection === key); }, @@ -38,7 +38,7 @@ export default { return; } - this.setDiscussionSortDirection(direction); + this.setDiscussionSortDirection({ direction }); this.track('change_discussion_sort_direction', { property: direction }); }, isDropdownItemActive(sortDir) { @@ -49,33 +49,28 @@ export default { </script> <template> - <div - data-testid="sort-discussion-filter" - class="gl-mr-2 gl-display-inline-block gl-vertical-align-bottom full-width-mobile" - > + <div class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile"> <local-storage-sync :value="sortDirection" :storage-key="storageKey" - @input="setDiscussionSortDirection" + :persist="persistSortOrder" + @input="setDiscussionSortDirection({ direction: $event })" /> - <button class="btn btn-sm js-dropdown-text" data-toggle="dropdown" aria-expanded="false"> - {{ dropdownText }} - <gl-icon name="chevron-down" /> - </button> - <div ref="dropdownMenu" class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"> - <div class="dropdown-content"> - <ul> - <li v-for="{ text, key, cls } in $options.SORT_OPTIONS" :key="key"> - <button - :class="[cls, { 'is-active': isDropdownItemActive(key) }]" - type="button" - @click="fetchSortedDiscussions(key)" - > - {{ text }} - </button> - </li> - </ul> - </div> - </div> + <gl-dropdown + :text="dropdownText" + data-testid="sort-discussion-filter" + class="js-dropdown-text full-width-mobile" + > + <gl-dropdown-item + v-for="{ text, key, cls } in $options.SORT_OPTIONS" + :key="key" + :class="cls" + :is-check-item="true" + :is-checked="isDropdownItemActive(key)" + @click="fetchSortedDiscussions(key)" + > + {{ text }} + </gl-dropdown-item> + </gl-dropdown> </div> </template> diff --git a/app/assets/javascripts/notes/components/timeline_toggle.vue b/app/assets/javascripts/notes/components/timeline_toggle.vue new file mode 100644 index 00000000000..d1ffe0a3601 --- /dev/null +++ b/app/assets/javascripts/notes/components/timeline_toggle.vue @@ -0,0 +1,60 @@ +<script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { mapActions, mapGetters } from 'vuex'; +import { s__ } from '~/locale'; +import { COMMENTS_ONLY_FILTER_VALUE, DESC } from '../constants'; +import notesEventHub from '../event_hub'; +import TrackEventDirective from '~/vue_shared/directives/track_event'; +import { trackToggleTimelineView } from '../utils'; + +export const timelineEnabledTooltip = s__('Timeline|Turn timeline view off'); +export const timelineDisabledTooltip = s__('Timeline|Turn timeline view on'); + +export default { + components: { + GlButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + TrackEvent: TrackEventDirective, + }, + computed: { + ...mapGetters(['timelineEnabled', 'sortDirection']), + tooltip() { + return this.timelineEnabled ? timelineEnabledTooltip : timelineDisabledTooltip; + }, + }, + methods: { + ...mapActions(['setTimelineView', 'setDiscussionSortDirection']), + trackToggleTimelineView, + setSort() { + if (this.timelineEnabled && this.sortDirection !== DESC) { + this.setDiscussionSortDirection({ direction: DESC, persist: false }); + } + }, + setFilter() { + notesEventHub.$emit('dropdownSelect', COMMENTS_ONLY_FILTER_VALUE, false); + }, + toggleTimeline(event) { + event.currentTarget.blur(); + this.setTimelineView(!this.timelineEnabled); + this.setSort(); + this.setFilter(); + }, + }, +}; +</script> + +<template> + <gl-button + v-gl-tooltip + v-track-event="trackToggleTimelineView(timelineEnabled)" + icon="comments" + size="small" + :selected="timelineEnabled" + :title="tooltip" + :aria-label="tooltip" + class="gl-mr-3" + @click="toggleTimeline" + /> +</template> diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue index bddac60647d..f49fd2c3fa3 100644 --- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue +++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue @@ -57,7 +57,12 @@ export default { tooltip-placement="bottom" /> </div> - <button class="btn btn-link js-replies-text qa-expand-replies" type="button" @click="toggle"> + <button + class="btn btn-link js-replies-text" + data-qa-selector="expand_replies_button" + type="button" + @click="toggle" + > {{ replies.length }} {{ n__('reply', 'replies', replies.length) }} </button> {{ __('Last reply by') }} @@ -68,7 +73,8 @@ export default { </template> <span v-else - class="collapse-replies-btn js-collapse-replies qa-collapse-replies" + class="collapse-replies-btn js-collapse-replies" + data-qa-selector="collapse_replies_button" @click="toggle" > <gl-icon name="chevron-down" /> {{ s__('Notes|Collapse replies') }} diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index b81aae7c257..7acf2ad57c8 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -14,8 +14,9 @@ export const MERGE_REQUEST_NOTEABLE_TYPE = 'MergeRequest'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const RESOLVE_NOTE_METHOD_NAME = 'post'; export const DESCRIPTION_TYPE = 'changed the description'; -export const HISTORY_ONLY_FILTER_VALUE = 2; export const DISCUSSION_FILTERS_DEFAULT_VALUE = 0; +export const COMMENTS_ONLY_FILTER_VALUE = 1; +export const HISTORY_ONLY_FILTER_VALUE = 2; export const DISCUSSION_TAB_LABEL = 'show'; export const NOTE_UNDERSCORE = 'note_'; export const TIME_DIFFERENCE_VALUE = 10; diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 7bf465482b3..1f0b2afab9e 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -2,18 +2,21 @@ import Vue from 'vue'; import notesApp from './components/notes_app.vue'; import initDiscussionFilters from './discussion_filters'; import initSortDiscussions from './sort_discussions'; +import initTimelineToggle from './timeline'; import { store } from './stores'; -document.addEventListener('DOMContentLoaded', () => { +const el = document.getElementById('js-vue-notes'); + +if (el) { // eslint-disable-next-line no-new new Vue({ - el: '#js-vue-notes', + el, components: { notesApp, }, store, data() { - const notesDataset = document.getElementById('js-vue-notes').dataset; + const notesDataset = el.dataset; const parsedUserData = JSON.parse(notesDataset.currentUserData); const noteableData = JSON.parse(notesDataset.noteableData); let currentUserData = {}; @@ -55,4 +58,5 @@ document.addEventListener('DOMContentLoaded', () => { initDiscussionFilters(store); initSortDiscussions(store); -}); + initTimelineToggle(store); +} diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 9c63a7e3cd4..37986c8a02d 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -99,8 +99,12 @@ export const updateDiscussion = ({ commit, state }, discussion) => { return utils.findNoteObjectById(state.discussions, discussion.id); }; -export const setDiscussionSortDirection = ({ commit }, direction) => { - commit(types.SET_DISCUSSIONS_SORT, direction); +export const setDiscussionSortDirection = ({ commit }, { direction, persist = true }) => { + commit(types.SET_DISCUSSIONS_SORT, { direction, persist }); +}; + +export const setTimelineView = ({ commit }, enabled) => { + commit(types.SET_TIMELINE_VIEW, enabled); }; export const setSelectedCommentPosition = ({ commit }, position) => { diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 7d60fbffb10..5b3ffa425a0 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -5,6 +5,23 @@ import { collapseSystemNotes } from './collapse_utils'; export const discussions = state => { let discussionsInState = clone(state.discussions); // NOTE: not testing bc will be removed when backend is finished. + + if (state.isTimelineEnabled) { + discussionsInState = discussionsInState + .reduce((acc, discussion) => { + const transformedToIndividualNotes = discussion.notes.map(note => ({ + ...discussion, + id: note.id, + created_at: note.created_at, + individual_note: true, + notes: [note], + })); + + return acc.concat(transformedToIndividualNotes); + }, []) + .sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); + } + if (state.discussionSortOrder === constants.DESC) { discussionsInState = discussionsInState.reverse(); } @@ -27,6 +44,10 @@ export const isNotesFetched = state => state.isNotesFetched; export const sortDirection = state => state.discussionSortOrder; +export const persistSortOrder = state => state.persistSortOrder; + +export const timelineEnabled = state => state.isTimelineEnabled; + export const isLoading = state => state.isLoading; export const getNotesDataByProp = state => prop => state.notesData[prop]; @@ -231,3 +252,6 @@ export const getDiscussion = state => discussionId => state.discussions.find(discussion => discussion.id === discussionId); export const commentsDisabled = state => state.commentsDisabled; + +export const suggestionsCount = (state, getters) => + Object.values(getters.notesById).filter(n => n.suggestions.length).length; diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index 161c9b8b1b5..a8738fa7c5f 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -7,6 +7,7 @@ export default () => ({ state: { discussions: [], discussionSortOrder: ASC, + persistSortOrder: true, convertedDisscussionIds: [], targetNoteHash: null, lastFetchedAt: null, @@ -45,6 +46,7 @@ export default () => ({ resolvableDiscussionsCount: 0, unresolvedDiscussionsCount: 0, descriptionVersions: {}, + isTimelineEnabled: false, }, actions, getters, diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 23515cdd9e3..7496dd630f6 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -34,6 +34,7 @@ export const SET_EXPAND_DISCUSSIONS = 'SET_EXPAND_DISCUSSIONS'; export const UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS = 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS'; export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID'; export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT'; +export const SET_TIMELINE_VIEW = 'SET_TIMELINE_VIEW'; export const SET_SELECTED_COMMENT_POSITION = 'SET_SELECTED_COMMENT_POSITION'; export const SET_SELECTED_COMMENT_POSITION_HOVER = 'SET_SELECTED_COMMENT_POSITION_HOVER'; export const SET_FETCHING_DISCUSSIONS = 'SET_FETCHING_DISCUSSIONS'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index a8bd94cc763..6c11d53dba3 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -313,8 +313,13 @@ export default { discussion.truncated_diff_lines = utils.prepareDiffLines(diffLines); }, - [types.SET_DISCUSSIONS_SORT](state, sort) { - state.discussionSortOrder = sort; + [types.SET_DISCUSSIONS_SORT](state, { direction, persist }) { + state.discussionSortOrder = direction; + state.persistSortOrder = persist; + }, + + [types.SET_TIMELINE_VIEW](state, value) { + state.isTimelineEnabled = value; }, [types.SET_SELECTED_COMMENT_POSITION](state, position) { diff --git a/app/assets/javascripts/notes/timeline.js b/app/assets/javascripts/notes/timeline.js new file mode 100644 index 00000000000..df6d1b21400 --- /dev/null +++ b/app/assets/javascripts/notes/timeline.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import TimelineToggle from './components/timeline_toggle.vue'; + +export default function initTimelineToggle(store) { + const el = document.getElementById('js-incidents-timeline-toggle'); + + if (!el) return null; + + return new Vue({ + el, + store, + render(createElement) { + return createElement(TimelineToggle); + }, + }); +} diff --git a/app/assets/javascripts/notes/utils.js b/app/assets/javascripts/notes/utils.js new file mode 100644 index 00000000000..e6c2eb06a51 --- /dev/null +++ b/app/assets/javascripts/notes/utils.js @@ -0,0 +1,12 @@ +/* eslint-disable @gitlab/require-i18n-strings */ + +/** + * Tracks snowplow event when User toggles timeline view + * @param {Boolean} enabled that will be send as a property for the event + */ +export const trackToggleTimelineView = enabled => ({ + category: 'Incident Management', + action: 'toggle_incident_comments_into_timeline_view', + label: 'Status', + property: enabled, +}); |