Skip to content

Commit f9428af

Browse files
authored
fix(open-folder): refresh renamed entries with URI-aware matching (#2142)
1 parent c047504 commit f9428af

2 files changed

Lines changed: 168 additions & 48 deletions

File tree

src/components/fileTree/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,27 @@ export default class FileTree {
335335
}
336336
}
337337

338+
/**
339+
* Refresh a loaded folder in this tree or one of its expanded child trees.
340+
* @param {string} url
341+
* @param {(a: string, b: string) => boolean} [isSameUrl]
342+
* @returns {Promise<boolean>}
343+
*/
344+
async refreshFolder(url, isSameUrl = (a, b) => a === b) {
345+
if (this.currentUrl && isSameUrl(this.currentUrl, url)) {
346+
await this.refresh();
347+
return true;
348+
}
349+
350+
for (const childTree of this.childTrees.values()) {
351+
if (await childTree.refreshFolder(url, isSameUrl)) {
352+
return true;
353+
}
354+
}
355+
356+
return false;
357+
}
358+
338359
/**
339360
* Destroy all expanded child trees and clear their references.
340361
*/

src/lib/openFolder.js

Lines changed: 147 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -655,25 +655,18 @@ function execOperation(type, action, url, $target, name) {
655655
}
656656

657657
newName = Url.basename(newUrl);
658-
$target.querySelector(":scope>.text").textContent = newName;
659-
$target.dataset.url = newUrl;
660-
$target.dataset.name = newName;
661658
if (helpers.isFile(type)) {
662-
$target.querySelector(":scope>span").className =
663-
helpers.getIconForFile(newName);
664659
let file = editorManager.getFile(url, "uri");
665660
if (file) {
666661
file.uri = newUrl;
667662
file.filename = newName;
668663
}
669664
} else {
670665
helpers.updateUriOfAllActiveFiles(url, newUrl);
671-
//Reloading the folder by collapsing and expanding the folder
672-
$target.click(); //collapse
673-
$target.click(); //expand
674666
}
675-
toast(strings.success);
676667
FileList.rename(url, newUrl);
668+
await refreshRenamedEntryInOpenFolders(url, newUrl);
669+
toast(strings.success);
677670
}
678671

679672
async function createNew() {
@@ -1000,6 +993,115 @@ function getLoadedFileTree($el) {
1000993
);
1001994
}
1002995

