Skip to content

fix(app-shell): the flow designer's three add-node paths open one palette and add into the path (objectui#11778) - #11832

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11778-flow-add-node-paths
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11778-flow-add-node-paths

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11778

Clause-②: no

What was wrong, measured on main (2b30d39)

A probe through the real FlowCanvas on the draft Studio creates for a new flow (start → end, edge e1), before any change:

Gesture What it wrote
the edge's "+" (Insert node here) a create_record node at once, no palette, pinned at position {x: 28, y: 89}: the midpoint of the two cards' corners, on top of the Start card
Start's "Add connected node" a create_record node at once, no palette, wired start → node_1 beside start → end: a second branch that leads nowhere
toolbar Add node, nothing selected the picked notify node with no edge at all
toolbar Add node, Start selected the picked notify node as a second branch start → node_1 beside start → end (the card's report)
the Node Type select on Start the trigger read the raw start; the list was the hand list FLOW_NODE_TYPE_OPTIONS, which has no notify and spells http_request, connector_action and try_catch raw

What changed

  • Every "+" opens the one add-node palette. The edge's "+" and a card's "Add connected node" now wrap their button in the same NodePalette the toolbar uses (grouped, searchable, with Recently used). Nothing is added until a type is picked. The canvas keeps one key for which "+" has the palette open (toolbar, edge:EDGEKEY or node:ID), so at most one is open and a pick closes it wherever it was opened. No second picker was built.

  • The picked node goes into the path, by one named rule. The edge "+" splits its edge, as before: A → B becomes A → N (the original edge, its id and guard kept) plus N → B. The other two entry points share placeAfter, "add after this node":

    • the anchor is the card's node for "Add connected node", and for the toolbar the selected node, else Start;
    • an anchor with exactly one way on: split that edge, so N takes the next node's place in the path (start → end becomes start → N → end);
    • an anchor with no way on: append (anchor → N);
    • a branching anchor (decision, approval, parallel_gateway), or one that already leads to two or more nodes, has no single path after it: N becomes a new branch from it. That is the existing append, so a decision still carries its next unused condition onto the new edge;
    • End has no "after": with End selected the toolbar puts N before End, splitting its one way in;
    • no single path to insert into — an End reached by several paths or by none, or a flow with no Start and nothing selected: N is added with no edge, and the Problems panel names it unreachable, as it names any unconnected node.

    "A way on" is a forward edge between two distinct nodes that both exist: a declared back-edge is not one, and neither is an edge naming a missing node (objectui#11772 leaves those for the Problems panel to name).
    Because the canvas selects the node it adds, toolbar adds in a row build the path in order: start → N1 → N2 → N3 → end (pinned).

  • The inserted node is laid out. The edge insert no longer pins position at its endpoints' midpoint. The new node carries no position, like an append always did, so the layered layout gives it its own row and moves the nodes below it down. Nodes the author dragged keep their stored positions.

  • The Node Type select is the palette's list. FlowNodeInspector reads useFlowNodePalette() (the hardcoded NODE_PALETTE merged with the engine's published descriptors, the same list the canvas adds from) through a new module-local helper paletteTypeOptions, which keeps the palette's order (its category sections) and its display names (translateNodeLabel over the palette's label, exactly as the palette draws them). So notify is offered, no raw type name is shown, and an engine-published type joins the list as it joins the palette. A stored type the palette does not offer (start, the deprecated alias http_request, a plugin type whose engine is not answering) is added for that node only, under its display name, so the trigger still says what the node is.

  • FLOW_NODE_TYPE_OPTIONS is retired. The select was its only production reader. Its six test readers used it as the population of a sweep over the inspector's field-group table; they now walk FLOW_NODE_CONFIG_TYPES, the table's own keys, a strict superset of the retired list (it adds notify, boundary_event, parallel_gateway, join_gateway, legacy_action), so no sweep lost a type. flow-node-config.spec-reconciliation.test.ts now also asserts its source-parsed table keys equal that export exactly.

