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 { isGid } from '~/graphql_shared/utils';
import { ListType, MilestoneIDs } from './constants';
export function getMilestone() {
......@@ -95,6 +96,9 @@ export function fullMilestoneId(id) {
}
export function fullLabelId(label) {
if (isGid(label.id)) {
return label.id;
}
if (label.project_id && label.project_id !== null) {
return `gid://gitlab/ProjectLabel/${label.id}`;
}
......
......@@ -57,39 +57,16 @@ export default {
type: Boolean,
required: true,
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: 0,
},
groupId: {
type: Number,
required: false,
default: 0,
},
weights: {
type: Array,
required: false,
default: () => [],
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
currentBoard: {
type: Object,
required: true,
......@@ -288,16 +265,7 @@ export default {
this.board.iteration_id = iterationId;
},
setBoardLabels(labels) {
labels.forEach((label) => {
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);
}
});
this.board.labels = labels;
},
setAssignee(assigneeId) {
this.$set(this.board, 'assignee', {
......@@ -371,11 +339,6 @@ export default {
:collapse-scope="isNewForm"
:board="board"
:can-admin-board="canAdminBoard"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
:weights="weights"
@set-iteration="setIteration"
@set-board-labels="setBoardLabels"
......
......@@ -64,22 +64,6 @@ export default {
type: Boolean,
required: true,
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
},
groupId: {
type: Number,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: true,
......@@ -88,11 +72,6 @@ export default {
type: Array,
required: true,
},
enabledScopedLabels: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
......@@ -354,14 +333,9 @@ export default {
<board-form
v-if="currentPage"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
:current-board="currentBoard"
:current-page="currentPage"
@cancel="cancel"
......
......@@ -142,5 +142,7 @@ export default () => {
fullPath: $boardApp.dataset.fullPath,
rootPath: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
allowScopedLabels: $boardApp.dataset.scopedLabels,
labelsManagePath: $boardApp.dataset.labelsManagePath,
});
};
......@@ -13,6 +13,7 @@ const apolloProvider = new VueApollo({
export default (params = {}) => {
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
const { dataset } = boardsSwitcherElement;
return new Vue({
el: boardsSwitcherElement,
components: {
......@@ -24,18 +25,17 @@ export default (params = {}) => {
fullPath: params.fullPath,
rootPath: params.rootPath,
recentBoardsEndpoint: params.recentBoardsEndpoint,
allowScopedLabels: params.allowScopedLabels,
labelsManagePath: params.labelsManagePath,
allowLabelCreate: parseBoolean(dataset.canAdminBoard),
},
data() {
const { dataset } = boardsSwitcherElement;
const boardsSelectorProps = {
...dataset,
currentBoard: JSON.parse(dataset.currentBoard),
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
canAdminBoard: parseBoolean(dataset.canAdminBoard),
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
projectId: dataset.projectId ? Number(dataset.projectId) : 0,
groupId: Number(dataset.groupId),
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
weights: JSON.parse(dataset.weights),
};
......
......@@ -3,6 +3,7 @@
query usersSearch($search: String!, $fullPath: ID!) {
workspace: group(fullPath: $fullPath) {
id
users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) {
nodes {
user {
......
query searchProjectMembers($fullPath: ID!, $search: String) {
project(fullPath: $fullPath) {
id
projectMembers(search: $search) {
nodes {
user {
......
......@@ -45,7 +45,7 @@ export default {
default: false,
},
selected: {
type: Object,
type: [Object, Array],
required: false,
default: () => {},
},
......@@ -54,6 +54,11 @@ export default {
required: false,
default: '',
},
allowMultiselect: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isSearchEmpty() {
......@@ -66,8 +71,14 @@ export default {
methods: {
selectOption(option) {
this.$emit('set-option', option || null);
if (!this.allowMultiselect) {
this.$refs.dropdown.hide();
}
},
isSelected(option) {
if (Array.isArray(this.selected)) {
return this.selected.some((label) => label.title === option.title);
}
return (
this.selected &&
((option.name && this.selected.name === option.name) ||
......@@ -78,7 +89,7 @@ export default {
this.$refs.dropdown.show();
},
setFocus() {
this.$refs.search.focusInput();
this.$refs.search?.focusInput();
},
setSearchTerm(search) {
this.$emit('set-search', search);
......@@ -108,56 +119,60 @@ export default {
@shown="setFocus"
>
<template #header>
<gl-search-box-by-type
ref="search"
:value="searchTerm"
:placeholder="searchText"
class="js-dropdown-input-field"
@input="setSearchTerm"
/>
<slot name="header">
<gl-search-box-by-type
ref="search"
:value="searchTerm"
:placeholder="searchText"
class="js-dropdown-input-field"
@input="setSearchTerm"
/>
</slot>
</template>
<gl-dropdown-form class="gl-relative gl-min-h-7">
<gl-loading-icon
v-if="isLoading"
size="md"
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<template v-else>
<template v-if="isSearchEmpty && presetOptions.length > 0">
<slot name="default">
<gl-dropdown-form class="gl-relative gl-min-h-7">
<gl-loading-icon
v-if="isLoading"
size="md"
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
/>
<template v-else>
<template v-if="isSearchEmpty && presetOptions.length > 0">
<gl-dropdown-item
v-for="option in presetOptions"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
@click.native.capture.stop="selectOption(option)"
>
<slot name="preset-item" :item="option">
{{ option.title }}
</slot>
</gl-dropdown-item>
<gl-dropdown-divider />
</template>
<gl-dropdown-item
v-for="option in presetOptions"
v-for="option in options"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
@click="selectOption(option)"
:avatar-url="avatarUrl(option)"
:secondary-text="secondaryText(option)"
data-testid="unselected-option"
@click.native.capture.stop="selectOption(option)"
>
<slot name="preset-item" :item="option">
<slot name="item" :item="option">
{{ option.title }}
</slot>
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
</template>
<gl-dropdown-item
v-for="option in options"
:key="option.id"
:is-checked="isSelected(option)"
:is-check-centered="true"
:is-check-item="true"
:avatar-url="avatarUrl(option)"
:secondary-text="secondaryText(option)"
data-testid="unselected-option"
@click="selectOption(option)"
>
<slot name="item" :item="option">
{{ option.title }}
</slot>
</gl-dropdown-item>
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
</template>
</gl-dropdown-form>
</gl-dropdown-form>
</slot>
<template #footer>
<slot name="footer"></slot>
</template>
......
......@@ -39,7 +39,7 @@ export default {
},
methods: {
focusInput() {
this.$refs.searchInput.focusInput();
this.$refs.searchInput?.focusInput();
},
},
};
......
......@@ -2,7 +2,8 @@
query groupLabels($fullPath: ID!, $searchTerm: String) {
workspace: group(fullPath: $fullPath) {
labels(searchTerm: $searchTerm, onlyGroupLabels: true) {
id
labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) {
nodes {
...Label
}
......
......@@ -2,6 +2,7 @@
query projectLabels($fullPath: ID!, $searchTerm: String) {
workspace: project(fullPath: $fullPath) {
id
labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
nodes {
...Label
......
......@@ -9,9 +9,5 @@
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,
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',
weights: weights.to_json } }
......@@ -29,16 +29,6 @@ export default {
required: false,
default: false,
},
groupId: {
type: Number,
required: false,
default: 0,
},
projectId: {
type: Number,
required: false,
default: 0,
},
},
data() {
return {
......
<script>
import { mapGetters } from 'vuex';
import { __ } from '~/locale';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import AssigneeSelect from './assignee_select.vue';
import BoardScopeCurrentIteration from './board_scope_current_iteration.vue';
import BoardLabelsSelect from './labels_select.vue';
import BoardMilestoneSelect from './milestone_select.vue';
import BoardWeightSelect from './weight_select.vue';
export default {
components: {
AssigneeSelect,
LabelsSelect,
BoardLabelsSelect,
BoardMilestoneSelect,
BoardScopeCurrentIteration,
BoardWeightSelect,
......@@ -28,29 +28,6 @@ export default {
type: Object,
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: {
type: Array,
required: false,
......@@ -103,8 +80,6 @@ export default {
<board-milestone-select
v-if="isIssueBoard"
:board="board"
:group-id="groupId"
:project-id="projectId"
:can-edit="canAdminBoard"
@set-milestone="$emit('set-milestone', $event)"
/>
......@@ -116,33 +91,17 @@ export default {
@set-iteration="$emit('set-iteration', $event)"
/>
<labels-select
:allow-label-edit="canAdminBoard"
:allow-label-create="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"
<board-labels-select
:board="board"
:can-edit="canAdminBoard"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleLabelClick"
>
{{ __('Any label') }}
</labels-select>
@set-labels="handleLabelClick"
/>
<assignee-select
v-if="isIssueBoard"
:board="board"
:can-edit="canAdminBoard"
:project-id="projectId"
:group-id="groupId"
@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 {
type: Object,
required: true,
},
groupId: {
type: Number,
required: false,
default: 0,
},
projectId: {
type: Number,
required: false,
default: 0,
},
canEdit: {
type: Boolean,
required: false,
......
......@@ -116,7 +116,7 @@ RSpec.describe 'Scoped issue boards', :js do
page.within('.labels') do
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).not_to have_content(project_label.title)
end
......@@ -381,7 +381,7 @@ RSpec.describe 'Scoped issue boards', :js do
page.within('.labels') do
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).not_to have_content(project_label.title)
end
......@@ -581,7 +581,7 @@ RSpec.describe 'Scoped issue boards', :js do
click_button 'Edit'
if value.is_a?(Array)
value.each { |value| click_link value }
value.each { |value| click_on value }
elsif filter == 'weight'
page.within(".dropdown-menu") do
click_button value
......
......@@ -351,7 +351,7 @@ RSpec.describe 'epic boards', :js do
if value.is_a?(Array)
value.each { |value| click_link value }
else
click_link value
click_on value
end
end
end
......
......@@ -39,7 +39,7 @@ RSpec.describe 'Labels Hierarchy', :js do
wait_for_requests
click_link label.title
click_on label.title
end
click_button 'Save changes'
......
......@@ -35,6 +35,9 @@ describe('Assignee select component', () => {
const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
store = new Vuex.Store({
...defaultStore,
actions: {
setError: jest.fn(),
},
getters: {
isGroupBoard: () => isGroupBoard,
isProjectBoard: () => isProjectBoard,
......
......@@ -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 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 { formType } from '~/boards/constants';
......@@ -37,8 +36,6 @@ const currentBoard = {
const defaultProps = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard,
currentPage: '',
};
......
......@@ -2,8 +2,9 @@ import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardScope from 'ee/boards/components/board_scope.vue';
import { TEST_HOST } from 'helpers/test_constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import BoardLabelsSelect from 'ee/boards/components/labels_select.vue';
import { mockLabel1 } from 'jest/boards/mock_data';
Vue.use(Vuex);
......@@ -26,17 +27,16 @@ describe('BoardScope', () => {
store,
propsData: {
collapseScope: false,
canAdminBoard: false,
canAdminBoard: true,
board: {
labels: [],
assignee: {},
},
labelsPath: `${TEST_HOST}/labels`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
},
stubs: {
AssigneeSelect: true,
BoardMilestoneSelect: true,
BoardLabelsSelect: true,
},
});
}
......@@ -49,15 +49,13 @@ describe('BoardScope', () => {
wrapper.destroy();
});
const findLabelSelect = () => wrapper.findComponent(LabelsSelect);
const findLabelSelect = () => wrapper.findComponent(BoardLabelsSelect);
describe('BoardScope', () => {
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().text()).toContain('Any label');
expect(findLabelSelect().props('selectedLabels')).toHaveLength(0);
findLabelSelect().vm.$emit('updateSelectedLabels', labels);
findLabelSelect().vm.$emit('set-labels', labels);
await nextTick();
expect(wrapper.emitted('set-board-labels')).toEqual([[labels]]);
});
......
......@@ -86,10 +86,6 @@ describe('BoardsSelector', () => {
hasMissingBoards: false,
canAdminBoard: true,
multipleIssueBoardsAvailable: true,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/labels`,
projectId: 42,
groupId: 19,
scopedIssueBoardFeatureEnabled: true,
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 ""
msgid "BoardScope|An error occurred while getting milestones, please try again."
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."
msgstr ""
......@@ -5500,12 +5503,21 @@ msgstr ""
msgid "BoardScope|Any assignee"
msgstr ""
msgid "BoardScope|Any label"
msgstr ""
msgid "BoardScope|Assignee"
msgstr ""
msgid "BoardScope|Choose labels"
msgstr ""
msgid "BoardScope|Edit"
msgstr ""
msgid "BoardScope|Labels"
msgstr ""
msgid "BoardScope|Milestone"
msgstr ""
......@@ -5518,6 +5530,9 @@ msgstr ""
msgid "BoardScope|Select assignee"
msgstr ""
msgid "BoardScope|Select labels"
msgstr ""
msgid "BoardScope|Select milestone"
msgstr ""
......
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BoardForm from '~/boards/components/board_form.vue';
......@@ -31,8 +30,6 @@ const currentBoard = {
const defaultProps = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
currentBoard,
currentPage: '',
};
......
......@@ -78,10 +78,6 @@ describe('BoardsSelector', () => {
hasMissingBoards: false,
canAdminBoard: true,
multipleIssueBoardsAvailable: true,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/labels`,
projectId: 42,
groupId: 19,
scopedIssueBoardFeatureEnabled: true,
weights: [],
},
......
......@@ -12,6 +12,7 @@ export const boardObj = {
id: 1,
name: 'test',
milestone_id: null,
labels: [],
};
export const listObj = {
......@@ -609,3 +610,43 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
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', () => {
// invokes `show` method of BDropdown used inside GlDropdown.
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
};
beforeEach(() => {
......@@ -67,10 +68,7 @@ describe('DropdownWidget component', () => {
});
it('emits set-option event when clicking on an option', async () => {
wrapper
.findAll('[data-testid="unselected-option"]')
.at(1)
.vm.$emit('click', new Event('click'));
wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
......
......@@ -92,6 +92,7 @@ export const createLabelSuccessfulResponse = {
export const workspaceLabelsQueryResponse = {
data: {
workspace: {
id: 'gid://gitlab/Project/126',
labels: {
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