FN-7584: register builtin:brainstorming workflow with ask-user/exit-gate loop
Adds a discoverable built-in Brainstorming workflow composing the ask-user + exit-gate reach-out loop ahead of the standard coding plan/execute/review/merge spine, plus a WorkflowNodeEditor fix so clearing the ask-user question textarea deletes the config key instead of persisting an empty string. - Add packages/core/src/builtin-brainstorming-workflow-ir.ts registering builtin:brainstorming (non-default, default-enabled): ask-user -> refine prompt -> exit-gate-on-approval ahead of the unmodified Coding plan/execute/review/merge spine - Wire the new builtin into packages/core/src/builtin-workflows.ts and extend the builtin-workflows parity test suite - Add builtin-brainstorming-workflow-ir.test.ts covering the new workflow's IR shape and validation - Fix WorkflowNodeEditor.tsx ask-user question textarea onChange to delete the config.question key when cleared to empty (validateAskUserAndExitGateNodes rejects present-but-empty question; only an absent key falls back to the engine default) - Update docs/workflow-steps.md to document builtin:brainstorming as a selectable built-in composition - Add .changeset/fn-7584-brainstorming-builtin.md (minor, feature) Files changed: .changeset/fn-7584-brainstorming-builtin.md | 7 ++ docs/workflow-steps.md | 2 +- .../builtin-brainstorming-workflow-ir.test.ts | 79 ++++++++++++++++ .../core/src/__tests__/builtin-workflows.test.ts | 66 +++++++++++++ .../core/src/builtin-brainstorming-workflow-ir.ts | 103 +++++++++++++++++++++ packages/core/src/builtin-workflows.ts | 46 +++++++++ .../app/components/WorkflowNodeEditor.tsx | 18 +++- 7 files changed, 319 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-7584 Fusion-Task-Lineage: 2c0258c2-9a35-403a-8688-ee49393a7231 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7584-brainstorming-builtin.md
Normal file
7
.changeset/fn-7584-brainstorming-builtin.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Add a built-in "Brainstorming" workflow that talks to you before planning.
|
||||
category: feature
|
||||
dev: Registers `builtin:brainstorming` (non-default, default-enabled) composing FN-7579's `ask-user` → refine → `exit-gate`-on-approval phase ahead of the normal coding plan/execute/review/merge spine. Parity suite (`builtin-workflows.test.ts`) extended for the new entry.
|
||||
@@ -382,7 +382,7 @@ start → ask (ask-user: "Anything to refine?")
|
||||
the PR review loop)
|
||||
```
|
||||
|
||||
Each turn, the user is asked to refine; once they reply "looks good" (or whatever the condition matches), the exit-gate routes the task out of the brainstorm loop. This is a documented composition, not a registered built-in workflow — copy the shape into a custom workflow's IR via `fn_workflow_create`/`fn_workflow_update`.
|
||||
Each turn, the user is asked to refine; once they reply "looks good" (or whatever the condition matches), the exit-gate routes the task out of the brainstorm loop. This composition is also available as a discoverable built-in: `builtin:brainstorming` (FN-7584) registers exactly this shape — `ask-user` → a refine prompt step → `exit-gate`-on-approval — ahead of the unmodified default Coding plan/execute/review/merge spine, selectable directly from the workflow picker. Copy the shape into a custom workflow's IR via `fn_workflow_create`/`fn_workflow_update` when you need a different downstream pipeline than the standard coding one.
|
||||
|
||||
#### Workflow-defined custom task fields
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { BUILTIN_BRAINSTORMING_WORKFLOW_IR } from "../builtin-brainstorming-workflow-ir.js";
|
||||
import { parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.js";
|
||||
import type { WorkflowIrV2 } from "../workflow-ir-types.js";
|
||||
|
||||
/*
|
||||
FNXC:WorkflowBrainstorming 2026-07-05-00:00:
|
||||
FN-7584 registers builtin:brainstorming as a discoverable built-in composing
|
||||
FN-7579's ask-user -> refine -> exit-gate-on-approval phase ahead of the normal
|
||||
coding plan/execute path. These tests pin the IR shape: it parses, orders the
|
||||
brainstorm nodes before the plan/execute spine, and round-trips through
|
||||
serialize/parse.
|
||||
*/
|
||||
|
||||
describe("builtin brainstorming workflow IR", () => {
|
||||
it("parses through parseWorkflowIr", () => {
|
||||
expect(() => parseWorkflowIr(BUILTIN_BRAINSTORMING_WORKFLOW_IR)).not.toThrow();
|
||||
const ir = BUILTIN_BRAINSTORMING_WORKFLOW_IR as WorkflowIrV2;
|
||||
expect(ir.version).toBe("v2");
|
||||
});
|
||||
|
||||
it("contains an ask-user and an exit-gate node ordered before the plan/execute spine", () => {
|
||||
const ir = BUILTIN_BRAINSTORMING_WORKFLOW_IR as WorkflowIrV2;
|
||||
|
||||
const askUser = ir.nodes.find((node) => node.kind === "ask-user");
|
||||
const exitGate = ir.nodes.find((node) => node.kind === "exit-gate");
|
||||
expect(askUser?.id).toBe("brainstorm-ask");
|
||||
expect(exitGate?.id).toBe("brainstorm-exit");
|
||||
|
||||
const startIndex = ir.nodes.findIndex((node) => node.id === "start");
|
||||
const askIndex = ir.nodes.findIndex((node) => node.id === "brainstorm-ask");
|
||||
const refineIndex = ir.nodes.findIndex((node) => node.id === "brainstorm-refine");
|
||||
const exitIndex = ir.nodes.findIndex((node) => node.id === "brainstorm-exit");
|
||||
const planIndex = ir.nodes.findIndex((node) => node.id === "plan");
|
||||
const parseIndex = ir.nodes.findIndex((node) => node.id === "parse");
|
||||
const stepsIndex = ir.nodes.findIndex((node) => node.id === "steps");
|
||||
|
||||
expect(startIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(askIndex).toBeGreaterThan(startIndex);
|
||||
expect(refineIndex).toBeGreaterThan(askIndex);
|
||||
expect(exitIndex).toBeGreaterThan(refineIndex);
|
||||
expect(planIndex).toBeGreaterThan(exitIndex);
|
||||
expect(parseIndex).toBeGreaterThan(planIndex);
|
||||
expect(stepsIndex).toBeGreaterThan(parseIndex);
|
||||
|
||||
// Graph wiring: start feeds the brainstorm loop, and the exit-gate's
|
||||
// outcome:exit edge is what rejoins the unmodified plan/execute spine.
|
||||
expect(ir.edges).toContainEqual({ from: "start", to: "brainstorm-ask", condition: "success" });
|
||||
expect(ir.edges).toContainEqual({ from: "brainstorm-ask", to: "brainstorm-refine", condition: "success" });
|
||||
expect(ir.edges).toContainEqual({ from: "brainstorm-refine", to: "brainstorm-exit", condition: "success" });
|
||||
expect(ir.edges).toContainEqual({ from: "brainstorm-exit", to: "plan", condition: "outcome:exit" });
|
||||
expect(ir.edges).toContainEqual({
|
||||
from: "brainstorm-exit",
|
||||
to: "brainstorm-ask",
|
||||
condition: "outcome:continue",
|
||||
kind: "rework",
|
||||
});
|
||||
|
||||
// The exit-gate's rework edge targets a declared top-level rework-region head.
|
||||
expect(askUser?.config?.reworkRegion).toBe(true);
|
||||
expect(typeof askUser?.config?.maxReworkCycles).toBe("number");
|
||||
|
||||
// ask-user carries a non-empty question (validator-enforced when present).
|
||||
expect(typeof askUser?.config?.question).toBe("string");
|
||||
expect((askUser?.config?.question as string).trim().length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("round-trips serialize -> parse to identical bytes", () => {
|
||||
const serialized = serializeWorkflowIr(BUILTIN_BRAINSTORMING_WORKFLOW_IR);
|
||||
const reparsed = parseWorkflowIr(serialized);
|
||||
expect(serializeWorkflowIr(reparsed)).toBe(serialized);
|
||||
});
|
||||
|
||||
it("carries the shared built-in workflow settings", () => {
|
||||
const ir = BUILTIN_BRAINSTORMING_WORKFLOW_IR as WorkflowIrV2;
|
||||
expect(ir.settings?.some((setting) => setting.id === "planReviewMaxRevisions")).toBe(true);
|
||||
expect(ir.settings?.some((setting) => setting.id === "codeReviewMaxRevisions")).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -343,6 +343,72 @@ describe("built-in workflows", () => {
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* FNXC:WorkflowBrainstorming 2026-07-05-00:00:
|
||||
* FN-7584 parity coverage for the registered builtin:brainstorming built-in
|
||||
* (FN-7579's ask-user -> refine -> exit-gate-on-approval composition,
|
||||
* discoverable from the workflow picker ahead of the normal coding spine).
|
||||
*/
|
||||
it("registers builtin:brainstorming as a default-enabled workflow ordered after the existing built-ins", () => {
|
||||
const brainstorming = getBuiltinWorkflow("builtin:brainstorming");
|
||||
expect(brainstorming).toBeDefined();
|
||||
expect(brainstorming!.kind).toBe("workflow");
|
||||
expect(() => parseWorkflowIr(brainstorming!.ir)).not.toThrow();
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:brainstorming");
|
||||
expect(BUILTIN_WORKFLOWS.findIndex((workflow) => workflow.id === "builtin:brainstorming")).toBeGreaterThan(
|
||||
BUILTIN_WORKFLOWS.findIndex((workflow) => workflow.id === "builtin:lead-generation"),
|
||||
);
|
||||
// Ordering assertion the suite pins elsewhere (`.slice(0, 5)`) must stay untouched.
|
||||
expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([
|
||||
"builtin:coding",
|
||||
"builtin:coding-ideas",
|
||||
"builtin:legacy-coding",
|
||||
"builtin:quick-fix",
|
||||
"builtin:review-heavy",
|
||||
]);
|
||||
});
|
||||
|
||||
it("orders builtin:brainstorming's ask-user/exit-gate loop ahead of the plan/execute spine", () => {
|
||||
const brainstorming = getBuiltinWorkflow("builtin:brainstorming")!;
|
||||
const nodes = brainstorming.ir.nodes;
|
||||
const askIndex = nodes.findIndex((node) => node.kind === "ask-user");
|
||||
const exitIndex = nodes.findIndex((node) => node.kind === "exit-gate");
|
||||
const planIndex = nodes.findIndex((node) => node.id === "plan");
|
||||
const parseIndex = nodes.findIndex((node) => node.id === "parse");
|
||||
const stepsIndex = nodes.findIndex((node) => node.id === "steps");
|
||||
|
||||
expect(askIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(exitIndex).toBeGreaterThan(askIndex);
|
||||
expect(planIndex).toBeGreaterThan(exitIndex);
|
||||
expect(parseIndex).toBeGreaterThan(planIndex);
|
||||
expect(stepsIndex).toBeGreaterThan(parseIndex);
|
||||
});
|
||||
|
||||
it("builtin:brainstorming still satisfies every merge-capable built-in invariant (completion summary, post-merge-verification, merge primitives, settings)", () => {
|
||||
const brainstorming = getBuiltinWorkflow("builtin:brainstorming")!;
|
||||
const summaryNodes = brainstorming.ir.nodes.filter(
|
||||
(node) => node.kind === "prompt" && (node.config as { summaryTarget?: unknown } | undefined)?.summaryTarget === "task",
|
||||
);
|
||||
expect(summaryNodes).toHaveLength(1);
|
||||
expect((summaryNodes[0]!.config as { toolMode?: unknown }).toolMode).toBe("readonly");
|
||||
|
||||
expect(brainstorming.ir.nodes.some((node) => node.id === "post-merge-verification")).toBe(true);
|
||||
for (const id of [
|
||||
"merge-gate",
|
||||
"merge-retry",
|
||||
"merge-manual-hold",
|
||||
"branch-group-member-integration",
|
||||
"branch-group-promotion",
|
||||
"merge-attempt",
|
||||
"recovery-router",
|
||||
]) {
|
||||
expect(brainstorming.ir.nodes.some((node) => node.id === id)).toBe(true);
|
||||
}
|
||||
|
||||
expect(brainstorming.ir.settings?.some((setting) => setting.id === "planReviewMaxRevisions")).toBe(true);
|
||||
expect(brainstorming.ir.settings?.some((setting) => setting.id === "codeReviewMaxRevisions")).toBe(true);
|
||||
});
|
||||
|
||||
it("default workflow column ids equal the legacy enum values, in legacy order (KTD-1)", () => {
|
||||
expect(BUILTIN_CODING_WORKFLOW_IR.version).toBe("v2");
|
||||
if (BUILTIN_CODING_WORKFLOW_IR.version !== "v2") throw new Error("expected v2");
|
||||
|
||||
103
packages/core/src/builtin-brainstorming-workflow-ir.ts
Normal file
103
packages/core/src/builtin-brainstorming-workflow-ir.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { WorkflowIr, WorkflowIrV2 } from "./workflow-ir-types.js";
|
||||
import { parseWorkflowIr } from "./workflow-ir.js";
|
||||
import { BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR } from "./builtin-stepwise-final-review-coding-workflow-ir.js";
|
||||
import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js";
|
||||
|
||||
/*
|
||||
FNXC:WorkflowBrainstorming 2026-07-05-00:00:
|
||||
FN-7584 closes the loop FN-7579 opened: `ask-user` and `exit-gate` node kinds
|
||||
existed only as a documented composition (docs/workflow-steps.md, "Brainstorming
|
||||
/ chat reach-out composition"), not a discoverable built-in. This module clones
|
||||
the default Coding graph (BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR,
|
||||
the same IR backing `builtin:coding`) and prepends a bounded brainstorming phase
|
||||
between `start` and `plan`:
|
||||
|
||||
start -> brainstorm-ask (ask-user: "what do you want to brainstorm?")
|
||||
-> brainstorm-refine (prompt, seam:"planning": turn the conversation
|
||||
so far into a refined brief)
|
||||
-> brainstorm-exit (exit-gate: condition on the ask-user's answer
|
||||
containing an approval phrase)
|
||||
outcome:exit -> plan (rejoins the unmodified coding spine)
|
||||
outcome:continue -> brainstorm-ask (rework edge; the ask-user node
|
||||
carries config.reworkRegion:true so this is a
|
||||
legal top-level rework-region head per the U6
|
||||
convention validateWorkflowIr enforces)
|
||||
|
||||
The reused coding spine (parse-steps/foreach/step-review, optional
|
||||
browser-verification/code-review groups, completion-summary,
|
||||
post-merge-verification, and the full merge-primitive region) is left byte-for-byte
|
||||
identical to `builtin:coding`'s graph, so every parity invariant that graph
|
||||
already satisfies carries over unchanged; only the three new brainstorm nodes
|
||||
and their four edges are new surface for the parity suite to cover.
|
||||
*/
|
||||
|
||||
const APPROVAL_PHRASE = "looks good";
|
||||
|
||||
const RAW_BUILTIN_BRAINSTORMING_WORKFLOW_IR: WorkflowIr = (() => {
|
||||
const ir = JSON.parse(JSON.stringify(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)) as WorkflowIrV2;
|
||||
ir.name = "builtin-brainstorming";
|
||||
|
||||
// Insert the three brainstorm nodes right after `start` (order is cosmetic —
|
||||
// edges determine the walk — but keeping them near `start` mirrors the
|
||||
// reading order of the graph).
|
||||
const startIndex = ir.nodes.findIndex((node) => node.id === "start");
|
||||
if (startIndex < 0) {
|
||||
throw new Error("brainstorming built-in requires the cloned graph's start node");
|
||||
}
|
||||
const startColumn = ir.nodes[startIndex]!.column;
|
||||
const planNode = ir.nodes.find((node) => node.id === "plan");
|
||||
const brainstormColumn = planNode?.column ?? startColumn;
|
||||
|
||||
ir.nodes.splice(
|
||||
startIndex + 1,
|
||||
0,
|
||||
{
|
||||
id: "brainstorm-ask",
|
||||
kind: "ask-user",
|
||||
column: brainstormColumn,
|
||||
config: {
|
||||
question:
|
||||
`What would you like to brainstorm? Share your idea and any constraints; reply "${APPROVAL_PHRASE}" once you're ready to move into planning.`,
|
||||
// FNXC:WorkflowBrainstorming 2026-07-05-00:00: top-level rework-region
|
||||
// head (U6 convention) — the loop bound the exit-gate's rework edge
|
||||
// resets to when it targets this node.
|
||||
reworkRegion: true,
|
||||
maxReworkCycles: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "brainstorm-refine",
|
||||
kind: "prompt",
|
||||
column: brainstormColumn,
|
||||
config: {
|
||||
seam: "planning",
|
||||
name: "Refine brainstorm",
|
||||
prompt:
|
||||
"You are helping the user brainstorm before this task is planned. Read the task description and the user's latest reply. Turn the conversation so far into a short, structured refined brief: 1) the core idea/goal, 2) key constraints or requirements mentioned so far, 3) open questions still unresolved, 4) a one-line readiness assessment (ready to plan, or what's still missing). Keep it concise — this is a running scratchpad the user reviews each turn, not a final spec.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "brainstorm-exit",
|
||||
kind: "exit-gate",
|
||||
column: brainstormColumn,
|
||||
config: {
|
||||
condition: { type: "output-contains", nodeId: "brainstorm-ask", value: APPROVAL_PHRASE },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Rewire start -> plan through the new brainstorm loop.
|
||||
ir.edges = ir.edges.filter((edge) => !(edge.from === "start" && edge.to === "plan"));
|
||||
ir.edges.unshift(
|
||||
{ from: "start", to: "brainstorm-ask", condition: "success" },
|
||||
{ from: "brainstorm-ask", to: "brainstorm-refine", condition: "success" },
|
||||
{ from: "brainstorm-refine", to: "brainstorm-exit", condition: "success" },
|
||||
{ from: "brainstorm-exit", to: "plan", condition: "outcome:exit" },
|
||||
{ from: "brainstorm-exit", to: "brainstorm-ask", condition: "outcome:continue", kind: "rework" },
|
||||
);
|
||||
|
||||
ir.settings = BUILTIN_WORKFLOW_SETTINGS;
|
||||
return ir;
|
||||
})();
|
||||
|
||||
export const BUILTIN_BRAINSTORMING_WORKFLOW_IR = parseWorkflowIr(RAW_BUILTIN_BRAINSTORMING_WORKFLOW_IR);
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BUILTIN_CODING_WORKFLOW_IR } from "./builtin-coding-workflow-ir.js";
|
||||
import { BUILTIN_CODING_IDEAS_WORKFLOW_IR } from "./builtin-coding-ideas-workflow-ir.js";
|
||||
import { BUILTIN_BRAINSTORMING_WORKFLOW_IR } from "./builtin-brainstorming-workflow-ir.js";
|
||||
import { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "./builtin-lead-generation-workflow-ir.js";
|
||||
import { BUILTIN_MARKETING_WORKFLOW_IR } from "./builtin-marketing-workflow-ir.js";
|
||||
import { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js";
|
||||
@@ -708,6 +709,51 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [
|
||||
createdAt: BUILTIN_TS,
|
||||
updatedAt: BUILTIN_TS,
|
||||
},
|
||||
/*
|
||||
* FNXC:WorkflowBrainstorming 2026-07-05-00:00:
|
||||
* FN-7584 registers a discoverable built-in for FN-7579's ask-user -> refine ->
|
||||
* exit-gate-on-approval brainstorming composition (docs/workflow-steps.md). It
|
||||
* clones the default Coding graph (BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)
|
||||
* and prepends the brainstorm loop between `start` and `plan`, so every downstream
|
||||
* node/edge is byte-identical to `builtin:coding`. Appended LAST (after
|
||||
* lead-generation) so it does not disturb the defaultEnabledBuiltinWorkflowIds()
|
||||
* `.slice(0, 5)` ordering assertion in builtin-workflows.test.ts.
|
||||
*/
|
||||
{
|
||||
id: "builtin:brainstorming",
|
||||
name: "Brainstorming",
|
||||
description:
|
||||
"Brainstorm with the user first: ask a question, refine the idea, and exit the loop on approval, then run the standard coding plan/execute/review/merge pipeline.",
|
||||
kind: "workflow",
|
||||
ir: BUILTIN_BRAINSTORMING_WORKFLOW_IR,
|
||||
layout: {
|
||||
start: { x: 60, y: 160 },
|
||||
"brainstorm-ask": { x: 230, y: 160 },
|
||||
"brainstorm-refine": { x: 400, y: 160 },
|
||||
"brainstorm-exit": { x: 570, y: 160 },
|
||||
plan: { x: 740, y: 160 },
|
||||
"plan-review": { x: 910, y: 160 },
|
||||
"plan-replan": { x: 910, y: 320 },
|
||||
parse: { x: 1080, y: 160 },
|
||||
steps: { x: 1250, y: 160 },
|
||||
"browser-verification": { x: 1420, y: 160 },
|
||||
"browser-verification-remediation": { x: 1420, y: 320 },
|
||||
"code-review": { x: 1590, y: 160 },
|
||||
"code-review-remediation": { x: 1590, y: 320 },
|
||||
"completion-summary": { x: 1760, y: 160 },
|
||||
"merge-gate": { x: 1930, y: 160 },
|
||||
"branch-group-member-integration": { x: 2100, y: 80 },
|
||||
"branch-group-promotion": { x: 2270, y: 80 },
|
||||
"merge-attempt": { x: 2440, y: 160 },
|
||||
"merge-retry": { x: 2610, y: 80 },
|
||||
"recovery-router": { x: 2610, y: 240 },
|
||||
"merge-manual-hold": { x: 2100, y: 240 },
|
||||
"post-merge-verification": { x: 2780, y: 160 },
|
||||
end: { x: 2950, y: 160 },
|
||||
},
|
||||
createdAt: BUILTIN_TS,
|
||||
updatedAt: BUILTIN_TS,
|
||||
},
|
||||
];
|
||||
|
||||
const BUILTIN_BY_ID = new Map(BUILTIN_WORKFLOWS.map((wf) => [wf.id, wf]));
|
||||
|
||||
@@ -4754,7 +4754,23 @@ function InnerEditor({
|
||||
"This workflow is waiting for your input.",
|
||||
)}
|
||||
value={String(selectedNode.data.config?.question ?? "")}
|
||||
onChange={(e) => updateSelectedData({ config: { question: e.target.value } })}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
// FNXC:WorkflowAskUser 2026-07-05-02:00: validateAskUserAndExitGateNodes
|
||||
// rejects a PRESENT-but-empty `question` (only an ABSENT question falls
|
||||
// back to the engine default). Clearing the textarea back to "" must
|
||||
// therefore delete the key, not persist `question: ""`, or the node
|
||||
// silently becomes unsavable the moment an author reverts to the
|
||||
// documented "leave blank for the default" behavior.
|
||||
updateSelectedData({
|
||||
config: (prev) => {
|
||||
const next = { ...prev };
|
||||
if (value.trim() === "") delete next.question;
|
||||
else next.question = value;
|
||||
return next;
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<p className="wf-inspector-note wf-inspector-note--info">
|
||||
|
||||
Reference in New Issue
Block a user