summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/notes
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 07:08:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 07:08:36 +0000
commit48aff82709769b098321c738f3444b9bdaa694c6 (patch)
treee00c7c43e2d9b603a5a6af576b1685e400410dee /app/assets/javascripts/notes
parent879f5329ee916a948223f8f43d77fba4da6cd028 (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue15
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue7
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue105
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue81
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue13
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue5
-rw-r--r--app/assets/javascripts/notes/components/sort_discussion.vue53
-rw-r--r--app/assets/javascripts/notes/components/timeline_toggle.vue60
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue10
-rw-r--r--app/assets/javascripts/notes/constants.js3
-rw-r--r--app/assets/javascripts/notes/index.js12
-rw-r--r--app/assets/javascripts/notes/stores/actions.js8
-rw-r--r--app/assets/javascripts/notes/stores/getters.js24
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js9
-rw-r--r--app/assets/javascripts/notes/timeline.js16
-rw-r--r--app/assets/javascripts/notes/utils.js12
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,
+});