diff options
Diffstat (limited to 'app/assets/javascripts/work_items/components/notes/work_item_note.vue')
-rw-r--r-- | app/assets/javascripts/work_items/components/notes/work_item_note.vue | 159 |
1 files changed, 143 insertions, 16 deletions
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue index 5efa9c94f2b..5dd21a5f76f 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue @@ -1,42 +1,126 @@ <script> -import { GlAvatarLink, GlAvatar } from '@gitlab/ui'; +import { GlAvatarLink, GlAvatar, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; +import { __ } from '~/locale'; +import { updateDraft, clearDraft } from '~/lib/utils/autosave'; +import { renderMarkdown } from '~/notes/utils'; +import EditedAt from '~/issues/show/components/edited.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import NoteBody from '~/work_items/components/notes/work_item_note_body.vue'; import NoteHeader from '~/notes/components/note_header.vue'; +import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue'; +import updateWorkItemNoteMutation from '../../graphql/notes/update_work_item_note.mutation.graphql'; +import WorkItemCommentForm from './work_item_comment_form.vue'; export default { + name: 'WorkItemNoteThread', + i18n: { + moreActionsText: __('More actions'), + deleteNoteText: __('Delete comment'), + }, components: { - NoteHeader, - NoteBody, TimelineEntryItem, - GlAvatarLink, + NoteBody, + NoteHeader, + NoteActions, GlAvatar, + GlAvatarLink, + GlDropdown, + GlDropdownItem, + WorkItemCommentForm, + EditedAt, + }, + directives: { + GlTooltip: GlTooltipDirective, }, props: { note: { type: Object, required: true, }, + isFirstNote: { + type: Boolean, + required: false, + default: false, + }, + workItemType: { + type: String, + required: true, + }, + }, + data() { + return { + isEditing: false, + }; }, computed: { author() { return this.note.author; }, - noteAnchorId() { - return `note_${this.note.id}`; + entryClass() { + return { + 'note note-wrapper note-comment': true, + 'gl-p-4': !this.isFirstNote, + }; + }, + showReply() { + return this.note.userPermissions.createNote && this.isFirstNote; + }, + autosaveKey() { + // eslint-disable-next-line @gitlab/require-i18n-strings + return `${this.note.id}-comment`; + }, + lastEditedBy() { + return this.note.lastEditedBy; + }, + hasAdminPermission() { + return this.note.userPermissions.adminNote; + }, + }, + methods: { + showReplyForm() { + this.$emit('startReplying'); + }, + startEditing() { + this.isEditing = true; + updateDraft(this.autosaveKey, this.note.body); + }, + async updateNote(newText) { + this.isEditing = false; + try { + await this.$apollo.mutate({ + mutation: updateWorkItemNoteMutation, + variables: { + input: { + id: this.note.id, + body: newText, + }, + }, + optimisticResponse: { + updateNote: { + errors: [], + note: { + ...this.note, + bodyHtml: renderMarkdown(newText), + }, + }, + }, + }); + clearDraft(this.autosaveKey); + } catch (error) { + updateDraft(this.autosaveKey, newText); + this.isEditing = true; + this.$emit('error', __('Something went wrong when updating a comment. Please try again')); + Sentry.captureException(error); + } }, }, }; </script> <template> - <timeline-entry-item - :id="noteAnchorId" - :class="{ 'internal-note': note.internal }" - :data-note-id="note.id" - class="note note-wrapper note-comment" - > - <div class="timeline-avatar gl-float-left"> + <timeline-entry-item :class="entryClass"> + <div v-if="!isFirstNote" :key="note.id" class="timeline-avatar gl-float-left"> <gl-avatar-link :href="author.webUrl"> <gl-avatar :src="author.avatarUrl" @@ -46,14 +130,57 @@ export default { /> </gl-avatar-link> </div> - - <div class="timeline-content"> + <work-item-comment-form + v-if="isEditing" + :work-item-type="workItemType" + :aria-label="__('Edit comment')" + :autosave-key="autosaveKey" + :initial-value="note.body" + :comment-button-text="__('Save comment')" + :class="{ 'gl-pl-8': !isFirstNote }" + @cancelEditing="isEditing = false" + @submitForm="updateNote" + /> + <div v-else class="timeline-content-inner" data-testid="note-wrapper"> <div class="note-header"> <note-header :author="author" :created-at="note.createdAt" :note-id="note.id" /> + <note-actions + :show-reply="showReply" + :show-edit="hasAdminPermission" + @startReplying="showReplyForm" + @startEditing="startEditing" + /> + <!-- v-if condition should be moved to "delete" dropdown item as soon as we implement copying the link --> + <gl-dropdown + v-if="hasAdminPermission" + v-gl-tooltip + icon="ellipsis_v" + text-sr-only + right + :text="$options.i18n.moreActionsText" + :title="$options.i18n.moreActionsText" + category="tertiary" + no-caret + > + <gl-dropdown-item + variant="danger" + data-testid="delete-note-action" + @click="$emit('deleteNote')" + > + {{ $options.i18n.deleteNoteText }} + </gl-dropdown-item> + </gl-dropdown> </div> <div class="timeline-discussion-body"> - <note-body :note="note" /> + <note-body ref="noteBody" :note="note" /> </div> + <edited-at + v-if="note.lastEditedBy" + :updated-at="note.lastEditedAt" + :updated-by-name="lastEditedBy.name" + :updated-by-path="lastEditedBy.webPath" + :class="isFirstNote ? 'gl-pl-3' : 'gl-pl-8'" + /> </div> </timeline-entry-item> </template> |