Skip to content
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

Merged
merged 6 commits into from
Jan 3, 2023
Merged

refactor Canvas #189

merged 6 commits into from
Jan 3, 2023

Conversation

lihebi
Copy link
Collaborator

@lihebi lihebi commented Jan 2, 2023

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 logic
    • useNodeLocation: provide checkNodesEndLocation to check if the node is dropped into a scope, if so, set parent and update levels.
    • useNodeOperations: provide addNode to create new nodes
    • useInitNodes: the logic to load nodes on entry. Steps:
      1. connect Yjs provider
      2. If Y.doc is inconsistent with database, use database to overwrite Y.doc
  • [refactor 3] cleaner separation among the following three data stores: (1) Yjs, (2) Zustand store, (3) database.

    • the Canvas only operates on Yjs'snodesMap (with a few exceptions e.g., addPod)
    • The nodesMap operation (add, delete, update(x,y,width,height,parent))) will trigger observer to push to Zustand store and remote database. Here's the logic:
      1. Always push to Zustand store
      2. When 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.
      • Additional note: now this update(x,y,width,height,parent) observer will push node position and parent to Zustand store and database, so
        • there's no need to useEffect on xPos,yPos in e.g. CodeNode.
        • there's no need to setPodPosition and setPodParent in checkNodesEndLocation

Other small refactors:

  • use node.width instead of node.style.width
  • use node.data.level instead of node.level
  • deletePod(apolloClient, {id, toDelete}) no longer accepts toDelete.

@@ -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: [] });
Copy link
Collaborator

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.

Copy link
Collaborator Author

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.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.

Copy link
Collaborator Author

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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

@li-xin-yi li-xin-yi Jan 3, 2023

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

@li-xin-yi li-xin-yi Jan 3, 2023

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

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.

Copy link
Collaborator Author

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!

Copy link
Collaborator Author

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

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).

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.

Comment on lines +701 to +703
// NOTE we have to trigger an update here, otherwise the nodes are not
// rendered.
triggerUpdate();
Copy link
Collaborator Author

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.

@lihebi lihebi changed the title (WIP) refactor Canvas refactor Canvas Jan 3, 2023
Comment on lines +123 to +129
// 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 }] });
Copy link
Collaborator Author

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.

@lihebi lihebi merged commit d61f9c7 into main Jan 3, 2023
@lihebi lihebi deleted the refactor-canvas branch June 30, 2023 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants