Skip to content

Commit 35d9c8c

Browse files
committed
feat(tables): configure reference columns in sidebar
1 parent b801e22 commit 35d9c8c

4 files changed

Lines changed: 324 additions & 57 deletions

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
8+
interface ComboboxOption {
9+
label: string
10+
value: string
11+
}
12+
13+
interface ComboboxProps {
14+
options: ComboboxOption[]
15+
value?: string
16+
placeholder?: string
17+
searchable?: boolean
18+
searchPlaceholder?: string
19+
onChange?: (value: string) => void
20+
}
21+
22+
interface SelectOptionsEditorProps {
23+
options: Array<{ id: string; name: string }>
24+
onChange: (options: Array<{ id: string; name: string }>) => void
25+
}
26+
27+
const {
28+
capturedComboboxes,
29+
capturedSelectEditor,
30+
mockAddColumn,
31+
mockUpdateColumn,
32+
mockUseTablesList,
33+
} = vi.hoisted(() => ({
34+
capturedComboboxes: { current: [] as ComboboxProps[] },
35+
capturedSelectEditor: { current: null as SelectOptionsEditorProps | null },
36+
mockAddColumn: vi.fn(),
37+
mockUpdateColumn: vi.fn(),
38+
mockUseTablesList: vi.fn(),
39+
}))
40+
41+
vi.mock('@sim/emcn', () => ({
42+
Button: ({ children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
43+
<button {...props}>{children}</button>
44+
),
45+
ChipCombobox: (props: ComboboxProps) => {
46+
capturedComboboxes.current.push(props)
47+
return <div data-placeholder={props.placeholder} />
48+
},
49+
ChipInput: (props: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
50+
FieldDivider: () => <hr />,
51+
Label: ({ children, ...props }: React.LabelHTMLAttributes<HTMLLabelElement>) => (
52+
<label {...props}>{children}</label>
53+
),
54+
Switch: ({ checked }: { checked?: boolean }) => (
55+
<button type='button' aria-pressed={checked}>
56+
Toggle
57+
</button>
58+
),
59+
cn: (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(' '),
60+
toast: { error: vi.fn(), success: vi.fn() },
61+
}))
62+
63+
vi.mock('@sim/emcn/icons', () => ({
64+
PlayOutline: () => <svg />,
65+
X: () => <svg />,
66+
}))
67+
68+
vi.mock('@/lib/table/column-types', () => ({
69+
ALL_COLUMN_TYPES: [
70+
{ id: 'string', label: 'Text', icon: () => null },
71+
{ id: 'select', label: 'Select', icon: () => null },
72+
{ id: 'reference', label: 'Reference', icon: () => null },
73+
],
74+
columnTypeOf: (type: string) => ({ supportsUnique: type !== 'select' }),
75+
}))
76+
77+
vi.mock('@/hooks/queries/tables', () => ({
78+
useAddTableColumn: () => ({ isPending: false, mutateAsync: mockAddColumn }),
79+
useTablesList: mockUseTablesList,
80+
useUpdateColumn: () => ({ isPending: false, mutateAsync: mockUpdateColumn }),
81+
}))
82+
83+
vi.mock('@/app/workspace/[workspaceId]/tables/[tableId]/components/select-field', () => ({
84+
SelectOptionsEditor: (props: SelectOptionsEditorProps) => {
85+
capturedSelectEditor.current = props
86+
return <div data-testid='select-options-editor' />
87+
},
88+
}))
89+
90+
import { ColumnConfigSidebar } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar'
91+
92+
let container: HTMLDivElement
93+
let root: Root
94+
95+
function findCombobox(placeholder: string): ComboboxProps | undefined {
96+
return capturedComboboxes.current.find((combobox) => combobox.placeholder === placeholder)
97+
}
98+
99+
function findButton(label: string): HTMLButtonElement | undefined {
100+
return [...container.querySelectorAll<HTMLButtonElement>('button')].find(
101+
(button) => button.textContent === label
102+
)
103+
}
104+
105+
beforeEach(() => {
106+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
107+
container = document.createElement('div')
108+
document.body.appendChild(container)
109+
root = createRoot(container)
110+
capturedComboboxes.current = []
111+
capturedSelectEditor.current = null
112+
mockUseTablesList.mockReturnValue({
113+
data: [
114+
{ id: 'table-current', name: 'Current table' },
115+
{ id: 'table-customers', name: 'Customers' },
116+
],
117+
})
118+
mockAddColumn.mockResolvedValue({ data: { columns: [] } })
119+
mockUpdateColumn.mockResolvedValue({ data: { columns: [] } })
120+
})
121+
122+
afterEach(() => {
123+
act(() => root.unmount())
124+
container.remove()
125+
vi.clearAllMocks()
126+
})
127+
128+
describe('ColumnConfigSidebar', () => {
129+
it('creates a Reference column with the selected workspace table', async () => {
130+
await act(async () => {
131+
root.render(
132+
<ColumnConfigSidebar
133+
config={{ mode: 'create', proposedName: 'Related row', type: 'reference' }}
134+
onClose={vi.fn()}
135+
existingColumn={null}
136+
workspaceId='workspace-1'
137+
tableId='table-current'
138+
/>
139+
)
140+
})
141+
142+
expect(mockUseTablesList).toHaveBeenCalledWith('workspace-1', 'active', { enabled: true })
143+
expect(container.querySelector<HTMLInputElement>('#column-sidebar-name')?.value).toBe(
144+
'Related row'
145+
)
146+
expect(findCombobox('Select table')).toMatchObject({
147+
options: [
148+
{ label: 'Current table', value: 'table-current' },
149+
{ label: 'Customers', value: 'table-customers' },
150+
],
151+
searchable: true,
152+
searchPlaceholder: 'Search tables',
153+
})
154+
155+
act(() => findCombobox('Select table')?.onChange?.('table-customers'))
156+
await act(async () => findButton('Save')?.click())
157+
158+
expect(mockAddColumn).toHaveBeenCalledWith({
159+
name: 'Related row',
160+
type: 'reference',
161+
referenceTableId: 'table-customers',
162+
})
163+
})
164+
165+
it('edits Reference configuration without exposing column renaming', async () => {
166+
await act(async () => {
167+
root.render(
168+
<ColumnConfigSidebar
169+
config={{ mode: 'edit', columnName: 'col-reference' }}
170+
onClose={vi.fn()}
171+
existingColumn={{
172+
id: 'col-reference',
173+
name: 'Related row',
174+
type: 'reference',
175+
referenceTableId: 'table-current',
176+
}}
177+
workspaceId='workspace-1'
178+
tableId='table-current'
179+
/>
180+
)
181+
})
182+
183+
expect(container).not.toHaveTextContent('Column name')
184+
expect(container.querySelector('#column-sidebar-name')).toBeNull()
185+
186+
act(() => findCombobox('Select table')?.onChange?.('table-customers'))
187+
await act(async () => findButton('Save')?.click())
188+
189+
expect(mockUpdateColumn).toHaveBeenCalledWith({
190+
columnName: 'col-reference',
191+
updates: { referenceTableId: 'table-customers' },
192+
})
193+
})
194+
195+
it('keeps Select options in the edit sidebar', async () => {
196+
await act(async () => {
197+
root.render(
198+
<ColumnConfigSidebar
199+
config={{ mode: 'edit', columnName: 'col-status' }}
200+
onClose={vi.fn()}
201+
existingColumn={{
202+
id: 'col-status',
203+
name: 'Status',
204+
type: 'select',
205+
options: [{ id: 'option-ready', name: 'Ready' }],
206+
}}
207+
workspaceId='workspace-1'
208+
tableId='table-current'
209+
/>
210+
)
211+
})
212+
213+
expect(container).toHaveTextContent('Options')
214+
expect(container).toHaveTextContent('Multiselect')
215+
act(() =>
216+
capturedSelectEditor.current?.onChange([
217+
{ id: 'option-ready', name: 'Ready' },
218+
{ id: 'option-done', name: 'Done' },
219+
])
220+
)
221+
await act(async () => findButton('Save')?.click())
222+
223+
expect(mockUpdateColumn).toHaveBeenCalledWith({
224+
columnName: 'col-status',
225+
updates: {
226+
options: [
227+
{ id: 'option-ready', name: 'Ready' },
228+
{ id: 'option-done', name: 'Done' },
229+
],
230+
},
231+
})
232+
})
233+
})

0 commit comments

Comments
 (0)