feat(campaigns): add Case Quick Access selector
Module Package Release / publish-packages (push) Successful in 13s
Module Package Release / publish-packages (push) Successful in 13s
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-campaign"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
description = "GovOPlaN campaigns module with backend and WebUI integration."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -36,6 +36,7 @@ from govoplan_core.core.modules import (
|
||||
NavItem,
|
||||
PermissionDefinition,
|
||||
ProductAreaContribution,
|
||||
QuickAccessTool,
|
||||
RoleTemplate,
|
||||
)
|
||||
from govoplan_core.core.reporting import REPORT_PROVIDER_CAPABILITY_PREFIX
|
||||
@@ -384,7 +385,7 @@ def _campaigns_router(context: ModuleContext):
|
||||
manifest = ModuleManifest(
|
||||
id="campaigns",
|
||||
name="Campaigns",
|
||||
version="0.1.18",
|
||||
version="0.1.19",
|
||||
required_capabilities=(
|
||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||
@@ -617,6 +618,32 @@ manifest = ModuleManifest(
|
||||
label="Campaign activity widget",
|
||||
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(
|
||||
@@ -668,6 +695,48 @@ manifest = ModuleManifest(
|
||||
),
|
||||
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(
|
||||
id="campaigns.privacy.data-subject-requests",
|
||||
title="Review Campaign data in a data-subject request",
|
||||
|
||||
@@ -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.frontend.nav_items}
|
||||
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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/campaign-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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
@@ -6,10 +6,12 @@ import {
|
||||
type ApiSettings,
|
||||
type AuthInfo,
|
||||
type DashboardWidgetsUiCapability,
|
||||
type PlatformWebModule
|
||||
type PlatformWebModule,
|
||||
type QuickAccessToolsUiCapability
|
||||
} from "@govoplan/core-webui";
|
||||
import { getCampaign } from "./api/campaigns";
|
||||
import CampaignActivityWidget from "./features/campaigns/CampaignActivityWidget";
|
||||
import CampaignQuickAccess from "./features/campaigns/CampaignQuickAccess";
|
||||
import { OPERATOR_QUEUE_ROUTE_SCOPES } from "./features/operator/operatorQueueAccess";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
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 = {
|
||||
id: "campaigns",
|
||||
@@ -100,6 +110,7 @@ export const campaignModule: PlatformWebModule = {
|
||||
{ path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 22, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) }],
|
||||
uiCapabilities: {
|
||||
"dashboard.widgets": campaignDashboardWidgets,
|
||||
"quickAccess.tools": campaignQuickAccessTools,
|
||||
"wizard.directories": campaignWizardDirectories
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user