WebUI module split; docs
This commit is contained in:
51
README.md
51
README.md
@@ -1,18 +1,59 @@
|
|||||||
# govoplan-campaign
|
# govoplan-campaign
|
||||||
|
|
||||||
GovOPlaN Campaign module.
|
GovOPlaN Campaign is the campaign authoring, validation, review, sending-control, and reporting module. It bundles backend campaign APIs with the campaign WebUI package.
|
||||||
|
|
||||||
This repository owns the Campaign module manifest, backend router, campaign schemas, campaign ORM models, campaign JSON validation, persistence, reports, message building, mock-send, queue/send control services, and campaign/operator/template/address-book WebUI package. The module currently has hard runtime dependencies on `govoplan-files` and `govoplan-mail` because managed attachments and SMTP/IMAP execution are imported directly.
|
## Ownership
|
||||||
|
|
||||||
The remaining `app.*` imports are transitional core adapters for auth, audit, Celery dispatch, and legacy access models.
|
This repository owns:
|
||||||
|
|
||||||
|
- backend module manifest `campaigns`
|
||||||
|
- campaign, recipient, report, and campaign-audit permissions
|
||||||
|
- 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
|
||||||
|
- WebUI package `@govoplan/campaign-webui`
|
||||||
|
- route contributions for `/campaigns`, `/campaigns/:campaignId/*`, `/operator`, `/reports`, `/address-book`, and `/templates`
|
||||||
|
|
||||||
|
Core owns auth, tenants, RBAC evaluation, database/session primitives, CSRF/API helpers, shell layout, and route rendering. Files and mail own their respective storage and transport capabilities.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
The module has runtime dependencies on:
|
||||||
|
|
||||||
|
- `govoplan-core` for platform services
|
||||||
|
- `govoplan-files` for managed attachment integration
|
||||||
|
- `govoplan-mail` for SMTP/IMAP profile and delivery integration
|
||||||
|
|
||||||
|
Optional UI behavior can still check installed module availability through core module metadata, but the current backend package declares files and mail as required.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Install through the core development environment:
|
Install through the core environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /mnt/DATA/git/govoplan-core
|
cd /mnt/DATA/git/govoplan-core
|
||||||
./.venv/bin/python -m pip install -r requirements-dev.txt
|
./.venv/bin/python -m pip install -r requirements-dev.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
The backend module is registered through the `govoplan.modules` entry point `campaigns`. The frontend package is `@govoplan/campaign-webui`.
|
Run the WebUI from the core host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/DATA/git/govoplan-core/webui
|
||||||
|
PATH=/home/zemion/.nvm/versions/node/v22.22.3/bin:$PATH /home/zemion/.nvm/versions/node/v22.22.3/bin/npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module integration
|
||||||
|
|
||||||
|
Backend entry point:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project.entry-points."govoplan.modules"]
|
||||||
|
campaigns = "govoplan_campaign.backend.manifest:get_manifest"
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend package:
|
||||||
|
|
||||||
|
```text
|
||||||
|
@govoplan/campaign-webui
|
||||||
|
```
|
||||||
|
|
||||||
|
Platform RBAC and governance rules are documented in `govoplan-core/docs/`.
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ manifest = ModuleManifest(
|
|||||||
route_factory=_campaigns_router,
|
route_factory=_campaigns_router,
|
||||||
role_templates=ROLE_TEMPLATES,
|
role_templates=ROLE_TEMPLATES,
|
||||||
nav_items=(
|
nav_items=(
|
||||||
NavItem(path="/campaigns", label="Campaigns", icon="mail-check", required_any=("campaigns:campaign:read",), order=20),
|
NavItem(path="/campaigns", label="Campaigns", icon="mail", required_any=("campaigns:campaign:read",), order=20),
|
||||||
NavItem(
|
NavItem(
|
||||||
path="/operator",
|
path="/operator",
|
||||||
label="Operator Queue",
|
label="Operator Queue",
|
||||||
@@ -128,13 +128,13 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
order=30,
|
order=30,
|
||||||
),
|
),
|
||||||
NavItem(path="/reports", label="Reports", icon="file-text", required_any=("campaigns:report:read",), order=70),
|
NavItem(path="/reports", label="Reports", icon="reports", required_any=("campaigns:report:read",), order=70),
|
||||||
),
|
),
|
||||||
frontend=FrontendModule(
|
frontend=FrontendModule(
|
||||||
module_id="campaigns",
|
module_id="campaigns",
|
||||||
package_name="@govoplan/campaign-webui",
|
package_name="@govoplan/campaign-webui",
|
||||||
nav_items=(
|
nav_items=(
|
||||||
NavItem(path="/campaigns", label="Campaigns", icon="mail-check", required_any=("campaigns:campaign:read",), order=20),
|
NavItem(path="/campaigns", label="Campaigns", icon="mail", required_any=("campaigns:campaign:read",), order=20),
|
||||||
NavItem(
|
NavItem(
|
||||||
path="/operator",
|
path="/operator",
|
||||||
label="Operator Queue",
|
label="Operator Queue",
|
||||||
@@ -148,7 +148,7 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
order=30,
|
order=30,
|
||||||
),
|
),
|
||||||
NavItem(path="/reports", label="Reports", icon="file-text", required_any=("campaigns:report:read",), order=70),
|
NavItem(path="/reports", label="Reports", icon="reports", required_any=("campaigns:report:read",), order=70),
|
||||||
NavItem(path="/address-book", label="Address Book", icon="users", order=80),
|
NavItem(path="/address-book", label="Address Book", icon="users", order=80),
|
||||||
NavItem(path="/templates", label="Templates", icon="form", order=90),
|
NavItem(path="/templates", label="Templates", icon="form", order=90),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { Activity, FileText, Form, MailCheck, Users } from "lucide-react";
|
import { Activity, FileText, Form, MailCheck, Users } from "lucide-react";
|
||||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
import { createElement, lazy, useCallback } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Card, ResourceAccessBoundary, type ApiSettings, type AuthInfo, type PlatformWebModule } from "@govoplan/core-webui";
|
||||||
|
import { getCampaign } from "./api/campaigns";
|
||||||
|
|
||||||
|
const AddressBookPage = lazy(() => import("./features/addressbook/AddressBookPage"));
|
||||||
|
const CampaignListPage = lazy(() => import("./features/campaigns/CampaignListPage"));
|
||||||
|
const CampaignWorkspace = lazy(() => import("./features/campaigns/CampaignWorkspace"));
|
||||||
|
const OperatorQueuePage = lazy(() => import("./features/operator/OperatorQueuePage"));
|
||||||
|
const TemplatesPage = lazy(() => import("./features/templates/TemplatesPage"));
|
||||||
|
|
||||||
|
const campaignRead = ["campaigns:campaign:read"];
|
||||||
|
const operatorScopes = ["campaigns:campaign:queue", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:campaign:control", "campaigns:campaign:send"];
|
||||||
|
|
||||||
export const campaignModule: PlatformWebModule = {
|
export const campaignModule: PlatformWebModule = {
|
||||||
id: "campaigns",
|
id: "campaigns",
|
||||||
@@ -8,18 +20,53 @@ export const campaignModule: PlatformWebModule = {
|
|||||||
dependencies: ["access"],
|
dependencies: ["access"],
|
||||||
optionalDependencies: ["files", "mail"],
|
optionalDependencies: ["files", "mail"],
|
||||||
navItems: [
|
navItems: [
|
||||||
{ to: "/campaigns", label: "Campaigns", icon: MailCheck, anyOf: ["campaigns:campaign:read"], order: 20 },
|
{ to: "/campaigns", label: "Campaigns", icon: MailCheck, anyOf: campaignRead, order: 20 },
|
||||||
{
|
{
|
||||||
to: "/operator",
|
to: "/operator",
|
||||||
label: "Operator Queue",
|
label: "Operator Queue",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
anyOf: ["campaigns:campaign:queue", "campaigns:campaign:retry", "campaigns:campaign:reconcile", "campaigns:campaign:control", "campaigns:campaign:send"],
|
anyOf: operatorScopes,
|
||||||
order: 30
|
order: 30
|
||||||
},
|
},
|
||||||
{ to: "/reports", label: "Reports", icon: FileText, anyOf: ["campaigns:report:read"], order: 70 },
|
{ to: "/reports", label: "Reports", icon: FileText, anyOf: ["campaigns:report:read"], order: 70 },
|
||||||
{ to: "/address-book", label: "Address Book", icon: Users, order: 80 },
|
{ to: "/address-book", label: "Address Book", icon: Users, order: 80 },
|
||||||
{ to: "/templates", label: "Templates", icon: Form, order: 90 }
|
{ to: "/templates", label: "Templates", icon: Form, order: 90 }
|
||||||
|
],
|
||||||
|
routes: [
|
||||||
|
{ path: "/campaigns", anyOf: campaignRead, order: 20, render: ({ settings }) => createElement(CampaignListPage, { settings }) },
|
||||||
|
{ path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 21, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) },
|
||||||
|
{ path: "/operator", anyOf: operatorScopes, order: 30, render: ({ settings }) => createElement(OperatorQueuePage, { settings }) },
|
||||||
|
{ path: "/reports", anyOf: ["campaigns:report:read"], order: 70, render: () => createElement(ReportsPage) },
|
||||||
|
{ path: "/address-book", order: 80, render: () => createElement(AddressBookPage) },
|
||||||
|
{ path: "/templates", order: 90, render: () => createElement(TemplatesPage) }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function CampaignResourceRoute({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
||||||
|
const { campaignId = "" } = useParams();
|
||||||
|
const tenantId = (auth.active_tenant ?? auth.tenant).id;
|
||||||
|
const probe = useCallback(() => getCampaign(settings, campaignId), [settings.apiBaseUrl, settings.apiKey, settings.accessToken, campaignId, tenantId]);
|
||||||
|
|
||||||
|
return createElement(ResourceAccessBoundary, {
|
||||||
|
probe,
|
||||||
|
resetKey: `${tenantId}:${campaignId}`,
|
||||||
|
fallback: "/campaigns",
|
||||||
|
children: createElement(CampaignWorkspace, { settings, auth })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReportsPage() {
|
||||||
|
return createElement(
|
||||||
|
"div",
|
||||||
|
{ className: "content-pad" },
|
||||||
|
createElement(
|
||||||
|
"div",
|
||||||
|
{ className: "page-heading" },
|
||||||
|
createElement("h1", null, "Reports"),
|
||||||
|
createElement("p", null, "This module is prepared but not implemented yet.")
|
||||||
|
),
|
||||||
|
createElement(Card, null, createElement("p", { className: "muted" }, "Next passes will add functionality here."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default campaignModule;
|
export default campaignModule;
|
||||||
|
|||||||
Reference in New Issue
Block a user