<script> import $ from 'jquery'; import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; import Autosize from 'autosize'; import { __, sprintf } from '~/locale'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import Flash from '../../flash'; import Autosave from '../../autosave'; 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, TimelineEntryItem, }, 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(); }, 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, ]); } }, 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" /> <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> <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-success 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> </timeline-entry-item> </ul> </div> </template>