Commit 0e1b481c authored by Natalia Tepluhina's avatar Natalia Tepluhina Committed by Simon Knox

Fixed labels dropdown to work with embedded

Removed Vuex and skip query
Fixed fetching workspace labels
Moved selected labels to dropdown contents
Applies  onlyGroup filter for epics labels
Replaced project path with full path
parent cbb9ab43
...@@ -53,30 +53,32 @@ export default { ...@@ -53,30 +53,32 @@ export default {
handleDropdownClose() { handleDropdownClose() {
$(this.$el).trigger('hidden.gl.dropdown'); $(this.$el).trigger('hidden.gl.dropdown');
}, },
getUpdateVariables(dropdownLabels) { getUpdateVariables(labels) {
let labelIds = [];
if (this.glFeatures.labelsWidget) {
labelIds = labels.map(({ id }) => toLabelGid(id));
} else {
const currentLabelIds = this.selectedLabels.map((label) => label.id); const currentLabelIds = this.selectedLabels.map((label) => label.id);
const dropdownLabelIds = dropdownLabels.map((label) => label.id); const userAddedLabelIds = labels.filter((label) => label.set).map((label) => label.id);
const userAddedLabelIds = this.glFeatures.labelsWidget const userRemovedLabelIds = labels.filter((label) => !label.set).map((label) => label.id);
? difference(dropdownLabelIds, currentLabelIds)
: dropdownLabels.filter((label) => label.set).map((label) => label.id);
const userRemovedLabelIds = this.glFeatures.labelsWidget
? difference(currentLabelIds, dropdownLabelIds)
: dropdownLabels.filter((label) => !label.set).map((label) => label.id);
const labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds); labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds).map(
toLabelGid,
);
}
switch (this.issuableType) { switch (this.issuableType) {
case IssuableType.Issue: case IssuableType.Issue:
return { return {
addLabelIds: userAddedLabelIds,
iid: this.iid, iid: this.iid,
projectPath: this.projectPath, projectPath: this.projectPath,
removeLabelIds: userRemovedLabelIds, labelIds,
}; };
case IssuableType.MergeRequest: case IssuableType.MergeRequest:
return { return {
iid: this.iid, iid: this.iid,
labelIds: labelIds.map(toLabelGid), labelIds,
operationMode: MutationOperationMode.Replace, operationMode: MutationOperationMode.Replace,
projectPath: this.projectPath, projectPath: this.projectPath,
}; };
...@@ -152,8 +154,8 @@ export default { ...@@ -152,8 +154,8 @@ export default {
:labels-select-in-progress="isLabelsSelectInProgress" :labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
:variant="$options.variant" :variant="$options.variant"
:issuable-type="issuableType"
data-qa-selector="labels_block" data-qa-selector="labels_block"
@onDropdownClose="handleDropdownClose"
@onLabelRemove="handleLabelRemove" @onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels" @updateSelectedLabels="handleUpdateSelectedLabels"
> >
......
...@@ -31,6 +31,10 @@ import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subs ...@@ -31,6 +31,10 @@ import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subs
import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql'; import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql'; import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql'; import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql';
import groupLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql'; import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql'; import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
...@@ -105,6 +109,17 @@ export const referenceQueries = { ...@@ -105,6 +109,17 @@ export const referenceQueries = {
}, },
}; };
export const labelsQueries = {
[IssuableType.Issue]: {
issuableQuery: issueLabelsQuery,
workspaceQuery: projectLabelsQuery,
},
[IssuableType.Epic]: {
issuableQuery: epicLabelsQuery,
workspaceQuery: groupLabelsQuery,
},
};
export const dateTypes = { export const dateTypes = {
start: 'startDate', start: 'startDate',
due: 'dueDate', due: 'dueDate',
......
...@@ -241,6 +241,7 @@ function mountMilestoneSelect() { ...@@ -241,6 +241,7 @@ function mountMilestoneSelect() {
export function mountSidebarLabels() { export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels'); const el = document.querySelector('.js-sidebar-labels');
const { fullPath } = getSidebarOptions();
if (!el) { if (!el) {
return false; return false;
...@@ -251,6 +252,7 @@ export function mountSidebarLabels() { ...@@ -251,6 +252,7 @@ export function mountSidebarLabels() {
apolloProvider, apolloProvider,
provide: { provide: {
...el.dataset, ...el.dataset,
fullPath,
allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate), allowLabelCreate: parseBoolean(el.dataset.allowLabelCreate),
allowLabelEdit: parseBoolean(el.dataset.canEdit), allowLabelEdit: parseBoolean(el.dataset.canEdit),
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels), allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
......
<script> <script>
import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui'; import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue'; import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue'; import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils'; import { isDropdownVariantStandalone } from './utils';
export default { export default {
components: { components: {
...@@ -48,10 +48,15 @@ export default { ...@@ -48,10 +48,15 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
issuableType: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
showDropdownContentsCreateView: false, showDropdownContentsCreateView: false,
localSelectedLabels: [...this.selectedLabels],
}; };
}, },
computed: { computed: {
...@@ -64,28 +69,42 @@ export default { ...@@ -64,28 +69,42 @@ export default {
dropdownTitle() { dropdownTitle() {
return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle; return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
}, },
buttonText() {
if (!this.localSelectedLabels.length) {
return this.dropdownButtonText || __('Label');
} else if (this.localSelectedLabels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: this.localSelectedLabels[0].title,
remainingLabelCount: this.localSelectedLabels.length - 1,
});
}
return this.localSelectedLabels[0].title;
},
showDropdownFooter() { showDropdownFooter() {
return ( return !this.showDropdownContentsCreateView && !this.isStandalone;
!this.showDropdownContentsCreateView &&
(this.isDropdownVariantSidebar(this.variant) ||
this.isDropdownVariantEmbedded(this.variant))
);
}, },
isStandalone() {
return isDropdownVariantStandalone(this.variant);
}, },
methods: { },
showDropdown() { mounted() {
this.$refs.dropdown.show(); this.$refs.dropdown.show();
}, },
methods: {
toggleDropdownContentsCreateView() { toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView; this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
}, },
toggleDropdownContent() { toggleDropdownContent() {
this.toggleDropdownContentsCreateView(); this.toggleDropdownContentsCreateView();
// Required to recalculate dropdown position as its size changes // Required to recalculate dropdown position as its size changes
if (this.$refs.dropdown?.$refs.dropdown) {
this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate(); this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
}
},
closeDropdown() {
this.$emit('setLabels', this.localSelectedLabels);
this.$refs.dropdown.hide();
}, },
isDropdownVariantSidebar,
isDropdownVariantEmbedded,
}, },
}; };
</script> </script>
...@@ -93,14 +112,16 @@ export default { ...@@ -93,14 +112,16 @@ export default {
<template> <template>
<gl-dropdown <gl-dropdown
ref="dropdown" ref="dropdown"
:text="dropdownButtonText" :text="buttonText"
class="gl-w-full gl-mt-2" class="gl-w-full gl-mt-2"
data-qa-selector="labels_dropdown_content" data-qa-selector="labels_dropdown_content"
@hide="$emit('setLabels', localSelectedLabels)"
> >
<template #header> <template #header>
<div <div
v-if="isDropdownVariantSidebar(variant) || isDropdownVariantEmbedded(variant)" v-if="!isStandalone"
class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!" class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
data-testid="dropdown-header"
> >
<gl-button <gl-button
v-if="showDropdownContentsCreateView" v-if="showDropdownContentsCreateView"
...@@ -119,27 +140,31 @@ export default { ...@@ -119,27 +140,31 @@ export default {
size="small" size="small"
class="dropdown-header-button gl-p-0!" class="dropdown-header-button gl-p-0!"
icon="close" icon="close"
@click="$emit('closeDropdown')" data-testid="close-button"
@click="closeDropdown"
/> />
</div> </div>
</template> </template>
<template #default>
<component <component
:is="dropdownContentsView" :is="dropdownContentsView"
v-model="localSelectedLabels"
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect" :allow-multiselect="allowMultiselect"
:issuable-type="issuableType"
@hideCreateView="toggleDropdownContentsCreateView" @hideCreateView="toggleDropdownContentsCreateView"
@setLabels="$emit('setLabels', $event)"
/> />
</template>
<template #footer> <template #footer>
<div v-if="showDropdownFooter" data-testid="dropdown-footer"> <div v-if="showDropdownFooter" data-testid="dropdown-footer">
<gl-dropdown-item <gl-dropdown-item
v-if="allowLabelCreate" v-if="allowLabelCreate"
data-testid="create-label-button" data-testid="create-label-button"
@click.native.capture.stop="toggleDropdownContent" @click.capture.native.stop="toggleDropdownContent"
> >
{{ footerCreateLabelTitle }} {{ footerCreateLabelTitle }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item :href="labelsManagePath" @click.native.capture.stop> <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
{{ footerManageLabelTitle }} {{ footerManageLabelTitle }}
</gl-dropdown-item> </gl-dropdown-item>
</div> </div>
......
...@@ -2,9 +2,10 @@ ...@@ -2,9 +2,10 @@
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer'; import produce from 'immer';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { labelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql'; import createLabelMutation from './graphql/create_label.mutation.graphql';
import projectLabelsQuery from './graphql/project_labels.query.graphql';
const errorMessage = __('Error creating label.'); const errorMessage = __('Error creating label.');
...@@ -19,10 +20,16 @@ export default { ...@@ -19,10 +20,16 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
inject: { inject: {
projectPath: { fullPath: {
default: '', default: '',
}, },
}, },
props: {
issuableType: {
type: String,
required: true,
},
},
data() { data() {
return { return {
labelTitle: '', labelTitle: '',
...@@ -38,6 +45,19 @@ export default { ...@@ -38,6 +45,19 @@ export default {
const colorsMap = gon.suggested_label_colors; const colorsMap = gon.suggested_label_colors;
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] })); return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
}, },
mutationVariables() {
return this.issuableType === IssuableType.Epic
? {
title: this.labelTitle,
color: this.selectedColor,
groupPath: this.fullPath,
}
: {
title: this.labelTitle,
color: this.selectedColor,
projectPath: this.fullPath,
};
},
}, },
methods: { methods: {
getColorCode(color) { getColorCode(color) {
...@@ -51,8 +71,8 @@ export default { ...@@ -51,8 +71,8 @@ export default {
}, },
updateLabelsInCache(store, label) { updateLabelsInCache(store, label) {
const sourceData = store.readQuery({ const sourceData = store.readQuery({
query: projectLabelsQuery, query: labelsQueries[this.issuableType].workspaceQuery,
variables: { fullPath: this.projectPath, searchTerm: '' }, variables: { fullPath: this.fullPath, searchTerm: '' },
}); });
const collator = new Intl.Collator('en'); const collator = new Intl.Collator('en');
...@@ -63,8 +83,8 @@ export default { ...@@ -63,8 +83,8 @@ export default {
}); });
store.writeQuery({ store.writeQuery({
query: projectLabelsQuery, query: labelsQueries[this.issuableType].workspaceQuery,
variables: { fullPath: this.projectPath, searchTerm: '' }, variables: { fullPath: this.fullPath, searchTerm: '' },
data, data,
}); });
}, },
...@@ -75,11 +95,7 @@ export default { ...@@ -75,11 +95,7 @@ export default {
data: { labelCreate }, data: { labelCreate },
} = await this.$apollo.mutate({ } = await this.$apollo.mutate({
mutation: createLabelMutation, mutation: createLabelMutation,
variables: { variables: this.mutationVariables,
title: this.labelTitle,
color: this.selectedColor,
projectPath: this.projectPath,
},
update: ( update: (
store, store,
{ {
......
<script> <script>
import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; import {
GlDropdownForm,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlIntersectionObserver,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale'; import { __ } from '~/locale';
import projectLabelsQuery from './graphql/project_labels.query.graphql'; import { labelsQueries } from '~/sidebar/constants';
import LabelItem from './label_item.vue'; import LabelItem from './label_item.vue';
export default { export default {
...@@ -15,9 +21,13 @@ export default { ...@@ -15,9 +21,13 @@ export default {
GlDropdownItem, GlDropdownItem,
GlLoadingIcon, GlLoadingIcon,
GlSearchBoxByType, GlSearchBoxByType,
GlIntersectionObserver,
LabelItem, LabelItem,
}, },
inject: ['projectPath'], inject: ['fullPath'],
model: {
prop: 'localSelectedLabels',
},
props: { props: {
selectedLabels: { selectedLabels: {
type: Array, type: Array,
...@@ -27,20 +37,29 @@ export default { ...@@ -27,20 +37,29 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
issuableType: {
type: String,
required: true,
},
localSelectedLabels: {
type: Array,
required: true,
},
}, },
data() { data() {
return { return {
searchKey: '', searchKey: '',
labels: [], labels: [],
localSelectedLabels: [...this.selectedLabels],
}; };
}, },
apollo: { apollo: {
labels: { labels: {
query: projectLabelsQuery, query() {
return labelsQueries[this.issuableType].workspaceQuery;
},
variables() { variables() {
return { return {
fullPath: this.projectPath, fullPath: this.fullPath,
searchTerm: this.searchKey, searchTerm: this.searchKey,
}; };
}, },
...@@ -50,8 +69,8 @@ export default { ...@@ -50,8 +69,8 @@ export default {
update: (data) => data.workspace?.labels?.nodes || [], update: (data) => data.workspace?.labels?.nodes || [],
async result() { async result() {
if (this.$refs.searchInput) { if (this.$refs.searchInput) {
await this.$nextTick(); await this.$nextTick;
this.$refs.searchInput.focusInput(); this.focusInputField();
} }
}, },
error() { error() {
...@@ -82,7 +101,6 @@ export default { ...@@ -82,7 +101,6 @@ export default {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
}, },
beforeDestroy() { beforeDestroy() {
this.$emit('setLabels', this.localSelectedLabels);
this.debouncedSearchKeyUpdate.cancel(); this.debouncedSearchKeyUpdate.cancel();
}, },
methods: { methods: {
...@@ -109,16 +127,19 @@ export default { ...@@ -109,16 +127,19 @@ export default {
} }
}, },
updateSelectedLabels(label) { updateSelectedLabels(label) {
let labels;
if (this.isLabelSelected(label)) { if (this.isLabelSelected(label)) {
this.localSelectedLabels = this.localSelectedLabels.filter( labels = this.localSelectedLabels.filter(({ id }) => id !== getIdFromGraphQLId(label.id));
({ id }) => id !== getIdFromGraphQLId(label.id),
);
} else { } else {
this.localSelectedLabels.push({ labels = [
...this.localSelectedLabels,
{
...label, ...label,
id: getIdFromGraphQLId(label.id), id: getIdFromGraphQLId(label.id),
}); },
];
} }
this.$emit('input', labels);
}, },
handleLabelClick(label) { handleLabelClick(label) {
this.updateSelectedLabels(label); this.updateSelectedLabels(label);
...@@ -129,11 +150,15 @@ export default { ...@@ -129,11 +150,15 @@ export default {
setSearchKey(value) { setSearchKey(value) {
this.searchKey = value; this.searchKey = value;
}, },
focusInputField() {
this.$refs.searchInput.focusInput();
},
}, },
}; };
</script> </script>
<template> <template>
<gl-intersection-observer @appear="focusInputField">
<gl-dropdown-form class="labels-select-contents-list js-labels-list"> <gl-dropdown-form class="labels-select-contents-list js-labels-list">
<gl-search-box-by-type <gl-search-box-by-type
ref="searchInput" ref="searchInput"
...@@ -146,7 +171,7 @@ export default { ...@@ -146,7 +171,7 @@ export default {
<div ref="labelsListContainer" data-testid="dropdown-content"> <div ref="labelsListContainer" data-testid="dropdown-content">
<gl-loading-icon <gl-loading-icon
v-if="labelsFetchInProgress" v-if="labelsFetchInProgress"
class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full" class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full gl-mb-3"
size="md" size="md"
/> />
<template v-else> <template v-else>
...@@ -171,4 +196,5 @@ export default { ...@@ -171,4 +196,5 @@ export default {
</template> </template>
</div> </div>
</gl-dropdown-form> </gl-dropdown-form>
</gl-intersection-observer>
</template> </template>
query epicLabels($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
issuable: epic(iid: $iid) {
id
labels {
nodes {
id
title
color
description
}
}
}
}
}
query groupLabels($fullPath: ID!, $searchTerm: String) {
workspace: group(fullPath: $fullPath) {
labels(searchTerm: $searchTerm, onlyGroupLabels: true) {
nodes {
id
title
color
description
}
}
}
}
<script> <script>
import Vue from 'vue'; import createFlash from '~/flash';
import Vuex from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { labelsQueries } from '~/sidebar/constants';
import { DropdownVariant } from './constants'; import { DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue'; import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import issueLabelsQuery from './graphql/issue_labels.query.graphql';
import { import {
isDropdownVariantSidebar, isDropdownVariantSidebar,
isDropdownVariantStandalone, isDropdownVariantStandalone,
isDropdownVariantEmbedded, isDropdownVariantEmbedded,
} from './utils'; } from './utils';
Vue.use(Vuex);
export default { export default {
components: { components: {
DropdownValue, DropdownValue,
...@@ -23,7 +20,15 @@ export default { ...@@ -23,7 +20,15 @@ export default {
DropdownValueCollapsed, DropdownValueCollapsed,
SidebarEditableItem, SidebarEditableItem,
}, },
inject: ['iid', 'projectPath', 'allowLabelEdit'], inject: {
iid: {
default: '',
},
allowLabelEdit: {
default: false,
},
fullPath: {},
},
props: { props: {
allowLabelRemove: { allowLabelRemove: {
type: Boolean, type: Boolean,
...@@ -90,43 +95,52 @@ export default { ...@@ -90,43 +95,52 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
issuableType: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
contentIsOnViewport: true, contentIsOnViewport: true,
issueLabels: [], issuableLabels: [],
}; };
}, },
computed: {
isLoading() {
return this.labelsSelectInProgress || this.$apollo.queries.issuableLabels.loading;
},
},
apollo: { apollo: {
issueLabels: { issuableLabels: {
query: issueLabelsQuery, query() {
return labelsQueries[this.issuableType].issuableQuery;
},
skip() {
return !isDropdownVariantSidebar(this.variant);
},
variables() { variables() {
return { return {
iid: this.iid, iid: this.iid,
fullPath: this.projectPath, fullPath: this.fullPath,
}; };
}, },
update(data) { update(data) {
return data.workspace?.issuable?.labels.nodes || []; return data.workspace?.issuable?.labels.nodes || [];
}, },
error() {
createFlash({ message: __('Error fetching labels.') });
},
}, },
}, },
methods: { methods: {
handleDropdownClose(labels) { handleDropdownClose(labels) {
if (labels.length) this.$emit('updateSelectedLabels', labels); this.$emit('updateSelectedLabels', labels);
this.$emit('onDropdownClose'); this.$refs.editable?.collapse();
},
collapseDropdown() {
this.$refs.editable.collapse();
}, },
handleCollapsedValueClick() { handleCollapsedValueClick() {
this.$emit('toggleCollapse'); this.$emit('toggleCollapse');
}, },
showDropdown() {
this.$nextTick(() => {
this.$refs.dropdownContents.showDropdown();
});
},
isDropdownVariantSidebar, isDropdownVariantSidebar,
isDropdownVariantStandalone, isDropdownVariantStandalone,
isDropdownVariantEmbedded, isDropdownVariantEmbedded,
...@@ -145,20 +159,19 @@ export default { ...@@ -145,20 +159,19 @@ export default {
<template v-if="isDropdownVariantSidebar(variant)"> <template v-if="isDropdownVariantSidebar(variant)">
<dropdown-value-collapsed <dropdown-value-collapsed
ref="dropdownButtonCollapsed" ref="dropdownButtonCollapsed"
:labels="issueLabels" :labels="issuableLabels"
@onValueClick="handleCollapsedValueClick" @onValueClick="handleCollapsedValueClick"
/> />
<sidebar-editable-item <sidebar-editable-item
ref="editable" ref="editable"
:title="__('Labels')" :title="__('Labels')"
:loading="labelsSelectInProgress" :loading="isLoading"
:can-edit="allowLabelEdit" :can-edit="allowLabelEdit"
@open="showDropdown"
> >
<template #collapsed> <template #collapsed>
<dropdown-value <dropdown-value
:disable-labels="labelsSelectInProgress" :disable-labels="labelsSelectInProgress"
:selected-labels="issueLabels" :selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove" :allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath" :labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam" :labels-filter-param="labelsFilterParam"
...@@ -170,7 +183,7 @@ export default { ...@@ -170,7 +183,7 @@ export default {
<template #default="{ edit }"> <template #default="{ edit }">
<dropdown-value <dropdown-value
:disable-labels="labelsSelectInProgress" :disable-labels="labelsSelectInProgress"
:selected-labels="issueLabels" :selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove" :allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath" :labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam" :labels-filter-param="labelsFilterParam"
...@@ -181,7 +194,6 @@ export default { ...@@ -181,7 +194,6 @@ export default {
</dropdown-value> </dropdown-value>
<dropdown-contents <dropdown-contents
v-if="edit" v-if="edit"
ref="dropdownContents"
:dropdown-button-text="dropdownButtonText" :dropdown-button-text="dropdownButtonText"
:allow-multiselect="allowMultiselect" :allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle" :labels-list-title="labelsListTitle"
...@@ -190,11 +202,25 @@ export default { ...@@ -190,11 +202,25 @@ export default {
:labels-create-title="labelsCreateTitle" :labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels" :selected-labels="selectedLabels"
:variant="variant" :variant="variant"
@closeDropdown="collapseDropdown" :issuable-type="issuableType"
@setLabels="handleDropdownClose" @setLabels="handleDropdownClose"
/> />
</template> </template>
</sidebar-editable-item> </sidebar-editable-item>
</template> </template>
<dropdown-contents
v-else
ref="dropdownContents"
:allow-multiselect="allowMultiselect"
:dropdown-button-text="dropdownButtonText"
:labels-list-title="labelsListTitle"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels"
:variant="variant"
:issuable-type="issuableType"
@setLabels="handleDropdownClose"
/>
</div> </div>
</template> </template>
...@@ -13,6 +13,8 @@ import { visitUrl } from '~/lib/utils/url_utility'; ...@@ -13,6 +13,8 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import LabelsSelectVue from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelectVue from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createEpic from '../queries/createEpic.mutation.graphql'; import createEpic from '../queries/createEpic.mutation.graphql';
export default { export default {
...@@ -25,7 +27,9 @@ export default { ...@@ -25,7 +27,9 @@ export default {
GlFormGroup, GlFormGroup,
MarkdownField, MarkdownField,
LabelsSelectVue, LabelsSelectVue,
LabelsSelectWidget,
}, },
mixins: [glFeatureFlagMixin()],
inject: [ inject: [
'groupPath', 'groupPath',
'groupEpicsPath', 'groupEpicsPath',
...@@ -106,6 +110,11 @@ export default { ...@@ -106,6 +110,11 @@ export default {
this.startDateFixed = val; this.startDateFixed = val;
}, },
handleUpdateSelectedLabels(labels) { handleUpdateSelectedLabels(labels) {
if (this.glFeatures.labelsWidget) {
this.labels = labels;
return;
}
const ids = []; const ids = [];
const allLabels = [...labels, ...this.labels]; const allLabels = [...labels, ...this.labels];
...@@ -177,7 +186,23 @@ export default { ...@@ -177,7 +186,23 @@ export default {
</gl-form-group> </gl-form-group>
<hr /> <hr />
<gl-form-group :label="__('Labels')"> <gl-form-group :label="__('Labels')">
<labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block"
:allow-label-create="true"
:allow-multiselect="true"
:allow-scoped-labels="false"
:labels-filter-base-path="groupEpicsPath"
:selected-labels="labels"
issuable-type="epic"
variant="embedded"
data-qa-selector="labels_block"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</labels-select-widget>
<labels-select-vue <labels-select-vue
v-else
:allow-label-edit="false" :allow-label-edit="false"
:allow-label-create="true" :allow-label-create="true"
:allow-multiselect="true" :allow-multiselect="true"
......
...@@ -35,6 +35,8 @@ export function initEpicForm() { ...@@ -35,6 +35,8 @@ export function initEpicForm() {
apolloProvider, apolloProvider,
provide: { provide: {
groupPath, groupPath,
fullPath: groupPath,
allowLabelCreate: true,
groupEpicsPath, groupEpicsPath,
labelsFetchPath, labelsFetchPath,
labelsManagePath, labelsManagePath,
......
...@@ -23,6 +23,7 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -23,6 +23,7 @@ class Groups::EpicsController < Groups::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:vue_epics_list, @group, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:vue_epics_list, @group, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:improved_emoji_picker, @group, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:improved_emoji_picker, @group, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, @group, default_enabled: :yaml)
end end
feature_category :epics feature_category :epics
......
...@@ -110,10 +110,9 @@ describe('sidebar labels', () => { ...@@ -110,10 +110,9 @@ describe('sidebar labels', () => {
mutation: updateIssueLabelsMutation, mutation: updateIssueLabelsMutation,
variables: { variables: {
input: { input: {
addLabelIds: [40],
iid: defaultProps.iid, iid: defaultProps.iid,
projectPath: defaultProps.projectPath, projectPath: defaultProps.projectPath,
removeLabelIds: [26, 55], labelIds: [toLabelGid(29), toLabelGid(28), toLabelGid(27), toLabelGid(40)],
}, },
}, },
}; };
......
...@@ -5,13 +5,14 @@ import VueApollo from 'vue-apollo'; ...@@ -5,13 +5,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { labelsQueries } from '~/sidebar/constants';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql'; import createLabelMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import { import {
mockSuggestedColors, mockSuggestedColors,
createLabelSuccessfulResponse, createLabelSuccessfulResponse,
labelsQueryResponse, workspaceLabelsQueryResponse,
} from './mock_data'; } from './mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -47,11 +48,14 @@ describe('DropdownContentsCreateView', () => { ...@@ -47,11 +48,14 @@ describe('DropdownContentsCreateView', () => {
findAllColors().at(0).vm.$emit('click', new Event('mouseclick')); findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
}; };
const createComponent = ({ mutationHandler = createLabelSuccessHandler } = {}) => { const createComponent = ({
mutationHandler = createLabelSuccessHandler,
issuableType = IssuableType.Issue,
} = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]); const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({ mockApollo.clients.defaultClient.cache.writeQuery({
query: projectLabelsQuery, query: labelsQueries[issuableType].workspaceQuery,
data: labelsQueryResponse.data, data: workspaceLabelsQueryResponse.data,
variables: { variables: {
fullPath: '', fullPath: '',
searchTerm: '', searchTerm: '',
...@@ -61,6 +65,9 @@ describe('DropdownContentsCreateView', () => { ...@@ -61,6 +65,9 @@ describe('DropdownContentsCreateView', () => {
wrapper = shallowMount(DropdownContentsCreateView, { wrapper = shallowMount(DropdownContentsCreateView, {
localVue, localVue,
apolloProvider: mockApollo, apolloProvider: mockApollo,
propsData: {
issuableType,
},
}); });
}; };
...@@ -135,15 +142,6 @@ describe('DropdownContentsCreateView', () => { ...@@ -135,15 +142,6 @@ describe('DropdownContentsCreateView', () => {
expect(findCreateButton().props('disabled')).toBe(false); expect(findCreateButton().props('disabled')).toBe(false);
}); });
it('calls a mutation with correct parameters on Create button click', () => {
findCreateButton().vm.$emit('click');
expect(createLabelSuccessHandler).toHaveBeenCalledWith({
color: '#009966',
projectPath: '',
title: 'Test title',
});
});
it('renders a loader spinner after Create button click', async () => { it('renders a loader spinner after Create button click', async () => {
findCreateButton().vm.$emit('click'); findCreateButton().vm.$emit('click');
await nextTick(); await nextTick();
...@@ -162,6 +160,30 @@ describe('DropdownContentsCreateView', () => { ...@@ -162,6 +160,30 @@ describe('DropdownContentsCreateView', () => {
}); });
}); });
it('calls a mutation with `projectPath` variable on the issue', () => {
createComponent();
fillLabelAttributes();
findCreateButton().vm.$emit('click');
expect(createLabelSuccessHandler).toHaveBeenCalledWith({
color: '#009966',
projectPath: '',
title: 'Test title',
});
});
it('calls a mutation with `groupPath` variable on the epic', () => {
createComponent({ issuableType: IssuableType.Epic });
fillLabelAttributes();
findCreateButton().vm.$emit('click');
expect(createLabelSuccessHandler).toHaveBeenCalledWith({
color: '#009966',
groupPath: '',
title: 'Test title',
});
});
it('calls createFlash is mutation has a user-recoverable error', async () => { it('calls createFlash is mutation has a user-recoverable error', async () => {
createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler }); createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler });
fillLabelAttributes(); fillLabelAttributes();
......
import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue'; import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql'; import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
import { mockConfig, labelsQueryResponse } from './mock_data'; import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
const selectedLabels = [ const localSelectedLabels = [
{ {
id: 28, color: '#2f7b2e',
title: 'Bug', description: null,
description: 'Label for bugs', id: 'gid://gitlab/ProjectLabel/2',
color: '#FF0000', title: 'Label2',
textColor: '#FFFFFF',
}, },
]; ];
describe('DropdownContentsLabelsView', () => { describe('DropdownContentsLabelsView', () => {
let wrapper; let wrapper;
const successfulQueryHandler = jest.fn().mockResolvedValue(labelsQueryResponse); const successfulQueryHandler = jest.fn().mockResolvedValue(workspaceLabelsQueryResponse);
const findFirstLabel = () => wrapper.findAllComponents(GlDropdownItem).at(0);
const createComponent = ({ const createComponent = ({
initialState = mockConfig, initialState = mockConfig,
...@@ -43,14 +45,15 @@ describe('DropdownContentsLabelsView', () => { ...@@ -43,14 +45,15 @@ describe('DropdownContentsLabelsView', () => {
localVue, localVue,
apolloProvider: mockApollo, apolloProvider: mockApollo,
provide: { provide: {
projectPath: 'test', fullPath: 'test',
iid: 1, iid: 1,
variant: DropdownVariant.Sidebar, variant: DropdownVariant.Sidebar,
...injected, ...injected,
}, },
propsData: { propsData: {
...initialState, ...initialState,
selectedLabels, localSelectedLabels,
issuableType: IssuableType.Issue,
}, },
stubs: { stubs: {
GlSearchBoxByType, GlSearchBoxByType,
...@@ -129,6 +132,15 @@ describe('DropdownContentsLabelsView', () => { ...@@ -129,6 +132,15 @@ describe('DropdownContentsLabelsView', () => {
createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') }); createComponent({ queryHandler: jest.fn().mockRejectedValue('Houston, we have a problem!') });
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises(); await waitForPromises();
expect(createFlash).toHaveBeenCalled(); expect(createFlash).toHaveBeenCalled();
}); });
it('emits an `input` event on label click', async () => {
createComponent();
await waitForPromises();
findFirstLabel().trigger('click');
expect(wrapper.emitted('input')[0][0]).toEqual(expect.arrayContaining(localSelectedLabels));
});
}); });
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue'; import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
...@@ -8,10 +7,25 @@ import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_s ...@@ -8,10 +7,25 @@ import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_s
import { mockLabels } from './mock_data'; import { mockLabels } from './mock_data';
const showDropdown = jest.fn();
const GlDropdownStub = {
template: `
<div data-testid="dropdown">
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
`,
methods: {
show: showDropdown,
},
};
describe('DropdownContent', () => { describe('DropdownContent', () => {
let wrapper; let wrapper;
const createComponent = ({ props = {}, injected = {} } = {}) => { const createComponent = ({ props = {}, injected = {}, data = {} } = {}) => {
wrapper = shallowMount(DropdownContents, { wrapper = shallowMount(DropdownContents, {
propsData: { propsData: {
labelsCreateTitle: 'test', labelsCreateTitle: 'test',
...@@ -22,38 +36,76 @@ describe('DropdownContent', () => { ...@@ -22,38 +36,76 @@ describe('DropdownContent', () => {
footerManageLabelTitle: 'manage', footerManageLabelTitle: 'manage',
dropdownButtonText: 'Labels', dropdownButtonText: 'Labels',
variant: 'sidebar', variant: 'sidebar',
issuableType: 'issue',
...props, ...props,
}, },
data() {
return {
...data,
};
},
provide: { provide: {
allowLabelCreate: true, allowLabelCreate: true,
labelsManagePath: 'foo/bar', labelsManagePath: 'foo/bar',
...injected, ...injected,
}, },
stubs: { stubs: {
GlDropdown, GlDropdown: GlDropdownStub,
}, },
}); });
}; };
beforeEach(() => {
createComponent();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
const findDropdown = () => wrapper.findComponent(GlDropdownStub);
const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]'); const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
const findDropdownHeader = () => wrapper.find('[data-testid="dropdown-header"]');
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]'); const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]'); const findGoBackButton = () => wrapper.find('[data-testid="go-back-button"]');
it('calls dropdown `show` method on component mount', () => {
createComponent();
expect(showDropdown).toHaveBeenCalled();
});
it('emits `setLabels` event on dropdown hide', () => {
createComponent();
findDropdown().vm.$emit('hide');
expect(wrapper.emitted('setLabels')).toEqual([[mockLabels]]);
});
it('does not render header on standalone variant', () => {
createComponent({ props: { variant: DropdownVariant.Standalone } });
expect(findDropdownHeader().exists()).toBe(false);
});
it('renders header on embedded variant', () => {
createComponent({ props: { variant: DropdownVariant.Embedded } });
expect(findDropdownHeader().exists()).toBe(true);
});
it('renders header on sidebar variant', () => {
createComponent();
expect(findDropdownHeader().exists()).toBe(true);
});
describe('Create view', () => { describe('Create view', () => {
beforeEach(() => { beforeEach(() => {
wrapper.vm.toggleDropdownContentsCreateView(); createComponent({ data: { showDropdownContentsCreateView: true } });
}); });
it('renders create view when `showDropdownContentsCreateView` prop is `true`', () => { it('renders create view when `showDropdownContentsCreateView` prop is `true`', () => {
expect(wrapper.findComponent(DropdownContentsCreateView).exists()).toBe(true); expect(findCreateView().exists()).toBe(true);
}); });
it('does not render footer', () => { it('does not render footer', () => {
...@@ -67,11 +119,31 @@ describe('DropdownContent', () => { ...@@ -67,11 +119,31 @@ describe('DropdownContent', () => {
it('renders go back button', () => { it('renders go back button', () => {
expect(findGoBackButton().exists()).toBe(true); expect(findGoBackButton().exists()).toBe(true);
}); });
it('changes the view to Labels view on back button click', async () => {
findGoBackButton().vm.$emit('click', new MouseEvent('click'));
await nextTick();
expect(findCreateView().exists()).toBe(false);
expect(findLabelsView().exists()).toBe(true);
});
it('changes the view to Labels view on `hideCreateView` event', async () => {
findCreateView().vm.$emit('hideCreateView');
await nextTick();
expect(findCreateView().exists()).toBe(false);
expect(findLabelsView().exists()).toBe(true);
});
}); });
describe('Labels view', () => { describe('Labels view', () => {
beforeEach(() => {
createComponent();
});
it('renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`', () => { it('renders labels view when `showDropdownContentsCreateView` when `showDropdownContentsCreateView` prop is `false`', () => {
expect(wrapper.findComponent(DropdownContentsLabelsView).exists()).toBe(true); expect(findLabelsView().exists()).toBe(true);
}); });
it('renders footer on sidebar dropdown', () => { it('renders footer on sidebar dropdown', () => {
...@@ -109,19 +181,12 @@ describe('DropdownContent', () => { ...@@ -109,19 +181,12 @@ describe('DropdownContent', () => {
expect(findCreateLabelButton().exists()).toBe(true); expect(findCreateLabelButton().exists()).toBe(true);
}); });
it('triggers `toggleDropdownContent` method on create label button click', () => { it('changes the view to Create on create label button click', async () => {
jest.spyOn(wrapper.vm, 'toggleDropdownContent').mockImplementation(() => {});
findCreateLabelButton().trigger('click'); findCreateLabelButton().trigger('click');
expect(wrapper.vm.toggleDropdownContent).toHaveBeenCalled(); await nextTick();
}); expect(findLabelsView().exists()).toBe(false);
}); });
}); });
describe('template', () => {
it('renders component container element with classes `gl-w-full gl-mt-2` and no styles', () => {
expect(wrapper.attributes('class')).toContain('gl-w-full gl-mt-2');
expect(wrapper.attributes('style')).toBeUndefined();
});
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue'; import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue'; import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { mockConfig, issuableLabelsQueryResponse } from './mock_data';
import { mockConfig } from './mock_data'; jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse);
const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
describe('LabelsSelectRoot', () => { describe('LabelsSelectRoot', () => {
let wrapper; let wrapper;
const createComponent = (config = mockConfig, slots = {}) => { const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findDropdownValue = () => wrapper.findComponent(DropdownValue);
const findDropdownContents = () => wrapper.findComponent(DropdownContents);
const expandDropdown = () => wrapper.vm.$refs.editable.expand();
const createComponent = ({
config = mockConfig,
slots = {},
queryHandler = successfulQueryHandler,
} = {}) => {
const mockApollo = createMockApollo([[issueLabelsQuery, queryHandler]]);
wrapper = shallowMount(LabelsSelectRoot, { wrapper = shallowMount(LabelsSelectRoot, {
slots, slots,
propsData: config, apolloProvider: mockApollo,
localVue,
propsData: {
...config,
issuableType: IssuableType.Issue,
},
stubs: { stubs: {
DropdownContents,
SidebarEditableItem, SidebarEditableItem,
}, },
provide: { provide: {
iid: '1', iid: '1',
projectPath: 'test', fullPath: 'test',
canUpdate: true, canUpdate: true,
allowLabelEdit: true, allowLabelEdit: true,
allowLabelCreate: true,
labelsManagePath: 'test',
}, },
}); });
}; };
...@@ -42,33 +73,67 @@ describe('LabelsSelectRoot', () => { ...@@ -42,33 +73,67 @@ describe('LabelsSelectRoot', () => {
${'embedded'} | ${'is-embedded'} ${'embedded'} | ${'is-embedded'}
`( `(
'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"', 'renders component root element with CSS class `$cssClass` when `state.variant` is "$variant"',
({ variant, cssClass }) => { async ({ variant, cssClass }) => {
createComponent({ createComponent({
...mockConfig, config: { ...mockConfig, variant },
variant,
}); });
return wrapper.vm.$nextTick(() => { await nextTick();
expect(wrapper.classes()).toContain(cssClass); expect(wrapper.classes()).toContain(cssClass);
});
}, },
); );
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', async () => { describe('if dropdown variant is `sidebar`', () => {
it('renders sidebar editable item', () => {
createComponent();
expect(findSidebarEditableItem().exists()).toBe(true);
});
it('passes true `loading` prop to sidebar editable item when loading labels', () => {
createComponent();
expect(findSidebarEditableItem().props('loading')).toBe(true);
});
describe('when labels are fetched successfully', () => {
beforeEach(async () => {
createComponent(); createComponent();
await wrapper.vm.$nextTick; await waitForPromises();
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true); });
it('passes true `loading` prop to sidebar editable item', () => {
expect(findSidebarEditableItem().props('loading')).toBe(false);
});
it('renders dropdown value component when query labels is resolved', () => {
expect(findDropdownValue().exists()).toBe(true);
expect(findDropdownValue().props('selectedLabels')).toEqual(
issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes,
);
}); });
it('renders `dropdown-value` component', async () => { it('emits `onLabelRemove` event on dropdown value label remove event', () => {
createComponent(mockConfig, { const label = { id: 'gid://gitlab/ProjectLabel/1' };
default: 'None', findDropdownValue().vm.$emit('onLabelRemove', label);
expect(wrapper.emitted('onLabelRemove')).toEqual([[label]]);
}); });
await wrapper.vm.$nextTick; });
it('creates flash with error message when query is rejected', async () => {
createComponent({ queryHandler: errorQueryHandler });
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
});
});
it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if there are labels to update', async () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' };
createComponent();
await waitForPromises();
const valueComp = wrapper.find(DropdownValue); expandDropdown();
await nextTick();
expect(valueComp.exists()).toBe(true); findDropdownContents().vm.$emit('setLabels', [label]);
expect(valueComp.text()).toBe('None'); expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]);
}); });
}); });
...@@ -86,7 +86,7 @@ export const createLabelSuccessfulResponse = { ...@@ -86,7 +86,7 @@ export const createLabelSuccessfulResponse = {
}, },
}; };
export const labelsQueryResponse = { export const workspaceLabelsQueryResponse = {
data: { data: {
workspace: { workspace: {
labels: { labels: {
...@@ -108,3 +108,23 @@ export const labelsQueryResponse = { ...@@ -108,3 +108,23 @@ export const labelsQueryResponse = {
}, },
}, },
}; };
export const issuableLabelsQueryResponse = {
data: {
workspace: {
issuable: {
id: '1',
labels: {
nodes: [
{
color: '#330066',
description: null,
id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1',
},
],
},
},
},
},
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment