diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget')
15 files changed, 419 insertions, 178 deletions
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js index 389eb174c0e..cd671b4d8f5 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js @@ -1,7 +1,13 @@ export const SCOPED_LABEL_DELIMITER = '::'; +export const DEBOUNCE_DROPDOWN_DELAY = 200; export const DropdownVariant = { Sidebar: 'sidebar', Standalone: 'standalone', Embedded: 'embedded', }; + +export const LabelType = { + group: 'group', + project: 'project', +}; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue index 3ee0baf8812..f7485de0342 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue @@ -1,20 +1,25 @@ <script> import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui'; +import { debounce } from 'lodash'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { __, s__, sprintf } from '~/locale'; import DropdownContentsCreateView from './dropdown_contents_create_view.vue'; import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue'; +import DropdownFooter from './dropdown_footer.vue'; +import DropdownHeader from './dropdown_header.vue'; import { isDropdownVariantStandalone, isDropdownVariantSidebar } from './utils'; export default { components: { DropdownContentsLabelsView, DropdownContentsCreateView, + DropdownHeader, + DropdownFooter, GlButton, GlDropdown, GlDropdownItem, GlLink, }, - inject: ['allowLabelCreate', 'labelsManagePath'], props: { labelsCreateTitle: { type: String, @@ -48,10 +53,6 @@ export default { type: String, required: true, }, - issuableType: { - type: String, - required: true, - }, isVisible: { type: Boolean, required: false, @@ -61,10 +62,17 @@ export default { type: String, required: true, }, + workspaceType: { + type: String, + required: true, + }, attrWorkspacePath: { type: String, - required: false, - default: undefined, + required: true, + }, + labelCreateType: { + type: String, + required: true, }, }, data() { @@ -72,6 +80,7 @@ export default { showDropdownContentsCreateView: false, localSelectedLabels: [...this.selectedLabels], isDirty: false, + searchKey: '', }; }, computed: { @@ -113,15 +122,24 @@ export default { if (newVal) { this.$refs.dropdown.show(); this.isDirty = false; + this.localSelectedLabels = this.selectedLabels; } else { this.$refs.dropdown.hide(); this.setLabels(); } }, selectedLabels(newVal) { - this.localSelectedLabels = newVal; + if (!this.isDirty) { + this.localSelectedLabels = newVal; + } }, }, + created() { + this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); + }, + beforeDestroy() { + this.debouncedSearchKeyUpdate.cancel(); + }, methods: { toggleDropdownContentsCreateView() { this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView; @@ -140,10 +158,20 @@ export default { this.$emit('setLabels', this.localSelectedLabels); }, handleDropdownHide() { + this.$emit('closeDropdown'); if (!isDropdownVariantSidebar(this.variant)) { this.setLabels(); } }, + setSearchKey(value) { + this.searchKey = value; + }, + setFocus() { + this.$refs.header.focusInput(); + }, + showDropdown() { + this.$refs.dropdown.show(); + }, }, }; </script> @@ -153,62 +181,44 @@ export default { ref="dropdown" :text="buttonText" class="gl-w-full gl-mt-2" + data-testid="labels-select-dropdown-contents" data-qa-selector="labels_dropdown_content" @hide="handleDropdownHide" + @shown="setFocus" > <template #header> - <div + <dropdown-header v-if="!isStandalone" - class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!" - data-testid="dropdown-header" - > - <gl-button - v-if="showDropdownContentsCreateView" - :aria-label="__('Go back')" - variant="link" - size="small" - class="js-btn-back dropdown-header-button gl-p-0" - icon="arrow-left" - data-testid="go-back-button" - @click.stop="toggleDropdownContent" - /> - <span class="gl-flex-grow-1">{{ dropdownTitle }}</span> - <gl-button - :aria-label="__('Close')" - variant="link" - size="small" - class="dropdown-header-button gl-p-0!" - icon="close" - data-testid="close-button" - @click="$emit('closeDropdown')" - /> - </div> + ref="header" + v-model="searchKey" + :labels-create-title="labelsCreateTitle" + :labels-list-title="labelsListTitle" + :show-dropdown-contents-create-view="showDropdownContentsCreateView" + @toggleDropdownContentsCreateView="toggleDropdownContent" + @closeDropdown="$emit('closeDropdown')" + @input="debouncedSearchKeyUpdate" + /> </template> <template #default> <component :is="dropdownContentsView" v-model="localSelectedLabels" - :selected-labels="selectedLabels" + :search-key="searchKey" :allow-multiselect="allowMultiselect" - :issuable-type="issuableType" :full-path="fullPath" + :workspace-type="workspaceType" :attr-workspace-path="attrWorkspacePath" - @hideCreateView="toggleDropdownContentsCreateView" + :label-create-type="labelCreateType" + @hideCreateView="toggleDropdownContent" /> </template> <template #footer> - <div v-if="showDropdownFooter" data-testid="dropdown-footer"> - <gl-dropdown-item - v-if="allowLabelCreate" - data-testid="create-label-button" - @click.capture.native.stop="toggleDropdownContent" - > - {{ footerCreateLabelTitle }} - </gl-dropdown-item> - <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop> - {{ footerManageLabelTitle }} - </gl-dropdown-item> - </div> + <dropdown-footer + v-if="showDropdownFooter" + :footer-create-label-title="footerCreateLabelTitle" + :footer-manage-label-title="footerManageLabelTitle" + @toggleDropdownContentsCreateView="toggleDropdownContent" + /> </template> </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue index a2ed08e6b28..da626a21b14 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue @@ -2,10 +2,10 @@ import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; import produce from 'immer'; import createFlash from '~/flash'; -import { IssuableType } from '~/issue_show/constants'; import { __ } from '~/locale'; -import { labelsQueries } from '~/sidebar/constants'; +import { workspaceLabelsQueries } from '~/sidebar/constants'; import createLabelMutation from './graphql/create_label.mutation.graphql'; +import { LabelType } from './constants'; const errorMessage = __('Error creating label.'); @@ -20,18 +20,21 @@ export default { GlTooltip: GlTooltipDirective, }, props: { - issuableType: { + fullPath: { type: String, required: true, }, - fullPath: { + attrWorkspacePath: { type: String, required: true, }, - attrWorkspacePath: { + labelCreateType: { + type: String, + required: true, + }, + workspaceType: { type: String, - required: false, - default: undefined, + required: true, }, }, data() { @@ -50,25 +53,13 @@ export default { return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] })); }, mutationVariables() { - if (this.issuableType === IssuableType.Epic) { - return { - title: this.labelTitle, - color: this.selectedColor, - groupPath: this.fullPath, - }; - } + const attributePath = this.labelCreateType === LabelType.group ? 'groupPath' : 'projectPath'; - return this.attrWorkspacePath !== undefined - ? { - title: this.labelTitle, - color: this.selectedColor, - groupPath: this.attrWorkspacePath, - } - : { - title: this.labelTitle, - color: this.selectedColor, - projectPath: this.fullPath, - }; + return { + title: this.labelTitle, + color: this.selectedColor, + [attributePath]: this.attrWorkspacePath, + }; }, }, methods: { @@ -82,8 +73,10 @@ export default { this.selectedColor = this.getColorCode(color); }, updateLabelsInCache(store, label) { + const { query } = workspaceLabelsQueries[this.workspaceType]; + const sourceData = store.readQuery({ - query: labelsQueries[this.issuableType].workspaceQuery, + query, variables: { fullPath: this.fullPath, searchTerm: '' }, }); @@ -95,7 +88,7 @@ export default { }); store.writeQuery({ - query: labelsQueries[this.issuableType].workspaceQuery, + query, variables: { fullPath: this.fullPath, searchTerm: '' }, data, }); @@ -180,7 +173,7 @@ export default { <gl-button class="js-btn-cancel-create" data-testid="cancel-button" - @click="$emit('hideCreateView')" + @click.stop="$emit('hideCreateView')" > {{ __('Cancel') }} </gl-button> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue index e6a25362ff0..e9a2d7747e2 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue @@ -1,18 +1,10 @@ <script> -import { - GlDropdownForm, - GlDropdownItem, - GlLoadingIcon, - GlSearchBoxByType, - GlIntersectionObserver, -} from '@gitlab/ui'; +import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import { debounce } from 'lodash'; import createFlash from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { __ } from '~/locale'; -import { labelsQueries } from '~/sidebar/constants'; +import { workspaceLabelsQueries } from '~/sidebar/constants'; import LabelItem from './label_item.vue'; export default { @@ -20,7 +12,6 @@ export default { GlDropdownForm, GlDropdownItem, GlLoadingIcon, - GlSearchBoxByType, GlIntersectionObserver, LabelItem, }, @@ -28,18 +19,10 @@ export default { prop: 'localSelectedLabels', }, props: { - selectedLabels: { - type: Array, - required: true, - }, allowMultiselect: { type: Boolean, required: true, }, - issuableType: { - type: String, - required: true, - }, localSelectedLabels: { type: Array, required: true, @@ -48,10 +31,17 @@ export default { type: String, required: true, }, + searchKey: { + type: String, + required: true, + }, + workspaceType: { + type: String, + required: true, + }, }, data() { return { - searchKey: '', labels: [], isVisible: false, }; @@ -59,7 +49,7 @@ export default { apollo: { labels: { query() { - return labelsQueries[this.issuableType].workspaceQuery; + return workspaceLabelsQueries[this.workspaceType].query; }, variables() { return { @@ -71,12 +61,6 @@ export default { return this.searchKey.length === 1 || !this.isVisible; }, update: (data) => data.workspace?.labels?.nodes || [], - async result() { - if (this.$refs.searchInput) { - await this.$nextTick; - this.$refs.searchInput.focusInput(); - } - }, error() { createFlash({ message: __('Error fetching labels.') }); }, @@ -101,12 +85,6 @@ export default { return Boolean(this.searchKey) && this.visibleLabels.length === 0; }, }, - created() { - this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); - }, - beforeDestroy() { - this.debouncedSearchKeyUpdate.cancel(); - }, methods: { isLabelSelected(label) { return this.localSelectedLabelsIds.includes(getIdFromGraphQLId(label.id)); @@ -137,13 +115,7 @@ export default { ({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id, ); } else { - labels = [ - ...this.localSelectedLabels, - { - ...label, - id: getIdFromGraphQLId(label.id), - }, - ]; + labels = [...this.localSelectedLabels, label]; } this.$emit('input', labels); }, @@ -153,12 +125,8 @@ export default { this.$emit('closeDropdown', this.localSelectedLabels); } }, - setSearchKey(value) { - this.searchKey = value; - }, onDropdownAppear() { this.isVisible = true; - this.$refs.searchInput.focusInput(); }, }, }; @@ -167,14 +135,6 @@ export default { <template> <gl-intersection-observer @appear="onDropdownAppear"> <gl-dropdown-form class="labels-select-contents-list js-labels-list"> - <gl-search-box-by-type - ref="searchInput" - :value="searchKey" - :disabled="labelsFetchInProgress" - data-qa-selector="dropdown_input_field" - data-testid="dropdown-input-field" - @input="debouncedSearchKeyUpdate" - /> <div ref="labelsListContainer" data-testid="dropdown-content"> <gl-loading-icon v-if="labelsFetchInProgress" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue new file mode 100644 index 00000000000..e67e704ffb8 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue @@ -0,0 +1,35 @@ +<script> +import { GlDropdownItem } from '@gitlab/ui'; + +export default { + components: { + GlDropdownItem, + }, + inject: ['allowLabelCreate', 'labelsManagePath'], + props: { + footerCreateLabelTitle: { + type: String, + required: true, + }, + footerManageLabelTitle: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div data-testid="dropdown-footer"> + <gl-dropdown-item + v-if="allowLabelCreate" + data-testid="create-label-button" + @click.capture.native.stop="$emit('toggleDropdownContentsCreateView')" + > + {{ footerCreateLabelTitle }} + </gl-dropdown-item> + <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop> + {{ footerManageLabelTitle }} + </gl-dropdown-item> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue new file mode 100644 index 00000000000..10064b01648 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue @@ -0,0 +1,82 @@ +<script> +import { GlButton, GlSearchBoxByType } from '@gitlab/ui'; + +export default { + components: { + GlButton, + GlSearchBoxByType, + }, + model: { + prop: 'searchKey', + }, + props: { + labelsCreateTitle: { + type: String, + required: true, + }, + labelsListTitle: { + type: String, + required: true, + }, + showDropdownContentsCreateView: { + type: Boolean, + required: true, + }, + labelsFetchInProgress: { + type: Boolean, + required: false, + default: false, + }, + searchKey: { + type: String, + required: true, + }, + }, + computed: { + dropdownTitle() { + return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle; + }, + }, + methods: { + focusInput() { + this.$refs.searchInput?.focusInput(); + }, + }, +}; +</script> + +<template> + <div data-testid="dropdown-header"> + <div class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"> + <gl-button + v-if="showDropdownContentsCreateView" + :aria-label="__('Go back')" + variant="link" + size="small" + class="js-btn-back dropdown-header-button gl-p-0" + icon="arrow-left" + data-testid="go-back-button" + @click.stop="$emit('toggleDropdownContentsCreateView')" + /> + <span class="gl-flex-grow-1">{{ dropdownTitle }}</span> + <gl-button + :aria-label="__('Close')" + variant="link" + size="small" + class="dropdown-header-button gl-p-0!" + icon="close" + data-testid="close-button" + @click="$emit('closeDropdown')" + /> + </div> + <gl-search-box-by-type + v-if="!showDropdownContentsCreateView" + ref="searchInput" + :value="searchKey" + :disabled="labelsFetchInProgress" + data-qa-selector="dropdown_input_field" + data-testid="dropdown-input-field" + @input="$emit('input', $event)" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue index 71d3d87cce5..aed5bc303ee 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue @@ -1,7 +1,6 @@ <script> import { GlLabel } from '@gitlab/ui'; import { sortBy } from 'lodash'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { isScopedLabel } from '~/lib/utils/common_utils'; export default { @@ -47,7 +46,7 @@ export default { return this.allowScopedLabels && isScopedLabel(label); }, removeLabel(labelId) { - this.$emit('onLabelRemove', getIdFromGraphQLId(labelId)); + this.$emit('onLabelRemove', labelId); }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql index eb478645a03..a9c791091fc 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql @@ -1,12 +1,11 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" + mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPath: ID) { labelCreate( input: { title: $title, color: $color, projectPath: $projectPath, groupPath: $groupPath } ) { label { - id - color - description - title + ...Label } errors } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql index a2e8579486f..c130cc426dc 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql @@ -1,13 +1,12 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" + query epicLabels($fullPath: ID!, $iid: ID) { workspace: group(fullPath: $fullPath) { issuable: epic(iid: $iid) { id labels { nodes { - id - title - color - description + ...Label } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql new file mode 100644 index 00000000000..45fcb50732e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql @@ -0,0 +1,15 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" + +mutation updateEpicLabels($input: UpdateEpicInput!) { + updateEpic(input: $input) { + epic { + id + labels { + nodes { + ...Label + } + } + } + errors + } +} diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql index acc9bcd2015..ce1a69f84c0 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql @@ -1,11 +1,11 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" + query groupLabels($fullPath: ID!, $searchTerm: String) { workspace: group(fullPath: $fullPath) { - labels(searchTerm: $searchTerm, onlyGroupLabels: true) { + id + labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) { nodes { - id - title - color - description + ...Label } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql index 1c2fd3bb7c0..e471d279b24 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql @@ -1,13 +1,12 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" + query issueLabels($fullPath: ID!, $iid: String) { workspace: project(fullPath: $fullPath) { issuable: issue(iid: $iid) { id labels { nodes { - id - title - color - description + ...Label } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql new file mode 100644 index 00000000000..dd80e89c8a7 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql @@ -0,0 +1,14 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" + +query mergeRequestLabels($fullPath: ID!, $iid: String!) { + workspace: project(fullPath: $fullPath) { + issuable: mergeRequest(iid: $iid) { + id + labels { + nodes { + ...Label + } + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql index dc39220487d..a7c24620aad 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql @@ -1,11 +1,11 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" + query projectLabels($fullPath: ID!, $searchTerm: String) { workspace: project(fullPath: $fullPath) { + id labels(searchTerm: $searchTerm, includeAncestorGroups: true) { nodes { - id - title - color - description + ...Label } } } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue index 6bd43da2203..97a65c13933 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue @@ -1,9 +1,12 @@ <script> +import { debounce } from 'lodash'; +import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils'; import createFlash from '~/flash'; +import { IssuableType } from '~/issue_show/constants'; import { __ } from '~/locale'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { labelsQueries } from '~/sidebar/constants'; -import { DropdownVariant } from './constants'; +import { issuableLabelsQueries } from '~/sidebar/constants'; +import { DEBOUNCE_DROPDOWN_DELAY, DropdownVariant } from './constants'; import DropdownContents from './dropdown_contents.vue'; import DropdownValue from './dropdown_value.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; @@ -50,16 +53,6 @@ export default { required: false, default: DropdownVariant.Sidebar, }, - selectedLabels: { - type: Array, - required: false, - default: () => [], - }, - labelsSelectInProgress: { - type: Boolean, - required: false, - default: false, - }, labelsFilterBasePath: { type: String, required: false, @@ -95,36 +88,44 @@ export default { required: false, default: __('Manage group labels'), }, - isEditing: { - type: Boolean, - required: false, - default: false, - }, issuableType: { type: String, required: true, }, + workspaceType: { + type: String, + required: true, + }, attrWorkspacePath: { type: String, - required: false, - default: undefined, + required: true, + }, + labelCreateType: { + type: String, + required: true, }, }, data() { return { contentIsOnViewport: true, issuableLabels: [], + labelsSelectInProgress: false, + oldIid: null, + sidebarExpandedOnClick: false, }; }, computed: { isLoading() { return this.labelsSelectInProgress || this.$apollo.queries.issuableLabels.loading; }, + issuableLabelIds() { + return this.issuableLabels.map((label) => label.id); + }, }, apollo: { issuableLabels: { query() { - return labelsQueries[this.issuableType].issuableQuery; + return issuableLabelsQueries[this.issuableType].issuableQuery; }, skip() { return !isDropdownVariantSidebar(this.variant); @@ -143,16 +144,140 @@ export default { }, }, }, + watch: { + iid(_, oldVal) { + this.oldIid = oldVal; + }, + }, + mounted() { + document.addEventListener('toggleSidebarRevealLabelsDropdown', this.handleCollapsedValueClick); + }, + beforeDestroy() { + document.removeEventListener( + 'toggleSidebarRevealLabelsDropdown', + this.handleCollapsedValueClick, + ); + }, methods: { handleDropdownClose(labels) { - this.$emit('updateSelectedLabels', labels); + if (this.iid !== '') { + this.updateSelectedLabels(this.getUpdateVariables(labels)); + } else { + this.$emit('updateSelectedLabels', { labels }); + } + this.collapseEditableItem(); }, collapseEditableItem() { this.$refs.editable?.collapse(); + if (this.sidebarExpandedOnClick) { + this.sidebarExpandedOnClick = false; + this.$emit('toggleCollapse'); + } }, handleCollapsedValueClick() { + this.sidebarExpandedOnClick = true; this.$emit('toggleCollapse'); + debounce(() => { + this.$refs.editable.toggle(); + this.$refs.dropdownContents.showDropdown(); + }, DEBOUNCE_DROPDOWN_DELAY)(); + }, + getUpdateVariables(labels) { + let labelIds = []; + + labelIds = labels.map(({ id }) => id); + const currentIid = this.oldIid || this.iid; + + const updateVariables = { + iid: currentIid, + projectPath: this.fullPath, + labelIds, + }; + + switch (this.issuableType) { + case IssuableType.Issue: + return updateVariables; + case IssuableType.MergeRequest: + return { + ...updateVariables, + operationMode: MutationOperationMode.Replace, + }; + case IssuableType.Epic: + return { + iid: currentIid, + groupPath: this.fullPath, + addLabelIds: labelIds.map((id) => getIdFromGraphQLId(id)), + removeLabelIds: this.issuableLabelIds + .filter((id) => !labelIds.includes(id)) + .map((id) => getIdFromGraphQLId(id)), + }; + default: + return {}; + } + }, + updateSelectedLabels(inputVariables) { + this.labelsSelectInProgress = true; + + this.$apollo + .mutate({ + mutation: issuableLabelsQueries[this.issuableType].mutation, + variables: { input: inputVariables }, + }) + .then(({ data }) => { + const { mutationName } = issuableLabelsQueries[this.issuableType]; + + if (data[mutationName]?.errors?.length) { + throw new Error(); + } + + this.$emit('updateSelectedLabels', { + id: data[mutationName]?.[this.issuableType]?.id, + labels: data[mutationName]?.[this.issuableType]?.labels?.nodes, + }); + }) + .catch((error) => + createFlash({ + message: __('An error occurred while updating labels.'), + captureError: true, + error, + }), + ) + .finally(() => { + this.labelsSelectInProgress = false; + }); + }, + getRemoveVariables(labelId) { + const removeVariables = { + iid: this.iid, + projectPath: this.fullPath, + }; + + switch (this.issuableType) { + case IssuableType.Issue: + return { + ...removeVariables, + removeLabelIds: [labelId], + }; + case IssuableType.MergeRequest: + return { + ...removeVariables, + labelIds: [labelId], + operationMode: MutationOperationMode.Remove, + }; + case IssuableType.Epic: + return { + iid: this.iid, + removeLabelIds: [getIdFromGraphQLId(labelId)], + groupPath: this.fullPath, + }; + default: + return {}; + } + }, + handleLabelRemove(labelId) { + this.updateSelectedLabels(this.getRemoveVariables(labelId)); + this.$emit('onLabelRemove', labelId); }, isDropdownVariantSidebar, isDropdownVariantStandalone, @@ -180,6 +305,7 @@ export default { :title="__('Labels')" :loading="isLoading" :can-edit="allowLabelEdit" + @open="oldIid = null" > <template #collapsed> <dropdown-value @@ -188,7 +314,7 @@ export default { :allow-label-remove="allowLabelRemove" :labels-filter-base-path="labelsFilterBasePath" :labels-filter-param="labelsFilterParam" - @onLabelRemove="$emit('onLabelRemove', $event)" + @onLabelRemove="handleLabelRemove" > <slot></slot> </dropdown-value> @@ -201,23 +327,25 @@ export default { :labels-filter-base-path="labelsFilterBasePath" :labels-filter-param="labelsFilterParam" class="gl-mb-2" - @onLabelRemove="$emit('onLabelRemove', $event)" + @onLabelRemove="handleLabelRemove" > <slot></slot> </dropdown-value> <dropdown-contents + ref="dropdownContents" :dropdown-button-text="dropdownButtonText" :allow-multiselect="allowMultiselect" :labels-list-title="labelsListTitle" :footer-create-label-title="footerCreateLabelTitle" :footer-manage-label-title="footerManageLabelTitle" :labels-create-title="labelsCreateTitle" - :selected-labels="selectedLabels" + :selected-labels="issuableLabels" :variant="variant" - :issuable-type="issuableType" :is-visible="edit" :full-path="fullPath" + :workspace-type="workspaceType" :attr-workspace-path="attrWorkspacePath" + :label-create-type="labelCreateType" @setLabels="handleDropdownClose" @closeDropdown="collapseEditableItem" /> @@ -233,10 +361,12 @@ export default { :footer-create-label-title="footerCreateLabelTitle" :footer-manage-label-title="footerManageLabelTitle" :labels-create-title="labelsCreateTitle" - :selected-labels="selectedLabels" + :selected-labels="issuableLabels" :variant="variant" - :issuable-type="issuableType" :full-path="fullPath" + :workspace-type="workspaceType" + :attr-workspace-path="attrWorkspacePath" + :label-create-type="labelCreateType" @setLabels="handleDropdownClose" /> </div> |