269 lines
7.1 KiB
Vue
269 lines
7.1 KiB
Vue
<script>
|
|
import { GlButton, GlFormGroup, GlSafeHtmlDirective } from '@gitlab/ui';
|
|
import * as Sentry from '@sentry/browser';
|
|
import { helpPagePath } from '~/helpers/help_page_helper';
|
|
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
|
|
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
|
import { __, s__ } from '~/locale';
|
|
import EditedAt from '~/issues/show/components/edited.vue';
|
|
import Tracking from '~/tracking';
|
|
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
|
import { getWorkItemQuery } from '../utils';
|
|
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
|
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
|
|
|
|
export default {
|
|
directives: {
|
|
SafeHtml: GlSafeHtmlDirective,
|
|
},
|
|
components: {
|
|
EditedAt,
|
|
GlButton,
|
|
GlFormGroup,
|
|
MarkdownField,
|
|
},
|
|
mixins: [Tracking.mixin()],
|
|
props: {
|
|
workItemId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
fullPath: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
fetchByIid: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
queryVariables: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
},
|
|
markdownDocsPath: helpPagePath('user/markdown'),
|
|
data() {
|
|
return {
|
|
workItem: {},
|
|
isEditing: false,
|
|
isSubmitting: false,
|
|
isSubmittingWithKeydown: false,
|
|
descriptionText: '',
|
|
};
|
|
},
|
|
apollo: {
|
|
workItem: {
|
|
query() {
|
|
return getWorkItemQuery(this.fetchByIid);
|
|
},
|
|
variables() {
|
|
return this.queryVariables;
|
|
},
|
|
update(data) {
|
|
return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
|
|
},
|
|
skip() {
|
|
return !this.workItemId;
|
|
},
|
|
error() {
|
|
this.error = i18n.fetchError;
|
|
},
|
|
},
|
|
},
|
|
computed: {
|
|
autosaveKey() {
|
|
return this.workItemId;
|
|
},
|
|
canEdit() {
|
|
return this.workItem?.userPermissions?.updateWorkItem;
|
|
},
|
|
tracking() {
|
|
return {
|
|
category: TRACKING_CATEGORY_SHOW,
|
|
label: 'item_description',
|
|
property: `type_${this.workItemType}`,
|
|
};
|
|
},
|
|
descriptionHtml() {
|
|
return this.workItemDescription?.descriptionHtml;
|
|
},
|
|
descriptionEmpty() {
|
|
return this.descriptionHtml?.trim() === '';
|
|
},
|
|
workItemDescription() {
|
|
const descriptionWidget = this.workItem?.widgets?.find(
|
|
(widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
|
|
);
|
|
return {
|
|
...descriptionWidget,
|
|
description: descriptionWidget?.description || '',
|
|
};
|
|
},
|
|
workItemType() {
|
|
return this.workItem?.workItemType?.name;
|
|
},
|
|
lastEditedAt() {
|
|
return this.workItemDescription?.lastEditedAt;
|
|
},
|
|
lastEditedByName() {
|
|
return this.workItemDescription?.lastEditedBy?.name;
|
|
},
|
|
lastEditedByPath() {
|
|
return this.workItemDescription?.lastEditedBy?.webPath;
|
|
},
|
|
markdownPreviewPath() {
|
|
return `${gon.relative_url_root || ''}/${this.fullPath}/preview_markdown?target_type=${
|
|
this.workItemType
|
|
}`;
|
|
},
|
|
},
|
|
methods: {
|
|
async startEditing() {
|
|
this.isEditing = true;
|
|
|
|
this.descriptionText = getDraft(this.autosaveKey) || this.workItemDescription?.description;
|
|
|
|
await this.$nextTick();
|
|
|
|
this.$refs.textarea.focus();
|
|
},
|
|
async cancelEditing() {
|
|
const isDirty = this.descriptionText !== this.workItemDescription?.description;
|
|
|
|
if (isDirty) {
|
|
const msg = s__('WorkItem|Are you sure you want to cancel editing?');
|
|
|
|
const confirmed = await confirmAction(msg, {
|
|
primaryBtnText: __('Discard changes'),
|
|
cancelBtnText: __('Continue editing'),
|
|
});
|
|
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.isEditing = false;
|
|
clearDraft(this.autosaveKey);
|
|
},
|
|
onInput() {
|
|
if (this.isSubmittingWithKeydown) {
|
|
return;
|
|
}
|
|
|
|
updateDraft(this.autosaveKey, this.descriptionText);
|
|
},
|
|
async updateWorkItem(event) {
|
|
if (event.key) {
|
|
this.isSubmittingWithKeydown = true;
|
|
}
|
|
|
|
this.isSubmitting = true;
|
|
|
|
try {
|
|
this.track('updated_description');
|
|
|
|
const {
|
|
data: { workItemUpdate },
|
|
} = await this.$apollo.mutate({
|
|
mutation: updateWorkItemMutation,
|
|
variables: {
|
|
input: {
|
|
id: this.workItem.id,
|
|
descriptionWidget: {
|
|
description: this.descriptionText,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (workItemUpdate.errors?.length) {
|
|
throw new Error(workItemUpdate.errors[0]);
|
|
}
|
|
|
|
this.isEditing = false;
|
|
clearDraft(this.autosaveKey);
|
|
} catch (error) {
|
|
this.$emit('error', error.message);
|
|
Sentry.captureException(error);
|
|
}
|
|
|
|
this.isSubmitting = false;
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<gl-form-group
|
|
v-if="isEditing"
|
|
class="gl-my-5 gl-border-t gl-pt-6"
|
|
:label="__('Description')"
|
|
label-for="work-item-description"
|
|
>
|
|
<markdown-field
|
|
can-attach-file
|
|
:textarea-value="descriptionText"
|
|
:is-submitting="isSubmitting"
|
|
:markdown-preview-path="markdownPreviewPath"
|
|
:markdown-docs-path="$options.markdownDocsPath"
|
|
class="gl-p-3 bordered-box gl-mt-5"
|
|
>
|
|
<template #textarea>
|
|
<textarea
|
|
id="work-item-description"
|
|
ref="textarea"
|
|
v-model="descriptionText"
|
|
:disabled="isSubmitting"
|
|
class="note-textarea js-gfm-input js-autosize markdown-area"
|
|
dir="auto"
|
|
data-supports-quick-actions="false"
|
|
:aria-label="__('Description')"
|
|
:placeholder="__('Write a comment or drag your files here…')"
|
|
@keydown.meta.enter="updateWorkItem"
|
|
@keydown.ctrl.enter="updateWorkItem"
|
|
@keydown.exact.esc.stop="cancelEditing"
|
|
@input="onInput"
|
|
></textarea>
|
|
</template>
|
|
</markdown-field>
|
|
|
|
<div class="gl-display-flex">
|
|
<gl-button
|
|
category="primary"
|
|
variant="confirm"
|
|
:loading="isSubmitting"
|
|
data-testid="save-description"
|
|
@click="updateWorkItem"
|
|
>{{ __('Save') }}</gl-button
|
|
>
|
|
<gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing">{{
|
|
__('Cancel')
|
|
}}</gl-button>
|
|
</div>
|
|
</gl-form-group>
|
|
<div v-else class="gl-mb-5 gl-border-t">
|
|
<div class="gl-display-inline-flex gl-align-items-center gl-mb-5">
|
|
<label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label>
|
|
<gl-button
|
|
v-if="canEdit"
|
|
class="gl-ml-auto"
|
|
icon="pencil"
|
|
data-testid="edit-description"
|
|
:aria-label="__('Edit description')"
|
|
@click="startEditing"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
|
|
<div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div>
|
|
<edited-at
|
|
v-if="lastEditedAt"
|
|
:updated-at="lastEditedAt"
|
|
:updated-by-name="lastEditedByName"
|
|
:updated-by-path="lastEditedByPath"
|
|
/>
|
|
</div>
|
|
</template>
|