BigW Consortium Gitlab

comment_form.vue 13.1 KB
Newer Older
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 9
  import * as constants from '../constants';
  import eventHub from '../event_hub';
Luke "Jared" Bennett committed
10
  import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
11 12
  import noteSignedOutWidget from './note_signed_out_widget.vue';
  import discussionLockedWidget from './discussion_locked_widget.vue';
13 14
  import markdownField from '../../vue_shared/components/markdown/field.vue';
  import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
Luke "Jared" Bennett committed
15
  import issuableStateMixin from '../mixins/issuable_state';
16

17
  export default {
18 19 20 21 22 23 24 25 26 27 28
    name: 'CommentForm',
    components: {
      issueWarning,
      noteSignedOutWidget,
      discussionLockedWidget,
      markdownField,
      userAvatarLink,
    },
    mixins: [
      issuableStateMixin,
    ],
29 30 31 32
    data() {
      return {
        note: '',
        noteType: constants.COMMENT,
33 34
        // Can't use mapGetters,
        // this needs to be in the data object because it belongs to the state
Simon Knox committed
35
        issueState: this.$store.getters.getNoteableData.state,
36
        isSubmitting: false,
37
        isSubmitButtonDisabled: true,
38
      };
39
    },
40
    computed: {
41
      ...mapGetters([
42
        'getCurrentUserLastNote',
43
        'getUserData',
Simon Knox committed
44
        'getNoteableData',
45
        'getNotesData',
46
      ]),
47
      isLoggedIn() {
Filipa Lacerda committed
48
        return this.getUserData.id;
49 50 51 52 53 54 55
      },
      commentButtonTitle() {
        return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
      },
      isIssueOpen() {
        return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
      },
56
      canCreateNote() {
Simon Knox committed
57
        return this.getNoteableData.current_user.can_create_note;
Luke "Jared" Bennett committed
58
      },
59 60 61
      issueActionButtonTitle() {
        if (this.note.length) {
          const actionText = this.isIssueOpen ? 'close' : 'reopen';
62

63 64 65
          return this.noteType === constants.COMMENT ?
            `Comment & ${actionText} issue` :
            `Start discussion & ${actionText} issue`;
66
        }
67

68 69 70 71 72 73 74 75 76 77
        return this.isIssueOpen ? 'Close issue' : 'Reopen issue';
      },
      actionButtonClassNames() {
        return {
          'btn-reopen': !this.isIssueOpen,
          'btn-close': this.isIssueOpen,
          'js-note-target-close': this.isIssueOpen,
          'js-note-target-reopen': !this.isIssueOpen,
        };
      },
78 79
      markdownDocsPath() {
        return this.getNotesData.markdownDocsPath;
80
      },
81 82
      quickActionsDocsPath() {
        return this.getNotesData.quickActionsDocsPath;
83
      },
84
      markdownPreviewPath() {
Simon Knox committed
85
        return this.getNoteableData.preview_note_path;
86 87 88 89 90
      },
      author() {
        return this.getUserData;
      },
      canUpdateIssue() {
Simon Knox committed
91
        return this.getNoteableData.current_user.can_update;
92 93
      },
      endpoint() {
Simon Knox committed
94
        return this.getNoteableData.create_note_path;
95
      },
96
    },
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
    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.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
      });

      this.initAutoSave();
      this.initTaskList();
    },
114
    methods: {
115
      ...mapActions([
116
        'saveNote',
117 118
        'stopPolling',
        'restartPolling',
119
        'removePlaceholderNotes',
120
      ]),
121
      setIsSubmitButtonDisabled(note, isSubmitting) {
122
        if (!_.isEmpty(note) && !isSubmitting) {
123 124 125 126 127
          this.isSubmitButtonDisabled = false;
        } else {
          this.isSubmitButtonDisabled = true;
        }
      },
128 129 130 131 132 133 134
      handleSave(withIssueAction) {
        if (this.note.length) {
          const noteData = {
            endpoint: this.endpoint,
            flashContainer: this.$el,
            data: {
              note: {
Filipa Lacerda committed
135
                noteable_type: constants.NOTEABLE_TYPE,
Simon Knox committed
136
                noteable_id: this.getNoteableData.id,
137 138
                note: this.note,
              },
139
            },
140
          };
141

142 143 144
          if (this.noteType === constants.DISCUSSION) {
            noteData.data.note.type = constants.DISCUSSION_NOTE;
          }
145
          this.isSubmitting = true;
146
          this.note = ''; // Empty textarea while being requested. Repopulate in catch
147
          this.resizeTextarea();
148
          this.stopPolling();
149

150
          this.saveNote(noteData)
151
            .then((res) => {
152
              this.isSubmitting = false;
153 154
              this.restartPolling();

155 156 157 158
              if (res.errors) {
                if (res.errors.commands_only) {
                  this.discard();
                } else {
159 160 161
                  Flash(
                    'Something went wrong while adding your comment. Please try again.',
                    'alert',
162
                    this.$refs.commentForm,
163
                  );
164
                }
165
              } else {
166
                this.discard();
167
              }
168 169 170 171

              if (withIssueAction) {
                this.toggleIssueState();
              }
172 173
            })
            .catch(() => {
174
              this.isSubmitting = false;
175
              this.discard(false);
176 177 178
              const msg =
                `Your comment could not be submitted!
Please check your network connection and try again.`;
179
              Flash(msg, 'alert', this.$el);
180
              this.note = noteData.data.note.note; // Restore textarea content.
181
              this.removePlaceholderNotes();
182
            });
183 184
        } else {
          this.toggleIssueState();
185
        }
186 187 188
      },
      toggleIssueState() {
        this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
189

190 191 192 193
        // This is out of scope for the Notes Vue component.
        // It was the shortest path to update the issue state and relevant places.
        const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close';
        $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click');
194 195 196 197 198 199 200 201 202
      },
      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 = '';
203
          this.resizeTextarea();
204
          this.$refs.markdownField.previewMarkdown = false;
205
        }
