Children are selected/unselected when parent directories are selected and then expanded.

This commit is contained in:
Roland 2025-12-25 10:05:00 -07:00
parent 4693686f4f
commit 6facb0856f
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 249 additions and 0 deletions

View file

@ -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);

View file

@ -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(<FileBrowser/>);
// 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(
<AllContextsWrapper>
<FileBrowser/>
</AllContextsWrapper>
);
});
// 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(<FileBrowser/>);
// 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(<FileBrowser/>);
// 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(<FileBrowser/>);
// 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(
<AllContextsWrapper>
<FileBrowser/>
</AllContextsWrapper>
);
});
// 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();