diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx new file mode 100644 index 00000000000..098ca44957f --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.test.tsx @@ -0,0 +1,200 @@ +/** + * @vitest-environment jsdom + */ +import type { ComponentType } from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + +vi.mock('@sim/emcn', () => ({ + cn: (...classes: Array) => classes.filter(Boolean).join(' '), + Library: () => , +})) + +vi.mock('next/navigation', () => ({ + useParams: () => ({ workspaceId: 'workspace-current' }), + useRouter: () => ({ push: vi.fn() }), +})) + +vi.mock('posthog-js/react', () => ({ + usePostHog: () => null, +})) + +vi.mock('@/app/workspace/[workspaceId]/providers/global-commands-provider', () => ({ + useInvokeGlobalCommand: () => vi.fn(), +})) + +vi.mock('@/app/workspace/[workspaceId]/w/components/sidebar/sidebar', () => ({ + SIDEBAR_SCROLL_EVENT: 'sidebar-scroll-to-item', +})) + +vi.mock('@/hooks/use-permission-config', () => ({ + usePermissionConfig: () => ({ + config: { + hideIntegrationsTab: false, + hideTablesTab: false, + hideFilesTab: false, + hideKnowledgeBaseTab: false, + }, + }), +})) + +vi.mock('@/hooks/use-settings-navigation', () => ({ + useSettingsNavigation: () => ({ navigateToSettings: vi.fn() }), +})) + +vi.mock('@/lib/posthog/client', () => ({ + captureEvent: vi.fn(), +})) + +vi.mock('@/lib/workflows/triggers/trigger-utils', () => ({ + hasTriggerCapability: () => false, +})) + +import { SearchModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal' +import { useSearchModalStore } from '@/stores/modals/search/store' +import { SEARCH_SECTIONS } from '@/stores/modals/search/types' + +function TestIcon() { + return +} + +const workspace = { + id: 'workspace-acme', + name: 'Acme', + href: '/workspace/workspace-acme/w', +} + +const chat = { + id: 'chat-acme', + name: 'Acme', + href: '/workspace/workspace-current/chat/chat-acme', +} + +class ResizeObserverMock { + observe = vi.fn() + unobserve = vi.fn() + disconnect = vi.fn() +} + +let container: HTMLDivElement +let root: Root +const originalScrollIntoView = Element.prototype.scrollIntoView + +function headings(): string[] { + return [...document.body.querySelectorAll('[cmdk-group-heading]')].map( + (heading) => heading.textContent ?? '' + ) +} + +async function renderSearchModal({ + isOnWorkflowPage = false, +}: { + isOnWorkflowPage?: boolean +} = {}) { + await act(async () => { + root.render( + + ) + }) +} + +async function searchFor(value: string) { + const input = document.body.querySelector('[cmdk-input]') + if (!input) throw new Error('Search input not found') + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set + if (!nativeInputValueSetter) throw new Error('Native input value setter not found') + + await act(async () => { + nativeInputValueSetter.call(input, value) + input.dispatchEvent(new Event('input', { bubbles: true })) + }) +} + +beforeEach(() => { + vi.stubGlobal('ResizeObserver', ResizeObserverMock) + Element.prototype.scrollIntoView = vi.fn() + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + useSearchModalStore.setState({ + isOpen: true, + sections: null, + pendingConnect: null, + data: { + blocks: [], + tools: [], + triggers: [], + toolOperations: [], + docs: [], + isInitialized: true, + }, + }) +}) + +afterEach(() => { + act(() => root.unmount()) + container.remove() + vi.clearAllMocks() + vi.unstubAllGlobals() + if (originalScrollIntoView) { + Element.prototype.scrollIntoView = originalScrollIntoView + } else { + Reflect.deleteProperty(Element.prototype, 'scrollIntoView') + } +}) + +describe('SearchModal section ordering', () => { + it('renders Workspaces directly after Actions and before Chats', async () => { + await renderSearchModal() + + expect(SEARCH_SECTIONS.slice(0, 2)).toEqual(['actions', 'workspaces']) + expect(headings().slice(0, 3)).toEqual(['Actions', 'Workspaces', 'Chats']) + }) + + it('keeps a matching workspace ahead of a matching chat', async () => { + await renderSearchModal() + await searchFor('Acme') + + expect(headings()).toEqual(['Workspaces', 'Chats']) + }) + + it('does not add Workspaces to a canvas-restricted palette', async () => { + useSearchModalStore.setState({ + sections: ['blocks', 'tools', 'toolOperations'], + data: { + blocks: [ + { + id: 'agent', + name: 'Agent', + icon: TestIcon as ComponentType<{ className?: string }>, + bgColor: '#000000', + type: 'agent', + }, + ], + tools: [], + triggers: [], + toolOperations: [], + docs: [], + isInitialized: true, + }, + }) + + await renderSearchModal({ isOnWorkflowPage: true }) + + expect(headings()).toEqual(['Blocks']) + expect(document.body).not.toHaveTextContent('Workspaces') + expect(document.body).not.toHaveTextContent('Acme') + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 6968fbc91f9..3959a9d09b6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -714,6 +714,9 @@ export function SearchModal({ {showSection('actions') && ( )} + {showSection('workspaces') && ( + + )} {showSection('connectedAccounts') && ( )} - {showSection('workspaces') && ( - - )} {showSection('docs') && } {showSection('pages') && ( diff --git a/apps/sim/stores/modals/search/types.ts b/apps/sim/stores/modals/search/types.ts index 32146b9472b..d7c995bf308 100644 --- a/apps/sim/stores/modals/search/types.ts +++ b/apps/sim/stores/modals/search/types.ts @@ -58,6 +58,7 @@ export interface SearchData { */ export const SEARCH_SECTIONS = [ 'actions', + 'workspaces', 'connectedAccounts', 'integrations', 'blocks', @@ -69,7 +70,6 @@ export const SEARCH_SECTIONS = [ 'files', 'knowledgeBases', 'toolOperations', - 'workspaces', 'docs', 'pages', ] as const