Skip to content

Commit 2ead723

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(studio): use the searchable icon picker for every icon field (#1832)
Icon fields previously rendered as a free-text 'type a Lucide name' input unless a spec form pinned widget:'icon' — admins had to know exact icon names. - SchemaForm: auto-detect icon fields by name convention (a string prop named `icon` or `*Icon`, no enum) -> the existing 'icon' picker, mirroring the field-ref convention. Every metadata type's icon field now gets the searchable Lucide picker in the generic form without per-form wiring. - widgets: export IconPickerWidget so curated inspectors can reuse it. - ActionDefaultInspector: render the Action icon with the picker instead of a text input. Verified live: App icon (generic form) and Action icon (curated inspector) both render the picker; the searchable grid opens correctly (not clipped) inside the inspector panel. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e914e8c commit 2ead723

3 files changed

Lines changed: 26 additions & 2 deletions

File tree

packages/app-shell/src/views/metadata-admin/SchemaForm.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,22 @@ function detectFieldRefWidget(
279279
return undefined;
280280
}
281281

282+
/**
283+
* Detect the icon picker by NAME CONVENTION: a string prop named `icon` (or
284+
* `*Icon`) with no enum. Mirrors {@link detectFieldRefWidget} so every metadata
285+
* type's icon field renders the searchable Lucide picker instead of a free-text
286+
* input, without each spec `*.form.ts` having to pin `widget: 'icon'`.
287+
*/
288+
function detectIconWidget(name: string, schema: JsonSchema | undefined): string | undefined {
289+
if (Array.isArray(schema?.enum)) return undefined;
290+
const isString =
291+
schema?.type === 'string' ||
292+
(Array.isArray(schema?.anyOf) && (schema!.anyOf as JsonSchema[]).some((b) => b?.type === 'string'));
293+
if (!isString) return undefined;
294+
if (name === 'icon' || /Icon$/.test(name)) return 'icon';
295+
return undefined;
296+
}
297+
282298
/* -------------------------------------------------------------------------- */
283299
/* FormView spec (subset) */
284300
/* -------------------------------------------------------------------------- */
@@ -739,6 +755,10 @@ function FieldRow({
739755
if (!fieldSpec?.widget) {
740756
const refWidget = detectFieldRefWidget(name, schema, widgetContext);
741757
if (refWidget) widget = refWidget;
758+
else {
759+
const iconWidget = detectIconWidget(name, schema);
760+
if (iconWidget) widget = iconWidget;
761+
}
742762
}
743763

744764
// Booleans with a schema default are never *missing* — don't show the

packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
import { useObjectOptions } from '../previews/useObjectOptions';
4949
import { useObjectFields } from '../previews/useObjectFields';
5050
import { ConditionBuilder } from './ConditionBuilder';
51+
import { IconPickerWidget } from '../widgets';
5152

5253
/* ─────────────── constants ─────────────── */
5354

@@ -253,7 +254,10 @@ export function ActionDefaultInspector({
253254
hint={objectName ? 'Bound action — surfaces in this object’s views per the placement below.' : 'Empty = global action — must be referenced by a page’s quick actions, global nav, a flow, or AI to appear.'}
254255
/>
255256
<div className="grid grid-cols-2 gap-2">
256-
<InspectorTextField label="Icon" value={str('icon')} onCommit={(v) => onPatch({ icon: v })} placeholder="lucide name" disabled={readOnly} />
257+
<div className="space-y-1">
258+
<Label className="text-xs text-muted-foreground">Icon</Label>
259+
<IconPickerWidget schema={{ type: 'string' }} value={str('icon')} onChange={(v) => onPatch({ icon: (v as string) || undefined })} readOnly={readOnly} />
260+
</div>
257261
<InspectorSelectField label="Variant" value={str('variant') || undefined} options={VARIANT_OPTS} onCommit={(v) => onPatch({ variant: v })} disabled={readOnly} />
258262
</div>
259263

packages/app-shell/src/views/metadata-admin/widgets.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1036,7 +1036,7 @@ const ICON_RESULT_LIMIT = 60;
10361036
* Built inline (no Radix portal) so the search + grid render eagerly — the same
10371037
* jsdom-friendly choice the other pickers' tests rely on.
10381038
*/
1039-
function IconPickerWidget({ id, value, onChange, readOnly }: WidgetProps) {
1039+
export function IconPickerWidget({ id, value, onChange, readOnly }: WidgetProps) {
10401040
const locale = detectLocale();
10411041
const current = value == null ? '' : String(value);
10421042
const [open, setOpen] = React.useState(false);

0 commit comments

Comments
 (0)