@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
@@ -0,0 +1,13 @@
1
+ import React, { RefObject } from "react";
2
+ export interface GifPickerProps {
3
+ anchorRef: RefObject<HTMLElement | null>;
4
+ onSelect: (url: string) => void;
5
+ onClose: () => void;
6
+ }
7
+ /** "Insert GIF" popup — a search box plus a scrollable grid of Giphy thumbnails (trending by default,
8
+ * live-searched as the user types). Selecting one inserts its full-resolution URL into the message
9
+ * body — see `ComposeToolbar.tsx`'s own doc comment on why that's a plain `<img>` reference rather
10
+ * than an uploaded attachment (unlike "Insert image"): a GIF is already hosted at a stable public
11
+ * URL, so there's nothing to upload. Rendered via `PopoverPortal` — see that component's own doc
12
+ * comment on why (escaping the toolbar's `overflow-hidden` ancestors). */
13
+ export default function GifPicker({ anchorRef, onSelect, onClose }: GifPickerProps): React.JSX.Element;
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ export interface RichTextEditorProps {
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ height?: string;
6
+ /** When true, the editor fills its parent's height via flexbox instead of a fixed `height` — for
7
+ * embedding inside a flex-column container of variable height (the floating Compose window) where
8
+ * a literal pixel height can't be computed up front. `height` is ignored when this is set. */
9
+ fill?: boolean;
10
+ /** Passed straight through to `ComposeToolbar` — see its own doc comment on this prop. */
11
+ onUploadImage: (file: File) => Promise<string | null>;
12
+ }
13
+ /**
14
+ * A WYSIWYG rich-text editor for the Compose window's message body — replaces `MonacoHtmlEditor` (an HTML
15
+ * *source* editor) with a real Outlook-Home-tab-style formatting experience, built on TipTap/ProseMirror.
16
+ * Used by `ComposeWindow` (the floating overlay, not a dedicated page — see `ComposeContext.tsx`'s doc
17
+ * comment).
18
+ *
19
+ * `value` seeds the editor's initial content only — like `MonacoHtmlEditor`, this never re-syncs from a
20
+ * later `value` prop change; TipTap owns its own document once created. Live edits flow out as HTML (via
21
+ * `editor.getHTML()`) through `onChange`.
22
+ *
23
+ * `immediatelyRender: false` is required for this framework's SSR: the page embedding `ComposeWindow` is
24
+ * rendered server-side under plain Node before hydration (see `ReactRoute`), and TipTap's default
25
+ * (`immediatelyRender: true`) tries to mount its ProseMirror view during that very first render, which
26
+ * both can't work without a real DOM and produces a hydration mismatch. With it `false`, `useEditor`
27
+ * returns `null` until the editor actually mounts client-side after hydration — `ComposeToolbar`/
28
+ * `EditorContent` both already handle a `null` editor by rendering their disabled/empty state, so this
29
+ * needs no extra loading-placeholder logic here. Unlike `MonacoHtmlEditor`, no dynamic `import()` is
30
+ * needed to defer loading TipTap's modules off the SSR path — `immediatelyRender: false` alone is
31
+ * sufficient here since TipTap (unlike Monaco) never touches the DOM at module-evaluation time, only when
32
+ * an editor view actually mounts.
33
+ */
34
+ export default function RichTextEditor({ value, onChange, height, fill, onUploadImage }: RichTextEditorProps): React.JSX.Element;
@@ -0,0 +1,16 @@
1
+ import React, { RefObject } from "react";
2
+ export interface ScheduleSendPickerProps {
3
+ anchorRef: RefObject<HTMLElement | null>;
4
+ onClose: () => void;
5
+ /** Called with the chosen instant as a UTC ISO string, already validated to be in the future. */
6
+ onSchedule: (scheduledSendTimeIso: string) => void;
7
+ }
8
+ /**
9
+ * The "Send later" popover next to Compose's Send button — same `PopoverPortal` shell
10
+ * `EmojiPicker`/`GifPicker`/`ResourcePicker` already use, just holding a single `datetime-local` input
11
+ * instead of a grid/list. Validates the chosen time is genuinely in the future before calling
12
+ * `onSchedule` — `@rapidmx/restapi`'s own `send()` only defers when `scheduledSendTime` is strictly
13
+ * later than "now", so a past/immediate pick here would silently send right away instead of scheduling,
14
+ * which would surprise a reader expecting the picker's own choice to be honored.
15
+ */
16
+ export default function ScheduleSendPicker({ anchorRef, onClose, onSchedule }: ScheduleSendPickerProps): React.JSX.Element;
@@ -0,0 +1,27 @@
1
+ /** Saves a compose window's unsaved edits (or waits for a save already on the wire), resolving to `false` when
2
+ * something the window holds is still unsaved afterwards (any other value counts as saved). Never rejects. */
3
+ export type ComposeFlush = () => Promise<unknown>;
4
+ /**
5
+ * Marks the app as signing out (explicitly, or forced by a sign-out in another tab), so compose windows stop
6
+ * asking the browser to confirm leaving the page - the "Leave site?" prompt would otherwise let the user cancel
7
+ * the sign-out's own navigation and stay on a page whose session is being ended. Call it before flushing drafts
8
+ * and navigating. `clearSigningOut()` undoes it (a sign-out that didn't leave the page after all).
9
+ */
10
+ export declare function markSigningOut(): void;
11
+ /** Undoes `markSigningOut()`. */
12
+ export declare function clearSigningOut(): void;
13
+ /** Whether `markSigningOut()` has been called (and not cleared). */
14
+ export declare function isSigningOut(): boolean;
15
+ /**
16
+ * Registers an open compose window's draft flush, so leaving the app on purpose (Sign Out) can save the
17
+ * last edits still waiting on the autosave debounce before the session ends. Returns the unregister
18
+ * function. Module-level rather than React context: `AppShell` (the sign-out handler) renders the
19
+ * `ComposeProvider` itself, so it can't read that provider's context.
20
+ */
21
+ export declare function registerComposeFlush(flush: ComposeFlush): () => void;
22
+ /**
23
+ * Runs every registered flush and waits for all of them, but no longer than `timeoutMs`. Resolves to `true`
24
+ * only when every window reported its content saved in time (a window whose save failed also shows the user
25
+ * why, with Discard / Keep editing). Never rejects.
26
+ */
27
+ export declare function flushComposeDrafts(timeoutMs: number): Promise<boolean>;
@@ -0,0 +1,57 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { AppShellProps } from "../../layout/AppShell.js";
4
+ export type MailShellProps = Omit<AppShellProps, "active">;
5
+ /** One mailbox's own mail folders (already filtered to `MAIL_FOLDER_TYPES` and unsorted) - one entry per
6
+ * mailbox in `mailboxes`, fetched in parallel so every accessible mailbox's folder tree can render
7
+ * simultaneously (see `MailShell`'s own doc comment on why this replaced the old single-mailbox `folders`
8
+ * field). `error` is set (and `folders` left empty) for a mailbox whose own `listFolders()` call failed -
9
+ * one mailbox's fetch failure must never blank out every other mailbox's section. */
10
+ export interface MailboxFolders {
11
+ mailbox: Mailbox;
12
+ folders: Folder[];
13
+ error?: string;
14
+ }
15
+ /** The well-known mail folder types an "aggregate" pseudo-folder can merge across every accessible
16
+ * mailbox - a fixed, small set: `outbox` is deliberately excluded (transient per-mailbox send-queue
17
+ * state, not a "merge across mailboxes" concept), and there's no aggregate concept for `calendar`/
18
+ * `contacts`/`tasks`/`notes`/`user` folders (Mail's own sidebar already excludes those - see
19
+ * `MAIL_FOLDER_TYPES`). */
20
+ export declare const AGGREGATE_FOLDER_TYPES: readonly ["inbox", "sent_items", "drafts", "deleted_items", "junk"];
21
+ export type AggregateFolderType = (typeof AGGREGATE_FOLDER_TYPES)[number];
22
+ export interface MailShellContextValue {
23
+ /** The mailbox currently selected (`?mailboxUid=`) - `undefined` while an aggregate pseudo-folder is
24
+ * active instead (`aggregateFolderType` set), since there's no single mailbox to speak of then. */
25
+ mailboxUid?: string;
26
+ /** The folder currently selected (`?folderUid=`, or the selected mailbox's Inbox) - `undefined` in
27
+ * aggregate mode, for the same reason. */
28
+ folderUid?: string;
29
+ /** Set only when an aggregate pseudo-folder (`?aggregate=inbox` etc.) is selected instead of a real,
30
+ * single mailbox+folder - mutually exclusive with `mailboxUid`/`folderUid` above. */
31
+ aggregateFolderType?: AggregateFolderType;
32
+ mailboxes: Mailbox[];
33
+ /** Every accessible mailbox's own mail folders - replaces the old single-mailbox `folders: Folder[]`
34
+ * now that every mailbox's tree renders at once (see `MailboxFolders`'s own doc comment). */
35
+ mailboxFolders: MailboxFolders[];
36
+ }
37
+ /** Reads the mailbox/folder a page is currently showing, as resolved by the enclosing `MailShell`. */
38
+ export declare function useMailShell(): MailShellContextValue;
39
+ /** Page size of the one `listMailboxes()` call this shell makes. */
40
+ export declare const MAILBOX_LIST_LIMIT = 100;
41
+ /**
42
+ * Mail's own contextual sidebar (every accessible mailbox's own folder tree, plus a merged "All Mailboxes"
43
+ * aggregate section) + content area, rendered inside the shared `AppShell` chrome (icon rail, header,
44
+ * impersonation banner — see that component). There is no client-side router in this framework (see
45
+ * `ReactRoute`'s file-convention resolver) — the selected mailbox/folder (or aggregate pseudo-folder) live
46
+ * in the URL's `?mailboxUid=`/`?folderUid=`/`?aggregate=` query params, read once on mount (never during
47
+ * the initial render itself, matching every other query-param reader in this codebase — e.g.
48
+ * `apps/admin/quarantine`'s `readMailboxUid()` — so the server-rendered and just-hydrated client markup
49
+ * match).
50
+ *
51
+ * Every accessible mailbox's folder tree renders at once - there is no "switch mailbox" affordance
52
+ * anymore (the mailbox `<select>` this shell used to have is gone) - so a single-mailbox user sees zero
53
+ * behavior change from before this, and a multi-mailbox user sees every mailbox's folders (and the new
54
+ * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
55
+ * of folders" requirement.
56
+ */
57
+ export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<MailShellProps>): React.JSX.Element;
@@ -90,7 +90,7 @@ function ComposeButton() {
90
90
  * aggregate section) simultaneously, matching the shared-mailbox feature's own "display as a separate set
