intermittent commit

This commit is contained in:
2026-07-14 13:22:11 +02:00
parent 4865de5007
commit b0337b2d26
28 changed files with 2264 additions and 490 deletions
@@ -37,6 +37,16 @@ import { ConfirmDialog } from "@govoplan/core-webui";
import { Dialog } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { FormField, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui";
import {
mailProfileChildDescriptors,
mailProfileEditTargetInitialSection,
mailProfileEditTargetPanelMode,
mailProfileEditTargetShowsProfileFields,
mailProfileEditTargetShowsSettingsPanel,
mailProfileEditTargetVisibleSections,
type MailProfileEditTarget,
type MailProfileProtocol
} from "./mailProfileEditorModel";
export type MailProfileTargetOption = {
id: string;
label: string;
@@ -96,7 +106,8 @@ type PolicyFlagValue = "inherit" | "allow" | "deny";
type CredentialInheritanceValue = "inherit" | "profile" | "local";
type MailProfileTreeRow =
{kind: "profile";id: string;profile: MailServerProfile;} |
{kind: "credential";id: string;profile: MailServerProfile;protocol: "smtp" | "imap";};
{kind: "server";id: string;profile: MailServerProfile;protocol: MailProfileProtocol;} |
{kind: "credential";id: string;profile: MailServerProfile;protocol: MailProfileProtocol;};
const securityOptions = mailServerSecurityOptions as readonly MailSecurity[];
@@ -135,6 +146,7 @@ export function MailProfileScopeManager({
const [profiles, setProfiles] = useState<MailServerProfile[]>([]);
const [selectedTargetId, setSelectedTargetId] = useState(scopeId || targetOptions[0]?.id || "");
const [editing, setEditing] = useState<EditingProfile>(null);
const [editingTarget, setEditingTarget] = useState<MailProfileEditTarget>({ kind: "create" });
const [deactivating, setDeactivating] = useState<MailServerProfile | null>(null);
const [draft, setDraft] = useState<ProfileDraft>(emptyProfileDraft());
const [savedProfileDraftKey, setSavedProfileDraftKey] = useState(profileDraftKey(emptyProfileDraft()));
@@ -233,15 +245,17 @@ export function MailProfileScopeManager({
const nextDraft = emptyProfileDraft();
setDraft(nextDraft);
setSavedProfileDraftKey(profileDraftKey(nextDraft));
setEditingTarget({ kind: "create" });
setEditing("new");
setError("");
setSuccess("");
}
function openEdit(profile: MailServerProfile) {
function openEdit(profile: MailServerProfile, target: MailProfileEditTarget = { kind: "profile" }) {
const nextDraft = profileToDraft(profile);
setDraft(nextDraft);
setSavedProfileDraftKey(profileDraftKey(nextDraft));
setEditingTarget(target);
setEditing(profile);
setError("");
setSuccess("");
@@ -252,6 +266,7 @@ export function MailProfileScopeManager({
const nextDraft = emptyProfileDraft();
setDraft(nextDraft);
setSavedProfileDraftKey(profileDraftKey(nextDraft));
setEditingTarget({ kind: "create" });
}
async function saveProfile(): Promise<boolean> {
@@ -308,6 +323,8 @@ export function MailProfileScopeManager({
width: "minmax(260px, 1.4fr)",
render: (row) => row.kind === "profile" ?
<div className="connection-tree-main"><strong>{row.profile.name}</strong><div className="connection-tree-muted-line">{row.profile.slug} · {scopeLabel(row.profile)}</div></div> :
row.kind === "server" ?
<MailServerTreeCell profile={row.profile} protocol={row.protocol} /> :
<MailCredentialTreeCell profile={row.profile} protocol={row.protocol} />
},
{
@@ -322,7 +339,7 @@ export function MailProfileScopeManager({
id: "policy",
header: "i18n:govoplan-mail.policy.bb9cf141",
width: "minmax(150px, 0.8fr)",
render: (row) => row.kind === "profile" ? scopeLabel(row.profile) : mailCredentialPolicySummary(profileEffectivePolicy, row.protocol)
render: (row) => row.kind === "profile" ? scopeLabel(row.profile) : row.kind === "server" ? row.protocol.toUpperCase() : mailCredentialPolicySummary(profileEffectivePolicy, row.protocol)
},
{
id: "status",
@@ -330,22 +347,33 @@ export function MailProfileScopeManager({
width: "110px",
render: (row) => row.kind === "profile" ?
<span className={`status-badge ${row.profile.is_active ? "success" : "neutral"}`}>{row.profile.is_active ? "i18n:govoplan-mail.active.a733b809" : "i18n:govoplan-mail.inactive.09af574c"}</span> :
row.kind === "server" ?
<span className={`status-badge ${transportConfigured(row.profile, row.protocol) ? "success" : "neutral"}`}>{transportConfigured(row.profile, row.protocol) ? "i18n:govoplan-core.configured.668c5fff" : "i18n:govoplan-core.not_configured.811931bb"}</span> :
<span className={`status-badge ${mailCredentialConfigured(row.profile, row.protocol) ? "success" : "neutral"}`}>{mailCredentialConfigured(row.profile, row.protocol) ? "i18n:govoplan-mail.saved.c0ae8f6e" : "i18n:govoplan-mail.local.dc99d54d"}</span>
}],
[profileEffectivePolicy]);
function mailProfileChildren(row: MailProfileTreeRow): MailProfileTreeRow[] {
if (row.kind !== "profile") return [];
const children: MailProfileTreeRow[] = [{ kind: "credential", id: `credential:${row.profile.id}:smtp`, profile: row.profile, protocol: "smtp" }];
if (row.profile.imap) children.push({ kind: "credential", id: `credential:${row.profile.id}:imap`, profile: row.profile, protocol: "imap" });
const children: MailProfileTreeRow[] = [
...mailProfileChildDescriptors(row.profile).map((child) => ({ ...child, profile: row.profile }))
];
return children;
}
function renderMailProfileActions(row: MailProfileTreeRow) {
if (row.kind === "server") {
const label = `Edit ${row.protocol.toUpperCase()} server`;
return (
<div className="admin-icon-actions">
<Button type="button" className="admin-icon-button" title={label} aria-label={label} onClick={() => openEdit(row.profile, { kind: "server", protocol: row.protocol })} disabled={!canWriteProfiles || busy}><Pencil size={16} /></Button>
</div>);
}
if (row.kind === "credential") {
return (
<div className="admin-icon-actions">
<Button type="button" className="admin-icon-button" title={i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: row.protocol.toUpperCase() })} aria-label={i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: row.protocol.toUpperCase() })} onClick={() => openEdit(row.profile)} disabled={!canWriteProfiles || busy}><Pencil size={16} /></Button>
<Button type="button" className="admin-icon-button" title={i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: row.protocol.toUpperCase() })} aria-label={i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: row.protocol.toUpperCase() })} onClick={() => openEdit(row.profile, { kind: "credentials", protocol: row.protocol })} disabled={!canWriteProfiles || busy}><Pencil size={16} /></Button>
</div>);
}
@@ -411,13 +439,13 @@ export function MailProfileScopeManager({
<Dialog
open={editing !== null}
title={editing === "new" ? "i18n:govoplan-mail.create_mail_profile.4d2f8f9f" : "i18n:govoplan-mail.edit_mail_profile.95d1af9c"}
onClose={() => !busy && setEditing(null)}
title={profileDialogTitle(editing, editingTarget)}
onClose={() => !busy && closeProfileEditor()}
className="admin-dialog admin-dialog-wide mail-profile-dialog"
closeDisabled={busy}
footer={<><Button onClick={() => setEditing(null)} disabled={busy}>i18n:govoplan-mail.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveProfile()} disabled={!canWriteProfiles || busy || !draft.name.trim() || !scopeReady}>{busy ? "i18n:govoplan-mail.saving.56a2285c" : "i18n:govoplan-mail.save_profile.f597c0e8"}</Button></>}>
footer={<><Button onClick={closeProfileEditor} disabled={busy}>i18n:govoplan-mail.cancel.77dfd213</Button><Button variant="primary" onClick={() => void saveProfile()} disabled={!canWriteProfiles || busy || !draft.name.trim() || !scopeReady}>{busy ? "i18n:govoplan-mail.saving.56a2285c" : "i18n:govoplan-mail.save_profile.f597c0e8"}</Button></>}>
<ProfileForm settings={settings} draft={draft} setDraft={setDraft} editing={editing} busy={busy} canWrite={canWriteProfiles} canManageCredentials={canManageCredentials} effectivePolicy={profileEffectivePolicy} />
<ProfileForm settings={settings} draft={draft} setDraft={setDraft} editing={editing} busy={busy} canWrite={canWriteProfiles} canManageCredentials={canManageCredentials} effectivePolicy={profileEffectivePolicy} editTarget={editingTarget} />
</Dialog>
<ConfirmDialog
@@ -771,17 +799,9 @@ function ProfileForm({
busy,
canWrite,
canManageCredentials,
effectivePolicy
}: {settings: ApiSettings;draft: ProfileDraft;setDraft: (draft: ProfileDraft) => void;editing: EditingProfile;busy: boolean;canWrite: boolean;canManageCredentials: boolean;effectivePolicy?: MailProfilePolicy | null;}) {
effectivePolicy,
editTarget
}: {settings: ApiSettings;draft: ProfileDraft;setDraft: (draft: ProfileDraft) => void;editing: EditingProfile;busy: boolean;canWrite: boolean;canManageCredentials: boolean;effectivePolicy?: MailProfilePolicy | null;editTarget: MailProfileEditTarget;}) {
const [smtpTestResult, setSmtpTestResult] = useState<MailServerConnectionTestResult | null>(null);
const [imapTestResult, setImapTestResult] = useState<MailServerConnectionTestResult | null>(null);
const [folderResult, setFolderResult] = useState<MailServerFolderLookupResult | null>(null);
@@ -790,6 +810,11 @@ function ProfileForm({
const disabled = busy || !canWrite;
const credentialDisabled = disabled || !canManageCredentials;
const existingProfile = editing !== "new" ? editing : null;
const initialSection = mailProfileEditTargetInitialSection(editTarget);
const settingsPanelMode = mailProfileEditTargetPanelMode(editTarget);
const visibleSections = mailProfileEditTargetVisibleSections(editTarget);
const showProfileFields = mailProfileEditTargetShowsProfileFields(editTarget);
const showSettingsPanel = mailProfileEditTargetShowsSettingsPanel(editTarget);
const draftHasImap = hasDraftImapSettings(draft);
const useSavedSmtpTest = Boolean(existingProfile && !draft.smtpPassword && existingProfile.smtp_password_configured);
const useSavedImapTest = Boolean(existingProfile && !draft.imapPassword && existingProfile.imap_password_configured);
@@ -803,7 +828,7 @@ function ProfileForm({
setImapTestResult(null);
setFolderResult(null);
setMailActionState(null);
}, [editing]);
}, [editing, editTarget]);
function patchSmtpSettings(patch: Partial<MailServerSmtpSettings>) {
setDraft({
@@ -896,12 +921,18 @@ function ProfileForm({
return (
<div className="mail-profile-form">
{showProfileFields &&
<div className="admin-form-grid two-columns">
<FormField label="i18n:govoplan-mail.name.709a2322"><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.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.description.55f8ebc8"><textarea rows={3} value={draft.description} disabled={disabled} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
</div>
}
{editTarget.kind === "profile" && existingProfile &&
<ProfileTransportSummary profile={existingProfile} />
}
{policyMessages.length > 0 &&
<DismissibleAlert tone="warning" resetKey={policyMessages.map((item) => `${item.key}:${item.value}`).join("|")} dismissible={false}>
@@ -910,6 +941,7 @@ function ProfileForm({
</DismissibleAlert>
}
{showSettingsPanel && settingsPanelMode &&
<MailServerSettingsPanel
smtp={{ host: draft.smtpHost, port: draft.smtpPort, security: draft.smtpSecurity, timeout_seconds: draft.smtpTimeout }}
imap={{ host: draft.imapHost, port: draft.imapPort, security: draft.imapSecurity, sent_folder: draft.imapSentFolder, timeout_seconds: draft.imapTimeout }}
@@ -936,7 +968,11 @@ function ProfileForm({
imapTestResult={imapTestResult}
folderLookupResult={folderResult}
onUseDetectedFolder={useDetectedSentFolder}
useDetectedFolderDisabled={disabled || !draftHasImap} />
useDetectedFolderDisabled={disabled || !draftHasImap}
initialSection={initialSection}
visibleSections={visibleSections}
mode={settingsPanelMode} />
}
</div>);
@@ -992,6 +1028,33 @@ function MailCredentialTreeCell({ profile, protocol }: {profile: MailServerProfi
}
function ProfileTransportSummary({ profile }: {profile: MailServerProfile;}) {
return (
<div className="mail-profile-transport-summary">
<div>
<span>SMTP server</span>
<strong>{transportLabel(profile.smtp)}</strong>
<small>{mailCredentialConfigured(profile, "smtp") ? "i18n:govoplan-mail.password_saved.f6fab237" : "i18n:govoplan-mail.no_saved_password.32ce2b16"}</small>
</div>
<div>
<span>IMAP server</span>
<strong>{transportLabel(profile.imap)}</strong>
<small>{profile.imap ? mailCredentialConfigured(profile, "imap") ? "i18n:govoplan-mail.password_saved.f6fab237" : "i18n:govoplan-mail.no_saved_password.32ce2b16" : "i18n:govoplan-mail.not_configured.811931bb"}</small>
</div>
</div>);
}
function MailServerTreeCell({ profile, protocol }: {profile: MailServerProfile;protocol: "smtp" | "imap";}) {
const transport = protocol === "smtp" ? profile.smtp : profile.imap;
return (
<div className="connection-tree-main">
<strong>{protocol.toUpperCase()} server</strong>
<div className="connection-tree-muted-line">{transportLabel(transport)}</div>
</div>);
}
function TransportCell({ profile, protocol }: {profile: MailServerProfile;protocol: "smtp" | "imap";}) {
const transport = protocol === "smtp" ? profile.smtp : profile.imap;
if (!transport) return <span className="muted">i18n:govoplan-mail.not_configured.811931bb</span>;
@@ -999,6 +1062,11 @@ function TransportCell({ profile, protocol }: {profile: MailServerProfile;protoc
return <div><strong>{transportLabel(transport)}</strong><div className="muted small-note">i18n:govoplan-mail.password.8be3c943 {hasPassword ? "configured" : "i18n:govoplan-mail.not_configured.67f2141f"}</div></div>;
}
function transportConfigured(profile: MailServerProfile, protocol: "smtp" | "imap"): boolean {
const transport = protocol === "smtp" ? profile.smtp : profile.imap;
return Boolean(transport?.host);
}
function mailCredentialConfigured(profile: MailServerProfile, protocol: "smtp" | "imap"): boolean {
return protocol === "smtp" ? profile.smtp_password_configured : profile.imap_password_configured;
}
@@ -1007,6 +1075,13 @@ function mailCredentialPolicySummary(policy: MailProfilePolicy | null, protocol:
return credentialInheritanceLabel(protocol === "smtp" ? policy?.smtp_credentials : policy?.imap_credentials);
}
function profileDialogTitle(editing: EditingProfile, target: MailProfileEditTarget): string {
if (editing === "new") return "i18n:govoplan-mail.create_mail_profile.4d2f8f9f";
if (target.kind === "server") return `Edit ${target.protocol.toUpperCase()} server`;
if (target.kind === "credentials") return i18nMessage("i18n:govoplan-mail.edit_value_credentials.1e2dc4f6", { value0: target.protocol.toUpperCase() });
return "i18n:govoplan-mail.edit_mail_profile.95d1af9c";
}
function emptyProfileDraft(): ProfileDraft {
return {
name: "",
@@ -1112,7 +1187,7 @@ function rawImapPayload(draft: ProfileDraft, preserveBlankPassword: boolean): Ma
}
function hasDraftImapSettings(draft: ProfileDraft): boolean {
return hasMailImapSettings([draft.imapHost, draft.imapUsername, draft.imapPassword]);
return hasMailImapSettings([draft.imapHost]);
}
function normalizePolicy(value: MailProfilePolicy | null | undefined): MailProfilePolicy {
+54 -24
View File
@@ -11,8 +11,8 @@ import {
type ApiSettings
} from "@govoplan/core-webui";
import {
bootstrapMailbox,
getMailboxMessage,
listMailboxFolders,
listMailboxMessages,
listMailServerProfiles,
type MailImapFolderResponse,
@@ -50,6 +50,7 @@ export default function MailboxPage({ settings }: {settings: ApiSettings;}) {
const messageListRequestRef = useRef(0);
const messageDetailRequestRef = useRef(0);
const mailboxPageCursorsRef = useRef<Record<string, string | null>>({});
const skipNextMessageLoadRef = useRef(false);
const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) ?? null;
const imapProfiles = useMemo(() => profiles.filter((profile) => profile.is_active && profile.imap), [profiles]);
@@ -70,8 +71,15 @@ export default function MailboxPage({ settings }: {settings: ApiSettings;}) {
useEffect(() => {void loadProfiles();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
useEffect(() => {selectedMessageKeyRef.current = selectedMessageKeyState;}, [selectedMessageKeyState]);
useEffect(() => {if (messagePage > messagePageCount) setMessagePage(messagePageCount);}, [messagePage, messagePageCount]);
useEffect(() => {if (selectedProfileId) void loadFolders(selectedProfileId);}, [selectedProfileId]);
useEffect(() => {if (selectedProfileId && selectedFolder && foldersReady) void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize);}, [foldersReady, messagePage, messagePageSize, selectedProfileId, selectedFolder]);
useEffect(() => {if (selectedProfileId) void loadMailboxBootstrap(selectedProfileId);}, [selectedProfileId]);
useEffect(() => {
if (!selectedProfileId || !selectedFolder || !foldersReady) return;
if (skipNextMessageLoadRef.current) {
skipNextMessageLoadRef.current = false;
return;
}
void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize);
}, [foldersReady, messagePage, messagePageSize, selectedProfileId, selectedFolder]);
useEffect(() => {
const handlePreviewShortcut = (event: KeyboardEvent) => {
@@ -130,6 +138,8 @@ export default function MailboxPage({ settings }: {settings: ApiSettings;}) {
setSelectedMessage(null);
setSelectedMessageKeyState("");
setPendingMessageKey("");
mailboxPageCursorsRef.current = {};
skipNextMessageLoadRef.current = false;
}
} catch (err) {
setError(errorText(err));
@@ -138,52 +148,70 @@ export default function MailboxPage({ settings }: {settings: ApiSettings;}) {
}
}
async function loadFolders(profileId = selectedProfileId) {
async function loadMailboxBootstrap(profileId = selectedProfileId, refresh = false) {
if (!profileId) return;
const requestId = ++folderRequestRef.current;
messageListRequestRef.current += 1;
const folderRequestId = ++folderRequestRef.current;
const messageRequestId = ++messageListRequestRef.current;
messageDetailRequestRef.current += 1;
setLoadingFolders(true);
setLoadingMessages(true);
setFoldersLoadedForProfile("");
setMessageTotalCount(null);
setMessagePage(1);
setSelectedMessage(null);
setSelectedMessageKeyState("");
selectedMessageKeyRef.current = "";
setPendingMessageKey("");
setFolderError("");
setMessageError("");
setDetailError("");
setError("");
try {
const response = await listMailboxFolders(settings, profileId);
if (requestId !== folderRequestRef.current) return;
if (!response.ok) throw new Error(response.message || "i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e");
const loaded = response.folders?.length ? response.folders : [{ name: "INBOX", flags: [] }];
setFolders(loaded);
setExpandedFolders(new Set());
let nextFolder = selectedFolder;
if (!nextFolder || !loaded.some((folder) => folder.name === nextFolder)) {
nextFolder = loaded.some((folder) => folder.name === "INBOX") ? "INBOX" : response.detected_sent_folder || loaded[0]?.name || "INBOX";
const response = await bootstrapMailbox(settings, profileId, selectedFolder || "INBOX", messagePageSize, 0, refresh);
if (folderRequestId !== folderRequestRef.current || messageRequestId !== messageListRequestRef.current) return;
if (!response.folders.ok) throw new Error(response.folders.message || "i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e");
const loadedFolders = response.folders.folders?.length ? response.folders.folders : [{ name: "INBOX", flags: [] }];
const loadedMessages = response.messages.messages ?? [];
const total = response.messages.total_count ?? loadedMessages.length;
let nextFolder = response.folder || response.messages.folder || selectedFolder || "INBOX";
if (!loadedFolders.some((folder) => folder.name === nextFolder)) {
nextFolder = loadedFolders.some((folder) => folder.name === "INBOX") ? "INBOX" : response.folders.detected_sent_folder || loadedFolders[0]?.name || "INBOX";
}
const foldersWithCounts = loadedFolders.map((folder) => folder.name === nextFolder ? { ...folder, message_count: total } : folder);
const cursorKey = mailboxCursorKey(profileId, nextFolder, messagePageSize);
mailboxPageCursorsRef.current = {
[`${cursorKey}:1`]: null,
[`${cursorKey}:2`]: response.messages.next_cursor ?? null
};
skipNextMessageLoadRef.current = foldersLoadedForProfile !== profileId || nextFolder !== selectedFolder || messagePage !== 1;
setFolders(foldersWithCounts);
setExpandedFolders(new Set());
setSelectedFolder(nextFolder);
setFoldersLoadedForProfile(profileId);
setMessages(loadedMessages);
setMessageTotalCount(total);
} catch (err) {
if (requestId !== folderRequestRef.current) return;
setFolderError(errorText(err));
setError(errorText(err));
if (folderRequestId !== folderRequestRef.current || messageRequestId !== messageListRequestRef.current) return;
const message = errorText(err);
skipNextMessageLoadRef.current = false;
setFolderError(message);
setMessageError(message);
setError(message);
setFolders([]);
setFoldersLoadedForProfile("");
setMessages([]);
setMessageTotalCount(null);
setSelectedMessage(null);
setSelectedMessageKeyState("");
selectedMessageKeyRef.current = "";
setPendingMessageKey("");
} finally {
if (requestId === folderRequestRef.current) setLoadingFolders(false);
if (folderRequestId === folderRequestRef.current) setLoadingFolders(false);
if (messageRequestId === messageListRequestRef.current) setLoadingMessages(false);
}
}
async function loadMessages(profileId = selectedProfileId, folder = selectedFolder, page = messagePage, pageSize = messagePageSize) {
async function loadMessages(profileId = selectedProfileId, folder = selectedFolder, page = messagePage, pageSize = messagePageSize, refresh = false) {
if (!profileId || !folder) return;
const requestId = ++messageListRequestRef.current;
const offset = (Math.max(1, page) - 1) * pageSize;
@@ -194,12 +222,12 @@ export default function MailboxPage({ settings }: {settings: ApiSettings;}) {
setDetailError("");
setError("");
try {
const response = await listMailboxMessages(settings, profileId, folder, pageSize, offset, cursor);
const response = await listMailboxMessages(settings, profileId, folder, pageSize, offset, cursor, refresh);
if (requestId !== messageListRequestRef.current) return;
const loaded = response.messages ?? [];
const total = response.total_count ?? loaded.length;
if (page <= 1) mailboxPageCursorsRef.current[`${cursorKey}:1`] = null;
if (response.next_cursor) mailboxPageCursorsRef.current[`${cursorKey}:${page + 1}`] = response.next_cursor;
mailboxPageCursorsRef.current[`${cursorKey}:${page + 1}`] = response.next_cursor ?? null;
setMessages(loaded);
setMessageTotalCount(total);
setFolderMessageCount(folder, total);
@@ -271,6 +299,8 @@ export default function MailboxPage({ settings }: {settings: ApiSettings;}) {
setLoadingFolders(false);
setLoadingMessages(false);
setLoadingMessage(false);
mailboxPageCursorsRef.current = {};
skipNextMessageLoadRef.current = false;
}
function openFolderNode(node: MailFolderNode) {
@@ -365,11 +395,11 @@ export default function MailboxPage({ settings }: {settings: ApiSettings;}) {
<RefreshCw size={16} aria-hidden="true" />
i18n:govoplan-mail.profiles.0c2a9300
</Button>
<Button onClick={() => void loadFolders(selectedProfileId)} disabled={!selectedProfileId || loadingFolders} title="i18n:govoplan-mail.refresh_mailbox_folders.d9af9963">
<Button onClick={() => void loadMailboxBootstrap(selectedProfileId, true)} disabled={!selectedProfileId || loadingFolders || loadingMessages} title="i18n:govoplan-mail.refresh_mailbox_folders.d9af9963">
<RefreshCw size={16} aria-hidden="true" />
i18n:govoplan-mail.folders.19adc47b
</Button>
<Button onClick={() => void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize)} disabled={!selectedProfileId || !selectedFolder || !foldersReady || loadingMessages} title="i18n:govoplan-mail.refresh_messages_in_the_current_folder.b6546a2c">
<Button onClick={() => void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize, true)} disabled={!selectedProfileId || !selectedFolder || !foldersReady || loadingMessages} title="i18n:govoplan-mail.refresh_messages_in_the_current_folder.b6546a2c">
<RefreshCw size={16} aria-hidden="true" />
i18n:govoplan-mail.messages.f1702b46
</Button>
@@ -0,0 +1,60 @@
export type MailProfileProtocol = "smtp" | "imap";
export type MailProfileEditSection = MailProfileProtocol | "advanced";
export type MailProfilePanelMode = "all" | "server" | "credentials";
export type MailProfileEditTarget =
| {kind: "create";}
| {kind: "profile";}
| {kind: "server";protocol: MailProfileProtocol;}
| {kind: "credentials";protocol: MailProfileProtocol;};
export type MailProfileTransportLike = {
host?: string | null;
};
export type MailProfileTreeProfileLike = {
id: string;
imap?: MailProfileTransportLike | null;
};
export type MailProfileChildDescriptor = {
kind: "server" | "credential";
id: string;
protocol: MailProfileProtocol;
};
export function mailProfileChildDescriptors(profile: MailProfileTreeProfileLike): MailProfileChildDescriptor[] {
const children: MailProfileChildDescriptor[] = [
{ kind: "server", id: `server:${profile.id}:smtp`, protocol: "smtp" },
{ kind: "credential", id: `credential:${profile.id}:smtp`, protocol: "smtp" },
{ kind: "server", id: `server:${profile.id}:imap`, protocol: "imap" }
];
if (profile.imap) children.push({ kind: "credential", id: `credential:${profile.id}:imap`, protocol: "imap" });
return children;
}
export function mailProfileEditTargetInitialSection(target: MailProfileEditTarget): MailProfileEditSection {
if (target.kind === "server" || target.kind === "credentials") return target.protocol;
return "smtp";
}
export function mailProfileEditTargetPanelMode(target: MailProfileEditTarget): MailProfilePanelMode | null {
if (target.kind === "create") return "all";
if (target.kind === "server") return "server";
if (target.kind === "credentials") return "credentials";
return null;
}
export function mailProfileEditTargetVisibleSections(target: MailProfileEditTarget): MailProfileEditSection[] {
if (target.kind === "server" || target.kind === "credentials") return [target.protocol];
if (target.kind === "create") return ["smtp", "imap", "advanced"];
return [];
}
export function mailProfileEditTargetShowsProfileFields(target: MailProfileEditTarget): boolean {
return target.kind === "create" || target.kind === "profile";
}
export function mailProfileEditTargetShowsSettingsPanel(target: MailProfileEditTarget): boolean {
return target.kind !== "profile";
}