From 4d718db5a02c199e9d686db6f17fe1e583aa735f Mon Sep 17 00:00:00 2001
From: Shine1i
Date: Sun, 1 Mar 2026 10:21:02 +0100
Subject: [PATCH] feat(recipe-studio): auto-fit editor viewport on tab switch,
track manual viewport adjustments
---
.../executions/execution-overview-tab.tsx | 2 +-
.../recipe-studio/recipe-studio-page.tsx | 56 +++++++++++++++++++
2 files changed, 57 insertions(+), 1 deletion(-)
diff --git a/studio/frontend/src/features/recipe-studio/components/executions/execution-overview-tab.tsx b/studio/frontend/src/features/recipe-studio/components/executions/execution-overview-tab.tsx
index f0808c1a5d..97f1be0513 100644
--- a/studio/frontend/src/features/recipe-studio/components/executions/execution-overview-tab.tsx
+++ b/studio/frontend/src/features/recipe-studio/components/executions/execution-overview-tab.tsx
@@ -104,7 +104,7 @@ export function ExecutionOverviewTab({
{nullRate?.toFixed(1) ?? "--"}%
- Dropped columns
+ Side-effect columns
{formatMetricValue(sideEffects.length)}
{sideEffects.length > 0 && (
diff --git a/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx b/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx
index bacd5506aa..27806f8e37 100644
--- a/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx
+++ b/studio/frontend/src/features/recipe-studio/recipe-studio-page.tsx
@@ -164,6 +164,9 @@ export function RecipeStudioPage({
ReactFlowInstance, Edge> | null
>(null);
const lastProcessedFitTickRef = useRef(0);
+ const previousActiveViewRef = useRef("editor");
+ const pendingEditorTabFitRef = useRef(false);
+ const viewportMovedSinceAutoFitRef = useRef(true);
const handleExecutionStart = useCallback(() => {
setActiveView("executions");
}, []);
@@ -336,6 +339,53 @@ export function RecipeStudioPage({
const runDialogLoading =
runDialogKind === "preview" ? previewLoading : fullLoading;
+ useEffect(() => {
+ if (previousActiveViewRef.current !== activeView && activeView === "editor") {
+ pendingEditorTabFitRef.current = true;
+ }
+ previousActiveViewRef.current = activeView;
+ }, [activeView]);
+
+ useEffect(() => {
+ if (
+ !reactFlowInstance ||
+ activeView !== "editor" ||
+ !pendingEditorTabFitRef.current
+ ) {
+ return;
+ }
+ pendingEditorTabFitRef.current = false;
+ if (!viewportMovedSinceAutoFitRef.current) {
+ return;
+ }
+ const targetNodes = getFitNodeIdsIgnoringNotes(reactFlowInstance.getNodes());
+ if (targetNodes.length === 0) {
+ return;
+ }
+ viewportMovedSinceAutoFitRef.current = false;
+ let frame2 = 0;
+ let frame3 = 0;
+ const frame1 = window.requestAnimationFrame(() => {
+ frame2 = window.requestAnimationFrame(() => {
+ frame3 = window.requestAnimationFrame(() => {
+ reactFlowInstance.fitView({
+ duration: 320,
+ nodes: targetNodes,
+ });
+ });
+ });
+ });
+ return () => {
+ window.cancelAnimationFrame(frame1);
+ if (frame2) {
+ window.cancelAnimationFrame(frame2);
+ }
+ if (frame3) {
+ window.cancelAnimationFrame(frame3);
+ }
+ };
+ }, [activeView, reactFlowInstance]);
+
useEffect(() => {
if (!reactFlowInstance || fitViewTick === 0 || activeView !== "editor") {
return;
@@ -348,6 +398,7 @@ export function RecipeStudioPage({
if (targetNodes.length === 0) {
return;
}
+ viewportMovedSinceAutoFitRef.current = false;
let frame2 = 0;
let frame3 = 0;
const frame1 = window.requestAnimationFrame(() => {
@@ -410,6 +461,11 @@ export function RecipeStudioPage({
onNodeClick={handleNodeClick}
onNodeDoubleClick={handleNodeDoubleClick}
isValidConnection={isValidConnection}
+ onMoveEnd={(event) => {
+ if (event) {
+ viewportMovedSinceAutoFitRef.current = true;
+ }
+ }}
nodesDraggable={canvasInteractive}
nodesConnectable={canvasInteractive}
elementsSelectable={canvasInteractive}