Commit 19c60014 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '292103' into 'master'

Consolidate `label_select` on boards

See merge request gitlab-org/gitlab!62626
parents 62f03812 233554a8
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
......@@ -224,9 +225,6 @@ export default {
},
methods: {
...mapActions(['setError', 'unsetError']),
setIteration(iterationId) {
this.board.iteration_id = iterationId;
},
boardCreateResponse(data) {
return data.createBoard.board.webPath;
},
......@@ -237,6 +235,9 @@ export default {
: '';
return `${path}${param}`;
},
cancel() {
this.$emit('cancel');
},
async createOrUpdateBoard() {
const response = await this.$apollo.mutate({
mutation: this.currentMutation,
......@@ -280,9 +281,6 @@ export default {
}
}
},
cancel() {
this.$emit('cancel');
},
resetFormState() {
if (this.isNewForm) {
// Clear the form when we open the "New board" modal
......@@ -291,6 +289,25 @@ export default {
this.board = { ...boardDefaults, ...this.currentBoard };
}
},
setIteration(iterationId) {
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(
new ListLabel({
id: label.id,
title: label.title,
color: label.color,
textColor: label.text_color,
}),
);
} else if (!label.set) {
this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
}
});
},
},
};
</script>
......@@ -357,6 +374,7 @@ export default {
:group-id="groupId"
:weights="weights"
@set-iteration="setIteration"
@set-board-labels="setBoardLabels"
/>
</form>
</gl-modal>
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import LabelsSelect from '~/labels_select';
import { __ } from '~/locale';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import { DropdownVariant } from '../labels_select_vue/constants';
import DropdownButton from './dropdown_button.vue';
import DropdownCreateLabel from './dropdown_create_label.vue';
import DropdownFooter from './dropdown_footer.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
export default {
DropdownVariant,
components: {
DropdownTitle,
DropdownValue,
DropdownValueCollapsed,
DropdownButton,
DropdownHiddenInput,
DropdownHeader,
DropdownSearchInput,
DropdownFooter,
DropdownCreateLabel,
GlLoadingIcon,
},
props: {
showCreate: {
type: Boolean,
required: false,
default: false,
},
isProject: {
type: Boolean,
required: false,
default: false,
},
abilityName: {
type: String,
required: true,
},
context: {
type: Object,
required: true,
},
namespace: {
type: String,
required: false,
default: '',
},
updatePath: {
type: String,
required: false,
default: '',
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: false,
default: '',
},
labelFilterBasePath: {
type: String,
required: false,
default: '',
},
canEdit: {
type: Boolean,
required: false,
default: false,
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
variant: {
type: String,
required: false,
default: DropdownVariant.Sidebar,
},
},
computed: {
hiddenInputName() {
return this.showCreate ? `${this.abilityName}[label_names][]` : 'label_id[]';
},
createLabelTitle() {
if (this.isProject) {
return __('Create project label');
}
return __('Create group label');
},
manageLabelsTitle() {
if (this.isProject) {
return __('Manage project labels');
}
return __('Manage group labels');
},
},
mounted() {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick,
});
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
},
methods: {
handleClick(label) {
this.$emit('onLabelClick', label);
},
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
},
};
</script>
<template>
<div class="block labels js-labels-block">
<dropdown-value-collapsed
v-if="showCreate && variant === $options.DropdownVariant.Sidebar"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/>
<dropdown-title :can-edit="canEdit" />
<dropdown-value
:labels="context.labels"
:label-filter-base-path="labelFilterBasePath"
:enable-scoped-labels="enableScopedLabels"
>
<slot></slot>
</dropdown-value>
<div v-if="canEdit" class="selectbox js-selectbox" style="display: none">
<dropdown-hidden-input
v-for="label in context.labels"
:key="label.id"
:name="hiddenInputName"
:value="label.id"
/>
<div ref="dropdown" class="dropdown">
<dropdown-button
:ability-name="abilityName"
:field-name="hiddenInputName"
:update-path="updatePath"
:labels-path="labelsPath"
:namespace="namespace"
:labels="context.labels"
:show-extra-options="!showCreate || variant !== $options.DropdownVariant.Sidebar"
:enable-scoped-labels="enableScopedLabels"
/>
<div
class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"
>
<div class="dropdown-page-one">
<dropdown-header v-if="showCreate && variant === $options.DropdownVariant.Sidebar" />
<dropdown-search-input />
<div class="dropdown-content" data-qa-selector="labels_dropdown_content"></div>
<div class="dropdown-loading">
<gl-loading-icon
class="gl-display-flex gl-justify-content-center gl-align-items-center gl-h-full"
/>
</div>
<dropdown-footer
v-if="showCreate"
:labels-web-url="labelsWebUrl"
:create-label-title="createLabelTitle"
:manage-labels-title="manageLabelsTitle"
/>
</div>
<dropdown-create-label
v-if="showCreate"
:is-project="isProject"
:header-title="createLabelTitle"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
export default {
components: {
GlIcon,
},
props: {
abilityName: {
type: String,
required: true,
},
fieldName: {
type: String,
required: true,
},
updatePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
namespace: {
type: String,
required: true,
},
labels: {
type: Array,
required: true,
},
showExtraOptions: {
type: Boolean,
required: true,
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
dropdownToggleText() {
if (this.labels.length === 0) {
return __('Label');
}
if (this.labels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: this.labels[0].title,
remainingLabelCount: this.labels.length - 1,
});
}
return this.labels[0].title;
},
},
};
</script>
<template>
<!-- eslint-disable @gitlab/vue-no-data-toggle -->
<button
ref="dropdownButton"
:class="{ 'js-extra-options': showExtraOptions }"
:data-ability-name="abilityName"
:data-field-name="fieldName"
:data-issue-update="updatePath"
:data-labels="labelsPath"
:data-namespace-path="namespace"
:data-show-any="showExtraOptions"
:data-scoped-labels="enableScopedLabels"
type="button"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
data-toggle="dropdown"
>
<span class="dropdown-toggle-text"> {{ dropdownToggleText }} </span>
<gl-icon
name="chevron-down"
class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
:size="16"
/>
</button>
</template>
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
headerTitle: {
type: String,
required: false,
default: () => __('Create new label'),
},
},
created() {
const rawLabelsColors = gon.suggested_label_colors;
this.suggestedColors = Object.keys(rawLabelsColors).map((colorCode) => ({
colorCode,
title: rawLabelsColors[colorCode],
}));
},
};
</script>
<template>
<div class="dropdown-page-two dropdown-new-label">
<div
class="dropdown-title gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<gl-button
:aria-label="__('Go back')"
category="tertiary"
class="dropdown-menu-back"
icon="arrow-left"
size="small"
/>
{{ headerTitle }}
<gl-button
:aria-label="__('Close')"
category="tertiary"
class="dropdown-menu-close"
icon="close"
size="small"
/>
</div>
<div class="dropdown-content">
<div class="dropdown-labels-error js-label-error"></div>
<input
id="new_label_name"
:placeholder="__('Name new label')"
type="text"
class="default-dropdown-input"
/>
<div class="suggest-colors suggest-colors-dropdown">
<a
v-for="(color, index) in suggestedColors"
:key="index"
v-gl-tooltip
:data-color="color.colorCode"
:style="{
backgroundColor: color.colorCode,
}"
:title="color.title"
href="#"
>
&nbsp;
</a>
</div>
<div class="dropdown-label-color-input">
<div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
<input
id="new_label_color"
:placeholder="__('Assign custom color like #FF0000')"
type="text"
class="default-dropdown-input"
/>
</div>
<div class="clearfix">
<gl-button category="secondary" class="float-left js-new-label-btn disabled">
{{ __('Create') }}
</gl-button>
<gl-button category="secondary" class="float-right js-cancel-label-btn">
{{ __('Cancel') }}
</gl-button>
</div>
</div>
</div>
</template>
<script>
import { __ } from '~/locale';
export default {
props: {
labelsWebUrl: {
type: String,
required: true,
},
createLabelTitle: {
type: String,
required: false,
default: () => __('Create new label'),
},
manageLabelsTitle: {
type: String,
required: false,
default: () => __('Manage labels'),
},
},
};
</script>
<template>
<div class="dropdown-footer">
<ul class="dropdown-footer-list">
<li>
<a href="#" class="dropdown-toggle-page"> {{ createLabelTitle }} </a>
</li>
<li>
<a :href="labelsWebUrl" data-is-link="true" class="dropdown-external-link">
{{ manageLabelsTitle }}
</a>
</li>
</ul>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
export default {
components: {
GlIcon,
},
};
</script>
<template>
<div class="dropdown-title gl-display-flex gl-justify-content-center">
<span class="gl-ml-auto">{{ __('Assign labels') }}</span>
<button
:aria-label="__('Close')"
type="button"
class="dropdown-title-button dropdown-menu-close gl-ml-auto"
>
<gl-icon name="close" class="dropdown-menu-close-icon" />
</button>
</div>
</template>
<script>
import { GlIcon } from '@gitlab/ui';
export default {
components: {
GlIcon,
},
};
</script>
<template>
<div class="dropdown-input">
<input
:placeholder="__('Search')"
autocomplete="off"
class="dropdown-input-field"
type="search"
/>
<gl-icon
name="search"
class="dropdown-input-search gl-absolute gl-top-3 gl-right-5 gl-text-gray-300 gl-pointer-events-none"
/>
<gl-icon
name="close"
class="dropdown-input-clear js-dropdown-input-clear gl-absolute gl-top-3 gl-right-5 gl-text-gray-500"
/>
</div>
</template>
<script>
import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlLoadingIcon,
},
props: {
canEdit: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<div class="title hide-collapsed gl-mb-3">
{{ __('Labels') }}
<template v-if="canEdit">
<gl-loading-icon inline class="align-text-top block-loading" />
<button
type="button"
class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle"
data-qa-selector="labels_edit_button"
>
{{ __('Edit') }}
</button>
</template>
</div>
</template>
<script>
import { GlLabel } from '@gitlab/ui';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
GlLabel,
},
props: {
labels: {
type: Array,
required: true,
},
labelFilterBasePath: {
type: String,
required: true,
},
enableScopedLabels: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isEmpty() {
return this.labels.length === 0;
},
},
methods: {
labelFilterUrl(label) {
return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
},
scopedLabelsDescription({ description = '' }) {
return `<span class="font-weight-bold scoped-label-tooltip-title">Scoped label</span><br />${description}`;
},
showScopedLabels(label) {
return this.enableScopedLabels && isScopedLabel(label);
},
},
};
</script>
<template>
<div
:class="{
'has-labels': !isEmpty,
}"
class="hide-collapsed value issuable-show-labels js-value"
>
<span v-if="isEmpty" class="text-secondary">
<slot>{{ __('None') }}</slot>
</span>
<template v-for="label in labels" v-else>
<gl-label
:key="label.id"
:target="labelFilterUrl(label)"
:background-color="label.color"
:title="label.title"
:description="label.description"
:scoped="showScopedLabels(label)"
/>
</template>
</div>
</template>
......@@ -29,7 +29,7 @@ export default {
<gl-loading-icon v-show="labelsSelectInProgress" inline />
<gl-button
variant="link"
class="float-right js-sidebar-dropdown-toggle"
class="gl-text-gray-800! float-right js-sidebar-dropdown-toggle"
data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>{{ __('Edit') }}</gl-button
......
......@@ -5,13 +5,12 @@ import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import labelsSelectModule from './store';
Vue.use(Vuex);
......@@ -61,6 +60,11 @@ export default {
required: false,
default: () => [],
},
hideCollapsedView: {
type: Boolean,
required: false,
default: false,
},
labelsSelectInProgress: {
type: Boolean,
required: false,
......@@ -294,6 +298,7 @@ export default {
>
<template v-if="isDropdownVariantSidebar">
<dropdown-value-collapsed
v-if="!hideCollapsedView"
ref="dropdownButtonCollapsed"
:labels="selectedLabels"
@onValueClick="handleCollapsedValueClick"
......
......@@ -5,7 +5,7 @@ import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
import { DropdownVariant } from './constants';
import DropdownButton from './dropdown_button.vue';
......
<script>
import { mapGetters } from 'vuex';
import ListLabel from '~/boards/models/label';
import { __ } from '~/locale';
import BoardLabelsSelect from '~/vue_shared/components/sidebar/labels_select/base.vue';
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 BoardMilestoneSelect from './milestone_select.vue';
......@@ -11,12 +10,11 @@ import BoardWeightSelect from './weight_select.vue';
export default {
components: {
AssigneeSelect,
BoardLabelsSelect,
LabelsSelect,
BoardMilestoneSelect,
BoardScopeCurrentIteration,
BoardWeightSelect,
},
props: {
collapseScope: {
type: Boolean,
......@@ -74,26 +72,12 @@ export default {
},
methods: {
handleLabelClick(label) {
if (label.isAny) {
// eslint-disable-next-line vue/no-mutating-props
this.board.labels = [];
} else if (!this.board.labels.find((l) => l.id === label.id)) {
// eslint-disable-next-line vue/no-mutating-props
this.board.labels.push(
new ListLabel({
id: label.id,
title: label.title,
color: label.color,
textColor: label.text_color,
}),
);
} else {
let { labels } = this.board;
labels = labels.filter((selected) => selected.id !== label.id);
// eslint-disable-next-line vue/no-mutating-props
this.board.labels = labels;
}
handleLabelClick(labels) {
this.$emit('set-board-labels', labels);
},
handleLabelRemove(labelId) {
const labelToRemove = [{ id: labelId, set: false }];
this.handleLabelClick(labelToRemove);
},
},
};
......@@ -126,18 +110,26 @@ export default {
@set-iteration="$emit('set-iteration', $event)"
/>
<board-labels-select
:context="board"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:can-edit="canAdminBoard"
:show-create="canAdminBoard"
:enable-scoped-labels="enableScopedLabels"
variant="standalone"
ability-name="issue"
@onLabelClick="handleLabelClick"
>{{ __('Any label') }}</board-labels-select
<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"
@onLabelRemove="handleLabelRemove"
@updateSelectedLabels="handleLabelClick"
>
{{ __('Any label') }}
</labels-select>
<assignee-select
v-if="isIssueBoard"
......
......@@ -70,7 +70,12 @@ export default {
<div class="block weight">
<div class="title gl-mb-3">
{{ __('Weight') }}
<gl-button v-if="canEdit" variant="link" class="float-right" @click="showDropdown">
<gl-button
v-if="canEdit"
variant="link"
class="float-right gl-text-gray-800!"
@click="showDropdown"
>
{{ __('Edit') }}
</gl-button>
</div>
......
......@@ -116,7 +116,7 @@ RSpec.describe 'Scoped issue boards', :js do
page.within('.labels') do
click_button 'Edit'
page.within('.dropdown') do
page.within('.labels-select-contents-list') do
expect(page).to have_content(group_label.title)
expect(page).not_to have_content(project_label.title)
end
......@@ -358,7 +358,7 @@ RSpec.describe 'Scoped issue boards', :js do
page.within('.labels') do
click_button 'Edit'
page.within('.dropdown') do
page.within('.labels-select-contents-list') do
expect(page).to have_content(group_label.title)
expect(page).not_to have_content(project_label.title)
end
......
......@@ -357,7 +357,7 @@ RSpec.describe 'epic boards', :js do
click_value(filter, value)
click_on_board_modal
send_keys :escape
click_button 'Create board'
......@@ -371,7 +371,7 @@ RSpec.describe 'epic boards', :js do
click_value(filter, value)
click_on_board_modal
send_keys :escape
click_button 'Save changes'
......@@ -384,12 +384,6 @@ RSpec.describe 'epic boards', :js do
update_board_scope('labels', label_title)
end
# Click on modal to make sure the dropdown is closed (e.g. label scenario)
#
def click_on_board_modal
find('.board-config-modal .modal-content').click
end
# This isnt the "best" matcher but because we have opts
# != and = the find function returns both links when finding by =
def click_token_equals
......
......@@ -32,18 +32,18 @@ RSpec.describe 'Labels Hierarchy', :js do
labels.each do |label|
page.within('.filter-dropdown-container') do
find('button', text: "Edit board").click
click_button 'Edit board'
end
page.within('.js-labels-block') do
find('.edit-link').click
end
page.within('.block.labels') do
click_button 'Edit'
wait_for_requests
wait_for_requests
find('a.label-item', text: label.title).click
click_link label.title
end
find('button', text: "Save changes").click
click_button 'Save changes'
wait_for_requests
......
import { createLocalVue, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import BoardScope from 'ee/boards/components/board_scope.vue';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import { TEST_HOST } from 'helpers/test_constants';
import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('BoardScope', () => {
let wrapper;
let vm;
let store;
useMockIntersectionObserver();
beforeEach(() => {
const propsData = {
collapseScope: false,
canAdminBoard: false,
board: {
labels: [],
assignee: {},
const createStore = () => {
return new Vuex.Store({
getters: {
isIssueBoard: () => true,
isEpicBoard: () => false,
},
labelsPath: `${TEST_HOST}/labels`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
};
const createStore = () => {
return new Vuex.Store({
getters: {
isIssueBoard: () => true,
isEpicBoard: () => false,
},
});
};
const store = createStore();
});
};
function mountComponent() {
wrapper = mount(BoardScope, {
localVue,
propsData,
store,
propsData: {
collapseScope: false,
canAdminBoard: false,
board: {
labels: [],
assignee: {},
},
labelsPath: `${TEST_HOST}/labels`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
},
});
}
({ vm } = wrapper);
beforeEach(() => {
store = createStore();
mountComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('methods', () => {
describe('handleLabelClick', () => {
const label = {
id: 1,
title: 'Foo',
color: ['#BADA55'],
text_color: '#FFFFFF',
};
it('initializes `board.labels` as empty array when `label.isAny` is `true`', () => {
const labelIsAny = { isAny: true };
vm.handleLabelClick(labelIsAny);
expect(Array.isArray(vm.board.labels)).toBe(true);
expect(vm.board.labels).toHaveLength(0);
});
it('adds provided `label` to board.labels', () => {
vm.handleLabelClick(label);
const findLabelSelect = () => wrapper.findComponent(LabelsSelect);
expect(vm.board.labels).toHaveLength(1);
expect(vm.board.labels[0].id).toBe(label.id);
vm.handleLabelClick(label);
});
describe('ee/app/assets/javascripts/boards/components/board_scope.vue', () => {
it('emits selected labels to be added and removed from the board', async () => {
const labels = [{ id: '1', set: true, color: '#BADA55', text_color: '#FFFFFF' }];
expect(findLabelSelect().exists()).toBe(true);
expect(findLabelSelect().text()).toContain('Any label');
expect(findLabelSelect().props('selectedLabels')).toHaveLength(0);
findLabelSelect().vm.$emit('updateSelectedLabels', labels);
await nextTick();
expect(wrapper.emitted('set-board-labels')).toEqual([[labels]]);
});
});
});
......@@ -30,7 +30,7 @@ module QA
element :labels_dropdown_content
end
base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue' do
base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue' do
element :labels_edit_button
end
......
......@@ -24,11 +24,11 @@ module QA
element :create_new_board_button
end
view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue' do
view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue' do
element :labels_dropdown_content
end
view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue' do
view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue' do
element :labels_edit_button
end
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import LabelsSelect from '~/labels_select';
import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
import { mockConfig, mockLabels } from './mock_data';
const createComponent = (config = mockConfig) =>
shallowMount(BaseComponent, {
propsData: config,
});
describe('BaseComponent', () => {
let wrapper;
let vm;
beforeEach((done) => {
wrapper = createComponent();
({ vm } = wrapper);
Vue.nextTick(done);
});
afterEach(() => {
wrapper.destroy();
});
describe('computed', () => {
describe('hiddenInputName', () => {
it('returns correct string when showCreate prop is `true`', () => {
expect(vm.hiddenInputName).toBe('issue[label_names][]');
});
it('returns correct string when showCreate prop is `false`', async () => {
await wrapper.setProps({ showCreate: false });
expect(vm.hiddenInputName).toBe('label_id[]');
});
});
describe('createLabelTitle', () => {
it('returns `Create project label` when `isProject` prop is true', () => {
expect(vm.createLabelTitle).toBe('Create project label');
});
it('return `Create group label` when `isProject` prop is false', async () => {
await wrapper.setProps({ isProject: false });
expect(vm.createLabelTitle).toBe('Create group label');
});
});
describe('manageLabelsTitle', () => {
it('returns `Manage project labels` when `isProject` prop is true', () => {
expect(vm.manageLabelsTitle).toBe('Manage project labels');
});
it('return `Manage group labels` when `isProject` prop is false', async () => {
await wrapper.setProps({ isProject: false });
expect(vm.manageLabelsTitle).toBe('Manage group labels');
});
});
});
describe('methods', () => {
describe('handleClick', () => {
it('emits onLabelClick event with label and list of labels as params', () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleClick(mockLabels[0]);
expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
});
});
describe('handleCollapsedValueClick', () => {
it('emits toggleCollapse event on component', () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleCollapsedValueClick();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
});
});
describe('handleDropdownHidden', () => {
it('emits onDropdownClose event on component', () => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleDropdownHidden();
expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
});
});
});
describe('mounted', () => {
it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => {
expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true);
});
});
describe('template', () => {
it('renders component container element with classes `block labels`', () => {
expect(vm.$el.classList.contains('block')).toBe(true);
expect(vm.$el.classList.contains('labels')).toBe(true);
});
it('renders `.selectbox` element', () => {
expect(vm.$el.querySelector('.selectbox')).not.toBeNull();
expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;');
});
it('renders `.dropdown` element', () => {
expect(vm.$el.querySelector('.dropdown')).not.toBeNull();
});
it('renders `.dropdown-menu` element', () => {
const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
expect(dropdownMenuEl).not.toBeNull();
expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull();
expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull();
expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull();
});
});
});
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
import { mockConfig, mockLabels } from './mock_data';
const componentConfig = {
...mockConfig,
fieldName: 'label_id[]',
labels: mockLabels,
showExtraOptions: false,
};
const createComponent = (config = componentConfig) => {
const Component = Vue.extend(dropdownButtonComponent);
return mountComponent(Component, config);
};
describe('DropdownButtonComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('dropdownToggleText', () => {
it('returns text as `Label` when `labels` prop is empty array', () => {
const mockEmptyLabels = { ...componentConfig, labels: [] };
const vmEmptyLabels = createComponent(mockEmptyLabels);
expect(vmEmptyLabels.dropdownToggleText).toBe('Label');
vmEmptyLabels.$destroy();
});
it('returns first label name with remaining label count when `labels` prop has more than one item', () => {
const mockMoreLabels = { ...componentConfig, labels: mockLabels.concat(mockLabels) };
const vmMoreLabels = createComponent(mockMoreLabels);
expect(vmMoreLabels.dropdownToggleText).toBe(
`Foo Label +${mockMoreLabels.labels.length - 1} more`,
);
vmMoreLabels.$destroy();
});
it('returns first label name when `labels` prop has only one item present', () => {
const singleLabel = { ...componentConfig, labels: [mockLabels[0]] };
const vmSingleLabel = createComponent(singleLabel);
expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
vmSingleLabel.$destroy();
});
});
});
describe('template', () => {
it('renders component container element of type `button`', () => {
expect(vm.$el.nodeName).toBe('BUTTON');
});
it('renders component container element with required data attributes', () => {
expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
expect(vm.$el.dataset.showAny).not.toBeDefined();
});
it('renders dropdown toggle text element', () => {
const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
expect(dropdownToggleTextEl).not.toBeNull();
expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more');
});
it('renders dropdown button icon', () => {
const dropdownIconEl = vm.$el.querySelector('.dropdown-menu-toggle .gl-icon');
expect(dropdownIconEl).not.toBeNull();
});
});
});
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
import { mockSuggestedColors } from './mock_data';
const createComponent = (headerTitle) => {
const Component = Vue.extend(dropdownCreateLabelComponent);
return mountComponent(Component, {
headerTitle,
});
};
describe('DropdownCreateLabelComponent', () => {
const colorsCount = Object.keys(mockSuggestedColors).length;
let vm;
beforeEach(() => {
gon.suggested_label_colors = mockSuggestedColors;
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('created', () => {
it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
expect(vm.suggestedColors.length).toBe(colorsCount);
});
});
describe('template', () => {
it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => {
expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true);
});
it('renders `Go back` button on component header', () => {
const backButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-back');
expect(backButtonEl).not.toBe(null);
expect(backButtonEl.querySelector('[data-testid="arrow-left-icon"]')).not.toBe(null);
});
it('renders component header element as `Create new label` when `headerTitle` prop is not provided', () => {
const headerEl = vm.$el.querySelector('.dropdown-title');
expect(headerEl.innerText.trim()).toContain('Create new label');
});
it('renders component header element with value of `headerTitle` prop', () => {
const headerTitle = 'Create project label';
const vmWithHeaderTitle = createComponent(headerTitle);
const headerEl = vmWithHeaderTitle.$el.querySelector('.dropdown-title');
expect(headerEl.innerText.trim()).toContain(headerTitle);
vmWithHeaderTitle.$destroy();
});
it('renders `Close` button on component header', () => {
const closeButtonEl = vm.$el.querySelector('.dropdown-title .dropdown-menu-close');
expect(closeButtonEl).not.toBe(null);
});
it('renders `Name new label` input element', () => {
expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null);
expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null);
});
it('renders suggested colors list elements', () => {
const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
expect(colorsListContainerEl).not.toBe(null);
expect(colorsListContainerEl.querySelectorAll('a').length).toBe(colorsCount);
const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode);
expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 153, 102);');
});
it('renders color input element', () => {
expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null);
expect(
vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview'),
).not.toBe(null);
expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null);
});
it('renders component action buttons', () => {
const createBtnEl = vm.$el.querySelector('button.js-new-label-btn');
const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn');
expect(createBtnEl).not.toBe(null);
expect(createBtnEl.innerText.trim()).toBe('Create');
expect(cancelBtnEl.innerText.trim()).toBe('Cancel');
});
});
});
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
import { mockConfig } from './mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
createLabelTitle,
manageLabelsTitle,
) => {
const Component = Vue.extend(dropdownFooterComponent);
return mountComponent(Component, {
labelsWebUrl,
createLabelTitle,
manageLabelsTitle,
});
};
describe('DropdownFooterComponent', () => {
const createLabelTitle = 'Create project label';
const manageLabelsTitle = 'Manage project labels';
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('template', () => {
it('renders link element with `Create new label` when `createLabelTitle` prop is not provided', () => {
const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page');
expect(createLabelEl).not.toBeNull();
expect(createLabelEl.innerText.trim()).toBe('Create new label');
});
it('renders link element with value of `createLabelTitle` prop', () => {
const vmWithCreateLabelTitle = createComponent(mockConfig.labelsWebUrl, createLabelTitle);
const createLabelEl = vmWithCreateLabelTitle.$el.querySelector(
'.dropdown-footer-list .dropdown-toggle-page',
);
expect(createLabelEl.innerText.trim()).toBe(createLabelTitle);
vmWithCreateLabelTitle.$destroy();
});
it('renders link element with `Manage labels` when `manageLabelsTitle` prop is not provided', () => {
const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link');
expect(manageLabelsEl).not.toBeNull();
expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl);
expect(manageLabelsEl.innerText.trim()).toBe('Manage labels');
});
it('renders link element with value of `manageLabelsTitle` prop', () => {
const vmWithManageLabelsTitle = createComponent(
mockConfig.labelsWebUrl,
createLabelTitle,
manageLabelsTitle,
);
const manageLabelsEl = vmWithManageLabelsTitle.$el.querySelector(
'.dropdown-footer-list .dropdown-external-link',
);
expect(manageLabelsEl.innerText.trim()).toBe(manageLabelsTitle);
vmWithManageLabelsTitle.$destroy();
});
});
});
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
const createComponent = () => {
const Component = Vue.extend(dropdownHeaderComponent);
return mountComponent(Component);
};
describe('DropdownHeaderComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('template', () => {
it('renders header text element', () => {
const headerEl = vm.$el.querySelector('.dropdown-title span');
expect(headerEl.innerText.trim()).toBe('Assign labels');
});
it('renders `Close` button element', () => {
const closeBtnEl = vm.$el.querySelector(
'.dropdown-title button.dropdown-title-button.dropdown-menu-close',
);
expect(closeBtnEl).not.toBeNull();
expect(closeBtnEl.querySelector('.dropdown-menu-close-icon')).not.toBeNull();
});
});
});
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
const createComponent = () => {
const Component = Vue.extend(dropdownSearchInputComponent);
return mountComponent(Component);
};
describe('DropdownSearchInputComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('template', () => {
it('renders input element with type `search`', () => {
const inputEl = vm.$el.querySelector('input.dropdown-input-field');
expect(inputEl).not.toBeNull();
expect(inputEl.getAttribute('type')).toBe('search');
});
it('renders search icon element', () => {
expect(vm.$el.querySelector('.dropdown-input-search')).not.toBeNull();
});
it('renders clear search icon element', () => {
expect(vm.$el.querySelector('.dropdown-input-clear.js-dropdown-input-clear')).not.toBeNull();
});
});
});
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
const createComponent = (canEdit = true) =>
shallowMount(dropdownTitleComponent, {
propsData: {
canEdit,
},
});
describe('DropdownTitleComponent', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('template', () => {
it('renders title text', () => {
expect(wrapper.vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true);
expect(wrapper.vm.$el.innerText.trim()).toContain('Labels');
});
it('renders spinner icon element', () => {
expect(wrapper.find(GlLoadingIcon)).not.toBeNull();
});
it('renders `Edit` button element', () => {
const editBtnEl = wrapper.vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle');
expect(editBtnEl).not.toBeNull();
expect(editBtnEl.innerText.trim()).toBe('Edit');
});
});
});
import { GlLabel } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
import { mockConfig, mockLabels } from './mock_data';
const createComponent = (
labels = mockLabels,
labelFilterBasePath = mockConfig.labelFilterBasePath,
) =>
mount(DropdownValueComponent, {
propsData: {
labels,
labelFilterBasePath,
enableScopedLabels: true,
},
stubs: {
GlLabel: true,
},
});
describe('DropdownValueComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.destroy();
});
describe('computed', () => {
describe('isEmpty', () => {
it('returns true if `labels` prop is empty', () => {
const vmEmptyLabels = createComponent([]);
expect(vmEmptyLabels.classes()).not.toContain('has-labels');
vmEmptyLabels.destroy();
});
it('returns false if `labels` prop is empty', () => {
expect(vm.classes()).toContain('has-labels');
});
});
});
describe('methods', () => {
describe('labelFilterUrl', () => {
it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
expect(vm.find(GlLabel).props('target')).toBe(
'/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
);
});
});
describe('showScopedLabels', () => {
it('returns true if the label is scoped label', () => {
const labels = vm.findAll(GlLabel);
expect(labels.length).toEqual(2);
expect(labels.at(1).props('scoped')).toBe(true);
});
});
});
describe('template', () => {
it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => {
expect(vm.classes()).toContain('hide-collapsed', 'value', 'issuable-show-labels');
});
it('render slot content inside component when `labels` prop is empty', () => {
const vmEmptyLabels = createComponent([]);
expect(vmEmptyLabels.find('.text-secondary').text().trim()).toBe(mockConfig.emptyValueText);
vmEmptyLabels.destroy();
});
it('renders DropdownValueComponent element', () => {
const labelEl = vm.find(GlLabel);
expect(labelEl.exists()).toBe(true);
});
});
});
export const mockLabels = [
{
id: 26,
title: 'Foo Label',
description: 'Foobar',
color: '#BADA55',
text_color: '#FFFFFF',
},
{
id: 27,
title: 'Foo::Bar',
description: 'Foobar',
color: '#0033CC',
text_color: '#FFFFFF',
},
];
export const mockSuggestedColors = {
'#009966': 'Green-cyan',
'#8fbc8f': 'Dark sea green',
'#3cb371': 'Medium sea green',
'#00b140': 'Green screen',
'#013220': 'Dark green',
'#6699cc': 'Blue-gray',
'#0000ff': 'Blue',
'#e6e6fa': 'Lavendar',
'#9400d3': 'Dark violet',
'#330066': 'Deep violet',
'#808080': 'Gray',
'#36454f': 'Charcoal grey',
'#f7e7ce': 'Champagne',
'#c21e56': 'Rose red',
'#cc338b': 'Magenta-pink',
'#dc143c': 'Crimson',
'#ff0000': 'Red',
'#cd5b45': 'Dark coral',
'#eee600': 'Titanium yellow',
'#ed9121': 'Carrot orange',
'#c39953': 'Aztec Gold',
};
export const mockConfig = {
showCreate: true,
isProject: true,
abilityName: 'issue',
context: {
labels: mockLabels,
},
namespace: 'gitlab-org',
updatePath: '/gitlab-org/my-project/issue/1',
labelsPath: '/gitlab-org/my-project/-/labels.json',
labelsWebUrl: '/gitlab-org/my-project/-/labels',
labelFilterBasePath: '/gitlab-org/my-project/issues',
canEdit: true,
suggestedColors: mockSuggestedColors,
emptyValueText: 'None',
};
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
import { mockLabels } from './mock_data';
import { mockCollapsedLabels as mockLabels } from './mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
......
......@@ -2,12 +2,12 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue';
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue';
import LabelsSelectRoot from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
......
......@@ -33,6 +33,23 @@ export const mockLabels = [
},
];
export const mockCollapsedLabels = [
{
id: 26,
title: 'Foo Label',
description: 'Foobar',
color: '#BADA55',
text_color: '#FFFFFF',
},
{
id: 27,
title: 'Foo::Bar',
description: 'Foobar',
color: '#0033CC',
text_color: '#FFFFFF',
},
];
export const mockConfig = {
allowLabelEdit: true,
allowLabelCreate: true,
......
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