The PM's mechanism hypotheses, measured

  • H1 (what each entry point produced): confirmed, with the two open halves now measured (table above). Add connected node called addNode('create_record', { from }), and the toolbar passed from: selectedId, so with nothing selected it wrote no edge and with Start selected it wrote the second branch.
  • H2 (the rule for "inserted into the path"): decided as above. "After the selected node" for the toolbar, with Start standing in for no selection, because the canvas selects every node it adds: repeated adds then extend the path in order instead of stacking after Start. "Add connected node" gets the same rule so the two never disagree; a decision or an approval keeps getting a new branch, because their out-edges are their outcomes and the canvas has no other gesture that draws an edge.
  • H3 (dropping the pin is the auto-layout): confirmed. Nothing else was needed; the layered layout already places an unpinned node. The objectui#11772 round trip is untouched by it: inserting and then removing the node gives the draft back byte for byte, and that pin stayed green under every ablation below, including the one that restores the pin.
  • H4 (FLOW_NODE_TYPE_OPTIONS readers): one production reader (the select) and six test readers, handled as above.
  • H5 (reuse NodePalette): confirmed, with one finding. A palette opened from an edge or a card is portaled out of the canvas in the DOM but still sits inside the canvas viewport in the React tree, so a press on a palette row bubbled to the viewport's pan handler, which clears the selection and takes pointer capture; the browser then fires the click at the viewport and the row's select never runs (objectui#11546's shape). NodePalette's content now stops that press. Ablation B below shows the five edge- and card-palette picks red without it, and the toolbar's (outside the viewport) green.

objectui#11772's pins

FlowPreview.removeNode-11772.test.tsx changed in three places, none of them an assertion's strength:

  • its palette hook stub returned [], which is not the hook's offline answer (NODE_PALETTE, as the stub's own comment says) and leaves a palette with nothing to pick; it returns NODE_PALETTE now;
  • its two gesture helpers pick "Create record" in the palette after the "+", the type the "+" used to add on its own;
  • the control "adding a connected node mints exactly what it did before" keeps its exact ids (node_1, edge_1) and its exact toEqual, but its wiring is this card's: e1: start → node_1, edge_1: node_1 → end, where it used to pin the second branch start → node_1 beside start → end, which is the defect this card reports.

The byte-for-byte round trip assertion is unchanged and green. FlowNodeInspector.removeEdges-11772.test.tsx and flow-problems.repeatedEdge-11772.test.ts are untouched and green.

Tests

New pins, through the real components:

  • previews/FlowCanvas.addNodePaths-11778.test.tsx (11): each "+" opens the palette and adds nothing before a pick; the edge "+" with Notify gives start → notify → end, keeps e1, and the node has no position and sits a full row below Start with End moved below it; Add connected node on Start inserts into the path, on a node with no way on appends, on a decision adds a branch carrying its Else default; the toolbar with nothing selected, with Start selected and with End selected, and three adds in a row. Palette rows are pressed with browserClick, which routes a click the way a browser does after pointer capture.
  • inspectors/FlowNodeInspector.nodeTypeOptions-11778.test.tsx (6): the option list equals the palette's labels in its order, Notify included; no raw type name; picking Notify writes type: 'notify'; zh-CN names; Start shown as "Start" on its own node and not offered to another; an engine-published type joins the list.

Touched existing tests: FlowCanvas.test.tsx (the insert pin now asserts no position, and both gestures pick a type), flow-node-producers.label.test.tsx (both gestures pick a type), FlowNodeInspector.connectorInput.test.tsx (its text query for the "Connector" field label is scoped to the label, because the Node Type trigger now reads "Connector" too), and the six sweep tests above.

Ablations, each through ablation-replace.mjs from the committed tree at 84f3800, each restore proven by blob hash equal to HEAD and an empty git diff HEAD, over the two new pin files, FlowCanvas.test.tsx and the objectui#11772 pin file (50 tests):

