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 (
- )}
+ className={classnames(`edge-svg-graph`, { 'is-selected': isSelected })}
onClick={onEdgeClick}
- />
- // Doesn't support children - See https://github.com/reaviz/reaflow/issues/67
- // Possible to use a custom children to achieve this, but not great DX because of manual x/y placement
- /*
-
- test
-
- * */
+ >
+ {
+ (edgeChildProps: EdgeChildProps) => {
+ const {
+ center,
+ } = edgeChildProps;
+
+ // Improve centering (because we have 3 icons), and position the foreignObject children above the line
+ const x = (center?.x || 0) - 25;
+ const y = (center?.y || 0) - 25;
+
+ return (
+ that are using the CSS "position" attribute won't be shown properly,
+ // unless they're wrapped into a container using a "fixed" position.
+ // Solves the display of React Select element.
+ // See https://github.com/chakra-ui/chakra-ui/issues/3288#issuecomment-776316200
+ position: fixed;
+ }
+
+ .svg-inline--fa {
+ cursor: pointer;
+ }
+ `}
+ >
+ {
+ isSelected && (
+
+
+
+
+
+
+
+ )
+ }
+
+ );
+ }
+ }
+
);
};
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