Commit 361633ca authored by Tom Quirk's avatar Tom Quirk

Add searchability to ci template dropdown

- adds search box + filtering algorithm
- adds more specs for dropdown

Changelog: changed
EE: true
parent 83f63653
<script> <script>
import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui'; import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export default { export default {
name: 'CiTemplateDropdown', name: 'CiTemplateDropdown',
components: { GlDropdown, GlSearchBoxByType, GlDropdownItem }, components: {
props: { GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
GlSearchBoxByType,
},
inject: {
initialSelectedGitlabCiYmlName: {
default: null,
},
gitlabCiYmls: { gitlabCiYmls: {
type: Object, default: {},
required: true,
}, },
}, },
data() { data() {
return { return {
selectedGitlabCiYml: { selectedGitlabCiYmlName: this.initialSelectedGitlabCiYmlName,
id: s__('Android'),
name: s__('Android'),
},
searchTerm: '', searchTerm: '',
}; };
}, },
computed: {
filteredYmls() {
if (!this.searchTerm) {
return this.gitlabCiYmls;
}
return Object.keys(this.gitlabCiYmls).reduce((filteredYmls, category) => {
const categoryYmls = this.gitlabCiYmls[category].filter((yml) =>
yml.name.toLowerCase().startsWith(this.searchTerm),
);
if (categoryYmls.length > 0) {
Object.assign(filteredYmls, {
[category]: categoryYmls,
});
}
return filteredYmls;
}, {});
},
filteredTemplateCategories() {
return Object.keys(this.filteredYmls);
},
dropdownText() {
return this.selectedGitlabCiYmlName || this.$options.i18n.defaultDropdownText;
},
selectedGitlabCiYmlValue() {
return this.selectedGitlabCiYmlName;
},
},
methods: {
isDropdownItemChecked(gitlabCiYml) {
return this.selectedGitlabCiYmlName === gitlabCiYml.name;
},
onDropdownItemClick(gitlabCiYml) {
if (this.selectedGitlabCiYmlName === gitlabCiYml.name) {
this.selectedGitlabCiYmlName = null;
} else {
this.selectedGitlabCiYmlName = gitlabCiYml.name;
}
},
},
i18n: {
defaultDropdownHeaderText: s__('AdminSettings|Select a CI/CD template'),
defaultDropdownText: s__('AdminSettings|No required pipeline'),
},
TYPING_DELAY: 100, // offset user's typing slightly to potentially save excessive DOM updates
}; };
</script> </script>
<template> <template>
<gl-dropdown <div>
:text="selectedGitlabCiYml.name" <input
:header-text="s__('SelectTemplate|Select template')" id="required_instance_ci_template_name"
> type="hidden"
<template #header> name="application_setting[required_instance_ci_template]"
<gl-search-box-by-type v-model.trim="searchTerm" /> :value="selectedGitlabCiYmlValue"
</template> />
<gl-dropdown-item <gl-dropdown :text="dropdownText" :header-text="$options.i18n.defaultDropdownHeaderText" block>
v-for="gitlabCiYml in gitlabCiYmls.General" <template #header>
:key="gitlabCiYml.id" <gl-search-box-by-type v-model.trim="searchTerm" :debounce="$options.TYPING_DELAY" />
is-check-item </template>
:is-checked="selectedGitlabCiYml.id === gitlabCiYml.id"
> <div v-for="categoryName in filteredTemplateCategories" :key="categoryName">
{{ gitlabCiYml.name }} <gl-dropdown-divider />
</gl-dropdown-item> <gl-dropdown-section-header>
</gl-dropdown> {{ categoryName }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="gitlabCiYml in filteredYmls[categoryName]"
:key="gitlabCiYml.id"
is-check-item
:is-checked="isDropdownItemChecked(gitlabCiYml)"
@click="onDropdownItemClick(gitlabCiYml)"
>
{{ gitlabCiYml.name }}
</gl-dropdown-item>
</div>
</gl-dropdown>
</div>
</template> </template>
...@@ -2,16 +2,16 @@ import Vue from 'vue'; ...@@ -2,16 +2,16 @@ import Vue from 'vue';
import CiTemplateDropdown from './ci_template_dropdown.vue'; import CiTemplateDropdown from './ci_template_dropdown.vue';
const el = document.querySelector('.js-ci-template-dropdown'); const el = document.querySelector('.js-ci-template-dropdown');
const { gitlabCiYmls } = el.dataset; const { gitlabCiYmls, value } = el.dataset;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
provide: {
gitlabCiYmls: JSON.parse(gitlabCiYmls),
initialSelectedGitlabCiYmlName: value,
},
render(createElement) { render(createElement) {
return createElement(CiTemplateDropdown, { return createElement(CiTemplateDropdown);
props: {
gitlabCiYmls: JSON.parse(gitlabCiYmls),
},
});
}, },
}); });
...@@ -18,10 +18,7 @@ ...@@ -18,10 +18,7 @@
%fieldset %fieldset
.form-group .form-group
= f.label :required_instance_ci_template, s_('AdminSettings|Select a CI/CD template'), class: 'text-muted' = f.label :required_instance_ci_template, s_('AdminSettings|Select a CI/CD template')
= dropdown_tag(s_('AdminSettings|No required pipeline'), options: { toggle_class: 'js-ci-template-dropdown dropdown-select', title: s_('AdminSettings|Select a CI/CD template'), filter: true, placeholder: _("Filter"), data: { data: gitlab_ci_ymls(nil) } } ) .js-ci-template-dropdown{ data: { gitlab_ci_ymls: gitlab_ci_ymls(@project).to_json, value: @application_setting.required_instance_ci_template } }
-# -# = dropdown_tag(s_('AdminSettings|No required pipeline'), options: { toggle_class: 'js-ci-template-dropdown dropdown-select', title: s_('AdminSettings|Select a template'), filter: true, placeholder: _("Filter"), data: { data: gitlab_ci_ymls(nil) } } )
-# .js-ci-template-dropdown{ data: { gitlab_ci_ymls: gitlab_ci_ymls(nil).to_json } }
= f.text_field :required_instance_ci_template, value: @application_setting.required_instance_ci_template, id: 'required_instance_ci_template_name', class: 'hidden'
= f.submit _('Save changes'), class: "gl-button btn btn-confirm" = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
import { GlDropdown } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import CiTemplateDropdown from 'ee/pages/admin/application_settings/ci_cd/ci_template_dropdown.vue'; import CiTemplateDropdown from 'ee/pages/admin/application_settings/ci_cd/ci_template_dropdown.vue';
import { MOCK_CI_YMLS } from './mock_data';
describe('CiTemplateDropdown', () => { describe('CiTemplateDropdown', () => {
let wrapper; let wrapper;
const createComponent = () => {
wrapper = shallowMount(CiTemplateDropdown); const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownItem = () => findDropdownItems().at(0);
const findDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const search = (searchTerm) => findSearchBox().vm.$emit('input', searchTerm);
const createComponent = ({ mountFn = shallowMount, provide } = {}) => {
wrapper = mountFn(CiTemplateDropdown, {
provide: { gitlabCiYmls: MOCK_CI_YMLS, ...provide },
});
};
const assertDefaultDropdownItems = () => {
const allYmls = Object.keys(MOCK_CI_YMLS).reduce((ymls, key) => {
MOCK_CI_YMLS[key].forEach((yml) => ymls.push(yml));
return ymls;
}, []);
expect(findDropdownItems()).toHaveLength(allYmls.length);
expect(findDropdownItems().wrappers.map((h) => h.text())).toEqual(
allYmls.map((yml) => yml.name),
);
};
const assetDefaultDropdownHeaders = () => {
expect(findDropdownHeaders()).toHaveLength(Object.keys(MOCK_CI_YMLS).length);
expect(findDropdownHeaders().wrappers.map((h) => h.text())).toEqual(Object.keys(MOCK_CI_YMLS));
}; };
it('has a GlDropdown', () => { afterEach(() => {
createComponent(); wrapper.destroy();
});
describe('renders', () => {
beforeEach(() => {
createComponent();
});
it('dropdown', () => {
expect(findDropdown().exists()).toBe(true);
});
it('dropdown items', () => {
assertDefaultDropdownItems();
});
it('dropdown section headers', () => {
assetDefaultDropdownHeaders();
});
it('dropown `text` prop with default text', () => {
expect(findDropdown().props('text')).toBe('No required pipeline');
});
});
describe('when providing `initialSelectedGitlabCiYmlName` data', () => {
it('sets respective dropdown item `isChecked` prop', () => {
createComponent({ provide: { initialSelectedGitlabCiYmlName: 'test' } });
const dropdownItem = findFirstDropdownItem();
expect(dropdownItem.props('isChecked')).toBe(true);
});
});
describe('when searching', () => {
beforeEach(async () => {
createComponent({ mountFn: mount });
await search('te');
});
it('renders filtered dropdown items', () => {
const dropdownItems = findDropdownItems();
expect(dropdownItems).toHaveLength(1);
expect(dropdownItems.at(0).text()).toBe('test');
});
it('only renders section headers for sections with items', () => {
expect(findDropdownHeaders().wrappers.map((h) => h.text())).toEqual(['General']);
});
describe('when search is cleared', () => {
it('resets template to default state', async () => {
await search('');
assertDefaultDropdownItems();
assetDefaultDropdownHeaders();
});
});
});
describe('when dropdown item is clicked', () => {
beforeEach(async () => {
createComponent();
const dropdownItem = findFirstDropdownItem();
await dropdownItem.vm.$emit('click');
});
it('sets dropdown item `isChecked` prop', () => {
const dropdownItem = findFirstDropdownItem();
expect(dropdownItem.props('isChecked')).toBe(true);
});
it('`isChecked` prop of other dropdown items remains unset', () => {
const dropdownItems = findDropdownItems().wrappers.slice(1);
expect(dropdownItems.some((item) => item.props('isChecked') === true)).toBe(false);
});
it('sets dropdown `text` prop to item name', () => {
expect(findDropdown().props('text')).toBe('test');
});
const dropdown = wrapper.findComponent(GlDropdown); describe('when the selected dropdown item is clicked again', () => {
it("unsets item's `isChecked` prop", async () => {
const dropdownItem = findFirstDropdownItem();
await dropdownItem.vm.$emit('click');
expect(dropdown.exists()).toBe(true); expect(dropdownItem.props('isChecked')).toBe(false);
});
});
}); });
}); });
export const MOCK_CI_YMLS = {
General: [
{
name: 'test',
id: 'test',
},
{
name: 'node',
id: 'node',
},
{
name: 'ruby',
id: 'ruby',
},
],
Security: [
{
name: 'fizz',
id: 'fizz',
},
{
name: 'buzz',
id: 'buzz',
},
{
name: 'bar',
id: 'bar',
},
],
};
...@@ -22135,9 +22135,6 @@ msgstr "" ...@@ -22135,9 +22135,6 @@ msgstr ""
msgid "No repository" msgid "No repository"
msgstr "" msgstr ""
msgid "No required pipeline"
msgstr ""
msgid "No runner executable" msgid "No runner executable"
msgstr "" msgstr ""
......
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