From e309580745fce7ddfc4d58cb1a92f410540a20c6 Mon Sep 17 00:00:00 2001 From: Daniel Pokorny Date: Wed, 26 Feb 2025 10:22:07 +0100 Subject: [PATCH] merge types and snippets --- src/App.tsx | 3 +- src/components/canvas/Canvas.tsx | 12 +++--- src/components/canvas/Toolbar.tsx | 29 +-------------- src/contexts/EntityContext.tsx | 6 ++- src/hooks/useContentModel.ts | 12 +++++- src/hooks/useGraphData.ts | 61 ++++++++----------------------- src/hooks/useNodeLayout.ts | 37 ++----------------- src/main.tsx | 1 + src/utils/layout.ts | 11 +++--- src/utils/mapi.ts | 19 ++++++++++ 10 files changed, 68 insertions(+), 123 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index cc7662e..36c67fb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,7 @@ import { useContentModel } from "./hooks/useContentModel"; const App: React.FC = () => { const [selectedNodeId, setSelectedNodeId] = useState(null); - const { contentTypes, snippets, loading, error } = useContentModel(); + const { contentTypes, snippets, typesWithSnippets, loading, error } = useContentModel(); const handleNodeSelect = useCallback((nodeId: string) => { setSelectedNodeId(nodeId); @@ -35,6 +35,7 @@ const App: React.FC = () => { diff --git a/src/components/canvas/Canvas.tsx b/src/components/canvas/Canvas.tsx index 2e1ca5d..d29d04a 100644 --- a/src/components/canvas/Canvas.tsx +++ b/src/components/canvas/Canvas.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import ReactFlow, { MiniMap, Controls, Background, applyNodeChanges, useReactFlow } from "reactflow"; import "reactflow/dist/style.css"; import { useEntities } from "../../contexts/EntityContext"; @@ -18,18 +18,16 @@ export const Canvas: React.FC = ({ selectedNodeId, onNodeSelect, }) => { - const { contentTypes, snippets } = useEntities(); + const { contentTypes, snippets, typesWithSnippets } = useEntities(); const { expandedNodes, isolatedNodeId } = useNodeState(); const { setNodes } = useReactFlow(); - const processedGraph = useGraphData(contentTypes, snippets); - const [showSnippets, setShowSnippets] = useState(false); + const processedGraph = useGraphData(typesWithSnippets); useNodeLayout( processedGraph, selectedNodeId, expandedNodes, isolatedNodeId, - showSnippets, setNodes, ); @@ -37,10 +35,10 @@ export const Canvas: React.FC = ({
- setShowSnippets(!showSnippets)} /> + setNodes(nodes => applyNodeChanges(changes, nodes))} nodeTypes={nodeTypes} onNodeClick={(_, node) => onNodeSelect(node.id)} diff --git a/src/components/canvas/Toolbar.tsx b/src/components/canvas/Toolbar.tsx index 416ea13..77b442a 100644 --- a/src/components/canvas/Toolbar.tsx +++ b/src/components/canvas/Toolbar.tsx @@ -9,24 +9,13 @@ import IconCollapse from "../icons/IconCollapse"; import IconArrowReturn from "../icons/IconArrowReturn"; import IconFilePdf from "../icons/IconFilePdf"; -type ToolbarProps = { - onToggleSnippets: () => void; -}; - -export const Toolbar: React.FC = ({ onToggleSnippets }) => { +export const Toolbar: React.FC = () => { const { expandedNodes, toggleNode, resetIsolation } = useNodeState(); const { getNodes, fitView } = useReactFlow(); const { customApp } = useAppContext(); const [isExporting, setIsExporting] = useState(false); const environmentId = customApp.context.environmentId; - const handleSnippetToggle = () => { - onToggleSnippets(); - setTimeout(() => { - fitView({ duration: 800 }); - }, 50); - }; - const handleExpandCollapse = () => { const nodes = getNodes(); const allExpanded = nodes.every(node => expandedNodes.has(node.id)); @@ -86,16 +75,6 @@ export const Toolbar: React.FC = ({ onToggleSnippets }) => { ); - const toolbarCheckbox = (onClick: () => void, id: string, content: string, className?: string) => ( - - - - ); - return (
{toolbarButton( @@ -112,12 +91,6 @@ export const Toolbar: React.FC = ({ onToggleSnippets }) => { Reset View
, )} - {toolbarCheckbox( - handleSnippetToggle, - "toggleSnippets", - "Include Snippets", - "pl-4 caret-transparent", - )}
{toolbarButton( diff --git a/src/contexts/EntityContext.tsx b/src/contexts/EntityContext.tsx index f04f977..08789ff 100644 --- a/src/contexts/EntityContext.tsx +++ b/src/contexts/EntityContext.tsx @@ -1,9 +1,11 @@ import React, { createContext, useContext } from "react"; import { ContentTypeModels, ContentTypeSnippetModels } from "@kontent-ai/management-sdk"; +import { TypeWithResolvedSnippets } from "../utils/mapi"; type EntityContextType = { contentTypes: ContentTypeModels.ContentType[]; snippets: ContentTypeSnippetModels.ContentTypeSnippet[]; + typesWithSnippets: TypeWithResolvedSnippets[]; getEntityById: (id: string) => { type: "contentType" | "snippet"; name: string; @@ -17,7 +19,8 @@ export const EntityProvider: React.FC<{ children: React.ReactNode; contentTypes: ContentTypeModels.ContentType[]; snippets: ContentTypeSnippetModels.ContentTypeSnippet[]; -}> = ({ children, contentTypes, snippets }) => { + typesWithSnippets: TypeWithResolvedSnippets[]; +}> = ({ children, contentTypes, snippets, typesWithSnippets }) => { const getEntityById = (id: string) => { const contentType = contentTypes.find(t => t.id === id); if (contentType) return { type: "contentType" as const, name: contentType.name, data: contentType }; @@ -33,6 +36,7 @@ export const EntityProvider: React.FC<{ value={{ contentTypes, snippets, + typesWithSnippets, getEntityById, }} > diff --git a/src/hooks/useContentModel.ts b/src/hooks/useContentModel.ts index 449e73f..48b3137 100644 --- a/src/hooks/useContentModel.ts +++ b/src/hooks/useContentModel.ts @@ -1,5 +1,12 @@ import { useState, useEffect } from "react"; -import { ContentType, getContentTypes, getContentTypeSnippets, Snippet } from "../utils/mapi"; +import { + ContentType, + getContentTypes, + getContentTypeSnippets, + mergeTypesWithSnippets, + Snippet, + TypeWithResolvedSnippets, +} from "../utils/mapi"; import { useAppContext } from "../contexts/AppContext"; export const useContentModel = () => { @@ -8,6 +15,7 @@ export const useContentModel = () => { const [snippets, setSnippets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<{ description: string; code: string } | null>(null); + const [typesWithSnippets, setTypesWithSnippets] = useState([]); useEffect(() => { const fetchData = async () => { @@ -22,6 +30,7 @@ export const useContentModel = () => { setContentTypes(typesResult.data || []); setSnippets(snippetsResult.data || []); + setTypesWithSnippets(mergeTypesWithSnippets(typesResult.data || [], snippetsResult.data || [])); } catch (err) { console.error(err); setError({ description: "Failed to fetch content types and snippets", code: "FETCH_ERROR" }); @@ -36,6 +45,7 @@ export const useContentModel = () => { return { contentTypes, snippets, + typesWithSnippets, loading, error, }; diff --git a/src/hooks/useGraphData.ts b/src/hooks/useGraphData.ts index 56064a3..1654694 100644 --- a/src/hooks/useGraphData.ts +++ b/src/hooks/useGraphData.ts @@ -1,9 +1,9 @@ import { useMemo } from "react"; -import { isRelationshipElement, isSnippetElement, ProcessedEdge, ProcessedGraph } from "../utils/layout"; -import { Element, ContentType, Snippet } from "../utils/mapi"; +import { isRelationshipElement, ProcessedEdge, ProcessedGraph } from "../utils/layout"; +import { Element, TypeWithResolvedSnippets } from "../utils/mapi"; -const createNodes = (types: ContentType[], snippets: Snippet[]) => { - const typeNodes = types.map((type) => ({ +const createNodes = (types: TypeWithResolvedSnippets[]) => + types.map((type) => ({ id: type.id, type: "contentType", data: { @@ -21,26 +21,11 @@ const createNodes = (types: ContentType[], snippets: Snippet[]) => { position: { x: 0, y: 0 }, })); - const snippetNodes = snippets.map((snippet) => ({ - id: snippet.id, - type: "snippet", - data: { - id: snippet.id, - label: snippet.name, - elements: snippet.elements, - }, - position: { x: 0, y: 0 }, - })); - - return { typeNodes, snippetNodes }; -}; - const createEdgesFromSources = ( - sources: ContentType[] | Snippet[], -): { typeEdges: ProcessedEdge[], snippetEdges: ProcessedEdge[] } => { + sources: TypeWithResolvedSnippets[], +): ProcessedEdge[] => { const edgeSet = new Set(); - const typeEdges: ProcessedEdge[] = []; - const snippetEdges: ProcessedEdge[] = []; + const edges: ProcessedEdge[] = []; sources.forEach((type) => { type.elements.forEach((element: Element) => { @@ -51,7 +36,7 @@ const createEdgesFromSources = ( const edgeKey = `${type.id}-${element.id}-${allowed.id}`; if (!edgeSet.has(edgeKey)) { edgeSet.add(edgeKey); - typeEdges.push({ + edges.push({ id: edgeKey, source: type.id, target: allowed.id ?? "", @@ -62,36 +47,20 @@ const createEdgesFromSources = ( } }); } - if (isSnippetElement(element)) { - const edgeKey = `${type.id}-${element.id}-${element.snippet.id}`; - if (!edgeSet.has(edgeKey)) { - edgeSet.add(edgeKey); - snippetEdges.push({ - id: edgeKey, - source: element.snippet.id ?? "", - target: type.id ?? "", // snippet edges are created in reverse - sourceHandle: "source", - targetHandle: `target-${element.id}`, - edgeType: "snippet", - }); - } - } }); }); - return { typeEdges, snippetEdges }; + return edges; }; -export const useGraphData = (types: ContentType[], snippets: Snippet[]): ProcessedGraph => { +export const useGraphData = (types: TypeWithResolvedSnippets[]): ProcessedGraph => { return useMemo(() => { console.log("useGraphData called"); - const { typeNodes, snippetNodes } = createNodes(types, snippets); - const { typeEdges, snippetEdges } = createEdgesFromSources([...types, ...snippets]); + const nodes = createNodes(types); + const edges = createEdgesFromSources(types); return { - typeNodes, - snippetNodes, - typeEdges, - snippetEdges, + nodes, + edges, }; - }, [types, snippets]); + }, [types]); }; diff --git a/src/hooks/useNodeLayout.ts b/src/hooks/useNodeLayout.ts index 6934ca9..c99c6dd 100644 --- a/src/hooks/useNodeLayout.ts +++ b/src/hooks/useNodeLayout.ts @@ -8,49 +8,20 @@ export const useNodeLayout = ( selectedNodeId: string | null, expandedNodes: Set, isolatedNodeId: string | null, - showSnippets: boolean, setNodes: (nodes: Node[]) => void, ) => { useEffect(() => { console.log("useNodeLayout called"); - const typeNodes = processedGraph.typeNodes.map(node => ({ + const typeNodes = processedGraph.nodes.map(node => ({ ...node, selected: node.id === selectedNodeId, data: { ...node.data, isExpanded: expandedNodes.has(node.id), }, - hidden: isolatedNodeId ? !isNodeRelated(node.id, isolatedNodeId, processedGraph.typeEdges) : false, + hidden: isolatedNodeId ? !isNodeRelated(node.id, isolatedNodeId, processedGraph.edges) : false, })); - if (!showSnippets) { - setNodes(getLayoutedElements(typeNodes, processedGraph.typeEdges).nodes); - return; - } - - const snippetNodes = processedGraph.snippetNodes.map(node => ({ - ...node, - selected: node.id === selectedNodeId, - data: { - ...node.data, - isExpanded: expandedNodes.has(node.id), - }, - hidden: isolatedNodeId - ? !isNodeRelated(node.id, isolatedNodeId, processedGraph.typeEdges) - : false, - })); - - const allNodes = [ - ...getLayoutedElements(typeNodes, processedGraph.typeEdges).nodes, - ...getLayoutedElements(snippetNodes, processedGraph.snippetEdges).nodes.map((node) => ({ - ...node, - position: { - x: node.position.x - 400, - y: node.position.y, - }, - })), - ]; - - setNodes(allNodes); - }, [processedGraph, selectedNodeId, expandedNodes, isolatedNodeId, showSnippets, setNodes]); + setNodes(getLayoutedElements(typeNodes, processedGraph.edges).nodes); + }, [processedGraph, selectedNodeId, expandedNodes, isolatedNodeId, setNodes]); }; diff --git a/src/main.tsx b/src/main.tsx index cabf324..e21a6ac 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -16,6 +16,7 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/src/utils/layout.ts b/src/utils/layout.ts index 7c199b3..6681906 100644 --- a/src/utils/layout.ts +++ b/src/utils/layout.ts @@ -43,10 +43,8 @@ export type ProcessedEdge = { }; export type ProcessedGraph = { - typeNodes: ProcessedNode[]; - snippetNodes: ProcessedNode[]; - typeEdges: ProcessedEdge[]; - snippetEdges: ProcessedEdge[]; + nodes: ProcessedNode[]; + edges: ProcessedEdge[]; }; type NodeData = { @@ -83,10 +81,11 @@ export const getLayoutedElements = ( dagreGraph.setGraph({ rankdir: direction, - nodesep: 50, + nodesep: 100, ranksep: 200, align: "UL", // UL, UR, DL, DR - ranker: "tight-tree", // network-simplex, tight-tree, longest-path + ranker: "network-simplex", // network-simplex, tight-tree, longest-path + // acyclicer: "greedy", }); nodes.forEach((node) => { diff --git a/src/utils/mapi.ts b/src/utils/mapi.ts index 8f9532c..88f3bfd 100644 --- a/src/utils/mapi.ts +++ b/src/utils/mapi.ts @@ -13,6 +13,10 @@ export type Snippet = ContentTypeSnippetModels.ContentTypeSnippet; export type SnippetElement = ContentTypeElements.ISnippetElement; +export type TypeWithResolvedSnippets = Omit & { + elements: Exclude[]; +}; + export type ElementType = Element["type"]; export type NamedElement = Exclude< @@ -80,3 +84,18 @@ export const getContentTypeSnippets = async ( ): Promise> => { return makeMapiRequest(environmentId, "getContentTypeSnippets"); }; + +export const mergeTypesWithSnippets = ( + types: ContentType[], + snippets: Snippet[], +): TypeWithResolvedSnippets[] => + types.map(type => ({ + ...type, + elements: type.elements.flatMap(element => { + if (element.type === "snippet") { + const snippet = snippets.find(s => s.id === element.snippet.id); + return snippet?.elements ?? []; + } + return [element]; + }) as Exclude[], + }));