91
91
  * of folders" requirement.
92
92
  */
93
- export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
93
+ export default function MailShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
94
94
  const [status, setStatus] = useState("checking");
95
95
  const [error, setError] = useState(null);
96
96
  const [mailboxes, setMailboxes] = useState([]);
@@ -219,5 +219,5 @@ export default function MailShell({ userUid, authServerUrl, impersonating, imper
219
219
  canProvision: !impersonating && activeMailbox?.ownerUserUid === userUid, children: [_jsx(LocalIndexLifecycle, { mailboxUid: activeMailboxUid, mailboxKeys: activeMailbox?.keys, folders: mailboxFolders.find((mf) => mf.mailbox.uid === activeMailboxUid)?.folders ?? [],
220
220
  // A full page may be truncated - pruning against it would delete indexes of accessible mailboxes
221
221
  // beyond the limit, so it's only trusted as the complete list when it came back short.
222
- accessibleMailboxUids: status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, children: inner })] }));
222
+ accessibleMailboxUids: status === "ready" && mailboxes.length < MAILBOX_LIST_LIMIT ? mailboxes.map((mb) => mb.uid) : undefined }), _jsx(AppShell, { active: "mail", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner })] }));
223
223
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The trusted signing-key fingerprints for a received message's sender, for `evaluateMessageSecurity()`'s
3
+ * `pinnedSignerFingerprints`: the keys key discovery already pinned on the contacts in the reading mailbox's own
4
+ * contacts folder(s). Reads only stored contacts - never triggers a Discovery lookup, which must not happen on
5
+ * message receipt.
6
+ *
7
+ * `MessageDetailPane` renders once per opened message (and once per message in a thread), so each mailbox's
8
+ * contact list is fetched once and reused for `CONTACTS_CACHE_TTL_MS`; a failed load isn't cached. Any failure
9
+ * leaves the caller with no pins, which `evaluateMessageSecurity()` reports as an unverified signer - never as
10
+ * verified.
11
+ *
12
+ * The cache is dropped (`clearPinnedSignerCache()`) whenever a key session locks (subscribed on first use), on sign-out
13
+ * (`AppShell`), and after the reader adds, edits or deletes contacts (the contacts pages) - so a contact removed
14
+ * because its key was revoked doesn't keep vouching for signatures until the TTL runs out.
15
+ */
16
+ import { SignerKeyState } from "@rapidmx/react-shared/contacts/contactsApi.js";
17
+ /** How long one mailbox's loaded contact list is reused before it is fetched again. */
18
+ export declare const CONTACTS_CACHE_TTL_MS = 60000;
19
+ /** Forgets every cached contact list - on sign-out, a key-session lock, after the reader changes their contacts, and
20
+ * in tests. */
21
+ export declare function clearPinnedSignerCache(): void;
22
+ /** The sender `address`'s pinned signing fingerprints from `mailboxUid`'s contacts. Rejects if they can't be loaded. */
23
+ export declare function getPinnedSignerFingerprints(mailboxUid: string, address: string): Promise<string[]>;
24
+ /** A sender's signing-key state (`signerKeyStateFor()`) plus what a key-changed notice needs to show and link. */
25
+ export interface SenderKeyState extends SignerKeyState {
26
+ /** The contact holding the first pinned signing key, else the one holding the conflict, else the first match. */
27
+ contactUid?: string;
28
+ /** When the first pinned signing key started being trusted - see `keyPinnedSince()`. */
29
+ pinnedSince?: number;
30
+ }
31
+ /** The sender `address`'s signing-key state from `mailboxUid`'s (cached) contacts, for a "signing key changed" comparison
32
+ * or to spot a recorded conflict. Reads stored contacts only. Rejects if they can't be loaded. */
33
+ export declare function getSignerKeyState(mailboxUid: string, address: string): Promise<SenderKeyState>;
@@ -0,0 +1,22 @@
1
+ import { type Message } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ import type { SealToWrite } from "@rapidmx/react-shared/crypto/messageSecurity.js";
3
+ /** How long one mailbox's vault generation is reused before it is read again. */
4
+ export declare const VAULT_GENERATION_TTL_MS = 60000;
5
+ /** Forgets the cached vault generations and this session's seal writes - in tests. */
6
+ export declare function clearVerificationSealCache(): void;
7
+ /** Reads `mailboxUid`'s current vault `masterKeyGeneration`, uncached: `undefined` when the vault can't be read or
8
+ * reports none. Never rejects. */
9
+ export declare function readVaultGeneration(mailboxUid: string): Promise<number | undefined>;
10
+ /** `mailboxUid`'s current vault `masterKeyGeneration`, or `undefined` when it can't be read. Never rejects. */
11
+ export declare function getVaultGeneration(mailboxUid: string): Promise<number | undefined>;
12
+ /** The seal to check `message` against: the one this session stored for it when that is newer than the message's own
13
+ * copy (which may predate the write), else the message's `verificationSeal`/`verificationSealGeneration`. */
14
+ export declare function currentVerificationSeal(message: Pick<Message, "uid" | "verificationSeal" | "verificationSealGeneration">): {
15
+ seal?: string;
16
+ sealGeneration?: number;
17
+ };
18
+ /** Whether `sendVerificationSeal()` would still send a seal for `messageUid` at `generation`. */
19
+ export declare function verificationSealPending(messageUid: string, generation: number): boolean;
20
+ /** Stores `sealToWrite` on `messageUid`, best effort - see this module's doc comment. Resolves `true` when a request was
21
+ * sent, `false` when the write was skipped as already attempted. Never rejects. */
22
+ export declare function sendVerificationSeal(messageUid: string, sealToWrite: SealToWrite): Promise<boolean>;
@@ -0,0 +1,15 @@
1
+ import type { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /** `true` writable, `false` known view-only, `undefined` couldn't tell (treated as writable). */
3
+ export type MailboxWritability = boolean | undefined;
4
+ /** Forgets every cached answer - for tests, and anything that changes the caller's access mid-session. */
5
+ export declare function clearMailboxWritabilityCache(): void;
6
+ /** The already-known answer for `mailbox`, without making a request. */
7
+ export declare function peekMailboxWritability(mailbox: Mailbox, userUid: string | undefined, trusted?: boolean): MailboxWritability;
8
+ /** Whether the caller can create items in `mailbox`. Never rejects. */
9
+ export declare function getMailboxWritability(mailbox: Mailbox, userUid: string | undefined, trusted?: boolean): Promise<MailboxWritability>;
10
+ /** Per-mailbox writability answers for `mailboxes`, filled in as each one arrives (already-known answers are
11
+ * present from the first render). */
12
+ export declare function useMailboxWritability(mailboxes: Mailbox[], userUid: string | undefined, trusted?: boolean): Record<string, MailboxWritability>;
13
+ /** `mailboxes` minus any known to be view-only, always keeping `keepUid` (the picker's current/default value,
14
+ * so the control never shows a value missing from its own options). Lists everything until told otherwise. */
15
+ export declare function useWritableMailboxes(mailboxes: Mailbox[], userUid: string | undefined, keepUid?: string, trusted?: boolean): Mailbox[];
@@ -0,0 +1,70 @@
1
+ import React, { ReactNode } from "react";
2
+ /** One condition field this scope's `Conditions` shape supports — `"list"` renders an add/remove chip
3
+ * editor for a `string[]` field, `"boolean"` a plain checkbox, `"select"` a fixed-choice dropdown for a
4
+ * field with a small closed set of string values (e.g. `MailFilterConditions.importance`). */
5
+ export interface ConditionFieldDef {
6
+ key: string;
7
+ label: string;
8
+ kind: "list" | "boolean" | "select";
9
+ placeholder?: string;
10
+ /** Required when `kind` is `"select"` — the fixed set of values this field may hold, plus an
11
+ * implicit "Any" option that clears the field entirely. */
12
+ options?: {
13
+ value: string;
14
+ label: string;
15
+ }[];
16
+ }
17
+ /** One action type this scope's `Action` union supports — the per-scope registry the shared condition
18
+ * editor/chrome below is deliberately kept out of (transport-rule and mail-filter actions are
19
+ * different enough vocabularies that one component special-casing both would be worse than this). */
20
+ export interface ActionTypeDef<A> {
21
+ value: string;
22
+ label: string;
23
+ /** Builds a fresh default action of this type when picked from "Add action". */
24
+ createDefault: () => A;
25
+ /** Renders this action's own type-specific fields (e.g. header name/value). `null` for an action
26
+ * type with nothing further to configure. */
27
+ render: (action: A, onChange: (next: A) => void) => ReactNode;
28
+ }
29
+ export interface RuleBuilderValue<C extends object, A extends {
30
+ type: string;
31
+ }> {
32
+ enabled: boolean;
33
+ sequence: number;
34
+ stopProcessingRules: boolean;
35
+ conditions: C;
36
+ actions: A[];
37
+ }
38
+ export interface RuleBuilderProps<C extends object, A extends {
39
+ type: string;
40
+ }> {
41
+ value: RuleBuilderValue<C, A>;
42
+ onChange: (next: RuleBuilderValue<C, A>) => void;
43
+ conditionFields: ConditionFieldDef[];
44
+ actionTypes: ActionTypeDef<A>[];
45
+ /** Set by the page once the user has tried to save a rule `hasConditions()` rejects - turns the
46
+ * "add at least one condition" hint into an error. */
47
+ showValidation?: boolean;
48
+ }
49
+ /**
50
+ * Whether `conditions` holds at least one real condition: a list with a non-blank entry, a ticked
51
+ * boolean, or a chosen select value. A rule with none would match *every* message, so the pages that save
52
+ * a `RuleBuilder` value (mail filters, transport rules) must refuse to save unless this returns `true`.
53
+ */
54
+ export declare function hasConditions(conditions: object): boolean;
55
+ /**
56
+ * Shared condition-row editor and enabled/sequence/stop-processing chrome for a rule that matches
57
+ * conditions and runs actions in order — `TransportRule` (admin, org-wide) and `MailFilterRule`
58
+ * (mailbox-scoped) both have this same `{enabled, sequence, stopProcessingRules, conditions, actions}`
59
+ * shape. Conditions are driven by a declarative `conditionFields` list so both scopes share this one
60
+ * editor despite their `Conditions` types not being identical — `MailFilterConditions.importance`
61
+ * (`MailFilterRule`'s one condition field `TransportRuleConditions` has no equivalent of) is what forced
62
+ * this list beyond `"list"`/`"boolean"` to add a third `"select"` kind for a small fixed-choice field,
63
+ * confirming the shared editor still generalizes rather than needing a fork; actions are driven by a
64
+ * small per-scope `actionTypes` registry instead, since the two action vocabularies (reject/quarantine/
65
+ * add-header/add-recipient vs. move/copy/delete/mark-read/forward) are different enough that one
66
+ * component special-casing both would be worse than this split.
67
+ */
68
+ export default function RuleBuilder<C extends object, A extends {
69
+ type: string;
70
+ }>({ value, onChange, conditionFields, actionTypes, showValidation, }: RuleBuilderProps<C, A>): React.JSX.Element;
@@ -0,0 +1,41 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { AppShellProps } from "../../layout/AppShell.js";
4
+ import { PluginNav } from "../../../plugins/pluginNav.js";
5
+ export interface SettingsSectionDef {
6
+ id: string;
7
+ href: string;
8
+ label: string;
9
+ }
10
+ /** Every settings section with its own sidebar entry. */
11
+ export declare const SETTINGS_SECTIONS: SettingsSectionDef[];
12
+ /** A `SETTINGS_SECTIONS` id, or a plugin's `settingsSections` item id (see `PluginNav`). */
13
+ export type SettingsSectionId = string;
14
+ /** `SETTINGS_SECTIONS` followed by the plugins' `settingsSections` items, core ids winning - see
15
+ * `mergePluginNavItems`. */
16
+ export declare function settingsSections(pluginNav?: PluginNav): SettingsSectionDef[];
17
+ export interface SettingsShellProps extends Omit<AppShellProps, "active"> {
18
+ /** Which entry in `SETTINGS_SECTIONS` (or plugin `settingsSections` item) is highlighted in this shell's own sidebar — distinct from
19
+ * `AppShell`'s own `active` (always hardcoded to `"settings"` below), since Settings isn't a rail
20
+ * icon (see `AppShell.tsx`'s `AppShellApp` doc comment) but still needs its own section
21
+ * highlighting. Each settings page passes its own section id explicitly, the same "gotcha" this
22
+ * plan already calls out for every shell's `active` prop. */
23
+ active: SettingsSectionId;
24
+ }
25
+ export interface SettingsShellContextValue {
26
+ /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox) —
27
+ * every settings section so far (Automatic Replies) edits mailbox-scoped fields. */
28
+ mailboxUid?: string;
29
+ mailboxes: Mailbox[];
30
+ }
31
+ /** Reads the mailbox Settings is currently scoped to, as resolved by the enclosing `SettingsShell`. */
32
+ export declare function useSettingsShell(): SettingsShellContextValue;
33
+ /**
34
+ * The Settings area's shell — reached via a `UserMenu` entry rather than a 5th `AppShell` icon (per
35
+ * this plan's own confirmed decision), so it always passes `active="settings"` to `AppShell` regardless
36
+ * of which section is showing. Unlike `ContactsShell`/`TasksShell` (whose sidebar is just a mailbox
37
+ * switcher, since each has exactly one well-known folder), this shell's sidebar is a real list of
38
+ * settings sections (`SETTINGS_SECTIONS`) — full-page navigation between them, same as the top-level
39
+ * app rail, since this framework has no client-side router.
40
+ */
41
+ export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<SettingsShellProps>): React.JSX.Element;
@@ -12,6 +12,7 @@ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
12
12
  import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
