fix(housekeeping): link only reachable preview routes
This commit is contained in:
1 parent
543607a80e
commit
d8fb8edff6
6 files changed
+216
-162
No files matched your search
@@ -4,9 +4,11 @@ import type { ReactNode } from "react";
|
|||||||
import { satisfiesCapability } from "@/features/housekeeping/foundation/capability-context";
|
import { satisfiesCapability } from "@/features/housekeeping/foundation/capability-context";
|
||||||
import { buildHousekeepingNavigation } from "@/features/housekeeping/foundation/navigation";
|
import { buildHousekeepingNavigation } from "@/features/housekeeping/foundation/navigation";
|
||||||
import { createHousekeepingRegistry } from "@/features/housekeeping/foundation/registry";
|
import { createHousekeepingRegistry } from "@/features/housekeeping/foundation/registry";
|
||||||
|
import { createHousekeepingRouteRuntime } from "@/features/housekeeping/foundation/routing/runtime";
|
||||||
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
|
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
|
||||||
import { HousekeepingShell } from "@/features/housekeeping/foundation/shell/housekeeping-shell";
|
import { HousekeepingShell } from "@/features/housekeeping/foundation/shell/housekeeping-shell";
|
||||||
import { HOUSEKEEPING_MANIFESTS } from "@/features/housekeeping/manifests";
|
import { HOUSEKEEPING_MANIFESTS } from "@/features/housekeeping/manifests";
|
||||||
|
import { HOUSEKEEPING_ROUTE_HANDLERS } from "@/features/housekeeping/route-handlers";
|
||||||
|
|
||||||
const MESSAGE_PREFIX = "pages.housekeeping.";
|
const MESSAGE_PREFIX = "pages.housekeeping.";
|
||||||
|
|
||||||
@@ -27,6 +29,10 @@ export default async function AdminNextDomainLayout({
|
|||||||
}) {
|
}) {
|
||||||
const { domain } = await params;
|
const { domain } = await params;
|
||||||
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
|
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
|
||||||
|
const runtime = createHousekeepingRouteRuntime(
|
||||||
|
registry,
|
||||||
|
HOUSEKEEPING_ROUTE_HANDLERS,
|
||||||
|
);
|
||||||
const activeDomain = registry.domains.find((entry) => entry.id === domain);
|
const activeDomain = registry.domains.find((entry) => entry.id === domain);
|
||||||
|
|
||||||
if (!activeDomain) notFound();
|
if (!activeDomain) notFound();
|
||||||
@@ -35,7 +41,7 @@ export default async function AdminNextDomainLayout({
|
|||||||
if (!satisfiesCapability(context, activeDomain.capability)) notFound();
|
if (!satisfiesCapability(context, activeDomain.capability)) notFound();
|
||||||
|
|
||||||
const translate = await getTranslations("pages.housekeeping");
|
const translate = await getTranslations("pages.housekeeping");
|
||||||
const navigation = buildHousekeepingNavigation(registry, context, (key) =>
|
const navigation = buildHousekeepingNavigation(runtime, context, (key) =>
|
||||||
translate(namespaceKey(key) as never),
|
translate(namespaceKey(key) as never),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { PERMS } from "@/lib/permission-slugs";
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
|
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
|
||||||
import { anyCapability, type HousekeepingCapabilityContext } from "./contracts";
|
import {
|
||||||
|
anyCapability,
|
||||||
|
type HousekeepingCapabilityContext,
|
||||||
|
type HousekeepingDomainManifest,
|
||||||
|
} from "./contracts";
|
||||||
import { buildHousekeepingNavigation } from "./navigation";
|
import { buildHousekeepingNavigation } from "./navigation";
|
||||||
import { createHousekeepingRegistry } from "./registry";
|
import { createHousekeepingRegistry } from "./registry";
|
||||||
|
import type { HousekeepingRouteHandler } from "./routing/route-handler";
|
||||||
|
import { createHousekeepingRouteRuntime } from "./routing/runtime";
|
||||||
|
|
||||||
const context = (
|
const capabilityContext = (
|
||||||
granted: readonly string[],
|
granted: readonly string[],
|
||||||
): HousekeepingCapabilityContext => ({
|
): HousekeepingCapabilityContext => ({
|
||||||
actor: { id: 42, username: "moderator", rank: 3 },
|
actor: { id: 42, username: "moderator", rank: 3 },
|
||||||
@@ -15,159 +21,168 @@ const context = (
|
|||||||
hasAll: (...slugs) => slugs.every((slug) => granted.includes(slug)),
|
hasAll: (...slugs) => slugs.every((slug) => granted.includes(slug)),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("housekeeping navigation", () => {
|
const peopleManifest = {
|
||||||
it("shows People to a moderator with a mod view capability while hiding Economy", () => {
|
id: "people",
|
||||||
const registry = createHousekeepingRegistry([
|
labelKey: "people.title",
|
||||||
{
|
descriptionKey: "people.description",
|
||||||
id: "people",
|
iconId: "users",
|
||||||
labelKey: "pages.housekeeping.domains.people.title",
|
previewHref: "/ase-next/people",
|
||||||
descriptionKey: "pages.housekeeping.domains.people.description",
|
capability: anyCapability(
|
||||||
iconId: "users",
|
PERMS.USERS_VIEW,
|
||||||
previewHref: "/ase-next/people",
|
PERMS.TICKETS_VIEW,
|
||||||
capability: anyCapability(PERMS.MOD_CFH_VIEW),
|
PERMS.BANS_VIEW,
|
||||||
landingRouteId: null,
|
),
|
||||||
routes: [],
|
landingRouteId: "people.users",
|
||||||
searchProviders: [],
|
routes: [
|
||||||
inboxSources: [],
|
{
|
||||||
widgets: [],
|
id: "people.users",
|
||||||
},
|
labelKey: "people.users",
|
||||||
{
|
href: "/ase-next/people/users",
|
||||||
id: "economy",
|
capability: anyCapability(PERMS.USERS_VIEW),
|
||||||
labelKey: "pages.housekeeping.domains.economy.title",
|
},
|
||||||
descriptionKey: "pages.housekeeping.domains.economy.description",
|
{
|
||||||
iconId: "gem",
|
id: "people.user-detail",
|
||||||
previewHref: "/ase-next/economy",
|
labelKey: "people.userDetail",
|
||||||
capability: anyCapability(PERMS.CATALOG_VIEW),
|
href: "/ase-next/people/users/:id",
|
||||||
landingRouteId: null,
|
capability: anyCapability(PERMS.USERS_VIEW),
|
||||||
routes: [],
|
},
|
||||||
searchProviders: [],
|
{
|
||||||
inboxSources: [],
|
id: "people.tickets",
|
||||||
widgets: [],
|
labelKey: "people.tickets",
|
||||||
},
|
href: "/ase-next/people/support/tickets",
|
||||||
]);
|
capability: anyCapability(PERMS.TICKETS_VIEW),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
searchProviders: [],
|
||||||
|
inboxSources: [],
|
||||||
|
widgets: [],
|
||||||
|
} satisfies HousekeepingDomainManifest;
|
||||||
|
|
||||||
|
const handler = (routeId: string): HousekeepingRouteHandler => ({
|
||||||
|
routeId,
|
||||||
|
render: async () => `Rendered ${routeId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const peopleRuntime = createHousekeepingRouteRuntime(
|
||||||
|
createHousekeepingRegistry([peopleManifest]),
|
||||||
|
peopleManifest.routes.map((route) => handler(route.id)),
|
||||||
|
);
|
||||||
|
const identityTranslate = (key: string) => key;
|
||||||
|
|
||||||
|
describe("housekeeping navigation", () => {
|
||||||
|
it("links a domain to its accessible handled landing route", () => {
|
||||||
const navigation = buildHousekeepingNavigation(
|
const navigation = buildHousekeepingNavigation(
|
||||||
registry,
|
peopleRuntime,
|
||||||
context([PERMS.MOD_CFH_VIEW]),
|
capabilityContext([PERMS.USERS_VIEW]),
|
||||||
(key) => key,
|
identityTranslate,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(navigation).toEqual([
|
expect(navigation).toEqual([
|
||||||
{
|
{
|
||||||
id: "people",
|
id: "people",
|
||||||
href: "/ase-next/people",
|
href: "/ase-next/people/users",
|
||||||
iconId: "users",
|
iconId: "users",
|
||||||
label: "pages.housekeeping.domains.people.title",
|
label: "people.title",
|
||||||
description: "pages.housekeeping.domains.people.description",
|
description: "people.description",
|
||||||
items: [],
|
items: [
|
||||||
|
{
|
||||||
|
id: "people.users",
|
||||||
|
href: "/ase-next/people/users",
|
||||||
|
label: "people.users",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("filters unauthorized domains and routes before translation", () => {
|
it("falls back to the first accessible handled route", () => {
|
||||||
const registry = createHousekeepingRegistry([
|
const navigation = buildHousekeepingNavigation(
|
||||||
{
|
peopleRuntime,
|
||||||
id: "people",
|
capabilityContext([PERMS.TICKETS_VIEW]),
|
||||||
labelKey: "people.title",
|
identityTranslate,
|
||||||
descriptionKey: "people.description",
|
);
|
||||||
iconId: "users",
|
|
||||||
previewHref: "/ase-next/people",
|
expect(navigation[0]?.href).toBe("/ase-next/people/support/tickets");
|
||||||
capability: anyCapability(PERMS.USERS_VIEW),
|
expect(navigation[0]?.items.map((item) => item.id)).toEqual([
|
||||||
landingRouteId: "users",
|
"people.tickets",
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
id: "users",
|
|
||||||
labelKey: "people.users",
|
|
||||||
href: "/ase-next/people/users",
|
|
||||||
capability: anyCapability(PERMS.USERS_VIEW),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "bans",
|
|
||||||
labelKey: "people.bans",
|
|
||||||
href: "/ase-next/people/bans",
|
|
||||||
capability: anyCapability(PERMS.BANS_VIEW),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
searchProviders: [],
|
|
||||||
inboxSources: [],
|
|
||||||
widgets: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "system",
|
|
||||||
labelKey: "system.title",
|
|
||||||
descriptionKey: "system.description",
|
|
||||||
iconId: "settings",
|
|
||||||
previewHref: "/ase-next/system",
|
|
||||||
capability: anyCapability(PERMS.SETTINGS_VIEW),
|
|
||||||
landingRouteId: null,
|
|
||||||
routes: [],
|
|
||||||
searchProviders: [],
|
|
||||||
inboxSources: [],
|
|
||||||
widgets: [],
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits a domain with no accessible handled concrete route", () => {
|
||||||
|
expect(
|
||||||
|
buildHousekeepingNavigation(
|
||||||
|
peopleRuntime,
|
||||||
|
capabilityContext([PERMS.BANS_VIEW]),
|
||||||
|
identityTranslate,
|
||||||
|
),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not expose dynamic route patterns as navigation links", () => {
|
||||||
|
const navigation = buildHousekeepingNavigation(
|
||||||
|
peopleRuntime,
|
||||||
|
capabilityContext([PERMS.USERS_VIEW]),
|
||||||
|
identityTranslate,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(navigation[0]?.items.map((item) => item.id)).not.toContain(
|
||||||
|
"people.user-detail",
|
||||||
|
);
|
||||||
|
expect(navigation[0]?.items.map((item) => item.href)).not.toContain(
|
||||||
|
"/ase-next/people/users/:id",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters inaccessible routes before translation", () => {
|
||||||
const translate = vi.fn((key: string) => `translated:${key}`);
|
const translate = vi.fn((key: string) => `translated:${key}`);
|
||||||
|
|
||||||
const navigation = buildHousekeepingNavigation(
|
buildHousekeepingNavigation(
|
||||||
registry,
|
peopleRuntime,
|
||||||
context([PERMS.USERS_VIEW]),
|
capabilityContext([PERMS.TICKETS_VIEW]),
|
||||||
translate,
|
translate,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(navigation).toEqual([
|
expect(translate).toHaveBeenCalledWith("people.title");
|
||||||
{
|
expect(translate).toHaveBeenCalledWith("people.description");
|
||||||
id: "people",
|
expect(translate).toHaveBeenCalledWith("people.tickets");
|
||||||
href: "/ase-next/people",
|
expect(translate).not.toHaveBeenCalledWith("people.users");
|
||||||
iconId: "users",
|
expect(translate).not.toHaveBeenCalledWith("people.userDetail");
|
||||||
label: "translated:people.title",
|
|
||||||
description: "translated:people.description",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
id: "users",
|
|
||||||
href: "/ase-next/people/users",
|
|
||||||
label: "translated:people.users",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(translate).not.toHaveBeenCalledWith("people.bans");
|
|
||||||
expect(translate).not.toHaveBeenCalledWith("system.title");
|
|
||||||
expect(translate).not.toHaveBeenCalledWith("system.description");
|
|
||||||
});
|
});
|
||||||
it("shows real domains to operators authorized by owned migration capabilities", () => {
|
|
||||||
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
|
it("keeps current empty manifests out of navigation until a vertical ships", () => {
|
||||||
const translate = (key: string) => key;
|
const emptyRuntime = createHousekeepingRouteRuntime(
|
||||||
|
createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
buildHousekeepingNavigation(
|
buildHousekeepingNavigation(
|
||||||
registry,
|
emptyRuntime,
|
||||||
context([PERMS.MOD_CFH_VIEW]),
|
capabilityContext([PERMS.MOD_CFH_VIEW]),
|
||||||
translate,
|
identityTranslate,
|
||||||
).map((domain) => domain.id),
|
),
|
||||||
).toContain("people");
|
).toEqual([]);
|
||||||
expect(
|
});
|
||||||
buildHousekeepingNavigation(
|
|
||||||
registry,
|
|
||||||
context([PERMS.MOD_CFH_VIEW]),
|
|
||||||
translate,
|
|
||||||
).map((domain) => domain.id),
|
|
||||||
).not.toContain("economy");
|
|
||||||
|
|
||||||
for (const [slug, expectedDomain] of [
|
it("projects only hrefs that the runtime can resolve", () => {
|
||||||
[PERMS.MOD_ACTIONS, "people"],
|
const capabilityProfiles = [
|
||||||
[PERMS.USERS_EDIT, "people"],
|
capabilityContext([PERMS.USERS_VIEW]),
|
||||||
[PERMS.SETTINGS_VIEW, "people"],
|
capabilityContext([PERMS.TICKETS_VIEW]),
|
||||||
[PERMS.NEWS_EDIT, "content"],
|
capabilityContext([PERMS.BANS_VIEW]),
|
||||||
[PERMS.SHOP_EDIT, "economy"],
|
capabilityContext([PERMS.USERS_VIEW, PERMS.TICKETS_VIEW]),
|
||||||
[PERMS.ROOMS_EDIT, "hotel"],
|
];
|
||||||
] as const) {
|
|
||||||
const visibleDomainIds = buildHousekeepingNavigation(
|
|
||||||
registry,
|
|
||||||
context([slug]),
|
|
||||||
translate,
|
|
||||||
).map((domain) => domain.id);
|
|
||||||
|
|
||||||
expect(visibleDomainIds, slug).toContain(expectedDomain);
|
for (const profile of capabilityProfiles) {
|
||||||
|
for (const domain of buildHousekeepingNavigation(
|
||||||
|
peopleRuntime,
|
||||||
|
profile,
|
||||||
|
identityTranslate,
|
||||||
|
)) {
|
||||||
|
expect(peopleRuntime.match(domain.href), domain.href).not.toBeNull();
|
||||||
|
for (const item of domain.items) {
|
||||||
|
expect(peopleRuntime.match(item.href), item.href).not.toBeNull();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3,37 +3,63 @@ import { satisfiesCapability } from "./capability-context";
|
|||||||
import type {
|
import type {
|
||||||
HousekeepingCapabilityContext,
|
HousekeepingCapabilityContext,
|
||||||
HousekeepingDomainManifest,
|
HousekeepingDomainManifest,
|
||||||
|
HousekeepingPreviewHref,
|
||||||
} from "./contracts";
|
} from "./contracts";
|
||||||
import type { HousekeepingRegistry } from "./registry";
|
import type { HousekeepingRouteRuntime } from "./routing/runtime";
|
||||||
|
|
||||||
export interface HousekeepingNavigationDomain {
|
export interface HousekeepingNavigationDomain {
|
||||||
id: HousekeepingDomainId;
|
id: HousekeepingDomainId;
|
||||||
href: string;
|
href: HousekeepingPreviewHref;
|
||||||
iconId: HousekeepingDomainManifest["iconId"];
|
iconId: HousekeepingDomainManifest["iconId"];
|
||||||
label: string;
|
label: string;
|
||||||
description: string;
|
description: string;
|
||||||
items: readonly { id: string; href: string; label: string }[];
|
items: readonly {
|
||||||
|
id: string;
|
||||||
|
href: HousekeepingPreviewHref;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildHousekeepingNavigation(
|
export function buildHousekeepingNavigation(
|
||||||
registry: HousekeepingRegistry,
|
runtime: HousekeepingRouteRuntime,
|
||||||
context: HousekeepingCapabilityContext,
|
context: HousekeepingCapabilityContext,
|
||||||
translate: (key: string) => string,
|
translate: (key: string) => string,
|
||||||
): readonly HousekeepingNavigationDomain[] {
|
): readonly HousekeepingNavigationDomain[] {
|
||||||
return registry.domains
|
return runtime.registry.domains.flatMap((domain) => {
|
||||||
.filter((domain) => satisfiesCapability(context, domain.capability))
|
if (!satisfiesCapability(context, domain.capability)) return [];
|
||||||
.map((domain) => ({
|
|
||||||
id: domain.id,
|
const items = domain.routes
|
||||||
href: domain.previewHref,
|
.filter(
|
||||||
iconId: domain.iconId,
|
(route) =>
|
||||||
label: translate(domain.labelKey),
|
runtime.handlers.has(route.id) &&
|
||||||
description: translate(domain.descriptionKey),
|
isConcreteNavigationHref(route.href) &&
|
||||||
items: domain.routes
|
satisfiesCapability(context, route.capability),
|
||||||
.filter((route) => satisfiesCapability(context, route.capability))
|
)
|
||||||
.map((route) => ({
|
.map((route) => ({
|
||||||
id: route.id,
|
id: route.id,
|
||||||
href: route.href,
|
href: route.href,
|
||||||
label: translate(route.labelKey),
|
label: translate(route.labelKey),
|
||||||
})),
|
}));
|
||||||
}));
|
|
||||||
|
if (items.length === 0) return [];
|
||||||
|
|
||||||
|
const landing =
|
||||||
|
items.find((item) => item.id === domain.landingRouteId) ?? items[0];
|
||||||
|
if (!landing) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: domain.id,
|
||||||
|
href: landing.href,
|
||||||
|
iconId: domain.iconId,
|
||||||
|
label: translate(domain.labelKey),
|
||||||
|
description: translate(domain.descriptionKey),
|
||||||
|
items,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConcreteNavigationHref(href: HousekeepingPreviewHref): boolean {
|
||||||
|
return !href.split("/").some((segment) => segment.startsWith(":"));
|
||||||
}
|
}
|
||||||
@@ -483,7 +483,7 @@ describe("/ase-next/[domain] layout", () => {
|
|||||||
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
|
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders localized People shell from one refreshed capability context", async () => {
|
it("renders the shell without exposing a domain that has no concrete routes", async () => {
|
||||||
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
|
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
|
||||||
capabilityContext([PERMS.MOD_CFH_VIEW]),
|
capabilityContext([PERMS.MOD_CFH_VIEW]),
|
||||||
);
|
);
|
||||||
@@ -502,9 +502,12 @@ describe("/ase-next/[domain] layout", () => {
|
|||||||
expect(html).toContain("HK::command-disabled");
|
expect(html).toContain("HK::command-disabled");
|
||||||
expect(html).toContain("HK::preview-badge");
|
expect(html).toContain("HK::preview-badge");
|
||||||
expect(html).toContain("HK::back-to-site");
|
expect(html).toContain("HK::back-to-site");
|
||||||
expect(html).toContain("HK::people-title");
|
expect(html).not.toContain("HK::people-title");
|
||||||
expect(html).toContain("People body");
|
expect(html).toContain("People body");
|
||||||
expect(html).not.toContain("Localized Economy");
|
expect(html).not.toContain("Localized Economy");
|
||||||
|
expect(routeMocks.translate).not.toHaveBeenCalledWith(
|
||||||
|
"domains.people.title",
|
||||||
|
);
|
||||||
expect(routeMocks.translate).not.toHaveBeenCalledWith(
|
expect(routeMocks.translate).not.toHaveBeenCalledWith(
|
||||||
"domains.economy.title",
|
"domains.economy.title",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const labels = {
|
|||||||
const domains: readonly HousekeepingNavigationDomain[] = [
|
const domains: readonly HousekeepingNavigationDomain[] = [
|
||||||
{
|
{
|
||||||
id: "operations",
|
id: "operations",
|
||||||
href: "/ase-next/operations",
|
href: "/ase-next/operations/queue",
|
||||||
iconId: "inbox",
|
iconId: "inbox",
|
||||||
label: "Operations",
|
label: "Operations",
|
||||||
description: "Manage operations",
|
description: "Manage operations",
|
||||||
@@ -27,7 +27,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "people",
|
id: "people",
|
||||||
href: "/ase-next/people",
|
href: "/ase-next/people/users",
|
||||||
iconId: "users",
|
iconId: "users",
|
||||||
label: "People",
|
label: "People",
|
||||||
description: "Manage people",
|
description: "Manage people",
|
||||||
@@ -35,7 +35,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "content",
|
id: "content",
|
||||||
href: "/ase-next/content",
|
href: "/ase-next/content/articles",
|
||||||
iconId: "file-text",
|
iconId: "file-text",
|
||||||
label: "Content",
|
label: "Content",
|
||||||
description: "Manage content",
|
description: "Manage content",
|
||||||
@@ -43,7 +43,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "economy",
|
id: "economy",
|
||||||
href: "/ase-next/economy",
|
href: "/ase-next/economy/catalog",
|
||||||
iconId: "gem",
|
iconId: "gem",
|
||||||
label: "Economy",
|
label: "Economy",
|
||||||
description: "Manage economy",
|
description: "Manage economy",
|
||||||
@@ -51,7 +51,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "hotel",
|
id: "hotel",
|
||||||
href: "/ase-next/hotel",
|
href: "/ase-next/hotel/rooms",
|
||||||
iconId: "hotel",
|
iconId: "hotel",
|
||||||
label: "Hotel",
|
label: "Hotel",
|
||||||
description: "Manage hotel",
|
description: "Manage hotel",
|
||||||
@@ -59,7 +59,7 @@ const domains: readonly HousekeepingNavigationDomain[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "system",
|
id: "system",
|
||||||
href: "/ase-next/system",
|
href: "/ase-next/system/settings",
|
||||||
iconId: "settings",
|
iconId: "settings",
|
||||||
label: "System",
|
label: "System",
|
||||||
description: "Manage system",
|
description: "Manage system",
|
||||||
@@ -196,7 +196,7 @@ describe("HousekeepingShell", () => {
|
|||||||
const activeAnchors = allAnchors.filter((anchor) =>
|
const activeAnchors = allAnchors.filter((anchor) =>
|
||||||
anchor.includes('aria-current="page"'),
|
anchor.includes('aria-current="page"'),
|
||||||
);
|
);
|
||||||
const peopleAnchor = anchorForHref(html, "/ase-next/people");
|
const peopleAnchor = anchorForHref(html, "/ase-next/people/users");
|
||||||
for (const iconClass of [
|
for (const iconClass of [
|
||||||
"lucide-inbox",
|
"lucide-inbox",
|
||||||
"lucide-users",
|
"lucide-users",
|
||||||
@@ -222,7 +222,7 @@ describe("HousekeepingShell", () => {
|
|||||||
);
|
);
|
||||||
expect(() => renderShell("system", domainsWithoutSystem)).not.toThrow();
|
expect(() => renderShell("system", domainsWithoutSystem)).not.toThrow();
|
||||||
const html = renderShell("system", domainsWithoutSystem);
|
const html = renderShell("system", domainsWithoutSystem);
|
||||||
expect(html).not.toContain('href="/ase-next/operations/queue"');
|
expect(html.match(/href="\/ase-next\/operations\/queue"/g)).toHaveLength(1);
|
||||||
expect(html).not.toContain(">Queue<");
|
expect(html).not.toContain(">Queue<");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import type { HousekeepingRouteHandler } from "./foundation/routing/route-handler";
|
||||||
|
|
||||||
|
export const HOUSEKEEPING_ROUTE_HANDLERS =
|
||||||
|
[] as const satisfies readonly HousekeepingRouteHandler[];
|
||||||
Reference in new issue
Block a user