996+
function normalizeUrlPathKey(url) {
997+
if (!url) return url;
998+
const { url: parsedUrl } = Url.parse(url);
999+
1000+
if (Url.getProtocol(parsedUrl) === "content:") {
1001+
try {
1002+
const { rootUri, docId } = Uri.parse(parsedUrl);
1003+
const normalizedDocId = docId.endsWith("/") ? docId.slice(0, -1) : docId;
1004+
return `${rootUri}::${normalizedDocId}`;
1005+
} catch (error) {
1006+
return parsedUrl;
1007+
}
1008+
}
1009+
1010+
if (parsedUrl.endsWith("/") && Url.pathname(parsedUrl) !== "/") {
1011+
return parsedUrl.slice(0, -1);
1012+
}
1013+
1014+
return parsedUrl;
1015+
}
1016+
1017+
function areSameOpenFolderUrl(leftUrl, rightUrl) {
1018+
return normalizeUrlPathKey(leftUrl) === normalizeUrlPathKey(rightUrl);
1019+
}
1020+
1021+
function isInsideOpenFolder(url, folderUrl) {
1022+
const urlKey = normalizeUrlPathKey(url);
1023+
const folderKey = normalizeUrlPathKey(folderUrl);
1024+
if (!urlKey || !folderKey) return false;
1025+
1026+
return urlKey === folderKey || urlKey.startsWith(`${folderKey}/`);
1027+
}
1028+
1029+
function appendUrlPathSuffix(url, suffix) {
1030+
if (!suffix) return url;
1031+
const { url: parsedUrl, query } = Url.parse(url);
1032+
if (parsedUrl.endsWith("/") && suffix.startsWith("/")) {
1033+
return parsedUrl.slice(0, -1) + suffix + query;
1034+
}
1035+
return parsedUrl + suffix + query;
1036+
}
1037+
1038+
function preserveTrailingSlashShape(url, sourceUrl) {
1039+
const { url: sourcePath } = Url.parse(sourceUrl);
1040+
if (!sourcePath.endsWith("/")) return url;
1041+
1042+
const { url: targetPath, query } = Url.parse(url);
1043+
if (targetPath.endsWith("/")) return url;
1044+
1045+
return `${targetPath}/${query}`;
1046+
}
1047+
1048+
function getListStateEntries(listState) {
1049+
if (!listState) return [];
1050+
if (listState instanceof Map) return Array.from(listState.entries());
1051+
return Object.entries(listState);
1052+
}
1053+
1054+
function setListStateEntry(listState, key, value) {
1055+
if (listState instanceof Map) {
1056+
listState.set(key, value);
1057+
return;
1058+
}
1059+
1060+
listState[key] = value;
1061+
}
1062+
1063+
function deleteListStateEntry(listState, key) {
1064+
if (listState instanceof Map) {
1065+
listState.delete(key);
1066+
return;
1067+
}
1068+
1069+
delete listState[key];
1070+
}
1071+
1072+
/**
1073+
* Move saved expanded-state keys after a folder rename.
1074+
* @param {string} oldUrl
1075+
* @param {string} newUrl
1076+
*/
1077+
function migrateOpenFolderStateUrls(oldUrl, newUrl) {
1078+
if (!oldUrl || !newUrl || areSameOpenFolderUrl(oldUrl, newUrl)) return;
1079+
1080+
const oldKey = normalizeUrlPathKey(oldUrl);
1081+
1082+
addedFolder.forEach(({ listState }) => {
1083+
const matchingEntries = getListStateEntries(listState).filter(
1084+
([folderUrl]) => {
1085+
return isInsideOpenFolder(folderUrl, oldUrl);
1086+
},
1087+
);
1088+
1089+
matchingEntries.forEach(([folderUrl, isExpanded]) => {
1090+
const suffix = normalizeUrlPathKey(folderUrl).slice(oldKey.length);
1091+
const migratedUrl = preserveTrailingSlashShape(
1092+
appendUrlPathSuffix(newUrl, suffix),
1093+
folderUrl,
1094+
);
1095+
deleteListStateEntry(listState, folderUrl);
1096+
setListStateEntry(listState, migratedUrl, isExpanded);
1097+
});
1098+
});
1099+
}
1100+
1101+
function getParentUrl(url) {
1102+
return Url.dirname(url);
1103+
}
1104+
10031105
/**
10041106
* Remove matching rendered entries from expanded folder views.
10051107
* This keeps FileTree's in-memory state aligned with the rendered tree.
@@ -1068,21 +1170,38 @@ function appendEntryToOpenFolder(parentUrl, entryUrl, type) {
10681170
* @param {string} folderUrl
10691171
*/
10701172
async function refreshOpenFolder(folderUrl) {
1071-
const filesApp = sidebarApps.get("files");
1072-
const $els = filesApp.getAll(`[data-url="${folderUrl}"]`);
1173+
const folder = openFolder.find(folderUrl);
1174+
if (!folder) return;
10731175

1074-
await Promise.all(
1075-
Array.from($els).map(async ($el) => {
1076-
if (!(helpers.isDir($el.dataset.type) || $el.dataset.type === "root")) {
1077-
return;
1078-
}
1176+
const fileTree = getLoadedFileTree(folder.$node.$title);
1177+
if (!fileTree) return;
10791178

1080-
const fileTree = getLoadedFileTree($el);
1081-
if (fileTree) {
1082-
await fileTree.refresh();
1083-
}
1084-
}),
1085-
);
1179+
await fileTree.refreshFolder(folderUrl, areSameOpenFolderUrl);
1180+
}
1181+
1182+
/**
1183+
* Refresh affected folder trees after a rename/move.
1184+
* @param {string} oldUrl
1185+
* @param {string} newUrl
1186+
*/
1187+
async function refreshRenamedEntryInOpenFolders(
1188+
oldUrl,
1189+
newUrl,
1190+
oldParentUrl = getParentUrl(oldUrl),
1191+
newParentUrl = getParentUrl(newUrl),
1192+
) {
1193+
if (!oldUrl || !newUrl || areSameOpenFolderUrl(oldUrl, newUrl)) return;
1194+
1195+
migrateOpenFolderStateUrls(oldUrl, newUrl);
1196+
1197+
const parentUrls = [oldParentUrl, newParentUrl].filter(Boolean);
1198+
const refreshUrls = parentUrls.filter((parentUrl, index) => {
1199+
return !parentUrls.some((otherUrl, otherIndex) => {
1200+
return otherIndex < index && areSameOpenFolderUrl(otherUrl, parentUrl);
1201+
});
1202+
});
1203+
1204+
await Promise.all(refreshUrls.map(refreshOpenFolder));
10861205
}
10871206

10881207
/**
@@ -1132,29 +1251,11 @@ openFolder.add = async (url, type) => {
11321251
appendEntryToOpenFolder(parent, url, type);
11331252
};
11341253

1135-
openFolder.renameItem = (oldFile, newFile, newFilename) => {
1254+
openFolder.renameItem = (oldFile, newFile) => {
11361255
FileList.rename(oldFile, newFile);
11371256

11381257
helpers.updateUriOfAllActiveFiles(oldFile, newFile);
1139-
1140-
const filesApp = sidebarApps.get("files");
1141-
const $els = filesApp.getAll(`[data-url="${oldFile}"]`);
1142-
Array.from($els).forEach(($el) => {
1143-
if ($el.dataset.type === "dir") {
1144-
$el = $el.$title;
1145-
setTimeout(() => {
1146-
$el.collapse();
1147-
$el.expand();
1148-
}, 0);
1149-
} else {
1150-
$el.querySelector(":scope>span").className =
1151-
helpers.getIconForFile(newFilename);
1152-
}
1153-
1154-
$el.dataset.url = newFile;
1155-
$el.dataset.name = newFilename;
1156-
$el.querySelector(":scope>.text").textContent = newFilename;
1157-
});
1258+
refreshRenamedEntryInOpenFolders(oldFile, newFile).catch(helpers.error);
11581259
};
11591260

11601261
openFolder.removeItem = (url) => {
@@ -1185,13 +1286,11 @@ openFolder.removeFolders = (url) => {
11851286
* @returns {Folder}
11861287
*/
11871288
openFolder.find = (url) => {
1188-
const found = addedFolder.find((folder) => folder.url === url);
1289+
const found = addedFolder.find((folder) =>
1290+
areSameOpenFolderUrl(folder.url, url),
1291+
);
11891292
if (found) return found;
1190-
return addedFolder.find((folder) => {
1191-
const { url: furl } = Url.parse(folder.url);
1192-
const regex = new RegExp("^" + escapeStringRegexp(furl));
1193-
return regex.test(url);
1194-
});
1293+
return addedFolder.find((folder) => isInsideOpenFolder(url, folder.url));
11951294
};
11961295

11971296
export default openFolder;

0 commit comments

Comments
 (0)