Commit 6f1082ba authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '332327-feature-flag-rollout-of-labels_widget' into 'master'

Clean up labels_widget feature flag

See merge request gitlab-org/gitlab!75302
parents 6491ae14 4e53ae6c
...@@ -3,6 +3,7 @@ import Mousetrap from 'mousetrap'; ...@@ -3,6 +3,7 @@ import Mousetrap from 'mousetrap';
import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard'; import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard';
import { getSelectedFragment } from '~/lib/utils/common_utils'; import { getSelectedFragment } from '~/lib/utils/common_utils';
import { isElementVisible } from '~/lib/utils/dom_utils'; import { isElementVisible } from '~/lib/utils/dom_utils';
import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import Sidebar from '../../right_sidebar'; import Sidebar from '../../right_sidebar';
import { CopyAsGFM } from '../markdown/copy_as_gfm'; import { CopyAsGFM } from '../markdown/copy_as_gfm';
import { import {
...@@ -114,6 +115,11 @@ export default class ShortcutsIssuable extends Shortcuts { ...@@ -114,6 +115,11 @@ export default class ShortcutsIssuable extends Shortcuts {
static openSidebarDropdown(name) { static openSidebarDropdown(name) {
Sidebar.instance.openDropdown(name); Sidebar.instance.openDropdown(name);
// Wait for the sidebar to trigger('click') open
// so it doesn't cause our dropdown to close preemptively
setTimeout(() => {
document.querySelector(`.block.${name} .shortcut-sidebar-dropdown-toggle`).click();
}, DEBOUNCE_DROPDOWN_DELAY);
return false; return false;
} }
......
...@@ -4,7 +4,6 @@ import { MountingPortal } from 'portal-vue'; ...@@ -4,7 +4,6 @@ import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue'; import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
...@@ -26,7 +25,6 @@ export default { ...@@ -26,7 +25,6 @@ export default {
SidebarDateWidget, SidebarDateWidget,
SidebarConfidentialityWidget, SidebarConfidentialityWidget,
BoardSidebarTimeTracker, BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
SidebarLabelsWidget, SidebarLabelsWidget,
SidebarSubscriptionsWidget, SidebarSubscriptionsWidget,
SidebarDropdownWidget, SidebarDropdownWidget,
...@@ -210,7 +208,6 @@ export default { ...@@ -210,7 +208,6 @@ export default {
data-testid="sidebar-due-date" data-testid="sidebar-due-date"
/> />
<sidebar-labels-widget <sidebar-labels-widget
v-if="glFeatures.labelsWidget"
class="block labels" class="block labels"
data-testid="sidebar-labels" data-testid="sidebar-labels"
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"
...@@ -230,7 +227,6 @@ export default { ...@@ -230,7 +227,6 @@ export default {
> >
{{ __('None') }} {{ __('None') }}
</sidebar-labels-widget> </sidebar-labels-widget>
<board-sidebar-labels-select v-else class="block labels" />
<sidebar-weight-widget <sidebar-weight-widget
v-if="weightFeatureAvailable" v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"
......
...@@ -39,7 +39,6 @@ import boardLabelsQuery from '../graphql/board_labels.query.graphql'; ...@@ -39,7 +39,6 @@ import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql'; import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql'; import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql'; import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
...@@ -609,33 +608,6 @@ export default { ...@@ -609,33 +608,6 @@ export default {
setActiveIssueLabels: async ({ commit, getters }, input) => { setActiveIssueLabels: async ({ commit, getters }, input) => {
const { activeBoardItem } = getters; const { activeBoardItem } = getters;
if (!gon.features?.labelsWidget) {
const { data } = await gqlClient.mutate({
mutation: issueSetLabelsMutation,
variables: {
input: {
iid: input.iid || String(activeBoardItem.iid),
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.UPDATE_BOARD_ITEM_BY_ID, {
itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
prop: 'labels',
value: data.updateIssue?.issue?.labels.nodes,
});
return;
}
let labels = input?.labels || []; let labels = input?.labels || [];
if (input.removeLabelIds) { if (input.removeLabelIds) {
labels = activeBoardItem.labels.filter( labels = activeBoardItem.labels.filter(
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
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';
...@@ -127,14 +126,6 @@ Sidebar.prototype.openDropdown = function (blockOrName) { ...@@ -127,14 +126,6 @@ Sidebar.prototype.openDropdown = function (blockOrName) {
this.setCollapseAfterUpdate($block); this.setCollapseAfterUpdate($block);
this.toggleSidebar('open'); this.toggleSidebar('open');
} }
// Wait for the sidebar to trigger('click') open
// so it doesn't cause our dropdown to close preemptively
setTimeout(() => {
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) {
......
<script> <script>
import $ from 'jquery';
import { camelCase, difference, union } from 'lodash';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import createFlash from '~/flash';
import { getIdFromGraphQLId, MutationOperationMode } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
import { __ } from '~/locale';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
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 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 { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const mutationMap = {
[IssuableType.Issue]: {
mutation: updateIssueLabelsMutation,
mutationName: 'updateIssue',
},
[IssuableType.MergeRequest]: {
mutation: updateMergeRequestLabelsMutation,
mutationName: 'mergeRequestSetLabels',
},
};
export default { export default {
components: { components: {
LabelsSelect,
LabelsSelectWidget, LabelsSelectWidget,
}, },
variant: DropdownVariant.Sidebar, variant: DropdownVariant.Sidebar,
mixins: [glFeatureFlagMixin()], inject: ['allowLabelEdit', 'iid', 'fullPath', 'issuableType', 'projectIssuesPath'],
inject: [
'allowLabelCreate',
'allowLabelEdit',
'allowScopedLabels',
'iid',
'fullPath',
'initiallySelectedLabels',
'issuableType',
'labelsFetchPath',
'labelsManagePath',
'projectIssuesPath',
'projectPath',
],
data() { data() {
return { return {
isLabelsSelectInProgress: false,
selectedLabels: this.initiallySelectedLabels,
LabelType, LabelType,
}; };
}, },
methods: {
handleDropdownClose() {
$(this.$el).trigger('hidden.gl.dropdown');
},
getUpdateVariables(labels) {
let labelIds = [];
if (this.glFeatures.labelsWidget) {
labelIds = labels.map(({ id }) => toLabelGid(id));
} else {
const currentLabelIds = this.selectedLabels.map((label) => label.id);
const userAddedLabelIds = labels.filter((label) => label.set).map((label) => label.id);
const userRemovedLabelIds = labels.filter((label) => !label.set).map((label) => label.id);
labelIds = difference(union(currentLabelIds, userAddedLabelIds), userRemovedLabelIds).map(
toLabelGid,
);
}
switch (this.issuableType) {
case IssuableType.Issue:
return {
iid: this.iid,
projectPath: this.projectPath,
labelIds,
};
case IssuableType.MergeRequest:
return {
iid: this.iid,
labelIds,
operationMode: MutationOperationMode.Replace,
projectPath: this.projectPath,
};
default:
return {};
}
},
handleUpdateSelectedLabels(dropdownLabels) {
this.updateSelectedLabels(this.getUpdateVariables(dropdownLabels));
},
getRemoveVariables(labelId) {
switch (this.issuableType) {
case IssuableType.Issue:
return {
iid: this.iid,
projectPath: this.projectPath,
removeLabelIds: [labelId],
};
case IssuableType.MergeRequest:
return {
iid: this.iid,
labelIds: [toLabelGid(labelId)],
operationMode: MutationOperationMode.Remove,
projectPath: this.projectPath,
};
default:
return {};
}
},
handleLabelRemove(labelId) {
this.updateSelectedLabels(this.getRemoveVariables(labelId));
},
updateSelectedLabels(inputVariables) {
this.isLabelsSelectInProgress = true;
this.$apollo
.mutate({
mutation: mutationMap[this.issuableType].mutation,
variables: { input: inputVariables },
})
.then(({ data }) => {
const { mutationName } = mutationMap[this.issuableType];
if (data[mutationName]?.errors?.length) {
throw new Error();
}
const issuableType = camelCase(this.issuableType);
this.selectedLabels = data[mutationName]?.[issuableType]?.labels?.nodes?.map((label) => ({
...label,
id: getIdFromGraphQLId(label.id),
}));
})
.catch(() => createFlash({ message: __('An error occurred while updating labels.') }))
.finally(() => {
this.isLabelsSelectInProgress = false;
});
},
},
}; };
</script> </script>
<template> <template>
<labels-select-widget <labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block" class="block labels js-labels-block"
:iid="iid" :iid="iid"
:full-path="fullPath" :full-path="fullPath"
...@@ -165,28 +37,4 @@ export default { ...@@ -165,28 +37,4 @@ export default {
> >
{{ __('None') }} {{ __('None') }}
</labels-select-widget> </labels-select-widget>
<labels-select
v-else
class="block labels js-labels-block"
:allow-label-remove="allowLabelEdit"
:allow-label-create="allowLabelCreate"
:allow-label-edit="allowLabelEdit"
:allow-multiselect="true"
:allow-scoped-labels="allowScopedLabels"
:footer-create-label-title="__('Create project label')"
:footer-manage-label-title="__('Manage project labels')"
:labels-create-title="__('Create project label')"
:labels-fetch-path="labelsFetchPath"
:labels-filter-base-path="projectIssuesPath"
:labels-manage-path="labelsManagePath"
:labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels"
:variant="$options.sidebar"
data-qa-selector="labels_block"
@onDropdownClose="handleDropdownClose"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</labels-select>
</template> </template>
...@@ -231,9 +231,10 @@ export default { ...@@ -231,9 +231,10 @@ export default {
throw new Error(); throw new Error();
} }
this.issuableLabels = data[mutationName]?.[this.issuableType]?.labels?.nodes;
this.$emit('updateSelectedLabels', { this.$emit('updateSelectedLabels', {
id: data[mutationName]?.[this.issuableType]?.id, id: data[mutationName]?.[this.issuableType]?.id,
labels: data[mutationName]?.[this.issuableType]?.labels?.nodes, labels: this.issuableLabels,
}); });
}) })
.catch((error) => .catch((error) =>
......
...@@ -11,7 +11,6 @@ class Groups::BoardsController < Groups::ApplicationController ...@@ -11,7 +11,6 @@ class Groups::BoardsController < Groups::ApplicationController
push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml) push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml) push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml)
experiment(:prominent_create_board_btn, subject: current_user) do |e| experiment(:prominent_create_board_btn, subject: current_user) do |e|
e.use { } e.use { }
e.try { } e.try { }
......
...@@ -11,7 +11,6 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -11,7 +11,6 @@ class Projects::BoardsController < Projects::ApplicationController
push_frontend_feature_flag(:issue_boards_filtered_search, project, default_enabled: :yaml) push_frontend_feature_flag(:issue_boards_filtered_search, project, default_enabled: :yaml)
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml) push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
experiment(:prominent_create_board_btn, subject: current_user) do |e| experiment(:prominent_create_board_btn, subject: current_user) do |e|
e.use { } e.use { }
e.try { } e.try { }
......
...@@ -51,7 +51,6 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -51,7 +51,6 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:real_time_issue_sidebar, @project, default_enabled: :yaml) push_frontend_feature_flag(:real_time_issue_sidebar, @project, default_enabled: :yaml)
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml) push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml) push_frontend_feature_flag(:issue_assignees_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml) push_frontend_feature_flag(:paginated_issue_discussions, @project, default_enabled: :yaml)
experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance| experiment(:invite_members_in_comment, namespace: @project.root_ancestor) do |experiment_instance|
......
...@@ -42,7 +42,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -42,7 +42,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml) push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
push_frontend_feature_flag(:mr_changes_fluid_layout, project, default_enabled: :yaml) push_frontend_feature_flag(:mr_changes_fluid_layout, project, default_enabled: :yaml)
push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml) push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
# Usage data feature flags # Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml) push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
......
---
name: labels_widget
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62898
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332327
milestone: '14.0'
type: development
group: group::project management
default_enabled: false
import $ from 'jquery'; import $ from 'jquery';
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import { import {
keysFor, keysFor,
...@@ -8,7 +7,6 @@ import { ...@@ -8,7 +7,6 @@ import {
ISSUABLE_EDIT_DESCRIPTION, ISSUABLE_EDIT_DESCRIPTION,
} from '~/behaviors/shortcuts/keybindings'; } from '~/behaviors/shortcuts/keybindings';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { parseBoolean } from '~/lib/utils/common_utils';
export default class ShortcutsEpic extends ShortcutsIssuable { export default class ShortcutsEpic extends ShortcutsIssuable {
constructor() { constructor() {
...@@ -23,15 +21,7 @@ export default class ShortcutsEpic extends ShortcutsIssuable { ...@@ -23,15 +21,7 @@ export default class ShortcutsEpic extends ShortcutsIssuable {
Mousetrap.bind(keysFor(ISSUABLE_EDIT_DESCRIPTION), ShortcutsIssuable.editIssue); Mousetrap.bind(keysFor(ISSUABLE_EDIT_DESCRIPTION), ShortcutsIssuable.editIssue);
} }
static openSidebarDropdown($block) { static openSidebarDropdown() {
if (gon.features?.labelsWidget) { document.dispatchEvent(new Event('toggleSidebarRevealLabelsDropdown'));
document.dispatchEvent(new Event('toggleSidebarRevealLabelsDropdown'));
return;
}
if (parseBoolean(Cookies.get('collapsed_gutter'))) {
document.dispatchEvent(new Event('toggleSidebarRevealLabelsDropdown'));
} else {
$block.find('.js-sidebar-dropdown-toggle').get(0).dispatchEvent(new Event('click'));
}
} }
} }
...@@ -3,7 +3,6 @@ import { GlDrawer } from '@gitlab/ui'; ...@@ -3,7 +3,6 @@ import { GlDrawer } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue'; import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue'; import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
...@@ -19,7 +18,6 @@ export default { ...@@ -19,7 +18,6 @@ export default {
components: { components: {
GlDrawer, GlDrawer,
SidebarTodoWidget, SidebarTodoWidget,
BoardSidebarLabelsSelect,
BoardSidebarTitle, BoardSidebarTitle,
SidebarLabelsWidget, SidebarLabelsWidget,
SidebarConfidentialityWidget, SidebarConfidentialityWidget,
...@@ -113,7 +111,6 @@ export default { ...@@ -113,7 +111,6 @@ export default {
:can-inherit="true" :can-inherit="true"
/> />
<sidebar-labels-widget <sidebar-labels-widget
v-if="glFeatures.labelsWidget"
class="block labels" class="block labels"
data-testid="sidebar-labels" data-testid="sidebar-labels"
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"
...@@ -130,7 +127,6 @@ export default { ...@@ -130,7 +127,6 @@ export default {
> >
{{ __('None') }} {{ __('None') }}
</sidebar-labels-widget> </sidebar-labels-widget>
<board-sidebar-labels-select v-else class="labels" />
<sidebar-confidentiality-widget <sidebar-confidentiality-widget
:iid="activeBoardItem.iid" :iid="activeBoardItem.iid"
:full-path="fullPath" :full-path="fullPath"
......
mutation updateBoardEpicLabels($input: UpdateEpicInput!) {
updateEpic(input: $input) {
epic {
id
labels {
nodes {
id
title
color
description
}
}
}
errors
}
}
...@@ -36,7 +36,6 @@ import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics ...@@ -36,7 +36,6 @@ import listUpdateLimitMetricsMutation from '../graphql/list_update_limit_metrics
import listsEpicsQuery from '../graphql/lists_epics.query.graphql'; import listsEpicsQuery from '../graphql/lists_epics.query.graphql';
import subGroupsQuery from '../graphql/sub_groups.query.graphql'; import subGroupsQuery from '../graphql/sub_groups.query.graphql';
import updateBoardEpicUserPreferencesMutation from '../graphql/update_board_epic_user_preferences.mutation.graphql'; import updateBoardEpicUserPreferencesMutation from '../graphql/update_board_epic_user_preferences.mutation.graphql';
import updateEpicLabelsMutation from '../graphql/update_epic_labels.mutation.graphql';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -631,35 +630,9 @@ export default { ...@@ -631,35 +630,9 @@ export default {
} }
}, },
setActiveEpicLabels: async ({ commit, getters, state }, input) => { setActiveEpicLabels: async ({ commit, getters }, input) => {
const { activeBoardItem } = getters; const { activeBoardItem } = getters;
if (!gon.features?.labelsWidget) {
const { data } = await gqlClient.mutate({
mutation: updateEpicLabelsMutation,
variables: {
input: {
iid: String(activeBoardItem.iid),
addLabelIds: input.addLabelIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
groupPath: state.fullPath,
},
},
});
if (data.updateEpic?.errors?.length > 0) {
throw new Error(data.updateEpic.errors);
}
commit(typesCE.UPDATE_BOARD_ITEM_BY_ID, {
itemId: activeBoardItem.id,
prop: 'labels',
value: data.updateEpic.epic.labels.nodes,
});
return;
}
let labels = input?.labels || []; let labels = input?.labels || [];
if (input.removeLabelIds) { if (input.removeLabelIds) {
labels = activeBoardItem.labels.filter( labels = activeBoardItem.labels.filter(
......
...@@ -13,10 +13,8 @@ import { formatDate } from '~/lib/utils/datetime_utility'; ...@@ -13,10 +13,8 @@ import { formatDate } from '~/lib/utils/datetime_utility';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import LabelsSelectVue from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import 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 { 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'; import createEpic from '../queries/createEpic.mutation.graphql';
export default { export default {
...@@ -28,18 +26,9 @@ export default { ...@@ -28,18 +26,9 @@ export default {
GlFormInput, GlFormInput,
GlFormGroup, GlFormGroup,
MarkdownField, MarkdownField,
LabelsSelectVue,
LabelsSelectWidget, LabelsSelectWidget,
}, },
mixins: [glFeatureFlagMixin()], inject: ['groupPath', 'groupEpicsPath', 'markdownPreviewPath', 'markdownDocsPath'],
inject: [
'groupPath',
'groupEpicsPath',
'labelsFetchPath',
'labelsManagePath',
'markdownPreviewPath',
'markdownDocsPath',
],
data() { data() {
return { return {
title: '', title: '',
...@@ -113,20 +102,7 @@ export default { ...@@ -113,20 +102,7 @@ export default {
this.startDateFixed = val; this.startDateFixed = val;
}, },
handleUpdateSelectedLabels(labels) { handleUpdateSelectedLabels(labels) {
if (this.glFeatures.labelsWidget) { this.labels = labels.map((label) => ({ ...label, id: getIdFromGraphQLId(label.id) }));
this.labels = labels.map((label) => ({ ...label, id: getIdFromGraphQLId(label.id) }));
return;
}
const ids = [];
const allLabels = [...labels, ...this.labels];
this.labels = allLabels.filter((label) => {
const exists = ids.includes(label.id);
ids.push(label.id);
return !exists && label.set;
});
}, },
}, },
}; };
...@@ -190,7 +166,6 @@ export default { ...@@ -190,7 +166,6 @@ export default {
<hr /> <hr />
<gl-form-group :label="__('Labels')"> <gl-form-group :label="__('Labels')">
<labels-select-widget <labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block" class="block labels js-labels-block"
:full-path="groupPath" :full-path="groupPath"
:allow-label-create="true" :allow-label-create="true"
...@@ -207,24 +182,6 @@ export default { ...@@ -207,24 +182,6 @@ export default {
> >
{{ __('None') }} {{ __('None') }}
</labels-select-widget> </labels-select-widget>
<labels-select-vue
v-else
:allow-label-edit="false"
:allow-label-create="true"
:allow-multiselect="true"
:allow-scoped-labels="false"
:selected-labels="labels"
:labels-fetch-path="labelsFetchPath"
:labels-manage-path="labelsManagePath"
:labels-filter-base-path="groupEpicsPath"
:labels-list-title="__('Select label')"
:dropdown-button-text="__('Choose labels')"
variant="embedded"
class="block labels js-labels-block"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</labels-select-vue>
</gl-form-group> </gl-form-group>
<gl-form-group :label="__('Start date')" :description="$options.i18n.epicDatesHint"> <gl-form-group :label="__('Start date')" :description="$options.i18n.epicDatesHint">
<div class="gl-display-inline-block gl-mr-2"> <div class="gl-display-inline-block gl-mr-2">
......
...@@ -22,7 +22,6 @@ import { dateTypes } from '../constants'; ...@@ -22,7 +22,6 @@ import { dateTypes } from '../constants';
import epicUtils from '../utils/epic_utils'; import epicUtils from '../utils/epic_utils';
import SidebarDatePicker from './sidebar_items/sidebar_date_picker.vue'; import SidebarDatePicker from './sidebar_items/sidebar_date_picker.vue';
import SidebarHeader from './sidebar_items/sidebar_header.vue'; import SidebarHeader from './sidebar_items/sidebar_header.vue';
import SidebarLabels from './sidebar_items/sidebar_labels.vue';
export default { export default {
dateTypes, dateTypes,
...@@ -30,7 +29,6 @@ export default { ...@@ -30,7 +29,6 @@ export default {
SidebarHeader, SidebarHeader,
SidebarDatePicker, SidebarDatePicker,
SidebarDatePickerCollapsed, SidebarDatePickerCollapsed,
SidebarLabels,
SidebarAncestorsWidget, SidebarAncestorsWidget,
SidebarParticipantsWidget, SidebarParticipantsWidget,
SidebarConfidentialityWidget, SidebarConfidentialityWidget,
...@@ -239,7 +237,6 @@ export default { ...@@ -239,7 +237,6 @@ export default {
@toggleCollapse="toggleSidebar({ sidebarCollapsed })" @toggleCollapse="toggleSidebar({ sidebarCollapsed })"
/> />
<labels-select-widget <labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block" class="block labels js-labels-block"
:iid="String(iid)" :iid="String(iid)"
:full-path="fullPath" :full-path="fullPath"
...@@ -256,12 +253,6 @@ export default { ...@@ -256,12 +253,6 @@ export default {
> >
{{ __('None') }} {{ __('None') }}
</labels-select-widget> </labels-select-widget>
<sidebar-labels
v-else
:can-update="canUpdate"
:sidebar-collapsed="sidebarCollapsed"
data-testid="labels-select"
/>
<sidebar-confidentiality-widget <sidebar-confidentiality-widget
:iid="String(iid)" :iid="String(iid)"
:full-path="fullPath" :full-path="fullPath"
......
...@@ -8,9 +8,6 @@ class Groups::EpicBoardsController < Groups::ApplicationController ...@@ -8,9 +8,6 @@ class Groups::EpicBoardsController < Groups::ApplicationController
before_action :redirect_to_recent_board, only: [:index] before_action :redirect_to_recent_board, only: [:index]
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml)
end
track_redis_hll_event :index, :show, name: 'g_project_management_users_viewing_epic_boards' track_redis_hll_event :index, :show, name: 'g_project_management_users_viewing_epic_boards'
......
...@@ -23,7 +23,6 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -23,7 +23,6 @@ class Groups::EpicsController < Groups::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:vue_epics_list, @group, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:vue_epics_list, @group, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:improved_emoji_picker, @group, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:improved_emoji_picker, @group, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, @group, default_enabled: :yaml)
end end
feature_category :portfolio_management feature_category :portfolio_management
......
...@@ -69,9 +69,29 @@ exports[`ee/BoardContentSidebar matches the snapshot 1`] = ` ...@@ -69,9 +69,29 @@ exports[`ee/BoardContentSidebar matches the snapshot 1`] = `
issuabletype="issue" issuabletype="issue"
/> />
<boardsidebarlabelsselect-stub <sidebarlabelswidget-stub
allowmultiselect="true"
attrworkspacepath="gitlab-org/gitlab-test"
class="block labels" class="block labels"
/> data-testid="sidebar-labels"
dropdownbuttontext="Label"
footercreatelabeltitle="Create project label"
footermanagelabeltitle="Manage project labels"
fullpath="gitlab-org/gitlab-test"
iid="27"
issuabletype="issue"
labelcreatetype="project"
labelscreatetitle="Create project label"
labelsfilterbasepath=""
labelsfilterparam="label_name"
labelslisttitle="Assign labels"
variant="sidebar"
workspacetype="project"
>
None
</sidebarlabelswidget-stub>
<sidebarweightwidget-stub <sidebarweightwidget-stub
full-path="gitlab-org/gitlab-test" full-path="gitlab-org/gitlab-test"
......
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import { ISSUABLE, issuableTypes } from '~/boards/constants'; import { ISSUABLE, issuableTypes } from '~/boards/constants';
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data'; import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
Vue.use(Vuex);
describe('ee/BoardContentSidebar', () => { describe('ee/BoardContentSidebar', () => {
let wrapper; let wrapper;
let store; let store;
...@@ -25,6 +28,7 @@ describe('ee/BoardContentSidebar', () => { ...@@ -25,6 +28,7 @@ describe('ee/BoardContentSidebar', () => {
projectPathForActiveIssue: () => mockIssueProjectPath, projectPathForActiveIssue: () => mockIssueProjectPath,
groupPathForActiveIssue: () => mockIssueGroupPath, groupPathForActiveIssue: () => mockIssueGroupPath,
isSidebarOpen: () => true, isSidebarOpen: () => true,
isGroupBoard: () => false,
...mockGetters, ...mockGetters,
}, },
actions: mockActions, actions: mockActions,
...@@ -69,7 +73,7 @@ describe('ee/BoardContentSidebar', () => { ...@@ -69,7 +73,7 @@ describe('ee/BoardContentSidebar', () => {
BoardEditableItem: true, BoardEditableItem: true,
BoardSidebarTitle: true, BoardSidebarTitle: true,
BoardSidebarTimeTracker: true, BoardSidebarTimeTracker: true,
BoardSidebarLabelsSelect: true, SidebarLabelsWidget: true,
SidebarAssigneesWidget: true, SidebarAssigneesWidget: true,
SidebarConfidentialityWidget: true, SidebarConfidentialityWidget: true,
SidebarDateWidget: true, SidebarDateWidget: true,
......
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue'; import { MountingPortal } from 'portal-vue';
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import EpicBoardContentSidebar from 'ee_component/boards/components/epic_board_content_sidebar.vue'; import EpicBoardContentSidebar from 'ee_component/boards/components/epic_board_content_sidebar.vue';
import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue'; import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tree/sidebar_ancestors_widget.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
...@@ -13,8 +13,11 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue ...@@ -13,8 +13,11 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue'; import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_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 LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { mockFormattedBoardEpic } from '../mock_data'; import { mockFormattedBoardEpic } from '../mock_data';
Vue.use(Vuex);
describe('EpicBoardContentSidebar', () => { describe('EpicBoardContentSidebar', () => {
let wrapper; let wrapper;
let store; let store;
...@@ -95,8 +98,8 @@ describe('EpicBoardContentSidebar', () => { ...@@ -95,8 +98,8 @@ describe('EpicBoardContentSidebar', () => {
); );
}); });
it('renders BoardSidebarLabelsSelect', () => { it('renders LabelsSelectWidget', () => {
expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true); expect(wrapper.findComponent(LabelsSelectWidget).exists()).toBe(true);
}); });
it('renders BoardSidebarTitle', () => { it('renders BoardSidebarTitle', () => {
......
...@@ -1340,7 +1340,7 @@ describe('setSelectedGroup', () => { ...@@ -1340,7 +1340,7 @@ describe('setSelectedGroup', () => {
describe('setActiveEpicLabels', () => { describe('setActiveEpicLabels', () => {
const state = { boardItems: { [mockEpic.id]: mockEpic } }; const state = { boardItems: { [mockEpic.id]: mockEpic } };
const getters = { activeBoardItem: mockEpic }; const getters = { activeBoardItem: { ...mockEpic, labels } };
const testLabelIds = labels.map((label) => label.id); const testLabelIds = labels.map((label) => label.id);
const input = { const input = {
addLabelIds: testLabelIds, addLabelIds: testLabelIds,
...@@ -1348,11 +1348,7 @@ describe('setActiveEpicLabels', () => { ...@@ -1348,11 +1348,7 @@ describe('setActiveEpicLabels', () => {
groupPath: 'h/b', groupPath: 'h/b',
}; };
it('should assign labels on success', (done) => { it('should assign labels', () => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateEpic: { epic: { labels: { nodes: labels } } } } });
const payload = { const payload = {
itemId: getters.activeBoardItem.id, itemId: getters.activeBoardItem.id,
prop: 'labels', prop: 'labels',
...@@ -1365,78 +1361,32 @@ describe('setActiveEpicLabels', () => { ...@@ -1365,78 +1361,32 @@ describe('setActiveEpicLabels', () => {
{ ...state, ...getters }, { ...state, ...getters },
[ [
{ {
type: typesCE.UPDATE_BOARD_ITEM_BY_ID, type: types.UPDATE_BOARD_ITEM_BY_ID,
payload, payload,
}, },
], ],
[], [],
done,
); );
}); });
it('throws error if fails', async () => { it('should remove label', () => {
jest const payload = {
.spyOn(gqlClient, 'mutate') itemId: getters.activeBoardItem.id,
.mockResolvedValue({ data: { updateEpic: { errors: ['failed mutation'] } } }); prop: 'labels',
value: [labels[1]],
await expect(actions.setActiveEpicLabels({ 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.setActiveEpicLabels,
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( testAction(
actions.setActiveEpicLabels, actions.setActiveEpicLabels,
{ ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] }, { ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] },
{ ...state, ...getters }, { ...state, ...getters },
[ [
{ {
type: types.UPDATE_BOARD_ITEM_BY_ID, type: types.UPDATE_BOARD_ITEM_BY_ID,
payload, payload,
}, },
], ],
[], [],
); );
});
}); });
}); });
...@@ -6,7 +6,7 @@ import EpicForm from 'ee/epic/components/epic_form.vue'; ...@@ -6,7 +6,7 @@ import EpicForm from 'ee/epic/components/epic_form.vue';
import createEpic from 'ee/epic/queries/createEpic.mutation.graphql'; import createEpic from 'ee/epic/queries/createEpic.mutation.graphql';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
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';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(), visitUrl: jest.fn(),
...@@ -48,7 +48,7 @@ describe('ee/epic/components/epic_form.vue', () => { ...@@ -48,7 +48,7 @@ describe('ee/epic/components/epic_form.vue', () => {
}); });
const findForm = () => wrapper.find(GlForm); const findForm = () => wrapper.find(GlForm);
const findLabels = () => wrapper.find(LabelsSelectVue); const findLabels = () => wrapper.find(LabelsSelectWidget);
const findTitle = () => wrapper.find('[data-testid="epic-title"]'); const findTitle = () => wrapper.find('[data-testid="epic-title"]');
const findDescription = () => wrapper.find('[data-testid="epic-description"]'); const findDescription = () => wrapper.find('[data-testid="epic-description"]');
const findConfidentialityCheck = () => wrapper.find('[data-testid="epic-confidentiality"]'); const findConfidentialityCheck = () => wrapper.find('[data-testid="epic-confidentiality"]');
...@@ -113,7 +113,9 @@ describe('ee/epic/components/epic_form.vue', () => { ...@@ -113,7 +113,9 @@ describe('ee/epic/components/epic_form.vue', () => {
findTitle().vm.$emit('input', title); findTitle().vm.$emit('input', title);
findDescription().setValue(description); findDescription().setValue(description);
findConfidentialityCheck().vm.$emit('input', confidential); findConfidentialityCheck().vm.$emit('input', confidential);
findLabels().vm.$emit('updateSelectedLabels', [{ id: 1, set: 1 }]); findLabels().vm.$emit('updateSelectedLabels', {
labels: [{ id: 'gid://gitlab/GroupLabel/1' }],
});
// Make sure the submitted values for start and due dates are date strings without timezone info. // Make sure the submitted values for start and due dates are date strings without timezone info.
// (Datepicker emits a Date object but the submitted value must be a date string). // (Datepicker emits a Date object but the submitted value must be a date string).
......
...@@ -124,15 +124,6 @@ module QA ...@@ -124,15 +124,6 @@ module QA
click_element(:more_assignees_link) click_element(:more_assignees_link)
end end
# When the labels_widget feature flag is enabled, wait until the labels widget appears
def wait_for_labels_widget_feature_flag
Support::Retrier.retry_until(max_duration: 60, reload_page: page, retry_on_exception: true, sleep_interval: 5) do
within_element(:labels_block) do
find_element(:edit_link)
end
end
end
private private
def wait_assignees_block_finish_loading def wait_assignees_block_finish_loading
......
...@@ -19,8 +19,6 @@ module QA ...@@ -19,8 +19,6 @@ module QA
end end
before do before do
Runtime::Feature.enable(:labels_widget, project: issue.project)
Flow::Login.sign_in Flow::Login.sign_in
[ [
...@@ -43,9 +41,6 @@ module QA ...@@ -43,9 +41,6 @@ module QA
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1181' testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1181'
) do ) do
Page::Project::Issue::Show.perform do |show| Page::Project::Issue::Show.perform do |show|
# TODO: Remove this method when the `Runtime::Feature.enable` method call is removed
show.wait_for_labels_widget_feature_flag
show.select_labels( show.select_labels(
[ [
new_label_same_scope, new_label_same_scope,
......
...@@ -15,7 +15,6 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -15,7 +15,6 @@ RSpec.describe "Issues > User edits issue", :js do
context 'with authorized user' do context 'with authorized user' do
before do before do
stub_feature_flags(labels_widget: false)
project.add_developer(user) project.add_developer(user)
project_with_milestones.add_developer(user) project_with_milestones.add_developer(user)
sign_in(user) sign_in(user)
...@@ -151,7 +150,7 @@ RSpec.describe "Issues > User edits issue", :js do ...@@ -151,7 +150,7 @@ RSpec.describe "Issues > User edits issue", :js do
page.within '.block.labels' do page.within '.block.labels' do
# Remove `verisimilitude` label # Remove `verisimilitude` label
within '.gl-label' do within '.gl-label' do
click_button click_button 'Remove label'
end end
expect(page).to have_text('syzygy') expect(page).to have_text('syzygy')
......
...@@ -17,7 +17,6 @@ RSpec.describe 'Labels Hierarchy', :js do ...@@ -17,7 +17,6 @@ RSpec.describe 'Labels Hierarchy', :js do
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') } let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
before do before do
stub_feature_flags(labels_widget: false)
grandparent.add_owner(user) grandparent.add_owner(user)
sign_in(user) sign_in(user)
...@@ -28,13 +27,12 @@ RSpec.describe 'Labels Hierarchy', :js do ...@@ -28,13 +27,12 @@ RSpec.describe 'Labels Hierarchy', :js do
[grandparent_group_label, parent_group_label, project_label_1].each do |label| [grandparent_group_label, parent_group_label, project_label_1].each do |label|
page.within('.block.labels') do page.within('.block.labels') do
click_on 'Edit' click_on 'Edit'
end
wait_for_requests wait_for_requests
find('a.label-item', text: label.title).click click_on label.title
wait_for_requests click_on 'Close'
click_on 'Close' end
wait_for_requests wait_for_requests
......
import { GlDrawer } from '@gitlab/ui'; import { GlDrawer } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue'; import { MountingPortal } from 'portal-vue';
import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue'; import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants'; import { ISSUABLE } from '~/boards/constants';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; 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 { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data'; import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
Vue.use(Vuex);
describe('BoardContentSidebar', () => { describe('BoardContentSidebar', () => {
let wrapper; let wrapper;
let store; let store;
...@@ -32,6 +34,7 @@ describe('BoardContentSidebar', () => { ...@@ -32,6 +34,7 @@ describe('BoardContentSidebar', () => {
groupPathForActiveIssue: () => mockIssueGroupPath, groupPathForActiveIssue: () => mockIssueGroupPath,
projectPathForActiveIssue: () => mockIssueProjectPath, projectPathForActiveIssue: () => mockIssueProjectPath,
isSidebarOpen: () => true, isSidebarOpen: () => true,
isGroupBoard: () => false,
...mockGetters, ...mockGetters,
}, },
actions: mockActions, actions: mockActions,
...@@ -115,8 +118,8 @@ describe('BoardContentSidebar', () => { ...@@ -115,8 +118,8 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(SidebarTodoWidget).exists()).toBe(true); expect(wrapper.findComponent(SidebarTodoWidget).exists()).toBe(true);
}); });
it('renders BoardSidebarLabelsSelect', () => { it('renders SidebarLabelsWidget', () => {
expect(wrapper.findComponent(BoardSidebarLabelsSelect).exists()).toBe(true); expect(wrapper.findComponent(SidebarLabelsWidget).exists()).toBe(true);
}); });
it('renders BoardSidebarTitle', () => { it('renders BoardSidebarTitle', () => {
......
...@@ -1570,7 +1570,7 @@ describe('addListNewIssue', () => { ...@@ -1570,7 +1570,7 @@ describe('addListNewIssue', () => {
describe('setActiveIssueLabels', () => { describe('setActiveIssueLabels', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } }; const state = { boardItems: { [mockIssue.id]: mockIssue } };
const getters = { activeBoardItem: mockIssue }; const getters = { activeBoardItem: { ...mockIssue, labels } };
const testLabelIds = labels.map((label) => label.id); const testLabelIds = labels.map((label) => label.id);
const input = { const input = {
labelIds: testLabelIds, labelIds: testLabelIds,
...@@ -1579,11 +1579,7 @@ describe('setActiveIssueLabels', () => { ...@@ -1579,11 +1579,7 @@ describe('setActiveIssueLabels', () => {
labels, labels,
}; };
it('should assign labels on success', (done) => { it('should assign labels', () => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
const payload = { const payload = {
itemId: getters.activeBoardItem.id, itemId: getters.activeBoardItem.id,
prop: 'labels', prop: 'labels',
...@@ -1601,74 +1597,28 @@ describe('setActiveIssueLabels', () => { ...@@ -1601,74 +1597,28 @@ describe('setActiveIssueLabels', () => {
}, },
], ],
[], [],
done,
); );
}); });
it('throws error if fails', async () => { it('should remove label', () => {
jest const payload = {
.spyOn(gqlClient, 'mutate') itemId: getters.activeBoardItem.id,
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } }); prop: 'labels',
value: [labels[1]],
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( testAction(
actions.setActiveIssueLabels, actions.setActiveIssueLabels,
{ ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] }, { ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] },
{ ...state, ...getters }, { ...state, ...getters },
[ [
{ {
type: types.UPDATE_BOARD_ITEM_BY_ID, type: types.UPDATE_BOARD_ITEM_BY_ID,
payload, payload,
}, },
], ],
[], [],
); );
});
}); });
}); });
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import {
mockLabels,
mockRegularLabel,
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import { MutationOperationMode } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
import SidebarLabels from '~/sidebar/components/labels/sidebar_labels.vue'; import SidebarLabels from '~/sidebar/components/labels/sidebar_labels.vue';
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql'; import {
import { toLabelGid } from '~/sidebar/utils'; DropdownVariant,
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants'; LabelType,
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
describe('sidebar labels', () => { describe('sidebar labels', () => {
let wrapper; let wrapper;
const defaultProps = { const defaultProps = {
allowLabelCreate: true,
allowLabelEdit: true, allowLabelEdit: true,
allowScopedLabels: true,
canEdit: true,
iid: '1', iid: '1',
initiallySelectedLabels: mockLabels,
issuableType: 'issue', issuableType: 'issue',
labelsFetchPath: '/gitlab-org/gitlab-test/-/labels.json?include_ancestor_groups=true',
labelsManagePath: '/gitlab-org/gitlab-test/-/labels',
projectIssuesPath: '/gitlab-org/gitlab-test/-/issues', projectIssuesPath: '/gitlab-org/gitlab-test/-/issues',
projectPath: 'gitlab-org/gitlab-test',
fullPath: 'gitlab-org/gitlab-test', fullPath: 'gitlab-org/gitlab-test',
}; };
const $apollo = {
mutate: jest.fn().mockResolvedValue(),
};
const userUpdatedLabels = [
{
...mockRegularLabel,
set: false,
},
{
id: 40,
title: 'Security',
color: '#ddd',
text_color: '#fff',
set: true,
},
{
id: 55,
title: 'Tooling',
color: '#ddd',
text_color: '#fff',
set: false,
},
];
const findLabelsSelect = () => wrapper.find(LabelsSelect); const findLabelsSelect = () => wrapper.find(LabelsSelect);
const mountComponent = (props = {}) => { const mountComponent = (props = {}) => {
...@@ -63,9 +25,6 @@ describe('sidebar labels', () => { ...@@ -63,9 +25,6 @@ describe('sidebar labels', () => {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
mocks: {
$apollo,
},
}); });
}; };
...@@ -75,115 +34,31 @@ describe('sidebar labels', () => { ...@@ -75,115 +34,31 @@ describe('sidebar labels', () => {
}); });
describe('LabelsSelect props', () => { describe('LabelsSelect props', () => {
beforeEach(() => { describe.each`
mountComponent(); issuableType
}); ${'issue'}
${'merge_request'}
it('are as expected', () => { `('issuableType $issuableType', ({ issuableType }) => {
expect(findLabelsSelect().props()).toMatchObject({ beforeEach(() => {
allowLabelCreate: defaultProps.allowLabelCreate, mountComponent({ issuableType });
allowLabelEdit: defaultProps.allowLabelEdit,
allowMultiselect: true,
allowScopedLabels: defaultProps.allowScopedLabels,
footerCreateLabelTitle: 'Create project label',
footerManageLabelTitle: 'Manage project labels',
labelsCreateTitle: 'Create project label',
labelsFetchPath: defaultProps.labelsFetchPath,
labelsFilterBasePath: defaultProps.projectIssuesPath,
labelsManagePath: defaultProps.labelsManagePath,
labelsSelectInProgress: false,
selectedLabels: defaultProps.initiallySelectedLabels,
variant: DropdownVariant.Sidebar,
});
});
});
describe('when type is issue', () => {
beforeEach(() => {
mountComponent({ issuableType: IssuableType.Issue });
});
describe('when labels are updated', () => {
it('invokes a mutation', () => {
findLabelsSelect().vm.$emit('updateSelectedLabels', userUpdatedLabels);
const expected = {
mutation: updateIssueLabelsMutation,
variables: {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
labelIds: [toLabelGid(29), toLabelGid(28), toLabelGid(27), toLabelGid(40)],
},
},
};
expect($apollo.mutate).toHaveBeenCalledWith(expected);
}); });
});
describe('when label `x` is clicked', () => {
it('invokes a mutation', () => {
findLabelsSelect().vm.$emit('onLabelRemove', 27);
const expected = {
mutation: updateIssueLabelsMutation,
variables: {
input: {
iid: defaultProps.iid,
projectPath: defaultProps.projectPath,
removeLabelIds: [27],
},
},
};
expect($apollo.mutate).toHaveBeenCalledWith(expected);
});
});
});
describe('when type is merge_request', () => {
beforeEach(() => {
mountComponent({ issuableType: IssuableType.MergeRequest });
});
describe('when labels are updated', () => {
it('invokes a mutation', () => {
findLabelsSelect().vm.$emit('updateSelectedLabels', userUpdatedLabels);
const expected = {
mutation: updateMergeRequestLabelsMutation,
variables: {
input: {
iid: defaultProps.iid,
labelIds: [toLabelGid(29), toLabelGid(28), toLabelGid(27), toLabelGid(40)],
operationMode: MutationOperationMode.Replace,
projectPath: defaultProps.projectPath,
},
},
};
expect($apollo.mutate).toHaveBeenCalledWith(expected);
});
});
describe('when label `x` is clicked', () => {
it('invokes a mutation', () => {
findLabelsSelect().vm.$emit('onLabelRemove', 27);
const expected = {
mutation: updateMergeRequestLabelsMutation,
variables: {
input: {
iid: defaultProps.iid,
labelIds: [toLabelGid(27)],
operationMode: MutationOperationMode.Remove,
projectPath: defaultProps.projectPath,
},
},
};
expect($apollo.mutate).toHaveBeenCalledWith(expected); it('has expected props', () => {
expect(findLabelsSelect().props()).toMatchObject({
iid: defaultProps.iid,
fullPath: defaultProps.fullPath,
allowLabelRemove: defaultProps.allowLabelEdit,
allowMultiselect: true,
footerCreateLabelTitle: 'Create project label',
footerManageLabelTitle: 'Manage project labels',
labelsCreateTitle: 'Create project label',
labelsFilterBasePath: defaultProps.projectIssuesPath,
variant: DropdownVariant.Sidebar,
issuableType,
workspaceType: 'project',
attrWorkspacePath: defaultProps.fullPath,
labelCreateType: LabelType.project,
});
}); });
}); });
}); });
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -8,14 +8,14 @@ import { IssuableType } from '~/issue_show/constants'; ...@@ -8,14 +8,14 @@ import { IssuableType } from '~/issue_show/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue'; import DropdownContents from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue'; import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value_collapsed.vue';
import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql'; import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { mockConfig, issuableLabelsQueryResponse } from './mock_data'; import { mockConfig, issuableLabelsQueryResponse } from './mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
const localVue = createLocalVue(); Vue.use(VueApollo);
localVue.use(VueApollo);
const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse); const successfulQueryHandler = jest.fn().mockResolvedValue(issuableLabelsQueryResponse);
const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
...@@ -25,6 +25,7 @@ describe('LabelsSelectRoot', () => { ...@@ -25,6 +25,7 @@ describe('LabelsSelectRoot', () => {
const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem); const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findDropdownValue = () => wrapper.findComponent(DropdownValue); const findDropdownValue = () => wrapper.findComponent(DropdownValue);
const findDropdownValueCollapsed = () => wrapper.findComponent(DropdownValueCollapsed);
const findDropdownContents = () => wrapper.findComponent(DropdownContents); const findDropdownContents = () => wrapper.findComponent(DropdownContents);
const createComponent = ({ const createComponent = ({
...@@ -37,7 +38,6 @@ describe('LabelsSelectRoot', () => { ...@@ -37,7 +38,6 @@ describe('LabelsSelectRoot', () => {
wrapper = shallowMount(LabelsSelectRoot, { wrapper = shallowMount(LabelsSelectRoot, {
slots, slots,
apolloProvider: mockApollo, apolloProvider: mockApollo,
localVue,
propsData: { propsData: {
...config, ...config,
issuableType: IssuableType.Issue, issuableType: IssuableType.Issue,
...@@ -107,6 +107,9 @@ describe('LabelsSelectRoot', () => { ...@@ -107,6 +107,9 @@ describe('LabelsSelectRoot', () => {
expect(findDropdownValue().props('selectedLabels')).toEqual( expect(findDropdownValue().props('selectedLabels')).toEqual(
issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes, issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes,
); );
expect(findDropdownValueCollapsed().props('labels')).toEqual(
issuableLabelsQueryResponse.data.workspace.issuable.labels.nodes,
);
}); });
it('emits `onLabelRemove` event on dropdown value label remove event', () => { it('emits `onLabelRemove` event on dropdown value label remove event', () => {
......
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