feat(mail): return exact messages from Quick Access
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-21 16:03:29 +02:00
parent 2fe56fca00
commit cfd3847524
4 changed files with 66 additions and 11 deletions
+11 -1
View File
@@ -436,7 +436,9 @@ manifest = ModuleManifest(
full_page_path="/mail",
required_any=("mail:mailbox:read",),
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. "
"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. "
"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 "
"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."
@@ -639,6 +645,10 @@ manifest = ModuleManifest(
"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 "
"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 "
"noch eine GovOPlaN-Zustellung behauptet. Die gemeinsame Darstellung führt weder Kanalzustand noch "
"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):
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:
manifest = get_manifest()
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/mail-webui",
"version": "0.1.18",
"version": "0.1.19",
"private": true,
"type": "module",
"main": "src/index.ts",
+44 -8
View File
@@ -1,8 +1,9 @@
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 {
DashboardWidgetList,
Button,
DismissibleAlert,
EmailAddressInput,
LoadingFrame,
@@ -34,10 +35,16 @@ type MailQuickAccessData = {
type Props = Pick<
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 [recipients, setRecipients] = useState<MailboxAddress[]>([]);
const [suggestions, setSuggestions] = useState<MailboxAddress[]>([]);
@@ -60,6 +67,8 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
};
}, [settings]);
const { data, loading, error } = useDashboardWidgetData(load, 0);
const selectingForCase = launchContext.activeObject?.ownerModule === "cases"
&& launchContext.activeObject.kind === "case";
const lookupRecipients = useCallback(async (query: string) => {
const request = ++lookupRequestRef.current;
@@ -85,6 +94,12 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
return (
<LoadingFrame loading={loading} label="i18n:govoplan-mail.loading_messages.4294022c">
{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
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) => ({
@@ -93,12 +108,27 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
detail: message.from_header || data?.profileName,
meta: formatMessageDate(message.date),
leading: <Mail size={17} aria-hidden="true" />,
to: mailboxMessageLaunchPath(data!.profileId!, message),
state: quickAccessLaunchState(launchContext),
onClick: close
to: selectingForCase
? undefined
: 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">
<label>i18n:govoplan-mail.recipients</label>
<EmailAddressInput
@@ -125,7 +155,13 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
</div>
</div>
) : null}
{selectingForCase ? (
<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}>
<Pencil size={15} aria-hidden="true" /> i18n:govoplan-mail.compose
</button>
@@ -147,7 +183,7 @@ export default function MailQuickAccess({ settings, launchContext, close }: Prop
>
<ExternalLink size={15} aria-hidden="true" /> i18n:govoplan-mail.open_mail
</Link>
</div>
</div>}
</LoadingFrame>
);
}