summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/notes
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/notes')
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue128
-rw-r--r--app/assets/javascripts/notes/components/discussion_navigator.vue13
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue7
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue29
-rw-r--r--app/assets/javascripts/notes/components/note_attachment.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue10
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue26
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue7
-rw-r--r--app/assets/javascripts/notes/components/sort_discussion.vue11
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js17
-rw-r--r--app/assets/javascripts/notes/stores/getters.js27
12 files changed, 179 insertions, 100 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 08d7c745791..79d8ce78329 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -84,6 +84,7 @@ export default {
'getNoteableDataByProp',
'getNotesData',
'openState',
+ 'hasDrafts',
]),
...mapState(['isToggleStateButtonLoading']),
isNoteTypeComment() {
@@ -171,6 +172,9 @@ export default {
endpoint() {
return this.getNoteableData.create_note_path;
},
+ draftEndpoint() {
+ return this.getNotesData.draftsPath;
+ },
issuableTypeTitle() {
return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
? this.$options.i18n.mergeRequest
@@ -214,12 +218,15 @@ export default {
this.errors = [this.$options.i18n.GENERIC_UNSUBMITTABLE_NETWORK];
}
},
- handleSave(withIssueAction) {
+ handleSaveDraft() {
+ this.handleSave({ isDraft: true });
+ },
+ handleSave({ withIssueAction = false, isDraft = false } = {}) {
this.errors = [];
if (this.note.length) {
const noteData = {
- endpoint: this.endpoint,
+ endpoint: isDraft ? this.draftEndpoint : this.endpoint,
data: {
note: {
noteable_type: this.noteableType,
@@ -229,6 +236,7 @@ export default {
},
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
},
+ isDraft,
};
if (this.noteType === constants.DISCUSSION) {
@@ -392,62 +400,82 @@ export default {
</markdown-field>
</comment-field-layout>
<div class="note-form-actions">
- <gl-form-checkbox
- v-if="confidentialNotesEnabled && canSetConfidential"
- v-model="noteIsConfidential"
- class="gl-mb-6"
- data-testid="confidential-note-checkbox"
- >
- {{ $options.i18n.confidential }}
- <gl-icon
- v-gl-tooltip:tooltipcontainer.bottom
- name="question"
- :size="16"
- :title="$options.i18n.confidentialVisibility"
- class="gl-text-gray-500"
- />
- </gl-form-checkbox>
- <gl-dropdown
- split
- :text="commentButtonTitle"
- class="gl-mr-3 js-comment-button js-comment-submit-button comment-type-dropdown"
- category="primary"
- variant="success"
- :disabled="disableSubmitButton"
- data-testid="comment-button"
- data-qa-selector="comment_button"
- :data-track-label="trackingLabel"
- data-track-event="click_button"
- @click="handleSave()"
- >
- <gl-dropdown-item
- is-check-item
- :is-checked="isNoteTypeComment"
- :selected="isNoteTypeComment"
- @click="setNoteTypeToComment"
+ <template v-if="hasDrafts">
+ <gl-button
+ :disabled="disableSubmitButton"
+ data-testid="add-to-review-button"
+ type="submit"
+ category="primary"
+ variant="success"
+ @click.prevent="handleSaveDraft()"
+ >{{ __('Add to review') }}</gl-button
+ >
+ <gl-button
+ :disabled="disableSubmitButton"
+ data-testid="add-comment-now-button"
+ category="secondary"
+ @click.prevent="handleSave()"
+ >{{ __('Add comment now') }}</gl-button
+ >
+ </template>
+ <template v-else>
+ <gl-form-checkbox
+ v-if="confidentialNotesEnabled && canSetConfidential"
+ v-model="noteIsConfidential"
+ class="gl-mb-6"
+ data-testid="confidential-note-checkbox"
>
- <strong>{{ $options.i18n.submitButton.comment }}</strong>
- <p class="gl-m-0">{{ commentDescription }}</p>
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-dropdown-item
- is-check-item
- :is-checked="isNoteTypeDiscussion"
- :selected="isNoteTypeDiscussion"
- data-qa-selector="discussion_menu_item"
- @click="setNoteTypeToDiscussion"
+ {{ $options.i18n.confidential }}
+ <gl-icon
+ v-gl-tooltip:tooltipcontainer.bottom
+ name="question"
+ :size="16"
+ :title="$options.i18n.confidentialVisibility"
+ class="gl-text-gray-500"
+ />
+ </gl-form-checkbox>
+ <gl-dropdown
+ split
+ :text="commentButtonTitle"
+ class="gl-mr-3 js-comment-button js-comment-submit-button comment-type-dropdown"
+ category="primary"
+ variant="confirm"
+ :disabled="disableSubmitButton"
+ data-testid="comment-button"
+ data-qa-selector="comment_button"
+ :data-track-label="trackingLabel"
+ data-track-event="click_button"
+ @click="handleSave()"
>
- <strong>{{ $options.i18n.submitButton.startThread }}</strong>
- <p class="gl-m-0">{{ startDiscussionDescription }}</p>
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-dropdown-item
+ is-check-item
+ :is-checked="isNoteTypeComment"
+ :selected="isNoteTypeComment"
+ @click="setNoteTypeToComment"
+ >
+ <strong>{{ $options.i18n.submitButton.comment }}</strong>
+ <p class="gl-m-0">{{ commentDescription }}</p>
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-item
+ is-check-item
+ :is-checked="isNoteTypeDiscussion"
+ :selected="isNoteTypeDiscussion"
+ data-qa-selector="discussion_menu_item"
+ @click="setNoteTypeToDiscussion"
+ >
+ <strong>{{ $options.i18n.submitButton.startThread }}</strong>
+ <p class="gl-m-0">{{ startDiscussionDescription }}</p>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </template>
<gl-button
v-if="canToggleIssueState"
:loading="isToggleStateButtonLoading"
:class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']"
:disabled="isSubmitting"
data-testid="close-reopen-button"
- @click="handleSave(true)"
+ @click="handleSave({ withIssueAction: true })"
>{{ issueActionButtonTitle }}</gl-button
>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_navigator.vue b/app/assets/javascripts/notes/components/discussion_navigator.vue
index fa3c900c337..7e8bb75902b 100644
--- a/app/assets/javascripts/notes/components/discussion_navigator.vue
+++ b/app/assets/javascripts/notes/components/discussion_navigator.vue
@@ -1,6 +1,11 @@
<script>
/* global Mousetrap */
import 'mousetrap';
+import {
+ keysFor,
+ MR_NEXT_UNRESOLVED_DISCUSSION,
+ MR_PREVIOUS_UNRESOLVED_DISCUSSION,
+} from '~/behaviors/shortcuts/keybindings';
import eventHub from '~/notes/event_hub';
import discussionNavigation from '~/notes/mixins/discussion_navigation';
@@ -10,12 +15,12 @@ export default {
eventHub.$on('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion);
},
mounted() {
- Mousetrap.bind('n', this.jumpToNextDiscussion);
- Mousetrap.bind('p', this.jumpToPreviousDiscussion);
+ Mousetrap.bind(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION), this.jumpToNextDiscussion);
+ Mousetrap.bind(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION), this.jumpToPreviousDiscussion);
},
beforeDestroy() {
- Mousetrap.unbind('n');
- Mousetrap.unbind('p');
+ Mousetrap.unbind(keysFor(MR_NEXT_UNRESOLVED_DISCUSSION));
+ Mousetrap.unbind(keysFor(MR_PREVIOUS_UNRESOLVED_DISCUSSION));
eventHub.$off('jumpToFirstUnresolvedDiscussion', this.jumpToFirstUnresolvedDiscussion);
},
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 0f74d78c8e0..dfe2763d8bd 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -121,6 +121,7 @@ export default {
:is="componentName(firstNote)"
:note="componentData(firstNote)"
:line="line || diffLine"
+ :discussion-file="discussion.diff_file"
:commit="commit"
:help-page-path="helpPagePath"
:show-reply-button="userCanReply"
@@ -167,6 +168,7 @@ export default {
v-for="(note, index) in discussion.notes"
:key="note.id"
:note="componentData(note)"
+ :discussion-file="discussion.diff_file"
:help-page-path="helpPagePath"
:line="diffLine"
:discussion-root="index === 0"
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index cace382ccd6..5f429cbf462 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -1,7 +1,11 @@
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
export default {
+ i18n: {
+ buttonLabel: s__('MergeRequests|Resolve this thread in a new issue'),
+ },
name: 'ResolveWithIssueButton',
components: {
GlButton,
@@ -23,7 +27,8 @@ export default {
<gl-button
v-gl-tooltip
:href="url"
- :title="s__('MergeRequests|Resolve this thread in a new issue')"
+ :title="$options.i18n.buttonLabel"
+ :aria-label="$options.i18n.buttonLabel"
class="new-issue-for-discussion discussion-create-issue-btn"
icon="issue-new"
/>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index ed6701b34e8..24399e669a6 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -13,6 +13,12 @@ import { splitCamelCase } from '../../lib/utils/text_utility';
import ReplyButton from './note_actions/reply_button.vue';
export default {
+ i18n: {
+ addReactionLabel: __('Add reaction'),
+ editCommentLabel: __('Edit comment'),
+ deleteCommentLabel: __('Delete comment'),
+ moreActionsLabel: __('More actions'),
+ },
name: 'NoteActions',
components: {
GlIcon,
@@ -119,9 +125,11 @@ export default {
type: Boolean,
required: true,
},
+ // This can be undefined when `canAwardEmoji` is false
awardPath: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
},
computed: {
@@ -301,9 +309,9 @@ export default {
category="tertiary"
variant="default"
size="small"
- title="Add reaction"
+ :title="$options.i18n.addReactionLabel"
+ :aria-label="$options.i18n.addReactionLabel"
data-position="right"
- :aria-label="__('Add reaction')"
>
<span class="reaction-control-icon reaction-control-icon-neutral">
<gl-icon name="slight-smile" />
@@ -325,32 +333,35 @@ export default {
<gl-button
v-if="canEdit"
v-gl-tooltip
- title="Edit comment"
+ :title="$options.i18n.editCommentLabel"
+ :aria-label="$options.i18n.editCommentLabel"
icon="pencil"
size="small"
category="tertiary"
- class="note-action-button js-note-edit btn btn-transparent"
+ class="note-action-button js-note-edit"
data-qa-selector="note_edit_button"
@click="onEdit"
/>
<gl-button
v-if="showDeleteAction"
v-gl-tooltip
- title="Delete comment"
+ :title="$options.i18n.deleteCommentLabel"
+ :aria-label="$options.i18n.deleteCommentLabel"
size="small"
icon="remove"
category="tertiary"
- class="note-action-button js-note-delete btn btn-transparent"
+ class="note-action-button js-note-delete"
@click="onDelete"
/>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions">
<gl-button
v-gl-tooltip
- title="More actions"
+ :title="$options.i18n.moreActionsLabel"
+ :aria-label="$options.i18n.moreActionsLabel"
icon="ellipsis_v"
size="small"
category="tertiary"
- class="note-action-button more-actions-toggle btn btn-transparent"
+ class="note-action-button more-actions-toggle"
data-toggle="dropdown"
@click="closeTooltip"
/>
diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue
index b20facc4032..c49f3e2de99 100644
--- a/app/assets/javascripts/notes/components/note_attachment.vue
+++ b/app/assets/javascripts/notes/components/note_attachment.vue
@@ -24,7 +24,7 @@ export default {
target="_blank"
rel="noopener noreferrer"
>
- <img :src="attachment.url" class="note-image-attach" />
+ <img :src="attachment.url" class="note-image-attach col-lg-4" />
</a>
<div class="attachment">
<a
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index d74ade15de1..a70bac94b71 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -60,6 +60,11 @@ export default {
required: false,
default: null,
},
+ lines: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
note: {
type: Object,
required: false,
@@ -333,6 +338,7 @@ export default {
:help-page-path="helpPagePath"
:show-suggest-popover="showSuggestPopover"
:textarea-value="updatedNoteBody"
+ :lines="lines"
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
>
<template #textarea>
@@ -384,7 +390,7 @@ export default {
<gl-button
:disabled="isDisabled"
category="primary"
- variant="success"
+ variant="confirm"
class="gl-mr-3"
data-qa-selector="start_review_button"
@click="handleAddToReview"
@@ -418,7 +424,7 @@ export default {
<gl-button
:disabled="isDisabled"
category="primary"
- variant="success"
+ variant="confirm"
data-qa-selector="reply_comment_button"
class="gl-mr-3 js-vue-issue-save js-comment-button"
@click="handleUpdate()"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 185f4a70367..0feb77be653 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -48,6 +48,11 @@ export default {
required: false,
default: null,
},
+ discussionFile: {
+ type: Object,
+ required: false,
+ default: null,
+ },
helpPagePath: {
type: String,
required: false,
@@ -86,7 +91,7 @@ export default {
isRequesting: false,
isResolving: false,
commentLineStart: {},
- resolveAsThread: this.glFeatures.removeResolveNote,
+ resolveAsThread: true,
};
},
computed: {
@@ -139,14 +144,9 @@ export default {
return this.note.isDraft;
},
canResolve() {
- if (this.glFeatures.removeResolveNote && !this.discussionRoot) return false;
+ if (!this.discussionRoot) return false;
- if (this.glFeatures.removeResolveNote) return this.note.current_user.can_resolve_discussion;
-
- return (
- this.note.current_user.can_resolve ||
- (this.note.isDraft && this.note.discussion_id !== null)
- );
+ return this.note.current_user.can_resolve_discussion;
},
lineRange() {
return this.note.position?.line_range;
@@ -172,12 +172,18 @@ export default {
return commentLineOptions(lines, this.commentLineStart, this.line.line_code);
},
diffFile() {
+ let fileResolvedFromAvailableSource;
+
if (this.commentLineStart.line_code) {
const lineCode = this.commentLineStart.line_code.split('_')[0];
- return this.getDiffFileByHash(lineCode);
+ fileResolvedFromAvailableSource = this.getDiffFileByHash(lineCode);
+ }
+
+ if (!fileResolvedFromAvailableSource && this.discussionFile) {
+ fileResolvedFromAvailableSource = this.discussionFile;
}
- return null;
+ return fileResolvedFromAvailableSource || null;
},
},
created() {
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 58cfd150659..433f75a752d 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -3,8 +3,10 @@ import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { __ } from '~/locale';
import initUserPopovers from '~/user_popovers';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import draftNote from '../../batch_comments/components/draft_note.vue';
import { deprecatedCreateFlash as Flash } from '../../flash';
import { getLocationHash, doesHashExistInUrl } from '../../lib/utils/url_utility';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
@@ -32,6 +34,8 @@ export default {
discussionFilterNote,
OrderedLayout,
SidebarSubscription,
+ draftNote,
+ TimelineEntryItem,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -276,6 +280,9 @@ export default {
<ul id="notes-list" class="notes main-notes-list timeline">
<template v-for="discussion in allDiscussions">
<skeleton-loading-container v-if="discussion.isSkeletonNote" :key="discussion.id" />
+ <timeline-entry-item v-else-if="discussion.isDraft" :key="discussion.id">
+ <draft-note :draft="discussion" />
+ </timeline-entry-item>
<template v-else-if="discussion.isPlaceholderNote">
<placeholder-system-note
v-if="discussion.placeholderType === $options.systemNote"
diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue
index ed1f456c174..92c39fbb9f0 100644
--- a/app/assets/javascripts/notes/components/sort_discussion.vue
+++ b/app/assets/javascripts/notes/components/sort_discussion.vue
@@ -49,18 +49,17 @@ export default {
</script>
<template>
- <div class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile">
+ <div
+ data-testid="sort-discussion-filter"
+ class="gl-mr-3 gl-display-inline-block gl-vertical-align-bottom full-width-mobile"
+ >
<local-storage-sync
:value="sortDirection"
:storage-key="storageKey"
:persist="persistSortOrder"
@input="setDiscussionSortDirection({ direction: $event })"
/>
- <gl-dropdown
- :text="dropdownText"
- data-testid="sort-discussion-filter"
- class="js-dropdown-text full-width-mobile"
- >
+ <gl-dropdown :text="dropdownText" class="js-dropdown-text full-width-mobile">
<gl-dropdown-item
v-for="{ text, key, cls } in $options.SORT_OPTIONS"
:key="key"
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index baada4c5ce8..27ed8e203b0 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -1,24 +1,11 @@
import { deprecatedCreateFlash as Flash } from '~/flash';
import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
- mixins: [glFeatureFlagsMixin()],
computed: {
discussionResolved() {
if (this.discussion) {
- const { notes, resolved } = this.discussion;
-
- if (this.glFeatures.removeResolveNote) {
- return Boolean(resolved);
- }
-
- if (notes) {
- // Decide resolved state using store. Only valid for discussions.
- return notes.filter((note) => !note.system).every((note) => note.resolved);
- }
-
- return resolved;
+ return Boolean(this.discussion.resolved);
}
return this.note.resolved;
@@ -47,7 +34,7 @@ export default {
let endpoint =
discussion && this.discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`;
- if (this.glFeatures.removeResolveNote && this.discussionResolvePath) {
+ if (this.discussionResolvePath) {
endpoint = this.discussionResolvePath;
}
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 43d99937b8d..39f66063cfb 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -2,7 +2,23 @@ import { flattenDeep, clone } from 'lodash';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
-export const discussions = (state) => {
+const getDraftComments = (state) => {
+ if (!state.batchComments) {
+ return [];
+ }
+
+ return state.batchComments.drafts
+ .filter((draft) => !draft.file_path && !draft.discussion_id)
+ .map((x) => ({
+ ...x,
+ // Treat a top-level draft note as individual_note so it's not included in
+ // expand/collapse threads
+ individual_note: true,
+ }))
+ .sort((a, b) => a.id - b.id);
+};
+
+export const discussions = (state, getters, rootState) => {
let discussionsInState = clone(state.discussions);
// NOTE: not testing bc will be removed when backend is finished.
@@ -22,11 +38,15 @@ export const discussions = (state) => {
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
}
+ discussionsInState = collapseSystemNotes(discussionsInState);
+
+ discussionsInState = discussionsInState.concat(getDraftComments(rootState));
+
if (state.discussionSortOrder === constants.DESC) {
discussionsInState = discussionsInState.reverse();
}
- return collapseSystemNotes(discussionsInState);
+ return discussionsInState;
};
export const convertedDisscussionIds = (state) => state.convertedDisscussionIds;
@@ -257,3 +277,6 @@ export const commentsDisabled = (state) => state.commentsDisabled;
export const suggestionsCount = (state, getters) =>
Object.values(getters.notesById).filter((n) => n.suggestions.length).length;
+
+export const hasDrafts = (state, getters, rootState, rootGetters) =>
+ Boolean(rootGetters['batchComments/hasDrafts']);