Skip to content

FE-1500: Add controlled Petrinaut navigation - #9426

Merged
kube merged 5 commits into
mainfrom
claude/fe-1500-navigation
Sep 1, 2026
Merged

FE-1500: Add controlled Petrinaut navigation#9426
kube merged 5 commits into
mainfrom
claude/fe-1500-navigation

Conversation

@kube

@kube kube commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

Makes Petrinaut's app location controllable by the host through a new navigation prop: a router-neutral controller that reads and drives mode, Simulate section and resource, scenario, subnet, canvas selection, and creation drawers, and decides how each change is recorded in history. Without the prop, Petrinaut keeps its location in internal state and behaves as before. Stacked on #9359.

🔗 Related links

🔍 What does this change?

  • New react/navigation module: PetrinautNavigationState, the PetrinautNavigationController contract (state + onNavigate with updater functions + optional historyPolicy), navigation intents with a push-vs-replace history policy, and helpers such as openPetrinautSimulationResource and openPetrinautSubnet.
  • Moves the selection vocabulary into @hashintel/petrinaut-core: selectionItemTypes (now the source the SelectionItemType union derives from) and canonicalizeSelection are exported from a new dependency-free @hashintel/petrinaut-core/selection entry. Hosts that validate selection outside the app — a URL route, an HTTP handler — get the list and the ordering without bundling the model or React, and the editor's positional selection comparison can no longer disagree with a host's encoding.
  • Editor adoption: EditorProvider derives mode, Simulate view, drawer, and selection from navigation state; experiments, optimizations, simulation-scenario, and subnet selection navigate instead of setting local state; invalid locations normalize back with replace-history intents.
  • Selection gestures batch: React Flow callback bursts within one event coalesce through an optimistic-state ref, and drag-selection records one history entry per gesture.
  • Creation drawers become app locations: a central SimulationCreationDrawer host replaces the per-view drawer mounts, so opening and closing scenario, metric, experiment, and optimization drawers participates in Back/Forward.
  • Documents the behaviour in the user guide (drawing-a-net.md, experiments.md, visual-settings.md).
  • Review fixes: the optimistic-state ref re-syncs on every commit, so a host that declines a navigation (a guard, an aborted transition) cannot swallow later identical attempts; scenario switching goes through navigation only, with the effective-scenario effect owning simulation disposal and parameter resets; EditorProvider no longer seeds navigation-owned fields into its local state; the error banner's "Edit Item" button switches to edit mode even when the erroring item was deleted; removed two unused helper exports.

How a host injects its router

Petrinaut never imports a router. A host that wants URL-driven navigation passes a controller object; everything else is unchanged.

Without a router (default) — Petrinaut owns its location in internal state:

<Petrinaut handle={handle} />

With a router — the host owns the location. state is decoded from the host's current URL; onNavigate receives an updater and writes the result back to the URL:

const navigation: PetrinautNavigationController = {
  state: decodeSearch(search), // host URL -> PetrinautNavigationState
  onNavigate: (update, { history }) => {
    const next = update(decodeSearch(currentSearch()));
    void router.navigate({
      search: encodeSearch(next),
      replace: history === "replace",
    });
  },
};

return <Petrinaut handle={handle} navigation={navigation} />;

onNavigate receives an updater function rather than a computed state so that several navigations fired in one browser event each apply to the host's freshest state. The optional historyPolicy overrides the push-vs-replace default per navigation intent; an iframe host can pass historyPolicy: () => "replace" to keep the embed out of the parent page's history. #9362 wires this contract to TanStack Router for the website's example pages.

  • Review fixes (second round): canonicalizeSelection orders by UTF-16 code unit instead of localeCompare, so the canonical form agrees across locales and ICU builds (the two disagree on ids like place-b and placea); a creation drawer now layers over the record already open instead of clearing simulateResource, matching how the overlay is preserved on the adjacent line; the optimistic navigation preview is kept with the base it was derived from and expires with its event, which fixes a lost update when an asynchronous host rerenders mid-flight and a declined update swallowing the user's retry (both were written as failing tests first); deep-link normalization filters inside the updater rather than passing a render-time snapshot; the hamburger menu hides Layout on a read-only net, where it silently did nothing; removed the dead onCreated prop from both create drawers and the __reinitialize action, which this PR had turned into a partial reset with no caller.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

