@rapidmx/web-client 0.9.0 → 0.10.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/README.md +192 -88
- package/apps/shared/auth/accountUrl.ts +9 -0
- package/apps/shared/auth/adminAccess.ts +99 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +2 -1
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +8 -2
- package/apps/shared/components/contacts/ContactsToolbar.tsx +125 -115
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +15 -3
- package/apps/shared/components/layout/AppShell.tsx +411 -288
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -389
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -164
- package/apps/shared/components/layout/UserMenu.tsx +303 -185
- package/apps/shared/components/mail/ConversationList.tsx +269 -262
- package/apps/shared/components/mail/ConversationThreadPane.tsx +475 -434
- package/apps/shared/components/mail/LazyReadingPane.tsx +115 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +235 -222
- package/apps/shared/components/mail/MessageDetailPane.tsx +1458 -1387
- package/apps/shared/components/mail/NewMailToasts.tsx +141 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +279 -160
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -418
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1842 -1736
- package/apps/shared/components/mail/compose/ComposeWindowPlaceholder.tsx +111 -0
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -118
- package/apps/shared/components/mail/compose/composePerf.ts +46 -0
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -100
- package/apps/shared/components/mail/layout/MailShell.tsx +476 -472
- package/apps/shared/components/mail/unreadStyle.tsx +75 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +252 -240
- package/apps/shared/components/tasks/layout/TasksShell.tsx +15 -3
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -0
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -0
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -0
- package/apps/shared/keyboard/dispatch.ts +124 -0
- package/apps/shared/keyboard/format.ts +89 -0
- package/apps/shared/keyboard/keymap.ts +114 -0
- package/apps/shared/keyboard/match.ts +44 -0
- package/apps/shared/keyboard/parse.ts +136 -0
- package/apps/shared/keyboard/platform.ts +34 -0
- package/apps/shared/keyboard/registry.ts +65 -0
- package/apps/shared/keyboard/targets.ts +79 -0
- package/apps/shared/keyboard/useShortcut.ts +50 -0
- package/apps/shared/keyboard/useShortcutProps.ts +17 -0
- package/apps/shared/mail/folderCounts.ts +302 -0
- package/apps/shared/mail/listSnapshots.ts +87 -0
- package/apps/shared/mail/messageReadState.ts +85 -0
- package/apps/shared/mail/newMailNotifications.ts +183 -0
- package/apps/shared/mail/useMailConnection.ts +150 -0
- package/apps/shared/mail/useMailLiveUpdates.ts +232 -224
- package/apps/shared/mail/useMarkMessageRead.ts +47 -0
- package/apps/shared/mail/useNewMailNotifications.ts +163 -0
- package/apps/shared/mail/useUnreadTitle.ts +42 -0
- package/apps/shared/navigation/AppRouter.tsx +300 -0
- package/apps/shared/navigation/appHrefs.ts +23 -0
- package/apps/shared/navigation/frameContext.tsx +35 -0
- package/apps/shared/navigation/idle.ts +45 -0
- package/apps/shared/navigation/routerContext.tsx +83 -0
- package/apps/shared/navigation/routes.ts +72 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -98
- package/apps/shared/styles/app.css +28 -10
- package/apps/www/_routedPage.tsx +24 -0
- package/apps/www/_routes.ts +35 -0
- package/apps/www/calendar/index.tsx +55 -19
- package/apps/www/contacts/[uid].tsx +112 -107
- package/apps/www/contacts/index.tsx +584 -567
- package/apps/www/index.tsx +2280 -1917
- package/apps/www/messages/[uid].tsx +106 -101
- package/apps/www/settings/auto-reply/index.tsx +4 -1
- package/apps/www/settings/encryption/index.tsx +1252 -1249
- package/apps/www/settings/filters/[uid].tsx +4 -1
- package/apps/www/settings/filters/index.tsx +102 -99
- package/apps/www/settings/filters/new/index.tsx +138 -133
- package/apps/www/settings/labels/index.tsx +204 -201
- package/apps/www/settings/privacy/index.tsx +4 -1
- package/apps/www/settings/read-receipts/index.tsx +4 -1
- package/apps/www/settings/sharing/index.tsx +277 -274
- package/apps/www/settings/signatures/[uid].tsx +170 -167
- package/apps/www/settings/signatures/index.tsx +88 -85
- package/apps/www/settings/signatures/new/index.tsx +134 -129
- package/apps/www/tasks/index.tsx +18 -2
- package/dist/apps/shared/auth/accountUrl.d.ts +2 -0
- package/dist/apps/shared/auth/accountUrl.js +8 -0
- package/dist/apps/shared/auth/adminAccess.d.ts +30 -0
- package/dist/apps/shared/auth/adminAccess.js +89 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +1 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +8 -4
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +7 -4
- package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +1 -1
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +15 -5
- package/dist/apps/shared/components/layout/AppShell.d.ts +29 -4
- package/dist/apps/shared/components/layout/AppShell.js +73 -14
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +10 -9
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +3 -1
- package/dist/apps/shared/components/layout/UserMenu.d.ts +19 -3
- package/dist/apps/shared/components/layout/UserMenu.js +52 -5
- package/dist/apps/shared/components/mail/ConversationList.js +4 -11
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +4 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +52 -27
- package/dist/apps/shared/components/mail/LazyReadingPane.d.ts +9 -0
- package/dist/apps/shared/components/mail/LazyReadingPane.js +82 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +4 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +11 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +7 -1
- package/dist/apps/shared/components/mail/MessageDetailPane.js +83 -34
- package/dist/apps/shared/components/mail/NewMailToasts.d.ts +19 -0
- package/dist/apps/shared/components/mail/NewMailToasts.js +55 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +30 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +71 -5
- package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +4 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +1 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +98 -22
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.d.ts +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeWindowPlaceholder.js +33 -0
- package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +9 -1
- package/dist/apps/shared/components/mail/compose/RichTextEditor.js +20 -2
- package/dist/apps/shared/components/mail/compose/composePerf.d.ts +16 -0
- package/dist/apps/shared/components/mail/compose/composePerf.js +41 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +13 -0
- package/dist/apps/shared/components/mail/compose/quotedBody.js +67 -11
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +12 -4
- package/dist/apps/shared/components/mail/layout/MailShell.js +76 -96
- package/dist/apps/shared/components/mail/unreadStyle.d.ts +45 -0
- package/dist/apps/shared/components/mail/unreadStyle.js +61 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +1 -1
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +15 -5
- package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +15 -5
- package/dist/apps/shared/keyboard/GlobalShortcuts.d.ts +14 -0
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +37 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.d.ts +20 -0
- package/dist/apps/shared/keyboard/ShortcutProvider.js +52 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.d.ts +14 -0
- package/dist/apps/shared/keyboard/ShortcutsDialog.js +42 -0
- package/dist/apps/shared/keyboard/dispatch.d.ts +16 -0
- package/dist/apps/shared/keyboard/dispatch.js +109 -0
- package/dist/apps/shared/keyboard/format.d.ts +12 -0
- package/dist/apps/shared/keyboard/format.js +74 -0
- package/dist/apps/shared/keyboard/keymap.d.ts +294 -0
- package/dist/apps/shared/keyboard/keymap.js +84 -0
- package/dist/apps/shared/keyboard/match.d.ts +20 -0
- package/dist/apps/shared/keyboard/match.js +31 -0
- package/dist/apps/shared/keyboard/parse.d.ts +31 -0
- package/dist/apps/shared/keyboard/parse.js +107 -0
- package/dist/apps/shared/keyboard/platform.d.ts +15 -0
- package/dist/apps/shared/keyboard/platform.js +22 -0
- package/dist/apps/shared/keyboard/registry.d.ts +39 -0
- package/dist/apps/shared/keyboard/registry.js +33 -0
- package/dist/apps/shared/keyboard/targets.d.ts +14 -0
- package/dist/apps/shared/keyboard/targets.js +67 -0
- package/dist/apps/shared/keyboard/useShortcut.d.ts +19 -0
- package/dist/apps/shared/keyboard/useShortcut.js +35 -0
- package/dist/apps/shared/keyboard/useShortcutProps.d.ts +10 -0
- package/dist/apps/shared/keyboard/useShortcutProps.js +15 -0
- package/dist/apps/shared/mail/folderCounts.d.ts +78 -0
- package/dist/apps/shared/mail/folderCounts.js +212 -0
- package/dist/apps/shared/mail/listSnapshots.d.ts +46 -0
- package/dist/apps/shared/mail/listSnapshots.js +43 -0
- package/dist/apps/shared/mail/messageReadState.d.ts +32 -0
- package/dist/apps/shared/mail/messageReadState.js +63 -0
- package/dist/apps/shared/mail/newMailNotifications.d.ts +62 -0
- package/dist/apps/shared/mail/newMailNotifications.js +138 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +55 -0
- package/dist/apps/shared/mail/useMailConnection.js +96 -0
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +13 -6
- package/dist/apps/shared/mail/useMailLiveUpdates.js +28 -26
- package/dist/apps/shared/mail/useMarkMessageRead.d.ts +12 -0
- package/dist/apps/shared/mail/useMarkMessageRead.js +44 -0
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +41 -0
- package/dist/apps/shared/mail/useNewMailNotifications.js +110 -0
- package/dist/apps/shared/mail/useUnreadTitle.d.ts +18 -0
- package/dist/apps/shared/mail/useUnreadTitle.js +31 -0
- package/dist/apps/shared/navigation/AppRouter.d.ts +52 -0
- package/dist/apps/shared/navigation/AppRouter.js +242 -0
- package/dist/apps/shared/navigation/appHrefs.d.ts +14 -0
- package/dist/apps/shared/navigation/appHrefs.js +20 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +19 -0
- package/dist/apps/shared/navigation/frameContext.js +24 -0
- package/dist/apps/shared/navigation/idle.d.ts +14 -0
- package/dist/apps/shared/navigation/idle.js +43 -0
- package/dist/apps/shared/navigation/routerContext.d.ts +37 -0
- package/dist/apps/shared/navigation/routerContext.js +56 -0
- package/dist/apps/shared/navigation/routes.d.ts +32 -0
- package/dist/apps/shared/navigation/routes.js +37 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +17 -3
- package/dist/apps/shared/styles/app.css +28 -10
- package/dist/apps/www/_routedPage.d.ts +12 -0
- package/dist/apps/www/_routedPage.js +19 -0
- package/dist/apps/www/_routes.d.ts +11 -0
- package/dist/apps/www/_routes.js +29 -0
- package/dist/apps/www/calendar/index.d.ts +2 -2
- package/dist/apps/www/calendar/index.js +39 -8
- package/dist/apps/www/contacts/[uid].d.ts +3 -9
- package/dist/apps/www/contacts/[uid].js +6 -2
- package/dist/apps/www/contacts/index.d.ts +2 -2
- package/dist/apps/www/contacts/index.js +17 -4
- package/dist/apps/www/index.d.ts +2 -2
- package/dist/apps/www/index.js +301 -35
- package/dist/apps/www/messages/[uid].d.ts +3 -7
- package/dist/apps/www/messages/[uid].js +7 -4
- package/dist/apps/www/settings/auto-reply/index.d.ts +2 -2
- package/dist/apps/www/settings/auto-reply/index.js +3 -1
- package/dist/apps/www/settings/encryption/index.d.ts +2 -2
- package/dist/apps/www/settings/encryption/index.js +3 -1
- package/dist/apps/www/settings/filters/[uid].d.ts +2 -2
- package/dist/apps/www/settings/filters/[uid].js +3 -1
- package/dist/apps/www/settings/filters/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/index.js +3 -1
- package/dist/apps/www/settings/filters/new/index.d.ts +2 -2
- package/dist/apps/www/settings/filters/new/index.js +6 -2
- package/dist/apps/www/settings/labels/index.d.ts +2 -2
- package/dist/apps/www/settings/labels/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.d.ts +2 -2
- package/dist/apps/www/settings/privacy/index.js +3 -1
- package/dist/apps/www/settings/read-receipts/index.d.ts +2 -2
- package/dist/apps/www/settings/read-receipts/index.js +3 -1
- package/dist/apps/www/settings/sharing/index.d.ts +2 -2
- package/dist/apps/www/settings/sharing/index.js +3 -1
- package/dist/apps/www/settings/signatures/[uid].d.ts +2 -2
- package/dist/apps/www/settings/signatures/[uid].js +3 -1
- package/dist/apps/www/settings/signatures/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/index.js +3 -1
- package/dist/apps/www/settings/signatures/new/index.d.ts +2 -2
- package/dist/apps/www/settings/signatures/new/index.js +6 -2
- package/dist/apps/www/tasks/index.d.ts +2 -2
- package/dist/apps/www/tasks/index.js +15 -3
- package/package.json +2 -2
|
@@ -1,129 +1,134 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const [
|
|
41
|
-
const [
|
|
42
|
-
const [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { routedPage } from "../../../_routedPage.js";
|
|
6
|
+
import { useNavigate } from "../../../../shared/navigation/AppRouter.js";
|
|
7
|
+
import React, { FormEvent, useState } from "react";
|
|
8
|
+
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
9
|
+
import { createMailSignature, listMailSignatures } from "@rapidmx/react-shared/mail/mailSignaturesApi.js";
|
|
10
|
+
import { clearPreviousDefaults } from "../signatureDefaults.js";
|
|
11
|
+
import SettingsShell, { SettingsShellProps, useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
|
|
12
|
+
import RichTextEditor from "../../../../shared/components/mail/compose/RichTextEditor.js";
|
|
13
|
+
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
14
|
+
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
15
|
+
import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
|
|
16
|
+
|
|
17
|
+
const INPUT_CLASS =
|
|
18
|
+
"w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
19
|
+
|
|
20
|
+
export type NewSignaturePageProps = Omit<SettingsShellProps, "active">;
|
|
21
|
+
|
|
22
|
+
function NewSignaturePage(props: NewSignaturePageProps) {
|
|
23
|
+
return (
|
|
24
|
+
<SettingsShell {...props} active="signatures">
|
|
25
|
+
<NewSignatureForm />
|
|
26
|
+
</SettingsShell>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Signatures aren't backed by a draft/attachment record the way Compose's own inline images are (see
|
|
31
|
+
* `ComposeWindow.handleUploadImage()`) — inline images in a signature are out of scope for this phase
|
|
32
|
+
* (`.claude/NOTES.md`'s Phase 12 entry), so this is a real, deliberate no-op, not a stub left unfinished. */
|
|
33
|
+
async function handleUploadImage(): Promise<string | null> {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function NewSignatureForm() {
|
|
38
|
+
const navigate = useNavigate();
|
|
39
|
+
const { mailboxUid } = useSettingsShell();
|
|
40
|
+
const [name, setName] = useState("");
|
|
41
|
+
const [contentHtml, setContentHtml] = useState("");
|
|
42
|
+
const [isDefaultForNewMessages, setIsDefaultForNewMessages] = useState(false);
|
|
43
|
+
const [isDefaultForReplyForward, setIsDefaultForReplyForward] = useState(false);
|
|
44
|
+
const [error, setError] = useState<string | null>(null);
|
|
45
|
+
const [saving, setSaving] = useState(false);
|
|
46
|
+
|
|
47
|
+
async function handleSubmit(e: FormEvent) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
setError(null);
|
|
50
|
+
|
|
51
|
+
if (!name.trim()) {
|
|
52
|
+
setError("A name is required.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setSaving(true);
|
|
57
|
+
try {
|
|
58
|
+
if (isDefaultForNewMessages || isDefaultForReplyForward) {
|
|
59
|
+
const existing = await listMailSignatures(mailboxUid!, { limit: 100 });
|
|
60
|
+
await clearPreviousDefaults(existing, undefined, isDefaultForNewMessages, isDefaultForReplyForward);
|
|
61
|
+
}
|
|
62
|
+
const created = await createMailSignature({
|
|
63
|
+
mailboxUid: mailboxUid!,
|
|
64
|
+
name: name.trim(),
|
|
65
|
+
contentHtml,
|
|
66
|
+
isDefaultForNewMessages,
|
|
67
|
+
isDefaultForReplyForward,
|
|
68
|
+
});
|
|
69
|
+
navigate(`/settings/signatures/${encodeURIComponent(created.uid)}?mailboxUid=${encodeURIComponent(mailboxUid!)}`);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not create the signature.");
|
|
72
|
+
} finally {
|
|
73
|
+
setSaving(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex-1 min-w-0 overflow-y-auto p-6">
|
|
79
|
+
<div className="max-w-3xl">
|
|
80
|
+
<h1 className="text-lg font-bold tracking-tight mb-5">New signature</h1>
|
|
81
|
+
|
|
82
|
+
{error && <Alert>{error}</Alert>}
|
|
83
|
+
|
|
84
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
|
85
|
+
<div className="bg-surface border border-border rounded-md p-6 flex flex-col gap-4">
|
|
86
|
+
<FormField label="Name" htmlFor="name">
|
|
87
|
+
<input
|
|
88
|
+
id="name"
|
|
89
|
+
type="text"
|
|
90
|
+
className={INPUT_CLASS}
|
|
91
|
+
value={name}
|
|
92
|
+
onChange={(e) => setName(e.target.value)}
|
|
93
|
+
placeholder="Work signature"
|
|
94
|
+
/>
|
|
95
|
+
</FormField>
|
|
96
|
+
<label className="flex items-center gap-2 text-sm">
|
|
97
|
+
<input
|
|
98
|
+
type="checkbox"
|
|
99
|
+
checked={isDefaultForNewMessages}
|
|
100
|
+
onChange={(e) => setIsDefaultForNewMessages(e.target.checked)}
|
|
101
|
+
/>
|
|
102
|
+
Use for new messages
|
|
103
|
+
</label>
|
|
104
|
+
<label className="flex items-center gap-2 text-sm">
|
|
105
|
+
<input
|
|
106
|
+
type="checkbox"
|
|
107
|
+
checked={isDefaultForReplyForward}
|
|
108
|
+
onChange={(e) => setIsDefaultForReplyForward(e.target.checked)}
|
|
109
|
+
/>
|
|
110
|
+
Use for replies and forwards
|
|
111
|
+
</label>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="h-64">
|
|
115
|
+
<RichTextEditor value={contentHtml} onChange={setContentHtml} fill onUploadImage={handleUploadImage} />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="flex gap-3">
|
|
119
|
+
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
120
|
+
Create signature
|
|
121
|
+
</Button>
|
|
122
|
+
<a href={`/settings/signatures?mailboxUid=${encodeURIComponent(mailboxUid!)}`}>
|
|
123
|
+
<Button type="button" variant="secondary" className="!w-auto">
|
|
124
|
+
Cancel
|
|
125
|
+
</Button>
|
|
126
|
+
</a>
|
|
127
|
+
</div>
|
|
128
|
+
</form>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default routedPage("/settings/signatures/new", NewSignaturePage);
|
package/apps/www/tasks/index.tsx
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import
|
|
5
|
+
import { routedPage } from "../_routedPage.js";
|
|
6
|
+
import React, { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
|
6
7
|
import { endOfWeek, isAfter, isBefore, isToday, parseISO, startOfDay } from "date-fns";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import {
|
|
@@ -26,11 +27,14 @@ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
|
26
27
|
import { findWellKnownFolderUid } from "../../shared/mail/findWellKnownFolderUid.js";
|
|
27
28
|
import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
|
|
28
29
|
import { LIST_PAGE_SIZE, MAX_LIST_PAGES, listAllPages } from "../../shared/mail/listAllPages.js";
|
|
30
|
+
import { SHORTCUTS } from "../../shared/keyboard/keymap.js";
|
|
31
|
+
import { useShortcut } from "../../shared/keyboard/useShortcut.js";
|
|
32
|
+
import { useShortcutProps } from "../../shared/keyboard/useShortcutProps.js";
|
|
29
33
|
|
|
30
34
|
const INPUT_CLASS =
|
|
31
35
|
"text-sm py-1.5 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
function TasksPage(props: TasksShellProps) {
|
|
34
38
|
return (
|
|
35
39
|
<TasksShell {...props}>
|
|
36
40
|
<TasksContent />
|
|
@@ -103,6 +107,14 @@ function TasksContent() {
|
|
|
103
107
|
// checked task (the toolbar's Delete).
|
|
104
108
|
const [pendingDelete, setPendingDelete] = useState<{ type: "single"; task: Task } | { type: "bulk" } | null>(null);
|
|
105
109
|
|
|
110
|
+
// "New task" from the keyboard: a task is made in the quick-add form, so this moves the caret to its title field (which the Flagged view,
|
|
111
|
+
// a list of flagged mail rather than tasks, doesn't have). A dialog open - the delete confirmation - silences it.
|
|
112
|
+
const addTaskRef = useRef<HTMLInputElement | null>(null);
|
|
113
|
+
const canAddTask = view.type !== "flagged";
|
|
114
|
+
// Registered only while the form is rendered (`canAddTask`), so its field is there.
|
|
115
|
+
useShortcut(SHORTCUTS.tasks.create, () => addTaskRef.current!.focus(), { enabled: canAddTask });
|
|
116
|
+
const addTaskHint = useShortcutProps("Add a task", SHORTCUTS.tasks.create, canAddTask);
|
|
117
|
+
|
|
106
118
|
function reload(): Promise<void> {
|
|
107
119
|
if (!folderUid) {
|
|
108
120
|
setTasks([]);
|
|
@@ -344,8 +356,10 @@ function TasksContent() {
|
|
|
344
356
|
<>
|
|
345
357
|
<form onSubmit={handleCreate} className="flex items-center gap-2 bg-surface border border-border rounded-md p-2">
|
|
346
358
|
<input
|
|
359
|
+
ref={addTaskRef}
|
|
347
360
|
type="text"
|
|
348
361
|
aria-label="Add a task"
|
|
362
|
+
{...addTaskHint}
|
|
349
363
|
className={`${INPUT_CLASS} flex-1`}
|
|
350
364
|
value={title}
|
|
351
365
|
onChange={(e) => setTitle(e.target.value)}
|
|
@@ -636,3 +650,5 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete }:
|
|
|
636
650
|
</div>
|
|
637
651
|
);
|
|
638
652
|
}
|
|
653
|
+
|
|
654
|
+
export default routedPage("/tasks", TasksPage);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
/** auth-server's account page, or `undefined` when no auth-server is configured. Tolerates a configured URL with a trailing slash. */
|
|
6
|
+
export function accountUrlOf(authServerUrl) {
|
|
7
|
+
return authServerUrl ? `${authServerUrl.replace(/\/+$/, "")}/account` : undefined;
|
|
8
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the signed-in user holds an administrator role, learned without elevating.
|
|
3
|
+
*
|
|
4
|
+
* The JWT the browser carries can't say: auth-server strips every trusted role from a token that isn't elevated
|
|
5
|
+
* (`TokenUtils.resolveTokenUser()`), so `trusted` (from the JWT) is false for a real administrator until they confirm
|
|
6
|
+
* their identity on auth-server's elevation page. The role itself lives on the caller's own `User` record, which they may
|
|
7
|
+
* always read - `GET {authServerUrl}/api/users/me` (the same call auth-server's own UI makes "to check for admin access") -
|
|
8
|
+
* so the answer is read from there, with the same cross-origin credentials the profile lookups use.
|
|
9
|
+
*
|
|
10
|
+
* This only decides whether a menu item is shown. The `/admin` pages and API stay authoritative: they check the role and
|
|
11
|
+
* the elevation themselves, and the link only navigates (`/admin` then runs the elevation redirect), so a wrong answer
|
|
12
|
+
* - a stale cache, a tampered `sessionStorage` - can show a link that leads to "no administrator access", never more.
|
|
13
|
+
*/
|
|
14
|
+
/** The roles the server treats as trusted when it isn't configured otherwise (`trusted_roles`, default `["admin"]`). */
|
|
15
|
+
export declare const DEFAULT_TRUSTED_ROLES: readonly string[];
|
|
16
|
+
/** `sessionStorage` key prefix of a remembered answer; the user's uid follows, so another account signing in in the same
|
|
17
|
+
* tab never inherits it. */
|
|
18
|
+
export declare const ADMIN_ACCESS_KEY_PREFIX = "rapidmx-admin-access:";
|
|
19
|
+
/** How long a remembered answer is trusted - long enough that navigating between the apps costs no request, short enough
|
|
20
|
+
* that a role granted or removed shows up within the sitting. */
|
|
21
|
+
export declare const ADMIN_ACCESS_TTL_MS: number;
|
|
22
|
+
/** Forgets the lookups in flight (tests). */
|
|
23
|
+
export declare function resetAdminAccessLookups(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Resolves `true` when `userUid`'s own `User` record on auth-server carries one of `trustedRoles`, `false` when it
|
|
26
|
+
* doesn't, and `undefined` when that couldn't be established (unreachable, blocked by CORS, not signed in, an unexpected
|
|
27
|
+
* body, or a record that isn't this user's). Only a real answer is remembered; a failure is asked again on the next page.
|
|
28
|
+
* Never rejects.
|
|
29
|
+
*/
|
|
30
|
+
export declare function lookUpAdminAccess(authServerUrl: string, userUid: string, trustedRoles?: readonly string[], now?: number): Promise<boolean | undefined>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
|
|
6
|
+
/**
|
|
7
|
+
* Whether the signed-in user holds an administrator role, learned without elevating.
|
|
8
|
+
*
|
|
9
|
+
* The JWT the browser carries can't say: auth-server strips every trusted role from a token that isn't elevated
|
|
10
|
+
* (`TokenUtils.resolveTokenUser()`), so `trusted` (from the JWT) is false for a real administrator until they confirm
|
|
11
|
+
* their identity on auth-server's elevation page. The role itself lives on the caller's own `User` record, which they may
|
|
12
|
+
* always read - `GET {authServerUrl}/api/users/me` (the same call auth-server's own UI makes "to check for admin access") -
|
|
13
|
+
* so the answer is read from there, with the same cross-origin credentials the profile lookups use.
|
|
14
|
+
*
|
|
15
|
+
* This only decides whether a menu item is shown. The `/admin` pages and API stay authoritative: they check the role and
|
|
16
|
+
* the elevation themselves, and the link only navigates (`/admin` then runs the elevation redirect), so a wrong answer
|
|
17
|
+
* - a stale cache, a tampered `sessionStorage` - can show a link that leads to "no administrator access", never more.
|
|
18
|
+
*/
|
|
19
|
+
/** The roles the server treats as trusted when it isn't configured otherwise (`trusted_roles`, default `["admin"]`). */
|
|
20
|
+
export const DEFAULT_TRUSTED_ROLES = ["admin"];
|
|
21
|
+
/** `sessionStorage` key prefix of a remembered answer; the user's uid follows, so another account signing in in the same
|
|
22
|
+
* tab never inherits it. */
|
|
23
|
+
export const ADMIN_ACCESS_KEY_PREFIX = "rapidmx-admin-access:";
|
|
24
|
+
/** How long a remembered answer is trusted - long enough that navigating between the apps costs no request, short enough
|
|
25
|
+
* that a role granted or removed shows up within the sitting. */
|
|
26
|
+
export const ADMIN_ACCESS_TTL_MS = 30 * 60 * 1000;
|
|
27
|
+
/** Lookups in flight, by key, so two components asking at once make one request. */
|
|
28
|
+
const lookups = new Map();
|
|
29
|
+
/** Forgets the lookups in flight (tests). */
|
|
30
|
+
export function resetAdminAccessLookups() {
|
|
31
|
+
lookups.clear();
|
|
32
|
+
}
|
|
33
|
+
function readRemembered(key, now) {
|
|
34
|
+
try {
|
|
35
|
+
const stored = JSON.parse(sessionStorage.getItem(key) ?? "null");
|
|
36
|
+
if (typeof stored?.admin === "boolean" && typeof stored.at === "number" && now - stored.at >= 0 && now - stored.at < ADMIN_ACCESS_TTL_MS) {
|
|
37
|
+
return stored.admin;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Unreadable, or storage unavailable - ask again.
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
function remember(key, admin, now) {
|
|
46
|
+
try {
|
|
47
|
+
sessionStorage.setItem(key, JSON.stringify({ admin, at: now }));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Storage unavailable - the next page load asks again.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolves `true` when `userUid`'s own `User` record on auth-server carries one of `trustedRoles`, `false` when it
|
|
55
|
+
* doesn't, and `undefined` when that couldn't be established (unreachable, blocked by CORS, not signed in, an unexpected
|
|
56
|
+
* body, or a record that isn't this user's). Only a real answer is remembered; a failure is asked again on the next page.
|
|
57
|
+
* Never rejects.
|
|
58
|
+
*/
|
|
59
|
+
export function lookUpAdminAccess(authServerUrl, userUid, trustedRoles = DEFAULT_TRUSTED_ROLES, now = Date.now()) {
|
|
60
|
+
const key = `${ADMIN_ACCESS_KEY_PREFIX}${userUid}`;
|
|
61
|
+
const remembered = readRemembered(key, now);
|
|
62
|
+
if (remembered !== undefined) {
|
|
63
|
+
return Promise.resolve(remembered);
|
|
64
|
+
}
|
|
65
|
+
const inFlight = lookups.get(key);
|
|
66
|
+
if (inFlight) {
|
|
67
|
+
return inFlight;
|
|
68
|
+
}
|
|
69
|
+
const lookup = (async () => {
|
|
70
|
+
try {
|
|
71
|
+
const user = await authApiFetch(authServerUrl, "/users/me");
|
|
72
|
+
if (!user || user.uid !== userUid || !Array.isArray(user.roles)) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const admin = user.roles.some((role) => typeof role === "string" && trustedRoles.includes(role));
|
|
76
|
+
remember(key, admin, now);
|
|
77
|
+
return admin;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
// Answered: what was learned is in storage; a failure may be asked again.
|
|
84
|
+
lookups.delete(key);
|
|
85
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
lookups.set(key, lookup);
|
|
88
|
+
return lookup;
|
|
89
|
+
}
|
|
@@ -88,7 +88,7 @@ export default function DomainDnsSetup({ uid, onLoaded }) {
|
|
|
88
88
|
const ownership = dnsSetup.find((c) => c.type === "ownership");
|
|
89
89
|
const ownershipValue = ownership?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`;
|
|
90
90
|
const ownershipName = ownership?.recordName || domain.name;
|
|
91
|
-
return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] items-start gap-x-4 gap-y-2 text-sm", children: [_jsx("dt", { className: "text-text-muted py-2", children: "Type" }), _jsx("dd", { className: "py-2", children: "TXT" }), _jsx("dt", { className: "text-text-muted py-2", children: "Name / host" }), _jsx("dd", { className: "bg-surface-alt border border-border rounded-sm py-1.5 px-3", children: _jsx(CopyableValue, { value: ownershipName, copyLabel: "Copy name for the ownership TXT record" }) }), _jsx("dt", { className: "text-text-muted py-2", children: "Value" }), _jsx("dd", { className: "bg-surface-alt border border-border rounded-sm py-1.5 px-3", children: _jsx(CopyableValue, { value: ownershipValue, copyLabel: "Copy value for the ownership TXT record" }) })] }), verifyError && (_jsx("div", { className: "mt-3", children: _jsx(Alert, { children: verifyError }) })), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: verifyError ? "Try again" : "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Name / host", "Recommended value"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsxs("td", { className: "py-2.5 px-2.5 border-b border-border", children: [RECORD_TYPE_LABELS[check.type], _jsxs("div", { className: "text-xs text-text-muted", children: ["Type: ", check.recordKind] })] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: check.recordName ? (_jsx(CopyableValue, { value: check.recordName, copyLabel: `Copy name for the ${RECORD_TYPE_NAMES[check.type]} record` })) : (_jsx("code", { className: "text-xs", children: "\u2014" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: check.recommendedValue ? (_jsx(DnsRecordValue, { check: check, value: check.recommendedValue })) : (_jsx("code", { className: "text-xs", children: "\u2014" })) })] }, check.type))) })] }) }), _jsxs("p", { className: "text-xs text-text-muted mt-3", children: ["Some DNS providers want the name relative to the domain (for example ", _jsx("code", { children: "@" }), " for the domain itself) rather than the full name."] })] }))] }));
|
|
91
|
+
return (_jsxs("div", { className: "flex flex-col gap-5", children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] items-start gap-x-4 gap-y-2 text-sm", children: [_jsx("dt", { className: "text-text-muted py-2", children: "Type" }), _jsx("dd", { className: "py-2", children: "TXT" }), _jsx("dt", { className: "text-text-muted py-2", children: "Name / host" }), _jsx("dd", { className: "bg-surface-alt border border-border rounded-sm py-1.5 px-3", children: _jsx(CopyableValue, { value: ownershipName, copyLabel: "Copy name for the ownership TXT record" }) }), _jsx("dt", { className: "text-text-muted py-2", children: "Value" }), _jsx("dd", { className: "bg-surface-alt border border-border rounded-sm py-1.5 px-3", children: _jsx(CopyableValue, { value: ownershipValue, copyLabel: "Copy value for the ownership TXT record" }) })] }), verifyError && (_jsx("div", { className: "mt-3", children: _jsx(Alert, { children: verifyError }) })), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: verifyError ? "Try again" : "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Name / host", "Recommended value"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsxs("td", { className: "py-2.5 px-2.5 border-b border-border", children: [RECORD_TYPE_LABELS[check.type], _jsxs("div", { className: "text-xs text-text-muted", children: ["Type: ", check.recordKind] })] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted min-w-[10rem]", children: check.recordName ? (_jsx(CopyableValue, { value: check.recordName, copyLabel: `Copy name for the ${RECORD_TYPE_NAMES[check.type]} record` })) : (_jsx("code", { className: "text-xs", children: "\u2014" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: check.recommendedValue ? (_jsx(DnsRecordValue, { check: check, value: check.recommendedValue })) : (_jsx("code", { className: "text-xs", children: "\u2014" })) })] }, check.type))) })] }) }), _jsxs("p", { className: "text-xs text-text-muted mt-3", children: ["Some DNS providers want the name relative to the domain (for example ", _jsx("code", { children: "@" }), " for the domain itself) rather than the full name."] })] }))] }));
|
|
92
92
|
}
|
|
93
93
|
/** The value cell of a checklist row: an MX value as its two form fields (priority and mail server), anything else whole. */
|
|
94
94
|
function DnsRecordValue({ check, value }) {
|
|
@@ -37,4 +37,4 @@ export declare function useCalendarShell(): CalendarShellContextValue;
|
|
|
37
37
|
* state — it lives in `apps/www/calendar/index.tsx`'s own local state, since navigating between
|
|
38
38
|
* views/dates must be instant (no full page reload) and this framework has no client-side router.
|
|
39
39
|
*/
|
|
40
|
-
export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<CalendarShellProps>): React.JSX.Element;
|
|
40
|
+
export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<CalendarShellProps>): React.JSX.Element;
|
|
@@ -10,6 +10,7 @@ import { listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.j
|
|
|
10
10
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
11
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
12
12
|
import AppShell from "../../layout/AppShell.js";
|
|
13
|
+
import { useLocationSearch } from "../../../navigation/AppRouter.js";
|
|
13
14
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
14
15
|
const CalendarShellContext = createContext({
|
|
15
16
|
calendarFolders: [],
|
|
@@ -29,16 +30,19 @@ export function useCalendarShell() {
|
|
|
29
30
|
* state — it lives in `apps/www/calendar/index.tsx`'s own local state, since navigating between
|
|
30
31
|
* views/dates must be instant (no full page reload) and this framework has no client-side router.
|
|
31
32
|
*/
|
|
32
|
-
export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
|
|
33
|
+
export default function CalendarShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }) {
|
|
33
34
|
const [status, setStatus] = useState("checking");
|
|
34
35
|
const [error, setError] = useState(null);
|
|
35
36
|
const [mailboxes, setMailboxes] = useState([]);
|
|
36
37
|
const [mailboxCalendars, setMailboxCalendars] = useState([]);
|
|
37
38
|
const [requestedMailboxUid, setRequestedMailboxUid] = useState(null);
|
|
38
39
|
const [folderRefreshToken, setFolderRefreshToken] = useState(0);
|
|
40
|
+
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
41
|
+
// effect, not during render, so the server render and the hydrating render agree.
|
|
42
|
+
const search = useLocationSearch();
|
|
39
43
|
useEffect(() => {
|
|
40
|
-
setRequestedMailboxUid(new URLSearchParams(
|
|
41
|
-
}, []);
|
|
44
|
+
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
45
|
+
}, [search]);
|
|
42
46
|
useEffect(() => {
|
|
43
47
|
if (!userUid) {
|
|
44
48
|
return;
|
|
@@ -100,5 +104,5 @@ export default function CalendarShell({ userUid, authServerUrl, impersonating, i
|
|
|
100
104
|
else if (userUid && status === "ready") {
|
|
101
105
|
inner = (_jsx("div", { className: "flex-1 min-w-0 flex flex-col", children: _jsx(CalendarShellContext.Provider, { value: contextValue, children: children }) }));
|
|
102
106
|
}
|
|
103
|
-
return (_jsx(AppShell, { active: "calendar", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
|
|
107
|
+
return (_jsx(AppShell, { active: "calendar", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, trustedRoles: trustedRoles, pluginNav: pluginNav, children: inner }));
|
|
104
108
|
}
|
|
@@ -14,6 +14,8 @@ export interface ContactsToolbarProps {
|
|
|
14
14
|
/** A `.vcf` file the user picked via the toolbar's own hidden file input — parsing/import is the
|
|
15
15
|
* page's responsibility (it has the mailbox/folder context and the actual `createContact` calls). */
|
|
16
16
|
onImportFile: (file: File) => void;
|
|
17
|
+
/** The keyboard's "New contact" is registered by the page: the button names its shortcut in its tooltip and `aria-keyshortcuts`. */
|
|
18
|
+
shortcuts?: boolean;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Contacts' Outlook-style ribbon toolbar. Deliberately presentational — every action is a callback the
|
|
@@ -22,4 +24,4 @@ export interface ContactsToolbarProps {
|
|
|
22
24
|
* needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
|
|
23
25
|
* "+ New list" affordance next to the "Your contact lists" heading.
|
|
24
26
|
*/
|
|
25
|
-
export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, }: ContactsToolbarProps): React.JSX.Element;
|
|
27
|
+
export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, shortcuts, }: ContactsToolbarProps): React.JSX.Element;
|
|
@@ -5,8 +5,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
6
|
import { useRef } from "react";
|
|
7
7
|
import { HiOutlineArrowDownTray, HiOutlineArrowUpTray, HiOutlineEnvelope, HiOutlinePencil, HiOutlineStar, HiOutlineTag, HiOutlineTrash, HiOutlineUserPlus, HiStar, } from "react-icons/hi2";
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { SHORTCUTS } from "../../keyboard/keymap.js";
|
|
9
|
+
import { useShortcutProps } from "../../keyboard/useShortcutProps.js";
|
|
10
|
+
function ToolbarButton({ label, icon: Icon, disabled, onClick, hint, }) {
|
|
11
|
+
return (_jsxs("button", { type: "button", onClick: onClick, disabled: disabled, ...hint, className: "flex flex-col items-center gap-1 text-xs text-text-muted hover:text-text disabled:opacity-40 disabled:cursor-not-allowed px-2.5 py-1.5 rounded-sm hover:not-disabled:bg-surface-alt", children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: label })] }));
|
|
10
12
|
}
|
|
11
13
|
function Divider() {
|
|
12
14
|
return _jsx("div", { className: "w-px self-stretch my-1 bg-border", "aria-hidden": "true" });
|
|
@@ -18,7 +20,8 @@ function Divider() {
|
|
|
18
20
|
* needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
|
|
19
21
|
* "+ New list" affordance next to the "Your contact lists" heading.
|
|
20
22
|
*/
|
|
21
|
-
export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, }) {
|
|
23
|
+
export default function ContactsToolbar({ selectedCount, allSelectedFavorited, onNewContact, onEdit, onDelete, onEmail, onToggleFavorite, onAddCategory, onExportVCard, onImportFile, shortcuts, }) {
|
|
24
|
+
const newContactHint = useShortcutProps("New contact", SHORTCUTS.contacts.create, !!shortcuts);
|
|
22
25
|
const importInputRef = useRef(null);
|
|
23
26
|
const hasSelection = selectedCount > 0;
|
|
24
27
|
function handleImportFileChosen(e) {
|
|
@@ -28,5 +31,5 @@ export default function ContactsToolbar({ selectedCount, allSelectedFavorited, o
|
|
|
28
31
|
onImportFile(file);
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
|
-
return (_jsxs("div", { role: "toolbar", "aria-label": "Contacts actions", className: "border-b border-border bg-surface-alt flex items-center gap-0.5 px-2 py-1", children: [_jsx(ToolbarButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Edit", icon: HiOutlinePencil, disabled: selectedCount !== 1, onClick: onEdit }), _jsx(ToolbarButton, { label: "Delete", icon: HiOutlineTrash, disabled: !hasSelection, onClick: onDelete }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Email", icon: HiOutlineEnvelope, disabled: !hasSelection, onClick: onEmail }), _jsx(ToolbarButton, { label: allSelectedFavorited ? "Unfavorite" : "Favorite", icon: allSelectedFavorited ? HiStar : HiOutlineStar, disabled: !hasSelection, onClick: onToggleFavorite }), _jsx(ToolbarButton, { label: "Add category", icon: HiOutlineTag, disabled: !hasSelection, onClick: onAddCategory }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Export", icon: HiOutlineArrowUpTray, disabled: !hasSelection, onClick: onExportVCard }), _jsx(ToolbarButton, { label: "Import", icon: HiOutlineArrowDownTray, onClick: () => importInputRef.current?.click() }), _jsx("input", { ref: importInputRef, type: "file", accept: ".vcf", className: "sr-only", "aria-label": "Import contacts file", onChange: handleImportFileChosen })] }));
|
|
34
|
+
return (_jsxs("div", { role: "toolbar", "aria-label": "Contacts actions", className: "border-b border-border bg-surface-alt flex items-center gap-0.5 px-2 py-1", children: [_jsx(ToolbarButton, { label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Edit", icon: HiOutlinePencil, disabled: selectedCount !== 1, onClick: onEdit }), _jsx(ToolbarButton, { label: "Delete", icon: HiOutlineTrash, disabled: !hasSelection, onClick: onDelete }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Email", icon: HiOutlineEnvelope, disabled: !hasSelection, onClick: onEmail }), _jsx(ToolbarButton, { label: allSelectedFavorited ? "Unfavorite" : "Favorite", icon: allSelectedFavorited ? HiStar : HiOutlineStar, disabled: !hasSelection, onClick: onToggleFavorite }), _jsx(ToolbarButton, { label: "Add category", icon: HiOutlineTag, disabled: !hasSelection, onClick: onAddCategory }), _jsx(Divider, {}), _jsx(ToolbarButton, { label: "Export", icon: HiOutlineArrowUpTray, disabled: !hasSelection, onClick: onExportVCard }), _jsx(ToolbarButton, { label: "Import", icon: HiOutlineArrowDownTray, onClick: () => importInputRef.current?.click() }), _jsx("input", { ref: importInputRef, type: "file", accept: ".vcf", className: "sr-only", "aria-label": "Import contacts file", onChange: handleImportFileChosen })] }));
|
|
32
35
|
}
|
|
@@ -18,4 +18,4 @@ export declare function useContactsShell(): ContactsShellContextValue;
|
|
|
18
18
|
* sidebar is therefore just a mailbox switcher (when the caller has more than one); the actual contact
|
|
19
19
|
* list/detail UI is `apps/www/contacts/index.tsx`'s own content, rendered as `children` here.
|
|
20
20
|
*/
|
|
21
|
-
export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<ContactsShellProps>): React.JSX.Element;
|
|
21
|
+
export default function ContactsShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, trustedRoles, pluginNav, children, }: PropsWithChildren<ContactsShellProps>): React.JSX.Element;
|