issue_comment_form.vue 12.8 KB
Newer Older
1
<script>
2
  import { mapActions, mapGetters } from 'vuex';
3
  import _ from 'underscore';
4
  import Autosize from 'autosize';
Phil Hughes's avatar
Phil Hughes committed
5
  import Flash from '../../flash';
6
  import Autosave from '../../autosave';
Filipa Lacerda's avatar
Filipa Lacerda committed
7
  import TaskList from '../../task_list';
8 9
  import * as constants from '../constants';
  import eventHub from '../event_hub';
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
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 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's avatar
Luke "Jared" Bennett committed
15
  import issuableStateMixin from '../mixins/issuable_state';
16

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

Filipa Lacerda's avatar
Filipa Lacerda committed
68 69
          return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`;
        }
70

Filipa Lacerda's avatar
Filipa Lacerda committed
71 72 73 74 75 76 77 78 79 80
        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,
        };
      },
81 82
      markdownDocsPath() {
        return this.getNotesData.markdownDocsPath;
83
      },
84 85
      quickActionsDocsPath() {
        return this.getNotesData.quickActionsDocsPath;
86
      },
87
      markdownPreviewPath() {
88 89 90 91 92 93 94 95 96 97
        return this.getIssueData.preview_note_path;
      },
      author() {
        return this.getUserData;
      },
      canUpdateIssue() {
        return this.getIssueData.current_user.can_update;
      },
      endpoint() {
        return this.getIssueData.create_note_path;
98
      },
99
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
100
    methods: {
101
      ...mapActions([
102
        'saveNote',
103 104
        'stopPolling',
        'restartPolling',
105
        'removePlaceholderNotes',
106
      ]),
107
      setIsSubmitButtonDisabled(note, isSubmitting) {
108
        if (!_.isEmpty(note) && !isSubmitting) {
109 110 111 112 113
          this.isSubmitButtonDisabled = false;
        } else {
          this.isSubmitButtonDisabled = true;
        }
      },
Filipa Lacerda's avatar
Filipa Lacerda committed
114 115 116 117 118 119 120
      handleSave(withIssueAction) {
        if (this.note.length) {
          const noteData = {
            endpoint: this.endpoint,
            flashContainer: this.$el,
            data: {
              note: {
Filipa Lacerda's avatar
Filipa Lacerda committed
121
                noteable_type: constants.NOTEABLE_TYPE,
122
                noteable_id: this.getIssueData.id,
Filipa Lacerda's avatar
Filipa Lacerda committed
123 124
                note: this.note,
              },
125
            },
Filipa Lacerda's avatar
Filipa Lacerda committed
126
          };
127

Filipa Lacerda's avatar
Filipa Lacerda committed
128 129 130
          if (this.noteType === constants.DISCUSSION) {
            noteData.data.note.type = constants.DISCUSSION_NOTE;
          }
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)
Filipa Lacerda's avatar
Filipa Lacerda committed
137
            .then((res) => {
138
              this.isSubmitting = false;
139 140
              this.restartPolling();

Filipa Lacerda's avatar
Filipa Lacerda committed
141 142 143 144
              if (res.errors) {
                if (res.errors.commands_only) {
                  this.discard();
                } else {
145 146 147
                  Flash(
                    'Something went wrong while adding your comment. Please try again.',
                    'alert',
Phil Hughes's avatar
Phil Hughes committed
148
                    this.$refs.commentForm,
149
                  );
Filipa Lacerda's avatar
Filipa Lacerda committed
150
                }
151
              } else {
152
                this.discard();
153
              }
154 155 156 157

              if (withIssueAction) {
                this.toggleIssueState();
              }
Filipa Lacerda's avatar
Filipa Lacerda committed
158 159
            })
            .catch(() => {
160
              this.isSubmitting = false;
Filipa Lacerda's avatar
Filipa Lacerda committed
161
              this.discard(false);
162
              const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
Phil Hughes's avatar
Phil Hughes committed
163
              Flash(msg, 'alert', this.$el);
164
              this.note = noteData.data.note.note; // Restore textarea content.
165
              this.removePlaceholderNotes();
Filipa Lacerda's avatar
Filipa Lacerda committed
166
            });
167 168
        } else {
          this.toggleIssueState();
169
        }
170 171 172
      },
      toggleIssueState() {
        this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
173

174 175 176 177
        // 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');
Filipa Lacerda's avatar
Filipa Lacerda committed
178 179 180 181 182 183 184 185 186
      },
      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 = '';
187
          this.resizeTextarea();
188
          this.$refs.markdownField.previewMarkdown = false;
Filipa Lacerda's avatar
Filipa Lacerda committed
189
        }
190 191 192

        // reset autostave
        this.autosave.reset();
Filipa Lacerda's avatar
Filipa Lacerda committed
193 194 195 196
      },
      setNoteType(type) {
        this.noteType = type;
      },
197
      editCurrentUserLastNote() {
Filipa Lacerda's avatar
Filipa Lacerda committed
198
        if (this.note === '') {
199
          const lastNote = this.getCurrentUserLastNote;
200

201
          if (lastNote) {
Filipa Lacerda's avatar
Filipa Lacerda committed
202
            eventHub.$emit('enterEditMode', {
203
              noteId: lastNote.id,
Filipa Lacerda's avatar
Filipa Lacerda committed
204 205
            });
          }
206
        }
Filipa Lacerda's avatar
Filipa Lacerda committed
207
      },
208
      initAutoSave() {
209 210 211
        if (this.isLoggedIn) {
          this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue');
        }
212
      },
213 214 215 216 217 218
      initTaskList() {
        return new TaskList({
          dataType: 'note',
          fieldName: 'note',
          selector: '.notes',
        });
219
      },
220 221
      resizeTextarea() {
        this.$nextTick(() => {
222
          Autosize.update(this.$refs.textarea);
223 224
        });
      },
225
    },
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
226 227 228
    mixins: [
      issuableStateMixin,
    ],
Filipa Lacerda's avatar
Filipa Lacerda committed
229
    mounted() {
230 231
      // jQuery is needed here because it is a custom event being dispatched with jQuery.
      $(document).on('issuable:change', (e, isClosed) => {
Filipa Lacerda's avatar
Filipa Lacerda committed
232 233
        this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
      });
234

235
      this.initAutoSave();
236
      this.initTaskList();
237
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
238
  };
239 240 241
</script>

<template>
242 243
  <div>
    <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">
Filipa Lacerda's avatar
Filipa Lacerda committed
248
      <li class="timeline-entry">
249
        <div class="timeline-entry-inner">
250
          <div class="flash-container error-alert timeline-content"></div>
251 252 253
          <div class="timeline-icon hidden-xs hidden-sm">
            <user-avatar-link
              v-if="author"
254 255 256
              :link-href="author.path"
              :img-src="author.avatar_url"
              :img-alt="author.name"
Filipa Lacerda's avatar
Filipa Lacerda committed
257 258
              :img-size="40"
              />
259
          </div>
260
          <div class="timeline-content timeline-content-form">
Filipa Lacerda's avatar
Filipa Lacerda committed
261
            <form
Filipa Lacerda's avatar
Filipa Lacerda committed
262
              ref="commentForm"
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
263 264 265
              class="new-note js-quick-submit common-note-form gfm-form js-main-target-form"
            >

266
              <div class="error-alert"></div>
267

Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
268
              <issue-warning
269 270 271
                v-if="hasWarning(getIssueData)"
                :is-locked="isLocked(getIssueData)"
                :is-confidential="isConfidential(getIssueData)"
Luke "Jared" Bennett's avatar
Luke "Jared" Bennett committed
272 273
              />

274
              <markdown-field
275 276 277
                :markdown-preview-path="markdownPreviewPath"
                :markdown-docs-path="markdownDocsPath"
                :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 285 286 287 288
                  data-supports-quick-actions="true"
                  aria-label="Description"
                  v-model="note"
                  ref="textarea"
                  slot="textarea"
289
                  :disabled="isSubmitting"
290 291 292 293 294 295 296 297
                  placeholder="Write a comment or drag your files here..."
                  @keydown.up="editCurrentUserLastNote()"
                  @keydown.meta.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
298
                    @click.prevent="handleSave()"
299
                    :disabled="isSubmitButtonDisabled"
Filipa Lacerda's avatar
Filipa Lacerda committed
300
                    class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
301
                    type="submit">
302 303 304
                    {{commentButtonTitle}}
                  </button>
                  <button
305
                    :disabled="isSubmitButtonDisabled"
306 307
                    name="button"
                    type="button"
308
                    class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 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
                    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">
                          </i>
                        <div class="description">
                          <strong>Start discussion</strong>
                          <p>
                            Discuss a specific suggestion or question.
                          </p>
                        </div>
                      </button>
                    </li>
                  </ul>
                </div>
355
                <button
Filipa Lacerda's avatar
Filipa Lacerda committed
356 357
                  type="button"
                  @click="handleSave(true)"
358 359
                  v-if="canUpdateIssue"
                  :class="actionButtonClassNames"
360
                  class="btn btn-comment btn-comment-and-close">
361
                  {{issueActionButtonTitle}}
362
                </button>
363 364
                <button
                  type="button"
365 366 367 368
                  v-if="note.length"
                  @click="discard"
                  class="btn btn-cancel js-note-discard">
                  Discard draft
369 370
                </button>
              </div>
371
            </form>
372 373
          </div>
        </div>
374 375 376
      </li>
    </ul>
  </div>
377
</template>