Skip to content
Merged
51 changes: 51 additions & 0 deletions studio/docs/specs/react-studio-preview/parity-observations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"sourceHead": "5828b2e09d19abfe198098fbbac481cb734f7a13",
"workflowLogging": {
"submittedOption": "logPersistenceStrategyType",
"optionSurvivesReload": false,
"runtimeLogConfigPresent": false
},
"httpPorts": [
{
"type": "Elsa.FlowSendHttpRequest",
"descriptorPresent": true,
"configuredStatusCodes": [
200,
201
],
"actualPorts": [
"Done"
]
},
{
"type": "Elsa.DownloadHttpFile",
"descriptorPresent": true,
"configuredStatusCodes": [
200,
201
],
"actualPorts": [
"Done"
]
}
],
"httpEmbedded": {
"descriptorPresent": true,
"expectedSlot": "expectedStatusCodes.0.activity",
"actualSlots": [
"unmatchedStatusCode",
"failedToConnect",
"timeout"
]
},
"clipboard": {
"originalNames": [
"Task1",
"Task2"
],
"pastedNames": [
"Task3",
"Task3"
]
}
}
11 changes: 11 additions & 0 deletions studio/docs/specs/react-studio-preview/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,14 @@ records the wire contracts, visibility and verification boundaries. Browser fixt
exercise the actual production modules; a local reference-backend check verifies
save/reload of each setting and a published reusable activity's paired version IDs.
This does not close the broader partial-parity or release acceptance gates.


## Partial parity verification

