Adopt shared WebUI layout primitives

This commit is contained in:
2026-08-18 10:42:51 +02:00
parent 68459fde15
commit cf8d7fab11
11 changed files with 65 additions and 65 deletions
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from "react";
import { Archive, CalendarClock, Copy, ExternalLink, LockKeyhole, LockOpen, Pause, Play, Trash2 } from "lucide-react"; import { Archive, CalendarClock, Copy, ExternalLink, LockKeyhole, LockOpen, Pause, Play, Trash2 } from "lucide-react";
import { Link } from "react-router"; import { Link } from "react-router";
import type { ApiSettings, AuthInfo } from "../../types"; import type { ApiSettings, AuthInfo } from "../../types";
import { Button } from "@govoplan/core-webui"; import { FormGrid, Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui";
import { ConfirmDialog } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui";
import { Dialog } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui";
@@ -389,7 +389,7 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
title="i18n:govoplan-campaign.campaign_identity.a00ca574" title="i18n:govoplan-campaign.campaign_identity.a00ca574"
collapsible collapsible
actions={<Link className="btn btn-secondary" to="wizard">i18n:govoplan-campaign.edit_with_wizard.672a7d1a</Link>}> actions={<Link className="btn btn-secondary" to="wizard">i18n:govoplan-campaign.edit_with_wizard.672a7d1a</Link>}>
<div className="form-grid campaign-identity-grid"> <FormGrid columns={1} collapseAt="standard" className="campaign-identity-grid">
<FormField label="i18n:govoplan-campaign.campaign_id.4c4ed79e"> <FormField label="i18n:govoplan-campaign.campaign_id.4c4ed79e">
<input value={identity.external_id} onChange={(event) => patchIdentity("external_id", event.target.value)} /> <input value={identity.external_id} onChange={(event) => patchIdentity("external_id", event.target.value)} />
</FormField> </FormField>
@@ -404,7 +404,7 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
<FormField label="i18n:govoplan-campaign.description.55f8ebc8"> <FormField label="i18n:govoplan-campaign.description.55f8ebc8">
<textarea rows={4} value={identity.description} onChange={(event) => patchIdentity("description", event.target.value)} /> <textarea rows={4} value={identity.description} onChange={(event) => patchIdentity("description", event.target.value)} />
</FormField> </FormField>
</div> </FormGrid>
</Card> </Card>
{(canSchedule || schedules.length > 0) && <Card {(canSchedule || schedules.length > 0) && <Card
@@ -14,7 +14,7 @@ import {
type CampaignJobsResponse, type CampaignJobsResponse,
type CampaignRetentionReport } from type CampaignRetentionReport } from
"../../api/campaigns"; "../../api/campaigns";
import { Card } from "@govoplan/core-webui"; import { ContentGrid, Card } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui"; import { Button } from "@govoplan/core-webui";
import { ConfirmDialog } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui";
import { DataGrid, type DataGridColumn, type DataGridListOption, type DataGridQueryState } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn, type DataGridListOption, type DataGridQueryState } from "@govoplan/core-webui";
@@ -461,7 +461,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
} }
> >
<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"> <ContentGrid columns={2} collapseAt="workspace" className="">
<Card title="i18n:govoplan-campaign.delivery_outcome.f9d7c085"> <Card title="i18n:govoplan-campaign.delivery_outcome.f9d7c085">
<dl className="detail-list"> <dl className="detail-list">
<div><dt>i18n:govoplan-campaign.generated.8eefdd52</dt><dd>{formatDateTime(data.summary?.generated_at)}</dd></div> <div><dt>i18n:govoplan-campaign.generated.8eefdd52</dt><dd>{formatDateTime(data.summary?.generated_at)}</dd></div>
@@ -536,7 +536,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
<Button onClick={() => void runExplicitAction("unattempted")} disabled={!version || Boolean(busyAction)}>i18n:govoplan-campaign.send_unattempted_jobs.db7acc9f</Button> <Button onClick={() => void runExplicitAction("unattempted")} disabled={!version || Boolean(busyAction)}>i18n:govoplan-campaign.send_unattempted_jobs.db7acc9f</Button>
</div> </div>
</Card> </Card>
</div> </ContentGrid>
<Card title="i18n:govoplan-campaign.recipient_delivery_jobs.52492608"> <Card title="i18n:govoplan-campaign.recipient_delivery_jobs.52492608">
<p className="muted small-note"> <p className="muted small-note">
@@ -6,7 +6,7 @@ import {
type CampaignCalendarCatalog, type CampaignCalendarCatalog,
type CampaignPostboxCatalog type CampaignPostboxCatalog
} from "../../api/campaigns"; } from "../../api/campaigns";
import { Button } from "@govoplan/core-webui"; import { FormGrid, ContentGrid, 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";
@@ -211,7 +211,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
} }
<div className="dashboard-grid below-grid"> <ContentGrid columns={2} collapseAt="workspace" className="below-grid">
<Card title="i18n:govoplan-campaign.validation_policy.57dcc756" collapsible> <Card title="i18n:govoplan-campaign.validation_policy.57dcc756" collapsible>
<PolicyTable className="campaign-policy-table" rowClassName="campaign-policy-row" headerClassName="campaign-policy-row-header" fieldLabel="i18n:govoplan-campaign.policy.bb9cf141" settingLabel="i18n:govoplan-campaign.setting.fb449f71" effectiveLabel="i18n:govoplan-campaign.effective_behavior.3daa11c2" showEffectiveColumn> <PolicyTable className="campaign-policy-table" rowClassName="campaign-policy-row" headerClassName="campaign-policy-row-header" fieldLabel="i18n:govoplan-campaign.policy.bb9cf141" settingLabel="i18n:govoplan-campaign.setting.fb449f71" effectiveLabel="i18n:govoplan-campaign.effective_behavior.3daa11c2" showEffectiveColumn>
<PolicyRow <PolicyRow
@@ -321,20 +321,20 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
</PolicyTable> </PolicyTable>
</Card> </Card>
</div> </ContentGrid>
</> : </> :
<> <>
{data.campaign && <CampaignAccessCard settings={settings} auth={auth} campaign={data.campaign} onChanged={reload} onError={setError} />} {data.campaign && <CampaignAccessCard settings={settings} auth={auth} campaign={data.campaign} onChanged={reload} onError={setError} />}
<div className="dashboard-grid below-grid"> <ContentGrid columns={2} collapseAt="workspace" className="below-grid">
<Card title="i18n:govoplan-campaign.delivery_defaults.33cc3b97" collapsible> <Card title="i18n:govoplan-campaign.delivery_defaults.33cc3b97" collapsible>
<div className="form-grid compact responsive-form-grid"> <FormGrid columns={2} collapseAt="wide" className="">
<FormField label="i18n:govoplan-campaign.messages_per_minute.bea49348"><input type="number" min={1} value={getNumber(rateLimit, "messages_per_minute", 5)} disabled={locked} onChange={(event) => patch(["delivery", "rate_limit", "messages_per_minute"], Number(event.target.value || 1))} /></FormField> <FormField label="i18n:govoplan-campaign.messages_per_minute.bea49348"><input type="number" min={1} value={getNumber(rateLimit, "messages_per_minute", 5)} disabled={locked} onChange={(event) => patch(["delivery", "rate_limit", "messages_per_minute"], Number(event.target.value || 1))} /></FormField>
<FormField label="i18n:govoplan-campaign.concurrency.2ec390bf"><input type="number" min={1} value={getNumber(rateLimit, "concurrency", 1)} disabled={locked} onChange={(event) => patch(["delivery", "rate_limit", "concurrency"], Number(event.target.value || 1))} /></FormField> <FormField label="i18n:govoplan-campaign.concurrency.2ec390bf"><input type="number" min={1} value={getNumber(rateLimit, "concurrency", 1)} disabled={locked} onChange={(event) => patch(["delivery", "rate_limit", "concurrency"], Number(event.target.value || 1))} /></FormField>
<FormField label="i18n:govoplan-campaign.max_attempts.f684fef4"><input type="number" min={1} value={getNumber(retry, "max_attempts", 3)} disabled={locked} onChange={(event) => patch(["delivery", "retry", "max_attempts"], Number(event.target.value || 1))} /></FormField> <FormField label="i18n:govoplan-campaign.max_attempts.f684fef4"><input type="number" min={1} value={getNumber(retry, "max_attempts", 3)} disabled={locked} onChange={(event) => patch(["delivery", "retry", "max_attempts"], Number(event.target.value || 1))} /></FormField>
<ToggleSwitch label="i18n:govoplan-campaign.status_tracking.15f61f88" checked={getBool(statusTracking, "enabled", true)} disabled={locked} onChange={(checked) => patch(["status_tracking", "enabled"], checked)} /> <ToggleSwitch label="i18n:govoplan-campaign.status_tracking.15f61f88" checked={getBool(statusTracking, "enabled", true)} disabled={locked} onChange={(checked) => patch(["status_tracking", "enabled"], checked)} />
</div> </FormGrid>
{postboxModuleInstalled && {postboxModuleInstalled &&
<div className="campaign-postbox-delivery-settings"> <div className="campaign-postbox-delivery-settings">
<FormField label="Delivery channels"> <FormField label="Delivery channels">
@@ -375,7 +375,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
</Card> </Card>
<Card title="Calendar invitations" collapsible> <Card title="Calendar invitations" collapsible>
<div className="form-grid compact responsive-form-grid"> <FormGrid columns={2} collapseAt="wide" className="">
<ToggleSwitch <ToggleSwitch
label="Send individualized invitations" label="Send individualized invitations"
checked={getBool(calendarInvitation, "enabled")} checked={getBool(calendarInvitation, "enabled")}
@@ -454,7 +454,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
onChange={(event) => patch(["delivery", "calendar_invitation", "description_template"], event.target.value)} onChange={(event) => patch(["delivery", "calendar_invitation", "description_template"], event.target.value)}
/> />
</FormField> </FormField>
</div> </FormGrid>
{!mailModuleInstalled && {!mailModuleInstalled &&
<DismissibleAlert tone="warning" dismissible={false}> <DismissibleAlert tone="warning" dismissible={false}>
Mail must be active to deliver iCalendar invitations. Mail must be active to deliver iCalendar invitations.
@@ -483,7 +483,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
</div> </div>
<p className="muted small-note">i18n:govoplan-campaign.these_opt_ins_are_stored_in_the_draft_editor_met.dc6ffbfd</p> <p className="muted small-note">i18n:govoplan-campaign.these_opt_ins_are_stored_in_the_draft_editor_met.dc6ffbfd</p>
</Card> </Card>
</div> </ContentGrid>
</> </>
} }
</LoadingFrame> </LoadingFrame>
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import { FormGrid,
Button, Button,
Card, Card,
Dialog, Dialog,
@@ -375,7 +375,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
<Button variant="primary" onClick={openCredentialDialog} disabled={locked || profilesLoading || !selectedProfile || !selectedSmtpServer && !selectedImapServer}>Add campaign credential</Button> <Button variant="primary" onClick={openCredentialDialog} disabled={locked || profilesLoading || !selectedProfile || !selectedSmtpServer && !selectedImapServer}>Add campaign credential</Button>
</div>}> </div>}>
<p className="muted small-note">The campaign stores stable references to the envelope, selected servers, and credentials.</p> <p className="muted small-note">The campaign stores stable references to the envelope, selected servers, and credentials.</p>
<div className="form-grid compact responsive-form-grid"> <FormGrid columns={2} collapseAt="wide" className="">
<FormField label="i18n:govoplan-campaign.profile.ff4fc027"> <FormField label="i18n:govoplan-campaign.profile.ff4fc027">
<select value={selectedProfileId} disabled={locked || profilesLoading} onChange={(event) => selectMailProfile(event.target.value)}> <select value={selectedProfileId} disabled={locked || profilesLoading} onChange={(event) => selectMailProfile(event.target.value)}>
<option value="">i18n:govoplan-campaign.select_a_mail_profile.76480af0</option> <option value="">i18n:govoplan-campaign.select_a_mail_profile.76480af0</option>
@@ -410,7 +410,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
)} )}
</select> </select>
</FormField> </FormField>
</div> </FormGrid>
{selectedProfileUnavailable && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.the_referenced_mail_profile_is_inactive_unavaila.abeebe26</DismissibleAlert>} {selectedProfileUnavailable && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-campaign.the_referenced_mail_profile_is_inactive_unavaila.abeebe26</DismissibleAlert>}
{selectedProfile && <div className="metric-grid inside"> {selectedProfile && <div className="metric-grid inside">
<MetricCard label="i18n:govoplan-campaign.profile.ff4fc027" value={selectedProfile.name} /> <MetricCard label="i18n:govoplan-campaign.profile.ff4fc027" value={selectedProfile.name} />
@@ -427,12 +427,12 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
{profileError && <DismissibleAlert tone="warning" resetKey={profileError} floating>{profileError}</DismissibleAlert>} {profileError && <DismissibleAlert tone="warning" resetKey={profileError} floating>{profileError}</DismissibleAlert>}
</Card>} </Card>}
<Dialog <Dialog variant="administration" size="wide"
open={credentialDialogOpen} open={credentialDialogOpen}
title="Add campaign credential" title="Add campaign credential"
onClose={() => setCredentialDialogOpen(false)} onClose={() => setCredentialDialogOpen(false)}
closeDisabled={credentialSaving} closeDisabled={credentialSaving}
className="admin-dialog admin-dialog-wide adaptive-config-dialog" className="adaptive-config-dialog"
footerClassName="button-row compact-actions" footerClassName="button-row compact-actions"
footer={ footer={
<> <>
@@ -459,7 +459,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
<h3>Campaign login</h3> <h3>Campaign login</h3>
<p>This credential belongs to this campaign and can use only the selected mail servers.</p> <p>This credential belongs to this campaign and can use only the selected mail servers.</p>
</header> </header>
<div className="form-grid two"> <FormGrid columns={2} collapseAt="standard" className="">
<FormField label="Name"> <FormField label="Name">
<input value={credentialDraft.name} disabled={credentialSaving} onChange={(event) => setCredentialDraft({ ...credentialDraft, name: event.target.value })} autoFocus /> <input value={credentialDraft.name} disabled={credentialSaving} onChange={(event) => setCredentialDraft({ ...credentialDraft, name: event.target.value })} autoFocus />
</FormField> </FormField>
@@ -469,17 +469,17 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
<FormField label="Password"> <FormField label="Password">
<PasswordField value={credentialDraft.password} onValueChange={(password) => setCredentialDraft({ ...credentialDraft, password })} disabled={credentialSaving} generator autoComplete="new-password" /> <PasswordField value={credentialDraft.password} onValueChange={(password) => setCredentialDraft({ ...credentialDraft, password })} disabled={credentialSaving} generator autoComplete="new-password" />
</FormField> </FormField>
</div> </FormGrid>
<div className="form-grid two"> <FormGrid columns={2} collapseAt="standard" className="">
{selectedSmtpServer && <ToggleSwitch checked={credentialDraft.useSmtp} disabled={credentialSaving} onChange={(useSmtp) => setCredentialDraft({ ...credentialDraft, useSmtp })} label={`Use for SMTP: ${selectedSmtpServer.name}`} />} {selectedSmtpServer && <ToggleSwitch checked={credentialDraft.useSmtp} disabled={credentialSaving} onChange={(useSmtp) => setCredentialDraft({ ...credentialDraft, useSmtp })} label={`Use for SMTP: ${selectedSmtpServer.name}`} />}
{selectedImapServer && <ToggleSwitch checked={credentialDraft.useImap} disabled={credentialSaving} onChange={(useImap) => setCredentialDraft({ ...credentialDraft, useImap })} label={`Use for IMAP: ${selectedImapServer.name}`} />} {selectedImapServer && <ToggleSwitch checked={credentialDraft.useImap} disabled={credentialSaving} onChange={(useImap) => setCredentialDraft({ ...credentialDraft, useImap })} label={`Use for IMAP: ${selectedImapServer.name}`} />}
</div> </FormGrid>
</section> </section>
</div> </div>
</Dialog> </Dialog>
{!isPolicyView && <Card title="i18n:govoplan-campaign.imap_append.8c0d9e96" collapsible> {!isPolicyView && <Card title="i18n:govoplan-campaign.imap_append.8c0d9e96" collapsible>
<div className="form-grid compact responsive-form-grid mail-server-form-grid"> <FormGrid columns={2} collapseAt="wide" className="mail-server-form-grid">
<div className="mail-server-field-span mail-server-toggle-row mail-server-plain-toggle-row"> <div className="mail-server-field-span mail-server-toggle-row mail-server-plain-toggle-row">
<ToggleSwitch <ToggleSwitch
label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8" label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8"
@@ -499,7 +499,7 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
</div> </div>
</FormField> </FormField>
<MailServerFolderLookupResultView result={folderResult} disabled={locked || !imapAppendEnabled} onUseDetected={useDetectedSentFolder} floatingFailures /> <MailServerFolderLookupResultView result={folderResult} disabled={locked || !imapAppendEnabled} onUseDetected={useDetectedSentFolder} floatingFailures />
</div> </FormGrid>
</Card>} </Card>}
</> </>
</LoadingFrame> </LoadingFrame>
@@ -37,7 +37,7 @@ import {
type CampaignSummary } from type CampaignSummary } from
"../../api/campaigns"; "../../api/campaigns";
import { getMockMailboxMessage, type MockMailboxMessage } from "../../api/mail"; import { getMockMailboxMessage, type MockMailboxMessage } from "../../api/mail";
import { Button, Dialog, FormField, SegmentedControl, hasScope, useDeltaWatermarks, useGuardedNavigate, usePlatformUiCapability, type AuthInfo, type MailDevMailboxUiCapability } from "@govoplan/core-webui"; import { FormGrid, Button, Dialog, FormField, SegmentedControl, hasScope, useDeltaWatermarks, useGuardedNavigate, usePlatformUiCapability, type AuthInfo, type MailDevMailboxUiCapability } from "@govoplan/core-webui";
import { DataGrid, type DataGridQueryState } from "@govoplan/core-webui"; 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";
@@ -1899,7 +1899,7 @@ export default function ReviewSendPage({
{busy === "approval" ? "Requesting..." : "Request approval"} {busy === "approval" ? "Requesting..." : "Request approval"}
</Button> </Button>
</>}> </>}>
<div className="form-grid"> <FormGrid columns={1} collapseAt="standard" className="">
<p className="muted small-note form-grid-wide">The request covers the exact built message, attachment, recipient, policy, and transport snapshot. Rebuilding invalidates and removes this gate.</p> <p className="muted small-note form-grid-wide">The request covers the exact built message, attachment, recipient, policy, and transport snapshot. Rebuilding invalidates and removes this gate.</p>
<FormField label="Approver type"> <FormField label="Approver type">
<select value={approvalSelectorKind} disabled={busy === "approval"} onChange={(event) => setApprovalSelectorKind(event.target.value as typeof approvalSelectorKind)}> <select value={approvalSelectorKind} disabled={busy === "approval"} onChange={(event) => setApprovalSelectorKind(event.target.value as typeof approvalSelectorKind)}>
@@ -1933,7 +1933,7 @@ export default function ReviewSendPage({
})} />)} })} />)}
</div> </div>
</div> </div>
</div> </FormGrid>
</Dialog> </Dialog>
<ConfirmDialog <ConfirmDialog
@@ -2029,7 +2029,7 @@ export default function ReviewSendPage({
} }
> >
{singleSendConfirmRow && ( {singleSendConfirmRow && (
<div className="form-grid"> <FormGrid columns={1} collapseAt="standard" className="">
<SegmentedControl <SegmentedControl
value={singleMessageActionKind} value={singleMessageActionKind}
width="fill" width="fill"
@@ -2089,7 +2089,7 @@ export default function ReviewSendPage({
/> />
</label> </label>
)} )}
</div> </FormGrid>
)} )}
</Dialog> </Dialog>
@@ -11,7 +11,7 @@ import {
type CampaignContentLibraryTarget, type CampaignContentLibraryTarget,
type CampaignPrintTemplate type CampaignPrintTemplate
} from "../../api/campaigns"; } from "../../api/campaigns";
import { Button } from "@govoplan/core-webui"; import { FormGrid, ContentGrid, Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui"; 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";
@@ -433,9 +433,9 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50"> <LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
<> <>
<div className="dashboard-grid template-editor-grid"> <ContentGrid columns={2} collapseAt="workspace" className="template-editor-grid">
<Card title="i18n:govoplan-campaign.editable_template.5e747a1e" actions={<Button onClick={() => setPreviewOpen(true)}>i18n:govoplan-campaign.preview.f1fbb2b4</Button>}> <Card title="i18n:govoplan-campaign.editable_template.5e747a1e" actions={<Button onClick={() => setPreviewOpen(true)}>i18n:govoplan-campaign.preview.f1fbb2b4</Button>}>
<div className="form-grid"> <FormGrid columns={1} collapseAt="standard" className="">
<FormField label="i18n:govoplan-campaign.subject.8d183dbd"> <FormField label="i18n:govoplan-campaign.subject.8d183dbd">
<input <input
ref={subjectRef} ref={subjectRef}
@@ -506,13 +506,13 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
<Button onClick={() => setContentLibraryOpen(true)} disabled={locked}>i18n:govoplan-campaign.load_from_library.327ada7c</Button> <Button onClick={() => setContentLibraryOpen(true)} disabled={locked}>i18n:govoplan-campaign.load_from_library.327ada7c</Button>
<Button onClick={openContentSaveDialog} disabled={locked}>i18n:govoplan-campaign.save_to_library.396649bf</Button> <Button onClick={openContentSaveDialog} disabled={locked}>i18n:govoplan-campaign.save_to_library.396649bf</Button>
</div> </div>
</div> </FormGrid>
</Card> </Card>
<div className="template-side-stack"> <div className="template-side-stack">
<Card title="Printable output"> <Card title="Printable output">
<LoadingFrame loading={printTemplatesLoading} label="Loading printable templates"> <LoadingFrame loading={printTemplatesLoading} label="Loading printable templates">
<div className="form-grid"> <FormGrid columns={1} collapseAt="standard" className="">
{printTemplatesError && <DismissibleAlert tone="danger" compact resetKey={printTemplatesError}>{printTemplatesError}</DismissibleAlert>} {printTemplatesError && <DismissibleAlert tone="danger" compact resetKey={printTemplatesError}>{printTemplatesError}</DismissibleAlert>}
{!printTemplatesError && !printTemplatesAvailable && ( {!printTemplatesError && !printTemplatesAvailable && (
<DismissibleAlert tone="info" compact dismissible={false}> <DismissibleAlert tone="info" compact dismissible={false}>
@@ -565,7 +565,7 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
/> />
</> </>
)} )}
</div> </FormGrid>
</LoadingFrame> </LoadingFrame>
</Card> </Card>
<Card title="i18n:govoplan-campaign.fields.e8b68527"> <Card title="i18n:govoplan-campaign.fields.e8b68527">
@@ -598,7 +598,7 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
<UndefinedPlaceholderList items={undefinedPlaceholders} onSelect={setUndefinedDialog} /> <UndefinedPlaceholderList items={undefinedPlaceholders} onSelect={setUndefinedDialog} />
</Card> </Card>
</div> </div>
</div> </ContentGrid>
</> </>
</LoadingFrame> </LoadingFrame>
@@ -1,6 +1,6 @@
import { useEffect, useRef, type ReactNode } from "react"; import { useEffect, useRef, type ReactNode } from "react";
import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react"; import { ArrowBigLeft, ArrowBigLeftDash, ArrowBigRight, ArrowBigRightDash } from "lucide-react";
import { Button, Dialog, MessageDisplayPanel, type MessageDisplayAttachment } from "@govoplan/core-webui"; import { ActionToolbar, Button, Dialog, MessageDisplayPanel, type MessageDisplayAttachment } from "@govoplan/core-webui";
// Campaign review/template/mock previews need recipient navigation, review notes, // Campaign review/template/mock previews need recipient navigation, review notes,
// raw MIME inspection and attachment grouping. Generic mailbox reading uses // raw MIME inspection and attachment grouping. Generic mailbox reading uses
@@ -106,7 +106,7 @@ export default function CampaignMessagePreviewOverlay({
> >
<div ref={contentRef} className="message-preview-content"> <div ref={contentRef} className="message-preview-content">
{(recipientLabel || recipientNote || navigation) && {(recipientLabel || recipientNote || navigation) &&
<div className="template-preview-toolbar"> <ActionToolbar className="template-preview-toolbar">
<div> <div>
{recipientLabel && <strong>{recipientLabel}</strong>} {recipientLabel && <strong>{recipientLabel}</strong>}
{recipientNote && <p className="muted small-note">{recipientNote}</p>} {recipientNote && <p className="muted small-note">{recipientNote}</p>}
@@ -120,7 +120,7 @@ export default function CampaignMessagePreviewOverlay({
<button type="button" className="version-arrow" onClick={navigation.onLast} disabled={navigation.index >= navigation.total - 1} title="i18n:govoplan-campaign.last_message.83741110" aria-label="i18n:govoplan-campaign.last_message.83741110"><ArrowBigRightDash aria-hidden="true" /></button> <button type="button" className="version-arrow" onClick={navigation.onLast} disabled={navigation.index >= navigation.total - 1} title="i18n:govoplan-campaign.last_message.83741110" aria-label="i18n:govoplan-campaign.last_message.83741110"><ArrowBigRightDash aria-hidden="true" /></button>
</div> </div>
} }
</div> </ActionToolbar>
} }
<MessageDisplayPanel <MessageDisplayPanel
@@ -1,5 +1,5 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Button, Dialog, DismissibleAlert, FormField, TableActionGroup, ToggleSwitch } from "@govoplan/core-webui"; import { FormGrid, ActionToolbar, Button, Dialog, DismissibleAlert, FormField, TableActionGroup, ToggleSwitch } from "@govoplan/core-webui";
import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react"; import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react";
import type { import type {
CampaignPostboxCatalog, CampaignPostboxCatalog,
@@ -101,7 +101,7 @@ export default function PostboxTargetsDialog({
</> </>
} }
> >
<div className="campaign-postbox-target-toolbar"> <ActionToolbar className="campaign-postbox-target-toolbar">
{showMerge && {showMerge &&
<ToggleSwitch <ToggleSwitch
label="Postbox target defaults" label="Postbox target defaults"
@@ -121,7 +121,7 @@ export default function PostboxTargetsDialog({
<Plus aria-hidden="true" /> <Plus aria-hidden="true" />
Add target Add target
</Button> </Button>
</div> </ActionToolbar>
{error && <DismissibleAlert tone="danger" dismissible={false}>{error}</DismissibleAlert>} {error && <DismissibleAlert tone="danger" dismissible={false}>{error}</DismissibleAlert>}
{draftTargets.length === 0 && {draftTargets.length === 0 &&
@@ -181,7 +181,7 @@ function PostboxTargetRow({
{ id: "remove", label: "Remove target", icon: <Trash2 aria-hidden="true" />, disabled: locked, variant: "danger", onClick: onRemove } { id: "remove", label: "Remove target", icon: <Trash2 aria-hidden="true" />, disabled: locked, variant: "danger", onClick: onRemove }
]} /> ]} />
</div> </div>
<div className="form-grid compact responsive-form-grid campaign-postbox-target-grid"> <FormGrid columns={2} collapseAt="wide" className="campaign-postbox-target-grid">
<FormField label="Target type"> <FormField label="Target type">
<select <select
value={target.mode} value={target.mode}
@@ -219,7 +219,7 @@ function PostboxTargetRow({
onChange={onChange} onChange={onChange}
/> />
} }
</div> </FormGrid>
</section> </section>
); );
} }
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Button } from "@govoplan/core-webui"; import { FormGrid, Button } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui"; import { FieldLabel } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui";
@@ -22,7 +22,7 @@ export type WizardStepProps = {
export function BasicsStep({ draft, patch }: WizardStepProps) { export function BasicsStep({ draft, patch }: WizardStepProps) {
const campaign = asRecord(draft.campaign); const campaign = asRecord(draft.campaign);
return ( return (
<div className="form-grid"> <FormGrid columns={1} collapseAt="standard" className="">
<FormField label="i18n:govoplan-campaign.campaign_name.aa5d0e72" help="i18n:govoplan-campaign.a_human_readable_name_shown_in_lists_and_reports.afc23e7e"> <FormField label="i18n:govoplan-campaign.campaign_name.aa5d0e72" help="i18n:govoplan-campaign.a_human_readable_name_shown_in_lists_and_reports.afc23e7e">
<input value={getText(campaign, "name")} onChange={(event) => patch(["campaign", "name"], event.target.value)} /> <input value={getText(campaign, "name")} onChange={(event) => patch(["campaign", "name"], event.target.value)} />
</FormField> </FormField>
@@ -39,7 +39,7 @@ export function BasicsStep({ draft, patch }: WizardStepProps) {
<FormField label="i18n:govoplan-campaign.description.55f8ebc8"> <FormField label="i18n:govoplan-campaign.description.55f8ebc8">
<textarea rows={5} value={getText(campaign, "description")} onChange={(event) => patch(["campaign", "description"], event.target.value)} /> <textarea rows={5} value={getText(campaign, "description")} onChange={(event) => patch(["campaign", "description"], event.target.value)} />
</FormField> </FormField>
</div>); </FormGrid>);
} }
@@ -54,7 +54,7 @@ export function SenderStep({ draft, patch }: WizardStepProps) {
const delivery = asRecord(draft.delivery); const delivery = asRecord(draft.delivery);
const imapAppend = asRecord(delivery.imap_append_sent); const imapAppend = asRecord(delivery.imap_append_sent);
return ( return (
<div className="form-grid"> <FormGrid columns={1} collapseAt="standard" className="">
<FormField label="i18n:govoplan-campaign.default_from_address.b9ee6d77"> <FormField label="i18n:govoplan-campaign.default_from_address.b9ee6d77">
<EmailAddressInput <EmailAddressInput
value={from} value={from}
@@ -108,7 +108,7 @@ export function SenderStep({ draft, patch }: WizardStepProps) {
</FormField> </FormField>
<p className="muted small-note">i18n:govoplan-campaign.select_the_delivery_profile_on_the_mail_settings.a89cbb5f</p> <p className="muted small-note">i18n:govoplan-campaign.select_the_delivery_profile_on_the_mail_settings.a89cbb5f</p>
<ToggleSwitch label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8" checked={getBool(imapAppend, "enabled")} onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} /> <ToggleSwitch label="i18n:govoplan-campaign.append_successful_messages_to_sent_via_imap.dbd1b1d8" checked={getBool(imapAppend, "enabled")} onChange={(checked) => patch(["delivery", "imap_append_sent", "enabled"], checked)} />
</div>); </FormGrid>);
} }
@@ -144,7 +144,7 @@ export function TemplateStep({ draft, patch }: WizardStepProps) {
<h2>i18n:govoplan-campaign.template.3ec1ae06</h2> <h2>i18n:govoplan-campaign.template.3ec1ae06</h2>
<p>i18n:govoplan-campaign.compose_the_subject_and_body_merge_fields_can_la.4fb6e97b</p> <p>i18n:govoplan-campaign.compose_the_subject_and_body_merge_fields_can_la.4fb6e97b</p>
</div> </div>
<div className="form-grid"> <FormGrid columns={1} collapseAt="standard" className="">
<FormField label="i18n:govoplan-campaign.subject.8d183dbd"><input value={getText(template, "subject")} onChange={(event) => patch(["template", "subject"], event.target.value)} /></FormField> <FormField label="i18n:govoplan-campaign.subject.8d183dbd"><input value={getText(template, "subject")} onChange={(event) => patch(["template", "subject"], event.target.value)} /></FormField>
<FormField label="i18n:govoplan-campaign.plain_text_body.030a0da0"><textarea rows={12} value={getText(template, "text")} onChange={(event) => patch(["template", "text"], event.target.value)} /></FormField> <FormField label="i18n:govoplan-campaign.plain_text_body.030a0da0"><textarea rows={12} value={getText(template, "text")} onChange={(event) => patch(["template", "text"], event.target.value)} /></FormField>
<div className="form-field"> <div className="form-field">
@@ -157,7 +157,7 @@ export function TemplateStep({ draft, patch }: WizardStepProps) {
onChange={(value) => patch(["template", "html"], value)} onChange={(value) => patch(["template", "html"], value)}
/> />
</div> </div>
</div> </FormGrid>
</div>); </div>);
} }
@@ -170,12 +170,12 @@ export function AttachmentsStep({ draft, patch }: WizardStepProps) {
<h2>i18n:govoplan-campaign.attachments.6771ade6</h2> <h2>i18n:govoplan-campaign.attachments.6771ade6</h2>
<p>i18n:govoplan-campaign.configure_campaign_wide_attachment_behavior_and_.441a6cd2</p> <p>i18n:govoplan-campaign.configure_campaign_wide_attachment_behavior_and_.441a6cd2</p>
</div> </div>
<div className="form-grid compact responsive-form-grid"> <FormGrid columns={2} collapseAt="wide" className="">
<FormField label="i18n:govoplan-campaign.campaign_attachment_base_path.84827619"><input value={getText(attachments, "base_path", ".")} onChange={(event) => patch(["attachments", "base_path"], event.target.value)} /></FormField> <FormField label="i18n:govoplan-campaign.campaign_attachment_base_path.84827619"><input value={getText(attachments, "base_path", ".")} onChange={(event) => patch(["attachments", "base_path"], event.target.value)} /></FormField>
<FormField label="i18n:govoplan-campaign.missing_behavior.de3d1ac7"><select value={getText(attachments, "missing_behavior", "warn")} onChange={(event) => patch(["attachments", "missing_behavior"], event.target.value)}>{behaviorOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField> <FormField label="i18n:govoplan-campaign.missing_behavior.de3d1ac7"><select value={getText(attachments, "missing_behavior", "warn")} onChange={(event) => patch(["attachments", "missing_behavior"], event.target.value)}>{behaviorOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField>
<FormField label="i18n:govoplan-campaign.ambiguous_behavior.e86e724b"><select value={getText(attachments, "ambiguous_behavior", "ask")} onChange={(event) => patch(["attachments", "ambiguous_behavior"], event.target.value)}>{behaviorOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField> <FormField label="i18n:govoplan-campaign.ambiguous_behavior.e86e724b"><select value={getText(attachments, "ambiguous_behavior", "ask")} onChange={(event) => patch(["attachments", "ambiguous_behavior"], event.target.value)}>{behaviorOptions.map((option) => <option key={option}>{option}</option>)}</select></FormField>
<ToggleSwitch label="i18n:govoplan-campaign.allow_individual_attachments.a6ff0e87" checked={getBool(attachments, "allow_individual")} onChange={(checked) => patch(["attachments", "allow_individual"], checked)} /> <ToggleSwitch label="i18n:govoplan-campaign.allow_individual_attachments.a6ff0e87" checked={getBool(attachments, "allow_individual")} onChange={(checked) => patch(["attachments", "allow_individual"], checked)} />
</div> </FormGrid>
<JsonEditor value={attachments.global ?? []} onValid={(value) => patch(["attachments", "global"], value)} /> <JsonEditor value={attachments.global ?? []} onValid={(value) => patch(["attachments", "global"], value)} />
</div>); </div>);
@@ -208,10 +208,10 @@ export function SendStep({ draft, patch }: WizardStepProps) {
<h2>i18n:govoplan-campaign.send_preparation.6e078f43</h2> <h2>i18n:govoplan-campaign.send_preparation.6e078f43</h2>
<p>i18n:govoplan-campaign.configure_rate_limits_and_prepare_the_final_send.af3d4e48</p> <p>i18n:govoplan-campaign.configure_rate_limits_and_prepare_the_final_send.af3d4e48</p>
</div> </div>
<div className="form-grid compact responsive-form-grid"> <FormGrid columns={2} collapseAt="wide" className="">
<FormField label="i18n:govoplan-campaign.messages_per_minute.bea49348"><input type="number" min={1} value={getNumber(rateLimit, "messages_per_minute", 5)} onChange={(event) => patch(["delivery", "rate_limit", "messages_per_minute"], Number(event.target.value || 1))} /></FormField> <FormField label="i18n:govoplan-campaign.messages_per_minute.bea49348"><input type="number" min={1} value={getNumber(rateLimit, "messages_per_minute", 5)} onChange={(event) => patch(["delivery", "rate_limit", "messages_per_minute"], Number(event.target.value || 1))} /></FormField>
<FormField label="i18n:govoplan-campaign.concurrency.2ec390bf"><input type="number" min={1} value={getNumber(rateLimit, "concurrency", 1)} onChange={(event) => patch(["delivery", "rate_limit", "concurrency"], Number(event.target.value || 1))} /></FormField> <FormField label="i18n:govoplan-campaign.concurrency.2ec390bf"><input type="number" min={1} value={getNumber(rateLimit, "concurrency", 1)} onChange={(event) => patch(["delivery", "rate_limit", "concurrency"], Number(event.target.value || 1))} /></FormField>
</div> </FormGrid>
<p className="muted">i18n:govoplan-campaign.test_send_and_queue_actions_remain_in_the_send_w.60dee716</p> <p className="muted">i18n:govoplan-campaign.test_send_and_queue_actions_remain_in_the_send_w.60dee716</p>
</div>); </div>);
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Eye, RefreshCw } from "lucide-react"; import { Eye, RefreshCw } from "lucide-react";
import { useSearchParams } from "react-router"; import { useSearchParams } from "react-router";
import { import { ContentGrid,
Button, Button,
Card, Card,
DataGrid, DataGrid,
@@ -208,7 +208,7 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
{/* Aggregate outcome cards are deliberately not filter shortcuts: each subgroup {/* Aggregate outcome cards are deliberately not filter shortcuts: each subgroup
would require a separately suppressed aggregate response from the server. */} would require a separately suppressed aggregate response from the server. */}
<div className="dashboard-grid"> <ContentGrid columns={2} collapseAt="workspace" className="">
<MetricCard label="i18n:govoplan-campaign.smtp_accepted.e3aa7603" value={countValue(outcomes.smtp_accepted)} tone="good" /> <MetricCard label="i18n:govoplan-campaign.smtp_accepted.e3aa7603" value={countValue(outcomes.smtp_accepted)} tone="good" />
<MetricCard label="Postbox accepted" value={countValue(outcomes.postbox_accepted)} tone="good" /> <MetricCard label="Postbox accepted" value={countValue(outcomes.postbox_accepted)} tone="good" />
<MetricCard label="Print accepted" value={countValue(outcomes.print_accepted)} tone="good" /> <MetricCard label="Print accepted" value={countValue(outcomes.print_accepted)} tone="good" />
@@ -220,7 +220,7 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
<MetricCard label="i18n:govoplan-campaign.not_attempted.e1be3c69" value={countValue(outcomes.not_attempted)} /> <MetricCard label="i18n:govoplan-campaign.not_attempted.e1be3c69" value={countValue(outcomes.not_attempted)} />
<MetricCard label="i18n:govoplan-campaign.cancelled.a1bf92ef" value={countValue(outcomes.cancelled)} /> <MetricCard label="i18n:govoplan-campaign.cancelled.a1bf92ef" value={countValue(outcomes.cancelled)} />
<MetricCard label="i18n:govoplan-campaign.excluded.9804952b" value={countValue(outcomes.excluded)} /> <MetricCard label="i18n:govoplan-campaign.excluded.9804952b" value={countValue(outcomes.excluded)} />
</div> </ContentGrid>
<Card title="i18n:govoplan-campaign.population_and_exclusions.cdff62ad"> <Card title="i18n:govoplan-campaign.population_and_exclusions.cdff62ad">
<dl className="detail-list"> <dl className="detail-list">
@@ -1,6 +1,6 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { ExternalLink } from "lucide-react"; import { ExternalLink } from "lucide-react";
import { Button } from "@govoplan/core-webui"; import { FormGrid, ContentGrid, Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui";
import { PageLayout, WorkspaceLayout } from "@govoplan/core-webui"; import { PageLayout, WorkspaceLayout } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui"; import { FieldLabel } from "@govoplan/core-webui";
@@ -204,7 +204,7 @@ export default function TemplatesPage() {
function TemplateOverview({ template }: {template: TemplateRecord;}) { function TemplateOverview({ template }: {template: TemplateRecord;}) {
return ( return (
<div className="dashboard-grid settings-dashboard-grid"> <ContentGrid columns={2} collapseAt="workspace" className="">
<Card title="i18n:govoplan-campaign.template_status.8bec97cf"> <Card title="i18n:govoplan-campaign.template_status.8bec97cf">
<dl className="detail-list compact-detail-list"> <dl className="detail-list compact-detail-list">
<div><dt>i18n:govoplan-campaign.status.bae7d5be</dt><dd><StatusBadge status={template.status} /></dd></div> <div><dt>i18n:govoplan-campaign.status.bae7d5be</dt><dd><StatusBadge status={template.status} /></dd></div>
@@ -221,7 +221,7 @@ function TemplateOverview({ template }: {template: TemplateRecord;}) {
<span>i18n:govoplan-campaign.a_mapping_wizard_can_be_added_later.6b2fe0f3</span> <span>i18n:govoplan-campaign.a_mapping_wizard_can_be_added_later.6b2fe0f3</span>
</div> </div>
</Card> </Card>
</div>); </ContentGrid>);
} }
@@ -231,10 +231,10 @@ function TemplateContent({ template }: {template: TemplateRecord;}) {
const body = translateText(template.body); const body = translateText(template.body);
return ( return (
<Card title="i18n:govoplan-campaign.template_content.48630575" actions={<Button disabled>i18n:govoplan-campaign.save_changes.179359b3</Button>}> <Card title="i18n:govoplan-campaign.template_content.48630575" actions={<Button disabled>i18n:govoplan-campaign.save_changes.179359b3</Button>}>
<div className="form-grid compact responsive-form-grid"> <FormGrid columns={2} collapseAt="wide" className="">
<label className="form-field"><FieldLabel className="form-label" help="i18n:govoplan-campaign.read_only_subject_stored_in_this_reusable_templa.549ae246">i18n:govoplan-campaign.subject.8d183dbd</FieldLabel><input value={subject} readOnly /></label> <label className="form-field"><FieldLabel className="form-label" help="i18n:govoplan-campaign.read_only_subject_stored_in_this_reusable_templa.549ae246">i18n:govoplan-campaign.subject.8d183dbd</FieldLabel><input value={subject} readOnly /></label>
<label className="form-field full-span"><FieldLabel className="form-label" help="i18n:govoplan-campaign.read_only_body_stored_in_this_reusable_template_.98fbf178">i18n:govoplan-campaign.body.718a7e8a</FieldLabel><textarea rows={10} value={body} readOnly /></label> <label className="form-field full-span"><FieldLabel className="form-label" help="i18n:govoplan-campaign.read_only_body_stored_in_this_reusable_template_.98fbf178">i18n:govoplan-campaign.body.718a7e8a</FieldLabel><textarea rows={10} value={body} readOnly /></label>
</div> </FormGrid>
</Card>); </Card>);
} }