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>2023-01-09 15:07:31 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-01-09 15:07:31 +0300
commit1935f3e81b99c00697bf0b4d6a44d64068b34745 (patch)
treee2c42218945d0ae19c4566e844d4707513cc2fd6 /app/assets/javascripts/work_items
parenta352bc8e72b948a834b0569d0d4288e95a9c529e (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/work_items')
-rw-r--r--app/assets/javascripts/work_items/components/work_item_comment_form.vue223
-rw-r--r--app/assets/javascripts/work_items/components/work_item_comment_locked.vue66
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_note_signed_out.vue31
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue27
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql5
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/index.js4
9 files changed, 356 insertions, 7 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/work_item_comment_form.vue
new file mode 100644
index 00000000000..5c843d84ae0
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_comment_form.vue
@@ -0,0 +1,223 @@
+<script>
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { __, s__ } from '~/locale';
+import Tracking from '~/tracking';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import { getWorkItemQuery } from '../utils';
+import createNoteMutation from '../graphql/create_work_item_note.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
+import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
+import WorkItemCommentLocked from './work_item_comment_locked.vue';
+
+export default {
+ constantOptions: {
+ markdownDocsPath: helpPagePath('user/markdown'),
+ avatarUrl: window.gon.current_user_avatar_url,
+ },
+ components: {
+ GlAvatar,
+ GlButton,
+ MarkdownEditor,
+ WorkItemNoteSignedOut,
+ WorkItemCommentLocked,
+ },
+ mixins: [glFeatureFlagMixin(), Tracking.mixin()],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ workItem: {},
+ isEditing: false,
+ isSubmitting: false,
+ isSubmittingWithKeydown: false,
+ commentText: '',
+ };
+ },
+ apollo: {
+ workItem: {
+ query() {
+ return getWorkItemQuery(this.fetchByIid);
+ },
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
+ },
+ skip() {
+ return !this.queryVariables.id && !this.queryVariables.iid;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ },
+ computed: {
+ signedIn() {
+ return Boolean(window.gon.current_user_id);
+ },
+ autosaveKey() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `${this.workItemId}-comment`;
+ },
+ canEdit() {
+ // maybe this should use `NotePermissions.updateNote`, but if
+ // we don't have any notes yet, that permission isn't on WorkItem
+ return Boolean(this.workItem?.userPermissions?.updateWorkItem);
+ },
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_comment',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ workItemType() {
+ return this.workItem?.workItemType?.name;
+ },
+ markdownPreviewPath() {
+ return `${gon.relative_url_root || ''}/${this.fullPath}/preview_markdown?target_type=${
+ this.workItemType
+ }`;
+ },
+ isProjectArchived() {
+ return this.workItem?.project?.archived;
+ },
+ },
+ methods: {
+ startEditing() {
+ this.isEditing = true;
+ this.commentText = getDraft(this.autosaveKey) || '';
+ },
+ async cancelEditing() {
+ if (this.commentText) {
+ const msg = s__('WorkItem|Are you sure you want to cancel editing?');
+
+ const confirmed = await confirmAction(msg, {
+ primaryBtnText: __('Discard changes'),
+ cancelBtnText: __('Continue editing'),
+ });
+
+ if (!confirmed) {
+ return;
+ }
+ }
+
+ this.isEditing = false;
+ clearDraft(this.autosaveKey);
+ },
+ async updateWorkItem(event = {}) {
+ const { key } = event;
+
+ if (key) {
+ this.isSubmittingWithKeydown = true;
+ }
+
+ this.isSubmitting = true;
+
+ try {
+ this.track('add_work_item_comment');
+
+ const {
+ data: { createNote },
+ } = await this.$apollo.mutate({
+ mutation: createNoteMutation,
+ variables: {
+ input: {
+ noteableId: this.workItem.id,
+ body: this.commentText,
+ },
+ },
+ });
+
+ if (createNote.errors?.length) {
+ throw new Error(createNote.errors[0]);
+ }
+
+ this.isEditing = false;
+ clearDraft(this.autosaveKey);
+ } catch (error) {
+ this.$emit('error', error.message);
+ Sentry.captureException(error);
+ }
+
+ this.isSubmitting = false;
+ },
+ setCommentText(newText) {
+ this.commentText = newText;
+ updateDraft(this.autosaveKey, this.commentText);
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="timeline-entry">
+ <work-item-note-signed-out v-if="!signedIn" />
+ <work-item-comment-locked
+ v-else-if="!canEdit"
+ :work-item-type="workItemType"
+ :is-project-archived="isProjectArchived"
+ />
+ <div v-else class="gl-display-flex gl-align-items-flex-start gl-flex-wrap-nowrap">
+ <gl-avatar :src="$options.constantOptions.avatarUrl" :size="32" class="gl-mr-3" />
+ <form v-if="isEditing" class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
+ <markdown-editor
+ class="gl-mb-3"
+ :value="commentText"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="$options.constantOptions.markdownDocsPath"
+ :form-field-aria-label="__('Add a comment')"
+ :form-field-placeholder="__('Write a comment or drag your files hereā€¦')"
+ form-field-id="work-item-add-comment"
+ form-field-name="work-item-add-comment"
+ enable-autocomplete
+ autofocus
+ use-bottom-toolbar
+ @input="setCommentText"
+ @keydown.meta.enter="updateWorkItem"
+ @keydown.ctrl.enter="updateWorkItem"
+ @keydown.esc="cancelEditing"
+ />
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ @click="updateWorkItem"
+ >{{ __('Comment') }}
+ </gl-button>
+ <gl-button category="tertiary" class="gl-ml-3" @click="cancelEditing"
+ >{{ __('Cancel') }}
+ </gl-button>
+ </form>
+ <gl-button
+ v-else
+ class="gl-flex-grow-1 gl-justify-content-start! gl-text-secondary!"
+ @click="startEditing"
+ >{{ __('Add a comment') }}</gl-button
+ >
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_comment_locked.vue b/app/assets/javascripts/work_items/components/work_item_comment_locked.vue
new file mode 100644
index 00000000000..f837d025b7f
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_comment_locked.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlLink, GlIcon } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { TASK_TYPE_NAME } from '~/work_items/constants';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ workItemType: {
+ required: false,
+ type: String,
+ default: TASK_TYPE_NAME,
+ },
+ isProjectArchived: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
+ },
+ constantOptions: {
+ archivedProjectDocsPath: helpPagePath('user/project/settings/index.md', {
+ anchor: 'archive-a-project',
+ }),
+ lockedIssueDocsPath: helpPagePath('user/discussions/index.md', {
+ anchor: 'prevent-comments-by-locking-the-discussion',
+ }),
+ projectArchivedWarning: __('This project is archived and cannot be commented on.'),
+ },
+ computed: {
+ issuableDisplayName() {
+ return this.workItemType.replace(/_/g, ' ');
+ },
+ lockedIssueWarning() {
+ return sprintf(
+ __('This %{issuableDisplayName} is locked. Only project members can comment.'),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="disabled-comment text-center">
+ <span class="issuable-note-warning gl-display-inline-block">
+ <gl-icon name="lock" class="gl-mr-2" />
+ <template v-if="isProjectArchived">
+ {{ $options.constantOptions.projectArchivedWarning }}
+ <gl-link :href="$options.constantOptions.archivedProjectDocsPath" class="learn-more">
+ {{ __('Learn more') }}
+ </gl-link>
+ </template>
+
+ <template v-else>
+ {{ lockedIssueWarning }}
+ <gl-link :href="$options.constantOptions.lockedIssueDocsPath" class="learn-more">
+ {{ __('Learn more') }}
+ </gl-link>
+ </template>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 1855a0a37f2..130442476a3 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -633,7 +633,7 @@ export default {
@addWorkItemChild="addChild"
@removeChild="removeChild"
/>
- <template v-if="workItemsMvc2Enabled">
+ <template v-if="workItemsMvcEnabled">
<work-item-notes
v-if="workItemNotes"
:work-item-id="workItem.id"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index edad0e9b616..a7405b6d86c 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -18,6 +18,8 @@ export default function initWorkItemLinks() {
iid,
wiHasIterationsFeature,
wiHasIssuableHealthStatusFeature,
+ registerPath,
+ signInPath,
} = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
@@ -35,6 +37,8 @@ export default function initWorkItemLinks() {
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
hasIterationsFeature: wiHasIterationsFeature,
hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature,
+ registerPath,
+ signInPath,
},
render: (createElement) =>
createElement('work-item-links', {
diff --git a/app/assets/javascripts/work_items/components/work_item_note_signed_out.vue b/app/assets/javascripts/work_items/components/work_item_note_signed_out.vue
new file mode 100644
index 00000000000..3ef4a16bc57
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_note_signed_out.vue
@@ -0,0 +1,31 @@
+<script>
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { __, sprintf } from '~/locale';
+
+export default {
+ directives: {
+ SafeHtml,
+ },
+ inject: ['registerPath', 'signInPath'],
+ computed: {
+ signedOutText() {
+ return sprintf(
+ __(
+ 'Please %{startTagRegister}register%{endRegisterTag} or %{startTagSignIn}sign in%{endSignInTag} to reply',
+ ),
+ {
+ startTagRegister: `<a href="${this.registerPath}">`,
+ startTagSignIn: `<a href="${this.signInPath}">`,
+ endRegisterTag: '</a>',
+ endSignInTag: '</a>',
+ },
+ false,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-safe-html="signedOutText" class="disabled-comment gl-text-center"></div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index b8019dcad1a..8bd4e7cdb73 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -6,6 +6,7 @@ import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants';
import { ASC, DESC } from '~/notes/constants';
import { getWorkItemNotesQuery } from '~/work_items/utils';
+import WorkItemCommentForm from './work_item_comment_form.vue';
export default {
i18n: {
@@ -17,29 +18,34 @@ export default {
height: 40,
},
components: {
- SystemNote,
GlSkeletonLoader,
ActivityFilter,
GlIntersectionObserver,
+ SystemNote,
+ WorkItemCommentForm,
},
props: {
workItemId: {
type: String,
required: true,
},
- fetchByIid: {
- type: Boolean,
- required: false,
- default: false,
- },
queryVariables: {
type: Object,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
workItemType: {
type: String,
required: true,
},
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -57,6 +63,9 @@ export default {
pageInfo() {
return this.workItemNotes?.pageInfo;
},
+ avatarUrl() {
+ return window.gon.current_user_avatar_url;
+ },
hasNextPage() {
return this.pageInfo?.hasNextPage;
},
@@ -196,6 +205,12 @@ export default {
:note="note.notes.nodes[0]"
:data-testid="note.notes.nodes[0].id"
/>
+ <work-item-comment-form
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :work-item-id="workItemId"
+ @error="$emit('error', $event)"
+ />
</ul>
</template>
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql
new file mode 100644
index 00000000000..6a7afd7bd5b
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql
@@ -0,0 +1,5 @@
+mutation createWorkItemNote($input: CreateNoteInput!) {
+ createNote(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 6a81cc230b1..3ee263c149d 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -12,6 +12,7 @@ fragment WorkItem on WorkItem {
project {
id
fullPath
+ archived
}
workItemType {
id
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index a056fde6928..98b59449af7 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -10,6 +10,8 @@ export const initWorkItemsRoot = () => {
fullPath,
hasIssueWeightsFeature,
issuesListPath,
+ registerPath,
+ signInPath,
hasIterationsFeature,
hasOkrsFeature,
hasIssuableHealthStatusFeature,
@@ -26,6 +28,8 @@ export const initWorkItemsRoot = () => {
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
+ registerPath,
+ signInPath,
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
},