13
13
  import AppShell from "../../layout/AppShell.js";
14
14
  import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
15
+ import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
15
16
  /** Every settings section with its own sidebar entry. */
16
17
  export const SETTINGS_SECTIONS = [
17
18
  { id: "auto-reply", href: "/settings/auto-reply", label: "Automatic Replies" },
@@ -20,11 +21,19 @@ export const SETTINGS_SECTIONS = [
20
21
  { id: "labels", href: "/settings/labels", label: "Labels" },
21
22
  { id: "focused-inbox", href: "/settings/focused-inbox", label: "Focused Inbox" },
22
23
  { id: "read-receipts", href: "/settings/read-receipts", label: "Read Receipts" },
23
- { id: "booking-types", href: "/settings/booking-types", label: "Booking Links" },
24
24
  { id: "encryption", href: "/settings/encryption", label: "Encryption" },
25
25
  { id: "sharing", href: "/settings/sharing", label: "Sharing" },
26
26
  { id: "privacy", href: "/settings/privacy", label: "Privacy & Data" },
27
27
  ];
28
+ /** `SETTINGS_SECTIONS` followed by the plugins' `settingsSections` items, core ids winning - see
29
+ * `mergePluginNavItems`. */
30
+ export function settingsSections(pluginNav) {
31
+ return mergePluginNavItems(SETTINGS_SECTIONS, pluginNav?.settingsSections, ({ id, href, label }) => ({
32
+ id,
33
+ href,
34
+ label,
35
+ }));
36
+ }
28
37
  const SettingsShellContext = createContext({ mailboxes: [] });
29
38
  /** Reads the mailbox Settings is currently scoped to, as resolved by the enclosing `SettingsShell`. */
30
39
  export function useSettingsShell() {
@@ -38,7 +47,7 @@ export function useSettingsShell() {
38
47
  * settings sections (`SETTINGS_SECTIONS`) — full-page navigation between them, same as the top-level
39
48
  * app rail, since this framework has no client-side router.
40
49
  */
41
- export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
50
+ export default function SettingsShell({ active, userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
42
51
  const [status, setStatus] = useState("checking");
43
52
  const [error, setError] = useState(null);
44
53
  const [mailboxes, setMailboxes] = useState([]);
@@ -77,16 +86,17 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
77
86
  inner = (_jsx("div", { className: "flex-1 flex items-center justify-center p-8", children: _jsx("div", { className: "w-full max-w-md", children: _jsx(Alert, { children: error }) }) }));
78
87
  }
79
88
  else if (userUid && status === "ready") {
89
+ const sections = settingsSections(pluginNav);
80
90
  // A function, not a plain JSX constant — same reasoning as ContactsShell's identical comment:
81
91
  // rendered twice (desktop `<aside>` + mobile `Drawer`), possibly simultaneously mounted, so the
82
92
  // `<select>`'s `id`/its `<label>`'s `htmlFor` need a distinct value per instance.
83
93
  const sidebarContent = (idPrefix) => (_jsxs(_Fragment, { children: [mailboxes.length > 1 && (_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-bold uppercase tracking-wide text-text-muted mb-1", htmlFor: `${idPrefix}-settings-mailbox-switcher`, children: "Mailbox" }), _jsx("select", { id: `${idPrefix}-settings-mailbox-switcher`, className: "w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: mailboxUid, onChange: (e) => {
84
- // `active` always names a real entry in `SETTINGS_SECTIONS` — every settings
85
- // page passes its own section id explicitly (see `SettingsShellProps.active`'s
86
- // own doc comment), so this lookup can't miss.
87
- const section = SETTINGS_SECTIONS.find((s) => s.id === active);
88
- window.location.href = `${section.href}?mailboxUid=${encodeURIComponent(e.target.value)}`;
89
- }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children: SETTINGS_SECTIONS.map((section) => (_jsx("a", {
94
+ // Every core settings page passes its own section id (see
95
+ // `SettingsShellProps.active`), but a plugin page's section is missing when the
96
+ // server didn't send its nav item, so that falls back to the current path.
97
+ const href = sections.find((s) => s.id === active)?.href ?? window.location.pathname;
98
+ window.location.href = `${href}?mailboxUid=${encodeURIComponent(e.target.value)}`;
99
+ }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), _jsx("nav", { "aria-label": "Settings sections", className: "flex flex-col gap-0.5", children: sections.map((section) => (_jsx("a", {
90
100
  // `inner` (this whole branch) is only ever computed once `status === "ready"`
91
101
  // and `mailboxUid` has already resolved truthy — a falsy `mailboxUid` returns
92
102
  // `<MailboxProvisioning />` above instead (and TypeScript's own narrowing of
@@ -100,5 +110,5 @@ export default function SettingsShell({ active, userUid, authServerUrl, imperson
100
110
  ].join(" "), children: section.label }, section.id))) })] }));
101
111
  inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Settings", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [_jsx("button", { type: "button", className: "md:hidden m-3 self-start w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open settings menu", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx(SettingsShellContext.Provider, { value: contextValue, children: children })] })] }));
102
112
  }
103
- return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, children: inner }));
113
+ return (_jsx(AppShell, { active: "settings", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
104
114
  }
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import { Task } from "@rapidmx/react-shared/tasks/tasksApi.js";
3
+ export type TasksView = {
4
+ type: "myDay";
5
+ } | {
6
+ type: "important";
7
+ } | {
8
+ type: "planned";
9
+ } | {
10
+ type: "assignedToMe";
11
+ } | {
12
+ type: "flagged";
13
+ } | {
14
+ type: "all";
15
+ } | {
16
+ type: "list";
17
+ uid: string;
18
+ name: string;
19
+ };
20
+ export declare function tasksViewKey(view: TasksView): string;
21
+ export interface TasksSidebarProps {
22
+ mailboxUid?: string;
23
+ /** The caller's own (incomplete) tasks — used to compute every smart-filter's count. Deliberately not
24
+ * fetched again here; the parent already has this loaded for the main list. */
25
+ tasks: Task[];
26
+ userUid?: string;
27
+ active: TasksView;
28
+ onSelect: (view: TasksView) => void;
29
+ /** Bumped by the parent to force a contact-list-style re-fetch of task lists (mirrors
30
+ * `ContactsSidebar`'s identical prop) — not currently used by any caller, kept for parity/future use. */
31
+ refreshToken?: number;
32
+ }
33
+ /**
34
+ * Tasks' Outlook To-Do-style left sidebar. "My Day"/"Assigned to me" are real persisted fields
35
+ * (`Task.myDay`/`assignedTo`, from Phase 0); "Important"/"Planned" are pure smart-filters derived from
36
+ * already-loaded fields (`priority === "high"`/`dueDate != null`) with no backend concept of their own;
37
+ * "Flagged email" is a `Message` smart-filter (see `flaggedMessages.ts`), not a `Task` one at all — the
38
+ * page renders it as a distinct list, not the task table. "Tasks" is the flat, unfiltered default. Custom
39
+ * lists are real `TaskList` records, fetched here (same pattern as `ContactsSidebar`'s contact lists).
40
+ */
41
+ export default function TasksSidebar({ mailboxUid, tasks, userUid, active, onSelect, refreshToken }: TasksSidebarProps): React.JSX.Element;
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ export type TasksViewMode = "list" | "grid";
3
+ export interface TasksToolbarProps {
4
+ viewMode: TasksViewMode;
5
+ onViewModeChange: (mode: TasksViewMode) => void;
6
+ selectedCount: number;
7
+ onComplete: () => void;
8
+ onDelete: () => void;
9
+ onAddToMyDay: () => void;
10
+ }
11
+ /** Tasks' toolbar: a Grid/List view-mode toggle (local, not persisted) plus bulk actions for whatever
12
+ * rows are checked (Complete, Add to My Day, Delete) — deliberately presentational, matching
13
+ * `ContactsToolbar`'s convention, since the page owns the actual task data/selection. */
14
+ export default function TasksToolbar({ viewMode, onViewModeChange, selectedCount, onComplete, onDelete, onAddToMyDay }: TasksToolbarProps): React.JSX.Element;
@@ -0,0 +1,24 @@
1
+ import React, { PropsWithChildren } from "react";
2
+ import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ import { AppShellProps } from "../../layout/AppShell.js";
4
+ export type TasksShellProps = Omit<AppShellProps, "active">;
5
+ export interface TasksShellContextValue {
6
+ /** The mailbox currently selected (`?mailboxUid=`, or the caller's first accessible mailbox). */
7
+ mailboxUid?: string;
8
+ /** The selected mailbox's single `tasks`-type folder. */
9
+ folderUid?: string;
10
+ mailboxes: Mailbox[];
11
+ /** The caller's own uid — exposed here (rather than only as `AppShell`'s prop) so the page content
12
+ * can compute "assigned to me" without needing it threaded through separately. */
13
+ userUid?: string;
14
+ }
15
+ /** Reads the mailbox/folder Tasks is currently showing, as resolved by the enclosing `TasksShell`. */
16
+ export declare function useTasksShell(): TasksShellContextValue;
17
+ /**
18
+ * The Tasks app's shell — structurally identical to `ContactsShell` (a mailbox has exactly one
19
+ * well-known `tasks` folder, guaranteed to exist by `BaseMailboxRoute.create()`'s eager provisioning
20
+ * in `@rapidmx/restapi`), just bound to a different folder type. Kept as its own small component
21
+ * (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
22
+ * grow its own app-specific controls later without threading extra props through a shared one.
23
+ */
24
+ export default function TasksShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }: PropsWithChildren<TasksShellProps>): React.JSX.Element;
@@ -24,7 +24,7 @@ export function useTasksShell() {
24
24
  * (rather than a single generic "SingleFolderShell" parameterized by type) so each app's sidebar can
25
25
  * grow its own app-specific controls later without threading extra props through a shared one.
26
26
  */
27
- export default function TasksShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, children, }) {
27
+ export default function TasksShell({ userUid, authServerUrl, impersonating, impersonationBaseUrl, trusted, pluginNav, children, }) {
28
28
  const [status, setStatus] = useState("checking");
29
29
  const [error, setError] = useState(null);
30
30
  const [mailboxes, setMailboxes] = useState([]);
@@ -88,5 +88,5 @@ export default function TasksShell({ userUid, authServerUrl, impersonating, impe
88
88
  }, children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid ? "" : " (shared)"] }, mb.uid))) })] })), folderError && _jsx(Alert, { children: folderError })] }));
