From 3ae055ae56408f2d7a96019aa3bfa395eb142a9e Mon Sep 17 00:00:00 2001
From: Olena Horal-Koretska <ohoralkoretska@gitlab.com>
Date: Wed, 21 Jun 2023 10:03:09 +0000
Subject: [PATCH] Command palette jump to project file

---
 .../command_palette/command_palette_items.vue | 55 ++++++++++++--
 .../command_palette/constants.js              |  7 +-
 .../command_palette/fake_search_input.vue     |  4 +-
 .../global_search/command_palette/utils.js    | 14 +++-
 .../components/global_search.vue              | 27 +++++--
 .../super_sidebar/super_sidebar_bundle.js     | 14 +++-
 app/helpers/sidebars_helper.rb                | 10 +++
 app/views/layouts/_page.html.haml             |  2 +-
 locale/gitlab.pot                             |  8 +-
 .../command_palette_items_spec.js             | 73 ++++++++++++++++++-
 .../command_palette/mock_data.js              | 12 +++
 .../command_palette/utils_spec.js             | 13 ++++
 .../components/global_search_spec.js          | 10 ++-
 spec/helpers/sidebars_helper_spec.rb          | 27 +++++++
 14 files changed, 246 insertions(+), 30 deletions(-)

diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
index 96e6c9bab9ef..b8921bd0bfa4 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -5,18 +5,20 @@ import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
 import axios from '~/lib/utils/axios_utils';
 import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
 import { getFormattedItem } from '../utils';
+
 import {
   COMMON_HANDLES,
   COMMAND_HANDLE,
   USER_HANDLE,
   PROJECT_HANDLE,
   ISSUE_HANDLE,
-  GLOBAL_COMMANDS_GROUP_TITLE,
+  PATH_HANDLE,
   PAGES_GROUP_TITLE,
+  PATH_GROUP_TITLE,
   GROUP_TITLES,
 } from './constants';
 import SearchItem from './search_item.vue';
