Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save groups in the graph #455

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/graph-editor/src/annotations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const hidden = 'ui.hidden';
//Used on nodes to indicate meta from a ui
export const xpos = 'ui.position.x';
export const ypos = 'ui.position.y';
export const width = 'ui.width';
export const height = 'ui.height';

//Used on nodes and graph
export const title = 'ui.title';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GROUP } from '@/ids.js';
import { Graph } from 'graphlib';
import { Item, Menu, Separator } from 'react-contexify';
import { Node, ReactFlowInstance, useReactFlow } from 'reactflow';
Expand Down Expand Up @@ -159,9 +160,11 @@ export const NodeContextMenu = ({ id, nodes }: INodeContextMenuProps) => {
}
}, [graph, nodes]);

const canDuplicate = nodes[0]?.type !== GROUP;

return (
<Menu id={id}>
<Item onClick={onDuplicate}>Duplicate</Item>
{canDuplicate && <Item onClick={onDuplicate}>Duplicate</Item>}
<Item onClick={focus}>Focus</Item>
<Item disabled={!isDeletable} onClick={deleteEl}>
Delete
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Edge, Graph } from '@tokens-studio/graph-engine';
import { GROUP } from '@/ids.js';
import { GROUP_NODE_PADDING } from '@/constants.js';
import { Item, Menu, Separator } from 'react-contexify';
import { Node, getRectOfNodes, useReactFlow, useStoreApi } from 'reactflow';
import { NodeTypes } from '../flow/types.js';
import { getId } from '../flow/utils.js';
import { Node, getNodesBounds, useReactFlow, useStoreApi } from 'reactflow';
import { useAction } from '@/editor/actions/provider.js';
import { useLocalGraph } from '@/hooks/index.js';
import { v4 as uuid } from 'uuid';
Expand All @@ -13,8 +13,6 @@ export type INodeContextMenuProps = {
nodes: Node[];
};

const padding = 25;

export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => {
const reactFlowInstance = useReactFlow();
const graph = useLocalGraph();
Expand All @@ -24,54 +22,64 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => {

//Note that we use a filter here to prevent getting nodes that have a parent node, ie are part of a group
const selectedNodes = nodes.filter(
(node) => node.selected && !node.parentNode,
(node) => node.selected && !node.parentId,
);
const selectedNodeIds = selectedNodes.map((node) => node.id);

const onGroup = useCallback(() => {
const rectOfNodes = getRectOfNodes(nodes);
const groupId = getId('group');
const bounds = getNodesBounds(nodes);
const parentPosition = {
x: rectOfNodes.x,
y: rectOfNodes.y,
x: bounds.x,
y: bounds.y,
};
const groupNode = {
id: groupId,
type: NodeTypes.GROUP,
position: parentPosition,
style: {
width: rectOfNodes.width + padding * 2,
height: rectOfNodes.height + padding * 2,
},
data: {
expandable: true,
expanded: true,
},
} as Node;

store.getState().resetSelectedElements();
store.setState({ nodesSelectionActive: false });

const newNodes = createNode({
type: GROUP,
position: parentPosition,
});

if (!newNodes) {
return;
}

const { flowNode } = newNodes;

reactFlowInstance.setNodes((nodes) => {
//Note that group nodes should always occur before their parents
return [groupNode].concat(
nodes.map((node) => {
// Note that group nodes should always occur before their children
return [{
...flowNode,
dragHandle: undefined,
style: {
width: bounds.width + GROUP_NODE_PADDING * 2,
height: bounds.height + GROUP_NODE_PADDING * 2,
},
data: {
expandable: true,
expanded: true,
}
} as Node]
.concat(nodes)
.map((node) => {
if (selectedNodeIds.includes(node.id)) {
return {
...node,
position: {
x: node.position.x - parentPosition.x + padding,
y: node.position.y - parentPosition.y + padding,
x: node.position.x - parentPosition.x + GROUP_NODE_PADDING,
y: node.position.y - parentPosition.y + GROUP_NODE_PADDING,
},
extent: 'parent' as const,
parentNode: groupId,
parentId: flowNode.id,
};
}

return node;
}),
);
});
});
}, [nodes, reactFlowInstance, selectedNodeIds, store]);

}, [createNode, nodes, reactFlowInstance, selectedNodeIds, store]);

