Commit efa5169e authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '343606-user-labels-widget-mr-and-epic-boards' into 'master'

Use labels widget on epic board and MR sidebar [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!73132
parents e94f48e8 d3dacc2d
......@@ -54,6 +54,9 @@ export default {
allowLabelEdit: {
default: false,
},
labelsFilterBasePath: {
default: '',
},
},
inheritAttrs: false,
computed: {
......@@ -90,6 +93,11 @@ export default {
labelType() {
return this.isGroupBoard ? LabelType.group : LabelType.project;
},
labelsFilterPath() {
return this.isGroupBoard
? this.labelsFilterBasePath.replace(':project_path', this.projectPathForActiveIssue)
: this.labelsFilterBasePath;
},
},
methods: {
...mapActions([
......@@ -212,7 +220,7 @@ export default {
:footer-create-label-title="createLabelTitle"
:footer-manage-label-title="manageLabelTitle"
:labels-create-title="createLabelTitle"
:labels-filter-base-path="projectPathForActiveIssue"
:labels-filter-base-path="labelsFilterPath"
:attr-workspace-path="attrWorkspacePath"
workspace-type="project"
:issuable-type="issuableType"
......
......@@ -37,6 +37,7 @@ import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widge
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 issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
import mergeRequestLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql';
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
......@@ -128,6 +129,7 @@ export const issuableLabelsQueries = {
mutationName: 'updateIssue',
},
[IssuableType.MergeRequest]: {
issuableQuery: mergeRequestLabelsQuery,
mutation: updateMergeRequestLabelsMutation,
mutationName: 'mergeRequestSetLabels',
},
......
......@@ -260,6 +260,10 @@ export function mountSidebarLabels() {
variant: DropdownVariant.Sidebar,
canUpdate: parseBoolean(el.dataset.canEdit),
isClassicSidebar: true,
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage()
? IssuableType.Issue
: IssuableType.MergeRequest,
},
render: (createElement) => createElement(SidebarLabels),
});
......
......@@ -2,6 +2,7 @@ mutation mergeRequestSetLabels($input: MergeRequestSetLabelsInput!) {
mergeRequestSetLabels(input: $input) {
errors
mergeRequest {
id
labels {
nodes {
color
......
......@@ -53,10 +53,6 @@ export default {
type: String,
required: true,
},
issuableType: {
type: String,
required: true,
},
isVisible: {
type: Boolean,
required: false,
......@@ -209,7 +205,6 @@ export default {
v-model="localSelectedLabels"
:search-key="searchKey"
:allow-multiselect="allowMultiselect"
:issuable-type="issuableType"
:full-path="fullPath"
:workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath"
......
......@@ -32,11 +32,6 @@ export default {
type: String,
required: true,
},
issuableType: {
type: String,
required: false,
default: undefined,
},
workspaceType: {
type: String,
required: true,
......
......@@ -23,10 +23,6 @@ export default {
type: Boolean,
required: true,
},
issuableType: {
type: String,
required: true,
},
localSelectedLabels: {
type: Array,
required: true,
......@@ -119,13 +115,7 @@ export default {
({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id,
);
} else {
labels = [
...this.localSelectedLabels,
{
...label,
id: getIdFromGraphQLId(label.id),
},
];
labels = [...this.localSelectedLabels, label];
}
this.$emit('input', labels);
},
......
<script>
import { GlLabel } from '@gitlab/ui';
import { sortBy } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
......@@ -47,7 +46,7 @@ export default {
return this.allowScopedLabels && isScopedLabel(label);
},
removeLabel(labelId) {
this.$emit('onLabelRemove', getIdFromGraphQLId(labelId));
this.$emit('onLabelRemove', labelId);
},
},
};
......
#import "~/graphql_shared/fragments/label.fragment.graphql"
query mergeRequestLabels($fullPath: ID!, $iid: String!) {
workspace: project(fullPath: $fullPath) {
issuable: mergeRequest(iid: $iid) {
id
labels {
nodes {
...Label
}
}
}
}
}
......@@ -207,7 +207,7 @@ export default {
return {
iid: currentIid,
groupPath: this.fullPath,
addLabelIds: labelIds,
addLabelIds: labelIds.map((id) => getIdFromGraphQLId(id)),
removeLabelIds: this.issuableLabelIds
.filter((id) => !labelIds.includes(id))
.map((id) => getIdFromGraphQLId(id)),
......@@ -232,8 +232,8 @@ export default {
}
this.$emit('updateSelectedLabels', {
id: data[mutationName]?.[this.issuableType].id,
labels: data[mutationName]?.[this.issuableType].labels?.nodes,
id: data[mutationName]?.[this.issuableType]?.id,
labels: data[mutationName]?.[this.issuableType]?.labels?.nodes,
});
})
.catch((error) =>
......@@ -268,7 +268,7 @@ export default {
case IssuableType.Epic:
return {
iid: this.iid,
removeLabelIds: [labelId],
removeLabelIds: [getIdFromGraphQLId(labelId)],
groupPath: this.fullPath,
};
default:
......@@ -341,7 +341,6 @@ export default {
:labels-create-title="labelsCreateTitle"
:selected-labels="issuableLabels"
:variant="variant"
:issuable-type="issuableType"
:is-visible="edit"
:full-path="fullPath"
:workspace-type="workspaceType"
......@@ -364,7 +363,6 @@ export default {
:labels-create-title="labelsCreateTitle"
:selected-labels="issuableLabels"
:variant="variant"
:issuable-type="issuableType"
:full-path="fullPath"
:workspace-type="workspaceType"
:attr-workspace-path="attrWorkspacePath"
......
......@@ -470,6 +470,10 @@
.labels-select-wrapper.is-embedded .labels-select-wrapper.is-embedded {
width: auto;
}
.show.dropdown .dropdown-menu {
@include gl-w-full;
}
}
.board-header-collapsed-info-icon:hover {
......
......@@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
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_attention_requests, project, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
# Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
......
......@@ -43,7 +43,7 @@ module BoardsHelper
def build_issue_link_base
if board.group_board?
"#{group_path(@board.group)}/:project_path/issues"
"/:project_path/-/issues"
else
project_issues_path(@project)
end
......
......@@ -6,11 +6,14 @@ import SidebarAncestorsWidget from 'ee_component/sidebar/components/ancestors_tr
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_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 glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
......@@ -18,6 +21,7 @@ export default {
SidebarTodoWidget,
BoardSidebarLabelsSelect,
BoardSidebarTitle,
SidebarLabelsWidget,
SidebarConfidentialityWidget,
SidebarDateWidget,
SidebarParticipantsWidget,
......@@ -25,6 +29,8 @@ export default {
SidebarAncestorsWidget,
MountingPortal,
},
mixins: [glFeatureFlagMixin()],
inject: ['canUpdate', 'labelsFilterBasePath'],
inheritAttrs: false,
computed: {
...mapGetters(['isSidebarOpen', 'activeBoardItem']),
......@@ -40,10 +46,30 @@ export default {
},
},
methods: {
...mapActions(['toggleBoardItem', 'setActiveItemConfidential', 'setActiveItemSubscribed']),
...mapActions([
'toggleBoardItem',
'setActiveItemConfidential',
'setActiveItemSubscribed',
'setActiveBoardItemLabels',
]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
handleUpdateSelectedLabels({ labels, id }) {
this.setActiveBoardItemLabels({
id,
groupPath: this.fullPath,
labelIds: labels.map((label) => getIdFromGraphQLId(label.id)),
labels,
});
},
handleLabelRemove(removeLabelId) {
this.setActiveBoardItemLabels({
iid: this.activeBoardItem.iid,
groupPath: this.fullPath,
removeLabelIds: [removeLabelId],
});
},
},
};
</script>
......@@ -86,7 +112,25 @@ export default {
:issuable-type="issuableType"
:can-inherit="true"
/>
<board-sidebar-labels-select class="labels" />
<sidebar-labels-widget
v-if="glFeatures.labelsWidget"
class="block labels"
data-testid="sidebar-labels"
:iid="activeBoardItem.iid"
:full-path="fullPath"
:allow-label-remove="canUpdate"
:allow-multiselect="true"
:labels-filter-base-path="labelsFilterBasePath"
:attr-workspace-path="fullPath"
workspace-type="group"
:issuable-type="issuableType"
label-create-type="group"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</sidebar-labels-widget>
<board-sidebar-labels-select v-else class="labels" />
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
......
......@@ -12,6 +12,7 @@ import listsIssuesQuery from '~/boards/graphql/lists_issues.query.graphql';
import projectBoardMembersQuery from '~/boards/graphql/project_board_members.query.graphql';
import actionsCE from '~/boards/stores/actions';
import * as typesCE from '~/boards/stores/mutation_types';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { historyPushState, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { mergeUrlParams, removeParams, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
......@@ -583,26 +584,43 @@ export default {
setActiveEpicLabels: async ({ commit, getters, state }, input) => {
const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: updateEpicLabelsMutation,
variables: {
input: {
iid: String(activeBoardItem.iid),
addLabelIds: input.addLabelIds ?? [],
removeLabelIds: input.removeLabelIds ?? [],
groupPath: state.fullPath,
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);
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;
}
commit(typesCE.UPDATE_BOARD_ITEM_BY_ID, {
itemId: activeBoardItem.id,
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: input.id || activeBoardItem.id,
prop: 'labels',
value: data.updateEpic.epic.labels.nodes,
value: labels,
});
},
};
......@@ -8,6 +8,7 @@ import {
GlFormInput,
} from '@gitlab/ui';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
......@@ -113,7 +114,7 @@ export default {
},
handleUpdateSelectedLabels(labels) {
if (this.glFeatures.labelsWidget) {
this.labels = labels;
this.labels = labels.map((label) => ({ ...label, id: getIdFromGraphQLId(label.id) }));
return;
}
......
......@@ -97,6 +97,9 @@ function mountBoardApp(el) {
iterationListsAvailable: false,
issuableType: issuableTypes.epic,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
allowLabelCreate: parseBoolean(el.dataset.canUpdate),
allowLabelEdit: parseBoolean(el.dataset.canUpdate),
allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
},
render: (createComponent) => createComponent(BoardApp),
});
......
......@@ -8,6 +8,9 @@ class Groups::EpicBoardsController < Groups::ApplicationController
before_action :redirect_to_recent_board, only: [:index]
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'
......
......@@ -71,6 +71,13 @@ module EE
super
end
override :build_issue_link_base
def build_issue_link_base
return group_epics_path(@group) if board.is_a?(::Boards::EpicBoard)
super
end
override :board_base_url
def board_base_url
return group_epic_boards_url(@group) if board.is_a?(::Boards::EpicBoard)
......
......@@ -161,9 +161,9 @@ RSpec.describe 'Epic boards sidebar', :js do
wait_for_requests
click_link bug.title
click_on bug.title
find('[data-testid="close-icon"]').click
click_button 'Close'
wait_for_requests
......
......@@ -15,6 +15,8 @@ describe('ee/BoardContent', () => {
provide: {
timeTrackingLimitToHours: false,
canAdminList: false,
canUpdate: false,
labelsFilterBasePath: '',
},
propsData: {
lists: [],
......
......@@ -45,6 +45,7 @@ describe('EpicBoardContentSidebar', () => {
canUpdate: true,
rootPath: '/',
groupId: 1,
labelsFilterBasePath: '',
},
store,
stubs: {
......
......@@ -122,6 +122,12 @@ export const labels = [
color: '#34ebec',
description: null,
},
{
id: 'gid://gitlab/GroupLabel/6',
title: 'Brock',
color: '#e082b6',
description: null,
},
];
export const rawIssue = {
......
......@@ -14,6 +14,7 @@ import { mockMoveIssueParams, mockMoveData, mockMoveState } from 'jest/boards/mo
import { formatListIssues } from '~/boards/boards_util';
import listsIssuesQuery from '~/boards/graphql/lists_issues.query.graphql';
import * as typesCE from '~/boards/stores/mutation_types';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
import {
......@@ -1380,4 +1381,62 @@ describe('setActiveEpicLabels', () => {
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(
actions.setActiveEpicLabels,
{ ...input, removeLabelIds: [getIdFromGraphQLId(labels[0].id)] },
{ ...state, ...getters },
[
{
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
[],
);
});
});
});
......@@ -33,6 +33,19 @@ RSpec.describe BoardsHelper do
end
end
describe '#build_issue_link_base' do
context 'when epic board' do
let_it_be(:epic_board) { create(:epic_board, group: group) }
it 'generates the correct url' do
assign(:board, epic_board)
assign(:group, group)
expect(helper.build_issue_link_base).to eq "/groups/#{group.full_path}/-/epics"
end
end
end
describe '#board_base_url' do
context 'when epic board' do
let_it_be(:epic_board) { create(:epic_board, group: group) }
......
......@@ -10,7 +10,6 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
......@@ -57,7 +56,6 @@ describe('DropdownContentsLabelsView', () => {
propsData: {
...initialState,
localSelectedLabels,
issuableType: IssuableType.Issue,
searchKey,
labelCreateType: 'project',
workspaceType: 'project',
......
......@@ -39,7 +39,6 @@ describe('DropdownContent', () => {
footerManageLabelTitle: 'manage',
dropdownButtonText: 'Labels',
variant: 'sidebar',
issuableType: 'issue',
fullPath: 'test',
workspaceType: 'project',
labelCreateType: 'project',
......
......@@ -23,14 +23,14 @@ RSpec.describe BoardsHelper do
it 'returns correct path for base group' do
assign(:board, group_board)
expect(helper.build_issue_link_base).to eq('/base/:project_path/issues')
expect(helper.build_issue_link_base).to eq('/:project_path/-/issues')
end
it 'returns correct path for subgroup' do
subgroup = create(:group, parent: base_group, path: 'sub')
assign(:board, create(:board, group: subgroup))
expect(helper.build_issue_link_base).to eq('/base/sub/:project_path/issues')
expect(helper.build_issue_link_base).to eq('/:project_path/-/issues')
end
end
end
......@@ -149,7 +149,7 @@ RSpec.describe BoardsHelper do
end
it 'returns correct path for base group' do
expect(helper.build_issue_link_base).to eq("/#{base_group.full_path}/:project_path/issues")
expect(helper.build_issue_link_base).to eq("/:project_path/-/issues")
end
it 'returns required label endpoints' do
......
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