Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/lib/fileIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,9 +687,9 @@ class FileIconRegistry {
for (const $tile of root.querySelectorAll<HTMLElement>(
'[data-type="dir"][data-name], [data-type="root"][data-name]',
)) {
const expanded = !$tile
.closest(".collapsible")
?.classList.contains("hidden");
const $collapsible = $tile.closest<HTMLElement>(".collapsible");
const expanded =
$collapsible !== null && !$collapsible.classList.contains("hidden");
applyLeadClass(
$tile,
this.icon({
Expand Down Expand Up @@ -1094,7 +1094,22 @@ function applyLeadClass($tile: HTMLElement, className: string): void {
) {
return;
}
$lead.className = className;

// Some consumers add structural classes around the resolved icon class.
// Keep those classes when a late-loading plugin refreshes an existing row.
// File Browser uses `icon` for sizing. `user-added-storage` is its colour
// hook, and it must survive after the builtin `folder` glyph class is
// replaced. Keeping `folder` would also paint the icon-font glyph.
const structuralClasses = ["icon", "user-added-storage"].filter((name) =>
$lead.classList.contains(name),
);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const resolvedClasses = new Set(className.split(/\s+/).filter(Boolean));
const missingStructuralClasses = structuralClasses.filter(
(name) => !resolvedClasses.has(name),
);
$lead.className = [className, ...missingStructuralClasses]
.filter(Boolean)
.join(" ");
}

const fileIcons = new FileIconRegistry();
Expand Down
8 changes: 5 additions & 3 deletions src/pages/fileBrowser/fileBrowser.scss
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@

&.folder {
color: rgb(206, 206, 53);
}

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

&.code {
Expand Down
59 changes: 35 additions & 24 deletions tests/unit/codeHighlight.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,30 +134,41 @@ describe("codeHighlight", () => {
});

it("updates fallback styles while the shadow host is still detached", () => {
initHighlighting();
const host = document.createElement("div");
const shadow = host.attachShadow({ mode: "open" });
Object.defineProperty(shadow, "adoptedStyleSheets", {
configurable: true,
get() {
throw new Error("adoptedStyleSheets unavailable");
},
set() {
throw new Error("adoptedStyleSheets unavailable");
},
});

applyHighlightStyles(shadow);
const style = shadow.querySelector("#cm-static-highlight-styles");
expect(style).toBeTruthy();
expect(style.isConnected).toBe(false);
expect(style.textContent).toContain("#c678dd");

settings.value.editorTheme = "githubLight";
for (const listener of themeListeners) listener();

expect(style.parentNode).toBe(shadow);
expect(style.textContent).toContain("#cf222e");
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
initHighlighting();
const host = document.createElement("div");
const shadow = host.attachShadow({ mode: "open" });
Object.defineProperty(shadow, "adoptedStyleSheets", {
configurable: true,
get() {
throw new Error("adoptedStyleSheets unavailable");
},
set() {
throw new Error("adoptedStyleSheets unavailable");
},
});

applyHighlightStyles(shadow);
const style = shadow.querySelector("#cm-static-highlight-styles");
expect(style).toBeTruthy();
expect(style.isConnected).toBe(false);
expect(style.textContent).toContain("#c678dd");

settings.value.editorTheme = "githubLight";
for (const listener of themeListeners) listener();

expect(style.parentNode).toBe(shadow);
expect(style.textContent).toContain("#cf222e");
expect(warn).toHaveBeenCalledWith(
"Failed to adopt highlight stylesheet",
expect.objectContaining({
message: "adoptedStyleSheets unavailable",
}),
);
} finally {
warn.mockRestore();
}
});

it("updates adopted shadow styles when the editor theme changes", () => {
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/fileIconAssets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,36 @@ describe("icon assets", () => {
expect(tile.firstElementChild?.className).toBe(fileIcons.icon("notes.txt"));
});

it("preserves file browser layout and closed-folder state during a late plugin refresh", () => {
document.body.innerHTML = `
<div data-type="file" data-name="app.js">
<span class="icon file file_type_js"></span>
</div>
<div data-type="dir" data-name="Project">
<span class="icon folder user-added-storage"></span>
</div>`;
fileIcons.register({
id: "classes",
pluginId: "test.plugin",
icons: {
file: { className: "pack-file" },
folder: { className: "pack-folder" },
"folder-open": { className: "pack-folder-open" },
},
file: "file",
folder: "folder",
folderExpanded: "folder-open",
});

fileIcons.use("classes", { persist: false });

const [file, folder] = document.body.querySelectorAll("span");
expect(file.className).toBe("pack-file icon");
expect(folder.className).toBe(
"pack-folder icon user-added-storage",
);
});

it("uses distinct CSS classes for IDs that previously collided", () => {
fileIcons.register({
id: "ids",
Expand Down
51 changes: 34 additions & 17 deletions tests/unit/sftpProfiles.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,23 +257,40 @@ describe("SFTP secure profiles", () => {
});

it("scrubs malformed legacy SFTP values instead of marking them migrated", async () => {
globalThis.sftp = { saveProfile: vi.fn() };
localStorage.setItem(
"recentFiles",
JSON.stringify([
"sftp:///missing-host",
"sftp://user:%E0%A4%A@example.com/bad-encoding",
]),
);

const result = await migrateLegacySftpProfiles();

expect(JSON.parse(localStorage.getItem("recentFiles"))).toEqual([]);
expect(result.failures[0].message).toBe(
"The saved SFTP address is incomplete",
);
expect(result.failures[1].message).toBe("URI malformed");
expect(localStorage.getItem("sftpNativeProfileMigration")).toBe("2");
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
globalThis.sftp = { saveProfile: vi.fn() };
localStorage.setItem(
"recentFiles",
JSON.stringify([
"sftp:///missing-host",
"sftp://user:%E0%A4%A@example.com/bad-encoding",
]),
);

const result = await migrateLegacySftpProfiles();

expect(JSON.parse(localStorage.getItem("recentFiles"))).toEqual([]);
expect(result.failures[0].message).toBe(
"The saved SFTP address is incomplete",
);
expect(result.failures[1].message).toBe("URI malformed");
expect(localStorage.getItem("sftpNativeProfileMigration")).toBe("2");
expect(warn).toHaveBeenNthCalledWith(
1,
"Could not migrate legacy SFTP URL",
expect.objectContaining({
message: "The saved SFTP address is incomplete",
}),
);
expect(warn).toHaveBeenNthCalledWith(
2,
"Could not migrate legacy SFTP URL",
expect.objectContaining({ message: "URI malformed" }),
);
} finally {
warn.mockRestore();
}
});

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