feat: Guided Tours Framework (Subgraphs)#2365
Conversation
🎩 PreviewA preview build has been created at: |
49656f5 to
a182d2f
Compare
57b8cdd to
afb2772
Compare
a182d2f to
5adf525
Compare
113a417 to
78cc132
Compare
5adf525 to
514c488
Compare
78cc132 to
7767801
Compare
514c488 to
05098d2
Compare
7767801 to
ee03e31
Compare
05098d2 to
b571f54
Compare
ee03e31 to
8dd2a4e
Compare
b571f54 to
0124745
Compare
8dd2a4e to
d60f1d2
Compare
0124745 to
75ab404
Compare
d60f1d2 to
721f5de
Compare
677a1a3 to
601b598
Compare
4ee52d2 to
65b41d0
Compare
0bf07b8 to
ccceaaf
Compare
108348f to
f153d01
Compare
ccceaaf to
d97d028
Compare
f153d01 to
7941742
Compare
d97d028 to
adb10d0
Compare
7941742 to
4071803
Compare
adb10d0 to
3830d88
Compare
4071803 to
d89b58f
Compare
3830d88 to
0f6861d
Compare
d89b58f to
59dc796
Compare
0f6861d to
4ba9aae
Compare
| } | ||
|
|
||
| if (interaction === "unpack-subgraph") { | ||
| const countSubgraphTasks = () => { |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
This countSubgraphTasks is defined identically here and in the create-subgraph branch (line ~722) — both count spec.tasks.filter((t) => t.subgraphSpec !== undefined).length. The two branches differ only in direction (current < baseline for unpack, current > baseline for create).
This is the same count-delta logic the file already centralizes in countForInteraction / isCountInteraction at the top. Per project-conventions (extract duplicated logic, especially against helpers established in the same file), hoist a module-level function countSubgraphTasks(spec: ComponentSpec | null): number next to countForInteraction and have both branches call it — each branch then only owns its direction check.
| let cancelled = false; | ||
| const start = Date.now(); | ||
| const wantSelector = typeof stepSelector === "string" ? stepSelector : null; | ||
| const waitForDom = () => { |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
This waitForDom loop is structurally identical to the tryRefresh loop in the resetLibrarySearch effect (around line 292): same Date.now() 1500ms budget, same 50ms setTimeout recursion, same cancelled guard, and the same setSteps?.((prev) => [...prev]) trick to force reactour to re-query the step selector.
Two copies of a non-obvious mechanism (the setSteps clone especially) will drift. Since this PR introduces the second copy, it's the right place to extract — e.g. a shared pollForSelectorThenRefreshSteps({ wantSelector, fallbackSelector, setSteps }) that returns a cancel function, called from both effects.
| const { steps, currentStep, setCurrentStep, setSteps, isOpen } = useTour(); | ||
| const { windows, navigation, editor } = useSharedStores(); | ||
| const { markStepComplete, isStepComplete } = useTourProgress(); | ||
| const { x: viewportX, y: viewportY, zoom: viewportZoom } = useViewport(); |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
useViewport is called unconditionally, so EditorTourBridge (mounted whenever the editor is) re-renders on every viewport change — pan and zoom, every frame — regardless of isOpen.
The render itself is cheap (returns null, and the heavy interaction effect's deps don't change), and the resize-dispatch effect below is correctly isOpen-gated and rAF-guarded, so this is Low — just flagging that the subscription cost is paid outside tour mode too. If it ever shows up in profiling, scoping the viewport-follow into a child that mounts only when isOpen would isolate the subscription; otherwise not worth the added indirection.
| if (!targetName) return true; | ||
| const last = | ||
| navigation.navigationPath[navigation.navigationPath.length - 1]; | ||
| return last?.displayName?.toLowerCase() === targetName; |
There was a problem hiding this comment.
🤖 This is an AI-generated code review comment.
Minor: displayName is a required string on NavigationEntry, so the second optional chain is redundant — last?.displayName.toLowerCase() suffices (the last?. already short-circuits when the path entry is undefined).

Description
The subgraph-specific interaction mechanics for the guided-tour framework, layered on the framework stack (#2348 / #2299 / #2347 / #2340). Foundation for the Subgraphs tour (#2366) — this PR adds the interaction types and the bridge logic that detects them; the tour content lands in #2366.
Five new tour interactions cover navigating and authoring subgraphs, plus a canvas re-measurement fix so spotlights track the viewport when navigation changes the view.
What changed
Interaction vocabulary (
src/components/Learn/tours/registry.ts)Five new
TourStep.interactionvalues, plus atargetMinCountfield:navigate-into-subgraph— descends a level (optionally gated to a specifictargetTaskName)navigate-to-root— returns to the top levelunpack-subgraph— the active spec's subgraph-task count drops (a subgraph was flattened)multi-select-tasks— ≥targetMinCount(default 2) task nodes are selectedcreate-subgraph— the subgraph-task count rises (a subgraph was created)Bridge handlers (
src/routes/v2/pages/Editor/components/EditorTourBridge.tsx)Each interaction is detected by observing the existing MobX stores —
navigation.navigationDepth/navigationPathfor level changes,navigation.activeSpec.tasks(countingsubgraphSpec) for unpack/create, andeditor.multiSelectionfor multi-select. Following the model from #2299, a detected interaction marks the step complete in the shared tour-progress state (it does not advance the tour itself); already-satisfied-on-entry states are marked complete immediately. Gated "Next" / auto-advance (#2340) handle progression.Contextual checklist labels
Adds the checklist copy for these interactions (using the contextual bold-target support from #2340):
navigate-into-subgraph→ "Open the {targetTaskName} subgraph"multi-select-tasks→ "Select {targetMinCount} tasks"navigate-to-root→ "Return to the top level"unpack-subgraph→ "Unpack the subgraph"create-subgraph→ "Create a subgraph from the selected tasks"Spotlight re-measurement while navigating
resizeevent (viarequestAnimationFrame) whenever the xyflow viewport pans/zooms during an open tour, so reactour re-positions the highlight/popover after navigating into a subgraph shifts the canvas.ensureWindowRestorednow waits for the target DOM to actually appear — the step's ownselector, else the dock window — polling up to 1.5s before forcing a re-measure, instead of measuring against a not-yet-rendered target.Related Issue and Pull requests
Progresses https://github.com/Shopify/oasis-frontend/issues/583
Builds on the framework stack (#2348 / #2299 / #2347 / #2340); consumed by the Subgraphs tour in #2366.
Type of Change
Checklist
Test Instructions
The registry is otherwise inert in this PR — the interactions are exercised end-to-end by the Subgraphs tour in #2366. To verify in isolation:
ensureWindowRestoredsteps, and the dock/window highlight steps still work.Additional Comments
Selector/anchor additions and the seed pipeline for the subgraph tour live in #2366, following the convention that tour-specific DOM anchors ship alongside the tour that needs them.