Commit 755a0df3 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch...

Merge branch '322725-display-labels-when-editing-labels-in-the-new-sidebar-and-reflect-in-boards' into 'master'

Board sidebar - Migrate labels select to widget [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!71791
parents 4e4ba490 06c996d8
......@@ -3,15 +3,18 @@ import { GlDrawer } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
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 BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
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 glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
......@@ -23,6 +26,7 @@ export default {
SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
SidebarLabelsWidget,
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
SidebarTodoWidget,
......@@ -46,16 +50,20 @@ export default {
weightFeatureAvailable: {
default: false,
},
allowLabelEdit: {
default: false,
},
},
inheritAttrs: false,
computed: {
...mapGetters([
'isGroupBoard',
'isSidebarOpen',
'activeBoardItem',
'groupPathForActiveIssue',
'projectPathForActiveIssue',
]),
...mapState(['sidebarType', 'issuableType']),
...mapState(['sidebarType', 'issuableType', 'isSettingLabels']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
......@@ -65,17 +73,48 @@ export default {
fullPath() {
return this.activeBoardItem?.referencePath?.split('#')[0] || '';
},
createLabelTitle() {
return sprintf(__('Create %{workspace} label'), {
workspace: this.isGroupBoard ? 'group' : 'project',
});
},
manageLabelTitle() {
return sprintf(__('Manage %{workspace} labels'), {
workspace: this.isGroupBoard ? 'group' : 'project',
});
},
attrWorkspacePath() {
return this.isGroupBoard ? this.groupPathForActiveIssue : undefined;
},
},
methods: {
...mapActions([
'toggleBoardItem',
'setAssignees',
'setActiveItemConfidential',
'setActiveBoardItemLabels',
'setActiveItemWeight',
]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
handleUpdateSelectedLabels(input) {
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),
});
},
handleLabelRemove(input) {
this.setActiveBoardItemLabels({
iid: this.activeBoardItem.iid,
projectPath: this.projectPathForActiveIssue,
removeLabelIds: [input],
});
},
},
};
</script>
......@@ -160,7 +199,28 @@ export default {
:issuable-type="issuableType"
data-testid="sidebar-due-date"
/>
<board-sidebar-labels-select class="block labels" />
<sidebar-labels-widget
v-if="glFeatures.labelsWidget"
class="block labels"
data-testid="sidebar-labels"
:iid="activeBoardItem.iid"
: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"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleUpdateSelectedLabels"
>
{{ __('None') }}
</sidebar-labels-widget>
<board-sidebar-labels-select v-else class="block labels" />
<sidebar-weight-widget
v-if="weightFeatureAvailable"
:iid="activeBoardItem.iid"
......
......@@ -87,6 +87,9 @@ function mountBoardApp(el) {
iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable),
issuableType: issuableTypes.issue,
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
allowLabelCreate: parseBoolean(el.dataset.canUpdate),
allowLabelEdit: parseBoolean(el.dataset.canUpdate),
allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
},
render: (createComponent) => createComponent(BoardApp),
});
......
......@@ -656,6 +656,7 @@ export default {
},
setActiveIssueLabels: async ({ commit, getters }, input) => {
commit(types.SET_LABELS_LOADING, true);
const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetLabelsMutation,
......@@ -669,6 +670,8 @@ export default {
},
});
commit(types.SET_LABELS_LOADING, false);
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
......
......@@ -28,6 +28,7 @@ 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,6 +195,10 @@ 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,6 +12,7 @@ export default () => ({
listsFlags: {},
boardItemsByListId: {},
backupItemsList: [],
isSettingLabels: false,
isSettingAssignees: false,
pageInfoByListId: {},
boardItems: {},
......
......@@ -36,6 +36,7 @@ export default {
'allowLabelEdit',
'allowScopedLabels',
'iid',
'fullPath',
'initiallySelectedLabels',
'issuableType',
'labelsFetchPath',
......@@ -145,6 +146,8 @@ export default {
<labels-select-widget
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block"
:iid="iid"
:full-path="fullPath"
:allow-label-remove="allowLabelEdit"
:allow-multiselect="true"
:footer-create-label-title="__('Create project label')"
......
......@@ -57,6 +57,15 @@ export default {
required: false,
default: false,
},
fullPath: {
type: String,
required: true,
},
attrWorkspacePath: {
type: String,
required: false,
default: undefined,
},
},
data() {
return {
......@@ -182,6 +191,8 @@ export default {
:selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect"
:issuable-type="issuableType"
:full-path="fullPath"
:attr-workspace-path="attrWorkspacePath"
@hideCreateView="toggleDropdownContentsCreateView"
/>
</template>
......
......@@ -19,16 +19,20 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
inject: {
fullPath: {
default: '',
},
},
props: {
issuableType: {
type: String,
required: true,
},
fullPath: {
type: String,
required: true,
},
attrWorkspacePath: {
type: String,
required: false,
default: undefined,
},
},
data() {
return {
......@@ -46,11 +50,19 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
return this.issuableType === IssuableType.Epic
? {
if (this.issuableType === IssuableType.Epic) {
return {
title: this.labelTitle,
color: this.selectedColor,
groupPath: this.fullPath,
};
}
return this.attrWorkspacePath !== undefined
? {
title: this.labelTitle,
color: this.selectedColor,
groupPath: this.attrWorkspacePath,
}
: {
title: this.labelTitle,
......
......@@ -24,7 +24,6 @@ export default {
GlIntersectionObserver,
LabelItem,
},
inject: ['fullPath'],
model: {
prop: 'localSelectedLabels',
},
......@@ -45,6 +44,10 @@ export default {
type: Array,
required: true,
},
fullPath: {
type: String,
required: true,
},
},
data() {
return {
......@@ -84,7 +87,7 @@ export default {
return this.$apollo.queries.labels.loading;
},
localSelectedLabelsIds() {
return this.localSelectedLabels.map((label) => label.id);
return this.localSelectedLabels.map((label) => getIdFromGraphQLId(label.id));
},
visibleLabels() {
if (this.searchKey) {
......@@ -130,7 +133,9 @@ export default {
updateSelectedLabels(label) {
let labels;
if (this.isLabelSelected(label)) {
labels = this.localSelectedLabels.filter(({ id }) => id !== getIdFromGraphQLId(label.id));
labels = this.localSelectedLabels.filter(
({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id,
);
} else {
labels = [
...this.localSelectedLabels,
......
......@@ -21,15 +21,20 @@ export default {
SidebarEditableItem,
},
inject: {
iid: {
default: '',
},
allowLabelEdit: {
default: false,
},
fullPath: {},
},
props: {
iid: {
type: String,
required: false,
default: '',
},
fullPath: {
type: String,
required: true,
},
allowLabelRemove: {
type: Boolean,
required: false,
......@@ -99,6 +104,11 @@ export default {
type: String,
required: true,
},
attrWorkspacePath: {
type: String,
required: false,
default: undefined,
},
},
data() {
return {
......@@ -206,6 +216,8 @@ export default {
:variant="variant"
:issuable-type="issuableType"
:is-visible="edit"
:full-path="fullPath"
:attr-workspace-path="attrWorkspacePath"
@setLabels="handleDropdownClose"
@closeDropdown="collapseEditableItem"
/>
......@@ -224,6 +236,7 @@ export default {
:selected-labels="selectedLabels"
:variant="variant"
:issuable-type="issuableType"
:full-path="fullPath"
@setLabels="handleDropdownClose"
/>
</div>
......
......@@ -11,6 +11,7 @@ class Groups::BoardsController < Groups::ApplicationController
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(:iteration_cadences, group, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml)
end
feature_category :boards
......
......@@ -11,6 +11,7 @@ class Projects::BoardsController < Projects::ApplicationController
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(:iteration_cadences, project&.group, default_enabled: :yaml)
push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
end
feature_category :boards
......
......@@ -31,6 +31,7 @@ export default {
},
mixins: [glFeatureFlagMixin()],
inject: [
'iid',
'groupPath',
'groupEpicsPath',
'labelsFetchPath',
......@@ -189,6 +190,8 @@ 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"
......
......@@ -295,15 +295,15 @@ RSpec.describe 'Issue Boards', :js do
wait_for_requests
click_link scoped_label_1.title
click_button scoped_label_1.title
wait_for_requests
click_link scoped_label_2.title
click_button scoped_label_2.title
wait_for_requests
find('[data-testid="close-icon"]').click
click_button 'Close'
page.within('.value') do
aggregate_failures do
......@@ -335,11 +335,11 @@ RSpec.describe 'Issue Boards', :js do
wait_for_requests
click_link scoped_label_2.title
click_button scoped_label_2.title
wait_for_requests
find('[data-testid="close-icon"]').click
click_button 'Close'
page.within('.value') do
aggregate_failures do
......
......@@ -22,6 +22,7 @@ describe('ee/epic/components/epic_form.vue', () => {
const createWrapper = ({ mutationResult = TEST_NEW_EPIC } = {}) => {
wrapper = shallowMount(EpicForm, {
provide: {
iid: '1',
groupPath: TEST_GROUP_PATH,
groupEpicsPath: TEST_HOST,
labelsFetchPath: TEST_HOST,
......
......@@ -9546,6 +9546,9 @@ msgstr ""
msgid "Create %{type}"
msgstr ""
msgid "Create %{workspace} label"
msgstr ""
msgid "Create New Directory"
msgstr ""
......@@ -20769,6 +20772,9 @@ msgstr ""
msgid "Makes this issue confidential."
msgstr ""
msgid "Manage %{workspace} labels"
msgstr ""
msgid "Manage Web IDE features."
msgstr ""
......
......@@ -29,12 +29,11 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
end
context 'labels' do
# https://gitlab.com/gitlab-org/gitlab/-/issues/322725
xit 'shows current labels when editing' do
it 'shows current labels when editing' do
click_card(card)
page.within('.labels') do
click_link 'Edit'
click_button 'Edit'
wait_for_requests
......@@ -54,9 +53,9 @@ RSpec.describe 'Project issue boards sidebar labels', :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
......@@ -79,11 +78,11 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
wait_for_requests
click_link bug.title
click_on bug.title
click_link regression.title
click_on regression.title
find('[data-testid="close-icon"]').click
click_button 'Close'
wait_for_requests
......@@ -108,9 +107,9 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
wait_for_requests
click_link stretch.title
click_button stretch.title
find('[data-testid="close-icon"]').click
click_button 'Close'
wait_for_requests
......@@ -125,43 +124,22 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
expect(card).not_to have_content(stretch.title)
end
# https://gitlab.com/gitlab-org/gitlab/-/issues/324290
xit 'creates project label' do
it 'creates project label' do
click_card(card)
page.within('.labels') do
click_link 'Edit'
click_button 'Edit'
wait_for_requests
click_link 'Create project label'
fill_in 'new_label_name', with: 'test label'
click_on 'Create project label'
fill_in 'Name new label', with: 'test label'
first('.suggest-colors-dropdown a').click
click_button 'Create'
wait_for_requests
expect(page).to have_link 'test label'
expect(page).to have_button 'test label'
end
expect(page).to have_selector('.board', count: 3)
end
# https://gitlab.com/gitlab-org/gitlab/-/issues/324290
xit 'creates project label and list' do
click_card(card)
page.within('.labels') do
click_link 'Edit'
wait_for_requests
click_link 'Create project label'
fill_in 'new_label_name', with: 'test label'
first('.suggest-colors-dropdown a').click
first('.js-add-list').click
click_button 'Create'
wait_for_requests
expect(page).to have_link 'test label'
end
expect(page).to have_selector('.board', count: 4)
end
end
end
......@@ -1577,7 +1577,7 @@ describe('setActiveIssueLabels', () => {
projectPath: 'h/b',
};
it('should assign labels on success', (done) => {
it('should assign labels on success, and sets loading state for labels', (done) => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { issue: { labels: { nodes: labels } } } } });
......@@ -1593,6 +1593,14 @@ 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,
......
......@@ -27,6 +27,7 @@ describe('sidebar labels', () => {
labelsManagePath: '/gitlab-org/gitlab-test/-/labels',
projectIssuesPath: '/gitlab-org/gitlab-test/-/issues',
projectPath: 'gitlab-org/gitlab-test',
fullPath: 'gitlab-org/gitlab-test',
};
const $apollo = {
......
......@@ -67,6 +67,7 @@ describe('DropdownContentsCreateView', () => {
apolloProvider: mockApollo,
propsData: {
issuableType,
fullPath: '',
},
});
};
......
......@@ -50,8 +50,6 @@ describe('DropdownContentsLabelsView', () => {
localVue,
apolloProvider: mockApollo,
provide: {
fullPath: 'test',
iid: 1,
variant: DropdownVariant.Sidebar,
...injected,
},
......
......@@ -38,6 +38,7 @@ describe('DropdownContent', () => {
dropdownButtonText: 'Labels',
variant: 'sidebar',
issuableType: 'issue',
fullPath: 'test',
...props,
},
data() {
......
......@@ -46,8 +46,6 @@ describe('LabelsSelectRoot', () => {
SidebarEditableItem,
},
provide: {
iid: '1',
fullPath: 'test',
canUpdate: true,
allowLabelEdit: true,
allowLabelCreate: true,
......
......@@ -34,6 +34,8 @@ export const mockLabels = [
];
export const mockConfig = {
iid: '1',
fullPath: 'test',
allowMultiselect: true,
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
......
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