diff --git a/MAYBE_JSON_BINDING_REVIEW.md b/MAYBE_JSON_BINDING_REVIEW.md index 2973416..a47673f 100644 --- a/MAYBE_JSON_BINDING_REVIEW.md +++ b/MAYBE_JSON_BINDING_REVIEW.md @@ -169,7 +169,7 @@ The JSON UI system already supports `events` for action execution and `bindings` "onImport": { "actions": [{ "id": "import-json", "type": "custom" }] }, "onExport": { "actions": [{ "id": "export-json", "type": "custom" }] }, "onCopy": { "actions": [{ "id": "copy-json", "type": "custom" }] }, - "onPreview": { "actions": [{ "id": "open-preview", "type": "open-dialog", "target": "preview" }] }, + "onPreview": { "actions": [{ "id": "open-preview", "type": "open-dialog", "target": "uiState", "path": "preview" }] }, "onClear": { "actions": [{ "id": "clear-schema", "type": "set-value", "target": "schema.components", "value": [] }] } } } @@ -177,6 +177,8 @@ The JSON UI system already supports `events` for action execution and `bindings` **Why:** these are pure event triggers; `custom` actions cover app-specific flows that aren’t part of the built-in action types. +**Dialog storage convention:** `open-dialog`/`close-dialog` actions store booleans in `uiState.dialogs.`. Use `target` for the data source (typically `uiState`) and `path` for the dialog id. + ### 5) Drag-and-drop/hover state (CanvasRenderer, ComponentTree) **Bindings:** IDs and `dropPosition` stored in data; events mapped to custom actions for editor logic. @@ -229,7 +231,7 @@ The JSON UI system already supports `events` for action execution and `bindings` }, "onEdit": { "actions": [ - { "id": "open-source-editor", "type": "open-dialog", "target": "dataSourceEditor" } + { "id": "open-source-editor", "type": "open-dialog", "target": "uiState", "path": "dataSourceEditor" } ] }, "onDelete": { diff --git a/src/hooks/ui/use-action-executor.ts b/src/hooks/ui/use-action-executor.ts index 5765ec8..fd8364f 100644 --- a/src/hooks/ui/use-action-executor.ts +++ b/src/hooks/ui/use-action-executor.ts @@ -17,6 +17,38 @@ export function useActionExecutor(context: JSONUIContext) { const executeAction = useCallback(async (action: Action, event?: any) => { try { const evaluationContext = { data, event } + const updateByPath = (sourceId: string, path: string, value: any) => { + if (updatePath) { + updatePath(sourceId, path, value) + return + } + + const sourceData = data[sourceId] ?? {} + const pathParts = path.split('.') + const newData = { ...sourceData } + let current: any = newData + + for (let i = 0; i < pathParts.length - 1; i++) { + const key = pathParts[i] + current[key] = typeof current[key] === 'object' && current[key] !== null ? { ...current[key] } : {} + current = current[key] + } + + current[pathParts[pathParts.length - 1]] = value + updateData(sourceId, newData) + } + + const resolveDialogTarget = () => { + const defaultSourceId = 'uiState' + const hasExplicitTarget = Boolean(action.target && action.path) + const sourceId = hasExplicitTarget ? action.target : defaultSourceId + const dialogId = action.path ?? action.target + + if (!dialogId) return null + + const dialogPath = dialogId.startsWith('dialogs.') ? dialogId : `dialogs.${dialogId}` + return { sourceId, dialogPath } + } switch (action.type) { case 'create': { @@ -182,6 +214,20 @@ export function useActionExecutor(context: JSONUIContext) { break } + case 'open-dialog': { + const dialogTarget = resolveDialogTarget() + if (!dialogTarget) return + updateByPath(dialogTarget.sourceId, dialogTarget.dialogPath, true) + break + } + + case 'close-dialog': { + const dialogTarget = resolveDialogTarget() + if (!dialogTarget) return + updateByPath(dialogTarget.sourceId, dialogTarget.dialogPath, false) + break + } + case 'custom': { if (contextExecute) { await contextExecute(action, event)