Dismissable alert use, DataGrid rework

This commit is contained in:
2026-06-13 02:35:14 +02:00
parent 403a6722b8
commit 8d2fe5b77b
22 changed files with 207 additions and 108 deletions

View File

@@ -3,6 +3,7 @@ import type { ApiSettings, LoginResponse } from "../../types";
import { login } from "../../api/auth";
import Button from "../../components/Button";
import FormField from "../../components/FormField";
import DismissibleAlert from "../../components/DismissibleAlert";
export default function LoginModal({
settings,
@@ -42,7 +43,7 @@ export default function LoginModal({
</header>
<div className="modal-body form-grid">
<div className="login-hint">Development default: user <strong>admin@example.local</strong>, password <strong>dev-admin</strong>.</div>
{error && <div className="alert danger">{error}</div>}
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<FormField label="Email">
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</FormField>

View File

@@ -86,8 +86,8 @@ export default function AttachmentsDataPage({ settings, campaignId }: { settings
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing attachments." />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">

View File

@@ -23,7 +23,7 @@ export default function CampaignAuditPage({ settings, campaignId }: { settings:
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
<LoadingFrame loading={loading} label="Loading audit data…">
<Card title="Recent audit events">

View File

@@ -156,9 +156,9 @@ export default function CampaignFieldsPage({ settings, campaignId }: { settings:
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{fieldNameWarning && <DismissibleAlert tone="warning" resetKey={fieldNameWarning}>{fieldNameWarning}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{fieldNameWarning && <DismissibleAlert tone="warning" resetKey={fieldNameWarning} floating>{fieldNameWarning}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing fields." />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">

View File

@@ -28,7 +28,7 @@ export default function CampaignJsonView({ settings, campaignId }: { settings: A
<Button onClick={() => downloadJson(filename, campaignJson)} disabled={!version}>Download JSON</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
<LoadingFrame loading={loading} label="Loading JSON…">
<Card>
{!loading || version ? <pre className="code-panel">{JSON.stringify(campaignJson, null, 2)}</pre> : <pre className="code-panel">{"{}"}</pre>}

View File

@@ -115,7 +115,7 @@ export default function CampaignListPage({ settings }: { settings: ApiSettings }
<DismissibleAlert tone="warning">Sign in with your user account or configure an automation API key under Settings to load campaigns.</DismissibleAlert>
)}
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
<div className="page-heading split workspace-heading">
<div>

View File

@@ -96,8 +96,8 @@ export default function CampaignOverviewPage({ settings, campaignId }: { setting
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{message && <DismissibleAlert tone="success" resetKey={message}>{message}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{message && <DismissibleAlert tone="success" resetKey={message} floating>{message}</DismissibleAlert>}
<LoadingFrame loading={loading} label="Loading campaign overview…">
<Card title="Campaign identity">

View File

@@ -24,7 +24,7 @@ export default function CampaignReportPage({ settings, campaignId }: { settings:
<Button onClick={reload} disabled={loading}>Reload</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
<LoadingFrame loading={loading} label="Loading report data…">
<div className="dashboard-grid">
<Card title="Report summary">

View File

@@ -68,8 +68,8 @@ export default function GlobalSettingsPage({ settings, campaignId }: { settings:
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing global settings." />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">

View File

@@ -261,8 +261,8 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing server settings." />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">
@@ -331,7 +331,7 @@ export default function MailSettingsPage({ settings, campaignId }: { settings: A
<Button variant="danger" onClick={clearMockMailbox} disabled={mailActionState === "mock" || mockMessages.length === 0}>Clear</Button>
</div>
</div>
{mockError && <DismissibleAlert tone="danger" resetKey={mockError}>{mockError}</DismissibleAlert>}
{mockError && <DismissibleAlert tone="danger" resetKey={mockError} floating>{mockError}</DismissibleAlert>}
<DataGrid
id={`campaign-${campaignId}-server-mock-mailbox`}
rows={mockMessages}
@@ -411,23 +411,23 @@ function MailActionResult({ result }: { result: MailConnectionTestResponse | nul
if (!result) return null;
const authenticated = result.details?.authenticated;
return (
<div className={`alert ${result.ok ? "success" : "danger"}`}>
<DismissibleAlert tone={result.ok ? "success" : "danger"} resetKey={`${result.ok}:${result.message}`}>
{result.message}
{result.ok && typeof authenticated === "boolean" && (
<span> Authentication: {authenticated ? "credentials accepted" : "not used"}.</span>
)}
</div>
</DismissibleAlert>
);
}
function FolderLookupResult({ result, disabled, onUseDetected }: { result: MailImapFolderListResponse | null; disabled?: boolean; onUseDetected: () => void }) {
if (!result) return null;
if (!result.ok) {
return <div className="alert danger">{result.message}</div>;
return <DismissibleAlert tone="danger" resetKey={result.message}>{result.message}</DismissibleAlert>;
}
return (
<div className="alert success">
<DismissibleAlert tone="success" resetKey={`${result.message}:${result.detected_sent_folder || ""}`}>
<p>{result.message}</p>
<p>Detected Sent folder: <strong>{result.detected_sent_folder || "—"}</strong></p>
{result.detected_sent_folder && <Button onClick={onUseDetected} disabled={disabled}>Use detected folder</Button>}
@@ -439,6 +439,6 @@ function FolderLookupResult({ result, disabled, onUseDetected }: { result: MailI
{result.folders.length > 12 && <span className="field-chip">+{result.folders.length - 12} more</span>}
</div>
)}
</div>
</DismissibleAlert>
);
}

View File

@@ -136,8 +136,8 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing sender or recipient profiles." />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">

View File

@@ -81,8 +81,8 @@ export default function RecipientDetailsPage({ settings, campaignId }: { setting
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing recipient data." />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">

View File

@@ -148,9 +148,9 @@ export default function SendDataPage({ settings, campaignId }: { settings: ApiSe
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{sendMessage && <DismissibleAlert tone="info" resetKey={sendMessage}>{sendMessage}</DismissibleAlert>}
{mockMessage && <DismissibleAlert tone="info" resetKey={mockMessage}>{mockMessage}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{sendMessage && <DismissibleAlert tone="info" resetKey={sendMessage} floating>{sendMessage}</DismissibleAlert>}
{mockMessage && <DismissibleAlert tone="info" resetKey={mockMessage} floating>{mockMessage}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Send snapshot. Copy to edit." />}
<LoadingFrame loading={loading || queuedOrSending} label={queuedOrSending ? "Refreshing send state…" : "Loading send data…"}>

View File

@@ -142,8 +142,8 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing the template." />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">

View File

@@ -107,7 +107,7 @@ export default function ReviewWorkflowCards({
return (
<>
{actionMessage && <DismissibleAlert tone="info" resetKey={actionMessage}>{actionMessage}</DismissibleAlert>}
{actionMessage && <DismissibleAlert tone="info" resetKey={actionMessage} floating>{actionMessage}</DismissibleAlert>}
<Card
title="Review actions"

View File

@@ -1,6 +1,7 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import Button from "../../../components/Button";
import DismissibleAlert from "../../../components/DismissibleAlert";
type NavigationAction = () => void;
@@ -146,7 +147,7 @@ export function CampaignUnsavedChangesProvider({ children }: { children: ReactNo
</header>
<div className="modal-body">
<p>{registration.message ?? "This campaign page has unsaved changes. Save them before leaving, or discard the changes and continue."}</p>
{saveError && <div className="alert danger">{saveError}</div>}
{saveError && <DismissibleAlert tone="danger" resetKey={saveError}>{saveError}</DismissibleAlert>}
</div>
<footer className="modal-footer unsaved-changes-actions">
<Button onClick={() => setPendingAction(null)} disabled={saving}>Cancel</Button>

View File

@@ -117,8 +117,8 @@ export default function CreateWizard({ settings, campaignId }: { settings: ApiSe
</div>
<div className="save-state">{saveState}</div>
</div>
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
{validationMessage && <DismissibleAlert tone="info" resetKey={validationMessage}>{validationMessage}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{validationMessage && <DismissibleAlert tone="info" resetKey={validationMessage} floating>{validationMessage}</DismissibleAlert>}
<Card>
{draft && activeStep === "basics" && <BasicsStep draft={draft} patch={patch} />}
{draft && activeStep === "sender" && <SenderStep draft={draft} patch={patch} />}

View File

@@ -1102,6 +1102,8 @@ export default function FilesPage({ settings }: { settings: ApiSettings }) {
return `${label} ${sortDirection === "asc" ? "↑" : "↓"}`;
}
const noticeTone = message.startsWith("No files") ? "warning" : message.endsWith("…") ? "info" : "success";
const toolbar = (
<div className="file-manager-toolbar" aria-label="File actions">
<Button variant="primary" onClick={() => openDialog("upload", toolbarTarget())} disabled={busy || !activeSpace}><UploadCloud size={16} aria-hidden="true" /> Upload</Button>
@@ -1119,6 +1121,9 @@ export default function FilesPage({ settings }: { settings: ApiSettings }) {
{error && (
<DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>
)}
{message && !error && (
<DismissibleAlert tone={noticeTone} resetKey={message} floating>{message}</DismissibleAlert>
)}
<div className={`file-manager-shell ${busy ? "is-loading" : ""}`}>
<aside className="file-tree-panel" aria-label="File spaces and folders">

View File

@@ -7,6 +7,7 @@ import PageTitle from "../../components/PageTitle";
import ToggleSwitch from "../../components/ToggleSwitch";
import { apiFetch } from "../../api/client";
import ModuleSubnav, { type ModuleSubnavGroup } from "../../layout/ModuleSubnav";
import DismissibleAlert from "../../components/DismissibleAlert";
type SettingsSection = "interface" | "workspace" | "local-connection" | "notifications";
@@ -134,7 +135,7 @@ export default function SettingsPage({ settings, onSettingsChange }: { settings:
<div className="button-row compact-actions">
<Button variant="primary" onClick={testConnection} disabled={testing}>{testing ? "Testing…" : "Test connection"}</Button>
</div>
{testResult && <div className={`alert ${testResult.startsWith("Connection successful") ? "success" : "warning"}`}>{testResult}</div>}
{testResult && <DismissibleAlert tone={testResult.startsWith("Connection successful") ? "success" : "warning"} resetKey={testResult}>{testResult}</DismissibleAlert>}
</div>
</Card>
<Card title="Session state">