* feat: add an auto or fixed columns picker to card grids

* feat: add safe mode that only runs layout and theme plugins

* docs: add simple layout to plugin table

* fix: ask for local network access in the macOS app

* feat: allow plugin registries on listed private hosts (#1705)

* chore: add private registry hosts to the changelog

* feat: use the proxy for plugin registry downloads

* fix: allow changing connection origin on shared hosts

* chore: update translations and plugin pins

* chore: lint, format, and bump version to 26.10.5

---------

Co-authored-by: Lukas <80832688+0skater0@users.noreply.github.com>
This commit is contained in:
Luke Gustafson
2026-10-22 13:30:48 -05:00
committed by GitHub
co-authored by Lukas
parent 7b7b17e31f
commit 96b8520fb4
96 changed files with 1885 additions and 474 deletions
@@ -34,6 +34,24 @@ describe("DataView", () => {
expect(screen.queryByText("Name")).toBeNull();
});
it("uses the picked size over breakpoint columns", () => {
const { container } = render(
<DataView
items={items}
view="grid"
getKey={(item) => item.id}
renderCard={(item) => <div>card {item.name}</div>}
columns={{ base: 1, md: 2 }}
size="3"
listColumns={[]}
empty={null}
/>,
);
const grid = container.firstElementChild as HTMLElement;
expect(grid.className).not.toContain("md:grid-cols-2");
expect(grid.style.gridTemplateColumns).toContain("/ 3");
});
it("renders a header and clickable rows in list mode", () => {
const onRowClick = vi.fn();
renderView("list", onRowClick);
@@ -11,6 +11,7 @@ import {
PanelShell,
Segmented,
ViewToggle,
gridSizeStyle,
} from "@/components/panel-layout";
afterEach(cleanup);
@@ -72,6 +73,34 @@ describe("ViewToggle", () => {
fireEvent.click(screen.getByLabelText("panel.compactRows"));
expect(onDensity).toHaveBeenCalledWith("compact");
});
it("shows the columns picker only in grid view", () => {
const props = { onView: vi.fn(), columns: "auto" as const };
const { rerender } = render(
<ViewToggle view="grid" {...props} onColumns={vi.fn()} />,
);
expect(screen.getByLabelText("panel.columns.title")).toBeTruthy();
rerender(<ViewToggle view="list" {...props} onColumns={vi.fn()} />);
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
rerender(<ViewToggle view="grid" {...props} />);
expect(screen.queryByLabelText("panel.columns.title")).toBeNull();
});
});
describe("gridSizeStyle", () => {
it("fits auto cards to the pane", () => {
expect(gridSizeStyle("auto", 280).gridTemplateColumns).toBe(
"repeat(auto-fill, minmax(min(100%, 280px), 1fr))",
);
});
it("splits the width for a fixed count", () => {
expect(gridSizeStyle("4").gridTemplateColumns).toContain(
"(100% - 3 * 0.5rem) / 4",
);
});
});
describe("Facts", () => {
+7
View File
@@ -236,6 +236,13 @@ describe("plugin loader", () => {
expect(railIds()).not.toContain("headless-panel");
});
it("skips plugins safe mode keeps off", async () => {
modules.paused = railPlugin("paused");
plugins = [summary("paused", { state: "safe-mode" })];
await syncPlugins();
expect(railIds()).not.toContain("paused-panel");
});
it("keeps every plugin when the pre-login list lands after sign-in", async () => {
modules.login = railPlugin("login");
modules.alpha = railPlugin("alpha");
+2 -6
View File
@@ -183,17 +183,13 @@ describe("PluginsTab", () => {
it("switches to a table and remembers it", async () => {
await renderTab();
expect(screen.queryByRole("table")).toBeNull();
expect(
screen.getByLabelText(en.plugins.manager.columns.title),
).toBeTruthy();
expect(screen.getByLabelText(en.panel.columns.title)).toBeTruthy();
fireEvent.click(screen.getByLabelText(en.panel.listView));
expect(screen.getByRole("table")).toBeTruthy();
expect(screen.getByText(en.plugins.manager.table.version)).toBeTruthy();
expect(
screen.queryByLabelText(en.plugins.manager.columns.title),
).toBeNull();
expect(screen.queryByLabelText(en.panel.columns.title)).toBeNull();
expect(localStorage.getItem("pluginsTab.view")).toBe("list");
cleanup();
@@ -157,6 +157,7 @@ describe("pluginStatus", () => {
"disabled",
);
expect(pluginStatus(summary())).toBe("running");
expect(pluginStatus(summary({ state: "safe-mode" }))).toBe("safeMode");
expect(pluginStatus(summary({ state: "failed", runsOnServer: true }))).toBe(
"server",
);
@@ -0,0 +1,45 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render } from "@testing-library/react";
const toast = vi.hoisted(() => ({ warning: vi.fn() }));
vi.mock("sonner", () => ({ toast }));
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
const look = vi.hoisted(() => ({ safeMode: false }));
vi.mock("@/look/look-store", () => ({
useLookState: () => ({
loaded: true,
response: { safeMode: look.safeMode },
}),
}));
const { SafeModeNotice } = await import("@/shell/SafeModeNotice");
afterEach(() => {
cleanup();
toast.warning.mockReset();
});
describe("SafeModeNotice", () => {
it("shows nothing when safe mode is off", () => {
look.safeMode = false;
render(<SafeModeNotice />);
expect(toast.warning).not.toHaveBeenCalled();
});
it("shows a lasting warning when the server runs in safe mode", () => {
look.safeMode = true;
render(<SafeModeNotice />);
expect(toast.warning).toHaveBeenCalledWith(
"safeMode.title",
expect.objectContaining({
id: "safe-mode",
description: "safeMode.description",
duration: Infinity,
}),
);
});
});