fix(recipe-studio): preserve note positions during auto-layout and fit workflow only
This commit is contained in:
parent
d1047646a9
commit
a53d8cb272
5 changed files with 61 additions and 10 deletions
|
|
@ -5,6 +5,7 @@ import {
|
|||
useUpdateNodeInternals,
|
||||
} from "@xyflow/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getFitNodeIdsIgnoringNotes } from "../../utils/graph/fit-view";
|
||||
|
||||
type LayoutControlsProps = {
|
||||
direction: "LR" | "TB";
|
||||
|
|
@ -32,20 +33,29 @@ export function LayoutControls({
|
|||
requestAnimationFrame(() => {
|
||||
refreshNodeInternals();
|
||||
requestAnimationFrame(() => {
|
||||
fitView({ duration: 250 });
|
||||
fitView({
|
||||
duration: 250,
|
||||
nodes: getFitNodeIdsIgnoringNotes(getNodes()),
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [fitView, onLayout, refreshNodeInternals]);
|
||||
}, [fitView, getNodes, onLayout, refreshNodeInternals]);
|
||||
|
||||
const handleToggleDirection = useCallback(() => {
|
||||
onToggleDirection();
|
||||
requestAnimationFrame(() => {
|
||||
refreshNodeInternals();
|
||||
onLayout();
|
||||
requestAnimationFrame(() => {
|
||||
refreshNodeInternals();
|
||||
requestAnimationFrame(() => {
|
||||
fitView({
|
||||
duration: 250,
|
||||
nodes: getFitNodeIdsIgnoringNotes(getNodes()),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [onToggleDirection, refreshNodeInternals]);
|
||||
}, [fitView, getNodes, onLayout, onToggleDirection, refreshNodeInternals]);
|
||||
|
||||
return (
|
||||
<Panel position="top-left" className="m-3 flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { type ReactElement, useCallback } from "react";
|
|||
import { Lock, LockOpen, Maximize2, Minus, Plus } from "lucide-react";
|
||||
import { Panel, useReactFlow } from "@xyflow/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getFitNodeIdsIgnoringNotes } from "../../utils/graph/fit-view";
|
||||
import { RECIPE_FLOATING_ICON_BUTTON_CLASS } from "../recipe-floating-icon-button-class";
|
||||
|
||||
type ViewportControlsProps = {
|
||||
|
|
@ -13,7 +14,7 @@ export function ViewportControls({
|
|||
interactive,
|
||||
onToggleInteractive,
|
||||
}: ViewportControlsProps): ReactElement {
|
||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||
const { zoomIn, zoomOut, fitView, getNodes } = useReactFlow();
|
||||
|
||||
const handleZoomIn = useCallback(() => {
|
||||
zoomIn({ duration: 150 });
|
||||
|
|
@ -24,8 +25,11 @@ export function ViewportControls({
|
|||
}, [zoomOut]);
|
||||
|
||||
const handleFitView = useCallback(() => {
|
||||
fitView({ duration: 250 });
|
||||
}, [fitView]);
|
||||
fitView({
|
||||
duration: 250,
|
||||
nodes: getFitNodeIdsIgnoringNotes(getNodes()),
|
||||
});
|
||||
}, [fitView, getNodes]);
|
||||
|
||||
return (
|
||||
<Panel position="bottom-left" className="m-3 flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import type {
|
|||
RecipeNodeData,
|
||||
} from "./types";
|
||||
import { deriveDisplayGraph } from "./utils/graph/derive-display-graph";
|
||||
import { getFitNodeIdsIgnoringNotes } from "./utils/graph/fit-view";
|
||||
import { buildRecipePayload } from "./utils/payload";
|
||||
import type { RecipePayload } from "./utils/payload/types";
|
||||
import { buildDefaultSchemaTransform } from "./utils/processors";
|
||||
|
|
@ -370,7 +371,10 @@ export function RecipeStudioPage({
|
|||
let frame2 = 0;
|
||||
const frame1 = window.requestAnimationFrame(() => {
|
||||
frame2 = window.requestAnimationFrame(() => {
|
||||
reactFlowInstance.fitView({ duration: 250 });
|
||||
reactFlowInstance.fitView({
|
||||
duration: 250,
|
||||
nodes: getFitNodeIdsIgnoringNotes(reactFlowInstance.getNodes()),
|
||||
});
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
|
|
@ -421,7 +425,7 @@ export function RecipeStudioPage({
|
|||
nodesDraggable={interactive}
|
||||
nodesConnectable={interactive}
|
||||
elementsSelectable={interactive}
|
||||
fitView={true}
|
||||
fitView={false}
|
||||
className="h-full w-full rounded-t-none"
|
||||
>
|
||||
<LayoutControls
|
||||
|
|
|
|||
|
|
@ -258,6 +258,12 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
applyLayout: () =>
|
||||
set((state) => {
|
||||
const isTopBottom = state.layoutDirection === "TB";
|
||||
const noteNodeIds = new Set(
|
||||
Object.values(state.configs)
|
||||
.filter((config) => config.kind === "markdown_note")
|
||||
.map((config) => config.id),
|
||||
);
|
||||
|
||||
const displayGraph = deriveDisplayGraph({
|
||||
nodes: state.nodes,
|
||||
edges: state.edges,
|
||||
|
|
@ -266,7 +272,14 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
auxNodePositions: {},
|
||||
llmAuxVisibility: state.llmAuxVisibility,
|
||||
});
|
||||
const { nodes } = getLayoutedElements(displayGraph.nodes, displayGraph.edges, {
|
||||
const layoutNodes = displayGraph.nodes.filter(
|
||||
(node) => !noteNodeIds.has(node.id),
|
||||
);
|
||||
const layoutNodeIds = new Set(layoutNodes.map((node) => node.id));
|
||||
const layoutEdges = displayGraph.edges.filter(
|
||||
(edge) => layoutNodeIds.has(edge.source) && layoutNodeIds.has(edge.target),
|
||||
);
|
||||
const { nodes } = getLayoutedElements(layoutNodes, layoutEdges, {
|
||||
direction: state.layoutDirection,
|
||||
nodesep: isTopBottom ? 120 : 80,
|
||||
ranksep: isTopBottom ? 140 : 80,
|
||||
|
|
@ -275,6 +288,9 @@ export const useRecipeStudioStore = create<RecipeStudioState>((set, get) => ({
|
|||
nodes.map((node) => [node.id, node.position] as const),
|
||||
);
|
||||
const nextNodes = state.nodes.map((node) => {
|
||||
if (noteNodeIds.has(node.id)) {
|
||||
return node;
|
||||
}
|
||||
const position = layoutedPositions.get(node.id);
|
||||
if (!position) {
|
||||
return node;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import type { Node } from "@xyflow/react";
|
||||
|
||||
function isMarkdownNoteNode(node: Node): boolean {
|
||||
if (node.type !== "builder") {
|
||||
return false;
|
||||
}
|
||||
if (!node.data || typeof node.data !== "object") {
|
||||
return false;
|
||||
}
|
||||
return (node.data as { kind?: string }).kind === "note";
|
||||
}
|
||||
|
||||
export function getFitNodeIdsIgnoringNotes(nodes: Node[]): Array<{ id: string }> {
|
||||
const nodesWithoutNotes = nodes.filter((node) => !isMarkdownNoteNode(node));
|
||||
const targetNodes = nodesWithoutNotes.length > 0 ? nodesWithoutNotes : nodes;
|
||||
return targetNodes.map((node) => ({ id: node.id }));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue