diff options
Diffstat (limited to 'app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue')
-rw-r--r-- | app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue | 360 |
1 files changed, 360 insertions, 0 deletions
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue new file mode 100644 index 00000000000..c80ccc928b3 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -0,0 +1,360 @@ +<script> +import { + GlLink, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlSearchBoxByType, + GlDropdownDivider, + GlLoadingIcon, + GlIcon, + GlTooltipDirective, +} from '@gitlab/ui'; +import createFlash from '~/flash'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { IssuableType } from '~/issue_show/constants'; +import { __, s__, sprintf } from '~/locale'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { + IssuableAttributeState, + IssuableAttributeType, + issuableAttributesQueries, + noAttributeId, +} from '../constants'; + +export default { + noAttributeId, + IssuableAttributeState, + issuableAttributesQueries, + i18n: { + [IssuableAttributeType.Milestone]: __('Milestone'), + none: __('None'), + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + SidebarEditableItem, + GlLink, + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlDropdownDivider, + GlSearchBoxByType, + GlIcon, + GlLoadingIcon, + }, + inject: { + isClassicSidebar: { + default: false, + }, + }, + props: { + issuableAttribute: { + type: String, + required: true, + validator(value) { + return [IssuableAttributeType.Milestone].includes(value); + }, + }, + workspacePath: { + required: true, + type: String, + }, + iid: { + required: true, + type: String, + }, + attrWorkspacePath: { + required: true, + type: String, + }, + issuableType: { + type: String, + required: true, + validator(value) { + return value === IssuableType.Issue; + }, + }, + }, + apollo: { + currentAttribute: { + query() { + const { current } = this.issuableAttributeQuery; + const { query } = current[this.issuableType]; + + return query; + }, + variables() { + return { + fullPath: this.workspacePath, + iid: this.iid, + }; + }, + update(data) { + return data?.workspace?.issuable.attribute; + }, + error(error) { + createFlash({ + message: this.i18n.currentFetchError, + captureError: true, + error, + }); + }, + }, + attributesList: { + query() { + const { list } = this.issuableAttributeQuery; + const { query } = list[this.issuableType]; + + return query; + }, + skip() { + return !this.editing; + }, + debounce: 250, + variables() { + return { + fullPath: this.attrWorkspacePath, + title: this.searchTerm, + state: this.$options.IssuableAttributeState[this.issuableAttribute], + }; + }, + update(data) { + if (data?.workspace) { + return data?.workspace?.attributes.nodes; + } + return []; + }, + error(error) { + createFlash({ message: this.i18n.listFetchError, captureError: true, error }); + }, + }, + }, + data() { + return { + searchTerm: '', + editing: false, + updating: false, + selectedTitle: null, + currentAttribute: null, + attributesList: [], + tracking: { + label: 'right_sidebar', + event: 'click_edit_button', + property: this.issuableAttribute, + }, + }; + }, + computed: { + issuableAttributeQuery() { + return this.$options.issuableAttributesQueries[this.issuableAttribute]; + }, + attributeTitle() { + return this.currentAttribute?.title || this.i18n.noAttribute; + }, + attributeUrl() { + return this.currentAttribute?.webUrl; + }, + dropdownText() { + return this.currentAttribute + ? this.currentAttribute?.title + : this.$options.i18n[this.issuableAttribute]; + }, + loading() { + return this.$apollo.queries.currentAttribute.loading; + }, + emptyPropsList() { + return this.attributesList.length === 0; + }, + attributeTypeTitle() { + return this.$options.i18n[this.issuableAttribute]; + }, + i18n() { + return { + noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), { + issuableAttribute: this.issuableAttribute, + }), + assignAttribute: sprintf(s__('DropdownWidget|Assign %{issuableAttribute}'), { + issuableAttribute: this.issuableAttribute, + }), + noAttributesFound: sprintf(s__('DropdownWidget|No %{issuableAttribute} found'), { + issuableAttribute: this.issuableAttribute, + }), + updateError: sprintf( + s__( + 'DropdownWidget|Failed to set %{issuableAttribute} on this %{issuableType}. Please try again.', + ), + { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType }, + ), + listFetchError: sprintf( + s__( + 'DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again.', + ), + { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType }, + ), + currentFetchError: sprintf( + s__( + 'DropdownWidget|An error occurred while fetching the assigned %{issuableAttribute} of the selected %{issuableType}.', + ), + { issuableAttribute: this.issuableAttribute, issuableType: this.issuableType }, + ), + }; + }, + }, + methods: { + updateAttribute(attributeId) { + if (this.currentAttribute === null && attributeId === null) return; + if (attributeId === this.currentAttribute?.id) return; + + this.updating = true; + + const selectedAttribute = + Boolean(attributeId) && this.attributesList.find((p) => p.id === attributeId); + this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.$options.i18n.none; + + const { current } = this.issuableAttributeQuery; + const { mutation } = current[this.issuableType]; + + this.$apollo + .mutate({ + mutation, + variables: { + fullPath: this.workspacePath, + attributeId: + this.issuableAttribute === IssuableAttributeType.Milestone + ? getIdFromGraphQLId(attributeId) + : attributeId, + iid: this.iid, + }, + }) + .then(({ data }) => { + if (data.issuableSetAttribute?.errors?.length) { + createFlash({ + message: data.issuableSetAttribute.errors[0], + captureError: true, + error: data.issuableSetAttribute.errors[0], + }); + } else { + this.$emit('attribute-updated', data); + } + }) + .catch((error) => { + createFlash({ message: this.i18n.updateError, captureError: true, error }); + }) + .finally(() => { + this.updating = false; + this.searchTerm = ''; + this.selectedTitle = null; + }); + }, + isAttributeChecked(attributeId = undefined) { + return ( + attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId) + ); + }, + showDropdown() { + this.$refs.newDropdown.show(); + }, + handleOpen() { + this.editing = true; + this.showDropdown(); + }, + handleClose() { + this.editing = false; + }, + setFocus() { + this.$refs.search.focusInput(); + }, + }, +}; +</script> + +<template> + <sidebar-editable-item + ref="editable" + :title="attributeTypeTitle" + :data-testid="`${issuableAttribute}-edit`" + :tracking="tracking" + :loading="updating || loading" + @open="handleOpen" + @close="handleClose" + > + <template #collapsed> + <div v-if="isClassicSidebar" v-gl-tooltip class="sidebar-collapsed-icon"> + <gl-icon :size="16" :aria-label="attributeTypeTitle" :name="issuableAttribute" /> + <span class="collapse-truncated-title">{{ attributeTitle }}</span> + </div> + <div + :data-testid="`select-${issuableAttribute}`" + :class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'" + > + <span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span> + <span v-else-if="!currentAttribute" class="gl-text-gray-500"> + {{ $options.i18n.none }} + </span> + <slot + v-else + name="value" + :attributeTitle="attributeTitle" + :attributeUrl="attributeUrl" + :currentAttribute="currentAttribute" + > + <gl-link class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl"> + {{ attributeTitle }} + </gl-link> + </slot> + </div> + </template> + <template #default> + <gl-dropdown + ref="newDropdown" + lazy + :header-text="i18n.assignAttribute" + :text="dropdownText" + :loading="loading" + class="gl-w-full" + @shown="setFocus" + > + <gl-search-box-by-type ref="search" v-model="searchTerm" /> + <gl-dropdown-item + :data-testid="`no-${issuableAttribute}-item`" + :is-check-item="true" + :is-checked="isAttributeChecked($options.noAttributeId)" + @click="updateAttribute($options.noAttributeId)" + > + {{ i18n.noAttribute }} + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-loading-icon + v-if="$apollo.queries.attributesList.loading" + class="gl-py-4" + data-testid="loading-icon-dropdown" + /> + <template v-else> + <gl-dropdown-text v-if="emptyPropsList"> + {{ i18n.noAttributesFound }} + </gl-dropdown-text> + <slot + v-else + name="list" + :attributesList="attributesList" + :isAttributeChecked="isAttributeChecked" + :updateAttribute="updateAttribute" + > + <gl-dropdown-item + v-for="attrItem in attributesList" + :key="attrItem.id" + :is-check-item="true" + :is-checked="isAttributeChecked(attrItem.id)" + :data-testid="`${issuableAttribute}-items`" + @click="updateAttribute(attrItem.id)" + > + {{ attrItem.title }} + </gl-dropdown-item> + </slot> + </template> + </gl-dropdown> + </template> + </sidebar-editable-item> +</template> |