Mutation Red
A: the edge "+" adds create_record at once, no palette (the card's suggested ablation) 6: the three edge-"+" pins, the 11772 reproduction and round trip (they now need a palette row), the FlowCanvas.test.tsx insert pin
B: the palette no longer stops a press on its rows 5: every edge- and card-palette pick; the four toolbar pins stay green
C: the toolbar back to addNode(type, { from: selectedId }) 4: all four toolbar pins
D: Add connected node back to a plain from append 2: the Start pin and the 11772 control
E: the edge insert pinned at the midpoint again 4: the layout pin, the Start insert, both FlowCanvas.test.tsx position pins; the 11772 round trip stays green
F: the select back to the hand list with raw names 6: all six inspector pins

The first attempt at B measured nothing: the tool refused it before running, because the replacement text was a substring of the anchor. It was re-run with a distinct marker; the row above is that run.

Browser check: Chromium (/opt/pw-browsers/chromium) on the console's preview gallery (preview-gallery.html?only=flow, the offline palette), real mouse clicks. The edge "+" opened the palette beside the "+" and added nothing; picking Notify inserted node_1 between Start and the next node on its own row (card tops after the pick: Start 28, new node 150, the next node 272); a card's "+" then Create record inserted below that card; the toolbar then put a Script node after the selected one; the inspector's Node Type read "Script" and listed the palette's 18 names, Notify among them; no page errors.

Local verification at 3947a9a (HEAD):

  • pnpm exec vitest run over packages/app-shell/src/views/metadata-admin/previews/, .../inspectors/ and the two Studio files that count the canvas's "+" buttons: 230 files, 3043 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell type-check: exit 0. Both of its projects ran; tsconfig.test.json --listFiles includes every touched and new test file.
  • pnpm exec eslint on the 16 touched .ts/.tsx files: 0 errors, 12 warnings, one more than on main (a react-refresh/only-export-components warning for the new paletteTypeOptions export, the same warning the file's seven other non-component exports carry).
  • check:control-bytes, check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check:new-line-citations, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:unreferenced-sources, check-changeset-presence, check-changeset-no-major: all exit 0.
  • The whole @object-ui/app-shell suite ran once, at acdf998: 1064 files, 2 failed. One was FlowNodeInspector.connectorInput.test.tsx, the label query fixed in 84f3800. The other, AppContent.inaccessibleAppStrand.test.tsx, failed on a network escape to localhost:3000 under full-suite load; this branch does not touch it, and it passed alone. The suite was not re-run after 84f3800, which changed a test file and the changeset only; CI runs it.

Files and surface

Every file is on the claim's file surface: FlowCanvas.tsx, flow-canvas-parts.tsx, flow-node-config.ts, FlowNodeInspector.tsx, tests in previews/ and inspectors/, and .changeset/11778-flow-add-node-paths.md. i18n.ts is not touched: every string these entry points show already existed. StudioDesignSurface.tsx is not touched.

Nothing is added to the package entry. After a build, none of the four changed source modules is reachable from dist/index.d.ts by its relative imports (168 declaration files reached; SchemaForm.d.ts, which the entry re-exports, is the positive control). Patch changeset on @object-ui/app-shell.

Acceptance notes

  • content/docs/guide/flow-designer.md still describes the card's "+" as "Append a connected child node", and its palette section names only the toolbar's Add node. It is not wrong, but it does not mention that every "+" opens the palette or the edge "+". It is off the claim's surface, so it is not edited here.
  • The inspector now reads GET /api/v1/automation/actions twice per mount (useActionConfigSchemas and useFlowNodePalette each run useActionDescriptors). Sharing one read means changing useFlowNodePalette.ts, which is off the surface; nearly every inspector test stubs that module by name, so a combined hook would also need those stubs changed.
  • Offline, the Node Type list no longer offers map, which the hand list had: NODE_PALETTE does not list it. The engine publishes a map descriptor, so against a running backend the palette and the list both offer it.
  • When nodes around an insert carry dragged positions, the new node takes its layout slot, not a point between its pinned neighbours.
  • main moved to d53fd02 after this branch was cut (objectui#11773, objectui#11780). Neither commit touches the flow canvas or inspector files; this branch has not merged it.

Generated by Claude Code

claude added 3 commits October 7, 2026 19:48
…ette and add into the path

Every "+" on the flow canvas (an edge's, a node's Add connected node, the
toolbar's Add node) opens the same add-node palette, and the picked node
goes into the flow's path: the edge "+" splits its edge, the other two put
the node after a node (the selected one, else Start, for the toolbar) by
one rule, `placeAfter`. A node inserted on an edge is no longer pinned at
its endpoints' midpoint, so the layered layout places it. The node
inspector's Node Type select offers the palette's list with its display
names, so `notify` is offered and no raw type name is shown; the hand list
FLOW_NODE_TYPE_OPTIONS it read is retired, and the table's own types
(FLOW_NODE_CONFIG_TYPES) are what the sweep tests walk.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…ot the Node Type trigger

The Node Type select now names a connector_action node "Connector" (the
palette's name), so an unscoped text query for "Connector" found two
elements. The pin asks for the field's label, which is what it meant.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3519.1 KB 3551.8 KB
Main entry chunk (gzip) 156.8 KB 350 KB
Entry file index-B1OdDL51.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 579.68KB 139.46KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.88KB 67.46KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 38.37KB 10.31KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.82KB 39.17KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.86KB 65.27KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 239.75KB 65.92KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.07KB 22.93KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants