feat(webui): expose stable product destinations
This commit is contained in:
@@ -1062,6 +1062,23 @@ available owner route. It emits `govoplan:product-surface-route-resolved` before
|
|||||||
the redirect so migration telemetry can observe alias use without making the
|
the redirect so migration telemetry can observe alias use without making the
|
||||||
technical module part of the ordinary label.
|
technical module part of the ordinary label.
|
||||||
|
|
||||||
|
The shell projects every authorized, View-visible owner route with a product
|
||||||
|
contribution into one stable product navigation item. The product label and
|
||||||
|
entry path replace package topology in the primary rail; every contributing
|
||||||
|
owner path still marks that item active. `All available tools` is a collapsed,
|
||||||
|
permission-derived catalogue built independently of the active View, so a
|
||||||
|
focused workflow cannot remove the explicit escape. It may reveal an
|
||||||
|
authorized owner route that a View omitted, but never an unauthorized route.
|
||||||
|
Navigation visibility preferences do not delete catalogue entries, and the
|
||||||
|
original owner routes remain compatible deep links.
|
||||||
|
|
||||||
|
The initial promoted destinations are `work.items` at `/work`,
|
||||||
|
`meetings.calendar` at `/agenda`, `communication.messages` at `/messages`
|
||||||
|
(with `/inbox` as an alias), and `records.files` at `/documents`. Their labels
|
||||||
|
and availability language are centralized in Core while Tasks, Calendar,
|
||||||
|
Mail/Postbox, and Files retain route, command, search, help, documentation,
|
||||||
|
authorization, and data ownership.
|
||||||
|
|
||||||
Use `ProductAvailabilityState` for unavailable and degraded outcomes. The
|
Use `ProductAvailabilityState` for unavailable and degraded outcomes. The
|
||||||
ordinary state explains the attempted outcome, consequence, recovery path and
|
ordinary state explains the attempted outcome, consequence, recovery path and
|
||||||
responsible role. Exact module, capability, provider and correlation values may
|
responsible role. Exact module, capability, provider and correlation values may
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-core"
|
name = "govoplan-core"
|
||||||
version = "0.1.43"
|
version = "0.1.44"
|
||||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"initialJs": {
|
"initialJs": {
|
||||||
"rawBytes": 524288,
|
"rawBytes": 524288,
|
||||||
"gzipBytes": 163840
|
"gzipBytes": 164128
|
||||||
},
|
},
|
||||||
"asyncChunk": {
|
"asyncChunk": {
|
||||||
"rawBytes": 393216,
|
"rawBytes": 393216,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { FileText, GitBranch, Inbox, Search, ShieldCheck } from "lucide-react";
|
import { CalendarDays, FileText, Folder, GitBranch, Inbox, ListChecks, Mail, Search, ShieldCheck } from "lucide-react";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
import FormInstancePage from "../../../govoplan-forms-runtime/webui/src/features/forms/FormInstancePage";
|
import FormInstancePage from "../../../govoplan-forms-runtime/webui/src/features/forms/FormInstancePage";
|
||||||
import FormsRuntimePage from "../../../govoplan-forms-runtime/webui/src/features/forms/FormsRuntimePage";
|
import FormsRuntimePage from "../../../govoplan-forms-runtime/webui/src/features/forms/FormsRuntimePage";
|
||||||
@@ -30,12 +30,23 @@ import WorkspaceLayout from "../src/components/WorkspaceLayout";
|
|||||||
import WorkspaceActionBar from "../src/components/WorkspaceActionBar";
|
import WorkspaceActionBar from "../src/components/WorkspaceActionBar";
|
||||||
import BreadcrumbBar from "../src/layout/BreadcrumbBar";
|
import BreadcrumbBar from "../src/layout/BreadcrumbBar";
|
||||||
import HelpMenu from "../src/layout/HelpMenu";
|
import HelpMenu from "../src/layout/HelpMenu";
|
||||||
|
import IconRail from "../src/layout/IconRail";
|
||||||
import { useGuardedNavigate } from "../src/components/UnsavedChangesGuard";
|
import { useGuardedNavigate } from "../src/components/UnsavedChangesGuard";
|
||||||
import {
|
import {
|
||||||
createQuickAccessLaunchContext,
|
createQuickAccessLaunchContext,
|
||||||
quickAccessLaunchState
|
quickAccessLaunchState
|
||||||
} from "../src/platform/launchContext";
|
} from "../src/platform/launchContext";
|
||||||
import type { ApiSettings, AuthInfo, EffectiveViewProjection, QuickAccessToolMetadata } from "../src/types";
|
import { projectProductNavigation } from "../src/platform/productSurfaces";
|
||||||
|
import type {
|
||||||
|
ApiSettings,
|
||||||
|
AuthInfo,
|
||||||
|
EffectiveViewProjection,
|
||||||
|
PlatformNavItem,
|
||||||
|
PlatformWebModule,
|
||||||
|
ProductAreaContribution,
|
||||||
|
ProductSurfaceContribution,
|
||||||
|
QuickAccessToolMetadata
|
||||||
|
} from "../src/types";
|
||||||
|
|
||||||
export default function ConformanceApp() {
|
export default function ConformanceApp() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -43,6 +54,10 @@ export default function ConformanceApp() {
|
|||||||
const [editorDirty, setEditorDirty] = useState(true);
|
const [editorDirty, setEditorDirty] = useState(true);
|
||||||
const [metricDrilldown, setMetricDrilldown] = useState("");
|
const [metricDrilldown, setMetricDrilldown] = useState("");
|
||||||
|
|
||||||
|
if (new URLSearchParams(location.search).has("product-navigation")) {
|
||||||
|
return <ProductNavigationScenario />;
|
||||||
|
}
|
||||||
|
|
||||||
if (location.pathname.startsWith("/forms/public/")) {
|
if (location.pathname.startsWith("/forms/public/")) {
|
||||||
return <PublicFormPage settings={CONFORMANCE_SETTINGS} auth={FORMS_RUNTIME_AUTH} />;
|
return <PublicFormPage settings={CONFORMANCE_SETTINGS} auth={FORMS_RUNTIME_AUTH} />;
|
||||||
}
|
}
|
||||||
@@ -185,6 +200,39 @@ export default function ConformanceApp() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProductNavigationScenario() {
|
||||||
|
const projection = useMemo(
|
||||||
|
() => projectProductNavigation(
|
||||||
|
PRODUCT_NAV_ITEMS,
|
||||||
|
PRODUCT_NAV_MODULES,
|
||||||
|
PRODUCT_NAV_AUTH
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div className="app-shell" data-conformance-id="product-navigation">
|
||||||
|
<IconRail
|
||||||
|
navItems={projection.primaryItems}
|
||||||
|
allToolItems={projection.allToolItems}
|
||||||
|
productAreas={PRODUCT_NAV_AREAS}
|
||||||
|
/>
|
||||||
|
<main className="main-area">
|
||||||
|
<PageLayout
|
||||||
|
archetype="overview"
|
||||||
|
mode="embedded"
|
||||||
|
title="Anwohnerparkausweis bearbeiten"
|
||||||
|
description="Die Navigation beschreibt Arbeit und Ergebnisse; technische Eigentümer bleiben nachvollziehbar erreichbar."
|
||||||
|
>
|
||||||
|
<StatePanel
|
||||||
|
title="Vorgang ist bereit"
|
||||||
|
description="Nutzen Sie Arbeit, Kalender, Nachrichten oder Dateien für den nächsten Schritt."
|
||||||
|
/>
|
||||||
|
</PageLayout>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function HelpConformanceScenario() {
|
function HelpConformanceScenario() {
|
||||||
return (
|
return (
|
||||||
<section className="conformance-section" aria-labelledby="help-heading">
|
<section className="conformance-section" aria-labelledby="help-heading">
|
||||||
@@ -303,6 +351,122 @@ const CONFORMANCE_AUTH = {
|
|||||||
groups_loaded: true
|
groups_loaded: true
|
||||||
} satisfies AuthInfo;
|
} satisfies AuthInfo;
|
||||||
|
|
||||||
|
const PRODUCT_NAV_AUTH = {
|
||||||
|
...CONFORMANCE_AUTH,
|
||||||
|
scopes: [
|
||||||
|
"tasks:item:read",
|
||||||
|
"calendar:event:read",
|
||||||
|
"mail:mailbox:read",
|
||||||
|
"postbox:message:read",
|
||||||
|
"files:file:read"
|
||||||
|
]
|
||||||
|
} satisfies AuthInfo;
|
||||||
|
|
||||||
|
const PRODUCT_NAV_ITEMS: PlatformNavItem[] = [
|
||||||
|
{ to: "/tasks", label: "Tasks", icon: ListChecks, surfaceId: "tasks.nav.tasks", anyOf: ["tasks:item:read"], order: 30 },
|
||||||
|
{ to: "/files", label: "Files", icon: Folder, surfaceId: "files.nav.files", anyOf: ["files:file:read"], order: 40 },
|
||||||
|
{ to: "/mail", label: "Mail", icon: Mail, surfaceId: "mail.nav.mail", anyOf: ["mail:mailbox:read"], order: 50 },
|
||||||
|
{ to: "/postbox", label: "Postbox", icon: Inbox, surfaceId: "postbox.nav.postbox", anyOf: ["postbox:message:read"], order: 51 },
|
||||||
|
{ to: "/calendar", label: "Calendar", icon: CalendarDays, surfaceId: "calendar.nav.calendar", anyOf: ["calendar:event:read"], order: 55 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRODUCT_NAV_AREAS: ProductAreaContribution[] = [
|
||||||
|
{ id: "work", moduleId: "tasks", label: "i18n:govoplan-core.product_area.work", iconName: "list-checks", surfaceIds: ["tasks.nav.tasks"], order: 10 },
|
||||||
|
{ id: "records-documents", moduleId: "files", label: "i18n:govoplan-core.product_area.records_documents", iconName: "folder", surfaceIds: ["files.nav.files"], order: 30 },
|
||||||
|
{ id: "communication", moduleId: "mail", label: "i18n:govoplan-core.product_area.communication", iconName: "mail", surfaceIds: ["mail.nav.mail", "postbox.nav.postbox"], order: 40 },
|
||||||
|
{ id: "meetings-decisions", moduleId: "calendar", label: "i18n:govoplan-core.product_area.meetings_decisions", iconName: "calendar", surfaceIds: ["calendar.nav.calendar"], order: 50 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRODUCT_NAV_MODULES: PlatformWebModule[] = [
|
||||||
|
productModule("tasks", productSurface({
|
||||||
|
id: "work.items",
|
||||||
|
moduleId: "tasks",
|
||||||
|
label: "i18n:govoplan-core.product_surface.work",
|
||||||
|
description: "i18n:govoplan-core.product_surface.work_description",
|
||||||
|
iconName: "list-checks",
|
||||||
|
entryPath: "/work",
|
||||||
|
routePath: "/tasks",
|
||||||
|
surfaceIds: ["tasks.nav.tasks"],
|
||||||
|
anyOf: ["tasks:item:read"]
|
||||||
|
})),
|
||||||
|
productModule("files", productSurface({
|
||||||
|
id: "records.files",
|
||||||
|
moduleId: "files",
|
||||||
|
label: "i18n:govoplan-core.product_surface.files",
|
||||||
|
description: "i18n:govoplan-core.product_surface.files_description",
|
||||||
|
iconName: "folder",
|
||||||
|
entryPath: "/documents",
|
||||||
|
routePath: "/files",
|
||||||
|
surfaceIds: ["files.nav.files"],
|
||||||
|
anyOf: ["files:file:read"]
|
||||||
|
})),
|
||||||
|
productModule("mail", productSurface({
|
||||||
|
id: "communication.messages",
|
||||||
|
moduleId: "mail",
|
||||||
|
label: "i18n:govoplan-core.product_surface.messages",
|
||||||
|
description: "i18n:govoplan-core.product_surface.messages_description",
|
||||||
|
iconName: "mail",
|
||||||
|
entryPath: "/messages",
|
||||||
|
routePath: "/mail",
|
||||||
|
surfaceIds: ["mail.nav.mail"],
|
||||||
|
anyOf: ["mail:mailbox:read"],
|
||||||
|
aliases: ["/inbox"]
|
||||||
|
})),
|
||||||
|
productModule("postbox", productSurface({
|
||||||
|
id: "communication.messages",
|
||||||
|
moduleId: "postbox",
|
||||||
|
label: "i18n:govoplan-core.product_surface.messages",
|
||||||
|
description: "i18n:govoplan-core.product_surface.messages_description",
|
||||||
|
iconName: "mail",
|
||||||
|
entryPath: "/messages",
|
||||||
|
routePath: "/postbox",
|
||||||
|
surfaceIds: ["postbox.nav.postbox"],
|
||||||
|
anyOf: ["postbox:message:read"],
|
||||||
|
aliases: ["/inbox"],
|
||||||
|
order: 20
|
||||||
|
})),
|
||||||
|
productModule("calendar", productSurface({
|
||||||
|
id: "meetings.calendar",
|
||||||
|
moduleId: "calendar",
|
||||||
|
label: "i18n:govoplan-core.product_surface.calendar",
|
||||||
|
description: "i18n:govoplan-core.product_surface.calendar_description",
|
||||||
|
iconName: "calendar",
|
||||||
|
entryPath: "/agenda",
|
||||||
|
routePath: "/calendar",
|
||||||
|
surfaceIds: ["calendar.nav.calendar"],
|
||||||
|
anyOf: ["calendar:event:read"]
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
function productModule(id: string, surface: ProductSurfaceContribution): PlatformWebModule {
|
||||||
|
return { id, label: id, version: "test", productSurfaces: [surface] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function productSurface(
|
||||||
|
partial: Pick<ProductSurfaceContribution,
|
||||||
|
"id" | "moduleId" | "label" | "description" | "iconName" | "entryPath" |
|
||||||
|
"routePath" | "surfaceIds" | "anyOf"> & Partial<ProductSurfaceContribution>
|
||||||
|
): ProductSurfaceContribution {
|
||||||
|
return {
|
||||||
|
contractVersion: "1",
|
||||||
|
presentations: ["task", "reader"],
|
||||||
|
capabilityIds: [],
|
||||||
|
searchSourceIds: [],
|
||||||
|
helpContextIds: [],
|
||||||
|
documentationTopicIds: [],
|
||||||
|
allOf: [],
|
||||||
|
aliases: [],
|
||||||
|
order: 10,
|
||||||
|
unavailable: {
|
||||||
|
reason: "authorization",
|
||||||
|
title: "Not available",
|
||||||
|
description: "The destination is not available for this responsibility.",
|
||||||
|
resolution: "Ask the access administrator to review the assignment."
|
||||||
|
},
|
||||||
|
...partial
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const FORMS_RUNTIME_AUTH = {
|
const FORMS_RUNTIME_AUTH = {
|
||||||
...CONFORMANCE_AUTH,
|
...CONFORMANCE_AUTH,
|
||||||
scopes: [
|
scopes: [
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
|
|||||||
import { BrowserRouter, Route, Routes } from "react-router";
|
import { BrowserRouter, Route, Routes } from "react-router";
|
||||||
import ConformanceApp from "./ConformanceApp";
|
import ConformanceApp from "./ConformanceApp";
|
||||||
import { generatedTranslations as formsRuntimeTranslations } from "../../../govoplan-forms-runtime/webui/src/i18n/generatedTranslations";
|
import { generatedTranslations as formsRuntimeTranslations } from "../../../govoplan-forms-runtime/webui/src/i18n/generatedTranslations";
|
||||||
|
import { productSurfaceTranslations } from "../src/index";
|
||||||
import { UnsavedChangesProvider } from "../src/components/UnsavedChangesGuard";
|
import { UnsavedChangesProvider } from "../src/components/UnsavedChangesGuard";
|
||||||
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
||||||
import { PlatformModulesProvider } from "../src/platform/ModuleContext";
|
import { PlatformModulesProvider } from "../src/platform/ModuleContext";
|
||||||
@@ -39,7 +40,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<PlatformModulesProvider modules={CONFORMANCE_MODULES}>
|
<PlatformModulesProvider modules={CONFORMANCE_MODULES}>
|
||||||
<PlatformLanguageProvider
|
<PlatformLanguageProvider
|
||||||
preferredLanguageCode="de"
|
preferredLanguageCode="de"
|
||||||
moduleTranslations={[formsRuntimeTranslations]}>
|
moduleTranslations={[formsRuntimeTranslations, productSurfaceTranslations]}>
|
||||||
<UnsavedChangesProvider>
|
<UnsavedChangesProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/forms/public/:publicId" element={<ConformanceApp />} />
|
<Route path="/forms/public/:publicId" element={<ConformanceApp />} />
|
||||||
|
|||||||
@@ -295,6 +295,33 @@ test("View focus has a deliberate permission-derived all-tools escape", async ({
|
|||||||
await expect(page.getByRole("button", { name: "Messages" })).toHaveCount(0);
|
await expect(page.getByRole("button", { name: "Messages" })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("product navigation hides package topology behind stable bilingual destinations", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1280, height: 900 });
|
||||||
|
await page.goto("/?theme=light&product-navigation=1");
|
||||||
|
await page.getByRole("button", { name: "Expand navigation" }).click();
|
||||||
|
|
||||||
|
const primary = page.locator(".icon-nav > .icon-nav-group");
|
||||||
|
await expect(primary.getByRole("link")).toHaveText([
|
||||||
|
"Arbeit",
|
||||||
|
"Dateien",
|
||||||
|
"Nachrichten",
|
||||||
|
"Kalender"
|
||||||
|
]);
|
||||||
|
await expect(primary.getByRole("link", { name: /Tasks|Files|Mail|Postbox|Calendar/ })).toHaveCount(0);
|
||||||
|
|
||||||
|
const allTools = page.locator("[data-product-navigation='all-tools']");
|
||||||
|
await expect(allTools.getByText("Alle verfügbaren Werkzeuge", { exact: true })).toBeVisible();
|
||||||
|
await allTools.locator("summary").click();
|
||||||
|
await expect(allTools.getByRole("link")).toHaveText([
|
||||||
|
"Tasks",
|
||||||
|
"Files",
|
||||||
|
"Mail",
|
||||||
|
"Postbox",
|
||||||
|
"Calendar"
|
||||||
|
]);
|
||||||
|
await expectNoAccessibilityViolations(page);
|
||||||
|
});
|
||||||
|
|
||||||
test("a stale View focus falls back safely in a sparse optional-module catalogue", async ({ page }) => {
|
test("a stale View focus falls back safely in a sparse optional-module catalogue", async ({ page }) => {
|
||||||
await page.route("**/api/v1/quick-access/effective*", async (route) => {
|
await page.route("**/api/v1/quick-access/effective*", async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.43",
|
"version": "0.1.44",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.43",
|
"version": "0.1.44",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.43",
|
"version": "0.1.44",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.43",
|
"version": "0.1.44",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.24",
|
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.24",
|
||||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22",
|
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22",
|
||||||
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.20",
|
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.20",
|
||||||
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.22",
|
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.23",
|
||||||
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.27",
|
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.27",
|
||||||
"@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
|
"@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
|
||||||
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.20",
|
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.20",
|
||||||
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.22",
|
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.22",
|
||||||
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.24",
|
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.25",
|
||||||
"@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20",
|
"@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20",
|
||||||
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.24",
|
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.24",
|
||||||
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.26",
|
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.26",
|
||||||
@@ -805,10 +805,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/calendar-webui": {
|
"node_modules/@govoplan/calendar-webui": {
|
||||||
"version": "0.1.22",
|
"version": "0.1.23",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#3792e9ab8a10ff6e301c59df7a3fa47d5ab14952",
|
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#3e5fc05ca3728464131067d1fa1bc25befeae939",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.44",
|
||||||
"@vitejs/plugin-react": "^5.2.0",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
@@ -894,10 +894,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/files-webui": {
|
"node_modules/@govoplan/files-webui": {
|
||||||
"version": "0.1.24",
|
"version": "0.1.25",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#6176e9f40e070387f03c15d10b3b3d2f132508b5",
|
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#2baa8f265707a4c3dd6bfaba78d86ac7a0edc7bb",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.44",
|
||||||
"@vitejs/plugin-react": "^5.2.0",
|
"@vitejs/plugin-react": "^5.2.0",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
@@ -2027,9 +2027,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.11.18",
|
"version": "2.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz",
|
||||||
"integrity": "sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==",
|
"integrity": "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.cjs"
|
"baseline-browser-mapping": "dist/cli.cjs"
|
||||||
@@ -2349,9 +2349,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
|
||||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|||||||
+5
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.43",
|
"version": "0.1.44",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -22,6 +22,10 @@
|
|||||||
"./wysiwyg": {
|
"./wysiwyg": {
|
||||||
"types": "./src/wysiwyg.ts",
|
"types": "./src/wysiwyg.ts",
|
||||||
"import": "./src/wysiwyg.ts"
|
"import": "./src/wysiwyg.ts"
|
||||||
|
},
|
||||||
|
"./outcome-product-surface-translations": {
|
||||||
|
"types": "./src/i18n/outcomeProductSurfaceTranslations.ts",
|
||||||
|
"import": "./src/i18n/outcomeProductSurfaceTranslations.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.43",
|
"version": "0.1.44",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.24",
|
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.24",
|
||||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22",
|
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22",
|
||||||
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.20",
|
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.20",
|
||||||
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.22",
|
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.23",
|
||||||
"@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
|
"@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
|
||||||
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.20",
|
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.20",
|
||||||
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.22",
|
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.22",
|
||||||
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.24",
|
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.25",
|
||||||
"@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20",
|
"@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20",
|
||||||
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.24",
|
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.24",
|
||||||
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.26",
|
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.26",
|
||||||
|
|||||||
+6
-2
@@ -8,7 +8,7 @@ import AppShell from "./layout/AppShell";
|
|||||||
import PublicLandingPage from "./features/auth/PublicLandingPage";
|
import PublicLandingPage from "./features/auth/PublicLandingPage";
|
||||||
import LoginModal from "./features/auth/LoginModal";
|
import LoginModal from "./features/auth/LoginModal";
|
||||||
import { PermissionBoundary } from "./components/AccessBoundary";
|
import { PermissionBoundary } from "./components/AccessBoundary";
|
||||||
import { firstAccessibleRoute, loadInstalledPublicWebModules, loadInstalledWebModules, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, routeContributionsForModules, uiCapability } from "./platform/modules";
|
import { configurableNavigationItemsForModules, firstAccessibleRoute, loadInstalledPublicWebModules, loadInstalledWebModules, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, routeContributionsForModules, uiCapability } from "./platform/modules";
|
||||||
import { PlatformModulesProvider } from "./platform/ModuleContext";
|
import { PlatformModulesProvider } from "./platform/ModuleContext";
|
||||||
import { PlatformViewProvider } from "./platform/ViewContext";
|
import { PlatformViewProvider } from "./platform/ViewContext";
|
||||||
import { PlatformTemporalProvider } from "./platform/TemporalContext";
|
import { PlatformTemporalProvider } from "./platform/TemporalContext";
|
||||||
@@ -73,6 +73,10 @@ export default function App() {
|
|||||||
() => navItemsForModules(webModules, viewProjection),
|
() => navItemsForModules(webModules, viewProjection),
|
||||||
[viewProjection, webModules]
|
[viewProjection, webModules]
|
||||||
);
|
);
|
||||||
|
const allToolItems = useMemo(
|
||||||
|
() => configurableNavigationItemsForModules(webModules),
|
||||||
|
[webModules]
|
||||||
|
);
|
||||||
const moduleRoutes = useMemo(() => routeContributionsForModules(webModules), [webModules]);
|
const moduleRoutes = useMemo(() => routeContributionsForModules(webModules), [webModules]);
|
||||||
const publicRoutes = useMemo(() => publicRouteContributionsForModules(publicWebModules), [publicWebModules]);
|
const publicRoutes = useMemo(() => publicRouteContributionsForModules(publicWebModules), [publicWebModules]);
|
||||||
const contextModules = auth ? webModules : publicWebModules;
|
const contextModules = auth ? webModules : publicWebModules;
|
||||||
@@ -550,7 +554,7 @@ export default function App() {
|
|||||||
<PlatformViewProvider modules={webModules} projection={viewProjection}>
|
<PlatformViewProvider modules={webModules} projection={viewProjection}>
|
||||||
<PlatformActiveObjectProvider>
|
<PlatformActiveObjectProvider>
|
||||||
<UnsavedChangesProvider>
|
<UnsavedChangesProvider>
|
||||||
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
|
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} allToolItems={allToolItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
|
||||||
<ModuleLoadBoundary resetKey={`${location.pathname}:${temporalRevision}`} loading={webModulesLoading}>
|
<ModuleLoadBoundary resetKey={`${location.pathname}:${temporalRevision}`} loading={webModulesLoading}>
|
||||||
<Routes key={`${(auth.active_tenant ?? auth.tenant).id}:${temporalRevision}`}>
|
<Routes key={`${(auth.active_tenant ?? auth.tenant).id}:${temporalRevision}`}>
|
||||||
<Route path="/" element={<Navigate to={defaultRoute} replace />} />
|
<Route path="/" element={<Navigate to={defaultRoute} replace />} />
|
||||||
|
|||||||
@@ -43,10 +43,7 @@ export const DEFAULT_AVAILABLE_LANGUAGES: PlatformLanguage[] = [
|
|||||||
{ code: "en", label: "i18n:govoplan-core.english.649df08a", nativeLabel: "i18n:govoplan-core.language_native_english" }];
|
{ code: "en", label: "i18n:govoplan-core.english.649df08a", nativeLabel: "i18n:govoplan-core.language_native_english" }];
|
||||||
|
|
||||||
|
|
||||||
export const DEFAULT_TRANSLATIONS: PlatformTranslations = {
|
export const DEFAULT_TRANSLATIONS: PlatformTranslations = generatedTranslations;
|
||||||
en: generatedTranslations.en,
|
|
||||||
de: generatedTranslations.de
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlatformLanguageContext = createContext<PlatformLanguageContextValue | null>(null);
|
const PlatformLanguageContext = createContext<PlatformLanguageContextValue | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-core.append_target_folder.0aaacc0c": "Append target folder",
|
"i18n:govoplan-core.append_target_folder.0aaacc0c": "Append target folder",
|
||||||
"i18n:govoplan-core.application_notices": "Application notices",
|
"i18n:govoplan-core.application_notices": "Application notices",
|
||||||
"i18n:govoplan-core.more_tools": "More tools",
|
"i18n:govoplan-core.more_tools": "More tools",
|
||||||
|
"i18n:govoplan-core.all_available_tools": "All available tools",
|
||||||
"i18n:govoplan-core.product_area.work": "Work",
|
"i18n:govoplan-core.product_area.work": "Work",
|
||||||
"i18n:govoplan-core.product_area.services_cases": "Services and cases",
|
"i18n:govoplan-core.product_area.services_cases": "Services and cases",
|
||||||
"i18n:govoplan-core.product_area.communication": "Communication",
|
"i18n:govoplan-core.product_area.communication": "Communication",
|
||||||
@@ -874,6 +875,7 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-core.append_target_folder.0aaacc0c": "Append target folder",
|
"i18n:govoplan-core.append_target_folder.0aaacc0c": "Append target folder",
|
||||||
"i18n:govoplan-core.application_notices": "Anwendungshinweise",
|
"i18n:govoplan-core.application_notices": "Anwendungshinweise",
|
||||||
"i18n:govoplan-core.more_tools": "Weitere Werkzeuge",
|
"i18n:govoplan-core.more_tools": "Weitere Werkzeuge",
|
||||||
|
"i18n:govoplan-core.all_available_tools": "Alle verfügbaren Werkzeuge",
|
||||||
"i18n:govoplan-core.product_area.work": "Arbeit",
|
"i18n:govoplan-core.product_area.work": "Arbeit",
|
||||||
"i18n:govoplan-core.product_area.services_cases": "Leistungen und Vorgänge",
|
"i18n:govoplan-core.product_area.services_cases": "Leistungen und Vorgänge",
|
||||||
"i18n:govoplan-core.product_area.communication": "Kommunikation",
|
"i18n:govoplan-core.product_area.communication": "Kommunikation",
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { PlatformTranslations } from "../types";
|
||||||
|
|
||||||
|
export const generatedTranslations = {
|
||||||
|
en: {
|
||||||
|
"i18n:govoplan-core.product_surface.work": "Work",
|
||||||
|
"i18n:govoplan-core.product_surface.work_description": "Review and resume authorized work without navigating by package ownership.",
|
||||||
|
"i18n:govoplan-core.product_surface.calendar": "Calendar",
|
||||||
|
"i18n:govoplan-core.product_surface.calendar_description": "Plan and review authorized events through one stable calendar destination.",
|
||||||
|
"i18n:govoplan-core.product_surface.files": "Files",
|
||||||
|
"i18n:govoplan-core.product_surface.files_description": "Find, select, and manage authorized files without exposing their storage implementation.",
|
||||||
|
"i18n:govoplan-core.product_surface.unavailable": "Destination is unavailable",
|
||||||
|
"i18n:govoplan-core.product_surface.unavailable_description": "No product destination is available for your current responsibility and permissions.",
|
||||||
|
"i18n:govoplan-core.product_surface.unavailable_resolution": "Ask the responsible access administrator to review your assignment or permissions.",
|
||||||
|
"i18n:govoplan-core.access_administrator": "Access administrator"
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
"i18n:govoplan-core.product_surface.work": "Arbeit",
|
||||||
|
"i18n:govoplan-core.product_surface.work_description": "Berechtigte Arbeit prüfen und fortsetzen, ohne nach Paketzuständigkeit zu navigieren.",
|
||||||
|
"i18n:govoplan-core.product_surface.calendar": "Kalender",
|
||||||
|
"i18n:govoplan-core.product_surface.calendar_description": "Berechtigte Termine über ein stabiles Kalenderziel planen und prüfen.",
|
||||||
|
"i18n:govoplan-core.product_surface.files": "Dateien",
|
||||||
|
"i18n:govoplan-core.product_surface.files_description": "Berechtigte Dateien finden, auswählen und verwalten, ohne ihre Speicherimplementierung offenzulegen.",
|
||||||
|
"i18n:govoplan-core.product_surface.unavailable": "Das Produktziel ist nicht verfügbar",
|
||||||
|
"i18n:govoplan-core.product_surface.unavailable_description": "Für Ihre aktuelle Verantwortung und Berechtigungen ist kein Produktziel verfügbar.",
|
||||||
|
"i18n:govoplan-core.product_surface.unavailable_resolution": "Bitten Sie die zuständige Zugriffsadministration, Ihre Zuordnung oder Berechtigungen zu prüfen.",
|
||||||
|
"i18n:govoplan-core.access_administrator": "Zugriffsadministration"
|
||||||
|
}
|
||||||
|
} satisfies PlatformTranslations;
|
||||||
+3
-1
@@ -33,7 +33,9 @@ export * from "./platform/moduleEvents";
|
|||||||
export * from "./platform/ViewContext";
|
export * from "./platform/ViewContext";
|
||||||
export * from "./platform/views";
|
export * from "./platform/views";
|
||||||
export * from "./platform/productSurfaces";
|
export * from "./platform/productSurfaces";
|
||||||
export { generatedTranslations as messagesProductSurfaceTranslations } from "./i18n/productSurfaceTranslations";
|
export { productSurfaceTranslations } from "./productSurfaceTranslations";
|
||||||
|
export { generatedTranslations as messagesProductSurfaceTranslations } from "./i18n/messagesProductSurfaceTranslations";
|
||||||
|
export { generatedTranslations as outcomeProductSurfaceTranslations } from "./i18n/outcomeProductSurfaceTranslations";
|
||||||
export * from "./platform/temporal";
|
export * from "./platform/temporal";
|
||||||
export * from "./platform/TemporalContext";
|
export * from "./platform/TemporalContext";
|
||||||
export * from "./platform/ActiveObjectContext";
|
export * from "./platform/ActiveObjectContext";
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Settings } from "lucide-react";
|
||||||
|
import { NavLink, useLocation } from "react-router";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
|
import { useGuardedNavigate } from "../components/UnsavedChangesGuard";
|
||||||
|
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
||||||
|
import type { PlatformNavItem } from "../types";
|
||||||
|
|
||||||
|
export default function AllToolsNavigation({
|
||||||
|
items,
|
||||||
|
rememberedTargets
|
||||||
|
}: {
|
||||||
|
items: PlatformNavItem[];
|
||||||
|
rememberedTargets: Record<string, string>;
|
||||||
|
}) {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useGuardedNavigate();
|
||||||
|
const { translateText } = usePlatformLanguage();
|
||||||
|
|
||||||
|
function handleClick(event: MouseEvent<HTMLAnchorElement>, target: string) {
|
||||||
|
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return;
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details className="icon-nav-all-tools" data-product-navigation="all-tools">
|
||||||
|
<summary
|
||||||
|
className="icon-nav-all-tools-summary"
|
||||||
|
title={translateText("i18n:govoplan-core.all_available_tools")}
|
||||||
|
>
|
||||||
|
<Settings size={20} aria-hidden="true" />
|
||||||
|
<span className="icon-nav-label">
|
||||||
|
{translateText("i18n:govoplan-core.all_available_tools")}
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div className="icon-nav-all-tools-items">
|
||||||
|
{items.map(({ to, label, icon: Icon }) => {
|
||||||
|
const target = rememberedTargets[to] ?? to;
|
||||||
|
const renderedLabel = translateText(label);
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={to}
|
||||||
|
to={target}
|
||||||
|
className={`icon-nav-item ${pathActive(location.pathname, to) ? "active" : ""}`}
|
||||||
|
title={renderedLabel}
|
||||||
|
onClick={(event) => handleClick(event, target)}
|
||||||
|
>
|
||||||
|
{Icon ? <Icon size={20} /> : <span className="icon-nav-fallback">{renderedLabel.slice(0, 1)}</span>}
|
||||||
|
<span className="icon-nav-label">{renderedLabel}</span>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathActive(pathname: string, root: string): boolean {
|
||||||
|
return pathname === root || pathname.startsWith(`${root}/`);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import { useActiveObject } from "../platform/ActiveObjectContext";
|
|||||||
import { createQuickAccessLaunchContext } from "../platform/launchContext";
|
import { createQuickAccessLaunchContext } from "../platform/launchContext";
|
||||||
import { isViewSurfaceVisible } from "../platform/views";
|
import { isViewSurfaceVisible } from "../platform/views";
|
||||||
import { hasAnyScope, hasScope } from "../utils/permissions";
|
import { hasAnyScope, hasScope } from "../utils/permissions";
|
||||||
|
import { projectProductNavigation } from "../platform/productSurfaces";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -20,6 +21,7 @@ type Props = {
|
|||||||
onAuthChange: (auth: AuthUpdate | null, accessToken?: string) => void;
|
onAuthChange: (auth: AuthUpdate | null, accessToken?: string) => void;
|
||||||
publicMode?: boolean;
|
publicMode?: boolean;
|
||||||
navItems?: PlatformNavItem[];
|
navItems?: PlatformNavItem[];
|
||||||
|
allToolItems?: PlatformNavItem[];
|
||||||
maintenanceMode?: { enabled: boolean; message?: string | null };
|
maintenanceMode?: { enabled: boolean; message?: string | null };
|
||||||
backendReachable?: boolean;
|
backendReachable?: boolean;
|
||||||
};
|
};
|
||||||
@@ -32,6 +34,7 @@ export default function AppShell({
|
|||||||
onAuthChange,
|
onAuthChange,
|
||||||
publicMode = false,
|
publicMode = false,
|
||||||
navItems = [],
|
navItems = [],
|
||||||
|
allToolItems = navItems,
|
||||||
maintenanceMode,
|
maintenanceMode,
|
||||||
backendReachable = true
|
backendReachable = true
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@@ -68,11 +71,15 @@ export default function AppShell({
|
|||||||
() => modules.flatMap((module) => module.productAreas ?? []),
|
() => modules.flatMap((module) => module.productAreas ?? []),
|
||||||
[modules]
|
[modules]
|
||||||
);
|
);
|
||||||
|
const productNavigation = useMemo(
|
||||||
|
() => projectProductNavigation(navItems, modules, auth, projection, allToolItems),
|
||||||
|
[allToolItems, auth, modules, navItems, projection]
|
||||||
|
);
|
||||||
|
|
||||||
if (publicMode) {
|
if (publicMode) {
|
||||||
return (
|
return (
|
||||||
<div className="app-shell public-shell">
|
<div className="app-shell public-shell">
|
||||||
<IconRail compact auth={auth} navItems={navItems} />
|
<IconRail compact navItems={navItems} />
|
||||||
<div className="app-main public-main">
|
<div className="app-main public-main">
|
||||||
<Titlebar settings={settings} auth={auth} onSettingsChange={onSettingsChange} onAuthChange={onAuthChange} maintenanceMode={maintenanceMode} backendReachable={backendReachable} />
|
<Titlebar settings={settings} auth={auth} onSettingsChange={onSettingsChange} onAuthChange={onAuthChange} maintenanceMode={maintenanceMode} backendReachable={backendReachable} />
|
||||||
<main className="public-content">{children}</main>
|
<main className="public-content">{children}</main>
|
||||||
@@ -84,8 +91,8 @@ export default function AppShell({
|
|||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<IconRail
|
<IconRail
|
||||||
auth={auth}
|
navItems={productNavigation.primaryItems}
|
||||||
navItems={navItems}
|
allToolItems={productNavigation.allToolItems}
|
||||||
productAreas={productAreas}
|
productAreas={productAreas}
|
||||||
presentation={projection?.presentation}
|
presentation={projection?.presentation}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,45 +1,35 @@
|
|||||||
import { PanelLeftClose, PanelLeftOpen, Settings } from "lucide-react";
|
import { PanelLeftClose, PanelLeftOpen, Settings } from "lucide-react";
|
||||||
import { NavLink, useLocation } from "react-router";
|
import { NavLink, useLocation } from "react-router";
|
||||||
import { useEffect, useMemo, useState, type MouseEvent } from "react";
|
import { lazy, Suspense, useEffect, useMemo, useState, type MouseEvent } from "react";
|
||||||
import type {
|
import type {
|
||||||
AuthInfo,
|
|
||||||
PlatformNavItem,
|
PlatformNavItem,
|
||||||
ProductAreaContribution,
|
ProductAreaContribution,
|
||||||
ViewPresentation
|
ViewPresentation
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { hasAnyScope, hasScope } from "../utils/permissions";
|
|
||||||
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
||||||
import { useGuardedNavigate } from "../components/UnsavedChangesGuard";
|
import { useGuardedNavigate } from "../components/UnsavedChangesGuard";
|
||||||
import { groupNavigationItems } from "../platform/productAreas";
|
import { groupNavigationItems } from "../platform/productAreas";
|
||||||
|
|
||||||
const MODULE_NAV_STORAGE_KEY = "govoplan.lastModuleNav";
|
const MODULE_NAV_STORAGE_KEY = "govoplan.lastModuleNav";
|
||||||
const RAIL_EXPANDED_STORAGE_KEY = "govoplan.iconRailExpanded";
|
const RAIL_EXPANDED_STORAGE_KEY = "govoplan.iconRailExpanded";
|
||||||
|
const AllToolsNavigation = lazy(() => import("./AllToolsNavigation"));
|
||||||
function visibleNavItems(auth: AuthInfo | null | undefined, navItems: PlatformNavItem[]): PlatformNavItem[] {
|
|
||||||
return [...navItems].
|
|
||||||
sort((left, right) => (left.order ?? 100) - (right.order ?? 100)).
|
|
||||||
filter((item) => {
|
|
||||||
if (item.allOf?.length && !item.allOf.every((scope) => hasScope(auth, scope))) return false;
|
|
||||||
if (item.anyOf?.length && !hasAnyScope(auth, item.anyOf)) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function IconRail({
|
export default function IconRail({
|
||||||
compact = false,
|
compact = false,
|
||||||
auth = null,
|
|
||||||
navItems = [],
|
navItems = [],
|
||||||
|
allToolItems = [],
|
||||||
productAreas = [],
|
productAreas = [],
|
||||||
presentation
|
presentation
|
||||||
}: {
|
}: {
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
auth?: AuthInfo | null;
|
|
||||||
navItems?: PlatformNavItem[];
|
navItems?: PlatformNavItem[];
|
||||||
|
allToolItems?: PlatformNavItem[];
|
||||||
productAreas?: ProductAreaContribution[];
|
productAreas?: ProductAreaContribution[];
|
||||||
presentation?: ViewPresentation;
|
presentation?: ViewPresentation;
|
||||||
}) {
|
}) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const items = visibleNavItems(auth, navItems);
|
const items = navItems;
|
||||||
|
const technicalItems = allToolItems;
|
||||||
const [rememberedTargets, setRememberedTargets] = useState<Record<string, string>>(() => loadRememberedTargets());
|
const [rememberedTargets, setRememberedTargets] = useState<Record<string, string>>(() => loadRememberedTargets());
|
||||||
const [expanded, setExpanded] = useState(() => loadRailExpanded());
|
const [expanded, setExpanded] = useState(() => loadRailExpanded());
|
||||||
const topLevelItems = useMemo(() => items.map((item) => item.to), [items]);
|
const topLevelItems = useMemo(() => items.map((item) => item.to), [items]);
|
||||||
@@ -95,9 +85,9 @@ export default function IconRail({
|
|||||||
{translateText(group.label)}
|
{translateText(group.label)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{group.items.map(({ to, label, icon: Icon }) => {
|
{group.items.map(({ to, label, icon: Icon, activePaths }) => {
|
||||||
const target = rememberedTargets[to] ?? to;
|
const target = rememberedTargets[to] ?? to;
|
||||||
const active = modulePathActive(location.pathname, to);
|
const active = modulePathActive(location.pathname, to, activePaths);
|
||||||
const renderedLabel = translateText(label);
|
const renderedLabel = translateText(label);
|
||||||
const areaLabel = group.areaLabel
|
const areaLabel = group.areaLabel
|
||||||
? translateText(group.areaLabel)
|
? translateText(group.areaLabel)
|
||||||
@@ -120,6 +110,11 @@ export default function IconRail({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{technicalItems.length > 0 && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<AllToolsNavigation items={technicalItems} rememberedTargets={rememberedTargets} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div className="icon-rail-bottom">
|
<div className="icon-rail-bottom">
|
||||||
@@ -144,9 +139,15 @@ export default function IconRail({
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function modulePathActive(pathname: string, root: string): boolean {
|
function modulePathActive(
|
||||||
if (root === "/") return pathname === "/";
|
pathname: string,
|
||||||
return pathname === root || pathname.startsWith(`${root}/`);
|
root: string,
|
||||||
|
activePaths: string[] = []
|
||||||
|
): boolean {
|
||||||
|
return [root, ...activePaths].some((candidate) => {
|
||||||
|
if (candidate === "/") return pathname === "/";
|
||||||
|
return pathname === candidate || pathname.startsWith(`${candidate}/`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadRememberedTargets(): Record<string, string> {
|
function loadRememberedTargets(): Record<string, string> {
|
||||||
|
|||||||
@@ -87,13 +87,5 @@ export function groupNavigationItems(
|
|||||||
items: remaining
|
items: remaining
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const overview = groups.filter((group) => group.id === "overview");
|
return groups;
|
||||||
const configurable = groups
|
|
||||||
.filter((group) => group.id !== "overview")
|
|
||||||
.sort((left, right) => minimumOrder(left.items) - minimumOrder(right.items));
|
|
||||||
return [...overview, ...configurable];
|
|
||||||
}
|
|
||||||
|
|
||||||
function minimumOrder(items: PlatformNavItem[]): number {
|
|
||||||
return Math.min(...items.map((item) => item.order ?? 100), 10_000);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type {
|
|||||||
AuthInfo,
|
AuthInfo,
|
||||||
ComposedProductSurface,
|
ComposedProductSurface,
|
||||||
EffectiveViewProjection,
|
EffectiveViewProjection,
|
||||||
|
PlatformNavItem,
|
||||||
ProductSurfaceMetadata,
|
ProductSurfaceMetadata,
|
||||||
PlatformWebModule,
|
PlatformWebModule,
|
||||||
ProductSurfaceContribution
|
ProductSurfaceContribution
|
||||||
@@ -20,6 +21,11 @@ export type ProductSurfaceRouteResolvedEventDetail = {
|
|||||||
usedAlias: boolean;
|
usedAlias: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ProductNavigationProjection = {
|
||||||
|
primaryItems: PlatformNavItem[];
|
||||||
|
allToolItems: PlatformNavItem[];
|
||||||
|
};
|
||||||
|
|
||||||
export function composeProductSurfaces(
|
export function composeProductSurfaces(
|
||||||
modules: readonly PlatformWebModule[]
|
modules: readonly PlatformWebModule[]
|
||||||
): ComposedProductSurface[] {
|
): ComposedProductSurface[] {
|
||||||
@@ -110,6 +116,65 @@ export function availableProductSurfaceContributors(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace authorized owner routes with stable product entries while retaining
|
||||||
|
* the complete permission-derived catalogue as an explicit escape.
|
||||||
|
*/
|
||||||
|
export function projectProductNavigation(
|
||||||
|
items: readonly PlatformNavItem[],
|
||||||
|
modules: readonly PlatformWebModule[],
|
||||||
|
auth: AuthInfo | null | undefined,
|
||||||
|
projection?: EffectiveViewProjection | null,
|
||||||
|
catalogueItems: readonly PlatformNavItem[] = items
|
||||||
|
): ProductNavigationProjection {
|
||||||
|
const authorizedItems = items.filter((item) => navigationItemAuthorized(item, auth));
|
||||||
|
const allToolItems = catalogueItems.filter((item) => navigationItemAuthorized(item, auth));
|
||||||
|
const ownerItemByPath = new Map(authorizedItems.map((item) => [item.to, item]));
|
||||||
|
const consumedOwnerPaths = new Set<string>();
|
||||||
|
const replacementByPath = new Map<string, PlatformNavItem>();
|
||||||
|
|
||||||
|
for (const surface of composeProductSurfaces(modules)) {
|
||||||
|
const contributors = availableProductSurfaceContributors(
|
||||||
|
surface,
|
||||||
|
auth,
|
||||||
|
modules,
|
||||||
|
projection
|
||||||
|
);
|
||||||
|
const navigable = contributors
|
||||||
|
.map((contribution) => ({
|
||||||
|
contribution,
|
||||||
|
item: ownerItemByPath.get(contribution.routePath)
|
||||||
|
}))
|
||||||
|
.filter((candidate): candidate is {
|
||||||
|
contribution: ProductSurfaceContribution;
|
||||||
|
item: PlatformNavItem;
|
||||||
|
} => candidate.item !== undefined);
|
||||||
|
const target = navigable[0];
|
||||||
|
if (!target) continue;
|
||||||
|
|
||||||
|
navigable.forEach(({ contribution }) => {
|
||||||
|
consumedOwnerPaths.add(contribution.routePath);
|
||||||
|
});
|
||||||
|
replacementByPath.set(target.contribution.routePath, {
|
||||||
|
...target.item,
|
||||||
|
to: surface.entryPath,
|
||||||
|
label: surface.label,
|
||||||
|
navigationId: surface.id,
|
||||||
|
activePaths: [
|
||||||
|
...surface.aliases,
|
||||||
|
...navigable.map(({ contribution }) => contribution.routePath)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryItems = authorizedItems.flatMap((item) => {
|
||||||
|
const replacement = replacementByPath.get(item.to);
|
||||||
|
if (replacement) return [replacement];
|
||||||
|
return consumedOwnerPaths.has(item.to) ? [] : [item];
|
||||||
|
});
|
||||||
|
return { primaryItems, allToolItems };
|
||||||
|
}
|
||||||
|
|
||||||
export function dispatchProductSurfaceRouteResolved(
|
export function dispatchProductSurfaceRouteResolved(
|
||||||
detail: ProductSurfaceRouteResolvedEventDetail
|
detail: ProductSurfaceRouteResolvedEventDetail
|
||||||
): void {
|
): void {
|
||||||
@@ -141,3 +206,11 @@ function compareContributions(
|
|||||||
): number {
|
): number {
|
||||||
return left.order - right.order || left.moduleId.localeCompare(right.moduleId);
|
return left.order - right.order || left.moduleId.localeCompare(right.moduleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function navigationItemAuthorized(
|
||||||
|
item: PlatformNavItem,
|
||||||
|
auth: AuthInfo | null | undefined
|
||||||
|
): boolean {
|
||||||
|
return !item.allOf?.some((scope) => !hasScope(auth, scope))
|
||||||
|
&& (!item.anyOf?.length || hasAnyScope(auth, item.anyOf));
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { PlatformTranslations } from "./types";
|
||||||
|
import { generatedTranslations as messages } from "./i18n/messagesProductSurfaceTranslations";
|
||||||
|
import { generatedTranslations as outcomes } from "./i18n/outcomeProductSurfaceTranslations";
|
||||||
|
|
||||||
|
export const productSurfaceTranslations = {
|
||||||
|
en: { ...messages.en, ...outcomes.en },
|
||||||
|
de: { ...messages.de, ...outcomes.de }
|
||||||
|
} satisfies PlatformTranslations;
|
||||||
@@ -29,6 +29,16 @@
|
|||||||
.icon-nav-label { display: none; min-width: 0; overflow: hidden; padding-right: 14px; font-size: 13px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
.icon-nav-label { display: none; min-width: 0; overflow: hidden; padding-right: 14px; font-size: 13px; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.icon-rail.expanded .icon-nav-label { display: block; }
|
.icon-rail.expanded .icon-nav-label { display: block; }
|
||||||
.icon-nav-item:hover, .icon-nav-item.active { background: var(--rail-bg-active); color: var(--on-accent); border-left-color: var(--accent); }
|
.icon-nav-item:hover, .icon-nav-item.active { background: var(--rail-bg-active); color: var(--on-accent); border-left-color: var(--accent); }
|
||||||
|
.icon-nav-all-tools { width: 100%; min-width: 0; border-top: 1px solid var(--rail-bg-active); }
|
||||||
|
.icon-nav-all-tools-summary { width: 100%; height: 52px; display: grid; grid-template-columns: 55px minmax(0, 1fr); align-items: center; box-sizing: border-box; border-left: 3px solid transparent; color: var(--rail-text-muted); cursor: pointer; list-style: none; }
|
||||||
|
.icon-nav-all-tools-summary::-webkit-details-marker { display: none; }
|
||||||
|
.icon-nav-all-tools-summary > svg { justify-self: center; }
|
||||||
|
.icon-nav-all-tools-summary:hover,
|
||||||
|
.icon-nav-all-tools-summary:focus-visible,
|
||||||
|
.icon-nav-all-tools[open] > .icon-nav-all-tools-summary { background: var(--rail-bg-active); color: var(--on-accent); outline: none; }
|
||||||
|
.icon-nav-all-tools-summary:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
|
||||||
|
.icon-nav-all-tools-items { width: 100%; min-width: 0; background: color-mix(in srgb, var(--rail-bg-active) 45%, var(--rail-bg)); }
|
||||||
|
.icon-nav-all-tools-items .icon-nav-item { min-height: 46px; height: 46px; }
|
||||||
.icon-rail.compact { width: 58px; }
|
.icon-rail.compact { width: 58px; }
|
||||||
.app-main { min-width: 0; min-height: 0; height: 100vh; display: grid; grid-template-rows: 64px 51px minmax(0, 1fr); }
|
.app-main { min-width: 0; min-height: 0; height: 100vh; display: grid; grid-template-rows: 64px 51px minmax(0, 1fr); }
|
||||||
.titlebar { position: relative; background: var(--titlebar-bg); border-bottom: var(--border-line); display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; padding: 0 18px; gap: 18px; z-index: 100; box-shadow: var(--shadow-chrome); }
|
.titlebar { position: relative; background: var(--titlebar-bg); border-bottom: var(--border-line); display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; padding: 0 18px; gap: 18px; z-index: 100; box-shadow: var(--shadow-chrome); }
|
||||||
|
|||||||
@@ -304,6 +304,8 @@ export type PlatformNavItem = {
|
|||||||
navigationVisibilitySource?: string;
|
navigationVisibilitySource?: string;
|
||||||
navigationLockSource?: string | null;
|
navigationLockSource?: string | null;
|
||||||
navigationLayers?: Partial<Record<"module" | "system" | "tenant", NavigationLayerState>>;
|
navigationLayers?: Partial<Record<"module" | "system" | "tenant", NavigationLayerState>>;
|
||||||
|
/** Additional stable or owner paths that should mark a composed product entry active. */
|
||||||
|
activePaths?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NavigationLayerState = {
|
export type NavigationLayerState = {
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import {
|
|||||||
visibleRoutesForProjection
|
visibleRoutesForProjection
|
||||||
} from "../src/platform/views";
|
} from "../src/platform/views";
|
||||||
import { groupNavigationItems } from "../src/platform/productAreas";
|
import { groupNavigationItems } from "../src/platform/productAreas";
|
||||||
import { composeProductSurfaces } from "../src/platform/productSurfaces";
|
import {
|
||||||
|
composeProductSurfaces,
|
||||||
|
projectProductNavigation
|
||||||
|
} from "../src/platform/productSurfaces";
|
||||||
import { hasAnyScope, scopeGrants } from "../src/utils/permissions";
|
import { hasAnyScope, scopeGrants } from "../src/utils/permissions";
|
||||||
|
|
||||||
function assert(condition: unknown, message: string): void {
|
function assert(condition: unknown, message: string): void {
|
||||||
@@ -260,6 +263,64 @@ assert(composedMessages[0]?.entryPath === "/messages", "the composed identity sh
|
|||||||
assert(composedMessages[0]?.contributors.map((item) => item.moduleId).join(",") === "mail,postbox", "composition should retain ordered technical provenance");
|
assert(composedMessages[0]?.contributors.map((item) => item.moduleId).join(",") === "mail,postbox", "composition should retain ordered technical provenance");
|
||||||
assert(composedMessages[0]?.aliases.join(",") === "/inbox", "migration aliases should be de-duplicated across owners");
|
assert(composedMessages[0]?.aliases.join(",") === "/inbox", "migration aliases should be de-duplicated across owners");
|
||||||
|
|
||||||
|
const messageNavigation = projectProductNavigation(
|
||||||
|
[
|
||||||
|
{ to: "/dashboard", label: "Dashboard", order: 1 },
|
||||||
|
{ to: "/mail", label: "Mail", order: 50, anyOf: ["mail:mailbox:read"] },
|
||||||
|
{ to: "/postbox", label: "Postbox", order: 51, anyOf: ["postbox:message:read"] },
|
||||||
|
{ to: "/admin", label: "Administration", order: 90, anyOf: ["core:admin:read"] }
|
||||||
|
],
|
||||||
|
messageSurfaceModules,
|
||||||
|
{ scopes: ["mail:mailbox:read", "postbox:message:read"] } as AuthInfo
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
messageNavigation.primaryItems.map((item) => item.to).join(",") === "/dashboard,/messages",
|
||||||
|
"ordinary navigation should replace authorized owner routes with one stable product entry"
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
messageNavigation.primaryItems.find((item) => item.to === "/messages")?.activePaths?.includes("/postbox"),
|
||||||
|
"the product entry should remain active on an owner route"
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
messageNavigation.allToolItems.map((item) => item.to).join(",") === "/dashboard,/mail,/postbox",
|
||||||
|
"the explicit tool catalogue should retain authorized technical routes"
|
||||||
|
);
|
||||||
|
const mailOnlyNavigation = projectProductNavigation(
|
||||||
|
[
|
||||||
|
{ to: "/mail", label: "Mail", order: 50, anyOf: ["mail:mailbox:read"] },
|
||||||
|
{ to: "/postbox", label: "Postbox", order: 51, anyOf: ["postbox:message:read"] }
|
||||||
|
],
|
||||||
|
messageSurfaceModules,
|
||||||
|
{ scopes: ["mail:mailbox:read"] } as AuthInfo
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
mailOnlyNavigation.primaryItems.map((item) => item.to).join(",") === "/messages",
|
||||||
|
"composition should remain stable when only one optional contributor is authorized"
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
mailOnlyNavigation.allToolItems.map((item) => item.to).join(",") === "/mail",
|
||||||
|
"the tool catalogue must not disclose unauthorized owner routes"
|
||||||
|
);
|
||||||
|
|
||||||
|
const focusedMessageNavigation = projectProductNavigation(
|
||||||
|
[{ to: "/mail", label: "Mail", order: 50, anyOf: ["mail:mailbox:read"] }],
|
||||||
|
messageSurfaceModules,
|
||||||
|
{ scopes: ["mail:mailbox:read", "postbox:message:read"] } as AuthInfo,
|
||||||
|
null,
|
||||||
|
[
|
||||||
|
{ to: "/mail", label: "Mail", order: 50, anyOf: ["mail:mailbox:read"] },
|
||||||
|
{ to: "/postbox", label: "Postbox", order: 51, anyOf: ["postbox:message:read"] }
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
focusedMessageNavigation.primaryItems.map((item) => item.to).join(",") === "/messages",
|
||||||
|
"a focused View should keep the selected stable product destination"
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
focusedMessageNavigation.allToolItems.map((item) => item.to).join(",") === "/mail,/postbox",
|
||||||
|
"All available tools should provide an explicit permission-derived escape from View focus"
|
||||||
|
);
|
||||||
|
|
||||||
const viewAwareFiles: PlatformWebModule = {
|
const viewAwareFiles: PlatformWebModule = {
|
||||||
...files,
|
...files,
|
||||||
navItems: [{ to: "/files", label: "Files", order: 20, anyOf: ["files:file:read"] }],
|
navItems: [{ to: "/files", label: "Files", order: 20, anyOf: ["files:file:read"] }],
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
],
|
],
|
||||||
"@govoplan/core-webui/wysiwyg": [
|
"@govoplan/core-webui/wysiwyg": [
|
||||||
"./src/wysiwyg.ts"
|
"./src/wysiwyg.ts"
|
||||||
|
],
|
||||||
|
"@govoplan/core-webui/outcome-product-surface-translations": [
|
||||||
|
"./src/i18n/outcomeProductSurfaceTranslations.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export default defineConfig({
|
|||||||
{ find: "@govoplan/core-webui/app", replacement: fileURLToPath(new URL("./src/app.ts", import.meta.url)) },
|
{ find: "@govoplan/core-webui/app", replacement: fileURLToPath(new URL("./src/app.ts", import.meta.url)) },
|
||||||
{ find: "@govoplan/core-webui/definition-graph", replacement: fileURLToPath(new URL("./src/definitionGraph.ts", import.meta.url)) },
|
{ find: "@govoplan/core-webui/definition-graph", replacement: fileURLToPath(new URL("./src/definitionGraph.ts", import.meta.url)) },
|
||||||
{ find: "@govoplan/core-webui/wysiwyg", replacement: fileURLToPath(new URL("./src/wysiwyg.ts", import.meta.url)) },
|
{ find: "@govoplan/core-webui/wysiwyg", replacement: fileURLToPath(new URL("./src/wysiwyg.ts", import.meta.url)) },
|
||||||
|
{ find: "@govoplan/core-webui/outcome-product-surface-translations", replacement: fileURLToPath(new URL("./src/i18n/outcomeProductSurfaceTranslations.ts", import.meta.url)) },
|
||||||
{ find: "@govoplan/core-webui", replacement: fileURLToPath(new URL("./src/index.ts", import.meta.url)) }
|
{ find: "@govoplan/core-webui", replacement: fileURLToPath(new URL("./src/index.ts", import.meta.url)) }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user