-
Notifications
You must be signed in to change notification settings - Fork 16
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
refactor Canvas #189
refactor Canvas #189
Conversation
ui/src/lib/nodes.tsx
Outdated
@@ -128,19 +148,22 @@ export function useNodesStateSynced(nodeList) { | |||
} else if (change.action === "delete") { | |||
const node = change.oldValue; | |||
console.log("todelete", node); | |||
deletePod(null, { id: node.id, toDelete: [] }); | |||
deletePod(apolloClient, { id: node.id, toDelete: [] }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imagine the situation: If a node is deleted by a collaborator, a deletion operation is synced to nodesMap
. On my side, I pulled the update from nodesMap
, it will schedule a duplicate deletion request to DB. That's why I set the client to null
, only update store.pods
in local when detecting any deletion from the remote side. The deletion in DB should only be requested by the user who explicitly deletes it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I’m adding a condition and setting it to null for remote peers.
!node.data.hasOwnProperty("clientId") || | ||
node.data.clientId === clientId | ||
) | ||
.sort((a: Node, b: Node) => a.data.level - b.data.level) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.sort((a: Node, b: Node) => a.data.level - b.data.level) | |
.sort((a: Node, b: Node) => a.data.level - b.data.level) | |
.map((node) => ({ | |
...node, | |
selected: selectedPods.has(node.id), | |
hidden: node.data?.hidden === clientId, | |
})) |
and then you can re-use triggerUpdate
in many places to replace setNodes
, such as obverser
of the nodesMap
, selectPod
, onFocus
functions in RichNode and codeNode (which unselect all pods), etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I've added this to triggerUpdate
.
setPasting(null); | ||
// delete the original (hidden) node | ||
if (cutting) { | ||
nodesMap.delete(cutting); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nodesMap.delete(cutting); | |
reactFlowInstance.deleteElements({ nodes: [{ id: cutting }] }); |
As I mentioned before, make sure the remote deletion only scheduled by the actual user who request to do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel deleteElements
is the same thing as (and triggers) nodesMap.delete
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really. onNodesDelete
only be called when deleteElements
is explicitly called or nodes are deleting by shortcut (Del). From that, we can distinguish whether the deletion is issued by local user or remote peers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think onNodesChange
covers onNodesDelete
. I actually removed onNodesDelete
.
I plan to use Yjs's transaction.origin
(ref) to distinguish between local and remote edits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I think you can call remote deletions to DB only in onNodesChange
here
Line 79 in abc81df
if (isNodeRemoveChange(change)) { |
Then all deletions observed from nodesMap
are synced from remote peers (yes maybe from myself, but deletePod
will check if the pod exists first).
Anyway, it's up to you. But be careful, I don't know if the onNodesChange
really exactly covers only all explicit deletions. I will test it when your PR finishes tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've finished this PR. Feel free to leave comments!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I think you can call remote deletions to DB only in
onNodesChange
hereLine 79 in abc81df
if (isNodeRemoveChange(change)) { Then all deletions observed from
nodesMap
are synced from remote peers (yes maybe from myself, butdeletePod
will check if the pod exists first).
I'm handling both zustand-store and database updates in Yjs observer (which is triggered when this user or other users make change to nodesMap
). And I'm using onNodesChange
only for updating nodesMap when there're reactflow operations.
// NOTE we have to trigger an update here, otherwise the nodes are not | ||
// rendered. | ||
triggerUpdate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually where I use triggerUpdate
. Without this, the canvas will not show anything.
// This does not work, will throw "Parent node | ||
// jqgdsz2ns6k57vich0bf not found" when deleting a scope. | ||
// | ||
// nodesMap.delete(id); | ||
// | ||
// But this works: | ||
reactFlowInstance.deleteElements({ nodes: [{ id }] }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@li-xin-yi nodesMap.delete
and reactFlowInstance.deleteElements
are indeed different, te latter seems to support recursive deletion of a scope.
Refactor:
[refactor 1] split node types into
nodes/[Code|Scope|Rich].tsx
.[refactor 2] separate logics from the Canvas component into hooks:
useCopyPaste
: the copy paste cut logicuseNodeLocation
: providecheckNodesEndLocation
to check if the node is dropped into a scope, if so, set parent and update levels.useNodeOperations
: provideaddNode
to create new nodesuseInitNodes
: the logic to load nodes on entry. Steps:[refactor 3] cleaner separation among the following three data stores: (1) Yjs, (2) Zustand store, (3) database.
nodesMap
(with a few exceptions e.g.,addPod
)add
,delete
,update(x,y,width,height,parent)
)) will trigger observer to push to Zustand store and remote database. Here's the logic:transaction.local === true && !node.data?.clientId
(i.e., the change is made by the current user, and it is not a temporary pasting node), write to database.update(x,y,width,height,parent)
observer will push node position and parent to Zustand store and database, souseEffect
onxPos
,yPos
in e.g. CodeNode.setPodPosition
andsetPodParent
incheckNodesEndLocation
Other small refactors:
node.width
instead ofnode.style.width
node.data.level
instead ofnode.level
deletePod(apolloClient, {id, toDelete})
no longer acceptstoDelete
.