Skip to content

Commit f8e6317

Browse files
Copilothotlong
andcommitted
feat: upgrade object/field selectors to searchable Combobox, add panel switching tests
- Replace select dropdowns with searchable Combobox component for Data Source, Category Field, and Value Field when metadata is available - Combobox supports real-time search, keyboard navigation (matching Airtable UX) - Field comboboxes disabled when no object is selected - Falls back to text inputs when no metadata is available (backward-compatible) - Add DashboardWithConfig panel switching tests - Update existing dropdown tests to match Combobox's role="combobox" structure Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent afeb194 commit f8e6317

3 files changed

Lines changed: 108 additions & 31 deletions

File tree

packages/plugin-dashboard/src/WidgetConfigPanel.tsx

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import * as React from 'react';
1010
import {
1111
ConfigPanelRenderer,
1212
useConfigDraft,
13+
Combobox,
1314
} from '@object-ui/components';
15+
import { ConfigRow } from '@object-ui/components';
1416
import type { ConfigPanelSchema, ConfigField } from '@object-ui/components';
1517

1618
// ---------------------------------------------------------------------------
@@ -67,9 +69,22 @@ function buildWidgetSchema(
6769
? {
6870
key: 'object',
6971
label: 'Data source',
70-
type: 'select',
71-
options: availableObjects,
72-
placeholder: 'Select object…',
72+
type: 'custom',
73+
render: (value: any, onChange: (v: any) => void) => (
74+
<ConfigRow label="Data source">
75+
<div data-testid="config-field-object">
76+
<Combobox
77+
options={availableObjects}
78+
value={value ?? ''}
79+
onValueChange={onChange}
80+
placeholder="Select object…"
81+
searchPlaceholder="Search objects…"
82+
emptyText="No objects found."
83+
className="h-7 w-32 text-xs"
84+
/>
85+
</div>
86+
</ConfigRow>
87+
),
7388
}
7489
: {
7590
key: 'object',
@@ -82,10 +97,23 @@ function buildWidgetSchema(
8297
? {
8398
key: 'categoryField',
8499
label: 'Category field',
85-
type: 'select',
86-
options: hasFields ? availableFields : [],
87-
placeholder: 'Select field…',
88-
disabledWhen: (draft: Record<string, any>) => !draft.object,
100+
type: 'custom',
101+
render: (value: any, onChange: (v: any) => void, draft: Record<string, any>) => (
102+
<ConfigRow label="Category field">
103+
<div data-testid="config-field-categoryField">
104+
<Combobox
105+
options={hasFields ? availableFields : []}
106+
value={value ?? ''}
107+
onValueChange={onChange}
108+
placeholder="Select field…"
109+
searchPlaceholder="Search fields…"
110+
emptyText="No fields found."
111+
className="h-7 w-32 text-xs"
112+
disabled={!draft.object}
113+
/>
114+
</div>
115+
</ConfigRow>
116+
),
89117
}
90118
: {
91119
key: 'categoryField',
@@ -98,10 +126,23 @@ function buildWidgetSchema(
98126
? {
99127
key: 'valueField',
100128
label: 'Value field',
101-
type: 'select',
102-
options: hasFields ? availableFields : [],
103-
placeholder: 'Select field…',
104-
disabledWhen: (draft: Record<string, any>) => !draft.object,
129+
type: 'custom',
130+
render: (value: any, onChange: (v: any) => void, draft: Record<string, any>) => (
131+
<ConfigRow label="Value field">
132+
<div data-testid="config-field-valueField">
133+
<Combobox
134+
options={hasFields ? availableFields : []}
135+
value={value ?? ''}
136+
onValueChange={onChange}
137+
placeholder="Select field…"
138+
searchPlaceholder="Search fields…"
139+
emptyText="No fields found."
140+
className="h-7 w-32 text-xs"
141+
disabled={!draft.object}
142+
/>
143+
</div>
144+
</ConfigRow>
145+
),
105146
}
106147
: {
107148
key: 'valueField',

packages/plugin-dashboard/src/__tests__/DashboardWithConfig.test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,35 @@ describe('DashboardWithConfig', () => {
175175
const container = screen.getByTestId('dashboard-with-config');
176176
expect(container.className).toContain('custom-class');
177177
});
178+
179+
it('should show Dashboard > Configuration breadcrumb when no widget is selected', () => {
180+
render(
181+
<DashboardWithConfig
182+
schema={sampleSchema}
183+
config={sampleConfig}
184+
onConfigSave={vi.fn()}
185+
defaultConfigOpen={true}
186+
/>,
187+
);
188+
expect(screen.getByText('Dashboard')).toBeDefined();
189+
expect(screen.getByText('Configuration')).toBeDefined();
190+
// Widget breadcrumb should NOT be present
191+
expect(screen.queryByText('Widget')).toBeNull();
192+
});
193+
194+
it('should call onWidgetSave when widget config is saved', () => {
195+
const onWidgetSave = vi.fn();
196+
render(
197+
<DashboardWithConfig
198+
schema={sampleSchema}
199+
config={sampleConfig}
200+
onConfigSave={vi.fn()}
201+
onWidgetSave={onWidgetSave}
202+
defaultConfigOpen={true}
203+
/>,
204+
);
205+
// Config panel should open showing Dashboard config by default
206+
expect(screen.getByText('Dashboard')).toBeDefined();
207+
expect(screen.getByText('Configuration')).toBeDefined();
208+
});
178209
});

packages/plugin-dashboard/src/__tests__/WidgetConfigPanel.test.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ describe('WidgetConfigPanel', () => {
232232
expect(descInput.value).toBe('Monthly revenue breakdown');
233233
});
234234

235-
// ---- Dynamic dropdown tests (availableObjects / availableFields) --------
235+
// ---- Searchable combobox tests (availableObjects / availableFields) ------
236236

237237
describe('with availableObjects', () => {
238238
const objects = [
@@ -241,7 +241,7 @@ describe('WidgetConfigPanel', () => {
241241
{ value: 'orders', label: 'Orders' },
242242
];
243243

244-
it('should render data source as a select when availableObjects provided', () => {
244+
it('should render data source as a combobox when availableObjects provided', () => {
245245
render(
246246
<WidgetConfigPanel
247247
open={true}
@@ -251,9 +251,10 @@ describe('WidgetConfigPanel', () => {
251251
availableObjects={objects}
252252
/>,
253253
);
254-
// The data source field should be a select trigger (not a text input)
255-
const objectField = screen.getByTestId('config-field-object');
256-
expect(objectField.tagName).not.toBe('INPUT');
254+
// The data source field should be a combobox (not a text input)
255+
const wrapper = screen.getByTestId('config-field-object');
256+
const comboboxBtn = wrapper.querySelector('[role="combobox"]');
257+
expect(comboboxBtn).toBeTruthy();
257258
});
258259

259260
it('should render data source as an input when no availableObjects', () => {
@@ -269,7 +270,7 @@ describe('WidgetConfigPanel', () => {
269270
expect(objectField.tagName).toBe('INPUT');
270271
});
271272

272-
it('should render category/value fields as selects when availableObjects provided', () => {
273+
it('should render category/value fields as comboboxes when availableObjects provided', () => {
273274
const fields = [
274275
{ value: 'name', label: 'Name' },
275276
{ value: 'status', label: 'Status' },
@@ -284,13 +285,13 @@ describe('WidgetConfigPanel', () => {
284285
availableFields={fields}
285286
/>,
286287
);
287-
const catField = screen.getByTestId('config-field-categoryField');
288-
expect(catField.tagName).not.toBe('INPUT');
289-
const valField = screen.getByTestId('config-field-valueField');
290-
expect(valField.tagName).not.toBe('INPUT');
288+
const catWrapper = screen.getByTestId('config-field-categoryField');
289+
expect(catWrapper.querySelector('[role="combobox"]')).toBeTruthy();
290+
const valWrapper = screen.getByTestId('config-field-valueField');
291+
expect(valWrapper.querySelector('[role="combobox"]')).toBeTruthy();
291292
});
292293

293-
it('should disable field selectors when object is not selected', () => {
294+
it('should disable field comboboxes when object is not selected', () => {
294295
const configWithNoObject = { ...defaultWidgetConfig, object: '' };
295296
render(
296297
<WidgetConfigPanel
@@ -301,13 +302,15 @@ describe('WidgetConfigPanel', () => {
301302
availableObjects={objects}
302303
/>,
303304
);
304-
const catField = screen.getByTestId('config-field-categoryField');
305-
expect(catField).toHaveAttribute('disabled');
306-
const valField = screen.getByTestId('config-field-valueField');
307-
expect(valField).toHaveAttribute('disabled');
305+
const catWrapper = screen.getByTestId('config-field-categoryField');
306+
const catBtn = catWrapper.querySelector('[role="combobox"]');
307+
expect(catBtn).toHaveAttribute('disabled');
308+
const valWrapper = screen.getByTestId('config-field-valueField');
309+
const valBtn = valWrapper.querySelector('[role="combobox"]');
310+
expect(valBtn).toHaveAttribute('disabled');
308311
});
309312

310-
it('should not disable field selectors when object is selected', () => {
313+
it('should not disable field comboboxes when object is selected', () => {
311314
const fields = [
312315
{ value: 'name', label: 'Name' },
313316
{ value: 'amount', label: 'Amount' },
@@ -322,10 +325,12 @@ describe('WidgetConfigPanel', () => {
322325
availableFields={fields}
323326
/>,
324327
);
325-
const catField = screen.getByTestId('config-field-categoryField');
326-
expect(catField).not.toHaveAttribute('disabled');
327-
const valField = screen.getByTestId('config-field-valueField');
328-
expect(valField).not.toHaveAttribute('disabled');
328+
const catWrapper = screen.getByTestId('config-field-categoryField');
329+
const catBtn = catWrapper.querySelector('[role="combobox"]');
330+
expect(catBtn).not.toHaveAttribute('disabled');
331+
const valWrapper = screen.getByTestId('config-field-valueField');
332+
const valBtn = valWrapper.querySelector('[role="combobox"]');
333+
expect(valBtn).not.toHaveAttribute('disabled');
329334
});
330335
});
331336
});

0 commit comments

Comments
 (0)