debian-mirror-gitlab/app/assets/javascripts/notes/components/comment_form.vue

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

454 lines
14 KiB
Vue
Raw Normal View History

2018-03-17 18:26:18 +05:30
<script>
2021-11-11 11:23:49 +05:30
import { GlAlert, GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
2021-03-11 19:13:27 +05:30
import Autosize from 'autosize';
2018-05-09 12:01:36 +05:30
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
2021-02-22 17:27:13 +05:30
import Autosave from '~/autosave';
2021-03-11 19:13:27 +05:30
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
2022-11-25 23:54:43 +05:30
import { createAlert } from '~/flash';
2022-07-16 23:28:13 +05:30
import { badgeState } from '~/issuable/components/status_box.vue';
2021-04-17 20:07:23 +05:30
import httpStatusCodes from '~/lib/utils/http_status';
2018-12-05 23:21:45 +05:30
import {
capitalizeFirstCharacter,
convertToCamelCase,
2019-07-31 22:56:46 +05:30
slugifyWithUnderscore,
2021-02-22 17:27:13 +05:30
} from '~/lib/utils/text_utility';
2021-04-17 20:07:23 +05:30
import { sprintf } from '~/locale';
2022-10-11 01:57:18 +05:30
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
2021-03-11 19:13:27 +05:30
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
2021-03-08 18:12:59 +05:30
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
2021-04-17 20:07:23 +05:30
2021-03-11 19:13:27 +05:30
import * as constants from '../constants';
import eventHub from '../event_hub';
2021-04-17 20:07:23 +05:30
import { COMMENT_FORM } from '../i18n';
2018-05-09 12:01:36 +05:30
import issuableStateMixin from '../mixins/issuable_state';
2021-03-08 18:12:59 +05:30
import CommentFieldLayout from './comment_field_layout.vue';
2021-11-11 11:23:49 +05:30
import CommentTypeDropdown from './comment_type_dropdown.vue';
2022-10-11 01:57:18 +05:30
import DiscussionLockedWidget from './discussion_locked_widget.vue';
import NoteSignedOutWidget from './note_signed_out_widget.vue';
2018-03-17 18:26:18 +05:30
2021-04-17 20:07:23 +05:30
const { UNPROCESSABLE_ENTITY } = httpStatusCodes;
2018-05-09 12:01:36 +05:30
export default {
name: 'CommentForm',
2021-04-17 20:07:23 +05:30
i18n: COMMENT_FORM,
2018-05-09 12:01:36 +05:30
components: {
2022-10-11 01:57:18 +05:30
NoteSignedOutWidget,
DiscussionLockedWidget,
MarkdownField,
2021-04-17 20:07:23 +05:30
GlAlert,
2020-11-24 15:15:51 +05:30
GlButton,
2019-02-15 15:39:39 +05:30
TimelineEntryItem,
2021-01-29 00:20:46 +05:30
GlIcon,
2021-03-08 18:12:59 +05:30
CommentFieldLayout,
2021-11-11 11:23:49 +05:30
CommentTypeDropdown,
2021-03-11 19:13:27 +05:30
GlFormCheckbox,
},
directives: {
GlTooltip: GlTooltipDirective,
2018-05-09 12:01:36 +05:30
},
2021-03-08 18:12:59 +05:30
mixins: [glFeatureFlagsMixin(), issuableStateMixin],
2018-05-09 12:01:36 +05:30
props: {
noteableType: {
type: String,
required: true,
2018-03-17 18:26:18 +05:30
},
2018-05-09 12:01:36 +05:30
},
data() {
return {
note: '',
noteType: constants.COMMENT,
2021-04-17 20:07:23 +05:30
errors: [],
2022-07-23 23:45:48 +05:30
noteIsInternal: false,
2018-05-09 12:01:36 +05:30
isSubmitting: false,
};
},
computed: {
...mapGetters([
'getCurrentUserLastNote',
'getUserData',
'getNoteableData',
2020-06-23 00:09:42 +05:30
'getNoteableDataByProp',
2018-05-09 12:01:36 +05:30
'getNotesData',
'openState',
2021-04-29 21:17:54 +05:30
'hasDrafts',
2018-05-09 12:01:36 +05:30
]),
2021-02-22 17:27:13 +05:30
...mapState(['isToggleStateButtonLoading']),
2018-05-09 12:01:36 +05:30
noteableDisplayName() {
2022-01-26 12:08:38 +05:30
const displayNameMap = {
[constants.ISSUE_NOTEABLE_TYPE]: this.$options.i18n.issue,
[constants.EPIC_NOTEABLE_TYPE]: this.$options.i18n.epic,
[constants.MERGE_REQUEST_NOTEABLE_TYPE]: this.$options.i18n.mergeRequest,
};
const noteableTypeKey =
constants.NOTEABLE_TYPE_MAPPING[this.noteableType] || constants.ISSUE_NOTEABLE_TYPE;
return displayNameMap[noteableTypeKey];
2018-03-27 19:54:05 +05:30
},
2018-05-09 12:01:36 +05:30
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
2022-07-16 23:28:13 +05:30
const { comment, internalComment, startThread, startInternalThread } = this.$options.i18n;
2022-07-23 23:45:48 +05:30
if (this.noteIsInternal) {
2022-07-16 23:28:13 +05:30
return this.noteType === constants.COMMENT ? internalComment : startInternalThread;
}
return this.noteType === constants.COMMENT ? comment : startThread;
},
textareaPlaceholder() {
2022-07-23 23:45:48 +05:30
return this.noteIsInternal
2022-07-16 23:28:13 +05:30
? this.$options.i18n.bodyPlaceholderInternal
: this.$options.i18n.bodyPlaceholder;
2018-11-08 19:23:39 +05:30
},
2021-11-11 11:23:49 +05:30
discussionsRequireResolution() {
return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE;
2018-03-17 18:26:18 +05:30
},
2018-05-09 12:01:36 +05:30
isOpen() {
2018-11-08 19:23:39 +05:30
return this.openState === constants.OPENED || this.openState === constants.REOPENED;
2018-05-09 12:01:36 +05:30
},
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
},
2022-07-23 23:45:48 +05:30
canSetInternalNote() {
2022-11-25 23:54:43 +05:30
return (
this.getNoteableData.current_user.can_create_confidential_note &&
(this.isIssue || this.isEpic)
);
2021-03-11 19:13:27 +05:30
},
2018-05-09 12:01:36 +05:30
issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen';
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
if (this.note.length) {
2022-01-26 12:08:38 +05:30
return sprintf(this.$options.i18n.actionButton.withNote[openOrClose], {
2018-05-09 12:01:36 +05:30
actionText: this.commentButtonTitle,
noteable: this.noteableDisplayName,
});
}
2018-03-17 18:26:18 +05:30
2022-01-26 12:08:38 +05:30
return sprintf(this.$options.i18n.actionButton.withoutNote[openOrClose], {
2018-05-09 12:01:36 +05:30
noteable: this.noteableDisplayName,
});
2018-03-17 18:26:18 +05:30
},
2018-05-09 12:01:36 +05:30
actionButtonClassNames() {
return {
'btn-reopen': !this.isOpen,
'btn-close': this.isOpen,
'js-note-target-close': this.isOpen,
'js-note-target-reopen': !this.isOpen,
};
2018-03-17 18:26:18 +05:30
},
2018-05-09 12:01:36 +05:30
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
quickActionsDocsPath() {
return this.getNotesData.quickActionsDocsPath;
},
markdownPreviewPath() {
return this.getNoteableData.preview_note_path;
},
author() {
return this.getUserData;
},
2019-07-31 22:56:46 +05:30
canToggleIssueState() {
return (
this.getNoteableData.current_user.can_update &&
2021-06-08 01:23:25 +05:30
this.openState !== constants.MERGED &&
2020-07-28 23:09:34 +05:30
!this.closedAndLocked
2019-07-31 22:56:46 +05:30
);
2018-05-09 12:01:36 +05:30
},
2020-07-28 23:09:34 +05:30
closedAndLocked() {
return !this.isOpen && this.isLocked(this.getNoteableData);
},
2018-05-09 12:01:36 +05:30
endpoint() {
return this.getNoteableData.create_note_path;
},
2021-04-29 21:17:54 +05:30
draftEndpoint() {
return this.getNotesData.draftsPath;
},
2021-02-22 17:27:13 +05:30
isIssue() {
2022-01-26 12:08:38 +05:30
return constants.NOTEABLE_TYPE_MAPPING[this.noteableType] === constants.ISSUE_NOTEABLE_TYPE;
2020-06-23 00:09:42 +05:30
},
2022-07-16 23:28:13 +05:30
isEpic() {
return constants.NOTEABLE_TYPE_MAPPING[this.noteableType] === constants.EPIC_NOTEABLE_TYPE;
},
2019-07-31 22:56:46 +05:30
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
},
2021-03-11 19:13:27 +05:30
disableSubmitButton() {
return this.note.length === 0 || this.isSubmitting;
2018-05-09 12:01:36 +05:30
},
},
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
2018-11-08 19:23:39 +05:30
this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
2018-05-09 12:01:36 +05:30
});
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
this.initAutoSave();
},
methods: {
...mapActions([
'saveNote',
'stopPolling',
'restartPolling',
'removePlaceholderNotes',
2021-02-22 17:27:13 +05:30
'closeIssuable',
'reopenIssuable',
2018-05-09 12:01:36 +05:30
'toggleIssueLocalState',
]),
2021-04-17 20:07:23 +05:30
handleSaveError({ data, status }) {
if (status === UNPROCESSABLE_ENTITY && data.errors?.commands_only?.length) {
this.errors = data.errors.commands_only;
} else {
this.errors = [this.$options.i18n.GENERIC_UNSUBMITTABLE_NETWORK];
}
},
2021-04-29 21:17:54 +05:30
handleSaveDraft() {
this.handleSave({ isDraft: true });
},
handleSave({ withIssueAction = false, isDraft = false } = {}) {
2021-04-17 20:07:23 +05:30
this.errors = [];
2018-05-09 12:01:36 +05:30
if (this.note.length) {
const noteData = {
2021-04-29 21:17:54 +05:30
endpoint: isDraft ? this.draftEndpoint : this.endpoint,
2018-05-09 12:01:36 +05:30
data: {
note: {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
2022-10-11 01:57:18 +05:30
internal: this.noteIsInternal,
2018-05-09 12:01:36 +05:30
note: this.note,
2018-03-17 18:26:18 +05:30
},
2018-11-08 19:23:39 +05:30
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
2018-05-09 12:01:36 +05:30
},
2021-04-29 21:17:54 +05:30
isDraft,
2018-05-09 12:01:36 +05:30
};
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
if (this.noteType === constants.DISCUSSION) {
noteData.data.note.type = constants.DISCUSSION_NOTE;
}
2018-03-27 19:54:05 +05:30
2018-05-09 12:01:36 +05:30
this.note = ''; // Empty textarea while being requested. Repopulate in catch
this.resizeTextarea();
this.stopPolling();
2018-03-17 18:26:18 +05:30
2021-02-22 17:27:13 +05:30
this.isSubmitting = true;
2018-05-09 12:01:36 +05:30
this.saveNote(noteData)
2020-01-01 13:55:28 +05:30
.then(() => {
2018-05-09 12:01:36 +05:30
this.restartPolling();
2020-01-01 13:55:28 +05:30
this.discard();
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
if (withIssueAction) {
this.toggleIssueState();
}
})
2021-04-17 20:07:23 +05:30
.catch(({ response }) => {
this.handleSaveError(response);
2018-05-09 12:01:36 +05:30
this.discard(false);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
2021-02-22 17:27:13 +05:30
})
.finally(() => {
this.isSubmitting = false;
2018-05-09 12:01:36 +05:30
});
} else {
this.toggleIssueState();
}
},
2021-09-04 01:27:46 +05:30
handleEnter() {
if (this.hasDrafts) {
this.handleSaveDraft();
} else {
this.handleSave();
}
},
2018-05-09 12:01:36 +05:30
toggleIssueState() {
2021-02-22 17:27:13 +05:30
if (this.isIssue) {
// We want to invoke the close/reopen logic in the issue header
// since that is where the blocked-by issues modal logic is also defined
eventHub.$emit('toggle.issuable.state');
2020-05-24 23:13:21 +05:30
return;
}
2019-02-15 15:39:39 +05:30
2021-02-22 17:27:13 +05:30
const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable;
2019-02-15 15:39:39 +05:30
2021-02-22 17:27:13 +05:30
toggleState()
2022-07-16 23:28:13 +05:30
.then(() => badgeState.updateStatus && badgeState.updateStatus())
2021-02-22 17:27:13 +05:30
.then(refreshUserMergeRequestCounts)
2021-09-30 23:02:18 +05:30
.catch(() =>
2022-11-25 23:54:43 +05:30
createAlert({
2021-09-30 23:02:18 +05:30
message: constants.toggleStateErrorMessage[this.noteableType][this.openState],
}),
);
2020-05-24 23:13:21 +05:30
},
2018-05-09 12:01:36 +05:30
discard(shouldClear = true) {
// `blur` is needed to clear slash commands autocomplete cache if event fired.
// `focus` is needed to remain cursor in the textarea.
this.$refs.textarea.blur();
this.$refs.textarea.focus();
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
if (shouldClear) {
this.note = '';
2022-07-23 23:45:48 +05:30
this.noteIsInternal = false;
2018-05-09 12:01:36 +05:30
this.resizeTextarea();
this.$refs.markdownField.previewMarkdown = false;
}
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
this.autosave.reset();
},
editCurrentUserLastNote() {
if (this.note === '') {
const lastNote = this.getCurrentUserLastNote;
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
if (lastNote) {
eventHub.$emit('enterEditMode', {
noteId: lastNote.id,
});
2018-03-17 18:26:18 +05:30
}
2018-05-09 12:01:36 +05:30
}
},
initAutoSave() {
if (this.isLoggedIn) {
2018-11-08 19:23:39 +05:30
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
2018-03-27 19:54:05 +05:30
2018-05-09 12:01:36 +05:30
this.autosave = new Autosave($(this.$refs.textarea), [
2021-04-17 20:07:23 +05:30
this.$options.i18n.note,
2018-05-09 12:01:36 +05:30
noteableType,
this.getNoteableData.id,
]);
}
},
resizeTextarea() {
this.$nextTick(() => {
Autosize.update(this.$refs.textarea);
});
2018-03-17 18:26:18 +05:30
},
2021-03-08 18:12:59 +05:30
hasEmailParticipants() {
return this.getNoteableData.issue_email_participants?.length;
},
2021-04-17 20:07:23 +05:30
dismissError(index) {
this.errors.splice(index, 1);
},
2018-05-09 12:01:36 +05:30
},
};
2018-03-17 18:26:18 +05:30
</script>
<template>
<div>
<note-signed-out-widget v-if="!isLoggedIn" />
2022-01-26 12:08:38 +05:30
<discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="noteableDisplayName" />
2019-02-15 15:39:39 +05:30
<ul v-else-if="canCreateNote" class="notes notes-form timeline">
<timeline-entry-item class="note-form">
2021-04-17 20:07:23 +05:30
<gl-alert
v-for="(error, index) in errors"
:key="index"
variant="danger"
class="gl-mb-2"
@dismiss="() => dismissError(index)"
>
{{ error }}
</gl-alert>
2019-02-15 15:39:39 +05:30
<div class="timeline-content timeline-content-form">
<form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
2021-03-08 18:12:59 +05:30
<comment-field-layout
:with-alert-container="true"
:noteable-data="getNoteableData"
2022-07-23 23:45:48 +05:30
:is-internal-note="noteIsInternal"
2020-07-28 23:09:34 +05:30
:noteable-type="noteableType"
2019-02-15 15:39:39 +05:30
>
2021-03-08 18:12:59 +05:30
<markdown-field
ref="markdownField"
:is-submitting="isSubmitting"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false"
:textarea-value="note"
>
<template #textarea>
<textarea
id="note-body"
ref="textarea"
v-model="note"
dir="auto"
:disabled="isSubmitting"
name="note[note]"
class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area"
data-qa-selector="comment_field"
data-testid="comment-field"
2022-04-04 11:22:00 +05:30
data-supports-quick-actions="true"
2021-04-17 20:07:23 +05:30
:aria-label="$options.i18n.comment"
2022-07-16 23:28:13 +05:30
:placeholder="textareaPlaceholder"
2021-03-08 18:12:59 +05:30
@keydown.up="editCurrentUserLastNote()"
2021-09-04 01:27:46 +05:30
@keydown.meta.enter="handleEnter()"
@keydown.ctrl.enter="handleEnter()"
2021-03-08 18:12:59 +05:30
></textarea>
</template>
</markdown-field>
</comment-field-layout>
2019-02-15 15:39:39 +05:30
<div class="note-form-actions">
2021-04-29 21:17:54 +05:30
<template v-if="hasDrafts">
<gl-button
:disabled="disableSubmitButton"
data-testid="add-to-review-button"
type="submit"
category="primary"
2022-07-16 23:28:13 +05:30
variant="confirm"
2021-04-29 21:17:54 +05:30
@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
2022-08-13 15:12:31 +05:30
v-if="canSetInternalNote"
2022-07-23 23:45:48 +05:30
v-model="noteIsInternal"
class="gl-mb-2"
data-testid="internal-note-checkbox"
2019-02-15 15:39:39 +05:30
>
2022-07-23 23:45:48 +05:30
{{ $options.i18n.internal }}
2021-04-29 21:17:54 +05:30
<gl-icon
v-gl-tooltip:tooltipcontainer.bottom
name="question"
:size="16"
2022-07-23 23:45:48 +05:30
:title="$options.i18n.internalVisibility"
2021-04-29 21:17:54 +05:30
class="gl-text-gray-500"
/>
</gl-form-checkbox>
2021-11-11 11:23:49 +05:30
<comment-type-dropdown
v-model="noteType"
class="gl-mr-3"
2021-04-29 21:17:54 +05:30
:disabled="disableSubmitButton"
2021-11-11 11:23:49 +05:30
:tracking-label="trackingLabel"
2022-07-23 23:45:48 +05:30
:is-internal-note="noteIsInternal"
2021-11-11 11:23:49 +05:30
:noteable-display-name="noteableDisplayName"
:discussions-require-resolution="discussionsRequireResolution"
@click="handleSave"
/>
2021-04-29 21:17:54 +05:30
</template>
2020-11-24 15:15:51 +05:30
<gl-button
2021-04-17 20:07:23 +05:30
v-if="canToggleIssueState"
2019-02-15 15:39:39 +05:30
:loading="isToggleStateButtonLoading"
2021-02-22 17:27:13 +05:30
:class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']"
:disabled="isSubmitting"
data-testid="close-reopen-button"
2021-04-29 21:17:54 +05:30
@click="handleSave({ withIssueAction: true })"
2020-11-24 15:15:51 +05:30
>{{ issueActionButtonTitle }}</gl-button
>
2019-02-15 15:39:39 +05:30
</div>
</form>
2018-03-17 18:26:18 +05:30
</div>
2019-02-15 15:39:39 +05:30
</timeline-entry-item>
</ul>
2018-03-17 18:26:18 +05:30
</div>
</template>