@rapidmx/web-client 0.5.0 → 0.6.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.
Files changed (164) hide show
  1. package/README.md +88 -0
  2. package/apps/shared/components/admin/layout/AdminShell.tsx +25 -8
  3. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  4. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  5. package/apps/shared/components/layout/AppShell.tsx +36 -18
  6. package/apps/shared/components/mail/layout/MailShell.tsx +420 -418
  7. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
  8. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  9. package/apps/shared/plugins/pluginNav.ts +73 -0
  10. package/apps/shared/search/localIndexRpcClient.ts +3 -1
  11. package/dist/apps/admin/_404.d.ts +2 -0
  12. package/dist/apps/admin/_500.d.ts +2 -0
  13. package/dist/apps/admin/_layout.d.ts +8 -0
  14. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  15. package/dist/apps/admin/branding/index.d.ts +3 -0
  16. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  17. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  18. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  19. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  20. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  21. package/dist/apps/admin/domains/index.d.ts +3 -0
  22. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  23. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  24. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  25. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  26. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  27. package/dist/apps/admin/index.d.ts +3 -0
  28. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  29. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  30. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  31. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  32. package/dist/apps/admin/plugins/index.d.ts +3 -0
  33. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  34. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  35. package/dist/apps/admin/setup/index.d.ts +3 -0
  36. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  37. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  38. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  39. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  40. package/dist/apps/escrow/_layout.d.ts +10 -0
  41. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  42. package/dist/apps/escrow/index.d.ts +3 -0
  43. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  44. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  45. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  46. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  47. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  48. package/dist/apps/shared/components/admin/layout/AdminShell.js +12 -5
  49. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  50. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  51. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  52. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  53. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
  54. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  55. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
  56. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  57. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  58. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
  59. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
  60. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
  61. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  62. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  63. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  64. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  65. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  66. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  67. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  68. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  69. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  70. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  71. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  72. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  73. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  74. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
  75. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  76. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  77. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  78. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  79. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  80. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  81. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  82. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  83. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  84. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  85. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  86. package/dist/apps/shared/components/layout/AppShell.js +15 -13
  87. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  88. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  89. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  90. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  91. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  92. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  93. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
  94. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
  95. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
  96. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
  97. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  98. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  99. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  100. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  101. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
  102. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  103. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  104. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
  105. package/dist/apps/shared/components/mail/layout/MailShell.js +2 -2
  106. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  107. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  108. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  109. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  110. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  111. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
  112. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  113. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  114. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  115. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  116. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  117. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  118. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  119. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  120. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  121. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  122. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  123. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  124. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  125. package/dist/apps/shared/search/localIndexRpcClient.js +3 -1
  126. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  127. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  128. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  129. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  130. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  131. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  132. package/dist/apps/www/_404.d.ts +2 -0
  133. package/dist/apps/www/_500.d.ts +2 -0
  134. package/dist/apps/www/_layout.d.ts +14 -0
  135. package/dist/apps/www/calendar/index.d.ts +3 -0
  136. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  137. package/dist/apps/www/contacts/index.d.ts +3 -0
  138. package/dist/apps/www/index.d.ts +3 -0
  139. package/dist/apps/www/messages/[uid].d.ts +11 -0
  140. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  141. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  142. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  143. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  144. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  145. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  146. package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
  147. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  148. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  149. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  150. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  151. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  152. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  153. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  154. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  155. package/dist/apps/www/tasks/index.d.ts +3 -0
  156. package/package.json +3 -3
  157. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  158. package/apps/www/settings/booking-types/[uid].tsx +0 -306
  159. package/apps/www/settings/booking-types/index.tsx +0 -98
  160. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  161. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  162. package/dist/apps/www/settings/booking-types/[uid].js +0 -134
  163. package/dist/apps/www/settings/booking-types/index.js +0 -29
  164. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # RapidMX: Web Client
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@rapidmx/web-client)](https://www.npmjs.com/package/@rapidmx/web-client)
4
+
5
+ RapidMX's webmail (`apps/www`), admin console (`apps/admin`) and escrow console (`apps/escrow`) React UI. The pages are
6
+ served and hydrated by [`rapidmx/server`](https://github.com/RapidMX/server) through `@rapidrest/react`'s file-convention
7
+ routes, and `@rapidmx/electron-client` reuses the same components. Platform-agnostic API clients, hooks and generic UI
8
+ primitives live in [`@rapidmx/react-shared`](https://github.com/RapidMX/react-shared).
9
+
10
+ ## Package layout
11
+
12
+ The package ships the TSX sources (`apps/`) and a compiled mirror (`dist/apps/`, JavaScript plus `.d.ts` declarations).
13
+ There is no root export. Every module is its own subpath, mapped by `package.json`'s `exports` from
14
+ `@rapidmx/web-client/<path>.js` to `dist/apps/<path>.js`:
15
+
16
+ ```ts
17
+ import SettingsShell from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
18
+ ```
19
+
20
+ `@rapidmx/web-client/shared/styles/app.css` is the Tailwind entry point and design tokens.
21
+
22
+ ## Plugin UI surface
23
+
24
+ Server plugins can ship their own pages (see the plugin manifest's `ui` field in `@rapidmx/restapi`). The server builds
25
+ them together with this package, so they share one React, one `@rapidmx/react-shared` state and one stylesheet. The
26
+ modules below are the **supported surface for plugin pages**. Anything else under `apps/` is internal and may change in
27
+ any release.
28
+
29
+ ### Shells
30
+
31
+ | Import | Use |
32
+ | --- | --- |
33
+ | `shared/components/layout/AppShell.js` | Chrome for webmail apps: app rail, header, user menu, impersonation banner, compose and unlock providers. `active` is a core app or the plugin's `appRail` item id. |
34
+ | `shared/components/settings/layout/SettingsShell.js` | Settings chrome with the section list and mailbox switcher. `active` is the plugin's `settingsSections` item id. `useSettingsShell()` gives the selected `mailboxUid` and the accessible `mailboxes`. |
35
+ | `shared/components/admin/layout/AdminShell.js` | Admin console chrome, gated on administrator access. `active` is the plugin's `adminNav` item id. |
36
+ | `shared/components/layout/BrandingChrome.js` | `BrandingHeader` and `BrandingFooter`, for pages that don't use a shell, such as public pages. |
37
+ | `shared/plugins/pluginNav.js` | The `PluginNav`, `PluginUiNavItem` and `PluginNavProps` types. |
38
+
39
+ Every www and admin page receives a `pluginNav` prop from the server. It lists the settings sections, admin sections and
40
+ app rail entries of every enabled plugin whose UI built. The shells append those entries after their own, with a generic
41
+ icon. An entry whose id matches a core entry is skipped, and so is one whose `href` isn't a same-origin path. Pass the
42
+ page props straight to the shell so the navigation shows:
43
+
44
+ ```tsx
45
+ import React from "react";
46
+ import SettingsShell, {
47
+ SettingsShellProps,
48
+ useSettingsShell,
49
+ } from "@rapidmx/web-client/shared/components/settings/layout/SettingsShell.js";
50
+
51
+ export default function RemindersSettingsPage(props: Omit<SettingsShellProps, "active">) {
52
+ return (
53
+ <SettingsShell {...props} active="reminders">
54
+ <RemindersSettings />
55
+ </SettingsShell>
56
+ );
57
+ }
58
+
59
+ function RemindersSettings() {
60
+ const { mailboxUid } = useSettingsShell();
61
+ return <p>Settings for {mailboxUid}</p>;
62
+ }
63
+ ```
64
+
65
+ Public and escrow pages get no `pluginNav`.
66
+
67
+ ### From `@rapidmx/react-shared`
68
+
69
+ Plugin pages import these directly from `@rapidmx/react-shared`, which the server resolves to the same copy the shells
70
+ use:
71
+
72
+ - `branding/useBranding.js`: `useBranding()`, for the branding and icon of pages outside a shell;
73
+ - `auth/session.js`: `useRedirectIfUnauthenticated()`, already called by every shell;
74
+ - `util/api.js`: `apiFetch()` and `ApiRequestError`, for calling the plugin's own API routes;
75
+ - `mail/mailApi.js`: mailboxes and folders, such as `listMailboxes()` and `listFolders()`;
76
+ - `components/buttons/Button.js`, `components/feedback/Alert.js` and `components/feedback/Skeleton.js`;
77
+ - `components/forms/FormField.js`;
78
+ - `components/overlays/Modal.js` and `components/overlays/Drawer.js`;
79
+ - `components/pickers/MiniDatePicker.js`.
80
+
81
+ ## Development
82
+
83
+ ```sh
84
+ yarn install
85
+ yarn test # vitest with coverage gates
86
+ yarn lint
87
+ yarn build # tsc into dist/apps
88
+ ```
@@ -30,6 +30,7 @@ import BottomTabBar, { NavItem } from "@rapidmx/react-shared/components/navigati
30
30
  import { BrandingFooter, BrandingHeader } from "../../layout/BrandingChrome.js";
31
31
  import UserMenu from "../../layout/UserMenu.js";
32
32
  import { signOutOfConsole } from "../signOut.js";
33
+ import { mergePluginNavItems, PluginNav, PluginNavProps } from "../../../plugins/pluginNav.js";
33
34
 
34
35
  export type AdminSection =
35
36
  | "mailboxes"
@@ -48,9 +49,13 @@ export type AdminSection =
48
49
  | "plugins"
49
50
  | "branding";
50
51
 
51
- export interface AdminShellProps {
52
- /** Which icon in the rail is highlighted as the current section. */
53
- active: AdminSection;
52
+ /** A core `AdminSection`, or a plugin's `adminNav` item id (see `PluginNav`). */
53
+ export type AdminShellActive = AdminSection | (string & {});
54
+
55
+ export interface AdminShellProps extends PluginNavProps {
56
+ /** Which icon in the rail is highlighted as the current section - a plugin admin page passes its own
57
+ * `adminNav` item id. */
58
+ active: AdminShellActive;
54
59
  /** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
55
60
  userUid?: string;
56
61
  /** auth-server's base URL, injected via the route's `fetchProps` — see `src/mongo/routes/AdminConsoleRoute.ts`. */
@@ -129,7 +134,18 @@ const MAILBOX_SCOPED_ITEMS: NavItem[] = [
129
134
  /** The setup wizard - reached by redirect or from the Mailboxes page, not from the rail. */
130
135
  const SETUP_ITEM: NavItem = { id: "setup", href: "/admin/setup", label: "Setup", icon: HiOutlineRocketLaunch };
131
136
 
132
- const ALL_ITEMS: NavItem[] = [...NAV_ITEMS, ...MAILBOX_SCOPED_ITEMS, SETUP_ITEM];
137
+ const OFF_RAIL_ITEMS: NavItem[] = [...MAILBOX_SCOPED_ITEMS, SETUP_ITEM];
138
+
139
+ /** `NAV_ITEMS` followed by the plugins' `adminNav` items (generic icon). Core ids win, including the
140
+ * off-rail sections' - see `mergePluginNavItems`. */
141
+ export function adminNavItems(pluginNav?: PluginNav): NavItem[] {
142
+ return mergePluginNavItems<NavItem>(
143
+ NAV_ITEMS,
144
+ pluginNav?.adminNav,
145
+ ({ id, href, label }) => ({ id, href, label, icon: HiOutlinePuzzlePiece }),
146
+ OFF_RAIL_ITEMS.map((item) => item.id),
147
+ );
148
+ }
133
149
 
134
150
  /**
135
151
  * Gates every `apps/admin` page behind the `admin` trusted role. Uses `GET /api/admin/release-notes` (any
@@ -137,7 +153,7 @@ const ALL_ITEMS: NavItem[] = [...NAV_ITEMS, ...MAILBOX_SCOPED_ITEMS, SETUP_ITEM]
137
153
  * caller's JWT carries a trusted role, a 403 means it doesn't. There is no local step-up/elevation flow
138
154
  * (that would need a cross-origin call to auth-server's own elevation endpoint — not wired up yet).
139
155
  */
140
- export default function AdminShell({ active, userUid, authServerUrl, children }: PropsWithChildren<AdminShellProps>) {
156
+ export default function AdminShell({ active, userUid, authServerUrl, pluginNav, children }: PropsWithChildren<AdminShellProps>) {
141
157
  const [status, setStatus] = useState<Status>("checking");
142
158
  const [error, setError] = useState<string | null>(null);
143
159
  const { branding, iconSrc } = useBranding();
@@ -199,7 +215,8 @@ export default function AdminShell({ active, userUid, authServerUrl, children }:
199
215
  </div>
200
216
  );
201
217
  } else {
202
- const activeItem = ALL_ITEMS.find((item) => item.id === active);
218
+ const navItems = adminNavItems(pluginNav);
219
+ const activeItem = [...navItems, ...OFF_RAIL_ITEMS].find((item) => item.id === active);
203
220
  content = (
204
221
  <div className="min-h-screen flex flex-col bg-surface-alt">
205
222
  <div className="flex-1 flex min-h-0">
@@ -208,7 +225,7 @@ export default function AdminShell({ active, userUid, authServerUrl, children }:
208
225
  className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1"
209
226
  >
210
227
  <img src={iconSrc} width="96" height="96" alt="" className="mb-3" />
211
- {NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (
228
+ {navItems.map(({ id, href, label, icon: Icon }) => (
212
229
  <a
213
230
  key={id}
214
231
  href={href}
@@ -226,7 +243,7 @@ export default function AdminShell({ active, userUid, authServerUrl, children }:
226
243
  </a>
227
244
  ))}
228
245
  </nav>
229
- <BottomTabBar apps={NAV_ITEMS} active={active} />
246
+ <BottomTabBar apps={navItems} active={active} />
230
247
  <div className="flex-1 flex flex-col min-w-0">
231
248
  <header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
232
249
  <span className="font-display font-bold text-lg uppercase tracking-wide">{activeItem?.label}</span>
@@ -1,192 +1,194 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import React, { createContext, PropsWithChildren, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
6
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
- import { accentColorForMailbox, colorForFolder } from "@rapidmx/react-shared/calendar/calendarColors.js";
8
- import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
9
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
- import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
11
- import AppShell, { AppShellProps } from "../../layout/AppShell.js";
12
- import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
13
-
14
- export type CalendarShellProps = Omit<AppShellProps, "active">;
15
-
16
- /** One mailbox's own `calendar`-type folders. `error` is set (and `calendarFolders` left empty) when that
17
- * mailbox's own `listFolders()` failed - one mailbox's failure never hides every other mailbox's calendars. */
18
- export interface MailboxCalendars {
19
- mailbox: Mailbox;
20
- calendarFolders: Folder[];
21
- error?: string;
22
- }
23
-
24
- export interface CalendarShellContextValue {
25
- /** The default mailbox for "+ New event" (`?mailboxUid=` if accessible, else the caller's own mailbox,
26
- * else the first accessible one) - every accessible mailbox's calendars are shown regardless. */
27
- mailboxUid?: string;
28
- /** `mailboxUid`'s first `calendar`-type folder - the default target for "+ New event". */
29
- folderUid?: string;
30
- /** Every accessible mailbox's `calendar`-type folders, flattened - the set of calendars that can be
31
- * checked/shown (see `CalendarListSidebar`). Each folder's own `mailboxUid` says which mailbox it's in. */
32
- calendarFolders: Folder[];
33
- /** The same folders, grouped per mailbox, for the sidebar's per-mailbox sections. */
34
- mailboxCalendars: MailboxCalendars[];
35
- mailboxes: Mailbox[];
36
- /** Re-fetches every mailbox's folders (e.g. after creating a new calendar) without a full reload. */
37
- reloadFolders: () => void;
38
- /** The display color for a calendar folder: its own `color`, else the default for the caller's own
39
- * mailbox, else its mailbox's `accentColorForMailbox()` - so a shared mailbox's calendar is visually
40
- * distinct from the caller's own even when neither has a color set. */
41
- colorFor: (folder: Folder) => string;
42
- }
43
-
44
- const CalendarShellContext = createContext<CalendarShellContextValue>({
45
- calendarFolders: [],
46
- mailboxCalendars: [],
47
- mailboxes: [],
48
- reloadFolders: () => undefined,
49
- colorFor: (folder) => colorForFolder(folder),
50
- });
51
-
52
- /** Reads the calendars the Calendar is currently showing, as resolved by the enclosing `CalendarShell`. */
53
- export function useCalendarShell(): CalendarShellContextValue {
54
- return useContext(CalendarShellContext);
55
- }
56
-
57
- type Status = "checking" | "error" | "ready";
58
-
59
- /**
60
- * The Calendar app's shell. Fetches every accessible mailbox's calendar folders in parallel so shared
61
- * mailboxes' calendars show alongside the caller's own, each mailbox color-coded (see `colorFor`) - there
62
- * is no mailbox switcher. The actual view being looked at (month/week/day, and which one) is *not* shell
63
- * state — it lives in `apps/www/calendar/index.tsx`'s own local state, since navigating between
64
- * views/dates must be instant (no full page reload) and this framework has no client-side router.
65
- */
66
- export default function CalendarShell({
67
- userUid,
68
- authServerUrl,
69
- impersonating,
70
- impersonationBaseUrl,
71
- trusted,
72
- children,
73
- }: PropsWithChildren<CalendarShellProps>) {
74
- const [status, setStatus] = useState<Status>("checking");
75
- const [error, setError] = useState<string | null>(null);
76
- const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
77
- const [mailboxCalendars, setMailboxCalendars] = useState<MailboxCalendars[]>([]);
78
- const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
79
- const [folderRefreshToken, setFolderRefreshToken] = useState(0);
80
-
81
- useEffect(() => {
82
- setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
83
- }, []);
84
-
85
- useEffect(() => {
86
- if (!userUid) {
87
- return;
88
- }
89
- listMailboxes({ limit: 100 })
90
- .then((result) => {
91
- setMailboxes(result);
92
- setStatus("ready");
93
- })
94
- .catch((err) => {
95
- setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
96
- setStatus("error");
97
- });
98
- }, [userUid]);
99
-
100
- const ownMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
101
- const mailboxUid: string | undefined =
102
- (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
103
- ownMailboxUid;
104
-
105
- useEffect(() => {
106
- if (mailboxes.length === 0) {
107
- setMailboxCalendars([]);
108
- return;
109
- }
110
- let cancelled = false;
111
- void Promise.all(
112
- mailboxes.map((mailbox) =>
113
- listFolders(mailbox.uid)
114
- .then((folders): MailboxCalendars => ({ mailbox, calendarFolders: folders.filter((f) => f.type === "calendar") }))
115
- .catch(
116
- (err): MailboxCalendars => ({
117
- mailbox,
118
- calendarFolders: [],
119
- error: err instanceof ApiRequestError ? err.message : "Could not load this mailbox's calendar folder.",
120
- }),
121
- ),
122
- ),
123
- ).then((result) => {
124
- if (!cancelled) {
125
- setMailboxCalendars(result);
126
- }
127
- });
128
- return () => {
129
- cancelled = true;
130
- };
131
- }, [mailboxes, folderRefreshToken]);
132
-
133
- const calendarFolders = useMemo(() => mailboxCalendars.flatMap((mc) => mc.calendarFolders), [mailboxCalendars]);
134
- const folderUid: string | undefined = calendarFolders.find((f) => f.mailboxUid === mailboxUid)?.uid;
135
-
136
- const colorFor = useCallback(
137
- (folder: Folder) => colorForFolder(folder, folder.mailboxUid === ownMailboxUid ? undefined : accentColorForMailbox(folder.mailboxUid)),
138
- [ownMailboxUid],
139
- );
140
- const reloadFolders = useCallback(() => setFolderRefreshToken((t) => t + 1), []);
141
-
142
- const contextValue = useMemo<CalendarShellContextValue>(
143
- () => ({ mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor }),
144
- [mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor],
145
- );
146
-
147
- // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
148
- // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
149
- if (userUid && status === "ready" && mailboxes.length === 0) {
150
- return <MailboxProvisioning />;
151
- }
152
-
153
- let inner: ReactNode = null;
154
- if (userUid && status === "checking") {
155
- // Renders immediately (no network round trip needed) so switching into Calendar never shows a
156
- // blank pane while `listMailboxes()` is in flight — mimics the rough shape of the real sidebar
157
- // (mini date-picker + calendar list) that `apps/www/calendar/index.tsx` renders.
158
- inner = (
159
- <div className="w-56 shrink-0 bg-surface border-r border-border p-3 flex flex-col gap-4">
160
- <Skeleton height="h-40" className="rounded-md" />
161
- <SkeletonList count={4} />
162
- </div>
163
- );
164
- } else if (userUid && status === "error") {
165
- inner = (
166
- <div className="flex-1 flex items-center justify-center p-8">
167
- <div className="w-full max-w-md">
168
- <Alert>{error}</Alert>
169
- </div>
170
- </div>
171
- );
172
- } else if (userUid && status === "ready") {
173
- inner = (
174
- <div className="flex-1 min-w-0 flex flex-col">
175
- <CalendarShellContext.Provider value={contextValue}>{children}</CalendarShellContext.Provider>
176
- </div>
177
- );
178
- }
179
-
180
- return (
181
- <AppShell
182
- active="calendar"
183
- userUid={userUid}
184
- authServerUrl={authServerUrl}
185
- impersonating={impersonating}
186
- impersonationBaseUrl={impersonationBaseUrl}
187
- trusted={trusted}
188
- >
189
- {inner}
190
- </AppShell>
191
- );
192
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { createContext, PropsWithChildren, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
6
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
7
+ import { accentColorForMailbox, colorForFolder } from "@rapidmx/react-shared/calendar/calendarColors.js";
8
+ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shared/mail/mailApi.js";
9
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
+ import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
11
+ import AppShell, { AppShellProps } from "../../layout/AppShell.js";
12
+ import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
13
+
14
+ export type CalendarShellProps = Omit<AppShellProps, "active">;
15
+
16
+ /** One mailbox's own `calendar`-type folders. `error` is set (and `calendarFolders` left empty) when that
17
+ * mailbox's own `listFolders()` failed - one mailbox's failure never hides every other mailbox's calendars. */
18
+ export interface MailboxCalendars {
19
+ mailbox: Mailbox;
20
+ calendarFolders: Folder[];
21
+ error?: string;
22
+ }
23
+
24
+ export interface CalendarShellContextValue {
25
+ /** The default mailbox for "+ New event" (`?mailboxUid=` if accessible, else the caller's own mailbox,
26
+ * else the first accessible one) - every accessible mailbox's calendars are shown regardless. */
27
+ mailboxUid?: string;
28
+ /** `mailboxUid`'s first `calendar`-type folder - the default target for "+ New event". */
29
+ folderUid?: string;
30
+ /** Every accessible mailbox's `calendar`-type folders, flattened - the set of calendars that can be
31
+ * checked/shown (see `CalendarListSidebar`). Each folder's own `mailboxUid` says which mailbox it's in. */
32
+ calendarFolders: Folder[];
33
+ /** The same folders, grouped per mailbox, for the sidebar's per-mailbox sections. */
34
+ mailboxCalendars: MailboxCalendars[];
35
+ mailboxes: Mailbox[];
36
+ /** Re-fetches every mailbox's folders (e.g. after creating a new calendar) without a full reload. */
37
+ reloadFolders: () => void;
38
+ /** The display color for a calendar folder: its own `color`, else the default for the caller's own
39
+ * mailbox, else its mailbox's `accentColorForMailbox()` - so a shared mailbox's calendar is visually
40
+ * distinct from the caller's own even when neither has a color set. */
41
+ colorFor: (folder: Folder) => string;
42
+ }
43
+
44
+ const CalendarShellContext = createContext<CalendarShellContextValue>({
45
+ calendarFolders: [],
46
+ mailboxCalendars: [],
47
+ mailboxes: [],
48
+ reloadFolders: () => undefined,
49
+ colorFor: (folder) => colorForFolder(folder),
50
+ });
51
+
52
+ /** Reads the calendars the Calendar is currently showing, as resolved by the enclosing `CalendarShell`. */
53
+ export function useCalendarShell(): CalendarShellContextValue {
54
+ return useContext(CalendarShellContext);
55
+ }
56
+
57
+ type Status = "checking" | "error" | "ready";
58
+
59
+ /**
60
+ * The Calendar app's shell. Fetches every accessible mailbox's calendar folders in parallel so shared
61
+ * mailboxes' calendars show alongside the caller's own, each mailbox color-coded (see `colorFor`) - there
62
+ * is no mailbox switcher. The actual view being looked at (month/week/day, and which one) is *not* shell
63
+ * state — it lives in `apps/www/calendar/index.tsx`'s own local state, since navigating between
64
+ * views/dates must be instant (no full page reload) and this framework has no client-side router.
65
+ */
66
+ export default function CalendarShell({
67
+ userUid,
68
+ authServerUrl,
69
+ impersonating,
70
+ impersonationBaseUrl,
71
+ trusted,
72
+ pluginNav,
73
+ children,
74
+ }: PropsWithChildren<CalendarShellProps>) {
75
+ const [status, setStatus] = useState<Status>("checking");
76
+ const [error, setError] = useState<string | null>(null);
77
+ const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
78
+ const [mailboxCalendars, setMailboxCalendars] = useState<MailboxCalendars[]>([]);
79
+ const [requestedMailboxUid, setRequestedMailboxUid] = useState<string | null>(null);
80
+ const [folderRefreshToken, setFolderRefreshToken] = useState(0);
81
+
82
+ useEffect(() => {
83
+ setRequestedMailboxUid(new URLSearchParams(window.location.search).get("mailboxUid"));
84
+ }, []);
85
+
86
+ useEffect(() => {
87
+ if (!userUid) {
88
+ return;
89
+ }
90
+ listMailboxes({ limit: 100 })
91
+ .then((result) => {
92
+ setMailboxes(result);
93
+ setStatus("ready");
94
+ })
95
+ .catch((err) => {
96
+ setError(err instanceof ApiRequestError ? err.message : "Could not load your mailboxes.");
97
+ setStatus("error");
98
+ });
99
+ }, [userUid]);
100
+
101
+ const ownMailboxUid = mailboxes.find((mb) => mb.ownerUserUid === userUid)?.uid ?? mailboxes[0]?.uid;
102
+ const mailboxUid: string | undefined =
103
+ (requestedMailboxUid && mailboxes.some((mb) => mb.uid === requestedMailboxUid) ? requestedMailboxUid : undefined) ??
104
+ ownMailboxUid;
105
+
106
+ useEffect(() => {
107
+ if (mailboxes.length === 0) {
108
+ setMailboxCalendars([]);
109
+ return;
110
+ }
111
+ let cancelled = false;
112
+ void Promise.all(
113
+ mailboxes.map((mailbox) =>
114
+ listFolders(mailbox.uid)
115
+ .then((folders): MailboxCalendars => ({ mailbox, calendarFolders: folders.filter((f) => f.type === "calendar") }))
116
+ .catch(
117
+ (err): MailboxCalendars => ({
118
+ mailbox,
119
+ calendarFolders: [],
120
+ error: err instanceof ApiRequestError ? err.message : "Could not load this mailbox's calendar folder.",
121
+ }),
122
+ ),
123
+ ),
124
+ ).then((result) => {
125
+ if (!cancelled) {
126
+ setMailboxCalendars(result);
127
+ }
128
+ });
129
+ return () => {
130
+ cancelled = true;
131
+ };
132
+ }, [mailboxes, folderRefreshToken]);
133
+
134
+ const calendarFolders = useMemo(() => mailboxCalendars.flatMap((mc) => mc.calendarFolders), [mailboxCalendars]);
135
+ const folderUid: string | undefined = calendarFolders.find((f) => f.mailboxUid === mailboxUid)?.uid;
136
+
137
+ const colorFor = useCallback(
138
+ (folder: Folder) => colorForFolder(folder, folder.mailboxUid === ownMailboxUid ? undefined : accentColorForMailbox(folder.mailboxUid)),
139
+ [ownMailboxUid],
140
+ );
141
+ const reloadFolders = useCallback(() => setFolderRefreshToken((t) => t + 1), []);
142
+
143
+ const contextValue = useMemo<CalendarShellContextValue>(
144
+ () => ({ mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor }),
145
+ [mailboxUid, folderUid, calendarFolders, mailboxCalendars, mailboxes, reloadFolders, colorFor],
146
+ );
147
+
148
+ // A full-screen takeover, not nested inside the rest of the app's chrome — there's nothing else
149
+ // for a mailbox-less caller to do here yet, so the icon rail/header don't render at all.
150
+ if (userUid && status === "ready" && mailboxes.length === 0) {
151
+ return <MailboxProvisioning />;
152
+ }
153
+
154
+ let inner: ReactNode = null;
155
+ if (userUid && status === "checking") {
156
+ // Renders immediately (no network round trip needed) so switching into Calendar never shows a
157
+ // blank pane while `listMailboxes()` is in flight — mimics the rough shape of the real sidebar
158
+ // (mini date-picker + calendar list) that `apps/www/calendar/index.tsx` renders.
159
+ inner = (
160
+ <div className="w-56 shrink-0 bg-surface border-r border-border p-3 flex flex-col gap-4">
161
+ <Skeleton height="h-40" className="rounded-md" />
162
+ <SkeletonList count={4} />
163
+ </div>
164
+ );
165
+ } else if (userUid && status === "error") {
166
+ inner = (
167
+ <div className="flex-1 flex items-center justify-center p-8">
168
+ <div className="w-full max-w-md">
169
+ <Alert>{error}</Alert>
170
+ </div>
171
+ </div>
172
+ );
173
+ } else if (userUid && status === "ready") {
174
+ inner = (
175
+ <div className="flex-1 min-w-0 flex flex-col">
176
+ <CalendarShellContext.Provider value={contextValue}>{children}</CalendarShellContext.Provider>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ return (
182
+ <AppShell
183
+ active="calendar"
184
+ userUid={userUid}
185
+ authServerUrl={authServerUrl}
186
+ impersonating={impersonating}
187
+ impersonationBaseUrl={impersonationBaseUrl}
188
+ trusted={trusted}
189
+ pluginNav={pluginNav}
190
+ >
191
+ {inner}
192
+ </AppShell>
193
+ );
194
+ }