summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/work_items/components/notes/work_item_note.vue
diff options
context:
space:
mode:
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.vue159
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>