feat(dashboard): template picker on workflow creation
This commit is contained in:
@@ -737,6 +737,71 @@
|
||||
}
|
||||
|
||||
/* Create-workflow dialog (KTD-7). */
|
||||
/* Template picker (U4/R7): radiogroup of Blank + built-ins + user workflows. */
|
||||
.wf-template-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-2xs);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.wf-template-section {
|
||||
margin: var(--space-xs) 0 var(--space-2xs);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.wf-template-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2xs);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.wf-template-option:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.wf-template-option.selected {
|
||||
border-color: var(--accent);
|
||||
background: var(--bg-active);
|
||||
}
|
||||
|
||||
.wf-template-option:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
|
||||
.wf-template-option-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wf-template-option-desc {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wf-template-option-count {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.wf-create-error {
|
||||
margin: var(--space-xs) 0 0;
|
||||
font-size: 0.8rem;
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
flowToIr,
|
||||
emptyWorkflowIr,
|
||||
emptyWorkflowLayout,
|
||||
copyIrWithFreshIds,
|
||||
columnsOf,
|
||||
fieldsOf,
|
||||
columnsToBandNodes,
|
||||
@@ -170,16 +171,39 @@ const USER_NODE_KINDS: ReadonlySet<WorkflowEditorNodeKind> = new Set<WorkflowEdi
|
||||
"merge",
|
||||
]);
|
||||
|
||||
/** A pickable creation template: "Blank" (id null) or a copyable source
|
||||
* workflow (built-in or user kind="workflow"). U4/R7. */
|
||||
interface WorkflowCreateTemplate {
|
||||
/** null = blank; otherwise the source definition's id. */
|
||||
id: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
/** Node count of the source IR (0 for blank). */
|
||||
nodeCount: number;
|
||||
/** Source definition for seeding via copyIrWithFreshIds (absent for blank). */
|
||||
source?: WorkflowDefinition;
|
||||
/** True for built-in sources (grouped separately). */
|
||||
builtin: boolean;
|
||||
}
|
||||
|
||||
/** Local create-workflow dialog (KTD-7). Built on the shared `.modal` primitives
|
||||
* (precedent: NewTaskModal). Owns its own name/description/error state; the
|
||||
* parent supplies an async `onCreate` that performs the createWorkflow call and
|
||||
* throws on failure so the dialog can surface server rejections inline without
|
||||
* losing the typed input. Escape/overlay close (no dirty state of its own). */
|
||||
* (precedent: NewTaskModal). Owns its own template/name/description/error state;
|
||||
* the parent supplies the candidate `workflows` (fragments filtered out here)
|
||||
* and an async `onCreate` that performs the createWorkflow call and throws on
|
||||
* failure so the dialog can surface server rejections inline without losing the
|
||||
* typed input. Escape/overlay close (no dirty state of its own).
|
||||
*
|
||||
* U4/R7: a template step precedes the name/description fields — a
|
||||
* radiogroup-semantics option list (Blank default-selected + built-ins + user
|
||||
* workflows) navigable by ArrowUp/Down; selecting a template prefills the name
|
||||
* ("<source> copy") while untouched and inherits the source description. */
|
||||
function CreateWorkflowDialog({
|
||||
workflows,
|
||||
onCreate,
|
||||
onClose,
|
||||
}: {
|
||||
onCreate: (name: string, description: string) => Promise<void>;
|
||||
workflows: WorkflowDefinition[];
|
||||
onCreate: (name: string, description: string, template: WorkflowCreateTemplate) => Promise<void>;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation("app");
|
||||
@@ -187,12 +211,88 @@ function CreateWorkflowDialog({
|
||||
const [description, setDescription] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
// Tracks whether the user has edited the name; once true, selecting a template
|
||||
// no longer overwrites it (R7: prefill only when untouched).
|
||||
const [nameTouched, setNameTouched] = useState(false);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
const optionRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
|
||||
// Build the option list: Blank first (default), then built-in workflows, then
|
||||
// the user's own kind="workflow" definitions. Fragments are excluded entirely.
|
||||
const templates = useMemo<WorkflowCreateTemplate[]>(() => {
|
||||
const blank: WorkflowCreateTemplate = {
|
||||
id: null,
|
||||
name: t("workflows.templateBlank", "Blank"),
|
||||
description: t("workflows.templateBlankDescription", "Start from an empty start → end graph."),
|
||||
nodeCount: 0,
|
||||
builtin: false,
|
||||
};
|
||||
const usable = workflows.filter((w) => w.kind !== "fragment");
|
||||
const toTemplate = (w: WorkflowDefinition): WorkflowCreateTemplate => ({
|
||||
id: w.id,
|
||||
name: w.name,
|
||||
description: w.description ?? "",
|
||||
nodeCount: w.ir.nodes.length,
|
||||
source: w,
|
||||
builtin: isBuiltinWorkflowId(w.id),
|
||||
});
|
||||
const builtins = usable.filter((w) => isBuiltinWorkflowId(w.id)).map(toTemplate);
|
||||
const yours = usable.filter((w) => !isBuiltinWorkflowId(w.id)).map(toTemplate);
|
||||
return [blank, ...builtins, ...yours];
|
||||
}, [workflows, t]);
|
||||
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const selected = templates[selectedIndex] ?? templates[0];
|
||||
|
||||
useEffect(() => {
|
||||
nameRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
// Apply a template selection: move the radio focus state and (R7) prefill the
|
||||
// name ("<source> copy") + description from the source, but only while the user
|
||||
// has not edited the name.
|
||||
const selectTemplate = useCallback(
|
||||
(index: number) => {
|
||||
const tmpl = templates[index];
|
||||
if (!tmpl) return;
|
||||
setSelectedIndex(index);
|
||||
if (!nameTouched) {
|
||||
if (tmpl.id === null) {
|
||||
setName("");
|
||||
setDescription("");
|
||||
} else {
|
||||
setName(t("workflows.templateCopyName", "{{name}} copy", { name: tmpl.name }));
|
||||
setDescription(tmpl.description);
|
||||
}
|
||||
}
|
||||
if (error) setError(null);
|
||||
},
|
||||
[templates, nameTouched, error, t],
|
||||
);
|
||||
|
||||
// ArrowUp/Down move the radio selection; Enter confirms and shifts focus to
|
||||
// the name input. Other keys (incl. Escape) bubble to the dialog handler.
|
||||
const handleOptionKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
const next = Math.min(selectedIndex + 1, templates.length - 1);
|
||||
selectTemplate(next);
|
||||
optionRefs.current[next]?.focus();
|
||||
} else if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
const prev = Math.max(selectedIndex - 1, 0);
|
||||
selectTemplate(prev);
|
||||
optionRefs.current[prev]?.focus();
|
||||
} else if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
selectTemplate(selectedIndex);
|
||||
nameRef.current?.focus();
|
||||
}
|
||||
},
|
||||
[selectedIndex, templates.length, selectTemplate],
|
||||
);
|
||||
|
||||
const overlayProps = useOverlayDismiss(onClose);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
@@ -206,16 +306,21 @@ function CreateWorkflowDialog({
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await onCreate(trimmed, description.trim());
|
||||
await onCreate(trimmed, description.trim(), selected);
|
||||
// Success path closes the dialog from the parent.
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err) || t("workflows.createFailed", "Failed to create workflow"));
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
[name, description, onCreate, t],
|
||||
[name, description, selected, onCreate, t],
|
||||
);
|
||||
|
||||
// Section boundaries for group headers (built-ins / your workflows). Blank is
|
||||
// always index 0; built-ins follow, then user workflows.
|
||||
const firstBuiltinIndex = templates.findIndex((tmpl) => tmpl.id !== null && tmpl.builtin);
|
||||
const firstYoursIndex = templates.findIndex((tmpl) => tmpl.id !== null && !tmpl.builtin);
|
||||
|
||||
return (
|
||||
<div className="modal-overlay open wf-create-overlay" {...overlayProps}>
|
||||
<div
|
||||
@@ -245,6 +350,59 @@ function CreateWorkflowDialog({
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="modal-body">
|
||||
<div className="wf-field">
|
||||
<span id="wf-template-label">{t("workflows.templatePickerLabel", "Start from")}</span>
|
||||
<div
|
||||
className="wf-template-list"
|
||||
role="radiogroup"
|
||||
aria-labelledby="wf-template-label"
|
||||
data-testid="wf-template-list"
|
||||
>
|
||||
{templates.map((tmpl, index) => {
|
||||
const isSelected = index === selectedIndex;
|
||||
const optionKey = tmpl.id ?? "blank";
|
||||
return (
|
||||
<div key={optionKey}>
|
||||
{index === firstBuiltinIndex && firstBuiltinIndex >= 0 && (
|
||||
<p className="wf-template-section">
|
||||
{t("workflows.templateSectionBuiltin", "Built-in workflows")}
|
||||
</p>
|
||||
)}
|
||||
{index === firstYoursIndex && firstYoursIndex >= 0 && (
|
||||
<p className="wf-template-section">
|
||||
{t("workflows.templateSectionYours", "Your workflows")}
|
||||
</p>
|
||||
)}
|
||||
<div
|
||||
ref={(el) => {
|
||||
optionRefs.current[index] = el;
|
||||
}}
|
||||
role="radio"
|
||||
aria-checked={isSelected}
|
||||
tabIndex={isSelected ? 0 : -1}
|
||||
className={`wf-template-option${isSelected ? " selected" : ""}`}
|
||||
data-testid={tmpl.id === null ? "wf-template-option-blank" : `wf-template-option-${tmpl.id}`}
|
||||
onClick={() => {
|
||||
selectTemplate(index);
|
||||
optionRefs.current[index]?.focus();
|
||||
}}
|
||||
onKeyDown={handleOptionKeyDown}
|
||||
>
|
||||
<span className="wf-template-option-name">{tmpl.name}</span>
|
||||
{tmpl.description && (
|
||||
<span className="wf-template-option-desc">{tmpl.description}</span>
|
||||
)}
|
||||
{tmpl.id !== null && (
|
||||
<span className="wf-template-option-count">
|
||||
{t("workflows.templateNodeCount", "{{count}} nodes", { count: tmpl.nodeCount })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<label className="wf-field">
|
||||
<span>{t("workflows.createName", "Name")}</span>
|
||||
<input
|
||||
@@ -253,6 +411,7 @@ function CreateWorkflowDialog({
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setNameTouched(true);
|
||||
if (error) setError(null);
|
||||
}}
|
||||
/>
|
||||
@@ -828,13 +987,20 @@ function InnerEditor({
|
||||
// Perform the createWorkflow call. Throws on failure so the dialog surfaces
|
||||
// the server error (e.g. duplicate name) inline without losing the input.
|
||||
const handleCreateWorkflow = useCallback(
|
||||
async (workflowName: string, workflowDescription: string) => {
|
||||
async (workflowName: string, workflowDescription: string, template: WorkflowCreateTemplate) => {
|
||||
// Blank → empty start→end graph; template → a fresh-ID copy of the source
|
||||
// graph + layout (U4/R7, never a reference). Always created kind "workflow".
|
||||
const seed =
|
||||
template.source !== undefined
|
||||
? copyIrWithFreshIds(template.source.ir, template.source.layout)
|
||||
: { ir: emptyWorkflowIr(workflowName), layout: emptyWorkflowLayout() };
|
||||
const created = await createWorkflow(
|
||||
{
|
||||
name: workflowName,
|
||||
description: workflowDescription || undefined,
|
||||
ir: emptyWorkflowIr(workflowName),
|
||||
layout: emptyWorkflowLayout(),
|
||||
kind: "workflow",
|
||||
ir: seed.ir,
|
||||
layout: seed.layout,
|
||||
},
|
||||
projectId,
|
||||
);
|
||||
@@ -2215,7 +2381,11 @@ function InnerEditor({
|
||||
)}
|
||||
</div>
|
||||
{createOpen && (
|
||||
<CreateWorkflowDialog onCreate={handleCreateWorkflow} onClose={closeCreateDialog} />
|
||||
<CreateWorkflowDialog
|
||||
workflows={workflows}
|
||||
onCreate={handleCreateWorkflow}
|
||||
onClose={closeCreateDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,25 @@ function v2Def(): WorkflowDefinition {
|
||||
|
||||
function builtinDef(): WorkflowDefinition {
|
||||
const d = v2Def();
|
||||
return { ...d, id: "builtin:coding", name: "Default coding workflow" };
|
||||
return { ...d, id: "builtin:coding", name: "Default coding workflow", description: "Ships with Fusion" };
|
||||
}
|
||||
|
||||
function fragmentDef(): WorkflowDefinition {
|
||||
return {
|
||||
id: "WF-FRAG",
|
||||
kind: "fragment",
|
||||
name: "Lint fragment",
|
||||
description: "A single lint step",
|
||||
ir: {
|
||||
version: "v1",
|
||||
name: "Lint fragment",
|
||||
nodes: [{ id: "lint", kind: "gate", config: { scriptName: "lint" } }],
|
||||
edges: [],
|
||||
},
|
||||
layout: { lint: { x: 0, y: 0 } },
|
||||
createdAt: "2026-06-03T00:00:00.000Z",
|
||||
updatedAt: "2026-06-03T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
function def(): WorkflowDefinition {
|
||||
@@ -872,6 +890,119 @@ describe("WorkflowNodeEditor — U4 create dialog / delete / inline rename / dir
|
||||
expect((nameInput as HTMLInputElement).value).toBe("Dup");
|
||||
});
|
||||
|
||||
// ── Template picker (U4/R7) ────────────────────────────────────────────────
|
||||
|
||||
it("shows Blank first (selected), built-ins, and user workflows; fragments absent", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef(), v2Def(), fragmentDef()]);
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
// Open via the strip "New workflow" button (the empty CTA only shows with no
|
||||
// workflows; here we have some, so use the toolbar button).
|
||||
fireEvent.click(await screen.findByTestId("wf-new-workflow"));
|
||||
await screen.findByTestId("wf-create-dialog");
|
||||
|
||||
const blank = screen.getByTestId("wf-template-option-blank");
|
||||
expect(blank).toHaveAttribute("aria-checked", "true");
|
||||
// Blank is the first radio in the group.
|
||||
const group = screen.getByTestId("wf-template-list");
|
||||
const options = within(group).getAllByRole("radio");
|
||||
expect(options[0]).toBe(blank);
|
||||
|
||||
// Built-in + user workflow present; fragment excluded.
|
||||
expect(screen.getByTestId("wf-template-option-builtin:coding")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("wf-template-option-WF-002")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("wf-template-option-WF-FRAG")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders node count text for template entries", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
fireEvent.click(await screen.findByTestId("wf-new-workflow"));
|
||||
await screen.findByTestId("wf-create-dialog");
|
||||
// v2Def has 3 IR nodes (start, step, end).
|
||||
expect(screen.getByTestId("wf-template-option-WF-002")).toHaveTextContent("3 nodes");
|
||||
});
|
||||
|
||||
it("with no user workflows lists Blank + built-ins only (no Your-workflows header)", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef()]);
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
fireEvent.click(await screen.findByTestId("wf-new-workflow"));
|
||||
await screen.findByTestId("wf-create-dialog");
|
||||
expect(screen.getByTestId("wf-template-option-blank")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("wf-template-option-builtin:coding")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Your workflows")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("selecting a builtin template prefills '<name> copy' and inherits the description", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef()]);
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
fireEvent.click(await screen.findByTestId("wf-new-workflow"));
|
||||
await screen.findByTestId("wf-create-dialog");
|
||||
|
||||
fireEvent.click(screen.getByTestId("wf-template-option-builtin:coding"));
|
||||
expect((screen.getByTestId("wf-create-name") as HTMLInputElement).value).toBe(
|
||||
"Default coding workflow copy",
|
||||
);
|
||||
expect((screen.getByTestId("wf-create-description") as HTMLTextAreaElement).value).toBe(
|
||||
"Ships with Fusion",
|
||||
);
|
||||
});
|
||||
|
||||
it("submitting a template seeds a fresh-ID copy: same node count, all ids differ, description inherited", async () => {
|
||||
const builtin = builtinDef();
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([builtin]);
|
||||
vi.mocked(createWorkflow).mockResolvedValue({ ...v2Def(), id: "WF-NEW", name: "Default coding workflow copy" });
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
fireEvent.click(await screen.findByTestId("wf-new-workflow"));
|
||||
await screen.findByTestId("wf-create-dialog");
|
||||
|
||||
fireEvent.click(screen.getByTestId("wf-template-option-builtin:coding"));
|
||||
fireEvent.click(screen.getByTestId("wf-create-submit"));
|
||||
|
||||
await waitFor(() => expect(createWorkflow).toHaveBeenCalled());
|
||||
const [input] = vi.mocked(createWorkflow).mock.calls[0];
|
||||
const created = input as { name: string; description?: string; kind?: string; ir: { nodes: { id: string }[] } };
|
||||
expect(created.kind).toBe("workflow");
|
||||
expect(created.description).toBe("Ships with Fusion");
|
||||
// Same node count as the source IR.
|
||||
expect(created.ir.nodes).toHaveLength(builtin.ir.nodes.length);
|
||||
// Every node id is fresh (none shared with the source).
|
||||
const sourceIds = new Set(builtin.ir.nodes.map((n) => n.id));
|
||||
for (const n of created.ir.nodes) {
|
||||
expect(sourceIds.has(n.id)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("blank flow seeds an emptyWorkflowIr-shaped graph (start → end)", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef()]);
|
||||
vi.mocked(createWorkflow).mockResolvedValue({ ...v2Def(), id: "WF-NEW", name: "Fresh" });
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
fireEvent.click(await screen.findByTestId("wf-new-workflow"));
|
||||
await screen.findByTestId("wf-create-dialog");
|
||||
|
||||
// Blank is default-selected; just name + submit.
|
||||
fireEvent.change(screen.getByTestId("wf-create-name"), { target: { value: "Fresh" } });
|
||||
fireEvent.click(screen.getByTestId("wf-create-submit"));
|
||||
|
||||
await waitFor(() => expect(createWorkflow).toHaveBeenCalled());
|
||||
const [input] = vi.mocked(createWorkflow).mock.calls[0];
|
||||
const created = input as { ir: { nodes: { kind: string }[]; edges: unknown[] } };
|
||||
expect(created.ir.nodes.map((n) => n.kind)).toEqual(["start", "end"]);
|
||||
expect(created.ir.edges).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("ArrowDown moves the selected radio (keyboard a11y)", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([builtinDef()]);
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
fireEvent.click(await screen.findByTestId("wf-new-workflow"));
|
||||
await screen.findByTestId("wf-create-dialog");
|
||||
|
||||
const blank = screen.getByTestId("wf-template-option-blank");
|
||||
expect(blank).toHaveAttribute("aria-checked", "true");
|
||||
fireEvent.keyDown(blank, { key: "ArrowDown" });
|
||||
expect(blank).toHaveAttribute("aria-checked", "false");
|
||||
expect(screen.getByTestId("wf-template-option-builtin:coding")).toHaveAttribute("aria-checked", "true");
|
||||
});
|
||||
|
||||
// ── Delete confirm ─────────────────────────────────────────────────────────
|
||||
|
||||
it("does not delete when no ConfirmDialogProvider is mounted (fallback cancels)", async () => {
|
||||
|
||||
@@ -6861,7 +6861,15 @@
|
||||
"saved": "Workflow saved",
|
||||
"savedNotCompilable": "Workflow saved but cannot be compiled",
|
||||
"saveFailed": "Failed to save workflow",
|
||||
"selectOrCreate": "Select or create a workflow to start editing."
|
||||
"selectOrCreate": "Select or create a workflow to start editing.",
|
||||
"templateBlank": "Blank",
|
||||
"templateBlankDescription": "Start from an empty start → end graph.",
|
||||
"templateCopyName": "{{name}} copy",
|
||||
"templateNodeCount_one": "{{count}} nodes",
|
||||
"templateNodeCount_other": "{{count}} nodes",
|
||||
"templatePickerLabel": "Start from",
|
||||
"templateSectionBuiltin": "Built-in workflows",
|
||||
"templateSectionYours": "Your workflows"
|
||||
},
|
||||
"workflowSelector": {
|
||||
"switchActiveMessage": "This task has an active session. Switching workflows aborts it and re-homes the card into the new workflow's entry column. Continue?",
|
||||
|
||||
10
packages/i18n/src/resources.d.ts
vendored
10
packages/i18n/src/resources.d.ts
vendored
@@ -6869,7 +6869,15 @@ export default interface Resources {
|
||||
"saveFailed": "Failed to save workflow",
|
||||
"saved": "Workflow saved",
|
||||
"savedNotCompilable": "Workflow saved but cannot be compiled",
|
||||
"selectOrCreate": "Select or create a workflow to start editing."
|
||||
"selectOrCreate": "Select or create a workflow to start editing.",
|
||||
"templateBlank": "Blank",
|
||||
"templateBlankDescription": "Start from an empty start → end graph.",
|
||||
"templateCopyName": "{{name}} copy",
|
||||
"templateNodeCount_one": "{{count}} nodes",
|
||||
"templateNodeCount_other": "{{count}} nodes",
|
||||
"templatePickerLabel": "Start from",
|
||||
"templateSectionBuiltin": "Built-in workflows",
|
||||
"templateSectionYours": "Your workflows"
|
||||
},
|
||||
"workspace": {
|
||||
"projectRoot": "Project Root",
|
||||
|
||||
Reference in New Issue
Block a user