206 207 208

        // reset autostave
        this.autosave.reset();
209 210 211 212
      },
      setNoteType(type) {
        this.noteType = type;
      },
213
      editCurrentUserLastNote() {
214
        if (this.note === '') {
215
          const lastNote = this.getCurrentUserLastNote;
216

217
          if (lastNote) {
218
            eventHub.$emit('enterEditMode', {
219
              noteId: lastNote.id,
220 221
            });
          }
222
        }
223
      },
224
      initAutoSave() {
225
        if (this.isLoggedIn) {
226 227 228 229 230
          this.autosave = new Autosave(
            $(this.$refs.textarea),
            ['Note', 'Issue', this.getNoteableData.id],
            'issue',
          );
231
        }
232
      },
233 234 235 236 237 238
      initTaskList() {
        return new TaskList({
          dataType: 'note',
          fieldName: 'note',
          selector: '.notes',
        });
239
      },
240 241
      resizeTextarea() {
        this.$nextTick(() => {
242
          Autosize.update(this.$refs.textarea);
243 244
        });
      },
245
    },
246
  };
247 248 249
</script>

<template>
250
  <div>
251 252 253 254 255
    <note-signed-out-widget v-if="!isLoggedIn" />
    <discussion-locked-widget
      issuable-type="issue"
      v-else-if="!canCreateNote"
    />
256
    <ul
257
      v-else
258
      class="notes notes-form timeline">
Filipa Lacerda committed
259
      <li class="timeline-entry">
260
        <div class="timeline-entry-inner">
261
          <div class="flash-container error-alert timeline-content"></div>
262 263 264
          <div class="timeline-icon hidden-xs hidden-sm">
            <user-avatar-link
              v-if="author"
265 266 267
              :link-href="author.path"
              :img-src="author.avatar_url"
              :img-alt="author.name"
268
              :img-size="40"
269
            />
270
          </div>
271
          <div class="timeline-content timeline-content-form">
Filipa Lacerda committed
272
            <form
Filipa Lacerda committed
273
              ref="commentForm"
274
              class="new-note common-note-form gfm-form js-main-target-form"
Luke "Jared" Bennett committed
275 276
            >

277
              <div class="error-alert"></div>
278

Luke "Jared" Bennett committed
279
              <issue-warning
Simon Knox committed
280 281 282
                v-if="hasWarning(getNoteableData)"
                :is-locked="isLocked(getNoteableData)"
                :is-confidential="isConfidential(getNoteableData)"
Luke "Jared" Bennett committed
283 284
              />

285
              <markdown-field
286 287 288
                :markdown-preview-path="markdownPreviewPath"
                :markdown-docs-path="markdownDocsPath"
                :quick-actions-docs-path="quickActionsDocsPath"
289
                :add-spacing-classes="false"
290
                ref="markdownField">
291
                <textarea
292
                  id="note-body"
293
                  name="note[note]"
Filipa Lacerda committed
294 295
                  class="note-textarea js-vue-comment-form
js-gfm-input js-autosize markdown-area js-vue-textarea"
296 297 298 299 300
                  data-supports-quick-actions="true"
                  aria-label="Description"
                  v-model="note"
                  ref="textarea"
                  slot="textarea"
301
                  :disabled="isSubmitting"
302 303
                  placeholder="Write a comment or drag your files here..."
                  @keydown.up="editCurrentUserLastNote()"
304 305
                  @keydown.meta.enter="handleSave()"
                  @keydown.ctrl.enter="handleSave()">
306 307 308
                </textarea>
              </markdown-field>
              <div class="note-form-actions">
Filipa Lacerda committed
309 310
                <div
                  class="pull-left btn-group
Filipa Lacerda committed
311
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
312
                  <button
313
                    @click.prevent="handleSave()"
314
                    :disabled="isSubmitButtonDisabled"
Filipa Lacerda committed
315
                    class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
316
                    type="submit">
317
                    {{ commentButtonTitle }}
318 319
                  </button>
                  <button
320
                    :disabled="isSubmitButtonDisabled"
321 322
                    name="button"
                    type="button"
323
                    class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
                    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 issue.
                          </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">
359
                        </i>
360 361 362 363 364 365 366 367 368 369
                        <div class="description">
                          <strong>Start discussion</strong>
                          <p>
                            Discuss a specific suggestion or question.
                          </p>
                        </div>
                      </button>
                    </li>
                  </ul>
                </div>
370
                <button
Filipa Lacerda committed
371 372
                  type="button"
                  @click="handleSave(true)"
373 374
                  v-if="canUpdateIssue"
                  :class="actionButtonClassNames"
375 376
                  :disabled="isSubmitting"
                  class="btn btn-comment btn-comment-and-close js-action-button">
377
                  {{ issueActionButtonTitle }}
378
                </button>
379 380
                <button
                  type="button"
381 382 383 384
                  v-if="note.length"
                  @click="discard"
                  class="btn btn-cancel js-note-discard">
                  Discard draft
385 386
                </button>
              </div>
387
            </form>
388 389
          </div>
        </div>
390 391 392
      </li>
    </ul>
  </div>
393
</template>