Commit 30540a98 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '342970-labels-widget-architecture-consolidation' into 'master'

Consolidate labels widget architecture

See merge request gitlab-org/gitlab!72356
parents a42a6d5e 7f13bc9e
...@@ -15,6 +15,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue ...@@ -15,6 +15,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue'; import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
...@@ -63,7 +64,7 @@ export default { ...@@ -63,7 +64,7 @@ export default {
'groupPathForActiveIssue', 'groupPathForActiveIssue',
'projectPathForActiveIssue', 'projectPathForActiveIssue',
]), ]),
...mapState(['sidebarType', 'issuableType', 'isSettingLabels']), ...mapState(['sidebarType', 'issuableType']),
isIssuableSidebar() { isIssuableSidebar() {
return this.sidebarType === ISSUABLE; return this.sidebarType === ISSUABLE;
}, },
...@@ -84,7 +85,10 @@ export default { ...@@ -84,7 +85,10 @@ export default {
}); });
}, },
attrWorkspacePath() { attrWorkspacePath() {
return this.isGroupBoard ? this.groupPathForActiveIssue : undefined; return this.isGroupBoard ? this.groupPathForActiveIssue : this.projectPathForActiveIssue;
},
labelType() {
return this.isGroupBoard ? LabelType.group : LabelType.project;
}, },
}, },
methods: { methods: {
...@@ -98,21 +102,19 @@ export default { ...@@ -98,21 +102,19 @@ export default {
handleClose() { handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType }); this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
}, },
handleUpdateSelectedLabels(input) { handleUpdateSelectedLabels({ labels, id }) {
this.setActiveBoardItemLabels({ this.setActiveBoardItemLabels({
iid: this.activeBoardItem.iid, id,
projectPath: this.projectPathForActiveIssue, projectPath: this.projectPathForActiveIssue,
addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)), labelIds: labels.map((label) => getIdFromGraphQLId(label.id)),
removeLabelIds: this.activeBoardItem.labels labels,
.filter((label) => !input.find((selected) => selected.id === label.id))
.map((label) => label.id),
}); });
}, },
handleLabelRemove(input) { handleLabelRemove(removeLabelId) {
this.setActiveBoardItemLabels({ this.setActiveBoardItemLabels({
iid: this.activeBoardItem.iid, iid: this.activeBoardItem.iid,
projectPath: this.projectPathForActiveIssue, projectPath: this.projectPathForActiveIssue,
removeLabelIds: [input], removeLabelIds: [removeLabelId],
}); });
}, },
}, },
...@@ -207,14 +209,13 @@ export default { ...@@ -207,14 +209,13 @@ export default {
:full-path="projectPathForActiveIssue" :full-path="projectPathForActiveIssue"
:allow-label-remove="allowLabelEdit" :allow-label-remove="allowLabelEdit"
:allow-multiselect="true" :allow-multiselect="true"
:selected-labels="activeBoardItem.labels"
:labels-select-in-progress="isSettingLabels"
:footer-create-label-title="createLabelTitle" :footer-create-label-title="createLabelTitle"
:footer-manage-label-title="manageLabelTitle" :footer-manage-label-title="manageLabelTitle"
:labels-create-title="createLabelTitle" :labels-create-title="createLabelTitle"
:labels-filter-base-path="projectPathForActiveIssue" :labels-filter-base-path="projectPathForActiveIssue"
:attr-workspace-path="attrWorkspacePath" :attr-workspace-path="attrWorkspacePath"
:issuable-type="issuableType" :issuable-type="issuableType"
:label-type="labelType"
@onLabelRemove="handleLabelRemove" @onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels" @updateSelectedLabels="handleUpdateSelectedLabels"
> >
......
...@@ -91,9 +91,7 @@ export default { ...@@ -91,9 +91,7 @@ export default {
try { try {
const addLabelIds = payload.filter((label) => label.set).map((label) => label.id); const addLabelIds = payload.filter((label) => label.set).map((label) => label.id);
const removeLabelIds = this.selectedLabels const removeLabelIds = payload.filter((label) => !label.set).map((label) => label.id);
.filter((label) => !payload.find((selected) => selected.id === label.id))
.map((label) => label.id);
const input = { const input = {
addLabelIds, addLabelIds,
...@@ -164,7 +162,7 @@ export default { ...@@ -164,7 +162,7 @@ export default {
:labels-list-title="__('Select label')" :labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')" :dropdown-button-text="__('Choose labels')"
:is-editing="edit" :is-editing="edit"
variant="embedded" variant="sidebar"
class="gl-display-block labels gl-w-full" class="gl-display-block labels gl-w-full"
@updateSelectedLabels="setLabels" @updateSelectedLabels="setLabels"
> >
......
...@@ -656,30 +656,45 @@ export default { ...@@ -656,30 +656,45 @@ export default {
}, },
setActiveIssueLabels: async ({ commit, getters }, input) => { setActiveIssueLabels: async ({ commit, getters }, input) => {
commit(types.SET_LABELS_LOADING, true);
const { activeBoardItem } = getters; const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabelsMutation, if (!gon.features?.labelsWidget) {
variables: { const { data } = await gqlClient.mutate({
input: { mutation: issueSetLabelsMutation,
iid: input.iid || String(activeBoardItem.iid), variables: {
addLabelIds: input.addLabelIds ?? [], input: {
removeLabelIds: input.removeLabelIds ?? [], iid: input.iid || String(activeBoardItem.iid),
projectPath: input.projectPath, labelIds: input.labelsId ?? undefined,
addLabelIds: input.addLabelIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
projectPath: input.projectPath,
},
}, },
}, });
});
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
commit(types.SET_LABELS_LOADING, false); commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
prop: 'labels',
value: data.updateIssue?.issue?.labels.nodes,
});
if (data.updateIssue?.errors?.length > 0) { return;
throw new Error(data.updateIssue.errors);
} }
let labels = input?.labels || [];
if (input.removeLabelIds) {
labels = activeBoardItem.labels.filter(
(label) => input.removeLabelIds[0] !== getIdFromGraphQLId(label.id),
);
}
commit(types.UPDATE_BOARD_ITEM_BY_ID, { commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: data.updateIssue?.issue?.id || activeBoardItem.id, itemId: input.id || activeBoardItem.id,
prop: 'labels', prop: 'labels',
value: data.updateIssue.issue.labels.nodes, value: labels,
}); });
}, },
......
...@@ -28,7 +28,6 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST'; ...@@ -28,7 +28,6 @@ export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST'; export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID'; export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
export const SET_LABELS_LOADING = 'SET_LABELS_LOADING';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING'; export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES'; export const RESET_ISSUES = 'RESET_ISSUES';
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS'; export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
......
...@@ -195,10 +195,6 @@ export default { ...@@ -195,10 +195,6 @@ export default {
Vue.set(state.boardItems[itemId], prop, value); Vue.set(state.boardItems[itemId], prop, value);
}, },
[mutationTypes.SET_LABELS_LOADING](state, isLoading) {
state.isSettingLabels = isLoading;
},
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) { [mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
state.isSettingAssignees = isLoading; state.isSettingAssignees = isLoading;
}, },
......
...@@ -12,7 +12,6 @@ export default () => ({ ...@@ -12,7 +12,6 @@ export default () => ({
listsFlags: {}, listsFlags: {},
boardItemsByListId: {}, boardItemsByListId: {},
backupItemsList: [], backupItemsList: [],
isSettingLabels: false,
isSettingAssignees: false, isSettingAssignees: false,
pageInfoByListId: {}, pageInfoByListId: {},
boardItems: {}, boardItems: {},
......
...@@ -11,6 +11,7 @@ import { toLabelGid } from '~/sidebar/utils'; ...@@ -11,6 +11,7 @@ import { toLabelGid } from '~/sidebar/utils';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants'; import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; import LabelsSelect 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 LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const mutationMap = { const mutationMap = {
...@@ -48,6 +49,7 @@ export default { ...@@ -48,6 +49,7 @@ export default {
return { return {
isLabelsSelectInProgress: false, isLabelsSelectInProgress: false,
selectedLabels: this.initiallySelectedLabels, selectedLabels: this.initiallySelectedLabels,
LabelType,
}; };
}, },
methods: { methods: {
...@@ -154,13 +156,11 @@ export default { ...@@ -154,13 +156,11 @@ export default {
:footer-manage-label-title="__('Manage project labels')" :footer-manage-label-title="__('Manage project labels')"
:labels-create-title="__('Create project label')" :labels-create-title="__('Create project label')"
:labels-filter-base-path="projectIssuesPath" :labels-filter-base-path="projectIssuesPath"
:labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels"
:variant="$options.variant" :variant="$options.variant"
:issuable-type="issuableType" :issuable-type="issuableType"
:attr-workspace-path="fullPath"
:label-type="LabelType.project"
data-qa-selector="labels_block" data-qa-selector="labels_block"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
> >
{{ __('None') }} {{ __('None') }}
</labels-select-widget> </labels-select-widget>
......
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import { IssuableType } from '~/issue_show/constants'; 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 epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
...@@ -29,6 +30,7 @@ import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_conf ...@@ -29,6 +30,7 @@ import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_conf
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql'; import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql'; import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql'; import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.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 epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql';
...@@ -120,6 +122,17 @@ export const labelsQueries = { ...@@ -120,6 +122,17 @@ export const labelsQueries = {
}, },
}; };
export const labelsMutations = {
[IssuableType.Issue]: {
mutation: updateIssueLabelsMutation,
mutationName: 'updateIssue',
},
[IssuableType.MergeRequest]: {
mutation: updateMergeRequestLabelsMutation,
mutationName: 'mergeRequestSetLabels',
},
};
export const dateTypes = { export const dateTypes = {
start: 'startDate', start: 'startDate',
due: 'dueDate', due: 'dueDate',
......
...@@ -5,3 +5,8 @@ export const DropdownVariant = { ...@@ -5,3 +5,8 @@ export const DropdownVariant = {
Standalone: 'standalone', Standalone: 'standalone',
Embedded: 'embedded', Embedded: 'embedded',
}; };
export const LabelType = {
group: 'GroupLabel',
project: 'ProjectLabel',
};
...@@ -68,8 +68,11 @@ export default { ...@@ -68,8 +68,11 @@ export default {
}, },
attrWorkspacePath: { attrWorkspacePath: {
type: String, type: String,
required: false, required: true,
default: undefined, },
labelType: {
type: String,
required: true,
}, },
}, },
data() { data() {
...@@ -119,13 +122,16 @@ export default { ...@@ -119,13 +122,16 @@ export default {
if (newVal) { if (newVal) {
this.$refs.dropdown.show(); this.$refs.dropdown.show();
this.isDirty = false; this.isDirty = false;
this.localSelectedLabels = this.selectedLabels;
} else { } else {
this.$refs.dropdown.hide(); this.$refs.dropdown.hide();
this.setLabels(); this.setLabels();
} }
}, },
selectedLabels(newVal) { selectedLabels(newVal) {
this.localSelectedLabels = newVal; if (!this.isDirty) {
this.localSelectedLabels = newVal;
}
}, },
}, },
created() { created() {
...@@ -193,11 +199,11 @@ export default { ...@@ -193,11 +199,11 @@ export default {
:is="dropdownContentsView" :is="dropdownContentsView"
v-model="localSelectedLabels" v-model="localSelectedLabels"
:search-key="searchKey" :search-key="searchKey"
:selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect" :allow-multiselect="allowMultiselect"
:issuable-type="issuableType" :issuable-type="issuableType"
:full-path="fullPath" :full-path="fullPath"
:attr-workspace-path="attrWorkspacePath" :attr-workspace-path="attrWorkspacePath"
:label-type="labelType"
@hideCreateView="toggleDropdownContentsCreateView" @hideCreateView="toggleDropdownContentsCreateView"
/> />
</template> </template>
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +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 { labelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql'; import createLabelMutation from './graphql/create_label.mutation.graphql';
import { LabelType } from './constants';
const errorMessage = __('Error creating label.'); const errorMessage = __('Error creating label.');
...@@ -30,8 +30,11 @@ export default { ...@@ -30,8 +30,11 @@ export default {
}, },
attrWorkspacePath: { attrWorkspacePath: {
type: String, type: String,
required: false, required: true,
default: undefined, },
labelType: {
type: String,
required: true,
}, },
}, },
data() { data() {
...@@ -50,25 +53,13 @@ export default { ...@@ -50,25 +53,13 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] })); return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
}, },
mutationVariables() { mutationVariables() {
if (this.issuableType === IssuableType.Epic) { const attributePath = this.labelType === LabelType.group ? 'groupPath' : 'projectPath';
return {
title: this.labelTitle,
color: this.selectedColor,
groupPath: this.fullPath,
};
}
return this.attrWorkspacePath !== undefined return {
? { title: this.labelTitle,
title: this.labelTitle, color: this.selectedColor,
color: this.selectedColor, [attributePath]: this.attrWorkspacePath,
groupPath: this.attrWorkspacePath, };
}
: {
title: this.labelTitle,
color: this.selectedColor,
projectPath: this.fullPath,
};
}, },
}, },
methods: { methods: {
......
...@@ -19,10 +19,6 @@ export default { ...@@ -19,10 +19,6 @@ export default {
prop: 'localSelectedLabels', prop: 'localSelectedLabels',
}, },
props: { props: {
selectedLabels: {
type: Array,
required: true,
},
allowMultiselect: { allowMultiselect: {
type: Boolean, type: Boolean,
required: true, required: true,
......
<script> <script>
import { MutationOperationMode } from '~/graphql_shared/utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
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 { labelsQueries, labelsMutations } 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';
...@@ -50,16 +52,6 @@ export default { ...@@ -50,16 +52,6 @@ export default {
required: false, required: false,
default: DropdownVariant.Sidebar, default: DropdownVariant.Sidebar,
}, },
selectedLabels: {
type: Array,
required: false,
default: () => [],
},
labelsSelectInProgress: {
type: Boolean,
required: false,
default: false,
},
labelsFilterBasePath: { labelsFilterBasePath: {
type: String, type: String,
required: false, required: false,
...@@ -95,25 +87,25 @@ export default { ...@@ -95,25 +87,25 @@ export default {
required: false, required: false,
default: __('Manage group labels'), default: __('Manage group labels'),
}, },
isEditing: {
type: Boolean,
required: false,
default: false,
},
issuableType: { issuableType: {
type: String, type: String,
required: true, required: true,
}, },
attrWorkspacePath: { attrWorkspacePath: {
type: String, type: String,
required: false, required: true,
default: undefined, },
labelType: {
type: String,
required: true,
}, },
}, },
data() { data() {
return { return {
contentIsOnViewport: true, contentIsOnViewport: true,
issuableLabels: [], issuableLabels: [],
labelsSelectInProgress: false,
oldIid: null,
}; };
}, },
computed: { computed: {
...@@ -143,9 +135,19 @@ export default { ...@@ -143,9 +135,19 @@ export default {
}, },
}, },
}, },
watch: {
iid(_, oldVal) {
this.oldIid = oldVal;
},
},
methods: { methods: {
handleDropdownClose(labels) { handleDropdownClose(labels) {
this.$emit('updateSelectedLabels', labels); if (this.iid !== '') {
this.updateSelectedLabels(this.getUpdateVariables(labels));
} else {
this.$emit('updateSelectedLabels', { labels });
}
this.collapseEditableItem(); this.collapseEditableItem();
}, },
collapseEditableItem() { collapseEditableItem() {
...@@ -154,6 +156,85 @@ export default { ...@@ -154,6 +156,85 @@ export default {
handleCollapsedValueClick() { handleCollapsedValueClick() {
this.$emit('toggleCollapse'); this.$emit('toggleCollapse');
}, },
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:
updateVariables.operationMode = MutationOperationMode.Replace;
return updateVariables;
default:
return {};
}
},
updateSelectedLabels(inputVariables) {
this.labelsSelectInProgress = true;
this.$apollo
.mutate({
mutation: labelsMutations[this.issuableType].mutation,
variables: { input: inputVariables },
})
.then(({ data }) => {
const { mutationName } = labelsMutations[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,
};
default:
return {};
}
},
handleLabelRemove(labelId) {
this.updateSelectedLabels(this.getRemoveVariables(labelId));
this.$emit('onLabelRemove', labelId);
},
isDropdownVariantSidebar, isDropdownVariantSidebar,
isDropdownVariantStandalone, isDropdownVariantStandalone,
isDropdownVariantEmbedded, isDropdownVariantEmbedded,
...@@ -180,6 +261,7 @@ export default { ...@@ -180,6 +261,7 @@ export default {
:title="__('Labels')" :title="__('Labels')"
:loading="isLoading" :loading="isLoading"
:can-edit="allowLabelEdit" :can-edit="allowLabelEdit"
@open="oldIid = null"
> >
<template #collapsed> <template #collapsed>
<dropdown-value <dropdown-value
...@@ -188,7 +270,7 @@ export default { ...@@ -188,7 +270,7 @@ export default {
: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"
@onLabelRemove="$emit('onLabelRemove', $event)" @onLabelRemove="handleLabelRemove"
> >
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
...@@ -201,7 +283,7 @@ export default { ...@@ -201,7 +283,7 @@ export default {
:labels-filter-base-path="labelsFilterBasePath" :labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam" :labels-filter-param="labelsFilterParam"
class="gl-mb-2" class="gl-mb-2"
@onLabelRemove="$emit('onLabelRemove', $event)" @onLabelRemove="handleLabelRemove"
> >
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
...@@ -212,12 +294,13 @@ export default { ...@@ -212,12 +294,13 @@ export default {
:footer-create-label-title="footerCreateLabelTitle" :footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle" :footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle" :labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels" :selected-labels="issuableLabels"
:variant="variant" :variant="variant"
:issuable-type="issuableType" :issuable-type="issuableType"
:is-visible="edit" :is-visible="edit"
:full-path="fullPath" :full-path="fullPath"
:attr-workspace-path="attrWorkspacePath" :attr-workspace-path="attrWorkspacePath"
:label-type="labelType"
@setLabels="handleDropdownClose" @setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem" @closeDropdown="collapseEditableItem"
/> />
...@@ -233,10 +316,12 @@ export default { ...@@ -233,10 +316,12 @@ export default {
:footer-create-label-title="footerCreateLabelTitle" :footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle" :footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle" :labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels" :selected-labels="issuableLabels"
:variant="variant" :variant="variant"
:issuable-type="issuableType" :issuable-type="issuableType"
:full-path="fullPath" :full-path="fullPath"
:attr-workspace-path="attrWorkspacePath"
:label-type="labelType"
@setLabels="handleDropdownClose" @setLabels="handleDropdownClose"
/> />
</div> </div>
......
...@@ -14,6 +14,7 @@ import { s__ } from '~/locale'; ...@@ -14,6 +14,7 @@ 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 LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createEpic from '../queries/createEpic.mutation.graphql'; import createEpic from '../queries/createEpic.mutation.graphql';
...@@ -31,7 +32,6 @@ export default { ...@@ -31,7 +32,6 @@ export default {
}, },
mixins: [glFeatureFlagMixin()], mixins: [glFeatureFlagMixin()],
inject: [ inject: [
'iid',
'groupPath', 'groupPath',
'groupEpicsPath', 'groupEpicsPath',
'labelsFetchPath', 'labelsFetchPath',
...@@ -48,6 +48,7 @@ export default { ...@@ -48,6 +48,7 @@ export default {
startDateFixed: null, startDateFixed: null,
dueDateFixed: null, dueDateFixed: null,
loading: false, loading: false,
LabelType,
}; };
}, },
computed: { computed: {
...@@ -190,17 +191,17 @@ export default { ...@@ -190,17 +191,17 @@ export default {
<labels-select-widget <labels-select-widget
v-if="glFeatures.labelsWidget" v-if="glFeatures.labelsWidget"
class="block labels js-labels-block" class="block labels js-labels-block"
:iid="iid"
:full-path="groupPath" :full-path="groupPath"
:allow-label-create="true" :allow-label-create="true"
:allow-multiselect="true" :allow-multiselect="true"
:allow-scoped-labels="false" :allow-scoped-labels="false"
:labels-filter-base-path="groupEpicsPath" :labels-filter-base-path="groupEpicsPath"
:selected-labels="labels" :attr-workspace-path="groupPath"
:label-type="LabelType.group"
issuable-type="epic" issuable-type="epic"
variant="embedded" variant="embedded"
data-qa-selector="labels_block" data-qa-selector="labels_block"
@updateSelectedLabels="handleUpdateSelectedLabels" @updateSelectedLabels="handleUpdateSelectedLabels($event.labels)"
> >
{{ __('None') }} {{ __('None') }}
</labels-select-widget> </labels-select-widget>
......
...@@ -11,12 +11,11 @@ RSpec.describe 'Labels Hierarchy', :js do ...@@ -11,12 +11,11 @@ RSpec.describe 'Labels Hierarchy', :js do
let!(:grandparent_group_label) { create(:group_label, group: grandparent, title: 'Label_1') } let!(:grandparent_group_label) { create(:group_label, group: grandparent, title: 'Label_1') }
let!(:parent_group_label) { create(:group_label, group: parent, title: 'Label_2') } let!(:parent_group_label) { create(:group_label, group: parent, title: 'Label_2') }
let!(:child_group_label) { create(:group_label, group: child, title: 'Label_3') } let!(:project_label_1) { create(:label, project: project_1, title: 'Label_3') }
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
let!(:labeled_issue_1) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label, child_group_label]) } let!(:labeled_issue_1) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label]) }
let!(:labeled_issue_2) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label]) } let!(:labeled_issue_2) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label]) }
let!(:labeled_issue_3) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label, child_group_label, project_label_1]) } let!(:labeled_issue_3) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label, project_label_1]) }
let!(:not_labeled) { create(:issue, project: project_1) } let!(:not_labeled) { create(:issue, project: project_1) }
before do before do
...@@ -26,8 +25,8 @@ RSpec.describe 'Labels Hierarchy', :js do ...@@ -26,8 +25,8 @@ RSpec.describe 'Labels Hierarchy', :js do
end end
shared_examples 'filter for scoped boards' do |project = false| shared_examples 'filter for scoped boards' do |project = false|
it 'scopes board to ancestor and descendant labels' do it 'scopes board to ancestor and current group labels' do
labels = [grandparent_group_label, parent_group_label, child_group_label] labels = [grandparent_group_label, parent_group_label]
labels.push(project_label_1) if project labels.push(project_label_1) if project
labels.each do |label| labels.each do |label|
......
...@@ -105,6 +105,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => { ...@@ -105,6 +105,7 @@ describe('~/boards/components/sidebar/board_sidebar_labels_select.vue', () => {
describe('when labels are updated over existing labels', () => { describe('when labels are updated over existing labels', () => {
const testLabelsPayload = [ const testLabelsPayload = [
{ id: 5, set: true }, { id: 5, set: true },
{ id: 6, set: false },
{ id: 7, set: true }, { id: 7, set: true },
]; ];
const expectedLabels = [{ id: 5 }, { id: 7 }]; const expectedLabels = [{ id: 5 }, { id: 7 }];
......
...@@ -27,6 +27,7 @@ import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql' ...@@ -27,6 +27,7 @@ import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql'
import actions from '~/boards/stores/actions'; import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types'; import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations'; import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { import {
mockLists, mockLists,
...@@ -1572,12 +1573,13 @@ describe('setActiveIssueLabels', () => { ...@@ -1572,12 +1573,13 @@ describe('setActiveIssueLabels', () => {
const getters = { activeBoardItem: mockIssue }; const getters = { activeBoardItem: mockIssue };
const testLabelIds = labels.map((label) => label.id); const testLabelIds = labels.map((label) => label.id);
const input = { const input = {
addLabelIds: testLabelIds, labelIds: testLabelIds,
removeLabelIds: [], removeLabelIds: [],
projectPath: 'h/b', projectPath: 'h/b',
labels,
}; };
it('should assign labels on success, and sets loading state for labels', (done) => { it('should assign labels on success', (done) => {
jest jest
.spyOn(gqlClient, 'mutate') .spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } }); .mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
...@@ -1593,14 +1595,6 @@ describe('setActiveIssueLabels', () => { ...@@ -1593,14 +1595,6 @@ describe('setActiveIssueLabels', () => {
input, input,
{ ...state, ...getters }, { ...state, ...getters },
[ [
{
type: types.SET_LABELS_LOADING,
payload: true,
},
{
type: types.SET_LABELS_LOADING,
payload: false,
},
{ {
type: types.UPDATE_BOARD_ITEM_BY_ID, type: types.UPDATE_BOARD_ITEM_BY_ID,
payload, payload,
...@@ -1618,6 +1612,64 @@ describe('setActiveIssueLabels', () => { ...@@ -1618,6 +1612,64 @@ describe('setActiveIssueLabels', () => {
await expect(actions.setActiveIssueLabels({ getters }, input)).rejects.toThrow(Error); await expect(actions.setActiveIssueLabels({ getters }, input)).rejects.toThrow(Error);
}); });
describe('labels_widget FF on', () => {
beforeEach(() => {
window.gon = {
features: { labelsWidget: true },
};
getters.activeBoardItem = { ...mockIssue, labels };
});
afterEach(() => {
window.gon = {
features: {},
};
});
it('should assign labels', () => {
const payload = {
itemId: getters.activeBoardItem.id,
prop: 'labels',
value: labels,
};
testAction(
actions.setActiveIssueLabels,
input,
{ ...state, ...getters },
[
{
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
[],
);
});
it('should remove label', () => {
const payload = {
itemId: getters.activeBoardItem.id,
prop: 'labels',
value: [labels[1]],
};
testAction(
actions.setActiveIssueLabels,
{ ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] },
{ ...state, ...getters },
[
{
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
[],
);
});
});
}); });
describe('setActiveItemSubscribed', () => { describe('setActiveItemSubscribed', () => {
......
...@@ -51,6 +51,7 @@ describe('DropdownContentsCreateView', () => { ...@@ -51,6 +51,7 @@ describe('DropdownContentsCreateView', () => {
const createComponent = ({ const createComponent = ({
mutationHandler = createLabelSuccessHandler, mutationHandler = createLabelSuccessHandler,
issuableType = IssuableType.Issue, issuableType = IssuableType.Issue,
labelType = 'ProjectLabel',
} = {}) => { } = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]); const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({ mockApollo.clients.defaultClient.cache.writeQuery({
...@@ -68,6 +69,8 @@ describe('DropdownContentsCreateView', () => { ...@@ -68,6 +69,8 @@ describe('DropdownContentsCreateView', () => {
propsData: { propsData: {
issuableType, issuableType,
fullPath: '', fullPath: '',
attrWorkspacePath: '',
labelType,
}, },
}); });
}; };
...@@ -174,7 +177,7 @@ describe('DropdownContentsCreateView', () => { ...@@ -174,7 +177,7 @@ describe('DropdownContentsCreateView', () => {
}); });
it('calls a mutation with `groupPath` variable on the epic', () => { it('calls a mutation with `groupPath` variable on the epic', () => {
createComponent({ issuableType: IssuableType.Epic }); createComponent({ issuableType: IssuableType.Epic, labelType: 'GroupLabel' });
fillLabelAttributes(); fillLabelAttributes();
findCreateButton().vm.$emit('click'); findCreateButton().vm.$emit('click');
......
...@@ -41,6 +41,8 @@ describe('DropdownContent', () => { ...@@ -41,6 +41,8 @@ describe('DropdownContent', () => {
variant: 'sidebar', variant: 'sidebar',
issuableType: 'issue', issuableType: 'issue',
fullPath: 'test', fullPath: 'test',
labelType: 'ProjectLabel',
attrWorkspacePath: 'path',
...props, ...props,
}, },
data() { data() {
......
...@@ -41,6 +41,7 @@ describe('LabelsSelectRoot', () => { ...@@ -41,6 +41,7 @@ describe('LabelsSelectRoot', () => {
propsData: { propsData: {
...config, ...config,
issuableType: IssuableType.Issue, issuableType: IssuableType.Issue,
labelType: 'ProjectLabel',
}, },
stubs: { stubs: {
SidebarEditableItem, SidebarEditableItem,
...@@ -121,11 +122,11 @@ describe('LabelsSelectRoot', () => { ...@@ -121,11 +122,11 @@ describe('LabelsSelectRoot', () => {
}); });
}); });
it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event', async () => { it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' }; const label = { id: 'gid://gitlab/ProjectLabel/1' };
createComponent(); createComponent({ config: { ...mockConfig, iid: undefined } });
findDropdownContents().vm.$emit('setLabels', [label]); findDropdownContents().vm.$emit('setLabels', [label]);
expect(wrapper.emitted('updateSelectedLabels')).toEqual([[[label]]]); expect(wrapper.emitted('updateSelectedLabels')).toEqual([[{ labels: [label] }]]);
}); });
}); });
...@@ -40,12 +40,12 @@ export const mockConfig = { ...@@ -40,12 +40,12 @@ export const mockConfig = {
labelsListTitle: 'Assign labels', labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label', labelsCreateTitle: 'Create label',
variant: 'sidebar', variant: 'sidebar',
selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false, labelsSelectInProgress: false,
labelsFilterBasePath: '/gitlab-org/my-project/issues', labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name', labelsFilterParam: 'label_name',
footerCreateLabelTitle: 'create', footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage', footerManageLabelTitle: 'manage',
attrWorkspacePath: 'test',
}; };
export const mockSuggestedColors = { export const mockSuggestedColors = {
......
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