Skip to content

Commit

Permalink
refactor: completely rewrite Flow generator from scratch
Browse files Browse the repository at this point in the history
Fixes some bugs that existed around table order in the schema with
relationships, and now also takes previous layout into account when
editing existing tables.

Closes #63 and #45
  • Loading branch information
Ovyerus committed Nov 8, 2023
1 parent c5e6614 commit 38546cb
Show file tree
Hide file tree
Showing 9 changed files with 528 additions and 342 deletions.
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 18.18.0
37 changes: 17 additions & 20 deletions components/FlowView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import listTree from "@iconify/icons-gg/list-tree";
import { Icon } from "@iconify/react";
import { ElkNode } from "elkjs/lib/elk.bundled";
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import ReactFlow, {
applyNodeChanges,
Background,
Expand All @@ -16,8 +16,8 @@ import ReactFlow, {
import EnumNode from "~/components/EnumNode";
import ModelNode from "~/components/ModelNode";
import RelationEdge from "~/components/RelationEdge";
import { dmmfToElements } from "~/util/dmmfToElements";
import { getLayout } from "~/util/layout";
import { generateFlowFromDMMF } from "~/util/prismaToFlow";
import { DMMFToElementsResult } from "~/util/types";

import type { DMMF } from "@prisma/generator-helper";
Expand All @@ -32,33 +32,30 @@ const edgeTypes = {
};

const FlowView = ({ dmmf }: FlowViewProps) => {
// TODO: move to controlled nodes/edges, and change this to generate a NodeChanges[] as a diff so that positions gets preserved.
// Will be more complex but gives us better control over how they're handled, and makes storing locations EZ.
// https://reactflow.dev/docs/guides/migrate-to-v10/#11-controlled-nodes-and-edges
const [layout, setLayout] = useState<ElkNode | null>(null);
const [nodes, setNodes] = useState<DMMFToElementsResult["nodes"]>([]);
const [edges, setEdges] = useState<DMMFToElementsResult["edges"]>([]);

useEffect(() => {
const { nodes, edges } = dmmf
? dmmfToElements(dmmf, layout)
const regenerateNodes = (layout: ElkNode | null) => {
const { nodes: newNodes, edges: newEdges } = dmmf
? generateFlowFromDMMF(dmmf, nodes, layout)
: ({ nodes: [], edges: [] } as DMMFToElementsResult);

// See if `applyNodeChanges` can work here?
setNodes(nodes);
setEdges(edges);
}, [dmmf, layout]);
setNodes(newNodes);
setEdges(newEdges);
};

const refreshLayout = useCallback(async () => {
const refreshLayout = async () => {
const layout = await getLayout(nodes, edges);
setLayout(layout);
}, [nodes, edges]);
regenerateNodes(layout);
};

const onNodesChange: OnNodesChange = useCallback(
(changes) =>
setNodes((nodes) => applyNodeChanges(changes, nodes as any) as any),
[setNodes],
);
const onNodesChange: OnNodesChange = (changes) =>
setNodes((nodes) => applyNodeChanges(changes, nodes as any) as any);

useEffect(() => {
regenerateNodes(null);
}, [dmmf]);

Check warning on line 58 in components/FlowView.tsx

View workflow job for this annotation

GitHub Actions / Build and lint

React Hook useEffect has a missing dependency: 'regenerateNodes'. Either include it or remove the dependency array

Check warning on line 58 in components/FlowView.tsx

View workflow job for this annotation

GitHub Actions / Build and lint

React Hook useEffect has a missing dependency: 'regenerateNodes'. Either include it or remove the dependency array

return (
<>
Expand Down
153 changes: 88 additions & 65 deletions components/ModelNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,16 @@ import { Handle, Position, useReactFlow, useStoreApi } from "reactflow";

import styles from "./Node.module.scss";

import {
enumEdgeTargetHandleId,
relationEdgeSourceHandleId,
relationEdgeTargetHandleId,
} from "~/util/prismaToFlow";
import { ModelNodeData } from "~/util/types";

type ColumnData = ModelNodeData["columns"][number];

const isTarget = ({
kind,
isList,
relationFromFields,
relationName,
relationType,
}: ColumnData) =>
kind === "enum" ||
((relationType === "1-n" || relationType === "m-n") && !isList) ||
(relationType === "1-1" && !relationFromFields?.length) ||
// Fallback for implicit m-n tables (maybe they should act like the child in a
// 1-n instead)
(kind === "scalar" && !!relationName);

const isSource = ({ isList, relationFromFields, relationType }: ColumnData) =>
((relationType === "1-n" || relationType === "m-n") && isList) ||
(relationType === "1-1" && !!relationFromFields?.length);
const isRelationed = ({ relationData }: ColumnData) => !!relationData?.side;

const ModelNode = ({ data }: ModelNodeProps) => {
const store = useStoreApi();
Expand Down Expand Up @@ -68,55 +57,89 @@ const ModelNode = ({ data }: ModelNodeProps) => {
</tr>
</thead>
<tbody>
{data.columns.map((col) => (
<tr key={col.name} className={styles.row} title={col.documentation}>
<td className="font-mono font-semibold border-t-2 border-r-2 border-gray-300">
<button
type="button"
className={cc([
"relative",
"p-2",
{ "cursor-pointer": isTarget(col) || isSource(col) },
])}
onClick={() => {
if (!isTarget(col) && !isSource(col)) return;
{data.columns.map((col) => {
const reled = isRelationed(col);
let targetHandle: JSX.Element | null = null;
let sourceHandle: JSX.Element | null = null;

if (col.kind === "enum") {
const handleId = enumEdgeTargetHandleId(data.name, col.name);
targetHandle = (
<Handle
key={handleId}
className={cc([styles.handle, styles.left])}
type="target"
id={handleId}
position={Position.Left}
isConnectable={false}
/>
);
} else if (col.relationData) {
const targetHandleId = relationEdgeTargetHandleId(
data.name,
col.relationData.name,
);
const sourceHandleId = relationEdgeSourceHandleId(
data.name,
col.relationData.name,
col.name,
);

targetHandle =
col.relationData.side === "target" ? (
<Handle
key={targetHandleId}
className={cc([styles.handle, styles.left])}
type="target"
id={targetHandleId}
position={Position.Left}
isConnectable={false}
/>
) : null;
sourceHandle =
col.relationData.side === "source" ? (
<Handle
key={sourceHandleId}
className={cc([styles.handle, styles.right])}
type="source"
id={sourceHandleId}
position={Position.Right}
isConnectable={false}
/>
) : null;
}

focusNode(col.type);
}}
>
{col.name}
{isTarget(col) && (
<Handle
key={`${data.name}-${col.relationName || col.name}`}
className={cc([styles.handle, styles.left])}
type="target"
id={`${data.name}-${col.relationName || col.name}`}
position={Position.Left}
isConnectable={false}
/>
)}
</button>
</td>
<td className="p-2 font-mono border-t-2 border-r-2 border-gray-300">
{col.displayType}
</td>
<td className="font-mono border-t-2 border-gray-300">
<div className="relative p-2">
{col.defaultValue || ""}
{isSource(col) && (
<Handle
key={`${data.name}-${col.relationName}-${col.name}`}
className={cc([styles.handle, styles.right])}
type="source"
id={`${data.name}-${col.relationName}-${col.name}`}
position={Position.Right}
isConnectable={false}
/>
)}
</div>
</td>
</tr>
))}
return (
<tr key={col.name} className={styles.row} title={col.documentation}>
<td className="font-mono font-semibold border-t-2 border-r-2 border-gray-300">
<button
type="button"
className={cc([
"relative",
"p-2",
{ "cursor-pointer": reled },
])}
onClick={() => {
if (!reled) return;
focusNode(col.type);
}}
>
{col.name}
{targetHandle}
</button>
</td>
<td className="p-2 font-mono border-t-2 border-r-2 border-gray-300">
{col.displayType}
</td>
<td className="font-mono border-t-2 border-gray-300">
<div className="relative p-2">
{col.defaultValue || ""}
{sourceHandle}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
);
Expand Down
2 changes: 1 addition & 1 deletion pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const IndexPage = () => {
}));
const [model] = monaco.editor.getModels();

monaco.editor.setModelMarkers(model, "prismaliser", markers);
monaco.editor.setModelMarkers(model!, "prismaliser", markers);
}, [monaco, schemaErrors]);

useEffect(() => {
Expand Down
23 changes: 6 additions & 17 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
Expand All @@ -16,21 +12,14 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noUncheckedIndexedAccess": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"~/*": [
"./*"
]
"~/*": ["./*"]
},
"incremental": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
Loading

0 comments on commit 38546cb

Please sign in to comment.