@@ -397,7 +393,7 @@ export const InputExpressionControl = ({
? { ...candidate, value: next }
: candidate),
})}
- sourceSuggestions={sourceSuggestions}
+ sourceOptions={sourceOptions}
state={entry.value}
/>
{!requiredField && (
@@ -474,7 +470,7 @@ export const InputExpressionControl = ({
idPrefix={`${controlId}-leaf`}
label={label}
onChange={onChange}
- sourceSuggestions={sourceSuggestions}
+ sourceOptions={sourceOptions}
state={state}
/>
);
diff --git a/web/apps/console/src/workspace/authoring/SelectedCapabilityInspector.test.tsx b/web/apps/console/src/workspace/authoring/SelectedCapabilityInspector.test.tsx
index 53bd39dd..b425398b 100644
--- a/web/apps/console/src/workspace/authoring/SelectedCapabilityInspector.test.tsx
+++ b/web/apps/console/src/workspace/authoring/SelectedCapabilityInspector.test.tsx
@@ -1,13 +1,78 @@
-import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { afterEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { CapabilityDetail } from "../domain/capability-models.js";
+import type { AuthoringContractInventory } from "../domain/authoring-contract-models.js";
import type { DraftWorkspace } from "../domain/draft-workspace-models.js";
import { SelectedCapabilityInspector } from "./SelectedCapabilityInspector.js";
+import { useAuthoringContract } from "./useAuthoringContract.js";
import type { DraftAuthoringController } from "./useDraftAuthoring.js";
+vi.mock("./useAuthoringContract.js", () => ({ useAuthoringContract: vi.fn() }));
+
+const mockedUseAuthoringContract = vi.mocked(useAuthoringContract);
+
afterEach(() => cleanup());
+const inventory: AuthoringContractInventory = {
+ workspaceId: "draft-report",
+ revision: 3,
+ selectedStepId: "read",
+ readableSources: [{
+ path: "input.title",
+ label: "Inventory title",
+ origin: "workflow_input",
+ schema: { type: "string" },
+ required: true,
+ availability: "available",
+ uses: ["step_input"],
+ }],
+ stepInputTargets: [{
+ path: "step_input.title",
+ label: "Inventory title target",
+ origin: "step_input",
+ schema: { type: "string" },
+ required: true,
+ availability: "available",
+ uses: ["step_input"],
+ }],
+ stepOutputSources: [{
+ path: "step_output.text",
+ label: "Inventory text",
+ origin: "step_output",
+ schema: { type: "string" },
+ required: false,
+ availability: "available",
+ uses: ["step_output_source"],
+ }],
+ stateTargets: [{
+ path: "state.existing",
+ label: "Inventory state",
+ origin: "workflow_state",
+ schema: { type: "string" },
+ required: false,
+ availability: "available",
+ uses: ["state_target"],
+ }],
+ workflowOutputTargets: [],
+ entrySteps: [],
+ workflowOutcomes: [],
+ warnings: [],
+};
+
+beforeEach(() => {
+ mockedUseAuthoringContract.mockReturnValue({
+ phase: "disconnected",
+ inventory: null,
+ message: null,
+ refresh: vi.fn(),
+ });
+});
+
+const customInput = (label: string) =>
+ within(screen.getByRole("region", { name: label }))
+ .getByRole("textbox", { name: `Custom ${label}` });
+
const detail: CapabilityDetail = {
kind: "node_spec",
name: "demo.read",
@@ -82,6 +147,42 @@ const controllerFor = (workspace: DraftWorkspace): DraftAuthoringController => (
});
describe("SelectedCapabilityInspector", () => {
+ it("threads inventory-backed pickers into the selected step editors", async () => {
+ const user = userEvent.setup();
+ mockedUseAuthoringContract.mockReturnValue({
+ phase: "ready",
+ inventory,
+ message: null,
+ refresh: vi.fn(),
+ });
+ const workspace = draft(
+ "read",
+ [{ target: "title", path: "input.title" }],
+ [{ source: "text", target: "state.existing" }],
+ );
+
+ render(
+
,
+ );
+
+ await user.click(screen.getByRole("tab", { name: "Inputs" }));
+ expect(screen.getByRole("button", { name: /Inventory title target/ })).toBeInTheDocument();
+ expect(screen.queryByRole("combobox", { name: "Source path for input row 1" })).toBeNull();
+
+ await user.click(screen.getByRole("tab", { name: "Outputs" }));
+ expect(screen.getByRole("button", { name: /Inventory text/ })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /Inventory state/ })).toBeInTheDocument();
+ });
+
it("composes setup, inputs, and outputs while preserving malformed rows and dispatching focused saves", async () => {
const user = userEvent.setup();
const workspace = draft(
@@ -252,7 +353,7 @@ describe("SelectedCapabilityInspector", () => {
await user.click(screen.getByRole("tab", { name: "Inputs" }));
expect(screen.getByRole("group", { name: "items" })).toBeInTheDocument();
expect(screen.getByRole("combobox", { name: "Value source for items item 1" })).toHaveValue("path");
- expect(screen.getByRole("combobox", { name: "Path for items item 1" })).toHaveValue("state.foo");
+ expect(customInput("Path for items item 1")).toHaveValue("state.foo");
expect(screen.getByRole("textbox", { name: "Items item" })).toHaveValue("wowcool");
await user.click(screen.getByRole("button", { name: "Save inputs" }));
@@ -322,7 +423,7 @@ describe("SelectedCapabilityInspector", () => {
stepId="read"
/>,
);
- expect(screen.getByRole("combobox", { name: "Path for items item 1" })).toHaveValue("state.bar");
+ expect(customInput("Path for items item 1")).toHaveValue("state.bar");
expect(screen.getByRole("textbox", { name: "Items item" })).toHaveValue("after");
});
@@ -353,10 +454,10 @@ describe("SelectedCapabilityInspector", () => {
});
await user.click(screen.getByRole("button", { name: "Save setup" }));
await user.click(screen.getByRole("tab", { name: "Inputs" }));
- await user.clear(screen.getByRole("combobox", { name: "Target for row 1" }));
+ await user.clear(customInput("Target for row 1"));
await user.click(screen.getByRole("button", { name: "Save inputs" }));
await user.click(screen.getByRole("tab", { name: "Outputs" }));
- await user.clear(screen.getByRole("combobox", { name: "Target for output row 1" }));
+ await user.clear(customInput("Target for output row 1"));
await user.click(screen.getByRole("button", { name: "Save outputs" }));
const diagnosticIds = [...document.querySelectorAll('[id$="-error"], [id$="-errors"]')]
@@ -383,7 +484,7 @@ describe("SelectedCapabilityInspector", () => {
);
await userEvent.setup().click(screen.getByRole("tab", { name: "Inputs" }));
- expect(screen.getByRole("combobox", { name: "Target for row 1" })).toHaveValue("title");
+ expect(customInput("Target for row 1")).toHaveValue("title");
rerender(
{
/>,
);
await userEvent.setup().click(screen.getByRole("tab", { name: "Inputs" }));
- expect(screen.getByRole("combobox", { name: "Target for row 1" })).toHaveValue("title");
+ expect(customInput("Target for row 1")).toHaveValue("title");
expect(screen.getByRole("textbox", { name: "Title" })).toHaveValue("Second");
});
diff --git a/web/apps/console/src/workspace/authoring/SelectedCapabilityInspector.tsx b/web/apps/console/src/workspace/authoring/SelectedCapabilityInspector.tsx
index 0b5ad336..4b8c7e92 100644
--- a/web/apps/console/src/workspace/authoring/SelectedCapabilityInspector.tsx
+++ b/web/apps/console/src/workspace/authoring/SelectedCapabilityInspector.tsx
@@ -5,12 +5,14 @@ import { CapabilitySetupForm } from "./CapabilitySetupForm.js";
import { StepInputBindingsForm } from "./StepInputBindingsForm.js";
import { StepOutputBindingsForm } from "./StepOutputBindingsForm.js";
import {
+ authoringOptionsForUse,
bindingDiagnosticsForStep,
outputBindingRows,
projectSelectedStepDataflow,
stepInputBindingRows,
} from "./selected-step-dataflow.js";
import type { DraftAuthoringController } from "./useDraftAuthoring.js";
+import { useAuthoringContract } from "./useAuthoringContract.js";
type InspectorTab = "setup" | "inputs" | "outputs";
@@ -120,6 +122,16 @@ export const SelectedCapabilityInspector = ({
if (target !== undefined) activateTab(target);
};
const rawStep = selectedStep(draft, stepId);
+ const authoringContract = useAuthoringContract({
+ workspaceId: draft.workspaceId,
+ revision: draft.revision,
+ selectedStepId: stepId,
+ });
+ const inventory = authoringContract.inventory;
+ const inputSourceOptions = authoringOptionsForUse(inventory?.readableSources ?? [], "step_input");
+ const inputTargetOptions = authoringOptionsForUse(inventory?.stepInputTargets ?? [], "step_input");
+ const outputSourceOptions = authoringOptionsForUse(inventory?.stepOutputSources ?? [], "step_output_source");
+ const outputTargetOptions = authoringOptionsForUse(inventory?.stateTargets ?? [], "state_target");
const projected = projectSelectedStepDataflow(draft, stepId);
const preservedForm = controller.preservedCapabilityForm?.kind === "update" &&
controller.preservedCapabilityForm.input.stepId === stepId
@@ -202,8 +214,8 @@ export const SelectedCapabilityInspector = ({
canonicalVersion={`${stepId}:${controller.resetGeneration}`}
initialRows={inputRows}
inputSchema={capabilityDetail.inputSchema}
- workflowInputSchema={isRecord(draft.draft) ? draft.draft.input_schema : undefined}
- workflowStateSchema={isRecord(draft.draft) ? draft.draft.state_schema : undefined}
+ sourceOptions={inputSourceOptions}
+ targetOptions={inputTargetOptions}
onDirtyChange={controller.markDirty}
onSubmit={controller.setStepInputs}
rowDiagnostics={inputDiagnostics.rowIssues}
@@ -216,6 +228,8 @@ export const SelectedCapabilityInspector = ({
onDirtyChange={controller.markDirty}
onSubmit={controller.setStepOutputs}
outputSchema={capabilityDetail.outputSchema}
+ sourceOptions={outputSourceOptions}
+ targetOptions={outputTargetOptions}
rowDiagnostics={outputDiagnostics.rowIssues}
stateSchema={(isRecord(draft.draft) ? draft.draft.state_schema : null) ?? emptyStateSchema}
/>
diff --git a/web/apps/console/src/workspace/authoring/StepInputBindingsForm.test.tsx b/web/apps/console/src/workspace/authoring/StepInputBindingsForm.test.tsx
index 221231cf..3a1f005c 100644
--- a/web/apps/console/src/workspace/authoring/StepInputBindingsForm.test.tsx
+++ b/web/apps/console/src/workspace/authoring/StepInputBindingsForm.test.tsx
@@ -1,6 +1,7 @@
-import { cleanup, render, screen } from "@testing-library/react";
+import { cleanup, render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
+import type { AuthoringPathOption } from "../domain/authoring-contract-models.js";
import type { StepInputBinding } from "../domain/draft-workspace-models.js";
import { StepInputBindingsForm } from "./StepInputBindingsForm.js";
import { displayGraphInputPath, displayLocalInputPath } from "./input-binding-paths.js";
@@ -19,7 +20,71 @@ const schema = {
},
};
+const authoringOption = (
+ path: string,
+ label: string,
+ origin: AuthoringPathOption["origin"],
+ uses: AuthoringPathOption["uses"],
+): AuthoringPathOption => ({
+ path,
+ label,
+ origin,
+ schema: { type: "string" },
+ required: false,
+ availability: "available",
+ uses,
+});
+
+const customInput = (label: string) =>
+ within(screen.getByRole("region", { name: label }))
+ .getByRole("textbox", { name: `Custom ${label}` });
+
+const editableCustomInput = async (
+ user: ReturnType,
+ label: string,
+) => {
+ const picker = screen.getByRole("region", { name: label });
+ const details = picker.querySelector("details");
+ if (!(details instanceof HTMLDetailsElement) || !details.open) {
+ await user.click(within(picker).getByText("Advanced"));
+ }
+ return within(picker).getByRole("textbox", { name: `Custom ${label}` });
+};
+
describe("StepInputBindingsForm", () => {
+ it("uses inventory pickers for sources and targets while preserving canonical bindings", async () => {
+ const user = userEvent.setup();
+ const submissions: ReadonlyArray[] = [];
+ const sourceOptions = [
+ authoringOption("input.request.id", "Request id", "workflow_input", ["step_input"]),
+ authoringOption("context.loop_item", "Loop item", "runtime_context", ["step_input"]),
+ ];
+ const targetOptions = [
+ authoringOption("step_input.profile.name", "Profile name", "step_input", ["step_input"]),
+ ];
+
+ render(
+ { submissions.push(value); }}
+ />,
+ );
+
+ expect(screen.getByRole("group", { name: "Workflow input" })).toBeInTheDocument();
+ expect(screen.getByRole("group", { name: "Runtime context" })).toBeInTheDocument();
+ expect(screen.getByRole("group", { name: "Step input" })).toBeInTheDocument();
+ expect(screen.queryByRole("combobox", { name: "Source path for input row 1" })).toBeNull();
+
+ await user.click(screen.getByRole("button", { name: /Request id/ }));
+ await user.click(screen.getByRole("button", { name: /Profile name/ }));
+ await user.click(screen.getByRole("button", { name: "Save inputs" }));
+
+ expect(submissions).toEqual([[{ path: "input.request.id", target: "profile.name" }]]);
+ });
+
it("formats local and graph path objects at whole and nested paths", () => {
expect(displayLocalInputPath({ root: "local", parts: [] })).toBe(".");
expect(displayLocalInputPath({ root: "local", parts: ["payload", "item"] })).toBe("payload.item");
@@ -42,11 +107,11 @@ describe("StepInputBindingsForm", () => {
);
expect(screen.getByRole("group", { name: "Input row 1" })).toBeInTheDocument();
- expect(screen.getByRole("combobox", { name: "Target for row 1" })).toHaveValue("title");
+ expect(customInput("Target for row 1")).toHaveValue("title");
expect(screen.getByRole("radio", { name: "Path for input row 1" })).toBeChecked();
- expect(screen.getByRole("combobox", { name: "Source path for input row 1" })).toHaveValue("input.title");
+ expect(customInput("Source path for input row 1")).toHaveValue("input.title");
expect(screen.getByRole("combobox", { name: "Nullable" })).toHaveValue("0:null");
- expect(screen.getByRole("combobox", { name: "Target for row 3" })).toHaveValue("nested.name");
+ expect(customInput("Target for row 3")).toHaveValue("nested.name");
expect(screen.getByText("Unsupported input binding.")).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Raw unsupported input row 4" })).toHaveTextContent('"target"');
expect(screen.getByRole("button", { name: "Remove unsupported input row 4" })).toBeInTheDocument();
@@ -178,10 +243,10 @@ describe("StepInputBindingsForm", () => {
/>,
);
- expect(screen.getByRole("combobox", { name: "Target for row 1" })).toHaveValue("payload.item");
- expect(screen.getByRole("combobox", { name: "Target for row 2" })).toHaveValue(".");
- expect(screen.getByRole("combobox", { name: "Source path for input row 1" })).toHaveValue("input.source");
- expect(screen.getByRole("combobox", { name: "Source path for input row 2" })).toHaveValue("state.audit.latest");
+ expect(customInput("Target for row 1")).toHaveValue("payload.item");
+ expect(customInput("Target for row 2")).toHaveValue(".");
+ expect(customInput("Source path for input row 1")).toHaveValue("input.source");
+ expect(customInput("Source path for input row 2")).toHaveValue("state.audit.latest");
await user.click(screen.getByRole("button", { name: "Save inputs" }));
@@ -218,8 +283,8 @@ describe("StepInputBindingsForm", () => {
await user.clear(screen.getByRole("textbox", { name: "Name" }));
await user.type(screen.getByRole("textbox", { name: "Name" }), "after");
await user.click(screen.getByRole("radio", { name: "Path for input row 1" }));
- await user.clear(screen.getByRole("combobox", { name: "Source path for input row 1" }));
- await user.type(screen.getByRole("combobox", { name: "Source path for input row 1" }), "input.nested");
+ await user.clear(await editableCustomInput(user, "Source path for input row 1"));
+ await user.type(await editableCustomInput(user, "Source path for input row 1"), "input.nested");
await user.click(screen.getByRole("radio", { name: "Literal value for input row 1" }));
await user.click(screen.getByRole("button", { name: "Save inputs" }));
@@ -258,13 +323,13 @@ describe("StepInputBindingsForm", () => {
/>,
);
- await user.clear(screen.getByRole("combobox", { name: "Target for row 1" }));
+ await user.clear(await editableCustomInput(user, "Target for row 1"));
await user.click(screen.getByRole("button", { name: "Save inputs" }));
- const target = screen.getByRole("combobox", { name: "Target for row 1" });
+ const target = customInput("Target for row 1");
const describedBy = target.getAttribute("aria-describedby");
+ expect(describedBy).not.toBeNull();
expect(target).toHaveAttribute("aria-invalid", "true");
- expect(describedBy).toBeTruthy();
expect(document.getElementById(describedBy ?? "")).toHaveTextContent("Target is required.");
});
@@ -277,35 +342,24 @@ describe("StepInputBindingsForm", () => {
type: "object",
properties: { profile: { type: "object", properties: { name: { type: "string" } } } },
}}
- workflowInputSchema={{
- type: "object",
- properties: { request: { type: "object", properties: { id: { type: "string" } } } },
- }}
- workflowStateSchema={{
- type: "object",
- properties: { session: { type: "object", properties: { token: { type: "string" } } } },
- }}
+ sourceOptions={[
+ authoringOption("input.request.id", "Request id", "workflow_input", ["step_input"]),
+ authoringOption("state.session.token", "Session token", "workflow_state", ["step_input"]),
+ ]}
+ targetOptions={[authoringOption("step_input.profile.name", "Profile name", "step_input", ["step_input"])]}
initialBindings={[{ path: "input.request.id", target: "profile.name" }]}
onSubmit={(value) => { submissions.push(value); }}
/>,
);
- const target = screen.getByRole("combobox", { name: "Target for row 1" });
- const targetList = document.getElementById(target.getAttribute("list") ?? "");
- expect(target.getAttribute("list")).toBeTruthy();
- expect(targetList).not.toBeNull();
- expect(targetList?.querySelector('option[value="profile.name"]')).not.toBeNull();
- const source = screen.getByRole("combobox", { name: "Source path for input row 1" });
- const sourceList = document.getElementById(source.getAttribute("list") ?? "");
- expect(source.getAttribute("list")).toBeTruthy();
- expect(sourceList).not.toBeNull();
- expect(sourceList?.querySelector('option[value="input.request.id"]')).not.toBeNull();
- expect(sourceList?.querySelector('option[value="state.session.token"]')).not.toBeNull();
+ expect(screen.getByRole("button", { name: /Profile name/ })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /Request id/ })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /Session token/ })).toBeInTheDocument();
- await user.clear(target);
- await user.type(target, "profile.custom");
- await user.clear(source);
- await user.type(source, "context.custom");
+ await user.clear(await editableCustomInput(user, "Target for row 1"));
+ await user.type(await editableCustomInput(user, "Target for row 1"), "profile.custom");
+ await user.clear(await editableCustomInput(user, "Source path for input row 1"));
+ await user.type(await editableCustomInput(user, "Source path for input row 1"), "context.custom");
await user.click(screen.getByRole("button", { name: "Save inputs" }));
expect(submissions).toEqual([[{ path: "context.custom", target: "profile.custom" }]]);
@@ -325,13 +379,13 @@ describe("StepInputBindingsForm", () => {
/>,
);
- await user.clear(screen.getByRole("combobox", { name: "Target for row 2" }));
- await user.type(screen.getByRole("combobox", { name: "Target for row 2" }), "title");
+ await user.clear(await editableCustomInput(user, "Target for row 2"));
+ await user.type(await editableCustomInput(user, "Target for row 2"), "title");
await user.click(screen.getByRole("button", { name: "Save inputs" }));
expect(submissions).toEqual([]);
- expect(screen.getByRole("combobox", { name: "Target for row 1" })).toHaveAttribute("aria-invalid", "true");
- expect(screen.getByRole("combobox", { name: "Target for row 2" })).toHaveAttribute("aria-invalid", "true");
+ expect(screen.getByRole("region", { name: "Target for row 1" })).toBeInTheDocument();
+ expect(screen.getByRole("region", { name: "Target for row 2" })).toBeInTheDocument();
expect(screen.getAllByRole("alert").filter((alert) =>
alert.textContent?.includes("Target is duplicated") ?? false,
)).toHaveLength(2);
@@ -351,14 +405,14 @@ describe("StepInputBindingsForm", () => {
/>,
);
- const secondTarget = screen.getByRole("combobox", { name: "Target for row 2" });
+ const secondTarget = await editableCustomInput(user, "Target for row 2");
await user.clear(secondTarget);
- await user.type(secondTarget, "title");
+ await user.type(await editableCustomInput(user, "Target for row 2"), "title");
await user.click(screen.getByRole("button", { name: "Save inputs" }));
expect(submissions).toEqual([]);
- await user.clear(secondTarget);
- await user.type(secondTarget, "nullable");
+ await user.clear(await editableCustomInput(user, "Target for row 2"));
+ await user.type(await editableCustomInput(user, "Target for row 2"), "nullable");
expect(screen.getByRole("button", { name: "Save inputs" })).not.toBeDisabled();
await user.click(screen.getByRole("button", { name: "Save inputs" }));
expect(submissions).toHaveLength(1);
@@ -379,7 +433,7 @@ describe("StepInputBindingsForm", () => {
);
expect(screen.getByRole("button", { name: "Save inputs" })).toBeDisabled();
- const source = screen.getByRole("combobox", { name: "Source path for input row 1" });
+ const source = await editableCustomInput(user, "Source path for input row 1");
await user.clear(source);
await user.type(source, "context.profile");
@@ -399,7 +453,7 @@ describe("StepInputBindingsForm", () => {
onSubmit={() => undefined}
/>,
);
- const target = screen.getByRole("combobox", { name: "Target for row 1" });
+ const target = customInput("Target for row 1");
rerender(
{
onSubmit={() => undefined}
/>,
);
- expect(screen.getByRole("combobox", { name: "Target for row 1" })).toHaveValue("edited");
+ expect(customInput("Target for row 1")).toHaveValue("edited");
rerender(
{
onSubmit={() => undefined}
/>,
);
- expect(screen.getByRole("combobox", { name: "Target for row 1" })).toHaveValue("nullable");
+ expect(customInput("Target for row 1")).toHaveValue("nullable");
});
it("shows row diagnostics at the row that owns them", () => {
@@ -467,13 +521,13 @@ describe("StepInputBindingsForm", () => {
);
await user.click(screen.getByRole("button", { name: "Add input row" }));
- await user.type(screen.getByRole("combobox", { name: "Target for row 1" }), "items");
+ await user.type(await editableCustomInput(user, "Target for row 1"), "items");
await user.click(screen.getByRole("radio", { name: "Construct value for input row 1" }));
await user.click(screen.getByRole("button", { name: "Add item to items" }));
await user.click(screen.getByRole("button", { name: "Add item to items" }));
await user.selectOptions(screen.getByRole("combobox", { name: "Value source for items item 1" }), "path");
- const itemPath = screen.getByRole("combobox", { name: "Path for items item 1" });
+ const itemPath = customInput("Path for items item 1");
await user.clear(itemPath);
await user.type(itemPath, "state.foo");
await user.selectOptions(screen.getByRole("combobox", { name: "Value source for items item 2" }), "literal");
@@ -482,7 +536,7 @@ describe("StepInputBindingsForm", () => {
await user.type(itemValue, "wowcool");
await user.click(screen.getByRole("button", { name: "Add input row" }));
- await user.type(screen.getByRole("combobox", { name: "Target for row 2" }), "separator");
+ await user.type(await editableCustomInput(user, "Target for row 2"), "separator");
await user.click(screen.getByRole("radio", { name: "Literal value for input row 2" }));
await user.clear(screen.getByRole("textbox", { name: "Separator" }));
await user.type(screen.getByRole("textbox", { name: "Separator" }), " ");
diff --git a/web/apps/console/src/workspace/authoring/StepInputBindingsForm.tsx b/web/apps/console/src/workspace/authoring/StepInputBindingsForm.tsx
index 84c2f73d..30f70edc 100644
--- a/web/apps/console/src/workspace/authoring/StepInputBindingsForm.tsx
+++ b/web/apps/console/src/workspace/authoring/StepInputBindingsForm.tsx
@@ -1,4 +1,7 @@
import { useId, useRef, useState, type FormEvent } from "react";
+import type {
+ AuthoringPathOption,
+} from "../domain/authoring-contract-models.js";
import type {
DraftDiagnostic,
StepInputBinding,
@@ -21,13 +24,14 @@ import {
import { serializeSchemaValues, type FieldSources } from "../schema-form/schema-values.js";
import { formatBoundedJson } from "../domain/format-bounded-json.js";
import { displayGraphInputPath, displayLocalInputPath } from "./input-binding-paths.js";
+import { AuthoringPathPicker } from "./AuthoringPathPicker.js";
import {
- capabilityLocalPathSuggestions,
+ localPathFromPickerValue,
+ pickerValueForLocalPath,
isJsonValue,
serializeInputBindingRow,
serializeStepInputBindingRow,
stepInputBindingRows,
- workflowSourceSuggestions,
type StepInputBindingRow,
} from "./selected-step-dataflow.js";
@@ -51,8 +55,8 @@ type FormRow = EditableRow | UnsupportedRow;
export type StepInputBindingsFormProps = {
readonly inputSchema: unknown;
- readonly workflowInputSchema?: unknown;
- readonly workflowStateSchema?: unknown;
+ readonly sourceOptions?: ReadonlyArray;
+ readonly targetOptions?: ReadonlyArray;
/** Changes only when the parent has accepted a new canonical draft. */
readonly canonicalVersion?: string | number | null;
readonly initialRows?: ReadonlyArray;
@@ -294,8 +298,8 @@ const duplicateIssuesForRows = (
const StepInputBindingsFormContent = ({
canonicalVersion = null,
inputSchema,
- workflowInputSchema,
- workflowStateSchema,
+ sourceOptions = [],
+ targetOptions = [],
initialRows,
initialBindings,
rowDiagnostics = EMPTY_DIAGNOSTICS,
@@ -305,10 +309,6 @@ const StepInputBindingsFormContent = ({
}: StepInputBindingsFormProps) => {
const formId = useId();
const root = normalizeSchema(inputSchema);
- const sourceSuggestions = workflowSourceSuggestions(workflowInputSchema ?? null, workflowStateSchema ?? null);
- const targetSuggestions = capabilityLocalPathSuggestions(inputSchema);
- const sourceListId = `${formId}-workflow-sources`;
- const targetListId = `${formId}-capability-targets`;
const [rows, setRows] = useState>(() =>
rowsFrom(inputRows(initialRows, initialBindings), formId, root),
);
@@ -420,12 +420,6 @@ const StepInputBindingsFormContent = ({
return (