Commit 1559ca6d authored by Natalia Tepluhina's avatar Natalia Tepluhina

Refactored dropdown_value component

Changelog: changed

Passed down selected labels

Fixed query and mutation to match

Removed Vuex from dropdown value

Added missing injections to labels_select

Fixed DropdownValue test to pass

Added helpers for finding labels

Started test refactoring

Added tests for labels

Refactored dropdown-value spec
parent 0d86d7e2
mutation issueSetLabels($input: UpdateIssueInput!) { mutation issueSetLabels($input: UpdateIssueInput!) {
updateIssue(input: $input) { updateIssue(input: $input) {
issue { issue {
id
labels { labels {
nodes { nodes {
id id
......
<script> <script>
import { GlLabel } from '@gitlab/ui'; import { GlLabel } from '@gitlab/ui';
import { mapState } from 'vuex'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils'; import { isScopedLabel } from '~/lib/utils/common_utils';
export default { export default {
...@@ -14,15 +13,26 @@ export default { ...@@ -14,15 +13,26 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
}, selectedLabels: {
computed: { type: Array,
...mapState([ required: true,
'selectedLabels', },
'allowLabelRemove', allowLabelRemove: {
'allowScopedLabels', type: Boolean,
'labelsFilterBasePath', required: true,
'labelsFilterParam', },
]), allowScopedLabels: {
type: Boolean,
required: true,
},
labelsFilterBasePath: {
type: String,
required: true,
},
labelsFilterParam: {
type: String,
required: true,
},
}, },
methods: { methods: {
labelFilterUrl(label) { labelFilterUrl(label) {
...@@ -33,6 +43,9 @@ export default { ...@@ -33,6 +43,9 @@ export default {
scopedLabel(label) { scopedLabel(label) {
return this.allowScopedLabels && isScopedLabel(label); return this.allowScopedLabels && isScopedLabel(label);
}, },
removeLabel(labelId) {
this.$emit('onLabelRemove', getIdFromGraphQLId(labelId));
},
}, },
}; };
</script> </script>
...@@ -43,12 +56,14 @@ export default { ...@@ -43,12 +56,14 @@ export default {
'has-labels': selectedLabels.length, 'has-labels': selectedLabels.length,
}" }"
class="hide-collapsed value issuable-show-labels js-value" class="hide-collapsed value issuable-show-labels js-value"
data-testid="value-wrapper"
> >
<span v-if="!selectedLabels.length" class="text-secondary"> <span v-if="!selectedLabels.length" class="text-secondary" data-testid="empty-placeholder">
<slot></slot> <slot></slot>
</span> </span>
<template v-for="label in selectedLabels" v-else> <template v-else>
<gl-label <gl-label
v-for="label in selectedLabels"
:key="label.id" :key="label.id"
data-qa-selector="selected_label_content" data-qa-selector="selected_label_content"
:data-qa-label-name="label.title" :data-qa-label-name="label.title"
...@@ -60,7 +75,7 @@ export default { ...@@ -60,7 +75,7 @@ export default {
:show-close-button="allowLabelRemove" :show-close-button="allowLabelRemove"
:disabled="disableLabels" :disabled="disableLabels"
tooltip-placement="top" tooltip-placement="top"
@close="$emit('onLabelRemove', label.id)" @close="removeLabel(label.id)"
/> />
</template> </template>
</div> </div>
......
query issueLabels($fullPath: ID!, $iid: String) {
workspace: project(fullPath: $fullPath) {
issuable: issue(iid: $iid) {
id
labels {
nodes {
id
title
color
description
}
}
}
}
}
...@@ -11,6 +11,7 @@ import DropdownContents from './dropdown_contents.vue'; ...@@ -11,6 +11,7 @@ import DropdownContents from './dropdown_contents.vue';
import DropdownTitle from './dropdown_title.vue'; import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import issueLabelsQuery from './graphql/issue_labels.query.graphql';
import labelsSelectModule from './store'; import labelsSelectModule from './store';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -24,6 +25,7 @@ export default { ...@@ -24,6 +25,7 @@ export default {
DropdownContents, DropdownContents,
DropdownValueCollapsed, DropdownValueCollapsed,
}, },
inject: ['iid', 'projectPath'],
props: { props: {
allowLabelRemove: { allowLabelRemove: {
type: Boolean, type: Boolean,
...@@ -119,8 +121,23 @@ export default { ...@@ -119,8 +121,23 @@ export default {
data() { data() {
return { return {
contentIsOnViewport: true, contentIsOnViewport: true,
issueLabels: [],
}; };
}, },
apollo: {
issueLabels: {
query: issueLabelsQuery,
variables() {
return {
iid: this.iid,
fullPath: this.projectPath,
};
},
update(data) {
return data.workspace?.issuable?.labels.nodes || [];
},
},
},
computed: { computed: {
...mapState(['showDropdownButton', 'showDropdownContents']), ...mapState(['showDropdownButton', 'showDropdownContents']),
...mapGetters([ ...mapGetters([
...@@ -293,7 +310,7 @@ export default { ...@@ -293,7 +310,7 @@ export default {
<template v-if="isDropdownVariantSidebar"> <template v-if="isDropdownVariantSidebar">
<dropdown-value-collapsed <dropdown-value-collapsed
ref="dropdownButtonCollapsed" ref="dropdownButtonCollapsed"
:labels="selectedLabels" :labels="issueLabels"
@onValueClick="handleCollapsedValueClick" @onValueClick="handleCollapsedValueClick"
/> />
<dropdown-title <dropdown-title
...@@ -302,6 +319,11 @@ export default { ...@@ -302,6 +319,11 @@ export default {
/> />
<dropdown-value <dropdown-value
:disable-labels="labelsSelectInProgress" :disable-labels="labelsSelectInProgress"
:selected-labels="issueLabels"
:allow-label-remove="allowLabelRemove"
:allow-scoped-labels="allowScopedLabels"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@onLabelRemove="$emit('onLabelRemove', $event)" @onLabelRemove="$emit('onLabelRemove', $event)"
> >
<slot></slot> <slot></slot>
......
import { GlLabel } from '@gitlab/ui'; import { GlLabel } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue'; import DropdownValue from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_widget/store'; import { mockRegularLabel, mockScopedLabel } from './mock_data';
import { mockConfig, mockRegularLabel, mockScopedLabel } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('DropdownValue', () => { describe('DropdownValue', () => {
let wrapper; let wrapper;
const createComponent = (initialState = {}, slots = {}) => { const findAllLabels = () => wrapper.findAllComponents(GlLabel);
const store = new Vuex.Store(labelsSelectModule()); const findRegularLabel = () => findAllLabels().at(0);
const findScopedLabel = () => findAllLabels().at(1);
store.dispatch('setInitialState', { ...mockConfig, ...initialState }); const findWrapper = () => wrapper.find('[data-testid="value-wrapper"]');
const findEmptyPlaceholder = () => wrapper.find('[data-testid="empty-placeholder"]');
const createComponent = (props = {}, slots = {}) => {
wrapper = shallowMount(DropdownValue, { wrapper = shallowMount(DropdownValue, {
localVue,
store,
slots, slots,
propsData: {
selectedLabels: [mockRegularLabel, mockScopedLabel],
allowLabelRemove: true,
allowScopedLabels: true,
labelsFilterBasePath: '/gitlab-org/my-project/issues',
labelsFilterParam: 'label_name',
...props,
},
}); });
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('methods', () => { describe('when there are no labels', () => {
describe('labelFilterUrl', () => { beforeEach(() => {
it('returns a label filter URL based on provided label param', () => { createComponent(
createComponent(); {
selectedLabels: [],
expect(wrapper.vm.labelFilterUrl(mockRegularLabel)).toBe( },
'/gitlab-org/my-project/issues?label_name[]=Foo%20Label', {
); default: 'None',
}); },
);
}); });
describe('scopedLabel', () => { it('does not apply `has-labels` class to the wrapping container', () => {
beforeEach(() => { expect(findWrapper().classes()).not.toContain('has-labels');
createComponent(); });
});
it('returns `true` when provided label param is a scoped label', () => { it('renders an empty placeholder', () => {
expect(wrapper.vm.scopedLabel(mockScopedLabel)).toBe(true); expect(findEmptyPlaceholder().exists()).toBe(true);
}); expect(findEmptyPlaceholder().text()).toBe('None');
});
it('returns `false` when provided label param is a regular label', () => { it('does not render any labels', () => {
expect(wrapper.vm.scopedLabel(mockRegularLabel)).toBe(false); expect(findAllLabels().length).toBe(0);
});
}); });
}); });
describe('template', () => { describe('when there are labels', () => {
it('renders class `has-labels` on component container element when `selectedLabels` is not empty', () => { beforeEach(() => {
createComponent(); createComponent();
});
expect(wrapper.attributes('class')).toContain('has-labels'); it('applies `has-labels` class to the wrapping container', () => {
expect(findWrapper().classes()).toContain('has-labels');
}); });
it('renders element containing `None` when `selectedLabels` is empty', () => { it('does not render an empty placeholder', () => {
createComponent( expect(findEmptyPlaceholder().exists()).toBe(false);
{ });
selectedLabels: [],
},
{
default: 'None',
},
);
const noneEl = wrapper.find('span.text-secondary');
expect(noneEl.exists()).toBe(true); it('renders a list of two labels', () => {
expect(noneEl.text()).toBe('None'); expect(findAllLabels().length).toBe(2);
}); });
it('renders labels when `selectedLabels` is not empty', () => { it('passes correct props to the regular label', () => {
createComponent(); expect(findRegularLabel().props('target')).toBe(
'/gitlab-org/my-project/issues?label_name[]=Foo%20Label',
);
expect(findRegularLabel().props('scoped')).toBe(false);
});
it('passes correct props to the scoped label', () => {
expect(findScopedLabel().props('target')).toBe(
'/gitlab-org/my-project/issues?label_name[]=Foo%3A%3ABar',
);
expect(findScopedLabel().props('scoped')).toBe(true);
});
expect(wrapper.findAll(GlLabel).length).toBe(2); it('emits `onLabelRemove` event with the correct ID', () => {
findRegularLabel().vm.$emit('close');
expect(wrapper.emitted('onLabelRemove')).toEqual([[mockRegularLabel.id]]);
}); });
}); });
}); });
...@@ -34,6 +34,10 @@ describe('LabelsSelectRoot', () => { ...@@ -34,6 +34,10 @@ describe('LabelsSelectRoot', () => {
stubs: { stubs: {
'dropdown-contents': DropdownContents, 'dropdown-contents': DropdownContents,
}, },
provide: {
iid: '1',
projectPath: 'test',
},
}); });
}; };
......
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