diff --git a/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx b/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx index e791815ec..5eba73c4f 100644 --- a/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx +++ b/packages/app-shell/src/views/metadata-admin/inspectors/ActionDefaultInspector.tsx @@ -47,6 +47,7 @@ import { } from './_shared'; import { useObjectOptions } from '../previews/useObjectOptions'; import { useObjectFields } from '../previews/useObjectFields'; +import { useMetaOptions } from '../previews/useMetaOptions'; import { ConditionBuilder } from './ConditionBuilder'; import { IconPickerWidget } from '../widgets'; @@ -126,6 +127,38 @@ const TARGET_FIELD: Record = { flow: 'flow', modal: 'page', form: 'view' }; + +/** Target binding: a reference picker for flow/modal/form (names come from the + * matching metadata type), else a free-text input (url/api). An out-of-catalog + * value is preserved as a synthesized option so it is never silently dropped. */ +function ActionTargetField({ type, value, onCommit, cfg, readOnly }: { + type: string; + value: string; + onCommit: (v: string) => void; + cfg: { label: string; placeholder: string; hint: string }; + readOnly?: boolean; +}) { + const metaType = TARGET_META_TYPE[type] ?? null; + const { options } = useMetaOptions(metaType); + const usePicker = !!metaType && options.length > 0; + const opts = usePicker && value && !options.some((o) => o.value === value) + ? [{ value, label: `${value} (not found)` }, ...options] + : options; + return ( +
+ {usePicker ? ( + + ) : ( + + )} +
{cfg.hint}
+
+ ); +} + /** Keys this inspector edits with its own controls — hidden from the fallback. */ const CURATED_FIELDS = [ 'name', 'label', 'objectName', 'icon', 'variant', 'component', @@ -295,10 +328,13 @@ export function ActionDefaultInspector({ onPatch({ method: v })} disabled={readOnly} /> )} {targetCfg && ( -
- onPatch({ target: v })} placeholder={targetCfg.placeholder} disabled={readOnly} mono /> -
{targetCfg.hint}
-
+ onPatch({ target: v })} + cfg={targetCfg} + readOnly={readOnly} + /> )} )} diff --git a/packages/app-shell/src/views/metadata-admin/previews/useMetaOptions.ts b/packages/app-shell/src/views/metadata-admin/previews/useMetaOptions.ts new file mode 100644 index 000000000..314fa4ff2 --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/previews/useMetaOptions.ts @@ -0,0 +1,58 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * useMetaOptions — name+label options for ANY metadata type (generalises + * {@link useObjectOptions}). Powers reference pickers that point at a metadata + * record by name (e.g. an Action's `target` → a flow / page / view). Async; + * degrades to an empty list so callers fall back to a free-text input. Pass + * `null` to skip fetching. + */ +import * as React from 'react'; +import { useMetadataClient } from '../useMetadata'; + +export interface MetaOption { + value: string; + label: string; +} + +export function useMetaOptions(type: string | null): { options: MetaOption[]; loading: boolean } { + const client = useMetadataClient(); + const [options, setOptions] = React.useState([]); + const [loading, setLoading] = React.useState(!!type); + + React.useEffect(() => { + if (!type) { + setOptions([]); + setLoading(false); + return; + } + let cancelled = false; + setLoading(true); + client + .list<{ name?: string; label?: string }>(type) + .then((items) => { + if (cancelled) return; + const mapped = (items ?? []) + .map((raw) => (raw && typeof raw === 'object' && 'item' in raw ? (raw as any).item : raw)) + .filter((i: any) => typeof i?.name === 'string' && i.name) + .map((i: any) => ({ + value: i.name as string, + label: i.label ? `${i.label} (${i.name})` : (i.name as string), + })) + .sort((a: MetaOption, b: MetaOption) => a.value.localeCompare(b.value)); + setOptions(mapped); + setLoading(false); + }) + .catch(() => { + if (!cancelled) { + setOptions([]); + setLoading(false); + } + }); + return () => { + cancelled = true; + }; + }, [client, type]); + + return { options, loading }; +}