431 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Vue
		
	
	
	
			
		
		
	
	
			431 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Vue
		
	
	
	
<script>
 | 
						|
import { mapGetters, mapActions, mapState } from 'vuex';
 | 
						|
import { mergeUrlParams } from '~/lib/utils/url_utility';
 | 
						|
import eventHub from '../event_hub';
 | 
						|
import NoteableWarning from '../../vue_shared/components/notes/noteable_warning.vue';
 | 
						|
import markdownField from '../../vue_shared/components/markdown/field.vue';
 | 
						|
import issuableStateMixin from '../mixins/issuable_state';
 | 
						|
import resolvable from '../mixins/resolvable';
 | 
						|
import { __, sprintf } from '~/locale';
 | 
						|
import { getDraft, updateDraft } from '~/lib/utils/autosave';
 | 
						|
 | 
						|
export default {
 | 
						|
  name: 'NoteForm',
 | 
						|
  components: {
 | 
						|
    NoteableWarning,
 | 
						|
    markdownField,
 | 
						|
  },
 | 
						|
  mixins: [issuableStateMixin, resolvable],
 | 
						|
  props: {
 | 
						|
    noteBody: {
 | 
						|
      type: String,
 | 
						|
      required: false,
 | 
						|
      default: '',
 | 
						|
    },
 | 
						|
    noteId: {
 | 
						|
      type: [String, Number],
 | 
						|
      required: false,
 | 
						|
      default: '',
 | 
						|
    },
 | 
						|
    saveButtonTitle: {
 | 
						|
      type: String,
 | 
						|
      required: false,
 | 
						|
      default: __('Save comment'),
 | 
						|
    },
 | 
						|
    discussion: {
 | 
						|
      type: Object,
 | 
						|
      required: false,
 | 
						|
      default: () => ({}),
 | 
						|
    },
 | 
						|
    isEditing: {
 | 
						|
      type: Boolean,
 | 
						|
      required: true,
 | 
						|
    },
 | 
						|
    lineCode: {
 | 
						|
      type: String,
 | 
						|
      required: false,
 | 
						|
      default: '',
 | 
						|
    },
 | 
						|
    resolveDiscussion: {
 | 
						|
      type: Boolean,
 | 
						|
      required: false,
 | 
						|
      default: false,
 | 
						|
    },
 | 
						|
    line: {
 | 
						|
      type: Object,
 | 
						|
      required: false,
 | 
						|
      default: null,
 | 
						|
    },
 | 
						|
    note: {
 | 
						|
      type: Object,
 | 
						|
      required: false,
 | 
						|
      default: null,
 | 
						|
    },
 | 
						|
    diffFile: {
 | 
						|
      type: Object,
 | 
						|
      required: false,
 | 
						|
      default: null,
 | 
						|
    },
 | 
						|
    helpPagePath: {
 | 
						|
      type: String,
 | 
						|
      required: false,
 | 
						|
      default: '',
 | 
						|
    },
 | 
						|
    autosaveKey: {
 | 
						|
      type: String,
 | 
						|
      required: false,
 | 
						|
      default: '',
 | 
						|
    },
 | 
						|
    showSuggestPopover: {
 | 
						|
      type: Boolean,
 | 
						|
      required: false,
 | 
						|
      default: false,
 | 
						|
    },
 | 
						|
    isDraft: {
 | 
						|
      type: Boolean,
 | 
						|
      required: false,
 | 
						|
      default: false,
 | 
						|
    },
 | 
						|
  },
 | 
						|
  data() {
 | 
						|
    let updatedNoteBody = this.noteBody;
 | 
						|
 | 
						|
    if (!updatedNoteBody && this.autosaveKey) {
 | 
						|
      updatedNoteBody = getDraft(this.autosaveKey) || '';
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
      updatedNoteBody,
 | 
						|
      conflictWhileEditing: false,
 | 
						|
      isSubmitting: false,
 | 
						|
      isResolving: this.resolveDiscussion,
 | 
						|
      isUnresolving: !this.resolveDiscussion,
 | 
						|
      resolveAsThread: true,
 | 
						|
      isSubmittingWithKeydown: false,
 | 
						|
    };
 | 
						|
  },
 | 
						|
  computed: {
 | 
						|
    ...mapGetters([
 | 
						|
      'getDiscussionLastNote',
 | 
						|
      'getNoteableData',
 | 
						|
      'getNoteableDataByProp',
 | 
						|
      'getNotesDataByProp',
 | 
						|
      'getUserDataByProp',
 | 
						|
    ]),
 | 
						|
    ...mapState({
 | 
						|
      withBatchComments: state => state.batchComments?.withBatchComments,
 | 
						|
    }),
 | 
						|
    ...mapGetters('batchComments', ['hasDrafts']),
 | 
						|
    showBatchCommentsActions() {
 | 
						|
      return this.withBatchComments && this.noteId === '' && !this.discussion.for_commit;
 | 
						|
    },
 | 
						|
    showResolveDiscussionToggle() {
 | 
						|
      return (this.discussion?.id && this.discussion.resolvable) || this.isDraft;
 | 
						|
    },
 | 
						|
    noteHash() {
 | 
						|
      if (this.noteId) {
 | 
						|
        return `#note_${this.noteId}`;
 | 
						|
      }
 | 
						|
      return '#';
 | 
						|
    },
 | 
						|
    diffParams() {
 | 
						|
      if (this.diffFile) {
 | 
						|
        return {
 | 
						|
          filePath: this.diffFile.file_path,
 | 
						|
          refs: this.diffFile.diff_refs,
 | 
						|
        };
 | 
						|
      } else if (this.note && this.note.position) {
 | 
						|
        return {
 | 
						|
          filePath: this.note.position.new_path,
 | 
						|
          refs: this.note.position,
 | 
						|
        };
 | 
						|
      } else if (this.discussion && this.discussion.diff_file) {
 | 
						|
        return {
 | 
						|
          filePath: this.discussion.diff_file.file_path,
 | 
						|
          refs: this.discussion.diff_file.diff_refs,
 | 
						|
        };
 | 
						|
      }
 | 
						|
 | 
						|
      return null;
 | 
						|
    },
 | 
						|
    markdownPreviewPath() {
 | 
						|
      const notable = this.getNoteableDataByProp('preview_note_path');
 | 
						|
 | 
						|
      const previewSuggestions = this.line && this.diffParams;
 | 
						|
      const params = previewSuggestions
 | 
						|
        ? {
 | 
						|
            preview_suggestions: previewSuggestions,
 | 
						|
            line: this.line.new_line,
 | 
						|
            file_path: this.diffParams.filePath,
 | 
						|
            base_sha: this.diffParams.refs.base_sha,
 | 
						|
            start_sha: this.diffParams.refs.start_sha,
 | 
						|
            head_sha: this.diffParams.refs.head_sha,
 | 
						|
          }
 | 
						|
        : {};
 | 
						|
 | 
						|
      return mergeUrlParams(params, notable);
 | 
						|
    },
 | 
						|
    markdownDocsPath() {
 | 
						|
      return this.getNotesDataByProp('markdownDocsPath');
 | 
						|
    },
 | 
						|
    quickActionsDocsPath() {
 | 
						|
      return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
 | 
						|
    },
 | 
						|
    currentUserId() {
 | 
						|
      return this.getUserDataByProp('id');
 | 
						|
    },
 | 
						|
    isDisabled() {
 | 
						|
      return !this.updatedNoteBody.length || this.isSubmitting;
 | 
						|
    },
 | 
						|
    discussionNote() {
 | 
						|
      const discussionNote = this.discussion.id
 | 
						|
        ? this.getDiscussionLastNote(this.discussion)
 | 
						|
        : this.note;
 | 
						|
      return discussionNote || {};
 | 
						|
    },
 | 
						|
    canSuggest() {
 | 
						|
      return (
 | 
						|
        this.getNoteableData.can_receive_suggestion &&
 | 
						|
        (this.line && this.line.can_receive_suggestion)
 | 
						|
      );
 | 
						|
    },
 | 
						|
    changedCommentText() {
 | 
						|
      return sprintf(
 | 
						|
        __(
 | 
						|
          'This comment has changed since you started editing, please review the %{startTag}updated comment%{endTag} to ensure information is not lost.',
 | 
						|
        ),
 | 
						|
        {
 | 
						|
          startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`,
 | 
						|
          endTag: '</a>',
 | 
						|
        },
 | 
						|
        false,
 | 
						|
      );
 | 
						|
    },
 | 
						|
  },
 | 
						|
  watch: {
 | 
						|
    noteBody() {
 | 
						|
      if (this.updatedNoteBody === this.noteBody) {
 | 
						|
        this.updatedNoteBody = this.noteBody;
 | 
						|
      } else {
 | 
						|
        this.conflictWhileEditing = true;
 | 
						|
      }
 | 
						|
    },
 | 
						|
  },
 | 
						|
  mounted() {
 | 
						|
    this.$refs.textarea.focus();
 | 
						|
  },
 | 
						|
  methods: {
 | 
						|
    ...mapActions(['toggleResolveNote']),
 | 
						|
    shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState) {
 | 
						|
      const newResolvedStateAfterUpdate =
 | 
						|
        this.shouldBeResolved && this.shouldBeResolved(shouldResolve);
 | 
						|
 | 
						|
      const shouldToggleState =
 | 
						|
        newResolvedStateAfterUpdate !== undefined &&
 | 
						|
        beforeSubmitDiscussionState !== newResolvedStateAfterUpdate;
 | 
						|
 | 
						|
      return shouldResolve || shouldToggleState;
 | 
						|
    },
 | 
						|
    editMyLastNote() {
 | 
						|
      if (this.updatedNoteBody === '') {
 | 
						|
        const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
 | 
						|
 | 
						|
        if (lastNoteInDiscussion) {
 | 
						|
          eventHub.$emit('enterEditMode', {
 | 
						|
            noteId: lastNoteInDiscussion.id,
 | 
						|
          });
 | 
						|
        }
 | 
						|
      }
 | 
						|
    },
 | 
						|
    cancelHandler(shouldConfirm = false) {
 | 
						|
      // Sends information about confirm message and if the textarea has changed
 | 
						|
      this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody);
 | 
						|
    },
 | 
						|
    onInput() {
 | 
						|
      if (this.isSubmittingWithKeydown) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      if (this.autosaveKey) {
 | 
						|
        const { autosaveKey, updatedNoteBody: text } = this;
 | 
						|
        updateDraft(autosaveKey, text);
 | 
						|
      }
 | 
						|
    },
 | 
						|
    handleKeySubmit() {
 | 
						|
      if (this.showBatchCommentsActions) {
 | 
						|
        this.handleAddToReview();
 | 
						|
      } else {
 | 
						|
        this.isSubmittingWithKeydown = true;
 | 
						|
        this.handleUpdate();
 | 
						|
      }
 | 
						|
    },
 | 
						|
    handleUpdate(shouldResolve) {
 | 
						|
      const beforeSubmitDiscussionState = this.discussionResolved;
 | 
						|
      this.isSubmitting = true;
 | 
						|
 | 
						|
      this.$emit(
 | 
						|
        'handleFormUpdate',
 | 
						|
        this.updatedNoteBody,
 | 
						|
        this.$refs.editNoteForm,
 | 
						|
        () => {
 | 
						|
          this.isSubmitting = false;
 | 
						|
 | 
						|
          if (this.shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState)) {
 | 
						|
            this.resolveHandler(beforeSubmitDiscussionState);
 | 
						|
          }
 | 
						|
        },
 | 
						|
        this.discussionResolved ? !this.isUnresolving : this.isResolving,
 | 
						|
      );
 | 
						|
    },
 | 
						|
    shouldBeResolved(resolveStatus) {
 | 
						|
      if (this.withBatchComments) {
 | 
						|
        return (
 | 
						|
          (this.discussionResolved && !this.isUnresolving) ||
 | 
						|
          (!this.discussionResolved && this.isResolving)
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      return resolveStatus;
 | 
						|
    },
 | 
						|
    handleAddToReview() {
 | 
						|
      // check if draft should resolve thread
 | 
						|
      const shouldResolve =
 | 
						|
        (this.discussionResolved && !this.isUnresolving) ||
 | 
						|
        (!this.discussionResolved && this.isResolving);
 | 
						|
      this.isSubmitting = true;
 | 
						|
 | 
						|
      this.$emit('handleFormUpdateAddToReview', this.updatedNoteBody, shouldResolve);
 | 
						|
    },
 | 
						|
  },
 | 
						|
};
 | 
						|
</script>
 | 
						|
 | 
						|
<template>
 | 
						|
  <div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form">
 | 
						|
    <div
 | 
						|
      v-if="conflictWhileEditing"
 | 
						|
      class="js-conflict-edit-warning alert alert-danger"
 | 
						|
      v-html="changedCommentText"
 | 
						|
    ></div>
 | 
						|
    <div class="flash-container timeline-content"></div>
 | 
						|
    <form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
 | 
						|
      <noteable-warning
 | 
						|
        v-if="hasWarning(getNoteableData)"
 | 
						|
        :is-locked="isLocked(getNoteableData)"
 | 
						|
        :is-confidential="isConfidential(getNoteableData)"
 | 
						|
        :locked-noteable-docs-path="lockedIssueDocsPath"
 | 
						|
        :confidential-noteable-docs-path="confidentialIssueDocsPath"
 | 
						|
      />
 | 
						|
 | 
						|
      <markdown-field
 | 
						|
        :markdown-preview-path="markdownPreviewPath"
 | 
						|
        :markdown-docs-path="markdownDocsPath"
 | 
						|
        :quick-actions-docs-path="quickActionsDocsPath"
 | 
						|
        :line="line"
 | 
						|
        :note="discussionNote"
 | 
						|
        :can-suggest="canSuggest"
 | 
						|
        :add-spacing-classes="false"
 | 
						|
        :help-page-path="helpPagePath"
 | 
						|
        :show-suggest-popover="showSuggestPopover"
 | 
						|
        @handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
 | 
						|
      >
 | 
						|
        <textarea
 | 
						|
          id="note_note"
 | 
						|
          ref="textarea"
 | 
						|
          slot="textarea"
 | 
						|
          v-model="updatedNoteBody"
 | 
						|
          :data-supports-quick-actions="!isEditing"
 | 
						|
          name="note[note]"
 | 
						|
          class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
 | 
						|
          dir="auto"
 | 
						|
          :aria-label="__('Description')"
 | 
						|
          :placeholder="__('Write a comment or drag your files here…')"
 | 
						|
          @keydown.meta.enter="handleKeySubmit()"
 | 
						|
          @keydown.ctrl.enter="handleKeySubmit()"
 | 
						|
          @keydown.exact.up="editMyLastNote()"
 | 
						|
          @keydown.exact.esc="cancelHandler(true)"
 | 
						|
          @input="onInput"
 | 
						|
        ></textarea>
 | 
						|
      </markdown-field>
 | 
						|
      <div class="note-form-actions clearfix">
 | 
						|
        <template v-if="showBatchCommentsActions">
 | 
						|
          <p v-if="showResolveDiscussionToggle">
 | 
						|
            <label>
 | 
						|
              <template v-if="discussionResolved">
 | 
						|
                <input
 | 
						|
                  v-model="isUnresolving"
 | 
						|
                  type="checkbox"
 | 
						|
                  class="js-unresolve-checkbox"
 | 
						|
                  data-qa-selector="unresolve_review_discussion_checkbox"
 | 
						|
                />
 | 
						|
                {{ __('Unresolve thread') }}
 | 
						|
              </template>
 | 
						|
              <template v-else>
 | 
						|
                <input
 | 
						|
                  v-model="isResolving"
 | 
						|
                  type="checkbox"
 | 
						|
                  class="js-resolve-checkbox"
 | 
						|
                  data-qa-selector="resolve_review_discussion_checkbox"
 | 
						|
                />
 | 
						|
                {{ __('Resolve thread') }}
 | 
						|
              </template>
 | 
						|
            </label>
 | 
						|
          </p>
 | 
						|
          <div>
 | 
						|
            <button
 | 
						|
              :disabled="isDisabled"
 | 
						|
              type="button"
 | 
						|
              class="btn btn-success qa-start-review"
 | 
						|
              @click="handleAddToReview"
 | 
						|
            >
 | 
						|
              <template v-if="hasDrafts">{{ __('Add to review') }}</template>
 | 
						|
              <template v-else>{{ __('Start a review') }}</template>
 | 
						|
            </button>
 | 
						|
            <button
 | 
						|
              :disabled="isDisabled"
 | 
						|
              type="button"
 | 
						|
              class="btn qa-comment-now js-comment-button"
 | 
						|
              @click="handleUpdate()"
 | 
						|
            >
 | 
						|
              {{ __('Add comment now') }}
 | 
						|
            </button>
 | 
						|
            <button
 | 
						|
              class="btn note-edit-cancel js-close-discussion-note-form"
 | 
						|
              type="button"
 | 
						|
              data-testid="cancelBatchCommentsEnabled"
 | 
						|
              @click="cancelHandler(true)"
 | 
						|
            >
 | 
						|
              {{ __('Cancel') }}
 | 
						|
            </button>
 | 
						|
          </div>
 | 
						|
        </template>
 | 
						|
        <template v-else>
 | 
						|
          <button
 | 
						|
            :disabled="isDisabled"
 | 
						|
            type="button"
 | 
						|
            class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button"
 | 
						|
            @click="handleUpdate()"
 | 
						|
          >
 | 
						|
            {{ saveButtonTitle }}
 | 
						|
          </button>
 | 
						|
          <button
 | 
						|
            v-if="discussion.resolvable"
 | 
						|
            class="btn btn-nr btn-default gl-mr-3 js-comment-resolve-button"
 | 
						|
            @click.prevent="handleUpdate(true)"
 | 
						|
          >
 | 
						|
            {{ resolveButtonTitle }}
 | 
						|
          </button>
 | 
						|
          <button
 | 
						|
            class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
 | 
						|
            type="button"
 | 
						|
            data-testid="cancel"
 | 
						|
            @click="cancelHandler(true)"
 | 
						|
          >
 | 
						|
            {{ __('Cancel') }}
 | 
						|
          </button>
 | 
						|
        </template>
 | 
						|
      </div>
 | 
						|
    </form>
 | 
						|
  </div>
 | 
						|
</template>
 |