diff --git a/pyproject.toml b/pyproject.toml index 725bf12..fe051fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-mail" -version = "0.1.2" +version = "0.1.3" description = "GovOPlaN mail module with backend and WebUI integration." readme = "README.md" requires-python = ">=3.12" license = { file = "LICENSE" } authors = [{ name = "GovOPlaN" }] dependencies = [ - "govoplan-core>=0.1.2", + "govoplan-core>=0.1.3", "pydantic>=2,<3", "redis>=5,<6", "SQLAlchemy>=2,<3", diff --git a/src/govoplan_mail/backend/manifest.py b/src/govoplan_mail/backend/manifest.py index f6d27d6..e4b66c0 100644 --- a/src/govoplan_mail/backend/manifest.py +++ b/src/govoplan_mail/backend/manifest.py @@ -65,7 +65,7 @@ def _mail_router(context: ModuleContext): manifest = ModuleManifest( id="mail", name="Mail", - version="1.0.0", + version="0.1.2", dependencies=("access",), optional_dependencies=(), permissions=PERMISSIONS, @@ -90,4 +90,3 @@ manifest = ModuleManifest( def get_manifest() -> ModuleManifest: return manifest - diff --git a/webui/package.json b/webui/package.json index fe50766..03b2b30 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/mail-webui", - "version": "0.1.1", + "version": "0.1.2", "private": true, "type": "module", "main": "src/index.ts", @@ -14,7 +14,7 @@ "./styles/mail-profiles.css": "./src/styles/mail-profiles.css" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.1", + "@govoplan/core-webui": "^0.1.2", "lucide-react": "^0.555.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/webui/src/features/mail/MailboxPage.tsx b/webui/src/features/mail/MailboxPage.tsx index 2d14613..66b68e1 100644 --- a/webui/src/features/mail/MailboxPage.tsx +++ b/webui/src/features/mail/MailboxPage.tsx @@ -31,7 +31,7 @@ export default function MailboxPage({ settings }: { settings: ApiSettings }) { const [messages, setMessages] = useState([]); const [messageTotalCount, setMessageTotalCount] = useState(null); const [messagePage, setMessagePage] = useState(1); - const [messagePageSize, setMessagePageSize] = useState(50); + const [messagePageSize, setMessagePageSize] = useState(10); const [messageQuery, setMessageQuery] = useState(""); const [selectedMessage, setSelectedMessage] = useState(null); const [selectedMessageKeyState, setSelectedMessageKeyState] = useState(""); @@ -71,6 +71,37 @@ export default function MailboxPage({ settings }: { settings: ApiSettings }) { useEffect(() => { if (selectedProfileId) void loadFolders(selectedProfileId); }, [selectedProfileId]); useEffect(() => { if (selectedProfileId && selectedFolder && foldersReady) void loadMessages(selectedProfileId, selectedFolder, messagePage, messagePageSize); }, [foldersReady, messagePage, messagePageSize, selectedProfileId, selectedFolder]); + useEffect(() => { + const handlePreviewShortcut = (event: KeyboardEvent) => { + if (isEditableTarget(event.target)) return; + if (event.key === "Escape") { + if (selectedMessage || selectedMessageKey || pendingMessageKey || detailError) { + event.preventDefault(); + setSelectedMessage(null); + setSelectedMessageKeyState(""); + selectedMessageKeyRef.current = ""; + setPendingMessageKey(""); + setDetailError(""); + } + return; + } + if (shellBusy || loadingMessage || filteredMessages.length === 0) return; + const selectedIndex = filteredMessages.findIndex((message) => mailboxMessageKey(message.folder || selectedFolder, message.uid) === selectedMessageKey); + let nextIndex: number | null = null; + if (event.key === "ArrowLeft") nextIndex = selectedIndex > 0 ? selectedIndex - 1 : 0; + else if (event.key === "ArrowRight") nextIndex = selectedIndex >= 0 ? Math.min(filteredMessages.length - 1, selectedIndex + 1) : 0; + else if (event.key === "Home") nextIndex = 0; + else if (event.key === "End") nextIndex = filteredMessages.length - 1; + if (nextIndex === null) return; + event.preventDefault(); + const nextMessage = filteredMessages[nextIndex]; + if (nextMessage) void openMessage(nextMessage); + }; + + window.addEventListener("keydown", handlePreviewShortcut); + return () => window.removeEventListener("keydown", handlePreviewShortcut); + }, [detailError, filteredMessages, loadingMessage, pendingMessageKey, selectedFolder, selectedMessage, selectedMessageKey, shellBusy]); + useEffect(() => { const ancestorIds = folderAncestorIds(folderTree, selectedFolder); if (ancestorIds.length === 0) return; @@ -458,7 +489,7 @@ function MailboxPagination({ page, pageSize, totalRows, disabled, onPageChange, const pageCount = Math.max(1, Math.ceil(totalRows / pageSize)); const first = totalRows === 0 ? 0 : (page - 1) * pageSize + 1; const last = Math.min(totalRows, page * pageSize); - const options = [25, 50, 100]; + const options = [10, 25, 50, 100]; return (
{first}-{last} of {totalRows}
@@ -549,3 +580,8 @@ function formatBytes(value?: number | null): string { function errorText(error: unknown): string { return error instanceof Error ? error.message : String(error); } + +function isEditableTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return false; + return target.isContentEditable || ["INPUT", "SELECT", "TEXTAREA"].includes(target.tagName); +}