Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/notes/components/comment_form.vue')
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue208
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"