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>
import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'CiTemplateDropdown',
components: { GlDropdown, GlSearchBoxByType, GlDropdownItem },
props: {
components: {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
GlSearchBoxByType,
},
inject: {
initialSelectedGitlabCiYmlName: {
default: null,
},
gitlabCiYmls: {
type: Object,
required: true,
default: {},
},
},
data() {
return {
selectedGitlabCiYml: {
id: s__('Android'),
name: s__('Android'),
},
selectedGitlabCiYmlName: this.initialSelectedGitlabCiYmlName,
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>
<template>
<gl-dropdown
:text="selectedGitlabCiYml.name"
:header-text="s__('SelectTemplate|Select template')"
>
<template #header>
<gl-search-box-by-type v-model.trim="searchTerm" />
</template>
<gl-dropdown-item
v-for="gitlabCiYml in gitlabCiYmls.General"
:key="gitlabCiYml.id"
is-check-item
:is-checked="selectedGitlabCiYml.id === gitlabCiYml.id"
>
{{ gitlabCiYml.name }}
</gl-dropdown-item>
</gl-dropdown>
<div>
<input
id="required_instance_ci_template_name"
type="hidden"
name="application_setting[required_instance_ci_template]"
:value="selectedGitlabCiYmlValue"
/>
<gl-dropdown :text="dropdownText" :header-text="$options.i18n.defaultDropdownHeaderText" block>
<template #header>
<gl-search-box-by-type v-model.trim="searchTerm" :debounce="$options.TYPING_DELAY" />
</template>
<div v-for="categoryName in filteredTemplateCategories" :key="categoryName">
<gl-dropdown-divider />
<gl-dropdown-section-header>
{{ 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>
......@@ -2,16 +2,16 @@ import Vue from 'vue';
import CiTemplateDropdown from './ci_template_dropdown.vue';
const el = document.querySelector('.js-ci-template-dropdown');
const { gitlabCiYmls } = el.dataset;
const { gitlabCiYmls, value } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
provide: {
gitlabCiYmls: JSON.parse(gitlabCiYmls),
initialSelectedGitlabCiYmlName: value,
},
render(createElement) {
return createElement(CiTemplateDropdown, {
props: {
gitlabCiYmls: JSON.parse(gitlabCiYmls),
},
});
return createElement(CiTemplateDropdown);
},
});
......@@ -18,10 +18,7 @@
%fieldset
.form-group
= f.label :required_instance_ci_template, s_('AdminSettings|Select a CI/CD template'), class: 'text-muted'
= 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) } } )
-# -# = 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.label :required_instance_ci_template, s_('AdminSettings|Select a CI/CD template')
.js-ci-template-dropdown{ data: { gitlab_ci_ymls: gitlab_ci_ymls(@project).to_json, value: @application_setting.required_instance_ci_template } }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import CiTemplateDropdown from 'ee/pages/admin/application_settings/ci_cd/ci_template_dropdown.vue';
import { MOCK_CI_YMLS } from './mock_data';
describe('CiTemplateDropdown', () => {
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', () => {
createComponent();
afterEach(() => {
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 ""
msgid "No repository"
msgstr ""
msgid "No required pipeline"
msgstr ""
msgid "No runner executable"
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