Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:31 +02:00
parent 28f357e151
commit 761dd76b9e
5 changed files with 36 additions and 48 deletions
+4 -1
View File
@@ -13,7 +13,7 @@ mailbox, policy, and delivery-evidence consequences described here.
| System/tenant/group/user/campaign profile surfaces | Compare profiles, protocol servers, reusable credentials, status, and scope | Administration/configuration | High because endpoints, credentials, and inheritance control external communication | Shared `ConnectionTree`, stable row actions, textual status, permission/target blockers, and contextual admin help | | System/tenant/group/user/campaign profile surfaces | Compare profiles, protocol servers, reusable credentials, status, and scope | Administration/configuration | High because endpoints, credentials, and inheritance control external communication | Shared `ConnectionTree`, stable row actions, textual status, permission/target blockers, and contextual admin help |
| Profile creation and focused profile/server/credential editors | Create a governed transport identity or edit one hierarchy object | Guided setup plus adaptive create/edit | High because saving may enable provider access or replace encrypted credentials | Shared `Dialog` and `StageRail` for multi-object setup; focused edit modes show only the selected hierarchy object; field help, connection tests, unsaved-draft guard, and disabled-save reasons | | Profile creation and focused profile/server/credential editors | Create a governed transport identity or edit one hierarchy object | Guided setup plus adaptive create/edit | High because saving may enable provider access or replace encrypted credentials | Shared `Dialog` and `StageRail` for multi-object setup; focused edit modes show only the selected hierarchy object; field help, connection tests, unsaved-draft guard, and disabled-save reasons |
| Mail profile policy card | Narrow visible profiles, lower-scope definitions, hosts, senders, and recipients | Effective-policy editor | High because inherited allow/deny rules govern delivery and lower scopes | Shared policy rows, typed selectors, source path, locked/read-only blocker, dirty-save state, and contextual policy help | | Mail profile policy card | Narrow visible profiles, lower-scope definitions, hosts, senders, and recipients | Effective-policy editor | High because inherited allow/deny rules govern delivery and lower scopes | Shared policy rows, typed selectors, source path, locked/read-only blocker, dirty-save state, and contextual policy help |
| `/mail/bounces` watcher table | Configure bounded IMAP evidence sources and run an explicit scan | Operational administration | High because it accesses a provider mailbox and changes durable evidence cursors | Shared `DataGrid`, status, loading/error feedback, field help, actionable no-profile blocker, and stable row actions | | `/mail/bounces` watcher table | Configure bounded IMAP evidence sources and run an explicit scan | Operational administration | High because it accesses a provider mailbox and changes durable evidence cursors | Shared `PageLayout`, `ContentGrid`, `DataGrid`, status, loading/error feedback, field help, actionable no-profile blocker, and stable row actions |
| `/mail/bounces` observation table | Inspect correlated or unmatched delivery-status evidence | Evidence/reporting | Medium because recipient and diagnostic data may be sensitive | Bounded sanitized rows, textual status, filters, correlation state, and no raw bounce body | | `/mail/bounces` observation table | Inspect correlated or unmatched delivery-status evidence | Evidence/reporting | Medium because recipient and diagnostic data may be sensitive | Bounded sanitized rows, textual status, filters, correlation state, and no raw bounce body |
| Bounce watcher removal | Stop future scans while retaining evidence | Destructive confirmation | Medium and reversible by recreating the watcher; observations are retained | Shared `ConfirmDialog` states the immediate consequence and retained evidence | | Bounce watcher removal | Stop future scans while retaining evidence | Destructive confirmation | Medium and reversible by recreating the watcher; observations are retained | Shared `ConfirmDialog` states the immediate consequence and retained evidence |
| `mail.profiles` and credential-reference capabilities | Let another module select or validate Mail-owned transport without receiving secrets | Governed capability composition | High because the selected identity can perform external effects | Stable references and Core capability boundaries; no sibling-private WebUI import; authorization and credential resolution remain Mail-owned | | `mail.profiles` and credential-reference capabilities | Let another module select or validate Mail-owned transport without receiving secrets | Governed capability composition | High because the selected identity can perform external effects | Stable references and Core capability boundaries; no sibling-private WebUI import; authorization and credential resolution remain Mail-owned |
@@ -22,6 +22,9 @@ mailbox, policy, and delivery-evidence consequences described here.
- Loading, success, error, empty results, permission blockers, and destructive - Loading, success, error, empty results, permission blockers, and destructive
confirmation use Core components. Mail does not reproduce the shell. confirmation use Core components. Mail does not reproduce the shell.
- Bounce processing now delegates its page inset, sticky responsive heading,
route actions, alert regions, loading boundary, scrolling, and help audience
to Core `PageLayout`; Mail retains only watcher and evidence semantics.
- A target-dependent profile surface cannot load until a concrete user, group, - A target-dependent profile surface cannot load until a concrete user, group,
or campaign is selected. The blocker identifies the responsible actor and or campaign is selected. The blocker identifies the responsible actor and
destination instead of silently hiding the editor. destination instead of silently hiding the editor.
+17 -17
View File
@@ -7,12 +7,9 @@ import { FormGrid, ContentGrid,
ConfirmDialog, ConfirmDialog,
DataGrid, DataGrid,
Dialog, Dialog,
DismissibleAlert,
DocumentationHelpLink, DocumentationHelpLink,
FormField, FormField,
LoadingFrame, PageLayout,
PageScrollViewport,
PageTitle,
StatusBadge, StatusBadge,
TableActionGroup, TableActionGroup,
ToggleSwitch, ToggleSwitch,
@@ -194,20 +191,24 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
]; ];
return ( return (
<PageScrollViewport> <>
<div className="content-pad workspace-data-page"> <PageLayout
<div className="page-heading split workspace-heading"> title="Bounce processing"
<div><PageTitle loading={loading}>Bounce processing</PageTitle><p>Watch IMAP delivery-status folders and correlate recipient failures with Mail delivery commands.</p></div> description="Watch IMAP delivery-status folders and correlate recipient failures with Mail delivery commands."
<div className="button-row compact-actions"> actions={(
<>
<Button onClick={() => navigate("/mail")}><ArrowLeft size={16} aria-hidden="true" /> Mailbox</Button> <Button onClick={() => navigate("/mail")}><ArrowLeft size={16} aria-hidden="true" /> Mailbox</Button>
<DocumentationHelpLink reference={MAIL_BOUNCE_DOCUMENTATION} /> <DocumentationHelpLink reference={MAIL_BOUNCE_DOCUMENTATION} />
<Button onClick={() => void load()} disabled={Boolean(pageMutationBlocker)} disabledReason={pageMutationBlocker}><RefreshCw size={16} aria-hidden="true" /> Reload</Button> <Button onClick={() => void load()} disabled={Boolean(pageMutationBlocker)} disabledReason={pageMutationBlocker}><RefreshCw size={16} aria-hidden="true" /> Reload</Button>
<Button variant="primary" onClick={() => setAddOpen(true)} disabled={Boolean(addWatcherBlocker)} disabledReason={addWatcherBlocker}><Plus size={16} aria-hidden="true" /> Add watcher</Button> <Button variant="primary" onClick={() => setAddOpen(true)} disabled={Boolean(addWatcherBlocker)} disabledReason={addWatcherBlocker}><Plus size={16} aria-hidden="true" /> Add watcher</Button>
</div> </>
</div> )}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>} loading={loading}
{message && <DismissibleAlert tone="success" resetKey={message} floating>{message}</DismissibleAlert>} loadingLabel="Loading bounce processing"
<LoadingFrame loading={loading} label="Loading bounce processing"> error={error}
success={message}
documentationType="admin"
>
<ContentGrid columns={2} collapseAt="workspace" className=""> <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." />
@@ -216,8 +217,7 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
<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>
</ContentGrid> </ContentGrid>
</LoadingFrame> </PageLayout>
</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></>}>
<FormGrid columns={1} collapseAt="standard" className=""> <FormGrid columns={1} collapseAt="standard" className="">
@@ -254,6 +254,6 @@ export default function MailBouncePage({ settings }: { settings: ApiSettings })
busy={Boolean(busy)} busy={Boolean(busy)}
onCancel={() => setDeleteSource(null)} onCancel={() => setDeleteSource(null)}
onConfirm={() => void removeSource()} /> onConfirm={() => void removeSource()} />
</PageScrollViewport> </>
); );
} }
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type ReactNode } from "react"; import { useEffect, useMemo, useState, type ReactNode } from "react";
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 { ActionToolbar, 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 {
@@ -1020,13 +1020,13 @@ export function MailProfilePolicyEditor({
{success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>} {success && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
<section className="mail-policy-section policy-section"> <section className="mail-policy-section policy-section">
<div className="subsection-heading split"> <ActionToolbar surface="section-header" className="subsection-heading split">
<h3>i18n:govoplan-mail.profile_allow_list.507dfe6c</h3> <h3>i18n:govoplan-mail.profile_allow_list.507dfe6c</h3>
<div className="button-row compact-actions"> <div className="button-row compact-actions">
{lowerLevelLimitToggle("allowed_profile_ids")} {lowerLevelLimitToggle("allowed_profile_ids")}
<Button onClick={() => patchPolicy({ allowed_profile_ids: [] })} disabled={disabled || profileAllowListLocked || selectedProfileIds.size === 0}>i18n:govoplan-mail.clear_allow_list.f69c8c67</Button> <Button onClick={() => patchPolicy({ allowed_profile_ids: [] })} disabled={disabled || profileAllowListLocked || selectedProfileIds.size === 0}>i18n:govoplan-mail.clear_allow_list.f69c8c67</Button>
</div> </div>
</div> </ActionToolbar>
<p className="muted small-note">{selectedProfileIds.size === 0 ? "i18n:govoplan-mail.no_local_profile_allow_list_is_set.31072e39" : i18nMessage("i18n:govoplan-mail.value_profile_s_allowed_by_this_scope.6fe9ba44", { value0: selectedProfileIds.size })}</p> <p className="muted small-note">{selectedProfileIds.size === 0 ? "i18n:govoplan-mail.no_local_profile_allow_list_is_set.31072e39" : i18nMessage("i18n:govoplan-mail.value_profile_s_allowed_by_this_scope.6fe9ba44", { value0: selectedProfileIds.size })}</p>
<AdminSelectionList <AdminSelectionList
options={candidateProfiles.map((profile) => ({ options={candidateProfiles.map((profile) => ({
+3 -2
View File
@@ -3,6 +3,7 @@ import { Activity, ChevronRight, Home, Mail, Paperclip, RefreshCw, Search, X } f
import { ToolbarGroup, ActionToolbar, import { ToolbarGroup, ActionToolbar,
ActionBlockerHint, ActionBlockerHint,
Button, Button,
CountBadge,
DataGridPaginationBar, DataGridPaginationBar,
DocumentationHelpLink, DocumentationHelpLink,
DismissibleAlert, DismissibleAlert,
@@ -395,7 +396,7 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
<span className="mailbox-tree-node-label"> <span className="mailbox-tree-node-label">
<span className="mailbox-tree-node-main"> <span className="mailbox-tree-node-main">
<span>{node.label}</span> <span>{node.label}</span>
{showCount && <small className="mailbox-folder-count">{node.messageCount}</small>} {showCount && <CountBadge tone="neutral" size="compact" className="mailbox-folder-count">{node.messageCount}</CountBadge>}
</span> </span>
{flagText && <small>{flagText}</small>} {flagText && <small>{flagText}</small>}
</span>); </span>);
@@ -407,7 +408,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">
<ActionToolbar className="file-manager-toolbar mailbox-toolbar" aria-label="i18n:govoplan-mail.mail_actions.c08b5f08"> <ActionToolbar justify="between" 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)}>
+1 -17
View File
@@ -186,14 +186,6 @@
gap: 18px; gap: 18px;
} }
.mailbox-toolbar,
.mailbox-message-toolbar {
display: flex;
align-items: center;
gap: 12px;
justify-content: space-between;
}
.mailbox-toolbar label { .mailbox-toolbar label {
display: grid; display: grid;
gap: 5px; gap: 5px;
@@ -396,8 +388,7 @@
} }
@media (max-width: 760px) { @media (max-width: 760px) {
.mailbox-toolbar, .mailbox-toolbar {
.mailbox-message-toolbar {
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
} }
@@ -458,13 +449,6 @@
.mailbox-folder-count { .mailbox-folder-count {
flex: 0 0 auto; flex: 0 0 auto;
min-width: 20px;
padding: 1px 6px;
border: var(--border-line);
border-radius: 999px;
background: var(--surface-subtle);
color: var(--text);
text-align: center;
} }
.mailbox-toolbar.file-manager-toolbar { .mailbox-toolbar.file-manager-toolbar {