/app/assets/javascripts/notes/components/issue_comment_form.vue
Vue | 377 lines | 352 code | 17 blank | 8 comment | 17 complexity | ebb57e55b03020b86540165f4779d311 MD5 | raw file
1<script>
2 import { mapActions, mapGetters } from 'vuex';
3 import _ from 'underscore';
4 import Autosize from 'autosize';
5 import Flash from '../../flash';
6 import Autosave from '../../autosave';
7 import TaskList from '../../task_list';
8 import * as constants from '../constants';
9 import eventHub from '../event_hub';
10 import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
11 import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
12 import issueDiscussionLockedWidget from './issue_discussion_locked_widget.vue';
13 import markdownField from '../../vue_shared/components/markdown/field.vue';
14 import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
15 import issuableStateMixin from '../mixins/issuable_state';
16
17 export default {
18 name: 'issueCommentForm',
19 data() {
20 return {
21 note: '',
22 noteType: constants.COMMENT,
23 // Can't use mapGetters,
24 // this needs to be in the data object because it belongs to the state
25 issueState: this.$store.getters.getIssueData.state,
26 isSubmitting: false,
27 isSubmitButtonDisabled: true,
28 };
29 },
30 components: {
31 issueWarning,
32 issueNoteSignedOutWidget,
33 issueDiscussionLockedWidget,
34 markdownField,
35 userAvatarLink,
36 },
37 watch: {
38 note(newNote) {
39 this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
40 },
41 isSubmitting(newValue) {
42 this.setIsSubmitButtonDisabled(this.note, newValue);
43 },
44 },
45 computed: {
46 ...mapGetters([
47 'getCurrentUserLastNote',
48 'getUserData',
49 'getIssueData',
50 'getNotesData',
51 ]),
52 isLoggedIn() {
53 return this.getUserData.id;
54 },
55 commentButtonTitle() {
56 return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
57 },
58 isIssueOpen() {
59 return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
60 },
61 canCreateNote() {
62 return this.getIssueData.current_user.can_create_note;
63 },
64 issueActionButtonTitle() {
65 if (this.note.length) {
66 const actionText = this.isIssueOpen ? 'close' : 'reopen';
67
68 return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`;
69 }
70
71 return this.isIssueOpen ? 'Close issue' : 'Reopen issue';
72 },
73 actionButtonClassNames() {
74 return {
75 'btn-reopen': !this.isIssueOpen,
76 'btn-close': this.isIssueOpen,
77 'js-note-target-close': this.isIssueOpen,
78 'js-note-target-reopen': !this.isIssueOpen,
79 };
80 },
81 markdownDocsPath() {
82 return this.getNotesData.markdownDocsPath;
83 },
84 quickActionsDocsPath() {
85 return this.getNotesData.quickActionsDocsPath;
86 },
87 markdownPreviewPath() {
88 return this.getIssueData.preview_note_path;
89 },
90 author() {
91 return this.getUserData;
92 },
93 canUpdateIssue() {
94 return this.getIssueData.current_user.can_update;
95 },
96 endpoint() {
97 return this.getIssueData.create_note_path;
98 },
99 },
100 methods: {
101 ...mapActions([
102 'saveNote',
103 'stopPolling',
104 'restartPolling',
105 'removePlaceholderNotes',
106 ]),
107 setIsSubmitButtonDisabled(note, isSubmitting) {
108 if (!_.isEmpty(note) && !isSubmitting) {
109 this.isSubmitButtonDisabled = false;
110 } else {
111 this.isSubmitButtonDisabled = true;
112 }
113 },
114 handleSave(withIssueAction) {
115 if (this.note.length) {
116 const noteData = {
117 endpoint: this.endpoint,
118 flashContainer: this.$el,
119 data: {
120 note: {
121 noteable_type: constants.NOTEABLE_TYPE,
122 noteable_id: this.getIssueData.id,
123 note: this.note,
124 },
125 },
126 };
127
128 if (this.noteType === constants.DISCUSSION) {
129 noteData.data.note.type = constants.DISCUSSION_NOTE;
130 }
131 this.isSubmitting = true;
132 this.note = ''; // Empty textarea while being requested. Repopulate in catch
133 this.resizeTextarea();
134 this.stopPolling();
135
136 this.saveNote(noteData)
137 .then((res) => {
138 this.isSubmitting = false;
139 this.restartPolling();
140
141 if (res.errors) {
142 if (res.errors.commands_only) {
143 this.discard();
144 } else {
145 Flash(
146 'Something went wrong while adding your comment. Please try again.',
147 'alert',
148 this.$refs.commentForm,
149 );
150 }
151 } else {
152 this.discard();
153 }
154
155 if (withIssueAction) {
156 this.toggleIssueState();
157 }
158 })
159 .catch(() => {
160 this.isSubmitting = false;
161 this.discard(false);
162 const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
163 Flash(msg, 'alert', this.$el);
164 this.note = noteData.data.note.note; // Restore textarea content.
165 this.removePlaceholderNotes();
166 });
167 } else {
168 this.toggleIssueState();
169 }
170 },
171 toggleIssueState() {
172 this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
173
174 // This is out of scope for the Notes Vue component.
175 // It was the shortest path to update the issue state and relevant places.
176 const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close';
177 $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click');
178 },
179 discard(shouldClear = true) {
180 // `blur` is needed to clear slash commands autocomplete cache if event fired.
181 // `focus` is needed to remain cursor in the textarea.
182 this.$refs.textarea.blur();
183 this.$refs.textarea.focus();
184
185 if (shouldClear) {
186 this.note = '';
187 this.resizeTextarea();
188 this.$refs.markdownField.previewMarkdown = false;
189 }
190
191 // reset autostave
192 this.autosave.reset();
193 },
194 setNoteType(type) {
195 this.noteType = type;
196 },
197 editCurrentUserLastNote() {
198 if (this.note === '') {
199 const lastNote = this.getCurrentUserLastNote;
200
201 if (lastNote) {
202 eventHub.$emit('enterEditMode', {
203 noteId: lastNote.id,
204 });
205 }
206 }
207 },
208 initAutoSave() {
209 if (this.isLoggedIn) {
210 this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue');
211 }
212 },
213 initTaskList() {
214 return new TaskList({
215 dataType: 'note',
216 fieldName: 'note',
217 selector: '.notes',
218 });
219 },
220 resizeTextarea() {
221 this.$nextTick(() => {
222 Autosize.update(this.$refs.textarea);
223 });
224 },
225 },
226 mixins: [
227 issuableStateMixin,
228 ],
229 mounted() {
230 // jQuery is needed here because it is a custom event being dispatched with jQuery.
231 $(document).on('issuable:change', (e, isClosed) => {
232 this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
233 });
234
235 this.initAutoSave();
236 this.initTaskList();
237 },
238 };
239</script>
240
241<template>
242 <div>
243 <issue-note-signed-out-widget v-if="!isLoggedIn" />
244 <issue-discussion-locked-widget v-else-if="!canCreateNote" />
245 <ul
246 v-else
247 class="notes notes-form timeline">
248 <li class="timeline-entry">
249 <div class="timeline-entry-inner">
250 <div class="flash-container error-alert timeline-content"></div>
251 <div class="timeline-icon hidden-xs hidden-sm">
252 <user-avatar-link
253 v-if="author"
254 :link-href="author.path"
255 :img-src="author.avatar_url"
256 :img-alt="author.name"
257 :img-size="40"
258 />
259 </div>
260 <div class="timeline-content timeline-content-form">
261 <form
262 ref="commentForm"
263 class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
264 >
265
266 <div class="error-alert"></div>
267
268 <issue-warning
269 v-if="hasWarning(getIssueData)"
270 :is-locked="isLocked(getIssueData)"
271 :is-confidential="isConfidential(getIssueData)"
272 />
273
274 <markdown-field
275 :markdown-preview-path="markdownPreviewPath"
276 :markdown-docs-path="markdownDocsPath"
277 :quick-actions-docs-path="quickActionsDocsPath"
278 :add-spacing-classes="false"
279 ref="markdownField">
280 <textarea
281 id="note-body"
282 name="note[note]"
283 class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea"
284 data-supports-quick-actions="true"
285 aria-label="Description"
286 v-model="note"
287 ref="textarea"
288 slot="textarea"
289 :disabled="isSubmitting"
290 placeholder="Write a comment or drag your files here..."
291 @keydown.up="editCurrentUserLastNote()"
292 @keydown.meta.enter="handleSave()">
293 </textarea>
294 </markdown-field>
295 <div class="note-form-actions">
296 <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
297 <button
298 @click.prevent="handleSave()"
299 :disabled="isSubmitButtonDisabled"
300 class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
301 type="submit">
302 {{commentButtonTitle}}
303 </button>
304 <button
305 :disabled="isSubmitButtonDisabled"
306 name="button"
307 type="button"
308 class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
309 data-toggle="dropdown"
310 aria-label="Open comment type dropdown">
311 <i
312 aria-hidden="true"
313 class="fa fa-caret-down toggle-icon">
314 </i>
315 </button>
316
317 <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
318 <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
319 <button
320 type="button"
321 class="btn btn-transparent"
322 @click.prevent="setNoteType('comment')">
323 <i
324 aria-hidden="true"
325 class="fa fa-check icon">
326 </i>
327 <div class="description">
328 <strong>Comment</strong>
329 <p>
330 Add a general comment to this issue.
331 </p>
332 </div>
333 </button>
334 </li>
335 <li class="divider droplab-item-ignore"></li>
336 <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
337 <button
338 type="button"
339 class="btn btn-transparent"
340 @click.prevent="setNoteType('discussion')">
341 <i
342 aria-hidden="true"
343 class="fa fa-check icon">
344 </i>
345 <div class="description">
346 <strong>Start discussion</strong>
347 <p>
348 Discuss a specific suggestion or question.
349 </p>
350 </div>
351 </button>
352 </li>
353 </ul>
354 </div>
355 <button
356 type="button"
357 @click="handleSave(true)"
358 v-if="canUpdateIssue"
359 :class="actionButtonClassNames"
360 class="btn btn-comment btn-comment-and-close">
361 {{issueActionButtonTitle}}
362 </button>
363 <button
364 type="button"
365 v-if="note.length"
366 @click="discard"
367 class="btn btn-cancel js-note-discard">
368 Discard draft
369 </button>
370 </div>
371 </form>
372 </div>
373 </div>
374 </li>
375 </ul>
376 </div>
377</template>