Skip to content

Commit

Permalink
fix: delete nodes (#41)
Browse files Browse the repository at this point in the history
* init debugging

* found bug and overwrite `xyflow` behaviour until issue is resolved

* update feedback

* update docs

* update docs

* adding docs to navigation
  • Loading branch information
xiduzo authored Dec 15, 2024
1 parent 8385fff commit 844e778
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 119 deletions.
2 changes: 1 addition & 1 deletion FEEDBACK.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Some thoughts

- [ ] it feels a bit unhandy that the inputs are on the left side of the node and the outputs are a the bottom. This kind of forces the program to grow in a starcase shape downwards towards the right, which forces scrolling in two directions instead of just up-down or left-right. In biggish sketches it would be more usable to have a dataflow that travel in one axis only.
- [ ] for some reason to delete a node I must select it and delete it twice
- [x] for some reason to delete a node I must select it and delete it twice
- [x] difference between the `save` and the `save & close` button in the settings pannel not very clear, what is saved? you mean apply new settings to node? or save state in file? bit unclear? could the settings be applied just 'on change' in the dialog entry?
- [x] when I was messing around with the codebase to add a new node type I saw the app fail silently... I thought that was a javascript feature that was not possible with typescript.

Expand Down
2 changes: 1 addition & 1 deletion apps/electron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"@microflow/ui": "workspaces:*",
"@microflow/utils": "workspaces:*",
"@tsparticles/react": "3.0.0",
"@xyflow/react": "12.3.1",
"@xyflow/react": "12.3.6",
"abcjs": "6.4.3",
"electron-log": "5.2.0",
"firmata": "https://github.com/xiduzo/firmata.js.git",
Expand Down
4 changes: 2 additions & 2 deletions apps/electron-app/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ body {

/* Nodes */
.react-flow__minimap .react-flow__minimap-node {
@apply fill-muted transition-all;
@apply fill-muted transition-colors;
}
.react-flow__minimap .react-flow__minimap-node.selected {
@apply fill-blue-500;
}

/* Edges */
.react-flow__edges .react-flow__edge-path {
@apply stroke-[4] stroke-neutral-600 transition-all;
@apply stroke-[4] stroke-neutral-600 transition-colors;
}

.react-flow__edges .react-flow__edge.selected .react-flow__edge-path,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useAutoAnimate } from '@ui/index';
import { Background, Controls, MiniMap, Panel, ReactFlow } from '@xyflow/react';
import { useShallow } from 'zustand/react/shallow';
import { NODE_TYPES } from '../../../common/nodes';
import { AppState, useReactFlowStore } from '../../stores/react-flow';
import { SerialConnectionStatusPanel } from './panels/SerialConnectionStatusPanel';
import { SettingsPanel } from './panels/SettingsPanel';

const selector = (state: AppState) => ({
nodes: state.nodes,
Expand All @@ -15,9 +15,6 @@ const selector = (state: AppState) => ({

export function ReactFlowCanvas() {
const store = useReactFlowStore(useShallow(selector));
const [animationRef] = useAutoAnimate({
duration: 100,
});

return (
<ReactFlow
Expand All @@ -27,7 +24,6 @@ export function ReactFlowCanvas() {
colorMode="dark"
minZoom={0.2}
maxZoom={2}
disableKeyboardA11y={true}
>
<Controls />
<MiniMap nodeBorderRadius={12} />
Expand All @@ -51,9 +47,7 @@ export function ReactFlowCanvas() {
</Panel>

<Panel position="top-right">
<section id="settings-panels" className="flex flex-col space-y-2" ref={animationRef}>
{/* Filled by settings */}
</section>
<SettingsPanel />
</Panel>
</ReactFlow>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function NodeHeader(props: { error?: string; selected?: boolean }) {
</TooltipProvider>
)}
</div>
<span className="text-muted-foreground/40 text-xs font-extralight">{id}</span>
<span className="text-muted-foreground text-xs font-extralight">{id}</span>
</div>
<NodeSettingsButton />
</header>
Expand Down Expand Up @@ -167,7 +167,6 @@ function NodeSettingsPane<T extends Record<string, unknown>>(
const updateNode = useUpdateNode(id);

const ref = useRef<HTMLDivElement>(null);
const autoUpdate = useRef<NodeJS.Timeout | null>(null);
const settings = useRef(data);
const handlesToDelete = useRef<string[]>([]);

Expand All @@ -192,10 +191,7 @@ function NodeSettingsPane<T extends Record<string, unknown>>(
pane.on('change', event => {
if (!event.last) return;

autoUpdate.current && clearTimeout(autoUpdate.current);
autoUpdate.current = setTimeout(() => {
saveSettings();
}, 1_000);
saveSettings();
});

pane.registerPlugin(TweakpaneEssentialPlugin);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useAutoAnimate } from '@ui/index';

export function SettingsPanel() {
const [animationRef] = useAutoAnimate({
duration: 100,
});

return (
<section id="settings-panels" className="flex flex-col space-y-2" ref={animationRef}>
{/* Filled by settings */}
</section>
);
}
103 changes: 68 additions & 35 deletions apps/electron-app/src/render/providers/NewNodeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,32 @@ import {
DialogTitle,
} from '@microflow/ui';
import { useReactFlow } from '@xyflow/react';
import { memo, useEffect } from 'react';
import { memo, useEffect, useMemo } from 'react';
import { DEFAULT_NODE_DATA, NodeType } from '../../common/nodes';
import { useTempNode } from '../stores/react-flow';
import { useDeleteSelectedNodes, useNodesChange } from '../stores/react-flow';
import { useNewNodeStore } from '../stores/new-node';
import { useShallow } from 'zustand/react/shallow';
import { useWindowSize } from 'usehooks-ts';

const NODE_SIZE = {
width: 208,
height: 176,
};

export const NewNodeCommandDialog = memo(function NewNodeCommandDialog() {
useDraggableNewNode();
useBackspaceOverwrite();

const { open, setOpen, setNodeToAdd } = useNewNodeStore();
const { addNode } = useTempNode();
const { flowToScreenPosition } = useReactFlow();
const changeNodes = useNodesChange();
const windowSize = useWindowSize();

const position = useMemo(() => {
return flowToScreenPosition({
x: windowSize.width / 2 - NODE_SIZE.width / 2,
y: windowSize.height / 2 - NODE_SIZE.height / 2,
});
}, [flowToScreenPosition, windowSize]);

function selectNode(type: NodeType, options?: { label?: string; subType?: string }) {
return function () {
Expand All @@ -36,13 +52,12 @@ export const NewNodeCommandDialog = memo(function NewNodeCommandDialog() {
},
id,
type,
position: { x: 0, y: 0 },
position,
selected: true,
};

addNode(newNode);
changeNodes([{ item: newNode, type: 'add' }]);
setNodeToAdd(id);
setOpen(false);
};
}

Expand Down Expand Up @@ -218,54 +233,54 @@ export const NewNodeCommandDialog = memo(function NewNodeCommandDialog() {
});

function useDraggableNewNode() {
const { nodeToAdd, setNodeToAdd } = useNewNodeStore(
useShallow(state => ({ nodeToAdd: state.nodeToAdd, setNodeToAdd: state.setNodeToAdd })),
);
const { screenToFlowPosition, updateNode } = useReactFlow();
const { addNode, deleteNode } = useTempNode();
const { nodeToAdd, setNodeToAdd } = useNewNodeStore();
const { screenToFlowPosition, getZoom } = useReactFlow();
const changeNodes = useNodesChange();

useEffect(() => {
if (!nodeToAdd) return;

function handleKeyDown(event: KeyboardEvent) {
if (!nodeToAdd) return;
if (event.key === 'Escape' || event.key === 'Backspace') {
setNodeToAdd(null);
deleteNode(nodeToAdd);
}

if (event.key === 'Enter') {
const element = event.target as HTMLElement;
if (element !== document.body) return;
switch (event.key) {
case 'Backspace':
case 'Escape':
changeNodes([{ id: nodeToAdd, type: 'remove' }]);
setNodeToAdd(null);
break;
case 'Enter':
const element = event.target as HTMLElement;
if (element !== document.body) return;

setNodeToAdd(null);
updateNode(nodeToAdd, { selected: false });
changeNodes([{ id: nodeToAdd, type: 'select', selected: false }]);
setNodeToAdd(null);
break;
}
}

function handleMouseDown(event: MouseEvent) {
if (!nodeToAdd) return;
updateNode(nodeToAdd, {
position: screenToFlowPosition({
x: event.clientX - 120,
y: event.clientY - 75,
}),
});
const element = event.target as HTMLElement;
if (!element.closest('.react-flow__node')) return;

changeNodes([{ id: nodeToAdd, type: 'select', selected: false }]);
setNodeToAdd(null);
updateNode(nodeToAdd, { selected: false });
}

function handleMouseMove(event: MouseEvent) {
if (!nodeToAdd) return;
updateNode(nodeToAdd, {
position: screenToFlowPosition({
x: event.clientX - 120,
y: event.clientY - 75,
}),
});
const zoom = getZoom();
changeNodes([
{
id: nodeToAdd,
type: 'position',
position: screenToFlowPosition({
x: event.clientX - (NODE_SIZE.width / 2) * zoom,
y: event.clientY - (NODE_SIZE.height / 2) * zoom,
}),
},
]);
}

document.addEventListener('keydown', handleKeyDown);
Expand All @@ -279,7 +294,25 @@ function useDraggableNewNode() {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('click', handleMouseDown);
};
}, [nodeToAdd, addNode, deleteNode]);
}, [nodeToAdd, getZoom, changeNodes]);

return null;
}

// https://github.com/xyflow/xyflow/issues/4761
export function useBackspaceOverwrite() {
const deleteSelectedNodes = useDeleteSelectedNodes();

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === 'Backspace') {
deleteSelectedNodes();
}
};
window.addEventListener('keydown', handleKeyDown);

return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [deleteSelectedNodes]);
}
2 changes: 2 additions & 0 deletions apps/electron-app/src/render/stores/new-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ export const useNewNodeStore = create<NewNodeStore>((set, get) => ({
nodeToAdd: null as string | null,
setNodeToAdd: (nodeId: string | null) => {
set({ nodeToAdd: nodeId });

if (nodeId) set({ open: false });
},
}));
54 changes: 26 additions & 28 deletions apps/electron-app/src/render/stores/react-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { LinkedList } from '../../common/LinkedList';
import { INTRODUCTION_EDGES, INTRODUCTION_NODES } from './introduction';
import { useShallow } from 'zustand/react/shallow';

const HISTORY_DEBOUNCE_TIME_IN_MS = 1000;
const HISTORY_DEBOUNCE_TIME_IN_MS = 100;

export type AppState<NodeData extends Record<string, unknown> = {}> = {
nodes: Node<NodeData>[];
Expand All @@ -25,8 +25,7 @@ export type AppState<NodeData extends Record<string, unknown> = {}> = {
setNodes: (nodes: Node<NodeData>[]) => void;
setEdges: (edges: Edge[]) => void;
deleteEdges: (nodeId: string, handles?: string[]) => void;
addNode: (node: Node<NodeData>) => void;
deleteNode: (nodeId: string) => void;
deleteSelectedNodes: () => void;
history: LinkedList<{ nodes: Node[]; edges: Edge[] }>;
undo: () => void;
redo: () => void;
Expand Down Expand Up @@ -61,9 +60,7 @@ export const useReactFlowStore = create<AppState>((set, get) => {
const initialEdges = hasSeenIntroduction ? localEdges : INTRODUCTION_EDGES;

let historyUpdateDebounce: NodeJS.Timeout | undefined;
function updateHistory(update: Partial<Pick<AppState, 'nodes' | 'edges'>>) {
set(update);

function updateHistory() {
historyUpdateDebounce && clearTimeout(historyUpdateDebounce);
historyUpdateDebounce = setTimeout(() => {
const { nodes, edges, history } = get();
Expand All @@ -76,19 +73,28 @@ export const useReactFlowStore = create<AppState>((set, get) => {
edges: initialEdges,
history: new LinkedList({ nodes: initialNodes, edges: initialEdges }),
onNodesChange: changes => {
updateHistory({ nodes: applyNodeChanges(changes, get().nodes) });
set({
nodes: applyNodeChanges(changes, get().nodes),
});

updateHistory();
},
onEdgesChange: changes => {
updateHistory({ edges: applyEdgeChanges(changes, get().edges) });
set({
edges: applyEdgeChanges(changes, get().edges),
});
updateHistory();
},
onConnect: connection => {
updateHistory({ edges: addEdge(connection, get().edges) });
set({
edges: addEdge(connection, get().edges),
});
},
setNodes: nodes => {
updateHistory({ nodes });
set({ nodes });
},
setEdges: edges => {
updateHistory({ edges });
set({ edges });
},
deleteEdges: (nodeId, handles = []) => {
if (!handles.length) return;
Expand All @@ -104,19 +110,12 @@ export const useReactFlowStore = create<AppState>((set, get) => {

return false;
});
updateHistory({ edges });
},
addNode: node => {
if (!node.data) node.data = {};

updateHistory({ nodes: [...get().nodes, node] });
set({ edges });
},
deleteNode: nodeId => {
const nodes = get().nodes.filter(node => node.id !== nodeId);
deleteSelectedNodes: () => {
const nodes = get().nodes.filter(node => !node.selected);
set({ nodes });

const edges = get().edges.filter(edge => edge.source !== nodeId && edge.target !== nodeId);
updateHistory({ edges });
},
undo: () => {
const history = get().history;
Expand Down Expand Up @@ -146,13 +145,8 @@ export function useNodeAndEdgeCount() {
);
}

export function useTempNode() {
return useReactFlowStore(
useShallow(state => ({
addNode: state.addNode,
deleteNode: state.deleteNode,
})),
);
export function useNodesChange() {
return useReactFlowStore(useShallow(state => state.onNodesChange));
}

export function useDeleteEdges() {
Expand All @@ -162,3 +156,7 @@ export function useDeleteEdges() {
export function useEdges() {
return useReactFlowStore(useShallow(state => state.edges));
}

export function useDeleteSelectedNodes() {
return useReactFlowStore(useShallow(state => state.deleteSelectedNodes));
}
Loading

0 comments on commit 844e778

Please sign in to comment.