diff --git a/README.md b/README.md index 16cea9c..e8ef225 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,12 @@ It comes with the following features: - **Selection** of nodes and edges, one at a time - Uses **`Recoil`** for shared state management - Automatically re-calculate the **height** of nodes when jumping lines in `textarea` -- Graph data (nodes, edges) are **persisted** in the browser **localstorage** and loaded upon page reload +- ~~Graph data (nodes, edges) are **persisted** in the browser **localstorage** and automatically loaded upon page reload~~ + - Graph data (nodes, edges) are **persisted** in FaunaDB and automatically loaded upon page reload +- Real-time support for collaboration (open 2 tabs), using FaunaDB + - FaunaDB token is public and has read/update access rights on one table of the DB only + - All users share the same "Canvas" document in the DB + - This POC will **not improve further** the collaborative experience, it's only a POC (undo/redo undoes peer actions, undo/redo seems a bit broken sometimes) Known limitations: - Editor direction is `RIGHT` (hardcoded) and adding nodes will add them to the right side, always (even if you change the direction) @@ -36,6 +41,15 @@ Known limitations: > This POC can be used as a boilerplate to start your own project using Reaflow. +## Variants + +While working on this project, I've reached several milestones with a different set of features, available as "Examples": + +1. [`with-local-storage`](https://github.com/Vadorequest/poc-nextjs-reaflow/tree/with-local-storage) + ([Demo](https://poc-nextjs-reaflow-git-with-local-storage-ambroise-dhenain.vercel.app/) | [Diff](https://github.com/Vadorequest/poc-nextjs-reaflow/pull/14)): + The canvas dataset is stored in the browser localstorage. + There is no real-time and no authentication. + ## Getting started - `yarn` diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 0000000..4b59f2d --- /dev/null +++ b/examples/index.html @@ -0,0 +1,59 @@ + + + + + +

Scores stream

+

+ Scores: +

+ + + + + diff --git a/package.json b/package.json index b1f089b..94efa6d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "build": "next build", "start": "next dev --port 8890", "type-check": "tsc", - "link:reaflow": "yarn link reaflow && yarn link react && yarn link react-dom" + "link:reaflow": "yarn link reaflow && yarn link react && yarn link react-dom", + "deploy:fake": "git commit --allow-empty -m \"Fake empty commit (force CI trigger)\"" }, "dependencies": { "@chakra-ui/icons": "1.0.4", @@ -25,6 +26,7 @@ "@welldone-software/why-did-you-render": "6.0.5", "animate.css": "4.1.1", "classnames": "2.2.6", + "faunadb": "4.1.1", "framer-motion": "3.2.1", "lodash.capitalize": "4.2.1", "lodash.clonedeep": "4.5.0", @@ -45,7 +47,7 @@ "react-dom": "17.0.1", "react-select": "4.0.2", "react-textarea-autosize": "8.3.0", - "reaflow": "3.0.13", + "reaflow": "3.0.14", "recoil": "0.1.2", "recoil-devtools-dock": "^0.1.6", "recoil-devtools-log-monitor": "^0.2.7", diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 35ad0c1..095ded1 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -35,11 +35,11 @@ const Footer: React.FunctionComponent = (props) => { Demo: - With Local storage + With Real-time @@ -50,13 +50,22 @@ const Footer: React.FunctionComponent = (props) => { /> Made with {' '}Next.js{' '} + {' '}, {' '} + + {' '}FaunaDB{' '} + + {' '}and{' '} = (props) => { diff --git a/src/components/edges/BaseEdge.tsx b/src/components/edges/BaseEdge.tsx index d45aa47..244893f 100644 --- a/src/components/edges/BaseEdge.tsx +++ b/src/components/edges/BaseEdge.tsx @@ -1,10 +1,12 @@ +import { css } from '@emotion/react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classnames from 'classnames'; import cloneDeep from 'lodash.clonedeep'; import now from 'lodash.now'; import React from 'react'; import { Edge, - EdgeData, + EdgeChildProps, } from 'reaflow'; import { SetterOrUpdater, @@ -31,7 +33,6 @@ import { getDefaultNodePropsWithFallback, upsertNodeThroughPorts, } from '../../utils/nodes'; -import EdgeActions from './EdgeActions'; type Props = {} & BaseEdgeProps; @@ -87,9 +88,8 @@ const BaseEdge: React.FunctionComponent = (props) => { * by splitting the edge in two parts and adding the new node in between. * * @param event - * @param edge_DO_NOT_USE */ - const onAddIconClick = (event: React.MouseEvent, edge_DO_NOT_USE: EdgeData): void => { + const onAddIconClick = (event: React.MouseEvent): void => { console.log('onAdd edge', edge, event); const onBlockClick: OnBlockClick = (nodeType: NodeType) => { console.log('onBlockClick (from edge add)', nodeType, edge); @@ -122,9 +122,8 @@ const BaseEdge: React.FunctionComponent = (props) => { * Removes the selected edge. * * @param event - * @param edge */ - const onRemoveIconClick = (event: React.MouseEvent, edge: EdgeData): void => { + const onRemoveIconClick = (event: React.MouseEvent): void => { console.log('onRemoveIconClick', event, edge); setEdges(edges.filter((edge: BaseEdgeData) => edge.id !== id)); }; @@ -146,34 +145,72 @@ const BaseEdge: React.FunctionComponent = (props) => { return ( ); }; diff --git a/src/components/editor/CanvasContainer.tsx b/src/components/editor/CanvasContainer.tsx index 6ede80f..7ab09b6 100644 --- a/src/components/editor/CanvasContainer.tsx +++ b/src/components/editor/CanvasContainer.tsx @@ -16,6 +16,7 @@ import { useUndo, } from 'reaflow'; import { useRecoilState } from 'recoil'; +import { updateSharedCanvasDocument } from '../../lib/faunadbClient'; import settings from '../../settings'; import { blockPickerMenuSelector } from '../../states/blockPickerMenuState'; import { canvasDatasetSelector } from '../../states/canvasDatasetSelector'; @@ -25,11 +26,15 @@ import { selectedEdgesSelector } from '../../states/selectedEdgesState'; import { selectedNodesSelector } from '../../states/selectedNodesState'; import BaseNodeData from '../../types/BaseNodeData'; import { isOlderThan } from '../../utils/date'; +import { createEdge } from '../../utils/edges'; import { createNodeFromDefaultProps, getDefaultNodePropsWithFallback, } from '../../utils/nodes'; -import { persistCanvasDatasetInLS } from '../../utils/persistCanvasDataset'; +import { + getDefaultFromPort, + getDefaultToPort, +} from '../../utils/ports'; import canvasUtilsContext from '../context/canvasUtilsContext'; import BaseEdge from '../edges/BaseEdge'; import NodeRouter from '../nodes/NodeRouter'; @@ -80,12 +85,13 @@ const CanvasContainer: React.FunctionComponent = (props): JSX.Element | n const [cursorXY, setCursorXY] = useState<[number, number]>([0, 0]); /** - * When nodes or edges are modified, updates the persisted data in the local storage. + * When nodes or edges are modified, updates the persisted data in FaunaDB. * * Persisted data are automatically loaded upon page refresh. */ useEffect(() => { - persistCanvasDatasetInLS(canvasDataset); + // persistCanvasDatasetInLS(canvasDataset); + updateSharedCanvasDocument(canvasDataset); }, [canvasDataset]); /** @@ -118,19 +124,30 @@ const CanvasContainer: React.FunctionComponent = (props): JSX.Element | n }); /** - * Ensures the start node is always present. + * Ensures the "start" node and "end" node are always present. * - * Will automatically create the start node even if all the nodes are deleted. + * Will automatically create the start/end nodes, even when all the nodes have been deleted. */ useEffect(() => { - const startNode: BaseNodeData | undefined = nodes?.find((node: BaseNodeData) => node?.data?.type === 'start'); + const existingStartNode: BaseNodeData | undefined = nodes?.find((node: BaseNodeData) => node?.data?.type === 'start'); + const existingEndNode: BaseNodeData | undefined = nodes?.find((node: BaseNodeData) => node?.data?.type === 'end'); - if (!startNode) { + if (!existingStartNode || !existingEndNode) { console.info(`No "start" node found. Creating one automatically.`, nodes); - setNodes([ - ...nodes, - createNodeFromDefaultProps(getDefaultNodePropsWithFallback('start')), - ]); + const startNode: BaseNodeData = createNodeFromDefaultProps(getDefaultNodePropsWithFallback('start')); + const endNode: BaseNodeData = createNodeFromDefaultProps(getDefaultNodePropsWithFallback('end')); + const newNodes = [ + startNode, + endNode, + ]; + const newEdges = [ + createEdge(startNode, endNode, getDefaultFromPort(startNode), getDefaultToPort(endNode)), + ]; + + setCanvasDataset({ + nodes: newNodes, + edges: newEdges, + }); // Clearing the undo/redo history to avoid allowing the editor to "undo" the creation of the "start" node // If the "start" node creation step is "undoed" then it'd be re-created automatically, which would erase the whole history @@ -235,7 +252,7 @@ const CanvasContainer: React.FunctionComponent = (props): JSX.Element | n // CSS rules applied to the whole (global rules, within the ) .reaflow-canvas { // Make all edges display an infinite dash animation - .edge { + .edge-svg-graph { stroke: ${settings.canvas.edges.strokeColor}; stroke-dasharray: 5; animation: dashdraw .5s linear infinite; diff --git a/src/components/editor/PlaygroundContainer.tsx b/src/components/editor/PlaygroundContainer.tsx index 48eccb2..1b79f8a 100644 --- a/src/components/editor/PlaygroundContainer.tsx +++ b/src/components/editor/PlaygroundContainer.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/react'; import React, { MutableRefObject } from 'react'; import { CanvasRef } from 'reaflow'; import BlockPickerMenu from '../blocks/BlockPickerMenu'; +import AbsoluteTooltip from '../plugins/AbsoluteTooltip'; import CanvasContainer from './CanvasContainer'; type Props = { @@ -45,6 +46,7 @@ const PlaygroundContainer: React.FunctionComponent = (props): JSX.Element /> + ); }; diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index 55dd475..d7e74d8 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -38,6 +38,7 @@ import { } from '../../utils/nodes'; import { createPort } from '../../utils/ports'; import BasePort from '../ports/BasePort'; +import BasePortChild from '../ports/BasePortChild'; type Props = BaseNodeProps & { hasCloneAction?: boolean; @@ -270,7 +271,16 @@ const BaseNode: BaseNodeComponent = (props) => { onKeyDown={onKeyDown} onRemove={onNodeRemove} remove={(