diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteContent/CommandPaletteContent.test.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteContent/CommandPaletteContent.test.tsx index 24ff12352c..4cb111d3ce 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteContent/CommandPaletteContent.test.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteContent/CommandPaletteContent.test.tsx @@ -17,8 +17,40 @@ import { CommandPaletteContent } from './CommandPaletteContent' const schema: SchemaProviderValue = { current: { tables: { - users: aTable({ name: 'users' }), - posts: aTable({ name: 'posts' }), + users: aTable({ + name: 'users', + columns: { + id: { + name: 'id', + type: 'bigint', + default: null, + check: null, + notNull: true, + comment: null, + }, + name: { + name: 'name', + type: 'text', + default: null, + check: null, + notNull: true, + comment: null, + }, + }, + }), + posts: aTable({ + name: 'posts', + columns: { + created_at: { + name: 'created_at', + type: 'timestamp', + default: null, + check: null, + notNull: true, + comment: null, + }, + }, + }), follows: aTable({ name: 'follows' }), user_settings: aTable({ name: 'user_settings' }), }, @@ -175,6 +207,41 @@ describe('input mode and filters', () => { ).toBeInTheDocument() }) }) + + describe('table input mode', () => { + it('renders selected table option and its column options', async () => { + const user = userEvent.setup() + render(, { wrapper }) + + // switch to "table" input mode of the "users" table + await user.keyboard('users') + await user.keyboard('{Tab}') + + // the "users" table option is always displayed on the top of the options list + expect(screen.getByRole('option', { name: 'users' })).toBeInTheDocument() + // column options of the "users" table + expect(screen.getByRole('option', { name: 'id' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'name' })).toBeInTheDocument() + }) + + it('filters column options by user input in the combobox while keeping the selected table option visible', async () => { + const user = userEvent.setup() + render(, { wrapper }) + + // switch to "table" input mode of the "users" table + await user.keyboard('users') + await user.keyboard('{Tab}') + // filter the column options by typing "name" + await user.keyboard('name') + + expect(screen.getByRole('option', { name: 'users' })).toBeInTheDocument() + expect(screen.getByRole('option', { name: 'name' })).toBeInTheDocument() + // the "id" column option is filtered out + expect( + screen.queryByRole('option', { name: 'id' }), + ).not.toBeInTheDocument() + }) + }) }) describe('preview', () => { @@ -192,6 +259,29 @@ describe('preview', () => { await user.hover(screen.getByRole('option', { name: 'posts' })) expect(within(previewContainer).getByText('posts')).toBeInTheDocument() }) + + it('renders a table preview when a column option is selected', async () => { + const user = userEvent.setup() + render(, { wrapper }) + const previewContainer = screen.getByTestId('CommandPalettePreview') + + // renders the "users" preview when the "users/name" option is selected + await user.hover(screen.getByRole('option', { name: 'users' })) + // go to the "table" input mode of the "users" table + await user.keyboard('{Tab}') + await user.hover(screen.getByRole('option', { name: 'name' })) + expect(within(previewContainer).getByText('users')).toBeInTheDocument() + + // back to the "default" input mode + await user.keyboard('{Backspace}') + + // renders the "posts" preview when the "posts/created_at" option is selected + await user.hover(screen.getByRole('option', { name: 'posts' })) + // go to the "table" input mode of the "posts" table + await user.keyboard('{Tab}') + await user.hover(screen.getByRole('option', { name: 'created_at' })) + expect(within(previewContainer).getByText('posts')).toBeInTheDocument() + }) }) describe('command preview', () => { diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteContent/CommandPaletteContent.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteContent/CommandPaletteContent.tsx index 7808c05445..0591b442d0 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteContent/CommandPaletteContent.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteContent/CommandPaletteContent.tsx @@ -1,31 +1,68 @@ import { Button } from '@liam-hq/ui' import { DialogClose } from '@radix-ui/react-dialog' -import { Command, defaultFilter } from 'cmdk' +import { Command, defaultFilter as cmdkBaseFilter } from 'cmdk' import { type FC, useMemo, useState } from 'react' import { CommandPaletteCommandOptions, TableOptions, } from '../CommandPaletteOptions' +import { TableColumnOptions } from '../CommandPaletteOptions/TableColumnOptions' import { CommandPreview, TablePreview } from '../CommandPalettePreview' import { CommandPaletteSearchInput } from '../CommandPaletteSearchInput' import type { CommandPaletteInputMode } from '../types' import { textToSuggestion } from '../utils' import styles from './CommandPaletteContent.module.css' -const commandPaletteFilter: typeof defaultFilter = (value, ...rest) => { +const defaultCommandPaletteFilter: typeof cmdkBaseFilter = (value, ...rest) => { const suggestion = textToSuggestion(value) // if the value is inappropriate for suggestion, it returns 0 and the options is hidden // https://github.com/pacocoursey/cmdk/blob/d6fde235386414196bf80d9b9fa91e2cf89a72ea/cmdk/src/index.tsx#L91-L95 if (!suggestion) return 0 - return defaultFilter(suggestion.name, ...rest) + // displays 'table' and 'command' type suggestions in the "default" input mode + if (suggestion.type === 'table' || suggestion.type === 'command') { + return cmdkBaseFilter(suggestion.name, ...rest) + } + + return 0 +} + +const tableInputModeFilter: typeof cmdkBaseFilter = (value, ...rest) => { + const suggestion = textToSuggestion(value) + + // if the value is inappropriate for suggestion, it returns 0 and the options is hidden + // https://github.com/pacocoursey/cmdk/blob/d6fde235386414196bf80d9b9fa91e2cf89a72ea/cmdk/src/index.tsx#L91-L95 + if (!suggestion) return 0 + + // always display the table itself on the top of the options list + if (suggestion.type === 'table') return 1 + if (suggestion.type === 'column') + return cmdkBaseFilter(suggestion.columnName, ...rest) + + // it displays only 'table' and 'column' type suggestions in the "table" input mode + return 0 } -export const CommandPaletteContent: FC = () => { +type Props = { + isTableModeActivatable?: boolean +} + +export const CommandPaletteContent: FC = ({ + isTableModeActivatable = false, +}) => { const [inputMode, setInputMode] = useState({ type: 'default', }) + const filter = useMemo(() => { + switch (inputMode.type) { + case 'default': + case 'command': + return defaultCommandPaletteFilter + case 'table': + return tableInputModeFilter + } + }, [inputMode.type]) const [suggestionText, setSuggestionText] = useState('') const suggestion = useMemo( @@ -37,7 +74,7 @@ export const CommandPaletteContent: FC = () => { setSuggestionText(v)} - filter={commandPaletteFilter} + filter={filter} >
{ suggestion={suggestion} setMode={setInputMode} onBlur={(event) => event.target.focus()} + isTableModeActivatable={isTableModeActivatable} />