diff --git a/ee/app/assets/javascripts/dependencies/components/filtered_search/tokens/component_token.vue b/ee/app/assets/javascripts/dependencies/components/filtered_search/tokens/component_token.vue index b4c0df7da9cfe7da8faf9bfc14f1ff9d22e688d8..ae42247fe463a59c900f2a43acbf841fa0490a8c 100644 --- a/ee/app/assets/javascripts/dependencies/components/filtered_search/tokens/component_token.vue +++ b/ee/app/assets/javascripts/dependencies/components/filtered_search/tokens/component_token.vue @@ -12,6 +12,8 @@ import { NAMESPACE_GROUP, NAMESPACE_PROJECT } from 'ee/dependencies/constants'; import groupComponentsQuery from 'ee/dependencies/graphql/group_components.query.graphql'; import projectComponentsQuery from 'ee/dependencies/graphql/project_components.query.graphql'; +const MIN_CHARS = 3; + export default { components: { GlIcon, @@ -40,12 +42,18 @@ export default { searchTerm: '', components: [], selectedComponents: [], + /** + * Not using apollo.loading because debounce causes a UX issue where "noResult" state + * shows during the debounce period. Manual setting the loading state allows to show + * loading during the debounce period. + * + * More info here: + * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181132#note_2342436669 + */ + isLoadingComponents: false, }; }, computed: { - isLoadingComponents() { - return this.$apollo.queries.components.loading; - }, filteredComponents() { if (!this.searchTerm) { return this.components; @@ -93,6 +101,12 @@ export default { { namespaceType: this.namespaceType }, ); }, + isSearchTermTooShort() { + return this.searchTerm.length < MIN_CHARS; + }, + shouldShowPlaceholder() { + return this.isSearchTermTooShort && !this.components.length; + }, }, apollo: { components: { @@ -110,13 +124,18 @@ export default { // Remove __typename return data[this.namespaceType]?.components?.map(({ id, name }) => ({ name, id })); }, + result() { + this.isLoadingComponents = false; + }, error() { + this.isLoadingComponents = false; + createAlert({ message: this.fetchErrorMessage, }); }, skip() { - return this.searchTerm === ''; + return this.isSearchTermTooShort; }, }, }, @@ -135,9 +154,14 @@ export default { // the data can be either a string or an array, in which case we don't want to perform the search if (typeof token.data === 'string') { this.searchTerm = token.data.toLowerCase(); + this.isLoadingComponents = token.data.length >= MIN_CHARS; } }, }, + i18n: { + placeholder: s__('Dependencies|Enter at least 3 characters to view available components.'), + noResult: s__('Dependencies|No components found.'), + }, }; </script> @@ -180,6 +204,12 @@ export default { {{ component.name }} </div> </gl-filtered-search-suggestion> + <div v-if="shouldShowPlaceholder" class="gl-p-2 gl-text-secondary"> + {{ $options.i18n.placeholder }} + </div> + <div v-else-if="!components.length" class="gl-p-2 gl-text-secondary"> + {{ $options.i18n.noResult }} + </div> </template> </template> </gl-filtered-search-token> diff --git a/ee/spec/frontend/dependencies/components/filtered_search/tokens/component_token_spec.js b/ee/spec/frontend/dependencies/components/filtered_search/tokens/component_token_spec.js index ffc8a3c36b73fda21d78661d5e43fae04c2b200e..2c86fa64ecd9e28443d2785d6a89ee8693faa1ef 100644 --- a/ee/spec/frontend/dependencies/components/filtered_search/tokens/component_token_spec.js +++ b/ee/spec/frontend/dependencies/components/filtered_search/tokens/component_token_spec.js @@ -251,4 +251,37 @@ describe('ee/dependencies/components/filtered_search/tokens/component_token.vue' expect(findSuggestions().exists()).toBe(false); }); }); + + describe('where there is a suggestion dropdown', () => { + it('displays when user types less than 3 characters', async () => { + createComponent({ mountFn: mountExtended }); + + const suggestionText = 'Enter at least 3 characters to view available components.'; + + await searchForComponent('we'); + expect(wrapper.text()).toBe(suggestionText); + + await searchForComponent('web'); + expect(wrapper.text()).not.toBe(suggestionText); + }); + + it('displays when no results are found', async () => { + createComponent({ + handlers: { + getGroupComponentsHandler: jest.fn().mockResolvedValue({ + data: { + group: { + id: 'some-group-id', + components: [], + }, + }, + }), + }, + }); + + await searchForComponent('XXXXXXXX'); + + expect(wrapper.text()).toBe('No components found.'); + }); + }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ac4d67ff14a971a71bb1d9c917e8bf194eecb426..023e6efe67189f0ee295007d9315e6a39c1d3c0d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19473,6 +19473,9 @@ msgstr "" msgid "Dependencies|Direct dependents" msgstr "" +msgid "Dependencies|Enter at least 3 characters to view available components." +msgstr "" + msgid "Dependencies|Error exporting the dependency list. Please reload the page." msgstr "" @@ -19500,6 +19503,9 @@ msgstr "" msgid "Dependencies|Location" msgstr "" +msgid "Dependencies|No components found." +msgstr "" + msgid "Dependencies|Packager" msgstr ""