issue_comment_form.vue 10.8 KB
Newer Older
1
<script>
2
  /* global Flash, Autosave */
3

4
  import { mapActions, mapGetters } from 'vuex';
5
  import _ from 'underscore';
Filipa Lacerda's avatar
Filipa Lacerda committed
6 7 8 9 10
  import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
  import markdownField from '../../vue_shared/components/markdown/field.vue';
  import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
  import eventHub from '../event_hub';
  import * as constants from '../constants';
11
  import '../../autosave';
12

Filipa Lacerda's avatar
Filipa Lacerda committed
13 14 15 16 17
  export default {
    data() {
      return {
        note: '',
        noteType: constants.COMMENT,
18 19 20
        // Can't use mapGetters,
        // this needs to be in the data object because it belongs to the state
        issueState: this.$store.getters.getIssueData.state,
21
        isSubmitting: false,
22
        isSubmitButtonDisabled: true,
Filipa Lacerda's avatar
Filipa Lacerda committed
23
      };
24
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
25 26 27 28
    components: {
      userAvatarLink,
      markdownField,
      issueNoteSignedOutWidget,
29
    },
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
    watch: {
      note(newNote) {
        if (!_.isEmpty(newNote) && !this.isSubmitting) {
          this.isSubmitButtonDisabled = false;
        } else {
          this.isSubmitButtonDisabled = true;
        }
      },
      isSubmitting(newValue) {
        if (!_.isEmpty(this.note) && !newValue) {
          this.isSubmitButtonDisabled = false;
        } else {
          this.isSubmitButtonDisabled = true;
        }
      },
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
46
    computed: {
47
      ...mapGetters([
48
        'getCurrentUserLastNote',
49
        'getUserData',
50
        'getIssueData',
51
        'getNotesData',
52
      ]),
Filipa Lacerda's avatar
Filipa Lacerda committed
53
      isLoggedIn() {
54
        return this.getUserData !== null;
Filipa Lacerda's avatar
Filipa Lacerda committed
55 56 57 58 59 60 61 62 63 64
      },
      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';
65

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

Filipa Lacerda's avatar
Filipa Lacerda committed
69 70 71 72 73 74 75 76 77 78
        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,
        };
      },
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
      markdownDocsUrl() {
        return this.getNotesData.markdownDocs;
      },
      quickActionsDocsUrl() {
        return this.getNotesData.quickActionsDocs;
      },
      markdownPreviewUrl() {
        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;
96
      },
97
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
98
    methods: {
99
      ...mapActions([
100
        'saveNote',
101
      ]),
Filipa Lacerda's avatar
Filipa Lacerda committed
102 103 104 105 106 107 108 109 110
      handleSave(withIssueAction) {
        if (this.note.length) {
          const noteData = {
            endpoint: this.endpoint,
            flashContainer: this.$el,
            data: {
              full_data: true,
              note: {
                noteable_type: 'Issue',
111
                noteable_id: this.getIssueData.id,
Filipa Lacerda's avatar
Filipa Lacerda committed
112 113
                note: this.note,
              },
114
            },
Filipa Lacerda's avatar
Filipa Lacerda committed
115
          };
116

Filipa Lacerda's avatar
Filipa Lacerda committed
117 118 119
          if (this.noteType === constants.DISCUSSION) {
            noteData.data.note.type = constants.DISCUSSION_NOTE;
          }
120

121 122
          this.isSubmitting = true;

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

Filipa Lacerda's avatar
Filipa Lacerda committed
146 147 148 149
        if (withIssueAction) {
          if (this.isIssueOpen) {
            this.issueState = constants.CLOSED;
          } else {
150
            this.issueState = constants.REOPENED;
Filipa Lacerda's avatar
Filipa Lacerda committed
151
          }
152

Filipa Lacerda's avatar
Filipa Lacerda committed
153
          this.isIssueOpen = !this.isIssueOpen;
154

Filipa Lacerda's avatar
Filipa Lacerda committed
155 156 157 158 159 160 161 162 163 164 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');
        }
      },
      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 = '';
        }
170 171 172

        // reset autostave
        this.autosave.reset();
Filipa Lacerda's avatar
Filipa Lacerda committed
173 174 175 176
      },
      setNoteType(type) {
        this.noteType = type;
      },
177
      editCurrentUserLastNote() {
Filipa Lacerda's avatar
Filipa Lacerda committed
178
        if (this.note === '') {
179
          const lastNote = this.getCurrentUserLastNote(window.gon.current_user_id);
180

181
          if (lastNote) {
Filipa Lacerda's avatar
Filipa Lacerda committed
182
            eventHub.$emit('enterEditMode', {
183
              noteId: lastNote.id,
Filipa Lacerda's avatar
Filipa Lacerda committed
184 185
            });
          }
186
        }
Filipa Lacerda's avatar
Filipa Lacerda committed
187
      },
188
      initAutoSave() {
189
        this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id]);
190
      },
191
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
192
    mounted() {
193 194
      // 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
195 196
        this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
      });
197

198
      this.initAutoSave();
199
    },
Filipa Lacerda's avatar
Filipa Lacerda committed
200
  };
201 202 203
</script>

<template>
204 205 206 207 208
  <div>
    <issue-note-signed-out-widget v-if="!isLoggedIn" />
    <ul
      v-if="isLoggedIn"
      class="notes notes-form timeline new-note">
209
      <li class="timeline-entry" ref="commentForm">
210
        <div class="timeline-entry-inner">
211
          <div class="flash-container timeline-content"></div>
212 213 214
          <div class="timeline-icon hidden-xs hidden-sm">
            <user-avatar-link
              v-if="author"
215 216 217
              :link-href="author.path"
              :img-src="author.avatar_url"
              :img-alt="author.name"
Filipa Lacerda's avatar
Filipa Lacerda committed
218 219
              :img-size="40"
              />
220
          </div>
221
          <div class="js-main-target-form timeline-content timeline-content-form common-note-form">
222 223 224 225 226 227 228 229 230
            <form>
              <markdown-field
                :markdown-preview-url="markdownPreviewUrl"
                :markdown-docs="markdownDocsUrl"
                :quick-actions-docs="quickActionsDocsUrl"
                :add-spacing-classes="false">
                <textarea
                  id="note-body"
                  name="note[note]"
231
                  class="note-textarea js-gfm-input markdown-area"
232 233 234 235 236 237 238 239 240 241 242 243 244 245
                  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
                    @click="handleSave()"
246
                    :disabled="isSubmitButtonDisabled"
247 248 249 250 251
                    class="btn btn-nr btn-create comment-btn js-comment-button js-comment-submit-button"
                    type="button">
                    {{commentButtonTitle}}
                  </button>
                  <button
252
                    :disabled="isSubmitButtonDisabled"
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
                    name="button"
                    type="button"
                    class="btn btn-nr comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
                    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>
302
                <button
303 304 305 306 307 308
                  type="button"
                  @click="handleSave(true)"
                  v-if="canUpdateIssue"
                  :class="actionButtonClassNames"
                  class="btn btn-nr btn-comment btn-comment-and-close">
                  {{issueActionButtonTitle}}
309
                </button>
310 311
                <button
                  type="button"
312 313 314 315
                  v-if="note.length"
                  @click="discard"
                  class="btn btn-cancel js-note-discard">
                  Discard draft
316 317
                </button>
              </div>
318
            </form>
319 320
          </div>
        </div>
321 322 323
      </li>
    </ul>
  </div>
324
</template>