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