fix(files): incremental directory refresh on create/rename/delete (#721)
Replace the full-tree nuke-and-reload in refreshRoot() with a targeted refreshDirectory(parentPath) that only evicts and reloads the single parent directory whose contents changed. Before: every create/rename/delete call cleared the entire childrenByDir cache and reloaded only the root, causing all expanded subdirectories to flash empty and visually reset. After: only the parent directory of the affected entry is reloaded in-place; every other expanded directory keeps its cached children, so the tree state is preserved and there is no visible flash or scroll-position reset. Applies to both FilesView and SidebarFilesTree.
This commit is contained in:
committed by
GitHub
parent
683d0c1798
commit
b4949c6e33
@@ -423,6 +423,24 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
await loadDirectory(root);
|
await loadDirectory(root);
|
||||||
}, [loadDirectory, root]);
|
}, [loadDirectory, root]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incrementally refresh a single directory without nuking the rest of the
|
||||||
|
* tree. Only the given directory is reloaded in-place; every other expanded
|
||||||
|
* directory keeps its cached children so the UI does not flash/reset.
|
||||||
|
*/
|
||||||
|
const refreshDirectory = React.useCallback(async (dirPath: string) => {
|
||||||
|
if (!dirPath) {
|
||||||
|
await refreshRoot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = normalizePath(dirPath);
|
||||||
|
loadedDirsRef.current = new Set(loadedDirsRef.current);
|
||||||
|
loadedDirsRef.current.delete(normalized);
|
||||||
|
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
|
||||||
|
inFlightDirsRef.current.delete(normalized);
|
||||||
|
await loadDirectory(normalized);
|
||||||
|
}, [loadDirectory, refreshRoot]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
|
|
||||||
@@ -587,7 +605,7 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('File created');
|
toast.success('File created');
|
||||||
await refreshRoot();
|
await refreshDirectory(parentPath);
|
||||||
}
|
}
|
||||||
closeDialog();
|
closeDialog();
|
||||||
})
|
})
|
||||||
@@ -611,7 +629,7 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('Folder created');
|
toast.success('Folder created');
|
||||||
await refreshRoot();
|
await refreshDirectory(parentPath);
|
||||||
}
|
}
|
||||||
closeDialog();
|
closeDialog();
|
||||||
})
|
})
|
||||||
@@ -641,7 +659,7 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('Renamed successfully');
|
toast.success('Renamed successfully');
|
||||||
await refreshRoot();
|
await refreshDirectory(parentDir);
|
||||||
if (root) {
|
if (root) {
|
||||||
removeOpenPathsByPrefix(root, oldPath);
|
removeOpenPathsByPrefix(root, oldPath);
|
||||||
}
|
}
|
||||||
@@ -663,15 +681,17 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await files.delete(dialogData.path)
|
const deletedPath = dialogData.path;
|
||||||
|
const parentDir = deletedPath.split('/').slice(0, -1).join('/');
|
||||||
|
await files.delete(deletedPath)
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('Deleted successfully');
|
toast.success('Deleted successfully');
|
||||||
await refreshRoot();
|
await refreshDirectory(parentDir);
|
||||||
if (root) {
|
if (root) {
|
||||||
removeOpenPathsByPrefix(root, dialogData.path);
|
removeOpenPathsByPrefix(root, deletedPath);
|
||||||
}
|
}
|
||||||
if (selectedPath === dialogData.path || (selectedPath && selectedPath.startsWith(dialogData.path + '/'))) {
|
if (selectedPath === deletedPath || (selectedPath && selectedPath.startsWith(deletedPath + '/'))) {
|
||||||
setSelectedPath(root, null);
|
setSelectedPath(root, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -683,7 +703,7 @@ export const SidebarFilesTree: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}, [activeDialog, dialogData, dialogInputValue, files, refreshRoot, removeOpenPathsByPrefix, root, selectedPath, setSelectedPath]);
|
}, [activeDialog, dialogData, dialogInputValue, files, refreshDirectory, removeOpenPathsByPrefix, root, selectedPath, setSelectedPath]);
|
||||||
|
|
||||||
// --- Tree rendering (matching FilesView with indent guides) ---
|
// --- Tree rendering (matching FilesView with indent guides) ---
|
||||||
|
|
||||||
|
|||||||
@@ -835,6 +835,27 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
await loadDirectory(root);
|
await loadDirectory(root);
|
||||||
}, [loadDirectory, root]);
|
}, [loadDirectory, root]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incrementally refresh a single directory without nuking the rest of the
|
||||||
|
* tree. After the operation the parent directory is reloaded in-place so
|
||||||
|
* the new/renamed/deleted entry becomes visible immediately while every
|
||||||
|
* other expanded directory keeps its cached children.
|
||||||
|
*/
|
||||||
|
const refreshDirectory = React.useCallback(async (dirPath: string) => {
|
||||||
|
if (!dirPath) {
|
||||||
|
await refreshRoot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = normalizePath(dirPath);
|
||||||
|
// Remove from loaded set so loadDirectory will actually fetch again.
|
||||||
|
loadedDirsRef.current = new Set(loadedDirsRef.current);
|
||||||
|
loadedDirsRef.current.delete(normalized);
|
||||||
|
// Also cancel any in-flight request for this dir so the new fetch wins.
|
||||||
|
inFlightDirsRef.current = new Set(inFlightDirsRef.current);
|
||||||
|
inFlightDirsRef.current.delete(normalized);
|
||||||
|
await loadDirectory(normalized);
|
||||||
|
}, [loadDirectory, refreshRoot]);
|
||||||
|
|
||||||
const lastFilesViewDirRef = React.useRef<string>('');
|
const lastFilesViewDirRef = React.useRef<string>('');
|
||||||
const lastFilesViewTreeKeyRef = React.useRef<string>('');
|
const lastFilesViewTreeKeyRef = React.useRef<string>('');
|
||||||
|
|
||||||
@@ -905,7 +926,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('File created');
|
toast.success('File created');
|
||||||
await refreshRoot();
|
await refreshDirectory(parentPath);
|
||||||
}
|
}
|
||||||
finishDialogOperation();
|
finishDialogOperation();
|
||||||
})
|
})
|
||||||
@@ -928,7 +949,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('Folder created');
|
toast.success('Folder created');
|
||||||
await refreshRoot();
|
await refreshDirectory(parentPath);
|
||||||
}
|
}
|
||||||
finishDialogOperation();
|
finishDialogOperation();
|
||||||
})
|
})
|
||||||
@@ -959,7 +980,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('Renamed successfully');
|
toast.success('Renamed successfully');
|
||||||
await refreshRoot();
|
await refreshDirectory(parentDir);
|
||||||
if (root) {
|
if (root) {
|
||||||
removeOpenPathsByPrefix(root, oldPath);
|
removeOpenPathsByPrefix(root, oldPath);
|
||||||
}
|
}
|
||||||
@@ -990,15 +1011,17 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await files.delete(dialogData.path)
|
const deletedPath = dialogData.path;
|
||||||
|
const parentDir = deletedPath.split('/').slice(0, -1).join('/');
|
||||||
|
await files.delete(deletedPath)
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success('Deleted successfully');
|
toast.success('Deleted successfully');
|
||||||
await refreshRoot();
|
await refreshDirectory(parentDir);
|
||||||
if (root) {
|
if (root) {
|
||||||
removeOpenPathsByPrefix(root, dialogData.path);
|
removeOpenPathsByPrefix(root, deletedPath);
|
||||||
}
|
}
|
||||||
if (selectedFile?.path === dialogData.path || selectedFile?.path.startsWith(`${dialogData.path}/`)) {
|
if (selectedFile?.path === deletedPath || selectedFile?.path.startsWith(`${deletedPath}/`)) {
|
||||||
if (root) {
|
if (root) {
|
||||||
setSelectedPath(root, null);
|
setSelectedPath(root, null);
|
||||||
}
|
}
|
||||||
@@ -1019,7 +1042,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}, [activeDialog, dialogData, dialogInputValue, files, refreshRoot, isMobile, removeOpenPathsByPrefix, root, selectedFile?.path, setSelectedPath]);
|
}, [activeDialog, dialogData, dialogInputValue, files, refreshDirectory, isMobile, removeOpenPathsByPrefix, root, selectedFile?.path, setSelectedPath]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!currentDirectory) {
|
if (!currentDirectory) {
|
||||||
|
|||||||
Reference in New Issue
Block a user