Skip to content

Commit

Permalink
merge types and snippets
Browse files Browse the repository at this point in the history
  • Loading branch information
pokornyd committed Feb 26, 2025
1 parent 74b650c commit e309580
Show file tree
Hide file tree
Showing 10 changed files with 68 additions and 123 deletions.
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useContentModel } from "./hooks/useContentModel";

const App: React.FC = () => {
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const { contentTypes, snippets, loading, error } = useContentModel();
const { contentTypes, snippets, typesWithSnippets, loading, error } = useContentModel();

const handleNodeSelect = useCallback((nodeId: string) => {
setSelectedNodeId(nodeId);
Expand All @@ -35,6 +35,7 @@ const App: React.FC = () => {
<EntityProvider
contentTypes={contentTypes}
snippets={snippets}
typesWithSnippets={typesWithSnippets}
>
<Canvas selectedNodeId={selectedNodeId} onNodeSelect={handleNodeSelect} />
</EntityProvider>
Expand Down
12 changes: 5 additions & 7 deletions src/components/canvas/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -18,29 +18,27 @@ export const Canvas: React.FC<CanvasProps> = ({
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,
);

return (
<div className="flex h-full w-full">
<Sidebar types={contentTypes} snippets={snippets} onMenuSelect={onNodeSelect} />
<div className="flex-1 w-full h-full pb-14">
<Toolbar onToggleSnippets={() => setShowSnippets(!showSnippets)} />
<Toolbar />
<ReactFlow
defaultNodes={[]}
edges={[...processedGraph.typeEdges, ...processedGraph.snippetEdges]}
edges={processedGraph.edges}
onNodesChange={(changes) => setNodes(nodes => applyNodeChanges(changes, nodes))}
nodeTypes={nodeTypes}
onNodeClick={(_, node) => onNodeSelect(node.id)}
Expand Down
29 changes: 1 addition & 28 deletions src/components/canvas/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ToolbarProps> = ({ 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));
Expand Down Expand Up @@ -86,16 +75,6 @@ export const Toolbar: React.FC<ToolbarProps> = ({ onToggleSnippets }) => {
</button>
);

const toolbarCheckbox = (onClick: () => void, id: string, content: string, className?: string) => (
<span className={`${className || ""}`}>
<label className={`checkbox`}>
<input type="checkbox" id={id} onClick={onClick} />
<span className="checkmark"></span>
<span>{content}</span>
</label>
</span>
);

return (
<div className="flex items-center gap-2 px-4 h-14 border-b border-gray-200">
{toolbarButton(
Expand All @@ -112,12 +91,6 @@ export const Toolbar: React.FC<ToolbarProps> = ({ onToggleSnippets }) => {
<IconArrowReturn /> <span>Reset View</span>
</div>,
)}
{toolbarCheckbox(
handleSnippetToggle,
"toggleSnippets",
"Include Snippets",
"pl-4 caret-transparent",
)}
<div className="flex-1">
</div>
{toolbarButton(
Expand Down
6 changes: 5 additions & 1 deletion src/contexts/EntityContext.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 };
Expand All @@ -33,6 +36,7 @@ export const EntityProvider: React.FC<{
value={{
contentTypes,
snippets,
typesWithSnippets,
getEntityById,
}}
>
Expand Down
12 changes: 11 additions & 1 deletion src/hooks/useContentModel.ts
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand All @@ -8,6 +15,7 @@ export const useContentModel = () => {
const [snippets, setSnippets] = useState<Snippet[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<{ description: string; code: string } | null>(null);
const [typesWithSnippets, setTypesWithSnippets] = useState<TypeWithResolvedSnippets[]>([]);

useEffect(() => {
const fetchData = async () => {
Expand All @@ -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" });
Expand All @@ -36,6 +45,7 @@ export const useContentModel = () => {
return {
contentTypes,
snippets,
typesWithSnippets,
loading,
error,
};
Expand Down
61 changes: 15 additions & 46 deletions src/hooks/useGraphData.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand All @@ -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<string>();
const typeEdges: ProcessedEdge[] = [];
const snippetEdges: ProcessedEdge[] = [];
const edges: ProcessedEdge[] = [];

sources.forEach((type) => {
type.elements.forEach((element: Element) => {
Expand All @@ -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 ?? "",
Expand All @@ -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]);
};
37 changes: 4 additions & 33 deletions src/hooks/useNodeLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,20 @@ export const useNodeLayout = (
selectedNodeId: string | null,
expandedNodes: Set<string>,
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]);
};
1 change: 1 addition & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<EntityProvider
contentTypes={[]}
snippets={[]}
typesWithSnippets={[]}
>
<App />
</EntityProvider>
Expand Down
11 changes: 5 additions & 6 deletions src/utils/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,8 @@ export type ProcessedEdge = {
};

export type ProcessedGraph = {
typeNodes: ProcessedNode[];
snippetNodes: ProcessedNode[];
typeEdges: ProcessedEdge[];
snippetEdges: ProcessedEdge[];
nodes: ProcessedNode[];
edges: ProcessedEdge[];
};

type NodeData = {
Expand Down Expand Up @@ -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) => {
Expand Down
19 changes: 19 additions & 0 deletions src/utils/mapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export type Snippet = ContentTypeSnippetModels.ContentTypeSnippet;

export type SnippetElement = ContentTypeElements.ISnippetElement;

export type TypeWithResolvedSnippets = Omit<ContentTypeModels.ContentType, "elements"> & {
elements: Exclude<Element, SnippetElement>[];
};

export type ElementType = Element["type"];

export type NamedElement = Exclude<
Expand Down Expand Up @@ -80,3 +84,18 @@ export const getContentTypeSnippets = async (
): Promise<ApiResponse<ContentTypeSnippetModels.ContentTypeSnippet[]>> => {
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<Element, SnippetElement>[],
}));

0 comments on commit e309580

Please sign in to comment.