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

Hide/show child nodes of a branch #325

Conversation

sorenjohanson
Copy link
Collaborator

closes #105

@sorenjohanson
Copy link
Collaborator Author

Do we want this to be a client-side only feature or server-side as well? Right now, there's still a bug left that the hidden state is not updated across all clients immediately (though I'd assume it calls map.draw.update() whenever something changes), but nodes are correctly hidden if the client updates the page.

Regarding an icon: Somehow very tricky to implement correctly and synchronize across all clients simultaneously.

@JannikStreek
Copy link
Member

Do we want this to be a client-side only feature or server-side as well? Right now, there's still a bug left that the hidden state is not updated across all clients immediately (though I'd assume it calls map.draw.update() whenever something changes), but nodes are correctly hidden if the client updates the page.

Regarding an icon: Somehow very tricky to implement correctly and synchronize across all clients simultaneously.

good question, I would say as it only affects visibility it should only be on the client side? otherwise people might confuse this for that a user deleted a branch.

@sorenjohanson
Copy link
Collaborator Author

good question, I would say as it only affects visibility it should only be on the client side? otherwise people might confuse this for that a user deleted a branch.

Does it make sense to keep the changes to the data model then? Since those changes are synced server-side, inevitably leading new clients to consider selected nodes hidden as well.

@JannikStreek
Copy link
Member

good question, I would say as it only affects visibility it should only be on the client side? otherwise people might confuse this for that a user deleted a branch.

Does it make sense to keep the changes to the data model then? Since those changes are synced server-side, inevitably leading new clients to consider selected nodes hidden as well.

Either we sync it across clients so everybody is watching the same and we have it in our data model - or we just store that property on the client side without adding it to the data model. I think I would prefer the latter one.

@sorenjohanson
Copy link
Collaborator Author

Works up to 99% percent, but there's still a weird and oddly specific bug where if two clients hide the same branches at the same time (so branch A and B), client A will unhide branch A when clicking on branch B.

Also missing: Changing the eye icon in the toolbar based on whether or not any child nodes are hidden.

@JannikStreek
Copy link
Member

Works up to 99% percent, but there's still a weird and oddly specific bug where if two clients hide the same branches at the same time (so branch A and B), client A will unhide branch A when clicking on branch B.

Also missing: Changing the eye icon in the toolbar based on whether or not any child nodes are hidden.

i dont really understand this problem if hiding/showing is a local operation 🤔

@sorenjohanson
Copy link
Collaborator Author

Works up to 99% percent, but there's still a weird and oddly specific bug where if two clients hide the same branches at the same time (so branch A and B), client A will unhide branch A when clicking on branch B.
Also missing: Changing the eye icon in the toolbar based on whether or not any child nodes are hidden.

i dont really understand this problem if hiding/showing is a local operation 🤔

It's not really consistently reproducible, but it's there - I'm trying to figure out if it maybe has something to do with how nodes selected by other users are rendered (maybe it affects the selectedNode variable in nodes.ts?). It's purely a client side issue though, most probably in how D3 renders the nodes, especially when other clients are involved.

@sorenjohanson
Copy link
Collaborator Author

Think I found the issue: The NodeUpdated event gets called for Client B whenever Client A hides certain nodes. However, if Client B has already hidden the same set of nodes, the "hidden" values gets overwritten with null, since UtilsService.get(newNode, propertyPath) doesn't return a proper value. That means Client B needs to click twice to unhide already hidden nodes, since it's set from null to true to false.

This can also lead to very funny issues when Client A hides a child, Client B hides every node except the root node and Client A then decides to also hide the root node, because this then hides every node except the originally hidden child.

@JannikStreek
Copy link
Member

Think I found the issue: The NodeUpdated event gets called for Client B whenever Client A hides certain nodes. However, if Client B has already hidden the same set of nodes, the "hidden" values gets overwritten with null, since UtilsService.get(newNode, propertyPath) doesn't return a proper value. That means Client B needs to click twice to unhide already hidden nodes, since it's set from null to true to false.

This can also lead to very funny issues when Client A hides a child, Client B hides every node except the root node and Client A then decides to also hide the root node, because this then hides every node except the originally hidden child.

