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:
Diffstat (limited to 'app/assets/javascripts/work_items')
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue30
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue84
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue15
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue86
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue257
-rw-r--r--app/assets/javascripts/work_items/components/work_item_information.vue14
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue109
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue121
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue20
-rw-r--r--app/assets/javascripts/work_items/components/work_item_weight.vue162
-rw-r--r--app/assets/javascripts/work_items/constants.js30
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/provider.js77
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql10
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql37
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql13
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_links.query.graphql4
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql36
-rw-r--r--app/assets/javascripts/work_items/index.js4
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue21
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue17
31 files changed, 738 insertions, 462 deletions
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
index 551ebbadb21..b2c8b7ae1db 100644
--- a/app/assets/javascripts/work_items/components/item_title.vue
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -39,14 +39,14 @@ export default {
:class="{ 'gl-cursor-text': disabled }"
aria-labelledby="item-title"
>
- <div
+ <span
id="item-title"
ref="titleEl"
role="textbox"
:aria-label="__('Title')"
:data-placeholder="placeholder"
:contenteditable="!disabled"
- class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base"
+ class="gl-px-4 gl-py-3 gl-ml-n4 gl-border gl-border-white gl-rounded-base gl-display-block"
:class="{ 'gl-hover-border-gray-200 gl-pseudo-placeholder': !disabled }"
@blur="handleBlur"
@keyup="handleInput"
@@ -55,8 +55,7 @@ export default {
@keydown.meta.u.prevent
@keydown.ctrl.b.prevent
@keydown.meta.b.prevent
+ >{{ title }}</span
>
- {{ title }}
- </div>
</h2>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 2753c3fa388..9f9d94ec3c2 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -8,10 +8,14 @@ import {
} from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
+import {
+ sprintfWorkItem,
+ I18N_WORK_ITEM_DELETE,
+ I18N_WORK_ITEM_ARE_YOU_SURE_DELETE,
+} from '../constants';
export default {
i18n: {
- deleteTask: s__('WorkItem|Delete task'),
enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'),
disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'),
},
@@ -31,6 +35,11 @@ export default {
required: false,
default: null,
},
+ workItemType: {
+ type: String,
+ required: false,
+ default: null,
+ },
canUpdate: {
type: Boolean,
required: false,
@@ -53,6 +62,14 @@ export default {
},
},
emits: ['deleteWorkItem', 'toggleWorkItemConfidentiality'],
+ computed: {
+ i18n() {
+ return {
+ deleteWorkItem: sprintfWorkItem(I18N_WORK_ITEM_DELETE, this.workItemType),
+ areYouSureDelete: sprintfWorkItem(I18N_WORK_ITEM_ARE_YOU_SURE_DELETE, this.workItemType),
+ };
+ },
+ },
methods: {
handleToggleWorkItemConfidentiality() {
this.track('click_toggle_work_item_confidentiality');
@@ -75,6 +92,7 @@ export default {
<div>
<gl-dropdown
icon="ellipsis_v"
+ data-testid="work-item-actions-dropdown"
text-sr-only
:text="__('More actions')"
category="tertiary"
@@ -97,20 +115,18 @@ export default {
v-if="canDelete"
v-gl-modal="'work-item-confirm-delete'"
data-testid="delete-action"
- >{{ $options.i18n.deleteTask }}</gl-dropdown-item
+ >{{ i18n.deleteWorkItem }}</gl-dropdown-item
>
</gl-dropdown>
<gl-modal
modal-id="work-item-confirm-delete"
- :title="$options.i18n.deleteWorkItem"
- :ok-title="$options.i18n.deleteWorkItem"
+ :title="i18n.deleteWorkItem"
+ :ok-title="i18n.deleteWorkItem"
ok-variant="danger"
@ok="handleDeleteWorkItem"
@hide="handleCancelDeleteWorkItem"
>
- {{
- s__('WorkItem|Are you sure you want to delete the task? This action cannot be reversed.')
- }}
+ {{ i18n.areYouSureDelete }}
</gl-modal>
</div>
</template>
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 7342f215b5e..4585426edaa 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -8,6 +8,7 @@ import {
GlButton,
GlDropdownItem,
GlDropdownDivider,
+ GlIntersectionObserver,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -19,7 +20,7 @@ import Tracking from '~/tracking';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
+import { i18n, TRACKING_CATEGORY_SHOW, DEFAULT_PAGE_SIZE_ASSIGNEES } from '../constants';
function isTokenSelectorElement(el) {
return (
@@ -50,9 +51,9 @@ export default {
InviteMembersTrigger,
GlDropdownItem,
GlDropdownDivider,
+ GlIntersectionObserver,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
props: {
workItemId: {
type: String,
@@ -80,6 +81,10 @@ export default {
required: false,
default: false,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -87,12 +92,15 @@ export default {
searchStarted: false,
localAssignees: this.assignees.map(addClass),
searchKey: '',
- searchUsers: [],
+ users: {
+ nodes: [],
+ },
currentUser: null,
+ isLoadingMore: false,
};
},
apollo: {
- searchUsers: {
+ users: {
query() {
return userSearchQuery;
},
@@ -100,13 +108,14 @@ export default {
return {
fullPath: this.fullPath,
search: this.searchKey,
+ first: DEFAULT_PAGE_SIZE_ASSIGNEES,
};
},
skip() {
return !this.searchStarted;
},
update(data) {
- return data.workspace?.users?.nodes.map((node) => addClass({ ...node, ...node.user }));
+ return data.workspace?.users;
},
error() {
this.$emit('error', i18n.fetchError);
@@ -117,6 +126,12 @@ export default {
},
},
computed: {
+ searchUsers() {
+ return this.users.nodes.map((node) => addClass({ ...node, ...node.user }));
+ },
+ pageInfo() {
+ return this.users.pageInfo;
+ },
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@@ -131,7 +146,7 @@ export default {
return !this.isEditing ? 'gl-shadow-none!' : '';
},
isLoadingUsers() {
- return this.$apollo.queries.searchUsers.loading;
+ return this.$apollo.queries.users.loading;
},
assigneeText() {
return n__('WorkItem|Assignee', 'WorkItem|Assignees', this.localAssignees.length);
@@ -159,6 +174,12 @@ export default {
assigneeIds() {
return this.localAssignees.map(({ id }) => id);
},
+ hasNextPage() {
+ return this.pageInfo?.hasNextPage;
+ },
+ showIntersectionSkeletonLoader() {
+ return this.isLoadingMore && this.dropdownItems.length;
+ },
},
watch: {
assignees: {
@@ -221,6 +242,16 @@ export default {
this.isEditing = true;
this.searchStarted = true;
},
+ async fetchMoreAssignees() {
+ this.isLoadingMore = true;
+ await this.$apollo.queries.users.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ first: DEFAULT_PAGE_SIZE_ASSIGNEES,
+ },
+ });
+ this.isLoadingMore = false;
+ },
async focusTokenSelector() {
this.handleFocus();
await this.$nextTick();
@@ -263,7 +294,7 @@ export default {
</script>
<template>
- <div class="form-row gl-mb-5 work-item-assignees gl-relative">
+ <div class="form-row gl-mb-5 work-item-assignees gl-relative gl-flex-nowrap">
<span
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"
@@ -275,7 +306,7 @@ export default {
:container-class="containerClass"
:class="{ 'gl-hover-border-gray-200': canUpdate }"
:dropdown-items="dropdownItems"
- :loading="isLoadingUsers"
+ :loading="isLoadingUsers && !isLoadingMore"
:view-only="!canUpdate"
:allow-clear-all="isEditing"
class="assignees-selector gl-flex-grow-1 gl-border gl-border-white gl-rounded-base col-9 gl-align-self-start gl-px-0! gl-mx-2"
@@ -326,17 +357,32 @@ export default {
<rect width="280" height="20" x="10" y="130" rx="4" />
</gl-skeleton-loader>
</template>
- <template v-if="canInviteMembers" #dropdown-footer>
- <gl-dropdown-divider />
- <gl-dropdown-item @click="closeDropdown">
- <invite-members-trigger
- :display-text="__('Invite members')"
- trigger-element="side-nav"
- icon="plus"
- trigger-source="work-item-assignees-dropdown"
- classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2"
- />
- </gl-dropdown-item>
+ <template #dropdown-footer>
+ <gl-intersection-observer
+ v-if="hasNextPage && !isLoadingUsers"
+ @appear="fetchMoreAssignees"
+ />
+ <gl-skeleton-loader
+ v-if="showIntersectionSkeletonLoader"
+ :height="100"
+ data-testid="next-page-loading"
+ class="gl-text-center gl-py-3"
+ >
+ <rect width="380" height="20" x="10" y="15" rx="4" />
+ <rect width="280" height="20" x="10" y="50" rx="4" />
+ </gl-skeleton-loader>
+ <div v-if="canInviteMembers">
+ <gl-dropdown-divider />
+ <gl-dropdown-item @click="closeDropdown">
+ <invite-members-trigger
+ :display-text="__('Invite members')"
+ trigger-element="side-nav"
+ icon="plus"
+ trigger-source="work-item-assignees-dropdown"
+ classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2"
+ />
+ </gl-dropdown-item>
+ </div>
</template>
</gl-token-selector>
</div>
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 cf59789ce2d..c2e4a50fe31 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -8,7 +8,7 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import workItemQuery from '../graphql/work_item.query.graphql';
-import updateWorkItemWidgetsMutation from '../graphql/update_work_item_widgets.mutation.graphql';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
export default {
@@ -21,12 +21,15 @@ export default {
MarkdownField,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
props: {
workItemId: {
type: String,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
},
markdownDocsPath: helpPagePath('user/markdown'),
data() {
@@ -139,9 +142,9 @@ export default {
this.track('updated_description');
const {
- data: { workItemUpdateWidgets },
+ data: { workItemUpdate },
} = await this.$apollo.mutate({
- mutation: updateWorkItemWidgetsMutation,
+ mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItem.id,
@@ -152,8 +155,8 @@ export default {
},
});
- if (workItemUpdateWidgets.errors?.length) {
- throw new Error(workItemUpdateWidgets.errors[0]);
+ if (workItemUpdate.errors?.length) {
+ throw new Error(workItemUpdate.errors[0]);
}
this.isEditing = false;
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 a5580c14a7a..3d25df9fcb8 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -16,12 +16,14 @@ import {
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_LABELS,
WIDGET_TYPE_DESCRIPTION,
+ WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_VIEWED_STORAGE_KEY,
} 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 updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
@@ -30,9 +32,9 @@ import WorkItemActions from './work_item_actions.vue';
import WorkItemState from './work_item_state.vue';
import WorkItemTitle from './work_item_title.vue';
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 WorkItemWeight from './work_item_weight.vue';
import WorkItemInformation from './work_item_information.vue';
export default {
@@ -50,10 +52,11 @@ export default {
WorkItemAssignees,
WorkItemActions,
WorkItemDescription,
+ WorkItemDueDate,
WorkItemLabels,
WorkItemTitle,
WorkItemState,
- WorkItemWeight,
+ WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
WorkItemInformation,
LocalStorageSync,
WorkItemTypeIcon,
@@ -98,14 +101,36 @@ export default {
error() {
this.error = this.$options.i18n.fetchError;
},
- subscribeToMore: {
- document: workItemTitleSubscription,
- variables() {
- return {
- issuableId: this.workItemId,
- };
- },
+ result() {
+ if (!this.isModal) {
+ const path = this.workItem.project?.fullPath
+ ? ` · ${this.workItem.project.fullPath}`
+ : '';
+
+ document.title = `${this.workItem.title} · ${this.workItem?.workItemType?.name}${path}`;
+ }
},
+ subscribeToMore: [
+ {
+ document: workItemTitleSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
+ },
+ {
+ document: workItemDatesSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
+ skip() {
+ return !this.workItemDueDate;
+ },
+ },
+ ],
},
},
computed: {
@@ -121,6 +146,9 @@ export default {
canDelete() {
return this.workItem?.userPermissions?.deleteWorkItem;
},
+ fullPath() {
+ return this.workItem?.project.fullPath;
+ },
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
@@ -133,6 +161,11 @@ export default {
workItemLabels() {
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
},
+ workItemDueDate() {
+ return this.workItem?.widgets?.find(
+ (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE,
+ );
+ },
workItemWeight() {
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
},
@@ -276,11 +309,12 @@ export default {
<work-item-actions
v-if="canUpdate || canDelete"
:work-item-id="workItem.id"
+ :work-item-type="workItemType"
:can-delete="canDelete"
:can-update="canUpdate"
:is-confidential="workItem.confidential"
:is-parent-confidential="parentWorkItemConfidentiality"
- @deleteWorkItem="$emit('deleteWorkItem')"
+ @deleteWorkItem="$emit('deleteWorkItem', workItemType)"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="error = $event"
/>
@@ -317,21 +351,32 @@ export default {
:can-update="canUpdate"
@error="error = $event"
/>
+ <work-item-assignees
+ v-if="workItemAssignees"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :assignees="workItemAssignees.assignees.nodes"
+ :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
+ :work-item-type="workItemType"
+ :can-invite-members="workItemAssignees.canInviteMembers"
+ :full-path="fullPath"
+ @error="error = $event"
+ />
<template v-if="workItemsMvc2Enabled">
- <work-item-assignees
- v-if="workItemAssignees"
- :can-update="canUpdate"
- :work-item-id="workItem.id"
- :assignees="workItemAssignees.assignees.nodes"
- :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
- :work-item-type="workItemType"
- :can-invite-members="workItemAssignees.canInviteMembers"
- @error="error = $event"
- />
<work-item-labels
v-if="workItemLabels"
:work-item-id="workItem.id"
: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"
/>
</template>
@@ -347,6 +392,7 @@ export default {
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
+ :full-path="fullPath"
class="gl-pt-5"
@error="error = $event"
/>
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
new file mode 100644
index 00000000000..05f8fa8f5e1
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -0,0 +1,257 @@
+<script>
+import { GlButton, GlDatepicker, GlFormGroup } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ TRACKING_CATEGORY_SHOW,
+} from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+
+const nullObjectDate = new Date(0);
+
+export default {
+ i18n: {
+ addDueDate: s__('WorkItem|Add due date'),
+ addStartDate: s__('WorkItem|Add start date'),
+ dates: s__('WorkItem|Dates'),
+ dueDate: s__('WorkItem|Due date'),
+ none: s__('WorkItem|None'),
+ startDate: s__('WorkItem|Start date'),
+ },
+ dueDateInputId: 'due-date-input',
+ startDateInputId: 'start-date-input',
+ components: {
+ GlButton,
+ GlDatepicker,
+ GlFormGroup,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ dueDate: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ startDate: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ dirtyDueDate: null,
+ dirtyStartDate: null,
+ isUpdating: false,
+ showDueDateInput: false,
+ showStartDateInput: false,
+ };
+ },
+ computed: {
+ datesUnchanged() {
+ const dirtyDueDate = this.dirtyDueDate || nullObjectDate;
+ const dirtyStartDate = this.dirtyStartDate || nullObjectDate;
+ const dueDate = this.dueDate ? newDateAsLocaleTime(this.dueDate) : nullObjectDate;
+ const startDate = this.startDate ? newDateAsLocaleTime(this.startDate) : nullObjectDate;
+ return (
+ dirtyDueDate.getTime() === dueDate.getTime() &&
+ dirtyStartDate.getTime() === startDate.getTime()
+ );
+ },
+ isDatepickerDisabled() {
+ return !this.canUpdate || this.isUpdating;
+ },
+ isReadonlyWithOnlyDueDate() {
+ return !this.canUpdate && this.dueDate && !this.startDate;
+ },
+ isReadonlyWithOnlyStartDate() {
+ return !this.canUpdate && !this.dueDate && this.startDate;
+ },
+ isReadonlyWithNoDates() {
+ return !this.canUpdate && !this.dueDate && !this.startDate;
+ },
+ labelClass() {
+ return this.isReadonlyWithNoDates ? 'gl-align-self-center gl-pb-0!' : 'gl-mt-3 gl-pb-0!';
+ },
+ showDueDateButton() {
+ return this.canUpdate && !this.showDueDateInput;
+ },
+ showStartDateButton() {
+ return this.canUpdate && !this.showStartDateInput;
+ },
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_dates',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ },
+ watch: {
+ dueDate: {
+ handler(newDueDate) {
+ this.dirtyDueDate = newDateAsLocaleTime(newDueDate);
+ this.showDueDateInput = Boolean(newDueDate);
+ },
+ immediate: true,
+ },
+ startDate: {
+ handler(newStartDate) {
+ this.dirtyStartDate = newDateAsLocaleTime(newStartDate);
+ this.showStartDateInput = Boolean(newStartDate);
+ },
+ immediate: true,
+ },
+ },
+ methods: {
+ clearDueDatePicker() {
+ this.dirtyDueDate = null;
+ this.showDueDateInput = false;
+ this.updateDates();
+ },
+ clearStartDatePicker() {
+ this.dirtyStartDate = null;
+ this.showStartDateInput = false;
+ this.updateDates();
+ },
+ async clickShowDueDate() {
+ this.showDueDateInput = true;
+ await this.$nextTick();
+ this.$refs.dueDatePicker.calendar.show();
+ },
+ async clickShowStartDate() {
+ this.showStartDateInput = true;
+ await this.$nextTick();
+ this.$refs.startDatePicker.calendar.show();
+ },
+ handleStartDateInput() {
+ if (this.dirtyDueDate && this.dirtyStartDate > this.dirtyDueDate) {
+ this.dirtyDueDate = this.dirtyStartDate;
+ this.clickShowDueDate();
+ return;
+ }
+
+ this.updateDates();
+ },
+ updateDates() {
+ if (!this.canUpdate || this.datesUnchanged) {
+ return;
+ }
+
+ this.track('updated_dates');
+
+ this.isUpdating = true;
+
+ this.$apollo
+ .mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ startAndDueDateWidget: {
+ dueDate: getDateWithUTC(this.dirtyDueDate),
+ startDate: getDateWithUTC(this.dirtyStartDate),
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate.errors.length) {
+ throw new Error(data.workItemUpdate.errors.join('; '));
+ }
+ })
+ .catch((error) => {
+ const message = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', message);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="work-item-due-date"
+ :label="$options.i18n.dates"
+ :label-class="labelClass"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <span v-if="isReadonlyWithNoDates" class="gl-text-gray-400 gl-ml-4">
+ {{ $options.i18n.none }}
+ </span>
+ <div v-else class="gl-display-flex gl-flex-wrap gl-gap-5">
+ <gl-form-group
+ class="gl-display-flex gl-align-items-center gl-m-0"
+ :class="{ 'gl-ml-n3': isReadonlyWithOnlyDueDate }"
+ :label="$options.i18n.startDate"
+ :label-for="$options.startDateInputId"
+ :label-sr-only="!showStartDateInput"
+ label-class="gl-flex-shrink-0 gl-text-secondary gl-font-weight-normal! gl-pb-0! gl-ml-4 gl-mr-3"
+ >
+ <gl-datepicker
+ v-if="showStartDateInput"
+ ref="startDatePicker"
+ v-model="dirtyStartDate"
+ container="body"
+ :disabled="isDatepickerDisabled"
+ :input-id="$options.startDateInputId"
+ show-clear-button
+ :target="null"
+ @clear="clearStartDatePicker"
+ @close="handleStartDateInput"
+ />
+ <gl-button v-if="showStartDateButton" category="tertiary" @click="clickShowStartDate">
+ {{ $options.i18n.addStartDate }}
+ </gl-button>
+ </gl-form-group>
+ <gl-form-group
+ v-if="!isReadonlyWithOnlyStartDate"
+ class="gl-display-flex gl-align-items-center gl-m-0"
+ :class="{ 'gl-ml-n3': isReadonlyWithOnlyDueDate }"
+ :label="$options.i18n.dueDate"
+ :label-for="$options.dueDateInputId"
+ :label-sr-only="!showDueDateInput"
+ label-class="gl-flex-shrink-0 gl-text-secondary gl-font-weight-normal! gl-pb-0! gl-ml-4 gl-mr-3"
+ >
+ <gl-datepicker
+ v-if="showDueDateInput"
+ ref="dueDatePicker"
+ v-model="dirtyDueDate"
+ container="body"
+ :disabled="isDatepickerDisabled"
+ :input-id="$options.dueDateInputId"
+ :min-date="dirtyStartDate"
+ show-clear-button
+ :target="null"
+ @clear="clearDueDatePicker"
+ @close="updateDates"
+ />
+ <gl-button v-if="showDueDateButton" category="tertiary" @click="clickShowDueDate">
+ {{ $options.i18n.addDueDate }}
+ </gl-button>
+ </gl-form-group>
+ </div>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_information.vue b/app/assets/javascripts/work_items/components/work_item_information.vue
index 2ff7ba169ea..ce75cc98a75 100644
--- a/app/assets/javascripts/work_items/components/work_item_information.vue
+++ b/app/assets/javascripts/work_items/components/work_item_information.vue
@@ -5,16 +5,14 @@ import { helpPagePath } from '~/helpers/help_page_helper';
export default {
i18n: {
- learnTasksButtonText: s__('WorkItem|Learn about tasks'),
- workItemsText: s__('WorkItem|work items'),
+ learnTasksLinkText: s__('WorkItem|Learn about tasks.'),
tasksInformationTitle: s__('WorkItem|Introducing tasks'),
tasksInformationBody: s__(
- 'WorkItem|A task provides the ability to break down your work into smaller pieces tied to an issue. Tasks are the first items using our new %{workItemsLink} objects. Additional work item types will be coming soon.',
+ 'WorkItem|Use tasks to break down your work in an issue into smaller pieces. %{learnMoreLink}',
),
},
helpPageLinks: {
tasksDocLinkPath: helpPagePath('user/tasks'),
- workItemsLinkPath: helpPagePath(`development/work_items`),
},
components: {
GlAlert,
@@ -38,16 +36,14 @@ export default {
v-if="showInfoBanner"
variant="tip"
:title="$options.i18n.tasksInformationTitle"
- :primary-button-link="$options.helpPageLinks.tasksDocLinkPath"
- :primary-button-text="$options.i18n.learnTasksButtonText"
data-testid="work-item-information"
class="gl-mt-3"
@dismiss="$emit('work-item-banner-dismissed')"
>
<gl-sprintf :message="$options.i18n.tasksInformationBody">
- <template #workItemsLink>
- <gl-link :href="$options.helpPageLinks.workItemsLinkPath">{{
- $options.i18n.workItemsText
+ <template #learnMoreLink>
+ <gl-link :href="$options.helpPageLinks.tasksDocLinkPath">{{
+ $options.i18n.learnTasksLinkText
}}</gl-link>
</template>
></gl-sprintf
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 e73488bbd70..b8b5198be57 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -31,7 +31,6 @@ export default {
LabelItem,
},
mixins: [Tracking.mixin()],
- inject: ['fullPath'],
props: {
workItemId: {
type: String,
@@ -41,6 +40,10 @@ export default {
type: Boolean,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -189,7 +192,7 @@ export default {
</script>
<template>
- <div class="form-row gl-mb-5 work-item-labels gl-relative">
+ <div class="form-row gl-mb-5 work-item-labels gl-relative gl-flex-nowrap">
<span
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"
@@ -216,7 +219,7 @@ export default {
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"
data-testid="empty-state"
>
- <span v-if="canUpdate" class="gl-ml-2">{{ __('Select labels') }}</span>
+ <span v-if="canUpdate" class="gl-ml-2">{{ __('Add labels') }}</span>
<span v-else class="gl-ml-2">{{ __('None') }}</span>
</div>
</template>
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 86f03583ea3..8f31b07b6a3 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
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import { createApolloProvider } from '../../graphql/provider';
+import { apolloProvider } from '~/graphql_shared/issuable_client';
import WorkItemLinks from './work_item_links.vue';
Vue.use(GlToast);
@@ -16,18 +16,19 @@ export default function initWorkItemLinks() {
return;
}
- const { projectPath, wiHasIssueWeightsFeature } = workItemLinksRoot.dataset;
+ const { projectPath, wiHasIssueWeightsFeature, iid } = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
new Vue({
el: workItemLinksRoot,
name: 'WorkItemLinksRoot',
- apolloProvider: createApolloProvider(),
+ apolloProvider,
components: {
- workItemLinks: WorkItemLinks,
+ WorkItemLinks,
},
provide: {
projectPath,
+ iid,
fullPath: projectPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
},
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
new file mode 100644
index 00000000000..34874908f9b
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+
+import { __ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+
+import { STATE_OPEN } from '../../constants';
+import WorkItemLinksMenu from './work_item_links_menu.vue';
+
+export default {
+ components: {
+ GlButton,
+ GlIcon,
+ RichTimestampTooltip,
+ WorkItemLinksMenu,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ issuableGid: {
+ type: String,
+ required: true,
+ },
+ childItem: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ isItemOpen() {
+ return this.childItem.state === STATE_OPEN;
+ },
+ iconClass() {
+ return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
+ },
+ iconName() {
+ return this.isItemOpen ? 'issue-open-m' : 'issue-close';
+ },
+ stateTimestamp() {
+ return this.isItemOpen ? this.childItem.createdAt : this.childItem.closedAt;
+ },
+ stateTimestampTypeText() {
+ return this.isItemOpen ? __('Created') : __('Closed');
+ },
+ childPath() {
+ return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(this.childItem.id)}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
+ data-testid="links-child"
+ >
+ <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1">
+ <span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon">
+ <gl-icon :name="iconName" :class="iconClass" :aria-label="stateTimestampTypeText" />
+ </span>
+ <rich-timestamp-tooltip
+ :target="`stateIcon-${childItem.id}`"
+ :raw-timestamp="stateTimestamp"
+ :timestamp-type-text="stateTimestampTypeText"
+ />
+ <gl-icon
+ v-if="childItem.confidential"
+ v-gl-tooltip.top
+ name="eye-slash"
+ class="gl-mr-2 gl-text-orange-500"
+ data-testid="confidential-icon"
+ :aria-label="__('Confidential')"
+ :title="__('Confidential')"
+ />
+ <gl-button
+ :href="childPath"
+ category="tertiary"
+ variant="link"
+ class="gl-text-truncate gl-max-w-80 gl-text-black-normal!"
+ @click="$emit('click', childItem.id, $event)"
+ @mouseover="$emit('mouseover', childItem.id, $event)"
+ @mouseout="$emit('mouseout', childItem.id, $event)"
+ >
+ {{ childItem.title }}
+ </gl-button>
+ </div>
+ <div
+ v-if="canUpdate"
+ class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"
+ >
+ <work-item-links-menu
+ :work-item-id="childItem.id"
+ :parent-work-item-id="issuableGid"
+ data-testid="links-menu"
+ @removeChild="$emit('remove', childItem.id)"
+ />
+ </div>
+ </div>
+</template>
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 534ebabee08..840fd910272 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,22 +5,17 @@ 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 { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
-import SidebarEventHub from '~/sidebar/event_hub';
-import {
- STATE_OPEN,
- WIDGET_ICONS,
- WORK_ITEM_STATUS_TEXT,
- WIDGET_TYPE_HIERARCHY,
-} from '../../constants';
+import { WIDGET_ICONS, WORK_ITEM_STATUS_TEXT, WIDGET_TYPE_HIERARCHY } from '../../constants';
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import workItemQuery from '../../graphql/work_item.query.graphql';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
+import WorkItemLinkChild from './work_item_link_child.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
-import WorkItemLinksMenu from './work_item_links_menu.vue';
export default {
components: {
@@ -28,14 +23,14 @@ export default {
GlIcon,
GlAlert,
GlLoadingIcon,
+ WorkItemLinkChild,
WorkItemLinksForm,
- WorkItemLinksMenu,
WorkItemDetailModal,
},
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['projectPath'],
+ inject: ['projectPath', 'iid'],
props: {
workItemId: {
type: String,
@@ -63,6 +58,18 @@ export default {
this.error = e.message || this.$options.i18n.fetchError;
},
},
+ parentIssue: {
+ query: issueConfidentialQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ iid: String(this.iid),
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable;
+ },
+ },
},
data() {
return {
@@ -72,9 +79,13 @@ export default {
activeToast: null,
prefetchedWorkItem: null,
error: undefined,
+ parentIssue: null,
};
},
computed: {
+ confidential() {
+ return this.parentIssue?.confidential || this.workItem?.confidential || false;
+ },
children() {
return (
this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
@@ -84,9 +95,6 @@ export default {
canUpdate() {
return this.workItem?.userPermissions.updateWorkItem || false;
},
- confidential() {
- return this.workItem?.confidential || false;
- },
// Only used for children for now but should be extended later to support parents and siblings
isChildrenEmpty() {
return this.children?.length === 0;
@@ -95,9 +103,7 @@ export default {
return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
},
toggleLabel() {
- return this.isOpen
- ? s__('WorkItem|Collapse child items')
- : s__('WorkItem|Expand child items');
+ return this.isOpen ? s__('WorkItem|Collapse tasks') : s__('WorkItem|Expand tasks');
},
issuableGid() {
return this.issuableId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issuableId) : null;
@@ -112,22 +118,7 @@ export default {
return this.isLoading && this.children.length === 0 ? '...' : this.children.length;
},
},
- mounted() {
- SidebarEventHub.$on('confidentialityUpdated', this.refetchWorkItems);
- },
- destroyed() {
- SidebarEventHub.$off('confidentialityUpdated', this.refetchWorkItems);
- },
methods: {
- refetchWorkItems() {
- this.$apollo.queries.workItem.refetch();
- },
- iconClass(state) {
- return state === STATE_OPEN ? 'gl-text-green-500' : 'gl-text-blue-500';
- },
- iconName(state) {
- return state === STATE_OPEN ? 'issue-open-m' : 'issue-close';
- },
toggle() {
this.isOpen = !this.isOpen;
},
@@ -169,9 +160,6 @@ export default {
replace: true,
});
},
- childPath(childItemId) {
- return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(childItemId)}`;
- },
toggleChildFromCache(workItem, childId, store) {
const sourceData = store.readQuery({
query: getWorkItemLinksQuery,
@@ -242,14 +230,12 @@ export default {
},
},
i18n: {
- title: s__('WorkItem|Child items'),
- fetchError: s__(
- 'WorkItem|Something went wrong when fetching the items list. Please refresh this page.',
- ),
+ title: s__('WorkItem|Tasks'),
+ fetchError: s__('WorkItem|Something went wrong when fetching tasks. Please refresh this page.'),
emptyStateMessage: s__(
- 'WorkItem|No child items are currently assigned. Use child items to prioritize tasks that your team should complete in order to accomplish your goals!',
+ 'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.',
),
- addChildButtonLabel: s__('WorkItem|Add a task'),
+ addChildButtonLabel: s__('WorkItem|Add'),
},
WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK,
WORK_ITEM_STATUS_TEXT,
@@ -257,7 +243,10 @@ export default {
</script>
<template>
- <div class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10">
+ <div
+ class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-5"
+ data-testid="work-item-links"
+ >
<div
class="gl-px-5 gl-py-3 gl-display-flex gl-justify-content-space-between"
:class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
@@ -319,48 +308,18 @@ export default {
@cancel="hideAddForm"
@addWorkItemChild="addChild"
/>
- <div
+ <work-item-link-child
v-for="child in children"
:key="child.id"
- class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
- data-testid="links-child"
- >
- <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1">
- <gl-icon
- :name="iconName(child.state)"
- class="gl-mr-3"
- :class="iconClass(child.state)"
- />
- <gl-icon
- v-if="child.confidential"
- v-gl-tooltip.top
- name="eye-slash"
- class="gl-mr-2 gl-text-orange-500"
- data-testid="confidential-icon"
- :title="__('Confidential')"
- />
- <gl-button
- :href="childPath(child.id)"
- category="tertiary"
- variant="link"
- class="gl-text-truncate gl-max-w-80 gl-text-black-normal!"
- @click="openChild(child.id, $event)"
- @mouseover="prefetchWorkItem(child.id)"
- @mouseout="clearPrefetching"
- >
- {{ child.title }}
- </gl-button>
- </div>
- <div class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center">
- <work-item-links-menu
- v-if="canUpdate"
- :work-item-id="child.id"
- :parent-work-item-id="issuableGid"
- data-testid="links-menu"
- @removeChild="removeChild(child.id)"
- />
- </div>
- </div>
+ :project-path="projectPath"
+ :can-update="canUpdate"
+ :issuable-gid="issuableGid"
+ :child-item="child"
+ @click="openChild"
+ @mouseover="prefetchWorkItem"
+ @mouseout="clearPrefetching"
+ @remove="removeChild"
+ />
<work-item-detail-modal
ref="modal"
:work-item-id="activeChildId"
diff --git a/app/assets/javascripts/work_items/components/work_item_state.vue b/app/assets/javascripts/work_items/components/work_item_state.vue
index 080d4025cc3..3880ae25c8c 100644
--- a/app/assets/javascripts/work_items/components/work_item_state.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state.vue
@@ -2,7 +2,8 @@
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
import {
- i18n,
+ sprintfWorkItem,
+ I18N_WORK_ITEM_ERROR_UPDATING,
STATE_OPEN,
STATE_CLOSED,
STATE_EVENT_CLOSE,
@@ -93,7 +94,9 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- this.$emit('error', i18n.updateError);
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+
+ this.$emit('error', msg);
Sentry.captureException(error);
}
diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue
index cd5cc3894f6..c52a6854fad 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -1,7 +1,11 @@
<script>
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
-import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
+import {
+ sprintfWorkItem,
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ TRACKING_CATEGORY_SHOW,
+} from '../constants';
import { getUpdateWorkItemMutation } from './update_work_item';
import ItemTitle from './item_title.vue';
@@ -78,7 +82,8 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- this.$emit('error', i18n.updateError);
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', msg);
Sentry.captureException(error);
}
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 fd914fa350b..31e75663055 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
@@ -1,11 +1,14 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { WORK_ITEMS_TYPE_MAP } from '../constants';
export default {
components: {
GlIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
workItemType: {
type: String,
@@ -22,6 +25,11 @@ export default {
required: false,
default: '',
},
+ showTooltipOnHover: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
iconName() {
@@ -32,13 +40,21 @@ export default {
workItemTypeName() {
return WORK_ITEMS_TYPE_MAP[this.workItemType]?.name;
},
+ workItemTooltipTitle() {
+ return this.showTooltipOnHover ? this.workItemTypeName : '';
+ },
},
};
</script>
<template>
<span>
- <gl-icon :name="iconName" class="gl-mr-2" />
+ <gl-icon
+ v-gl-tooltip.hover="showTooltipOnHover"
+ :name="iconName"
+ :title="workItemTooltipTitle"
+ class="gl-mr-2 gl-text-gray-500"
+ />
<span v-if="workItemTypeName" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeName }}</span>
</span>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_weight.vue b/app/assets/javascripts/work_items/components/work_item_weight.vue
deleted file mode 100644
index b0ad7c97bb1..00000000000
--- a/app/assets/javascripts/work_items/components/work_item_weight.vue
+++ /dev/null
@@ -1,162 +0,0 @@
-<script>
-import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import { __ } from '~/locale';
-import Tracking from '~/tracking';
-import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
-import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-
-/* eslint-disable @gitlab/require-i18n-strings */
-const allowedKeys = [
- 'Alt',
- 'ArrowDown',
- 'ArrowLeft',
- 'ArrowRight',
- 'ArrowUp',
- 'Backspace',
- 'Control',
- 'Delete',
- 'End',
- 'Enter',
- 'Home',
- 'Meta',
- 'PageDown',
- 'PageUp',
- 'Tab',
- '0',
- '1',
- '2',
- '3',
- '4',
- '5',
- '6',
- '7',
- '8',
- '9',
-];
-/* eslint-enable @gitlab/require-i18n-strings */
-
-export default {
- inputId: 'weight-widget-input',
- components: {
- GlForm,
- GlFormGroup,
- GlFormInput,
- },
- mixins: [Tracking.mixin()],
- inject: ['hasIssueWeightsFeature'],
- props: {
- canUpdate: {
- type: Boolean,
- required: false,
- default: false,
- },
- weight: {
- type: Number,
- required: false,
- default: undefined,
- },
- workItemId: {
- type: String,
- required: true,
- },
- workItemType: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- isEditing: false,
- };
- },
- computed: {
- placeholder() {
- return this.canUpdate && this.isEditing ? __('Enter a number') : __('None');
- },
- tracking() {
- return {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_weight',
- property: `type_${this.workItemType}`,
- };
- },
- type() {
- return this.canUpdate && this.isEditing ? 'number' : 'text';
- },
- },
- methods: {
- blurInput() {
- this.$refs.input.$el.blur();
- },
- handleFocus() {
- this.isEditing = true;
- },
- handleKeydown(event) {
- if (!allowedKeys.includes(event.key)) {
- event.preventDefault();
- }
- },
- updateWeight(event) {
- if (!this.canUpdate) return;
- this.isEditing = false;
-
- const weight = Number(event.target.value);
- if (this.weight === weight) {
- return;
- }
-
- this.track('updated_weight');
- this.$apollo
- .mutate({
- mutation: updateWorkItemMutation,
- variables: {
- input: {
- id: this.workItemId,
- weightWidget: {
- weight: event.target.value === '' ? null : weight,
- },
- },
- },
- })
- .then(({ data }) => {
- if (data.workItemUpdate.errors.length) {
- throw new Error(data.workItemUpdate.errors.join('\n'));
- }
- })
- .catch((error) => {
- this.$emit('error', i18n.updateError);
- Sentry.captureException(error);
- });
- },
- },
-};
-</script>
-
-<template>
- <gl-form v-if="hasIssueWeightsFeature" @submit.prevent="blurInput">
- <gl-form-group
- class="gl-align-items-center"
- :label="__('Weight')"
- :label-for="$options.inputId"
- label-class="gl-pb-0! gl-overflow-wrap-break"
- label-cols="3"
- label-cols-lg="2"
- >
- <gl-form-input
- :id="$options.inputId"
- ref="input"
- min="0"
- :placeholder="placeholder"
- :readonly="!canUpdate"
- size="sm"
- :type="type"
- :value="weight"
- @blur="updateWeight"
- @focus="handleFocus"
- @keydown="handleKeydown"
- @keydown.exact.esc.stop="blurInput"
- />
- </gl-form-group>
- </gl-form>
-</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index a2aea3cd327..78219e62d01 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -1,4 +1,5 @@
-import { s__ } from '~/locale';
+import { s__, sprintf } from '~/locale';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export const STATE_OPEN = 'OPEN';
export const STATE_CLOSED = 'CLOSED';
@@ -13,6 +14,7 @@ export const TASK_TYPE_NAME = 'Task';
export const WIDGET_TYPE_ASSIGNEES = 'ASSIGNEES';
export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
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 WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
@@ -31,6 +33,30 @@ export const i18n = {
),
};
+export const I18N_WORK_ITEM_ERROR_CREATING = s__(
+ 'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
+);
+export const I18N_WORK_ITEM_ERROR_UPDATING = s__(
+ 'WorkItem|Something went wrong while updating the %{workItemType}. Please try again.',
+);
+export const I18N_WORK_ITEM_ERROR_DELETING = s__(
+ 'WorkItem|Something went wrong when deleting the %{workItemType}. Please try again.',
+);
+export const I18N_WORK_ITEM_DELETE = s__('WorkItem|Delete %{workItemType}');
+export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__(
+ 'WorkItem|Are you sure you want to delete the %{workItemType}? This action cannot be reversed.',
+);
+export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted');
+
+export const sprintfWorkItem = (msg, workItemTypeArg) => {
+ const workItemType = workItemTypeArg || s__('WorkItem|Work item');
+ return capitalizeFirstCharacter(
+ sprintf(msg, {
+ workItemType: workItemType.toLocaleLowerCase(),
+ }),
+ );
+};
+
export const WIDGET_ICONS = {
TASK: 'issue-type-task',
};
@@ -62,3 +88,5 @@ export const WORK_ITEMS_TYPE_MAP = {
name: s__('WorkItem|Requirements'),
},
};
+
+export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql
index 4cc23fa0071..1228c876a55 100644
--- a/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/create_work_item.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/work_items/graphql/work_item.fragment.graphql"
+#import "./work_item.fragment.graphql"
mutation createWorkItem($input: WorkItemCreateInput!) {
workItemCreate(input: $input) {
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
index 1f98cd4fa2b..ccfe62cc585 100644
--- a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/work_items/graphql/work_item.fragment.graphql"
+#import "./work_item.fragment.graphql"
mutation workItemCreateFromTask($input: WorkItemCreateFromTaskInput!) {
workItemCreateFromTask(input: $input) {
diff --git a/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql
index 790b8e60b6a..43c92cf89ec 100644
--- a/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/local_update_work_item.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/work_items/graphql/work_item.fragment.graphql"
+#import "./work_item.fragment.graphql"
mutation localUpdateWorkItem($input: LocalUpdateWorkItemInput) {
localUpdateWorkItem(input: $input) @client {
diff --git a/app/assets/javascripts/work_items/graphql/provider.js b/app/assets/javascripts/work_items/graphql/provider.js
deleted file mode 100644
index b70c06fddea..00000000000
--- a/app/assets/javascripts/work_items/graphql/provider.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import produce from 'immer';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import { WIDGET_TYPE_LABELS } from '../constants';
-import typeDefs from './typedefs.graphql';
-import workItemQuery from './work_item.query.graphql';
-
-export const temporaryConfig = {
- typeDefs,
- cacheConfig: {
- possibleTypes: {
- LocalWorkItemWidget: ['LocalWorkItemLabels'],
- },
- typePolicies: {
- WorkItem: {
- fields: {
- mockWidgets: {
- read(widgets) {
- return (
- widgets || [
- {
- __typename: 'LocalWorkItemLabels',
- type: WIDGET_TYPE_LABELS,
- allowScopedLabels: true,
- nodes: [],
- },
- ]
- );
- },
- },
- widgets: {
- merge(_, incoming) {
- return incoming;
- },
- },
- },
- },
- },
- },
-};
-
-export const resolvers = {
- Mutation: {
- localUpdateWorkItem(_, { input }, { cache }) {
- const sourceData = cache.readQuery({
- query: workItemQuery,
- variables: { id: input.id },
- });
-
- const data = produce(sourceData, (draftData) => {
- if (input.labels) {
- const labelsWidget = draftData.workItem.mockWidgets.find(
- (widget) => widget.type === WIDGET_TYPE_LABELS,
- );
- labelsWidget.nodes = [...input.labels];
- }
- });
-
- cache.writeQuery({
- query: workItemQuery,
- variables: { id: input.id },
- data,
- });
- },
- },
-};
-
-export function createApolloProvider() {
- Vue.use(VueApollo);
-
- const defaultClient = createDefaultClient(resolvers, temporaryConfig);
-
- return new VueApollo({
- defaultClient,
- });
-}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
index 0a887fcfc00..25eb8099251 100644
--- a/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/update_work_item.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/work_items/graphql/work_item.fragment.graphql"
+#import "./work_item.fragment.graphql"
mutation workItemUpdate($input: WorkItemUpdateInput!) {
workItemUpdate(input: $input) {
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
index fad5a9fa5bc..ad861a60d15 100644
--- a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
+++ b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/work_items/graphql/work_item.fragment.graphql"
+#import "./work_item.fragment.graphql"
mutation workItemUpdateTask($input: WorkItemUpdateTaskInput!) {
workItemUpdate: workItemUpdateTask(input: $input) {
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql
deleted file mode 100644
index 6a94c96b347..00000000000
--- a/app/assets/javascripts/work_items/graphql/update_work_item_widgets.mutation.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-#import "ee_else_ce/work_items/graphql/work_item.fragment.graphql"
-
-mutation workItemUpdateWidgets($input: WorkItemUpdateWidgetsInput!) {
- workItemUpdateWidgets(input: $input) {
- workItem {
- ...WorkItem
- }
- 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 e8ef27ec778..f4c77ed2ec0 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,5 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "ee_else_ce/work_items/graphql/work_item_widgets.fragment.graphql"
fragment WorkItem on WorkItem {
id
@@ -6,6 +7,12 @@ fragment WorkItem on WorkItem {
state
description
confidential
+ createdAt
+ closedAt
+ project {
+ id
+ fullPath
+ }
workItemType {
id
name
@@ -16,34 +23,6 @@ fragment WorkItem on WorkItem {
updateWorkItem
}
widgets {
- ... on WorkItemWidgetDescription {
- type
- description
- descriptionHtml
- }
- ... on WorkItemWidgetAssignees {
- type
- allowsMultipleAssignees
- canInviteMembers
- assignees {
- nodes {
- ...User
- }
- }
- }
- ... on WorkItemWidgetHierarchy {
- type
- parent {
- id
- iid
- title
- confidential
- }
- children {
- nodes {
- id
- }
- }
- }
+ ...WorkItemWidgets
}
}
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 a9f7b714551..276061af193 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -1,5 +1,5 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
-#import "ee_else_ce/work_items/graphql/work_item.fragment.graphql"
+#import "./work_item.fragment.graphql"
query workItem($id: WorkItemID!) {
workItem(id: $id) {
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
new file mode 100644
index 00000000000..7e045fdf431
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
@@ -0,0 +1,13 @@
+subscription issuableDatesUpdated($issuableId: IssuableID!) {
+ issuableDatesUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetStartAndDueDate {
+ dueDate
+ startDate
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
index df62ca1c143..7b63d9c7ca3 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
@@ -1,4 +1,4 @@
-query workItemQuery($id: WorkItemID!) {
+query workItemLinksQuery($id: WorkItemID!) {
workItem(id: $id) {
id
workItemType {
@@ -26,6 +26,8 @@ query workItemQuery($id: WorkItemID!) {
}
title
state
+ createdAt
+ closedAt
}
}
}
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
new file mode 100644
index 00000000000..3005069f59a
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -0,0 +1,36 @@
+fragment WorkItemWidgets on WorkItemWidget {
+ ... on WorkItemWidgetDescription {
+ type
+ description
+ descriptionHtml
+ }
+ ... on WorkItemWidgetAssignees {
+ type
+ allowsMultipleAssignees
+ canInviteMembers
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ ... on WorkItemWidgetStartAndDueDate {
+ type
+ dueDate
+ startDate
+ }
+ ... on WorkItemWidgetHierarchy {
+ type
+ parent {
+ id
+ iid
+ title
+ confidential
+ }
+ children {
+ nodes {
+ id
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index 6437df597b4..bb4c7052238 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { apolloProvider } from '~/graphql_shared/issuable_client';
import App from './components/app.vue';
import { createRouter } from './router';
-import { createApolloProvider } from './graphql/provider';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
@@ -12,7 +12,7 @@ export const initWorkItemsRoot = () => {
el,
name: 'WorkItemsRoot',
router: createRouter(el.dataset.fullPath),
- apolloProvider: createApolloProvider(),
+ apolloProvider,
provide: {
fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
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 482da5419c6..3b7257591e2 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -1,7 +1,9 @@
<script>
import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { getPreferredLocales, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+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';
@@ -10,7 +12,6 @@ import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.
import ItemTitle from '../components/item_title.vue';
export default {
- createErrorText: s__('WorkItem|Something went wrong when creating a task. Please try again'),
fetchTypesErrorText: s__(
'WorkItem|Something went wrong when fetching work item types. Please try again',
),
@@ -69,7 +70,7 @@ export default {
update(data) {
return data.workspace?.workItemTypes?.nodes.map((node) => ({
value: node.id,
- text: node.name,
+ text: capitalizeFirstCharacter(node.name.toLocaleLowerCase(getPreferredLocales()[0])),
}));
},
error() {
@@ -78,15 +79,19 @@ export default {
},
},
computed: {
- dropdownButtonText() {
- return this.selectedWorkItemType?.name || s__('WorkItem|Type');
- },
formOptions() {
return [{ value: null, text: s__('WorkItem|Select type') }, ...this.workItemTypes];
},
isButtonDisabled() {
return this.title.trim().length === 0 || !this.selectedWorkItemType;
},
+ createErrorText() {
+ const workItemType = this.workItemTypes.find(
+ (item) => item.value === this.selectedWorkItemType,
+ )?.text;
+
+ return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType);
+ },
},
methods: {
async createWorkItem() {
@@ -128,7 +133,7 @@ export default {
} = response;
this.$router.push({ name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } });
} catch {
- this.error = this.$options.createErrorText;
+ this.error = this.createErrorText;
}
},
async createWorkItemFromTask() {
@@ -150,7 +155,7 @@ export default {
});
this.$emit('onCreate', data.workItemCreateFromTask.workItem.descriptionHtml);
} catch {
- this.error = this.$options.createErrorText;
+ this.error = this.createErrorText;
}
},
handleTitleInput(title) {
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index e9840889bdb..a2cacd8bd7a 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -3,9 +3,13 @@ import { GlAlert } from '@gitlab/ui';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
import ZenMode from '~/zen_mode';
import WorkItemDetail from '../components/work_item_detail.vue';
+import {
+ sprintfWorkItem,
+ I18N_WORK_ITEM_ERROR_DELETING,
+ I18N_WORK_ITEM_DELETED,
+} from '../constants';
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
export default {
@@ -34,7 +38,7 @@ export default {
this.ZenMode = new ZenMode();
},
methods: {
- deleteWorkItem() {
+ deleteWorkItem(workItemType) {
this.$apollo
.mutate({
mutation: deleteWorkItemMutation,
@@ -53,13 +57,12 @@ export default {
throw new Error(workItemDelete.errors[0]);
}
- this.$toast.show(s__('WorkItem|Work item deleted'));
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_DELETED, workItemType);
+ this.$toast.show(msg);
visitUrl(this.issuesListPath);
})
.catch((e) => {
- this.error =
- e.message ||
- s__('WorkItem|Something went wrong when deleting the work item. Please try again.');
+ this.error = e.message || sprintfWorkItem(I18N_WORK_ITEM_ERROR_DELETING, workItemType);
});
},
},
@@ -69,6 +72,6 @@ export default {
<template>
<div>
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
- <work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem" />
+ <work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem($event)" />
</div>
</template>