feat: integrate governed address contacts
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Activity, ChevronRight, Database, Home, Mail, MailOpen, Paperclip, RefreshCw, Search, X } from "lucide-react";
|
||||
import { Activity, Check, ChevronRight, Database, Home, Mail, MailOpen, Paperclip, RefreshCw, Search, UserPlus, X } from "lucide-react";
|
||||
import { useLocation } from "react-router";
|
||||
import { ToolbarGroup, ActionToolbar,
|
||||
ActionBlockerHint,
|
||||
@@ -21,9 +21,12 @@ import { ToolbarGroup, ActionToolbar,
|
||||
} from "@govoplan/core-webui";
|
||||
import {
|
||||
bootstrapMailbox,
|
||||
createMailAddressContact,
|
||||
getMailboxMessage,
|
||||
listMailAddressWriteTargets,
|
||||
listMailboxMessages,
|
||||
listMailServerProfiles,
|
||||
type MailAddressWriteTarget,
|
||||
type MailImapFolderResponse,
|
||||
type MailMailboxMessageDetail,
|
||||
type MailMailboxMessageSummary,
|
||||
@@ -32,6 +35,7 @@ import {
|
||||
import { buildMailboxFolderTree, findFolderNodeId, folderAncestorIds, type MailFolderNode } from "./mailboxFolders";
|
||||
import { isMailboxMessageRead, mailboxSyncState, type MailboxSyncProvenance } from "./mailboxDisplay";
|
||||
import { mailboxLaunchFolder, parseMailboxLaunch, type MailboxLaunch } from "./mailboxLaunch";
|
||||
import { mailboxHeaderAddresses } from "./mailAddressIntegration";
|
||||
|
||||
const MAILBOX_DOCUMENTATION = {
|
||||
topicId: "mail.workflow.read-mailbox",
|
||||
@@ -628,6 +632,8 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
||||
}))}
|
||||
emptyText={previewEmptyText} />
|
||||
|
||||
<MailboxContactActions settings={settings} message={selectedMessage} />
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -641,6 +647,146 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
||||
|
||||
}
|
||||
|
||||
function MailboxContactActions({
|
||||
settings,
|
||||
message
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
message: MailMailboxMessageDetail | null;
|
||||
}) {
|
||||
const [available, setAvailable] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [targets, setTargets] = useState<MailAddressWriteTarget[]>([]);
|
||||
const [selectedTargetId, setSelectedTargetId] = useState("");
|
||||
const [creatingEmail, setCreatingEmail] = useState("");
|
||||
const [addedEmails, setAddedEmails] = useState<Set<string>>(() => new Set());
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setLoaded(false);
|
||||
void listMailAddressWriteTargets(settings)
|
||||
.then((response) => {
|
||||
if (!active) return;
|
||||
const writable = response.targets.filter((target) => target.allowed);
|
||||
setAvailable(response.available);
|
||||
setTargets(response.targets);
|
||||
setSelectedTargetId((current) => writable.some((target) => target.address_book_id === current)
|
||||
? current
|
||||
: writable[0]?.address_book_id || "");
|
||||
setError("");
|
||||
})
|
||||
.catch((loadError) => {
|
||||
if (!active) return;
|
||||
setAvailable(false);
|
||||
setTargets([]);
|
||||
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) setLoaded(true);
|
||||
});
|
||||
return () => { active = false; };
|
||||
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
setAddedEmails(new Set());
|
||||
setError("");
|
||||
setSuccess("");
|
||||
}, [message?.folder, message?.uid]);
|
||||
|
||||
if (!message || !loaded || !available) return null;
|
||||
|
||||
const writableTargets = targets.filter((target) => target.allowed);
|
||||
const blockedTargets = targets.filter((target) => !target.allowed);
|
||||
const addresses = uniqueMailboxAddresses([
|
||||
...mailboxHeaderAddresses(message.from_header),
|
||||
...mailboxHeaderAddresses(message.to_header),
|
||||
...mailboxHeaderAddresses(message.cc_header)
|
||||
]);
|
||||
|
||||
async function addContact(address: { name?: string | null; email: string }) {
|
||||
if (!selectedTargetId || creatingEmail) return;
|
||||
setCreatingEmail(address.email);
|
||||
setError("");
|
||||
setSuccess("");
|
||||
try {
|
||||
const result = await createMailAddressContact(settings, {
|
||||
address_book_id: selectedTargetId,
|
||||
display_name: address.name || address.email,
|
||||
email: address.email
|
||||
});
|
||||
setAddedEmails((current) => new Set(current).add(address.email));
|
||||
setSuccess(i18nMessage("i18n:govoplan-mail.contact_added", { value0: result.display_name }));
|
||||
} catch (createError) {
|
||||
setError(createError instanceof Error ? createError.message : String(createError));
|
||||
} finally {
|
||||
setCreatingEmail("");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mailbox-contact-actions" aria-label="i18n:govoplan-mail.address_book_actions">
|
||||
<h4>i18n:govoplan-mail.address_book_actions</h4>
|
||||
{error ? <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||
{success ? <DismissibleAlert tone="success" resetKey={success}>{success}</DismissibleAlert> : null}
|
||||
{writableTargets.length > 0 ? (
|
||||
<label className="mailbox-contact-target">
|
||||
<span>i18n:govoplan-mail.save_contacts_to</span>
|
||||
<select value={selectedTargetId} onChange={(event) => setSelectedTargetId(event.target.value)}>
|
||||
{writableTargets.map((target) => (
|
||||
<option key={target.address_book_id} value={target.address_book_id}>
|
||||
{target.address_book_label || target.address_book_id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
) : (
|
||||
<p className="form-help">i18n:govoplan-mail.no_writable_address_book</p>
|
||||
)}
|
||||
<div className="mailbox-contact-candidates">
|
||||
{addresses.map((address) => {
|
||||
const added = addedEmails.has(address.email);
|
||||
return (
|
||||
<Button
|
||||
key={address.email}
|
||||
className="compact"
|
||||
disabled={!selectedTargetId || Boolean(creatingEmail) || added}
|
||||
disabledReason={!selectedTargetId ? blockedTargets[0]?.message || "i18n:govoplan-mail.no_writable_address_book" : undefined}
|
||||
onClick={() => void addContact(address)}
|
||||
>
|
||||
{added ? <Check size={15} aria-hidden="true" /> : <UserPlus size={15} aria-hidden="true" />}
|
||||
{added ? "i18n:govoplan-mail.contact_added_short" : i18nMessage("i18n:govoplan-mail.add_value_to_contacts", { value0: address.name || address.email })}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{blockedTargets.length > 0 ? (
|
||||
<details className="mailbox-contact-policy">
|
||||
<summary>i18n:govoplan-mail.unavailable_address_books</summary>
|
||||
<ul>
|
||||
{blockedTargets.map((target) => (
|
||||
<li key={target.address_book_id}>
|
||||
<strong>{target.address_book_label || target.address_book_id}</strong>: {target.message}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function uniqueMailboxAddresses<T extends { email: string }>(addresses: T[]): T[] {
|
||||
const seen = new Set<string>();
|
||||
return addresses.filter((address) => {
|
||||
const email = address.email.toLocaleLowerCase();
|
||||
if (seen.has(email)) return false;
|
||||
seen.add(email);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function mailboxMessageKey(folder: string, uid: string): string {
|
||||
return `${folder || "INBOX"}::${uid}`;
|
||||
|
||||
Reference in New Issue
Block a user