gitlab-org--gitlab-foss/app/assets/javascripts/work_items/components/work_item_milestone.vue

249 lines
6.6 KiB
Vue

<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>