PageRenderTime 86ms CodeModel.GetById 18ms app.highlight 49ms RepoModel.GetById 0ms app.codeStats 1ms

/app/assets/javascripts/notes/components/issue_comment_form.vue

https://gitlab.com/mehlah/gitlab-ce
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>