chore: sync GovOPlaN module split state
This commit is contained in:
@@ -1,32 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { formatDateTime as formatPlatformDateTime, formatDateTimeFromDate } from "@govoplan/core-webui";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { formatDateTime as formatPlatformDateTime, formatDateTimeFromDate, mergeDeltaRows } from "@govoplan/core-webui";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { StatusBadge } from "@govoplan/core-webui";
|
||||
import { PageTitle } from "@govoplan/core-webui";
|
||||
import { LoadingFrame } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert, i18nMessage, useGuardedNavigate } from "@govoplan/core-webui";
|
||||
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
|
||||
import { createNewCampaign, listCampaigns } from "../../api/campaigns";
|
||||
import { createNewCampaign, listCampaignsDelta, type CampaignDeltaResponse } from "../../api/campaigns";
|
||||
import type { CampaignListItem } from "../../types";
|
||||
|
||||
export default function CampaignListPage({ settings }: { settings: ApiSettings }) {
|
||||
const navigate = useNavigate();
|
||||
export default function CampaignListPage({ settings }: {settings: ApiSettings;}) {
|
||||
const navigate = useGuardedNavigate();
|
||||
const [campaigns, setCampaigns] = useState<CampaignListItem[]>([]);
|
||||
const [error, setError] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [lastLoadedAt, setLastLoadedAt] = useState<string>("");
|
||||
const [campaignDeltaWatermark, setCampaignDeltaWatermark] = useState<string | null>(null);
|
||||
|
||||
async function load() {
|
||||
async function load(forcedSince: string | null | undefined = campaignDeltaWatermark) {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const data = await listCampaigns(settings);
|
||||
setCampaigns(data);
|
||||
let nextWatermark = forcedSince ?? null;
|
||||
let nextCampaigns = campaigns;
|
||||
let response: CampaignDeltaResponse;
|
||||
do {
|
||||
response = await listCampaignsDelta(settings, { since: nextWatermark });
|
||||
nextCampaigns = mergeCampaignDelta(nextCampaigns, response);
|
||||
nextWatermark = response.watermark ?? null;
|
||||
} while (response.has_more);
|
||||
setCampaigns(nextCampaigns);
|
||||
setCampaignDeltaWatermark(nextWatermark);
|
||||
setLastLoadedAt(formatLoadedAt(new Date()));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
@@ -49,114 +58,115 @@ export default function CampaignListPage({ settings }: { settings: ApiSettings }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
setCampaignDeltaWatermark(null);
|
||||
load(null);
|
||||
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
||||
|
||||
const columns: DataGridColumn<CampaignListItem>[] = [
|
||||
{
|
||||
id: "campaign",
|
||||
header: "Campaign",
|
||||
width: "minmax(260px, 1.8fr)",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
sticky: "start",
|
||||
render: (campaign) => (
|
||||
<div>
|
||||
{
|
||||
id: "campaign",
|
||||
header: "i18n:govoplan-campaign.campaign.69390e16",
|
||||
width: "minmax(260px, 1.8fr)",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
sticky: "start",
|
||||
render: (campaign) =>
|
||||
<div>
|
||||
<Link className="table-primary-link" to={`/campaigns/${campaign.id}`}>
|
||||
{campaign.name || campaign.external_id || campaign.id}
|
||||
</Link>
|
||||
<div className="table-subline">{campaign.description || campaign.external_id || campaign.id}</div>
|
||||
</div>
|
||||
),
|
||||
value: (campaign) => `${campaign.name ?? ""} ${campaign.external_id ?? ""} ${campaign.id}`
|
||||
</div>,
|
||||
|
||||
value: (campaign) => `${campaign.name ?? ""} ${campaign.external_id ?? ""} ${campaign.id}`
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "i18n:govoplan-campaign.status.bae7d5be",
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
columnType: "from-list",
|
||||
list: {
|
||||
options: [
|
||||
{ value: "draft", label: "i18n:govoplan-campaign.draft.23d33e22" },
|
||||
{ value: "active", label: "i18n:govoplan-campaign.active.a733b809" },
|
||||
{ value: "completed", label: "i18n:govoplan-campaign.completed.1798b3ba" },
|
||||
{ value: "archived", label: "i18n:govoplan-campaign.archived.eddc813f" }],
|
||||
|
||||
display: "pill"
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
width: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
columnType: "from-list",
|
||||
list: {
|
||||
options: [
|
||||
{ value: "draft", label: "Draft" },
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "completed", label: "Completed" },
|
||||
{ value: "archived", label: "Archived" }
|
||||
],
|
||||
display: "pill"
|
||||
},
|
||||
render: (campaign) => <StatusBadge status={campaign.status || "draft"} />,
|
||||
value: (campaign) => campaign.status || "draft"
|
||||
},
|
||||
{
|
||||
id: "current_version",
|
||||
header: "Current version",
|
||||
width: 170,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
className: "version-cell mono-small",
|
||||
render: (campaign) => <span title={campaign.current_version_id || undefined}>{campaign.current_version_id ? shortId(campaign.current_version_id) : "—"}</span>,
|
||||
value: (campaign) => campaign.current_version_id ?? ""
|
||||
},
|
||||
{
|
||||
id: "updated",
|
||||
header: "Updated",
|
||||
width: 190,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterType: "date",
|
||||
className: "updated-cell",
|
||||
render: (campaign) => formatDateTime(campaign.updated_at ?? campaign.updatedAt ?? campaign.created_at),
|
||||
value: (campaign) => campaign.updated_at ?? campaign.updatedAt ?? campaign.created_at ?? ""
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
width: 70,
|
||||
sticky: "end",
|
||||
align: "right",
|
||||
render: (campaign) => (
|
||||
<Link
|
||||
to={`/campaigns/${campaign.id}`}
|
||||
className="btn btn-primary admin-icon-button"
|
||||
aria-label={`Open ${campaign.name || campaign.external_id || campaign.id}`}
|
||||
title={`Open ${campaign.name || campaign.external_id || campaign.id}`}
|
||||
>
|
||||
render: (campaign) => <StatusBadge status={campaign.status || "draft"} />,
|
||||
value: (campaign) => campaign.status || "draft"
|
||||
},
|
||||
{
|
||||
id: "current_version",
|
||||
header: "i18n:govoplan-campaign.current_version.7be72582",
|
||||
width: 170,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
className: "version-cell mono-small",
|
||||
render: (campaign) => <span title={campaign.current_version_id || undefined}>{campaign.current_version_id ? shortId(campaign.current_version_id) : "—"}</span>,
|
||||
value: (campaign) => campaign.current_version_id ?? ""
|
||||
},
|
||||
{
|
||||
id: "updated",
|
||||
header: "i18n:govoplan-campaign.updated.f2f8570d",
|
||||
width: 190,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterType: "date",
|
||||
className: "updated-cell",
|
||||
render: (campaign) => formatDateTime(campaign.updated_at ?? campaign.updatedAt ?? campaign.created_at),
|
||||
value: (campaign) => campaign.updated_at ?? campaign.updatedAt ?? campaign.created_at ?? ""
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "i18n:govoplan-campaign.actions.c3cd636a",
|
||||
width: 70,
|
||||
sticky: "end",
|
||||
align: "right",
|
||||
render: (campaign) =>
|
||||
<Link
|
||||
to={`/campaigns/${campaign.id}`}
|
||||
className="btn btn-primary admin-icon-button"
|
||||
aria-label={i18nMessage("i18n:govoplan-campaign.open_value.a34416a9", { value0: campaign.name || campaign.external_id || campaign.id })}
|
||||
title={i18nMessage("i18n:govoplan-campaign.open_value.a34416a9", { value0: campaign.name || campaign.external_id || campaign.id })}>
|
||||
|
||||
<ExternalLink aria-hidden="true" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
}];
|
||||
|
||||
|
||||
return (
|
||||
<div className="content-pad campaigns-page">
|
||||
<div className="content-pad workspace-data-page campaigns-page">
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||
|
||||
<div className="page-heading split workspace-heading">
|
||||
<div>
|
||||
<PageTitle loading={loading}>All campaigns</PageTitle>
|
||||
<p className="mono-small">{lastLoadedAt ? `Last loaded: ${lastLoadedAt}` : "Not loaded yet"}</p>
|
||||
<PageTitle loading={loading}>i18n:govoplan-campaign.all_campaigns.2bd1ee3a</PageTitle>
|
||||
<p className="mono-small">{lastLoadedAt ? i18nMessage("i18n:govoplan-campaign.last_loaded_value.35ef046a", { value0: lastLoadedAt }) : "i18n:govoplan-campaign.not_loaded_yet.9968c191"}</p>
|
||||
</div>
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={load} disabled={loading}>Reload</Button>
|
||||
<Button onClick={load} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>
|
||||
<Button variant="primary" onClick={create} disabled={creating}>
|
||||
{creating ? "Creating…" : "New campaign"}
|
||||
{creating ? "i18n:govoplan-campaign.creating.94d7d8ee" : "i18n:govoplan-campaign.new_campaign.aaf9a8a4"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card title="Campaigns">
|
||||
<LoadingFrame loading={loading} label="Loading campaigns…">
|
||||
{campaigns.length === 0 ? (
|
||||
<Card title="i18n:govoplan-campaign.campaigns.01a23a28">
|
||||
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_campaigns.90a466bb">
|
||||
{campaigns.length === 0 ?
|
||||
<div className="empty-state">
|
||||
<h2>No campaigns yet</h2>
|
||||
<p>Start with a guided campaign draft. The WebUI will create a portable campaign JSON in the background.</p>
|
||||
<h2>i18n:govoplan-campaign.no_campaigns_yet.926409b3</h2>
|
||||
<p>i18n:govoplan-campaign.start_with_a_guided_campaign_draft_the_webui_wil.c8a675d0</p>
|
||||
<Button variant="primary" onClick={create} disabled={creating}>
|
||||
Create first campaign
|
||||
i18n:govoplan-campaign.create_first_campaign.9be974a4
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
</div> :
|
||||
|
||||
<div className="admin-table-surface campaigns-table-surface">
|
||||
<DataGrid
|
||||
id="campaigns"
|
||||
@@ -165,14 +175,14 @@ export default function CampaignListPage({ settings }: { settings: ApiSettings }
|
||||
getRowKey={(campaign) => campaign.id}
|
||||
initialSort={{ columnId: "updated", direction: "desc" }}
|
||||
className="campaign-table-wrap"
|
||||
emptyText="No campaigns found."
|
||||
/>
|
||||
emptyText="i18n:govoplan-campaign.no_campaigns_found.22d297f9" />
|
||||
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
</LoadingFrame>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
</div>);
|
||||
|
||||
}
|
||||
|
||||
function shortId(value: string): string {
|
||||
@@ -187,3 +197,17 @@ function formatDateTime(value?: string): string {
|
||||
function formatLoadedAt(value: Date): string {
|
||||
return formatDateTimeFromDate(value, { second: "2-digit" });
|
||||
}
|
||||
|
||||
function mergeCampaignDelta(current: CampaignListItem[], response: CampaignDeltaResponse): CampaignListItem[] {
|
||||
if (response.full) return response.campaigns;
|
||||
return mergeDeltaRows(current, response.campaigns, response.deleted, (campaign) => campaign.id, {
|
||||
deletedResourceType: "campaign",
|
||||
sort: sortCampaignsByUpdatedDesc
|
||||
});
|
||||
}
|
||||
|
||||
function sortCampaignsByUpdatedDesc(left: CampaignListItem, right: CampaignListItem): number {
|
||||
return String(right.updated_at ?? right.updatedAt ?? right.created_at ?? "").localeCompare(
|
||||
String(left.updated_at ?? left.updatedAt ?? left.created_at ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user