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);
|
||||
}, [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(() => {
|
||||
if (!root) return;
|
||||
|
||||
@@ -587,7 +605,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
.then(async (result) => {
|
||||
if (result.success) {
|
||||
toast.success('File created');
|
||||
await refreshRoot();
|
||||
await refreshDirectory(parentPath);
|
||||
}
|
||||
closeDialog();
|
||||
})
|
||||
@@ -611,7 +629,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
.then(async (result) => {
|
||||
if (result.success) {
|
||||
toast.success('Folder created');
|
||||
await refreshRoot();
|
||||
await refreshDirectory(parentPath);
|
||||
}
|
||||
closeDialog();
|
||||
})
|
||||
@@ -641,7 +659,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
.then(async (result) => {
|
||||
if (result.success) {
|
||||
toast.success('Renamed successfully');
|
||||
await refreshRoot();
|
||||
await refreshDirectory(parentDir);
|
||||
if (root) {
|
||||
removeOpenPathsByPrefix(root, oldPath);
|
||||
}
|
||||
@@ -663,15 +681,17 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
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) => {
|
||||
if (result.success) {
|
||||
toast.success('Deleted successfully');
|
||||
await refreshRoot();
|
||||
await refreshDirectory(parentDir);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -683,7 +703,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
}
|
||||
|
||||
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) ---
|
||||
|
||||
|
||||
@@ -835,6 +835,27 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
await 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 lastFilesViewTreeKeyRef = React.useRef<string>('');
|
||||
|
||||
@@ -905,7 +926,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
.then(async (result) => {
|
||||
if (result.success) {
|
||||
toast.success('File created');
|
||||
await refreshRoot();
|
||||
await refreshDirectory(parentPath);
|
||||
}
|
||||
finishDialogOperation();
|
||||
})
|
||||
@@ -928,7 +949,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
.then(async (result) => {
|
||||
if (result.success) {
|
||||
toast.success('Folder created');
|
||||
await refreshRoot();
|
||||
await refreshDirectory(parentPath);
|
||||
}
|
||||
finishDialogOperation();
|
||||
})
|
||||
@@ -959,7 +980,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
.then(async (result) => {
|
||||
if (result.success) {
|
||||
toast.success('Renamed successfully');
|
||||
await refreshRoot();
|
||||
await refreshDirectory(parentDir);
|
||||
if (root) {
|
||||
removeOpenPathsByPrefix(root, oldPath);
|
||||
}
|
||||
@@ -990,15 +1011,17 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
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) => {
|
||||
if (result.success) {
|
||||
toast.success('Deleted successfully');
|
||||
await refreshRoot();
|
||||
await refreshDirectory(parentDir);
|
||||
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) {
|
||||
setSelectedPath(root, null);
|
||||
}
|
||||
@@ -1019,7 +1042,7 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
|
||||
}
|
||||
|
||||
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(() => {
|
||||
if (!currentDirectory) {
|
||||
|
||||
Reference in New Issue
Block a user