Skip to content

Conversation

@paula-stacho
Copy link
Collaborator

@paula-stacho paula-stacho commented Nov 21, 2025

External Links

🎟️

Description

Changes:

  • in addition to the existing onNodeExpandToggle, the client can provide onFieldExpandToggle
  • within the NodeField, the client can provide expanded
  • whether the fields expandable is calculated internally, but the toggle is only displayed if onFieldExpandToggle is provided
  • the top level toggle now has two visuals
  • the top level toggle is not controlled by the client, instead it reacts to the field state and allows expanding if any fields are collapsed
  • [BREAKING CHANGE]: for field-to-field edges, the client is to provide the source|targetFieldId instead of source|targetFieldIndex. the latter isn't suitable anymore because not all fields are visible

Note: Expandability depends on the existence of children more than on the direct types (for example there can be an object without children, or an array/mixed field that does have children).

TODO

  • make sure the height calculation uses the filtered fields

📸 Screenshots/Screencasts

Node with collapse/expand functionality

Screenshot 2025-11-21 at 16 43 34

Node without collapse/expand functionality

Screenshot 2025-11-21 at 16 43 23

Top level and field level collapse behaviour

Screen.Recording.2025-11-20.at.13.52.36.mov

@paula-stacho paula-stacho marked this pull request as ready for review November 21, 2025 15:45
@paula-stacho
Copy link
Collaborator Author

Thank you for the feedback @gribnoysup ! I think I addressed everything we discussed and it does make more sense now. Can you take another look?

Copy link
Contributor

@gribnoysup gribnoysup left a comment

Choose a reason for hiding this comment

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

Left a few comments, but nothing blocking for this PR, just some things to consider. Good work and thanks for having a productive technical discussion on the topic, I think this helped to find a really good approach to this.


const { onChangeFieldName, onChangeFieldType, fieldTypes, onFieldExpandToggle } = useEditableDiagramInteractions();

const hasCollapseFunctionality = !!onFieldExpandToggle;
Copy link
Contributor

Choose a reason for hiding this comment

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

Absolutely not suggesting you need to do something about it, but something about the inconsistency of the variable saying "collapse", but the public method saying "expand" really made me pause here for a sec 😂

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's fair, let me rename it so that it doesn't confuse anyone

Comment on lines 260 to 288
const onNodeExpandToggle = useCallback((_evt: ReactMouseEvent, nodeId: string, expanded: boolean) => {
setExpanded(state => {
return {
...state,
[nodeId]: !state[nodeId],
[nodeId]: {
node: expanded,
fields: {},
},
};
});
}, []);

const onFieldExpandToggle = useCallback((_evt: ReactMouseEvent, nodeId: string, fieldId: FieldId) => {
const key = getExpandedFieldKey(fieldId);
setExpanded(state => {
const prevNodeState = state[nodeId] ?? {};
const prevFieldState = isFieldExpanded(state, nodeId, fieldId);
return {
...state,
[nodeId]: {
...prevNodeState,
fields: {
...(prevNodeState.fields ?? {}),
[key]: !prevFieldState,
},
},
};
});
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is slightly tangential, but want to call this out (because it's relevant for how to model this on compass side also):

I think instead of having expanded as an extra state on node in addition to fields having an expanded state, we should treat "expand / collapse all" as an action that just affects all fields, so instead of keeping this expanded state here for the node, I would suggest you update the fields directly. I think you'll see that this will simplify the logic in the callback props, and will remove the need to do some extra processing for the nodes before passing them down to the diagram component.

Following is not a fully tested suggestion, but kinda close to the real one:

Suggested change
const onNodeExpandToggle = useCallback((_evt: ReactMouseEvent, nodeId: string, expanded: boolean) => {
setExpanded(state => {
return {
...state,
[nodeId]: !state[nodeId],
[nodeId]: {
node: expanded,
fields: {},
},
};
});
}, []);
const onFieldExpandToggle = useCallback((_evt: ReactMouseEvent, nodeId: string, fieldId: FieldId) => {
const key = getExpandedFieldKey(fieldId);
setExpanded(state => {
const prevNodeState = state[nodeId] ?? {};
const prevFieldState = isFieldExpanded(state, nodeId, fieldId);
return {
...state,
[nodeId]: {
...prevNodeState,
fields: {
...(prevNodeState.fields ?? {}),
[key]: !prevFieldState,
},
},
};
});
}, []);
const onNodeExpandToggle = useCallback((_evt: ReactMouseEvent, nodeId: string, expanded: boolean) => {
setNodes(nodes =>
nodes.map(node =>
node.id === nodeId
? {
...node,
fields: node.fields.map(field => {
return { ...field, expanded }
}),
}
: node,
),
);
}, []);
const onFieldExpandToggle = useCallback((_evt: ReactMouseEvent, nodeId: string, fieldId: FieldId) => {
setNodes(nodes =>
nodes.map(node =>
node.id === nodeId
? {
...node,
fields: fields.map(field => {
return field.id === fieldId
? { ...field, expanded: !field.expanded }
: field
}),
}
: node,
),
);
}, []);

* Called when the button to expand / collapse a single field is clicked
* header.
*/
export type OnFieldExpandHandler = (event: ReactMouseEvent, nodeId: string, fieldPath: string[]) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure it's worth it, but maybe easier then on the other side instead and more consistent with OnNodeExpandHandler where we have to provide this value

Suggested change
export type OnFieldExpandHandler = (event: ReactMouseEvent, nodeId: string, fieldPath: string[]) => void;
export type OnFieldExpandHandler = (event: ReactMouseEvent, nodeId: string, fieldPath: string[], shouldExpand: boolean) => void;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, why not!

Comment on lines 6 to 7
function hasChildren(fields: NodeField[], index: number): boolean {
const fieldDepth = fields[index].depth ?? 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suuuuper nit, you're calling this in a place where we have the field already, so instead of picking it from an array by index, might just pass it down (and then this function matches the map function interface and this is slightly satisfying)

Suggested change
function hasChildren(fields: NodeField[], index: number): boolean {
const fieldDepth = fields[index].depth ?? 0;
function hasChildren(field: NodeField, index: number, fields: NodeField[]): boolean {
const fieldDepth = field.depth ?? 0;

Copy link
Collaborator Author

@paula-stacho paula-stacho Dec 11, 2025

Choose a reason for hiding this comment

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

Okay, the map function interface analogy really sold it :D

@paula-stacho paula-stacho merged commit c42d569 into main Dec 11, 2025
5 checks passed
@paula-stacho paula-stacho deleted the collapse-field-level branch December 11, 2025 13:19
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.

3 participants