issue_comment_form.vue 12.2 KB
Newer Older
1
<script>
2
  /* global Flash, Autosave */
3
  import { mapActions, mapGetters } from 'vuex';
4
  import _ from 'underscore';
5
  import autosize from 'vendor/autosize';
6
  import '../../autosave';
Filipa Lacerda's avatar
Filipa Lacerda committed
7
  import TaskList from '../../task_list';
8 9 10 11 12 13
  import * as constants from '../constants';
  import eventHub from '../event_hub';
  import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
  import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
  import markdownField from '../../vue_shared/components/markdown/field.vue';
  import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
14

Filipa Lacerda's avatar
Filipa Lacerda committed
15
  export default {
16
    name: 'issueCommentForm',
Filipa Lacerda's avatar
Filipa Lacerda committed
17 18 19 20
    data() {
      return {
        note: '',
        noteType: constants.COMMENT,
21 22 23
        // Can't use mapGetters,
        // this needs to be in the data object because it belongs to the state
        issueState: this.$store.getters.getIssueData.state,
24
        isSubmitting: false,
25
        isSubmitButtonDisabled: true,
Filipa Lacerda's avatar
Filipa Lacerda committed
26
      };
27
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
28
    components: {
29
      confidentialIssue,
Filipa Lacerda's avatar
Filipa Lacerda committed
30
      issueNoteSignedOutWidget,
31 32
      markdownField,
      userAvatarLink,
33
    },
34 35
    watch: {
      note(newNote) {
36
        this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
37 38
      },
      isSubmitting(newValue) {
39
        this.setIsSubmitButtonDisabled(this.note, newValue);
40 41
      },
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
42
    computed: {
43
      ...mapGetters([
44
        'getCurrentUserLastNote',
45
        'getUserData',
46
        'getIssueData',
47
        'getNotesData',
48
      ]),
Filipa Lacerda's avatar
Filipa Lacerda committed
49
      isLoggedIn() {
Filipa Lacerda's avatar
Filipa Lacerda committed
50
        return this.getUserData.id;
Filipa Lacerda's avatar
Filipa Lacerda committed
51 52 53 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;
      },
      issueActionButtonTitle() {
        if (this.note.length) {
          const actionText = this.isIssueOpen ? 'close' : 'reopen';
61

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

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

Filipa Lacerda's avatar
Filipa Lacerda committed
123 124 125
          if (this.noteType === constants.DISCUSSION) {
            noteData.data.note.type = constants.DISCUSSION_NOTE;
          }
126
          this.isSubmitting = true;
127
          this.note = ''; // Empty textarea while being requested. Repopulate in catch
128
          this.resizeTextarea();
129

130
          this.saveNote(noteData)
Filipa Lacerda's avatar
Filipa Lacerda committed
131
            .then((res) => {
132
              this.isSubmitting = false;
Filipa Lacerda's avatar
Filipa Lacerda committed
133 134 135 136
              if (res.errors) {
                if (res.errors.commands_only) {
                  this.discard();
                } else {
137 138 139 140 141
                  Flash(
                    'Something went wrong while adding your comment. Please try again.',
                    'alert',
                    $(this.$refs.commentForm),
                  );
Filipa Lacerda's avatar
Filipa Lacerda committed
142
                }
143
              } else {
144
                this.discard();
145
              }
146 147 148 149

              if (withIssueAction) {
                this.toggleIssueState();
              }
Filipa Lacerda's avatar
Filipa Lacerda committed
150 151
            })
            .catch(() => {
152
              this.isSubmitting = false;
Filipa Lacerda's avatar
Filipa Lacerda committed
153
              this.discard(false);
154 155 156
              const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
              Flash(msg, 'alert', $(this.$el));
              this.note = noteData.data.note.note; // Restore textarea content.
157
              this.removePlaceholderNotes();
Filipa Lacerda's avatar
Filipa Lacerda committed
158
            });
159 160
        } else {
          this.toggleIssueState();
161
        }
162 163 164
      },
      toggleIssueState() {
        this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
165

166 167 168 169
        // 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
170 171 172 173 174 175 176 177 178
      },
      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 = '';
179
          this.resizeTextarea();
Filipa Lacerda's avatar
Filipa Lacerda committed
180
        }
181 182 183

        // reset autostave
        this.autosave.reset();
Filipa Lacerda's avatar
Filipa Lacerda committed
184 185 186 187
      },
      setNoteType(type) {
        this.noteType = type;
      },
188
      editCurrentUserLastNote() {
Filipa Lacerda's avatar
Filipa Lacerda committed
189
        if (this.note === '') {
190
          const lastNote = this.getCurrentUserLastNote;
191

192
          if (lastNote) {
Filipa Lacerda's avatar
Filipa Lacerda committed
193
            eventHub.$emit('enterEditMode', {
194
              noteId: lastNote.id,
Filipa Lacerda's avatar
Filipa Lacerda committed
195 196
            });
          }
197
        }
Filipa Lacerda's avatar
Filipa Lacerda committed
198
      },
199
      initAutoSave() {
200 201 202
        if (this.isLoggedIn) {
          this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue');
        }
203
      },
204 205 206 207 208 209
      initTaskList() {
        return new TaskList({
          dataType: 'note',
          fieldName: 'note',
          selector: '.notes',
        });
210
      },
211 212 213 214 215
      resizeTextarea() {
        this.$nextTick(() => {
          autosize.update(this.$refs.textarea);
        });
      },
216
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
217
    mounted() {
218 219
      // 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
220 221
        this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
      });
222

223
      this.initAutoSave();
224
      this.initTaskList();
225
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
226
  };
