Commit 3c079000 authored by Florie Guibert's avatar Florie Guibert

Consolidate labels widget architecture

- Update labels within widget
parent 54cf5455
......@@ -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 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 { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
......@@ -63,7 +64,7 @@ export default {
'groupPathForActiveIssue',
'projectPathForActiveIssue',
]),
...mapState(['sidebarType', 'issuableType', 'isSettingLabels']),
...mapState(['sidebarType', 'issuableType']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
......@@ -84,7 +85,10 @@ export default {
});
},
attrWorkspacePath() {
return this.isGroupBoard ? this.groupPathForActiveIssue : undefined;
return this.isGroupBoard ? this.groupPathForActiveIssue : this.projectPathForActiveIssue;
},
labelType() {
return this.isGroupBoard ? LabelType.group : LabelType.project;
},
},
methods: {
......@@ -102,10 +106,8 @@ export default {
this.setActiveBoardItemLabels({
iid: this.activeBoardItem.iid,
projectPath: this.projectPathForActiveIssue,
addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)),
removeLabelIds: this.activeBoardItem.labels
.filter((label) => !input.find((selected) => selected.id === label.id))
.map((label) => label.id),
labelsIds: input.map((label) => getIdFromGraphQLId(label.id)),
labels: input,
});
},
handleLabelRemove(input) {
......@@ -207,14 +209,13 @@ export default {
:full-path="projectPathForActiveIssue"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
:selected-labels="activeBoardItem.labels"
:labels-select-in-progress="isSettingLabels"
:footer-create-label-title="createLabelTitle"
:footer-manage-label-title="manageLabelTitle"
:labels-create-title="createLabelTitle"
:labels-filter-base-path="projectPathForActiveIssue"
:attr-workspace-path="attrWorkspacePath"
:issuable-type="issuableType"
:label-type="labelType"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
......
......@@ -656,31 +656,43 @@ export default {
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
commit(types.SET_LABELS_LOADING, true);
const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabelsMutation,
variables: {
input: {
iid: input.iid || String(activeBoardItem.iid),
addLabelIds: input.addLabelIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
projectPath: input.projectPath,
if (!gon.features?.labelsWidget) {
const { data } = await gqlClient.mutate({
mutation: issueSetLabelsMutation,
variables: {
input: {
iid: input.iid || String(activeBoardItem.iid),
labelsIds: input.labelsIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
projectPath: input.projectPath,
},
},
},
});
});
commit(types.SET_LABELS_LOADING, false);
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
prop: 'labels',
value: data.updateIssue?.issue?.labels.nodes,
});
} else {
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, {
itemId: activeBoardItem.id,
prop: 'labels',
value: labels,
});
}
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
prop: 'labels',
value: data.updateIssue.issue.labels.nodes,
});
},
setActiveItemSubscribed: async ({ commit, getters, state }, input) => {
......
......@@ -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 SET_ACTIVE_ID = 'SET_ACTIVE_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 RESET_ISSUES = 'RESET_ISSUES';
export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
......
......@@ -195,10 +195,6 @@ export default {
Vue.set(state.boardItems[itemId], prop, value);
},
[mutationTypes.SET_LABELS_LOADING](state, isLoading) {
state.isSettingLabels = isLoading;
},
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
state.isSettingAssignees = isLoading;
},
......
......@@ -12,7 +12,6 @@ export default () => ({
listsFlags: {},
boardItemsByListId: {},
backupItemsList: [],
isSettingLabels: false,
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},
......
......@@ -11,6 +11,7 @@ import { toLabelGid } from '~/sidebar/utils';
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 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';
const mutationMap = {
......@@ -48,6 +49,7 @@ export default {
return {
isLabelsSelectInProgress: false,
selectedLabels: this.initiallySelectedLabels,
LabelType,
};
},
methods: {
......@@ -154,13 +156,11 @@ export default {
:footer-manage-label-title="__('Manage project labels')"
:labels-create-title="__('Create project label')"
:labels-filter-base-path="projectIssuesPath"
:labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels"
:variant="$options.variant"
:issuable-type="issuableType"
:attr-workspace-path="fullPath"
:label-type="LabelType.project"
data-qa-selector="labels_block"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</labels-select-widget>
......
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import { IssuableType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
......@@ -29,6 +30,7 @@ import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_conf
import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.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 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';
......@@ -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 = {
start: 'startDate',
due: 'dueDate',
......
......@@ -5,3 +5,8 @@ export const DropdownVariant = {
Standalone: 'standalone',
Embedded: 'embedded',
};
export const LabelType = {
group: 'GroupLabel',
project: 'ProjectLabel',
};
......@@ -68,8 +68,11 @@ export default {
},
attrWorkspacePath: {
type: String,
required: false,
default: undefined,
required: true,
},
labelType: {
type: String,
required: true,
},
},
data() {
......@@ -193,11 +196,11 @@ export default {
:is="dropdownContentsView"
v-model="localSelectedLabels"
:search-key="searchKey"
:selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect"
:issuable-type="issuableType"
:full-path="fullPath"
:attr-workspace-path="attrWorkspacePath"
:label-type="labelType"
@hideCreateView="toggleDropdownContentsCreateView"
/>
</template>
......
......@@ -2,10 +2,10 @@
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import { labelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
import { LabelType } from './constants';
const errorMessage = __('Error creating label.');
......@@ -30,8 +30,11 @@ export default {
},
attrWorkspacePath: {
type: String,
required: false,
default: undefined,
required: true,
},
labelType: {
type: String,
required: true,
},
},
data() {
......@@ -50,25 +53,13 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
if (this.issuableType === IssuableType.Epic) {
return {
title: this.labelTitle,
color: this.selectedColor,
groupPath: this.fullPath,
};
}
const attributePath = this.labelType === LabelType.group ? 'groupPath' : 'projectPath';
return this.attrWorkspacePath !== undefined
? {
title: this.labelTitle,
color: this.selectedColor,
groupPath: this.attrWorkspacePath,
}
: {
title: this.labelTitle,
color: this.selectedColor,
projectPath: this.fullPath,
};
return {
title: this.labelTitle,
color: this.selectedColor,
[attributePath]: this.attrWorkspacePath,
};
},
},
methods: {
......
......@@ -19,10 +19,6 @@ export default {
prop: 'localSelectedLabels',
},
props: {
selectedLabels: {
type: Array,
required: true,
},
allowMultiselect: {
type: Boolean,
required: true,
......
<script>
import { MutationOperationMode } from '~/graphql_shared/utils';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { labelsQueries } from '~/sidebar/constants';
import { labelsQueries, labelsMutations } from '~/sidebar/constants';
import { DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
......@@ -50,16 +52,6 @@ export default {
required: false,
default: DropdownVariant.Sidebar,
},
selectedLabels: {
type: Array,
required: false,
default: () => [],
},
labelsSelectInProgress: {
type: Boolean,
required: false,
default: false,
},
labelsFilterBasePath: {
type: String,
required: false,
......@@ -106,14 +98,18 @@ export default {
},
attrWorkspacePath: {
type: String,
required: false,
default: undefined,
required: true,
},
labelType: {
type: String,
required: true,
},
},
data() {
return {
contentIsOnViewport: true,
issuableLabels: [],
labelsSelectInProgress: false,
};
},
computed: {
......@@ -136,7 +132,9 @@ export default {
};
},
update(data) {
return data.workspace?.issuable?.labels.nodes || [];
const labels = data.workspace?.issuable?.labels.nodes || [];
this.selected = labels;
return labels;
},
error() {
createFlash({ message: __('Error fetching labels.') });
......@@ -145,6 +143,10 @@ export default {
},
methods: {
handleDropdownClose(labels) {
if (this.iid !== '') {
this.updateSelectedLabels(this.getUpdateVariables(labels));
}
this.$emit('updateSelectedLabels', labels);
this.collapseEditableItem();
},
......@@ -154,6 +156,72 @@ export default {
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
getUpdateVariables(labels) {
let labelIds = [];
labelIds = labels.map(({ id }) => id);
switch (this.issuableType) {
case IssuableType.Issue:
return {
iid: this.iid,
projectPath: this.fullPath,
labelIds,
};
case IssuableType.MergeRequest:
return {
iid: this.iid,
labelIds,
operationMode: MutationOperationMode.Replace,
projectPath: this.fullPath,
};
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();
}
})
.catch(() => createFlash({ message: __('An error occurred while updating labels.') }))
.finally(() => {
this.labelsSelectInProgress = false;
});
},
getRemoveVariables(labelId) {
switch (this.issuableType) {
case IssuableType.Issue:
return {
iid: this.iid,
projectPath: this.fullPath,
removeLabelIds: [labelId],
};
case IssuableType.MergeRequest:
return {
iid: this.iid,
labelIds: [labelId],
operationMode: MutationOperationMode.Remove,
projectPath: this.fullPath,
};
default:
return {};
}
},
handleLabelRemove(labelId) {
this.updateSelectedLabels(this.getRemoveVariables(labelId));
this.$emit('onLabelRemove', labelId);
},
isDropdownVariantSidebar,
isDropdownVariantStandalone,
isDropdownVariantEmbedded,
......@@ -188,7 +256,7 @@ export default {
:allow-label-remove="allowLabelRemove"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@onLabelRemove="$emit('onLabelRemove', $event)"
@onLabelRemove="handleLabelRemove"
>
<slot></slot>
</dropdown-value>
......@@ -201,7 +269,7 @@ export default {
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
class="gl-mb-2"
@onLabelRemove="$emit('onLabelRemove', $event)"
@onLabelRemove="handleLabelRemove"
>
<slot></slot>
</dropdown-value>
......@@ -212,12 +280,13 @@ export default {
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels"
:selected-labels="issuableLabels"
:variant="variant"
:issuable-type="issuableType"
:is-visible="edit"
:full-path="fullPath"
:attr-workspace-path="attrWorkspacePath"
:label-type="labelType"
@setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
......@@ -233,10 +302,12 @@ export default {
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
:labels-create-title="labelsCreateTitle"
:selected-labels="selectedLabels"
:selected-labels="issuableLabels"
:variant="variant"
:issuable-type="issuableType"
:full-path="fullPath"
:attr-workspace-path="attrWorkspacePath"
:label-type="labelType"
@setLabels="handleDropdownClose"
/>
</div>
......
......@@ -14,6 +14,7 @@ import { s__ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.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 { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createEpic from '../queries/createEpic.mutation.graphql';
......@@ -31,7 +32,6 @@ export default {
},
mixins: [glFeatureFlagMixin()],
inject: [
'iid',
'groupPath',
'groupEpicsPath',
'labelsFetchPath',
......@@ -48,6 +48,7 @@ export default {
startDateFixed: null,
dueDateFixed: null,
loading: false,
LabelType,
};
},
computed: {
......@@ -190,13 +191,13 @@ export default {
<labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block"
:iid="iid"
:full-path="groupPath"
:allow-label-create="true"
:allow-multiselect="true"
:allow-scoped-labels="false"
:labels-filter-base-path="groupEpicsPath"
:selected-labels="labels"
:attr-workspace-path="groupPath"
:label-type="LabelType.group"
issuable-type="epic"
variant="embedded"
data-qa-selector="labels_block"
......
......@@ -11,12 +11,11 @@ RSpec.describe 'Labels Hierarchy', :js do
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!(:child_group_label) { create(:group_label, group: child, title: 'Label_3') }
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_3') }
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_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) }
before do
......@@ -26,8 +25,8 @@ RSpec.describe 'Labels Hierarchy', :js do
end
shared_examples 'filter for scoped boards' do |project = false|
it 'scopes board to ancestor and descendant labels' do
labels = [grandparent_group_label, parent_group_label, child_group_label]
it 'scopes board to ancestor and current group labels' do
labels = [grandparent_group_label, parent_group_label]
labels.push(project_label_1) if project
labels.each do |label|
......
......@@ -1572,12 +1572,13 @@ describe('setActiveIssueLabels', () => {
const getters = { activeBoardItem: mockIssue };
const testLabelIds = labels.map((label) => label.id);
const input = {
addLabelIds: testLabelIds,
labelsIds: testLabelIds,
removeLabelIds: [],
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
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
......@@ -1593,14 +1594,6 @@ describe('setActiveIssueLabels', () => {
input,
{ ...state, ...getters },
[
{
type: types.SET_LABELS_LOADING,
payload: true,
},
{
type: types.SET_LABELS_LOADING,
payload: false,
},
{
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
......
......@@ -51,6 +51,7 @@ describe('DropdownContentsCreateView', () => {
const createComponent = ({
mutationHandler = createLabelSuccessHandler,
issuableType = IssuableType.Issue,
labelType = 'ProjectLabel',
} = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
......@@ -68,6 +69,8 @@ describe('DropdownContentsCreateView', () => {
propsData: {
issuableType,
fullPath: '',
attrWorkspacePath: '',
labelType,
},
});
};
......@@ -174,7 +177,7 @@ describe('DropdownContentsCreateView', () => {
});
it('calls a mutation with `groupPath` variable on the epic', () => {
createComponent({ issuableType: IssuableType.Epic });
createComponent({ issuableType: IssuableType.Epic, labelType: 'GroupLabel' });
fillLabelAttributes();
findCreateButton().vm.$emit('click');
......
......@@ -41,6 +41,8 @@ describe('DropdownContent', () => {
variant: 'sidebar',
issuableType: 'issue',
fullPath: 'test',
labelType: 'ProjectLabel',
attrWorkspacePath: 'path',
...props,
},
data() {
......
......@@ -41,6 +41,7 @@ describe('LabelsSelectRoot', () => {
propsData: {
...config,
issuableType: IssuableType.Issue,
labelType: 'ProjectLabel',
},
stubs: {
SidebarEditableItem,
......
......@@ -40,12 +40,12 @@ export const mockConfig = {
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
variant: 'sidebar',
selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false,
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
attrWorkspacePath: 'test',
};
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