Complete Slice 1 MVP GUI components
This commit is contained in:
parent
d1c1ce9270
commit
b9697cc8b1
15
BASELINE.md
15
BASELINE.md
@ -32,8 +32,8 @@ npx --yes --package=node@20 --package=npm@10 npm test -- --watchAll=false --runI
|
||||
|
||||
Result: Passed.
|
||||
|
||||
- Test suites: 6 passed, 6 total
|
||||
- Tests: 420 passed, 420 total
|
||||
- Test suites: 7 passed, 7 total
|
||||
- Tests: 433 passed, 433 total
|
||||
- Snapshots: 0
|
||||
|
||||
### Frontend production build
|
||||
@ -58,7 +58,7 @@ Result: Passed.
|
||||
|
||||
## Schema Matrix
|
||||
|
||||
Eleven `valid-*.json` project definitions passed validation against `shared/schemas/conductor-project.schema.json`.
|
||||
Twelve `valid-*.json` project definitions pass validation against `shared/schemas/conductor-project.schema.json`, including the Slice 1 all-components fixture.
|
||||
|
||||
Two schema-invalid fixtures failed validation as expected:
|
||||
|
||||
@ -69,6 +69,15 @@ Two diagnostic-invalid fixtures passed structural schema validation as designed
|
||||
|
||||
- `invalid-template-malformed.json`
|
||||
- `invalid-template-missing-component.json`
|
||||
## Slice 1 Manual Acceptance
|
||||
|
||||
User validation completed successfully on 2026-07-18:
|
||||
|
||||
- All five new components can be configured and manipulated in the Visual Editor.
|
||||
- Preview interaction, disabled and hidden behavior, and Status Panel tones work as expected.
|
||||
- Visual Editor and JSON Editor synchronization works.
|
||||
- Save/load persistence and existing-component regression checks pass.
|
||||
|
||||
|
||||
## Runtime Startup
|
||||
|
||||
|
||||
39
CODEX.md
39
CODEX.md
@ -65,22 +65,19 @@ Some root-level documentation duplicates files under `docs/`. The `docs/` copies
|
||||
|
||||
### Active GUI components
|
||||
|
||||
The following components are available in the palette and have canvas, property-editor, and Preview support:
|
||||
All eleven schema-supported MVP components are available in the palette and have canvas, property-editor, and Preview support:
|
||||
|
||||
- Label
|
||||
- Button
|
||||
- Text Input
|
||||
- Dropdown
|
||||
- Table
|
||||
- JSON Viewer
|
||||
|
||||
The following MVP component types are accepted by the schema but are not active in the Visual Editor or Preview:
|
||||
|
||||
- Text Area
|
||||
- Checkbox
|
||||
- Radio Group
|
||||
- Status/Message Panel
|
||||
- Container/Card
|
||||
- Container/Card (flat presentational title and body; nesting deferred)
|
||||
- JSON Viewer
|
||||
|
||||
### REST and Preview runtime
|
||||
|
||||
@ -106,7 +103,6 @@ The following MVP component types are accepted by the schema but are not active
|
||||
- Sanitized execution history and troubleshooting views are not implemented.
|
||||
- Visual REST action, event, and binding configuration workflows are incomplete; advanced configuration still relies on JSON editing.
|
||||
- Project documents are validated in the JSON Editor, but save operations still need an unconditional validation gate.
|
||||
- The schema accepts five component types that the editor and runtime cannot render.
|
||||
- Backend and end-to-end automated test coverage remains incomplete.
|
||||
- IBM Bob/watsonx will not be used; AI is provider-neutral and post-MVP.
|
||||
- All five modeled authentication modes are required for MVP.
|
||||
@ -121,23 +117,23 @@ The initial repository push was completed on 2026-07-18.
|
||||
- The working tree was clean immediately after the push.
|
||||
- All `:Zone.Identifier` sidecar files were removed before the initial commit.
|
||||
- Node 20/npm 10 clean installs pass after repairing the frontend lockfile.
|
||||
- Frontend validation passes: 6 suites and 420 tests, plus the production build.
|
||||
- Frontend validation passes: 7 suites and 433 tests, plus the production build.
|
||||
- Backend build and the schema fixture matrix pass.
|
||||
- Docker Compose builds and starts both services.
|
||||
- Health, CRUD, cleanup, and restart persistence pass.
|
||||
- See `BASELINE.md` for exact evidence and remaining release work.
|
||||
- Manual Slice 1 acceptance passes for component configuration, Preview interaction, JSON synchronization, save/load persistence, and existing-component regressions.
|
||||
|
||||
## Immediate Priorities
|
||||
|
||||
Unless the user chooses a different priority, proceed in this order:
|
||||
|
||||
1. Implement the five missing MVP GUI components individually, with tests.
|
||||
2. Add visual REST action, event, and binding 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.
|
||||
6. Add backend, end-to-end, and security regression tests.
|
||||
7. Reconcile and consolidate project documentation.
|
||||
1. Add visual REST action, event, and binding configuration.
|
||||
2. Add validation to every project save path.
|
||||
3. Implement authentication and secure secret handling.
|
||||
4. Harden the REST proxy and add sanitized execution logging.
|
||||
5. Add backend, end-to-end, and security regression tests.
|
||||
6. Reconcile and consolidate project documentation.
|
||||
|
||||
See `TASKS.md` for the complete actionable checklist.
|
||||
|
||||
@ -178,9 +174,10 @@ After material work:
|
||||
|
||||
Date: 2026-07-18
|
||||
|
||||
- Reviewed the documentation and current source implementation.
|
||||
- Created `TASKS.md` with completed work and remaining MVP tasks.
|
||||
- Removed 99 Windows `:Zone.Identifier` sidecar files.
|
||||
- Initialized the fork as a new Git repository and pushed `main` to Gitea.
|
||||
- Created this cross-session state file.
|
||||
- Next recommended action: install dependencies and establish a clean validation baseline before implementing the next MVP feature.
|
||||
- Completed Slice 1 by implementing Text Area, Checkbox, Radio Group, Status Panel, and Container/Card across the palette, canvas, property editor, and Preview.
|
||||
- Added runtime value and response-binding support while keeping ephemeral values out of canonical JSON.
|
||||
- Scoped Container/Card to a flat presentational title/body contract for MVP; nesting remains post-MVP.
|
||||
- Added `valid-mvp-components.json` and focused renderer/binding tests.
|
||||
- Validation passes: 7 frontend suites / 433 tests, frontend production build, backend TypeScript build, and the new schema fixture.
|
||||
- User completed the full manual Slice 1 checklist successfully.
|
||||
- Next recommended action: begin `SLICE2.md`.
|
||||
|
||||
55
SLICE1.md
55
SLICE1.md
@ -2,7 +2,7 @@
|
||||
|
||||
## Status
|
||||
|
||||
Not started
|
||||
Complete
|
||||
|
||||
## Objective
|
||||
|
||||
@ -25,31 +25,31 @@ Implement every GUI component required by the MVP so each schema-supported compo
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Define shared acceptance criteria and test patterns for component implementation.
|
||||
- [ ] Implement Text Area end-to-end.
|
||||
- [ ] Implement Checkbox end-to-end.
|
||||
- [ ] Implement Radio Group end-to-end.
|
||||
- [ ] Implement Status/Message Panel end-to-end.
|
||||
- [ ] Define and implement the minimum MVP Container/Card behavior and nesting rules.
|
||||
- [ ] Add component-specific binding support and diagnostics.
|
||||
- [ ] Add or update example projects for all five components.
|
||||
- [ ] Remove the schema/runtime mismatch for supported component types.
|
||||
- [ ] Update `TASKS.md`, `CODEX.md`, and relevant documentation.
|
||||
- [x] Define shared acceptance criteria and test patterns for component implementation.
|
||||
- [x] Implement Text Area end-to-end.
|
||||
- [x] Implement Checkbox end-to-end.
|
||||
- [x] Implement Radio Group end-to-end.
|
||||
- [x] Implement Status/Message Panel end-to-end.
|
||||
- [x] Define and implement the minimum MVP Container/Card behavior and nesting rules.
|
||||
- [x] Add component-specific binding support and diagnostics.
|
||||
- [x] Add or update example projects for all five components.
|
||||
- [x] Remove the schema/runtime mismatch for supported component types.
|
||||
- [x] Update `TASKS.md`, `CODEX.md`, and relevant documentation.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] All eleven MVP types can be added through the palette and render on the canvas and in Preview.
|
||||
- [ ] Properties survive JSON editing and save/load round trips.
|
||||
- [ ] Interactive values remain in runtime state, not canonical JSON.
|
||||
- [ ] Invalid configurations produce useful diagnostics.
|
||||
- [ ] Existing components and bindings continue to work.
|
||||
- [x] All eleven MVP types can be added through the palette and render on the canvas and in Preview.
|
||||
- [x] Properties use the canonical document and therefore survive JSON editing and persistence round trips.
|
||||
- [x] Interactive values remain in runtime state, not canonical JSON.
|
||||
- [x] Invalid option and binding configurations produce useful diagnostics.
|
||||
- [x] Existing components and bindings continue to work.
|
||||
|
||||
## Validation
|
||||
|
||||
- [ ] Frontend TypeScript check and production build pass.
|
||||
- [ ] Component and renderer tests pass.
|
||||
- [ ] Schema examples validate as expected.
|
||||
- [ ] Manual canvas, Preview, and save/load checks pass for each component.
|
||||
- [x] Frontend TypeScript check and production build pass.
|
||||
- [x] Component and renderer tests pass.
|
||||
- [x] Schema examples validate as expected.
|
||||
- [x] Manual canvas, Preview, JSON synchronization, save/load, and regression checks pass for all components.
|
||||
|
||||
## Risks and Open Questions
|
||||
|
||||
@ -59,10 +59,17 @@ Implement every GUI component required by the MVP so each schema-supported compo
|
||||
|
||||
## Progress Log
|
||||
|
||||
No work recorded yet.
|
||||
- Added Text Area, Checkbox, Radio Group, Status Panel, and Container/Card to the shared type model, palette, canvas, defaults, property editor, and Preview.
|
||||
- Kept all interactive input values in ephemeral Preview runtime state.
|
||||
- Added response-mapped `.value` support for Text Area, Checkbox, Radio Group, and Status Panel.
|
||||
- Defined Container/Card as a flat presentational card with title and body; nested component ownership is deferred beyond MVP.
|
||||
- Added a representative schema fixture covering all five components.
|
||||
- Added focused renderer and binding regression tests.
|
||||
- Validation passed: 7 suites / 433 tests, frontend production build, backend TypeScript build, and the new schema fixture.
|
||||
- User manual acceptance passed: startup, all five new component controls, Preview behavior, JSON synchronization, save/load persistence, and existing-component regression checks.
|
||||
|
||||
## Handoff
|
||||
|
||||
- Last completed: Slice plan created.
|
||||
- Next action: Define component contracts and implement Text Area as the reference pattern.
|
||||
- Known blockers: Container/Card nesting behavior requires a scoped decision.
|
||||
- Last completed: All Slice 1 GUI components and validation.
|
||||
- Next action: Begin Slice 2 visual REST action, event, and binding configuration.
|
||||
- Known blockers: None for Slice 1.
|
||||
|
||||
12
TASKS.md
12
TASKS.md
@ -61,12 +61,12 @@ This document summarizes the current implementation and the remaining work requi
|
||||
|
||||
### Missing MVP GUI components
|
||||
|
||||
- [ ] Implement the Text Area component in the palette, canvas, property editor, Preview runtime, and tests.
|
||||
- [ ] Implement the Checkbox component in the palette, canvas, property editor, Preview runtime, and tests.
|
||||
- [ ] Implement the Radio Group component in the palette, canvas, property editor, Preview runtime, and tests.
|
||||
- [ ] Implement the Status/Message Panel component in the palette, canvas, property editor, Preview runtime, response bindings, and tests.
|
||||
- [ ] Implement the Container/Card component in the palette, canvas, property editor, Preview runtime, and tests.
|
||||
- [ ] Add a clear validation diagnostic until every schema-supported component type is supported by both the Visual Editor and Preview runtime.
|
||||
- [x] Implement the Text Area component in the palette, canvas, property editor, Preview runtime, and tests.
|
||||
- [x] Implement the Checkbox component in the palette, canvas, property editor, Preview runtime, and tests.
|
||||
- [x] Implement the Radio Group component in the palette, canvas, property editor, Preview runtime, and tests.
|
||||
- [x] Implement the Status/Message Panel component in the palette, canvas, property editor, Preview runtime, response bindings, and tests.
|
||||
- [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
|
||||
|
||||
|
||||
51
examples/project-definitions/valid-mvp-components.json
Normal file
51
examples/project-definitions/valid-mvp-components.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"$schema": "../../shared/schemas/conductor-project.schema.json",
|
||||
"schemaVersion": "0.1.0",
|
||||
"project": {
|
||||
"id": "proj_mvp_components",
|
||||
"name": "MVP Components",
|
||||
"description": "Representative configuration for the five Slice 1 components.",
|
||||
"pages": [{
|
||||
"id": "page_main",
|
||||
"name": "Main",
|
||||
"order": 0,
|
||||
"components": [
|
||||
{
|
||||
"id": "notes", "type": "TextArea", "name": "notes",
|
||||
"position": { "x": 24, "y": 24 }, "size": { "width": 320, "height": 120 },
|
||||
"properties": { "label": "Notes", "placeholder": "Enter notes", "defaultValue": "", "required": true, "visible": true, "disabled": false },
|
||||
"events": []
|
||||
},
|
||||
{
|
||||
"id": "enabled", "type": "Checkbox", "name": "enabled",
|
||||
"position": { "x": 24, "y": 168 }, "size": { "width": 220, "height": 40 },
|
||||
"properties": { "label": "Enabled", "defaultValue": true, "visible": true, "disabled": false },
|
||||
"events": []
|
||||
},
|
||||
{
|
||||
"id": "region", "type": "RadioGroup", "name": "region",
|
||||
"position": { "x": 24, "y": 232 }, "size": { "width": 280, "height": 120 },
|
||||
"properties": { "label": "Region", "options": [{ "label": "East", "value": "east" }, { "label": "West", "value": "west" }], "value": "east", "visible": true, "disabled": false },
|
||||
"events": []
|
||||
},
|
||||
{
|
||||
"id": "status", "type": "StatusPanel", "name": "status",
|
||||
"position": { "x": 376, "y": 24 }, "size": { "width": 360, "height": 96 },
|
||||
"properties": { "label": "Status", "defaultValue": "Ready", "status": "success", "visible": true, "disabled": false },
|
||||
"events": []
|
||||
},
|
||||
{
|
||||
"id": "summary", "type": "Container", "name": "summary",
|
||||
"position": { "x": 376, "y": 144 }, "size": { "width": 400, "height": 220 },
|
||||
"properties": { "label": "Summary", "defaultValue": "Card content", "visible": true, "disabled": false },
|
||||
"events": []
|
||||
}
|
||||
],
|
||||
"events": []
|
||||
}],
|
||||
"actions": [],
|
||||
"bindings": [],
|
||||
"variables": {},
|
||||
"settings": {}
|
||||
}
|
||||
}
|
||||
@ -86,6 +86,7 @@ function Preview(): React.ReactElement {
|
||||
onTextInputChange={runtime.handleTextInputChange}
|
||||
onDropdownChange={runtime.handleDropdownChange}
|
||||
onTableRowSelect={runtime.handleTableRowSelect}
|
||||
onValueChange={runtime.handleValueChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -324,6 +324,17 @@
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.textArea { width: 100%; min-height: 84px; box-sizing: border-box; resize: vertical; }
|
||||
.choiceRow { display: flex; align-items: center; gap: 8px; width: 100%; font-size: 13px; }
|
||||
.radioGroup { display: flex; flex-direction: column; gap: 6px; width: 100%; margin: 0; padding: 0; border: 0; }
|
||||
.radioGroup legend { margin-bottom: 6px; font-size: 12px; font-weight: 600; }
|
||||
.statusPanel { width: 100%; height: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #93c5fd; border-radius: 6px; background: #eff6ff; color: #1e3a8a; overflow: auto; }
|
||||
.statusPanel[data-status='success'] { background: #ecfdf5; border-color: #6ee7b7; color: #065f46; }
|
||||
.statusPanel[data-status='warning'] { background: #fffbeb; border-color: #fcd34d; color: #92400e; }
|
||||
.statusPanel[data-status='error'] { background: #fef2f2; border-color: #fca5a5; color: #991b1b; }
|
||||
.card { width: 100%; height: 100%; box-sizing: border-box; padding: 16px; border: 1px solid #d0d7de; border-radius: 8px; background: #fff; box-shadow: 0 1px 3px rgba(31, 35, 40, 0.12); overflow: auto; }
|
||||
.cardTitle { margin-bottom: 8px; font-weight: 600; }
|
||||
.cardBody { color: #57606a; white-space: pre-wrap; }
|
||||
.tableTd:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
@ -81,6 +81,87 @@ function TextInputRenderer({
|
||||
);
|
||||
}
|
||||
|
||||
function TextAreaRenderer({ label, placeholder, value, disabled, onChange }: {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.inputWrapper}>
|
||||
{label && <label className={styles.inputLabel}>{label}</label>}
|
||||
<textarea
|
||||
className={styles.textArea}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckboxRenderer({ label, checked, disabled, onChange }: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
disabled: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<label className={styles.choiceRow}>
|
||||
<input type="checkbox" checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} />
|
||||
<span>{label || 'Checkbox'}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupRenderer({ label, options, value, disabled, onChange }: {
|
||||
label: string;
|
||||
options: DropdownOption[];
|
||||
value: string;
|
||||
disabled: boolean;
|
||||
onChange: (value: string) => void;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<fieldset className={styles.radioGroup} disabled={disabled}>
|
||||
{label && <legend>{label}</legend>}
|
||||
{options.map((option) => (
|
||||
<label key={option.value} className={styles.choiceRow}>
|
||||
<input type="radio" name={label} value={option.value} checked={value === option.value} onChange={() => onChange(option.value)} />
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusPanelRenderer({ label, value, status, loading, error }: {
|
||||
label: string;
|
||||
value: unknown;
|
||||
status: string;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
}): React.ReactElement {
|
||||
const message = error ?? (loading ? 'Loading…' : labelDisplayValue(value));
|
||||
const tone = error ? 'error' : status;
|
||||
return (
|
||||
<div className={styles.statusPanel} data-status={tone} role={tone === 'error' ? 'alert' : 'status'}>
|
||||
{label && <strong>{label}</strong>}
|
||||
<span>{message || 'Ready'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContainerRenderer({ label, body }: { label: string; body: string }): React.ReactElement {
|
||||
return (
|
||||
<section className={styles.card}>
|
||||
{label && <div className={styles.cardTitle}>{label}</div>}
|
||||
<div className={styles.cardBody}>{body || 'Card content'}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Dropdown renderer ─────────────────────────────────────────────────────────
|
||||
|
||||
type DropdownRendererProps = {
|
||||
@ -290,12 +371,14 @@ type PreviewComponentProps = {
|
||||
onButtonClick: (componentId: string) => void;
|
||||
onTextInputChange: (componentId: string, value: string) => void;
|
||||
onDropdownChange: (componentId: string, value: string) => void;
|
||||
onValueChange: (componentId: string, value: unknown) => void;
|
||||
onTableRowSelect: (componentId: string, index: number, row: Record<string, unknown>) => void;
|
||||
};
|
||||
|
||||
function PreviewComponent({
|
||||
component,
|
||||
runtimeState,
|
||||
onValueChange,
|
||||
isLoading,
|
||||
onButtonClick,
|
||||
onTextInputChange,
|
||||
@ -319,7 +402,7 @@ function PreviewComponent({
|
||||
left: position.x,
|
||||
top: position.y,
|
||||
width: size.width,
|
||||
...(type === 'TextInput' || type === 'JsonViewer' || type === 'Dropdown' || type === 'Table'
|
||||
...(type === 'TextInput' || type === 'TextArea' || type === 'JsonViewer' || type === 'Dropdown' || type === 'Table'
|
||||
? { minHeight: size.height }
|
||||
: { height: size.height }),
|
||||
}}
|
||||
@ -354,6 +437,52 @@ function PreviewComponent({
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'TextArea' && (
|
||||
<TextAreaRenderer
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
value={runtimeState?.textValue ?? (runtimeState?.value !== undefined ? labelDisplayValue(runtimeState.value) : defaultValue)}
|
||||
disabled={disabled}
|
||||
onChange={(val) => onTextInputChange(component.id, val)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'Checkbox' && (
|
||||
<CheckboxRenderer
|
||||
label={label}
|
||||
checked={runtimeState?.value !== undefined ? Boolean(runtimeState.value) : properties.defaultValue === true}
|
||||
disabled={disabled}
|
||||
onChange={(val) => onValueChange(component.id, val)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'RadioGroup' && (
|
||||
<RadioGroupRenderer
|
||||
label={label}
|
||||
options={Array.isArray(properties.options) ? (properties.options as DropdownOption[]) : []}
|
||||
value={runtimeState?.value !== undefined ? String(runtimeState.value) : (typeof properties.value === 'string' ? properties.value : '')}
|
||||
disabled={disabled}
|
||||
onChange={(val) => onValueChange(component.id, val)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'StatusPanel' && (
|
||||
<StatusPanelRenderer
|
||||
label={label}
|
||||
value={runtimeState?.value !== undefined ? runtimeState.value : properties.defaultValue}
|
||||
status={typeof properties.status === 'string' ? properties.status : 'info'}
|
||||
loading={runtimeState?.loading}
|
||||
error={runtimeState?.error}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'Container' && (
|
||||
<ContainerRenderer
|
||||
label={label}
|
||||
body={typeof properties.defaultValue === 'string' ? properties.defaultValue : ''}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === 'Table' && (() => {
|
||||
const rawColumns = properties.columns;
|
||||
const configuredColumns: TableColumn[] = Array.isArray(rawColumns)
|
||||
|
||||
@ -269,6 +269,11 @@ describe('SUPPORTED_TARGET_TYPES', () => {
|
||||
expect(SUPPORTED_TARGET_TYPES.has('Label')).toBe(true);
|
||||
});
|
||||
|
||||
test.each(['TextArea', 'Checkbox', 'RadioGroup', 'StatusPanel'] as const)(
|
||||
'%s supports response-mapped values',
|
||||
(type) => expect(SUPPORTED_TARGET_TYPES.has(type)).toBe(true),
|
||||
);
|
||||
|
||||
test('Dropdown is NOT in SUPPORTED_TARGET_TYPES (uses .options not .value)', () => {
|
||||
expect(SUPPORTED_TARGET_TYPES.has('Dropdown')).toBe(false);
|
||||
});
|
||||
@ -297,6 +302,11 @@ describe('isTargetPropertySupported', () => {
|
||||
expect(isTargetPropertySupported('Dropdown', 'options')).toBe(true);
|
||||
});
|
||||
|
||||
test.each(['TextArea', 'Checkbox', 'RadioGroup', 'StatusPanel'] as const)(
|
||||
'%s.value → true',
|
||||
(type) => expect(isTargetPropertySupported(type, 'value')).toBe(true),
|
||||
);
|
||||
|
||||
test('Dropdown.value → false', () => {
|
||||
expect(isTargetPropertySupported('Dropdown', 'value')).toBe(false);
|
||||
});
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
*
|
||||
* ── Supported target grammar ──────────────────────────────────────────────────
|
||||
*
|
||||
* components.<componentName>.value → JsonViewer, Label
|
||||
* components.<componentName>.value → JsonViewer, Label, TextArea, Checkbox, RadioGroup, StatusPanel
|
||||
* components.<componentName>.options → Dropdown only
|
||||
* components.<componentName>.rows → Table only (Step 17.5)
|
||||
* variables.<variableName> → runtime variable (Step 18.1)
|
||||
@ -60,13 +60,20 @@ export type ActionRuntimeStateMap = Record<string, ActionRuntimeState>;
|
||||
* Scoped to the `.value` property only.
|
||||
* Dropdown is intentionally excluded — it uses `.options` instead.
|
||||
*/
|
||||
export const SUPPORTED_TARGET_TYPES = new Set(['JsonViewer', 'Label']);
|
||||
export const SUPPORTED_TARGET_TYPES = new Set([
|
||||
'JsonViewer',
|
||||
'Label',
|
||||
'TextArea',
|
||||
'Checkbox',
|
||||
'RadioGroup',
|
||||
'StatusPanel',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Property-aware target capability check.
|
||||
*
|
||||
* Returns true when the given component type supports the given target property:
|
||||
* .value → JsonViewer, Label
|
||||
* .value → JsonViewer, Label, TextArea, Checkbox, RadioGroup, StatusPanel
|
||||
* .options → Dropdown
|
||||
* .rows → Table
|
||||
*
|
||||
|
||||
67
frontend/src/components/Preview/mvpComponents.test.tsx
Normal file
67
frontend/src/components/Preview/mvpComponents.test.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import type { CanvasComponent, ComponentType } from '../../types/project';
|
||||
import PreviewComponent from './PreviewComponent';
|
||||
import type { ComponentRuntimeState } from './usePreviewRuntime';
|
||||
|
||||
function render(type: ComponentType, properties: CanvasComponent['properties'], runtimeState?: ComponentRuntimeState): string {
|
||||
const component: CanvasComponent = {
|
||||
id: `test-${type}`,
|
||||
name: `test${type}`,
|
||||
type,
|
||||
position: { x: 0, y: 0 },
|
||||
size: { width: 320, height: 120 },
|
||||
properties: { visible: true, disabled: false, ...properties },
|
||||
};
|
||||
|
||||
return renderToStaticMarkup(
|
||||
<PreviewComponent
|
||||
component={component}
|
||||
isLoading={false}
|
||||
runtimeState={runtimeState}
|
||||
onButtonClick={() => undefined}
|
||||
onTextInputChange={() => undefined}
|
||||
onDropdownChange={() => undefined}
|
||||
onTableRowSelect={() => undefined}
|
||||
onValueChange={() => undefined}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('remaining MVP preview components', () => {
|
||||
test('renders a TextArea with its configured value', () => {
|
||||
expect(render('TextArea', { label: 'Notes', defaultValue: 'Initial notes' }))
|
||||
.toContain('Initial notes');
|
||||
expect(render('TextArea', { label: 'Notes', defaultValue: '' }, { value: 'Mapped notes' }))
|
||||
.toContain('Mapped notes');
|
||||
});
|
||||
|
||||
|
||||
test('renders a checked Checkbox', () => {
|
||||
const markup = render('Checkbox', { label: 'Enabled', defaultValue: true });
|
||||
expect(markup).toContain('type="checkbox"');
|
||||
expect(markup).toContain('checked=""');
|
||||
});
|
||||
|
||||
test('renders RadioGroup options and selection', () => {
|
||||
const markup = render('RadioGroup', {
|
||||
label: 'Region',
|
||||
options: [{ label: 'East', value: 'east' }, { label: 'West', value: 'west' }],
|
||||
value: 'west',
|
||||
});
|
||||
expect(markup).toContain('East');
|
||||
expect(markup).toContain('checked="" value="west"');
|
||||
});
|
||||
|
||||
test('renders StatusPanel with its configured tone and message', () => {
|
||||
const markup = render('StatusPanel', { label: 'State', defaultValue: 'Ready', status: 'success' });
|
||||
expect(markup).toContain('data-status="success"');
|
||||
expect(markup).toContain('Ready');
|
||||
});
|
||||
|
||||
test('renders Container as a titled card', () => {
|
||||
const markup = render('Container', { label: 'Summary', defaultValue: 'Card body' });
|
||||
expect(markup).toContain('<section');
|
||||
expect(markup).toContain('Card body');
|
||||
});
|
||||
});
|
||||
@ -50,7 +50,7 @@
|
||||
* 4. Writes resolved values to componentState[targetId].value
|
||||
*
|
||||
* Supported source paths: See docs/response-mapping-model.md §4
|
||||
* Supported target paths: components.<name>.value (JsonViewer, Label)
|
||||
* Supported target paths: components.<name>.value (display and input components)
|
||||
* variables.<name> (Step 18.1)
|
||||
* Supported triggers: onSuccess (canonical), onClick (legacy compat)
|
||||
*
|
||||
@ -162,6 +162,12 @@ export type PreviewRuntime = {
|
||||
*/
|
||||
handleTextInputChange: (componentId: string, value: string) => void;
|
||||
|
||||
/**
|
||||
* Stores a generic component value for Checkbox and RadioGroup inputs.
|
||||
* Runtime state remains separate from canonical project JSON.
|
||||
*/
|
||||
handleValueChange: (componentId: string, value: unknown) => void;
|
||||
|
||||
/**
|
||||
* Call this when a Dropdown selection changes in Preview Mode.
|
||||
* Stores the selected option value in componentState.value so
|
||||
@ -345,7 +351,7 @@ function applyResponseBindings(
|
||||
console.warn(
|
||||
`[Preview] Binding "${binding.id}": target property "${parsedTarget.property}" is not ` +
|
||||
`supported for component type "${targetComp.type}". ` +
|
||||
`Supported: .value → JsonViewer, Label; .options → Dropdown; .rows → Table. Binding skipped.`,
|
||||
`Supported: .value → JsonViewer, Label, TextArea, Checkbox, RadioGroup, StatusPanel; .options → Dropdown; .rows → Table. Binding skipped.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@ -414,7 +420,7 @@ function applyResponseBindings(
|
||||
error: undefined,
|
||||
};
|
||||
} else {
|
||||
// Value target (JsonViewer, Label)
|
||||
// Value target (display and input components)
|
||||
componentUpdates[targetComp.id] = {
|
||||
...(componentUpdates[targetComp.id] ?? {}),
|
||||
value: sourceResult.value,
|
||||
@ -458,6 +464,13 @@ export function usePreviewRuntime(doc: ProjectDocument): PreviewRuntime {
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleValueChange = useCallback((componentId: string, value: unknown) => {
|
||||
setComponentState((prev) => ({
|
||||
...prev,
|
||||
[componentId]: { ...prev[componentId], value },
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleButtonClick = useCallback(
|
||||
(buttonId: string) => {
|
||||
const { pages, actions, bindings, variables } = doc.project;
|
||||
@ -821,5 +834,6 @@ export function usePreviewRuntime(doc: ProjectDocument): PreviewRuntime {
|
||||
handleTextInputChange,
|
||||
handleDropdownChange,
|
||||
handleTableRowSelect,
|
||||
handleValueChange,
|
||||
};
|
||||
}
|
||||
|
||||
@ -218,6 +218,17 @@
|
||||
color: #57606a;
|
||||
}
|
||||
|
||||
.textAreaInner { width: 100%; min-height: 72px; resize: none; box-sizing: border-box; pointer-events: none; }
|
||||
.choiceRow { display: flex; align-items: center; gap: 8px; width: 100%; font-size: 13px; }
|
||||
.radioGroup { display: flex; flex-direction: column; gap: 6px; width: 100%; margin: 0; padding: 0; border: 0; }
|
||||
.radioGroup legend { margin-bottom: 6px; font-size: 12px; font-weight: 600; }
|
||||
.statusPanel { width: 100%; height: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #93c5fd; border-radius: 6px; background: #eff6ff; color: #1e3a8a; overflow: auto; }
|
||||
.statusPanel[data-status='success'] { background: #ecfdf5; border-color: #6ee7b7; color: #065f46; }
|
||||
.statusPanel[data-status='warning'] { background: #fffbeb; border-color: #fcd34d; color: #92400e; }
|
||||
.statusPanel[data-status='error'] { background: #fef2f2; border-color: #fca5a5; color: #991b1b; }
|
||||
.card { width: 100%; height: 100%; box-sizing: border-box; padding: 16px; border: 1px solid #d0d7de; border-radius: 8px; background: #fff; box-shadow: 0 1px 3px rgba(31, 35, 40, 0.12); overflow: auto; }
|
||||
.cardTitle { margin-bottom: 8px; font-weight: 600; }
|
||||
.cardBody { color: #57606a; white-space: pre-wrap; }
|
||||
.tableTd {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 3px 8px;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useRef } from 'react';
|
||||
import type { CanvasComponent as CanvasComponentType, TableColumn, TableRow } from '../../../types/project';
|
||||
import type { CanvasComponent as CanvasComponentType, DropdownOption, TableColumn, TableRow } from '../../../types/project';
|
||||
import styles from './CanvasComponent.module.css';
|
||||
|
||||
// ── Props ─────────────────────────────────────────────────────────────────────
|
||||
@ -64,6 +64,56 @@ function DropdownRenderer({
|
||||
);
|
||||
}
|
||||
|
||||
function TextAreaRenderer({ label, placeholder }: { label: string; placeholder?: string }): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.inputWrapper}>
|
||||
{label && <label className={styles.inputLabel}>{label}</label>}
|
||||
<textarea className={styles.textAreaInner} placeholder={placeholder || 'Enter multiple lines…'} readOnly tabIndex={-1} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckboxRenderer({ label, checked }: { label: string; checked: boolean }): React.ReactElement {
|
||||
return (
|
||||
<label className={styles.choiceRow}>
|
||||
<input type="checkbox" checked={checked} readOnly tabIndex={-1} />
|
||||
<span>{label || 'Checkbox'}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupRenderer({ label, options, value }: { label: string; options: DropdownOption[]; value: string }): React.ReactElement {
|
||||
return (
|
||||
<fieldset className={styles.radioGroup}>
|
||||
{label && <legend>{label}</legend>}
|
||||
{(options.length > 0 ? options : [{ label: 'Option', value: 'option' }]).map((option) => (
|
||||
<label key={option.value} className={styles.choiceRow}>
|
||||
<input type="radio" checked={option.value === value} readOnly tabIndex={-1} />
|
||||
<span>{option.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusPanelRenderer({ label, message, status }: { label: string; message: string; status: string }): React.ReactElement {
|
||||
return (
|
||||
<div className={styles.statusPanel} data-status={status}>
|
||||
{label && <strong>{label}</strong>}
|
||||
<span>{message || 'Ready'}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContainerRenderer({ label, body }: { label: string; body: string }): React.ReactElement {
|
||||
return (
|
||||
<section className={styles.card}>
|
||||
{label && <div className={styles.cardTitle}>{label}</div>}
|
||||
<div className={styles.cardBody}>{body || 'Card content'}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRenderer({
|
||||
label,
|
||||
columns,
|
||||
@ -239,6 +289,30 @@ function CanvasComponent({
|
||||
{type === 'TextInput' && <TextInputRenderer label={label} placeholder={placeholder} />}
|
||||
{type === 'JsonViewer' && <JsonViewerRenderer />}
|
||||
{type === 'Dropdown' && <DropdownRenderer label={label} placeholder={placeholder} />}
|
||||
{type === 'TextArea' && <TextAreaRenderer label={label} placeholder={placeholder} />}
|
||||
{type === 'Checkbox' && (
|
||||
<CheckboxRenderer label={label} checked={properties.defaultValue === true} />
|
||||
)}
|
||||
{type === 'RadioGroup' && (
|
||||
<RadioGroupRenderer
|
||||
label={label}
|
||||
options={Array.isArray(properties.options) ? (properties.options as DropdownOption[]) : []}
|
||||
value={typeof properties.value === 'string' ? properties.value : ''}
|
||||
/>
|
||||
)}
|
||||
{type === 'StatusPanel' && (
|
||||
<StatusPanelRenderer
|
||||
label={label}
|
||||
message={typeof properties.defaultValue === 'string' ? properties.defaultValue : ''}
|
||||
status={typeof properties.status === 'string' ? properties.status : 'info'}
|
||||
/>
|
||||
)}
|
||||
{type === 'Container' && (
|
||||
<ContainerRenderer
|
||||
label={label}
|
||||
body={typeof properties.defaultValue === 'string' ? properties.defaultValue : ''}
|
||||
/>
|
||||
)}
|
||||
{type === 'Table' && (
|
||||
<TableRenderer
|
||||
label={label}
|
||||
|
||||
@ -15,8 +15,13 @@ const PALETTE_ITEMS: PaletteItem[] = [
|
||||
{ type: 'Button', label: 'Button', description: 'Clickable action' },
|
||||
{ type: 'TextInput', label: 'Text Input', description: 'Single-line input' },
|
||||
{ type: 'JsonViewer', label: 'JSON Viewer', description: 'Formatted JSON' },
|
||||
{ type: 'TextArea', label: 'Text Area', description: 'Multi-line input' },
|
||||
{ type: 'Checkbox', label: 'Checkbox', description: 'Boolean input' },
|
||||
{ type: 'RadioGroup', label: 'Radio Group', description: 'Single choice' },
|
||||
{ type: 'Dropdown', label: 'Dropdown', description: 'Option selector' },
|
||||
{ type: 'Table', label: 'Table', description: 'Data table' },
|
||||
{ type: 'StatusPanel',label: 'Status Panel',description: 'Status message' },
|
||||
{ type: 'Container', label: 'Card', description: 'Content container' },
|
||||
];
|
||||
|
||||
// ── Props ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -515,6 +515,20 @@ function VisualEditor(): React.ReactElement {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{['TextInput', 'TextArea', 'Checkbox', 'RadioGroup'].includes(selectedComponent.type) && (
|
||||
<div className={styles.infoRow}>
|
||||
<label className={styles.infoKey} htmlFor="prop-required">Required</label>
|
||||
<input
|
||||
id="prop-required"
|
||||
type="checkbox"
|
||||
checked={selectedComponent.properties.required === true}
|
||||
onChange={(e) =>
|
||||
updateComponentProperty(selectedComponent.id, 'required', e.target.checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedComponent.properties.label !== undefined && (
|
||||
<div className={styles.infoRow}>
|
||||
<label className={styles.infoKey} htmlFor="prop-label">Label</label>
|
||||
@ -543,7 +557,7 @@ function VisualEditor(): React.ReactElement {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedComponent.type === 'Dropdown' && (
|
||||
{(selectedComponent.type === 'Dropdown' || selectedComponent.type === 'RadioGroup') && (
|
||||
<DropdownOptionsEditor
|
||||
componentId={selectedComponent.id}
|
||||
options={
|
||||
@ -564,6 +578,52 @@ function VisualEditor(): React.ReactElement {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{selectedComponent.type === 'Checkbox' && (
|
||||
<div className={styles.infoRow}>
|
||||
<label className={styles.infoKey} htmlFor="prop-checked">Default checked</label>
|
||||
<input
|
||||
id="prop-checked"
|
||||
type="checkbox"
|
||||
checked={selectedComponent.properties.defaultValue === true}
|
||||
onChange={(e) =>
|
||||
updateComponentProperty(selectedComponent.id, 'defaultValue', e.target.checked)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{['TextInput', 'TextArea', 'StatusPanel', 'Container'].includes(selectedComponent.type) && (
|
||||
<div className={styles.infoRow}>
|
||||
<label className={styles.infoKey} htmlFor="prop-default-value">
|
||||
{selectedComponent.type === 'Container' ? 'Body' : selectedComponent.type === 'StatusPanel' ? 'Message' : 'Default'}
|
||||
</label>
|
||||
<textarea
|
||||
id="prop-default-value"
|
||||
className={styles.infoInput}
|
||||
value={typeof selectedComponent.properties.defaultValue === 'string' ? selectedComponent.properties.defaultValue : ''}
|
||||
onChange={(e) =>
|
||||
updateComponentProperty(selectedComponent.id, 'defaultValue', e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{selectedComponent.type === 'StatusPanel' && (
|
||||
<div className={styles.infoRow}>
|
||||
<label className={styles.infoKey} htmlFor="prop-status">Tone</label>
|
||||
<select
|
||||
id="prop-status"
|
||||
className={styles.infoInput}
|
||||
value={typeof selectedComponent.properties.status === 'string' ? selectedComponent.properties.status : 'info'}
|
||||
onChange={(e) =>
|
||||
updateComponentProperty(selectedComponent.id, 'status', e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="info">Info</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{selectedComponent.type === 'Table' && (
|
||||
<>
|
||||
<TableColumnsEditor
|
||||
|
||||
@ -13,8 +13,13 @@ export const DEFAULT_SIZES: Record<ComponentType, { width: number; height: numbe
|
||||
Button: { width: 120, height: 40 },
|
||||
TextInput: { width: 240, height: 40 },
|
||||
JsonViewer: { width: 400, height: 200 },
|
||||
TextArea: { width: 320, height: 120 },
|
||||
Checkbox: { width: 220, height: 40 },
|
||||
RadioGroup: { width: 280, height: 120 },
|
||||
Dropdown: { width: 260, height: 72 },
|
||||
Table: { width: 520, height: 260 },
|
||||
StatusPanel:{ width: 360, height: 96 },
|
||||
Container: { width: 400, height: 220 },
|
||||
};
|
||||
|
||||
// ── Default label text per component type ─────────────────────────────────────
|
||||
@ -24,8 +29,13 @@ const DEFAULT_LABELS: Record<ComponentType, string> = {
|
||||
Button: 'Button',
|
||||
TextInput: '',
|
||||
JsonViewer: '',
|
||||
TextArea: '',
|
||||
Checkbox: 'Checkbox',
|
||||
RadioGroup: 'Radio Group',
|
||||
Dropdown: 'Dropdown',
|
||||
Table: 'Table',
|
||||
StatusPanel:'Status',
|
||||
Container: 'Card',
|
||||
};
|
||||
|
||||
// ── ID counter (session-stable, not persistent) ───────────────────────────────
|
||||
@ -83,7 +93,20 @@ export function useCanvasActions(
|
||||
const label = DEFAULT_LABELS[type];
|
||||
|
||||
const properties: CanvasComponent['properties'] = { label, visible: true, disabled: false };
|
||||
if (type === 'TextInput') properties.placeholder = 'Enter text…';
|
||||
if (type === 'TextInput' || type === 'TextArea') {
|
||||
properties.placeholder = type === 'TextArea' ? 'Enter multiple lines…' : 'Enter text…';
|
||||
properties.defaultValue = '';
|
||||
}
|
||||
if (type === 'Checkbox') properties.defaultValue = false;
|
||||
if (type === 'RadioGroup') {
|
||||
properties.options = [];
|
||||
properties.value = '';
|
||||
}
|
||||
if (type === 'StatusPanel') {
|
||||
properties.defaultValue = 'Ready';
|
||||
properties.status = 'info';
|
||||
}
|
||||
if (type === 'Container') properties.defaultValue = 'Card content';
|
||||
if (type === 'Dropdown') {
|
||||
properties.placeholder = 'Select an option';
|
||||
properties.options = [];
|
||||
|
||||
@ -112,20 +112,17 @@ export type ComponentType =
|
||||
| 'Label'
|
||||
| 'Button'
|
||||
| 'TextInput'
|
||||
| 'JsonViewer'
|
||||
| 'Dropdown'
|
||||
| 'Table';
|
||||
|
||||
// Schema-only component types not yet active in the runtime
|
||||
// (retained for future steps; not used by Visual Editor or Preview)
|
||||
export type FullComponentType =
|
||||
| ComponentType
|
||||
| 'TextArea'
|
||||
| 'Checkbox'
|
||||
| 'RadioGroup'
|
||||
| 'JsonViewer'
|
||||
| 'Dropdown'
|
||||
| 'Table'
|
||||
| 'StatusPanel'
|
||||
| 'Container';
|
||||
|
||||
/** All schema component types are active as of Slice 1. */
|
||||
export type FullComponentType = ComponentType;
|
||||
// ── Dropdown option ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@ -249,9 +246,10 @@ export type Size = { width: number; height: number };
|
||||
export type ComponentProperties = {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
defaultValue?: unknown;
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user