Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/components/Learn/tours/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ export type TourStep = StepType & {
| "connect-edge"
| "expand-folder"
| "library-search"
| "set-argument";
| "set-argument"
| "navigate-into-subgraph"
| "navigate-to-root"
| "unpack-subgraph"
| "multi-select-tasks"
| "create-subgraph";
targetWindowId?: string;
targetFolderName?: string;
targetArgumentName?: string;
targetSearchTerm?: string;
targetTaskName?: string;
targetComponentName?: string;
targetMinCount?: number;
targetEdge?: {
sourceTaskName: string;
sourcePortName: string;
Expand Down
10 changes: 10 additions & 0 deletions src/providers/TourProvider/tourActionLabels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export function tourActionLabel({ interaction }: TourActionLabelInput): string {
return "Connect the highlighted ports";
case "set-argument":
return "Set the highlighted value";
case "navigate-into-subgraph":
return "Open the highlighted subgraph";
case "navigate-to-root":
return "Return to the top level";
case "unpack-subgraph":
return "Unpack the subgraph";
case "multi-select-tasks":
return "Select the highlighted tasks";
case "create-subgraph":
return "Create a subgraph from the selected tasks";
default:
return GENERIC_LABEL;
}
Expand Down
179 changes: 176 additions & 3 deletions src/routes/v2/pages/Editor/components/EditorTourBridge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useTour } from "@reactour/tour";
import { useViewport } from "@xyflow/react";
import { reaction } from "mobx";
import { useEffect } from "react";

Expand Down Expand Up @@ -116,6 +117,15 @@ export function EditorTourBridge() {
const { steps, currentStep, setCurrentStep, setSteps, isOpen } = useTour();
const { windows, navigation, editor } = useSharedStores();
const { markStepComplete } = useTourProgress();
const { x: viewportX, y: viewportY, zoom: viewportZoom } = useViewport();

useEffect(() => {
if (!isOpen) return;
const rafId = requestAnimationFrame(() => {
window.dispatchEvent(new Event("resize"));
});
return () => cancelAnimationFrame(rafId);
}, [isOpen, viewportX, viewportY, viewportZoom]);

const step = steps[currentStep] as TourStep | undefined;
const interaction = step?.interaction;
Expand All @@ -125,15 +135,47 @@ export function EditorTourBridge() {
const ensureWindowRestoredId = step?.ensureWindowRestored;
const requiresTaskSelected = step?.requiresTaskSelected;
const libraryDragAllow = step?.targetComponentName ?? step?.targetTaskName;
const stepSelector = step?.selector;

useEffect(() => {
if (!isOpen) return;
if (!ensureWindowRestoredId) return;
const w = windows.getWindowById(ensureWindowRestoredId);
if (w && (w.state === "hidden" || w.isMinimized)) {
const wasHidden = !!w && (w.state === "hidden" || w.isMinimized);
if (wasHidden) {
w.restore();
}
}, [isOpen, ensureWindowRestoredId, currentStep, windows]);
if (!w) return;

let cancelled = false;
const start = Date.now();
const wantSelector = typeof stepSelector === "string" ? stepSelector : null;
const waitForDom = () => {
if (cancelled) return;
const found = wantSelector
? document.querySelector(wantSelector)
: document.querySelector(
`[data-dock-window="${ensureWindowRestoredId}"]`,
);
if (found || Date.now() - start > 1500) {
setSteps?.((prev) => [...prev]);
return;
}
window.setTimeout(waitForDom, 50);
};
window.setTimeout(waitForDom, 50);

return () => {
cancelled = true;
};
}, [
isOpen,
ensureWindowRestoredId,
currentStep,
windows,
stepSelector,
setSteps,
]);

useEffect(() => {
if (!isOpen) return undefined;
Expand Down Expand Up @@ -209,7 +251,6 @@ export function EditorTourBridge() {
};
}, [isOpen, libraryDragAllow]);

const stepSelector = step?.selector;
useEffect(() => {
if (!isOpen) return;
if (!resetLibrarySearchFlag) return;
Expand Down Expand Up @@ -552,6 +593,138 @@ export function EditorTourBridge() {
};
}

if (interaction === "navigate-into-subgraph") {
const targetName = step?.targetTaskName?.toLowerCase();
const baselineDepth = navigation.navigationDepth;

const matches = () => {
if (navigation.navigationDepth <= baselineDepth) return false;
if (!targetName) return true;
const last =
navigation.navigationPath[navigation.navigationPath.length - 1];
return last?.displayName?.toLowerCase() === targetName;
};

if (matches()) {
skip();
return stopFollow;
}

const dispose = reaction(
() => matches(),
(m) => {
if (m) {
dispose();
advance();
}
},
);

return () => {
stopFollow();
dispose();
};
}

if (interaction === "navigate-to-root") {
const isAtRoot = () => navigation.navigationDepth === 0;

if (isAtRoot()) {
skip();
return stopFollow;
}

const dispose = reaction(
() => isAtRoot(),
(m) => {
if (m) {
dispose();
advance();
}
},
);

return () => {
stopFollow();
dispose();
};
}

if (interaction === "unpack-subgraph") {
const countSubgraphTasks = () => {
const spec = navigation.activeSpec;
if (!spec) return 0;
return spec.tasks.filter((t) => t.subgraphSpec !== undefined).length;
};
const baseline = countSubgraphTasks();

const dispose = reaction(
() => countSubgraphTasks(),
(current) => {
if (current < baseline) {
dispose();
advance();
}
},
);

return () => {
stopFollow();
dispose();
};
}

if (interaction === "multi-select-tasks") {
const minCount = step?.targetMinCount ?? 2;

const taskSelectionCount = () =>
editor.multiSelection.filter((n) => n.type === "task").length;

if (taskSelectionCount() >= minCount) {
skip();
return stopFollow;
}

const dispose = reaction(
() => taskSelectionCount(),
(current) => {
if (current >= minCount) {
dispose();
advance();
}
},
);

return () => {
stopFollow();
dispose();
};
}

if (interaction === "create-subgraph") {
const countSubgraphTasks = () => {
const spec = navigation.activeSpec;
if (!spec) return 0;
return spec.tasks.filter((t) => t.subgraphSpec !== undefined).length;
};
const baseline = countSubgraphTasks();

const dispose = reaction(
() => countSubgraphTasks(),
(current) => {
if (current > baseline) {
dispose();
advance();
}
},
);

return () => {
stopFollow();
dispose();
};
}

if (isCountInteraction(interaction)) {
const baseline = countForInteraction(navigation.activeSpec, interaction);

Expand Down
Loading