feat(housekeeping): compose operations workspace
This commit is contained in:
1 parent
5574e601bb
commit
85ad0452d3
22 files changed
+1193
-40
No files matched your search
@@ -44,6 +44,7 @@ export default async function HousekeepingPreviewRoutePage({
|
|||||||
return handler.render({
|
return handler.render({
|
||||||
context,
|
context,
|
||||||
match,
|
match,
|
||||||
|
surface: "preview",
|
||||||
...(searchParams ? { searchParams: await searchParams } : {}),
|
...(searchParams ? { searchParams: await searchParams } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,97 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
|
import { authorizeHousekeeping } from "../../foundation/authorization";
|
||||||
|
import { satisfiesCapability } from "../../foundation/capability-context";
|
||||||
|
import {
|
||||||
|
anyCapability,
|
||||||
|
fail,
|
||||||
|
type HousekeepingCapabilityContext,
|
||||||
|
type HousekeepingInboxSource,
|
||||||
|
ok,
|
||||||
|
} from "../../foundation/contracts";
|
||||||
|
|
||||||
|
const capability = anyCapability(PERMS.ADMIN_DASHBOARD);
|
||||||
|
|
||||||
|
export interface OperationsAccessSummary {
|
||||||
|
readonly accessibleDomains: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationsInboxAdapters {
|
||||||
|
accessSummary(
|
||||||
|
context: HousekeepingCapabilityContext,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<OperationsAccessSummary>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOperationsInboxSources(
|
||||||
|
adapters: OperationsInboxAdapters,
|
||||||
|
): readonly HousekeepingInboxSource[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "operations.access-summary",
|
||||||
|
owner: "operations",
|
||||||
|
capability,
|
||||||
|
async getItems(context, signal) {
|
||||||
|
const authorization = authorizeHousekeeping(context, capability);
|
||||||
|
if (!authorization.ok) return authorization;
|
||||||
|
try {
|
||||||
|
const summary = await adapters.accessSummary(context, signal);
|
||||||
|
const occurredAt = new Date().toISOString();
|
||||||
|
return ok(
|
||||||
|
{
|
||||||
|
availability: "available" as const,
|
||||||
|
items:
|
||||||
|
summary.accessibleDomains === 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
sourceId: "operations.access-summary",
|
||||||
|
itemId: "limited-access",
|
||||||
|
deduplicationKey:
|
||||||
|
"operations.access-summary:limited-access",
|
||||||
|
domain: "operations" as const,
|
||||||
|
capability,
|
||||||
|
severity: "warning" as const,
|
||||||
|
priority: "high" as const,
|
||||||
|
ageMs: 0,
|
||||||
|
state: "limited-access",
|
||||||
|
occurredAt,
|
||||||
|
titleKey:
|
||||||
|
"pages.housekeeping.items.operationsLimitedAccess",
|
||||||
|
href: "/ase" as const,
|
||||||
|
freshness: "fresh" as const,
|
||||||
|
actions: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
authorization.correlationId,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return fail(
|
||||||
|
"DEPENDENCY_UNAVAILABLE",
|
||||||
|
"errors.housekeeping.dependencyUnavailable",
|
||||||
|
authorization.correlationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPERATIONS_INBOX_SOURCES = createOperationsInboxSources({
|
||||||
|
async accessSummary(context, signal) {
|
||||||
|
if (signal.aborted) throw new Error("aborted operations access summary");
|
||||||
|
const { HOUSEKEEPING_MANIFESTS } = await import("../../manifests");
|
||||||
|
return {
|
||||||
|
accessibleDomains: HOUSEKEEPING_MANIFESTS.filter(
|
||||||
|
(domain) =>
|
||||||
|
domain.id !== "operations" &&
|
||||||
|
satisfiesCapability(context, domain.capability) &&
|
||||||
|
domain.routes.some((route) =>
|
||||||
|
satisfiesCapability(context, route.capability),
|
||||||
|
),
|
||||||
|
).length,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -3,6 +3,10 @@ import {
|
|||||||
anyCapability,
|
anyCapability,
|
||||||
type HousekeepingDomainManifest,
|
type HousekeepingDomainManifest,
|
||||||
} from "../../foundation/contracts";
|
} from "../../foundation/contracts";
|
||||||
|
import { OPERATIONS_INBOX_SOURCES } from "./inbox";
|
||||||
|
import { OPERATIONS_ROUTES } from "./routes";
|
||||||
|
import { OPERATIONS_SEARCH_PROVIDERS } from "./search";
|
||||||
|
import { OPERATIONS_WIDGETS } from "./widgets";
|
||||||
|
|
||||||
export const operationsManifest = {
|
export const operationsManifest = {
|
||||||
id: "operations",
|
id: "operations",
|
||||||
@@ -11,8 +15,8 @@ export const operationsManifest = {
|
|||||||
iconId: "inbox",
|
iconId: "inbox",
|
||||||
canonicalHref: "/ase",
|
canonicalHref: "/ase",
|
||||||
capability: anyCapability(PERMS.ADMIN_DASHBOARD),
|
capability: anyCapability(PERMS.ADMIN_DASHBOARD),
|
||||||
routes: [],
|
routes: OPERATIONS_ROUTES,
|
||||||
searchProviders: [],
|
searchProviders: OPERATIONS_SEARCH_PROVIDERS,
|
||||||
inboxSources: [],
|
inboxSources: OPERATIONS_INBOX_SOURCES,
|
||||||
widgets: [],
|
widgets: OPERATIONS_WIDGETS,
|
||||||
} satisfies HousekeepingDomainManifest;
|
} satisfies HousekeepingDomainManifest;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
HousekeepingRouteDefinition,
|
||||||
|
HousekeepingSurface,
|
||||||
|
HousekeepingWidgetDefinition,
|
||||||
|
} from "../../../foundation/contracts";
|
||||||
|
import type { HousekeepingPreferences } from "../../../foundation/preferences/schema";
|
||||||
|
import type { HousekeepingWidgetLoadResult } from "../../../foundation/registry";
|
||||||
|
import { Favorites } from "../../../foundation/shell/favorites";
|
||||||
|
import { WidgetGrid } from "../../../foundation/shell/widget-grid";
|
||||||
|
import {
|
||||||
|
type HousekeepingPreferenceSaver,
|
||||||
|
WidgetSettings,
|
||||||
|
} from "../../../foundation/shell/widget-settings";
|
||||||
|
|
||||||
|
const persistPreferences: HousekeepingPreferenceSaver = async (preferences) => {
|
||||||
|
const { saveHousekeepingPreferences } = await import(
|
||||||
|
"@/actions/housekeeping-preferences"
|
||||||
|
);
|
||||||
|
return saveHousekeepingPreferences(preferences);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OperationsPersonalization({
|
||||||
|
preferences,
|
||||||
|
routes,
|
||||||
|
widgets,
|
||||||
|
widgetResult,
|
||||||
|
surface,
|
||||||
|
savePreferences = persistPreferences,
|
||||||
|
}: {
|
||||||
|
readonly preferences: HousekeepingPreferences;
|
||||||
|
readonly routes: readonly HousekeepingRouteDefinition[];
|
||||||
|
readonly widgets: readonly HousekeepingWidgetDefinition[];
|
||||||
|
readonly widgetResult: HousekeepingWidgetLoadResult;
|
||||||
|
readonly surface: HousekeepingSurface;
|
||||||
|
readonly savePreferences?: HousekeepingPreferenceSaver;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid gap-6 xl:grid-cols-2">
|
||||||
|
<Favorites
|
||||||
|
preferences={preferences}
|
||||||
|
routes={routes}
|
||||||
|
surface={surface}
|
||||||
|
savePreferences={savePreferences}
|
||||||
|
labels={{
|
||||||
|
title: "Favorites",
|
||||||
|
pin: "Pin",
|
||||||
|
unpin: "Unpin",
|
||||||
|
error: "Preferences could not be saved",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<WidgetSettings
|
||||||
|
preferences={preferences}
|
||||||
|
widgets={widgets}
|
||||||
|
savePreferences={savePreferences}
|
||||||
|
labels={{
|
||||||
|
title: "Widget settings",
|
||||||
|
mandatory: "Mandatory",
|
||||||
|
error: "Preferences could not be saved",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WidgetGrid
|
||||||
|
preferences={preferences}
|
||||||
|
result={widgetResult}
|
||||||
|
savePreferences={savePreferences}
|
||||||
|
labels={{
|
||||||
|
title: "Widgets",
|
||||||
|
partial: "Some widgets are unavailable",
|
||||||
|
error: "Widget order could not be saved",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,47 @@
|
|||||||
|
import { createElement } from "react";
|
||||||
|
import { satisfiesCapability } from "../../foundation/capability-context";
|
||||||
|
import { createHousekeepingRegistry } from "../../foundation/registry";
|
||||||
|
import { HOUSEKEEPING_MANIFESTS } from "../../manifests";
|
||||||
|
import type { HousekeepingRouteHandler } from "../../route-handlers";
|
||||||
|
import { OperationsWorkspacePage } from "./pages/workspace";
|
||||||
|
import { operationsWorkspaceQuery } from "./queries";
|
||||||
|
|
||||||
|
export async function renderOperationsWorkspacePage(
|
||||||
|
input: Parameters<HousekeepingRouteHandler["render"]>[0],
|
||||||
|
) {
|
||||||
|
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
|
||||||
|
const accessibleDomains = registry.domains.filter(
|
||||||
|
(domain) =>
|
||||||
|
domain.id !== "operations" &&
|
||||||
|
satisfiesCapability(input.context, domain.capability),
|
||||||
|
);
|
||||||
|
const routes = accessibleDomains.flatMap((domain) =>
|
||||||
|
domain.routes.filter((route) =>
|
||||||
|
satisfiesCapability(input.context, route.capability),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const widgetDefinitions = registry.domains
|
||||||
|
.filter((domain) => satisfiesCapability(input.context, domain.capability))
|
||||||
|
.flatMap((domain) =>
|
||||||
|
domain.widgets.filter((widget) =>
|
||||||
|
satisfiesCapability(input.context, widget.capability),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return createElement(OperationsWorkspacePage, {
|
||||||
|
result: await operationsWorkspaceQuery(input.context),
|
||||||
|
routes,
|
||||||
|
widgetDefinitions,
|
||||||
|
surface: input.surface ?? "preview",
|
||||||
|
...(input.savePreferences
|
||||||
|
? { savePreferences: input.savePreferences }
|
||||||
|
: {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPERATIONS_ROUTE_HANDLERS: readonly HousekeepingRouteHandler[] =
|
||||||
|
Object.freeze([
|
||||||
|
Object.freeze({
|
||||||
|
routeId: "operations.workspace",
|
||||||
|
render: renderOperationsWorkspacePage,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
|
import type { HousekeepingCapabilityContext } from "../../foundation/contracts";
|
||||||
|
import { operationsMigrationEntries } from "../../migration/operations";
|
||||||
|
import { OPERATIONS_INBOX_SOURCES } from "./inbox";
|
||||||
|
import { operationsManifest } from "./manifest";
|
||||||
|
import { OPERATIONS_ROUTES } from "./routes";
|
||||||
|
import { OPERATIONS_SEARCH_PROVIDERS } from "./search";
|
||||||
|
import { OPERATIONS_WIDGETS } from "./widgets";
|
||||||
|
|
||||||
|
const context: HousekeepingCapabilityContext = {
|
||||||
|
actor: { id: 42, username: "operator", rank: 7 },
|
||||||
|
isSuperAdmin: false,
|
||||||
|
has: (slug) => slug === PERMS.ADMIN_DASHBOARD,
|
||||||
|
hasAny: (...slugs) => slugs.includes(PERMS.ADMIN_DASHBOARD),
|
||||||
|
hasAll: (...slugs) => slugs.every((slug) => slug === PERMS.ADMIN_DASHBOARD),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Operations workspace registration", () => {
|
||||||
|
it("maps the one legacy dashboard row to operations.workspace at canonical /ase", () => {
|
||||||
|
expect(OPERATIONS_ROUTES).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "operations.workspace",
|
||||||
|
href: "/ase",
|
||||||
|
capability: { mode: "any", slugs: [PERMS.ADMIN_DASHBOARD] },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(operationsMigrationEntries).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
legacyPath: "/admin",
|
||||||
|
targetDomain: "operations",
|
||||||
|
targetPath: OPERATIONS_ROUTES[0]?.href,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("publishes a real route, safe navigation search, inbox summary, and mandatory widget", async () => {
|
||||||
|
expect(operationsManifest.routes).toEqual(OPERATIONS_ROUTES);
|
||||||
|
expect(operationsManifest.searchProviders).toEqual(
|
||||||
|
OPERATIONS_SEARCH_PROVIDERS,
|
||||||
|
);
|
||||||
|
expect(operationsManifest.inboxSources).toEqual(OPERATIONS_INBOX_SOURCES);
|
||||||
|
expect(operationsManifest.widgets).toEqual(OPERATIONS_WIDGETS);
|
||||||
|
|
||||||
|
const search = await OPERATIONS_SEARCH_PROVIDERS[0]?.search(context, {
|
||||||
|
term: "operations",
|
||||||
|
limit: 25,
|
||||||
|
});
|
||||||
|
expect(search).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
data: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "operations.workspace",
|
||||||
|
type: "navigation",
|
||||||
|
href: "/ase",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const inbox = await OPERATIONS_INBOX_SOURCES[0]?.getItems(
|
||||||
|
context,
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
expect(inbox).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
availability: "available",
|
||||||
|
items: [
|
||||||
|
expect.objectContaining({
|
||||||
|
sourceId: "operations.access-summary",
|
||||||
|
href: "/ase",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(OPERATIONS_WIDGETS[0]).toMatchObject({
|
||||||
|
id: "operations.workspace-summary",
|
||||||
|
kind: "mandatory",
|
||||||
|
owner: "operations",
|
||||||
|
});
|
||||||
|
const widget = await OPERATIONS_WIDGETS[0]?.load(
|
||||||
|
context,
|
||||||
|
new AbortController().signal,
|
||||||
|
);
|
||||||
|
expect(widget).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
data: { accessibleDomains: 0, accessibleRoutes: 0 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
|
import {
|
||||||
|
anyCapability,
|
||||||
|
type HousekeepingRouteDefinition,
|
||||||
|
} from "../../foundation/contracts";
|
||||||
|
|
||||||
|
export const OPERATIONS_ROUTES = [
|
||||||
|
{
|
||||||
|
id: "operations.workspace",
|
||||||
|
labelKey: "pages.housekeeping.routes.operations.workspace",
|
||||||
|
href: "/ase",
|
||||||
|
capability: anyCapability(PERMS.ADMIN_DASHBOARD),
|
||||||
|
},
|
||||||
|
] as const satisfies readonly HousekeepingRouteDefinition[];
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
|
import { authorizeHousekeeping } from "../../foundation/authorization";
|
||||||
|
import {
|
||||||
|
anyCapability,
|
||||||
|
type HousekeepingSearchProvider,
|
||||||
|
ok,
|
||||||
|
} from "../../foundation/contracts";
|
||||||
|
|
||||||
|
const capability = anyCapability(PERMS.ADMIN_DASHBOARD);
|
||||||
|
|
||||||
|
export const OPERATIONS_SEARCH_PROVIDERS = [
|
||||||
|
{
|
||||||
|
id: "operations.workspace",
|
||||||
|
owner: "operations",
|
||||||
|
capability,
|
||||||
|
async search(context, input) {
|
||||||
|
const authorization = authorizeHousekeeping(context, capability);
|
||||||
|
if (!authorization.ok) return authorization;
|
||||||
|
const term = input.term.normalize("NFC").trim().toLocaleLowerCase();
|
||||||
|
const matches = ["operations", "workspace", "dashboard"].some((value) =>
|
||||||
|
value.includes(term),
|
||||||
|
);
|
||||||
|
return ok(
|
||||||
|
matches
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "operations.workspace",
|
||||||
|
domain: "operations" as const,
|
||||||
|
type: "navigation" as const,
|
||||||
|
title: "Operations workspace",
|
||||||
|
description: "Open the operational home",
|
||||||
|
href: "/ase" as const,
|
||||||
|
capability,
|
||||||
|
},
|
||||||
|
].slice(0, Math.max(0, Math.trunc(input.limit)))
|
||||||
|
: [],
|
||||||
|
authorization.correlationId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const satisfies readonly HousekeepingSearchProvider[];
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
|
import { authorizeHousekeeping } from "../../foundation/authorization";
|
||||||
|
import { satisfiesCapability } from "../../foundation/capability-context";
|
||||||
|
import {
|
||||||
|
anyCapability,
|
||||||
|
fail,
|
||||||
|
type HousekeepingCapabilityContext,
|
||||||
|
type HousekeepingWidgetDefinition,
|
||||||
|
ok,
|
||||||
|
} from "../../foundation/contracts";
|
||||||
|
|
||||||
|
const capability = anyCapability(PERMS.ADMIN_DASHBOARD);
|
||||||
|
|
||||||
|
export interface OperationsWorkspaceSummary {
|
||||||
|
readonly accessibleDomains: number;
|
||||||
|
readonly accessibleRoutes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationsWidgetAdapters {
|
||||||
|
workspaceSummary(
|
||||||
|
context: HousekeepingCapabilityContext,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<OperationsWorkspaceSummary>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOperationsWidgets(
|
||||||
|
adapters: OperationsWidgetAdapters,
|
||||||
|
): readonly HousekeepingWidgetDefinition[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "operations.workspace-summary",
|
||||||
|
owner: "operations",
|
||||||
|
capability,
|
||||||
|
kind: "mandatory",
|
||||||
|
async load(context, signal) {
|
||||||
|
const authorization = authorizeHousekeeping(context, capability);
|
||||||
|
if (!authorization.ok) return authorization;
|
||||||
|
try {
|
||||||
|
return ok(
|
||||||
|
await adapters.workspaceSummary(context, signal),
|
||||||
|
authorization.correlationId,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return fail(
|
||||||
|
"DEPENDENCY_UNAVAILABLE",
|
||||||
|
"errors.housekeeping.dependencyUnavailable",
|
||||||
|
authorization.correlationId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPERATIONS_WIDGETS = createOperationsWidgets({
|
||||||
|
async workspaceSummary(context, signal) {
|
||||||
|
if (signal.aborted) throw new Error("aborted operations workspace summary");
|
||||||
|
const { HOUSEKEEPING_MANIFESTS } = await import("../../manifests");
|
||||||
|
const domains = HOUSEKEEPING_MANIFESTS.filter(
|
||||||
|
(domain) =>
|
||||||
|
domain.id !== "operations" &&
|
||||||
|
satisfiesCapability(context, domain.capability),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
accessibleDomains: domains.filter((domain) =>
|
||||||
|
domain.routes.some((route) =>
|
||||||
|
satisfiesCapability(context, route.capability),
|
||||||
|
),
|
||||||
|
).length,
|
||||||
|
accessibleRoutes: domains.reduce(
|
||||||
|
(total, domain) =>
|
||||||
|
total +
|
||||||
|
domain.routes.filter((route) =>
|
||||||
|
satisfiesCapability(context, route.capability),
|
||||||
|
).length,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -7,6 +7,7 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
import { CONTENT_ROUTE_IDS } from "../domains/content/routes";
|
import { CONTENT_ROUTE_IDS } from "../domains/content/routes";
|
||||||
import { ECONOMY_ROUTE_IDS } from "../domains/economy/routes";
|
import { ECONOMY_ROUTE_IDS } from "../domains/economy/routes";
|
||||||
import { HOTEL_ROUTE_IDS } from "../domains/hotel/routes";
|
import { HOTEL_ROUTE_IDS } from "../domains/hotel/routes";
|
||||||
|
import { OPERATIONS_ROUTES } from "../domains/operations/routes";
|
||||||
import { PEOPLE_ROUTE_IDS } from "../domains/people/routes";
|
import { PEOPLE_ROUTE_IDS } from "../domains/people/routes";
|
||||||
import { SYSTEM_ROUTE_IDS } from "../domains/system/routes";
|
import { SYSTEM_ROUTE_IDS } from "../domains/system/routes";
|
||||||
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
|
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
|
||||||
@@ -27,6 +28,33 @@ const SERVER_CAPABILITY_CONTEXT =
|
|||||||
const PERMISSIONS_ADAPTER = "src/lib/permissions";
|
const PERMISSIONS_ADAPTER = "src/lib/permissions";
|
||||||
const DOMAIN_MODULE_ROOT = "src/features/housekeeping/domains";
|
const DOMAIN_MODULE_ROOT = "src/features/housekeeping/domains";
|
||||||
const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
|
const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
|
||||||
|
[
|
||||||
|
"src/features/housekeeping/domains/operations/manifest.ts",
|
||||||
|
new Set([
|
||||||
|
"src/features/housekeeping/domains/operations/inbox",
|
||||||
|
"src/features/housekeeping/domains/operations/routes",
|
||||||
|
"src/features/housekeeping/domains/operations/search",
|
||||||
|
"src/features/housekeeping/domains/operations/widgets",
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/features/housekeeping/domains/operations/pages/personalization.tsx",
|
||||||
|
new Set(["src/actions/housekeeping-preferences"]),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/features/housekeeping/domains/operations/pages/workspace.tsx",
|
||||||
|
new Set([
|
||||||
|
"src/features/housekeeping/domains/operations/pages/personalization",
|
||||||
|
"src/features/housekeeping/domains/operations/queries",
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"src/features/housekeeping/domains/operations/route-handlers.ts",
|
||||||
|
new Set([
|
||||||
|
"src/features/housekeeping/domains/operations/pages/workspace",
|
||||||
|
"src/features/housekeeping/domains/operations/queries",
|
||||||
|
]),
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"src/features/housekeeping/foundation/shell/command-deck.tsx",
|
"src/features/housekeeping/foundation/shell/command-deck.tsx",
|
||||||
new Set(["src/actions/housekeeping-search"]),
|
new Set(["src/actions/housekeeping-search"]),
|
||||||
@@ -923,6 +951,7 @@ const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
|
|||||||
"src/features/housekeeping/domains/content/route-handlers",
|
"src/features/housekeeping/domains/content/route-handlers",
|
||||||
"src/features/housekeeping/domains/economy/route-handlers",
|
"src/features/housekeeping/domains/economy/route-handlers",
|
||||||
"src/features/housekeeping/domains/hotel/route-handlers",
|
"src/features/housekeeping/domains/hotel/route-handlers",
|
||||||
|
"src/features/housekeeping/domains/operations/route-handlers",
|
||||||
"src/features/housekeeping/domains/people/route-handlers",
|
"src/features/housekeeping/domains/people/route-handlers",
|
||||||
"src/features/housekeeping/domains/system/route-handlers",
|
"src/features/housekeeping/domains/system/route-handlers",
|
||||||
]),
|
]),
|
||||||
@@ -1523,7 +1552,7 @@ describe("housekeeping foundation completion contracts", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates the real six-domain registry with People, Content, Economy, Hotel, and System routes enabled", () => {
|
it("creates the real six-domain registry with every domain route enabled", () => {
|
||||||
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
|
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
|
||||||
|
|
||||||
expect(registry.domains.map((domain) => domain.id)).toEqual([
|
expect(registry.domains.map((domain) => domain.id)).toEqual([
|
||||||
@@ -1536,14 +1565,9 @@ describe("housekeeping foundation completion contracts", () => {
|
|||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
registry.domains
|
registry.domains
|
||||||
.filter(
|
.find((domain) => domain.id === "operations")
|
||||||
(domain) =>
|
?.routes.map((route) => route.id),
|
||||||
!["people", "content", "economy", "hotel", "system"].includes(
|
).toEqual(OPERATIONS_ROUTES.map((route) => route.id));
|
||||||
domain.id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.every((domain) => domain.routes.length === 0),
|
|
||||||
).toBe(true);
|
|
||||||
expect(
|
expect(
|
||||||
registry.domains
|
registry.domains
|
||||||
.find((domain) => domain.id === "people")
|
.find((domain) => domain.id === "people")
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ const routeMocks = vi.hoisted(() => {
|
|||||||
"routes.hotel.studio.repair-icons": "HK::hotel-studio-repair-icons",
|
"routes.hotel.studio.repair-icons": "HK::hotel-studio-repair-icons",
|
||||||
"routes.hotel.studio.sync": "HK::hotel-studio-sync",
|
"routes.hotel.studio.sync": "HK::hotel-studio-sync",
|
||||||
"routes.hotel.studio.upload": "HK::hotel-studio-upload",
|
"routes.hotel.studio.upload": "HK::hotel-studio-upload",
|
||||||
|
"routes.operations.workspace": "HK::operations-workspace",
|
||||||
"states.empty.title": "Localized empty title",
|
"states.empty.title": "Localized empty title",
|
||||||
"states.empty.description": "Localized empty description",
|
"states.empty.description": "Localized empty description",
|
||||||
};
|
};
|
||||||
@@ -276,6 +277,10 @@ vi.mock("@/features/housekeeping/manifests", async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
vi.mock("@/features/housekeeping/route-handlers", () => ({
|
vi.mock("@/features/housekeeping/route-handlers", () => ({
|
||||||
HOUSEKEEPING_ROUTE_HANDLERS: [
|
HOUSEKEEPING_ROUTE_HANDLERS: [
|
||||||
|
{
|
||||||
|
routeId: "operations.workspace",
|
||||||
|
render: routeMocks.renderHousekeepingRoute,
|
||||||
|
},
|
||||||
{ routeId: "people.users", render: routeMocks.renderHousekeepingRoute },
|
{ routeId: "people.users", render: routeMocks.renderHousekeepingRoute },
|
||||||
{
|
{
|
||||||
routeId: "people.user-detail",
|
routeId: "people.user-detail",
|
||||||
@@ -617,24 +622,30 @@ describe("/ase-next preview gate", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("/ase-next first visible domain", () => {
|
describe("/ase-next operational home", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects an administrator to the first permitted registered route", async () => {
|
it("renders the Operations workspace directly for an administrator", async () => {
|
||||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
|
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
|
||||||
capabilityContext([PERMS.ADMIN_DASHBOARD, PERMS.USERS_VIEW]),
|
capabilityContext([PERMS.ADMIN_DASHBOARD, PERMS.USERS_VIEW]),
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(AdminNextPage()).rejects.toThrow(
|
const html = await renderRoute(AdminNextPage());
|
||||||
"NEXT_REDIRECT:/ase-next/people/users",
|
expect(html).toContain("Rendered operations.workspace");
|
||||||
|
expect(html).toContain("HK::operations-workspace");
|
||||||
|
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
match: expect.objectContaining({ routeId: "operations.workspace" }),
|
||||||
|
surface: "preview",
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/people/users");
|
expect(routeMocks.redirect).not.toHaveBeenCalled();
|
||||||
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
|
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
|
expect(routeMocks.getTranslations).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects a moderator with only an approved mod view capability to People", async () => {
|
it("redirects a moderator with only an approved mod view capability to People", async () => {
|
||||||
@@ -790,6 +801,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
|
|||||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledTimes(1);
|
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledTimes(1);
|
||||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
||||||
context,
|
context,
|
||||||
|
surface: "preview",
|
||||||
match: {
|
match: {
|
||||||
routeId: "people.users",
|
routeId: "people.users",
|
||||||
domain: "people",
|
domain: "people",
|
||||||
@@ -815,6 +827,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
|
|||||||
|
|
||||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
||||||
context,
|
context,
|
||||||
|
surface: "preview",
|
||||||
match: {
|
match: {
|
||||||
routeId: "people.user-detail",
|
routeId: "people.user-detail",
|
||||||
domain: "people",
|
domain: "people",
|
||||||
@@ -839,6 +852,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
|
|||||||
|
|
||||||
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
|
||||||
context,
|
context,
|
||||||
|
surface: "preview",
|
||||||
match: {
|
match: {
|
||||||
routeId: "people.literal-tool",
|
routeId: "people.literal-tool",
|
||||||
domain: "people",
|
domain: "people",
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import { HOTEL_INBOX_SOURCES } from "../domains/hotel/inbox";
|
|||||||
import { HOTEL_ROUTES } from "../domains/hotel/routes";
|
import { HOTEL_ROUTES } from "../domains/hotel/routes";
|
||||||
import { HOTEL_SEARCH_PROVIDERS } from "../domains/hotel/search";
|
import { HOTEL_SEARCH_PROVIDERS } from "../domains/hotel/search";
|
||||||
import { HOTEL_WIDGETS } from "../domains/hotel/widgets";
|
import { HOTEL_WIDGETS } from "../domains/hotel/widgets";
|
||||||
|
import { OPERATIONS_INBOX_SOURCES } from "../domains/operations/inbox";
|
||||||
|
import { OPERATIONS_ROUTES } from "../domains/operations/routes";
|
||||||
|
import { OPERATIONS_SEARCH_PROVIDERS } from "../domains/operations/search";
|
||||||
|
import { OPERATIONS_WIDGETS } from "../domains/operations/widgets";
|
||||||
import { PEOPLE_INBOX_SOURCES } from "../domains/people/inbox";
|
import { PEOPLE_INBOX_SOURCES } from "../domains/people/inbox";
|
||||||
import { PEOPLE_ROUTES } from "../domains/people/routes";
|
import { PEOPLE_ROUTES } from "../domains/people/routes";
|
||||||
import { PEOPLE_SEARCH_PROVIDERS } from "../domains/people/search";
|
import { PEOPLE_SEARCH_PROVIDERS } from "../domains/people/search";
|
||||||
@@ -380,7 +384,9 @@ describe("housekeeping registry", () => {
|
|||||||
descriptionKey: expected.descriptionKey,
|
descriptionKey: expected.descriptionKey,
|
||||||
});
|
});
|
||||||
expect(actual.routes).toEqual(
|
expect(actual.routes).toEqual(
|
||||||
expected.id === "people"
|
expected.id === "operations"
|
||||||
|
? OPERATIONS_ROUTES
|
||||||
|
: expected.id === "people"
|
||||||
? PEOPLE_ROUTES
|
? PEOPLE_ROUTES
|
||||||
: expected.id === "content"
|
: expected.id === "content"
|
||||||
? CONTENT_ROUTES
|
? CONTENT_ROUTES
|
||||||
@@ -393,7 +399,9 @@ describe("housekeeping registry", () => {
|
|||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
expect(actual.searchProviders).toEqual(
|
expect(actual.searchProviders).toEqual(
|
||||||
expected.id === "people"
|
expected.id === "operations"
|
||||||
|
? OPERATIONS_SEARCH_PROVIDERS
|
||||||
|
: expected.id === "people"
|
||||||
? PEOPLE_SEARCH_PROVIDERS
|
? PEOPLE_SEARCH_PROVIDERS
|
||||||
: expected.id === "content"
|
: expected.id === "content"
|
||||||
? CONTENT_SEARCH_PROVIDERS
|
? CONTENT_SEARCH_PROVIDERS
|
||||||
@@ -406,7 +414,9 @@ describe("housekeeping registry", () => {
|
|||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
expect(actual.inboxSources).toEqual(
|
expect(actual.inboxSources).toEqual(
|
||||||
expected.id === "people"
|
expected.id === "operations"
|
||||||
|
? OPERATIONS_INBOX_SOURCES
|
||||||
|
: expected.id === "people"
|
||||||
? PEOPLE_INBOX_SOURCES
|
? PEOPLE_INBOX_SOURCES
|
||||||
: expected.id === "content"
|
: expected.id === "content"
|
||||||
? CONTENT_INBOX_SOURCES
|
? CONTENT_INBOX_SOURCES
|
||||||
@@ -419,7 +429,9 @@ describe("housekeeping registry", () => {
|
|||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
expect(actual.widgets).toEqual(
|
expect(actual.widgets).toEqual(
|
||||||
expected.id === "people"
|
expected.id === "operations"
|
||||||
|
? OPERATIONS_WIDGETS
|
||||||
|
: expected.id === "people"
|
||||||
? PEOPLE_WIDGETS
|
? PEOPLE_WIDGETS
|
||||||
: expected.id === "content"
|
: expected.id === "content"
|
||||||
? CONTENT_WIDGETS
|
? CONTENT_WIDGETS
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ const {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("react", () => ({ cache: wrapCache }));
|
vi.mock("react", async (importOriginal) => ({
|
||||||
|
...(await importOriginal<typeof import("react")>()),
|
||||||
|
cache: wrapCache,
|
||||||
|
}));
|
||||||
vi.mock("next/navigation", () => ({ notFound, redirect }));
|
vi.mock("next/navigation", () => ({ notFound, redirect }));
|
||||||
vi.mock("next-intl/server", () => ({ getTranslations }));
|
vi.mock("next-intl/server", () => ({ getTranslations }));
|
||||||
vi.mock("@/lib/auth", () => ({ auth }));
|
vi.mock("@/lib/auth", () => ({ auth }));
|
||||||
@@ -74,17 +77,17 @@ function adminContext(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invokeRequestConsumers(expectedPageError = "NEXT_NOT_FOUND") {
|
async function invokeRequestConsumers() {
|
||||||
const shell = await AdminNextDomainLayout({
|
const shell = await AdminNextDomainLayout({
|
||||||
children: null,
|
children: null,
|
||||||
params: Promise.resolve({ domain: "operations" }),
|
params: Promise.resolve({ domain: "operations" }),
|
||||||
});
|
});
|
||||||
await expect(AdminNextPage()).rejects.toThrow(expectedPageError);
|
const page = await AdminNextPage();
|
||||||
const commandContext = await requireHousekeepingCapability(
|
const commandContext = await requireHousekeepingCapability(
|
||||||
anyCapability("admin.dashboard"),
|
anyCapability("admin.dashboard"),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { commandContext, shell };
|
return { commandContext, page, shell };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("getHousekeepingCapabilityContext", () => {
|
describe("getHousekeepingCapabilityContext", () => {
|
||||||
@@ -102,11 +105,10 @@ describe("getHousekeepingCapabilityContext", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("isolates real shell, page, and command preflight consumers between logical requests", async () => {
|
it("isolates real shell, page, and command preflight consumers between logical requests", async () => {
|
||||||
const first = await invokeRequestConsumers(
|
const first = await invokeRequestConsumers();
|
||||||
"NEXT_REDIRECT:/ase-next/people/users",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(first.shell).toBeDefined();
|
expect(first.shell).toBeDefined();
|
||||||
|
expect(first.page).toBeDefined();
|
||||||
expect(first.commandContext.actor).toEqual({
|
expect(first.commandContext.actor).toEqual({
|
||||||
id: 42,
|
id: 42,
|
||||||
username: "operator",
|
username: "operator",
|
||||||
@@ -123,6 +125,7 @@ describe("getHousekeepingCapabilityContext", () => {
|
|||||||
const second = await invokeRequestConsumers();
|
const second = await invokeRequestConsumers();
|
||||||
|
|
||||||
expect(second.shell).toBeDefined();
|
expect(second.shell).toBeDefined();
|
||||||
|
expect(second.page).toBeDefined();
|
||||||
expect(second.commandContext.actor).toEqual({
|
expect(second.commandContext.actor).toEqual({
|
||||||
id: 84,
|
id: 84,
|
||||||
username: "second-operator",
|
username: "second-operator",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import type { HousekeepingSurface } from "../contracts";
|
||||||
import type { HousekeepingNavigationDomain } from "../navigation";
|
import type { HousekeepingNavigationDomain } from "../navigation";
|
||||||
import { HousekeepingShell } from "./housekeeping-shell";
|
import { HousekeepingShell } from "./housekeeping-shell";
|
||||||
|
|
||||||
@@ -147,6 +148,7 @@ const componentBoundaryRules = [
|
|||||||
function renderShell(
|
function renderShell(
|
||||||
activeDomainId: HousekeepingNavigationDomain["id"] = "operations",
|
activeDomainId: HousekeepingNavigationDomain["id"] = "operations",
|
||||||
availableDomains: readonly HousekeepingNavigationDomain[] = domains,
|
availableDomains: readonly HousekeepingNavigationDomain[] = domains,
|
||||||
|
surface: HousekeepingSurface = "preview",
|
||||||
): string {
|
): string {
|
||||||
return renderToStaticMarkup(
|
return renderToStaticMarkup(
|
||||||
<HousekeepingShell
|
<HousekeepingShell
|
||||||
@@ -154,6 +156,7 @@ function renderShell(
|
|||||||
activeDomainId={activeDomainId}
|
activeDomainId={activeDomainId}
|
||||||
domains={availableDomains}
|
domains={availableDomains}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
|
surface={surface}
|
||||||
>
|
>
|
||||||
<p>Deck body</p>
|
<p>Deck body</p>
|
||||||
</HousekeepingShell>,
|
</HousekeepingShell>,
|
||||||
@@ -230,6 +233,13 @@ describe("HousekeepingShell", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("only marks the preview surface as preview", () => {
|
||||||
|
expect(renderShell()).toContain(">Preview<");
|
||||||
|
expect(renderShell("operations", domains, "canonical")).not.toContain(
|
||||||
|
">Preview<",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not throw or render contextual items when a valid active domain is absent", () => {
|
it("does not throw or render contextual items when a valid active domain is absent", () => {
|
||||||
const domainsWithoutSystem = domains.filter(
|
const domainsWithoutSystem = domains.filter(
|
||||||
(domain) => domain.id !== "system",
|
(domain) => domain.id !== "system",
|
||||||
|
|||||||
@@ -54,9 +54,11 @@ export function HousekeepingShell({
|
|||||||
</a>
|
</a>
|
||||||
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--admin-border)] bg-[var(--admin-surface)] px-4 py-3">
|
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--admin-border)] bg-[var(--admin-surface)] px-4 py-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{surface === "preview" ? (
|
||||||
<span className="rounded-full border border-[var(--admin-border)] px-2 py-1 text-xs text-[var(--admin-accent)]">
|
<span className="rounded-full border border-[var(--admin-border)] px-2 py-1 text-xs text-[var(--admin-accent)]">
|
||||||
{labels.preview}
|
{labels.preview}
|
||||||
</span>
|
</span>
|
||||||
|
) : null}
|
||||||
<OperatorSummary actor={actor} />
|
<OperatorSummary actor={actor} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { CONTENT_ROUTE_IDS } from "./domains/content/routes";
|
import { CONTENT_ROUTE_IDS } from "./domains/content/routes";
|
||||||
import { ECONOMY_ROUTE_IDS } from "./domains/economy/routes";
|
import { ECONOMY_ROUTE_IDS } from "./domains/economy/routes";
|
||||||
import { HOTEL_ROUTE_IDS } from "./domains/hotel/routes";
|
import { HOTEL_ROUTE_IDS } from "./domains/hotel/routes";
|
||||||
|
import { OPERATIONS_ROUTES } from "./domains/operations/routes";
|
||||||
import { PEOPLE_ROUTE_IDS } from "./domains/people/routes";
|
import { PEOPLE_ROUTE_IDS } from "./domains/people/routes";
|
||||||
import { SYSTEM_ROUTE_IDS } from "./domains/system/routes";
|
import { SYSTEM_ROUTE_IDS } from "./domains/system/routes";
|
||||||
import { createHousekeepingRegistry } from "./foundation/registry";
|
import { createHousekeepingRegistry } from "./foundation/registry";
|
||||||
@@ -21,6 +22,7 @@ describe("housekeeping route handlers", () => {
|
|||||||
expect(new Set(handlerIds).size).toBe(handlerIds.length);
|
expect(new Set(handlerIds).size).toBe(handlerIds.length);
|
||||||
expect([...handlerIds].sort()).toEqual([...routeIds].sort());
|
expect([...handlerIds].sort()).toEqual([...routeIds].sort());
|
||||||
expect(handlerIds).toEqual([
|
expect(handlerIds).toEqual([
|
||||||
|
...OPERATIONS_ROUTES.map((route) => route.id),
|
||||||
...PEOPLE_ROUTE_IDS,
|
...PEOPLE_ROUTE_IDS,
|
||||||
...CONTENT_ROUTE_IDS,
|
...CONTENT_ROUTE_IDS,
|
||||||
...ECONOMY_ROUTE_IDS,
|
...ECONOMY_ROUTE_IDS,
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ import type { ReactNode } from "react";
|
|||||||
import { CONTENT_ROUTE_HANDLERS } from "./domains/content/route-handlers";
|
import { CONTENT_ROUTE_HANDLERS } from "./domains/content/route-handlers";
|
||||||
import { ECONOMY_ROUTE_HANDLERS } from "./domains/economy/route-handlers";
|
import { ECONOMY_ROUTE_HANDLERS } from "./domains/economy/route-handlers";
|
||||||
import { HOTEL_ROUTE_HANDLERS } from "./domains/hotel/route-handlers";
|
import { HOTEL_ROUTE_HANDLERS } from "./domains/hotel/route-handlers";
|
||||||
|
import { OPERATIONS_ROUTE_HANDLERS } from "./domains/operations/route-handlers";
|
||||||
import { PEOPLE_ROUTE_HANDLERS } from "./domains/people/route-handlers";
|
import { PEOPLE_ROUTE_HANDLERS } from "./domains/people/route-handlers";
|
||||||
import { SYSTEM_ROUTE_HANDLERS } from "./domains/system/route-handlers";
|
import { SYSTEM_ROUTE_HANDLERS } from "./domains/system/route-handlers";
|
||||||
import type { HousekeepingCapabilityContext } from "./foundation/contracts";
|
import type {
|
||||||
|
HousekeepingCapabilityContext,
|
||||||
|
HousekeepingSurface,
|
||||||
|
} from "./foundation/contracts";
|
||||||
import type { HousekeepingRouteMatch } from "./foundation/routing/match-route";
|
import type { HousekeepingRouteMatch } from "./foundation/routing/match-route";
|
||||||
|
import type { HousekeepingPreferenceSaver } from "./foundation/shell/widget-settings";
|
||||||
|
|
||||||
export interface HousekeepingPageInput {
|
export interface HousekeepingPageInput {
|
||||||
readonly context: HousekeepingCapabilityContext;
|
readonly context: HousekeepingCapabilityContext;
|
||||||
@@ -13,6 +18,8 @@ export interface HousekeepingPageInput {
|
|||||||
readonly searchParams?: Readonly<
|
readonly searchParams?: Readonly<
|
||||||
Record<string, string | readonly string[] | undefined>
|
Record<string, string | readonly string[] | undefined>
|
||||||
>;
|
>;
|
||||||
|
readonly surface?: HousekeepingSurface;
|
||||||
|
readonly savePreferences?: HousekeepingPreferenceSaver;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HousekeepingRouteHandler {
|
export interface HousekeepingRouteHandler {
|
||||||
@@ -22,6 +29,7 @@ export interface HousekeepingRouteHandler {
|
|||||||
|
|
||||||
export const HOUSEKEEPING_ROUTE_HANDLERS: readonly HousekeepingRouteHandler[] =
|
export const HOUSEKEEPING_ROUTE_HANDLERS: readonly HousekeepingRouteHandler[] =
|
||||||
Object.freeze([
|
Object.freeze([
|
||||||
|
...OPERATIONS_ROUTE_HANDLERS,
|
||||||
...PEOPLE_ROUTE_HANDLERS,
|
...PEOPLE_ROUTE_HANDLERS,
|
||||||
...CONTENT_ROUTE_HANDLERS,
|
...CONTENT_ROUTE_HANDLERS,
|
||||||
...ECONOMY_ROUTE_HANDLERS,
|
...ECONOMY_ROUTE_HANDLERS,
|
||||||
|
|||||||
Reference in new issue
Block a user