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-10-20 12:40:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 12:40:42 +0300
commitee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch)
treef8479f94a28f66654c6a4f6fb99bad6b4e86a40e /app/assets/javascripts/work_items
parent62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff)
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/work_items')
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue19
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue151
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue118
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue32
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue248
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue2
-rw-r--r--app/assets/javascripts/work_items/constants.js15
-rw-r--r--app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql9
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql16
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql9
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql21
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql19
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql16
-rw-r--r--app/assets/javascripts/work_items/index.js10
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue43
21 files changed, 613 insertions, 153 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 4585426edaa..4d6a27f61ac 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -10,7 +10,7 @@ import {
GlDropdownDivider,
GlIntersectionObserver,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
@@ -126,6 +126,9 @@ export default {
},
},
computed: {
+ assigneesTitleId() {
+ return uniqueId('assignees-title-');
+ },
searchUsers() {
return this.users.nodes.map((node) => addClass({ ...node, ...node.user }));
},
@@ -139,9 +142,6 @@ export default {
property: `type_${this.workItemType}`,
};
},
- assigneeListEmpty() {
- return this.assignees.length === 0;
- },
containerClass() {
return !this.isEditing ? 'gl-shadow-none!' : '';
},
@@ -296,12 +296,14 @@ export default {
<template>
<div class="form-row gl-mb-5 work-item-assignees gl-relative gl-flex-nowrap">
<span
+ :id="assigneesTitleId"
class="gl-font-weight-bold col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
data-testid="assignees-title"
>{{ assigneeText }}</span
>
<gl-token-selector
ref="tokenSelector"
+ :aria-labelledby="assigneesTitleId"
:selected-tokens="localAssignees"
:container-class="containerClass"
:class="{ 'gl-hover-border-gray-200': canUpdate }"
@@ -319,7 +321,7 @@ export default {
>
<template #empty-placeholder>
<div
- class="add-assignees gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-300 gl-pr-4 gl-pl-2 gl-top-2"
+ class="add-assignees gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-secondary gl-pr-4 gl-pl-2 gl-top-2"
data-testid="empty-state"
>
<gl-icon name="profile" />
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index c2e4a50fe31..57babe4569d 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -5,6 +5,7 @@ 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 EditedAt from '~/issues/show/components/edited.vue';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import workItemQuery from '../graphql/work_item.query.graphql';
@@ -16,6 +17,7 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
components: {
+ EditedAt,
GlButton,
GlFormGroup,
MarkdownField,
@@ -89,6 +91,15 @@ export default {
workItemType() {
return this.workItem?.workItemType?.name;
},
+ lastEditedAt() {
+ return this.workItemDescription?.lastEditedAt;
+ },
+ lastEditedByName() {
+ return this.workItemDescription?.lastEditedBy?.name;
+ },
+ lastEditedByPath() {
+ return this.workItemDescription?.lastEditedBy?.webPath;
+ },
markdownPreviewPath() {
return `${gon.relative_url_root || ''}/${this.fullPath}/preview_markdown?target_type=${
this.workItemType
@@ -228,12 +239,18 @@ export default {
class="gl-ml-auto"
icon="pencil"
data-testid="edit-description"
- :aria-label="__('Edit')"
+ :aria-label="__('Edit description')"
@click="startEditing"
/>
</div>
<div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
<div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div>
+ <edited-at
+ v-if="lastEditedAt"
+ :updated-at="lastEditedAt"
+ :updated-by-name="lastEditedByName"
+ :updated-by-path="lastEditedByPath"
+ />
</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 3d25df9fcb8..af9b8c6101a 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -7,7 +7,10 @@ import {
GlBadge,
GlButton,
GlTooltipDirective,
+ GlEmptyState,
} from '@gitlab/ui';
+import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
+import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -20,11 +23,14 @@ import {
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_VIEWED_STORAGE_KEY,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_ITERATION,
} from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
+import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
@@ -35,6 +41,7 @@ import WorkItemDescription from './work_item_description.vue';
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 WorkItemInformation from './work_item_information.vue';
export default {
@@ -49,6 +56,7 @@ export default {
GlLoadingIcon,
GlSkeletonLoader,
GlIcon,
+ GlEmptyState,
WorkItemAssignees,
WorkItemActions,
WorkItemDescription,
@@ -60,6 +68,8 @@ export default {
WorkItemInformation,
LocalStorageSync,
WorkItemTypeIcon,
+ WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
+ WorkItemMilestone,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -82,6 +92,7 @@ export default {
data() {
return {
error: undefined,
+ updateError: undefined,
workItem: {},
showInfoBanner: true,
updateInProgress: false,
@@ -100,9 +111,10 @@ export default {
},
error() {
this.error = this.$options.i18n.fetchError;
+ document.title = s__('404|Not found');
},
result() {
- if (!this.isModal) {
+ if (!this.isModal && this.workItem.project) {
const path = this.workItem.project?.fullPath
? ` · ${this.workItem.project.fullPath}`
: '';
@@ -127,7 +139,18 @@ export default {
};
},
skip() {
- return !this.workItemDueDate;
+ return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
+ },
+ },
+ {
+ document: workItemAssigneesSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
+ skip() {
+ return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
},
],
@@ -152,37 +175,44 @@ export default {
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
+ parentWorkItem() {
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
+ },
+ parentWorkItemConfidentiality() {
+ return this.parentWorkItem?.confidential;
+ },
+ parentUrl() {
+ return `../../issues/${this.parentWorkItem?.iid}`;
+ },
+ workItemIconName() {
+ return this.workItem?.workItemType?.iconName;
+ },
+ noAccessSvgPath() {
+ return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`;
+ },
hasDescriptionWidget() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
+ return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION);
},
workItemAssignees() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
+ return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
workItemLabels() {
- return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ return this.isWidgetPresent(WIDGET_TYPE_LABELS);
},
workItemDueDate() {
- return this.workItem?.widgets?.find(
- (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE,
- );
+ return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
},
workItemWeight() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
+ return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
},
workItemHierarchy() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
},
- parentWorkItem() {
- return this.workItemHierarchy?.parent;
+ workItemIteration() {
+ return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
},
- parentWorkItemConfidentiality() {
- return this.parentWorkItem?.confidential;
- },
- parentUrl() {
- return `../../issues/${this.parentWorkItem?.iid}`;
- },
- workItemIconName() {
- return this.workItem?.workItemType?.iconName;
+ workItemMilestone() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
},
},
beforeDestroy() {
@@ -191,6 +221,9 @@ export default {
this.dismissBanner();
},
methods: {
+ isWidgetPresent(type) {
+ return this.workItem?.widgets?.find((widget) => widget.type === type);
+ },
dismissBanner() {
this.showInfoBanner = false;
},
@@ -236,7 +269,7 @@ export default {
},
)
.catch((error) => {
- this.error = error.message;
+ this.updateError = error.message;
})
.finally(() => {
this.updateInProgress = false;
@@ -249,8 +282,13 @@ export default {
<template>
<section class="gl-pt-5">
- <gl-alert v-if="error" class="gl-mb-3" variant="danger" @dismiss="error = undefined">
- {{ error }}
+ <gl-alert
+ v-if="updateError"
+ class="gl-mb-3"
+ variant="danger"
+ @dismiss="updateError = undefined"
+ >
+ {{ updateError }}
</gl-alert>
<div v-if="workItemLoading" class="gl-max-w-26 gl-py-5">
@@ -289,7 +327,7 @@ export default {
</li>
</ul>
<work-item-type-icon
- v-else
+ v-else-if="!error"
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType && workItemType.toUpperCase()"
show-text
@@ -316,7 +354,7 @@ export default {
:is-parent-confidential="parentWorkItemConfidentiality"
@deleteWorkItem="$emit('deleteWorkItem', workItemType)"
@toggleWorkItemConfidentiality="toggleConfidentiality"
- @error="error = $event"
+ @error="updateError = $event"
/>
<gl-button
v-if="isModal"
@@ -332,24 +370,25 @@ export default {
:storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY"
>
<work-item-information
- v-if="showInfoBanner"
+ v-if="showInfoBanner && !error"
:show-info-banner="showInfoBanner"
@work-item-banner-dismissed="dismissBanner"
/>
</local-storage-sync>
<work-item-title
+ v-if="workItem.title"
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
- @error="error = $event"
+ @error="updateError = $event"
/>
<work-item-state
:work-item="workItem"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
- @error="error = $event"
+ @error="updateError = $event"
/>
<work-item-assignees
v-if="workItemAssignees"
@@ -360,24 +399,33 @@ export default {
:work-item-type="workItemType"
:can-invite-members="workItemAssignees.canInviteMembers"
:full-path="fullPath"
- @error="error = $event"
+ @error="updateError = $event"
+ />
+ <work-item-labels
+ v-if="workItemLabels"
+ :work-item-id="workItem.id"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
+ <work-item-due-date
+ v-if="workItemDueDate"
+ :can-update="canUpdate"
+ :due-date="workItemDueDate.dueDate"
+ :start-date="workItemDueDate.startDate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
/>
<template v-if="workItemsMvc2Enabled">
- <work-item-labels
- v-if="workItemLabels"
+ <work-item-milestone
+ v-if="workItemMilestone"
:work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.nodes[0]"
+ :work-item-type="workItemType"
:can-update="canUpdate"
:full-path="fullPath"
- @error="error = $event"
- />
- <work-item-due-date
- v-if="workItemDueDate"
- :can-update="canUpdate"
- :due-date="workItemDueDate.dueDate"
- :start-date="workItemDueDate.startDate"
- :work-item-id="workItem.id"
- :work-item-type="workItemType"
- @error="error = $event"
+ @error="updateError = $event"
/>
</template>
<work-item-weight
@@ -387,14 +435,31 @@ export default {
:weight="workItemWeight.weight"
:work-item-id="workItem.id"
:work-item-type="workItemType"
- @error="error = $event"
+ @error="updateError = $event"
/>
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-iteration
+ v-if="workItemIteration"
+ class="gl-mb-5"
+ :iteration="workItemIteration.iteration"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
+ />
+ </template>
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
:full-path="fullPath"
class="gl-pt-5"
- @error="error = $event"
+ @error="updateError = $event"
+ />
+ <gl-empty-state
+ v-if="error"
+ :title="$options.i18n.fetchErrorTitle"
+ :description="error"
+ :svg-path="noAccessSvgPath"
/>
</template>
</section>
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index 05f8fa8f5e1..eae11c2bb2f 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -198,7 +198,7 @@ export default {
label-cols="3"
label-cols-lg="2"
>
- <span v-if="isReadonlyWithNoDates" class="gl-text-gray-400 gl-ml-4">
+ <span v-if="isReadonlyWithNoDates" class="gl-text-secondary gl-ml-4">
{{ $options.i18n.none }}
</span>
<div v-else class="gl-display-flex gl-flex-wrap gl-gap-5">
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index b8b5198be57..05077862690 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -1,16 +1,22 @@
<script>
import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, uniqueId, without } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import workItemQuery from '../graphql/work_item.query.graphql';
-import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_LABELS } from '../constants';
+import {
+ i18n,
+ I18N_WORK_ITEM_ERROR_FETCHING_LABELS,
+ TRACKING_CATEGORY_SHOW,
+ WIDGET_TYPE_LABELS,
+} from '../constants';
function isTokenSelectorElement(el) {
return el?.classList.contains('gl-label-close') || el?.classList.contains('dropdown-item');
@@ -52,6 +58,8 @@ export default {
localLabels: [],
searchKey: '',
searchLabels: [],
+ addLabelIds: [],
+ removeLabelIds: [],
};
},
apollo: {
@@ -68,13 +76,21 @@ export default {
error() {
this.$emit('error', i18n.fetchError);
},
+ subscribeToMore: {
+ document: workItemLabelsSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
+ },
},
searchLabels: {
query: labelSearchQuery,
variables() {
return {
fullPath: this.fullPath,
- search: this.searchKey,
+ searchTerm: this.searchKey,
};
},
skip() {
@@ -84,11 +100,14 @@ export default {
return data.workspace?.labels?.nodes.map((node) => addClass({ ...node, ...node.label }));
},
error() {
- this.$emit('error', i18n.fetchError);
+ this.$emit('error', I18N_WORK_ITEM_ERROR_FETCHING_LABELS);
},
},
},
computed: {
+ labelsTitleId() {
+ return uniqueId('labels-title-');
+ },
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@@ -97,10 +116,7 @@ export default {
};
},
allowScopedLabels() {
- return this.labelsWidget.allowScopedLabels;
- },
- listEmpty() {
- return this.labels.length === 0;
+ return this.labelsWidget?.allowsScopedLabels;
},
containerClass() {
return !this.isEditing ? 'gl-shadow-none!' : '';
@@ -109,10 +125,10 @@ export default {
return this.$apollo.queries.searchLabels.loading;
},
labelsWidget() {
- return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
},
labels() {
- return this.labelsWidget?.nodes || [];
+ return this.labelsWidget?.labels?.nodes || [];
},
},
watch: {
@@ -131,44 +147,74 @@ export default {
},
removeLabel({ id }) {
this.localLabels = this.localLabels.filter((label) => label.id !== id);
+ this.removeLabelIds.push(id);
+ this.setLabels();
},
- setLabels(event) {
+ async setLabels() {
+ if (this.addLabelIds.length === 0 && this.removeLabelIds.length === 0) return;
+
this.searchKey = '';
- if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return;
this.isEditing = false;
- this.$apollo
- .mutate({
- mutation: localUpdateWorkItemMutation,
+ try {
+ const {
+ data: {
+ workItemUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
- labels: this.localLabels,
+ labelsWidget: {
+ addLabelIds: this.addLabelIds,
+ removeLabelIds: this.removeLabelIds,
+ },
},
},
- })
- .catch((e) => {
- this.$emit('error', e);
});
- this.track('updated_labels');
+
+ if (errors.length > 0) {
+ this.throwUpdateError();
+ return;
+ }
+
+ this.addLabelIds = [];
+ this.removeLabelIds = [];
+
+ this.track('updated_labels');
+ } catch {
+ this.throwUpdateError();
+ }
+ },
+ throwUpdateError() {
+ this.$emit('error', i18n.updateError);
+ // If mutation is rejected, we're rolling back to initial state
+ this.localLabels = this.labels.map(addClass);
+ this.addLabelIds = [];
+ this.removeLabelIds = [];
+ },
+ handleBlur(event) {
+ if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return;
+ this.setLabels();
},
handleFocus() {
this.isEditing = true;
this.searchStarted = true;
},
async focusTokenSelector(labels) {
- if (this.allowScopedLabels) {
- const newLabel = labels[labels.length - 1];
- const existingLabels = labels.slice(0, labels.length - 1);
-
- const newLabelKey = scopedLabelKey(newLabel);
+ const labelsToAdd = without(labels, ...this.localLabels).map((label) => label.id);
+ const labelsToRemove = without(this.localLabels, ...labels).map((label) => label.id);
- const removeLabelsWithSameScope = existingLabels.filter((label) => {
- const sameKey = newLabelKey === scopedLabelKey(label);
- return !sameKey;
- });
+ if (labelsToAdd.length > 0) {
+ this.addLabelIds.push(...labelsToAdd);
+ }
- this.localLabels = [...removeLabelsWithSameScope, newLabel];
+ if (labelsToRemove.length > 0) {
+ this.removeLabelIds.push(...labelsToRemove);
}
+
+ this.localLabels = labels;
+
this.handleFocus();
await this.$nextTick();
this.$refs.tokenSelector.focusTextInput();
@@ -194,13 +240,15 @@ export default {
<template>
<div class="form-row gl-mb-5 work-item-labels gl-relative gl-flex-nowrap">
<span
+ :id="labelsTitleId"
class="gl-font-weight-bold gl-mt-2 col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
data-testid="labels-title"
>{{ __('Labels') }}</span
>
<gl-token-selector
ref="tokenSelector"
- v-model="localLabels"
+ :selected-tokens="localLabels"
+ :aria-labelledby="labelsTitleId"
:container-class="containerClass"
:dropdown-items="searchLabels"
:loading="isLoading"
@@ -210,13 +258,13 @@ export default {
@input="focusTokenSelector"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
- @blur="setLabels"
+ @blur="handleBlur"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
>
<template #empty-placeholder>
<div
- class="add-labels gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-400 gl-pr-4 gl-top-2"
+ class="add-labels gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-secondary gl-pr-4 gl-top-2"
data-testid="empty-state"
>
<span v-if="canUpdate" class="gl-ml-2">{{ __('Add labels') }}</span>
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 8f31b07b6a3..37aa48be6e5 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
@@ -16,7 +16,13 @@ export default function initWorkItemLinks() {
return;
}
- const { projectPath, wiHasIssueWeightsFeature, iid } = workItemLinksRoot.dataset;
+ const {
+ projectPath,
+ wiHasIssueWeightsFeature,
+ iid,
+ wiHasIterationsFeature,
+ projectNamespace,
+ } = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -31,6 +37,8 @@ export default function initWorkItemLinks() {
iid,
fullPath: projectPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
+ hasIterationsFeature: wiHasIterationsFeature,
+ projectNamespace,
},
render: (createElement) =>
createElement('work-item-links', {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 840fd910272..0d3e951de7e 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -5,7 +5,7 @@ import { s__ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
-import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -59,7 +59,7 @@ export default {
},
},
parentIssue: {
- query: issueConfidentialQuery,
+ query: getIssueDetailsQuery,
variables() {
return {
fullPath: this.projectPath,
@@ -86,6 +86,9 @@ export default {
confidential() {
return this.parentIssue?.confidential || this.workItem?.confidential || false;
},
+ issuableIteration() {
+ return this.parentIssue?.iteration;
+ },
children() {
return (
this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
@@ -257,7 +260,7 @@ export default {
class="gl-display-inline-flex gl-align-items-center gl-line-height-24 gl-ml-3"
data-testid="children-count"
>
- <gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-2 gl-text-gray-500" />
+ <gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-2 gl-text-secondary" />
{{ childrenCountLabel }}
</span>
</div>
@@ -294,7 +297,7 @@ export default {
<template v-else>
<div v-if="isChildrenEmpty && !isShownAddForm && !error" data-testid="links-empty">
- <p class="gl-mt-3 gl-mb-4">
+ <p class="gl-mb-3">
{{ $options.i18n.emptyStateMessage }}
</p>
</div>
@@ -305,6 +308,7 @@ export default {
:issuable-gid="issuableGid"
:children-ids="childrenIds"
:parent-confidential="confidential"
+ :parent-iteration="issuableIteration"
@cancel="hideAddForm"
@addWorkItemChild="addChild"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 8b848995d44..a01f4616cab 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -16,7 +16,7 @@ export default {
GlFormGroup,
GlFormInput,
},
- inject: ['projectPath'],
+ inject: ['projectPath', 'hasIterationsFeature'],
props: {
issuableGid: {
type: String,
@@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
+ parentIteration: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
apollo: {
workItemTypes: {
@@ -77,6 +82,9 @@ export default {
taskWorkItemType() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
},
+ parentIterationId() {
+ return this.parentIteration?.id;
+ },
},
methods: {
getIdFromGraphQLId,
@@ -133,6 +141,13 @@ export default {
} else {
this.unsetError();
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
+ /**
+ * call update mutation only when there is an iteration associated with the issue
+ */
+ // TODO: setting the iteration should be moved to the creation mutation once the backend is done
+ if (this.parentIterationId && this.hasIterationsFeature) {
+ this.addIterationToWorkItem(data.workItemCreate.workItem.id);
+ }
}
})
.catch(() => {
@@ -143,6 +158,19 @@ export default {
this.childToCreateTitle = null;
});
},
+ async addIterationToWorkItem(workItemId) {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: workItemId,
+ iterationWidget: {
+ iterationId: this.parentIterationId,
+ },
+ },
+ },
+ });
+ },
},
i18n: {
inputLabel: __('Title'),
@@ -182,7 +210,7 @@ export default {
>
<template #result="{ item }">
<div class="gl-display-flex">
- <div class="gl-text-gray-400 gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div>
+ <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div>
<div>{{ item.title }}</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
new file mode 100644
index 00000000000..c4a36e36555
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -0,0 +1,248 @@
+<script>
+import {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSkeletonLoader,
+ GlSearchBoxByType,
+ GlDropdownText,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { debounce } from 'lodash';
+import Tracking from '~/tracking';
+import { s__ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ TRACKING_CATEGORY_SHOW,
+} from '../constants';
+
+const noMilestoneId = 'no-milestone-id';
+
+export default {
+ i18n: {
+ MILESTONE: s__('WorkItem|Milestone'),
+ NONE: s__('WorkItem|None'),
+ MILESTONE_PLACEHOLDER: s__('WorkItem|Add to milestone'),
+ NO_MATCHING_RESULTS: s__('WorkItem|No matching results'),
+ NO_MILESTONE: s__('WorkItem|No milestone'),
+ MILESTONE_FETCH_ERROR: s__(
+ 'WorkItem|Something went wrong while fetching milestones. Please try again.',
+ ),
+ },
+ components: {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSkeletonLoader,
+ GlSearchBoxByType,
+ GlDropdownText,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemMilestone: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ localMilestone: this.workItemMilestone,
+ searchTerm: '',
+ shouldFetch: false,
+ updateInProgress: false,
+ isFocused: false,
+ milestones: [],
+ };
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ emptyPlaceholder() {
+ return this.canUpdate ? this.$options.i18n.MILESTONE_PLACEHOLDER : this.$options.i18n.NONE;
+ },
+ dropdownText() {
+ return this.localMilestone?.title || this.emptyPlaceholder;
+ },
+ isLoadingMilestones() {
+ return this.$apollo.queries.milestones.loading;
+ },
+ isNoMilestone() {
+ return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id;
+ },
+ dropdownClasses() {
+ return {
+ 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
+ 'is-not-focused': !this.isFocused,
+ };
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ apollo: {
+ milestones: {
+ query: projectMilestonesQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ title: this.searchTerm,
+ first: 20,
+ };
+ },
+ skip() {
+ return !this.shouldFetch;
+ },
+ update(data) {
+ return data?.workspace?.attributes?.nodes || [];
+ },
+ error() {
+ this.$emit('error', this.i18n.MILESTONE_FETCH_ERROR);
+ },
+ },
+ },
+ methods: {
+ handleMilestoneClick(milestone) {
+ this.localMilestone = milestone;
+ },
+ onDropdownShown() {
+ this.$refs.search.focusInput();
+ this.shouldFetch = true;
+ this.isFocused = true;
+ },
+ onDropdownHide() {
+ this.isFocused = false;
+ this.searchTerm = '';
+ this.shouldFetch = false;
+ this.updateMilestone();
+ },
+ setSearchKey(value) {
+ this.searchTerm = value;
+ },
+ isMilestoneChecked(milestone) {
+ return this.localMilestone?.id === milestone?.id;
+ },
+ updateMilestone() {
+ if (this.workItemMilestone?.id === this.localMilestone?.id) {
+ return;
+ }
+
+ this.track('updated_milestone');
+ this.updateInProgress = true;
+ this.$apollo
+ .mutate({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ milestone: {
+ milestoneId: this.localMilestone?.id,
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate.errors.length) {
+ throw new Error(data.workItemUpdate.errors.join('\n'));
+ }
+ })
+ .catch((error) => {
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.updateInProgress = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="work-item-dropdown"
+ :label="$options.i18n.MILESTONE"
+ label-class="gl-pb-0! gl-overflow-wrap-break gl-mt-3"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <span
+ v-if="!canUpdate"
+ class="gl-text-secondary gl-ml-4 gl-mt-3 gl-display-inline-block gl-line-height-normal"
+ data-testid="disabled-text"
+ >
+ {{ dropdownText }}
+ </span>
+ <gl-dropdown
+ v-else
+ :toggle-class="dropdownClasses"
+ :text="dropdownText"
+ :loading="updateInProgress"
+ @shown="onDropdownShown"
+ @hide="onDropdownHide"
+ >
+ <template #header>
+ <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" />
+ </template>
+ <gl-dropdown-item
+ data-testid="no-milestone"
+ is-check-item
+ :is-checked="isNoMilestone"
+ @click="handleMilestoneClick({ id: 'no-milestone-id' })"
+ >
+ {{ $options.i18n.NO_MILESTONE }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-text v-if="isLoadingMilestones">
+ <gl-skeleton-loader :height="90">
+ <rect width="380" height="10" x="10" y="15" rx="4" />
+ <rect width="280" height="10" x="10" y="30" rx="4" />
+ <rect width="380" height="10" x="10" y="50" rx="4" />
+ <rect width="280" height="10" x="10" y="65" rx="4" />
+ </gl-skeleton-loader>
+ </gl-dropdown-text>
+ <template v-else-if="milestones.length">
+ <gl-dropdown-item
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ is-check-item
+ :is-checked="isMilestoneChecked(milestone)"
+ @click="handleMilestoneClick(milestone)"
+ >
+ {{ milestone.title }}
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
+ </gl-dropdown>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index 31e75663055..96a6493357c 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -53,7 +53,7 @@ export default {
v-gl-tooltip.hover="showTooltipOnHover"
:name="iconName"
:title="workItemTooltipTitle"
- class="gl-mr-2 gl-text-gray-500"
+ class="gl-mr-2 gl-text-secondary"
/>
<span v-if="workItemTypeName" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeName }}</span>
</span>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 78219e62d01..7737c535650 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -17,6 +17,9 @@ export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
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 WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
@@ -26,13 +29,19 @@ export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const i18n = {
- fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
+ fetchErrorTitle: s__('WorkItem|Work item not found'),
+ fetchError: s__(
+ "WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it.",
+ ),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
confidentialTooltip: s__(
'WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task.',
),
};
+export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
+ 'WorkItem|Something went wrong when fetching labels. Please try again.',
+);
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
);
@@ -48,6 +57,10 @@ export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__(
);
export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted');
+export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__(
+ 'WorkItem|Something went wrong when fetching iterations. Please try again.',
+);
+
export const sprintfWorkItem = (msg, workItemTypeArg) => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
diff --git a/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
new file mode 100644
index 00000000000..6edb6c89f16
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
@@ -0,0 +1,9 @@
+query issuableDetails($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ issuable: issue(iid: $iid) {
+ id
+ confidential
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index 36ffba8a540..36779dfe11e 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -1,6 +1,6 @@
enum LocalWidgetType {
ASSIGNEES
- LABELS
+ MILESTONE
}
interface LocalWorkItemWidget {
@@ -12,10 +12,9 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
nodes: [UserCore]
}
-type LocalWorkItemLabels implements LocalWorkItemWidget {
+type LocalWorkItemMilestone implements LocalWorkItemWidget {
type: LocalWidgetType!
- allowScopedLabels: Boolean!
- nodes: [Label!]
+ nodes: [Milestone!]
}
extend type WorkItem {
@@ -30,17 +29,14 @@ input LocalUserInput {
avatarUrl: String
}
-input LocalLabelInput {
- id: ID!
- title: String!
- color: String
- description: String
+input LocalMilestoneInput {
+ milestoneId: ID!
}
input LocalUpdateWorkItemInput {
id: WorkItemID!
assignees: [LocalUserInput!]
- labels: [LocalLabelInput]
+ milestone: LocalMilestoneInput!
}
type LocalWorkItemPayload {
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 f4c77ed2ec0..bb05c9b2135 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -1,4 +1,3 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "ee_else_ce/work_items/graphql/work_item_widgets.fragment.graphql"
fragment WorkItem on WorkItem {
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 276061af193..fa0ab56df75 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -1,15 +1,16 @@
-#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "./work_item.fragment.graphql"
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
mockWidgets @client {
- ... on LocalWorkItemLabels {
+ ... on LocalWorkItemMilestone {
type
- allowScopedLabels
nodes {
- ...Label
+ id
+ title
+ expired
+ dueDate
}
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql
new file mode 100644
index 00000000000..d5b2de8c4c6
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql
@@ -0,0 +1,21 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+subscription issuableAssignees($issuableId: IssuableID!) {
+ issuableAssigneesUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetAssignees {
+ type
+ allowsMultipleAssignees
+ canInviteMembers
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
index 7e045fdf431..d8760f147e1 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
@@ -4,6 +4,7 @@ subscription issuableDatesUpdated($issuableId: IssuableID!) {
id
widgets {
... on WorkItemWidgetStartAndDueDate {
+ type
dueDate
startDate
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql
new file mode 100644
index 00000000000..86d936bf4dd
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql
@@ -0,0 +1,19 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+subscription workItemLabels($issuableId: IssuableID!) {
+ issuableLabelsUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetLabels {
+ type
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ }
+ }
+ }
+}
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 3005069f59a..d404cfb10ed 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
@@ -1,8 +1,16 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
type
description
descriptionHtml
+ lastEditedAt
+ lastEditedBy {
+ name
+ webPath
+ }
}
... on WorkItemWidgetAssignees {
type
@@ -14,6 +22,14 @@ fragment WorkItemWidgets on WorkItemWidget {
}
}
}
+ ... on WorkItemWidgetLabels {
+ type
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
... on WorkItemWidgetStartAndDueDate {
type
dueDate
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index bb4c7052238..f872d8c6b12 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -6,7 +6,13 @@ import { createRouter } from './router';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const { fullPath, hasIssueWeightsFeature, issuesListPath } = el.dataset;
+ const {
+ fullPath,
+ hasIssueWeightsFeature,
+ issuesListPath,
+ projectNamespace,
+ hasIterationsFeature,
+ } = el.dataset;
return new Vue({
el,
@@ -17,6 +23,8 @@ export const initWorkItemsRoot = () => {
fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
issuesListPath,
+ projectNamespace,
+ hasIterationsFeature: parseBoolean(hasIterationsFeature),
},
render(createElement) {
return createElement(App);
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 3b7257591e2..4908b99e5b0 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -6,7 +6,6 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
-import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import ItemTitle from '../components/item_title.vue';
@@ -29,26 +28,6 @@ export default {
required: false,
default: '',
},
- issueGid: {
- type: String,
- required: false,
- default: '',
- },
- lockVersion: {
- type: Number,
- required: false,
- default: null,
- },
- lineNumberStart: {
- type: String,
- required: false,
- default: null,
- },
- lineNumberEnd: {
- type: String,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -136,28 +115,6 @@ export default {
this.error = this.createErrorText;
}
},
- async createWorkItemFromTask() {
- try {
- const { data } = await this.$apollo.mutate({
- mutation: createWorkItemFromTaskMutation,
- variables: {
- input: {
- id: this.issueGid,
- workItemData: {
- lockVersion: this.lockVersion,
- title: this.title,
- lineNumberStart: Number(this.lineNumberStart),
- lineNumberEnd: Number(this.lineNumberEnd),
- workItemTypeId: this.selectedWorkItemType,
- },
- },
- },
- });
- this.$emit('onCreate', data.workItemCreateFromTask.workItem.descriptionHtml);
- } catch {
- this.error = this.createErrorText;
- }
- },
handleTitleInput(title) {
this.title = title;
},