Commit 179454bc authored by Phil Hughes's avatar Phil Hughes

Merge branch 'kp-epics-list-vue-fixes' into 'master'

Fix labels fetch & text search for Vue Epics list

See merge request gitlab-org/gitlab!57480
parents c1cdae05 f977c348
...@@ -44,7 +44,7 @@ const Api = { ...@@ -44,7 +44,7 @@ const Api = {
projectMilestonesPath: '/api/:version/projects/:id/milestones', projectMilestonesPath: '/api/:version/projects/:id/milestones',
projectIssuePath: '/api/:version/projects/:id/issues/:issue_iid', projectIssuePath: '/api/:version/projects/:id/issues/:issue_iid',
mergeRequestsPath: '/api/:version/merge_requests', mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels', groupLabelsPath: '/api/:version/groups/:namespace_path/labels',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
issuableTemplatesPath: '/:namespace_path/:project_path/templates/:type', issuableTemplatesPath: '/:namespace_path/:project_path/templates/:type',
projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key', projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key',
...@@ -402,18 +402,29 @@ const Api = { ...@@ -402,18 +402,29 @@ const Api = {
newLabel(namespacePath, projectPath, data, callback) { newLabel(namespacePath, projectPath, data, callback) {
let url; let url;
let payload;
if (projectPath) { if (projectPath) {
url = Api.buildUrl(Api.projectLabelsPath) url = Api.buildUrl(Api.projectLabelsPath)
.replace(':namespace_path', namespacePath) .replace(':namespace_path', namespacePath)
.replace(':project_path', projectPath); .replace(':project_path', projectPath);
payload = {
label: data,
};
} else { } else {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
// groupLabelsPath uses public API which accepts
// `name` and `color` props.
payload = {
name: data.title,
color: data.color,
};
} }
return axios return axios
.post(url, { .post(url, {
label: data, ...payload,
}) })
.then((res) => callback(res.data)) .then((res) => callback(res.data))
.catch((e) => callback(e.response.data)); .catch((e) => callback(e.response.data));
......
...@@ -46,7 +46,7 @@ export default { ...@@ -46,7 +46,7 @@ export default {
}, },
activeLabel() { activeLabel() {
return this.labels.find( return this.labels.find(
(label) => label.title.toLowerCase() === stripQuotes(this.currentValue), (label) => this.getLabelName(label).toLowerCase() === stripQuotes(this.currentValue),
); );
}, },
containerStyle() { containerStyle() {
...@@ -69,6 +69,21 @@ export default { ...@@ -69,6 +69,21 @@ export default {
}, },
}, },
methods: { methods: {
/**
* There's an inconsistency between private and public API
* for labels where label name is included in a different
* property;
*
* Private API => `label.title`
* Public API => `label.name`
*
* This method allows compatibility as there may be instances
* where `config.fetchLabels` provided externally may still be
* using either of the two APIs.
*/
getLabelName(label) {
return label.name || label.title;
},
fetchLabelBySearchTerm(searchTerm) { fetchLabelBySearchTerm(searchTerm) {
this.loading = true; this.loading = true;
this.config this.config
...@@ -85,7 +100,7 @@ export default { ...@@ -85,7 +100,7 @@ export default {
}); });
}, },
searchLabels: debounce(function debouncedSearch({ data }) { searchLabels: debounce(function debouncedSearch({ data }) {
this.fetchLabelBySearchTerm(data); if (!this.loading) this.fetchLabelBySearchTerm(data);
}, DEBOUNCE_DELAY), }, DEBOUNCE_DELAY),
}, },
}; };
...@@ -100,7 +115,7 @@ export default { ...@@ -100,7 +115,7 @@ export default {
> >
<template #view-token="{ inputValue, cssClasses, listeners }"> <template #view-token="{ inputValue, cssClasses, listeners }">
<gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners" <gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners"
>~{{ activeLabel ? activeLabel.title : inputValue }}</gl-token >~{{ activeLabel ? getLabelName(activeLabel) : inputValue }}</gl-token
> >
</template> </template>
<template #suggestions> <template #suggestions>
...@@ -114,13 +129,17 @@ export default { ...@@ -114,13 +129,17 @@ export default {
<gl-dropdown-divider v-if="defaultLabels.length" /> <gl-dropdown-divider v-if="defaultLabels.length" />
<gl-loading-icon v-if="loading" /> <gl-loading-icon v-if="loading" />
<template v-else> <template v-else>
<gl-filtered-search-suggestion v-for="label in labels" :key="label.id" :value="label.title"> <gl-filtered-search-suggestion
<div class="gl-display-flex"> v-for="label in labels"
:key="label.id"
:value="getLabelName(label)"
>
<div class="gl-display-flex gl-align-items-center">
<span <span
:style="{ backgroundColor: label.color }" :style="{ backgroundColor: label.color }"
class="gl-display-inline-block mr-2 p-2" class="gl-display-inline-block mr-2 p-2"
></span> ></span>
<div>{{ label.title }}</div> <div>{{ getLabelName(label) }}</div>
</div> </div>
</gl-filtered-search-suggestion> </gl-filtered-search-suggestion>
</template> </template>
......
...@@ -9,6 +9,7 @@ query groupEpics( ...@@ -9,6 +9,7 @@ query groupEpics(
$labelName: [String!] $labelName: [String!]
$milestoneTitle: String = "" $milestoneTitle: String = ""
$confidential: Boolean $confidential: Boolean
$search: String = ""
$sortBy: EpicSort $sortBy: EpicSort
$firstPageSize: Int $firstPageSize: Int
$lastPageSize: Int $lastPageSize: Int
...@@ -22,6 +23,7 @@ query groupEpics( ...@@ -22,6 +23,7 @@ query groupEpics(
labelName: $labelName labelName: $labelName
milestoneTitle: $milestoneTitle milestoneTitle: $milestoneTitle
confidential: $confidential confidential: $confidential
search: $search
sort: $sortBy sort: $sortBy
first: $firstPageSize first: $firstPageSize
last: $lastPageSize last: $lastPageSize
......
...@@ -264,18 +264,18 @@ describe('Api', () => { ...@@ -264,18 +264,18 @@ describe('Api', () => {
it('fetches group labels', (done) => { it('fetches group labels', (done) => {
const options = { params: { search: 'foo' } }; const options = { params: { search: 'foo' } };
const expectedGroup = 'gitlab-org'; const expectedGroup = 'gitlab-org';
const expectedUrl = `${dummyUrlRoot}/groups/${expectedGroup}/-/labels`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${expectedGroup}/labels`;
mock.onGet(expectedUrl).reply(httpStatus.OK, [ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{ {
id: 1, id: 1,
title: 'Foo Label', name: 'Foo Label',
}, },
]); ]);
Api.groupLabels(expectedGroup, options) Api.groupLabels(expectedGroup, options)
.then((res) => { .then((res) => {
expect(res.length).toBe(1); expect(res.length).toBe(1);
expect(res[0].title).toBe('Foo Label'); expect(res[0].name).toBe('Foo Label');
}) })
.then(done) .then(done)
.catch(done.fail); .catch(done.fail);
...@@ -593,7 +593,7 @@ describe('Api', () => { ...@@ -593,7 +593,7 @@ describe('Api', () => {
}); });
describe('newLabel', () => { describe('newLabel', () => {
it('creates a new label', (done) => { it('creates a new project label', (done) => {
const namespace = 'some namespace'; const namespace = 'some namespace';
const project = 'some project'; const project = 'some project';
const labelData = { some: 'data' }; const labelData = { some: 'data' };
...@@ -618,26 +618,23 @@ describe('Api', () => { ...@@ -618,26 +618,23 @@ describe('Api', () => {
}); });
}); });
it('creates a group label', (done) => { it('creates a new group label', (done) => {
const namespace = 'group/subgroup'; const namespace = 'group/subgroup';
const labelData = { some: 'data' }; const labelData = { name: 'Foo', color: '#000000' };
const expectedUrl = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace); const expectedUrl = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace);
const expectedData = {
label: labelData,
};
mock.onPost(expectedUrl).reply((config) => { mock.onPost(expectedUrl).reply((config) => {
expect(config.data).toBe(JSON.stringify(expectedData)); expect(config.data).toBe(JSON.stringify({ color: labelData.color }));
return [ return [
httpStatus.OK, httpStatus.OK,
{ {
name: 'test', ...labelData,
}, },
]; ];
}); });
Api.newLabel(namespace, undefined, labelData, (response) => { Api.newLabel(namespace, undefined, labelData, (response) => {
expect(response.name).toBe('test'); expect(response.name).toBe('Foo');
done(); done();
}); });
}); });
......
...@@ -118,6 +118,22 @@ describe('LabelToken', () => { ...@@ -118,6 +118,22 @@ describe('LabelToken', () => {
wrapper = createComponent(); wrapper = createComponent();
}); });
describe('getLabelName', () => {
it('returns value of `name` or `title` property present in provided label param', () => {
let mockLabel = {
title: 'foo',
};
expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.title);
mockLabel = {
name: 'foo',
};
expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.name);
});
});
describe('fetchLabelBySearchTerm', () => { describe('fetchLabelBySearchTerm', () => {
it('calls `config.fetchLabels` with provided searchTerm param', () => { it('calls `config.fetchLabels` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchLabels'); jest.spyOn(wrapper.vm.config, 'fetchLabels');
......
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