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

501 lines
16 KiB
Vue
Raw Normal View History

2018-03-17 18:26:18 +05:30
<script>
2018-05-09 12:01:36 +05:30
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
2020-03-13 15:44:24 +05:30
import { isEmpty } from 'lodash';
2018-05-09 12:01:36 +05:30
import Autosize from 'autosize';
2020-05-24 23:13:21 +05:30
import { GlAlert, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui';
2018-05-09 12:01:36 +05:30
import { __, sprintf } from '~/locale';
2019-02-15 15:39:39 +05:30
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
2018-05-09 12:01:36 +05:30
import Flash from '../../flash';
import Autosave from '../../autosave';
2018-12-05 23:21:45 +05:30
import {
capitalizeFirstCharacter,
convertToCamelCase,
splitCamelCase,
2019-07-31 22:56:46 +05:30
slugifyWithUnderscore,
2018-12-05 23:21:45 +05:30
} from '../../lib/utils/text_utility';
2019-09-30 21:07:59 +05:30
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
2018-05-09 12:01:36 +05:30
import * as constants from '../constants';
import eventHub from '../event_hub';
2020-07-28 23:09:34 +05:30
import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue';
2018-05-09 12:01:36 +05:30
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
export default {
name: 'CommentForm',
components: {
2020-07-28 23:09:34 +05:30
NoteableWarning,
2018-05-09 12:01:36 +05:30
noteSignedOutWidget,
discussionLockedWidget,
markdownField,
userAvatarLink,
loadingButton,
2019-02-15 15:39:39 +05:30
TimelineEntryItem,
2020-05-24 23:13:21 +05:30
GlAlert,
GlIntersperse,
GlLink,
GlSprintf,
2018-05-09 12:01:36 +05:30
},
mixins: [issuableStateMixin],
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,
isSubmitting: false,
isSubmitButtonDisabled: true,
};
},
computed: {
...mapGetters([
'getCurrentUserLastNote',
'getUserData',
'getNoteableData',
2020-06-23 00:09:42 +05:30
'getNoteableDataByProp',
2018-05-09 12:01:36 +05:30
'getNotesData',
'openState',
2020-05-24 23:13:21 +05:30
'getBlockedByIssues',
2018-05-09 12:01:36 +05:30
]),
2020-05-24 23:13:21 +05:30
...mapState(['isToggleStateButtonLoading', 'isToggleBlockedIssueWarning']),
2018-05-09 12:01:36 +05:30
noteableDisplayName() {
2018-11-08 19:23:39 +05:30
return splitCamelCase(this.noteableType).toLowerCase();
2018-03-27 19:54:05 +05:30
},
2018-05-09 12:01:36 +05:30
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
2019-09-30 21:07:59 +05:30
return this.noteType === constants.COMMENT ? __('Comment') : __('Start thread');
2018-11-08 19:23:39 +05:30
},
startDiscussionDescription() {
2019-09-30 21:07:59 +05:30
return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
? __('Discuss a specific suggestion or question that needs to be resolved.')
: __('Discuss a specific suggestion or question.');
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;
},
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) {
return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), {
actionText: this.commentButtonTitle,
openOrClose,
noteable: this.noteableDisplayName,
});
}
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
return sprintf(__('%{openOrClose} %{noteable}'), {
openOrClose: capitalizeFirstCharacter(openOrClose),
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 &&
2020-07-28 23:09:34 +05:30
this.getNoteableData.state !== constants.MERGED &&
!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;
},
2018-11-08 19:23:39 +05:30
issuableTypeTitle() {
2018-12-05 23:21:45 +05:30
return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
2019-09-30 21:07:59 +05:30
? __('merge request')
: __('issue');
2018-11-08 19:23:39 +05:30
},
2020-06-23 00:09:42 +05:30
isIssueType() {
return this.noteableDisplayName === constants.ISSUE_NOTEABLE_TYPE;
},
2019-07-31 22:56:46 +05:30
trackingLabel() {
return slugifyWithUnderscore(`${this.commentButtonTitle} button`);
},
2018-05-09 12:01:36 +05:30
},
watch: {
note(newNote) {
this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
},
isSubmitting(newValue) {
this.setIsSubmitButtonDisabled(this.note, newValue);
},
},
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',
'closeIssue',
'reopenIssue',
'toggleIssueLocalState',
'toggleStateButtonLoading',
2020-05-24 23:13:21 +05:30
'toggleBlockedIssueWarning',
2018-05-09 12:01:36 +05:30
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
2020-03-13 15:44:24 +05:30
if (!isEmpty(note) && !isSubmitting) {
2018-05-09 12:01:36 +05:30
this.isSubmitButtonDisabled = false;
} else {
this.isSubmitButtonDisabled = true;
}
2018-03-17 18:26:18 +05:30
},
2018-05-09 12:01:36 +05:30
handleSave(withIssueAction) {
this.isSubmitting = true;
2018-03-27 19:54:05 +05:30
2018-05-09 12:01:36 +05:30
if (this.note.length) {
const noteData = {
endpoint: this.endpoint,
flashContainer: this.$el,
data: {
note: {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
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
},
};
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
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.enableButton();
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();
}
})
.catch(() => {
this.enableButton();
this.discard(false);
2019-09-30 21:07:59 +05:30
const msg = __(
'Your comment could not be submitted! Please check your network connection and try again.',
);
2018-05-09 12:01:36 +05:30
Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
});
} else {
this.toggleIssueState();
}
},
enableButton() {
this.isSubmitting = false;
},
toggleIssueState() {
2020-05-24 23:13:21 +05:30
if (
this.noteableType.toLowerCase() === constants.ISSUE_NOTEABLE_TYPE &&
this.isOpen &&
this.getBlockedByIssues &&
this.getBlockedByIssues.length > 0
) {
this.toggleBlockedIssueWarning(true);
return;
}
2018-05-09 12:01:36 +05:30
if (this.isOpen) {
2020-05-24 23:13:21 +05:30
this.forceCloseIssue();
2018-05-09 12:01:36 +05:30
} else {
this.reopenIssue()
2019-09-30 21:07:59 +05:30
.then(() => {
this.enableButton();
refreshUserMergeRequestCounts();
})
2019-02-15 15:39:39 +05:30
.catch(({ data }) => {
2018-05-09 12:01:36 +05:30
this.enableButton();
this.toggleStateButtonLoading(false);
2019-02-15 15:39:39 +05:30
let errorMessage = sprintf(
__('Something went wrong while reopening the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
2018-05-09 12:01:36 +05:30
);
2019-02-15 15:39:39 +05:30
if (data) {
errorMessage = Object.values(data).join('\n');
}
Flash(errorMessage);
2018-05-09 12:01:36 +05:30
});
}
},
2020-05-24 23:13:21 +05:30
forceCloseIssue() {
this.closeIssue()
.then(() => {
this.enableButton();
refreshUserMergeRequestCounts();
})
.catch(() => {
this.enableButton();
this.toggleStateButtonLoading(false);
Flash(
sprintf(
__('Something went wrong while closing the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName },
),
);
});
},
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 = '';
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();
},
setNoteType(type) {
this.noteType = type;
},
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), [
2019-09-30 21:07:59 +05:30
__('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
},
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" />
2019-02-15 15:39:39 +05:30
<discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" />
<ul v-else-if="canCreateNote" class="notes notes-form timeline">
<timeline-entry-item class="note-form">
<div class="flash-container error-alert timeline-content"></div>
<div class="timeline-icon d-none d-sm-none d-md-block">
<user-avatar-link
v-if="author"
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40"
/>
</div>
<div class="timeline-content timeline-content-form">
<form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
<div class="error-alert"></div>
2019-01-03 12:48:30 +05:30
2020-07-28 23:09:34 +05:30
<noteable-warning
v-if="hasWarning(getNoteableData)"
2019-02-15 15:39:39 +05:30
:is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getNoteableData)"
2020-07-28 23:09:34 +05:30
:noteable-type="noteableType"
:locked-noteable-docs-path="lockedIssueDocsPath"
:confidential-noteable-docs-path="confidentialIssueDocsPath"
2019-02-15 15:39:39 +05:30
/>
2020-07-28 23:09:34 +05:30
2019-02-15 15:39:39 +05:30
<markdown-field
ref="markdownField"
2020-03-13 15:44:24 +05:30
:is-submitting="isSubmitting"
2019-02-15 15:39:39 +05:30
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false"
>
<textarea
id="note-body"
ref="textarea"
slot="textarea"
v-model="note"
2019-07-31 22:56:46 +05:30
dir="auto"
2019-02-15 15:39:39 +05:30
:disabled="isSubmitting"
name="note[note]"
2020-07-28 23:09:34 +05:30
class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
2019-02-15 15:39:39 +05:30
data-supports-quick-actions="true"
2019-09-30 21:07:59 +05:30
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
2019-03-02 22:35:43 +05:30
@keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()"
@keydown.ctrl.enter="handleSave()"
2020-07-28 23:09:34 +05:30
></textarea>
2019-02-15 15:39:39 +05:30
</markdown-field>
2020-05-24 23:13:21 +05:30
<gl-alert
v-if="isToggleBlockedIssueWarning"
2020-07-28 23:09:34 +05:30
class="gl-mt-5"
2020-05-24 23:13:21 +05:30
:title="__('Are you sure you want to close this blocked issue?')"
:primary-button-text="__('Yes, close issue')"
:secondary-button-text="__('Cancel')"
variant="warning"
:dismissible="false"
@primaryAction="forceCloseIssue"
@secondaryAction="toggleBlockedIssueWarning(false) && enableButton()"
>
<p>
<gl-sprintf
:message="
__('This issue is currently blocked by the following issues: %{issues}.')
"
>
<template #issues>
<gl-intersperse>
<gl-link
v-for="blockingIssue in getBlockedByIssues"
:key="blockingIssue.web_url"
:href="blockingIssue.web_url"
>#{{ blockingIssue.iid }}</gl-link
>
</gl-intersperse>
</template>
</gl-sprintf>
</p>
</gl-alert>
2019-02-15 15:39:39 +05:30
<div class="note-form-actions">
<div
2020-07-28 23:09:34 +05:30
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
2019-02-15 15:39:39 +05:30
>
<button
:disabled="isSubmitButtonDisabled"
2020-07-28 23:09:34 +05:30
class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button"
2019-02-15 15:39:39 +05:30
type="submit"
2019-07-31 22:56:46 +05:30
:data-track-label="trackingLabel"
data-track-event="click_button"
2019-03-02 22:35:43 +05:30
@click.prevent="handleSave()"
2019-02-15 15:39:39 +05:30
>
2019-09-30 21:07:59 +05:30
{{ commentButtonTitle }}
2019-02-15 15:39:39 +05:30
</button>
2018-03-17 18:26:18 +05:30
<button
2019-02-15 15:39:39 +05:30
:disabled="isSubmitButtonDisabled"
name="button"
2018-11-08 19:23:39 +05:30
type="button"
2019-03-02 22:35:43 +05:30
class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
2019-02-15 15:39:39 +05:30
data-display="static"
data-toggle="dropdown"
2019-09-30 21:07:59 +05:30
:aria-label="__('Open comment type dropdown')"
2019-02-15 15:39:39 +05:30
>
2020-07-28 23:09:34 +05:30
<i aria-hidden="true" class="fa fa-caret-down toggle-icon"></i>
2018-03-17 18:26:18 +05:30
</button>
2019-02-15 15:39:39 +05:30
<ul class="note-type-dropdown dropdown-open-top dropdown-menu">
<li :class="{ 'droplab-item-selected': noteType === 'comment' }">
<button
type="button"
class="btn btn-transparent"
2019-03-02 22:35:43 +05:30
@click.prevent="setNoteType('comment')"
2019-02-15 15:39:39 +05:30
>
2020-07-28 23:09:34 +05:30
<i aria-hidden="true" class="fa fa-check icon"></i>
2019-02-15 15:39:39 +05:30
<div class="description">
2019-09-30 21:07:59 +05:30
<strong>{{ __('Comment') }}</strong>
<p>
{{
sprintf(__('Add a general comment to this %{noteableDisplayName}.'), {
noteableDisplayName,
})
}}
</p>
2019-02-15 15:39:39 +05:30
</div>
</button>
</li>
<li class="divider droplab-item-ignore"></li>
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
<button
type="button"
class="btn btn-transparent qa-discussion-option"
2019-03-02 22:35:43 +05:30
@click.prevent="setNoteType('discussion')"
2019-02-15 15:39:39 +05:30
>
2020-07-28 23:09:34 +05:30
<i aria-hidden="true" class="fa fa-check icon"></i>
2019-02-15 15:39:39 +05:30
<div class="description">
2019-09-30 21:07:59 +05:30
<strong>{{ __('Start thread') }}</strong>
2019-02-15 15:39:39 +05:30
<p>{{ startDiscussionDescription }}</p>
</div>
</button>
</li>
</ul>
2018-03-17 18:26:18 +05:30
</div>
2019-02-15 15:39:39 +05:30
<loading-button
2020-05-24 23:13:21 +05:30
v-if="canToggleIssueState && !isToggleBlockedIssueWarning"
2019-02-15 15:39:39 +05:30
:loading="isToggleStateButtonLoading"
:container-class="[
actionButtonClassNames,
'btn btn-comment btn-comment-and-close js-action-button',
]"
:disabled="isToggleStateButtonLoading || isSubmitting"
:label="issueActionButtonTitle"
2019-03-02 22:35:43 +05:30
@click="handleSave(true)"
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>