diff --git a/app/src/components/FileBrowser.js b/app/src/components/FileBrowser.js
index 902478e8..2d98025e 100644
--- a/app/src/components/FileBrowser.js
+++ b/app/src/components/FileBrowser.js
@@ -158,6 +158,8 @@ export function FileBrowser() {
const [singleDirectorySelected, setSingleDirectorySelected] = React.useState(false);
// Only true if single directory is selected, and it is in settings['ignored_directories']
const [isDirectoryIgnored, setIsDirectoryIgnored] = React.useState(false);
+ // Tracks folder that was opened while selected - its children will be auto-selected when they load
+ const pendingAutoSelectRef = React.useRef(null);
const {settings, fetchSettings} = React.useContext(SettingsContext);
const {inverted} = React.useContext(ThemeContext);
@@ -195,6 +197,39 @@ export function FileBrowser() {
}
}, [selectedPaths, settings]);
+ // Auto-select immediate children when a selected folder is opened
+ useEffect(() => {
+ const folderToAutoSelect = pendingAutoSelectRef.current;
+ if (folderToAutoSelect && browseFiles && typeof browseFiles === 'object') {
+ // browseFiles is a dict: {'map/': {path: 'map/', children: {...}}, ...}
+ // children is also a dict: {'file.txt': {path: 'map/file.txt', ...}, ...}
+ const findFolderChildren = (files, targetFolder) => {
+ if (!files || typeof files !== 'object') return null;
+ // Convert dict to array of values for iteration
+ const fileList = Array.isArray(files) ? files : Object.values(files);
+ for (const file of fileList) {
+ if (file.path === targetFolder && file.children) {
+ // Return children as array of values
+ return Array.isArray(file.children) ? file.children : Object.values(file.children);
+ }
+ if (file.children) {
+ const found = findFolderChildren(file.children, targetFolder);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+
+ const children = findFolderChildren(browseFiles, folderToAutoSelect);
+ if (children && children.length > 0) {
+ const childPaths = children.map(child => child.path);
+ const newSelectedPaths = [...new Set([...selectedPaths, ...childPaths])];
+ setSelectedPaths(newSelectedPaths);
+ }
+ pendingAutoSelectRef.current = null;
+ }
+ }, [browseFiles]);
+
const onIgnore = async () => {
setIgnoreDirectoryOpen(false);
await fetchSettings();
@@ -337,9 +372,18 @@ export function FileBrowser() {
if (openFolders.indexOf(folder) >= 0) {
// Remove the folder that was clicked on, as well as its sub-folders.
newFolders = openFolders.filter(i => !i.startsWith(folder));
+ // Unselect any children of the closed folder (they're no longer visible)
+ const newSelectedPaths = selectedPaths.filter(p => !p.startsWith(folder) || p === folder);
+ if (newSelectedPaths.length !== selectedPaths.length) {
+ setSelectedPaths(newSelectedPaths);
+ }
} else {
// Add the new folder to the opened folders.
newFolders = [...openFolders, folder];
+ // If folder is selected, mark it for auto-selection of its children
+ if (selectedPaths.includes(folder)) {
+ pendingAutoSelectRef.current = folder;
+ }
}
console.debug(`newFolders=${newFolders}`);
setOpenFolders(newFolders);
diff --git a/app/src/components/FileBrowser.test.js b/app/src/components/FileBrowser.test.js
index bcc5358e..528ea81d 100644
--- a/app/src/components/FileBrowser.test.js
+++ b/app/src/components/FileBrowser.test.js
@@ -176,6 +176,211 @@ describe('FileBrowser', () => {
});
});
+ describe('Auto-select directory contents', () => {
+ it('selects all children when opening a selected directory', async () => {
+ const mockSetOpenFolders = jest.fn();
+ const mockFetchFiles = jest.fn();
+
+ // Start with folder not open (no children loaded)
+ useBrowseFiles.mockReturnValue({
+ browseFiles: [
+ {path: 'testdir/', children: null, is_empty: false},
+ ],
+ openFolders: [],
+ setOpenFolders: mockSetOpenFolders,
+ fetchFiles: mockFetchFiles,
+ });
+
+ const {rerender} = renderFileBrowser();
+
+ // Select the directory checkbox
+ const checkbox = screen.getByRole('checkbox');
+ await act(async () => {
+ fireEvent.click(checkbox);
+ });
+
+ // Verify directory is selected
+ expect(checkbox).toBeChecked();
+
+ // Click to expand the directory (clicking on the folder name cell)
+ const folderCell = screen.getByText('testdir/');
+ await act(async () => {
+ fireEvent.click(folderCell);
+ });
+
+ // Verify setOpenFolders was called to open the directory
+ expect(mockSetOpenFolders).toHaveBeenCalledWith(['testdir/']);
+
+ // Simulate browseFiles update with children (as would happen after fetch)
+ useBrowseFiles.mockReturnValue({
+ browseFiles: [
+ {path: 'testdir/', children: [
+ {path: 'testdir/file1.txt', size: 100},
+ {path: 'testdir/file2.txt', size: 200},
+ ], is_empty: false},
+ ],
+ openFolders: ['testdir/'],
+ setOpenFolders: mockSetOpenFolders,
+ fetchFiles: mockFetchFiles,
+ });
+
+ // Rerender to trigger the useEffect with new browseFiles
+ await act(async () => {
+ rerender(
+
+
+
+ );
+ });
+
+ // Verify all checkboxes are checked (parent + 2 children)
+ await waitFor(() => {
+ const checkboxes = screen.getAllByRole('checkbox');
+ expect(checkboxes.length).toBe(3);
+ checkboxes.forEach(cb => expect(cb).toBeChecked());
+ });
+ });
+
+ it('children remain selected when parent is unselected', async () => {
+ // Setup: directory with children, parent and children are all selected
+ useBrowseFiles.mockReturnValue({
+ browseFiles: [
+ {path: 'testdir/', children: [
+ {path: 'testdir/file1.txt', size: 100},
+ {path: 'testdir/file2.txt', size: 200},
+ ], is_empty: false},
+ ],
+ openFolders: ['testdir/'],
+ setOpenFolders: jest.fn(),
+ fetchFiles: jest.fn(),
+ });
+
+ renderFileBrowser();
+
+ // Select all items (parent + children)
+ const checkboxes = screen.getAllByRole('checkbox');
+ expect(checkboxes.length).toBe(3);
+
+ // Select parent first
+ await act(async () => {
+ fireEvent.click(checkboxes[0]); // testdir/
+ });
+ // Select children
+ await act(async () => {
+ fireEvent.click(checkboxes[1]); // file1.txt
+ });
+ await act(async () => {
+ fireEvent.click(checkboxes[2]); // file2.txt
+ });
+
+ // Verify all are selected
+ checkboxes.forEach(cb => expect(cb).toBeChecked());
+
+ // Unselect the parent directory
+ await act(async () => {
+ fireEvent.click(checkboxes[0]);
+ });
+
+ // Parent should be unchecked, but children should remain checked
+ expect(checkboxes[0]).not.toBeChecked();
+ expect(checkboxes[1]).toBeChecked();
+ expect(checkboxes[2]).toBeChecked();
+ });
+
+ it('unselects children when parent directory is closed', async () => {
+ // Setup: directory with children, already open
+ useBrowseFiles.mockReturnValue({
+ browseFiles: [
+ {path: 'testdir/', children: [
+ {path: 'testdir/file1.txt', size: 100},
+ {path: 'testdir/file2.txt', size: 200},
+ ], is_empty: false},
+ ],
+ openFolders: ['testdir/'],
+ setOpenFolders: jest.fn(),
+ fetchFiles: jest.fn(),
+ });
+
+ renderFileBrowser();
+
+ // Select parent and children
+ const checkboxes = screen.getAllByRole('checkbox');
+ expect(checkboxes.length).toBe(3);
+
+ await act(async () => {
+ fireEvent.click(checkboxes[0]); // testdir/
+ });
+ await act(async () => {
+ fireEvent.click(checkboxes[1]); // file1.txt
+ });
+ await act(async () => {
+ fireEvent.click(checkboxes[2]); // file2.txt
+ });
+
+ // Verify all are selected
+ checkboxes.forEach(cb => expect(cb).toBeChecked());
+
+ // Click to close the directory
+ const folderCell = screen.getByText('testdir/');
+ await act(async () => {
+ fireEvent.click(folderCell);
+ });
+
+ // Parent should still be selected, but children should be unselected
+ // (children are no longer visible after closing)
+ expect(checkboxes[0]).toBeChecked();
+ });
+
+ it('does not auto-select children when opening unselected directory', async () => {
+ const mockSetOpenFolders = jest.fn();
+ const mockFetchFiles = jest.fn();
+
+ // Start with folder not open
+ useBrowseFiles.mockReturnValue({
+ browseFiles: [
+ {path: 'testdir/', children: null, is_empty: false},
+ ],
+ openFolders: [],
+ setOpenFolders: mockSetOpenFolders,
+ fetchFiles: mockFetchFiles,
+ });
+
+ const {rerender} = renderFileBrowser();
+
+ // Click to expand WITHOUT selecting first
+ const folderCell = screen.getByText('testdir/');
+ await act(async () => {
+ fireEvent.click(folderCell);
+ });
+
+ // Simulate browseFiles update with children
+ useBrowseFiles.mockReturnValue({
+ browseFiles: [
+ {path: 'testdir/', children: [
+ {path: 'testdir/file1.txt', size: 100},
+ {path: 'testdir/file2.txt', size: 200},
+ ], is_empty: false},
+ ],
+ openFolders: ['testdir/'],
+ setOpenFolders: mockSetOpenFolders,
+ fetchFiles: mockFetchFiles,
+ });
+
+ await act(async () => {
+ rerender(
+
+
+
+ );
+ });
+
+ // Verify NO checkboxes are checked
+ const checkboxes = screen.getAllByRole('checkbox');
+ expect(checkboxes.length).toBe(3);
+ checkboxes.forEach(cb => expect(cb).not.toBeChecked());
+ });
+ });
+
describe('Delete functionality', () => {
it('removes deleted directory from openFolders', async () => {
const mockSetOpenFolders = jest.fn();