Adopt shared WebUI structural primitives
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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,30 +191,33 @@ 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}
|
||||||
<ContentGrid columns={2} collapseAt="workspace" className="">
|
success={message}
|
||||||
<Card title="Watched mailboxes">
|
documentationType="admin"
|
||||||
<DataGrid id="mail-bounce-sources" rows={sources} columns={sourceColumns} getRowKey={(source) => source.id} emptyText="No bounce mailbox watchers configured." />
|
>
|
||||||
</Card>
|
<ContentGrid columns={2} collapseAt="workspace" className="">
|
||||||
<Card title="Delivery-status observations">
|
<Card title="Watched mailboxes">
|
||||||
<DataGrid id="mail-bounce-observations" rows={observations} columns={observationColumns} getRowKey={(item) => item.id} emptyText="No bounce observations recorded." />
|
<DataGrid id="mail-bounce-sources" rows={sources} columns={sourceColumns} getRowKey={(source) => source.id} emptyText="No bounce mailbox watchers configured." />
|
||||||
</Card>
|
</Card>
|
||||||
</ContentGrid>
|
<Card title="Delivery-status observations">
|
||||||
</LoadingFrame>
|
<DataGrid id="mail-bounce-observations" rows={observations} columns={observationColumns} getRowKey={(item) => item.id} emptyText="No bounce observations recorded." />
|
||||||
</div>
|
</Card>
|
||||||
|
</ContentGrid>
|
||||||
|
</PageLayout>
|
||||||
|
|
||||||
<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,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)}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user