468 lines
15 KiB
Vue
468 lines
15 KiB
Vue
<script>
|
|
import $ from 'jquery';
|
|
import { mapActions, mapGetters, mapState } from 'vuex';
|
|
import _ from 'underscore';
|
|
import Autosize from 'autosize';
|
|
import { __, sprintf } from '~/locale';
|
|
import Flash from '../../flash';
|
|
import Autosave from '../../autosave';
|
|
import TaskList from '../../task_list';
|
|
import {
|
|
capitalizeFirstCharacter,
|
|
convertToCamelCase,
|
|
splitCamelCase,
|
|
} from '../../lib/utils/text_utility';
|
|
import * as constants from '../constants';
|
|
import eventHub from '../event_hub';
|
|
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
|
|
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';
|
|
|
|
export default {
|
|
name: 'CommentForm',
|
|
components: {
|
|
issueWarning,
|
|
noteSignedOutWidget,
|
|
discussionLockedWidget,
|
|
markdownField,
|
|
userAvatarLink,
|
|
loadingButton,
|
|
},
|
|
mixins: [issuableStateMixin],
|
|
props: {
|
|
noteableType: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
markdownVersion: {
|
|
type: Number,
|
|
required: false,
|
|
default: 0,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
note: '',
|
|
noteType: constants.COMMENT,
|
|
isSubmitting: false,
|
|
isSubmitButtonDisabled: true,
|
|
};
|
|
},
|
|
computed: {
|
|
...mapGetters([
|
|
'getCurrentUserLastNote',
|
|
'getUserData',
|
|
'getNoteableData',
|
|
'getNotesData',
|
|
'openState',
|
|
]),
|
|
...mapState(['isToggleStateButtonLoading']),
|
|
noteableDisplayName() {
|
|
return splitCamelCase(this.noteableType).toLowerCase();
|
|
},
|
|
isLoggedIn() {
|
|
return this.getUserData.id;
|
|
},
|
|
commentButtonTitle() {
|
|
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
|
|
},
|
|
startDiscussionDescription() {
|
|
let text = 'Discuss a specific suggestion or question';
|
|
if (this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE) {
|
|
text += ' that needs to be resolved';
|
|
}
|
|
return `${text}.`;
|
|
},
|
|
isOpen() {
|
|
return this.openState === constants.OPENED || this.openState === constants.REOPENED;
|
|
},
|
|
canCreateNote() {
|
|
return this.getNoteableData.current_user.can_create_note;
|
|
},
|
|
issueActionButtonTitle() {
|
|
const openOrClose = this.isOpen ? 'close' : 'reopen';
|
|
|
|
if (this.note.length) {
|
|
return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), {
|
|
actionText: this.commentButtonTitle,
|
|
openOrClose,
|
|
noteable: this.noteableDisplayName,
|
|
});
|
|
}
|
|
|
|
return sprintf(__('%{openOrClose} %{noteable}'), {
|
|
openOrClose: capitalizeFirstCharacter(openOrClose),
|
|
noteable: this.noteableDisplayName,
|
|
});
|
|
},
|
|
actionButtonClassNames() {
|
|
return {
|
|
'btn-reopen': !this.isOpen,
|
|
'btn-close': this.isOpen,
|
|
'js-note-target-close': this.isOpen,
|
|
'js-note-target-reopen': !this.isOpen,
|
|
};
|
|
},
|
|
markdownDocsPath() {
|
|
return this.getNotesData.markdownDocsPath;
|
|
},
|
|
quickActionsDocsPath() {
|
|
return this.getNotesData.quickActionsDocsPath;
|
|
},
|
|
markdownPreviewPath() {
|
|
return this.getNoteableData.preview_note_path;
|
|
},
|
|
author() {
|
|
return this.getUserData;
|
|
},
|
|
canUpdateIssue() {
|
|
return this.getNoteableData.current_user.can_update;
|
|
},
|
|
endpoint() {
|
|
return this.getNoteableData.create_note_path;
|
|
},
|
|
issuableTypeTitle() {
|
|
return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
|
|
? 'merge request'
|
|
: 'issue';
|
|
},
|
|
},
|
|
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) => {
|
|
this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
|
|
});
|
|
|
|
this.initAutoSave();
|
|
this.initTaskList();
|
|
},
|
|
methods: {
|
|
...mapActions([
|
|
'saveNote',
|
|
'stopPolling',
|
|
'restartPolling',
|
|
'removePlaceholderNotes',
|
|
'closeIssue',
|
|
'reopenIssue',
|
|
'toggleIssueLocalState',
|
|
'toggleStateButtonLoading',
|
|
]),
|
|
setIsSubmitButtonDisabled(note, isSubmitting) {
|
|
if (!_.isEmpty(note) && !isSubmitting) {
|
|
this.isSubmitButtonDisabled = false;
|
|
} else {
|
|
this.isSubmitButtonDisabled = true;
|
|
}
|
|
},
|
|
handleSave(withIssueAction) {
|
|
this.isSubmitting = true;
|
|
|
|
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,
|
|
},
|
|
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
|
|
},
|
|
};
|
|
|
|
if (this.noteType === constants.DISCUSSION) {
|
|
noteData.data.note.type = constants.DISCUSSION_NOTE;
|
|
}
|
|
|
|
this.note = ''; // Empty textarea while being requested. Repopulate in catch
|
|
this.resizeTextarea();
|
|
this.stopPolling();
|
|
|
|
this.saveNote(noteData)
|
|
.then(res => {
|
|
this.enableButton();
|
|
this.restartPolling();
|
|
|
|
if (res.errors) {
|
|
if (res.errors.commands_only) {
|
|
this.discard();
|
|
} else {
|
|
Flash(
|
|
'Something went wrong while adding your comment. Please try again.',
|
|
'alert',
|
|
this.$refs.commentForm,
|
|
);
|
|
}
|
|
} else {
|
|
this.discard();
|
|
}
|
|
|
|
if (withIssueAction) {
|
|
this.toggleIssueState();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
this.enableButton();
|
|
this.discard(false);
|
|
const msg = `Your comment could not be submitted!
|
|
Please check your network connection and try again.`;
|
|
Flash(msg, 'alert', this.$el);
|
|
this.note = noteData.data.note.note; // Restore textarea content.
|
|
this.removePlaceholderNotes();
|
|
});
|
|
} else {
|
|
this.toggleIssueState();
|
|
}
|
|
},
|
|
enableButton() {
|
|
this.isSubmitting = false;
|
|
},
|
|
toggleIssueState() {
|
|
if (this.isOpen) {
|
|
this.closeIssue()
|
|
.then(() => this.enableButton())
|
|
.catch(() => {
|
|
this.enableButton();
|
|
this.toggleStateButtonLoading(false);
|
|
Flash(
|
|
sprintf(
|
|
__('Something went wrong while closing the %{issuable}. Please try again later'),
|
|
{ issuable: this.noteableDisplayName },
|
|
),
|
|
);
|
|
});
|
|
} else {
|
|
this.reopenIssue()
|
|
.then(() => this.enableButton())
|
|
.catch(() => {
|
|
this.enableButton();
|
|
this.toggleStateButtonLoading(false);
|
|
Flash(
|
|
sprintf(
|
|
__('Something went wrong while reopening the %{issuable}. Please try again later'),
|
|
{ issuable: this.noteableDisplayName },
|
|
),
|
|
);
|
|
});
|
|
}
|
|
},
|
|
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();
|
|
|
|
if (shouldClear) {
|
|
this.note = '';
|
|
this.resizeTextarea();
|
|
this.$refs.markdownField.previewMarkdown = false;
|
|
}
|
|
|
|
this.autosave.reset();
|
|
},
|
|
setNoteType(type) {
|
|
this.noteType = type;
|
|
},
|
|
editCurrentUserLastNote() {
|
|
if (this.note === '') {
|
|
const lastNote = this.getCurrentUserLastNote;
|
|
|
|
if (lastNote) {
|
|
eventHub.$emit('enterEditMode', {
|
|
noteId: lastNote.id,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
initAutoSave() {
|
|
if (this.isLoggedIn) {
|
|
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
|
|
|
|
this.autosave = new Autosave($(this.$refs.textarea), [
|
|
'Note',
|
|
noteableType,
|
|
this.getNoteableData.id,
|
|
]);
|
|
}
|
|
},
|
|
initTaskList() {
|
|
return new TaskList({
|
|
dataType: 'note',
|
|
fieldName: 'note',
|
|
selector: '.notes',
|
|
});
|
|
},
|
|
resizeTextarea() {
|
|
this.$nextTick(() => {
|
|
Autosize.update(this.$refs.textarea);
|
|
});
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<note-signed-out-widget v-if="!isLoggedIn" />
|
|
<discussion-locked-widget
|
|
v-else-if="!canCreateNote"
|
|
:issuable-type="issuableTypeTitle"
|
|
/>
|
|
<div
|
|
v-else-if="canCreateNote"
|
|
class="notes notes-form timeline">
|
|
<div class="timeline-entry note-form">
|
|
<div class="timeline-entry-inner">
|
|
<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>
|
|
|
|
<issue-warning
|
|
v-if="hasWarning(getNoteableData)"
|
|
:is-locked="isLocked(getNoteableData)"
|
|
:is-confidential="isConfidential(getNoteableData)"
|
|
/>
|
|
|
|
<markdown-field
|
|
ref="markdownField"
|
|
:markdown-preview-path="markdownPreviewPath"
|
|
:markdown-docs-path="markdownDocsPath"
|
|
:quick-actions-docs-path="quickActionsDocsPath"
|
|
:markdown-version="markdownVersion"
|
|
:add-spacing-classes="false">
|
|
<textarea
|
|
id="note-body"
|
|
ref="textarea"
|
|
slot="textarea"
|
|
v-model="note"
|
|
:disabled="isSubmitting"
|
|
name="note[note]"
|
|
class="note-textarea js-vue-comment-form js-note-text
|
|
js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
|
|
data-supports-quick-actions="true"
|
|
aria-label="Description"
|
|
placeholder="Write a comment or drag your files here…"
|
|
@keydown.up="editCurrentUserLastNote()"
|
|
@keydown.meta.enter="handleSave()"
|
|
@keydown.ctrl.enter="handleSave()">
|
|
</textarea>
|
|
</markdown-field>
|
|
<div class="note-form-actions">
|
|
<div
|
|
class="float-left btn-group
|
|
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
|
|
<button
|
|
:disabled="isSubmitButtonDisabled"
|
|
class="btn btn-create comment-btn js-comment-button js-comment-submit-button
|
|
qa-comment-button"
|
|
type="submit"
|
|
@click.prevent="handleSave()">
|
|
{{ __(commentButtonTitle) }}
|
|
</button>
|
|
<button
|
|
:disabled="isSubmitButtonDisabled"
|
|
name="button"
|
|
type="button"
|
|
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
|
|
data-display="static"
|
|
data-toggle="dropdown"
|
|
aria-label="Open comment type dropdown">
|
|
<i
|
|
aria-hidden="true"
|
|
class="fa fa-caret-down toggle-icon">
|
|
</i>
|
|
</button>
|
|
|
|
<ul class="note-type-dropdown dropdown-open-top dropdown-menu">
|
|
<li :class="{ 'droplab-item-selected': noteType === 'comment' }">
|
|
<button
|
|
type="button"
|
|
class="btn btn-transparent"
|
|
@click.prevent="setNoteType('comment')">
|
|
<i
|
|
aria-hidden="true"
|
|
class="fa fa-check icon">
|
|
</i>
|
|
<div class="description">
|
|
<strong>Comment</strong>
|
|
<p>
|
|
Add a general comment to this {{ noteableDisplayName }}.
|
|
</p>
|
|
</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"
|
|
@click.prevent="setNoteType('discussion')">
|
|
<i
|
|
aria-hidden="true"
|
|
class="fa fa-check icon">
|
|
</i>
|
|
<div class="description">
|
|
<strong>Start discussion</strong>
|
|
<p>
|
|
{{ startDiscussionDescription }}
|
|
</p>
|
|
</div>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<loading-button
|
|
v-if="canUpdateIssue"
|
|
:loading="isToggleStateButtonLoading"
|
|
:container-class="[
|
|
actionButtonClassNames,
|
|
'btn btn-comment btn-comment-and-close js-action-button'
|
|
]"
|
|
:disabled="isToggleStateButtonLoading || isSubmitting"
|
|
:label="issueActionButtonTitle"
|
|
@click="handleSave(true)"
|
|
/>
|
|
|
|
<button
|
|
v-if="note.length"
|
|
type="button"
|
|
class="btn btn-cancel js-note-discard"
|
|
@click="discard">
|
|
Discard draft
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|