-
Notifications
You must be signed in to change notification settings - Fork 21
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
Hide/show child nodes of a branch #325
Conversation
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. |
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. |
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. |
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 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. |
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. |
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. |
…ry attribute in IMmpClientNode
let tapedTwice = false | ||
|
||
dom.nodes.each((node: Node) => { |
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.
wouldnt it be easier to set the hidden field directly when the button is pressed for all nodes, and not inside this render method?
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 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.
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. |
I'll take a look at undo/redo. |
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. |
I've removed all text on export now, so neither "link" nor "visibility_off" should appear on export. |
…ties: hidden when parent is hidden
…n state when map is updated
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 |
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.
can't this be set directly, when the button is clicked (hide children)? Do we have to calculate this dynamically?
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.
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?
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.
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()
.
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.
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.
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.
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() |
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.
why let here? do you need to modify this ?
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.
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') |
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.
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?
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.
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.
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.
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) { |
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 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 |
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.
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; |
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.
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).
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.
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?
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.
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.
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.
Yeah, I've refactored this one to return undefined, I'll open an issue for the other ones.
closes #105