feat(campaigns): add Case Quick Access selector
Module Package Release / publish-packages (push) Successful in 13s

This commit is contained in:
2026-08-21 16:05:45 +02:00
parent 73cfad209a
commit 5a21067e44
6 changed files with 176 additions and 4 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-campaign" name = "govoplan-campaign"
version = "0.1.18" version = "0.1.19"
description = "GovOPlaN campaigns module with backend and WebUI integration." description = "GovOPlaN campaigns module with backend and WebUI integration."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+70 -1
View File
@@ -36,6 +36,7 @@ from govoplan_core.core.modules import (
NavItem, NavItem,
PermissionDefinition, PermissionDefinition,
ProductAreaContribution, ProductAreaContribution,
QuickAccessTool,
RoleTemplate, RoleTemplate,
) )
from govoplan_core.core.reporting import REPORT_PROVIDER_CAPABILITY_PREFIX from govoplan_core.core.reporting import REPORT_PROVIDER_CAPABILITY_PREFIX
@@ -384,7 +385,7 @@ def _campaigns_router(context: ModuleContext):
manifest = ModuleManifest( manifest = ModuleManifest(
id="campaigns", id="campaigns",
name="Campaigns", name="Campaigns",
version="0.1.18", version="0.1.19",
required_capabilities=( required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PERMISSION_EVALUATOR,
@@ -617,6 +618,32 @@ manifest = ModuleManifest(
label="Campaign activity widget", label="Campaign activity widget",
order=50, order=50,
), ),
ViewSurface(
id="campaigns.quick_access.campaigns",
module_id="campaigns",
kind="quick_access",
label="Campaign selection",
order=55,
),
),
quick_access_tools=(
QuickAccessTool(
id="campaigns.select",
module_id="campaigns",
category_id="campaigns",
label="Campaigns",
description="Select an authorized exact Campaign for the active Case.",
surface_id="campaigns.quick_access.campaigns",
icon="campaign",
full_page_path="/campaigns",
required_any=("campaigns:campaign:read",),
order=20,
modes=("select",),
availability="active_object",
accepted_reference_kinds=("cases.case",),
returned_reference_kinds=("campaigns.campaign",),
help_context_id="campaigns.quick_access.campaigns",
),
), ),
), ),
migration_spec=MigrationSpec( migration_spec=MigrationSpec(
@@ -668,6 +695,48 @@ manifest = ModuleManifest(
), ),
documentation=( documentation=(
*CAMPAIGN_USER_DOCUMENTATION, *CAMPAIGN_USER_DOCUMENTATION,
DocumentationTopic(
id="campaigns.workflow.link-exact-campaign-to-case",
title="Link an exact Campaign reference to an active Case",
summary=(
"Return an authorized Campaign and current immutable version through "
"Quick Access without copying campaign content."
),
body=(
"When a Case is the active object, Campaigns contributes a bounded "
"Quick Access selector. The normal Campaign list endpoint applies the "
"current actor's tenant, owner, group, share, and administration access "
"before candidates appear. Selecting a Campaign returns only its owner "
"module, stable Campaign ID, current version ID, display label, tenant, "
"and owner route through the versioned result contract. Cases discards "
"the label and stores no recipient, message, attachment, delivery, report, "
"or campaign configuration content. Opening the reference enters Campaigns "
"and rechecks current access. Campaigns disabled, access revoked, or the "
"source removed therefore leaves only an unavailable historical Case "
"reference; it never turns Case access into Campaign access."
),
layer="configured",
documentation_types=("admin", "user"),
audience=("campaign_manager", "case_manager", "operator", "module_admin"),
related_modules=("cases", "quick_access"),
conditions=(
DocumentationCondition(
required_modules=("campaigns", "cases"),
required_scopes=("campaigns:campaign:read",),
),
),
links=(
DocumentationLink(
label="Campaigns",
href="/campaigns",
kind="runtime",
),
),
metadata={
"kind": "workflow",
"help_contexts": ["campaigns.quick_access.campaigns"],
},
),
DocumentationTopic( DocumentationTopic(
id="campaigns.privacy.data-subject-requests", id="campaigns.privacy.data-subject-requests",
title="Review Campaign data in a data-subject request", title="Review Campaign data in a data-subject request",
+18
View File
@@ -73,3 +73,21 @@ def test_reusable_template_library_is_not_owned_by_campaign() -> None:
assert "/templates" not in {item.path for item in manifest.nav_items} assert "/templates" not in {item.path for item in manifest.nav_items}
assert "/templates" not in {item.path for item in manifest.frontend.nav_items} assert "/templates" not in {item.path for item in manifest.frontend.nav_items}
assert "/templates" not in {route.path for route in manifest.frontend.routes} assert "/templates" not in {route.path for route in manifest.frontend.routes}
def test_quick_access_selects_exact_campaigns_only_for_active_cases() -> None:
manifest = get_manifest()
assert manifest.frontend is not None
tool = next(
item
for item in manifest.frontend.quick_access_tools
if item.id == "campaigns.select"
)
assert tool.availability == "active_object"
assert tool.accepted_reference_kinds == ("cases.case",)
assert tool.returned_reference_kinds == ("campaigns.campaign",)
assert tool.modes == ("select",)
assert "campaigns.workflow.link-exact-campaign-to-case" in {
topic.id for topic in manifest.documentation
}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/campaign-webui", "name": "@govoplan/campaign-webui",
"version": "0.1.18", "version": "0.1.19",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -0,0 +1,74 @@
import { Megaphone, X } from "lucide-react";
import { useCallback } from "react";
import {
Button,
DashboardWidgetList,
DismissibleAlert,
LoadingFrame,
useDashboardWidgetData,
type QuickAccessToolRenderContext
} from "@govoplan/core-webui";
import { listCampaigns } from "../../api/campaigns";
type Props = Pick<
QuickAccessToolRenderContext,
"settings" | "launchContext" | "complete" | "cancel"
>;
export default function CampaignQuickAccess({
settings,
launchContext,
complete,
cancel
}: Props) {
const load = useCallback(
async () => (await listCampaigns(settings))
.filter((campaign) => Boolean(campaign.current_version_id))
.slice(0, 12),
[settings]
);
const { data, loading, error } = useDashboardWidgetData(load, 0);
return (
<LoadingFrame loading={loading} label="Loading authorized Campaigns">
<p className="muted small-note">
Select an exact Campaign version for {launchContext.activeObject?.label}.
Campaign content remains in Campaigns and access is checked again when opened.
</p>
{error ? (
<DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert>
) : null}
<DashboardWidgetList
emptyText="No authorized versioned Campaign is available."
items={(data ?? []).map((campaign) => ({
id: campaign.id,
title: campaign.name,
detail: campaign.description || campaign.status,
meta: campaign.status,
leading: <Megaphone size={17} aria-hidden="true" />,
onClick: () => complete({
contractVersion: "1",
outcome: "completed",
action: "selected",
reference: {
ownerModule: "campaigns",
kind: "campaign",
objectId: campaign.id,
tenantId: launchContext.tenantId,
label: campaign.name,
version: campaign.current_version_id!,
path: `/campaigns/${encodeURIComponent(campaign.id)}`
}
})
}))}
/>
<div className="dashboard-contribution-footer">
<Button onClick={() => cancel("user")}>
<X size={15} aria-hidden="true" /> Cancel selection
</Button>
</div>
</LoadingFrame>
);
}
+12 -1
View File
@@ -6,10 +6,12 @@ import {
type ApiSettings, type ApiSettings,
type AuthInfo, type AuthInfo,
type DashboardWidgetsUiCapability, type DashboardWidgetsUiCapability,
type PlatformWebModule type PlatformWebModule,
type QuickAccessToolsUiCapability
} 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 CampaignQuickAccess from "./features/campaigns/CampaignQuickAccess";
import { OPERATOR_QUEUE_ROUTE_SCOPES } from "./features/operator/operatorQueueAccess"; import { OPERATOR_QUEUE_ROUTE_SCOPES } from "./features/operator/operatorQueueAccess";
import { generatedTranslations } from "./i18n/generatedTranslations"; import { generatedTranslations } from "./i18n/generatedTranslations";
import { campaignWizardDirectories } from "./features/campaigns/wizard/directory"; import { campaignWizardDirectories } from "./features/campaigns/wizard/directory";
@@ -72,6 +74,14 @@ const campaignDashboardWidgets: DashboardWidgetsUiCapability = {
} }
] ]
}; };
const campaignQuickAccessTools: QuickAccessToolsUiCapability = {
tools: [
{
id: "campaigns.select",
render: (context) => createElement(CampaignQuickAccess, context)
}
]
};
export const campaignModule: PlatformWebModule = { export const campaignModule: PlatformWebModule = {
id: "campaigns", id: "campaigns",
@@ -100,6 +110,7 @@ export const campaignModule: PlatformWebModule = {
{ path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 22, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) }], { path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 22, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) }],
uiCapabilities: { uiCapabilities: {
"dashboard.widgets": campaignDashboardWidgets, "dashboard.widgets": campaignDashboardWidgets,
"quickAccess.tools": campaignQuickAccessTools,
"wizard.directories": campaignWizardDirectories "wizard.directories": campaignWizardDirectories
} }