Commit f30de9a7 authored by Simon Knox's avatar Simon Knox

Merge branch...

Merge branch '332321-refactor-dropdownvalue-component-to-fetch-selected-labels-with-graphql-apollo' into 'master'

Refactor `DropdownValue` component to rely on GraphQL + Apollo

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