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

464 lines
14 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';
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,
} 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';
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
export default {
name: 'CommentForm',
components: {
issueWarning,
noteSignedOutWidget,
discussionLockedWidget,
markdownField,
userAvatarLink,
loadingButton,
},
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',
'getNotesData',
'openState',
]),
...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
return this.noteableType.replace(/_/g, ' ');
2018-03-27 19:54:05 +05:30
},
2018-05-09 12:01:36 +05:30
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
return this.noteType === constants.COMMENT
? 'Comment'
: 'Start discussion';
2018-03-17 18:26:18 +05:30
},
2018-05-09 12:01:36 +05:30
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';
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
supportQuickActions() {
// Disable quick actions support for Epics
return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
},
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;
},
},
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,
);
});
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
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;
}
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-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)
.then(res => {
this.enableButton();
this.restartPolling();
2018-03-17 18:26:18 +05:30
2018-05-09 12:01:36 +05:30
if (res.errors) {
if (res.errors.commands_only) {
2018-03-17 18:26:18 +05:30
this.discard();
2018-05-09 12:01:36 +05:30
} else {
Flash(
'Something went wrong while adding your comment. Please try again.',
'alert',
this.$refs.commentForm,
);
2018-03-17 18:26:18 +05:30
}
2018-05-09 12:01:36 +05:30
} else {
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);
const msg = `Your comment could not be submitted!
2018-03-17 18:26:18 +05:30
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() {
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',
2018-03-27 19:54:05 +05:30
),
2018-05-09 12:01:36 +05:30
{ 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',
2018-03-27 19:54:05 +05:30
),
2018-05-09 12:01:36 +05:30
{ 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();
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) {
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), [
'Note',
noteableType,
this.getNoteableData.id,
]);
}
},
initTaskList() {
return new TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes',
});
},
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" />
<discussion-locked-widget
issuable-type="issue"
2018-05-09 12:01:36 +05:30
v-else-if="isLocked(getNoteableData) && !canCreateNote"
2018-03-17 18:26:18 +05:30
/>
<ul
2018-05-09 12:01:36 +05:30
v-else-if="canCreateNote"
2018-03-17 18:26:18 +05:30
class="notes notes-form timeline">
<li class="timeline-entry">
<div class="timeline-entry-inner">
<div class="flash-container error-alert timeline-content"></div>
<div class="timeline-icon hidden-xs hidden-sm">
<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
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false"
ref="markdownField">
<textarea
id="note-body"
name="note[note]"
class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea"
2018-05-09 12:01:36 +05:30
:data-supports-quick-actions="supportQuickActions"
2018-03-17 18:26:18 +05:30
aria-label="Description"
v-model="note"
ref="textarea"
slot="textarea"
:disabled="isSubmitting"
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="pull-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
<button
@click.prevent="handleSave()"
:disabled="isSubmitButtonDisabled"
class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
type="submit">
2018-03-27 19:54:05 +05:30
{{ __(commentButtonTitle) }}
2018-03-17 18:26:18 +05:30
</button>
<button
:disabled="isSubmitButtonDisabled"
name="button"
type="button"
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
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>
2018-03-27 19:54:05 +05:30
Add a general comment to this {{ noteableDisplayName }}.
2018-03-17 18:26:18 +05:30
</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"
@click.prevent="setNoteType('discussion')">
<i
aria-hidden="true"
class="fa fa-check icon">
</i>
<div class="description">
<strong>Start discussion</strong>
<p>
Discuss a specific suggestion or question.
</p>
</div>
</button>
</li>
</ul>
</div>
2018-03-27 19:54:05 +05:30
<loading-button
2018-03-17 18:26:18 +05:30
v-if="canUpdateIssue"
2018-03-27 19:54:05 +05:30
:loading="isToggleStateButtonLoading"
@click="handleSave(true)"
:container-class="[
actionButtonClassNames,
'btn btn-comment btn-comment-and-close js-action-button'
]"
:disabled="isToggleStateButtonLoading || isSubmitting"
:label="issueActionButtonTitle"
/>
2018-03-17 18:26:18 +05:30
<button
type="button"
v-if="note.length"
@click="discard"
class="btn btn-cancel js-note-discard">
Discard draft
</button>
</div>
</form>
</div>
</div>
</li>
</ul>
</div>
</template>