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>2022-12-15 09:07:50 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-15 09:07:50 +0300
commit3c5195204b69df0bc69a0c98c7d61d258dc39866 (patch)
tree10af5c42e5ff538069565954476925709d94b679 /app/assets/javascripts/work_items
parentc40b7517717b0d23893a92527819fd05c2531b93 (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/notes/system_note.vue229
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue19
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue109
-rw-r--r--app/assets/javascripts/work_items/constants.js2
-rw-r--r--app/assets/javascripts/work_items/graphql/discussion.fragment.graphql12
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql32
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql3
-rw-r--r--app/assets/javascripts/work_items/utils.js6
10 files changed, 439 insertions, 1 deletions
diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue
new file mode 100644
index 00000000000..f91a0d01581
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/system_note.vue
@@ -0,0 +1,229 @@
+<script>
+/**
+ * Common component to render a system note, icon and user information.
+ *
+ * This component need not be used with any store neither has any vuex dependency
+ *
+ * @example
+ * <system-note
+ * :note="{
+ * id: String,
+ * author: Object,
+ * createdAt: String,
+ * bodyHtml: String,
+ * systemNoteIconName: String
+ * }"
+ * />
+ */
+import { GlButton, GlSkeletonLoader, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import $ from 'jquery';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import '~/behaviors/markdown/render_gfm';
+import axios from '~/lib/utils/axios_utils';
+import { getLocationHash } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import NoteHeader from '~/notes/components/note_header.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
+
+export default {
+ i18n: {
+ deleteButtonLabel: __('Remove description history'),
+ },
+ name: 'SystemNote',
+ components: {
+ GlIcon,
+ NoteHeader,
+ TimelineEntryItem,
+ GlButton,
+ GlSkeletonLoader,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()],
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ lines: [],
+ showLines: false,
+ loadingDiff: false,
+ isLoadingDescriptionVersion: false,
+ };
+ },
+ computed: {
+ targetNoteHash() {
+ return getLocationHash();
+ },
+ descriptionVersions() {
+ return [];
+ },
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ isTargetNote() {
+ return this.targetNoteHash === this.noteAnchorId;
+ },
+ toggleIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
+ },
+ // following 2 methods taken from code in `collapseLongCommitList` of notes.js:
+ actionTextHtml() {
+ return $(this.note.bodyHtml).unwrap().html();
+ },
+ hasMoreCommits() {
+ return $(this.note.bodyHtml).filter('ul').children().length > MAX_VISIBLE_COMMIT_LIST_COUNT;
+ },
+ descriptionVersion() {
+ return this.descriptionVersions[this.note.description_version_id];
+ },
+ },
+ mounted() {
+ $(this.$refs['gfm-content']).renderGFM();
+ },
+ methods: {
+ fetchDescriptionVersion() {},
+ softDeleteDescriptionVersion() {},
+
+ async toggleDiff() {
+ this.showLines = !this.showLines;
+
+ if (!this.lines.length) {
+ this.loadingDiff = true;
+ const { data } = await axios.get(this.note.outdated_line_change_path);
+
+ this.lines = data.map((l) => ({
+ ...l,
+ rich_text: l.rich_text.replace(/^[+ -]/, ''),
+ }));
+ this.loadingDiff = false;
+ }
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'], // to support icon SVGs
+ },
+ userColorSchemeClass: window.gon.user_color_scheme,
+};
+</script>
+
+<template>
+ <timeline-entry-item
+ :id="noteAnchorId"
+ :class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
+ class="note system-note note-wrapper"
+ >
+ <div class="timeline-icon"><gl-icon :name="note.systemNoteIconName" /></div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header
+ :author="note.author"
+ :created-at="note.createdAt"
+ :note-id="note.id"
+ :is-system-note="true"
+ >
+ <span ref="gfm-content" v-safe-html="actionTextHtml"></span>
+ <template
+ v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
+ #extra-controls
+ >
+ &middot;
+ <gl-button
+ v-if="canSeeDescriptionVersion"
+ variant="link"
+ :icon="descriptionVersionToggleIcon"
+ data-testid="compare-btn"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
+ @click="toggleDescriptionVersion"
+ >{{ __('Compare with previous version') }}</gl-button
+ >
+ <gl-button
+ v-if="note.outdated_line_change_path"
+ :icon="showLines ? 'chevron-up' : 'chevron-down'"
+ variant="link"
+ data-testid="outdated-lines-change-btn"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
+ @click="toggleDiff"
+ >
+ {{ __('Compare changes') }}
+ </gl-button>
+ </template>
+ </note-header>
+ </div>
+ <div class="note-body">
+ <div
+ v-safe-html="note.bodyHtml"
+ :class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
+ class="note-text md"
+ ></div>
+ <div v-if="hasMoreCommits" class="flex-list">
+ <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
+ <gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" />
+ <span>{{ __('Toggle commit list') }}</span>
+ </div>
+ </div>
+ <div v-if="shouldShowDescriptionVersion" class="description-version pt-2">
+ <pre v-if="isLoadingDescriptionVersion" class="loading-state">
+ <gl-skeleton-loader />
+ </pre>
+ <pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre>
+ <gl-button
+ v-if="displayDeleteButton"
+ v-gl-tooltip
+ :title="$options.i18n.deleteButtonLabel"
+ :aria-label="$options.i18n.deleteButtonLabel"
+ variant="default"
+ category="tertiary"
+ icon="remove"
+ class="delete-description-history"
+ data-testid="delete-description-version-button"
+ @click="deleteDescriptionVersion"
+ />
+ </div>
+ <div
+ v-if="lines.length && showLines"
+ class="diff-content outdated-lines-wrapper gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
+ >
+ <table
+ :class="$options.userColorSchemeClass"
+ class="code js-syntax-highlight"
+ data-testid="outdated-lines"
+ >
+ <tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
+ <td
+ :class="line.type"
+ class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0! gl-border-0! gl-rounded-0!"
+ >
+ {{ line.old_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!"
+ >
+ {{ line.new_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="line_content gl-display-table-cell! gl-border-0! gl-rounded-0!"
+ v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
+ ></td>
+ </tr>
+ </table>
+ </div>
+ <div v-else-if="showLines" class="mt-4">
+ <gl-skeleton-loader />
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</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 e0ebc714dbb..4c5c5eb9de9 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -30,6 +30,7 @@ import {
WIDGET_TYPE_ITERATION,
WORK_ITEM_TYPE_VALUE_ISSUE,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WIDGET_TYPE_NOTES,
} from '../constants';
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
@@ -49,6 +50,7 @@ import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
import WorkItemMilestone from './work_item_milestone.vue';
+import WorkItemNotes from './work_item_notes.vue';
export default {
i18n,
@@ -75,6 +77,7 @@ export default {
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
WorkItemMilestone,
WorkItemTree,
+ WorkItemNotes,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath'],
@@ -258,6 +261,9 @@ export default {
workItemMilestone() {
return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
},
+ workItemNotes() {
+ return this.isWidgetPresent(WIDGET_TYPE_NOTES);
+ },
fetchByIid() {
return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
},
@@ -428,7 +434,7 @@ export default {
<div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
<ul
v-if="parentWorkItem"
- class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0"
+ class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0 gl-z-index-0"
data-testid="work-item-parent"
>
<li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-overflow-hidden">
@@ -589,6 +595,17 @@ export default {
@addWorkItemChild="addChild"
@removeChild="removeChild"
/>
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-notes
+ v-if="workItemNotes"
+ :work-item-id="workItem.id"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
+ class="gl-pt-5"
+ @error="updateError = $event"
+ />
+ </template>
<gl-empty-state
v-if="error"
:title="$options.i18n.fetchErrorTitle"
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index a6ef8886d71..e8726814aaf 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -139,6 +139,7 @@ export default {
size="lg"
modal-id="work-item-detail-modal"
header-class="gl-p-0 gl-pb-2!"
+ scrollable
@hide="closeModal"
>
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
new file mode 100644
index 00000000000..91e90589a93
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import SystemNote from '~/work_items/components/notes/system_note.vue';
+import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants';
+import { getWorkItemNotesQuery } from '~/work_items/utils';
+
+export default {
+ i18n: {
+ ACTIVITY_LABEL: s__('WorkItem|Activity'),
+ },
+ loader: {
+ repeat: 10,
+ width: 1000,
+ height: 40,
+ },
+ components: {
+ SystemNote,
+ GlSkeletonLoader,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ areNotesLoading() {
+ return this.$apollo.queries.workItemNotes.loading;
+ },
+ notes() {
+ return this.workItemNotes?.nodes;
+ },
+ pageInfo() {
+ return this.workItemNotes?.pageInfo;
+ },
+ },
+ apollo: {
+ workItemNotes: {
+ query() {
+ return getWorkItemNotesQuery(this.fetchByIid);
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ variables() {
+ return {
+ ...this.queryVariables,
+ pageSize: DEFAULT_PAGE_SIZE_NOTES,
+ };
+ },
+ update(data) {
+ const workItemWidgets = this.fetchByIid
+ ? data.workspace?.workItems?.nodes[0]?.widgets
+ : data.workItem?.widgets;
+ return workItemWidgets.find((widget) => widget.type === 'NOTES').discussions || [];
+ },
+ skip() {
+ return !this.queryVariables.id && !this.queryVariables.iid;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-border-t gl-mt-5">
+ <label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label>
+ <div v-if="areNotesLoading" class="gl-mt-5">
+ <gl-skeleton-loader
+ v-for="index in $options.loader.repeat"
+ :key="index"
+ :width="$options.loader.width"
+ :height="$options.loader.height"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <circle cx="20" cy="20" r="16" />
+ <rect width="500" x="45" y="15" height="10" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ <div v-else class="issuable-discussion gl-mb-5 work-item-notes">
+ <template v-if="notes && notes.length">
+ <ul class="notes main-notes-list timeline">
+ <system-note
+ v-for="note in notes"
+ :key="note.notes.nodes[0].id"
+ :note="note.notes.nodes[0]"
+ />
+ </ul>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 368bb6a85a4..791f06a612e 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -19,6 +19,7 @@ export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
export const WIDGET_TYPE_ITERATION = 'ITERATION';
+export const WIDGET_TYPE_NOTES = 'NOTES';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
@@ -145,3 +146,4 @@ export const FORM_TYPES = {
};
export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
+export const DEFAULT_PAGE_SIZE_NOTES = 100;
diff --git a/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql b/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql
new file mode 100644
index 00000000000..62ced6bdfea
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql
@@ -0,0 +1,12 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment Discussion on Note {
+ id
+ body
+ bodyHtml
+ systemNoteIconName
+ createdAt
+ author {
+ ...User
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
new file mode 100644
index 00000000000..9439f22f955
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
@@ -0,0 +1,27 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/discussion.fragment.graphql"
+
+query workItemNotes($id: WorkItemID!, $after: String, $pageSize: Int) {
+ workItem(id: $id) {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetNotes {
+ type
+ discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ id
+ notes {
+ nodes {
+ ...Discussion
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
new file mode 100644
index 00000000000..3e0960f3f54
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
@@ -0,0 +1,32 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/discussion.fragment.graphql"
+
+query workItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetNotes {
+ type
+ discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ id
+ notes {
+ nodes {
+ ...Discussion
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index 9b802a8e8fc..a8d4392c1a5 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -79,4 +79,7 @@ fragment WorkItemWidgets on WorkItemWidget {
...MilestoneFragment
}
}
+ ... on WorkItemWidgetNotes {
+ type
+ }
}
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 17f9c882c2d..e58fd19ea31 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,6 +1,12 @@
import workItemQuery from './graphql/work_item.query.graphql';
import workItemByIidQuery from './graphql/work_item_by_iid.query.graphql';
+import workItemNotesIdQuery from './graphql/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from './graphql/work_item_notes_by_iid.query.graphql';
export function getWorkItemQuery(isFetchedByIid) {
return isFetchedByIid ? workItemByIidQuery : workItemQuery;
}
+
+export function getWorkItemNotesQuery(isFetchedByIid) {
+ return isFetchedByIid ? workItemNotesByIidQuery : workItemNotesIdQuery;
+}