diff options
Diffstat (limited to 'app/assets/javascripts/work_items/components/work_item_milestone.vue')
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_milestone.vue | 248 |
1 files changed, 248 insertions, 0 deletions
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> |