mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
Children are selected/unselected when parent directories are selected and then expanded.
This commit is contained in:
parent
4693686f4f
commit
6facb0856f
2 changed files with 249 additions and 0 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue