mirror of
https://github.com/AykutSarac/jsoncrack.com.git
synced 2025-01-27 15:22:56 +08:00
implement collapse/expand
This commit is contained in:
parent
883d3a157a
commit
2143be74fc
@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import { BiChevronLeft, BiChevronRight } from "react-icons/bi";
|
||||
import { NodeData, EdgeData } from "reaflow";
|
||||
import { ConditionalWrapper, CustomNodeProps } from "src/components/CustomNode";
|
||||
import useConfig from "src/hooks/store/useConfig";
|
||||
import useGraph from "src/hooks/store/useGraph";
|
||||
import styled from "styled-components";
|
||||
import * as Styled from "./styles";
|
||||
|
||||
@ -23,7 +25,8 @@ const StyledExpand = styled.button`
|
||||
border-left: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
|
||||
`;
|
||||
|
||||
const TextNode: React.FC<CustomNodeProps<string>> = ({
|
||||
const TextNode: React.FC<CustomNodeProps<string> & { node: NodeData }> = ({
|
||||
node,
|
||||
width,
|
||||
height,
|
||||
value,
|
||||
@ -34,6 +37,7 @@ const TextNode: React.FC<CustomNodeProps<string>> = ({
|
||||
const performanceMode = useConfig((state) => state.performanceMode);
|
||||
const expand = useConfig((state) => state.expand);
|
||||
const [isExpanded, setIsExpanded] = React.useState(!expand);
|
||||
const toggleNode = useGraph((state) => state.toggleNode);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsExpanded(expand);
|
||||
@ -41,6 +45,7 @@ const TextNode: React.FC<CustomNodeProps<string>> = ({
|
||||
|
||||
const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
toggleNode(node, isExpanded);
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
|
@ -37,7 +37,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
|
||||
|
||||
return (
|
||||
<Node {...nodeProps} label={<Label style={baseLabelStyle} />}>
|
||||
{({ width, height, x, y }) => {
|
||||
{({ width, height, x, y, node }) => {
|
||||
if (properties.text instanceof Object) {
|
||||
const entries = Object.entries<string>(properties.text);
|
||||
|
||||
@ -54,6 +54,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
|
||||
|
||||
return (
|
||||
<TextNode
|
||||
node={node}
|
||||
isParent={properties.data.isParent}
|
||||
value={properties.text}
|
||||
width={width}
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
TransformComponent,
|
||||
TransformWrapper,
|
||||
} from "react-zoom-pan-pinch";
|
||||
import { Canvas, Edge, ElkRoot, useSelection } from "reaflow";
|
||||
import { Canvas, Edge, ElkRoot, NodeData, useSelection } from "reaflow";
|
||||
import { CustomNode } from "src/components/CustomNode";
|
||||
import { NodeModal } from "src/containers/Modals/NodeModal";
|
||||
import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers";
|
||||
@ -12,6 +12,7 @@ import useConfig from "src/hooks/store/useConfig";
|
||||
import styled from "styled-components";
|
||||
import shallow from "zustand/shallow";
|
||||
import useNodeTools from "src/hooks/store/useNodeTools";
|
||||
import useGraph from "src/hooks/store/useGraph";
|
||||
|
||||
interface LayoutProps {
|
||||
isWidget: boolean;
|
||||
@ -34,18 +35,9 @@ const StyledEditorWrapper = styled.div<{ isWidget: boolean }>`
|
||||
|
||||
const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
||||
const json = useConfig((state) => state.json);
|
||||
|
||||
const [nodes, edges, newNodes, newEdges, selectedNode] = useNodeTools(
|
||||
(state) => [
|
||||
state.nodes,
|
||||
state.edges,
|
||||
state.newNodes,
|
||||
state.newEdges,
|
||||
state.selectedNode,
|
||||
],
|
||||
shallow
|
||||
);
|
||||
const setNodeTools = useNodeTools((state) => state.setNodeTools);
|
||||
const nodes = useGraph((state) => state.nodes);
|
||||
const edges = useGraph((state) => state.edges);
|
||||
const setGraphValue = useGraph((state) => state.setGraphValue);
|
||||
|
||||
const [size, setSize] = React.useState({
|
||||
width: 2000,
|
||||
@ -61,11 +53,11 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
||||
React.useEffect(() => {
|
||||
const { nodes, edges } = getEdgeNodes(json, expand);
|
||||
|
||||
setNodeTools("nodes", nodes);
|
||||
setNodeTools("edges", edges);
|
||||
setNodeTools("newNodes", nodes);
|
||||
setNodeTools("newEdges", edges);
|
||||
}, [json, expand, setNodeTools]);
|
||||
setGraphValue("nodes", nodes);
|
||||
setGraphValue("edges", edges);
|
||||
setGraphValue("initialNodes", nodes);
|
||||
setGraphValue("initialEdges", edges);
|
||||
}, [expand, json, setGraphValue]);
|
||||
|
||||
const onInit = (ref: ReactZoomPanPinchRef) => {
|
||||
setConfig("zoomPanPinch", ref);
|
||||
@ -76,20 +68,6 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
||||
setSize({ width: layout.width, height: layout.height });
|
||||
};
|
||||
|
||||
const { selections, onClick, removeSelection } = useSelection({
|
||||
nodes,
|
||||
edges,
|
||||
onSelection: (s) => {
|
||||
if (!isWidget) {
|
||||
if (s[0] === selectedNode) {
|
||||
removeSelection(selectedNode);
|
||||
} else {
|
||||
setNodeTools("selectedNode", s[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledEditorWrapper isWidget={isWidget}>
|
||||
<TransformWrapper
|
||||
@ -119,11 +97,7 @@ const MemoizedGraph = React.memo(function Layout({ isWidget }: LayoutProps) {
|
||||
direction={layout}
|
||||
key={layout}
|
||||
onLayoutChange={onLayoutChange}
|
||||
selections={selections}
|
||||
node={(props) => <CustomNode {...props} />}
|
||||
edge={(props) =>
|
||||
newEdges.find((e) => e.id === props.id) ? <Edge /> : <></>
|
||||
}
|
||||
zoomable={false}
|
||||
readonly
|
||||
/>
|
||||
|
139
src/hooks/store/useGraph.tsx
Normal file
139
src/hooks/store/useGraph.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import create from "zustand";
|
||||
import { EdgeData, NodeData } from "reaflow";
|
||||
import { Graph } from "src/components/Graph";
|
||||
import { findEdgeChildren } from "src/utils/findEdgeChildren";
|
||||
import { findNodeChildren } from "src/utils/findNodeChildren";
|
||||
|
||||
export interface Graph {
|
||||
initialNodes: NodeData[];
|
||||
initialEdges: EdgeData[];
|
||||
nodes: NodeData[];
|
||||
edges: EdgeData[];
|
||||
collapsedNodes: { [key: string]: NodeData[] };
|
||||
collapsedEdges: { [key: string]: EdgeData[] };
|
||||
}
|
||||
|
||||
interface GraphActions {
|
||||
setGraphValue: (key: keyof Graph, value: any) => void;
|
||||
toggleNode: (node: NodeData, isExpanded: boolean) => void;
|
||||
}
|
||||
|
||||
function isNode(element: NodeData | EdgeData) {
|
||||
if ("text" in element) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export const getIncomers = (
|
||||
node: NodeData,
|
||||
nodes: NodeData[],
|
||||
edges: EdgeData[]
|
||||
): NodeData[] => {
|
||||
if (!isNode(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const incomersIds = edges.filter((e) => e.to === node.id).map((e) => e.from);
|
||||
return nodes.filter((n) => incomersIds.includes(n.id));
|
||||
};
|
||||
|
||||
export const getOutgoingEdges = (
|
||||
node: NodeData,
|
||||
edges: EdgeData[]
|
||||
): EdgeData[] => {
|
||||
if (!isNode(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return edges.filter((edge) => edge.from === node.id);
|
||||
};
|
||||
|
||||
export const getIncomingEdges = (
|
||||
node: NodeData,
|
||||
edges: EdgeData[]
|
||||
): EdgeData[] => {
|
||||
if (!isNode(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return edges.filter((edge) => edge.to === node.id);
|
||||
};
|
||||
|
||||
export const getOutgoers = (
|
||||
node: NodeData,
|
||||
nodes: NodeData[],
|
||||
edges: EdgeData[]
|
||||
) => {
|
||||
const allOutgoers: NodeData[] = [];
|
||||
|
||||
if (!isNode(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const runner = (currentNode: NodeData) => {
|
||||
const outgoerIds = edges
|
||||
.filter((e) => e.from === currentNode.id)
|
||||
.map((e) => e.to);
|
||||
const nodeList = nodes.filter((n) => outgoerIds.includes(n.id));
|
||||
allOutgoers.push(...nodeList);
|
||||
nodeList.forEach((node) => runner(node));
|
||||
};
|
||||
|
||||
return allOutgoers;
|
||||
};
|
||||
|
||||
const initialStates: Graph = {
|
||||
initialNodes: [],
|
||||
initialEdges: [],
|
||||
nodes: [],
|
||||
edges: [],
|
||||
collapsedNodes: {},
|
||||
collapsedEdges: {},
|
||||
};
|
||||
|
||||
const useGraph = create<Graph & GraphActions>((set, get) => ({
|
||||
...initialStates,
|
||||
setGraphValue: (key, value) =>
|
||||
set((state) => ({
|
||||
...state,
|
||||
[key]: value,
|
||||
})),
|
||||
toggleNode: (node, isExpanded) =>
|
||||
set((state) => {
|
||||
const childrenNodes = findNodeChildren(
|
||||
node.id,
|
||||
state.initialNodes,
|
||||
state.initialEdges
|
||||
);
|
||||
const childrenEdges = findEdgeChildren(
|
||||
node.id,
|
||||
childrenNodes,
|
||||
state.initialEdges
|
||||
);
|
||||
|
||||
const expand = () => {
|
||||
if (isExpanded) {
|
||||
return {
|
||||
nodes: state.nodes.filter(
|
||||
(sNode) => !childrenNodes.map((n) => n.id).includes(sNode.id)
|
||||
),
|
||||
edges: state.edges.filter(
|
||||
(sEdge) => !childrenEdges.map((n) => n.id).includes(sEdge.id)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: state.nodes.concat(childrenNodes),
|
||||
edges: state.edges.concat(childrenEdges),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
nodes: expand().nodes,
|
||||
edges: expand().edges,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
export default useGraph;
|
@ -1,15 +1,17 @@
|
||||
import { NodeData, EdgeData } from "reaflow/dist/types";
|
||||
|
||||
export const findEdgeChildren = (
|
||||
selectedNode: string,
|
||||
connections: NodeData[],
|
||||
edges: EdgeData[]
|
||||
) => {
|
||||
const nodeIds = connections.map((n) => n.id);
|
||||
|
||||
export const findEdgeChildren = (selectedNode: string, connections: NodeData[], edges: EdgeData[]) => {
|
||||
nodeIds.push(selectedNode);
|
||||
const newEdges = edges.filter(
|
||||
(e) =>
|
||||
nodeIds.includes(e.from as string) && nodeIds.includes(e.to as string)
|
||||
);
|
||||
|
||||
const nodeIds = connections.map((n) => n.id);
|
||||
nodeIds.push(selectedNode);
|
||||
const newEdges = edges.filter(
|
||||
(e) =>
|
||||
nodeIds.includes(e.from as string) && nodeIds.includes(e.to as string)
|
||||
);
|
||||
|
||||
return newEdges;
|
||||
|
||||
};
|
||||
return newEdges;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user