feat: finish the homepage plugin, move service links and canvas out of core (B19)

This commit is contained in:
LukeGus committed 2026-09-24 15:07:25 -05:00
1 parent 21937a6b02
commit d4ec444558
256 files changed
+27365 -9077

No files matched your search

@@ -1,120 +0,0 @@
import { afterEach, describe, expect, it } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { DashboardServiceLinkRepository } from "../../../database/repositories/dashboard-service-link-repository.js";
describe("DashboardServiceLinkRepository", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
if (adapter) {
await adapter.close();
adapter = null;
}
});
async function createRepository(
onWrite?: () => void | Promise<void>,
): Promise<DashboardServiceLinkRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
await adapter.exec(`
INSERT INTO users (id, username, password_hash)
VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash');
`);
return new DashboardServiceLinkRepository(context, onWrite);
}
it("creates and lists links ordered by order then id", async () => {
const repo = await createRepository();
const first = await repo.createForUser(
"user-1",
{ label: "Docs", url: "https://docs.example.com" },
"2026-06-27T00:00:00.000Z",
);
const second = await repo.createForUser("user-1", {
label: "Status",
url: "https://status.example.com",
});
await repo.createForUser("user-2", {
label: "Other",
url: "https://other.example.com",
});
expect(first).toMatchObject({
userId: "user-1",
label: "Docs",
order: 0,
createdAt: "2026-06-27T00:00:00.000Z",
});
expect(second.order).toBe(1);
expect(
(await repo.listByUserId("user-1")).map((link) => link.label),
).toEqual(["Docs", "Status"]);
});
it("finds, updates, and deletes a user-owned link", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
const link = await repo.createForUser("user-1", {
label: "Docs",
url: "https://docs.example.com",
});
expect(writeCount).toBe(1);
expect(await repo.findByIdForUser("user-2", link.id)).toBeNull();
const updated = await repo.updateForUser("user-1", link.id, {
label: "Docs renamed",
});
expect(updated).toMatchObject({
id: link.id,
label: "Docs renamed",
url: "https://docs.example.com",
});
expect(writeCount).toBe(2);
expect(await repo.updateForUser("user-2", link.id, { label: "Nope" })).toBe(
null,
);
expect(writeCount).toBe(2);
expect(await repo.deleteForUser("user-2", link.id)).toBeNull();
expect(await repo.deleteForUser("user-1", link.id)).toEqual({
syncId: expect.any(String),
});
expect(writeCount).toBe(3);
});
it("deletes all dashboard links for a user", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
await repo.createForUser("user-1", {
label: "Docs",
url: "https://docs.example.com",
});
await repo.createForUser("user-1", {
label: "Status",
url: "https://status.example.com",
});
await repo.createForUser("user-2", {
label: "Other",
url: "https://other.example.com",
});
await expect(repo.deleteByUserId("user-1")).resolves.toBe(2);
await expect(repo.deleteByUserId("missing")).resolves.toBe(0);
expect(await repo.listByUserId("user-1")).toEqual([]);
expect(
(await repo.listByUserId("user-2")).map((link) => link.label),
).toEqual(["Other"]);
expect(writeCount).toBe(4);
});
});
@@ -1,131 +0,0 @@
import { afterEach, describe, expect, it } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { HomepageItemRepository } from "../../../database/repositories/homepage-item-repository.js";
describe("HomepageItemRepository", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
if (adapter) {
await adapter.close();
adapter = null;
}
});
async function createRepository(
onWrite?: () => void | Promise<void>,
): Promise<HomepageItemRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
await adapter.exec(`
INSERT INTO users (id, username, password_hash)
VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash');
`);
return new HomepageItemRepository(context, onWrite);
}
it("creates and lists homepage items by id", async () => {
const repo = await createRepository();
const first = await repo.createForUser(
"user-1",
{ typeId: "clock", title: "Clock", config: "{}" },
"2026-06-27T00:00:00.000Z",
);
const second = await repo.createForUser("user-1", {
typeId: "terminal",
title: null,
config: '{"hostId":1}',
});
await repo.createForUser("user-2", {
typeId: "other",
title: "Other",
config: "{}",
});
expect(first).toMatchObject({
userId: "user-1",
typeId: "clock",
title: "Clock",
config: "{}",
createdAt: "2026-06-27T00:00:00.000Z",
});
expect((await repo.listByUserId("user-1")).map((item) => item.id)).toEqual([
first.id,
second.id,
]);
});
it("finds, updates, and deletes user-owned homepage items", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
const item = await repo.createForUser("user-1", {
typeId: "clock",
title: "Clock",
config: "{}",
});
expect(writeCount).toBe(1);
expect(await repo.findByIdForUser("user-2", item.id)).toBeNull();
const updated = await repo.updateForUser(
"user-1",
item.id,
{ title: "Clock renamed", config: '{"timezone":"UTC"}' },
"2026-06-27T01:00:00.000Z",
);
expect(updated).toMatchObject({
id: item.id,
title: "Clock renamed",
config: '{"timezone":"UTC"}',
updatedAt: "2026-06-27T01:00:00.000Z",
});
expect(writeCount).toBe(2);
expect(
await repo.updateForUser("user-2", item.id, { title: "Nope" }),
).toBeNull();
expect(writeCount).toBe(2);
expect(await repo.deleteForUser("user-2", item.id)).toBeNull();
expect(await repo.deleteForUser("user-1", item.id)).toEqual({
syncId: expect.any(String),
});
expect(writeCount).toBe(3);
});
it("deletes all homepage items for a user", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
await repo.createForUser("user-1", {
typeId: "clock",
title: "Clock",
config: "{}",
});
await repo.createForUser("user-1", {
typeId: "terminal",
title: "Terminal",
config: "{}",
});
await repo.createForUser("user-2", {
typeId: "other",
title: "Other",
config: "{}",
});
await expect(repo.deleteByUserId("user-1")).resolves.toBe(2);
await expect(repo.deleteByUserId("missing")).resolves.toBe(0);
expect(await repo.listByUserId("user-1")).toEqual([]);
expect(
(await repo.listByUserId("user-2")).map((item) => item.title),
).toEqual(["Other"]);
expect(writeCount).toBe(4);
});
});
@@ -1,85 +0,0 @@
import { afterEach, describe, expect, it } from "vitest";
import { TestSqliteDatabase } from "./test-support.js";
import { HomepageLayoutRepository } from "../../../database/repositories/homepage-layout-repository.js";
describe("HomepageLayoutRepository", () => {
let adapter: TestSqliteDatabase | null = null;
afterEach(async () => {
if (adapter) {
await adapter.close();
adapter = null;
}
});
async function createRepository(
onWrite?: () => void | Promise<void>,
): Promise<HomepageLayoutRepository> {
adapter = new TestSqliteDatabase();
const context = await adapter.connect();
await adapter.exec(`
INSERT INTO users (id, username, password_hash)
VALUES ('user-1', 'alice', 'hash'), ('user-2', 'bob', 'hash');
`);
return new HomepageLayoutRepository(context, onWrite);
}
it("finds, creates, and updates a layout by user id", async () => {
const repo = await createRepository();
expect(await repo.findByUserId("user-1")).toBeNull();
const created = await repo.upsertForUser(
"user-1",
JSON.stringify({ entries: [{ id: "w1" }], zoom: 1 }),
"2026-06-27T00:00:00.000Z",
);
expect(created).toMatchObject({
userId: "user-1",
layout: '{"entries":[{"id":"w1"}],"zoom":1}',
updatedAt: "2026-06-27T00:00:00.000Z",
});
const updated = await repo.upsertForUser(
"user-1",
JSON.stringify({ entries: [], zoom: 1.25 }),
"2026-06-27T01:00:00.000Z",
);
expect(updated).toMatchObject({
id: created.id,
userId: "user-1",
layout: '{"entries":[],"zoom":1.25}',
updatedAt: "2026-06-27T01:00:00.000Z",
});
});
it("triggers writes after create and update", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
await repo.upsertForUser("user-1", "{}");
await repo.upsertForUser("user-1", '{"zoom":2}');
expect(writeCount).toBe(2);
});
it("deletes a layout for a user", async () => {
let writeCount = 0;
const repo = await createRepository(() => {
writeCount += 1;
});
await repo.upsertForUser("user-1", '{"zoom":1}');
await repo.upsertForUser("user-2", '{"zoom":2}');
await expect(repo.deleteByUserId("user-1")).resolves.toBe(1);
await expect(repo.deleteByUserId("missing")).resolves.toBe(0);
expect(await repo.findByUserId("user-1")).toBeNull();
expect((await repo.findByUserId("user-2"))?.layout).toBe('{"zoom":2}');
expect(writeCount).toBe(3);
});
});