89
89
  inner = (_jsxs(_Fragment, { children: [_jsx("aside", { className: "hidden md:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3", children: sidebarContent("desktop") }), _jsx(Drawer, { open: drawerOpen, onClose: () => setDrawerOpen(false), title: "Mailbox", children: _jsx("div", { className: "flex flex-col gap-3", children: sidebarContent("mobile") }) }), _jsxs("main", { className: "flex-1 min-w-0 flex flex-col", children: [(mailboxes.length > 1 || folderError) && (_jsx("button", { type: "button", className: "md:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open mailbox switcher", onClick: () => setDrawerOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) })), _jsx(TasksShellContext.Provider, { value: contextValue, children: children })] })] }));
90
90
  }
91
- return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, children: inner }));
91
+ return (_jsx(AppShell, { active: "tasks", userUid: userUid, authServerUrl: authServerUrl, impersonating: impersonating, impersonationBaseUrl: impersonationBaseUrl, trusted: trusted, pluginNav: pluginNav, children: inner }));
92
92
  }
@@ -0,0 +1,5 @@
1
+ import { FolderType } from "@rapidmx/react-shared/mail/mailApi.js";
2
+ /** The uid of a mailbox's well-known folder of `type` (e.g. its Contacts or Tasks folder), for creating an
3
+ * item in a mailbox other than the one an app currently has loaded. Resolves `undefined` if the mailbox has
4
+ * no such folder; rejects if the folder list can't be fetched. */
5
+ export declare function findWellKnownFolderUid(mailboxUid: string, type: FolderType): Promise<string | undefined>;
@@ -0,0 +1,17 @@
1
+ /** Page size the list pages (Contacts, Tasks) request per call - restapi caps `limit` at 500. */
2
+ export declare const LIST_PAGE_SIZE = 500;
3
+ /** Safety cap on pages fetched - stops a server that ignores `page` (and keeps returning a full first
4
+ * page) from looping forever. 40 pages x 500 = 20,000 items. */
5
+ export declare const MAX_LIST_PAGES = 40;
6
+ export interface ListAllPagesResult<T> {
7
+ items: T[];
8
+ /** `true` when fetching stopped at `maxPages` with the last page still full - there may be more items
9
+ * the caller isn't showing, so it should say so rather than presenting `items` as the whole list. */
10
+ truncated: boolean;
11
+ }
12
+ /**
13
+ * Fetches every page of a zero-based `limit`/`page` list endpoint, stopping at the first short page
14
+ * (fewer than `pageSize` items) - so a folder with more items than one page holds is shown in full
15
+ * rather than silently cut off at the first page. Stops at `maxPages` regardless, reporting `truncated`.
16
+ */
17
+ export declare function listAllPages<T>(fetchPage: (page: number) => Promise<T[]>, pageSize?: number, maxPages?: number): Promise<ListAllPagesResult<T>>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * One navigation entry a plugin contributes through its manifest's `ui` field. Structurally identical to
3
+ * `@rapidmx/restapi`'s `PluginUiNavItem`, defined locally so this package doesn't depend on restapi.
4
+ */
5
+ export interface PluginUiNavItem {
6
+ /** Unique within its list. A plugin page passes this as its shell's `active` to highlight the entry. */
7
+ id: string;
8
+ label: string;
9
+ /** A same-origin path under the host's prefix (`/settings/...`, `/admin/...`, `/...`). */
10
+ href: string;
11
+ }
12
+ /**
13
+ * The navigation every enabled plugin with a successfully built UI contributes, supplied by the server's
14
+ * `WwwRoute`/`AdminConsoleRoute` `fetchProps` as the `pluginNav` page prop.
15
+ */
16
+ export interface PluginNav {
17
+ /** Appended to `SettingsShell`'s section list. */
18
+ settingsSections?: PluginUiNavItem[];
19
+ /** Appended to `AdminShell`'s icon rail and mobile tab bar. */
20
+ adminNav?: PluginUiNavItem[];
21
+ /** Appended to `AppShell`'s app rail and mobile tab bar. */
22
+ appRail?: PluginUiNavItem[];
23
+ }
24
+ /** The page prop every www and admin page receives - extended by `AppShellProps` and `AdminShellProps`, so
25
+ * a page that spreads its props onto its shell passes `pluginNav` through with no code of its own. */
26
+ export interface PluginNavProps {
27
+ pluginNav?: PluginNav;
28
+ }
29
+ /** A same-origin absolute path: starts with `/`, but not `//` or `/\` (both of which browsers treat as
30
+ * protocol-relative URLs to another host). */
31
+ export declare function isSafePluginHref(href: string): boolean;
32
+ /**
33
+ * Appends a plugin's nav items after the core ones. Skips any item that isn't well-formed, whose `href`
34
+ * isn't a same-origin path (see `isSafePluginHref`), or whose id is already taken - by a core item or
35
+ * `reservedIds` (core ids always win) or by an earlier plugin item.
36
+ */
37
+ export declare function mergePluginNavItems<T extends {
38
+ id: string;
39
+ }>(coreItems: T[], pluginItems: PluginUiNavItem[] | undefined, toItem: (item: PluginUiNavItem) => T, reservedIds?: string[]): T[];