Skip to content

Commit b57a418

Browse files
authored
fix(fileIcons api): preserve sizing and folder state on plugin refresh (#2923)
* fix(fileIcons api): preserve sizing and folder state on plugin refresh * fix
1 parent 218d370 commit b57a418

5 files changed

Lines changed: 123 additions & 48 deletions

File tree

‎src/lib/fileIcons.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -687,9 +687,9 @@ class FileIconRegistry {
687687
for (const $tile of root.querySelectorAll<HTMLElement>(
688688
'[data-type="dir"][data-name], [data-type="root"][data-name]',
689689
)) {
690-
const expanded = !$tile
691-
.closest(".collapsible")
692-
?.classList.contains("hidden");
690+
const $collapsible = $tile.closest<HTMLElement>(".collapsible");
691+
const expanded =
692+
$collapsible !== null && !$collapsible.classList.contains("hidden");
693693
applyLeadClass(
694694
$tile,
695695
this.icon({
@@ -1094,7 +1094,22 @@ function applyLeadClass($tile: HTMLElement, className: string): void {
10941094
) {
10951095
return;
10961096
}
1097-
$lead.className = className;
1097+
1098+
// Some consumers add structural classes around the resolved icon class.
1099+
// Keep those classes when a late-loading plugin refreshes an existing row.
1100+
// File Browser uses `icon` for sizing. `user-added-storage` is its colour
1101+
// hook, and it must survive after the builtin `folder` glyph class is
1102+
// replaced. Keeping `folder` would also paint the icon-font glyph.
1103+
const structuralClasses = ["icon", "user-added-storage"].filter((name) =>
1104+
$lead.classList.contains(name),
1105+
);
1106+
const resolvedClasses = new Set(className.split(/\s+/).filter(Boolean));
1107+
const missingStructuralClasses = structuralClasses.filter(
1108+
(name) => !resolvedClasses.has(name),
1109+
);
1110+
$lead.className = [className, ...missingStructuralClasses]
1111+
.filter(Boolean)
1112+
.join(" ");
10981113
}
10991114

11001115
const fileIcons = new FileIconRegistry();

‎src/pages/fileBrowser/fileBrowser.scss‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,12 @@
5454

5555
&.folder {
5656
color: rgb(206, 206, 53);
57+
}
5758

58-
&.user-added-storage {
59-
color: rgb(53, 101, 206);
60-
}
59+
// Plugin icon themes replace the builtin `folder` glyph class.
60+
// Storage colour has to match `user-added-storage` on its own.
61+
&.user-added-storage {
62+
color: rgb(53, 101, 206);
6163
}
6264

6365
&.code {

‎tests/unit/codeHighlight.test.js‎

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -134,30 +134,41 @@ describe("codeHighlight", () => {
134134
});
135135

136136
it("updates fallback styles while the shadow host is still detached", () => {
137-
initHighlighting();
138-
const host = document.createElement("div");
139-
const shadow = host.attachShadow({ mode: "open" });
140-
Object.defineProperty(shadow, "adoptedStyleSheets", {
141-
configurable: true,
142-
get() {
143-
throw new Error("adoptedStyleSheets unavailable");
144-
},
145-
set() {
146-
throw new Error("adoptedStyleSheets unavailable");
147-
},
148-
});
149-
150-
applyHighlightStyles(shadow);
151-
const style = shadow.querySelector("#cm-static-highlight-styles");
152-
expect(style).toBeTruthy();
153-
expect(style.isConnected).toBe(false);
154-
expect(style.textContent).toContain("#c678dd");
155-
156-
settings.value.editorTheme = "githubLight";
157-
for (const listener of themeListeners) listener();
158-
159-
expect(style.parentNode).toBe(shadow);
160-
expect(style.textContent).toContain("#cf222e");
137+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
138+
try {
139+
initHighlighting();
140+
const host = document.createElement("div");
141+
const shadow = host.attachShadow({ mode: "open" });
142+
Object.defineProperty(shadow, "adoptedStyleSheets", {
143+
configurable: true,
144+
get() {
145+
throw new Error("adoptedStyleSheets unavailable");
146+
},
147+
set() {
148+
throw new Error("adoptedStyleSheets unavailable");
149+
},
150+
});
151+
152+
applyHighlightStyles(shadow);
153+
const style = shadow.querySelector("#cm-static-highlight-styles");
154+
expect(style).toBeTruthy();
155+
expect(style.isConnected).toBe(false);
156+
expect(style.textContent).toContain("#c678dd");
157+
158+
settings.value.editorTheme = "githubLight";
159+
for (const listener of themeListeners) listener();
160+
161+
expect(style.parentNode).toBe(shadow);
162+
expect(style.textContent).toContain("#cf222e");
163+
expect(warn).toHaveBeenCalledWith(
164+
"Failed to adopt highlight stylesheet",
165+
expect.objectContaining({
166+
message: "adoptedStyleSheets unavailable",
167+
}),
168+
);
169+
} finally {
170+
warn.mockRestore();
171+
}
161172
});
162173

163174
it("updates adopted shadow styles when the editor theme changes", () => {

‎tests/unit/fileIconAssets.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,36 @@ describe("icon assets", () => {
9797
expect(tile.firstElementChild?.className).toBe(fileIcons.icon("notes.txt"));
9898
});
9999

100+
it("preserves file browser layout and closed-folder state during a late plugin refresh", () => {
101+
document.body.innerHTML = `
102+
<div data-type="file" data-name="app.js">
103+
<span class="icon file file_type_js"></span>
104+
</div>
105+
<div data-type="dir" data-name="Project">
106+
<span class="icon folder user-added-storage"></span>
107+
</div>`;
108+
fileIcons.register({
109+
id: "classes",
110+
pluginId: "test.plugin",
111+
icons: {
112+
file: { className: "pack-file" },
113+
folder: { className: "pack-folder" },
114+
"folder-open": { className: "pack-folder-open" },
115+
},
116+
file: "file",
117+
folder: "folder",
118+
folderExpanded: "folder-open",
119+
});
120+
121+
fileIcons.use("classes", { persist: false });
122+
123+
const [file, folder] = document.body.querySelectorAll("span");
124+
expect(file.className).toBe("pack-file icon");
125+
expect(folder.className).toBe(
126+
"pack-folder icon user-added-storage",
127+
);
128+
});
129+
100130
it("uses distinct CSS classes for IDs that previously collided", () => {
101131
fileIcons.register({
102132
id: "ids",

‎tests/unit/sftpProfiles.test.js‎

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -257,23 +257,40 @@ describe("SFTP secure profiles", () => {
257257
});
258258

259259
it("scrubs malformed legacy SFTP values instead of marking them migrated", async () => {
260-
globalThis.sftp = { saveProfile: vi.fn() };
261-
localStorage.setItem(
262-
"recentFiles",
263-
JSON.stringify([
264-
"sftp:///missing-host",
265-
"sftp://user:%E0%A4%A@example.com/bad-encoding",
266-
]),
267-
);
268-
269-
const result = await migrateLegacySftpProfiles();
270-
271-
expect(JSON.parse(localStorage.getItem("recentFiles"))).toEqual([]);
272-
expect(result.failures[0].message).toBe(
273-
"The saved SFTP address is incomplete",
274-
);
275-
expect(result.failures[1].message).toBe("URI malformed");
276-
expect(localStorage.getItem("sftpNativeProfileMigration")).toBe("2");
260+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
261+
try {
262+
globalThis.sftp = { saveProfile: vi.fn() };
263+
localStorage.setItem(
264+
"recentFiles",
265+
JSON.stringify([
266+
"sftp:///missing-host",
267+
"sftp://user:%E0%A4%A@example.com/bad-encoding",
268+
]),
269+
);
270+
271+
const result = await migrateLegacySftpProfiles();
272+
273+
expect(JSON.parse(localStorage.getItem("recentFiles"))).toEqual([]);
274+
expect(result.failures[0].message).toBe(
275+
"The saved SFTP address is incomplete",
276+
);
277+
expect(result.failures[1].message).toBe("URI malformed");
278+
expect(localStorage.getItem("sftpNativeProfileMigration")).toBe("2");
279+
expect(warn).toHaveBeenNthCalledWith(
280+
1,
281+
"Could not migrate legacy SFTP URL",
282+
expect.objectContaining({
283+
message: "The saved SFTP address is incomplete",
284+
}),
285+
);
286+
expect(warn).toHaveBeenNthCalledWith(
287+
2,
288+
"Could not migrate legacy SFTP URL",
289+
expect.objectContaining({ message: "URI malformed" }),
290+
);
291+
} finally {
292+
warn.mockRestore();
293+
}
277294
});
278295

279296
it("deletes an app-owned legacy key copy when its profile cannot migrate", async () => {

0 commit comments

Comments
 (0)