diff --git a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue index f4ce8f900ec4fbb919dda5c93e971961d0a7fc11..62420b0a278b0dc3c6e17fb5a5ccfe3e3f20981a 100644 --- a/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue +++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue @@ -29,6 +29,7 @@ import createPipelineMutation from '../graphql/mutations/create_pipeline.mutatio import ciConfigVariablesQuery from '../graphql/queries/ci_config_variables.graphql'; import filterVariables from '../utils/filter_variables'; import RefsDropdown from './refs_dropdown.vue'; +import VariableValuesListbox from './variable_values_listbox.vue'; let pollTimeout; export const POLLING_INTERVAL = 2000; @@ -67,6 +68,7 @@ export default { GlSprintf, GlLoadingIcon, RefsDropdown, + VariableValuesListbox, CcValidationRequiredAlert: () => import('ee_component/billings/components/cc_validation_required_alert.vue'), }, @@ -471,12 +473,10 @@ export default { data-testid="pipeline-form-ci-variable-key-field" @change="addEmptyVariable(refFullName)" /> - <gl-collapsible-listbox + <variable-values-listbox v-if="shouldShowValuesDropdown(variable.key)" :items="createListItemsFromVariableOptions(variable.key)" :selected="variable.value" - block - fluid-width :class="$options.formElementClasses" class="gl-flex-grow-1 gl-mr-0!" data-testid="pipeline-form-ci-variable-value-dropdown" diff --git a/app/assets/javascripts/ci/pipeline_new/components/variable_values_listbox.vue b/app/assets/javascripts/ci/pipeline_new/components/variable_values_listbox.vue new file mode 100644 index 0000000000000000000000000000000000000000..2c660afbeb9753e298efa94a3337439d7b965b0e --- /dev/null +++ b/app/assets/javascripts/ci/pipeline_new/components/variable_values_listbox.vue @@ -0,0 +1,67 @@ +<script> +import { GlCollapsibleListbox } from '@gitlab/ui'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { n__ } from '~/locale'; + +export default { + name: 'VariableValuesListbox', + components: { + GlCollapsibleListbox, + }, + props: { + selected: { + type: String, + required: true, + }, + items: { + type: Array, + required: true, + }, + }, + data() { + return { + searchTerm: '', + }; + }, + computed: { + searchSummary() { + return n__( + 'CiVariables|%d value found', + 'CiVariables|%d values found', + this.filteredItems.length, + ); + }, + filteredItems() { + if (this.searchTerm) { + return fuzzaldrinPlus.filter(this.items, this.searchTerm, { + key: ['text'], + }); + } + return this.items; + }, + }, + methods: { + onSearch(searchTerm) { + this.searchTerm = searchTerm.trim().toLowerCase(); + }, + }, +}; +</script> +<template> + <gl-collapsible-listbox + :items="filteredItems" + :toggle-text="selected" + :selected="selected" + :search-placeholder="s__('CiVariables|Search values')" + :no-results-text="s__('CiVariables|No matching values')" + searchable + block + fluid-width + @search="onSearch" + @select="$emit('select', $event)" + > + <template #search-summary-sr-only> + {{ searchSummary }} + </template> + </gl-collapsible-listbox> +</template> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0c8e45053b45835bd80e0407324c3aae27f49f13..c580954af7d7895a7320ce0fb7690ab6dab1f985 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10728,6 +10728,11 @@ msgstr "" msgid "CiStatusText|Warning" msgstr "" +msgid "CiVariables|%d value found" +msgid_plural "CiVariables|%d values found" +msgstr[0] "" +msgstr[1] "" + msgid "CiVariables|%{code_open}Expanded:%{code_close} Variables with %{code_open}$%{code_close} will be treated as the start of a reference to another variable." msgstr "" @@ -10800,6 +10805,9 @@ msgstr "" msgid "CiVariables|Maximum number of variables reached." msgstr "" +msgid "CiVariables|No matching values" +msgstr "" + msgid "CiVariables|Protect variable" msgstr "" @@ -10821,6 +10829,9 @@ msgstr "" msgid "CiVariables|Scope" msgstr "" +msgid "CiVariables|Search values" +msgstr "" + msgid "CiVariables|Specify variable values to be used in this run. The variables specified in the configuration file and %{linkStart}CI/CD settings%{linkEnd} are used by default." msgstr "" diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js index 369c8794fa9d592454d98e7fe6be38995b7b5047..b0a1232c2be0642ba74878bc54dc445de7caaecd 100644 --- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js +++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js @@ -20,6 +20,7 @@ import PipelineNewForm, { import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_variables.graphql'; import { resolvers } from '~/ci/pipeline_new/graphql/resolvers'; import RefsDropdown from '~/ci/pipeline_new/components/refs_dropdown.vue'; +import VariableValuesListbox from '~/ci/pipeline_new/components/variable_values_listbox.vue'; import { mockCreditCardValidationRequiredError, mockCiConfigVariablesResponse, @@ -62,8 +63,7 @@ describe('Pipeline New Form', () => { wrapper.findAllByTestId('pipeline-form-ci-variable-type'); const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key-field'); const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value-field'); - const findCollapsableListWithVariableOptions = () => - wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown').at(0); + const findCollapsableListWithVariableOptions = () => wrapper.findComponent(VariableValuesListbox); const findErrorAlert = () => wrapper.findByTestId('run-pipeline-error-alert'); const findPipelineConfigButton = () => wrapper.findByTestId('ci-cd-pipeline-configuration'); const findWarningAlert = () => wrapper.findByTestId('run-pipeline-warning-alert'); diff --git a/spec/frontend/ci/pipeline_new/components/variable_values_listbox_spec.js b/spec/frontend/ci/pipeline_new/components/variable_values_listbox_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..5226d962f6196f8a5a97c59e0bbe55d0f1c2a0eb --- /dev/null +++ b/spec/frontend/ci/pipeline_new/components/variable_values_listbox_spec.js @@ -0,0 +1,72 @@ +import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import VariableValuesListbox from '~/ci/pipeline_new/components/variable_values_listbox.vue'; +import { mockYamlVariables } from '../mock_data'; + +const { value, valueOptions } = mockYamlVariables[2]; + +describe('Variable values listbox', () => { + let wrapper; + + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findListboxItems = () => wrapper.findAllComponents(GlListboxItem); + const search = (searchString) => findListbox().vm.$emit('search', searchString); + + const createComponent = () => { + wrapper = shallowMountExtended(VariableValuesListbox, { + propsData: { + selected: value, + items: valueOptions.map((option) => ({ + text: option, + value: option, + })), + }, + stubs: { + GlCollapsibleListbox, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + it('multiple predefined values are rendered as a dropdown', () => { + for (let i = 0; i < valueOptions.length; i += 1) { + expect(findListboxItems().at(i).text()).toBe(valueOptions[i]); + } + }); + + it('variable with multiple predefined values sets value as the default', () => { + expect(findListbox().props('selected')).toBe(value); + }); + + it('filters options based on search', async () => { + const searchString = 'prod'; + + search(searchString); + + await nextTick(); + + expect(findListboxItems().length).toBe(1); + expect(findListboxItems().at(0).text()).toContain(searchString); + + search(''); + + await nextTick(); + + expect(findListboxItems().length).toBe(3); + }); + + it('filters options with fuzzy filtering', async () => { + const searchString = 'poduct'; + + search(searchString); + + await nextTick(); + + expect(findListboxItems().length).toBe(1); + expect(findListboxItems().at(0).text()).toBe('production'); + }); +});