Commit 8b7bc33f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '332434-replace-board-scope-label-select-with-labels-widget' into 'master'

Refactor labels selector in board scope

See merge request gitlab-org/gitlab!71983
parents 47c7b5cc fc2962ff
import { sortBy, cloneDeep } from 'lodash'; import { sortBy, cloneDeep } from 'lodash';
import { isGid } from '~/graphql_shared/utils';
import { ListType, MilestoneIDs } from './constants'; import { ListType, MilestoneIDs } from './constants';
export function getMilestone() { export function getMilestone() {
...@@ -95,6 +96,9 @@ export function fullMilestoneId(id) { ...@@ -95,6 +96,9 @@ export function fullMilestoneId(id) {
} }
export function fullLabelId(label) { export function fullLabelId(label) {
if (isGid(label.id)) {
return label.id;
}
if (label.project_id && label.project_id !== null) { if (label.project_id && label.project_id !== null) {
return `gid://gitlab/ProjectLabel/${label.id}`; return `gid://gitlab/ProjectLabel/${label.id}`;
} }
......
...@@ -57,39 +57,16 @@ export default { ...@@ -57,39 +57,16 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
scopedIssueBoardFeatureEnabled: { scopedIssueBoardFeatureEnabled: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
projectId: {
type: Number,
required: false,
default: 0,
},
groupId: {
type: Number,
required: false,
default: 0,
},
weights: { weights: {
type: Array, type: Array,
required: false, required: false,
default: () => [], default: () => [],
}, },
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
currentBoard: { currentBoard: {
type: Object, type: Object,
required: true, required: true,
...@@ -288,16 +265,7 @@ export default { ...@@ -288,16 +265,7 @@ export default {
this.board.iteration_id = iterationId; this.board.iteration_id = iterationId;
}, },
setBoardLabels(labels) { setBoardLabels(labels) {
labels.forEach((label) => { this.board.labels = labels;
if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
this.board.labels.push({
...label,
textColor: label.text_color,
});
} else if (!label.set) {
this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
}
});
}, },
setAssignee(assigneeId) { setAssignee(assigneeId) {
this.$set(this.board, 'assignee', { this.$set(this.board, 'assignee', {
...@@ -371,11 +339,6 @@ export default { ...@@ -371,11 +339,6 @@ export default {
:collapse-scope="isNewForm" :collapse-scope="isNewForm"
:board="board" :board="board"
:can-admin-board="canAdminBoard" :can-admin-board="canAdminBoard"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
:weights="weights" :weights="weights"
@set-iteration="setIteration" @set-iteration="setIteration"
@set-board-labels="setBoardLabels" @set-board-labels="setBoardLabels"
......
...@@ -64,22 +64,6 @@ export default { ...@@ -64,22 +64,6 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
groupId: {
type: Number,
required: true,
},
scopedIssueBoardFeatureEnabled: { scopedIssueBoardFeatureEnabled: {
type: Boolean, type: Boolean,
required: true, required: true,
...@@ -88,11 +72,6 @@ export default { ...@@ -88,11 +72,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
enabledScopedLabels: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -354,14 +333,9 @@ export default { ...@@ -354,14 +333,9 @@ export default {
<board-form <board-form
v-if="currentPage" v-if="currentPage"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard" :can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled" :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights" :weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard" :current-board="currentBoard"
:current-page="currentPage" :current-page="currentPage"
@cancel="cancel" @cancel="cancel"
......
...@@ -142,5 +142,7 @@ export default () => { ...@@ -142,5 +142,7 @@ export default () => {
fullPath: $boardApp.dataset.fullPath, fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint, rootPath: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint, recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
allowScopedLabels: $boardApp.dataset.scopedLabels,
labelsManagePath: $boardApp.dataset.labelsManagePath,
}); });
}; };
...@@ -13,6 +13,7 @@ const apolloProvider = new VueApollo({ ...@@ -13,6 +13,7 @@ const apolloProvider = new VueApollo({
export default (params = {}) => { export default (params = {}) => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher'); const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
const { dataset } = boardsSwitcherElement;
return new Vue({ return new Vue({
el: boardsSwitcherElement, el: boardsSwitcherElement,
components: { components: {
...@@ -24,18 +25,17 @@ export default (params = {}) => { ...@@ -24,18 +25,17 @@ export default (params = {}) => {
fullPath: params.fullPath, fullPath: params.fullPath,
rootPath: params.rootPath, rootPath: params.rootPath,
recentBoardsEndpoint: params.recentBoardsEndpoint, recentBoardsEndpoint: params.recentBoardsEndpoint,
allowScopedLabels: params.allowScopedLabels,
labelsManagePath: params.labelsManagePath,
allowLabelCreate: parseBoolean(dataset.canAdminBoard),
}, },
data() { data() {
const { dataset } = boardsSwitcherElement;
const boardsSelectorProps = { const boardsSelectorProps = {
...dataset, ...dataset,
currentBoard: JSON.parse(dataset.currentBoard), currentBoard: JSON.parse(dataset.currentBoard),
hasMissingBoards: parseBoolean(dataset.hasMissingBoards), hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard), canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable), multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
projectId: dataset.projectId ? Number(dataset.projectId) : 0,
groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled), scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights), weights: JSON.parse(dataset.weights),
}; };
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
query usersSearch($search: String!, $fullPath: ID!) { query usersSearch($search: String!, $fullPath: ID!) {
workspace: group(fullPath: $fullPath) { workspace: group(fullPath: $fullPath) {
id
users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) { users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) {
nodes { nodes {
user { user {
......
query searchProjectMembers($fullPath: ID!, $search: String) { query searchProjectMembers($fullPath: ID!, $search: String) {
project(fullPath: $fullPath) { project(fullPath: $fullPath) {
id
projectMembers(search: $search) { projectMembers(search: $search) {
nodes { nodes {
user { user {
......
...@@ -45,7 +45,7 @@ export default { ...@@ -45,7 +45,7 @@ export default {
default: false, default: false,
}, },
selected: { selected: {
type: Object, type: [Object, Array],
required: false, required: false,
default: () => {}, default: () => {},
}, },
...@@ -54,6 +54,11 @@ export default { ...@@ -54,6 +54,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
allowMultiselect: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
isSearchEmpty() { isSearchEmpty() {
...@@ -66,8 +71,14 @@ export default { ...@@ -66,8 +71,14 @@ export default {
methods: { methods: {
selectOption(option) { selectOption(option) {
this.$emit('set-option', option || null); this.$emit('set-option', option || null);
if (!this.allowMultiselect) {
this.$refs.dropdown.hide();
}
}, },
isSelected(option) { isSelected(option) {
if (Array.isArray(this.selected)) {
return this.selected.some((label) => label.title === option.title);
}
return ( return (
this.selected && this.selected &&
((option.name && this.selected.name === option.name) || ((option.name && this.selected.name === option.name) ||
...@@ -78,7 +89,7 @@ export default { ...@@ -78,7 +89,7 @@ export default {
this.$refs.dropdown.show(); this.$refs.dropdown.show();
}, },
setFocus() { setFocus() {
this.$refs.search.focusInput(); this.$refs.search?.focusInput();
}, },
setSearchTerm(search) { setSearchTerm(search) {
this.$emit('set-search', search); this.$emit('set-search', search);
...@@ -108,6 +119,7 @@ export default { ...@@ -108,6 +119,7 @@ export default {
@shown="setFocus" @shown="setFocus"
> >
<template #header> <template #header>
<slot name="header">
<gl-search-box-by-type <gl-search-box-by-type
ref="search" ref="search"
:value="searchTerm" :value="searchTerm"
...@@ -115,7 +127,9 @@ export default { ...@@ -115,7 +127,9 @@ export default {
class="js-dropdown-input-field" class="js-dropdown-input-field"
@input="setSearchTerm" @input="setSearchTerm"
/> />
</slot>
</template> </template>
<slot name="default">
<gl-dropdown-form class="gl-relative gl-min-h-7"> <gl-dropdown-form class="gl-relative gl-min-h-7">
<gl-loading-icon <gl-loading-icon
v-if="isLoading" v-if="isLoading"
...@@ -130,7 +144,7 @@ export default { ...@@ -130,7 +144,7 @@ export default {
:is-checked="isSelected(option)" :is-checked="isSelected(option)"
:is-check-centered="true" :is-check-centered="true"
:is-check-item="true" :is-check-item="true"
@click="selectOption(option)" @click.native.capture.stop="selectOption(option)"
> >
<slot name="preset-item" :item="option"> <slot name="preset-item" :item="option">
{{ option.title }} {{ option.title }}
...@@ -147,7 +161,7 @@ export default { ...@@ -147,7 +161,7 @@ export default {
:avatar-url="avatarUrl(option)" :avatar-url="avatarUrl(option)"
:secondary-text="secondaryText(option)" :secondary-text="secondaryText(option)"
data-testid="unselected-option" data-testid="unselected-option"
@click="selectOption(option)" @click.native.capture.stop="selectOption(option)"
> >
<slot name="item" :item="option"> <slot name="item" :item="option">
{{ option.title }} {{ option.title }}
...@@ -158,6 +172,7 @@ export default { ...@@ -158,6 +172,7 @@ export default {
</gl-dropdown-item> </gl-dropdown-item>
</template> </template>
</gl-dropdown-form> </gl-dropdown-form>
</slot>
<template #footer> <template #footer>
<slot name="footer"></slot> <slot name="footer"></slot>
</template> </template>
......
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
}, },
methods: { methods: {
focusInput() { focusInput() {
this.$refs.searchInput.focusInput(); this.$refs.searchInput?.focusInput();
}, },
}, },
}; };
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
query groupLabels($fullPath: ID!, $searchTerm: String) { query groupLabels($fullPath: ID!, $searchTerm: String) {
workspace: group(fullPath: $fullPath) { workspace: group(fullPath: $fullPath) {
labels(searchTerm: $searchTerm, onlyGroupLabels: true) { id
labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) {
nodes { nodes {
...Label ...Label
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
query projectLabels($fullPath: ID!, $searchTerm: String) { query projectLabels($fullPath: ID!, $searchTerm: String) {
workspace: project(fullPath: $fullPath) { workspace: project(fullPath: $fullPath) {
id
labels(searchTerm: $searchTerm, includeAncestorGroups: true) { labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes { nodes {
...Label ...Label
......
...@@ -9,9 +9,5 @@ ...@@ -9,9 +9,5 @@
has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s, has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s,
can_admin_board: can?(current_user, :admin_issue_board, parent).to_s, can_admin_board: can?(current_user, :admin_issue_board, parent).to_s,
multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s, multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s,
labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: true),
labels_web_url: parent.is_a?(Project) ? project_labels_path(@project) : group_labels_path(@group),
project_id: @project&.id,
group_id: @group&.id,
scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false', scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false',
weights: weights.to_json } } weights: weights.to_json } }
...@@ -29,16 +29,6 @@ export default { ...@@ -29,16 +29,6 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
groupId: {
type: Number,
required: false,
default: 0,
},
projectId: {
type: Number,
required: false,
default: 0,
},
}, },
data() { data() {
return { return {
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import AssigneeSelect from './assignee_select.vue'; import AssigneeSelect from './assignee_select.vue';
import BoardScopeCurrentIteration from './board_scope_current_iteration.vue'; import BoardScopeCurrentIteration from './board_scope_current_iteration.vue';
import BoardLabelsSelect from './labels_select.vue';
import BoardMilestoneSelect from './milestone_select.vue'; import BoardMilestoneSelect from './milestone_select.vue';
import BoardWeightSelect from './weight_select.vue'; import BoardWeightSelect from './weight_select.vue';
export default { export default {
components: { components: {
AssigneeSelect, AssigneeSelect,
LabelsSelect, BoardLabelsSelect,
BoardMilestoneSelect, BoardMilestoneSelect,
BoardScopeCurrentIteration, BoardScopeCurrentIteration,
BoardWeightSelect, BoardWeightSelect,
...@@ -28,29 +28,6 @@ export default { ...@@ -28,29 +28,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: 0,
},
groupId: {
type: Number,
required: false,
default: 0,
},
weights: { weights: {
type: Array, type: Array,
required: false, required: false,
...@@ -103,8 +80,6 @@ export default { ...@@ -103,8 +80,6 @@ export default {
<board-milestone-select <board-milestone-select
v-if="isIssueBoard" v-if="isIssueBoard"
:board="board" :board="board"
:group-id="groupId"
:project-id="projectId"
:can-edit="canAdminBoard" :can-edit="canAdminBoard"
@set-milestone="$emit('set-milestone', $event)" @set-milestone="$emit('set-milestone', $event)"
/> />
...@@ -116,33 +91,17 @@ export default { ...@@ -116,33 +91,17 @@ export default {
@set-iteration="$emit('set-iteration', $event)" @set-iteration="$emit('set-iteration', $event)"
/> />
<labels-select <board-labels-select
:allow-label-edit="canAdminBoard" :board="board"
:allow-label-create="canAdminBoard" :can-edit="canAdminBoard"
:allow-label-remove="canAdminBoard"
:allow-multiselect="true"
:allow-scoped-labels="enableScopedLabels"
:selected-labels="board.labels"
:hide-collapsed-view="true"
:labels-fetch-path="labelsPath"
:labels-manage-path="labelsWebUrl"
:labels-filter-base-path="labelsWebUrl"
:labels-list-title="__('Select labels')"
:dropdown-button-text="__('Choose labels')"
variant="sidebar"
class="block labels"
@onLabelRemove="handleLabelRemove" @onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleLabelClick" @set-labels="handleLabelClick"
> />
{{ __('Any label') }}
</labels-select>
<assignee-select <assignee-select
v-if="isIssueBoard" v-if="isIssueBoard"
:board="board" :board="board"
:can-edit="canAdminBoard" :can-edit="canAdminBoard"
:project-id="projectId"
:group-id="groupId"
@set-assignee="$emit('set-assignee', $event)" @set-assignee="$emit('set-assignee', $event)"
/> />
......
<script>
import { GlButton } from '@gitlab/ui';
import { debounce } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
import searchGroupLabels from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
import searchProjectLabels from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import { __, s__, sprintf } from '~/locale';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue';
import DropdownHeader from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue';
import DropdownFooter from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
export default {
components: {
DropdownWidget,
GlButton,
LabelItem,
DropdownValue,
DropdownContentsCreateView,
DropdownHeader,
DropdownFooter,
},
inject: ['fullPath'],
props: {
board: {
type: Object,
required: true,
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
search: '',
labels: [],
selected: this.board.labels,
isEditing: false,
showDropdownContentsCreateView: false,
};
},
apollo: {
labels: {
query() {
return this.isProjectBoard ? searchProjectLabels : searchGroupLabels;
},
variables() {
return {
fullPath: this.fullPath,
searchTerm: this.search,
first: 20,
};
},
skip() {
return !this.isEditing;
},
update(data) {
return data.workspace?.labels?.nodes.map((label) => ({
...label,
id: getIdFromGraphQLId(label.id),
}));
},
error() {
this.setError({ message: this.$options.i18n.errorSearchingLabels });
},
},
},
computed: {
...mapState(['boardType']),
...mapGetters(['isProjectBoard']),
isLabelsEmpty() {
return this.selected.length === 0;
},
selectedLabelsIds() {
return this.selected.map((label) => label.id);
},
isLoading() {
return this.$apollo.queries.labels.loading;
},
selectText() {
if (!this.selected.length) {
return this.$options.i18n.selectLabel;
} else if (this.selected.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: this.selected[0].title,
remainingLabelCount: this.selected.length - 1,
});
}
return this.selected[0].title;
},
footerCreateLabelTitle() {
return sprintf(__('Create %{workspace} label'), {
workspace: this.boardType,
});
},
footerManageLabelTitle() {
return sprintf(__('Manage %{workspace} labels'), {
workspace: this.boardType,
});
},
labelType() {
return this.boardType;
},
},
methods: {
...mapActions(['setError']),
isLabelSelected(label) {
return this.selectedLabelsIds.includes(label.id);
},
selectLabel(label) {
let labels = [];
if (this.isLabelSelected(label)) {
labels = this.selected.filter(({ id }) => id !== label.id);
} else {
labels = [...this.selected, label];
}
this.selected = labels;
this.$emit('set-labels', labels);
},
onLabelRemove(labelId) {
const labels = this.selected.filter(({ id }) => getIdFromGraphQLId(id) !== labelId);
this.selected = labels;
this.$emit('set-labels', labels);
},
toggleEdit() {
if (!this.isEditing) {
this.showDropdown();
} else {
this.hideDropdown();
}
},
showDropdown() {
this.isEditing = true;
this.$refs.editDropdown.showDropdown();
debounce(() => {
this.setFocus();
}, 50)();
},
hideDropdown() {
this.isEditing = false;
},
setSearch(search) {
this.search = search;
},
toggleDropdownContentsCreateView() {
this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
},
toggleDropdownContent() {
this.toggleDropdownContentsCreateView();
// Required to recalculate dropdown position as its size changes
if (this.$refs.editDropdown?.$refs.dropdown?.$refs.dropdown) {
this.$refs.editDropdown.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
}
},
setFocus() {
this.$refs.header?.focusInput();
},
},
i18n: {
label: s__('BoardScope|Labels'),
anyLabel: s__('BoardScope|Any label'),
selectLabel: s__('BoardScope|Choose labels'),
dropdownTitleText: s__('BoardScope|Select labels'),
errorSearchingLabels: s__(
'BoardScope|An error occurred while searching for labels, please try again.',
),
edit: s__('BoardScope|Edit'),
},
};
</script>
<template>
<div class="block labels labels-select-wrapper">
<div class="title gl-mb-3">
{{ $options.i18n.label }}
<gl-button
v-if="canEdit"
category="tertiary"
size="small"
class="edit-link float-right"
@click="toggleEdit"
>
{{ $options.i18n.edit }}
</gl-button>
</div>
<div class="gl-text-gray-500 gl-mb-2" data-testid="selected-labels">
<div v-if="isLabelsEmpty">{{ $options.i18n.anyLabel }}</div>
<dropdown-value
v-else
:disable-labels="isLoading"
:selected-labels="selected"
:allow-label-remove="canEdit"
:labels-filter-base-path="''"
:labels-filter-param="'label_name'"
class="gl-mb-2"
@onLabelRemove="onLabelRemove"
/>
</div>
<dropdown-widget
v-show="isEditing"
ref="editDropdown"
:select-text="selectText"
:options="labels"
:is-loading="isLoading"
:selected="selected"
:search-term="search"
:allow-multiselect="true"
data-testid="labels-select-contents-list"
@hide="hideDropdown"
@set-option="selectLabel"
@set-search="setSearch"
>
<template #header>
<dropdown-header
ref="header"
v-model="search"
:labels-create-title="footerCreateLabelTitle"
:labels-list-title="$options.i18n.dropdownTitleText"
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
@toggleDropdownContentsCreateView="toggleDropdownContent"
@closeDropdown="hideDropdown"
@input="setSearch"
/>
</template>
<template #item="{ item }">
<label-item :label="item" />
</template>
<template v-if="showDropdownContentsCreateView" #default>
<dropdown-contents-create-view
:full-path="fullPath"
:workspace-type="boardType"
:attr-workspace-path="fullPath"
:label-create-type="labelType"
@hideCreateView="toggleDropdownContent"
/>
</template>
<template #footer>
<dropdown-footer
v-if="!showDropdownContentsCreateView"
:footer-create-label-title="footerCreateLabelTitle"
:footer-manage-label-title="footerManageLabelTitle"
@toggleDropdownContentsCreateView="toggleDropdownContent"
/>
</template>
</dropdown-widget>
</div>
</template>
...@@ -23,16 +23,6 @@ export default { ...@@ -23,16 +23,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
groupId: {
type: Number,
required: false,
default: 0,
},
projectId: {
type: Number,
required: false,
default: 0,
},
canEdit: { canEdit: {
type: Boolean, type: Boolean,
required: false, required: false,
......
...@@ -116,7 +116,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -116,7 +116,7 @@ RSpec.describe 'Scoped issue boards', :js do
page.within('.labels') do page.within('.labels') do
click_button 'Edit' click_button 'Edit'
page.within('.labels-select-contents-list') do page.within('[data-testid="labels-select-contents-list"]') do
expect(page).to have_content(group_label.title) expect(page).to have_content(group_label.title)
expect(page).not_to have_content(project_label.title) expect(page).not_to have_content(project_label.title)
end end
...@@ -381,7 +381,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -381,7 +381,7 @@ RSpec.describe 'Scoped issue boards', :js do
page.within('.labels') do page.within('.labels') do
click_button 'Edit' click_button 'Edit'
page.within('.labels-select-contents-list') do page.within('[data-testid="labels-select-contents-list"]') do
expect(page).to have_content(group_label.title) expect(page).to have_content(group_label.title)
expect(page).not_to have_content(project_label.title) expect(page).not_to have_content(project_label.title)
end end
...@@ -581,7 +581,7 @@ RSpec.describe 'Scoped issue boards', :js do ...@@ -581,7 +581,7 @@ RSpec.describe 'Scoped issue boards', :js do
click_button 'Edit' click_button 'Edit'
if value.is_a?(Array) if value.is_a?(Array)
value.each { |value| click_link value } value.each { |value| click_on value }
elsif filter == 'weight' elsif filter == 'weight'
page.within(".dropdown-menu") do page.within(".dropdown-menu") do
click_button value click_button value
......
...@@ -351,7 +351,7 @@ RSpec.describe 'epic boards', :js do ...@@ -351,7 +351,7 @@ RSpec.describe 'epic boards', :js do
if value.is_a?(Array) if value.is_a?(Array)
value.each { |value| click_link value } value.each { |value| click_link value }
else else
click_link value click_on value
end end
end end
end end
......
...@@ -39,7 +39,7 @@ RSpec.describe 'Labels Hierarchy', :js do ...@@ -39,7 +39,7 @@ RSpec.describe 'Labels Hierarchy', :js do
wait_for_requests wait_for_requests
click_link label.title click_on label.title
end end
click_button 'Save changes' click_button 'Save changes'
......
...@@ -35,6 +35,9 @@ describe('Assignee select component', () => { ...@@ -35,6 +35,9 @@ describe('Assignee select component', () => {
const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => { const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({ store = new Vuex.Store({
...defaultStore, ...defaultStore,
actions: {
setError: jest.fn(),
},
getters: { getters: {
isGroupBoard: () => isGroupBoard, isGroupBoard: () => isGroupBoard,
isProjectBoard: () => isProjectBoard, isProjectBoard: () => isProjectBoard,
......
...@@ -8,7 +8,6 @@ import createEpicBoardMutation from 'ee/boards/graphql/epic_board_create.mutatio ...@@ -8,7 +8,6 @@ import createEpicBoardMutation from 'ee/boards/graphql/epic_board_create.mutatio
import destroyEpicBoardMutation from 'ee/boards/graphql/epic_board_destroy.mutation.graphql'; import destroyEpicBoardMutation from 'ee/boards/graphql/epic_board_destroy.mutation.graphql';
import updateEpicBoardMutation from 'ee/boards/graphql/epic_board_update.mutation.graphql'; import updateEpicBoardMutation from 'ee/boards/graphql/epic_board_update.mutation.graphql';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { formType } from '~/boards/constants'; import { formType } from '~/boards/constants';
...@@ -37,8 +36,6 @@ const currentBoard = { ...@@ -37,8 +36,6 @@ const currentBoard = {
const defaultProps = { const defaultProps = {
canAdminBoard: false, canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard, currentBoard,
currentPage: '', currentPage: '',
}; };
......
...@@ -2,8 +2,9 @@ import { mount } from '@vue/test-utils'; ...@@ -2,8 +2,9 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import BoardScope from 'ee/boards/components/board_scope.vue'; import BoardScope from 'ee/boards/components/board_scope.vue';
import { TEST_HOST } from 'helpers/test_constants'; import BoardLabelsSelect from 'ee/boards/components/labels_select.vue';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import { mockLabel1 } from 'jest/boards/mock_data';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -26,17 +27,16 @@ describe('BoardScope', () => { ...@@ -26,17 +27,16 @@ describe('BoardScope', () => {
store, store,
propsData: { propsData: {
collapseScope: false, collapseScope: false,
canAdminBoard: false, canAdminBoard: true,
board: { board: {
labels: [], labels: [],
assignee: {}, assignee: {},
}, },
labelsPath: `${TEST_HOST}/labels`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
}, },
stubs: { stubs: {
AssigneeSelect: true, AssigneeSelect: true,
BoardMilestoneSelect: true, BoardMilestoneSelect: true,
BoardLabelsSelect: true,
}, },
}); });
} }
...@@ -49,15 +49,13 @@ describe('BoardScope', () => { ...@@ -49,15 +49,13 @@ describe('BoardScope', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findLabelSelect = () => wrapper.findComponent(LabelsSelect); const findLabelSelect = () => wrapper.findComponent(BoardLabelsSelect);
describe('BoardScope', () => { describe('BoardScope', () => {
it('emits selected labels to be added and removed from the board', async () => { it('emits selected labels to be added and removed from the board', async () => {
const labels = [{ id: '1', set: true, color: '#BADA55', text_color: '#FFFFFF' }]; const labels = [mockLabel1];
expect(findLabelSelect().exists()).toBe(true); expect(findLabelSelect().exists()).toBe(true);
expect(findLabelSelect().text()).toContain('Any label'); findLabelSelect().vm.$emit('set-labels', labels);
expect(findLabelSelect().props('selectedLabels')).toHaveLength(0);
findLabelSelect().vm.$emit('updateSelectedLabels', labels);
await nextTick(); await nextTick();
expect(wrapper.emitted('set-board-labels')).toEqual([[labels]]); expect(wrapper.emitted('set-board-labels')).toEqual([[labels]]);
}); });
......
...@@ -86,10 +86,6 @@ describe('BoardsSelector', () => { ...@@ -86,10 +86,6 @@ describe('BoardsSelector', () => {
hasMissingBoards: false, hasMissingBoards: false,
canAdminBoard: true, canAdminBoard: true,
multipleIssueBoardsAvailable: true, multipleIssueBoardsAvailable: true,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/labels`,
projectId: 42,
groupId: 19,
scopedIssueBoardFeatureEnabled: true, scopedIssueBoardFeatureEnabled: true,
weights: [], weights: [],
}, },
......
import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import LabelsSelect from 'ee/boards/components/labels_select.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
boardObj,
mockProjectLabelsResponse,
mockGroupLabelsResponse,
mockLabel1,
} from 'jest/boards/mock_data';
import defaultStore from '~/boards/stores';
import searchGroupLabels from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
import searchProjectLabels from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Labels select component', () => {
let wrapper;
let fakeApollo;
let store;
const selectedText = () => wrapper.find('[data-testid="selected-labels"]').text();
const findEditButton = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(DropdownWidget);
const findDropdownValue = () => wrapper.findComponent(DropdownValue);
const projectLabelsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectLabelsResponse);
const groupLabelsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupLabelsResponse);
async function openLabelsDropdown() {
findEditButton().vm.$emit('click');
await waitForPromises();
}
const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
actions: {
setError: jest.fn(),
},
getters: {
isGroupBoard: () => isGroupBoard,
isProjectBoard: () => isProjectBoard,
},
state: {
boardType: isGroupBoard ? 'group' : 'project',
},
});
};
const createComponent = ({ props = {} } = {}) => {
fakeApollo = createMockApollo([
[searchProjectLabels, projectLabelsQueryHandlerSuccess],
[searchGroupLabels, groupLabelsQueryHandlerSuccess],
]);
wrapper = shallowMount(LabelsSelect, {
localVue,
store,
apolloProvider: fakeApollo,
propsData: {
board: boardObj,
canEdit: true,
...props,
},
provide: {
fullPath: 'gitlab-org',
labelsManagePath: 'gitlab-org/labels',
},
stubs: {
GlDropdown,
GlDropdownItem,
},
});
// We need to mock out `showDropdown` which
// invokes `show` method of BDropdown used inside GlDropdown.
wrapper.vm.$refs.editDropdown.showDropdown = jest.fn();
};
beforeEach(() => {
createStore({ isProjectBoard: true });
createComponent();
});
afterEach(() => {
wrapper.destroy();
fakeApollo = null;
store = null;
});
describe('when not editing', () => {
it('defaults to Any label', () => {
expect(selectedText()).toContain('Any label');
});
it('skips the queries and does not render dropdown', () => {
expect(projectLabelsQueryHandlerSuccess).not.toHaveBeenCalled();
expect(findDropdown().isVisible()).toBe(false);
});
it('renders selected labels in DropdownValue', async () => {
await openLabelsDropdown();
findDropdown().vm.$emit('set-option', mockLabel1);
await openLabelsDropdown();
expect(findDropdownValue().isVisible()).toBe(true);
expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1]);
});
});
describe('when editing', () => {
it('trigger query and renders dropdown with passed labels', async () => {
await openLabelsDropdown();
expect(projectLabelsQueryHandlerSuccess).toHaveBeenCalled();
expect(findDropdown().isVisible()).toBe(true);
expect(findDropdown().props('options')).toHaveLength(2);
});
});
describe('canEdit', () => {
it('hides Edit button', async () => {
wrapper.setProps({ canEdit: false });
await nextTick();
expect(findEditButton().exists()).toBe(false);
});
it('shows Edit button if true', () => {
expect(findEditButton().exists()).toBe(true);
});
});
it.each`
boardType | mockedResponse | queryHandler | notCalledHandler
${'group'} | ${mockGroupLabelsResponse} | ${groupLabelsQueryHandlerSuccess} | ${projectLabelsQueryHandlerSuccess}
${'project'} | ${mockProjectLabelsResponse} | ${projectLabelsQueryHandlerSuccess} | ${groupLabelsQueryHandlerSuccess}
`(
'fetches $boardType labels',
async ({ boardType, mockedResponse, queryHandler, notCalledHandler }) => {
createStore({ isProjectBoard: boardType === 'project', isGroupBoard: boardType === 'group' });
createComponent({
[queryHandler]: jest.fn().mockResolvedValue(mockedResponse),
});
await openLabelsDropdown();
expect(queryHandler).toHaveBeenCalled();
expect(notCalledHandler).not.toHaveBeenCalled();
},
);
});
...@@ -5491,6 +5491,9 @@ msgstr "" ...@@ -5491,6 +5491,9 @@ msgstr ""
msgid "BoardScope|An error occurred while getting milestones, please try again." msgid "BoardScope|An error occurred while getting milestones, please try again."
msgstr "" msgstr ""
msgid "BoardScope|An error occurred while searching for labels, please try again."
msgstr ""
msgid "BoardScope|An error occurred while searching for users, please try again." msgid "BoardScope|An error occurred while searching for users, please try again."
msgstr "" msgstr ""
...@@ -5500,12 +5503,21 @@ msgstr "" ...@@ -5500,12 +5503,21 @@ msgstr ""
msgid "BoardScope|Any assignee" msgid "BoardScope|Any assignee"
msgstr "" msgstr ""
msgid "BoardScope|Any label"
msgstr ""
msgid "BoardScope|Assignee" msgid "BoardScope|Assignee"
msgstr "" msgstr ""
msgid "BoardScope|Choose labels"
msgstr ""
msgid "BoardScope|Edit" msgid "BoardScope|Edit"
msgstr "" msgstr ""
msgid "BoardScope|Labels"
msgstr ""
msgid "BoardScope|Milestone" msgid "BoardScope|Milestone"
msgstr "" msgstr ""
...@@ -5518,6 +5530,9 @@ msgstr "" ...@@ -5518,6 +5530,9 @@ msgstr ""
msgid "BoardScope|Select assignee" msgid "BoardScope|Select assignee"
msgstr "" msgstr ""
msgid "BoardScope|Select labels"
msgstr ""
msgid "BoardScope|Select milestone" msgid "BoardScope|Select milestone"
msgstr "" msgstr ""
......
import { GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper'; import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import BoardForm from '~/boards/components/board_form.vue'; import BoardForm from '~/boards/components/board_form.vue';
...@@ -31,8 +30,6 @@ const currentBoard = { ...@@ -31,8 +30,6 @@ const currentBoard = {
const defaultProps = { const defaultProps = {
canAdminBoard: false, canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard, currentBoard,
currentPage: '', currentPage: '',
}; };
......
...@@ -78,10 +78,6 @@ describe('BoardsSelector', () => { ...@@ -78,10 +78,6 @@ describe('BoardsSelector', () => {
hasMissingBoards: false, hasMissingBoards: false,
canAdminBoard: true, canAdminBoard: true,
multipleIssueBoardsAvailable: true, multipleIssueBoardsAvailable: true,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/labels`,
projectId: 42,
groupId: 19,
scopedIssueBoardFeatureEnabled: true, scopedIssueBoardFeatureEnabled: true,
weights: [], weights: [],
}, },
......
...@@ -12,6 +12,7 @@ export const boardObj = { ...@@ -12,6 +12,7 @@ export const boardObj = {
id: 1, id: 1,
name: 'test', name: 'test',
milestone_id: null, milestone_id: null,
labels: [],
}; };
export const listObj = { export const listObj = {
...@@ -609,3 +610,43 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [ ...@@ -609,3 +610,43 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
unique: true, unique: true,
}, },
]; ];
export const mockLabel1 = {
id: 'gid://gitlab/GroupLabel/121',
title: 'To Do',
color: '#F0AD4E',
textColor: '#FFFFFF',
description: null,
};
export const mockLabel2 = {
id: 'gid://gitlab/GroupLabel/122',
title: 'Doing',
color: '#F0AD4E',
textColor: '#FFFFFF',
description: null,
};
export const mockProjectLabelsResponse = {
data: {
workspace: {
id: 'gid://gitlab/Project/1',
labels: {
nodes: [mockLabel1, mockLabel2],
},
},
__typename: 'Project',
},
};
export const mockGroupLabelsResponse = {
data: {
workspace: {
id: 'gid://gitlab/Group/1',
labels: {
nodes: [mockLabel1, mockLabel2],
},
},
__typename: 'Group',
},
};
...@@ -34,6 +34,7 @@ describe('DropdownWidget component', () => { ...@@ -34,6 +34,7 @@ describe('DropdownWidget component', () => {
// invokes `show` method of BDropdown used inside GlDropdown. // invokes `show` method of BDropdown used inside GlDropdown.
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679 // Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation(); jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
}; };
beforeEach(() => { beforeEach(() => {
...@@ -67,10 +68,7 @@ describe('DropdownWidget component', () => { ...@@ -67,10 +68,7 @@ describe('DropdownWidget component', () => {
}); });
it('emits set-option event when clicking on an option', async () => { it('emits set-option event when clicking on an option', async () => {
wrapper wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click');
.findAll('[data-testid="unselected-option"]')
.at(1)
.vm.$emit('click', new Event('click'));
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]); expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
......
...@@ -92,6 +92,7 @@ export const createLabelSuccessfulResponse = { ...@@ -92,6 +92,7 @@ export const createLabelSuccessfulResponse = {
export const workspaceLabelsQueryResponse = { export const workspaceLabelsQueryResponse = {
data: { data: {
workspace: { workspace: {
id: 'gid://gitlab/Project/126',
labels: { labels: {
nodes: [ nodes: [
{ {
......
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