The [evidence checklist](../../../react/docs/parity.md) covers every #8851 item.
Workflow metadata/settings, variable defaults, outcomes, versions and keyboard
history/clipboard now have mounted editor browser checks, with deeper designer-store
checks for IDs, nested clones and edge preservation. Confirmed gaps are tracked in
#8940 (workflow log persistence), #8941 (variable test values), #8942 (Applications/API
keys), #8943 (HTTP ports/embedded branches) and #8944 (batch paste names). The retained
sanitized observations identify the audited source head and real API boundaries.
1 change: 1 addition & 0 deletions studio/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ New to the project? Start with the [Getting started guide](docs/getting-started.
| [Getting started](docs/getting-started.md) | Install, run, connect to a server, first sign-in. |
| [Configuration](docs/configuration.md) | **Adding servers**, authentication providers, feature flags, theming, environment variables. |
| [Modules](docs/modules.md) | What every module does, how to enable it, and how to build a new one. |
| [Parity audit](docs/parity.md) | Verified editor behavior, evidence boundaries and linked Preview gaps. |
| [Activity settings](docs/activity-settings.md) | Commit, log persistence, resilience, task and version settings; instance retry history. |
| [Designer](docs/designer.md) | Containers, the quick-add palette, data flow, call context, keyboard shortcuts. |
| [Deployment](docs/deployment.md) | Docker image, standalone output, runtime configuration. |
Expand Down
38 changes: 38 additions & 0 deletions studio/react/docs/parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Partial parity audit

This is the evidence checklist for [#8851](https://github.com/elsa-workflows/elsa-core/issues/8851),
part of the 3.11 React Studio Preview. It describes the existing implementation and
confirmed gaps; it is not a declaration of complete Blazor parity. Baseline audited:
`5828b2e09d19abfe198098fbbac481cb734f7a13` on 2026-10-21.

| Area | Evidence and result | Follow-up |
| --- | --- | --- |
| Workflow settings | Actual editor browser test edits name/description, activation, incident and commit strategies, usable-as-activity, consumer auto-update and category through the normal save flow. Log persistence currently writes a field absent from server WorkflowOptions; a real Core save/reload drops it and creates no runtime configuration. | [#8940](https://github.com/elsa-workflows/elsa-core/issues/8940) fixes workflow-level logging. |
| Variable test values | Browser test edits and saves the separate default-value field. Source comparison with Blazor confirms there is no test-value editor for `customProperties.VariableTestValues`, which is keyed by variable ID. | [#8941](https://github.com/elsa-workflows/elsa-core/issues/8941); [#7402](https://github.com/elsa-workflows/elsa-core/issues/7402) is the separate user-scoped storage enhancement. |
| Outcomes | Actual editor browser test adds/removes named workflow outcomes, saves and reloads. Activity input dynamic-outcome tests separately cover JSON encoding, fixed outcomes and preservation of existing connection names. | No gap found in this scope. |
| Version viewer/delete | Actual editor browser test opens a historical version, verifies read-only metadata, returns to latest, confirms deletion of a non-current unpublished version, and checks the version ID sent to the API and list refresh. The UI uses the same definitions/delete permission as the server endpoint. | No gap found in this scope. |
| Applications/API keys | API-client operations exist. Security registers Users and Roles only; no application/API-key route or screen exists. No live credentials were created or logged during the audit. | [#8942](https://github.com/elsa-workflows/elsa-core/issues/8942). |
| Undo/redo | Designer-store tests restore activity settings and connections, clear redo after another edit, and undo nested duplication. Actual canvas keyboard test uses undo/redo after paste and verifies the saved/reloaded graph. | No gap found in those operations. |
| Copy/paste | Store tests verify new IDs/node IDs, deep-copy isolation, internal-edge remapping, exclusion of external edges, cut/undo and nested descendant IDs. Actual canvas keyboard test verifies normal paste/save/reload. Copying Task1 and Task2 together yields two Task3 names, confirmed through the public store API. | [#8944](https://github.com/elsa-workflows/elsa-core/issues/8944) fixes batch name allocation. |
| Activity port providers | Existing Switch slot tests cover discovery, assignment, traversal and fallback. Dynamic-outcome tests verify rendered port identities. Real Core HTTP descriptors plus status codes 200/201 produce only Done flow ports; SendHttpRequest status-code child slots are absent. | [#8943](https://github.com/elsa-workflows/elsa-core/issues/8943) adds HTTP provider parity. |

The per-activity commit/log/resilience/task/version settings and instance retries
have their own [contract and verification checklist](activity-settings.md).
BPMN, localization, tenant switching and C# completion remain separately owned epic
children. The selectable BPMN designer work in #8929 belongs to its own epic #8898
and explicitly depends on the import/export parity work here.

Evidence lives in `tests/parity.spec.ts`, `packages/sdk/src/designer/store.test.ts`,
the existing specialized-editor/Switch tests, and the sanitized
[reference-backend observations](../../docs/specs/react-studio-preview/parity-observations.json).
The browser tests use the production ESM modules and real ASP.NET host with
controlled API responses; they verify editing and HTTP contracts. The logging
observation uses the real Core API and removes its temporary definition. HTTP
observations use real Core descriptors with local SDK port/slot computation; no
external HTTP activity is executed. These checks do not claim that every production
feature or permission combination has been exercised.

Run `pnpm test`, `pnpm lint`, `pnpm typecheck` and `pnpm test:browser` from
`studio/react` after building the production assets and standalone host as described
in the deployment guide. Update this checklist and its observations as the linked
follow-ups are delivered.
85 changes: 85 additions & 0 deletions studio/react/packages/sdk/src/designer/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from "vitest";
import type { Activity, Flowchart } from "@elsa-workflows/react-studio-api-client/types/activities";
import { createDesignerStore } from "./store.js";

function graph(): Flowchart {
return { id: "root", nodeId: "root", type: "Elsa.Flowchart", version: 1, activities: [
{ id: "alpha", nodeId: "root:alpha", type: "Elsa.WriteLine", version: 1, name: "Alpha1", customProperties: { canStartWorkflow: true },
text: { typeName: "String", expression: { type: "Literal", value: "original" } }, metadata: { designer: { position: { x: 10, y: 20 } } } },
{ id: "beta", nodeId: "root:beta", type: "Elsa.WriteLine", version: 1, name: "Beta1" },
{ id: "outside", nodeId: "root:outside", type: "Elsa.WriteLine", version: 1, name: "Outside1" },
], connections: [
{ source: { activity: "alpha", port: "Done" }, target: { activity: "beta", port: "In" } },
{ source: { activity: "beta", port: "Done" }, target: { activity: "outside", port: "In" } },
] };
}

function current(store: ReturnType<typeof createDesignerStore>) { return store.getState().root as Flowchart; }

describe("designer history and clipboard parity", () => {
it("undoes and redoes activity data without changing connections and clears redo after another edit", () => {
const store = createDesignerStore(graph());
const original = structuredClone(current(store));
store.getState().updateActivity("alpha", activity => ({ ...activity, customProperties: { ...activity.customProperties, commitStrategyName: "CommitAlways" } }));
expect(current(store).activities[0].customProperties?.commitStrategyName).toBe("CommitAlways");
store.getState().undo();
expect(current(store)).toEqual(original);
store.getState().redo();
expect(current(store).activities[0].customProperties?.commitStrategyName).toBe("CommitAlways");
expect(current(store).connections).toEqual(original.connections);
store.getState().undo();
store.getState().updateActivity("alpha", activity => ({ ...activity, name: "Different" }));
store.getState().redo();
expect(current(store).activities[0].name).toBe("Different");
expect(store.getState().future).toHaveLength(0);
});

it("pastes deep copies with new IDs and only connections between copied activities", () => {
const store = createDesignerStore(graph());
store.getState().select(["alpha", "beta"]);
store.getState().copySelection();
store.getState().paste();
const root = current(store);
const [alpha, beta] = root.activities.slice(3);
expect(root.activities).toHaveLength(5);
expect(alpha.id).not.toBe("alpha");
expect(beta.id).not.toBe("beta");
expect(alpha.nodeId).toBe(`root:${alpha.id}`);
expect(alpha.customProperties?.canStartWorkflow).toBe(false);
expect(alpha.metadata?.designer?.position).toEqual({ x: 50, y: 60 });
expect(root.connections).toHaveLength(3);
expect(root.connections[2]).toEqual({ source: { activity: alpha.id, port: "Done" }, target: { activity: beta.id, port: "In" } });
expect(store.getState().selectedIds).toEqual([alpha.id, beta.id]);
const copiedText = alpha.text as { expression: { value: string } };
copiedText.expression.value = "changed copy";
expect((root.activities[0].text as typeof copiedText).expression.value).toBe("original");
});

it("cuts the selection and its edges, supports undo, and retains a pasteable clipboard", () => {
const store = createDesignerStore(graph());
const original = structuredClone(current(store));
store.getState().select(["alpha", "beta"]);
store.getState().cut();
expect(current(store).activities.map(activity => activity.id)).toEqual(["outside"]);
expect(current(store).connections).toEqual([]);
store.getState().undo();
expect(current(store)).toEqual(original);
store.getState().paste();
expect(current(store).activities).toHaveLength(5);
expect(current(store).connections).toHaveLength(3);
});

it("duplicates a nested activity without reusing descendant IDs", () => {
const nested: Activity = { id: "nested", type: "Elsa.Sequence", version: 1, name: "Sequence1", activities: [{ id: "inner", type: "Elsa.WriteLine", version: 1 }] };
const initial = graph();
initial.activities.push(nested);
const store = createDesignerStore(initial);
store.getState().duplicate("nested");
const copy = current(store).activities.at(-1)!;
expect(copy.id).not.toBe("nested");
expect((copy.activities as Activity[])[0].id).not.toBe("inner");
expect((copy.activities as Activity[])[0].type).toBe("Elsa.WriteLine");
store.getState().undo();
expect(current(store).activities).toEqual(initial.activities);
});
});
Loading
Loading