feat: integrate reports into Campaign navigation
This commit is contained in:
@@ -11,8 +11,9 @@ import { OPERATOR_QUEUE_ROUTE_SCOPES } from "../operator/operatorQueueAccess";
|
||||
|
||||
const CampaignListPage = lazy(() => import("./CampaignListPage"));
|
||||
const OperatorQueuePage = lazy(() => import("../operator/OperatorQueuePage"));
|
||||
const AggregateReportsPage = lazy(() => import("../reports/AggregateReportsPage"));
|
||||
|
||||
export type CampaignModuleSection = "campaigns" | "queue";
|
||||
export type CampaignModuleSection = "campaigns" | "queue" | "reports";
|
||||
|
||||
export default function CampaignModulePage({
|
||||
active,
|
||||
@@ -24,24 +25,42 @@ export default function CampaignModulePage({
|
||||
auth: AuthInfo;
|
||||
}) {
|
||||
const navigate = useGuardedNavigate();
|
||||
const canReadCampaigns = hasAnyScope(auth, ["campaigns:campaign:read"]);
|
||||
const canUseQueue =
|
||||
canReadCampaigns && hasAnyScope(auth, OPERATOR_QUEUE_ROUTE_SCOPES);
|
||||
const canReadReports = hasAnyScope(auth, ["campaigns:report:read"]);
|
||||
const groups: ModuleSubnavGroup<CampaignModuleSection>[] = [{
|
||||
items: [
|
||||
{
|
||||
id: "campaigns",
|
||||
label: "i18n:govoplan-campaign.all_campaigns.2bd1ee3a",
|
||||
primary: true
|
||||
},
|
||||
...(hasAnyScope(auth, OPERATOR_QUEUE_ROUTE_SCOPES)
|
||||
...(canReadCampaigns
|
||||
? [{
|
||||
id: "campaigns" as const,
|
||||
label: "i18n:govoplan-campaign.all_campaigns.2bd1ee3a",
|
||||
primary: true
|
||||
}]
|
||||
: []),
|
||||
...(canUseQueue
|
||||
? [{
|
||||
id: "queue" as const,
|
||||
label: "i18n:govoplan-campaign.operator_queue.72492fb5"
|
||||
}]
|
||||
: []),
|
||||
...(canReadReports
|
||||
? [{
|
||||
id: "reports" as const,
|
||||
label: "i18n:govoplan-campaign.reports.88bc3fe3",
|
||||
primary: !canReadCampaigns
|
||||
}]
|
||||
: [])
|
||||
]
|
||||
}];
|
||||
|
||||
function select(section: CampaignModuleSection) {
|
||||
navigate(section === "queue" ? "/campaigns/queue" : "/campaigns");
|
||||
const routes: Record<CampaignModuleSection, string> = {
|
||||
campaigns: "/campaigns",
|
||||
queue: "/campaigns/queue",
|
||||
reports: "/campaigns/reports"
|
||||
};
|
||||
navigate(routes[section]);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -50,7 +69,9 @@ export default function CampaignModulePage({
|
||||
<section className="workspace-content">
|
||||
{active === "queue"
|
||||
? <OperatorQueuePage settings={settings} auth={auth} />
|
||||
: <CampaignListPage settings={settings} />}
|
||||
: active === "reports"
|
||||
? <AggregateReportsPage settings={settings} />
|
||||
: <CampaignListPage settings={settings} />}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createElement, lazy, useCallback } from "react";
|
||||
import { Navigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
ResourceAccessBoundary,
|
||||
hasAnyScope,
|
||||
type ApiSettings,
|
||||
type AuthInfo,
|
||||
type DashboardWidgetsUiCapability,
|
||||
@@ -15,12 +16,15 @@ import "./styles/campaign-workspace.css";
|
||||
|
||||
const CampaignModulePage = lazy(() => import("./features/campaigns/CampaignModulePage"));
|
||||
const CampaignWorkspace = lazy(() => import("./features/campaigns/CampaignWorkspace"));
|
||||
const AggregateReportsPage = lazy(() => import("./features/reports/AggregateReportsPage"));
|
||||
const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage"));
|
||||
|
||||
const campaignRead = ["campaigns:campaign:read"];
|
||||
const reportRead = ["campaigns:report:read"];
|
||||
const campaignModuleRead = [...campaignRead, ...reportRead];
|
||||
// Preserve the former /operator route identity for saved View projections.
|
||||
const operatorQueueSurface = "campaigns.route.operator";
|
||||
// Preserve the former /reports route identity for saved View projections.
|
||||
const reportsSurface = "campaigns.route.reports";
|
||||
const translations = {
|
||||
en: generatedTranslations.en,
|
||||
de: generatedTranslations.de
|
||||
@@ -87,16 +91,16 @@ export const campaignModule: PlatformWebModule = {
|
||||
}
|
||||
],
|
||||
navItems: [
|
||||
{ to: "/campaigns", label: "i18n:govoplan-campaign.campaigns.01a23a28", iconName: "campaign", anyOf: campaignRead, order: 20 },
|
||||
{ to: "/reports", label: "i18n:govoplan-campaign.reports.88bc3fe3", iconName: "clipboard-pen-line", anyOf: ["campaigns:report:read"], order: 70 },
|
||||
{ to: "/campaigns", label: "i18n:govoplan-campaign.campaigns.01a23a28", iconName: "campaign", anyOf: campaignModuleRead, order: 20 },
|
||||
{ to: "/templates", label: "i18n:govoplan-campaign.templates.f25b700e", iconName: "layout-template", order: 90 }],
|
||||
|
||||
routes: [
|
||||
{ path: "/campaigns", anyOf: campaignRead, order: 20, render: ({ settings, auth }) => createElement(CampaignModulePage, { active: "campaigns", settings, auth }) },
|
||||
{ path: "/campaigns", anyOf: campaignModuleRead, order: 20, render: ({ settings, auth }) => createElement(CampaignModuleLandingRoute, { settings, auth }) },
|
||||
{ path: "/operator", anyOf: OPERATOR_QUEUE_ROUTE_SCOPES, allOf: campaignRead, order: 21, surfaceId: operatorQueueSurface, render: () => createElement(Navigate, { to: "/campaigns/queue", replace: true }) },
|
||||
{ path: "/campaigns/queue", anyOf: OPERATOR_QUEUE_ROUTE_SCOPES, allOf: campaignRead, order: 21, surfaceId: operatorQueueSurface, render: ({ settings, auth }) => createElement(CampaignModulePage, { active: "queue", settings, auth }) },
|
||||
{ path: "/reports", anyOf: reportRead, order: 22, surfaceId: reportsSurface, render: () => createElement(Navigate, { to: "/campaigns/reports", replace: true }) },
|
||||
{ path: "/campaigns/reports", anyOf: reportRead, order: 22, surfaceId: reportsSurface, render: ({ settings, auth }) => createElement(CampaignModulePage, { active: "reports", settings, auth }) },
|
||||
{ path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 22, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) },
|
||||
{ path: "/reports", anyOf: ["campaigns:report:read"], order: 70, render: ({ settings }) => createElement(AggregateReportsPage, { settings }) },
|
||||
{ path: "/templates", order: 90, render: () => createElement(TemplatesPage) }],
|
||||
uiCapabilities: {
|
||||
"dashboard.widgets": campaignDashboardWidgets
|
||||
@@ -104,6 +108,13 @@ export const campaignModule: PlatformWebModule = {
|
||||
|
||||
};
|
||||
|
||||
function CampaignModuleLandingRoute({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
|
||||
if (!hasAnyScope(auth, campaignRead)) {
|
||||
return createElement(Navigate, { to: "/campaigns/reports", replace: true });
|
||||
}
|
||||
return createElement(CampaignModulePage, { active: "campaigns", settings, auth });
|
||||
}
|
||||
|
||||
function CampaignResourceRoute({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
|
||||
const { campaignId = "" } = useParams();
|
||||
const tenantId = (auth.active_tenant ?? auth.tenant).id;
|
||||
|
||||
@@ -67,11 +67,19 @@ assert.doesNotMatch(source, /humanize\(/);
|
||||
assert.match(campaignModulePage, /<ModuleSubnav/);
|
||||
assert.match(campaignModulePage, /id: "campaigns"/);
|
||||
assert.match(campaignModulePage, /id: "queue"/);
|
||||
assert.match(campaignModulePage, /navigate\(section === "queue" \? "\/campaigns\/queue" : "\/campaigns"\)/);
|
||||
assert.match(campaignModulePage, /id: "reports"/);
|
||||
assert.match(campaignModulePage, /campaigns: "\/campaigns"/);
|
||||
assert.match(campaignModulePage, /queue: "\/campaigns\/queue"/);
|
||||
assert.match(campaignModulePage, /reports: "\/campaigns\/reports"/);
|
||||
assert.match(campaignModulePage, /navigate\(routes\[section\]\)/);
|
||||
assert.match(campaignModulePage, /active === "queue"[\s\S]*<OperatorQueuePage/);
|
||||
assert.match(campaignModulePage, /active === "reports"[\s\S]*<AggregateReportsPage/);
|
||||
assert.match(moduleSource, /path: "\/campaigns\/queue"/);
|
||||
assert.match(moduleSource, /path: "\/operator"[\s\S]*createElement\(Navigate, \{ to: "\/campaigns\/queue", replace: true \}\)/);
|
||||
assert.match(moduleSource, /path: "\/campaigns\/reports"/);
|
||||
assert.match(moduleSource, /path: "\/reports"[\s\S]*createElement\(Navigate, \{ to: "\/campaigns\/reports", replace: true \}\)/);
|
||||
assert.doesNotMatch(moduleSource, /to: "\/operator"/);
|
||||
assert.doesNotMatch(moduleSource, /to: "\/reports"/);
|
||||
assert.ok(
|
||||
moduleSource.indexOf('{ path: "/operator"') < moduleSource.indexOf('{ path: "/campaigns/queue"'),
|
||||
"the canonical Campaign route must replace the legacy alias in the shared view-surface catalogue"
|
||||
@@ -81,6 +89,24 @@ assert.equal(
|
||||
2,
|
||||
"the legacy redirect and canonical queue route share one configurable view surface"
|
||||
);
|
||||
assert.ok(
|
||||
moduleSource.indexOf('{ path: "/reports"') < moduleSource.indexOf('{ path: "/campaigns/reports"'),
|
||||
"the canonical Campaign reports route must replace the legacy alias in the shared view-surface catalogue"
|
||||
);
|
||||
assert.equal(
|
||||
moduleSource.split("surfaceId: reportsSurface").length - 1,
|
||||
2,
|
||||
"the legacy redirect and canonical reports route share one configurable view surface"
|
||||
);
|
||||
assert.equal(
|
||||
moduleSource.split("anyOf: campaignModuleRead").length - 1,
|
||||
2,
|
||||
"the Campaign navigation and landing route remain available to report-only readers"
|
||||
);
|
||||
assert.match(
|
||||
moduleSource,
|
||||
/function CampaignModuleLandingRoute[\s\S]*!hasAnyScope\(auth, campaignRead\)[\s\S]*to: "\/campaigns\/reports"/
|
||||
);
|
||||
|
||||
const localized = new Map([
|
||||
["i18n:govoplan-campaign.claimed.83c87884", ["Claimed", "Beansprucht"]],
|
||||
|
||||
Reference in New Issue
Block a user