From 071770316b6bf0bf518047a3ab707ee0687382bd Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Tue, 24 Feb 2026 18:46:06 +0530 Subject: [PATCH 1/2] Fix tree view checkbox selection for backup dialog. #9649 --- web/pgadmin/static/js/PgTreeView/index.jsx | 58 ++++++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/web/pgadmin/static/js/PgTreeView/index.jsx b/web/pgadmin/static/js/PgTreeView/index.jsx index 18492448c01..04fdd1c550c 100644 --- a/web/pgadmin/static/js/PgTreeView/index.jsx +++ b/web/pgadmin/static/js/PgTreeView/index.jsx @@ -80,39 +80,77 @@ export default function PgTreeView({ data = [], hasCheckbox = false, const [checkedState, setCheckedState] = React.useState({}); const { ref: containerRef, width, height } = useResizeObserver(); + // Handle checkbox toggle and collect all checked nodes + // to pass complete selection state to the backup dialog const toggleCheck = (node, isChecked) => { const newState = { ...checkedState }; - const selectedChNodes = []; - // Update the node itself and all descendants + // Update the clicked node and all its descendants with the new checked value const updateDescendants = (n, val) => { newState[n.id] = val; - if (val) { - selectedChNodes.push(n); - } n.children?.forEach(child => updateDescendants(child, val)); }; updateDescendants(node, isChecked); - // Update ancestors (Indeterminate logic) + // Update ancestor nodes to reflect the correct state (checked/unchecked/indeterminate) + // This ensures parent nodes show proper visual feedback based on children's state let parent = node.parent; while (parent && parent.id !== '__root__') { - const allChecked = parent.children.every(c => newState[c.id]); + // Check if ALL children are fully checked (state must be exactly true, + // not 'indeterminate') to mark parent as fully checked + const allChecked = parent.children.every(c => newState[c.id] === true); + // Check if ALL children are unchecked (falsy value: false, undefined, or null) const noneChecked = parent.children.every(c => !newState[c.id]); if (allChecked) { + // All children checked -> parent is fully checked newState[parent.id] = true; - // logic for custom indeterminate property if needed } else if (noneChecked) { + // No children checked -> parent is unchecked newState[parent.id] = false; } else { - newState[parent.id] = 'indeterminate'; // Store string for 3rd state + // Some children checked, some not -> parent shows indeterminate state + newState[parent.id] = 'indeterminate'; } parent = parent.parent; } setCheckedState(newState); - selectionChange?.(selectedChNodes); + + // Collect all checked/indeterminate nodes from the entire tree + // to provide complete selection state to selectionChange callback. + const allCheckedNodes = []; + const collectAllCheckedNodes = (n) => { + if (!n) return; + const state = newState[n.id]; + if (state === true || state === 'indeterminate') { + // Set isIndeterminate flag to differentiate full schema selection + // from partial selection (only specific tables) in backup dialog + n.data.isIndeterminate = (state === 'indeterminate'); + allCheckedNodes.push(n); + } + // Recursively check all children + n.children?.forEach(child => collectAllCheckedNodes(child)); + }; + + // Navigate up to find the root level of the tree (parent of root nodes is '__root__') + let rootNode = node; + while (rootNode.parent && rootNode.parent.id !== '__root__') { + rootNode = rootNode.parent; + } + + // Traverse all root-level nodes to collect checked nodes from entire tree + const rootParent = rootNode.parent; + if (rootParent && rootParent.children) { + // Iterate through all sibling root nodes to collect all checked nodes + rootParent.children.forEach(root => collectAllCheckedNodes(root)); + } else { + // Fallback: if we can't find siblings, just traverse from the found root + collectAllCheckedNodes(rootNode); + } + + // Pass all checked nodes to callback with current selection state. + selectionChange?.(allCheckedNodes); }; return ( From a3f1c055bf0759bc82894cd719df2425ccc3fcfa Mon Sep 17 00:00:00 2001 From: Rohit Bhati Date: Thu, 26 Feb 2026 15:43:34 +0530 Subject: [PATCH 2/2] Address CodeRabbit review: immutable selection state and lint-compliant forEach callbacks. --- web/pgadmin/static/js/PgTreeView/index.jsx | 19 +++++++++++-------- .../tools/backup/static/js/backup.ui.js | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/web/pgadmin/static/js/PgTreeView/index.jsx b/web/pgadmin/static/js/PgTreeView/index.jsx index 04fdd1c550c..529361cda2f 100644 --- a/web/pgadmin/static/js/PgTreeView/index.jsx +++ b/web/pgadmin/static/js/PgTreeView/index.jsx @@ -88,7 +88,7 @@ export default function PgTreeView({ data = [], hasCheckbox = false, // Update the clicked node and all its descendants with the new checked value const updateDescendants = (n, val) => { newState[n.id] = val; - n.children?.forEach(child => updateDescendants(child, val)); + n.children?.forEach(child => { updateDescendants(child, val); }); }; updateDescendants(node, isChecked); @@ -118,19 +118,22 @@ export default function PgTreeView({ data = [], hasCheckbox = false, setCheckedState(newState); // Collect all checked/indeterminate nodes from the entire tree - // to provide complete selection state to selectionChange callback. + // to provide complete selection state to selectionChange callback. + // We use wrapper objects to avoid mutating the original node data. const allCheckedNodes = []; const collectAllCheckedNodes = (n) => { if (!n) return; const state = newState[n.id]; if (state === true || state === 'indeterminate') { - // Set isIndeterminate flag to differentiate full schema selection - // from partial selection (only specific tables) in backup dialog - n.data.isIndeterminate = (state === 'indeterminate'); - allCheckedNodes.push(n); + // Pass wrapper object with isIndeterminate flag to differentiate + // full schema selection from partial selection in backup dialog + allCheckedNodes.push({ + node: n, + isIndeterminate: state === 'indeterminate' + }); } // Recursively check all children - n.children?.forEach(child => collectAllCheckedNodes(child)); + n.children?.forEach(child => { collectAllCheckedNodes(child); }); }; // Navigate up to find the root level of the tree (parent of root nodes is '__root__') @@ -143,7 +146,7 @@ export default function PgTreeView({ data = [], hasCheckbox = false, const rootParent = rootNode.parent; if (rootParent && rootParent.children) { // Iterate through all sibling root nodes to collect all checked nodes - rootParent.children.forEach(root => collectAllCheckedNodes(root)); + rootParent.children.forEach(root => { collectAllCheckedNodes(root); }); } else { // Fallback: if we can't find siblings, just traverse from the found root collectAllCheckedNodes(rootNode); diff --git a/web/pgadmin/tools/backup/static/js/backup.ui.js b/web/pgadmin/tools/backup/static/js/backup.ui.js index 329f126b009..9460b5b6a82 100644 --- a/web/pgadmin/tools/backup/static/js/backup.ui.js +++ b/web/pgadmin/tools/backup/static/js/backup.ui.js @@ -758,8 +758,8 @@ export default class BackupSchema extends BaseUISchema { 'foreign table': [], 'materialized view': [], }; - state?.objects?.forEach((node)=> { - if(node.data.is_schema && !node.data?.isIndeterminate) { + state?.objects?.forEach(({ node, isIndeterminate })=> { + if(node.data.is_schema && !isIndeterminate) { selectedNodeCollection['schema'].push(node.data.name); } else if(['table', 'view', 'materialized view', 'foreign table', 'sequence'].includes(node.data.type) && !node.data.is_collection && !selectedNodeCollection['schema'].includes(node.data.schema)) {