Commit 385dec63 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '343606-add-labels-widget-support-for-epic' into 'master'

Add Labels widget support for epic

See merge request gitlab-org/gitlab!72853
parents d122225a 002742ec
...@@ -214,8 +214,9 @@ export default { ...@@ -214,8 +214,9 @@ export default {
: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"
workspace-type="project"
:issuable-type="issuableType" :issuable-type="issuableType"
:label-type="labelType" :label-create-type="labelType"
@onLabelRemove="handleLabelRemove" @onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels" @updateSelectedLabels="handleUpdateSelectedLabels"
> >
......
...@@ -39,3 +39,8 @@ export const IncidentType = 'incident'; ...@@ -39,3 +39,8 @@ export const IncidentType = 'incident';
export const issueState = { issueType: undefined, isDirty: false }; export const issueState = { issueType: undefined, isDirty: false };
export const POLLING_DELAY = 2000; export const POLLING_DELAY = 2000;
export const WorkspaceType = {
project: 'project',
group: 'group',
};
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import $ from 'jquery'; import $ from 'jquery';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { hide, fixTitle } from '~/tooltips'; import { hide, fixTitle } from '~/tooltips';
import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import createFlash from './flash'; import createFlash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale'; import { sprintf, s__, __ } from './locale';
...@@ -130,8 +131,10 @@ Sidebar.prototype.openDropdown = function (blockOrName) { ...@@ -130,8 +131,10 @@ Sidebar.prototype.openDropdown = function (blockOrName) {
// Wait for the sidebar to trigger('click') open // Wait for the sidebar to trigger('click') open
// so it doesn't cause our dropdown to close preemptively // so it doesn't cause our dropdown to close preemptively
setTimeout(() => { setTimeout(() => {
$block.find('.js-sidebar-dropdown-toggle').trigger('click'); if (!gon.features?.labelsWidget && !$block.hasClass('labels-select-wrapper')) {
}); $block.find('.js-sidebar-dropdown-toggle').trigger('click');
}
}, DEBOUNCE_DROPDOWN_DELAY);
}; };
Sidebar.prototype.setCollapseAfterUpdate = function ($block) { Sidebar.prototype.setCollapseAfterUpdate = function ($block) {
......
...@@ -158,8 +158,9 @@ export default { ...@@ -158,8 +158,9 @@ export default {
:labels-filter-base-path="projectIssuesPath" :labels-filter-base-path="projectIssuesPath"
:variant="$options.variant" :variant="$options.variant"
:issuable-type="issuableType" :issuable-type="issuableType"
workspace-type="project"
:attr-workspace-path="fullPath" :attr-workspace-path="fullPath"
:label-type="LabelType.project" :label-create-type="LabelType.project"
data-qa-selector="labels_block" data-qa-selector="labels_block"
> >
{{ __('None') }} {{ __('None') }}
......
...@@ -134,7 +134,7 @@ export default { ...@@ -134,7 +134,7 @@ export default {
v-if="canUpdate && !initialLoading && canEdit" v-if="canUpdate && !initialLoading && canEdit"
category="tertiary" category="tertiary"
size="small" size="small"
class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2" class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2 shortcut-sidebar-dropdown-toggle"
data-testid="edit-button" data-testid="edit-button"
:data-track-action="tracking.event" :data-track-action="tracking.event"
:data-track-label="tracking.label" :data-track-label="tracking.label"
......
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql'; import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import { IssuableType } from '~/issue_show/constants'; import { IssuableType, WorkspaceType } 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';
import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql'; import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
...@@ -34,6 +34,7 @@ import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_req ...@@ -34,6 +34,7 @@ import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_req
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';
import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
import groupLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_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 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 projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
...@@ -111,19 +112,18 @@ export const referenceQueries = { ...@@ -111,19 +112,18 @@ export const referenceQueries = {
}, },
}; };
export const labelsQueries = { export const workspaceLabelsQueries = {
[IssuableType.Issue]: { [WorkspaceType.project]: {
issuableQuery: issueLabelsQuery, query: projectLabelsQuery,
workspaceQuery: projectLabelsQuery,
}, },
[IssuableType.Epic]: { [WorkspaceType.group]: {
issuableQuery: epicLabelsQuery, query: groupLabelsQuery,
workspaceQuery: groupLabelsQuery,
}, },
}; };
export const labelsMutations = { export const issuableLabelsQueries = {
[IssuableType.Issue]: { [IssuableType.Issue]: {
issuableQuery: issueLabelsQuery,
mutation: updateIssueLabelsMutation, mutation: updateIssueLabelsMutation,
mutationName: 'updateIssue', mutationName: 'updateIssue',
}, },
...@@ -131,6 +131,11 @@ export const labelsMutations = { ...@@ -131,6 +131,11 @@ export const labelsMutations = {
mutation: updateMergeRequestLabelsMutation, mutation: updateMergeRequestLabelsMutation,
mutationName: 'mergeRequestSetLabels', mutationName: 'mergeRequestSetLabels',
}, },
[IssuableType.Epic]: {
issuableQuery: epicLabelsQuery,
mutation: updateEpicLabelsMutation,
mutationName: 'updateEpic',
},
}; };
export const dateTypes = { export const dateTypes = {
......
...@@ -36,6 +36,7 @@ export default { ...@@ -36,6 +36,7 @@ export default {
<template> <template>
<div <div
class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute" class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute"
data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content" data-qa-selector="labels_dropdown_content"
:style="directionStyle" :style="directionStyle"
> >
......
export const SCOPED_LABEL_DELIMITER = '::'; export const SCOPED_LABEL_DELIMITER = '::';
export const DEBOUNCE_DROPDOWN_DELAY = 200;
export const DropdownVariant = { export const DropdownVariant = {
Sidebar: 'sidebar', Sidebar: 'sidebar',
...@@ -7,6 +8,6 @@ export const DropdownVariant = { ...@@ -7,6 +8,6 @@ export const DropdownVariant = {
}; };
export const LabelType = { export const LabelType = {
group: 'GroupLabel', group: 'group',
project: 'ProjectLabel', project: 'project',
}; };
...@@ -66,11 +66,15 @@ export default { ...@@ -66,11 +66,15 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
workspaceType: {
type: String,
required: true,
},
attrWorkspacePath: { attrWorkspacePath: {
type: String, type: String,
required: true, required: true,
}, },
labelType: { labelCreateType: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -158,6 +162,7 @@ export default { ...@@ -158,6 +162,7 @@ export default {
this.$emit('setLabels', this.localSelectedLabels); this.$emit('setLabels', this.localSelectedLabels);
}, },
handleDropdownHide() { handleDropdownHide() {
this.$emit('closeDropdown');
if (!isDropdownVariantSidebar(this.variant)) { if (!isDropdownVariantSidebar(this.variant)) {
this.setLabels(); this.setLabels();
} }
...@@ -168,6 +173,9 @@ export default { ...@@ -168,6 +173,9 @@ export default {
setFocus() { setFocus() {
this.$refs.header.focusInput(); this.$refs.header.focusInput();
}, },
showDropdown() {
this.$refs.dropdown.show();
},
}, },
}; };
</script> </script>
...@@ -177,6 +185,7 @@ export default { ...@@ -177,6 +185,7 @@ export default {
ref="dropdown" ref="dropdown"
:text="buttonText" :text="buttonText"
class="gl-w-full gl-mt-2" class="gl-w-full gl-mt-2"
data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content" data-qa-selector="labels_dropdown_content"
@hide="handleDropdownHide" @hide="handleDropdownHide"
@shown="setFocus" @shown="setFocus"
...@@ -202,9 +211,10 @@ export default { ...@@ -202,9 +211,10 @@ export default {
:allow-multiselect="allowMultiselect" :allow-multiselect="allowMultiselect"
:issuable-type="issuableType" :issuable-type="issuableType"
:full-path="fullPath" :full-path="fullPath"
:workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath" :attr-workspace-path="attrWorkspacePath"
:label-type="labelType" :label-create-type="labelCreateType"
@hideCreateView="toggleDropdownContentsCreateView" @hideCreateView="toggleDropdownContent"
/> />
</template> </template>
<template #footer> <template #footer>
......
...@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from ...@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from
import produce from 'immer'; import produce from 'immer';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { labelsQueries } from '~/sidebar/constants'; import { workspaceLabelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql'; import createLabelMutation from './graphql/create_label.mutation.graphql';
import { LabelType } from './constants'; import { LabelType } from './constants';
...@@ -20,19 +20,24 @@ export default { ...@@ -20,19 +20,24 @@ export default {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
props: { props: {
issuableType: { fullPath: {
type: String, type: String,
required: true, required: true,
}, },
fullPath: { attrWorkspacePath: {
type: String, type: String,
required: true, required: true,
}, },
attrWorkspacePath: { labelCreateType: {
type: String, type: String,
required: true, required: true,
}, },
labelType: { issuableType: {
type: String,
required: false,
default: undefined,
},
workspaceType: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -53,7 +58,7 @@ export default { ...@@ -53,7 +58,7 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] })); return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
}, },
mutationVariables() { mutationVariables() {
const attributePath = this.labelType === LabelType.group ? 'groupPath' : 'projectPath'; const attributePath = this.labelCreateType === LabelType.group ? 'groupPath' : 'projectPath';
return { return {
title: this.labelTitle, title: this.labelTitle,
...@@ -73,8 +78,10 @@ export default { ...@@ -73,8 +78,10 @@ export default {
this.selectedColor = this.getColorCode(color); this.selectedColor = this.getColorCode(color);
}, },
updateLabelsInCache(store, label) { updateLabelsInCache(store, label) {
const { query } = workspaceLabelsQueries[this.workspaceType];
const sourceData = store.readQuery({ const sourceData = store.readQuery({
query: labelsQueries[this.issuableType].workspaceQuery, query,
variables: { fullPath: this.fullPath, searchTerm: '' }, variables: { fullPath: this.fullPath, searchTerm: '' },
}); });
...@@ -86,7 +93,7 @@ export default { ...@@ -86,7 +93,7 @@ export default {
}); });
store.writeQuery({ store.writeQuery({
query: labelsQueries[this.issuableType].workspaceQuery, query,
variables: { fullPath: this.fullPath, searchTerm: '' }, variables: { fullPath: this.fullPath, searchTerm: '' },
data, data,
}); });
...@@ -171,7 +178,7 @@ export default { ...@@ -171,7 +178,7 @@ export default {
<gl-button <gl-button
class="js-btn-cancel-create" class="js-btn-cancel-create"
data-testid="cancel-button" data-testid="cancel-button"
@click="$emit('hideCreateView')" @click.stop="$emit('hideCreateView')"
> >
{{ __('Cancel') }} {{ __('Cancel') }}
</gl-button> </gl-button>
......
...@@ -4,7 +4,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; ...@@ -4,7 +4,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { labelsQueries } from '~/sidebar/constants'; import { workspaceLabelsQueries } from '~/sidebar/constants';
import LabelItem from './label_item.vue'; import LabelItem from './label_item.vue';
export default { export default {
...@@ -39,6 +39,10 @@ export default { ...@@ -39,6 +39,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
workspaceType: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -49,7 +53,7 @@ export default { ...@@ -49,7 +53,7 @@ export default {
apollo: { apollo: {
labels: { labels: {
query() { query() {
return labelsQueries[this.issuableType].workspaceQuery; return workspaceLabelsQueries[this.workspaceType].query;
}, },
variables() { variables() {
return { return {
......
#import "~/graphql_shared/fragments/label.fragment.graphql"
mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPath: ID) { mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPath: ID) {
labelCreate( labelCreate(
input: { title: $title, color: $color, projectPath: $projectPath, groupPath: $groupPath } input: { title: $title, color: $color, projectPath: $projectPath, groupPath: $groupPath }
) { ) {
label { label {
id ...Label
color
description
title
} }
errors errors
} }
......
#import "~/graphql_shared/fragments/label.fragment.graphql"
query epicLabels($fullPath: ID!, $iid: ID) { query epicLabels($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) { workspace: group(fullPath: $fullPath) {
issuable: epic(iid: $iid) { issuable: epic(iid: $iid) {
id id
labels { labels {
nodes { nodes {
id ...Label
title
color
description
} }
} }
} }
......
#import "~/graphql_shared/fragments/label.fragment.graphql"
mutation updateEpic($input: UpdateEpicInput!) {
updateEpic(input: $input) {
epic {
id
labels {
nodes {
...Label
}
}
}
errors
}
}
#import "~/graphql_shared/fragments/label.fragment.graphql"
query groupLabels($fullPath: ID!, $searchTerm: String) { query groupLabels($fullPath: ID!, $searchTerm: String) {
workspace: group(fullPath: $fullPath) { workspace: group(fullPath: $fullPath) {
labels(searchTerm: $searchTerm, onlyGroupLabels: true) { labels(searchTerm: $searchTerm, onlyGroupLabels: true) {
nodes { nodes {
id ...Label
title
color
description
} }
} }
} }
......
#import "~/graphql_shared/fragments/label.fragment.graphql"
query issueLabels($fullPath: ID!, $iid: String) { query issueLabels($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) { workspace: project(fullPath: $fullPath) {
issuable: issue(iid: $iid) { issuable: issue(iid: $iid) {
id id
labels { labels {
nodes { nodes {
id ...Label
title
color
description
} }
} }
} }
......
#import "~/graphql_shared/fragments/label.fragment.graphql"
query projectLabels($fullPath: ID!, $searchTerm: String) { query projectLabels($fullPath: ID!, $searchTerm: String) {
workspace: project(fullPath: $fullPath) { workspace: project(fullPath: $fullPath) {
labels(searchTerm: $searchTerm, includeAncestorGroups: true) { labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes { nodes {
id ...Label
title
color
description
} }
} }
} }
......
<script> <script>
import { MutationOperationMode } from '~/graphql_shared/utils'; import { debounce } from 'lodash';
import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants'; 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, labelsMutations } from '~/sidebar/constants'; import { issuableLabelsQueries } from '~/sidebar/constants';
import { DropdownVariant } from './constants'; import { DEBOUNCE_DROPDOWN_DELAY, 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';
...@@ -91,11 +92,15 @@ export default { ...@@ -91,11 +92,15 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
workspaceType: {
type: String,
required: true,
},
attrWorkspacePath: { attrWorkspacePath: {
type: String, type: String,
required: true, required: true,
}, },
labelType: { labelCreateType: {
type: String, type: String,
required: true, required: true,
}, },
...@@ -106,17 +111,21 @@ export default { ...@@ -106,17 +111,21 @@ export default {
issuableLabels: [], issuableLabels: [],
labelsSelectInProgress: false, labelsSelectInProgress: false,
oldIid: null, oldIid: null,
sidebarExpandedOnClick: false,
}; };
}, },
computed: { computed: {
isLoading() { isLoading() {
return this.labelsSelectInProgress || this.$apollo.queries.issuableLabels.loading; return this.labelsSelectInProgress || this.$apollo.queries.issuableLabels.loading;
}, },
issuableLabelIds() {
return this.issuableLabels.map((label) => label.id);
},
}, },
apollo: { apollo: {
issuableLabels: { issuableLabels: {
query() { query() {
return labelsQueries[this.issuableType].issuableQuery; return issuableLabelsQueries[this.issuableType].issuableQuery;
}, },
skip() { skip() {
return !isDropdownVariantSidebar(this.variant); return !isDropdownVariantSidebar(this.variant);
...@@ -140,6 +149,15 @@ export default { ...@@ -140,6 +149,15 @@ export default {
this.oldIid = oldVal; this.oldIid = oldVal;
}, },
}, },
mounted() {
document.addEventListener('toggleSidebarRevealLabelsDropdown', this.handleCollapsedValueClick);
},
beforeDestroy() {
document.removeEventListener(
'toggleSidebarRevealLabelsDropdown',
this.handleCollapsedValueClick,
);
},
methods: { methods: {
handleDropdownClose(labels) { handleDropdownClose(labels) {
if (this.iid !== '') { if (this.iid !== '') {
...@@ -152,9 +170,18 @@ export default { ...@@ -152,9 +170,18 @@ export default {
}, },
collapseEditableItem() { collapseEditableItem() {
this.$refs.editable?.collapse(); this.$refs.editable?.collapse();
if (this.sidebarExpandedOnClick) {
this.sidebarExpandedOnClick = false;
this.$emit('toggleCollapse');
}
}, },
handleCollapsedValueClick() { handleCollapsedValueClick() {
this.sidebarExpandedOnClick = true;
this.$emit('toggleCollapse'); this.$emit('toggleCollapse');
debounce(() => {
this.$refs.editable.toggle();
this.$refs.dropdownContents.showDropdown();
}, DEBOUNCE_DROPDOWN_DELAY)();
}, },
getUpdateVariables(labels) { getUpdateVariables(labels) {
let labelIds = []; let labelIds = [];
...@@ -172,8 +199,19 @@ export default { ...@@ -172,8 +199,19 @@ export default {
case IssuableType.Issue: case IssuableType.Issue:
return updateVariables; return updateVariables;
case IssuableType.MergeRequest: case IssuableType.MergeRequest:
updateVariables.operationMode = MutationOperationMode.Replace; return {
return updateVariables; ...updateVariables,
operationMode: MutationOperationMode.Replace,
};
case IssuableType.Epic:
return {
iid: currentIid,
groupPath: this.fullPath,
addLabelIds: labelIds,
removeLabelIds: this.issuableLabelIds
.filter((id) => !labelIds.includes(id))
.map((id) => getIdFromGraphQLId(id)),
};
default: default:
return {}; return {};
} }
...@@ -183,11 +221,11 @@ export default { ...@@ -183,11 +221,11 @@ export default {
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: labelsMutations[this.issuableType].mutation, mutation: issuableLabelsQueries[this.issuableType].mutation,
variables: { input: inputVariables }, variables: { input: inputVariables },
}) })
.then(({ data }) => { .then(({ data }) => {
const { mutationName } = labelsMutations[this.issuableType]; const { mutationName } = issuableLabelsQueries[this.issuableType];
if (data[mutationName]?.errors?.length) { if (data[mutationName]?.errors?.length) {
throw new Error(); throw new Error();
...@@ -227,6 +265,12 @@ export default { ...@@ -227,6 +265,12 @@ export default {
labelIds: [labelId], labelIds: [labelId],
operationMode: MutationOperationMode.Remove, operationMode: MutationOperationMode.Remove,
}; };
case IssuableType.Epic:
return {
iid: this.iid,
removeLabelIds: [labelId],
groupPath: this.fullPath,
};
default: default:
return {}; return {};
} }
...@@ -288,6 +332,7 @@ export default { ...@@ -288,6 +332,7 @@ export default {
<slot></slot> <slot></slot>
</dropdown-value> </dropdown-value>
<dropdown-contents <dropdown-contents
ref="dropdownContents"
:dropdown-button-text="dropdownButtonText" :dropdown-button-text="dropdownButtonText"
:allow-multiselect="allowMultiselect" :allow-multiselect="allowMultiselect"
:labels-list-title="labelsListTitle" :labels-list-title="labelsListTitle"
...@@ -299,8 +344,9 @@ export default { ...@@ -299,8 +344,9 @@ export default {
:issuable-type="issuableType" :issuable-type="issuableType"
:is-visible="edit" :is-visible="edit"
:full-path="fullPath" :full-path="fullPath"
:workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath" :attr-workspace-path="attrWorkspacePath"
:label-type="labelType" :label-create-type="labelCreateType"
@setLabels="handleDropdownClose" @setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem" @closeDropdown="collapseEditableItem"
/> />
...@@ -320,8 +366,9 @@ export default { ...@@ -320,8 +366,9 @@ export default {
:variant="variant" :variant="variant"
:issuable-type="issuableType" :issuable-type="issuableType"
:full-path="fullPath" :full-path="fullPath"
:workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath" :attr-workspace-path="attrWorkspacePath"
:label-type="labelType" :label-create-type="labelCreateType"
@setLabels="handleDropdownClose" @setLabels="handleDropdownClose"
/> />
</div> </div>
......
...@@ -24,6 +24,10 @@ export default class ShortcutsEpic extends ShortcutsIssuable { ...@@ -24,6 +24,10 @@ export default class ShortcutsEpic extends ShortcutsIssuable {
} }
static openSidebarDropdown($block) { static openSidebarDropdown($block) {
if (gon.features?.labelsWidget) {
document.dispatchEvent(new Event('toggleSidebarRevealLabelsDropdown'));
return;
}
if (parseBoolean(Cookies.get('collapsed_gutter'))) { if (parseBoolean(Cookies.get('collapsed_gutter'))) {
document.dispatchEvent(new Event('toggleSidebarRevealLabelsDropdown')); document.dispatchEvent(new Event('toggleSidebarRevealLabelsDropdown'));
} else { } else {
......
...@@ -197,7 +197,8 @@ export default { ...@@ -197,7 +197,8 @@ export default {
:allow-scoped-labels="false" :allow-scoped-labels="false"
:labels-filter-base-path="groupEpicsPath" :labels-filter-base-path="groupEpicsPath"
:attr-workspace-path="groupPath" :attr-workspace-path="groupPath"
:label-type="LabelType.group" workspace-type="group"
:label-create-type="LabelType.group"
issuable-type="epic" issuable-type="epic"
variant="embedded" variant="embedded"
data-qa-selector="labels_block" data-qa-selector="labels_block"
......
...@@ -14,6 +14,9 @@ import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sideb ...@@ -14,6 +14,9 @@ import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sideb
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue'; import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import sidebarEventHub from '~/sidebar/event_hub'; import sidebarEventHub from '~/sidebar/event_hub';
import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; import SidebarDatePickerCollapsed from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.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 { dateTypes } from '../constants'; import { dateTypes } from '../constants';
import epicUtils from '../utils/epic_utils'; import epicUtils from '../utils/epic_utils';
...@@ -34,11 +37,14 @@ export default { ...@@ -34,11 +37,14 @@ export default {
SidebarSubscriptionsWidget, SidebarSubscriptionsWidget,
SidebarReferenceWidget, SidebarReferenceWidget,
SidebarTodoWidget, SidebarTodoWidget,
LabelsSelectWidget,
}, },
mixins: [glFeatureFlagMixin()],
inject: ['iid'], inject: ['iid'],
data() { data() {
return { return {
sidebarExpandedOnClick: false, sidebarExpandedOnClick: false,
LabelType,
}; };
}, },
computed: { computed: {
...@@ -60,6 +66,7 @@ export default { ...@@ -60,6 +66,7 @@ export default {
'epicDueDateSaveInProgress', 'epicDueDateSaveInProgress',
'fullPath', 'fullPath',
'epicId', 'epicId',
'epicsWebUrl',
]), ]),
...mapGetters([ ...mapGetters([
'isUserSignedIn', 'isUserSignedIn',
...@@ -231,7 +238,26 @@ export default { ...@@ -231,7 +238,26 @@ export default {
:max-date="dueDateForCollapsedSidebar" :max-date="dueDateForCollapsedSidebar"
@toggleCollapse="toggleSidebar({ sidebarCollapsed })" @toggleCollapse="toggleSidebar({ sidebarCollapsed })"
/> />
<labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block"
:iid="String(iid)"
:full-path="fullPath"
:allow-label-remove="canUpdate"
:allow-multiselect="true"
:labels-filter-base-path="epicsWebUrl"
variant="sidebar"
issuable-type="epic"
workspace-type="group"
:attr-workspace-path="fullPath"
:label-create-type="LabelType.group"
data-testid="labels-select"
@toggleCollapse="handleSidebarToggle"
>
{{ __('None') }}
</labels-select-widget>
<sidebar-labels <sidebar-labels
v-else
:can-update="canUpdate" :can-update="canUpdate"
:sidebar-collapsed="sidebarCollapsed" :sidebar-collapsed="sidebarCollapsed"
data-testid="labels-select" data-testid="labels-select"
......
...@@ -43,9 +43,13 @@ export default () => { ...@@ -43,9 +43,13 @@ export default () => {
components: { EpicApp }, components: { EpicApp },
provide: { provide: {
canUpdate: epicData.canUpdate, canUpdate: epicData.canUpdate,
allowLabelCreate: parseBoolean(epicData.canUpdate),
allowLabelEdit: parseBoolean(epicData.canUpdate),
fullPath: epicData.fullPath, fullPath: epicData.fullPath,
iid: epicMeta.epicIid, iid: epicMeta.epicIid,
isClassicSidebar: true, isClassicSidebar: true,
allowScopedLabels: epicMeta.scopedLabels,
labelsManagePath: epicMeta.labelsWebUrl,
}, },
created() { created() {
this.setEpicMeta({ this.setEpicMeta({
......
...@@ -51,13 +51,14 @@ RSpec.describe 'Assign labels to an epic', :js do ...@@ -51,13 +51,14 @@ RSpec.describe 'Assign labels to an epic', :js do
it 'opens labels dropdown' do it 'opens labels dropdown' do
page.within('aside.right-sidebar') do page.within('aside.right-sidebar') do
expect(page).to have_css('.js-labels-block .labels-select-dropdown-contents') expect(page).to have_css('.js-labels-block [data-testid="labels-select-dropdown-contents"]')
end end
end end
it 'collapses sidebar when clicked outside' do it 'collapses sidebar when clicked outside' do
wait_for_requests
page.within('.content-wrapper') do page.within('.content-wrapper') do
find('.content').click find('.epic-page-container').click
expect(page).to have_css('.right-sidebar-collapsed') expect(page).to have_css('.right-sidebar-collapsed')
end end
......
...@@ -228,8 +228,8 @@ RSpec.describe 'Epic show', :js do ...@@ -228,8 +228,8 @@ RSpec.describe 'Epic show', :js do
describe 'Labels select' do describe 'Labels select' do
it 'opens dropdown when `Edit` is clicked' do it 'opens dropdown when `Edit` is clicked' do
page.within('aside.right-sidebar') do page.within('aside.right-sidebar [data-testid="labels-select"]') do
find('.js-sidebar-dropdown-toggle').click click_button 'Edit'
end end
wait_for_requests wait_for_requests
...@@ -239,20 +239,20 @@ RSpec.describe 'Epic show', :js do ...@@ -239,20 +239,20 @@ RSpec.describe 'Epic show', :js do
context 'when dropdown is open' do context 'when dropdown is open' do
before do before do
page.within('aside.right-sidebar') do page.within('aside.right-sidebar [data-testid="labels-select"]') do
find('.js-sidebar-dropdown-toggle').click click_button 'Edit'
end end
wait_for_requests wait_for_requests
end end
it 'shows labels within the label dropdown' do it 'shows labels within the label dropdown' do
page.within('.js-labels-list .dropdown-content') do page.within('.js-labels-list [data-testid="dropdown-content"]') do
expect(page).to have_selector('li', count: 3) expect(page).to have_selector('li', count: 3)
end end
end end
it 'shows checkmark next to label when label is clicked' do it 'shows checkmark next to label when label is clicked' do
page.within('.js-labels-list .dropdown-content') do page.within('.js-labels-list [data-testid="dropdown-content"]') do
find('li', text: label1.title).click find('li', text: label1.title).click
expect(find('li', text: label1.title)).to have_selector('.gl-icon', visible: true) expect(find('li', text: label1.title)).to have_selector('.gl-icon', visible: true)
...@@ -261,7 +261,7 @@ RSpec.describe 'Epic show', :js do ...@@ -261,7 +261,7 @@ RSpec.describe 'Epic show', :js do
it 'shows label create view when `Create group label` is clicked' do it 'shows label create view when `Create group label` is clicked' do
page.within('.js-labels-block') do page.within('.js-labels-block') do
find('a', text: 'Create group label').click click_on 'Create group label'
expect(page).to have_selector('.js-labels-create') expect(page).to have_selector('.js-labels-create')
end end
...@@ -269,7 +269,7 @@ RSpec.describe 'Epic show', :js do ...@@ -269,7 +269,7 @@ RSpec.describe 'Epic show', :js do
it 'creates new label using create view' do it 'creates new label using create view' do
page.within('.js-labels-block') do page.within('.js-labels-block') do
find('a', text: 'Create group label').click click_on 'Create group label'
find('.dropdown-input .gl-form-input').set('Test label') find('.dropdown-input .gl-form-input').set('Test label')
find('.suggest-colors-dropdown a', match: :first).click find('.suggest-colors-dropdown a', match: :first).click
...@@ -278,7 +278,7 @@ RSpec.describe 'Epic show', :js do ...@@ -278,7 +278,7 @@ RSpec.describe 'Epic show', :js do
wait_for_requests wait_for_requests
end end
page.within('.js-labels-list .dropdown-content') do page.within('.js-labels-list [data-testid="dropdown-content"]') do
expect(page).to have_selector('li', count: 4) expect(page).to have_selector('li', count: 4)
expect(page).to have_content('Test label') expect(page).to have_content('Test label')
end end
...@@ -286,7 +286,7 @@ RSpec.describe 'Epic show', :js do ...@@ -286,7 +286,7 @@ RSpec.describe 'Epic show', :js do
it 'shows labels list view when `Cancel` button is clicked from create view' do it 'shows labels list view when `Cancel` button is clicked from create view' do
page.within('.js-labels-block') do page.within('.js-labels-block') do
find('a', text: 'Create group label').click click_on 'Create group label'
find('.js-btn-cancel-create').click find('.js-btn-cancel-create').click
wait_for_requests wait_for_requests
...@@ -297,7 +297,7 @@ RSpec.describe 'Epic show', :js do ...@@ -297,7 +297,7 @@ RSpec.describe 'Epic show', :js do
it 'shows labels list view when back button is clicked from create view' do it 'shows labels list view when back button is clicked from create view' do
page.within('.js-labels-block') do page.within('.js-labels-block') do
find('a', text: 'Create group label').click click_on 'Create group label'
find('.js-btn-back').click find('.js-btn-back').click
wait_for_requests wait_for_requests
......
...@@ -30,7 +30,7 @@ RSpec.describe 'Epic shortcuts', :js do ...@@ -30,7 +30,7 @@ RSpec.describe 'Epic shortcuts', :js do
it "opens labels dropdown for editing" do it "opens labels dropdown for editing" do
find('body').native.send_key('l') find('body').native.send_key('l')
expect(find('.js-labels-block')).to have_selector('.labels-select-dropdown-contents') expect(find('.js-labels-block')).to have_selector('[data-testid="labels-select-dropdown-contents"]')
end end
end end
......
...@@ -5,8 +5,7 @@ import VueApollo from 'vue-apollo'; ...@@ -5,8 +5,7 @@ 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 { workspaceLabelsQueries } from '~/sidebar/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 { import {
...@@ -50,12 +49,12 @@ describe('DropdownContentsCreateView', () => { ...@@ -50,12 +49,12 @@ describe('DropdownContentsCreateView', () => {
const createComponent = ({ const createComponent = ({
mutationHandler = createLabelSuccessHandler, mutationHandler = createLabelSuccessHandler,
issuableType = IssuableType.Issue, labelCreateType = 'project',
labelType = 'ProjectLabel', workspaceType = 'project',
} = {}) => { } = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]); const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({ mockApollo.clients.defaultClient.cache.writeQuery({
query: labelsQueries[issuableType].workspaceQuery, query: workspaceLabelsQueries[workspaceType].query,
data: workspaceLabelsQueryResponse.data, data: workspaceLabelsQueryResponse.data,
variables: { variables: {
fullPath: '', fullPath: '',
...@@ -67,10 +66,10 @@ describe('DropdownContentsCreateView', () => { ...@@ -67,10 +66,10 @@ describe('DropdownContentsCreateView', () => {
localVue, localVue,
apolloProvider: mockApollo, apolloProvider: mockApollo,
propsData: { propsData: {
issuableType,
fullPath: '', fullPath: '',
attrWorkspacePath: '', attrWorkspacePath: '',
labelType, labelCreateType,
workspaceType,
}, },
}); });
}; };
...@@ -131,9 +130,11 @@ describe('DropdownContentsCreateView', () => { ...@@ -131,9 +130,11 @@ describe('DropdownContentsCreateView', () => {
it('emits a `hideCreateView` event on Cancel button click', () => { it('emits a `hideCreateView` event on Cancel button click', () => {
createComponent(); createComponent();
findCancelButton().vm.$emit('click'); const event = { stopPropagation: jest.fn() };
findCancelButton().vm.$emit('click', event);
expect(wrapper.emitted('hideCreateView')).toHaveLength(1); expect(wrapper.emitted('hideCreateView')).toHaveLength(1);
expect(event.stopPropagation).toHaveBeenCalled();
}); });
describe('when label title and selected color are set', () => { describe('when label title and selected color are set', () => {
...@@ -177,7 +178,7 @@ describe('DropdownContentsCreateView', () => { ...@@ -177,7 +178,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, labelType: 'GroupLabel' }); createComponent({ labelCreateType: 'group', workspaceType: 'group' });
fillLabelAttributes(); fillLabelAttributes();
findCreateButton().vm.$emit('click'); findCreateButton().vm.$emit('click');
......
...@@ -59,6 +59,8 @@ describe('DropdownContentsLabelsView', () => { ...@@ -59,6 +59,8 @@ describe('DropdownContentsLabelsView', () => {
localSelectedLabels, localSelectedLabels,
issuableType: IssuableType.Issue, issuableType: IssuableType.Issue,
searchKey, searchKey,
labelCreateType: 'project',
workspaceType: 'project',
}, },
stubs: { stubs: {
GlSearchBoxByType, GlSearchBoxByType,
......
...@@ -41,7 +41,8 @@ describe('DropdownContent', () => { ...@@ -41,7 +41,8 @@ describe('DropdownContent', () => {
variant: 'sidebar', variant: 'sidebar',
issuableType: 'issue', issuableType: 'issue',
fullPath: 'test', fullPath: 'test',
labelType: 'ProjectLabel', workspaceType: 'project',
labelCreateType: 'project',
attrWorkspacePath: 'path', attrWorkspacePath: 'path',
...props, ...props,
}, },
......
...@@ -41,7 +41,8 @@ describe('LabelsSelectRoot', () => { ...@@ -41,7 +41,8 @@ describe('LabelsSelectRoot', () => {
propsData: { propsData: {
...config, ...config,
issuableType: IssuableType.Issue, issuableType: IssuableType.Issue,
labelType: 'ProjectLabel', labelCreateType: 'project',
workspaceType: 'project',
}, },
stubs: { stubs: {
SidebarEditableItem, SidebarEditableItem,
......
...@@ -80,6 +80,7 @@ export const createLabelSuccessfulResponse = { ...@@ -80,6 +80,7 @@ export const createLabelSuccessfulResponse = {
color: '#dc143c', color: '#dc143c',
description: null, description: null,
title: 'ewrwrwer', title: 'ewrwrwer',
textColor: '#000000',
__typename: 'Label', __typename: 'Label',
}, },
errors: [], errors: [],
...@@ -98,12 +99,14 @@ export const workspaceLabelsQueryResponse = { ...@@ -98,12 +99,14 @@ export const workspaceLabelsQueryResponse = {
description: null, description: null,
id: 'gid://gitlab/ProjectLabel/1', id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1', title: 'Label1',
textColor: '#000000',
}, },
{ {
color: '#2f7b2e', color: '#2f7b2e',
description: null, description: null,
id: 'gid://gitlab/ProjectLabel/2', id: 'gid://gitlab/ProjectLabel/2',
title: 'Label2', title: 'Label2',
textColor: '#000000',
}, },
], ],
}, },
...@@ -123,6 +126,7 @@ export const issuableLabelsQueryResponse = { ...@@ -123,6 +126,7 @@ export const issuableLabelsQueryResponse = {
description: null, description: null,
id: 'gid://gitlab/ProjectLabel/1', id: 'gid://gitlab/ProjectLabel/1',
title: 'Label1', title: 'Label1',
textColor: '#000000',
}, },
], ],
}, },
......
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