feat(recipe-studio): auto-fit editor viewport on tab switch, track manual viewport adjustments

This commit is contained in:
Shine1i 2026-03-01 10:21:02 +01:00
commit 4d718db5a0
2 changed files with 57 additions and 1 deletions

View file

@ -104,7 +104,7 @@ export function ExecutionOverviewTab({
<span className="font-semibold">{nullRate?.toFixed(1) ?? "--"}%</span>
</p>
<p className="flex items-center justify-between gap-3">
<span className="text-muted-foreground">Dropped columns</span>
<span className="text-muted-foreground">Side-effect columns</span>
<span className="font-semibold">{formatMetricValue(sideEffects.length)}</span>
</p>
{sideEffects.length > 0 && (

View file

@ -164,6 +164,9 @@ export function RecipeStudioPage({
ReactFlowInstance<Node<RecipeNodeData | RecipeGraphAuxNodeData>, Edge> | null
>(null);
const lastProcessedFitTickRef = useRef(0);
const previousActiveViewRef = useRef<RecipeStudioView>("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}