const onCreateSubgraph = useCallback(() => {
//We need to work out which nodes do not have parents in the selection
Expand Down Expand Up @@ -294,12 +302,18 @@ export const SelectionContextMenu = ({ id, nodes }: INodeContextMenuProps) => {
duplicateNodes(selectedNodeIds);
};

const hasGroup = selectedNodes.some((node) => node.type === GROUP);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memoized


return (
<Menu id={id}>
<Item onClick={onGroup}>Create group</Item>
{!hasGroup && <Item onClick={onGroup}>Create group</Item>}
<Item onClick={onCreateSubgraph}>Create Subgraph</Item>
<Separator />
<Item onClick={onDuplicate}>Duplicate</Item>
{!hasGroup && (
<>
<Separator />
<Item onClick={onDuplicate}>Duplicate</Item>
</>
)}
</Menu>
);
};
20 changes: 12 additions & 8 deletions packages/graph-editor/src/components/flow/nodes/groupNode.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
import { Button, Stack } from '@tokens-studio/ui';
import { GROUP_NODE_PADDING } from '@/constants.js';
import {
NodeProps,
NodeToolbar,
getRectOfNodes,
getNodesBounds,
useReactFlow,
useStore,
useStoreApi,
} from 'reactflow';
import { NodeResizer } from '@reactflow/node-resizer';
import { useCallback } from 'react';
import { useLocalGraph } from '@/context/graph.js';
import React from 'react';
import useDetachNodes from '../../../hooks/useDetachNodes.js';

const lineStyle = { borderColor: 'white' };
const padding = 25;

function GroupNode(props: NodeProps) {
const { id, data } = props;
const store = useStoreApi();
const { deleteElements } = useReactFlow();
const detachNodes = useDetachNodes();
const graph = useLocalGraph()
const { minWidth, minHeight, hasChildNodes } = useStore((store) => {
const childNodes = Array.from(store.nodeInternals.values()).filter(
(n) => n.parentNode === id,
(n) => n.parentId === id,
);
const rect = getRectOfNodes(childNodes);
const bounds = getNodesBounds(childNodes);

return {
minWidth: rect.width + padding * 2,
minHeight: rect.height + padding * 2,
minWidth: bounds.width + GROUP_NODE_PADDING * 2,
minHeight: bounds.height + GROUP_NODE_PADDING * 2,
hasChildNodes: childNodes.length > 0,
};
}, isEqual);
Expand All @@ -39,11 +41,13 @@ function GroupNode(props: NodeProps) {

const onDetach = useCallback(() => {
const childNodeIds = Array.from(store.getState().nodeInternals.values())
.filter((n) => n.parentNode === id)
.filter((n) => n.parentId === id)
.map((n) => n.id);

detachNodes(childNodeIds, id);
}, [detachNodes, id, store]);

graph.removeNode(id);
}, [detachNodes, graph, id, store]);

return (
<div
Expand Down
4 changes: 2 additions & 2 deletions packages/graph-editor/src/components/flow/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { GROUP } from '@/ids.js';
import { Node } from 'reactflow';
import { NodeTypes } from './types.js';

// we have to make sure that parent nodes are rendered before their children
export const sortNodes = (a: Node, b: Node): number => {
if (a.type === b.type) {
return 0;
}
return a.type === NodeTypes.GROUP && b.type !== NodeTypes.GROUP ? -1 : 1;
return a.type === GROUP && b.type !== GROUP ? -1 : 1;
};

export const getId = (prefix = 'node') => `${prefix}_${Math.random() * 10000}`;
Expand Down
58 changes: 31 additions & 27 deletions packages/graph-editor/src/components/panels/dropPanel/data.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GROUP } from '@/ids.js';
import { nodes } from '@tokens-studio/graph-engine';
import { observable } from 'mobx';

Expand Down Expand Up @@ -99,36 +100,39 @@ function CapitalCase(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

const nodesToIgnoreInPanel = [GROUP];

export const defaultPanelGroupsFactory = (): DropPanelStore => {
const auto = Object.values<PanelGroup>(
nodes.reduce(
(acc, node) => {
const defaultGroup = node.type.split('.');
const groups = node.groups || [defaultGroup[defaultGroup.length - 2]];
nodes.filter(node => !nodesToIgnoreInPanel.includes(node.type))
.reduce(
(acc, node) => {
const defaultGroup = node.type.split('.');
const groups = node.groups || [defaultGroup[defaultGroup.length - 2]];

groups.forEach((group) => {
//If the group does not exist, create it
if (!acc[group]) {
acc[group] = new PanelGroup({
title: CapitalCase(group),
key: group,
items: [],
});
}
acc[group].items.push(
new PanelItem({
type: node.type,
text: CapitalCase(
node.title || defaultGroup[defaultGroup.length - 1],
),
description: node.description,
}),
);
});
return acc;
},
{} as Record<string, PanelGroup>,
),
groups.forEach((group) => {
//If the group does not exist, create it
if (!acc[group]) {
acc[group] = new PanelGroup({
title: CapitalCase(group),
key: group,
items: [],
});
}
acc[group].items.push(
new PanelItem({
type: node.type,
text: CapitalCase(
node.title || defaultGroup[defaultGroup.length - 1],
),
description: node.description,
}),
);
});
return acc;
},
{} as Record<string, PanelGroup>,
),
);

return new DropPanelStore(auto);
Expand Down
1 change: 1 addition & 0 deletions packages/graph-editor/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const MAIN_GRAPH_ID = 'graph1';
export const GROUP_NODE_PADDING = 25;
2 changes: 1 addition & 1 deletion packages/graph-editor/src/css/reactflow.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
border-radius: var(--radii-medium, 8px);
}

.react-flow__node-studio_tokens_group {
.react-flow__node-studio.tokens.generic.group {
background: transparent;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/graph-editor/src/editor/actions/createNode.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Dispatch } from '@/redux/store.js';
import { Graph, Node, NodeFactory } from '@tokens-studio/graph-engine';
import { ReactFlowInstance, Node as ReactFlowNode } from 'reactflow';
import { uiNodeType } from '@/annotations/index.js';

export type NodeRequest = {
type: string;
Expand Down Expand Up @@ -73,7 +74,7 @@ export const createNode = ({
node.annotations['ypos'] = finalPos.y;

if (customUI[nodeRequest.type]) {
node.annotations['uiNodeType'] = customUI[nodeRequest.type];
node.annotations[uiNodeType] = customUI[nodeRequest.type];
}

//Set values from the request
Expand Down
Loading
Loading