well why is an update event triggered in the first place? as the "hidden/show" property should be a local value, we shouldn't send a web socket update. An mmp event is fine as it's the local map that was updated. That should, however, not trigger a web socket update to other parties?

On the other hand, an update to not visible nodes should influence the local data model but not the drawing area.

@sorenjohanson
Copy link
Collaborator Author

sorenjohanson commented Jul 24, 2024

well why is an update event triggered in the first place? as the "hidden/show" property should be a local value, we shouldn't send a web socket update. An mmp event is fine as it's the local map that was updated. That should, however, not trigger a web socket update to other parties?

On the other hand, an update to not visible nodes should influence the local data model but not the drawing area.

notifyWithEvent is true by default, forgot to change that to false.

Good point about the local data model - I'll have to test that. Hiding nodes is much more stable now, though I do have odd one-off cases where the parent nodes get hidden whilst children are still visible. I'll set the PR to open for review soon and then we can both test and iron out any kinks.

@sorenjohanson sorenjohanson marked this pull request as ready for review July 24, 2024 10:49
@sorenjohanson
Copy link
Collaborator Author

Updating the data model works fine even when nodes are hidden. One thing that's still missing is some visible way to tell nodes are hidden, either by updating the icon in the toolbar or by attaching an SVG icon inside the drawing area to nodes.

@JannikStreek
Copy link
Member

Updating the data model works fine even when nodes are hidden. One thing that's still missing is some visible way to tell nodes are hidden, either by updating the icon in the toolbar or by attaching an SVG icon inside the drawing area to nodes.

a small svg icon somewhere in the node sounds good 👍 the toolbar should also change (thats already done for add/remove image). But I think an additional item inside the node makes also sense as otherwise one might forget it.

let tapedTwice = false

dom.nodes.each((node: Node) => {
Copy link
Member

Choose a reason for hiding this comment

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

wouldnt it be easier to set the hidden field directly when the button is pressed for all nodes, and not inside this render method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure what you mean by this? There's practically no difference whether you actually set a field on the node itself or do it like this, as you'd still have to call both methods (setHiddenChildrenIcon and removeHiddenChildrenIcon) separately whenever you add/remove the icon to make it work reliably, at least in my testing.

You also can't avoid calling dom.nodes.each as outer.each (which is based on the enter method of D3.js) only seems to include nodes that were changed, ie those that were hidden, whereas we explicitly want the node that wasn't hidden.

@JannikStreek
Copy link
Member

JannikStreek commented Jul 29, 2024

Looks good. Two bugs that need more checking:

  • Undo/Redo is bugged - I got very strange results with it with nodes flying around in space without being attached. If possible I wouldn't consider visibility changes as not un/redoable. But thats just on first thought.
  • Hiding nodes and then exporting the image looks like this:
Bildschirmfoto 2024-07-29 um 16 18 03

Not sure what the preferred behavior for this feature is on exports. Either completely hide it (like it is now) or just show it - but without this visibility note thingy. The same also happens for attached links, which just show "link" - but thats a separate issue.

@sorenjohanson
Copy link
Collaborator Author

Not sure what the preferred behavior for this feature is on exports. Either completely hide it (like it is now) or just show it - but without this visibility note thingy. The same also happens for attached links, which just show "link" - but thats a separate issue.

Re this: I think keeping notes invisible when exporting is the preferred option. However, it seems like it doesn't work with Material Icons, so I'm guessing removing the link text ("visibility_off") is the better option.

@sorenjohanson
Copy link
Collaborator Author

I'll take a look at undo/redo.

@sorenjohanson
Copy link
Collaborator Author

I don't think there's much we can do about undo/redo without a rework. The issue is that if we hide two nodes, that's two steps in the snapshot, with the farthest node being the first step in the snapshot to be undone. Similar weird behaviour can be observed when creating a node with text and undoing it: Creating the node is one step, with the text being a separate step, so it takes two presses of undo to delete a single node.

@sorenjohanson
Copy link
Collaborator Author

Not sure what the preferred behavior for this feature is on exports. Either completely hide it (like it is now) or just show it - but without this visibility note thingy. The same also happens for attached links, which just show "link" - but thats a separate issue.

I've removed all text on export now, so neither "link" nor "visibility_off" should appear on export.

@JannikStreek
Copy link
Member

There are still bugs: If another user adds nodes to a hidden parent node, it gets weird:

Bildschirmfoto 2024-08-10 um 14 08 34 Bildschirmfoto 2024-08-10 um 14 08 29

if (!x.parent.hidden && x.hidden) {
this.updateNode('hidden', false, false, false, false, x.id)
}
})
}

