feat: integrate operator queue into Campaign
This commit is contained in:
@@ -15,7 +15,8 @@ This repository owns:
|
|||||||
- campaign/version/job/issue/send-attempt/append-attempt models and migrations
|
- campaign/version/job/issue/send-attempt/append-attempt models and migrations
|
||||||
- campaign JSON schema, validation, message building, attachment resolution, ZIP handling, reports, queue/control services, and mock-send paths
|
- campaign JSON schema, validation, message building, attachment resolution, ZIP handling, reports, queue/control services, and mock-send paths
|
||||||
- WebUI package `@govoplan/campaign-webui`
|
- WebUI package `@govoplan/campaign-webui`
|
||||||
- route contributions for `/campaigns`, `/campaigns/:campaignId/*`, `/operator`, `/reports`, and `/templates`
|
- route contributions for `/campaigns`, the integrated `/campaigns/queue` view,
|
||||||
|
`/campaigns/:campaignId/*`, `/reports`, and `/templates`
|
||||||
|
|
||||||
Core owns the auth facade, RBAC/capability contracts, database/session
|
Core owns the auth facade, RBAC/capability contracts, database/session
|
||||||
primitives, CSRF/API helpers, shell layout, and route rendering. Tenancy is an
|
primitives, CSRF/API helpers, shell layout, and route rendering. Tenancy is an
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ CAMPAIGN_USER_DOCUMENTATION = (
|
|||||||
related_topic_ids=("campaigns.workflow.complete-review", "campaigns.workflow.retry-and-reconcile"),
|
related_topic_ids=("campaigns.workflow.complete-review", "campaigns.workflow.retry-and-reconcile"),
|
||||||
links=(
|
links=(
|
||||||
DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"),
|
DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"),
|
||||||
DocumentationLink(label="Campaign operator queue", href="/operator", kind="runtime"),
|
DocumentationLink(label="Campaign operator queue", href="/campaigns/queue", kind="runtime"),
|
||||||
),
|
),
|
||||||
related_modules=("mail", "notifications"),
|
related_modules=("mail", "notifications"),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -80,6 +80,15 @@ PERMISSIONS = (
|
|||||||
_permission("campaigns:report:send", "Send reports", "Email campaign reports to configured recipients.", "Reports"),
|
_permission("campaigns:report:send", "Send reports", "Email campaign reports to configured recipients.", "Reports"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OPERATOR_QUEUE_REQUIRED_ANY = (
|
||||||
|
"campaigns:campaign:queue",
|
||||||
|
"campaigns:campaign:retry",
|
||||||
|
"campaigns:campaign:reconcile",
|
||||||
|
"campaigns:campaign:control",
|
||||||
|
)
|
||||||
|
# Preserve the former /operator route identity for saved View projections.
|
||||||
|
OPERATOR_QUEUE_SURFACE_ID = "campaigns.route.operator"
|
||||||
|
|
||||||
ROLE_TEMPLATES = (
|
ROLE_TEMPLATES = (
|
||||||
RoleTemplate(
|
RoleTemplate(
|
||||||
slug="campaign_aggregate_reader",
|
slug="campaign_aggregate_reader",
|
||||||
@@ -222,19 +231,6 @@ manifest = ModuleManifest(
|
|||||||
tenant_summary_providers=(_tenant_summary,),
|
tenant_summary_providers=(_tenant_summary,),
|
||||||
nav_items=(
|
nav_items=(
|
||||||
NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=("campaigns:campaign:read",), order=20),
|
NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=("campaigns:campaign:read",), order=20),
|
||||||
NavItem(
|
|
||||||
path="/operator",
|
|
||||||
label="Operator Queue",
|
|
||||||
icon="radio-tower",
|
|
||||||
required_all=("campaigns:campaign:read",),
|
|
||||||
required_any=(
|
|
||||||
"campaigns:campaign:queue",
|
|
||||||
"campaigns:campaign:retry",
|
|
||||||
"campaigns:campaign:reconcile",
|
|
||||||
"campaigns:campaign:control",
|
|
||||||
),
|
|
||||||
order=30,
|
|
||||||
),
|
|
||||||
NavItem(path="/reports", label="Reports", icon="clipboard-pen-line", required_any=("campaigns:report:read",), order=70),
|
NavItem(path="/reports", label="Reports", icon="clipboard-pen-line", required_any=("campaigns:report:read",), order=70),
|
||||||
),
|
),
|
||||||
frontend=FrontendModule(
|
frontend=FrontendModule(
|
||||||
@@ -247,23 +243,19 @@ manifest = ModuleManifest(
|
|||||||
required_any=("campaigns:campaign:read",),
|
required_any=("campaigns:campaign:read",),
|
||||||
order=20,
|
order=20,
|
||||||
),
|
),
|
||||||
|
FrontendRoute(
|
||||||
|
path="/campaigns/queue",
|
||||||
|
component="OperatorQueuePage",
|
||||||
|
required_all=("campaigns:campaign:read",),
|
||||||
|
required_any=OPERATOR_QUEUE_REQUIRED_ANY,
|
||||||
|
order=21,
|
||||||
|
surface_id=OPERATOR_QUEUE_SURFACE_ID,
|
||||||
|
),
|
||||||
FrontendRoute(
|
FrontendRoute(
|
||||||
path="/campaigns/:campaignId/*",
|
path="/campaigns/:campaignId/*",
|
||||||
component="CampaignWorkspace",
|
component="CampaignWorkspace",
|
||||||
required_any=("campaigns:campaign:read",),
|
required_any=("campaigns:campaign:read",),
|
||||||
order=21,
|
order=22,
|
||||||
),
|
|
||||||
FrontendRoute(
|
|
||||||
path="/operator",
|
|
||||||
component="OperatorQueuePage",
|
|
||||||
required_all=("campaigns:campaign:read",),
|
|
||||||
required_any=(
|
|
||||||
"campaigns:campaign:queue",
|
|
||||||
"campaigns:campaign:retry",
|
|
||||||
"campaigns:campaign:reconcile",
|
|
||||||
"campaigns:campaign:control",
|
|
||||||
),
|
|
||||||
order=30,
|
|
||||||
),
|
),
|
||||||
FrontendRoute(
|
FrontendRoute(
|
||||||
path="/reports",
|
path="/reports",
|
||||||
@@ -275,19 +267,6 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
nav_items=(
|
nav_items=(
|
||||||
NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=("campaigns:campaign:read",), order=20),
|
NavItem(path="/campaigns", label="Campaigns", icon="campaign", required_any=("campaigns:campaign:read",), order=20),
|
||||||
NavItem(
|
|
||||||
path="/operator",
|
|
||||||
label="Operator Queue",
|
|
||||||
icon="radio-tower",
|
|
||||||
required_all=("campaigns:campaign:read",),
|
|
||||||
required_any=(
|
|
||||||
"campaigns:campaign:queue",
|
|
||||||
"campaigns:campaign:retry",
|
|
||||||
"campaigns:campaign:reconcile",
|
|
||||||
"campaigns:campaign:control",
|
|
||||||
),
|
|
||||||
order=30,
|
|
||||||
),
|
|
||||||
NavItem(path="/reports", label="Reports", icon="clipboard-pen-line", required_any=("campaigns:report:read",), order=70),
|
NavItem(path="/reports", label="Reports", icon="clipboard-pen-line", required_any=("campaigns:report:read",), order=70),
|
||||||
NavItem(path="/templates", label="Templates", icon="layout-template", order=90),
|
NavItem(path="/templates", label="Templates", icon="layout-template", order=90),
|
||||||
),
|
),
|
||||||
@@ -481,7 +460,7 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
links=(
|
links=(
|
||||||
DocumentationLink(label="Campaign operator queue", href="/operator", kind="runtime"),
|
DocumentationLink(label="Campaign operator queue", href="/campaigns/queue", kind="runtime"),
|
||||||
DocumentationLink(label="Campaign reports", href="/reports", kind="runtime"),
|
DocumentationLink(label="Campaign reports", href="/reports", kind="runtime"),
|
||||||
DocumentationLink(label="Campaign delivery runbook", href="govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md", kind="repository"),
|
DocumentationLink(label="Campaign delivery runbook", href="govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md", kind="repository"),
|
||||||
),
|
),
|
||||||
@@ -489,7 +468,7 @@ manifest = ModuleManifest(
|
|||||||
unlocks=("Fail-closed recovery without exposing Mail credentials.",),
|
unlocks=("Fail-closed recovery without exposing Mail credentials.",),
|
||||||
metadata={
|
metadata={
|
||||||
"kind": "reference",
|
"kind": "reference",
|
||||||
"route": "/operator",
|
"route": "/campaigns/queue",
|
||||||
"screen": "Campaign operator queue",
|
"screen": "Campaign operator queue",
|
||||||
"section": "Profile-backed delivery recovery",
|
"section": "Profile-backed delivery recovery",
|
||||||
"related_topic_ids": [
|
"related_topic_ids": [
|
||||||
@@ -619,14 +598,14 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
links=(
|
links=(
|
||||||
DocumentationLink(label="Campaign operator queue", href="/operator", kind="runtime"),
|
DocumentationLink(label="Campaign operator queue", href="/campaigns/queue", kind="runtime"),
|
||||||
DocumentationLink(label="Campaign delivery runbook", href="govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md", kind="repository"),
|
DocumentationLink(label="Campaign delivery runbook", href="govoplan-campaign/docs/CAMPAIGN_DELIVERY_RUNBOOK.md", kind="repository"),
|
||||||
),
|
),
|
||||||
related_modules=("mail", "audit"),
|
related_modules=("mail", "audit"),
|
||||||
unlocks=("Evidence-backed recovery without accidental duplicate external effects.",),
|
unlocks=("Evidence-backed recovery without accidental duplicate external effects.",),
|
||||||
metadata={
|
metadata={
|
||||||
"kind": "workflow",
|
"kind": "workflow",
|
||||||
"route": "/operator",
|
"route": "/campaigns/queue",
|
||||||
"screen": "Campaign operator queue",
|
"screen": "Campaign operator queue",
|
||||||
"help_contexts": ["campaign.review-send", "campaign.report", "campaign.audit"],
|
"help_contexts": ["campaign.review-send", "campaign.report", "campaign.audit"],
|
||||||
"prerequisites": [
|
"prerequisites": [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from govoplan_access.backend.db.models import Account, Group, User
|
|||||||
from govoplan_campaign.backend.capabilities import CampaignAccessService
|
from govoplan_campaign.backend.capabilities import CampaignAccessService
|
||||||
from govoplan_campaign.backend.db.models import Campaign, CampaignShare
|
from govoplan_campaign.backend.db.models import Campaign, CampaignShare
|
||||||
from govoplan_core.core.access import PrincipalRef
|
from govoplan_core.core.access import PrincipalRef
|
||||||
|
from govoplan_core.core.change_sequence import ChangeSequenceEntry
|
||||||
from govoplan_core.db.base import Base
|
from govoplan_core.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
@@ -72,7 +73,17 @@ class CampaignAccessProviderTests(unittest.TestCase):
|
|||||||
|
|
||||||
def _session():
|
def _session():
|
||||||
engine = create_engine("sqlite:///:memory:", future=True)
|
engine = create_engine("sqlite:///:memory:", future=True)
|
||||||
Base.metadata.create_all(bind=engine, tables=[Account.__table__, User.__table__, Group.__table__, Campaign.__table__, CampaignShare.__table__])
|
Base.metadata.create_all(
|
||||||
|
bind=engine,
|
||||||
|
tables=[
|
||||||
|
Account.__table__,
|
||||||
|
User.__table__,
|
||||||
|
Group.__table__,
|
||||||
|
Campaign.__table__,
|
||||||
|
CampaignShare.__table__,
|
||||||
|
ChangeSequenceEntry.__table__,
|
||||||
|
],
|
||||||
|
)
|
||||||
return sessionmaker(bind=engine, future=True)()
|
return sessionmaker(bind=engine, future=True)()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ def test_sender_sees_queue_and_send_only_with_the_mail_contract_and_profile_auth
|
|||||||
topic for topic in CAMPAIGN_USER_DOCUMENTATION
|
topic for topic in CAMPAIGN_USER_DOCUMENTATION
|
||||||
if topic.id == "campaigns.workflow.queue-delivery"
|
if topic.id == "campaigns.workflow.queue-delivery"
|
||||||
)
|
)
|
||||||
assert any(link.href == "/operator" for link in queue_topic.links)
|
assert any(link.href == "/campaigns/queue" for link in queue_topic.links)
|
||||||
|
|
||||||
|
|
||||||
def test_connected_authoring_tasks_require_their_declared_contracts_and_permissions() -> None:
|
def test_connected_authoring_tasks_require_their_declared_contracts_and_permissions() -> None:
|
||||||
|
|||||||
33
tests/test_manifest_navigation.py
Normal file
33
tests/test_manifest_navigation.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from govoplan_campaign.backend.manifest import (
|
||||||
|
OPERATOR_QUEUE_REQUIRED_ANY,
|
||||||
|
OPERATOR_QUEUE_SURFACE_ID,
|
||||||
|
get_manifest,
|
||||||
|
)
|
||||||
|
from govoplan_core.core.registry import manifest_view_surfaces
|
||||||
|
|
||||||
|
|
||||||
|
def test_operator_queue_is_an_integrated_campaign_view() -> None:
|
||||||
|
manifest = get_manifest()
|
||||||
|
assert manifest.frontend is not None
|
||||||
|
|
||||||
|
assert "/operator" not in {item.path for item in manifest.nav_items}
|
||||||
|
assert "/operator" not in {item.path for item in manifest.frontend.nav_items}
|
||||||
|
|
||||||
|
routes = {route.path: route for route in manifest.frontend.routes}
|
||||||
|
assert "/operator" not in routes
|
||||||
|
queue = routes["/campaigns/queue"]
|
||||||
|
assert queue.component == "OperatorQueuePage"
|
||||||
|
assert queue.required_all == ("campaigns:campaign:read",)
|
||||||
|
assert queue.required_any == OPERATOR_QUEUE_REQUIRED_ANY
|
||||||
|
assert OPERATOR_QUEUE_SURFACE_ID == "campaigns.route.operator"
|
||||||
|
assert queue.surface_id == OPERATOR_QUEUE_SURFACE_ID
|
||||||
|
|
||||||
|
queue_surfaces = [
|
||||||
|
surface
|
||||||
|
for surface in manifest_view_surfaces(manifest)
|
||||||
|
if surface.id == OPERATOR_QUEUE_SURFACE_ID
|
||||||
|
]
|
||||||
|
assert len(queue_surfaces) == 1
|
||||||
|
assert queue_surfaces[0].description == "/campaigns/queue"
|
||||||
57
webui/src/features/campaigns/CampaignModulePage.tsx
Normal file
57
webui/src/features/campaigns/CampaignModulePage.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { lazy } from "react";
|
||||||
|
import {
|
||||||
|
ModuleSubnav,
|
||||||
|
hasAnyScope,
|
||||||
|
useGuardedNavigate,
|
||||||
|
type ApiSettings,
|
||||||
|
type AuthInfo,
|
||||||
|
type ModuleSubnavGroup
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
|
import { OPERATOR_QUEUE_ROUTE_SCOPES } from "../operator/operatorQueueAccess";
|
||||||
|
|
||||||
|
const CampaignListPage = lazy(() => import("./CampaignListPage"));
|
||||||
|
const OperatorQueuePage = lazy(() => import("../operator/OperatorQueuePage"));
|
||||||
|
|
||||||
|
export type CampaignModuleSection = "campaigns" | "queue";
|
||||||
|
|
||||||
|
export default function CampaignModulePage({
|
||||||
|
active,
|
||||||
|
settings,
|
||||||
|
auth
|
||||||
|
}: {
|
||||||
|
active: CampaignModuleSection;
|
||||||
|
settings: ApiSettings;
|
||||||
|
auth: AuthInfo;
|
||||||
|
}) {
|
||||||
|
const navigate = useGuardedNavigate();
|
||||||
|
const groups: ModuleSubnavGroup<CampaignModuleSection>[] = [{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "campaigns",
|
||||||
|
label: "i18n:govoplan-campaign.all_campaigns.2bd1ee3a",
|
||||||
|
primary: true
|
||||||
|
},
|
||||||
|
...(hasAnyScope(auth, OPERATOR_QUEUE_ROUTE_SCOPES)
|
||||||
|
? [{
|
||||||
|
id: "queue" as const,
|
||||||
|
label: "i18n:govoplan-campaign.operator_queue.72492fb5"
|
||||||
|
}]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
}];
|
||||||
|
|
||||||
|
function select(section: CampaignModuleSection) {
|
||||||
|
navigate(section === "queue" ? "/campaigns/queue" : "/campaigns");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="workspace campaign-module-workspace">
|
||||||
|
<ModuleSubnav active={active} groups={groups} onSelect={select} />
|
||||||
|
<section className="workspace-content">
|
||||||
|
{active === "queue"
|
||||||
|
? <OperatorQueuePage settings={settings} auth={auth} />
|
||||||
|
: <CampaignListPage settings={settings} />}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
webui/src/features/operator/operatorQueueAccess.ts
Normal file
6
webui/src/features/operator/operatorQueueAccess.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const OPERATOR_QUEUE_ROUTE_SCOPES = [
|
||||||
|
"campaigns:campaign:queue",
|
||||||
|
"campaigns:campaign:retry",
|
||||||
|
"campaigns:campaign:reconcile",
|
||||||
|
"campaigns:campaign:control"
|
||||||
|
];
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createElement, lazy, useCallback } from "react";
|
import { createElement, lazy, useCallback } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { Navigate, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
ResourceAccessBoundary,
|
ResourceAccessBoundary,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
@@ -9,17 +9,18 @@ import {
|
|||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import { getCampaign } from "./api/campaigns";
|
import { getCampaign } from "./api/campaigns";
|
||||||
import CampaignActivityWidget from "./features/campaigns/CampaignActivityWidget";
|
import CampaignActivityWidget from "./features/campaigns/CampaignActivityWidget";
|
||||||
|
import { OPERATOR_QUEUE_ROUTE_SCOPES } from "./features/operator/operatorQueueAccess";
|
||||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
import "./styles/campaign-workspace.css";
|
import "./styles/campaign-workspace.css";
|
||||||
|
|
||||||
const CampaignListPage = lazy(() => import("./features/campaigns/CampaignListPage"));
|
const CampaignModulePage = lazy(() => import("./features/campaigns/CampaignModulePage"));
|
||||||
const CampaignWorkspace = lazy(() => import("./features/campaigns/CampaignWorkspace"));
|
const CampaignWorkspace = lazy(() => import("./features/campaigns/CampaignWorkspace"));
|
||||||
const OperatorQueuePage = lazy(() => import("./features/operator/OperatorQueuePage"));
|
|
||||||
const AggregateReportsPage = lazy(() => import("./features/reports/AggregateReportsPage"));
|
const AggregateReportsPage = lazy(() => import("./features/reports/AggregateReportsPage"));
|
||||||
const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage"));
|
const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage"));
|
||||||
|
|
||||||
const campaignRead = ["campaigns:campaign:read"];
|
const campaignRead = ["campaigns:campaign:read"];
|
||||||
const operatorScopes = ["campaigns:campaign:queue", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:campaign:control"];
|
// Preserve the former /operator route identity for saved View projections.
|
||||||
|
const operatorQueueSurface = "campaigns.route.operator";
|
||||||
const translations = {
|
const translations = {
|
||||||
en: generatedTranslations.en,
|
en: generatedTranslations.en,
|
||||||
de: generatedTranslations.de
|
de: generatedTranslations.de
|
||||||
@@ -87,21 +88,14 @@ export const campaignModule: PlatformWebModule = {
|
|||||||
],
|
],
|
||||||
navItems: [
|
navItems: [
|
||||||
{ to: "/campaigns", label: "i18n:govoplan-campaign.campaigns.01a23a28", iconName: "campaign", anyOf: campaignRead, order: 20 },
|
{ to: "/campaigns", label: "i18n:govoplan-campaign.campaigns.01a23a28", iconName: "campaign", anyOf: campaignRead, order: 20 },
|
||||||
{
|
|
||||||
to: "/operator",
|
|
||||||
label: "i18n:govoplan-campaign.operator_queue.ddf23260",
|
|
||||||
iconName: "radio-tower",
|
|
||||||
anyOf: operatorScopes,
|
|
||||||
allOf: campaignRead,
|
|
||||||
order: 30
|
|
||||||
},
|
|
||||||
{ to: "/reports", label: "i18n:govoplan-campaign.reports.88bc3fe3", iconName: "clipboard-pen-line", anyOf: ["campaigns:report:read"], order: 70 },
|
{ to: "/reports", label: "i18n:govoplan-campaign.reports.88bc3fe3", iconName: "clipboard-pen-line", anyOf: ["campaigns:report:read"], order: 70 },
|
||||||
{ to: "/templates", label: "i18n:govoplan-campaign.templates.f25b700e", iconName: "layout-template", order: 90 }],
|
{ to: "/templates", label: "i18n:govoplan-campaign.templates.f25b700e", iconName: "layout-template", order: 90 }],
|
||||||
|
|
||||||
routes: [
|
routes: [
|
||||||
{ path: "/campaigns", anyOf: campaignRead, order: 20, render: ({ settings }) => createElement(CampaignListPage, { settings }) },
|
{ path: "/campaigns", anyOf: campaignRead, order: 20, render: ({ settings, auth }) => createElement(CampaignModulePage, { active: "campaigns", settings, auth }) },
|
||||||
{ path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 21, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) },
|
{ path: "/operator", anyOf: OPERATOR_QUEUE_ROUTE_SCOPES, allOf: campaignRead, order: 21, surfaceId: operatorQueueSurface, render: () => createElement(Navigate, { to: "/campaigns/queue", replace: true }) },
|
||||||
{ path: "/operator", anyOf: operatorScopes, allOf: campaignRead, order: 30, render: ({ settings, auth }) => createElement(OperatorQueuePage, { settings, auth }) },
|
{ path: "/campaigns/queue", anyOf: OPERATOR_QUEUE_ROUTE_SCOPES, allOf: campaignRead, order: 21, surfaceId: operatorQueueSurface, render: ({ settings, auth }) => createElement(CampaignModulePage, { active: "queue", 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: "/reports", anyOf: ["campaigns:report:read"], order: 70, render: ({ settings }) => createElement(AggregateReportsPage, { settings }) },
|
||||||
{ path: "/templates", order: 90, render: () => createElement(TemplatesPage) }],
|
{ path: "/templates", order: 90, render: () => createElement(TemplatesPage) }],
|
||||||
uiCapabilities: {
|
uiCapabilities: {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import { fileURLToPath } from "node:url";
|
|||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.url));
|
const here = dirname(fileURLToPath(import.meta.url));
|
||||||
const source = readFileSync(resolve(here, "../src/features/operator/OperatorQueuePage.tsx"), "utf8");
|
const source = readFileSync(resolve(here, "../src/features/operator/OperatorQueuePage.tsx"), "utf8");
|
||||||
|
const campaignModulePage = readFileSync(resolve(here, "../src/features/campaigns/CampaignModulePage.tsx"), "utf8");
|
||||||
|
const moduleSource = readFileSync(resolve(here, "../src/module.ts"), "utf8");
|
||||||
const translations = readFileSync(resolve(here, "../src/i18n/generatedTranslations.ts"), "utf8");
|
const translations = readFileSync(resolve(here, "../src/i18n/generatedTranslations.ts"), "utf8");
|
||||||
const backendRouter = readFileSync(resolve(here, "../../src/govoplan_campaign/backend/router.py"), "utf8");
|
const jobQueries = readFileSync(resolve(here, "../../src/govoplan_campaign/backend/services/job_queries.py"), "utf8");
|
||||||
const responseSecurity = readFileSync(resolve(here, "../../src/govoplan_campaign/backend/response_security.py"), "utf8");
|
const responseSecurity = readFileSync(resolve(here, "../../src/govoplan_campaign/backend/response_security.py"), "utf8");
|
||||||
|
|
||||||
assert.match(source, /<DataGrid/);
|
assert.match(source, /<DataGrid/);
|
||||||
@@ -56,12 +58,29 @@ assert.match(source, /deliveryResultSummary[\s\S]*send_status[\s\S]*imap_status/
|
|||||||
assert.match(source, /durableResultText[\s\S]*slice\(0, 239\)/);
|
assert.match(source, /durableResultText[\s\S]*slice\(0, 239\)/);
|
||||||
assert.match(source, /permissions\.canOpenReport[\s\S]*you_do_not_have_permission/);
|
assert.match(source, /permissions\.canOpenReport[\s\S]*you_do_not_have_permission/);
|
||||||
assert.doesNotMatch(source, /claim_token|eml_local_path|storage_key|smtp_transport_revision|eml_sha256|message_id_header|smtp_response|provider_response|resolved_attachments/);
|
assert.doesNotMatch(source, /claim_token|eml_local_path|storage_key|smtp_transport_revision|eml_sha256|message_id_header|smtp_response|provider_response|resolved_attachments/);
|
||||||
assert.match(backendRouter, /"last_error": public_delivery_result_message\([\s\S]*last_error=job\.last_error/);
|
assert.match(jobQueries, /"last_error": public_delivery_result_message\([\s\S]*last_error=job\.last_error/);
|
||||||
assert.match(responseSecurity, /Map persisted provider text to a stable business-safe explanation/);
|
assert.match(responseSecurity, /Map persisted provider text to a stable business-safe explanation/);
|
||||||
assert.match(responseSecurity, /SMTP delivery failed; an operator can inspect restricted diagnostics\./);
|
assert.match(responseSecurity, /SMTP delivery failed; an operator can inspect restricted diagnostics\./);
|
||||||
assert.doesNotMatch(source, /window\.alert\s*\(/);
|
assert.doesNotMatch(source, /window\.alert\s*\(/);
|
||||||
assert.doesNotMatch(source, /function QueuePressureGrid|function OperatorJob/);
|
assert.doesNotMatch(source, /function QueuePressureGrid|function OperatorJob/);
|
||||||
assert.doesNotMatch(source, /humanize\(/);
|
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, /active === "queue"[\s\S]*<OperatorQueuePage/);
|
||||||
|
assert.match(moduleSource, /path: "\/campaigns\/queue"/);
|
||||||
|
assert.match(moduleSource, /path: "\/operator"[\s\S]*createElement\(Navigate, \{ to: "\/campaigns\/queue", replace: true \}\)/);
|
||||||
|
assert.doesNotMatch(moduleSource, /to: "\/operator"/);
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
moduleSource.split("surfaceId: operatorQueueSurface").length - 1,
|
||||||
|
2,
|
||||||
|
"the legacy redirect and canonical queue route share one configurable view surface"
|
||||||
|
);
|
||||||
|
|
||||||
const localized = new Map([
|
const localized = new Map([
|
||||||
["i18n:govoplan-campaign.claimed.83c87884", ["Claimed", "Beansprucht"]],
|
["i18n:govoplan-campaign.claimed.83c87884", ["Claimed", "Beansprucht"]],
|
||||||
|
|||||||
Reference in New Issue
Block a user