Skip to content

Commit caafc6b

Browse files
Fix broken checkbox selection in backup dialog objects tree. #9649
1 parent e9e5290 commit caafc6b

File tree

2 files changed

+54
-13
lines changed

2 files changed

+54
-13
lines changed

web/pgadmin/static/js/PgTreeView/index.jsx

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -80,39 +80,80 @@ export default function PgTreeView({ data = [], hasCheckbox = false,
8080
const [checkedState, setCheckedState] = React.useState({});
8181
const { ref: containerRef, width, height } = useResizeObserver();
8282

83+
// Handle checkbox toggle and collect all checked nodes
84+
// to pass complete selection state to the backup dialog
8385
const toggleCheck = (node, isChecked) => {
8486
const newState = { ...checkedState };
85-
const selectedChNodes = [];
8687

87-
// Update the node itself and all descendants
88+
// Update the clicked node and all its descendants with the new checked value
8889
const updateDescendants = (n, val) => {
8990
newState[n.id] = val;
90-
if (val) {
91-
selectedChNodes.push(n);
92-
}
93-
n.children?.forEach(child => updateDescendants(child, val));
91+
n.children?.forEach(child => { updateDescendants(child, val); });
9492
};
9593
updateDescendants(node, isChecked);
9694

97-
// Update ancestors (Indeterminate logic)
95+
// Update ancestor nodes to reflect the correct state (checked/unchecked/indeterminate)
96+
// This ensures parent nodes show proper visual feedback based on children's state
9897
let parent = node.parent;
9998
while (parent && parent.id !== '__root__') {
100-
const allChecked = parent.children.every(c => newState[c.id]);
99+
// Check if ALL children are fully checked (state must be exactly true,
100+
// not 'indeterminate') to mark parent as fully checked
101+
const allChecked = parent.children.every(c => newState[c.id] === true);
102+
// Check if ALL children are unchecked (falsy value: false, undefined, or null)
101103
const noneChecked = parent.children.every(c => !newState[c.id]);
102104

103105
if (allChecked) {
106+
// All children checked -> parent is fully checked
104107
newState[parent.id] = true;
105-
// logic for custom indeterminate property if needed
106108
} else if (noneChecked) {
109+
// No children checked -> parent is unchecked
107110
newState[parent.id] = false;
108111
} else {
109-
newState[parent.id] = 'indeterminate'; // Store string for 3rd state
112+
// Some children checked, some not -> parent shows indeterminate state
113+
newState[parent.id] = 'indeterminate';
110114
}
111115
parent = parent.parent;
112116
}
113117

114118
setCheckedState(newState);
115-
selectionChange?.(selectedChNodes);
119+
120+
// Collect all checked/indeterminate nodes from the entire tree
121+
// to provide complete selection state to selectionChange callback.
122+
// We use wrapper objects to avoid mutating the original node data.
123+
const allCheckedNodes = [];
124+
const collectAllCheckedNodes = (n) => {
125+
if (!n) return;
126+
const state = newState[n.id];
127+
if (state === true || state === 'indeterminate') {
128+
// Pass wrapper object with isIndeterminate flag to differentiate
129+
// full schema selection from partial selection in backup dialog
130+
allCheckedNodes.push({
131+
node: n,
132+
isIndeterminate: state === 'indeterminate'
133+
});
134+
}
135+
// Recursively check all children
136+
n.children?.forEach(child => { collectAllCheckedNodes(child); });
137+
};
138+
139+
// Navigate up to find the root level of the tree (parent of root nodes is '__root__')
140+
let rootNode = node;
141+
while (rootNode.parent && rootNode.parent.id !== '__root__') {
142+
rootNode = rootNode.parent;
143+
}
144+
145+
// Traverse all root-level nodes to collect checked nodes from entire tree
146+
const rootParent = rootNode.parent;
147+
if (rootParent && rootParent.children) {
148+
// Iterate through all sibling root nodes to collect all checked nodes
149+
rootParent.children.forEach(root => { collectAllCheckedNodes(root); });
150+
} else {
151+
// Fallback: if we can't find siblings, just traverse from the found root
152+
collectAllCheckedNodes(rootNode);
153+
}
154+
155+
// Pass all checked nodes to callback with current selection state.
156+
selectionChange?.(allCheckedNodes);
116157
};
117158

118159
return (<Root ref={containerRef} className={'PgTree-tree'}>

web/pgadmin/tools/backup/static/js/backup.ui.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -758,8 +758,8 @@ export default class BackupSchema extends BaseUISchema {
758758
'foreign table': [],
759759
'materialized view': [],
760760
};
761-
state?.objects?.forEach((node)=> {
762-
if(node.data.is_schema && !node.data?.isIndeterminate) {
761+
state?.objects?.forEach(({ node, isIndeterminate })=> {
762+
if(node.data.is_schema && !isIndeterminate) {
763763
selectedNodeCollection['schema'].push(node.data.name);
764764
} else if(['table', 'view', 'materialized view', 'foreign table', 'sequence'].includes(node.data.type) &&
765765
!node.data.is_collection && !selectedNodeCollection['schema'].includes(node.data.schema)) {

0 commit comments

Comments
 (0)