Skip to content

Commit c38b64c

Browse files
Copilothotlong
andcommitted
feat: add Schema-Driven Config Panel Framework - types, hook, renderers, DashboardConfigPanel
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 25ee04d commit c38b64c

8 files changed

Lines changed: 731 additions & 1 deletion

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import * as React from 'react';
10+
import { Input } from '../ui/input';
11+
import { Switch } from '../ui/switch';
12+
import { Checkbox } from '../ui/checkbox';
13+
import { Slider } from '../ui/slider';
14+
import {
15+
Select,
16+
SelectContent,
17+
SelectItem,
18+
SelectTrigger,
19+
SelectValue,
20+
} from '../ui/select';
21+
import { Button } from '../ui/button';
22+
import { cn } from '../lib/utils';
23+
import { ConfigRow } from './config-row';
24+
import type { ConfigField } from '../types/config-panel';
25+
26+
export interface ConfigFieldRendererProps {
27+
/** Field schema */
28+
field: ConfigField;
29+
/** Current value */
30+
value: any;
31+
/** Change handler */
32+
onChange: (value: any) => void;
33+
/** Full draft for visibility evaluation and custom render */
34+
draft: Record<string, any>;
35+
/** Object definition for field-picker controls */
36+
objectDef?: Record<string, any>;
37+
}
38+
39+
/**
40+
* Renders a single config field based on its ControlType.
41+
*
42+
* Supports: input, switch, select, checkbox, slider, color, icon-group, custom.
43+
* filter/sort/field-picker are rendered as placeholders that consumers can
44+
* override with type='custom' when full sub-editor integration is needed.
45+
*/
46+
export function ConfigFieldRenderer({
47+
field,
48+
value,
49+
onChange,
50+
draft,
51+
objectDef,
52+
}: ConfigFieldRendererProps) {
53+
// Visibility gate
54+
if (field.visibleWhen && !field.visibleWhen(draft)) {
55+
return null;
56+
}
57+
58+
const effectiveValue = value ?? field.defaultValue;
59+
60+
switch (field.type) {
61+
case 'input':
62+
return (
63+
<ConfigRow label={field.label}>
64+
<Input
65+
data-testid={`config-field-${field.key}`}
66+
className="h-7 w-32 text-xs"
67+
value={effectiveValue ?? ''}
68+
placeholder={field.placeholder}
69+
disabled={field.disabled}
70+
onChange={(e) => onChange(e.target.value)}
71+
/>
72+
</ConfigRow>
73+
);
74+
75+
case 'switch':
76+
return (
77+
<ConfigRow label={field.label}>
78+
<Switch
79+
data-testid={`config-field-${field.key}`}
80+
checked={!!effectiveValue}
81+
disabled={field.disabled}
82+
onCheckedChange={(checked) => onChange(checked)}
83+
className="scale-75"
84+
/>
85+
</ConfigRow>
86+
);
87+
88+
case 'checkbox':
89+
return (
90+
<ConfigRow label={field.label}>
91+
<Checkbox
92+
data-testid={`config-field-${field.key}`}
93+
checked={!!effectiveValue}
94+
disabled={field.disabled}
95+
onCheckedChange={(checked) => onChange(checked)}
96+
/>
97+
</ConfigRow>
98+
);
99+
100+
case 'select':
101+
return (
102+
<ConfigRow label={field.label}>
103+
<Select
104+
value={String(effectiveValue ?? '')}
105+
onValueChange={(val) => onChange(val)}
106+
disabled={field.disabled}
107+
>
108+
<SelectTrigger
109+
data-testid={`config-field-${field.key}`}
110+
className="h-7 w-32 text-xs"
111+
>
112+
<SelectValue placeholder={field.placeholder ?? 'Select…'} />
113+
</SelectTrigger>
114+
<SelectContent>
115+
{(field.options ?? []).map((opt) => (
116+
<SelectItem key={opt.value} value={opt.value}>
117+
{opt.label}
118+
</SelectItem>
119+
))}
120+
</SelectContent>
121+
</Select>
122+
</ConfigRow>
123+
);
124+
125+
case 'slider':
126+
return (
127+
<ConfigRow label={field.label}>
128+
<div className="flex items-center gap-2 w-32">
129+
<Slider
130+
data-testid={`config-field-${field.key}`}
131+
value={[Number(effectiveValue ?? field.min ?? 0)]}
132+
min={field.min ?? 0}
133+
max={field.max ?? 100}
134+
step={field.step ?? 1}
135+
disabled={field.disabled}
136+
onValueChange={(vals) => onChange(vals[0])}
137+
aria-label={field.label}
138+
/>
139+
<span className="text-xs text-muted-foreground w-6 text-right">
140+
{effectiveValue ?? field.min ?? 0}
141+
</span>
142+
</div>
143+
</ConfigRow>
144+
);
145+
146+
case 'color':
147+
return (
148+
<ConfigRow label={field.label}>
149+
<input
150+
data-testid={`config-field-${field.key}`}
151+
type="color"
152+
className="h-7 w-10 rounded border cursor-pointer"
153+
value={effectiveValue ?? '#000000'}
154+
disabled={field.disabled}
155+
onChange={(e) => onChange(e.target.value)}
156+
/>
157+
</ConfigRow>
158+
);
159+
160+
case 'icon-group':
161+
return (
162+
<ConfigRow label={field.label}>
163+
<div className="flex items-center gap-0.5" data-testid={`config-field-${field.key}`}>
164+
{(field.options ?? []).map((opt) => (
165+
<Button
166+
key={opt.value}
167+
type="button"
168+
size="sm"
169+
variant={effectiveValue === opt.value ? 'default' : 'ghost'}
170+
className={cn('h-7 w-7 p-0', effectiveValue === opt.value && 'ring-1 ring-primary')}
171+
disabled={field.disabled}
172+
onClick={() => onChange(opt.value)}
173+
title={opt.label}
174+
>
175+
{opt.icon ?? <span className="text-[10px]">{opt.label.charAt(0)}</span>}
176+
</Button>
177+
))}
178+
</div>
179+
</ConfigRow>
180+
);
181+
182+
case 'field-picker':
183+
return (
184+
<ConfigRow
185+
label={field.label}
186+
value={effectiveValue ?? field.placeholder ?? 'Select field…'}
187+
onClick={field.disabled ? undefined : () => {
188+
/* open field picker - consumers should use type='custom' for full integration */
189+
}}
190+
/>
191+
);
192+
193+
case 'filter':
194+
case 'sort':
195+
// Complex sub-editors — consumers should use type='custom' to embed
196+
// FilterBuilder / SortBuilder with full field binding.
197+
return (
198+
<ConfigRow
199+
label={field.label}
200+
value={field.placeholder ?? `Configure ${field.type}…`}
201+
/>
202+
);
203+
204+
case 'custom':
205+
if (field.render) {
206+
return <>{field.render(effectiveValue, onChange, draft)}</>;
207+
}
208+
return null;
209+
210+
default:
211+
return null;
212+
}
213+
}

0 commit comments

Comments
 (0)