From cb77b2d3b9371673c42b3a2d32ea97c72f20b3a7 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sun, 30 Aug 2026 19:38:19 +0200 Subject: [PATCH] fix(housekeeping): serialize workspace client props --- .../operations/pages/personalization.tsx | 4 +-- .../operations/pages/workspace.test.tsx | 7 ++--- .../domains/operations/pages/workspace.tsx | 4 +-- .../operations/personalization-model.test.ts | 29 +++++++++++++++++++ .../operations/personalization-model.ts | 15 ++++++++++ .../domains/operations/route-handlers.ts | 3 +- .../foundation/shell/widget-settings.tsx | 12 ++++---- 7 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 src/features/housekeeping/domains/operations/personalization-model.test.ts create mode 100644 src/features/housekeeping/domains/operations/personalization-model.ts diff --git a/src/features/housekeeping/domains/operations/pages/personalization.tsx b/src/features/housekeeping/domains/operations/pages/personalization.tsx index f8c40fa5..4bc79c65 100644 --- a/src/features/housekeeping/domains/operations/pages/personalization.tsx +++ b/src/features/housekeeping/domains/operations/pages/personalization.tsx @@ -3,7 +3,6 @@ import type { HousekeepingRouteDefinition, HousekeepingSurface, - HousekeepingWidgetDefinition, } from "../../../foundation/contracts"; import type { HousekeepingPreferences } from "../../../foundation/preferences/schema"; import type { HousekeepingWidgetLoadResult } from "../../../foundation/registry"; @@ -13,6 +12,7 @@ import { type HousekeepingPreferenceSaver, WidgetSettings, } from "../../../foundation/shell/widget-settings"; +import type { OperationsWidgetOption } from "../personalization-model"; export interface OperationsPersonalizationLabels { readonly favorites: { @@ -55,7 +55,7 @@ export function OperationsPersonalization({ }: { readonly preferences: HousekeepingPreferences; readonly routes: readonly HousekeepingRouteDefinition[]; - readonly widgets: readonly HousekeepingWidgetDefinition[]; + readonly widgets: readonly OperationsWidgetOption[]; readonly widgetResult: HousekeepingWidgetLoadResult; readonly surface: HousekeepingSurface; readonly labels: OperationsPersonalizationLabels; diff --git a/src/features/housekeeping/domains/operations/pages/workspace.test.tsx b/src/features/housekeeping/domains/operations/pages/workspace.test.tsx index ca5b8499..c3badcdf 100644 --- a/src/features/housekeeping/domains/operations/pages/workspace.test.tsx +++ b/src/features/housekeeping/domains/operations/pages/workspace.test.tsx @@ -4,10 +4,10 @@ import { PERMS } from "@/lib/permission-slugs"; import { anyCapability, type HousekeepingRouteDefinition, - type HousekeepingWidgetDefinition, ok, } from "../../../foundation/contracts"; import { defaultHousekeepingPreferences } from "../../../foundation/preferences/schema"; +import type { OperationsWidgetOption } from "../personalization-model"; import type { OperationsWorkspaceLoadResult } from "../queries"; import { OperationsWorkspacePage } from "./workspace"; @@ -20,13 +20,10 @@ const routes: readonly HousekeepingRouteDefinition[] = [ capability, }, ]; -const widgets: readonly HousekeepingWidgetDefinition[] = [ +const widgets: readonly OperationsWidgetOption[] = [ { id: "people.queue", - owner: "people", - capability, kind: "mandatory", - load: async () => ok({}, "widget"), }, ]; const preferences = { diff --git a/src/features/housekeeping/domains/operations/pages/workspace.tsx b/src/features/housekeeping/domains/operations/pages/workspace.tsx index 8e19dd19..4150a4d8 100644 --- a/src/features/housekeeping/domains/operations/pages/workspace.tsx +++ b/src/features/housekeeping/domains/operations/pages/workspace.tsx @@ -1,12 +1,12 @@ import type { HousekeepingRouteDefinition, HousekeepingSurface, - HousekeepingWidgetDefinition, } from "../../../foundation/contracts"; import { HousekeepingPageShell } from "../../../foundation/page/housekeeping-page-shell"; import { OperationalInbox } from "../../../foundation/shell/operational-inbox"; import { RecentWork } from "../../../foundation/shell/recent-work"; import type { HousekeepingPreferenceSaver } from "../../../foundation/shell/widget-settings"; +import type { OperationsWidgetOption } from "../personalization-model"; import type { OperationsWorkspaceLoadResult } from "../queries"; import { OperationsPersonalization, @@ -50,7 +50,7 @@ export function OperationsWorkspacePage({ }: { readonly result: OperationsWorkspaceLoadResult; readonly routes: readonly HousekeepingRouteDefinition[]; - readonly widgetDefinitions: readonly HousekeepingWidgetDefinition[]; + readonly widgetDefinitions: readonly OperationsWidgetOption[]; readonly surface: HousekeepingSurface; readonly labels: OperationsWorkspaceLabels; readonly routeTitles: Readonly>; diff --git a/src/features/housekeeping/domains/operations/personalization-model.test.ts b/src/features/housekeeping/domains/operations/personalization-model.test.ts new file mode 100644 index 00000000..04039f39 --- /dev/null +++ b/src/features/housekeeping/domains/operations/personalization-model.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it, vi } from "vitest"; +import { + anyCapability, + type HousekeepingWidgetDefinition, + ok, +} from "../../foundation/contracts"; +import { toOperationsWidgetOptions } from "./personalization-model"; + +describe("toOperationsWidgetOptions", () => { + it("projects provider definitions to a serializable client view model", () => { + const load = vi.fn(async () => ok({ count: 1 }, "widget")); + const definitions: readonly HousekeepingWidgetDefinition[] = [ + { + id: "people.queue", + owner: "people", + capability: anyCapability("admin.users.view"), + kind: "mandatory", + load, + }, + ]; + + const options = toOperationsWidgetOptions(definitions); + + expect(options).toEqual([{ id: "people.queue", kind: "mandatory" }]); + expect(structuredClone(options)).toEqual(options); + expect(options[0]).not.toHaveProperty("load"); + expect(load).not.toHaveBeenCalled(); + }); +}); diff --git a/src/features/housekeeping/domains/operations/personalization-model.ts b/src/features/housekeeping/domains/operations/personalization-model.ts new file mode 100644 index 00000000..e7c7f2f9 --- /dev/null +++ b/src/features/housekeeping/domains/operations/personalization-model.ts @@ -0,0 +1,15 @@ +import type { + HousekeepingWidgetDefinition, + HousekeepingWidgetKind, +} from "../../foundation/contracts"; + +export interface OperationsWidgetOption { + readonly id: string; + readonly kind: HousekeepingWidgetKind; +} + +export function toOperationsWidgetOptions( + definitions: readonly HousekeepingWidgetDefinition[], +): readonly OperationsWidgetOption[] { + return definitions.map(({ id, kind }) => ({ id, kind })); +} diff --git a/src/features/housekeeping/domains/operations/route-handlers.ts b/src/features/housekeeping/domains/operations/route-handlers.ts index cb71f312..be60b102 100644 --- a/src/features/housekeeping/domains/operations/route-handlers.ts +++ b/src/features/housekeeping/domains/operations/route-handlers.ts @@ -7,6 +7,7 @@ import { type OperationsWorkspaceLabels, OperationsWorkspacePage, } from "./pages/workspace"; +import { toOperationsWidgetOptions } from "./personalization-model"; import { operationsWorkspaceQuery } from "./queries"; export async function renderOperationsWorkspacePage( @@ -138,7 +139,7 @@ export async function renderOperationsWorkspacePage( return createElement(OperationsWorkspacePage, { result: workspaceResult, routes, - widgetDefinitions, + widgetDefinitions: toOperationsWidgetOptions(widgetDefinitions), surface: input.surface ?? "preview", labels, routeTitles, diff --git a/src/features/housekeeping/foundation/shell/widget-settings.tsx b/src/features/housekeeping/foundation/shell/widget-settings.tsx index 96711e7f..5d810585 100644 --- a/src/features/housekeeping/foundation/shell/widget-settings.tsx +++ b/src/features/housekeeping/foundation/shell/widget-settings.tsx @@ -1,10 +1,7 @@ "use client"; import { useState } from "react"; -import type { - HousekeepingResult, - HousekeepingWidgetDefinition, -} from "../contracts"; +import type { HousekeepingResult, HousekeepingWidgetKind } from "../contracts"; import type { HousekeepingPreferences } from "../preferences/schema"; export type HousekeepingPreferenceSaver = ( @@ -44,6 +41,11 @@ export interface WidgetSettingsLabels { readonly error: string; } +export interface HousekeepingWidgetOption { + readonly id: string; + readonly kind: HousekeepingWidgetKind; +} + export function WidgetSettings({ preferences: initialPreferences, widgets, @@ -52,7 +54,7 @@ export function WidgetSettings({ titles = {}, }: { readonly preferences: HousekeepingPreferences; - readonly widgets: readonly HousekeepingWidgetDefinition[]; + readonly widgets: readonly HousekeepingWidgetOption[]; readonly savePreferences: HousekeepingPreferenceSaver; readonly labels: WidgetSettingsLabels; readonly titles?: Readonly>;