227 228 229
</script>

<template>
230 231 232
  <div>
    <issue-note-signed-out-widget v-if="!isLoggedIn" />
    <ul
233
      v-else
234
      class="notes notes-form timeline">
Filipa Lacerda's avatar
Filipa Lacerda committed
235
      <li class="timeline-entry">
236
        <div class="timeline-entry-inner">
237
          <div class="flash-container error-alert timeline-content"></div>
238 239 240
          <div class="timeline-icon hidden-xs hidden-sm">
            <user-avatar-link
              v-if="author"
241 242 243
              :link-href="author.path"
              :img-src="author.avatar_url"
              :img-alt="author.name"
Filipa Lacerda's avatar
Filipa Lacerda committed
244 245
              :img-size="40"
              />
246
          </div>
247
          <div class="timeline-content timeline-content-form">
Filipa Lacerda's avatar
Filipa Lacerda committed
248
            <form
Filipa Lacerda's avatar
Filipa Lacerda committed
249
              ref="commentForm"
250
              class="new-note js-quick-submit common-note-form gfm-form js-main-target-form">
251
              <confidentialIssue v-if="isConfidentialIssue" />
252
              <div class="error-alert"></div>
253
              <markdown-field
254 255 256
                :markdown-preview-path="markdownPreviewPath"
                :markdown-docs-path="markdownDocsPath"
                :quick-actions-docs-path="quickActionsDocsPath"
257 258
                :add-spacing-classes="false"
                :is-confidential-issue="isConfidentialIssue">
259
                <textarea
260
                  id="note-body"
261
                  name="note[note]"
262
                  class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea"
263 264 265 266 267 268 269 270 271 272 273 274 275
                  data-supports-quick-actions="true"
                  aria-label="Description"
                  v-model="note"
                  ref="textarea"
                  slot="textarea"
                  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
276
                    @click.prevent="handleSave()"
277
                    :disabled="isSubmitButtonDisabled"
Filipa Lacerda's avatar
Filipa Lacerda committed
278
                    class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
279
                    type="submit">
280 281 282
                    {{commentButtonTitle}}
                  </button>
                  <button
283
                    :disabled="isSubmitButtonDisabled"
284 285
                    name="button"
                    type="button"
286
                    class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
                    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>
333
                <button
Filipa Lacerda's avatar
Filipa Lacerda committed
334 335
                  type="button"
                  @click="handleSave(true)"
336 337
                  v-if="canUpdateIssue"
                  :class="actionButtonClassNames"
338
                  class="btn btn-comment btn-comment-and-close">
339
                  {{issueActionButtonTitle}}
340
                </button>
341 342
                <button
                  type="button"
343 344 345 346
                  v-if="note.length"
                  @click="discard"
                  class="btn btn-cancel js-note-discard">
                  Discard draft
347 348
                </button>
              </div>
349
            </form>
350 351
          </div>
        </div>
352 353 354
      </li>
    </ul>
  </div>
355
</template>