fix: home tab, layout switch and missing layout fallbacks for looks

This commit is contained in:
LukeGus committed 2026-10-18 19:08:50 -05:00
1 parent 4c235bb23f
commit ec3cc9cf18
14 files changed
+761 -11

No files matched your search

+8 -3
View File
@@ -13,6 +13,7 @@ import {
LAYOUT_SETTING,
LOOK_ID_PATTERN,
THEME_SETTING,
installedLayoutIds,
resolveDefaultLook,
safeLayoutForced,
unavailableLooks,
@@ -35,7 +36,7 @@ async function adminDefaults() {
* /user-preferences/look:
* get:
* summary: Get the layout and theme to show the current user
* description: The user's own choice, the admin defaults, the default look of the bundled plugins, whether TERMIX_SAFE_LAYOUT forces the repair view, and installed layout or theme plugins that could not load. The browser resolves the final look from these.
* description: The user's own choice, the admin defaults (a layout no installed plugin brings reads as null), the default look of the bundled plugins, whether TERMIX_SAFE_LAYOUT forces the repair view, and installed layout or theme plugins that could not load. The browser resolves the final look from these.
* tags:
* - User Preferences
* responses:
@@ -101,10 +102,14 @@ router.get("/", authenticateJWT, async (req: Request, res: Response) => {
const preferences =
await createCurrentUserPreferenceRepository().findByUserId(userId);
const { loader } = getPluginRuntime();
const installed = installedLayoutIds(loader);
const known = (id: string | null | undefined) =>
id && installed.has(id) ? id : null;
const defaults = await adminDefaults();
res.json({
layoutId: preferences?.layoutId ?? null,
layoutId: known(preferences?.layoutId),
themeId: preferences?.themeId ?? null,
defaults: await adminDefaults(),
defaults: { ...defaults, layout: known(defaults.layout) },
defaultLook: resolveDefaultLook(loader),
safeLayout: safeLayoutForced(),
unavailable: unavailableLooks(loader),
+19
View File
@@ -14,6 +14,7 @@ export interface DefaultLook {
interface LookLoader {
get(pluginId: string): Pick<LoadedPlugin, "manifest"> | undefined;
list(): Array<Pick<LoadedPlugin, "manifest">>;
skipped(): SkippedPlugin[];
}
@@ -46,6 +47,24 @@ export function unavailableLooks(loader: LookLoader): SkippedPlugin[] {
.filter((plugin) => plugin.layouts.length > 0 || plugin.themes.length > 0);
}
/**
* Layout ids some installed plugin brings, whether it runs, is turned off or
* failed to load. A choice outside this set was uninstalled, so the next
* choice down applies instead of the repair view.
*/
export function installedLayoutIds(loader: LookLoader): Set<string> {
const ids = new Set<string>();
for (const plugin of loader.list()) {
for (const layout of plugin.manifest.contributes?.layouts ?? []) {
ids.add(layout.id);
}
}
for (const plugin of loader.skipped()) {
for (const id of plugin.layouts) ids.add(id);
}
return ids;
}
/** TERMIX_SAFE_LAYOUT=1 starts everyone in the layout repair view. */
export function safeLayoutForced(): boolean {
const value = process.env.TERMIX_SAFE_LAYOUT?.trim().toLowerCase();
@@ -11,6 +11,7 @@ const state = vi.hoisted(() => ({
audits: [] as unknown[],
skipped: [] as unknown[],
defaultLookIds: [] as string[],
installed: [] as string[],
}));
vi.mock("../../../utils/logger.js", () => ({
@@ -69,6 +70,15 @@ vi.mock("../../../plugins/index.js", () => ({
},
}
: undefined,
list: () => [
{
manifest: {
contributes: {
layouts: state.installed.map((id) => ({ id, label: id })),
},
},
},
],
skipped: () => state.skipped,
},
}),
@@ -90,6 +100,7 @@ beforeEach(async () => {
state.audits = [];
state.skipped = [];
state.defaultLookIds = [];
state.installed = [];
delete process.env.TERMIX_SAFE_LAYOUT;
const app = express();
@@ -133,6 +144,7 @@ describe("GET /user-preferences/look", () => {
it("returns the user's pick, the admin defaults and the default look", async () => {
state.prefs = { layoutId: "mine", themeId: "nord" };
state.installed = ["mine", "admin-layout"];
state.settings.set("ui.layout", "admin-layout");
state.settings.set("ui.theme", "gruvbox");
state.defaultLookIds = ["shipped-look"];
@@ -162,6 +174,31 @@ describe("GET /user-preferences/look", () => {
});
});
describe("GET /user-preferences/look with an uninstalled layout", () => {
it("drops a choice no installed plugin brings, so the next one applies", async () => {
state.prefs = { layoutId: "deleted", themeId: null };
state.settings.set("ui.layout", "also-deleted");
state.installed = ["kept"];
const { body } = await getLook();
expect(body.layoutId).toBeNull();
expect(body.defaults.layout).toBeNull();
});
it("keeps a choice whose plugin failed to load, for the repair view", async () => {
state.prefs = { layoutId: "b", themeId: null };
state.skipped = [
{
id: "broken",
version: "1.0.0",
layouts: ["b"],
themes: [],
error: "x",
},
];
expect((await getLook()).body.layoutId).toBe("b");
});
});
describe("PUT /user-preferences/look/defaults", () => {
it("is for admins only", async () => {
const { status } = await putDefaults({ layout: "x" });
+16
View File
@@ -6,6 +6,7 @@ vi.mock("../../plugins/bundled-index.js", () => ({
}));
import {
installedLayoutIds,
resolveDefaultLook,
safeLayoutForced,
unavailableLooks,
@@ -23,6 +24,10 @@ function loaderWith(
plugins[id]
? ({ manifest: { contributes: plugins[id] } } as never)
: undefined,
list: () =>
Object.values(plugins).map(
(contributes) => ({ manifest: { contributes } }) as never,
),
skipped: () => skipped,
};
}
@@ -103,3 +108,14 @@ describe("safeLayoutForced", () => {
expect(safeLayoutForced()).toBe(false);
});
});
describe("installedLayoutIds", () => {
it("lists layouts of loaded and skipped plugins", () => {
const ids = installedLayoutIds(
loaderWith({ a: { layouts: [{ id: "one" }], themes: [{ id: "t" }] } }, [
{ id: "b", version: null, layouts: ["two"], themes: [], error: "x" },
]),
);
expect([...ids].sort()).toEqual(["one", "two"]);
});
});