Skip to content

Commit 35a9953

Browse files
authored
Merge pull request #4006 from liam-hq/feat/show-and-hide-command
✨ feat(command palette): Add "Show All" and "Hide All" table visibility shortcuts and options in CommandPalette
2 parents 8978805 + a435dd6 commit 35a9953

File tree

7 files changed

+246
-5
lines changed

7 files changed

+246
-5
lines changed

.changeset/breezy-yaks-arrive.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@liam-hq/erd-core": patch
3+
---
4+
5+
- ✨ Add "Show All Tables" and "Hide All Tables" table visibility shortcuts and options in CommandPalette
6+
- Command Palette: new "Show All Tables" and "Hide All Tables" commands
7+
- Subscribe shortcuts: ⇧A for "Show All Tables" and ⇧H for "Hide All Tables"

frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/CommandOptions.test.tsx

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { ToastProvider } from '@liam-hq/ui'
22
import { render, screen } from '@testing-library/react'
3-
import { ReactFlowProvider } from '@xyflow/react'
3+
import { type Node, ReactFlowProvider } from '@xyflow/react'
44
import { Command } from 'cmdk'
5-
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
5+
import { NuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing'
66
import type { FC, PropsWithChildren } from 'react'
7-
import { expect, it } from 'vitest'
7+
import { describe, expect, it, vi } from 'vitest'
88
import { UserEditingProvider } from '../../../../../../stores'
99
import { CommandPaletteProvider } from '../CommandPaletteProvider'
1010
import { CommandPaletteCommandOptions } from './CommandOptions'
1111

12+
const mockDefaultNodes = vi.fn<() => Node[]>()
13+
const onUrlUpdate = vi.fn<() => [UrlUpdateEvent]>()
14+
1215
const wrapper: FC<PropsWithChildren> = ({ children }) => (
13-
<NuqsTestingAdapter>
16+
<NuqsTestingAdapter onUrlUpdate={onUrlUpdate}>
1417
<UserEditingProvider>
15-
<ReactFlowProvider>
18+
<ReactFlowProvider defaultNodes={mockDefaultNodes()}>
1619
<ToastProvider>
1720
<CommandPaletteProvider>
1821
<Command>{children}</Command>
@@ -45,3 +48,89 @@ it('renders options with descriptions', async () => {
4548
screen.getByRole('option', { name: 'Show Key Only ⇧ 4' }),
4649
).toBeInTheDocument()
4750
})
51+
52+
describe('show/hide all tables options', () => {
53+
it('shows "Show All Tables " option and hide "Hide All Tables" option when all tables are hidden', () => {
54+
mockDefaultNodes.mockReturnValueOnce([
55+
{
56+
id: '1',
57+
type: 'table',
58+
data: {},
59+
position: { x: 0, y: 0 },
60+
hidden: true,
61+
},
62+
{
63+
id: '2',
64+
type: 'table',
65+
data: {},
66+
position: { x: 0, y: 0 },
67+
hidden: true,
68+
},
69+
])
70+
71+
render(<CommandPaletteCommandOptions />, { wrapper })
72+
73+
expect(
74+
screen.getByRole('option', { name: 'Show All Tables ⇧ A' }),
75+
).toBeInTheDocument()
76+
expect(
77+
screen.queryByRole('option', { name: 'Hide All Tables ⇧ H' }),
78+
).not.toBeInTheDocument()
79+
})
80+
81+
it('shows "Hide All Tables" option and hide "Show All Tables" option when all tables are visible', () => {
82+
mockDefaultNodes.mockReturnValueOnce([
83+
{
84+
id: '1',
85+
type: 'table',
86+
data: {},
87+
position: { x: 0, y: 0 },
88+
hidden: false,
89+
},
90+
{
91+
id: '2',
92+
type: 'table',
93+
data: {},
94+
position: { x: 0, y: 0 },
95+
hidden: false,
96+
},
97+
])
98+
99+
render(<CommandPaletteCommandOptions />, { wrapper })
100+
101+
expect(
102+
screen.queryByRole('option', { name: 'Show All Tables ⇧ A' }),
103+
).not.toBeInTheDocument()
104+
expect(
105+
screen.getByRole('option', { name: 'Hide All Tables ⇧ H' }),
106+
).toBeInTheDocument()
107+
})
108+
109+
it('shows both "Show All Tables" and "Hide All Tables" options when some tables are visible and the others are hidden', () => {
110+
mockDefaultNodes.mockReturnValueOnce([
111+
{
112+
id: '1',
113+
type: 'table',
114+
data: {},
115+
position: { x: 0, y: 0 },
116+
hidden: true,
117+
},
118+
{
119+
id: '2',
120+
type: 'table',
121+
data: {},
122+
position: { x: 0, y: 0 },
123+
hidden: false,
124+
},
125+
])
126+
127+
render(<CommandPaletteCommandOptions />, { wrapper })
128+
129+
expect(
130+
screen.getByRole('option', { name: 'Show All Tables ⇧ A' }),
131+
).toBeInTheDocument()
132+
expect(
133+
screen.getByRole('option', { name: 'Hide All Tables ⇧ H' }),
134+
).toBeInTheDocument()
135+
})
136+
})

frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPaletteOptions/CommandOptions.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import {
22
Copy,
3+
Eye,
4+
EyeOff,
35
KeyRound,
46
PanelTop,
57
RectangleHorizontal,
@@ -9,6 +11,7 @@ import {
911
import { Command } from 'cmdk'
1012
import type { FC } from 'react'
1113
import { useUserEditingOrThrow } from '../../../../../../stores'
14+
import { useTableVisibility } from '../../hooks'
1215
import { useCommandPaletteOrThrow } from '../CommandPaletteProvider'
1316
import { useCopyLink } from '../hooks/useCopyLink'
1417
import { useFitScreen } from '../hooks/useFitScreen'
@@ -19,6 +22,7 @@ export const CommandPaletteCommandOptions: FC = () => {
1922
const { copyLink } = useCopyLink('command-palette')
2023
const { zoomToFit, tidyUp } = useFitScreen()
2124
const { setShowMode } = useUserEditingOrThrow()
25+
const { visibilityStatus, showAllNodes, hideAllNodes } = useTableVisibility()
2226

2327
const { setOpen } = useCommandPaletteOrThrow()
2428

@@ -103,6 +107,42 @@ export const CommandPaletteCommandOptions: FC = () => {
103107
<span className={styles.keyIcon}></span>
104108
<span className={styles.keyIcon}>4</span>
105109
</Command.Item>
110+
{visibilityStatus !== 'all-visible' && (
111+
<Command.Item
112+
className={styles.item}
113+
value={getSuggestionText({
114+
type: 'command',
115+
name: 'Show All Tables',
116+
})}
117+
onSelect={() => {
118+
showAllNodes()
119+
setOpen(false)
120+
}}
121+
>
122+
<Eye className={styles.itemIcon} />
123+
<span className={styles.itemText}>Show All Tables</span>
124+
<span className={styles.keyIcon}></span>
125+
<span className={styles.keyIcon}>A</span>
126+
</Command.Item>
127+
)}
128+
{visibilityStatus !== 'all-hidden' && (
129+
<Command.Item
130+
className={styles.item}
131+
value={getSuggestionText({
132+
type: 'command',
133+
name: 'Hide All Tables',
134+
})}
135+
onSelect={() => {
136+
hideAllNodes()
137+
setOpen(false)
138+
}}
139+
>
140+
<EyeOff className={styles.itemIcon} />
141+
<span className={styles.itemText}>Hide All Tables</span>
142+
<span className={styles.keyIcon}></span>
143+
<span className={styles.keyIcon}>H</span>
144+
</Command.Item>
145+
)}
106146
</Command.Group>
107147
)
108148
}

frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/CommandPalettePreview/CommandPreview.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type Props = {
55
commandName: string
66
}
77

8+
// TODO: set gif or image for "Show All Table" and "Hide All Table" commands
89
const COMMAND_VIDEO_SOURCE: Record<string, string> = {
910
'copy link':
1011
'https://assets.liambx.com/erd-core/2025-09-01/videos/copy-link.mp4',

frontend/packages/erd-core/src/features/erd/components/ERDRenderer/CommandPalette/hooks/useSubscribeCommands/useSubscribeCommands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useSubscribeCopyLinkCommand } from './useSubscribeCopyLinkCommand'
22
import { useSubscribeShowModeCommand } from './useSubscribeSwitchShowMode'
3+
import { useSubscribeTableVisibility } from './useSubscribeTableVisibility'
34
import { useSubscribeTidyUpCommand } from './useSubscribeTidyUpCommand'
45
import { useSubscribeZoomToFitCommand } from './useSubscribeZoomToFitCommand'
56

@@ -8,4 +9,5 @@ export const useSubscribeCommands = () => {
89
useSubscribeShowModeCommand()
910
useSubscribeTidyUpCommand()
1011
useSubscribeZoomToFitCommand()
12+
useSubscribeTableVisibility()
1113
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { renderHook, waitFor } from '@testing-library/react'
2+
import userEvent from '@testing-library/user-event'
3+
import { ReactFlowProvider } from '@xyflow/react'
4+
import { NuqsTestingAdapter, type UrlUpdateEvent } from 'nuqs/adapters/testing'
5+
import type { ReactNode } from 'react'
6+
import { afterEach, expect, it, vi } from 'vitest'
7+
import { UserEditingProvider } from '../../../../../../../stores'
8+
import { compressToEncodedUriComponent } from '../../../../../../../utils/compressToEncodedUriComponent'
9+
import { useSubscribeTableVisibility } from './useSubscribeTableVisibility'
10+
11+
const onUrlUpdate = vi.fn<() => [UrlUpdateEvent]>()
12+
13+
const wrapper = ({ children }: { children: ReactNode }) => (
14+
<NuqsTestingAdapter onUrlUpdate={onUrlUpdate}>
15+
<ReactFlowProvider
16+
defaultNodes={[
17+
{
18+
id: '1',
19+
type: 'table',
20+
data: {},
21+
position: { x: 0, y: 0 },
22+
hidden: false,
23+
},
24+
{
25+
id: '2',
26+
type: 'table',
27+
data: {},
28+
position: { x: 0, y: 0 },
29+
hidden: true,
30+
},
31+
]}
32+
>
33+
<UserEditingProvider>{children}</UserEditingProvider>
34+
</ReactFlowProvider>
35+
</NuqsTestingAdapter>
36+
)
37+
38+
afterEach(() => {
39+
vi.clearAllMocks()
40+
})
41+
42+
it('hides all table nodes by ⇧H', async () => {
43+
const user = userEvent.setup()
44+
renderHook(() => useSubscribeTableVisibility(), { wrapper })
45+
46+
await user.keyboard('{Shift>}H{/Shift}')
47+
48+
// hidden query parameter should be added with all node ids
49+
await waitFor(() => {
50+
expect(onUrlUpdate).toHaveBeenCalledWith(
51+
expect.objectContaining({
52+
queryString: `?hidden=${compressToEncodedUriComponent('1,2')}`,
53+
}),
54+
)
55+
})
56+
})
57+
58+
it('shows all table nodes by ⇧A', async () => {
59+
const user = userEvent.setup()
60+
renderHook(() => useSubscribeTableVisibility(), { wrapper })
61+
62+
await user.keyboard('{Shift>}A{/Shift}')
63+
64+
// hidden query parameter should be added with all node ids
65+
await waitFor(() => {
66+
expect(onUrlUpdate).toHaveBeenCalledWith(
67+
expect.objectContaining({
68+
queryString: '?hidden=',
69+
}),
70+
)
71+
})
72+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useEffect } from 'react'
2+
import { useTableVisibility } from '../../../hooks'
3+
4+
export const useSubscribeTableVisibility = () => {
5+
const { showAllNodes, hideAllNodes } = useTableVisibility()
6+
7+
// Show all nodes when ⇧A is pressed
8+
useEffect(() => {
9+
const down = async (event: KeyboardEvent) => {
10+
if (event.code === 'KeyA' && event.shiftKey) {
11+
showAllNodes()
12+
}
13+
}
14+
15+
document.addEventListener('keydown', down)
16+
return () => document.removeEventListener('keydown', down)
17+
}, [showAllNodes])
18+
19+
// Hide all nodes when ⇧H is pressed
20+
useEffect(() => {
21+
const down = async (event: KeyboardEvent) => {
22+
if (event.code === 'KeyH' && event.shiftKey) {
23+
hideAllNodes()
24+
}
25+
}
26+
27+
document.addEventListener('keydown', down)
28+
return () => document.removeEventListener('keydown', down)
29+
}, [hideAllNodes])
30+
}

0 commit comments

Comments
 (0)