diff options
Diffstat (limited to 'app/assets/javascripts/notes/components/comment_form.vue')
-rw-r--r-- | app/assets/javascripts/notes/components/comment_form.vue | 208 |
1 files changed, 113 insertions, 95 deletions
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 50db3b86025..08d7c745791 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -1,39 +1,61 @@ <script> -import { GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui'; +import { + GlAlert, + GlButton, + GlIcon, + GlFormCheckbox, + GlTooltipDirective, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, +} from '@gitlab/ui'; import Autosize from 'autosize'; import $ from 'jquery'; import { mapActions, mapGetters, mapState } from 'vuex'; import Autosave from '~/autosave'; import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import { deprecatedCreateFlash as Flash } from '~/flash'; +import httpStatusCodes from '~/lib/utils/http_status'; import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase, slugifyWithUnderscore, } from '~/lib/utils/text_utility'; -import { __, sprintf } from '~/locale'; +import { sprintf } from '~/locale'; import markdownField from '~/vue_shared/components/markdown/field.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; + import * as constants from '../constants'; import eventHub from '../event_hub'; +import { COMMENT_FORM } from '../i18n'; + import issuableStateMixin from '../mixins/issuable_state'; import CommentFieldLayout from './comment_field_layout.vue'; import discussionLockedWidget from './discussion_locked_widget.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue'; +const { UNPROCESSABLE_ENTITY } = httpStatusCodes; + export default { name: 'CommentForm', + i18n: COMMENT_FORM, + noteTypeComment: constants.COMMENT, + noteTypeDiscussion: constants.DISCUSSION, components: { noteSignedOutWidget, discussionLockedWidget, markdownField, + GlAlert, GlButton, TimelineEntryItem, GlIcon, CommentFieldLayout, GlFormCheckbox, + GlDropdown, + GlDropdownItem, + GlDropdownDivider, }, directives: { GlTooltip: GlTooltipDirective, @@ -49,6 +71,7 @@ export default { return { note: '', noteType: constants.COMMENT, + errors: [], noteIsConfidential: false, isSubmitting: false, }; @@ -63,6 +86,12 @@ export default { 'openState', ]), ...mapState(['isToggleStateButtonLoading']), + isNoteTypeComment() { + return this.noteType === constants.COMMENT; + }, + isNoteTypeDiscussion() { + return this.noteType === constants.DISCUSSION; + }, noteableDisplayName() { return splitCamelCase(this.noteableType).toLowerCase(); }, @@ -70,12 +99,19 @@ export default { return this.getUserData.id; }, commentButtonTitle() { - return this.noteType === constants.COMMENT ? __('Comment') : __('Start thread'); + return this.noteType === constants.COMMENT + ? this.$options.i18n.comment + : this.$options.i18n.startThread; }, startDiscussionDescription() { return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE - ? __('Discuss a specific suggestion or question that needs to be resolved.') - : __('Discuss a specific suggestion or question.'); + ? this.$options.i18n.discussionThatNeedsResolution + : this.$options.i18n.discussion; + }, + commentDescription() { + return sprintf(this.$options.i18n.submitButton.commentHelp, { + noteableDisplayName: this.noteableDisplayName, + }); }, isOpen() { return this.openState === constants.OPENED || this.openState === constants.REOPENED; @@ -90,21 +126,18 @@ export default { const openOrClose = this.isOpen ? 'close' : 'reopen'; if (this.note.length) { - return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), { + return sprintf(this.$options.i18n.actionButtonWithNote, { actionText: this.commentButtonTitle, openOrClose, noteable: this.noteableDisplayName, }); } - return sprintf(__('%{openOrClose} %{noteable}'), { + return sprintf(this.$options.i18n.actionButton, { openOrClose: capitalizeFirstCharacter(openOrClose), noteable: this.noteableDisplayName, }); }, - buttonVariant() { - return this.isOpen ? 'warning' : 'default'; - }, actionButtonClassNames() { return { 'btn-reopen': !this.isOpen, @@ -140,8 +173,8 @@ export default { }, issuableTypeTitle() { return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE - ? __('merge request') - : __('issue'); + ? this.$options.i18n.mergeRequest + : this.$options.i18n.issue; }, isIssue() { return this.noteableDisplayName === constants.ISSUE_NOTEABLE_TYPE; @@ -149,9 +182,6 @@ export default { trackingLabel() { return slugifyWithUnderscore(`${this.commentButtonTitle} button`); }, - hasCloseAndCommentButton() { - return !this.glFeatures.removeCommentCloseReopen; - }, confidentialNotesEnabled() { return Boolean(this.glFeatures.confidentialNotes); }, @@ -177,11 +207,19 @@ export default { 'reopenIssuable', 'toggleIssueLocalState', ]), + handleSaveError({ data, status }) { + if (status === UNPROCESSABLE_ENTITY && data.errors?.commands_only?.length) { + this.errors = data.errors.commands_only; + } else { + this.errors = [this.$options.i18n.GENERIC_UNSUBMITTABLE_NETWORK]; + } + }, handleSave(withIssueAction) { + this.errors = []; + if (this.note.length) { const noteData = { endpoint: this.endpoint, - flashContainer: this.$el, data: { note: { noteable_type: this.noteableType, @@ -212,12 +250,10 @@ export default { this.toggleIssueState(); } }) - .catch(() => { + .catch(({ response }) => { + this.handleSaveError(response); + this.discard(false); - 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. this.removePlaceholderNotes(); }) @@ -260,6 +296,12 @@ export default { setNoteType(type) { this.noteType = type; }, + setNoteTypeToComment() { + this.setNoteType(constants.COMMENT); + }, + setNoteTypeToDiscussion() { + this.setNoteType(constants.DISCUSSION); + }, editCurrentUserLastNote() { if (this.note === '') { const lastNote = this.getCurrentUserLastNote; @@ -276,7 +318,7 @@ export default { const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); this.autosave = new Autosave($(this.$refs.textarea), [ - __('Note'), + this.$options.i18n.note, noteableType, this.getNoteableData.id, ]); @@ -290,6 +332,9 @@ export default { hasEmailParticipants() { return this.getNoteableData.issue_email_participants?.length; }, + dismissError(index) { + this.errors.splice(index, 1); + }, }, }; </script> @@ -300,7 +345,15 @@ export default { <discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" /> <ul v-else-if="canCreateNote" class="notes notes-form timeline"> <timeline-entry-item class="note-form"> - <div class="flash-container error-alert timeline-content"></div> + <gl-alert + v-for="(error, index) in errors" + :key="index" + variant="danger" + class="gl-mb-2" + @dismiss="() => dismissError(index)" + > + {{ error }} + </gl-alert> <div class="timeline-content timeline-content-form"> <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form"> <comment-field-layout @@ -329,8 +382,8 @@ export default { data-qa-selector="comment_field" data-testid="comment-field" :data-supports-quick-actions="!glFeatures.tributeAutocomplete" - :aria-label="__('Description')" - :placeholder="__('Write a comment or drag your files hereā¦')" + :aria-label="$options.i18n.comment" + :placeholder="$options.i18n.bodyPlaceholder" @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()" @keydown.ctrl.enter="handleSave()" @@ -345,87 +398,52 @@ export default { class="gl-mb-6" data-testid="confidential-note-checkbox" > - {{ s__('Notes|Make this comment confidential') }} + {{ $options.i18n.confidential }} <gl-icon v-gl-tooltip:tooltipcontainer.bottom name="question" :size="16" - :title="s__('Notes|Confidential comments are only visible to project members')" + :title="$options.i18n.confidentialVisibility" class="gl-text-gray-500" /> </gl-form-checkbox> - <div - class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" + <gl-dropdown + split + :text="commentButtonTitle" + class="gl-mr-3 js-comment-button js-comment-submit-button comment-type-dropdown" + category="primary" + variant="success" + :disabled="disableSubmitButton" + data-testid="comment-button" + data-qa-selector="comment_button" + :data-track-label="trackingLabel" + data-track-event="click_button" + @click="handleSave()" > - <gl-button - :disabled="disableSubmitButton" - class="js-comment-button js-comment-submit-button" - data-qa-selector="comment_button" - data-testid="comment-button" - type="submit" - category="primary" - variant="success" - :data-track-label="trackingLabel" - data-track-event="click_button" - @click.prevent="handleSave()" - >{{ commentButtonTitle }}</gl-button + <gl-dropdown-item + is-check-item + :is-checked="isNoteTypeComment" + :selected="isNoteTypeComment" + @click="setNoteTypeToComment" > - <gl-button - :disabled="disableSubmitButton" - name="button" - category="primary" - variant="success" - class="note-type-toggle js-note-new-discussion dropdown-toggle" - data-qa-selector="note_dropdown" - data-display="static" - data-toggle="dropdown" - icon="chevron-down" - :aria-label="__('Open comment type dropdown')" - /> - - <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')" - > - <gl-icon name="check" class="icon gl-flex-shrink-0" /> - <div class="description"> - <strong>{{ __('Comment') }}</strong> - <p> - {{ - sprintf(__('Add a general comment to this %{noteableDisplayName}.'), { - noteableDisplayName, - }) - }} - </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" - data-qa-selector="discussion_menu_item" - @click.prevent="setNoteType('discussion')" - > - <gl-icon name="check" class="icon gl-flex-shrink-0" /> - <div class="description"> - <strong>{{ __('Start thread') }}</strong> - <p>{{ startDiscussionDescription }}</p> - </div> - </button> - </li> - </ul> - </div> - + <strong>{{ $options.i18n.submitButton.comment }}</strong> + <p class="gl-m-0">{{ commentDescription }}</p> + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-dropdown-item + is-check-item + :is-checked="isNoteTypeDiscussion" + :selected="isNoteTypeDiscussion" + data-qa-selector="discussion_menu_item" + @click="setNoteTypeToDiscussion" + > + <strong>{{ $options.i18n.submitButton.startThread }}</strong> + <p class="gl-m-0">{{ startDiscussionDescription }}</p> + </gl-dropdown-item> + </gl-dropdown> <gl-button - v-if="hasCloseAndCommentButton && canToggleIssueState" + v-if="canToggleIssueState" :loading="isToggleStateButtonLoading" - category="secondary" - :variant="buttonVariant" :class="[actionButtonClassNames, 'btn-comment btn-comment-and-close']" :disabled="isSubmitting" data-testid="close-reopen-button" |