feat(plugins): pin installed plugins at the top
This commit is contained in:
12 files changed
+205
-22
No files matched your search
@@ -0,0 +1,57 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { FrontendPluginDefinition } from "@/lib/plugins/frontendPlugin";
|
||||
import { loadPinnedPluginIds, savePinnedPluginIds, sortPluginsPinnedFirst } from "@/lib/plugins/pluginPinning";
|
||||
|
||||
function definition(id: string): FrontendPluginDefinition {
|
||||
return { plugin: { manifest: { id } } } as unknown as FrontendPluginDefinition;
|
||||
}
|
||||
|
||||
describe("sortPluginsPinnedFirst", () => {
|
||||
const defs = [definition("a"), definition("b"), definition("c"), definition("d")];
|
||||
|
||||
it("moves pinned plugins to the front while keeping relative order", () => {
|
||||
expect(sortPluginsPinnedFirst(defs, ["c", "a"]).map((d) => d.plugin.manifest.id)).toEqual(["a", "c", "b", "d"]);
|
||||
});
|
||||
|
||||
it("returns the original order when nothing is pinned", () => {
|
||||
expect(sortPluginsPinnedFirst(defs, [])).toEqual(defs);
|
||||
});
|
||||
|
||||
it("ignores pinned ids that no longer match an installed plugin", () => {
|
||||
expect(sortPluginsPinnedFirst(defs, ["gone", "d"]).map((d) => d.plugin.manifest.id)).toEqual(["d", "a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("returns the input array identity when no pin applies", () => {
|
||||
expect(sortPluginsPinnedFirst(defs, ["gone"])).toBe(defs);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pinned plugin storage", () => {
|
||||
const backing = new Map<string, string>();
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: (key: string) => backing.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void backing.set(key, value),
|
||||
removeItem: (key: string) => void backing.delete(key),
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("round-trips ids through localStorage", () => {
|
||||
savePinnedPluginIds(["io.example.a", "io.example.b"]);
|
||||
expect(loadPinnedPluginIds()).toEqual(["io.example.a", "io.example.b"]);
|
||||
savePinnedPluginIds([]);
|
||||
expect(loadPinnedPluginIds()).toEqual([]);
|
||||
});
|
||||
|
||||
it("tolerates corrupted stored values", () => {
|
||||
localStorage.setItem("dbx-plugin-pinned-ids", "{not json");
|
||||
expect(loadPinnedPluginIds()).toEqual([]);
|
||||
localStorage.setItem("dbx-plugin-pinned-ids", JSON.stringify([1, "ok", null]));
|
||||
expect(loadPinnedPluginIds()).toEqual(["ok"]);
|
||||
localStorage.removeItem("dbx-plugin-pinned-ids");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { FrontendPluginDefinition } from "@/lib/plugins/frontendPlugin";
|
||||
|
||||
const PINNED_PLUGINS_STORAGE_KEY = "dbx-plugin-pinned-ids";
|
||||
|
||||
export function loadPinnedPluginIds(): string[] {
|
||||
try {
|
||||
if (typeof localStorage === "undefined") return [];
|
||||
const saved = localStorage.getItem(PINNED_PLUGINS_STORAGE_KEY);
|
||||
const ids = saved ? JSON.parse(saved) : [];
|
||||
return Array.isArray(ids) ? ids.filter((id): id is string => typeof id === "string") : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function savePinnedPluginIds(ids: string[]): void {
|
||||
try {
|
||||
if (typeof localStorage === "undefined") return;
|
||||
localStorage.setItem(PINNED_PLUGINS_STORAGE_KEY, JSON.stringify(ids));
|
||||
} catch {
|
||||
// Storage may be unavailable (private mode); pinning just won't persist.
|
||||
}
|
||||
}
|
||||
|
||||
/** Stable partition: pinned plugins keep their relative order first, then
|
||||
* the rest follow in their original order. Unknown ids in the pinned list
|
||||
* are ignored so uninstalling a plugin leaves no phantom pins. */
|
||||
export function sortPluginsPinnedFirst<T extends FrontendPluginDefinition>(definitions: T[], pinnedIds: string[]): T[] {
|
||||
const pinned = new Set(pinnedIds);
|
||||
const pinnedPart = definitions.filter((definition) => pinned.has(definition.plugin.manifest.id));
|
||||
if (pinnedPart.length === 0) return definitions;
|
||||
return [...pinnedPart, ...definitions.filter((definition) => !pinned.has(definition.plugin.manifest.id))];
|
||||
}
|
||||
Reference in new issue
Block a user