Adopt shared WebUI layout primitives

This commit is contained in:
2026-08-18 10:42:52 +02:00
parent cff428b475
commit 28f357e151
3 changed files with 20 additions and 20 deletions
+5 -5
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { ArrowLeft, Plus, RefreshCw, RotateCw, Trash2 } from "lucide-react"; import { ArrowLeft, Plus, RefreshCw, RotateCw, Trash2 } from "lucide-react";
import { import { FormGrid, ContentGrid,
ActionBlockerHint, ActionBlockerHint,
Button, Button,
Card, Card,
@@ -208,19 +208,19 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>} {error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{message && <DismissibleAlert tone="success" resetKey={message} floating>{message}</DismissibleAlert>} {message && <DismissibleAlert tone="success" resetKey={message} floating>{message}</DismissibleAlert>}
<LoadingFrame loading={loading} label="Loading bounce processing"> <LoadingFrame loading={loading} label="Loading bounce processing">
<div className="dashboard-grid"> <ContentGrid columns={2} collapseAt="workspace" className="">
<Card title="Watched mailboxes"> <Card title="Watched mailboxes">
<DataGrid id="mail-bounce-sources" rows={sources} columns={sourceColumns} getRowKey={(source) => source.id} emptyText="No bounce mailbox watchers configured." /> <DataGrid id="mail-bounce-sources" rows={sources} columns={sourceColumns} getRowKey={(source) => source.id} emptyText="No bounce mailbox watchers configured." />
</Card> </Card>
<Card title="Delivery-status observations"> <Card title="Delivery-status observations">
<DataGrid id="mail-bounce-observations" rows={observations} columns={observationColumns} getRowKey={(item) => item.id} emptyText="No bounce observations recorded." /> <DataGrid id="mail-bounce-observations" rows={observations} columns={observationColumns} getRowKey={(item) => item.id} emptyText="No bounce observations recorded." />
</Card> </Card>
</div> </ContentGrid>
</LoadingFrame> </LoadingFrame>
</div> </div>
<Dialog open={addOpen} title="Add bounce mailbox watcher" onClose={() => !busy && setAddOpen(false)} footer={<><Button onClick={() => setAddOpen(false)} disabled={Boolean(busy)} disabledReason={busy ? "Wait for the current bounce-processing action to finish." : undefined}>Cancel</Button><Button variant="primary" onClick={() => void addSource()} disabled={Boolean(saveWatcherBlocker)} disabledReason={saveWatcherBlocker}>Add watcher</Button></>}> <Dialog open={addOpen} title="Add bounce mailbox watcher" onClose={() => !busy && setAddOpen(false)} footer={<><Button onClick={() => setAddOpen(false)} disabled={Boolean(busy)} disabledReason={busy ? "Wait for the current bounce-processing action to finish." : undefined}>Cancel</Button><Button variant="primary" onClick={() => void addSource()} disabled={Boolean(saveWatcherBlocker)} disabledReason={saveWatcherBlocker}>Add watcher</Button></>}>
<div className="form-grid"> <FormGrid columns={1} collapseAt="standard" className="">
{imapProfiles.length === 0 && {imapProfiles.length === 0 &&
<ActionBlockerHint <ActionBlockerHint
reason={{ reason={{
@@ -242,7 +242,7 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
<input value={folder} disabled={Boolean(busy)} onChange={(event) => setFolder(event.target.value)} placeholder="INBOX" /> <input value={folder} disabled={Boolean(busy)} onChange={(event) => setFolder(event.target.value)} placeholder="INBOX" />
</FormField> </FormField>
<ToggleSwitch checked={active} disabled={Boolean(busy)} onChange={setActive} label="Watch automatically" /> <ToggleSwitch checked={active} disabled={Boolean(busy)} onChange={setActive} label="Watch automatically" />
</div> </FormGrid>
</Dialog> </Dialog>
<ConfirmDialog <ConfirmDialog
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type ReactNode } from "react"; import { useEffect, useMemo, useState, type ReactNode } from "react";
import { ActionBlockerHint, AdminSelectionList, ConnectionTree, DocumentationHelpLink, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StageRail, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui"; import { FormGrid, ActionBlockerHint, AdminSelectionList, ConnectionTree, DocumentationHelpLink, FieldLabel, LoadingFrame, MailServerSettingsPanel, PolicyLockedHint, PolicyPathHelp, PolicyRow, PolicySourcePath, PolicyTable, StageRail, StatusBadge, TableActionGroup, ToggleSwitch, hasMailImapSettings, mailImapSettingsPayload, mailServerSecurityOptions, mailSmtpSettingsPayload, mailTextOrNull, mailTransportCredentialsPayload, mergeDeltaRows, normalizeMailServerSecurity, normalizePolicySourcePathItems, useDeltaWatermarks, type ConnectionTreeColumn, type MailServerConnectionTestResult, type MailServerCredentialSettings, type MailServerImapSettings, type MailServerSmtpSettings, type NormalizedPolicySourcePathItem, type PolicySourcePathItem } from "@govoplan/core-webui";
import { ArrowLeft, ArrowRight, Inbox, KeyRound, Link2, Pencil, Plus, Send, Settings2, Trash2, Unlink } from "lucide-react"; import { ArrowLeft, ArrowRight, Inbox, KeyRound, Link2, Pencil, Plus, Send, Settings2, Trash2, Unlink } from "lucide-react";
import type { ApiSettings } from "../../types"; import type { ApiSettings } from "../../types";
import { import {
@@ -740,11 +740,11 @@ export function MailProfileScopeManager({
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>} {error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>} {success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
<Dialog <Dialog variant="administration" size="wide"
open={editing !== null} open={editing !== null}
title={profileDialogTitle(editing, editingTarget)} title={profileDialogTitle(editing, editingTarget)}
onClose={() => !busy && closeProfileEditor()} onClose={() => !busy && closeProfileEditor()}
className="admin-dialog admin-dialog-wide mail-profile-dialog" className="mail-profile-dialog"
closeDisabled={busy} closeDisabled={busy}
footer={editing === "new" footer={editing === "new"
? <> ? <>
@@ -1343,7 +1343,7 @@ function ProfileForm({
documentation={MAIL_PROFILE_DOCUMENTATION} /> documentation={MAIL_PROFILE_DOCUMENTATION} />
} }
{showProfileFields && {showProfileFields &&
<div className="admin-form-grid two-columns"> <FormGrid columns={2} gap="small" collapseAt="workspace" className="">
<FormField label="i18n:govoplan-mail.name.709a2322" documentation={MAIL_PROFILE_DOCUMENTATION}><input value={draft.name} disabled={disabled} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField> <FormField label="i18n:govoplan-mail.name.709a2322" documentation={MAIL_PROFILE_DOCUMENTATION}><input value={draft.name} disabled={disabled} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-mail.slug.094da9b9"><input value={draft.slug} disabled={disabled} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} placeholder="i18n:govoplan-mail.generated_from_name.33d69a91" /></FormField> <FormField label="i18n:govoplan-mail.slug.094da9b9"><input value={draft.slug} disabled={disabled} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} placeholder="i18n:govoplan-mail.generated_from_name.33d69a91" /></FormField>
<FormField label="i18n:govoplan-mail.status.bae7d5be"><select value={draft.isActive ? "active" : "inactive"} disabled={disabled} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-mail.active.a733b809</option><option value="inactive">i18n:govoplan-mail.inactive.09af574c</option></select></FormField> <FormField label="i18n:govoplan-mail.status.bae7d5be"><select value={draft.isActive ? "active" : "inactive"} disabled={disabled} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-mail.active.a733b809</option><option value="inactive">i18n:govoplan-mail.inactive.09af574c</option></select></FormField>
@@ -1355,7 +1355,7 @@ function ProfileForm({
onChange={(checked) => setDraft({ ...draft, inheritToLowerScopes: checked })} onChange={(checked) => setDraft({ ...draft, inheritToLowerScopes: checked })}
label="Visible to lower scopes" /> label="Visible to lower scopes" />
</div> </div>
</div> </FormGrid>
} }
{editTarget.kind === "profile" && existingProfile && {editTarget.kind === "profile" && existingProfile &&
@@ -1363,7 +1363,7 @@ function ProfileForm({
} }
{editTarget.kind === "server" && {editTarget.kind === "server" &&
<div className="admin-form-grid two-columns"> <FormGrid columns={2} gap="small" collapseAt="workspace" className="">
<FormField label="Server name" documentation={MAIL_PROFILE_DOCUMENTATION}> <FormField label="Server name" documentation={MAIL_PROFILE_DOCUMENTATION}>
<input value={draft.serverName} disabled={disabled} onChange={(event) => setDraft({ ...draft, serverName: event.target.value })} /> <input value={draft.serverName} disabled={disabled} onChange={(event) => setDraft({ ...draft, serverName: event.target.value })} />
</FormField> </FormField>
@@ -1387,11 +1387,11 @@ function ProfileForm({
onChange={(checked) => setDraft({ ...draft, serverInheritToLowerScopes: checked })} onChange={(checked) => setDraft({ ...draft, serverInheritToLowerScopes: checked })}
label="Visible to lower scopes" /> label="Visible to lower scopes" />
</div> </div>
</div> </FormGrid>
} }
{editTarget.kind === "credentials" && {editTarget.kind === "credentials" &&
<div className="admin-form-grid two-columns"> <FormGrid columns={2} gap="small" collapseAt="workspace" className="">
{creatingCredential && {creatingCredential &&
<FormField label="Reusable credential" documentation={MAIL_PROFILE_WORKFLOW_DOCUMENTATION}> <FormField label="Reusable credential" documentation={MAIL_PROFILE_WORKFLOW_DOCUMENTATION}>
<select value={reuseCredentialId} disabled={credentialDisabled} onChange={(event) => setReuseCredentialId(event.target.value)}> <select value={reuseCredentialId} disabled={credentialDisabled} onChange={(event) => setReuseCredentialId(event.target.value)}>
@@ -1455,7 +1455,7 @@ function ProfileForm({
The selected credential remains independently managed and will be linked to this server. The selected credential remains independently managed and will be linked to this server.
</div> </div>
} }
</div> </FormGrid>
} }
{policyMessages.length > 0 && ( {policyMessages.length > 0 && (
+6 -6
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Activity, ChevronRight, Home, Mail, Paperclip, RefreshCw, Search, X } from "lucide-react"; import { Activity, ChevronRight, Home, Mail, Paperclip, RefreshCw, Search, X } from "lucide-react";
import { import { ToolbarGroup, ActionToolbar,
ActionBlockerHint, ActionBlockerHint,
Button, Button,
DataGridPaginationBar, DataGridPaginationBar,
@@ -407,7 +407,7 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
<section className="file-list-panel mailbox-message-list-panel" aria-label="i18n:govoplan-mail.mailbox_messages.5c06afaf"> <section className="file-list-panel mailbox-message-list-panel" aria-label="i18n:govoplan-mail.mailbox_messages.5c06afaf">
<div className="file-list-sticky"> <div className="file-list-sticky">
<div className="file-manager-toolbar mailbox-toolbar" aria-label="i18n:govoplan-mail.mail_actions.c08b5f08"> <ActionToolbar className="file-manager-toolbar mailbox-toolbar" aria-label="i18n:govoplan-mail.mail_actions.c08b5f08">
<label className="mailbox-profile-field"> <label className="mailbox-profile-field">
<span>i18n:govoplan-mail.imap_profile.5165df81</span> <span>i18n:govoplan-mail.imap_profile.5165df81</span>
<select value={selectedProfileId} disabled={loadingProfiles || loadingFolders || imapProfiles.length === 0} onChange={(event) => selectProfile(event.target.value)}> <select value={selectedProfileId} disabled={loadingProfiles || loadingFolders || imapProfiles.length === 0} onChange={(event) => selectProfile(event.target.value)}>
@@ -415,8 +415,8 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
{imapProfiles.map((profile) => <option key={profile.id} value={profile.id}>{profile.name}</option>)} {imapProfiles.map((profile) => <option key={profile.id} value={profile.id}>{profile.name}</option>)}
</select> </select>
</label> </label>
<span className="mailbox-toolbar-meta">{selectedProfile?.imap ? transportLabel(selectedProfile) : "i18n:govoplan-mail.no_imap_profile_selected.e7d1516f"}</span> <ToolbarGroup grow className="mailbox-toolbar-meta">{selectedProfile?.imap ? transportLabel(selectedProfile) : "i18n:govoplan-mail.no_imap_profile_selected.e7d1516f"}</ToolbarGroup>
<div className="mailbox-toolbar-actions"> <ToolbarGroup align="end" className="mailbox-toolbar-actions">
<DocumentationHelpLink reference={MAILBOX_DOCUMENTATION} /> <DocumentationHelpLink reference={MAILBOX_DOCUMENTATION} />
{hasAnyScope(auth, ["mail:bounce:read", "mail:bounce:manage"]) && {hasAnyScope(auth, ["mail:bounce:read", "mail:bounce:manage"]) &&
<Button onClick={() => navigate("/mail/bounces")} title="Open bounce processing"> <Button onClick={() => navigate("/mail/bounces")} title="Open bounce processing">
@@ -435,8 +435,8 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
<RefreshCw size={16} aria-hidden="true" /> <RefreshCw size={16} aria-hidden="true" />
i18n:govoplan-mail.messages.f1702b46 i18n:govoplan-mail.messages.f1702b46
</Button> </Button>
</div> </ToolbarGroup>
</div> </ActionToolbar>
{noImapProfiles && {noImapProfiles &&
<ActionBlockerHint <ActionBlockerHint