diff options
Diffstat (limited to 'app/assets/javascripts/notes')
12 files changed, 312 insertions, 171 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" diff --git a/app/assets/javascripts/notes/components/discussion_actions.vue b/app/assets/javascripts/notes/components/discussion_actions.vue index 27408bc3354..6f0745d4fb0 100644 --- a/app/assets/javascripts/notes/components/discussion_actions.vue +++ b/app/assets/javascripts/notes/components/discussion_actions.vue @@ -50,8 +50,8 @@ export default { <div class="discussion-with-resolve-btn clearfix"> <reply-placeholder data-qa-selector="discussion_reply_tab" - :button-text="s__('MergeRequests|Reply...')" - @onClick="$emit('showReplyForm')" + :placeholder-text="__('Reply…')" + @focus="$emit('showReplyForm')" /> <div v-if="userCanResolveDiscussion" class="btn-group discussion-actions" role="group"> diff --git a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue index dfeda4aae7c..663163a7552 100644 --- a/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue +++ b/app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue @@ -20,7 +20,7 @@ export default { 'li', { class: - 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base gl-overflow-hidden clearfix', + 'discussion-collapsible gl-border-solid gl-border-gray-100 gl-border-1 gl-rounded-base clearfix', }, [h('ul', { class: 'notes' }, children)], ); diff --git a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue index 0204169214b..1165a869d2b 100644 --- a/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue +++ b/app/assets/javascripts/notes/components/discussion_reply_placeholder.vue @@ -1,23 +1,30 @@ <script> +import { __ } from '~/locale'; + export default { name: 'ReplyPlaceholder', props: { - buttonText: { + placeholderText: { + type: String, + required: false, + default: __('Reply…'), + }, + labelText: { type: String, - required: true, + required: false, + default: __('Reply to comment'), }, }, }; </script> <template> - <button - ref="button" - type="button" - class="js-vue-discussion-reply btn btn-text-field" - :title="s__('MergeRequests|Add a reply')" - @click="$emit('onClick')" - > - {{ buttonText }} - </button> + <textarea + ref="textarea" + rows="1" + class="reply-placeholder-text-field js-vue-discussion-reply" + :placeholder="placeholderText" + :aria-label="labelText" + @focus="$emit('focus')" + ></textarea> </template> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 907a4316a93..ed6701b34e8 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,12 +1,14 @@ <script> import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui'; -import { mapGetters } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import Api from '~/api'; import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status'; import { deprecatedCreateFlash as flash } from '~/flash'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { __, sprintf } from '~/locale'; import eventHub from '~/sidebar/event_hub'; +import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { splitCamelCase } from '../../lib/utils/text_utility'; import ReplyButton from './note_actions/reply_button.vue'; @@ -17,11 +19,13 @@ export default { ReplyButton, GlButton, GlDropdownItem, + UserAccessRoleBadge, + EmojiPicker: () => import('~/emoji/components/picker.vue'), }, directives: { GlTooltip: GlTooltipDirective, }, - mixins: [resolvedStatusMixin], + mixins: [resolvedStatusMixin, glFeatureFlagsMixin()], props: { author: { type: Object, @@ -115,6 +119,10 @@ export default { type: Boolean, required: true, }, + awardPath: { + type: String, + required: true, + }, }, computed: { ...mapGetters(['getUserDataByProp', 'getNoteableData']), @@ -183,6 +191,7 @@ export default { }, }, methods: { + ...mapActions(['toggleAwardRequest']), onEdit() { this.$emit('handleEdit'); }, @@ -220,30 +229,43 @@ export default { .catch(() => flash(__('Something went wrong while updating assignees'))); } }, + setAwardEmoji(awardName) { + this.toggleAwardRequest({ + endpoint: this.awardPath, + noteId: this.noteId, + awardName, + }); + }, }, }; </script> <template> <div class="note-actions"> - <span + <user-access-role-badge v-if="isAuthor" - class="note-role user-access-role has-tooltip d-none d-md-inline-block" + v-gl-tooltip + class="gl-mx-3 d-none d-md-inline-block" :title="displayAuthorBadgeText" - >{{ __('Author') }}</span > - <span + {{ __('Author') }} + </user-access-role-badge> + <user-access-role-badge v-if="accessLevel" - class="note-role user-access-role has-tooltip" + v-gl-tooltip + class="gl-mx-3" :title="displayMemberBadgeText" - >{{ accessLevel }}</span > - <span + {{ accessLevel }} + </user-access-role-badge> + <user-access-role-badge v-else-if="isContributor" - class="note-role user-access-role has-tooltip" + v-gl-tooltip + class="gl-mx-3" :title="displayContributorBadgeText" - >{{ __('Contributor') }}</span > + {{ __('Contributor') }} + </user-access-role-badge> <gl-button v-if="canResolve" ref="resolveButton" @@ -259,19 +281,41 @@ export default { class="line-resolve-btn note-action-button" @click="onResolve" /> - <a - v-if="canAwardEmoji" - v-gl-tooltip - :class="{ 'js-user-authored': isAuthoredByCurrentUser }" - class="note-action-button note-emoji-button js-add-award js-note-emoji gl-text-gray-600 gl-m-2" - href="#" - title="Add reaction" - data-position="right" - > - <gl-icon class="link-highlight award-control-icon-neutral" name="slight-smile" /> - <gl-icon class="link-highlight award-control-icon-positive" name="smiley" /> - <gl-icon class="link-highlight award-control-icon-super-positive" name="smile" /> - </a> + <template v-if="canAwardEmoji"> + <emoji-picker + v-if="glFeatures.improvedEmojiPicker" + toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-2 gl-p-0! gl-shadow-none! gl-bg-transparent!" + @click="setAwardEmoji" + > + <template #button-content> + <gl-icon class="link-highlight award-control-icon-neutral gl-m-0!" name="slight-smile" /> + <gl-icon class="link-highlight award-control-icon-positive gl-m-0!" name="smiley" /> + <gl-icon class="link-highlight award-control-icon-super-positive gl-m-0!" name="smile" /> + </template> + </emoji-picker> + <gl-button + v-else + v-gl-tooltip + :class="{ 'js-user-authored': isAuthoredByCurrentUser }" + class="note-action-button note-emoji-button add-reaction-button js-add-award js-note-emoji" + category="tertiary" + variant="default" + size="small" + title="Add reaction" + data-position="right" + :aria-label="__('Add reaction')" + > + <span class="reaction-control-icon reaction-control-icon-neutral"> + <gl-icon name="slight-smile" /> + </span> + <span class="reaction-control-icon reaction-control-icon-positive"> + <gl-icon name="smiley" /> + </span> + <span class="reaction-control-icon reaction-control-icon-super-positive"> + <gl-icon name="smile" /> + </span> + </gl-button> + </template> <reply-button v-if="showReply" ref="replyButton" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 653bc450d0b..d74ade15de1 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -201,7 +201,7 @@ export default { changedCommentText() { return sprintf( __( - 'This comment has changed since you started editing, please review the %{startTag}updated comment%{endTag} to ensure information is not lost.', + 'This comment changed after you started editing it. Review the %{startTag}updated comment%{endTag} to ensure information is not lost.', ), { startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`, @@ -345,7 +345,7 @@ export default { class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form" data-qa-selector="reply_field" dir="auto" - :aria-label="__('Description')" + :aria-label="__('Reply to comment')" :placeholder="__('Write a comment or drag your files here…')" @keydown.meta.enter="handleKeySubmit()" @keydown.ctrl.enter="handleKeySubmit()" diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 4343fac3cfa..185f4a70367 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -1,7 +1,7 @@ <script> import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import $ from 'jquery'; -import { escape } from 'lodash'; +import { escape, isEmpty } from 'lodash'; import { mapGetters, mapActions } from 'vuex'; import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants'; import httpStatusCodes from '~/lib/utils/http_status'; @@ -282,9 +282,13 @@ export default { note: { target_type: this.getNoteableData.targetType, target_id: this.note.noteable_id, - note: { note: noteText, position: JSON.stringify(position) }, + note: { note: noteText }, }, }; + + // Stringifying an empty object yields `{}` which breaks graphql queries + // https://gitlab.com/gitlab-org/gitlab/-/issues/298827 + if (!isEmpty(position)) data.note.note.position = JSON.stringify(position); this.isRequesting = true; this.oldContent = this.note.note_html; // eslint-disable-next-line vue/no-mutating-props @@ -416,6 +420,7 @@ export default { :is-draft="note.isDraft" :resolve-discussion="note.isDraft && note.resolve_discussion" :discussion-id="discussionId" + :award-path="note.toggle_award_path" @handleEdit="editHandler" @handleDelete="deleteHandler" @handleResolve="resolveHandler" diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 2d66e0d24e3..58cfd150659 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -17,6 +17,7 @@ import commentForm from './comment_form.vue'; import discussionFilterNote from './discussion_filter_note.vue'; import noteableDiscussion from './noteable_discussion.vue'; import noteableNote from './noteable_note.vue'; +import SidebarSubscription from './sidebar_subscription.vue'; export default { name: 'NotesApp', @@ -30,6 +31,7 @@ export default { skeletonLoadingContainer, discussionFilterNote, OrderedLayout, + SidebarSubscription, }, mixins: [glFeatureFlagsMixin()], props: { @@ -261,6 +263,7 @@ export default { <template> <div v-show="shouldShow" id="notes"> + <sidebar-subscription :iid="noteableData.iid" :noteable-data="noteableData" /> <ordered-layout :slot-keys="slotKeys"> <template #form> <comment-form diff --git a/app/assets/javascripts/notes/components/sidebar_subscription.vue b/app/assets/javascripts/notes/components/sidebar_subscription.vue new file mode 100644 index 00000000000..047c04c8482 --- /dev/null +++ b/app/assets/javascripts/notes/components/sidebar_subscription.vue @@ -0,0 +1,58 @@ +<script> +import { mapActions } from 'vuex'; +import { IssuableType } from '~/issue_show/constants'; +import { fetchPolicies } from '~/lib/graphql'; +import { confidentialityQueries } from '~/sidebar/constants'; +import { defaultClient as gqlClient } from '~/sidebar/graphql'; + +export default { + props: { + noteableData: { + type: Object, + required: true, + }, + iid: { + type: Number, + required: true, + }, + }, + computed: { + fullPath() { + if (this.noteableData.web_url) { + return this.noteableData.web_url.split('/-/')[0].substring(1).replace('groups/', ''); + } + return null; + }, + issuableType() { + return this.noteableData.noteableType.toLowerCase(); + }, + }, + created() { + if (this.issuableType !== IssuableType.Issue && this.issuableType !== IssuableType.Epic) { + return; + } + + gqlClient + .watchQuery({ + query: confidentialityQueries[this.issuableType].query, + variables: { + iid: String(this.iid), + fullPath: this.fullPath, + }, + fetchPolicy: fetchPolicies.CACHE_ONLY, + }) + .subscribe((res) => { + const issuable = res.data?.workspace?.issuable; + if (issuable) { + this.setConfidentiality(issuable.confidential); + } + }); + }, + methods: { + ...mapActions(['setConfidentiality']), + }, + render() { + return null; + }, +}; +</script> diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js new file mode 100644 index 00000000000..1ffb94d11ad --- /dev/null +++ b/app/assets/javascripts/notes/i18n.js @@ -0,0 +1,26 @@ +import { __, s__ } from '~/locale'; + +export const COMMENT_FORM = { + GENERIC_UNSUBMITTABLE_NETWORK: __( + 'Your comment could not be submitted! Please check your network connection and try again.', + ), + note: __('Note'), + comment: __('Comment'), + issue: __('issue'), + startThread: __('Start thread'), + mergeRequest: __('merge request'), + bodyPlaceholder: __('Write a comment or drag your files here…'), + confidential: s__('Notes|Make this comment confidential'), + confidentialVisibility: s__('Notes|Confidential comments are only visible to project members'), + discussionThatNeedsResolution: __( + 'Discuss a specific suggestion or question that needs to be resolved.', + ), + discussion: __('Discuss a specific suggestion or question.'), + actionButtonWithNote: __('%{actionText} & %{openOrClose} %{noteable}'), + actionButton: __('%{openOrClose} %{noteable}'), + submitButton: { + startThread: __('Start thread'), + comment: __('Comment'), + commentHelp: __('Add a general comment to this %{noteableDisplayName}.'), + }, +}; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 19403c29cda..1204d68159f 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -2,9 +2,10 @@ import $ from 'jquery'; import Visibility from 'visibilityjs'; import Vue from 'vue'; import Api from '~/api'; +import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants'; import axios from '~/lib/utils/axios_utils'; import { __, sprintf } from '~/locale'; -import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql'; +import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql'; import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql'; import loadAwardsHandler from '../../awards_handler'; @@ -267,7 +268,7 @@ export const toggleStateButtonLoading = ({ commit }, value) => commit(types.TOGGLE_STATE_BUTTON_LOADING, value); export const emitStateChangedEvent = ({ getters }, data) => { - const event = new CustomEvent('issuable_vue_app:change', { + const event = new CustomEvent(EVENT_ISSUABLE_VUE_APP_CHANGE, { detail: { data, isClosed: getters.openState === constants.CLOSED, @@ -340,6 +341,15 @@ export const saveNote = ({ commit, dispatch }, noteData) => { if (hasQuickActions && message) { eTagPoll.makeRequest(); + // synchronizing the quick action with the sidebar widget + // this is a temporary solution until we have confidentiality real-time updates + if ( + confidentialWidget.setConfidentiality && + message.some((m) => m.includes('confidential')) + ) { + confidentialWidget.setConfidentiality(); + } + $('.js-gfm-input').trigger('clear-commands-cache.atwho'); Flash(message || __('Commands applied'), 'notice', noteData.flashContainer); @@ -719,33 +729,3 @@ export const updateAssignees = ({ commit }, assignees) => { export const updateDiscussionPosition = ({ commit }, updatedPosition) => { commit(types.UPDATE_DISCUSSION_POSITION, updatedPosition); }; - -export const updateConfidentialityOnIssuable = ( - { getters, commit }, - { confidential, fullPath }, -) => { - const { iid } = getters.getNoteableData; - - return utils.gqClient - .mutate({ - mutation: updateIssueConfidentialMutation, - variables: { - input: { - projectPath: fullPath, - iid: String(iid), - confidential, - }, - }, - }) - .then(({ data }) => { - const { - issueSetConfidential: { issue, errors }, - } = data; - - if (errors?.length) { - Flash(errors[0], 'alert'); - } else { - setConfidentiality({ commit }, issue.confidential); - } - }); -}; diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index 627e405c75c..592e634e034 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -1,4 +1,4 @@ -import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; +import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; // eslint-disable-line import/no-deprecated import createGqClient, { fetchPolicies } from '~/lib/graphql'; import AjaxCache from '~/lib/utils/ajax_cache'; import { sprintf, __ } from '~/locale'; @@ -34,7 +34,7 @@ export const hasQuickActions = (note) => createQuickActionsRegex().test(note); export const stripQuickActions = (note) => note.replace(createQuickActionsRegex(), '').trim(); export const prepareDiffLines = (diffLines) => - diffLines.map((line) => ({ ...trimFirstCharOfLineContent(line) })); + diffLines.map((line) => ({ ...trimFirstCharOfLineContent(line) })); // eslint-disable-line import/no-deprecated export const gqClient = createGqClient( {}, |