509 lines
15 KiB
Vue
509 lines
15 KiB
Vue
<script>
|
|
import { isEmpty } from 'lodash';
|
|
import {
|
|
GlAlert,
|
|
GlSkeletonLoader,
|
|
GlLoadingIcon,
|
|
GlIcon,
|
|
GlBadge,
|
|
GlButton,
|
|
GlTooltipDirective,
|
|
GlEmptyState,
|
|
} from '@gitlab/ui';
|
|
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
|
|
import { s__ } from '~/locale';
|
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
|
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';
|
|
import {
|
|
i18n,
|
|
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,
|
|
WIDGET_TYPE_MILESTONE,
|
|
WIDGET_TYPE_ITERATION,
|
|
} from '../constants';
|
|
|
|
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';
|
|
import { getWorkItemQuery } from '../utils';
|
|
|
|
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 WorkItemMilestone from './work_item_milestone.vue';
|
|
import WorkItemInformation from './work_item_information.vue';
|
|
|
|
export default {
|
|
i18n,
|
|
directives: {
|
|
GlTooltip: GlTooltipDirective,
|
|
},
|
|
components: {
|
|
GlAlert,
|
|
GlBadge,
|
|
GlButton,
|
|
GlLoadingIcon,
|
|
GlSkeletonLoader,
|
|
GlIcon,
|
|
GlEmptyState,
|
|
WorkItemAssignees,
|
|
WorkItemActions,
|
|
WorkItemDescription,
|
|
WorkItemDueDate,
|
|
WorkItemLabels,
|
|
WorkItemTitle,
|
|
WorkItemState,
|
|
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
|
|
WorkItemInformation,
|
|
LocalStorageSync,
|
|
WorkItemTypeIcon,
|
|
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
|
|
WorkItemMilestone,
|
|
},
|
|
mixins: [glFeatureFlagMixin()],
|
|
inject: ['fullPath'],
|
|
props: {
|
|
isModal: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false,
|
|
},
|
|
workItemId: {
|
|
type: String,
|
|
required: false,
|
|
default: null,
|
|
},
|
|
iid: {
|
|
type: String,
|
|
required: false,
|
|
default: null,
|
|
},
|
|
workItemParentId: {
|
|
type: String,
|
|
required: false,
|
|
default: null,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
error: undefined,
|
|
updateError: undefined,
|
|
workItem: {},
|
|
showInfoBanner: true,
|
|
updateInProgress: false,
|
|
};
|
|
},
|
|
apollo: {
|
|
workItem: {
|
|
query() {
|
|
return getWorkItemQuery(this.fetchByIid);
|
|
},
|
|
variables() {
|
|
return this.queryVariables;
|
|
},
|
|
skip() {
|
|
return !this.workItemId;
|
|
},
|
|
update(data) {
|
|
const workItem = this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
|
|
return workItem ?? {};
|
|
},
|
|
error() {
|
|
this.setEmptyState();
|
|
},
|
|
result() {
|
|
if (isEmpty(this.workItem)) {
|
|
this.setEmptyState();
|
|
}
|
|
if (!this.isModal && this.workItem.project) {
|
|
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.workItem.id,
|
|
};
|
|
},
|
|
skip() {
|
|
return !this.workItem?.id;
|
|
},
|
|
},
|
|
{
|
|
document: workItemDatesSubscription,
|
|
variables() {
|
|
return {
|
|
issuableId: this.workItem.id,
|
|
};
|
|
},
|
|
skip() {
|
|
return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE) || !this.workItem?.id;
|
|
},
|
|
},
|
|
{
|
|
document: workItemAssigneesSubscription,
|
|
variables() {
|
|
return {
|
|
issuableId: this.workItem.id,
|
|
};
|
|
},
|
|
skip() {
|
|
return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES) || !this.workItem?.id;
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
computed: {
|
|
workItemLoading() {
|
|
return this.$apollo.queries.workItem.loading;
|
|
},
|
|
workItemType() {
|
|
return this.workItem.workItemType?.name;
|
|
},
|
|
canUpdate() {
|
|
return this.workItem?.userPermissions?.updateWorkItem;
|
|
},
|
|
canDelete() {
|
|
return this.workItem?.userPermissions?.deleteWorkItem;
|
|
},
|
|
fullPath() {
|
|
return this.workItem?.project.fullPath;
|
|
},
|
|
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.isWidgetPresent(WIDGET_TYPE_DESCRIPTION);
|
|
},
|
|
workItemAssignees() {
|
|
return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
|
|
},
|
|
workItemLabels() {
|
|
return this.isWidgetPresent(WIDGET_TYPE_LABELS);
|
|
},
|
|
workItemDueDate() {
|
|
return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
|
|
},
|
|
workItemWeight() {
|
|
return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
|
|
},
|
|
workItemHierarchy() {
|
|
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
|
|
},
|
|
workItemIteration() {
|
|
return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
|
|
},
|
|
workItemMilestone() {
|
|
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
|
|
},
|
|
fetchByIid() {
|
|
return this.glFeatures.useIidInWorkItemsPath && parseBoolean(this.$route.query.iid_path);
|
|
},
|
|
queryVariables() {
|
|
return this.fetchByIid
|
|
? {
|
|
fullPath: this.fullPath,
|
|
iid: this.iid,
|
|
}
|
|
: {
|
|
id: this.workItemId,
|
|
};
|
|
},
|
|
},
|
|
beforeDestroy() {
|
|
/** make sure that if the user has not even dismissed the alert ,
|
|
* should no be able to see the information next time and update the local storage * */
|
|
this.dismissBanner();
|
|
},
|
|
methods: {
|
|
isWidgetPresent(type) {
|
|
return this.workItem?.widgets?.find((widget) => widget.type === type);
|
|
},
|
|
dismissBanner() {
|
|
this.showInfoBanner = false;
|
|
},
|
|
toggleConfidentiality(confidentialStatus) {
|
|
this.updateInProgress = true;
|
|
let updateMutation = updateWorkItemMutation;
|
|
let inputVariables = {
|
|
id: this.workItem.id,
|
|
confidential: confidentialStatus,
|
|
};
|
|
|
|
if (this.parentWorkItem) {
|
|
updateMutation = updateWorkItemTaskMutation;
|
|
inputVariables = {
|
|
id: this.parentWorkItem.id,
|
|
taskData: {
|
|
id: this.workItem.id,
|
|
confidential: confidentialStatus,
|
|
},
|
|
};
|
|
}
|
|
|
|
this.$apollo
|
|
.mutate({
|
|
mutation: updateMutation,
|
|
variables: {
|
|
input: inputVariables,
|
|
},
|
|
})
|
|
.then(
|
|
({
|
|
data: {
|
|
workItemUpdate: { errors, workItem, task },
|
|
},
|
|
}) => {
|
|
if (errors?.length) {
|
|
throw new Error(errors[0]);
|
|
}
|
|
|
|
this.$emit('workItemUpdated', {
|
|
confidential: workItem?.confidential || task?.confidential,
|
|
});
|
|
},
|
|
)
|
|
.catch((error) => {
|
|
this.updateError = error.message;
|
|
})
|
|
.finally(() => {
|
|
this.updateInProgress = false;
|
|
});
|
|
},
|
|
setEmptyState() {
|
|
this.error = this.$options.i18n.fetchError;
|
|
document.title = s__('404|Not found');
|
|
},
|
|
},
|
|
WORK_ITEM_VIEWED_STORAGE_KEY,
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<section class="gl-pt-5">
|
|
<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">
|
|
<gl-skeleton-loader :height="65" :width="240">
|
|
<rect width="240" height="20" x="5" y="0" rx="4" />
|
|
<rect width="100" height="20" x="5" y="45" rx="4" />
|
|
</gl-skeleton-loader>
|
|
</div>
|
|
<template v-else>
|
|
<div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
|
|
<ul
|
|
v-if="parentWorkItem"
|
|
class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0"
|
|
data-testid="work-item-parent"
|
|
>
|
|
<li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-overflow-hidden">
|
|
<gl-button
|
|
v-gl-tooltip.hover
|
|
class="gl-text-truncate gl-max-w-full"
|
|
icon="issues"
|
|
category="tertiary"
|
|
:href="parentUrl"
|
|
:title="parentWorkItem.title"
|
|
>{{ parentWorkItem.title }}</gl-button
|
|
>
|
|
<gl-icon name="chevron-right" :size="16" class="gl-flex-shrink-0" />
|
|
</li>
|
|
<li
|
|
class="gl-px-4 gl-py-3 gl-line-height-0 gl-display-flex gl-align-items-center gl-overflow-hidden gl-flex-shrink-0"
|
|
>
|
|
<work-item-type-icon
|
|
:work-item-icon-name="workItemIconName"
|
|
:work-item-type="workItemType && workItemType.toUpperCase()"
|
|
/>
|
|
{{ workItemType }}
|
|
</li>
|
|
</ul>
|
|
<work-item-type-icon
|
|
v-else-if="!error"
|
|
:work-item-icon-name="workItemIconName"
|
|
:work-item-type="workItemType && workItemType.toUpperCase()"
|
|
show-text
|
|
class="gl-font-weight-bold gl-text-secondary gl-mr-auto"
|
|
data-testid="work-item-type"
|
|
/>
|
|
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
|
|
<gl-badge
|
|
v-if="workItem.confidential"
|
|
v-gl-tooltip.bottom
|
|
:title="$options.i18n.confidentialTooltip"
|
|
variant="warning"
|
|
icon="eye-slash"
|
|
class="gl-mr-3 gl-cursor-help"
|
|
>{{ __('Confidential') }}</gl-badge
|
|
>
|
|
<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', { workItemType, workItemId: workItem.id })"
|
|
@toggleWorkItemConfidentiality="toggleConfidentiality"
|
|
@error="updateError = $event"
|
|
/>
|
|
<gl-button
|
|
v-if="isModal"
|
|
category="tertiary"
|
|
data-testid="work-item-close"
|
|
icon="close"
|
|
:aria-label="__('Close')"
|
|
@click="$emit('close')"
|
|
/>
|
|
</div>
|
|
<local-storage-sync
|
|
v-model="showInfoBanner"
|
|
:storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY"
|
|
>
|
|
<work-item-information
|
|
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="updateError = $event"
|
|
/>
|
|
<work-item-state
|
|
:work-item="workItem"
|
|
:work-item-parent-id="workItemParentId"
|
|
:can-update="canUpdate"
|
|
@error="updateError = $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="updateError = $event"
|
|
/>
|
|
<work-item-labels
|
|
v-if="workItemLabels"
|
|
:work-item-id="workItem.id"
|
|
:can-update="canUpdate"
|
|
:full-path="fullPath"
|
|
:fetch-by-iid="fetchByIid"
|
|
:query-variables="queryVariables"
|
|
@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-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="updateError = $event"
|
|
/>
|
|
</template>
|
|
<work-item-weight
|
|
v-if="workItemWeight"
|
|
class="gl-mb-5"
|
|
:can-update="canUpdate"
|
|
:weight="workItemWeight.weight"
|
|
:work-item-id="workItem.id"
|
|
:work-item-type="workItemType"
|
|
:fetch-by-iid="fetchByIid"
|
|
:query-variables="queryVariables"
|
|
@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"
|
|
:fetch-by-iid="fetchByIid"
|
|
:query-variables="queryVariables"
|
|
@error="updateError = $event"
|
|
/>
|
|
</template>
|
|
<work-item-description
|
|
v-if="hasDescriptionWidget"
|
|
:work-item-id="workItem.id"
|
|
:full-path="fullPath"
|
|
:fetch-by-iid="fetchByIid"
|
|
:query-variables="queryVariables"
|
|
class="gl-pt-5"
|
|
@error="updateError = $event"
|
|
/>
|
|
<gl-empty-state
|
|
v-if="error"
|
|
:title="$options.i18n.fetchErrorTitle"
|
|
:description="error"
|
|
:svg-path="noAccessSvgPath"
|
|
/>
|
|
</template>
|
|
</section>
|
|
</template>
|