Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,28 @@ 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)
- I don't plan on changing that at the moment

> 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`
Expand Down
59 changes: 59 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<html>
<head>
<style>
span.scores {
font-family: monospace;
white-space: pre;
}
</style>
</head>
<body>
<h1>Scores stream</h1>
<p>
Scores: <span class="scores"></span>
</p>
</body>

<script src="https://cdn.jsdelivr.net/npm/faunadb@latest/dist/faunadb-min.js"></script>
<script type="text/javascript">

var faunadb = window.faunadb;
var q = faunadb.query;

var client = new faunadb.Client({
secret: 'fnAEDdp0CWACBZUTQvkktsqAQeW03uDhZYY0Ttlg',
});

var docRef = q.Ref(q.Collection('Scores'), '1');

function report(e) {
console.log(e);
var data = ('action' in e)
? e['document'].data
: e.data;
document.body.innerHTML += '<p><span class="scores">' +
JSON.stringify(data) +
'</span></p>';
}

var stream;
const startStream = () => {
stream = client.stream.document(docRef)
.on('snapshot', snapshot => {
report(snapshot);
})
.on('version', version => {
report(version);
})
.on('error', error => {
console.log('Error:', error);
stream.close();
setTimeout(startStream, 1000);
})
.start();
};

startStream();

</script>
</html>
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
15 changes: 12 additions & 3 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ const Footer: React.FunctionComponent<Props> = (props) => {
<Box p="2">
Demo:
<Tooltip
label="This demo showcases Reaflow with a single document that is stored in the browser local storage."
label="This demo showcases FaunaDB real-time where there is a global document shared by all users and updated in real time. (using FaunaDB subscriptions)"
fontSize="md"
>
<Tag>
<b><TagLabel>With Local storage</TagLabel></b>
<b><TagLabel>With Real-time</TagLabel></b>
</Tag>
</Tooltip>
</Box>
Expand All @@ -50,13 +50,22 @@ const Footer: React.FunctionComponent<Props> = (props) => {
/>
Made with
<ChakraLink
href={'https://nextjs.org/'}
href={'https://nextjs.org'}
isExternal
color="teal.500"
>
{' '}Next.js{' '}
<ExternalLinkIcon mx="2px" />
</ChakraLink>
{' '}, {' '}
<ChakraLink
href={'https://fauna.com'}
isExternal
color="teal.500"
>
{' '}FaunaDB{' '}
<ExternalLinkIcon mx="2px" />
</ChakraLink>
{' '}and{' '}
<ChakraLink
href={'https://github.com/reaviz/reaflow'}
Expand Down
7 changes: 6 additions & 1 deletion src/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Center,
Flex,
Spacer,
Tooltip,
} from '@chakra-ui/react';
import { css } from '@emotion/react';
import React from 'react';
Expand All @@ -25,7 +30,7 @@ const Nav: React.FunctionComponent<Props> = (props) => {
<nav className={'nav'}>
<Center>
<Box p="2">
You are a guest working on your own document. Changes are automatically saved in the browser local storage and cannot be shared or seen by anyone else.
You are currently working on a shared document, updated in real-time by any visitor.
</Box>
</Center>
</nav>
Expand Down
103 changes: 70 additions & 33 deletions src/components/edges/BaseEdge.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -31,7 +33,6 @@ import {
getDefaultNodePropsWithFallback,
upsertNodeThroughPorts,
} from '../../utils/nodes';
import EdgeActions from './EdgeActions';

type Props = {} & BaseEdgeProps;

Expand Down Expand Up @@ -87,9 +88,8 @@ const BaseEdge: React.FunctionComponent<Props> = (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<SVGGElement, MouseEvent>, edge_DO_NOT_USE: EdgeData): void => {
const onAddIconClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
console.log('onAdd edge', edge, event);
const onBlockClick: OnBlockClick = (nodeType: NodeType) => {
console.log('onBlockClick (from edge add)', nodeType, edge);
Expand Down Expand Up @@ -122,9 +122,8 @@ const BaseEdge: React.FunctionComponent<Props> = (props) => {
* Removes the selected edge.
*
* @param event
* @param edge
*/
const onRemoveIconClick = (event: React.MouseEvent<SVGGElement, MouseEvent>, edge: EdgeData): void => {
const onRemoveIconClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
console.log('onRemoveIconClick', event, edge);
setEdges(edges.filter((edge: BaseEdgeData) => edge.id !== id));
};
Expand All @@ -146,34 +145,72 @@ const BaseEdge: React.FunctionComponent<Props> = (props) => {
return (
<Edge
{...props}
className={classnames(`edge`, { 'is-selected': isSelected })}
add={(
<EdgeActions
hidden={!isSelected}
onAdd={onAddIconClick}
onRemove={onRemoveIconClick}
/>
)}
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
/*
<foreignObject
width={30}
height={30}
// x={1272}
// y={231}
css={css`
position: absolute;
//left: 1272px;
//top: 231px;
color: black;
`}
>
test
</foreignObject>
* */
>
{
(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 (
<foreignObject
id={`edge-foreignObject-${edge.id}`}
className={classnames(`edge-container`, {
'is-selected': isSelected,
})}
width={100} // Content width will be limited by the width of the foreignObject
height={60}
x={x}
y={y}
css={css`
position: relative;
color: black;
z-index: 1;

.edge {
// XXX Elements within a <foreignObject> 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 && (
<div className={'edge'}>
<FontAwesomeIcon
icon={['fas', 'search-plus']}
onClick={onAddIconClick}
/>

<FontAwesomeIcon
icon={['fas', 'search-minus']}
onClick={onRemoveIconClick}
/>

<FontAwesomeIcon
icon={['fas', 'edit']}
onClick={onRemoveIconClick}
/>
</div>
)
}
</foreignObject>
);
}
}
</Edge>
);
};

Expand Down
Loading