-import { commandMapper, linksReducer, autocompleteQuery } from './utils';
+import { commandMapper, linksReducer, autocompleteQuery, fileMapper } from './utils';
 
 export default {
   name: 'CommandPaletteItems',
@@ -25,7 +27,14 @@ export default {
     GlLoadingIcon,
     SearchItem,
   },
-  inject: ['commandPaletteCommands', 'commandPaletteLinks', 'autocompletePath', 'searchContext'],
+  inject: [
+    'commandPaletteCommands',
+    'commandPaletteLinks',
+    'autocompletePath',
+    'searchContext',
+    'projectFilesPath',
+    'projectBlobPath',
+  ],
   props: {
     searchQuery: {
       type: String,
@@ -35,7 +44,7 @@ export default {
       type: String,
       required: true,
       validator: (value) => {
-        return COMMON_HANDLES.includes(value);
+        return [...COMMON_HANDLES, PATH_HANDLE].includes(value);
       },
     },
   },
@@ -43,13 +52,14 @@ export default {
     groups: [],
     error: null,
     loading: false,
+    projectFiles: [],
   }),
   computed: {
     isCommandMode() {
       return this.handle === COMMAND_HANDLE;
     },
-    isUserMode() {
-      return this.handle === USER_HANDLE;
+    isPathMode() {
+      return this.handle === PATH_HANDLE;
     },
     commands() {
       return this.commandPaletteCommands.map(commandMapper);
@@ -62,7 +72,7 @@ export default {
         ? this.commands
             .map(({ name, items }) => {
               return {
-                name: name || GLOBAL_COMMANDS_GROUP_TITLE,
+                name,
                 items: this.filterBySearchQuery(items, 'text'),
               };
             })
@@ -73,7 +83,7 @@ export default {
       return this.groups?.length && this.groups.some((group) => group.items?.length);
     },
     hasSearchQuery() {
-      if (this.isCommandMode) {
+      if (this.isCommandMode || this.isPathMode) {
         return this.searchQuery?.length > 0;
       }
       return this.searchQuery?.length > 2;
@@ -84,6 +94,12 @@ export default {
       }
       return this.searchQuery;
     },
+    filteredProjectFiles() {
+      if (!this.searchQuery) {
+        return this.projectFiles;
+      }
+      return this.filterBySearchQuery(this.projectFiles, 'text');
+    },
   },
   watch: {
     searchQuery: {
@@ -97,6 +113,9 @@ export default {
           case ISSUE_HANDLE:
             this.getScopedItems();
             break;
+          case PATH_HANDLE:
+            this.getProjectFiles();
+            break;
           default:
             break;
         }
@@ -162,6 +181,26 @@ export default {
         },
       ];
     },
+    async getProjectFiles() {
+      if (!this.projectFiles.length) {
+        this.loading = true;
+        try {
+          const response = await axios.get(this.projectFilesPath);
+          this.projectFiles = response?.data.map(fileMapper.bind(null, this.projectBlobPath));
+        } catch (error) {
+          this.error = error;
+        } finally {
+          this.loading = false;
+        }
+      }
+
+      this.groups = [
+        {
+          name: PATH_GROUP_TITLE,
+          items: this.filteredProjectFiles,
+        },
+      ];
+    },
   },
 };
 </script>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
index 9dab16984f50..780936c1b885 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
@@ -4,17 +4,19 @@ export const COMMAND_HANDLE = '>';
 export const USER_HANDLE = '@';
 export const PROJECT_HANDLE = '&';
 export const ISSUE_HANDLE = '#';
+export const PATH_HANDLE = '/';
 
 export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE, ISSUE_HANDLE];
 export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf(
   s__(
-    'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue or perform generic search...',
+    'CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue, %{pathHandle} for project file or perform generic search...',
   ),
   {
     commandHandle: COMMAND_HANDLE,
     userHandle: USER_HANDLE,
     issueHandle: ISSUE_HANDLE,
     projectHandle: PROJECT_HANDLE,
+    pathHandle: PATH_HANDLE,
   },
   false,
 );
@@ -24,6 +26,7 @@ export const SEARCH_SCOPE_PLACEHOLDER = {
   [USER_HANDLE]: s__('CommandPalette|user (enter at least 3 chars)'),
   [PROJECT_HANDLE]: s__('CommandPalette|project (enter at least 3 chars)'),
   [ISSUE_HANDLE]: s__('CommandPalette|issue (enter at least 3 chars)'),
+  [PATH_HANDLE]: s__('CommandPalette|go to project file'),
 };
 
 export const SEARCH_SCOPE = {
@@ -37,9 +40,11 @@ export const USERS_GROUP_TITLE = s__('GlobalSearch|Users');
 export const PAGES_GROUP_TITLE = s__('CommandPalette|Pages');
 export const PROJECTS_GROUP_TITLE = s__('GlobalSearch|Projects');
 export const ISSUE_GROUP_TITLE = s__('GlobalSearch|Recent issues');
+export const PATH_GROUP_TITLE = s__('CommandPalette|Project files');
 
 export const GROUP_TITLES = {
   [USER_HANDLE]: USERS_GROUP_TITLE,
   [PROJECT_HANDLE]: PROJECTS_GROUP_TITLE,
   [ISSUE_HANDLE]: ISSUE_GROUP_TITLE,
+  [PATH_HANDLE]: PATH_GROUP_TITLE,
 };
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
index dce2b24f551b..efd93e88fa9a 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/fake_search_input.vue
@@ -1,5 +1,5 @@
 <script>
-import { COMMON_HANDLES, SEARCH_SCOPE_PLACEHOLDER } from './constants';
+import { COMMON_HANDLES, PATH_HANDLE, SEARCH_SCOPE_PLACEHOLDER } from './constants';
 
 export default {
   name: 'FakeSearchInput',
@@ -11,7 +11,7 @@ export default {
     scope: {
       type: String,
       required: true,
-      validator: (value) => COMMON_HANDLES.includes(value),
+      validator: (value) => [...COMMON_HANDLES, PATH_HANDLE].includes(value),
     },
   },
   computed: {
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
index 5c8c0e59eaf8..347a8ffb0b44 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
@@ -1,12 +1,12 @@
 import { isNil, omitBy } from 'lodash';
-import { objectToQuery } from '~/lib/utils/url_utility';
-import { SEARCH_SCOPE } from './constants';
+import { objectToQuery, joinPaths } from '~/lib/utils/url_utility';
+import { SEARCH_SCOPE, GLOBAL_COMMANDS_GROUP_TITLE } from './constants';
 
 export const commandMapper = ({ name, items }) => {
   // TODO: we filter out invite_members for now, because it is complicated to add the invite members modal here
   // and is out of scope for the basic command palette items. If it proves to be useful, we can add it later.
   return {
-    name,
+    name: name || GLOBAL_COMMANDS_GROUP_TITLE,
     items: items.filter(({ component }) => component !== 'invite_members'),
   };
 };
@@ -32,6 +32,14 @@ export const linksReducer = (acc, menuItem) => {
   return acc;
 };
 
+export const fileMapper = (projectBlobPath, file) => {
+  return {
+    icon: 'doc-code',
+    text: file,
+    href: joinPaths(projectBlobPath, file),
+  };
+};
+
 export const autocompleteQuery = ({ path, searchTerm, handle, projectId }) => {
   const query = omitBy(
     {
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
index cb34f2b8c269..30b10756ca92 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue
@@ -38,7 +38,11 @@ import {
 } from '../constants';
 import CommandPaletteItems from '../command_palette/command_palette_items.vue';
 import FakeSearchInput from '../command_palette/fake_search_input.vue';
-import { COMMON_HANDLES, SEARCH_OR_COMMAND_MODE_PLACEHOLDER } from '../command_palette/constants';
+import {
+  COMMON_HANDLES,
+  PATH_HANDLE,
+  SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+} from '../command_palette/constants';
 import GlobalSearchAutocompleteItems from './global_search_autocomplete_items.vue';
 import GlobalSearchDefaultItems from './global_search_default_items.vue';
 import GlobalSearchScopedItems from './global_search_scoped_items.vue';
@@ -135,7 +139,11 @@ export default {
       return this.searchText?.trim().charAt(0);
     },
     isCommandMode() {
-      return this.glFeatures?.commandPalette && COMMON_HANDLES.includes(this.searchTextFirstChar);
+      return (
+        this.glFeatures?.commandPalette &&
+        (COMMON_HANDLES.includes(this.searchTextFirstChar) ||
+          (this.searchContext.project && this.searchTextFirstChar === PATH_HANDLE))
+      );
     },
     commandPaletteQuery() {
       if (this.isCommandMode) {
@@ -206,7 +214,7 @@ export default {
       }
     },
     focusSearchInput() {
-      this.$refs.searchInputBox.$el.querySelector('input').focus();
+      this.$refs.searchInput.$el.querySelector('input').focus();
     },
     focusNextItem(event, elements, offset) {
       const { target } = event;
@@ -226,6 +234,13 @@ export default {
       }
       visitUrl(this.searchQuery);
     },
+    onSearchModalShown() {
+      this.$emit('shown');
+    },
+    onSearchModalHidden() {
+      this.searchText = '';
+      this.$emit('hidden');
+    },
   },
   SEARCH_INPUT_DESCRIPTION,
   SEARCH_RESULTS_DESCRIPTION,
@@ -243,8 +258,8 @@ export default {
     body-class="gl-p-0!"
     modal-class="global-search-modal"
     :centered="false"
-    @hidden="$emit('hidden')"
-    @shown="$emit('shown')"
+    @shown="onSearchModalShown"
+    @hide="onSearchModalHidden"
   >
     <form
       role="search"
@@ -256,7 +271,7 @@ export default {
       <div class="gl-p-1 gl-relative">
         <gl-search-box-by-type
           id="search"
-          ref="searchInputBox"
+          ref="searchInput"
           v-model="searchText"
           role="searchbox"
           data-testid="global-search-input"
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index f6afde02fa55..322eca720160 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -65,13 +65,23 @@ export const initSuperSidebar = () => {
 
   if (!el) return false;
 
-  const { rootPath, sidebar, toggleNewNavEndpoint, forceDesktopExpandedSidebar } = el.dataset;
+  const {
+    rootPath,
+    sidebar,
+    toggleNewNavEndpoint,
+    forceDesktopExpandedSidebar,
+    commandPalette,
+  } = el.dataset;
 
   bindSuperSidebarCollapsedEvents(forceDesktopExpandedSidebar);
   initSuperSidebarCollapsedState(parseBoolean(forceDesktopExpandedSidebar));
 
   const sidebarData = JSON.parse(sidebar);
   const searchData = convertObjectPropsToCamelCase(sidebarData.search);
+
+  const commandPaletteData = JSON.parse(commandPalette);
+  const projectFilesPath = commandPaletteData.project_files_url;
+  const projectBlobPath = commandPaletteData.project_blob_url;
   const commandPaletteCommands = sidebarData.create_new_menu_groups || [];
   const commandPaletteLinks = convertObjectPropsToCamelCase(sidebarData.current_menu_items || []);
 
@@ -91,6 +101,8 @@ export const initSuperSidebar = () => {
       commandPaletteLinks,
       autocompletePath,
       searchContext,
+      projectFilesPath,
+      projectBlobPath,
     },
     store: createStore({
       searchPath,
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 02a912d02277..bcb24aa0f7ec 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -123,6 +123,16 @@ def super_sidebar_nav_panel(
     end
   end
 
+  def command_palette_data(project: nil)
+    return {} unless project&.repo_exists?
+    return {} if project.empty_repo?
+
+    {
+      project_files_url: project_files_path(project, project.default_branch, format: :json),
+      project_blob_url: project_blob_path(project, project.default_branch)
+    }
+  end
+
   private
 
   def search_data
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 8e52f973e9e1..eb3b6587beff 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -7,7 +7,7 @@
 
     - sidebar_panel = super_sidebar_nav_panel(nav: nav, user: current_user, group: group, project: @project, current_ref: current_ref, ref_type: @ref_type, viewed_user: @user)
     - sidebar_data = super_sidebar_context(current_user, group: group, project: @project, panel: sidebar_panel, panel_type: nav).to_json
-    %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s } }
+    %aside.js-super-sidebar.super-sidebar.super-sidebar-loading{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url, force_desktop_expanded_sidebar: @force_desktop_expanded_sidebar.to_s, command_palette: command_palette_data(project: @project).to_json } }
 
     - if display_whats_new?
       #whats-new-app{ data: { version_digest: whats_new_version_digest } }
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3cd30a587d8f..aa541d35083f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11241,12 +11241,18 @@ msgstr ""
 msgid "CommandPalette|Pages"
 msgstr ""
 
-msgid "CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue or perform generic search..."
+msgid "CommandPalette|Project files"
+msgstr ""
+
+msgid "CommandPalette|Type %{commandHandle} for command, %{userHandle} for user, %{projectHandle} for project, %{issueHandle} for issue, %{pathHandle} for project file or perform generic search..."
 msgstr ""
 
 msgid "CommandPalette|command"
 msgstr ""
 
+msgid "CommandPalette|go to project file"
+msgstr ""
+
 msgid "CommandPalette|issue (enter at least 3 chars)"
 msgstr ""
 
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
index 21d085dc0fb0..9714093e0016 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
@@ -6,18 +6,21 @@ import CommandPaletteItems from '~/super_sidebar/components/global_search/comman
 import {
   COMMAND_HANDLE,
   USERS_GROUP_TITLE,
+  PATH_GROUP_TITLE,
   USER_HANDLE,
+  PATH_HANDLE,
   SEARCH_SCOPE,
 } from '~/super_sidebar/components/global_search/command_palette/constants';
 import {
   commandMapper,
   linksReducer,
+  fileMapper,
 } from '~/super_sidebar/components/global_search/command_palette/utils';
 import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
 import axios from '~/lib/utils/axios_utils';
 import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
 import waitForPromises from 'helpers/wait_for_promises';
-import { COMMANDS, LINKS, USERS } from './mock_data';
+import { COMMANDS, LINKS, USERS, FILES } from './mock_data';
 
 const links = LINKS.reduce(linksReducer, []);
 
@@ -25,6 +28,8 @@ describe('CommandPaletteItems', () => {
   let wrapper;
   const autocompletePath = '/autocomplete';
   const searchContext = { project: { id: 1 }, group: { id: 2 } };
+  const projectFilesPath = 'project/files/path';
+  const projectBlobPath = '/blob/main';
 
   const createComponent = (props) => {
     wrapper = shallowMount(CommandPaletteItems, {
@@ -42,6 +47,8 @@ describe('CommandPaletteItems', () => {
         commandPaletteLinks: LINKS,
         autocompletePath,
         searchContext,
+        projectFilesPath,
+        projectBlobPath,
       },
     });
   };
@@ -50,7 +57,7 @@ describe('CommandPaletteItems', () => {
   const findGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup);
   const findLoader = () => wrapper.findComponent(GlLoadingIcon);
 
-  describe('COMMANDS & LINKS', () => {
+  describe('Commands and links', () => {
     it('renders all commands initially', () => {
       createComponent();
       const commandGroup = COMMANDS.map(commandMapper)[0];
@@ -90,7 +97,7 @@ describe('CommandPaletteItems', () => {
     });
   });
 
-  describe('USERS, ISSUES, PROJECTS', () => {
+  describe('Users, issues, and projects', () => {
     let mockAxios;
 
     beforeEach(() => {
@@ -140,4 +147,64 @@ describe('CommandPaletteItems', () => {
       expect(wrapper.text()).toBe('No results found');
     });
   });
+
+  describe('Project files', () => {
+    let mockAxios;
+
+    beforeEach(() => {
+      mockAxios = new MockAdapter(axios);
+    });
+
+    it('should request project files on first search', () => {
+      jest.spyOn(axios, 'get');
+      const searchQuery = 'gitlab-ci.yml';
+      createComponent({ handle: PATH_HANDLE, searchQuery });
+
+      expect(axios.get).toHaveBeenCalledWith(projectFilesPath);
+      expect(findLoader().exists()).toBe(true);
+    });
+
+    it('should render returned items', async () => {
+      const items = FILES.map(fileMapper.bind(null, projectBlobPath));
+      mockAxios.onGet().replyOnce(HTTP_STATUS_OK, FILES);
+      jest.spyOn(fuzzaldrinPlus, 'filter').mockReturnValue(items);
+
+      const searchQuery = 'gitlab-ci.yml';
+      createComponent({ handle: PATH_HANDLE, searchQuery });
+
+      await waitForPromises();
+
+      expect(findItems()).toHaveLength(items.length);
+      expect(findGroups().at(0).props('group')).toMatchObject({
+        name: PATH_GROUP_TITLE,
+        items,
+      });
+    });
+
+    it('should display no results message when no files matched the search query', async () => {
+      mockAxios.onGet().replyOnce(HTTP_STATUS_OK, []);
+      const searchQuery = 'gitlab-ci.yml';
+      createComponent({ handle: PATH_HANDLE, searchQuery });
+      await waitForPromises();
+      expect(wrapper.text()).toBe('No results found');
+    });
+
+    it('should not make additional server call on the search query change', async () => {
+      const searchQuery = 'gitlab-ci.yml';
+      const newSearchQuery = 'package.json';
+
+      jest.spyOn(axios, 'get');
+
+      createComponent({ handle: PATH_HANDLE, searchQuery });
+
+      mockAxios.onGet().replyOnce(HTTP_STATUS_OK, FILES);
+      await waitForPromises();
+
+      expect(axios.get).toHaveBeenCalledTimes(1);
+
+      await wrapper.setProps({ searchQuery: newSearchQuery });
+
+      expect(axios.get).toHaveBeenCalledTimes(1);
+    });
+  });
 });
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
index ec65a43d549b..26a6501c3389 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
@@ -131,3 +131,15 @@ export const ISSUE = {
   project_name: 'Flight',
   url: '/flightjs/Flight/-/issues/37',
 };
+
+export const FILES = [
+  '.codeclimate.yml',
+  '.gitignore',
+  '.gitlab-ci.yml',
+  '.gitlab/CODEOWNERS',
+  '.ruby-version',
+  '.tool-versions',
+  'CHANGELOG',
+  'CONTRIBUTING.md',
+  'Dangerfile',
+];
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
index 0b75787723e5..ebc52e2d910a 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
@@ -1,6 +1,7 @@
 import {
   commandMapper,
   linksReducer,
+  fileMapper,
 } from '~/super_sidebar/components/global_search/command_palette/utils';
 import { COMMANDS, LINKS, TRANSFORMED_LINKS } from './mock_data';
 
@@ -16,3 +17,15 @@ describe('commandMapper', () => {
     expect(COMMANDS.map(commandMapper)[0].items).toHaveLength(initialCommandsLength - 1);
   });
 });
+
+describe('fileMapper', () => {
+  it('should transform files', () => {
+    const file = 'file';
+    const projectBlobPath = 'project/blob/path';
+    expect(fileMapper(projectBlobPath, file)).toEqual({
+      icon: 'doc-code',
+      text: file,
+      href: `${projectBlobPath}/${file}`,
+    });
+  });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index 9b7b9e288dfb..cfbe508f3d08 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -12,6 +12,7 @@ import CommandPaletteItems from '~/super_sidebar/components/global_search/comman
 import {
   SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
   COMMON_HANDLES,
+  PATH_HANDLE,
 } from '~/super_sidebar/components/global_search/command_palette/constants';
 import {
   SEARCH_INPUT_DESCRIPTION,
@@ -319,7 +320,7 @@ describe('GlobalSearchModal', () => {
         });
       });
 
-      describe.each(COMMON_HANDLES)(
+      describe.each([...COMMON_HANDLES, PATH_HANDLE])(
         'when FF `command_palette` is enabled and search handle is %s',
         (handle) => {
           beforeEach(() => {
@@ -415,7 +416,7 @@ describe('GlobalSearchModal', () => {
 
     describe('Modal events', () => {
       beforeEach(() => {
-        createComponent();
+        createComponent({ search: 'searchQuery' });
       });
 
       it('should emit `shown` event when modal shown`', () => {
@@ -423,9 +424,10 @@ describe('GlobalSearchModal', () => {
         expect(wrapper.emitted('shown')).toHaveLength(1);
       });
 
-      it('should emit `hidden` event when modal hidden`', () => {
-        findGlobalSearchModal().vm.$emit('hidden');
+      it('should emit `hidden` event when modal hidden and clear the search input', () => {
+        findGlobalSearchModal().vm.$emit('hide');
         expect(wrapper.emitted('hidden')).toHaveLength(1);
+        expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), '');
       });
     });
   });
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 6648663b6344..8f0bf156d1d9 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -528,4 +528,31 @@
       expect(helper.super_sidebar_nav_panel(user: user)).to be_a(Sidebars::YourWork::Panel)
     end
   end
+
+  describe '#command_palette_data' do
+    it 'returns data for project files search' do
+      project = create(:project, :repository) # rubocop:disable RSpec/FactoryBot/AvoidCreate
+
+      expect(helper.command_palette_data(project: project)).to eq(
+        project_files_url: project_files_path(
+          project, project.default_branch, format: :json),
+        project_blob_url: project_blob_path(
+          project, project.default_branch)
+      )
+    end
+
+    it 'returns empty object when project is nil' do
+      expect(helper.command_palette_data(project: nil)).to eq({})
+    end
+
+    it 'returns empty object when project does not have repo' do
+      project = build(:project)
+      expect(helper.command_palette_data(project: project)).to eq({})
+    end
+
+    it 'returns empty object when project has repo but it is empty' do
+      project = build(:project, :empty_repo)
+      expect(helper.command_palette_data(project: project)).to eq({})
+    end
+  end
 end
-- 
GitLab