🛡 What tests cover this?

  • New: react/navigation/index.test.tsx (controlled and uncontrolled providers, optimistic state across same-event updates, duplicate suppression, declined-navigation retries, history policies, selection canonicalization, drawer conversions), react/state/editor-provider.test.tsx (selection gesture phases, react-flow microtask batching, interrupted gestures, deep-link normalization of unknown resources and stale selections), react/simulation/provider.test.tsx (scenario selection canonicalization), simulation-creation-drawer.test.tsx, and navigation assertions in the experiments and optimizations provider tests.
  • Existing: editor context fixtures updated across the suite.

❓ How to test this?

  1. Checkout the branch and run yarn workspace @hashintel/petrinaut dev.
  2. Confirm the editor behaves as before (Storybook passes no controller, so navigation is uncontrolled).
  3. Website-level Back/Forward behaviour is exercised by the examples PR stacked above (FE-1500: Add example models and the read-only example page #9362).

@kube kube self-assigned this Aug 29, 2026
@github-actions github-actions Bot added area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/apps > hash.design Affects the `hash.design` design site (app) labels Aug 29, 2026
@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 31, 2026 10:30pm
petrinaut Ready Ready Preview Aug 31, 2026 10:30pm
petrinaut-docs Ready Ready Preview Aug 31, 2026 10:30pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 31, 2026 10:30pm

Request Review

@kube
kube force-pushed the claude/fe-1500-navigation branch from 5895255 to e959446 Compare August 29, 2026 20:03
@kube
kube force-pushed the claude/fe-1500-navigation branch from e959446 to 7d0dac9 Compare August 30, 2026 17:24
@kube
kube changed the base branch from claude/fe-1500-presentation to codex/fe-1500-router August 31, 2026 01:03
@kube
kube force-pushed the claude/fe-1500-navigation branch from 7d0dac9 to d43067f Compare August 31, 2026 01:03
@kube
kube force-pushed the claude/fe-1500-navigation branch from d43067f to 1c3f652 Compare August 31, 2026 02:14
@kube
kube marked this pull request as ready for review August 31, 2026 02:17
@cursor

cursor Bot commented Aug 31, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Large refactor of where editor location lives and a new public embed API; mistakes could break deep links, history, or scenario/simulation sync, but behavior is heavily tested and unchanged when no controller is passed.

Overview
Adds a navigation prop on Petrinaut / PetrinautProvider: a router-neutral PetrinautNavigationController (state + onNavigate updaters, optional historyPolicy) so hosts can sync Edit/Simulate/Actual, Simulate section and open resource, scenario, subnet, canvas selection, viewport settings, and creation drawers with browser history. Without it, location stays in internal state as before.

@hashintel/petrinaut-core/selection now exports selectionItemTypes and canonicalizeSelection (UTF-16 ordering) so URL/server validation matches the editor. Editor/simulation/experiment/optimization/subnet flows navigate instead of holding parallel local state; EditorProvider derives mode, drawers, and selection from navigation, batches React Flow selection into one history entry per gesture, and normalizes invalid deep links with replace history. Creation UIs move to a shared SimulationCreationDrawer so create drawers stack on an already-open record; successful creates swap the create overlay for the new resource location. Layout is hidden in the hamburger menu on read-only nets; user docs describe Back/Forward vs document undo.

Reviewed by Cursor Bugbot for commit fe32d17. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread libs/@hashintel/petrinaut/src/react/navigation/index.tsx
@semgrep-code-hashintel

Copy link
Copy Markdown

Semgrep found 1 regex_dos finding:

  • apps/petrinaut-website/src/main/app/brunch-demo/brunch-endpoint.ts

Ensure that the regex used to compare with user supplied input is safe from regular expression denial of service.

Comment thread libs/@hashintel/petrinaut/src/react/state/editor-provider.tsx
Comment thread libs/@hashintel/petrinaut/src/react/state/editor-provider.tsx
CiaranMn
CiaranMn previously approved these changes Aug 31, 2026
Comment thread libs/@hashintel/petrinaut/src/react/navigation/index.tsx Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit eea0f99. Configure here.

Comment thread libs/@hashintel/petrinaut/src/react/state/editor-provider.tsx
@hash-release hash-release Bot mentioned this pull request Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash.design Affects the `hash.design` design site (app) area/apps area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants