From 5172a9247bc4fc7b5192247db7dfd27be4d94dce Mon Sep 17 00:00:00 2001
From: Andrew Fontaine <afontaine@gitlab.com>
Date: Tue, 26 Mar 2024 18:51:52 +0000
Subject: [PATCH] Add search functionality to sub group autocomplete

Before this, the group access dropdown had a search input that was not
configured to filter results. Here we fetch new results on query change,
passing that query value down to the GroupsFinder, where the search
functionality already exists.

Changelog: fixed
EE: true
---
 .../settings/api/access_dropdown_api.js       |  9 ++-
 .../settings/components/access_dropdown.vue   |  8 ++-
 .../autocomplete/group_subgroups_finder.rb    |  7 ++-
 .../group_subgroups_finder_spec.rb            | 11 ++++
 .../components/access_dropdown_spec.js        | 60 +++++++++++++++++++
 5 files changed, 89 insertions(+), 6 deletions(-)
 create mode 100644 spec/frontend/groups/settings/components/access_dropdown_spec.js

diff --git a/app/assets/javascripts/groups/settings/api/access_dropdown_api.js b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js
index e232b1977f0a2..5b43096325247 100644
--- a/app/assets/javascripts/groups/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js
@@ -7,16 +7,21 @@ const buildUrl = (urlRoot, url) => {
   return joinPaths(urlRoot, url);
 };
 
-const defaultOptions = { includeParentDescendants: false, includeParentSharedGroups: false };
+const defaultOptions = {
+  includeParentDescendants: false,
+  includeParentSharedGroups: false,
+  search: '',
+};
 
 export const getSubGroups = (options = defaultOptions) => {
-  const { includeParentDescendants, includeParentSharedGroups } = options;
+  const { includeParentDescendants, includeParentSharedGroups, search } = options;
 
   return axios.get(buildUrl(gon.relative_url_root || '', GROUP_SUBGROUPS_PATH), {
     params: {
       group_id: gon.current_group_id,
       include_parent_descendants: includeParentDescendants,
       include_parent_shared_groups: includeParentSharedGroups,
+      search,
     },
   });
 };
diff --git a/app/assets/javascripts/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
index 327f73b88c20b..e47092b896bfa 100644
--- a/app/assets/javascripts/groups/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
@@ -94,9 +94,11 @@ export default {
 
       if (this.hasLicense) {
         Promise.all([
-          this.groups.length
-            ? Promise.resolve({ data: this.groups })
-            : getSubGroups({ includeParentDescendants: true, includeParentSharedGroups: true }),
+          getSubGroups({
+            includeParentDescendants: true,
+            includeParentSharedGroups: true,
+            search: this.query,
+          }),
         ])
           .then(([groupsResponse]) => {
             this.consolidateData(groupsResponse.data);
diff --git a/ee/app/finders/autocomplete/group_subgroups_finder.rb b/ee/app/finders/autocomplete/group_subgroups_finder.rb
index 36bbd05248001..1b51da15f0060 100644
--- a/ee/app/finders/autocomplete/group_subgroups_finder.rb
+++ b/ee/app/finders/autocomplete/group_subgroups_finder.rb
@@ -23,12 +23,17 @@ def group_id
       params[:group_id]
     end
 
+    def search
+      params[:search]
+    end
+
     # rubocop: disable CodeReuse/Finder
     def execute
       group = ::Autocomplete::GroupFinder.new(current_user, nil, group_id: group_id).execute
       GroupsFinder.new(current_user, parent: group,
         include_parent_descendants: include_parent_descendants?,
-        include_parent_shared_groups: include_parent_shared_groups?).execute.limit(LIMIT)
+        include_parent_shared_groups: include_parent_shared_groups?,
+        search: search).execute.limit(LIMIT)
     end
     # rubocop: enable CodeReuse/Finder
   end
diff --git a/ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb b/ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb
index ac416291199a8..64366749ba9c3 100644
--- a/ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb
+++ b/ee/spec/finders/autocomplete/group_subgroups_finder_spec.rb
@@ -55,6 +55,17 @@
       end
     end
 
+    context 'when a search param is added' do
+      before do
+        params[:search] = subgroup_1.name
+      end
+
+      it 'returns only the searched for subgroups' do
+        expect(subject.count).to eq(1)
+        expect(subject).to contain_exactly(subgroup_1)
+      end
+    end
+
     context 'when the number of groups exceeds the limit' do
       before do
         stub_const("#{described_class}::LIMIT", 1)
diff --git a/spec/frontend/groups/settings/components/access_dropdown_spec.js b/spec/frontend/groups/settings/components/access_dropdown_spec.js
new file mode 100644
index 0000000000000..67a514ef35dbe
--- /dev/null
+++ b/spec/frontend/groups/settings/components/access_dropdown_spec.js
@@ -0,0 +1,60 @@
+import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { getSubGroups } from '~/groups/settings/api/access_dropdown_api';
+import AccessDropdown from '~/groups/settings/components/access_dropdown.vue';
+
+jest.mock('~/groups/settings/api/access_dropdown_api', () => ({
+  getSubGroups: jest.fn().mockResolvedValue({
+    data: [
+      { id: 4, name: 'group4' },
+      { id: 5, name: 'group5' },
+      { id: 6, name: 'group6' },
+    ],
+  }),
+}));
+
+describe('Access Level Dropdown', () => {
+  let wrapper;
+  const createComponent = ({ ...optionalProps } = {}) => {
+    wrapper = shallowMount(AccessDropdown, {
+      propsData: {
+        ...optionalProps,
+      },
+      stubs: {
+        GlDropdown,
+      },
+    });
+  };
+
+  const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+
+  describe('data request', () => {
+    it('should make an api call for sub-groups', () => {
+      createComponent();
+      expect(getSubGroups).toHaveBeenCalledWith({
+        includeParentDescendants: true,
+        includeParentSharedGroups: true,
+        search: '',
+      });
+    });
+
+    it('should not make an API call sub groups when user does not have a license', () => {
+      createComponent({ hasLicense: false });
+      expect(getSubGroups).not.toHaveBeenCalled();
+    });
+
+    it('should make api calls when search query is updated', async () => {
+      createComponent();
+      const search = 'root';
+
+      findSearchBox().vm.$emit('input', search);
+      await nextTick();
+      expect(getSubGroups).toHaveBeenCalledWith({
+        includeParentDescendants: true,
+        includeParentSharedGroups: true,
+        search,
+      });
+    });
+  });
+});
-- 
GitLab