Skip to content

Commit 4f576f4

Browse files
dhasilvaclaude
andcommitted
Backup: fix file-browser tree-state, indeterminate, and item count
Three related bugs in the modernized file-browser selection: - Deselecting a row inside a selected subtree only added an exception but never propagated up. Now walks up: when every loaded child of an ancestor is effectively deselected, the ancestor is deselected too (and re-selecting reverses the same walk: when every loaded child becomes effectively selected, the ancestor follows). Stops at the first ancestor with un-loaded children. - Indeterminate only fired for selected folders with a deselected descendant. Now symmetric: an unselected folder with a selected descendant also reads as indeterminate (e.g. visitor checks a single leaf without its ancestors). - `applySelect` cleared an own-negative entry but didn't add an own positive, so re-checking a path whose ancestor stayed in `deselected` would silently inherit unchecked. Now: drop the negative and, only if removing it wouldn't already leave the row inherited-selected from an ancestor, add the explicit positive. - The header "selected" count tracked `selected.size` (the explicit positives), which never grew as the visitor expanded an inherited subtree. Now hoists loaded folder children to the FileBrowser via a per-row callback and counts effectively-selected leaves only: a file counts as 1, a folder counts as 1 only when its contents haven't been loaded yet (otherwise the server treats it as one opaque download unit), and indeterminate folders contribute only the effectively-selected descendants underneath. BackupDetail reads the same count for its Download / Restore action labels via a new `onSelectionCountChange` callback, and the labels switch to proper singular/plural via `_n()`. `useMockFileTree` no longer seeds non-null callers with the root tree on first render — the seeding leaked the root tree into closed NodeRows' `children` state and let the registerChildren effect accidentally register MOCK_FILE_TREE for non-root paths during the window between "open clicked" and "fetch resolved", which infinitely recursed the count walk. FileBrowser now imports `MOCK_FILE_TREE` directly for its roots; the hook handles only lazy non-root loads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f69d275 commit 4f576f4

3 files changed

Lines changed: 376 additions & 109 deletions

File tree

projects/packages/backup/src/dashboard/components/backup-detail/index.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,52 @@
11
import { dateI18n } from '@wordpress/date';
22
import { useState } from '@wordpress/element';
3-
import { __, sprintf } from '@wordpress/i18n';
3+
import { __, _n, sprintf } from '@wordpress/i18n';
44
import { Icon, cloud, download as downloadIcon, rotateLeft } from '@wordpress/icons';
55
import { Link } from '@wordpress/route';
66
import { Card, Stack, Text } from '@wordpress/ui';
77
import FileBrowser, { EMPTY_FILE_SELECTION } from '../file-browser';
88
import './style.scss';
9-
import type { FileSelection } from '../file-browser';
109
import type { BackupActivityItem } from '../../types/activity';
10+
import type { FileSelection } from '../file-browser';
1111

1212
type Props = {
1313
item: BackupActivityItem;
1414
};
1515

1616
/**
17-
* Returns the appropriate "Download" header-label given how many files
17+
* Returns the appropriate "Download" header-label given how many items
1818
* the visitor has selected in the file browser. With zero selections we
1919
* default to the whole-backup download; otherwise we count the selected
20-
* paths.
20+
* items (files + folders) currently visible in the loaded tree.
2121
*
22-
* @param count - Number of currently selected files/folders.
22+
* @param count - Number of currently selected items.
2323
* @return Localized button label.
2424
*/
2525
function downloadLabel( count: number ): string {
2626
if ( count === 0 ) {
2727
return __( 'Download backup', 'jetpack-backup-pkg' );
2828
}
2929
return sprintf(
30-
/* translators: %d count of selected files */
31-
__( 'Download %d selected files', 'jetpack-backup-pkg' ),
30+
/* translators: %d count of selected items (files + opaque folders) */
31+
_n( 'Download %d selected item', 'Download %d selected items', count, 'jetpack-backup-pkg' ),
3232
count
3333
);
3434
}
3535

3636
/**
37-
* Returns the appropriate "Restore" header-label given how many files
37+
* Returns the appropriate "Restore" header-label given how many items
3838
* the visitor has selected in the file browser.
3939
*
40-
* @param count - Number of currently selected files/folders.
40+
* @param count - Number of currently selected items.
4141
* @return Localized button label.
4242
*/
4343
function restoreLabel( count: number ): string {
4444
if ( count === 0 ) {
4545
return __( 'Restore to this point', 'jetpack-backup-pkg' );
4646
}
4747
return sprintf(
48-
/* translators: %d count of selected files */
49-
__( 'Restore %d selected files', 'jetpack-backup-pkg' ),
48+
/* translators: %d count of selected items (files + opaque folders) */
49+
_n( 'Restore %d selected item', 'Restore %d selected items', count, 'jetpack-backup-pkg' ),
5050
count
5151
);
5252
}
@@ -66,11 +66,12 @@ function restoreLabel( count: number ): string {
6666
*/
6767
export default function BackupDetail( { item }: Props ) {
6868
const [ selection, setSelection ] = useState< FileSelection >( EMPTY_FILE_SELECTION );
69-
// "X selected files" in the header reflects the positive set only —
70-
// negative exceptions on a folder's subtree don't bump the count
71-
// up, so the label always matches what the visitor explicitly asked
72-
// for ("you picked N things, here are the exceptions inside them").
73-
const count = selection.selected.size;
69+
// `count` tracks the number of items the visitor sees ticked in the
70+
// loaded subtree (selections + inherited descendants + indeterminate
71+
// folders). FileBrowser owns the loaded children, so it reports the
72+
// count back here for the header labels to swap between "Download
73+
// backup" and "Download N items".
74+
const [ count, setCount ] = useState( 0 );
7475

7576
return (
7677
<Card.Root className="jpb-backup-detail">
@@ -112,6 +113,7 @@ export default function BackupDetail( { item }: Props ) {
112113
rewindId={ item.rewindId }
113114
selection={ selection }
114115
onSelectionChange={ setSelection }
116+
onSelectionCountChange={ setCount }
115117
/>
116118
</div>
117119
</Card.Content>

0 commit comments

Comments
 (0)