@rapidmx/web-client 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +382 -0
- package/apps/admin/_404.tsx +9 -0
- package/apps/admin/_500.tsx +9 -0
- package/apps/admin/_layout.tsx +41 -0
- package/apps/admin/audit-log/index.tsx +187 -0
- package/apps/admin/branding/index.tsx +404 -0
- package/apps/admin/distribution-lists/[uid].tsx +70 -0
- package/apps/admin/distribution-lists/index.tsx +115 -0
- package/apps/admin/distribution-lists/new/index.tsx +114 -0
- package/apps/admin/domains/[uid].tsx +259 -0
- package/apps/admin/domains/index.tsx +125 -0
- package/apps/admin/domains/new/index.tsx +84 -0
- package/apps/admin/index.tsx +82 -0
- package/apps/admin/ingest-queue/index.tsx +116 -0
- package/apps/admin/mailboxes/[uid].tsx +126 -0
- package/apps/admin/mailboxes/new/index.tsx +291 -0
- package/apps/admin/quarantine/index.tsx +171 -0
- package/apps/admin/transport-rules/[uid].tsx +143 -0
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +75 -0
- package/apps/admin/transport-rules/index.tsx +117 -0
- package/apps/admin/transport-rules/new/index.tsx +105 -0
- package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +117 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +186 -0
- package/apps/shared/components/admin/mailboxes/MailboxTable.tsx +73 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +154 -0
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +124 -0
- package/apps/shared/components/booking/AvailabilityEditor.tsx +118 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +119 -0
- package/apps/shared/components/calendar/EventModal.tsx +513 -0
- package/apps/shared/components/calendar/MonthView.tsx +139 -0
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +180 -0
- package/apps/shared/components/calendar/ResourcePicker.tsx +80 -0
- package/apps/shared/components/calendar/SplitDayView.tsx +137 -0
- package/apps/shared/components/calendar/TimeGridView.tsx +198 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +211 -0
- package/apps/shared/components/contacts/ContactDetailPane.tsx +99 -0
- package/apps/shared/components/contacts/ContactForm.tsx +314 -0
- package/apps/shared/components/contacts/ContactsSidebar.tsx +242 -0
- package/apps/shared/components/contacts/ContactsToolbar.tsx +115 -0
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +197 -0
- package/apps/shared/components/layout/AppShell.tsx +165 -0
- package/apps/shared/components/layout/BrandingChrome.tsx +25 -0
- package/apps/shared/components/layout/MailboxProvisioning.tsx +110 -0
- package/apps/shared/components/layout/UserMenu.tsx +150 -0
- package/apps/shared/components/mail/ConversationList.tsx +61 -0
- package/apps/shared/components/mail/ConversationThreadPane.tsx +174 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +390 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +114 -0
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +418 -0
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +518 -0
- package/apps/shared/components/mail/compose/EmojiPicker.tsx +64 -0
- package/apps/shared/components/mail/compose/GifPicker.tsx +82 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +83 -0
- package/apps/shared/components/mail/compose/ScheduleSendPicker.tsx +64 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +287 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +276 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +223 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +201 -0
- package/apps/shared/components/tasks/TasksToolbar.tsx +68 -0
- package/apps/shared/components/tasks/layout/TasksShell.tsx +199 -0
- package/apps/shared/styles/app.css +265 -0
- package/apps/www/_404.tsx +9 -0
- package/apps/www/_500.tsx +9 -0
- package/apps/www/_layout.tsx +45 -0
- package/apps/www/calendar/index.tsx +407 -0
- package/apps/www/contacts/[uid].tsx +88 -0
- package/apps/www/contacts/index.tsx +436 -0
- package/apps/www/index.tsx +375 -0
- package/apps/www/messages/[uid].tsx +70 -0
- package/apps/www/settings/auto-reply/index.tsx +129 -0
- package/apps/www/settings/booking-types/[uid].tsx +300 -0
- package/apps/www/settings/booking-types/index.tsx +98 -0
- package/apps/www/settings/booking-types/new/index.tsx +212 -0
- package/apps/www/settings/filters/[uid].tsx +155 -0
- package/apps/www/settings/filters/_mailFilterRuleConfig.tsx +102 -0
- package/apps/www/settings/filters/index.tsx +99 -0
- package/apps/www/settings/filters/new/index.tsx +126 -0
- package/apps/www/settings/focused-inbox/index.tsx +148 -0
- package/apps/www/settings/read-receipts/index.tsx +125 -0
- package/apps/www/settings/signatures/[uid].tsx +167 -0
- package/apps/www/settings/signatures/index.tsx +85 -0
- package/apps/www/settings/signatures/new/index.tsx +129 -0
- package/apps/www/settings/signatures/signatureDefaults.ts +36 -0
- package/apps/www/tasks/index.tsx +530 -0
- package/dist/apps/admin/_404.js +4 -0
- package/dist/apps/admin/_500.js +4 -0
- package/dist/apps/admin/_layout.js +10 -0
- package/dist/apps/admin/audit-log/index.js +66 -0
- package/dist/apps/admin/branding/index.js +126 -0
- package/dist/apps/admin/distribution-lists/[uid].js +34 -0
- package/dist/apps/admin/distribution-lists/index.js +31 -0
- package/dist/apps/admin/distribution-lists/new/index.js +51 -0
- package/dist/apps/admin/domains/[uid].js +102 -0
- package/dist/apps/admin/domains/index.js +31 -0
- package/dist/apps/admin/domains/new/index.js +41 -0
- package/dist/apps/admin/index.js +32 -0
- package/dist/apps/admin/ingest-queue/index.js +48 -0
- package/dist/apps/admin/mailboxes/[uid].js +59 -0
- package/dist/apps/admin/mailboxes/new/index.js +93 -0
- package/dist/apps/admin/quarantine/index.js +66 -0
- package/dist/apps/admin/transport-rules/[uid].js +87 -0
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +37 -0
- package/dist/apps/admin/transport-rules/index.js +32 -0
- package/dist/apps/admin/transport-rules/new/index.js +50 -0
- package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +64 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +94 -0
- package/dist/apps/shared/components/admin/mailboxes/MailboxTable.js +16 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +59 -0
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +67 -0
- package/dist/apps/shared/components/booking/AvailabilityEditor.js +45 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +44 -0
- package/dist/apps/shared/components/calendar/EventModal.js +167 -0
- package/dist/apps/shared/components/calendar/MonthView.js +38 -0
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +55 -0
- package/dist/apps/shared/components/calendar/ResourcePicker.js +36 -0
- package/dist/apps/shared/components/calendar/SplitDayView.js +44 -0
- package/dist/apps/shared/components/calendar/TimeGridView.js +58 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +96 -0
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -0
- package/dist/apps/shared/components/contacts/ContactForm.js +85 -0
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +105 -0
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +32 -0
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +93 -0
- package/dist/apps/shared/components/layout/AppShell.js +68 -0
- package/dist/apps/shared/components/layout/BrandingChrome.js +18 -0
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +75 -0
- package/dist/apps/shared/components/layout/UserMenu.js +59 -0
- package/dist/apps/shared/components/mail/ConversationList.js +17 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +112 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +153 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +55 -0
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +88 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +261 -0
- package/dist/apps/shared/components/mail/compose/EmojiPicker.js +25 -0
- package/dist/apps/shared/components/mail/compose/GifPicker.js +36 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +60 -0
- package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.js +33 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +139 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +78 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +100 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +81 -0
- package/dist/apps/shared/components/tasks/TasksToolbar.js +15 -0
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +92 -0
- package/dist/apps/shared/styles/app.css +265 -0
- package/dist/apps/www/_404.js +4 -0
- package/dist/apps/www/_500.js +4 -0
- package/dist/apps/www/_layout.js +8 -0
- package/dist/apps/www/calendar/index.js +228 -0
- package/dist/apps/www/contacts/[uid].js +62 -0
- package/dist/apps/www/contacts/index.js +262 -0
- package/dist/apps/www/index.js +228 -0
- package/dist/apps/www/messages/[uid].js +47 -0
- package/dist/apps/www/settings/auto-reply/index.js +54 -0
- package/dist/apps/www/settings/booking-types/[uid].js +129 -0
- package/dist/apps/www/settings/booking-types/index.js +29 -0
- package/dist/apps/www/settings/booking-types/new/index.js +86 -0
- package/dist/apps/www/settings/filters/[uid].js +94 -0
- package/dist/apps/www/settings/filters/_mailFilterRuleConfig.js +65 -0
- package/dist/apps/www/settings/filters/index.js +30 -0
- package/dist/apps/www/settings/filters/new/index.js +61 -0
- package/dist/apps/www/settings/focused-inbox/index.js +69 -0
- package/dist/apps/www/settings/read-receipts/index.js +51 -0
- package/dist/apps/www/settings/signatures/[uid].js +96 -0
- package/dist/apps/www/settings/signatures/index.js +34 -0
- package/dist/apps/www/settings/signatures/new/index.js +63 -0
- package/dist/apps/www/settings/signatures/signatureDefaults.js +30 -0
- package/dist/apps/www/tasks/index.js +280 -0
- package/package.json +87 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { updateMailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
9
|
+
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
export default function SettingsReadReceiptsPage(props) {
|
|
13
|
+
return (_jsx(SettingsShell, { ...props, active: "read-receipts", children: _jsx(ReadReceiptsContent, {}) }));
|
|
14
|
+
}
|
|
15
|
+
function ReadReceiptsContent() {
|
|
16
|
+
const { mailboxUid, mailboxes } = useSettingsShell();
|
|
17
|
+
// `SettingsShell` only ever renders its children once `mailboxes` has loaded and `mailboxUid` has
|
|
18
|
+
// resolved to one of them — same established non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
|
|
19
|
+
const mailbox = mailboxes.find((mb) => mb.uid === mailboxUid);
|
|
20
|
+
const [alwaysRequestReceiptInternal, setAlwaysRequestReceiptInternal] = useState(mailbox.alwaysRequestReceiptInternal ?? true);
|
|
21
|
+
const [alwaysRequestReceiptExternal, setAlwaysRequestReceiptExternal] = useState(mailbox.alwaysRequestReceiptExternal ?? false);
|
|
22
|
+
const [autoSendReceiptsInternal, setAutoSendReceiptsInternal] = useState(mailbox.autoSendReceiptsInternal ?? true);
|
|
23
|
+
const [autoSendReceiptsExternal, setAutoSendReceiptsExternal] = useState(mailbox.autoSendReceiptsExternal ?? false);
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const [saving, setSaving] = useState(false);
|
|
26
|
+
const [saved, setSaved] = useState(false);
|
|
27
|
+
async function handleSubmit(e) {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
setError(null);
|
|
30
|
+
setSaved(false);
|
|
31
|
+
setSaving(true);
|
|
32
|
+
try {
|
|
33
|
+
await updateMailbox({
|
|
34
|
+
uid: mailbox.uid,
|
|
35
|
+
version: mailbox.version,
|
|
36
|
+
alwaysRequestReceiptInternal,
|
|
37
|
+
alwaysRequestReceiptExternal,
|
|
38
|
+
autoSendReceiptsInternal,
|
|
39
|
+
autoSendReceiptsExternal,
|
|
40
|
+
});
|
|
41
|
+
setSaved(true);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not save read receipt settings.");
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setSaving(false);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Read Receipts" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Controls when this mailbox requests a delivery/read receipt on mail it sends, and how it responds to a receipt requested of it. A per-message \u201CRequest a read receipt\u201D checkbox in Compose always overrides the request-side settings below for that one message." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Request receipts when sending" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptInternal, onChange: (e) => setAlwaysRequestReceiptInternal(e.target.checked) }), "From internal recipients (mail to this organization)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptExternal, onChange: (e) => setAlwaysRequestReceiptExternal(e.target.checked) }), "From external recipients"] })] }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Respond automatically to requests" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsInternal, onChange: (e) => setAutoSendReceiptsInternal(e.target.checked) }), "From internal senders"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsExternal, onChange: (e) => setAutoSendReceiptsExternal(e.target.checked) }), "From external senders"] }), _jsx("p", { className: "text-xs text-text-muted", children: "Unchecked means a request from that sender waits in your Inbox for you to send or decline it explicitly." })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { getMailSignature, listMailSignatures, updateMailSignature } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
9
|
+
import { clearPreviousDefaults } from "./signatureDefaults.js";
|
|
10
|
+
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
11
|
+
import RichTextEditor from "../../../shared/components/mail/compose/RichTextEditor.js";
|
|
12
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
13
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
14
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
15
|
+
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
16
|
+
/** Signatures aren't backed by a draft/attachment record the way Compose's own inline images are — see
|
|
17
|
+
* `settings/signatures/new/index.tsx`'s identical stub for the full reasoning. */
|
|
18
|
+
async function handleUploadImage() {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export default function SignatureDetailPage(props) {
|
|
22
|
+
return (_jsx(SettingsShell, { ...props, active: "signatures", children: _jsx(SignatureDetailContent, { uid: props.params.uid }) }));
|
|
23
|
+
}
|
|
24
|
+
function SignatureDetailContent({ uid }) {
|
|
25
|
+
const { mailboxUid } = useSettingsShell();
|
|
26
|
+
const [original, setOriginal] = useState(null);
|
|
27
|
+
const [name, setName] = useState("");
|
|
28
|
+
const [contentHtml, setContentHtml] = useState("");
|
|
29
|
+
const [isDefaultForNewMessages, setIsDefaultForNewMessages] = useState(false);
|
|
30
|
+
const [isDefaultForReplyForward, setIsDefaultForReplyForward] = useState(false);
|
|
31
|
+
const [loading, setLoading] = useState(true);
|
|
32
|
+
const [error, setError] = useState(null);
|
|
33
|
+
const [saving, setSaving] = useState(false);
|
|
34
|
+
const [saved, setSaved] = useState(false);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
setError(null);
|
|
38
|
+
getMailSignature(uid)
|
|
39
|
+
.then((loaded) => {
|
|
40
|
+
setOriginal(loaded);
|
|
41
|
+
if (loaded) {
|
|
42
|
+
setName(loaded.name);
|
|
43
|
+
setContentHtml(loaded.contentHtml);
|
|
44
|
+
setIsDefaultForNewMessages(loaded.isDefaultForNewMessages);
|
|
45
|
+
setIsDefaultForReplyForward(loaded.isDefaultForReplyForward);
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this signature."))
|
|
49
|
+
.finally(() => setLoading(false));
|
|
50
|
+
}, [uid]);
|
|
51
|
+
// Only ever invoked from the form below, which itself only renders once `original` is loaded (the
|
|
52
|
+
// early returns above cover every other state) — the non-null assertions reflect that real
|
|
53
|
+
// invariant, matching `MailFilterDetailContent.handleSubmit`'s identical pattern.
|
|
54
|
+
async function handleSubmit(e) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
setError(null);
|
|
57
|
+
if (!name.trim()) {
|
|
58
|
+
setError("A name is required.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
setSaving(true);
|
|
62
|
+
setSaved(false);
|
|
63
|
+
try {
|
|
64
|
+
if (isDefaultForNewMessages || isDefaultForReplyForward) {
|
|
65
|
+
const existing = await listMailSignatures(mailboxUid, { limit: 100 });
|
|
66
|
+
await clearPreviousDefaults(existing, original.uid, isDefaultForNewMessages, isDefaultForReplyForward);
|
|
67
|
+
}
|
|
68
|
+
const updated = await updateMailSignature({
|
|
69
|
+
uid: original.uid,
|
|
70
|
+
version: original.version,
|
|
71
|
+
name: name.trim(),
|
|
72
|
+
contentHtml,
|
|
73
|
+
isDefaultForNewMessages,
|
|
74
|
+
isDefaultForReplyForward,
|
|
75
|
+
});
|
|
76
|
+
setOriginal(updated);
|
|
77
|
+
setSaved(true);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not save this signature.");
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
setSaving(false);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (loading) {
|
|
87
|
+
return _jsx("p", { className: "p-6 text-sm text-text-muted", children: "Loading\u2026" });
|
|
88
|
+
}
|
|
89
|
+
if (error && !original) {
|
|
90
|
+
return (_jsx("div", { className: "p-6", children: _jsx(Alert, { children: error }) }));
|
|
91
|
+
}
|
|
92
|
+
if (!original) {
|
|
93
|
+
return (_jsx("div", { className: "p-6", children: _jsx(Alert, { children: "Signature not found." }) }));
|
|
94
|
+
}
|
|
95
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("a", { href: `/settings/signatures?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 All signatures" }), _jsx("h1", { className: "text-lg font-bold tracking-tight mt-1 mb-5", children: original.name }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6 flex flex-col gap-4", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: isDefaultForNewMessages, onChange: (e) => setIsDefaultForNewMessages(e.target.checked) }), "Use for new messages"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: isDefaultForReplyForward, onChange: (e) => setIsDefaultForReplyForward(e.target.checked) }), "Use for replies and forwards"] })] }), _jsx("div", { className: "h-64", children: _jsx(RichTextEditor, { value: contentHtml, onChange: setContentHtml, fill: true, onUploadImage: handleUploadImage }) }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] })] }) }));
|
|
96
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
9
|
+
import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
|
|
10
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
+
export default function SettingsSignaturesPage(props) {
|
|
13
|
+
return (_jsx(SettingsShell, { ...props, active: "signatures", children: _jsx(SignaturesContent, {}) }));
|
|
14
|
+
}
|
|
15
|
+
function SignaturesContent() {
|
|
16
|
+
const { mailboxUid } = useSettingsShell();
|
|
17
|
+
const [signatures, setSignatures] = useState([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
// `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same established
|
|
21
|
+
// non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
listMailSignatures(mailboxUid)
|
|
24
|
+
.then(setSignatures)
|
|
25
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load signatures."))
|
|
26
|
+
.finally(() => setLoading(false));
|
|
27
|
+
}, [mailboxUid]);
|
|
28
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight", children: "Signatures" }), _jsx("a", { href: `/settings/signatures/new?mailboxUid=${encodeURIComponent(mailboxUid)}`, children: _jsx(Button, { type: "button", className: "!w-auto", children: "+ New signature" }) })] }), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : signatures.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No signatures yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: signatures.map((sig) => (_jsxs("li", { className: "flex items-center justify-between border border-border rounded-md p-3", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-semibold", children: sig.name }), (sig.isDefaultForNewMessages || sig.isDefaultForReplyForward) && (_jsx("div", { className: "text-xs text-text-muted mt-0.5", children: [
|
|
29
|
+
sig.isDefaultForNewMessages && "Default for new messages",
|
|
30
|
+
sig.isDefaultForReplyForward && "Default for replies/forwards",
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" · ") }))] }), _jsx("a", { href: `/settings/signatures/${encodeURIComponent(sig.uid)}?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-primary-dark hover:underline font-medium text-sm", children: "Edit" })] }, sig.uid))) }))] }) }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
+
import { createMailSignature, listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
9
|
+
import { clearPreviousDefaults } from "../signatureDefaults.js";
|
|
10
|
+
import SettingsShell, { useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
|
|
11
|
+
import RichTextEditor from "../../../../shared/components/mail/compose/RichTextEditor.js";
|
|
12
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
13
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
14
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
15
|
+
const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
16
|
+
export default function NewSignaturePage(props) {
|
|
17
|
+
return (_jsx(SettingsShell, { ...props, active: "signatures", children: _jsx(NewSignatureForm, {}) }));
|
|
18
|
+
}
|
|
19
|
+
/** Signatures aren't backed by a draft/attachment record the way Compose's own inline images are (see
|
|
20
|
+
* `ComposeWindow.handleUploadImage()`) — inline images in a signature are out of scope for this phase
|
|
21
|
+
* (`.claude/NOTES.md`'s Phase 12 entry), so this is a real, deliberate no-op, not a stub left unfinished. */
|
|
22
|
+
async function handleUploadImage() {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
function NewSignatureForm() {
|
|
26
|
+
const { mailboxUid } = useSettingsShell();
|
|
27
|
+
const [name, setName] = useState("");
|
|
28
|
+
const [contentHtml, setContentHtml] = useState("");
|
|
29
|
+
const [isDefaultForNewMessages, setIsDefaultForNewMessages] = useState(false);
|
|
30
|
+
const [isDefaultForReplyForward, setIsDefaultForReplyForward] = useState(false);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const [saving, setSaving] = useState(false);
|
|
33
|
+
async function handleSubmit(e) {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
setError(null);
|
|
36
|
+
if (!name.trim()) {
|
|
37
|
+
setError("A name is required.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
setSaving(true);
|
|
41
|
+
try {
|
|
42
|
+
if (isDefaultForNewMessages || isDefaultForReplyForward) {
|
|
43
|
+
const existing = await listMailSignatures(mailboxUid, { limit: 100 });
|
|
44
|
+
await clearPreviousDefaults(existing, undefined, isDefaultForNewMessages, isDefaultForReplyForward);
|
|
45
|
+
}
|
|
46
|
+
const created = await createMailSignature({
|
|
47
|
+
mailboxUid: mailboxUid,
|
|
48
|
+
name: name.trim(),
|
|
49
|
+
contentHtml,
|
|
50
|
+
isDefaultForNewMessages,
|
|
51
|
+
isDefaultForReplyForward,
|
|
52
|
+
});
|
|
53
|
+
window.location.href = `/settings/signatures/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid)}`;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not create the signature.");
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
setSaving(false);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-5", children: "New signature" }), error && _jsx(Alert, { children: error }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6 flex flex-col gap-4", children: [_jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "Work signature" }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: isDefaultForNewMessages, onChange: (e) => setIsDefaultForNewMessages(e.target.checked) }), "Use for new messages"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: isDefaultForReplyForward, onChange: (e) => setIsDefaultForReplyForward(e.target.checked) }), "Use for replies and forwards"] })] }), _jsx("div", { className: "h-64", children: _jsx(RichTextEditor, { value: contentHtml, onChange: setContentHtml, fill: true, onUploadImage: handleUploadImage }) }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create signature" }), _jsx("a", { href: `/settings/signatures?mailboxUid=${encodeURIComponent(mailboxUid)}`, children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", children: "Cancel" }) })] })] })] }) }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { updateMailSignature } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
6
|
+
/**
|
|
7
|
+
* `MailSignature.isDefaultForNewMessages`/`isDefaultForReplyForward` are each meant to hold on at most
|
|
8
|
+
* one signature per mailbox — enforced by convention, not a database constraint (see
|
|
9
|
+
* `mailSignaturesApi.ts`'s own doc comment, quoting restapi's). Before saving a signature with one of
|
|
10
|
+
* these flags newly turned on, this turns the flag off on whichever *other* signature in `signatures`
|
|
11
|
+
* currently has it — the "the composing client toggles the previous default off" responsibility that
|
|
12
|
+
* convention assigns to a caller like the New/Edit signature forms.
|
|
13
|
+
*/
|
|
14
|
+
export async function clearPreviousDefaults(signatures, savingUid, isDefaultForNewMessages, isDefaultForReplyForward) {
|
|
15
|
+
for (const signature of signatures) {
|
|
16
|
+
if (signature.uid === savingUid) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const patch = {};
|
|
20
|
+
if (isDefaultForNewMessages && signature.isDefaultForNewMessages) {
|
|
21
|
+
patch.isDefaultForNewMessages = false;
|
|
22
|
+
}
|
|
23
|
+
if (isDefaultForReplyForward && signature.isDefaultForReplyForward) {
|
|
24
|
+
patch.isDefaultForReplyForward = false;
|
|
25
|
+
}
|
|
26
|
+
if (Object.keys(patch).length > 0) {
|
|
27
|
+
await updateMailSignature({ uid: signature.uid, version: signature.version, ...patch });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
3
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
5
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
import { useEffect, useMemo, useState } from "react";
|
|
7
|
+
import { endOfWeek, isAfter, isBefore, isToday, startOfDay } from "date-fns";
|
|
8
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
|
+
import { createTask, deleteTask, listTasks, setTaskCompleted, setTaskMyDay, } from "@rapidmx/react-shared/tasks/tasksApi.js";
|
|
10
|
+
import { listFlaggedMessages } from "@rapidmx/react-shared/mail/flaggedMessages.js";
|
|
11
|
+
import TasksShell, { useTasksShell } from "../../shared/components/tasks/layout/TasksShell.js";
|
|
12
|
+
import TasksSidebar from "../../shared/components/tasks/TasksSidebar.js";
|
|
13
|
+
import TasksToolbar from "../../shared/components/tasks/TasksToolbar.js";
|
|
14
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
15
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
16
|
+
const INPUT_CLASS = "text-sm py-1.5 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
17
|
+
export default function TasksPage(props) {
|
|
18
|
+
return (_jsx(TasksShell, { ...props, children: _jsx(TasksContent, {}) }));
|
|
19
|
+
}
|
|
20
|
+
const BUCKET_ORDER = ["Overdue", "Today", "This Week", "Later", "No due date"];
|
|
21
|
+
/** Buckets a task by its due date relative to `now`, matching the Outlook/To-Do convention this list mirrors. */
|
|
22
|
+
function bucketFor(task, now) {
|
|
23
|
+
if (!task.dueDate) {
|
|
24
|
+
return "No due date";
|
|
25
|
+
}
|
|
26
|
+
const due = new Date(task.dueDate);
|
|
27
|
+
if (isBefore(due, startOfDay(now))) {
|
|
28
|
+
return "Overdue";
|
|
29
|
+
}
|
|
30
|
+
if (isToday(due)) {
|
|
31
|
+
return "Today";
|
|
32
|
+
}
|
|
33
|
+
if (!isAfter(due, endOfWeek(now, { weekStartsOn: 1 }))) {
|
|
34
|
+
return "This Week";
|
|
35
|
+
}
|
|
36
|
+
return "Later";
|
|
37
|
+
}
|
|
38
|
+
const PRIORITY_LABEL = { low: "Low", normal: "Normal", high: "High" };
|
|
39
|
+
const PRIORITY_CLASS = {
|
|
40
|
+
low: "text-text-muted",
|
|
41
|
+
normal: "text-text-muted",
|
|
42
|
+
high: "text-danger",
|
|
43
|
+
};
|
|
44
|
+
function TasksContent() {
|
|
45
|
+
const { folderUid, mailboxUid, userUid } = useTasksShell();
|
|
46
|
+
const [tasks, setTasks] = useState([]);
|
|
47
|
+
const [loading, setLoading] = useState(true);
|
|
48
|
+
const [error, setError] = useState(null);
|
|
49
|
+
const [title, setTitle] = useState("");
|
|
50
|
+
const [dueDate, setDueDate] = useState("");
|
|
51
|
+
const [priority, setPriority] = useState("normal");
|
|
52
|
+
const [createError, setCreateError] = useState(null);
|
|
53
|
+
const [creating, setCreating] = useState(false);
|
|
54
|
+
const [view, setView] = useState({ type: "all" });
|
|
55
|
+
const [viewMode, setViewMode] = useState("list");
|
|
56
|
+
const [checkedUids, setCheckedUids] = useState(new Set());
|
|
57
|
+
const [flaggedMessages, setFlaggedMessages] = useState([]);
|
|
58
|
+
const [flaggedLoading, setFlaggedLoading] = useState(false);
|
|
59
|
+
const [flaggedError, setFlaggedError] = useState(null);
|
|
60
|
+
function reload() {
|
|
61
|
+
if (!folderUid) {
|
|
62
|
+
setTasks([]);
|
|
63
|
+
setLoading(false);
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
setLoading(true);
|
|
67
|
+
setError(null);
|
|
68
|
+
return listTasks(folderUid, { limit: 500 })
|
|
69
|
+
.then(setTasks)
|
|
70
|
+
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load tasks."))
|
|
71
|
+
.finally(() => setLoading(false));
|
|
72
|
+
}
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
void reload();
|
|
75
|
+
}, [folderUid]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (view.type !== "flagged" || !mailboxUid) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
setFlaggedLoading(true);
|
|
81
|
+
setFlaggedError(null);
|
|
82
|
+
listFlaggedMessages(mailboxUid)
|
|
83
|
+
.then(setFlaggedMessages)
|
|
84
|
+
.catch((err) => setFlaggedError(err instanceof ApiRequestError ? err.message : "Could not load flagged email."))
|
|
85
|
+
.finally(() => setFlaggedLoading(false));
|
|
86
|
+
}, [view, mailboxUid]);
|
|
87
|
+
async function handleCreate(e) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
setCreateError(null);
|
|
90
|
+
if (!title.trim()) {
|
|
91
|
+
setCreateError("A title is required.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!mailboxUid || !folderUid) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setCreating(true);
|
|
98
|
+
try {
|
|
99
|
+
const created = await createTask({
|
|
100
|
+
mailboxUid,
|
|
101
|
+
folderUid,
|
|
102
|
+
title: title.trim(),
|
|
103
|
+
dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
|
|
104
|
+
priority,
|
|
105
|
+
});
|
|
106
|
+
setTasks((prev) => [...prev, created]);
|
|
107
|
+
setTitle("");
|
|
108
|
+
setDueDate("");
|
|
109
|
+
setPriority("normal");
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
setCreateError(err instanceof ApiRequestError ? err.message : "Could not create this task.");
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
setCreating(false);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function handleToggle(task) {
|
|
119
|
+
setError(null);
|
|
120
|
+
const nextCompleted = !task.completed;
|
|
121
|
+
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? { ...t, completed: nextCompleted } : t)));
|
|
122
|
+
try {
|
|
123
|
+
const updated = await setTaskCompleted(task, nextCompleted);
|
|
124
|
+
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? updated : t)));
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
setTasks((prev) => prev.map((t) => (t.uid === task.uid ? task : t)));
|
|
128
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not update this task.");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function handleDelete(task) {
|
|
132
|
+
setError(null);
|
|
133
|
+
try {
|
|
134
|
+
await deleteTask(task.uid, task.version);
|
|
135
|
+
setTasks((prev) => prev.filter((t) => t.uid !== task.uid));
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not delete this task.");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function handleSelectView(next) {
|
|
142
|
+
setView(next);
|
|
143
|
+
setCheckedUids(new Set());
|
|
144
|
+
}
|
|
145
|
+
function toggleChecked(uid) {
|
|
146
|
+
setCheckedUids((prev) => {
|
|
147
|
+
const next = new Set(prev);
|
|
148
|
+
if (next.has(uid)) {
|
|
149
|
+
next.delete(uid);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
next.add(uid);
|
|
153
|
+
}
|
|
154
|
+
return next;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const viewFiltered = useMemo(() => {
|
|
158
|
+
switch (view.type) {
|
|
159
|
+
case "myDay":
|
|
160
|
+
return tasks.filter((t) => t.myDay);
|
|
161
|
+
case "important":
|
|
162
|
+
return tasks.filter((t) => t.priority === "high");
|
|
163
|
+
case "planned":
|
|
164
|
+
return tasks.filter((t) => t.dueDate != null);
|
|
165
|
+
case "assignedToMe":
|
|
166
|
+
return tasks.filter((t) => t.assignedTo === userUid);
|
|
167
|
+
case "list":
|
|
168
|
+
return tasks.filter((t) => t.taskListUid === view.uid);
|
|
169
|
+
case "flagged":
|
|
170
|
+
return [];
|
|
171
|
+
case "all":
|
|
172
|
+
return tasks;
|
|
173
|
+
}
|
|
174
|
+
}, [tasks, view, userUid]);
|
|
175
|
+
const checkedTasks = viewFiltered.filter((t) => checkedUids.has(t.uid));
|
|
176
|
+
async function handleBulkComplete() {
|
|
177
|
+
let bulkError = null;
|
|
178
|
+
for (const task of checkedTasks) {
|
|
179
|
+
try {
|
|
180
|
+
await setTaskCompleted(task, true);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
bulkError = err instanceof ApiRequestError ? err.message : "Could not update one or more tasks.";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
setCheckedUids(new Set());
|
|
187
|
+
await reload();
|
|
188
|
+
setError(bulkError);
|
|
189
|
+
}
|
|
190
|
+
async function handleBulkAddToMyDay() {
|
|
191
|
+
let bulkError = null;
|
|
192
|
+
for (const task of checkedTasks) {
|
|
193
|
+
try {
|
|
194
|
+
await setTaskMyDay(task, true);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
bulkError = err instanceof ApiRequestError ? err.message : "Could not update one or more tasks.";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
setCheckedUids(new Set());
|
|
201
|
+
await reload();
|
|
202
|
+
setError(bulkError);
|
|
203
|
+
}
|
|
204
|
+
async function handleBulkDelete() {
|
|
205
|
+
let bulkError = null;
|
|
206
|
+
for (const task of checkedTasks) {
|
|
207
|
+
try {
|
|
208
|
+
await deleteTask(task.uid, task.version);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
bulkError = err instanceof ApiRequestError ? err.message : "Could not delete one or more tasks.";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
setCheckedUids(new Set());
|
|
215
|
+
await reload();
|
|
216
|
+
setError(bulkError);
|
|
217
|
+
}
|
|
218
|
+
const grouped = useMemo(() => {
|
|
219
|
+
const now = new Date();
|
|
220
|
+
const map = new Map(BUCKET_ORDER.map((b) => [b, []]));
|
|
221
|
+
for (const task of viewFiltered) {
|
|
222
|
+
if (task.completed) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
map.get(bucketFor(task, now)).push(task);
|
|
226
|
+
}
|
|
227
|
+
return map;
|
|
228
|
+
}, [viewFiltered]);
|
|
229
|
+
const completedTasks = viewFiltered.filter((t) => t.completed);
|
|
230
|
+
return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx(TasksSidebar, { mailboxUid: mailboxUid, tasks: tasks, userUid: userUid, active: view, onSelect: handleSelectView }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col overflow-y-auto", children: [_jsx(TasksToolbar, { viewMode: viewMode, onViewModeChange: setViewMode, selectedCount: checkedTasks.length, onComplete: handleBulkComplete, onDelete: handleBulkDelete, onAddToMyDay: handleBulkAddToMyDay }), _jsxs("div", { role: "region", "aria-label": "Tasks list", className: "max-w-2xl mx-auto w-full flex flex-col gap-6 p-6", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Tasks" }), error && _jsx(Alert, { children: error }), view.type === "flagged" ? (_jsx(FlaggedEmailList, { loading: flaggedLoading, error: flaggedError, messages: flaggedMessages })) : (_jsxs(_Fragment, { children: [_jsxs("form", { onSubmit: handleCreate, className: "flex items-center gap-2 bg-surface border border-border rounded-md p-2", children: [_jsx("input", { type: "text", "aria-label": "Add a task", className: `${INPUT_CLASS} flex-1`, value: title, onChange: (e) => setTitle(e.target.value), placeholder: "Add a task" }), _jsx("input", { type: "date", "aria-label": "Due date", className: INPUT_CLASS, value: dueDate, onChange: (e) => setDueDate(e.target.value) }), _jsxs("select", { "aria-label": "Priority", className: INPUT_CLASS, value: priority, onChange: (e) => setPriority(e.target.value), children: [_jsx("option", { value: "low", children: "Low" }), _jsx("option", { value: "normal", children: "Normal" }), _jsx("option", { value: "high", children: "High" })] }), _jsx(Button, { type: "submit", loading: creating, disabled: creating, className: "!w-auto shrink-0", children: "Add" })] }), createError && _jsx(Alert, { children: createError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : viewFiltered.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No tasks yet." })) : viewMode === "grid" ? (_jsx(TaskTable, { tasks: viewFiltered, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: handleDelete })) : (_jsxs(_Fragment, { children: [BUCKET_ORDER.map((bucket) => {
|
|
231
|
+
const items = grouped.get(bucket);
|
|
232
|
+
if (items.length === 0) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
return (_jsx(TaskGroup, { label: bucket, tasks: items, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: handleDelete }, bucket));
|
|
236
|
+
}), completedTasks.length > 0 && (_jsx(TaskGroup, { label: "Completed", tasks: completedTasks, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: handleDelete }))] }))] }))] })] })] }));
|
|
237
|
+
}
|
|
238
|
+
function FlaggedEmailList({ loading, error, messages }) {
|
|
239
|
+
if (error) {
|
|
240
|
+
return _jsx(Alert, { children: error });
|
|
241
|
+
}
|
|
242
|
+
if (loading) {
|
|
243
|
+
return _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" });
|
|
244
|
+
}
|
|
245
|
+
if (messages.length === 0) {
|
|
246
|
+
return _jsx("p", { className: "text-sm text-text-muted", children: "No flagged email." });
|
|
247
|
+
}
|
|
248
|
+
return (_jsx("ul", { className: "bg-surface border border-border rounded-md divide-y divide-border", children: messages.map((m) => (_jsxs("li", { className: "flex items-center gap-3 py-2.5 px-3 text-sm", children: [_jsx("span", { className: "flex-1 truncate", children: m.subject || "(no subject)" }), _jsx("span", { className: "text-xs text-text-muted shrink-0", children: m.from.address }), _jsx("span", { className: "text-xs text-text-muted shrink-0", children: new Date(m.receivedDate).toLocaleDateString() })] }, m.uid))) }));
|
|
249
|
+
}
|
|
250
|
+
/** A circular completion-toggle control matching Outlook's visual language (not a native checkbox), while
|
|
251
|
+
* staying a real accessible toggle. */
|
|
252
|
+
function CompletionToggle({ task, onToggle }) {
|
|
253
|
+
return (_jsx("button", { type: "button", role: "checkbox", "aria-checked": task.completed, "aria-label": `Mark "${task.title}" as ${task.completed ? "not complete" : "complete"}`, onClick: () => onToggle(task), className: [
|
|
254
|
+
"w-4 h-4 rounded-full border shrink-0 flex items-center justify-center text-[10px]",
|
|
255
|
+
task.completed ? "bg-primary border-primary text-white" : "border-border text-transparent",
|
|
256
|
+
].join(" "), children: "\u2713" }));
|
|
257
|
+
}
|
|
258
|
+
function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, onDelete }) {
|
|
259
|
+
return (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-2", children: label }), _jsx("ul", { className: "bg-surface border border-border rounded-md divide-y divide-border", children: tasks.map((task) => (_jsxs("li", { className: "flex items-center gap-3 py-2.5 px-3", children: [_jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }), _jsx(CompletionToggle, { task: task, onToggle: onToggle }), _jsx("span", { className: ["flex-1 text-sm", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), task.priority !== "normal" && (_jsx("span", { className: ["text-xs font-medium", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] })), task.dueDate && (_jsx("span", { className: "text-xs text-text-muted shrink-0", children: new Date(task.dueDate).toLocaleDateString() })), _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" })] }, task.uid))) })] }));
|
|
260
|
+
}
|
|
261
|
+
function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete }) {
|
|
262
|
+
const allChecked = tasks.length > 0 && tasks.every((t) => checkedUids.has(t.uid));
|
|
263
|
+
function toggleAll() {
|
|
264
|
+
if (allChecked) {
|
|
265
|
+
// Every task is already checked when this branch runs (that's what `allChecked` means) — no
|
|
266
|
+
// per-task guard needed, unlike the `else` branch below where only *some* tasks may be checked.
|
|
267
|
+
for (const t of tasks) {
|
|
268
|
+
onToggleChecked(t.uid);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
for (const t of tasks) {
|
|
273
|
+
if (!checkedUids.has(t.uid)) {
|
|
274
|
+
onToggleChecked(t.uid);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm bg-surface border border-border rounded-md", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all tasks", checked: allChecked, onChange: toggleAll }) }), _jsx("th", { className: "w-8 px-3 py-2" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Title" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Due Date" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Importance" }), _jsx("th", { className: "w-8 px-3 py-2" })] }) }), _jsx("tbody", { children: tasks.map((task) => (_jsxs("tr", { className: "border-b border-border last:border-0", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(CompletionToggle, { task: task, onToggle: onToggle }) }), _jsx("td", { className: ["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), _jsx("td", { className: "px-3 py-2 text-text-muted", children: task.dueDate ? new Date(task.dueDate).toLocaleDateString() : "" }), _jsx("td", { className: ["px-3 py-2", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" }) })] }, task.uid))) })] }) }));
|
|
280
|
+
}
|