Compare commits

...

1 Commits

Author SHA1 Message Date
dswbx 3180037b67 admin ui: store data canvas positions 2025-03-18 12:39:23 +01:00
6 changed files with 117 additions and 44 deletions
+5 -3
View File
@@ -22,7 +22,8 @@ type CanvasProps = ReactFlowProps & {
onDropNewEdge?: (base: any) => any; onDropNewEdge?: (base: any) => any;
}; };
export function Canvas({ export function Canvas(
{
nodes: _nodes, nodes: _nodes,
edges: _edges, edges: _edges,
externalProvider, externalProvider,
@@ -32,7 +33,9 @@ export function Canvas({
onDropNewNode, onDropNewNode,
onDropNewEdge, onDropNewEdge,
...props ...props
}: CanvasProps) { }: CanvasProps,
ref?: any,
) {
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []); const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []); const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
const { screenToFlowPosition } = useReactFlow(); const { screenToFlowPosition } = useReactFlow();
@@ -176,7 +179,6 @@ export function Canvas({
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
nodesConnectable={false} nodesConnectable={false}
/*panOnDrag={isSpacePressed}*/
panOnDrag={true} panOnDrag={true}
zoomOnScroll={isCommandPressed} zoomOnScroll={isCommandPressed}
panOnScroll={!isCommandPressed} panOnScroll={!isCommandPressed}
@@ -44,8 +44,10 @@ export const layoutWithDagre = ({ nodes, edges, graph }: LayoutProps) => {
const position = dagreGraph.node(node.id); const position = dagreGraph.node(node.id);
return { return {
...node, ...node,
position: {
x: position.x - (node.width ?? 0) / 2, x: position.x - (node.width ?? 0) / 2,
y: position.y - (node.height ?? 0) / 2, y: position.y - (node.height ?? 0) / 2,
},
}; };
}), }),
edges, edges,
@@ -35,7 +35,6 @@ export function Panels({ children, ...props }: PanelsProps) {
)} )}
<Panel unstyled position="bottom-right"> <Panel unstyled position="bottom-right">
{props.zoom && ( {props.zoom && (
<>
<Panel.Wrapper className="px-1.5"> <Panel.Wrapper className="px-1.5">
<Panel.IconButton Icon={TbPlus} round onClick={handleZoomIn} /> <Panel.IconButton Icon={TbPlus} round onClick={handleZoomIn} />
<Panel.Text className="px-2" mono onClick={handleZoomReset}> <Panel.Text className="px-2" mono onClick={handleZoomReset}>
@@ -44,7 +43,6 @@ export function Panels({ children, ...props }: PanelsProps) {
<Panel.IconButton Icon={TbMinus} round onClick={handleZoomOut} /> <Panel.IconButton Icon={TbMinus} round onClick={handleZoomOut} />
<Panel.IconButton Icon={TbMaximize} round onClick={handleZoomReset} /> <Panel.IconButton Icon={TbMaximize} round onClick={handleZoomReset} />
</Panel.Wrapper> </Panel.Wrapper>
</>
)} )}
{props.minimap && ( {props.minimap && (
<> <>
@@ -6,6 +6,10 @@ import { layoutWithDagre } from "ui/components/canvas/layouts";
import { Panels } from "ui/components/canvas/panels"; import { Panels } from "ui/components/canvas/panels";
import { EntityTableNode } from "./EntityTableNode"; import { EntityTableNode } from "./EntityTableNode";
import { useTheme } from "ui/client/use-theme"; import { useTheme } from "ui/client/use-theme";
import { Panel } from "ui/components/canvas/panels/Panel";
import { TbLayout } from "react-icons/tb";
import { useState } from "react";
import { type CanvasPosition, dataCanvasStore } from "ui/store";
function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] { function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] {
return Object.entries(entities ?? {}).map(([name, entity]) => { return Object.entries(entities ?? {}).map(([name, entity]) => {
@@ -65,12 +69,45 @@ const nodeTypes = {
entity: EntityTableNode.Component, entity: EntityTableNode.Component,
} as const; } as const;
function getNodeAutoLayout(nodes: Node<TAppDataEntity>[], edges: any[]): CanvasPosition[] {
const nodeLayout = layoutWithDagre({
nodes: nodes.map((n) => ({
id: n.id,
...EntityTableNode.getSize(n.data),
})),
edges,
graph: {
rankdir: "LR",
marginx: 50,
marginy: 50,
},
});
return nodeLayout.nodes.map((n) => ({
id: n.id,
...n.position,
}));
}
function setNodesLayout(nodes: Node<TAppDataEntity>[], layout: CanvasPosition[]) {
return nodes.map((node) => {
const pos = layout.find((l) => l.id === node.id);
if (pos) {
return {
...node,
position: { x: pos.x, y: pos.y },
};
}
return node;
});
}
export function DataSchemaCanvas() { export function DataSchemaCanvas() {
const { const {
config: { data }, config: { data },
} = useBknd(); } = useBknd();
const { theme } = useTheme(); const { theme } = useTheme();
const nodes = entitiesToNodes(data.entities);
const edges = relationsToEdges(data.relations).map((e) => ({ const edges = relationsToEdges(data.relations).map((e) => ({
...e, ...e,
style: { style: {
@@ -85,31 +122,35 @@ export function DataSchemaCanvas() {
}, },
})); }));
const nodeLayout = layoutWithDagre({ const entityNodes = entitiesToNodes(data.entities);
nodes: nodes.map((n) => ({ const positions = dataCanvasStore((state) => state.positions);
id: n.id, const setPositions = dataCanvasStore((state) => state.setPositions);
...EntityTableNode.getSize(n.data), const resetPositions = dataCanvasStore((state) => state.reset);
})),
edges,
graph: {
rankdir: "LR",
marginx: 50,
marginy: 50,
},
});
nodeLayout.nodes.forEach((node) => { const layout = positions ? positions : getNodeAutoLayout(entityNodes, edges);
const n = nodes.find((n) => n.id === node.id); const [nodes, setNodes] = useState<Node<TAppDataEntity>[]>(setNodesLayout(entityNodes, layout));
if (n) {
n.position = { x: node.x, y: node.y }; function setLayout(positions: CanvasPosition[] = getNodeAutoLayout(entityNodes, edges)) {
setNodes(setNodesLayout(entityNodes, positions));
}
function resetLayout() {
resetPositions();
setLayout();
} }
});
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<Canvas <Canvas
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onNodeDragStop={(e, node) => {
const positions = nodes
.map((n) => (n.id === node.id ? node : n))
.map((n) => ({ id: n.id, ...n.position }));
setPositions(positions);
setLayout(positions);
}}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
minZoom={0.1} minZoom={0.1}
maxZoom={2} maxZoom={2}
@@ -118,7 +159,11 @@ export function DataSchemaCanvas() {
maxZoom: 0.8, maxZoom: 0.8,
}} }}
> >
<Panels zoom minimap /> <Panels zoom minimap>
<Panel position="bottom-left">
<Panel.IconButton round Icon={TbLayout} onClick={resetLayout} />
</Panel>
</Panels>
</Canvas> </Canvas>
</ReactFlowProvider> </ReactFlowProvider>
); );
+25
View File
@@ -0,0 +1,25 @@
import { create } from "zustand";
import { combine, persist } from "zustand/middleware";
export type CanvasPosition = {
id: string;
x: number;
y: number;
};
export const dataCanvasStore = create(
persist(
combine(
{
positions: null as CanvasPosition[] | null,
},
(set) => ({
setPositions: (positions: CanvasPosition[]) => set(() => ({ positions })),
reset: () => set(() => ({ positions: null })),
}),
),
{
name: "datacanvas",
},
),
);
+1
View File
@@ -1 +1,2 @@
export { appShellStore } from "./appshell"; export { appShellStore } from "./appshell";
export { dataCanvasStore, type CanvasPosition } from "./datacanvas";