From 9e4eb33e05ba3f70565dead9dbf99d90bf336e88 Mon Sep 17 00:00:00 2001 From: Victor Wiebe Date: Mon, 20 Jul 2026 14:04:12 -0400 Subject: [PATCH] Add visual response bindings and variables --- BASELINE.md | 26 +++ CODEX.md | 31 +++- NEXT_SESSION_PROMPT.md | 15 +- SLICE2.md | 45 +++-- TASKS.md | 113 ++++++------ .../valid-visual-rest-actions.json | 33 +++- .../ActionInspector.module.css | 18 ++ .../ActionInspector/ActionInspector.test.tsx | 83 ++++++++- .../ActionInspector/ActionInspector.tsx | 101 +++++++++-- .../ActionInspector/ResponseBindingEditor.tsx | 62 +++++++ .../ActionInspector/VariableEditor.tsx | 41 +++++ .../configurationUtils.test.ts | 81 +++++++++ .../ActionInspector/configurationUtils.ts | 163 ++++++++++++++++++ frontend/src/types/project.ts | 7 +- 14 files changed, 723 insertions(+), 96 deletions(-) create mode 100644 frontend/src/components/ActionInspector/ResponseBindingEditor.tsx create mode 100644 frontend/src/components/ActionInspector/VariableEditor.tsx create mode 100644 frontend/src/components/ActionInspector/configurationUtils.test.ts create mode 100644 frontend/src/components/ActionInspector/configurationUtils.ts diff --git a/BASELINE.md b/BASELINE.md index f01bc7e..d2becbe 100644 --- a/BASELINE.md +++ b/BASELINE.md @@ -193,3 +193,29 @@ On 2026-07-19, focused tests passed (3 suites / 6 tests), full frontend tests pa On 2026-07-19, focused request-input tests passed at 3 suites / 9 tests, the full frontend suite passed at 14 suites / 467 tests, frontend and backend builds passed, and the schema matrix passed at 13 / 2 / 2. The UI writes executed component/variable templates to URL, header, query, and body fields while excluding path parameters and `inputMap`. Standalone `tsc` reproduced the recorded dependency mismatch. Manual request-input acceptance remains pending. + +### Pending manual request-input acceptance + +1. Start Docker Desktop and run Conductor through Docker Compose. +2. Open a project containing a Text Input or Dropdown, a Button, and an anonymous REST action. +3. Edit the action and add a query parameter such as `item`. +4. In Request value reference, choose the component, choose `Query: item`, and insert the reference. +5. Confirm canonical JSON immediately contains `"item": "{{components..value}}"` and does not add `inputMap`. +6. Assign the action to the Button, enter or select a Preview value, and execute it. +7. Confirm the outgoing request uses the runtime component value and loading, response, and error state remain ephemeral. +8. Confirm path parameters are not offered as destinations and raw URL/header/query/body template editing still works. +9. If an existing JSON-authored variable is available, repeat insertion and verify `{{variables.}}` resolution. + +## Slice 2 Response Binding and Variable Validation + +On 2026-07-20, focused configuration tests passed under Node 20/npm 10 at 2 suites / 14 tests. The full frontend suite passed at 15 suites / 477 tests with 0 snapshots. The frontend CRA production compile and backend TypeScript build passed. The schema fixture matrix passed at 13 valid fixtures, 2 expected schema failures, and 2 diagnostic-invalid fixtures that remain structurally valid. + +Automated coverage verifies canonical response-binding and variable creation/editing/deletion, the `onSuccess` default for new action-response bindings, supported and unambiguous target filtering, typed variable default parsing, canonical synchronization, binding-editor deletion lifecycle, and reference-aware variable deletion warnings. Existing legacy `onClick` compatibility remains in runtime and diagnostics and supports one-way visual migration; the UI cannot create a new `onClick` response binding. No deprecated `action.responseMapping` behavior was added. + +Standalone frontend `npx tsc --noEmit` continues to fail in `node_modules/@types/node/ffi.d.ts` because the project's TypeScript 4.9 compiler cannot parse the resolved `@types/node@26.1.0` declarations. This is reported separately from the successful CRA production TypeScript compile. + +Manual acceptance is pending. During the 2026-07-20 audit, the Docker CLI reported that it is unavailable in this WSL distro and Docker Desktop WSL integration must be enabled. Compose/service/health validation and the request-input browser checklist therefore could not be completed here. The earlier request-input workflow and the new response-binding/variable workflow have not been marked manually accepted. + +### Partial manual request-input checkpoint + +On 2026-07-20, the user confirmed that the `item` query-parameter row and guided Request value reference insertion into `Query: item` looked correct. This is a partial checkpoint, not full manual acceptance. Preview execution, outgoing-request inspection, ephemeral-state verification, path-destination exclusion, raw-template regression coverage, and variable reference resolution remain pending. diff --git a/CODEX.md b/CODEX.md index fe3c510..ba255e5 100644 --- a/CODEX.md +++ b/CODEX.md @@ -104,7 +104,7 @@ All eleven schema-supported MVP components are available in the palette and have - Secure credential storage and secret resolution are not implemented. - The REST proxy still needs endpoint allowlisting, SSRF protection, and stricter header and URL validation. - Sanitized execution history and troubleshooting views are not implemented. -- Visual REST action authoring is implemented; component events, request inputs, response bindings, variables, and page-load actions still rely on JSON editing. +- Visual REST action, Button event, request-input, response-binding, and variable authoring are implemented; page-load actions still rely on JSON editing. - Project documents are validated in the JSON Editor, but save operations still need an unconditional validation gate. - Backend and end-to-end automated test coverage remains incomplete. - IBM Bob/watsonx will not be used; AI is provider-neutral and post-MVP. @@ -133,8 +133,8 @@ The initial repository push was completed on 2026-07-18. Unless the user chooses a different priority, proceed in this order: -1. Add visual component-event configuration, beginning with Button `onClick`, then request input mapping. -2. Add visual response bindings, variables, and page-load action configuration. +1. Complete the pending manual acceptance for request-input authoring and the proportional response-binding/variable workflow. +2. Add visual page-load action configuration. 3. Add validation to every project save path. 4. Implement authentication and secure secret handling. 5. Harden the REST proxy and add sanitized execution logging. @@ -178,7 +178,7 @@ After material work: ## Latest Handoff -Date: 2026-07-19 +Date: 2026-07-20 - Completed Slice 2 Increment 1: visual anonymous REST action creation, editing, duplication, and reference-aware deletion. - Added method, URL, header, query, static path-parameter, and body controls with immediate canonical-document synchronization. @@ -188,8 +188,7 @@ Date: 2026-07-19 - Validation passes under Node 20/npm 10: 9 frontend suites / 456 tests, frontend production build, backend TypeScript build, and the 13 / 2 / 2 schema matrix. - The user manually accepted the Increment 1 REST action authoring and referenced-action deletion workflow after starting Docker Desktop and the app. - Standalone frontend `tsc --noEmit` remains blocked by the existing dependency mismatch. Browser automation remained unavailable; user manual acceptance passed. -- No commit or push was made; the work remains uncommitted on top of `b9697cc` as requested. -- Next recommended action: Slice 2 Increment 2, starting with visual Button `onClick` action selection and request inputs. +- Slice 2 REST action and Button event configuration was committed and pushed as `3994eaa`. ### Increment 2 Addendum @@ -200,7 +199,7 @@ Date: 2026-07-19 - Validation passes: 12 frontend suites / 462 tests, frontend production build, backend build, and the 13 / 2 / 2 schema matrix. - Standalone frontend `tsc --noEmit` retains the recorded dependency failure. - Automated browser validation was unavailable. The user manually accepted the Increment 2 Button `onClick` workflow on 2026-07-19 by creating a REST action and assigning it visually to a Button. -- No commit or push was made. +- Visual request-input authoring and its handoff were committed and pushed as `6fd084c`. ### Request Input Addendum @@ -211,3 +210,21 @@ Date: 2026-07-19 - Validation passes: 14 frontend suites / 467 tests, frontend production build, backend build, and the 13 / 2 / 2 schema matrix. - Standalone frontend `tsc --noEmit` retains the recorded dependency failure. - Manual request-input acceptance remains pending. +- `NEXT_SESSION_PROMPT.md` contains the continuation scope and manual request-input acceptance checklist. + +### Response Binding and Variable Addendum + +- Added visual create, edit, and delete support for canonical top-level `project.bindings`. +- New action-response bindings select an existing REST action and unambiguous supported component or variable target and always default to `trigger: "onSuccess"`. +- Existing legacy `onClick` bindings remain editable and executable and can migrate one-way to `onSuccess`; the UI cannot create new legacy triggers. Deprecated `action.responseMapping` remains unused. +- Added visual variable declaration, renaming, type/default-value/description editing, deletion, validation, and reference-aware deletion warnings. Preview continues to copy defaults into ephemeral runtime state. +- Updated `valid-visual-rest-actions.json` with representative `onSuccess` component and variable targets. +- Focused tests pass under Node 20/npm 10 at 2 suites / 14 tests; the full frontend suite passes at 15 suites / 477 tests. The CRA production compile, backend build, and 13 / 2 / 2 schema matrix pass. +- Standalone frontend `tsc --noEmit` still fails in `@types/node/ffi.d.ts` because TypeScript 4.9 cannot parse the resolved `@types/node@26.1.0`; this remains separate from the passing CRA compile. +- Docker validation could not begin during the 2026-07-20 audit because Docker Desktop integration is unavailable in this WSL distro. Request-input manual acceptance remains pending and has not been marked accepted. Response-binding/variable manual acceptance is also pending. +- Page-load action authoring remains the next Slice 2 increment. + +### Partial Manual Request-Input Checkpoint + +- On 2026-07-20, the user confirmed that adding the `item` query-parameter row and using Request value reference to insert the selected component into `Query: item` looked correct. +- This marks the guided query-reference insertion checkpoint complete only. Preview execution, outgoing-request inspection, runtime-state separation, raw-template regression checks, path-destination exclusion, and variable resolution still require explicit confirmation before the overall request-input workflow is accepted. diff --git a/NEXT_SESSION_PROMPT.md b/NEXT_SESSION_PROMPT.md index 703357f..a54ed96 100644 --- a/NEXT_SESSION_PROMPT.md +++ b/NEXT_SESSION_PROMPT.md @@ -27,6 +27,19 @@ Requirements: - Add focused tests, update representative fixtures and durable state documents, and run the Node 20/npm 10 focused/full frontend tests, frontend build, backend build, schema matrix, and proportional manual workflow. - Report the standalone TypeScript 4.9 / `@types/node` incompatibility separately from the passing CRA production compile. -Request-input manual acceptance is still pending. Do not mark it accepted without user confirmation. +Before implementing the next increment, complete or coordinate manual acceptance of the most recently completed request-input step: + +1. Start Docker Desktop and run Conductor with Docker Compose; verify both services and the health endpoint. +2. Open or create a project containing a Text Input or Dropdown, a Button, and an anonymous REST action. +3. Edit the REST action in Actions & Bindings and add a query parameter named `item`. +4. In Request value reference, select the input component, select `Query: item`, and click Insert reference. +5. Inspect canonical JSON and confirm the query value immediately becomes `{{components..value}}`. +6. Confirm no `ComponentEvent.inputMap` data was added and path parameters are not offered as destinations. +7. Assign the action to the Button, enter or select a runtime value in Preview, and execute the Button. +8. Confirm the outgoing request contains the runtime component value and runtime loading, response, and error state do not leak into canonical JSON. +9. Confirm raw URL/header/query/body template editing still works after guided insertion. +10. If the project already declares a variable through JSON, repeat insertion with it and verify `{{variables.}}` resolution. + +Record manual acceptance separately from automated coverage in `CODEX.md`, `SLICE2.md`, and `BASELINE.md`. Do not mark it accepted without user confirmation. Do not commit or push unless explicitly requested. diff --git a/SLICE2.md b/SLICE2.md index 3d7a7c7..fbec64f 100644 --- a/SLICE2.md +++ b/SLICE2.md @@ -2,7 +2,7 @@ ## Status -In progress — component events and request-input authoring complete; response bindings and variables next +In progress — response bindings and variable authoring complete; manual acceptance and page-load actions next ## Objective @@ -34,13 +34,13 @@ Allow an MVP project to be configured through the GUI without routine hand-editi - [x] Keep Slice 2 authentication authoring anonymous-only and defer credential-backed authentication controls to Slice 3. - [x] Implement component event configuration for Button `onClick`, including add/change/clear and dangling-reference diagnostics. - [x] Implement request input authoring through executed REST request templates; do not write inert `ComponentEvent.inputMap`. -- [ ] Implement response source and component/variable target selection. -- [ ] Default new action-response bindings to `onSuccess`. +- [x] Implement response source and component/variable target selection. +- [x] Default new action-response bindings to `onSuccess` without offering legacy `onClick` for new records. - [ ] Implement page-load action configuration for initial data population. -- [ ] Add variable declaration and default-value editing. -- [ ] Warn before deleting referenced actions, variables, or components. Action deletion is covered; variable and component deletion remain. +- [x] Add variable declaration and typed default-value editing. +- [ ] Warn before deleting referenced actions, variables, or components. Action and variable deletion are covered; component deletion remains. - [x] Ensure every REST action visual edit immediately updates canonical JSON while invalid drafts remain local. -- [ ] Add slice-wide tests, examples, and documentation updates. Increment 1 coverage and a representative REST fixture are complete. +- [ ] Add slice-wide tests, examples, and documentation updates. Coverage and the representative fixture are complete through response bindings and variables; page-load and final slice acceptance remain. ## Implementation Order @@ -63,9 +63,9 @@ Each increment writes only canonical configuration into the shared project docum ## Validation - [ ] Standalone frontend TypeScript check passes. TypeScript 4.9 currently cannot parse the resolved `@types/node@26.1.0`; the CRA production compile passes. -- [x] Frontend production build passes through the Button `onClick` increment. -- [x] Editor interaction and Preview dispatch tests pass through the Button `onClick` increment. -- [x] Increment 2 representative canonical JSON validates against the schema. +- [x] Frontend production build passes through the response-binding and variable increment. +- [x] Editor interaction and Preview dispatch tests pass through the response-binding and variable increment. +- [x] The representative canonical JSON validates through the response-binding and variable increment. - [x] Increment 1 manual REST action authoring and referenced-action deletion workflow passes. - [ ] Manual workflow-launcher and dependent-data scenarios pass. @@ -92,6 +92,27 @@ Each increment writes only canonical configuration into the shared project docum ## Handoff -- Last completed: Increment 2 visual request-input authoring for component and variable values, with automated validation. -- Next action: Implement visual response bindings and variable authoring, defaulting new action-response bindings to `onSuccess`. -- Known blockers: Manual request-input acceptance remains pending. Standalone frontend `tsc` retains its recorded tooling limitation. +- Last completed: Visual response-binding and variable authoring, with automated validation. +- Next action: Complete manual request-input and response-binding/variable acceptance, then implement visual page-load action authoring. +- Known blockers: Docker Desktop integration is unavailable in the current WSL distro, so manual acceptance remains pending. Standalone frontend `tsc` retains its recorded tooling limitation. +- Current pushed baseline: `6fd084c Add visual REST request input authoring`. +- Continuation and manual-test instructions: `NEXT_SESSION_PROMPT.md`. + +## Response Binding and Variable Increment + +Implemented and audited through 2026-07-20 in the working tree: + +- Visual canonical `project.bindings` create, edit, and delete controls. +- Existing REST action response selection with supported component and declared-variable targets. +- New bindings default to `onSuccess` and cannot select legacy `onClick`; existing legacy `onClick` bindings are preserved and can migrate one-way to `onSuccess`. +- New target choices exclude unsupported component properties and ambiguous duplicate component names. +- Visual variable declaration, rename, type, default value, description, and deletion controls with reference-aware warnings. +- No `action.responseMapping`, `ComponentEvent.inputMap`, authentication, secrets, page-load execution, or runtime-state persistence was added. +- Automated validation passes: 2 focused suites / 14 tests, 15 full frontend suites / 477 tests under Node 20/npm 10, frontend CRA production compile, backend build, and the 13 / 2 / 2 schema matrix. +- Standalone TypeScript remains blocked by the documented TypeScript 4.9 / `@types/node@26.1.0` parser incompatibility. + +Manual acceptance is separate from automated coverage. The preceding request-input workflow is still pending user confirmation because Docker Desktop integration is unavailable in this WSL distro. The response-binding and variable workflow is also pending proportional manual acceptance. Neither is marked accepted. + +On 2026-07-20, the user confirmed the partial request-input checkpoint for adding the `item` query parameter and inserting the selected component through `Request value reference` into `Query: item`. The remaining Preview, request inspection, runtime-state, destination-exclusion, raw-template, and variable-resolution checks are still pending; full manual acceptance is not recorded. + +Next increment: page-load action authoring. diff --git a/TASKS.md b/TASKS.md index 69013fd..9298f9b 100644 --- a/TASKS.md +++ b/TASKS.md @@ -68,74 +68,75 @@ This document summarizes the current implementation and the remaining work requi - [x] Implement the Container/Card component in the palette, canvas, property editor, Preview runtime, and tests. - [x] Remove the schema/runtime component-type mismatch; all eleven schema-supported types now render in the editor and Preview. -### Visual configuration workflows + ### Visual configuration workflows -- [x] Implement a visual REST Action editor for creating, editing, duplicating, and deleting anonymous actions without hand-editing JSON. -- [x] Implement visual Button `onClick` configuration with add/change/clear, missing-reference diagnostics, canonical synchronization, and Preview execution. -- [x] Implement visual component/variable request-input references through executed REST request templates. -- [ ] Implement a visual project-binding editor for request inputs and response targets. -- [ ] Implement page-load action configuration for automatically populated components. -- [ ] Complete component-specific property controls and basic styling controls required by the MVP specification. + - [x] Implement a visual REST Action editor for creating, editing, duplicating, and deleting anonymous actions without hand-editing JSON. + - [x] Implement visual Button `onClick` configuration with add/change/clear, missing-reference diagnostics, canonical synchronization, and Preview execution. + - [x] Implement visual component/variable request-input references through executed REST request templates. + - [x] Implement visual canonical response-binding CRUD with REST response sources, supported component/variable targets, `onSuccess` defaults, and legacy `onClick` compatibility. + - [x] Implement visual variable declaration and typed default-value editing with reference-aware diagnostics. + - [ ] Implement page-load action configuration for automatically populated components. + - [ ] Complete component-specific property controls and basic styling controls required by the MVP specification. -### Authentication and secrets + ### Authentication and secrets -- [ ] Implement Basic authentication during proxy execution. -- [ ] Implement Bearer token authentication during proxy execution. -- [ ] Implement API-key header authentication during proxy execution. -- [ ] Implement API-key query-parameter authentication during proxy execution. -- [ ] Add secure server-side credential storage and secret references. -- [ ] Ensure exported project JSON excludes secret values. -- [ ] Mask credentials and sensitive values in logs and error messages. + - [ ] Implement Basic authentication during proxy execution. + - [ ] Implement Bearer token authentication during proxy execution. + - [ ] Implement API-key header authentication during proxy execution. + - [ ] Implement API-key query-parameter authentication during proxy execution. + - [ ] Add secure server-side credential storage and secret references. + - [ ] Ensure exported project JSON excludes secret values. + - [ ] Mask credentials and sensitive values in logs and error messages. -### Proxy security and observability + ### Proxy security and observability -- [ ] Add permitted-origin or endpoint allowlisting for proxied requests. -- [ ] Add SSRF protections and stricter URL validation. -- [ ] Validate and sanitize forwarded request headers. -- [ ] Persist sanitized API execution history, including timestamp, method, endpoint, status, duration, and error details. -- [ ] Add a way to inspect basic execution history for troubleshooting. + - [ ] Add permitted-origin or endpoint allowlisting for proxied requests. + - [ ] Add SSRF protections and stricter URL validation. + - [ ] Validate and sanitize forwarded request headers. + - [ ] Persist sanitized API execution history, including timestamp, method, endpoint, status, duration, and error details. + - [ ] Add a way to inspect basic execution history for troubleshooting. -### Validation and error handling + ### Validation and error handling -- [ ] Validate the canonical project document on every save operation. -- [ ] Prevent invalid project documents from being persisted. -- [ ] Add user-facing Preview error states for all supported component targets. -- [ ] Reconcile schema descriptions and examples with the canonical `project.bindings` response-mapping model. -- [ ] Remove new uses of the deprecated `action.responseMapping` field. + - [ ] Validate the canonical project document on every save operation. + - [ ] Prevent invalid project documents from being persisted. + - [ ] Add user-facing Preview error states for all supported component targets. + - [ ] Reconcile schema descriptions and examples with the canonical `project.bindings` response-mapping model. + - [ ] Remove new uses of the deprecated `action.responseMapping` field. -### Testing and release validation + ### Testing and release validation -- [ ] Add backend tests for health, project CRUD, schema validation, and proxy input validation. -- [ ] Add proxy integration tests for successful responses, upstream errors, malformed responses, and timeouts. -- [ ] Add persistence round-trip tests for complete canonical project documents. -- [ ] Add editor tests for every MVP component and its property controls. -- [ ] Add end-to-end tests covering project creation, visual editing, action execution, response mapping, saving, and loading. -- [ ] Run frontend and backend TypeScript checks successfully. -- [ ] Run all frontend and backend automated tests successfully. -- [ ] Run schema validation against every valid and invalid example successfully. -- [ ] Produce successful frontend and backend production builds. -- [ ] Run the full application through Docker Compose and complete the documented integration checklist. + - [ ] Add backend tests for health, project CRUD, schema validation, and proxy input validation. + - [ ] Add proxy integration tests for successful responses, upstream errors, malformed responses, and timeouts. + - [ ] Add persistence round-trip tests for complete canonical project documents. + - [ ] Add editor tests for every MVP component and its property controls. + - [ ] Add end-to-end tests covering project creation, visual editing, action execution, response mapping, saving, and loading. + - [ ] Run frontend and backend TypeScript checks successfully. + - [ ] Run all frontend and backend automated tests successfully. + - [ ] Run schema validation against every valid and invalid example successfully. + - [ ] Produce successful frontend and backend production builds. + - [ ] Run the full application through Docker Compose and complete the documented integration checklist. -### MVP scope decision + ### MVP scope decision -- [x] Decide that IBM Bob/watsonx and other AI integrations are not part of the initial MVP. -- [x] Confirm that all five modeled authentication modes are required for MVP. -- [x] Define the six end-to-end MVP acceptance workflows in `MVP_SCOPE.md`. + - [x] Decide that IBM Bob/watsonx and other AI integrations are not part of the initial MVP. + - [x] Confirm that all five modeled authentication modes are required for MVP. + - [x] Define the six end-to-end MVP acceptance workflows in `MVP_SCOPE.md`. -### Documentation + ### Documentation -- [ ] Designate the `docs/` copies of project documentation as authoritative and remove or redirect duplicate root-level copies. -- [ ] Update the README documentation index to include the schema and response-mapping documents. -- [ ] Update the build plan with the current implementation status and remaining milestone order. -- [ ] Remove resolved questions from the requirements document or record their architectural decisions. -- [ ] Publish a final MVP acceptance checklist tied to tested user workflows. + - [ ] Designate the `docs/` copies of project documentation as authoritative and remove or redirect duplicate root-level copies. + - [ ] Update the README documentation index to include the schema and response-mapping documents. + - [ ] Update the build plan with the current implementation status and remaining milestone order. + - [ ] Remove resolved questions from the requirements document or record their architectural decisions. + - [ ] Publish a final MVP acceptance checklist tied to tested user workflows. -## Post-MVP Ideas + ## Post-MVP Ideas -- [ ] Add multi-page application support. -- [ ] Add OAuth 2.0 and IBM Cloud IAM authentication. -- [ ] Add OpenAPI import and generated forms/actions. -- [ ] Add reusable templates, themes, and component libraries. -- [ ] Add version history, Git integration, and team collaboration. -- [ ] Add role-based access control and enterprise audit retention. -- [ ] Add tabs, modal dialogs, date pickers, file uploads, charts, and progress indicators. + - [ ] Add multi-page application support. + - [ ] Add OAuth 2.0 and IBM Cloud IAM authentication. + - [ ] Add OpenAPI import and generated forms/actions. + - [ ] Add reusable templates, themes, and component libraries. + - [ ] Add version history, Git integration, and team collaboration. + - [ ] Add role-based access control and enterprise audit retention. + - [ ] Add tabs, modal dialogs, date pickers, file uploads, charts, and progress indicators. diff --git a/examples/project-definitions/valid-visual-rest-actions.json b/examples/project-definitions/valid-visual-rest-actions.json index 79ed23a..8afe285 100644 --- a/examples/project-definitions/valid-visual-rest-actions.json +++ b/examples/project-definitions/valid-visual-rest-actions.json @@ -4,7 +4,7 @@ "project": { "id": "proj_visual_rest_action_authoring", "name": "Visual REST Action Authoring", - "description": "Slice 2 fixture covering anonymous visual REST actions, Button onClick assignment, and executed component request templates.", + "description": "Slice 2 fixture covering anonymous visual REST actions, Button onClick assignment, executed request templates, response bindings, and variables.", "pages": [ { "id": "page_main", @@ -38,6 +38,14 @@ "events": [ { "event": "onClick", "actionId": "action_lookup_item" } ] + }, + { + "id": "cmp_result", + "type": "JsonViewer", + "name": "lookupResult", + "position": { "x": 24, "y": 152 }, + "size": { "width": 420, "height": 220 }, + "properties": { "label": "Lookup response", "visible": true } } ], "events": [] @@ -83,8 +91,27 @@ "authenticationType": "anonymous" } ], - "bindings": [], - "variables": {}, + "bindings": [ + { + "id": "binding_response_1", + "source": "actions.action_lookup_item.response.body", + "target": "components.lookupResult.value", + "trigger": "onSuccess" + }, + { + "id": "binding_response_2", + "source": "actions.action_lookup_item.response.body.url", + "target": "variables.lastLookupUrl", + "trigger": "onSuccess" + } + ], + "variables": { + "lastLookupUrl": { + "type": "string", + "defaultValue": "", + "description": "Ephemeral Preview value populated by the lookup response." + } + }, "settings": {} } } diff --git a/frontend/src/components/ActionInspector/ActionInspector.module.css b/frontend/src/components/ActionInspector/ActionInspector.module.css index 1e63f16..43b2d72 100644 --- a/frontend/src/components/ActionInspector/ActionInspector.module.css +++ b/frontend/src/components/ActionInspector/ActionInspector.module.css @@ -530,6 +530,24 @@ box-shadow: inset 3px 0 0 #2563eb; } +.configurationEditor { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + padding: 16px; + border-bottom: 1px solid #bfdbfe; + background: #f8fbff; + box-shadow: inset 3px 0 0 #2563eb; +} + +.variableCard { + padding: 14px 16px; + border-bottom: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + gap: 8px; +} + .actionEditorHeader { display: flex; align-items: flex-start; diff --git a/frontend/src/components/ActionInspector/ActionInspector.test.tsx b/frontend/src/components/ActionInspector/ActionInspector.test.tsx index 9db3abe..d90df00 100644 --- a/frontend/src/components/ActionInspector/ActionInspector.test.tsx +++ b/frontend/src/components/ActionInspector/ActionInspector.test.tsx @@ -1,4 +1,4 @@ -import React, { act } from 'react'; +import React, { act, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import type { Root } from 'react-dom/client'; import { ProjectProvider, useProject } from '../../context/ProjectContext'; @@ -17,6 +17,22 @@ function ProjectProbe(): React.ReactElement { ); } +function ProjectSeed({ legacyBinding = false }: { legacyBinding?: boolean }): null { + const { setDoc } = useProject(); + useEffect(() => setDoc((doc) => ({ + ...doc, + project: { + ...doc.project, + pages: [{ ...doc.project.pages[0], components: [ + { id: 'cmp_result', type: 'JsonViewer', name: 'result', position: { x: 0, y: 0 }, size: { width: 200, height: 100 }, properties: {} }, + ] }], + actions: [{ id: 'action_lookup', name: 'Lookup', method: 'GET', url: 'https://example.com', headers: {}, queryParameters: {}, pathParameters: {}, bodyTemplate: '', authenticationType: 'anonymous' }], + bindings: legacyBinding ? [{ id: 'binding_legacy', source: 'actions.action_lookup.response.body', target: 'components.result.value', trigger: 'onClick' }] : [], + }, + })), [setDoc]); + return null; +} + function currentDocument(container: HTMLElement): ProjectDocument { const probe = container.querySelector('[data-testid="project-probe"]'); if (!probe?.textContent) throw new Error('Project probe was not rendered.'); @@ -234,4 +250,69 @@ describe('Actions & Bindings visual REST action authoring', () => { 'Delete REST action "New REST Action"? This cannot be undone.', ); }); + + test('authors variables and onSuccess response bindings in canonical state', () => { + act(() => { + root.render(); + }); + + click(container.querySelector('[data-testid="add-variable"]')); + setControlValue(container.querySelector('input[aria-label="Variable name"]'), 'capturedId'); + setControlValue(container.querySelector('select[aria-label="Variable type"]'), 'number'); + setControlValue(container.querySelector('textarea[aria-label="Variable default value"]'), '7'); + click(Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Save variable') ?? null); + expect(currentDocument(container).project.variables.capturedId).toEqual({ type: 'number', defaultValue: 7 }); + + click(container.querySelector('[data-testid="add-response-binding"]')); + let binding = currentDocument(container).project.bindings[0]; + expect(binding).toMatchObject({ source: 'actions.action_lookup.response.body', target: 'components.result.value', trigger: 'onSuccess' }); + setControlValue(container.querySelector('input[aria-label="Binding response path"]'), 'body.id'); + setControlValue(container.querySelector('select[aria-label="Binding target"]'), 'variables.capturedId'); + binding = currentDocument(container).project.bindings[0]; + expect(binding.source).toBe('actions.action_lookup.response.body.id'); + expect(binding.target).toBe('variables.capturedId'); + expect(binding.trigger).toBe('onSuccess'); + }); + + test('warns about references when deleting a variable', () => { + act(() => { + root.render(); + }); + click(container.querySelector('[data-testid="add-variable"]')); + setControlValue(container.querySelector('input[aria-label="Variable name"]'), 'capturedId'); + click(Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Save variable') ?? null); + click(container.querySelector('[data-testid="add-response-binding"]')); + setControlValue(container.querySelector('select[aria-label="Binding target"]'), 'variables.capturedId'); + const confirm = jest.spyOn(window, 'confirm').mockReturnValue(false); + click(container.querySelector('button[aria-label="Delete variable capturedId"]')); + expect(confirm).toHaveBeenCalledWith(expect.stringContaining('binding "binding_response_1"')); + expect(currentDocument(container).project.variables.capturedId).toBeDefined(); + }); + + test('does not offer legacy onClick for new bindings and resets after deletion', () => { + act(() => { + root.render(); + }); + click(container.querySelector('[data-testid="add-response-binding"]')); + const trigger = container.querySelector('select[aria-label="Binding trigger"]'); + expect(Array.from(trigger?.options ?? []).map((option) => option.value)).toEqual(['onSuccess']); + const confirm = jest.spyOn(window, 'confirm').mockReturnValue(true); + click(container.querySelector('button[aria-label="Delete binding binding_response_1"]')); + expect(currentDocument(container).project.bindings).toEqual([]); + expect(container.querySelector('[data-testid="add-response-binding"]')?.disabled).toBe(false); + expect(confirm).toHaveBeenCalled(); + }); + + test('preserves an existing legacy onClick binding and allows one-way migration', () => { + act(() => { + root.render(); + }); + click(container.querySelector('button[aria-label="Edit binding binding_legacy"]')); + const trigger = container.querySelector('select[aria-label="Binding trigger"]'); + expect(Array.from(trigger?.options ?? []).map((option) => option.value)).toEqual(['onSuccess', 'onClick']); + expect(currentDocument(container).project.bindings[0].trigger).toBe('onClick'); + setControlValue(trigger, 'onSuccess'); + expect(currentDocument(container).project.bindings[0].trigger).toBe('onSuccess'); + expect(Array.from(container.querySelectorAll('select[aria-label="Binding trigger"] option')).map((option) => option.value)).toEqual(['onSuccess']); + }); }); diff --git a/frontend/src/components/ActionInspector/ActionInspector.tsx b/frontend/src/components/ActionInspector/ActionInspector.tsx index fc34b20..97a4430 100644 --- a/frontend/src/components/ActionInspector/ActionInspector.tsx +++ b/frontend/src/components/ActionInspector/ActionInspector.tsx @@ -50,6 +50,17 @@ import { } from '../Preview/bindingUtils'; import styles from './ActionInspector.module.css'; import RestActionEditor from './RestActionEditor'; +import ResponseBindingEditor from './ResponseBindingEditor'; +import VariableEditor from './VariableEditor'; +import { + createResponseBinding, + findVariableReferences, + removeBindingAt, + removeVariable, + replaceBindingAt, + responseTargetOptions, + setVariable, +} from './configurationUtils'; import { appendRestAction, buildDeleteConfirmation, @@ -537,8 +548,7 @@ function methodClass(method: string): string { } function parseActionSource(expr: string): string | null { - const m = /^actions\.([^.]+)\.response$/.exec(expr); - return m ? m[1] : null; + return parseActionSourcePath(expr)?.actionId ?? null; } function parseComponentExpr(expr: string): string | null { @@ -787,6 +797,8 @@ type BindingCardProps = { allComponents: CanvasComponent[]; allActions: RestAction[]; diags: Diagnostic[]; + onEdit: () => void; + onDelete: () => void; }; function BindingCard({ @@ -794,6 +806,8 @@ function BindingCard({ allComponents, allActions, diags, + onEdit, + onDelete, }: BindingCardProps): React.ReactElement { // Resolve source const sourceActionId = parseActionSource(binding.source); @@ -865,6 +879,10 @@ function BindingCard({ {/* ── Diagnostics ── */} +
+ + +
); } @@ -881,6 +899,9 @@ function ActionInspector(): React.ReactElement { ); const [editingActionIndex, setEditingActionIndex] = useState(null); + const [editingBindingIndex, setEditingBindingIndex] = useState(null); + const [editingVariableName, setEditingVariableName] = useState(null); + const [addingVariable, setAddingVariable] = useState(false); useEffect(() => { if ( @@ -891,6 +912,12 @@ function ActionInspector(): React.ReactElement { } }, [actions.length, editingActionIndex]); + useEffect(() => { + if (editingBindingIndex !== null && editingBindingIndex >= bindings.length) { + setEditingBindingIndex(null); + } + }, [bindings.length, editingBindingIndex]); + const handleAddAction = useCallback(() => { const action = createRestAction( actions, @@ -934,6 +961,27 @@ function ActionInspector(): React.ReactElement { [doc, setDoc], ); + const targetOptions = useMemo( + () => responseTargetOptions(allComponents, variables), + [allComponents, variables], + ); + + const handleAddBinding = useCallback(() => { + if (actions.length === 0 || targetOptions.length === 0) return; + const binding = createResponseBinding(bindings, actions[0], targetOptions[0].value); + setDoc((current) => ({ ...current, project: { ...current.project, bindings: [...current.project.bindings, binding] } })); + setEditingBindingIndex(bindings.length); + }, [actions, bindings, setDoc, targetOptions]); + + const handleDeleteVariable = useCallback((name: string) => { + const references = findVariableReferences(doc, name); + const suffix = references.length > 0 + ? `\n\nReferences will become unresolved:\n- ${references.join('\n- ')}` + : ''; + if (!window.confirm(`Delete variable "${name}"?${suffix}`)) return; + setDoc((current) => removeVariable(current, name)); + }, [doc, setDoc]); + // Compute all diagnostics once per render cycle const diagMap = useMemo( () => computeDiagnostics(actions, bindings, allComponents, variables), @@ -1048,7 +1096,10 @@ function ActionInspector(): React.ReactElement {
Bindings - {bindings.length} +
+ {bindings.length} + +
{/* Summary banner */} @@ -1065,22 +1116,46 @@ function ActionInspector(): React.ReactElement { {bindings.length === 0 ? (
- No bindings defined. Add bindings to{' '} - project.bindings in the JSON Editor. + No response bindings defined. Add a REST action and a supported + component or variable target to configure one visually.
) : ( - bindings.map((binding) => ( - + bindings.map((binding, bindingIndex) => ( + + setEditingBindingIndex(bindingIndex)} onDelete={() => { + if (window.confirm(`Delete binding "${binding.id}"?`)) { + setDoc((current) => removeBindingAt(current, bindingIndex)); + setEditingBindingIndex((current) => { + if (current === null || current === bindingIndex) return null; + return current > bindingIndex ? current - 1 : current; + }); + } + }} /> + {editingBindingIndex === bindingIndex && setDoc((current) => replaceBindingAt(current, bindingIndex, updated))} onDone={() => setEditingBindingIndex(null)} />} + )) )}
+
+
+ Variables +
{Object.keys(variables).length}
+
+ {addingVariable && { setDoc((current) => setVariable(current, name, variable)); setAddingVariable(false); }} onCancel={() => setAddingVariable(false)} />} + {Object.keys(variables).length === 0 && !addingVariable ?
No variables declared.
: Object.entries(variables).map(([name, variable]) => ( + +
+
{name} {variable.type}
+ {variable.defaultValue === undefined ? 'No default value' : JSON.stringify(variable.defaultValue)} + {variable.description && {variable.description}} +
+
+ {editingVariableName === name && { setDoc((current) => setVariable(current, nextName, nextVariable, name)); setEditingVariableName(null); }} onCancel={() => setEditingVariableName(null)} />} +
+ ))} +
+ {/* ══ Overall summary (only shown when there are issues) ═══════ */} {totalIssues === 0 && (actions.length > 0 || bindings.length > 0) && (
diff --git a/frontend/src/components/ActionInspector/ResponseBindingEditor.tsx b/frontend/src/components/ActionInspector/ResponseBindingEditor.tsx new file mode 100644 index 0000000..2a0e989 --- /dev/null +++ b/frontend/src/components/ActionInspector/ResponseBindingEditor.tsx @@ -0,0 +1,62 @@ +import React, { useMemo } from 'react'; +import type { Binding, CanvasComponent, RestAction, Variable } from '../../types/project'; +import { responseTargetOptions } from './configurationUtils'; +import styles from './ActionInspector.module.css'; + +type Props = { + binding: Binding; + actions: RestAction[]; + components: CanvasComponent[]; + variables: Record; + onChange: (binding: Binding) => void; + onDone: () => void; +}; + +export default function ResponseBindingEditor({ binding, actions, components, variables, onChange, onDone }: Props): React.ReactElement { + const targets = useMemo(() => responseTargetOptions(components, variables), [components, variables]); + const isLegacyOnClick = binding.trigger === 'onClick'; + const sourceMatch = /^actions\.([^.]+)\.response(?:\.(.*))?$/.exec(binding.source); + const actionId = sourceMatch?.[1] ?? ''; + const responsePath = sourceMatch?.[2] ?? ''; + const updateSource = (nextActionId: string, nextPath: string) => onChange({ + ...binding, + source: `actions.${nextActionId}.response${nextPath.trim() ? `.${nextPath.trim().replace(/^\.+/, '')}` : ''}`, + }); + + return ( +
+ + + + + + +
+ ); +} diff --git a/frontend/src/components/ActionInspector/VariableEditor.tsx b/frontend/src/components/ActionInspector/VariableEditor.tsx new file mode 100644 index 0000000..be60d2d --- /dev/null +++ b/frontend/src/components/ActionInspector/VariableEditor.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from 'react'; +import type { Variable, VariableType } from '../../types/project'; +import { formatVariableDefault, parseVariableDefault, validateVariableName } from './configurationUtils'; +import styles from './ActionInspector.module.css'; + +type Props = { + name: string; + variable: Variable; + variables: Record; + isNew?: boolean; + onSave: (name: string, variable: Variable) => void; + onCancel: () => void; +}; + +const TYPES: VariableType[] = ['string', 'number', 'boolean', 'object', 'array']; + +export default function VariableEditor({ name, variable, variables, isNew = false, onSave, onCancel }: Props): React.ReactElement { + const [nameDraft, setNameDraft] = useState(name); + const [type, setType] = useState(variable.type); + const [defaultDraft, setDefaultDraft] = useState(() => formatVariableDefault(variable)); + const [description, setDescription] = useState(variable.description ?? ''); + const nameError = validateVariableName(nameDraft, variables, isNew ? undefined : name); + let defaultValue: unknown; + let defaultError: string | null = null; + try { defaultValue = parseVariableDefault(type, defaultDraft); } catch (error) { defaultError = error instanceof Error ? error.message : String(error); } + const valid = !nameError && !defaultError; + + useEffect(() => { + if (type === 'boolean' && defaultDraft !== '' && defaultDraft !== 'true' && defaultDraft !== 'false') setDefaultDraft('false'); + }, [defaultDraft, type]); + + return ( +
+ + +