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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 21:18:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 21:18:33 +0300
commitf64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch)
treea2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/notes
parentbfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff)
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/notes')
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue208
-rw-r--r--app/assets/javascripts/notes/components/discussion_actions.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes_replies_wrapper.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_reply_placeholder.vue29
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue92
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue9
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue3
-rw-r--r--app/assets/javascripts/notes/components/sidebar_subscription.vue58
-rw-r--r--app/assets/javascripts/notes/i18n.js26
-rw-r--r--app/assets/javascripts/notes/stores/actions.js44
-rw-r--r--app/assets/javascripts/notes/stores/utils.js4
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(
{},