refactor: adopt shared page and workspace layouts

This commit is contained in:
2026-08-18 02:17:16 +02:00
parent c2efd6b7bd
commit 68459fde15
20 changed files with 314 additions and 287 deletions
+5 -2
View File
@@ -1,8 +1,11 @@
# Campaign Accessibility Review
The Campaign WebUI uses Core's semantic `Button`, `Dialog`, `DataGrid`, form,
alert, and segmented-control components. Core owns focus trapping, focus return,
Escape handling, labels, disabled state, and keyboard behavior for those shared
alert, and segmented-control components. Its page frames use Core `PageLayout`
and its full-canvas navigation/content shells use `WorkspaceLayout`, so pane
scrolling, sticky headings, action collapse, narrow-layout behavior, and help
scope remain platform-owned. Core also owns focus trapping, focus return,
Escape handling, labels, disabled state, and keyboard behavior for the shared
primitives.
## Repeatable review matrix
@@ -4,7 +4,7 @@ import { useGuardedNavigate, usePlatformModuleInstalled, usePlatformUiCapability
import type { ApiSettings } from "../../types";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
@@ -247,22 +247,24 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-campaign.attachments.6771ade6</PageTitle>
<VersionLine version={version} versions={data.versions} status={saveState} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title="i18n:govoplan-campaign.attachments.6771ade6"
description={<VersionLine version={version} versions={data.versions} status={saveState} />}
headerLoading={loading}
error={error}
actions={
<>
{filesModuleInstalled && <Button onClick={() => navigate("/files")}>i18n:govoplan-campaign.manage_files.90a419f7</Button>}
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</Button>
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!canSave}>i18n:govoplan-campaign.save.efc007a3</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
</>
}
notices={(localError || locked) ? <>
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={data.campaign?.current_version_id} reload={reload} message="i18n:govoplan-campaign.this_page_is_read_only_for_the_selected_version.dacf5743" />}
</> : undefined}
>
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
<>
@@ -452,7 +454,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
onConfirm={confirmIndividualDisable}
onCancel={() => setIndividualDisable(null)} />
</div>);
</PageLayout>);
}
@@ -1,11 +1,8 @@
import type { ApiSettings } from "../../types";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
import { LoadingFrame } from "@govoplan/core-webui";
import { ActionBlockerHint, DocumentationHelpLink } from "@govoplan/core-webui";
import { ActionBlockerHint, DocumentationHelpLink, PageLayout } from "@govoplan/core-webui";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
export default function CampaignAuditPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
@@ -13,20 +10,17 @@ export default function CampaignAuditPage({ settings, campaignId }: {settings: A
const version = data.currentVersion;
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-campaign.audit_log.3cfc5f1c</PageTitle>
<VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title="i18n:govoplan-campaign.audit_log.3cfc5f1c"
description={<VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at} />}
loading={loading}
loadingLabel="i18n:govoplan-campaign.loading_audit_data.af52b968"
error={error}
actions={
<Button onClick={() => void reload({ force: true })} disabled={loading}>Discard</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_audit_data.af52b968">
}
>
<Card title="i18n:govoplan-campaign.recent_audit_events.7ec32b1d">
<ActionBlockerHint
tone="info"
@@ -50,7 +44,6 @@ export default function CampaignAuditPage({ settings, campaignId }: {settings: A
label="Open Campaign assurance documentation"
/>
</Card>
</LoadingFrame>
</div>);
</PageLayout>);
}
@@ -1,7 +1,7 @@
import { useMemo, useRef } from "react";
import type { ApiSettings } from "../../types";
import { Button } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageLayout } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import LockedVersionNotice from "./components/LockedVersionNotice";
@@ -150,22 +150,24 @@ export default function CampaignFieldsPage({ settings, campaignId }: {settings:
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-campaign.fields.e8b68527</PageTitle>
<VersionLine version={version} versions={data.versions} status={saveState} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title="i18n:govoplan-campaign.fields.e8b68527"
description={<VersionLine version={version} versions={data.versions} status={saveState} />}
headerLoading={loading}
error={error}
actions={
<>
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</Button>
<Button variant="primary" onClick={saveFields} disabled={!canSave}>i18n:govoplan-campaign.save.efc007a3</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
</>
}
notices={(localError || fieldNameWarning || locked) ? <>
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{fieldNameWarning && <DismissibleAlert tone="warning" resetKey={fieldNameWarning} floating>{fieldNameWarning}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={data.campaign?.current_version_id} reload={reload} message="i18n:govoplan-campaign.this_page_is_read_only_for_the_selected_version.dacf5743" />}
</> : undefined}
>
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
<>
@@ -184,7 +186,7 @@ export default function CampaignFieldsPage({ settings, campaignId }: {settings:
</Card>
</>
</LoadingFrame>
</div>);
</PageLayout>);
}
@@ -1,10 +1,8 @@
import type { ApiSettings } from "../../types";
import { Card } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { DismissibleAlert, PageLayout } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
import { LoadingFrame } from "@govoplan/core-webui";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { asRecord, getCampaignJson } from "./utils/campaignView";
import { downloadJson, safeFileStem } from "./utils/draftEditor";
@@ -17,28 +15,30 @@ export default function CampaignJsonView({ settings, campaignId }: {settings: Ap
const filename = `${safeFileStem(String(campaign.id || data.campaign?.external_id || campaignId))}.json`;
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-campaign.json.031a4e76</PageTitle>
<VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title="i18n:govoplan-campaign.json.031a4e76"
description={<VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at} />}
loading={loading}
loadingLabel="i18n:govoplan-campaign.loading_json.812c7a50"
error={error}
actions={
<>
<Button onClick={() => void reload({ force: true })} disabled={loading}>Discard</Button>
<Button onClick={() => downloadJson(filename, campaignJson)} disabled={!version}>i18n:govoplan-campaign.download_json.d296a30a</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
</>
}
notices={
<DismissibleAlert tone="warning" dismissible={false}>
This expert view contains the complete authorized campaign configuration,
including recipient and message fields that may contain personal data.
Download and share it only for an authorized purpose.
</DismissibleAlert>
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_json.812c7a50">
}
>
<Card>
{!loading || version ? <pre className="code-panel">{JSON.stringify(campaignJson, null, 2)}</pre> : <pre className="code-panel">{"{}"}</pre>}
</Card>
</LoadingFrame>
</div>);
</PageLayout>);
}
@@ -6,10 +6,8 @@ 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 { PageScrollViewport } from "@govoplan/core-webui";
import { DismissibleAlert, TableActionGroup, i18nMessage, useGuardedNavigate } from "@govoplan/core-webui";
import { PageLayout, TableActionGroup, i18nMessage, useGuardedNavigate } from "@govoplan/core-webui";
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
import { createNewCampaign, listCampaignsDelta, type CampaignDeltaResponse } from "../../api/campaigns";
import type { CampaignListItem } from "../../types";
@@ -144,22 +142,22 @@ export default function CampaignListPage({ settings }: {settings: ApiSettings;})
return (
<PageScrollViewport>
<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}>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">
<PageLayout
mode="workspace"
className="campaigns-page"
title="i18n:govoplan-campaign.all_campaigns.2bd1ee3a"
description={<p className="mono-small">{lastLoadedAt ? i18nMessage("i18n:govoplan-campaign.last_loaded_value.35ef046a", { value0: lastLoadedAt }) : "i18n:govoplan-campaign.not_loaded_yet.9968c191"}</p>}
headerLoading={loading}
error={error}
actions={
<>
<Button onClick={() => void load(null)} disabled={loading}>Discard</Button>
<Button variant="primary" onClick={create} disabled={creating}>
{creating ? "i18n:govoplan-campaign.creating.94d7d8ee" : "i18n:govoplan-campaign.new_campaign.aaf9a8a4"}
</Button>
</div>
</div>
</>
}
>
<Card title="i18n:govoplan-campaign.campaigns.01a23a28">
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_campaigns.90a466bb">
@@ -186,8 +184,7 @@ export default function CampaignListPage({ settings }: {settings: ApiSettings;})
}
</LoadingFrame>
</Card>
</div>
</PageScrollViewport>);
</PageLayout>);
}
@@ -1,6 +1,7 @@
import { lazy } from "react";
import {
ModuleSubnav,
WorkspaceLayout,
hasAnyScope,
useGuardedNavigate,
type ApiSettings,
@@ -64,15 +65,19 @@ export default function CampaignModulePage({
}
return (
<div className="workspace campaign-module-workspace">
<ModuleSubnav active={active} groups={groups} onSelect={select} />
<section className="workspace-content">
<WorkspaceLayout
className="campaign-module-workspace"
primary={<ModuleSubnav active={active} groups={groups} onSelect={select} />}
primaryLabel="Campaign module sections"
contentLabel="Campaign module content"
interfaceId="campaign.module-workspace"
helpModuleId="campaign"
>
{active === "queue"
? <OperatorQueuePage settings={settings} auth={auth} />
: active === "reports"
? <AggregateReportsPage settings={settings} />
: <CampaignListPage settings={settings} />}
</section>
</div>
</WorkspaceLayout>
);
}
@@ -9,7 +9,7 @@ import { Dialog } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui";
import { DismissibleAlert, TableActionGroup, hasScope, i18nMessage, useGuardedNavigate, useUnsavedDraftGuard } from "@govoplan/core-webui";
@@ -332,13 +332,15 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
}
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>{campaign?.name || "i18n:govoplan-campaign.overview.0efc2e6b"}</PageTitle>
<p className="mono-small">i18n:govoplan-campaign.campaign_overview_version_independent_identity_a.ebaf1113</p>
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title={campaign?.name || "i18n:govoplan-campaign.overview.0efc2e6b"}
description={<p className="mono-small">i18n:govoplan-campaign.campaign_overview_version_independent_identity_a.ebaf1113</p>}
headerLoading={loading}
error={error}
success={message}
actions={
<>
{canCopy && data.currentVersion && <Button
onClick={() => void prepareLifecycleAction("copy_campaign", data.currentVersion ?? undefined)}
disabled={loading || savingIdentity || lockBusy || lifecycleBusy || identityDirty}
@@ -371,11 +373,9 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
</Button>}
<Button onClick={() => void discardOverview()} disabled={loading || savingIdentity || lockBusy}>Discard</Button>
<Button variant="primary" onClick={() => void saveIdentity()} disabled={!campaign || !identityDirty || savingIdentity}>{savingIdentity ? "i18n:govoplan-campaign.saving.56a2285c" : "i18n:govoplan-campaign.save.efc007a3"}</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{message && <DismissibleAlert tone="success" resetKey={message} floating>{message}</DismissibleAlert>}
</>
}
>
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_campaign_overview.ffa1adf0">
<div className="metric-grid campaign-overview-metrics">
@@ -618,7 +618,7 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
onCancel={() => setPendingLockAction(null)}
onConfirm={() => void applyLockAction()} />
</div>);
</PageLayout>);
}
@@ -21,7 +21,7 @@ import { DataGrid, type DataGridColumn, type DataGridListOption, type DataGridQu
import { Dialog } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
import { LoadingFrame, TableActionGroup, ToggleSwitch, i18nMessage } from "@govoplan/core-webui";
@@ -445,20 +445,21 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
[busyAction, retryFailedSynchronously]);
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-campaign.report.ee45c303</PageTitle>
<VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at ?? data.summary?.generated_at} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title="i18n:govoplan-campaign.report.ee45c303"
description={<VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at ?? data.summary?.generated_at} />}
headerLoading={loading}
error={error || actionError}
success={actionMessage}
actions={
<>
<Button onClick={() => void exportCsv()} disabled={busyAction === "csv"}>i18n:govoplan-campaign.download_csv.eaa216ad</Button>
<Button onClick={() => setEmailOpen(true)}>i18n:govoplan-campaign.email_report.ee3e7091</Button>
<Button onClick={() => void reloadAll()} disabled={loading || jobsLoading}>Discard</Button>
</div>
</div>
{(error || actionError) && <DismissibleAlert tone="danger" resetKey={`${error}${actionError}`} floating>{error || actionError}</DismissibleAlert>}
{actionMessage && <DismissibleAlert tone="success" resetKey={actionMessage} floating>{actionMessage}</DismissibleAlert>}
</>
}
>
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_report_data.0908ade5">
<div className="dashboard-grid">
<Card title="i18n:govoplan-campaign.delivery_outcome.f9d7c085">
@@ -647,7 +648,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
onConfirm={() => void reconcileOutcome()}
onCancel={() => setReconcile(null)} />
</div>);
</PageLayout>);
}
@@ -2,6 +2,7 @@ import { lazy, useEffect, useState } from "react";
import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from "react-router";
import {
ConcurrencyConflictProvider,
WorkspaceLayout,
useGuardedNavigate
} from "@govoplan/core-webui";
import type { ApiSettings, AuthInfo, CampaignWorkspaceSection } from "../../types";
@@ -85,9 +86,13 @@ function CampaignWorkspaceInner({ settings, auth }: { settings: ApiSettings; aut
}
return (
<div className="workspace">
<SectionSidebar active={active} onSelect={select} />
<section className="workspace-content">
<WorkspaceLayout
primary={<SectionSidebar active={active} onSelect={select} />}
primaryLabel="Campaign sections"
contentLabel="Campaign workspace"
interfaceId="campaign.workspace"
helpModuleId="campaign"
>
<Routes>
<Route index element={<CampaignOverviewPage settings={settings} auth={auth} campaignId={campaignId || ""} />} />
<Route path="data" element={<Navigate to="../recipients" replace />} />
@@ -121,8 +126,7 @@ function CampaignWorkspaceInner({ settings, auth }: { settings: ApiSettings; aut
<Route path="Review" element={<Navigate to="../" replace />} />
<Route path="*" element={<Navigate to="../" replace />} />
</Routes>
</section>
</div>
</WorkspaceLayout>
);
}
@@ -10,7 +10,7 @@ import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { PolicyRow } from "@govoplan/core-webui";
import { PolicyTable } from "@govoplan/core-webui";
@@ -178,21 +178,23 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
}
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>{pageTitle}</PageTitle>
<VersionLine version={version} versions={data.versions} status={saveState} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title={pageTitle}
description={<VersionLine version={version} versions={data.versions} status={saveState} />}
headerLoading={loading}
error={error}
actions={
<>
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</Button>
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"}</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
</>
}
notices={(localError || locked) ? <>
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={data.campaign?.current_version_id} reload={reload} message="i18n:govoplan-campaign.this_page_is_read_only_for_the_selected_version.dacf5743" />}
</> : undefined}
>
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
{isPolicyView ?
@@ -500,7 +502,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
onClose={() => setPostboxTargetsOpen(false)}
/>
}
</div>);
</PageLayout>);
}
@@ -8,7 +8,7 @@ import {
LoadingFrame,
MailServerFolderLookupResultView,
MetricCard,
PageTitle,
PageLayout,
PasswordField,
ToggleSwitch,
usePlatformModuleInstalled,
@@ -326,21 +326,23 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
}
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>{isPolicyView ? "i18n:govoplan-campaign.mail_policy.3eb5d32a" : "i18n:govoplan-campaign.mail_settings.19e07f55"}</PageTitle>
<VersionLine version={version} versions={data.versions} status={saveState} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title={isPolicyView ? "i18n:govoplan-campaign.mail_policy.3eb5d32a" : "i18n:govoplan-campaign.mail_settings.19e07f55"}
description={<VersionLine version={version} versions={data.versions} status={saveState} />}
headerLoading={loading}
error={error}
actions={
<>
{!isPolicyView && <Button variant="primary" onClick={() => void saveDraft("manual")} disabled={!canSave}>{dirty ? "i18n:govoplan-campaign.save_now.3989b7c0" : "i18n:govoplan-campaign.saved.c0ae8f6e"}</Button>}
<Button onClick={() => void discardDraft()} disabled={loading}>i18n:govoplan-campaign.discard.36fff63c</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
</>
}
notices={(localError || locked) ? <>
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={data.campaign?.current_version_id} reload={reload} message="i18n:govoplan-campaign.this_page_is_read_only_for_the_selected_version.dacf5743" />}
</> : undefined}
>
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
<>
@@ -501,7 +503,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
</Card>}
</>
</LoadingFrame>
</div>
</PageLayout>
);
}
+17 -18
View File
@@ -42,7 +42,7 @@ import { DataGrid, type DataGridQueryState } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { ConfirmDialog } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui";
import { i18nMessage } from "@govoplan/core-webui";
@@ -1364,24 +1364,23 @@ export default function ReviewSendPage({
reviewedExplicitCount === explicitReviewCount;
return (
<div className="content-pad workspace-data-page review-send-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading || Boolean(busy)}>i18n:govoplan-campaign.review_send.1627617d</PageTitle>
<VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
className="review-send-page"
title="i18n:govoplan-campaign.review_send.1627617d"
description={<VersionLine version={version} versions={data.versions} loadedAt={version?.updated_at} />}
headerLoading={loading || Boolean(busy)}
error={error}
actions={
<>
<Button onClick={() => void refreshQueueStatus(false)} disabled={!version || queueStatusLoading || Boolean(busy)}>
{queueStatusLoading ? "i18n:govoplan-campaign.refreshing_status.d8965739" : "i18n:govoplan-campaign.refresh_status.ade15a52"}
</Button>
<Button onClick={() => void reload({ force: true })} disabled={loading || Boolean(busy)}>Discard</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
</>
}
notices={(message || (version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion))) ? <>
{message && <DismissibleAlert tone="info" resetKey={message} floating>{message}</DismissibleAlert>}
{version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion) &&
<LockedVersionNotice
settings={settings}
@@ -1389,9 +1388,9 @@ export default function ReviewSendPage({
version={version}
currentVersionId={data.campaign?.current_version_id}
reload={reload}
message="i18n:govoplan-campaign.this_workflow_is_read_only_for_the_selected_vers.6626b342" />
}
message="i18n:govoplan-campaign.this_workflow_is_read_only_for_the_selected_vers.6626b342" />}
</> : undefined}
>
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_workflow_state.5319aae1">
<WorkflowNavigation stages={stages} onSelect={scrollToStage} />
@@ -2113,6 +2112,6 @@ export default function ReviewSendPage({
onClose={() => setSelectedMockMessage(null)} />
}
</div>);
</PageLayout>);
}
@@ -16,7 +16,7 @@ import { Card } from "@govoplan/core-webui";
import { Dialog } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { DismissibleAlert, SegmentedControl, ToggleSwitch, i18nMessage } from "@govoplan/core-webui";
import { WysiwygEditor, type WysiwygEditorHandle } from "@govoplan/core-webui/wysiwyg";
@@ -411,23 +411,25 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-campaign.template.3ec1ae06</PageTitle>
<VersionLine version={version} versions={data.versions} status={saveState} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title="i18n:govoplan-campaign.template.3ec1ae06"
description={<VersionLine version={version} versions={data.versions} status={saveState} />}
headerLoading={loading}
error={error}
success={contentLibraryNotice}
actions={
<>
<Button onClick={() => window.location.assign("/templates")}>i18n:govoplan-campaign.manage_templates.23688071</Button>
<Button onClick={() => void discardDraft()} disabled={loading}>Discard</Button>
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>i18n:govoplan-campaign.save.efc007a3</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
</>
}
notices={(localError || locked) ? <>
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{contentLibraryNotice && <DismissibleAlert tone="success" resetKey={contentLibraryNotice} floating>{contentLibraryNotice}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={data.campaign?.current_version_id} reload={reload} message="i18n:govoplan-campaign.this_page_is_read_only_for_the_selected_version.dacf5743" />}
</> : undefined}
>
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
<>
@@ -783,7 +785,7 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
localFields={localFieldOptions}
globalFields={globalFieldOptions} />
</div>);
</PageLayout>);
}
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { Button, DismissibleAlert, LoadingFrame, PageTitle } from "@govoplan/core-webui";
import { Button, DismissibleAlert, PageLayout } from "@govoplan/core-webui";
import type { ApiSettings } from "../../../types";
import type { CampaignVersionDetail, CampaignVersionListItem } from "../../../api/campaigns";
import LockedVersionNotice from "./LockedVersionNotice";
@@ -43,25 +43,26 @@ export default function CampaignDraftPageScaffold({
children
}: CampaignDraftPageScaffoldProps) {
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>{title}</PageTitle>
<VersionLine version={version} versions={versions} status={saveState} />
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
title={title}
description={<VersionLine version={version} versions={versions} status={saveState} />}
loading={loading || !draft}
loadingLabel="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50"
error={error || ""}
actions={
<>
<Button onClick={onReload} disabled={loading}>Discard</Button>
<Button variant="primary" onClick={onSave} disabled={!dirty || locked || !draft}>i18n:govoplan-campaign.save.efc007a3</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
</>
}
notices={(localError || locked) ?
<>
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={currentVersionId} reload={onReload} message="i18n:govoplan-campaign.this_page_is_read_only_for_the_selected_version.dacf5743" />}
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
<>{children}</>
</LoadingFrame>
</div>);
</> : undefined}
>
{children}
</PageLayout>);
}
@@ -1,6 +1,6 @@
import { Link } from "react-router";
import {
PageTitle,
PageLayout,
WizardDirectory,
usePlatformUiCapabilities,
wizardEntriesForContext,
@@ -38,18 +38,20 @@ export default function WizardDirectoryPage({
});
return (
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<PageTitle>i18n:govoplan-campaign.guided_workflows</PageTitle>
<PageLayout
mode="workspace"
title="i18n:govoplan-campaign.guided_workflows"
actions={
<Link className="btn btn-secondary" to="../">
i18n:govoplan-campaign.back_to_overview.ec986cba
</Link>
</div>
}
>
<WizardDirectory
entries={entries}
auth={auth}
emptyText="i18n:govoplan-campaign.no_guided_workflows"
/>
</div>
</PageLayout>
);
}
@@ -25,11 +25,9 @@ import {
type DataGridListOption,
type DataGridQueryState
} from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { DismissibleAlert, PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageScrollViewport } from "@govoplan/core-webui";
import {
StatusBadge,
TableActionGroup,
@@ -757,22 +755,22 @@ export default function OperatorQueuePage({ settings, auth }: {settings: ApiSett
], [navigate, permissions.canOpenReport, selectedRow, selectedVersionId]);
return (
<PageScrollViewport>
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>i18n:govoplan-campaign.operator_queue.72492fb5</PageTitle>
<PageLayout
mode="workspace"
title="i18n:govoplan-campaign.operator_queue.72492fb5"
description={
<>
<p>i18n:govoplan-campaign.queue_state_retry_candidates_and_reconciliation_.1b592bbe</p>
<p className="muted small-note">i18n:govoplan-campaign.queue_refreshes_automatically_while_delivery_is_active.df8dc205</p>
</div>
<div className="button-row compact-actions">
</>
}
error={error}
success={message}
actions={
<Button onClick={() => void refreshAll()} disabled={loading || jobsLoading}>i18n:govoplan-campaign.refresh.56e3badc</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{jobsError && <DismissibleAlert tone="danger" resetKey={jobsError} floating>{jobsError}</DismissibleAlert>}
{message && <DismissibleAlert tone="success" resetKey={message} floating>{message}</DismissibleAlert>}
}
notices={jobsError ? <DismissibleAlert tone="danger" resetKey={jobsError} floating>{jobsError}</DismissibleAlert> : undefined}
>
<section className="queue-pressure-section" aria-labelledby="operator-queue-pressure-title">
<h2 id="operator-queue-pressure-title" className="queue-pressure-heading">i18n:govoplan-campaign.queue_pressure.28eee15e</h2>
@@ -847,8 +845,7 @@ export default function OperatorQueuePage({ settings, auth }: {settings: ApiSett
if (cancelTarget) void runAction(cancelTarget, "cancel");
}}
onCancel={() => setCancelTarget(null)} />
</div>
</PageScrollViewport>);
</PageLayout>);
}
@@ -5,11 +5,9 @@ import {
Button,
Card,
DataGrid,
DismissibleAlert,
LoadingFrame,
MetricCard,
PageScrollViewport,
PageTitle,
PageLayout,
StatusBadge,
TableActionGroup,
formatDateTime,
@@ -167,24 +165,21 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
const outcomes = report?.outcomes;
return (
<PageScrollViewport className="aggregate-reports-page">
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={listLoading || reportLoading}>i18n:govoplan-campaign.reports.88bc3fe3</PageTitle>
<p className="muted">i18n:govoplan-campaign.privacy_protected_campaign_outcomes_without_reci.5b8a100d</p>
</div>
<div className="button-row compact-actions">
<PageLayout
mode="workspace"
className="aggregate-reports-page"
title="i18n:govoplan-campaign.reports.88bc3fe3"
description="i18n:govoplan-campaign.privacy_protected_campaign_outcomes_without_reci.5b8a100d"
error={error}
actions={
<Button
onClick={() => void Promise.all([loadCampaigns(), selectedFromUrl ? loadReport(selectedFromUrl) : Promise.resolve()])}
disabled={listLoading || reportLoading}
>
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-campaign.refresh.56e3badc
</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
}
>
<Card title="i18n:govoplan-campaign.campaign_reports_available_to_you.f14fa403">
<LoadingFrame loading={listLoading} label="i18n:govoplan-campaign.loading_campaign_reports_.61ec1ee8">
@@ -252,8 +247,7 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
</>
)}
</LoadingFrame>
</div>
</PageScrollViewport>
</PageLayout>
);
}
+30 -29
View File
@@ -2,8 +2,7 @@ import { useMemo, useState } from "react";
import { ExternalLink } from "lucide-react";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { PageScrollViewport } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { PageLayout, WorkspaceLayout } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui";
import { StatusBadge, TableActionGroup, i18nMessage } from "@govoplan/core-webui";
import { usePlatformLanguage } from "@govoplan/core-webui";
@@ -137,20 +136,25 @@ export default function TemplatesPage() {
if (selectedTemplate) {
return (
<div className="workspace module-workspace">
<ModuleSubnav active={active} groups={templateSubnav(() => setSelectedId(null))} onSelect={setActive} />
<section className="workspace-content">
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle>{selectedTemplate.name}</PageTitle>
<p>{selectedTemplate.description}</p>
</div>
<div className="button-row compact-actions">
<WorkspaceLayout
className="module-workspace"
primary={<ModuleSubnav active={active} groups={templateSubnav(() => setSelectedId(null))} onSelect={setActive} />}
primaryLabel="Template sections"
contentLabel="Template workspace"
interfaceId="campaign.template-workspace"
helpModuleId="campaign"
>
<PageLayout
mode="workspace"
title={selectedTemplate.name}
description={selectedTemplate.description}
actions={
<>
<Button disabled>i18n:govoplan-campaign.duplicate.972d5737</Button>
<Button variant="primary" disabled>i18n:govoplan-campaign.use_in_campaign.779163bc</Button>
</div>
</div>
</>
}
>
{active === "overview" && <TemplateOverview template={selectedTemplate} />}
{active === "content" && <TemplateContent template={selectedTemplate} />}
@@ -158,25 +162,23 @@ export default function TemplatesPage() {
{active === "preview" && <TemplatePreview template={selectedTemplate} />}
{active === "usage" && <TemplateUsage template={selectedTemplate} />}
{active === "versions" && <TemplateVersions template={selectedTemplate} />}
</div>
</section>
</div>);
</PageLayout>
</WorkspaceLayout>);
}
return (
<PageScrollViewport>
<div className="content-pad workspace-data-page module-entry-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle>i18n:govoplan-campaign.templates.f25b700e</PageTitle>
<p>i18n:govoplan-campaign.reusable_message_templates_open_a_template_to_ed.792e2afb</p>
</div>
<div className="button-row compact-actions">
<PageLayout
className="module-entry-page"
title="i18n:govoplan-campaign.templates.f25b700e"
description="i18n:govoplan-campaign.reusable_message_templates_open_a_template_to_ed.792e2afb"
actions={
<>
<Button disabled>i18n:govoplan-campaign.import.d6fbc9d2</Button>
<Button variant="primary" disabled>i18n:govoplan-campaign.export.f3e4fadb</Button>
</div>
</div>
</>
}
>
<Card
title={
@@ -196,8 +198,7 @@ export default function TemplatesPage() {
className="compact-table-wrap module-table-wrap module-entry-table" />
</Card>
</div>
</PageScrollViewport>);
</PageLayout>);
}
@@ -17,6 +17,26 @@ const sources = sourceFiles(sourceRoot).map((file) => ({
value: fs.readFileSync(file, "utf8"),
}));
for (const { file, value } of sources) {
assert.ok(
!value.includes('className="content-pad workspace-data-page'),
`${path.relative(sourceRoot, file)} must use Core PageLayout instead of a raw page frame`,
);
assert.ok(
!/<div\s+className="workspace(?:\s|\")/.test(value),
`${path.relative(sourceRoot, file)} must use Core WorkspaceLayout instead of a raw workspace`,
);
}
for (const relativePath of [
"features/campaigns/CampaignWorkspace.tsx",
"features/campaigns/CampaignModulePage.tsx",
"features/templates/TemplatesPage.tsx",
]) {
const value = fs.readFileSync(path.join(sourceRoot, relativePath), "utf8");
assert.ok(value.includes("<WorkspaceLayout"), `${relativePath} must retain the shared workspace shell`);
}
const nonSemanticClicks = sources.flatMap(({ file, value }) =>
[...value.matchAll(/<(div|span|li|tr)\b[^>]*\bonClick\s*=/g)].map(
(match) => `${path.relative(sourceRoot, file)}:${match.index}`,