feat(mail): complete Quick Access message launches
This commit is contained in:
@@ -497,6 +497,12 @@ Before claiming a Mail composition is production-ready:
|
|||||||
is stable.
|
is stable.
|
||||||
- POP3 except for a future explicit legacy download/import requirement.
|
- POP3 except for a future explicit legacy download/import requirement.
|
||||||
- A full mail client with compose/reply/move/delete/read-state mutation.
|
- A full mail client with compose/reply/move/delete/read-state mutation.
|
||||||
|
- Quick Access may launch the operating environment's configured composer via
|
||||||
|
`mailto:`. That explicit handoff is not a GovOPlaN Mail delivery: it selects
|
||||||
|
no Mail profile or credential, bypasses no Mail policy, and reports no
|
||||||
|
GovOPlaN delivery result. Recent-message and Drafts links remain read-only
|
||||||
|
deep links into the authorized Mail profile and preserve their Quick Access
|
||||||
|
return context.
|
||||||
- Recovery-ledger adoption for future provider-side move, delete, and flag
|
- Recovery-ledger adoption for future provider-side move, delete, and flag
|
||||||
mutations; no such production path exists in the current read-only mailbox.
|
mutations; no such production path exists in the current read-only mailbox.
|
||||||
- Proof that process-local throttling coordinates multiple workers when Redis
|
- Proof that process-local throttling coordinates multiple workers when Redis
|
||||||
|
|||||||
@@ -554,7 +554,11 @@ manifest = ModuleManifest(
|
|||||||
body=(
|
body=(
|
||||||
"Mail contributes its full mailbox to Communication. When Quick Access is enabled, its compact provider surface "
|
"Mail contributes its full mailbox to Communication. When Quick Access is enabled, its compact provider surface "
|
||||||
"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. "
|
||||||
"The shared drawer does not merge channel state, credentials, custody, delivery semantics, or authorization."
|
"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. "
|
||||||
|
"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."
|
||||||
),
|
),
|
||||||
layer="configured",
|
layer="configured",
|
||||||
documentation_types=("user", "admin"),
|
documentation_types=("user", "admin"),
|
||||||
@@ -567,7 +571,12 @@ manifest = ModuleManifest(
|
|||||||
"body": (
|
"body": (
|
||||||
"Mail ordnet das vollständige Postfach Kommunikation zu. Ist der Schnellzugriff aktiviert, erscheint die kompakte "
|
"Mail ordnet das vollständige Postfach Kommunikation zu. Ist der Schnellzugriff aktiviert, erscheint die kompakte "
|
||||||
"Mail-Oberfläche gemeinsam mit unabhängigen Beiträgen aus Postbox und künftig Chat unter Nachrichten. "
|
"Mail-Oberfläche gemeinsam mit unabhängigen Beiträgen aus Postbox und künftig Chat unter Nachrichten. "
|
||||||
"Die gemeinsame Darstellung führt weder Kanalzustand noch Zugangsdaten, Verwahrung oder Berechtigungen zusammen."
|
"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, "
|
||||||
|
"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."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:mail-ui": "rm -rf .mail-test-build && mkdir -p .mail-test-build && printf '{\"type\":\"commonjs\"}\\n' > .mail-test-build/package.json && tsc -p tsconfig.mail-tests.json && node .mail-test-build/tests/mailbox-display.test.js && node .mail-test-build/tests/mailbox-folders.test.js && node .mail-test-build/tests/mail-profile-editor-model.test.js && node .mail-test-build/tests/mail-policy-validation.test.js && node scripts/test-mailbox-icon-button-structure.mjs && node scripts/test-interface-pattern-language.mjs"
|
"test:mail-ui": "rm -rf .mail-test-build && mkdir -p .mail-test-build && printf '{\"type\":\"commonjs\"}\\n' > .mail-test-build/package.json && tsc -p tsconfig.mail-tests.json && node .mail-test-build/tests/mailbox-display.test.js && node .mail-test-build/tests/mailbox-folders.test.js && node .mail-test-build/tests/mailbox-launch.test.js && node .mail-test-build/tests/mail-profile-editor-model.test.js && node .mail-test-build/tests/mail-policy-validation.test.js && node scripts/test-mailbox-icon-button-structure.mjs && node scripts/test-interface-pattern-language.mjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
|
|||||||
@@ -1,27 +1,39 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Mail } from "lucide-react";
|
import { ExternalLink, FilePenLine, Mail, Pencil } from "lucide-react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import {
|
import {
|
||||||
DashboardWidgetList,
|
DashboardWidgetList,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
|
quickAccessLaunchState,
|
||||||
useDashboardWidgetData,
|
useDashboardWidgetData,
|
||||||
type ApiSettings
|
type QuickAccessToolRenderContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
bootstrapMailbox,
|
bootstrapMailbox,
|
||||||
listMailServerProfiles,
|
listMailServerProfiles,
|
||||||
type MailMailboxMessageSummary
|
type MailMailboxMessageSummary
|
||||||
} from "../../api/mail";
|
} from "../../api/mail";
|
||||||
|
import {
|
||||||
|
mailboxDraftsLaunchPath,
|
||||||
|
mailboxMessageLaunchPath
|
||||||
|
} from "./mailboxLaunch";
|
||||||
|
|
||||||
|
|
||||||
type MailQuickAccessData = {
|
type MailQuickAccessData = {
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
|
profileId?: string;
|
||||||
|
draftsFolder?: string | null;
|
||||||
messages: MailMailboxMessageSummary[];
|
messages: MailMailboxMessageSummary[];
|
||||||
available: boolean;
|
available: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MailQuickAccess({ settings }: { settings: ApiSettings }) {
|
type Props = Pick<
|
||||||
|
QuickAccessToolRenderContext,
|
||||||
|
"settings" | "launchContext" | "close"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default function MailQuickAccess({ settings, launchContext, close }: Props) {
|
||||||
const load = useCallback(async (): Promise<MailQuickAccessData> => {
|
const load = useCallback(async (): Promise<MailQuickAccessData> => {
|
||||||
const profiles = await listMailServerProfiles(settings);
|
const profiles = await listMailServerProfiles(settings);
|
||||||
const profile = profiles.find((item) => item.is_active && item.imap);
|
const profile = profiles.find((item) => item.is_active && item.imap);
|
||||||
@@ -29,6 +41,10 @@ export default function MailQuickAccess({ settings }: { settings: ApiSettings })
|
|||||||
const response = await bootstrapMailbox(settings, profile.id, "INBOX", 7, 0, false);
|
const response = await bootstrapMailbox(settings, profile.id, "INBOX", 7, 0, false);
|
||||||
return {
|
return {
|
||||||
profileName: profile.name,
|
profileName: profile.name,
|
||||||
|
profileId: profile.id,
|
||||||
|
draftsFolder: profile.imap?.folder_mappings?.drafts
|
||||||
|
|| response.folders.detected_folder_mappings?.drafts
|
||||||
|
|| null,
|
||||||
messages: response.messages.messages ?? [],
|
messages: response.messages.messages ?? [],
|
||||||
available: true
|
available: true
|
||||||
};
|
};
|
||||||
@@ -46,11 +62,33 @@ export default function MailQuickAccess({ settings }: { settings: ApiSettings })
|
|||||||
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: "/mail"
|
to: mailboxMessageLaunchPath(data!.profileId!, message),
|
||||||
|
state: quickAccessLaunchState(launchContext),
|
||||||
|
onClick: close
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<div className="dashboard-contribution-footer">
|
<div className="dashboard-contribution-footer">
|
||||||
<Link className="btn btn-secondary" to="/mail">i18n:govoplan-mail.mail.92379cbb</Link>
|
<a className="btn btn-secondary" href="mailto:" onClick={close}>
|
||||||
|
<Pencil size={15} aria-hidden="true" /> i18n:govoplan-mail.compose
|
||||||
|
</a>
|
||||||
|
{data?.profileId && data.draftsFolder ? (
|
||||||
|
<Link
|
||||||
|
className="btn btn-secondary"
|
||||||
|
to={mailboxDraftsLaunchPath(data.profileId, data.draftsFolder)}
|
||||||
|
state={quickAccessLaunchState(launchContext)}
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
<FilePenLine size={15} aria-hidden="true" /> i18n:govoplan-mail.drafts.22a31d86
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
<Link
|
||||||
|
className="btn btn-secondary"
|
||||||
|
to="/mail"
|
||||||
|
state={quickAccessLaunchState(launchContext)}
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
<ExternalLink size={15} aria-hidden="true" /> i18n:govoplan-mail.open_mail
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Activity, ChevronRight, Database, Home, Mail, MailOpen, Paperclip, RefreshCw, Search, X } from "lucide-react";
|
import { Activity, ChevronRight, Database, Home, Mail, MailOpen, Paperclip, RefreshCw, Search, X } from "lucide-react";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
import { ToolbarGroup, ActionToolbar,
|
import { ToolbarGroup, ActionToolbar,
|
||||||
ActionBlockerHint,
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
"../../api/mail";
|
"../../api/mail";
|
||||||
import { buildMailboxFolderTree, findFolderNodeId, folderAncestorIds, type MailFolderNode } from "./mailboxFolders";
|
import { buildMailboxFolderTree, findFolderNodeId, folderAncestorIds, type MailFolderNode } from "./mailboxFolders";
|
||||||
import { isMailboxMessageRead, mailboxSyncState, type MailboxSyncProvenance } from "./mailboxDisplay";
|
import { isMailboxMessageRead, mailboxSyncState, type MailboxSyncProvenance } from "./mailboxDisplay";
|
||||||
|
import { mailboxLaunchFolder, parseMailboxLaunch, type MailboxLaunch } from "./mailboxLaunch";
|
||||||
|
|
||||||
const MAILBOX_DOCUMENTATION = {
|
const MAILBOX_DOCUMENTATION = {
|
||||||
topicId: "mail.workflow.read-mailbox",
|
topicId: "mail.workflow.read-mailbox",
|
||||||
@@ -38,6 +40,7 @@ const MAILBOX_DOCUMENTATION = {
|
|||||||
|
|
||||||
export default function MailboxPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
export default function MailboxPage({ settings, auth }: { settings: ApiSettings; auth: AuthInfo }) {
|
||||||
const navigate = useGuardedNavigate();
|
const navigate = useGuardedNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const [profiles, setProfiles] = useState<MailServerProfile[]>([]);
|
const [profiles, setProfiles] = useState<MailServerProfile[]>([]);
|
||||||
const [selectedProfileId, setSelectedProfileId] = useState("");
|
const [selectedProfileId, setSelectedProfileId] = useState("");
|
||||||
const [folders, setFolders] = useState<MailImapFolderResponse[]>([]);
|
const [folders, setFolders] = useState<MailImapFolderResponse[]>([]);
|
||||||
@@ -67,6 +70,7 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
|||||||
const messageDetailRequestRef = useRef(0);
|
const messageDetailRequestRef = useRef(0);
|
||||||
const mailboxPageCursorsRef = useRef<Record<string, string | null>>({});
|
const mailboxPageCursorsRef = useRef<Record<string, string | null>>({});
|
||||||
const skipNextMessageLoadRef = useRef(false);
|
const skipNextMessageLoadRef = useRef(false);
|
||||||
|
const launchRequestRef = useRef<MailboxLaunch | null>(parseMailboxLaunch(location.search));
|
||||||
|
|
||||||
const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) ?? null;
|
const selectedProfile = profiles.find((profile) => profile.id === selectedProfileId) ?? null;
|
||||||
const imapProfiles = useMemo(() => profiles.filter((profile) => profile.is_active && profile.imap), [profiles]);
|
const imapProfiles = useMemo(() => profiles.filter((profile) => profile.is_active && profile.imap), [profiles]);
|
||||||
@@ -99,6 +103,21 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
useEffect(() => {void loadProfiles();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
useEffect(() => {void loadProfiles();}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
||||||
|
useEffect(() => {
|
||||||
|
const request = parseMailboxLaunch(location.search);
|
||||||
|
launchRequestRef.current = request;
|
||||||
|
if (!profiles.length || (!request.profileId && !request.folder && !request.folderRole && !request.messageUid)) return;
|
||||||
|
const usable = profiles.filter((profile) => profile.is_active && profile.imap);
|
||||||
|
const targetProfileId = request.profileId && usable.some((profile) => profile.id === request.profileId)
|
||||||
|
? request.profileId
|
||||||
|
: selectedProfileId || usable[0]?.id || "";
|
||||||
|
if (!targetProfileId) return;
|
||||||
|
if (targetProfileId !== selectedProfileId) {
|
||||||
|
selectProfile(targetProfileId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void loadMailboxBootstrap(targetProfileId);
|
||||||
|
}, [location.search]);
|
||||||
useEffect(() => {selectedMessageKeyRef.current = selectedMessageKeyState;}, [selectedMessageKeyState]);
|
useEffect(() => {selectedMessageKeyRef.current = selectedMessageKeyState;}, [selectedMessageKeyState]);
|
||||||
useEffect(() => {if (messagePage > messagePageCount) setMessagePage(messagePageCount);}, [messagePage, messagePageCount]);
|
useEffect(() => {if (messagePage > messagePageCount) setMessagePage(messagePageCount);}, [messagePage, messagePageCount]);
|
||||||
useEffect(() => {if (selectedProfileId) void loadMailboxBootstrap(selectedProfileId);}, [selectedProfileId]);
|
useEffect(() => {if (selectedProfileId) void loadMailboxBootstrap(selectedProfileId);}, [selectedProfileId]);
|
||||||
@@ -111,6 +130,19 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
|||||||
void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize);
|
void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize);
|
||||||
}, [foldersReady, messagePage, messagePageSize, selectedProfileId, selectedFolder]);
|
}, [foldersReady, messagePage, messagePageSize, selectedProfileId, selectedFolder]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const request = launchRequestRef.current;
|
||||||
|
if (!request || !foldersReady || loadingMessages) return;
|
||||||
|
const targetProfileMatches = !request.profileId || request.profileId === selectedProfileId;
|
||||||
|
const targetFolder = mailboxLaunchFolder(request, selectedProfile);
|
||||||
|
if (!targetProfileMatches || (targetFolder && targetFolder !== selectedFolder)) return;
|
||||||
|
const target = request.messageUid
|
||||||
|
? messages.find((message) => message.uid === request.messageUid)
|
||||||
|
: null;
|
||||||
|
launchRequestRef.current = null;
|
||||||
|
if (target) void openMessage(target);
|
||||||
|
}, [foldersReady, loadingMessages, messages, selectedFolder, selectedProfile, selectedProfileId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handlePreviewShortcut = (event: KeyboardEvent) => {
|
const handlePreviewShortcut = (event: KeyboardEvent) => {
|
||||||
if (isEditableTarget(event.target)) return;
|
if (isEditableTarget(event.target)) return;
|
||||||
@@ -158,8 +190,15 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
|||||||
try {
|
try {
|
||||||
const loaded = await listMailServerProfiles(settings);
|
const loaded = await listMailServerProfiles(settings);
|
||||||
const usable = loaded.filter((profile) => profile.is_active && profile.imap);
|
const usable = loaded.filter((profile) => profile.is_active && profile.imap);
|
||||||
|
const requestedProfileId = parseMailboxLaunch(location.search).profileId;
|
||||||
setProfiles(loaded);
|
setProfiles(loaded);
|
||||||
setSelectedProfileId((current) => current && usable.some((profile) => profile.id === current) ? current : usable[0]?.id ?? "");
|
setSelectedProfileId((current) =>
|
||||||
|
requestedProfileId && usable.some((profile) => profile.id === requestedProfileId)
|
||||||
|
? requestedProfileId
|
||||||
|
: current && usable.some((profile) => profile.id === current)
|
||||||
|
? current
|
||||||
|
: usable[0]?.id ?? ""
|
||||||
|
);
|
||||||
if (usable.length === 0) {
|
if (usable.length === 0) {
|
||||||
setFolders([]);
|
setFolders([]);
|
||||||
setFoldersLoadedForProfile("");
|
setFoldersLoadedForProfile("");
|
||||||
@@ -182,9 +221,13 @@ export default function MailboxPage({ settings, auth }: { settings: ApiSettings;
|
|||||||
async function loadMailboxBootstrap(profileId = selectedProfileId, refresh = false) {
|
async function loadMailboxBootstrap(profileId = selectedProfileId, refresh = false) {
|
||||||
if (!profileId) return;
|
if (!profileId) return;
|
||||||
const profileInbox = profiles.find((profile) => profile.id === profileId)?.imap?.folder_mappings?.inbox || "";
|
const profileInbox = profiles.find((profile) => profile.id === profileId)?.imap?.folder_mappings?.inbox || "";
|
||||||
const requestedFolder = foldersLoadedForProfile === profileId && selectedFolder
|
const requestedLaunch = launchRequestRef.current;
|
||||||
|
const requestedLaunchFolder = requestedLaunch && (!requestedLaunch.profileId || requestedLaunch.profileId === profileId)
|
||||||
|
? mailboxLaunchFolder(requestedLaunch, profiles.find((profile) => profile.id === profileId) ?? null)
|
||||||
|
: null;
|
||||||
|
const requestedFolder = requestedLaunchFolder || (foldersLoadedForProfile === profileId && selectedFolder
|
||||||
? selectedFolder
|
? selectedFolder
|
||||||
: profileInbox || "INBOX";
|
: profileInbox || "INBOX");
|
||||||
const folderRequestId = ++folderRequestRef.current;
|
const folderRequestId = ++folderRequestRef.current;
|
||||||
const messageRequestId = ++messageListRequestRef.current;
|
const messageRequestId = ++messageListRequestRef.current;
|
||||||
messageDetailRequestRef.current += 1;
|
messageDetailRequestRef.current += 1;
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
type MessageReference = { folder: string; uid: string };
|
||||||
|
type ProfileFolderMapping = {
|
||||||
|
imap?: { folder_mappings?: { drafts?: string | null } | null } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MailboxLaunch = {
|
||||||
|
profileId: string | null;
|
||||||
|
folder: string | null;
|
||||||
|
folderRole: "drafts" | null;
|
||||||
|
messageUid: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseMailboxLaunch(search: string): MailboxLaunch {
|
||||||
|
const params = new URLSearchParams(search);
|
||||||
|
return {
|
||||||
|
profileId: boundedValue(params.get("profile")),
|
||||||
|
folder: boundedValue(params.get("folder")),
|
||||||
|
folderRole: params.get("folderRole") === "drafts" ? "drafts" : null,
|
||||||
|
messageUid: boundedValue(params.get("message"))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mailboxMessageLaunchPath(
|
||||||
|
profileId: string,
|
||||||
|
message: MessageReference
|
||||||
|
): string {
|
||||||
|
return mailPath({
|
||||||
|
profile: profileId,
|
||||||
|
folder: message.folder,
|
||||||
|
message: message.uid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mailboxDraftsLaunchPath(profileId: string, folder?: string | null): string {
|
||||||
|
return mailPath(folder
|
||||||
|
? { profile: profileId, folder }
|
||||||
|
: { profile: profileId, folderRole: "drafts" });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mailboxLaunchFolder(
|
||||||
|
launch: MailboxLaunch,
|
||||||
|
profile: ProfileFolderMapping | null
|
||||||
|
): string | null {
|
||||||
|
if (launch.folder) return launch.folder;
|
||||||
|
if (launch.folderRole === "drafts") {
|
||||||
|
return boundedValue(profile?.imap?.folder_mappings?.drafts ?? null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mailPath(values: Record<string, string>): string {
|
||||||
|
const params = new URLSearchParams(values);
|
||||||
|
return `/mail?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundedValue(value: string | null): string | null {
|
||||||
|
const clean = value?.trim() ?? "";
|
||||||
|
return clean && clean.length <= 500 ? clean : null;
|
||||||
|
}
|
||||||
@@ -95,6 +95,8 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-mail.mail_profile_policy.f2ac4b92": "Mail profile policy",
|
"i18n:govoplan-mail.mail_profile_policy.f2ac4b92": "Mail profile policy",
|
||||||
"i18n:govoplan-mail.mail_server_profiles.b1726682": "Mail server profiles",
|
"i18n:govoplan-mail.mail_server_profiles.b1726682": "Mail server profiles",
|
||||||
"i18n:govoplan-mail.mail.92379cbb": "Mail",
|
"i18n:govoplan-mail.mail.92379cbb": "Mail",
|
||||||
|
"i18n:govoplan-mail.compose": "Compose",
|
||||||
|
"i18n:govoplan-mail.open_mail": "Open Mail",
|
||||||
"i18n:govoplan-mail.quick_access_description": "Recent mailbox messages and mail actions.",
|
"i18n:govoplan-mail.quick_access_description": "Recent mailbox messages and mail actions.",
|
||||||
"i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e": "Mailbox folders could not be loaded.",
|
"i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e": "Mailbox folders could not be loaded.",
|
||||||
"i18n:govoplan-mail.mailbox_folders.c92f6de4": "Mailbox folders",
|
"i18n:govoplan-mail.mailbox_folders.c92f6de4": "Mailbox folders",
|
||||||
@@ -293,6 +295,8 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-mail.mail_profile_policy.f2ac4b92": "Mail profile policy",
|
"i18n:govoplan-mail.mail_profile_policy.f2ac4b92": "Mail profile policy",
|
||||||
"i18n:govoplan-mail.mail_server_profiles.b1726682": "Mail server profiles",
|
"i18n:govoplan-mail.mail_server_profiles.b1726682": "Mail server profiles",
|
||||||
"i18n:govoplan-mail.mail.92379cbb": "Mail",
|
"i18n:govoplan-mail.mail.92379cbb": "Mail",
|
||||||
|
"i18n:govoplan-mail.compose": "Verfassen",
|
||||||
|
"i18n:govoplan-mail.open_mail": "Mail öffnen",
|
||||||
"i18n:govoplan-mail.quick_access_description": "Aktuelle Posteingangsnachrichten und Mail-Aktionen.",
|
"i18n:govoplan-mail.quick_access_description": "Aktuelle Posteingangsnachrichten und Mail-Aktionen.",
|
||||||
"i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e": "Mailbox folders could not be loaded.",
|
"i18n:govoplan-mail.mailbox_folders_could_not_be_loaded.c3e3880e": "Mailbox folders could not be loaded.",
|
||||||
"i18n:govoplan-mail.mailbox_folders.c92f6de4": "Mailbox folders",
|
"i18n:govoplan-mail.mailbox_folders.c92f6de4": "Mailbox folders",
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ const mailQuickAccessTools: QuickAccessToolsUiCapability = {
|
|||||||
tools: [
|
tools: [
|
||||||
{
|
{
|
||||||
id: "mail.messages",
|
id: "mail.messages",
|
||||||
render: ({ settings }) => createElement(MailQuickAccess, { settings })
|
render: (context) => createElement(MailQuickAccess, context)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
function assertEqual(actual: unknown, expected: unknown): void {
|
||||||
|
if (actual !== expected) throw new Error(`expected ${String(expected)}, got ${String(actual)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertDeepEqual(actual: unknown, expected: unknown): void {
|
||||||
|
const actualJson = JSON.stringify(actual);
|
||||||
|
const expectedJson = JSON.stringify(expected);
|
||||||
|
if (actualJson !== expectedJson) throw new Error(`expected ${expectedJson}, got ${actualJson}`);
|
||||||
|
}
|
||||||
|
import {
|
||||||
|
mailboxDraftsLaunchPath,
|
||||||
|
mailboxLaunchFolder,
|
||||||
|
mailboxMessageLaunchPath,
|
||||||
|
parseMailboxLaunch
|
||||||
|
} from "../src/features/mail/mailboxLaunch";
|
||||||
|
|
||||||
|
assertDeepEqual(
|
||||||
|
parseMailboxLaunch("?profile=profile-1&folder=INBOX%2FTeam&message=42"),
|
||||||
|
{
|
||||||
|
profileId: "profile-1",
|
||||||
|
folder: "INBOX/Team",
|
||||||
|
folderRole: null,
|
||||||
|
messageUid: "42"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEqual(
|
||||||
|
mailboxMessageLaunchPath("profile 1", { folder: "INBOX/Team", uid: "42" }),
|
||||||
|
"/mail?profile=profile+1&folder=INBOX%2FTeam&message=42"
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
mailboxDraftsLaunchPath("profile-1"),
|
||||||
|
"/mail?profile=profile-1&folderRole=drafts"
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
mailboxDraftsLaunchPath("profile-1", "Entwürfe"),
|
||||||
|
"/mail?profile=profile-1&folder=Entw%C3%BCrfe"
|
||||||
|
);
|
||||||
|
assertEqual(
|
||||||
|
mailboxLaunchFolder(parseMailboxLaunch("?folderRole=drafts"), {
|
||||||
|
imap: { folder_mappings: { drafts: "Entwürfe" } }
|
||||||
|
}),
|
||||||
|
"Entwürfe"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("mailbox launch tests passed");
|
||||||
@@ -19,10 +19,12 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"tests/mailbox-display.test.ts",
|
"tests/mailbox-display.test.ts",
|
||||||
"tests/mailbox-folders.test.ts",
|
"tests/mailbox-folders.test.ts",
|
||||||
|
"tests/mailbox-launch.test.ts",
|
||||||
"tests/mail-profile-editor-model.test.ts",
|
"tests/mail-profile-editor-model.test.ts",
|
||||||
"tests/mail-policy-validation.test.ts",
|
"tests/mail-policy-validation.test.ts",
|
||||||
"src/features/mail/mailboxDisplay.ts",
|
"src/features/mail/mailboxDisplay.ts",
|
||||||
"src/features/mail/mailboxFolders.ts",
|
"src/features/mail/mailboxFolders.ts",
|
||||||
|
"src/features/mail/mailboxLaunch.ts",
|
||||||
"src/features/mail/mailProfileEditorModel.ts",
|
"src/features/mail/mailProfileEditorModel.ts",
|
||||||
"src/features/mail/mailPolicyValidation.ts"
|
"src/features/mail/mailPolicyValidation.ts"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user