// Lengthy but definitive check to see if we have any hidden nodes after toggling
if (this.map.nodes.nodeChildren(this.selectedNode.id)?.filter(x => x.hidden).length > 0) {
this.selectedNode.hasHiddenChildNodes = true
Copy link
Member

Choose a reason for hiding this comment

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

can't this be set directly, when the button is clicked (hide children)? Do we have to calculate this dynamically?

Copy link
Member

@JannikStreek JannikStreek Aug 13, 2024

Choose a reason for hiding this comment

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

As I see it, this property makes sense. So we have two new fields:

  • hidden (on the descendants)
  • hiddenBranches (on the parent)

So I would set them both upon button press?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It was a computed property before, but there's an issue with the history if we make it computed: Suppose that parent node A has exactly one child node B which is hidden. If you undo/redo the creation of the child node B, there's no way to tell it's supposed to be hidden, as the previous map (client side) doesn't include child node B, only the snapshot we receive from the server does (which doesn't have the client side "hidden" attribute). That's why we have hasHiddenChildNodes, which is set on parent node A.

So now we have two attributes:

  • hasHiddenChildNodes, which is always set on the parent node of any hidden children
  • hidden, which is set on the actual nodes that are hidden

The former only affects history and rendering of the hidden eye icon, the latter affects whether the node is actually visible or not. Both of them are set when calling toggleBranchVisibility().

Copy link
Member

Choose a reason for hiding this comment

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

good point, maybe you can add this reason as a comment? that there is not only the button that triggers it and thats the reason you have to check here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good idea, I've added some comments.

nodes: this.map.dom.g.selectAll('.' + this.map.id + '_node').data(nodes),
branches: this.map.dom.g.selectAll('.' + this.map.id + '_branch').data(nodes.slice(1))
}
let nodes = this.map.nodes.getNodes()
Copy link
Member

Choose a reason for hiding this comment

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

why let here? do you need to modify this ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, must've been a mistake from when I separated out nodes and dom variable creation.

@@ -265,29 +282,59 @@ export default class Draw {
*/
public setLink(node: Node) {
let domLink = node.getLinkDOM()
const domText = document.createElementNS('http://www.w3.org/2000/svg', 'text')
Copy link
Member

Choose a reason for hiding this comment

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

why was this change needed? On first glance, if the link Dom is already present, it makes no sense to draw on top of 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.

Was testing to see if it made a difference in rendering, I should be able to revert the changes to the method since the issue was with the querySelector and not the setLink() method.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changes are reverted, works as before now.

* Check if given node has hidden children and render the eye icon if so
* @param {Node} node
*/
private checkForHiddenChildren(node: Node) {
Copy link
Member

Choose a reason for hiding this comment

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

this method does not only check, but actually hides or even removes Dom icons. That should be reflected in the name, as its otherwise confusing

} else if (this.checkSnapshotStructure(snapshot)) {
const previousData = this.map.export.asJSON()

// Find all nodes where we've set hasHiddenChildNodes in the previous map
Copy link
Member

Choose a reason for hiding this comment

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

maybe apply refactoring: extract method, as this is a lot of logic only dedicating to the hiding of nodes.

}

if (node.hidden !== hidden) {
node.hidden = hidden;
Copy link
Member

Choose a reason for hiding this comment

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

please check to return a consistent return type. here, you either return false or you assign a variable and return undefined (which is also false).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All the other methods also either assign a variable or return false, so I was just orienting myself on those. Maybe a general refactoring idea?

Copy link
Member

Choose a reason for hiding this comment

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

imho only because the other methods follow a bad practice, this doesn't mean we have to continue this way. The return value is useless if it's always false? But yes, the other methods could be refactored in a separate refactoring.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, I've refactored this one to return undefined, I'll open an issue for the other ones.

@JannikStreek JannikStreek merged commit c1b3dc2 into b310-digital:main Aug 14, 2024
5 checks passed
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.

Hide/Show child nodes of a branch
2 participants