From bdd56ddd4c1fd8457d0c14eaed4f37a185a5e935 Mon Sep 17 00:00:00 2001 From: Tomoya Kashifuku Date: Sat, 11 Oct 2025 09:49:31 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20move=20logics=20of=20T?= =?UTF-8?q?ableOptions=20into=20a=20custom=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandPaletteOptions/TableOptions.tsx | 49 +----- .../hooks/useTableOptionSelect.test.tsx | 162 ++++++++++++++++++ .../hooks/useTableOptionSelect.ts | 58 +++++++ 3 files changed, 224 insertions(+), 45 deletions(-) create mode 100644 frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.test.tsx create mode 100644 frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.ts diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableOptions.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableOptions.tsx index 91b3914a7b..d52ba0a13e 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableOptions.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableOptions.tsx @@ -1,54 +1,21 @@ import { Table2 } from '@liam-hq/ui' import { Command } from 'cmdk' -import { type FC, useCallback, useEffect } from 'react' +import type { FC } from 'react' import { useSchemaOrThrow } from '../../../../../../stores' -import { useTableSelection } from '../../../../hooks' import { getTableLinkHref } from '../../../../utils/url/getTableLinkHref' -import { useCommandPaletteOrThrow } from '../CommandPaletteProvider' import type { CommandPaletteSuggestion } from '../types' import { getSuggestionText } from '../utils' import styles from './CommandPaletteOptions.module.css' +import { useTableOptionSelect } from './hooks/useTableOptionSelect' type Props = { suggestion: CommandPaletteSuggestion | null } export const TableOptions: FC = ({ suggestion }) => { - const { setOpen } = useCommandPaletteOrThrow() - const schema = useSchemaOrThrow() - const { selectTable } = useTableSelection() - - const goToERD = useCallback( - (tableName: string) => { - selectTable({ tableId: tableName, displayArea: 'main' }) - setOpen(false) - }, - [selectTable, setOpen], - ) - - // Select option by pressing [Enter] key (with/without ⌘ key) - useEffect(() => { - // It doesn't subscribe a keydown event listener if the suggestion type is not "table" - if (suggestion?.type !== 'table') return - const down = (event: KeyboardEvent) => { - const suggestedTableName = suggestion.name - - if (event.key === 'Enter') { - event.preventDefault() - - if (event.metaKey || event.ctrlKey) { - window.open(getTableLinkHref(suggestedTableName)) - } else { - goToERD(suggestedTableName) - } - } - } - - document.addEventListener('keydown', down) - return () => document.removeEventListener('keydown', down) - }, [suggestion, goToERD]) + const { tableOptionSelectHandler } = useTableOptionSelect(suggestion) return ( @@ -60,15 +27,7 @@ export const TableOptions: FC = ({ suggestion }) => { { - // Do not call preventDefault to allow the default link behavior when ⌘ key is pressed - if (event.ctrlKey || event.metaKey) { - return - } - - event.preventDefault() - goToERD(table.name) - }} + onClick={(event) => tableOptionSelectHandler(event, table.name)} > {table.name} diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.test.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.test.tsx new file mode 100644 index 0000000000..6b57ec5463 --- /dev/null +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.test.tsx @@ -0,0 +1,162 @@ +import { render, renderHook, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { ReactFlowProvider } from '@xyflow/react' +import { NuqsTestingAdapter } from 'nuqs/adapters/testing' +import type { FC, ReactNode } from 'react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { UserEditingProvider } from '../../../../../../../stores' +import * as UseTableSelection from '../../../../../hooks' +import { CommandPaletteProvider } from '../../CommandPaletteProvider' +import * as UseCommandPalette from '../../CommandPaletteProvider/hooks' +import type { CommandPaletteSuggestion } from '../../types' +import { useTableOptionSelect } from './useTableOptionSelect' + +afterEach(() => { + vi.clearAllMocks() +}) + +const mockSetCommandPaletteDialogOpen = vi.fn() +const mockSelectTable = vi.fn() +const mockWindowOpen = vi.fn() + +const originalUseCommandPaletteOrThrow = + UseCommandPalette.useCommandPaletteOrThrow +vi.spyOn(UseCommandPalette, 'useCommandPaletteOrThrow').mockImplementation( + () => { + const original = originalUseCommandPaletteOrThrow() + return { + ...original, + setOpen: mockSetCommandPaletteDialogOpen, + } + }, +) +const originalUseTableSelection = UseTableSelection.useTableSelection +vi.spyOn(UseTableSelection, 'useTableSelection').mockImplementation(() => { + const original = originalUseTableSelection() + return { + ...original, + selectTable: mockSelectTable, + } +}) +vi.spyOn(window, 'open').mockImplementation(mockWindowOpen) + +const wrapper = ({ children }: { children: ReactNode }) => ( + + + + {children} + + + +) + +describe('keyboard interactions', () => { + describe('when suggestion is a table', () => { + it('moves to suggested table in ERD and closes the dialog on Enter', async () => { + const user = userEvent.setup() + renderHook(() => useTableOptionSelect({ type: 'table', name: 'users' }), { + wrapper, + }) + + await user.keyboard('{Enter}') + + expect(mockSelectTable).toHaveBeenCalledWith({ + displayArea: 'main', + tableId: 'users', + }) + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + + // other functions are not called + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + + it('opens suggested table in another tab on ⌘Enter', async () => { + const user = userEvent.setup() + renderHook(() => useTableOptionSelect({ type: 'table', name: 'users' }), { + wrapper, + }) + + await user.keyboard('{Meta>}{Enter}{/Meta}') + + expect(mockWindowOpen).toHaveBeenCalledWith('?active=users') + + // other functions are not called + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + }) + }) + + describe.each([ + { type: 'command', name: 'copy link' }, + { type: 'column', tableName: 'users', columnName: 'created_at' }, + null, + ])('when suggestion is other than tables, suggestion = %o', (suggestion) => { + it('does nothing when on Enter', async () => { + const user = userEvent.setup() + renderHook(() => useTableOptionSelect(suggestion), { wrapper }) + + await user.keyboard('{Enter}') + + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + + it('does nothing when on ⌘Enter', async () => { + const user = userEvent.setup() + renderHook(() => useTableOptionSelect(suggestion), { wrapper }) + + await user.keyboard('{Meta>}{Enter}{/Meta}') + + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + }) +}) + +describe('tableOptionSelectHandler', () => { + // a component for testing the "tableOptionSelectHandler" method of the hook + // in the test cases, we simulate the method clicking a link of a table option + const TableOptionLinkWithSelectHandler: FC<{ tableName: string }> = ({ + tableName, + }) => { + const { tableOptionSelectHandler } = useTableOptionSelect(null) + + return ( + tableOptionSelectHandler(event, tableName)} + > + table option link + + ) + } + + it('moves to clicked table in ERD and closes the dialog', async () => { + const user = userEvent.setup() + render(, { + wrapper, + }) + + await user.click(screen.getByRole('link', { name: 'table option link' })) + + expect(mockSelectTable).toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + }) + + it('does nothing with ⌘ + click (default browser action: open in new tab)', async () => { + const user = userEvent.setup() + render(, { + wrapper, + }) + + await user.keyboard('{Meta>}') + await user.click(screen.getByRole('link', { name: 'table option link' })) + await user.keyboard('{/Meta}') + + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + }) +}) diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.ts b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.ts new file mode 100644 index 0000000000..cdb8aa08d6 --- /dev/null +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.ts @@ -0,0 +1,58 @@ +import { useCallback, useEffect } from 'react' +import { useTableSelection } from '../../../../../../erd/hooks' +import { getTableLinkHref } from '../../../../../utils' +import { useCommandPaletteOrThrow } from '../../CommandPaletteProvider' +import type { CommandPaletteSuggestion } from '../../types' + +export const useTableOptionSelect = ( + suggestion: CommandPaletteSuggestion | null, +) => { + const { setOpen } = useCommandPaletteOrThrow() + + const { selectTable } = useTableSelection() + const goToERD = useCallback( + (tableName: string) => { + selectTable({ tableId: tableName, displayArea: 'main' }) + setOpen(false) + }, + [selectTable, setOpen], + ) + + const tableOptionSelectHandler = useCallback( + (event: React.MouseEvent, tableName: string) => { + // Do not call preventDefault to allow the default link behavior when ⌘ key is pressed + if (event.ctrlKey || event.metaKey) { + return + } + + event.preventDefault() + goToERD(tableName) + }, + [goToERD], + ) + + // Select option by pressing [Enter] key (with/without ⌘ key) + useEffect(() => { + // It doesn't subscribe a keydown event listener if the suggestion type is not "table" + if (suggestion?.type !== 'table') return + + const down = (event: KeyboardEvent) => { + const suggestedTableName = suggestion.name + + if (event.key === 'Enter') { + event.preventDefault() + + if (event.metaKey || event.ctrlKey) { + window.open(getTableLinkHref(suggestedTableName)) + } else { + goToERD(suggestedTableName) + } + } + } + + document.addEventListener('keydown', down) + return () => document.removeEventListener('keydown', down) + }, [suggestion, goToERD]) + + return { tableOptionSelectHandler } +} From 61b1bfa14eb5babd115b5a8c3d450811fb5c211f Mon Sep 17 00:00:00 2001 From: Tomoya Kashifuku Date: Sat, 11 Oct 2025 10:34:21 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=85=20add=20TableColumnOptions=20test?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TableColumnOptions.test.tsx | 294 ++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.test.tsx diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.test.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.test.tsx new file mode 100644 index 0000000000..13ae2dc8d3 --- /dev/null +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.test.tsx @@ -0,0 +1,294 @@ +import { aColumn, aTable } from '@liam-hq/schema' +import { render, screen, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { ReactFlowProvider } from '@xyflow/react' +import { Command } from 'cmdk' +import { NuqsTestingAdapter } from 'nuqs/adapters/testing' +import type { ReactNode } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + SchemaProvider, + type SchemaProviderValue, + UserEditingProvider, +} from '../../../../../../stores' +import * as UseTableSelection from '../../../../hooks' +import { CommandPaletteProvider } from '../CommandPaletteProvider' +import * as UseCommandPalette from '../CommandPaletteProvider/hooks' +import { TableColumnOptions } from './TableColumnOptions' + +beforeEach(() => { + window.location.hash = '' +}) + +afterEach(() => { + vi.clearAllMocks() +}) + +const mockSetCommandPaletteDialogOpen = vi.fn() +const mockSelectTable = vi.fn() +const mockWindowOpen = vi.fn() + +const originalUseCommandPaletteOrThrow = + UseCommandPalette.useCommandPaletteOrThrow +vi.spyOn(UseCommandPalette, 'useCommandPaletteOrThrow').mockImplementation( + () => { + const original = originalUseCommandPaletteOrThrow() + return { + ...original, + setOpen: mockSetCommandPaletteDialogOpen, + } + }, +) +const originalUseTableSelection = UseTableSelection.useTableSelection +vi.spyOn(UseTableSelection, 'useTableSelection').mockImplementation(() => { + const original = originalUseTableSelection() + return { + ...original, + selectTable: mockSelectTable, + } +}) +vi.spyOn(window, 'open').mockImplementation(mockWindowOpen) + +const schema: SchemaProviderValue = { + current: { + tables: { + users: aTable({ + name: 'users', + columns: { + id: aColumn({ name: 'id' }), + created_at: aColumn({ name: 'created_at', type: 'timestamp' }), + }, + }), + posts: aTable({ name: 'posts' }), + }, + enums: {}, + extensions: {}, + }, +} + +const wrapper = ({ children }: { children: ReactNode }) => ( + + + + + + {children} + + + + + +) + +it('displays selected table option and its columns', () => { + render(, { + wrapper, + }) + + // table option + const userTableOption = screen.getByRole('option', { name: 'users' }) + expect(userTableOption).toBeInTheDocument() + expect(within(userTableOption).getByRole('link')).toHaveAttribute( + 'href', + '?active=users', + ) + + // column options + const idColumnOption = screen.getByRole('option', { name: 'id' }) + expect(idColumnOption).toBeInTheDocument() + expect(within(idColumnOption).getByRole('link')).toHaveAttribute( + 'href', + '?active=users#users__columns__id', + ) + const createdAtColumnOption = screen.getByRole('option', { + name: 'created_at', + }) + expect(createdAtColumnOption).toBeInTheDocument() + expect(within(createdAtColumnOption).getByRole('link')).toHaveAttribute( + 'href', + '?active=users#users__columns__created_at', + ) + + // other tables are not displayed + expect(screen.queryByRole('link', { name: 'posts' })).not.toBeInTheDocument() +}) + +describe('mouse interactions', () => { + describe('table option', () => { + it('moves to clicked table in ERD and closes the dialog', async () => { + const user = userEvent.setup() + render(, { + wrapper, + }) + + await user.click(screen.getByRole('link', { name: 'users' })) + + expect(mockSelectTable).toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + }) + + it('does nothing with ⌘ + click (default browser action: open in new tab)', async () => { + const user = userEvent.setup() + render(, { + wrapper, + }) + + await user.keyboard('{Meta>}') + await user.click(screen.getByRole('link', { name: 'users' })) + await user.keyboard('{/Meta}') + + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + }) + }) + + describe('column options', () => { + it('moves to clicked table column in ERD and closes the dialog', async () => { + const user = userEvent.setup() + render(, { + wrapper, + }) + + await user.click(screen.getByRole('link', { name: 'created_at' })) + + expect(mockSelectTable).toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + expect(window.location.hash).toBe('#users__columns__created_at') + }) + + it('does nothing with ⌘ + click (default browser action: open in new tab)', async () => { + const user = userEvent.setup() + render(, { + wrapper, + }) + + await user.keyboard('{Meta>}') + await user.click(screen.getByRole('link', { name: 'created_at' })) + await user.keyboard('{/Meta}') + + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + + // FIXME: jsdom doesn't implement behavior of ⌘ + click to open a link in a new tab, but it changes the URL of the current window + // So, the following assertion doesn't pass + // expect(window.location.hash).toBe('') + }) + }) +}) + +describe('keyboard interactions', () => { + describe('table option', () => { + it('moves to suggested table in ERD and closes the dialog on Enter', async () => { + const user = userEvent.setup() + render( + , + { wrapper }, + ) + + await user.keyboard('{Enter}') + + expect(mockSelectTable).toHaveBeenCalledWith({ + displayArea: 'main', + tableId: 'users', + }) + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + expect(window.location.hash).toBe('') + + // other functions are not called + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + + it('opens suggested table in another tab on ⌘Enter', async () => { + const user = userEvent.setup() + render( + , + { wrapper }, + ) + + await user.keyboard('{Meta>}{Enter}{/Meta}') + + expect(mockWindowOpen).toHaveBeenCalledWith('?active=users') + + // other functions are not called + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + }) + }) + + describe('column option', () => { + it('moves to suggested table column in ERD and closes the dialog on Enter', async () => { + const user = userEvent.setup() + render( + , + { wrapper }, + ) + + await user.keyboard('{Enter}') + + expect(mockSelectTable).toHaveBeenCalledWith({ + displayArea: 'main', + tableId: 'users', + }) + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + expect(window.location.hash).toBe('#users__columns__created_at') + + // other functions are not called + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + + it('opens suggested table column in another tab on ⌘Enter', async () => { + const user = userEvent.setup() + render( + , + { wrapper }, + ) + + await user.keyboard('{Meta>}{Enter}{/Meta}') + + expect(mockWindowOpen).toHaveBeenCalledWith( + '?active=users#users__columns__created_at', + ) + + // other functions are not called + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + }) + }) + + it('does nothing on Enter when suggestion is not table', async () => { + const user = userEvent.setup() + render( + , + { wrapper }, + ) + + await user.keyboard('{Meta>}{Enter}{/Meta}') + + expect(mockWindowOpen).not.toHaveBeenCalled() + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + }) +}) From ae98328e99f8fbc0377a318fe89d3438dad330fe Mon Sep 17 00:00:00 2001 From: Tomoya Kashifuku Date: Sat, 11 Oct 2025 10:37:51 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20replace=20TableOption?= =?UTF-8?q?=20logics=20with=20useTableOptionSelect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TableColumnOptions.tsx | 36 +++---------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.tsx index 683fb3c037..8debb923db 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.tsx @@ -25,6 +25,7 @@ import { useCommandPaletteOrThrow } from '../CommandPaletteProvider' import type { CommandPaletteSuggestion } from '../types' import { getSuggestionText } from '../utils' import styles from './CommandPaletteOptions.module.css' +import { useTableOptionSelect } from './hooks/useTableOptionSelect' import { type ColumnType, getColumnTypeMap } from './utils/getColumnTypeMap' type Props = { @@ -64,29 +65,6 @@ export const TableColumnOptions: FC = ({ tableName, suggestion }) => { [setOpen, selectTable], ) - // Select option by pressing [Enter] key (with/without ⌘ key) - useEffect(() => { - // It doesn't subscribe a keydown event listener if the suggestion type is not "table" - if (suggestion?.type !== 'table') return - - const down = (event: KeyboardEvent) => { - const suggestedTableName = suggestion.name - - if (event.key === 'Enter') { - event.preventDefault() - - if (event.metaKey || event.ctrlKey) { - window.open(getTableLinkHref(suggestedTableName)) - } else { - goToERD(suggestedTableName) - } - } - } - - document.addEventListener('keydown', down) - return () => document.removeEventListener('keydown', down) - }, [suggestion, goToERD]) - // Select option by pressing [Enter] key (with/without ⌘ key) useEffect(() => { // It doesn't subscribe a keydown event listener if the suggestion type is not "column" @@ -110,6 +88,8 @@ export const TableColumnOptions: FC = ({ tableName, suggestion }) => { return () => document.removeEventListener('keydown', down) }, [suggestion, goToERD]) + const { tableOptionSelectHandler } = useTableOptionSelect(suggestion) + const table = schema.current.tables[tableName] const columnTypeMap = useMemo( () => (table ? getColumnTypeMap(table) : {}), @@ -128,15 +108,7 @@ export const TableColumnOptions: FC = ({ tableName, suggestion }) => { { - // Do not call preventDefault to allow the default link behavior when ⌘ key is pressed - if (event.ctrlKey || event.metaKey) { - return - } - - event.preventDefault() - goToERD(table.name) - }} + onClick={(event) => tableOptionSelectHandler(event, table.name)} > {table.name} From 7324f776823b4610cb858f1371d64463f8762a1f Mon Sep 17 00:00:00 2001 From: Tomoya Kashifuku Date: Sat, 11 Oct 2025 10:56:29 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20move=20column=20option?= =?UTF-8?q?=20logics=20into=20useTableOptionSelect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TableColumnOptions.tsx | 63 +------- .../CommandPaletteOptions/TableOptions.tsx | 4 +- .../hooks/useTableOptionSelect.test.tsx | 153 +++++++++++++++--- .../hooks/useTableOptionSelect.ts | 45 +++++- 4 files changed, 174 insertions(+), 91 deletions(-) diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.tsx index 8debb923db..8c19119603 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/TableColumnOptions.tsx @@ -7,21 +7,12 @@ import { } from '@liam-hq/ui' import clsx from 'clsx' import { Command } from 'cmdk' +import { type ComponentProps, type FC, useMemo } from 'react' import { - type ComponentProps, - type FC, - useCallback, - useEffect, - useMemo, -} from 'react' -import { - getTableColumnElementId, getTableColumnLinkHref, getTableLinkHref, } from '../../../../../../features' import { useSchemaOrThrow } from '../../../../../../stores' -import { useTableSelection } from '../../../../hooks' -import { useCommandPaletteOrThrow } from '../CommandPaletteProvider' import type { CommandPaletteSuggestion } from '../types' import { getSuggestionText } from '../utils' import styles from './CommandPaletteOptions.module.css' @@ -51,44 +42,8 @@ const ColumnIcon: FC & { columnType: ColumnType }> = ({ export const TableColumnOptions: FC = ({ tableName, suggestion }) => { const schema = useSchemaOrThrow() - const { selectTable } = useTableSelection() - const { setOpen } = useCommandPaletteOrThrow() - - const goToERD = useCallback( - (tableName: string, columnName?: string) => { - selectTable({ tableId: tableName, displayArea: 'main' }) - setOpen(false) - if (columnName) { - window.location.hash = getTableColumnElementId(tableName, columnName) - } - }, - [setOpen, selectTable], - ) - - // Select option by pressing [Enter] key (with/without ⌘ key) - useEffect(() => { - // It doesn't subscribe a keydown event listener if the suggestion type is not "column" - if (suggestion?.type !== 'column') return - - const down = (event: KeyboardEvent) => { - const { tableName, columnName } = suggestion - if (event.key === 'Enter') { - event.preventDefault() - - if (event.metaKey || event.ctrlKey) { - window.open(getTableColumnLinkHref(tableName, columnName)) - } else { - goToERD(tableName, columnName) - } - } - } - - document.addEventListener('keydown', down) - return () => document.removeEventListener('keydown', down) - }, [suggestion, goToERD]) - - const { tableOptionSelectHandler } = useTableOptionSelect(suggestion) + const { optionSelectHandler } = useTableOptionSelect(suggestion) const table = schema.current.tables[tableName] const columnTypeMap = useMemo( @@ -108,7 +63,7 @@ export const TableColumnOptions: FC = ({ tableName, suggestion }) => { tableOptionSelectHandler(event, table.name)} + onClick={(event) => optionSelectHandler(event, table.name)} > {table.name} @@ -126,15 +81,9 @@ export const TableColumnOptions: FC = ({ tableName, suggestion }) => { { - // Do not call preventDefault to allow the default link behavior when ⌘ key is pressed - if (event.ctrlKey || event.metaKey) { - return - } - - event.preventDefault() - goToERD(table.name, column.name) - }} + onClick={(event) => + optionSelectHandler(event, table.name, column.name) + } > {columnTypeMap[column.name] && ( = ({ suggestion }) => { const schema = useSchemaOrThrow() - const { tableOptionSelectHandler } = useTableOptionSelect(suggestion) + const { optionSelectHandler } = useTableOptionSelect(suggestion) return ( @@ -27,7 +27,7 @@ export const TableOptions: FC = ({ suggestion }) => { tableOptionSelectHandler(event, table.name)} + onClick={(event) => optionSelectHandler(event, table.name)} > {table.name} diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.test.tsx b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.test.tsx index 6b57ec5463..7474f7f157 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.test.tsx +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event' import { ReactFlowProvider } from '@xyflow/react' import { NuqsTestingAdapter } from 'nuqs/adapters/testing' import type { FC, ReactNode } from 'react' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { UserEditingProvider } from '../../../../../../../stores' import * as UseTableSelection from '../../../../../hooks' import { CommandPaletteProvider } from '../../CommandPaletteProvider' @@ -11,6 +11,10 @@ import * as UseCommandPalette from '../../CommandPaletteProvider/hooks' import type { CommandPaletteSuggestion } from '../../types' import { useTableOptionSelect } from './useTableOptionSelect' +beforeEach(() => { + window.location.hash = '' +}) + afterEach(() => { vi.clearAllMocks() }) @@ -86,9 +90,57 @@ describe('keyboard interactions', () => { }) }) + describe('when suggestion is a column', () => { + it('moves to suggested table column in ERD and closes the dialog on Enter', async () => { + const user = userEvent.setup() + renderHook( + () => + useTableOptionSelect({ + type: 'column', + tableName: 'users', + columnName: 'name', + }), + { wrapper }, + ) + + await user.keyboard('{Enter}') + + expect(mockSelectTable).toHaveBeenCalledWith({ + displayArea: 'main', + tableId: 'users', + }) + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + + // other functions are not called + expect(mockWindowOpen).not.toHaveBeenCalled() + }) + + it('opens suggested table in another tab on ⌘Enter', async () => { + const user = userEvent.setup() + renderHook( + () => + useTableOptionSelect({ + type: 'column', + tableName: 'users', + columnName: 'name', + }), + { wrapper }, + ) + + await user.keyboard('{Meta>}{Enter}{/Meta}') + + expect(mockWindowOpen).toHaveBeenCalledWith( + '?active=users#users__columns__name', + ) + + // other functions are not called + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + }) + }) + describe.each([ { type: 'command', name: 'copy link' }, - { type: 'column', tableName: 'users', columnName: 'created_at' }, null, ])('when suggestion is other than tables, suggestion = %o', (suggestion) => { it('does nothing when on Enter', async () => { @@ -115,48 +167,99 @@ describe('keyboard interactions', () => { }) }) -describe('tableOptionSelectHandler', () => { - // a component for testing the "tableOptionSelectHandler" method of the hook +describe('optionSelectHandler', () => { + // a component for testing the "optionSelectHandler" method of the hook // in the test cases, we simulate the method clicking a link of a table option - const TableOptionLinkWithSelectHandler: FC<{ tableName: string }> = ({ - tableName, - }) => { - const { tableOptionSelectHandler } = useTableOptionSelect(null) + const TableOptionLinkWithSelectHandler: FC<{ + tableName: string + columnName: string | undefined + }> = ({ tableName, columnName }) => { + const { optionSelectHandler } = useTableOptionSelect(null) return ( tableOptionSelectHandler(event, tableName)} + onClick={(event) => optionSelectHandler(event, tableName, columnName)} > table option link ) } - it('moves to clicked table in ERD and closes the dialog', async () => { - const user = userEvent.setup() - render(, { - wrapper, + describe('when passing only tableName', () => { + it('moves to clicked table in ERD and closes the dialog', async () => { + const user = userEvent.setup() + render( + , + { wrapper }, + ) + + await user.click(screen.getByRole('link', { name: 'table option link' })) + + expect(mockSelectTable).toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) }) - await user.click(screen.getByRole('link', { name: 'table option link' })) + it('does nothing with ⌘ + click (default browser action: open in new tab)', async () => { + const user = userEvent.setup() + render( + , + { wrapper }, + ) + + await user.keyboard('{Meta>}') + await user.click(screen.getByRole('link', { name: 'table option link' })) + await user.keyboard('{/Meta}') - expect(mockSelectTable).toHaveBeenCalled() - expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + }) }) - it('does nothing with ⌘ + click (default browser action: open in new tab)', async () => { - const user = userEvent.setup() - render(, { - wrapper, + describe('when passing both tableName and columnName', () => { + it('moves to clicked table in ERD and closes the dialog', async () => { + const user = userEvent.setup() + render( + , + { wrapper }, + ) + + await user.click(screen.getByRole('link', { name: 'table option link' })) + + expect(mockSelectTable).toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).toHaveBeenCalledWith(false) + expect(window.location.hash).toBe('#follows__columns__user_id') }) - await user.keyboard('{Meta>}') - await user.click(screen.getByRole('link', { name: 'table option link' })) - await user.keyboard('{/Meta}') + it('does nothing with ⌘ + click (default browser action: open in new tab)', async () => { + const user = userEvent.setup() + render( + , + { + wrapper, + }, + ) + + await user.keyboard('{Meta>}') + await user.click(screen.getByRole('link', { name: 'table option link' })) + await user.keyboard('{/Meta}') - expect(mockSelectTable).not.toHaveBeenCalled() - expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + expect(mockSelectTable).not.toHaveBeenCalled() + expect(mockSetCommandPaletteDialogOpen).not.toHaveBeenCalled() + expect(window.location.hash).toBe('') + }) }) }) diff --git a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.ts b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.ts index cdb8aa08d6..e184496bd0 100644 --- a/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.ts +++ b/frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/hooks/useTableOptionSelect.ts @@ -1,6 +1,10 @@ import { useCallback, useEffect } from 'react' import { useTableSelection } from '../../../../../../erd/hooks' -import { getTableLinkHref } from '../../../../../utils' +import { + getTableColumnElementId, + getTableColumnLinkHref, + getTableLinkHref, +} from '../../../../../utils' import { useCommandPaletteOrThrow } from '../../CommandPaletteProvider' import type { CommandPaletteSuggestion } from '../../types' @@ -11,27 +15,31 @@ export const useTableOptionSelect = ( const { selectTable } = useTableSelection() const goToERD = useCallback( - (tableName: string) => { + (tableName: string, columnName?: string) => { selectTable({ tableId: tableName, displayArea: 'main' }) + if (columnName) { + window.location.hash = getTableColumnElementId(tableName, columnName) + } + setOpen(false) }, [selectTable, setOpen], ) - const tableOptionSelectHandler = useCallback( - (event: React.MouseEvent, tableName: string) => { + const optionSelectHandler = useCallback( + (event: React.MouseEvent, tableName: string, columnName?: string) => { // Do not call preventDefault to allow the default link behavior when ⌘ key is pressed if (event.ctrlKey || event.metaKey) { return } event.preventDefault() - goToERD(tableName) + goToERD(tableName, columnName) }, [goToERD], ) - // Select option by pressing [Enter] key (with/without ⌘ key) + // Select table option by pressing [Enter] key (with/without ⌘ key) useEffect(() => { // It doesn't subscribe a keydown event listener if the suggestion type is not "table" if (suggestion?.type !== 'table') return @@ -54,5 +62,28 @@ export const useTableOptionSelect = ( return () => document.removeEventListener('keydown', down) }, [suggestion, goToERD]) - return { tableOptionSelectHandler } + // Select column option by pressing [Enter] key (with/without ⌘ key) + useEffect(() => { + // It doesn't subscribe a keydown event listener if the suggestion type is not "column" + if (suggestion?.type !== 'column') return + + const down = (event: KeyboardEvent) => { + const { tableName, columnName } = suggestion + + if (event.key === 'Enter') { + event.preventDefault() + + if (event.metaKey || event.ctrlKey) { + window.open(getTableColumnLinkHref(tableName, columnName)) + } else { + goToERD(tableName, columnName) + } + } + } + + document.addEventListener('keydown', down) + return () => document.removeEventListener('keydown', down) + }, [suggestion, goToERD]) + + return { optionSelectHandler } }