feat(mail): return exact messages from Quick Access

This commit is contained in:
2026-08-21 16:03:29 +02:00
parent 2fe56fca00
commit 3ce112497c
4 changed files with 66 additions and 11 deletions
+11 -1
View File
@@ -436,7 +436,9 @@ manifest = ModuleManifest(
full_page_path="/mail", full_page_path="/mail",
required_any=("mail:mailbox:read",), required_any=("mail:mailbox:read",),
order=10, order=10,
modes=("browse", "compose"), modes=("browse", "compose", "select"),
returned_reference_kinds=("mail.message",),
help_context_id="mail.quick_access.messages",
), ),
), ),
), ),
@@ -621,6 +623,10 @@ manifest = ModuleManifest(
"appears inside the shared Messages drawer alongside independent Postbox and future chat contributions. " "appears inside the shared Messages drawer alongside independent Postbox and future chat contributions. "
"Recent entries open the exact authorized profile, folder, and message; Drafts resolves the configured or " "Recent entries open the exact authorized profile, folder, and message; Drafts resolves the configured or "
"provider-detected Drafts folder, and the full Mail page keeps the versioned Quick Access return context. " "provider-detected Drafts folder, and the full Mail page keeps the versioned Quick Access return context. "
"When an active Case requests a selection, choosing a recent message returns only its tenant-bound profile, "
"folder, UID version, label, and owner route through the shared result contract. Cases discards the label and "
"does not receive headers, participants, preview, body, attachments, credentials, or an access decision; Mail "
"reauthorizes the mailbox list now and the exact message again whenever its owner route is opened. "
"Compose deliberately launches the user's configured mail application because GovOPlaN Mail's mailbox is " "Compose deliberately launches the user's configured mail application because GovOPlaN Mail's mailbox is "
"read-only; it neither selects a GovOPlaN transport profile nor claims a GovOPlaN delivery. The shared drawer " "read-only; it neither selects a GovOPlaN transport profile nor claims a GovOPlaN delivery. The shared drawer "
"does not merge channel state, credentials, custody, delivery semantics, or authorization." "does not merge channel state, credentials, custody, delivery semantics, or authorization."
@@ -639,6 +645,10 @@ manifest = ModuleManifest(
"Aktuelle Einträge öffnen das genaue berechtigte Profil, den Ordner und die Nachricht; Entwürfe verwendet " "Aktuelle Einträge öffnen das genaue berechtigte Profil, den Ordner und die Nachricht; Entwürfe verwendet "
"den konfigurierten oder vom Anbieter erkannten Entwurfsordner. Die vollständige Mail-Seite erhält den " "den konfigurierten oder vom Anbieter erkannten Entwurfsordner. Die vollständige Mail-Seite erhält den "
"versionierten Rücksprungkontext. Verfassen öffnet bewusst die konfigurierte Mail-Anwendung des Benutzers, " "versionierten Rücksprungkontext. Verfassen öffnet bewusst die konfigurierte Mail-Anwendung des Benutzers, "
"Bei einem aktiven Vorgang gibt die Auswahl einer aktuellen Nachricht nur den mandantengebundenen Profil-, "
"Ordner- und UID-Verweis sowie die Eigentümerroute über den gemeinsamen Ergebnisvertrag zurück. Vorgänge "
"übernehmen weder Kopfzeilen, Beteiligte, Vorschau, Inhalt, Anlagen, Zugangsdaten noch eine Zugriffsentscheidung; "
"Mail prüft den Zugriff beim Auflisten und beim späteren Öffnen erneut. "
"da das GovOPlaN-Mail-Postfach nur lesend arbeitet; dabei wird weder ein GovOPlaN-Transportprofil gewählt " "da das GovOPlaN-Mail-Postfach nur lesend arbeitet; dabei wird weder ein GovOPlaN-Transportprofil gewählt "
"noch eine GovOPlaN-Zustellung behauptet. Die gemeinsame Darstellung führt weder Kanalzustand noch " "noch eine GovOPlaN-Zustellung behauptet. Die gemeinsame Darstellung führt weder Kanalzustand noch "
"Zugangsdaten, Verwahrung, Zustelllogik oder Berechtigungen zusammen." "Zugangsdaten, Verwahrung, Zustelllogik oder Berechtigungen zusammen."
+9
View File
@@ -9,6 +9,15 @@ from govoplan_mail.backend.manifest import _mail_retirement_provider, get_manife
class MailManifestTests(unittest.TestCase): class MailManifestTests(unittest.TestCase):
def test_mail_quick_access_can_return_exact_message_references(self) -> None:
frontend = get_manifest().frontend
self.assertIsNotNone(frontend)
tool = next(
item for item in frontend.quick_access_tools if item.id == "mail.messages" # type: ignore[union-attr]
)
self.assertIn("select", tool.modes)
self.assertEqual(("mail.message",), tool.returned_reference_kinds)
def test_manifest_declares_optional_addresses_lookup(self) -> None: def test_manifest_declares_optional_addresses_lookup(self) -> None:
manifest = get_manifest() manifest = get_manifest()
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/mail-webui", "name": "@govoplan/mail-webui",
"version": "0.1.18", "version": "0.1.19",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+44 -8
View File
@@ -1,8 +1,9 @@
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { ExternalLink, FilePenLine, Mail, Pencil } from "lucide-react"; import { ExternalLink, FilePenLine, Mail, Pencil, X } from "lucide-react";
import { Link } from "react-router"; import { Link } from "react-router";
import { import {
DashboardWidgetList, DashboardWidgetList,
Button,
DismissibleAlert, DismissibleAlert,
EmailAddressInput, EmailAddressInput,
LoadingFrame, LoadingFrame,
@@ -34,10 +35,16 @@ type MailQuickAccessData = {
type Props = Pick< type Props = Pick<
QuickAccessToolRenderContext, QuickAccessToolRenderContext,
"settings" | "launchContext" | "close" "settings" | "launchContext" | "complete" | "cancel" | "close"
>; >;
export default function MailQuickAccess({ settings, launchContext, close }: Props) { export default function MailQuickAccess({
settings,
launchContext,
complete,
cancel,
close
}: Props) {
const [composing, setComposing] = useState(false); const [composing, setComposing] = useState(false);
const [recipients, setRecipients] = useState<MailboxAddress[]>([]); const [recipients, setRecipients] = useState<MailboxAddress[]>([]);
const [suggestions, setSuggestions] = useState<MailboxAddress[]>([]); const [suggestions, setSuggestions] = useState<MailboxAddress[]>([]);
@@ -60,6 +67,8 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
}; };
}, [settings]); }, [settings]);
const { data, loading, error } = useDashboardWidgetData(load, 0); const { data, loading, error } = useDashboardWidgetData(load, 0);
const selectingForCase = launchContext.activeObject?.ownerModule === "cases"
&& launchContext.activeObject.kind === "case";
const lookupRecipients = useCallback(async (query: string) => { const lookupRecipients = useCallback(async (query: string) => {
const request = ++lookupRequestRef.current; const request = ++lookupRequestRef.current;
@@ -85,6 +94,12 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
return ( return (
<LoadingFrame loading={loading} label="i18n:govoplan-mail.loading_messages.4294022c"> <LoadingFrame loading={loading} label="i18n:govoplan-mail.loading_messages.4294022c">
{error ? <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert> : null} {error ? <DismissibleAlert tone="warning" resetKey={error}>{error}</DismissibleAlert> : null}
{selectingForCase ? (
<p className="muted small-note">
Select an authorized exact message for {launchContext.activeObject?.label}.
Mail content remains in Mail and access is checked again when opened.
</p>
) : null}
<DashboardWidgetList <DashboardWidgetList
emptyText={data?.available ? "i18n:govoplan-mail.no_messages_in_this_folder.5c7fa25d" : "i18n:govoplan-mail.select_an_imap_profile.5445648c"} emptyText={data?.available ? "i18n:govoplan-mail.no_messages_in_this_folder.5c7fa25d" : "i18n:govoplan-mail.select_an_imap_profile.5445648c"}
items={(data?.messages ?? []).map((message) => ({ items={(data?.messages ?? []).map((message) => ({
@@ -93,12 +108,27 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
detail: message.from_header || data?.profileName, detail: message.from_header || data?.profileName,
meta: formatMessageDate(message.date), meta: formatMessageDate(message.date),
leading: <Mail size={17} aria-hidden="true" />, leading: <Mail size={17} aria-hidden="true" />,
to: mailboxMessageLaunchPath(data!.profileId!, message), to: selectingForCase
state: quickAccessLaunchState(launchContext), ? undefined
onClick: close : mailboxMessageLaunchPath(data!.profileId!, message),
state: selectingForCase ? undefined : quickAccessLaunchState(launchContext),
onClick: selectingForCase ? () => complete({
contractVersion: "1",
outcome: "completed",
action: "selected",
reference: {
ownerModule: "mail",
kind: "message",
objectId: `${data!.profileId!}:${message.folder}:${message.uid}`,
tenantId: launchContext.tenantId,
label: message.subject || "Mail message",
version: message.uid,
path: mailboxMessageLaunchPath(data!.profileId!, message)
}
}) : close
}))} }))}
/> />
{composing ? ( {!selectingForCase && composing ? (
<div className="mail-quick-compose" aria-label="i18n:govoplan-mail.compose"> <div className="mail-quick-compose" aria-label="i18n:govoplan-mail.compose">
<label>i18n:govoplan-mail.recipients</label> <label>i18n:govoplan-mail.recipients</label>
<EmailAddressInput <EmailAddressInput
@@ -125,7 +155,13 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
</div> </div>
</div> </div>
) : null} ) : null}
{selectingForCase ? (
<div className="dashboard-contribution-footer"> <div className="dashboard-contribution-footer">
<Button onClick={() => cancel("user")}>
<X size={15} aria-hidden="true" /> Cancel selection
</Button>
</div>
) : <div className="dashboard-contribution-footer">
<button type="button" className="btn btn-secondary" onClick={() => setComposing((current) => !current)} aria-expanded={composing}> <button type="button" className="btn btn-secondary" onClick={() => setComposing((current) => !current)} aria-expanded={composing}>
<Pencil size={15} aria-hidden="true" /> i18n:govoplan-mail.compose <Pencil size={15} aria-hidden="true" /> i18n:govoplan-mail.compose
</button> </button>
@@ -147,7 +183,7 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
> >
<ExternalLink size={15} aria-hidden="true" /> i18n:govoplan-mail.open_mail <ExternalLink size={15} aria-hidden="true" /> i18n:govoplan-mail.open_mail
</Link> </Link>
</div> </div>}
</LoadingFrame> </LoadingFrame>
); );
} }