@rapidmx/web-client 0.4.0 → 0.5.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 (169) hide show
  1. package/apps/admin/audit-log/index.tsx +35 -8
  2. package/apps/admin/data-requests/index.tsx +480 -485
  3. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  4. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  5. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  6. package/apps/admin/index.tsx +19 -1
  7. package/apps/admin/ingest-queue/index.tsx +44 -5
  8. package/apps/admin/mailboxes/[uid].tsx +211 -184
  9. package/apps/admin/quarantine/index.tsx +46 -14
  10. package/apps/admin/transport-rules/[uid].tsx +30 -1
  11. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  12. package/apps/admin/transport-rules/index.tsx +24 -6
  13. package/apps/admin/transport-rules/new/index.tsx +30 -1
  14. package/apps/escrow/_layout.tsx +2 -3
  15. package/apps/escrow/audit-log/index.tsx +196 -168
  16. package/apps/escrow/matters/[uid].tsx +621 -521
  17. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  18. package/apps/shared/components/admin/layout/AdminShell.tsx +3 -1
  19. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  20. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  21. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  22. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  23. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  24. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  25. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  26. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  27. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  28. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  29. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  30. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  31. package/apps/shared/components/admin/signOut.ts +79 -0
  32. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  33. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  34. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  35. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  36. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  37. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  38. package/apps/shared/components/calendar/allDay.ts +124 -0
  39. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  40. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  41. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  42. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  43. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  44. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  45. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  46. package/apps/shared/components/layout/AppShell.tsx +87 -15
  47. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  48. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  49. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  50. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  51. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  52. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  53. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  54. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  55. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  56. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  57. package/apps/shared/components/mail/layout/MailShell.tsx +12 -2
  58. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  59. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  60. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  61. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  62. package/apps/shared/mail/listAllPages.ts +39 -0
  63. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  64. package/apps/shared/search/localIndexBuilder.ts +481 -179
  65. package/apps/shared/search/localIndexRpcClient.ts +228 -28
  66. package/apps/shared/search/localIndexSchema.ts +15 -6
  67. package/apps/shared/search/localIndexStorage.ts +98 -0
  68. package/apps/shared/search/localIndexVFS.ts +32 -1
  69. package/apps/shared/search/localIndexWorker.ts +648 -186
  70. package/apps/www/calendar/index.tsx +438 -417
  71. package/apps/www/contacts/[uid].tsx +21 -2
  72. package/apps/www/contacts/index.tsx +116 -12
  73. package/apps/www/index.tsx +1314 -1041
  74. package/apps/www/messages/[uid].tsx +18 -6
  75. package/apps/www/settings/auto-reply/index.tsx +134 -129
  76. package/apps/www/settings/booking-types/[uid].tsx +306 -300
  77. package/apps/www/settings/encryption/index.tsx +739 -216
  78. package/apps/www/settings/filters/[uid].tsx +171 -155
  79. package/apps/www/settings/filters/new/index.tsx +8 -1
  80. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  81. package/apps/www/settings/privacy/index.tsx +494 -436
  82. package/apps/www/settings/read-receipts/index.tsx +148 -125
  83. package/apps/www/settings/sharing/index.tsx +17 -14
  84. package/apps/www/tasks/index.tsx +638 -579
  85. package/dist/apps/admin/audit-log/index.js +38 -8
  86. package/dist/apps/admin/data-requests/index.js +52 -72
  87. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  88. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  89. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  90. package/dist/apps/admin/index.js +4 -1
  91. package/dist/apps/admin/ingest-queue/index.js +24 -6
  92. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  93. package/dist/apps/admin/quarantine/index.js +20 -11
  94. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  95. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  96. package/dist/apps/admin/transport-rules/index.js +22 -6
  97. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  98. package/dist/apps/escrow/_layout.js +3 -3
  99. package/dist/apps/escrow/audit-log/index.js +26 -1
  100. package/dist/apps/escrow/matters/[uid].js +77 -42
  101. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  102. package/dist/apps/shared/components/admin/layout/AdminShell.js +3 -1
  103. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  104. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  105. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  106. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  107. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  108. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  109. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  110. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  111. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  112. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  113. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  114. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  115. package/dist/apps/shared/components/admin/signOut.js +75 -0
  116. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  117. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  118. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  119. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  120. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  121. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  122. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  123. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  124. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  125. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  126. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  127. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  128. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  129. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  130. package/dist/apps/shared/components/layout/AppShell.js +83 -14
  131. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  132. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  133. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  134. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  135. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  136. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  137. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  138. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  139. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  140. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  141. package/dist/apps/shared/components/mail/layout/MailShell.js +12 -3
  142. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  143. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  144. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  145. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  146. package/dist/apps/shared/mail/listAllPages.js +25 -0
  147. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  148. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  149. package/dist/apps/shared/search/localIndexRpcClient.js +188 -25
  150. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  151. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  152. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  153. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  154. package/dist/apps/www/calendar/index.js +28 -10
  155. package/dist/apps/www/contacts/[uid].js +13 -2
  156. package/dist/apps/www/contacts/index.js +80 -10
  157. package/dist/apps/www/index.js +339 -108
  158. package/dist/apps/www/messages/[uid].js +18 -6
  159. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  160. package/dist/apps/www/settings/booking-types/[uid].js +17 -12
  161. package/dist/apps/www/settings/encryption/index.js +514 -97
  162. package/dist/apps/www/settings/filters/[uid].js +22 -7
  163. package/dist/apps/www/settings/filters/new/index.js +8 -2
  164. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  165. package/dist/apps/www/settings/privacy/index.js +57 -22
  166. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  167. package/dist/apps/www/settings/sharing/index.js +7 -11
  168. package/dist/apps/www/tasks/index.js +50 -10
  169. package/package.json +2 -2
@@ -8,7 +8,7 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
9
9
  import { getMailFilterRule, updateMailFilterRule, } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
10
10
  import SettingsShell, { useSettingsShell } from "../../../shared/components/settings/layout/SettingsShell.js";
11
- import RuleBuilder from "../../../shared/components/rules/RuleBuilder.js";
11
+ import RuleBuilder, { hasConditions } from "../../../shared/components/rules/RuleBuilder.js";
12
12
  import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "./_mailFilterRuleConfig.js";
13
13
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
14
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
@@ -27,11 +27,17 @@ function MailFilterDetailContent({ uid }) {
27
27
  const [error, setError] = useState(null);
28
28
  const [saving, setSaving] = useState(false);
29
29
  const [saved, setSaved] = useState(false);
30
+ const [showValidation, setShowValidation] = useState(false);
31
+ // The folder picker lists the *rule's own* mailbox's folders, not the switcher's - a rule opened by
32
+ // direct link (or after switching mailboxes) can belong to a different mailbox than `mailboxUid`,
33
+ // and a move-to-folder action must only ever offer folders the rule can actually target.
34
+ const ruleMailboxUid = original?.mailboxUid;
30
35
  useEffect(() => {
31
- // `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same
32
- // established non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
33
- listFolders(mailboxUid).then(setFolders).catch(() => setFolders([]));
34
- }, [mailboxUid]);
36
+ if (!ruleMailboxUid) {
37
+ return;
38
+ }
39
+ listFolders(ruleMailboxUid).then(setFolders).catch(() => setFolders([]));
40
+ }, [ruleMailboxUid]);
35
41
  useEffect(() => {
36
42
  setLoading(true);
37
43
  setError(null);
@@ -62,6 +68,11 @@ function MailFilterDetailContent({ uid }) {
62
68
  setError("A name is required.");
63
69
  return;
64
70
  }
71
+ if (!hasConditions(rule.conditions)) {
72
+ setShowValidation(true);
73
+ setError("Add at least one condition. A filter without conditions would apply to every message.");
74
+ return;
75
+ }
65
76
  setSaving(true);
66
77
  setSaved(false);
67
78
  try {
@@ -81,7 +92,7 @@ function MailFilterDetailContent({ uid }) {
81
92
  setSaving(false);
82
93
  }
83
94
  }
84
- if (loading || folders === null) {
95
+ if (loading) {
85
96
  return _jsx("p", { className: "p-6 text-sm text-text-muted", children: "Loading\u2026" });
86
97
  }
87
98
  if (error && !original) {
@@ -90,5 +101,9 @@ function MailFilterDetailContent({ uid }) {
90
101
  if (!original || !rule) {
91
102
  return (_jsx("div", { className: "p-6", children: _jsx(Alert, { children: "Mail filter not found." }) }));
92
103
  }
93
- return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("a", { href: `/settings/filters?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 All mail filters" }), _jsx("h1", { className: "text-lg font-bold tracking-tight mt-1 mb-5", children: original.name }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }) }), _jsx(RuleBuilder, { value: rule, onChange: setRule, conditionFields: MAIL_FILTER_CONDITION_FIELDS, actionTypes: buildMailFilterActionTypes(folders) }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] })] }) }));
104
+ // The folder list is fetched for the rule's own mailbox, so it can only start once the rule has loaded.
105
+ if (folders === null) {
106
+ return _jsx("p", { className: "p-6 text-sm text-text-muted", children: "Loading\u2026" });
107
+ }
108
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("a", { href: `/settings/filters?mailboxUid=${encodeURIComponent(mailboxUid)}`, className: "text-sm text-primary-dark hover:underline", children: "\u2190 All mail filters" }), _jsx("h1", { className: "text-lg font-bold tracking-tight mt-1 mb-5", children: original.name }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value) }) }) }), _jsx(RuleBuilder, { value: rule, onChange: setRule, conditionFields: MAIL_FILTER_CONDITION_FIELDS, actionTypes: buildMailFilterActionTypes(folders), showValidation: showValidation }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save changes" }) })] })] }) }));
94
109
  }
@@ -8,7 +8,7 @@ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
9
9
  import { createMailFilterRule, } from "@rapidmx/react-shared/mail/mailFilterRulesApi.js";
10
10
  import SettingsShell, { useSettingsShell } from "../../../../shared/components/settings/layout/SettingsShell.js";
11
- import RuleBuilder from "../../../../shared/components/rules/RuleBuilder.js";
11
+ import RuleBuilder, { hasConditions } from "../../../../shared/components/rules/RuleBuilder.js";
12
12
  import { MAIL_FILTER_CONDITION_FIELDS, buildMailFilterActionTypes } from "../_mailFilterRuleConfig.js";
13
13
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
14
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
@@ -31,6 +31,7 @@ function NewMailFilterForm() {
31
31
  });
32
32
  const [error, setError] = useState(null);
33
33
  const [saving, setSaving] = useState(false);
34
+ const [showValidation, setShowValidation] = useState(false);
34
35
  // `SettingsShell` only ever renders its children once `mailboxUid` has resolved — same established
35
36
  // non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
36
37
  useEffect(() => {
@@ -45,6 +46,11 @@ function NewMailFilterForm() {
45
46
  setError("A name is required.");
46
47
  return;
47
48
  }
49
+ if (!hasConditions(rule.conditions)) {
50
+ setShowValidation(true);
51
+ setError("Add at least one condition. A filter without conditions would apply to every message.");
52
+ return;
53
+ }
48
54
  setSaving(true);
49
55
  try {
50
56
  const created = await createMailFilterRule({ mailboxUid: mailboxUid, name: name.trim(), ...rule });
@@ -57,5 +63,5 @@ function NewMailFilterForm() {
57
63
  setSaving(false);
58
64
  }
59
65
  }
60
- return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "New mail filter" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Applied in order once a message is verdicted safe to deliver, before it reaches your Inbox." }), error && _jsx(Alert, { children: error }), folderError && _jsx(Alert, { children: folderError }), folders === null ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "File newsletters" }) }) }), _jsx(RuleBuilder, { value: rule, onChange: setRule, conditionFields: MAIL_FILTER_CONDITION_FIELDS, actionTypes: buildMailFilterActionTypes(folders) }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create filter" }), _jsx("a", { href: `/settings/filters?mailboxUid=${encodeURIComponent(mailboxUid)}`, children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", children: "Cancel" }) })] })] }))] }) }));
66
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-3xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "New mail filter" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Applied in order once a message is verdicted safe to deliver, before it reaches your Inbox." }), error && _jsx(Alert, { children: error }), folderError && _jsx(Alert, { children: folderError }), folders === null ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsx(FormField, { label: "Name", htmlFor: "name", children: _jsx("input", { id: "name", type: "text", className: INPUT_CLASS, value: name, onChange: (e) => setName(e.target.value), placeholder: "File newsletters" }) }) }), _jsx(RuleBuilder, { value: rule, onChange: setRule, conditionFields: MAIL_FILTER_CONDITION_FIELDS, actionTypes: buildMailFilterActionTypes(folders), showValidation: showValidation }), _jsxs("div", { className: "flex gap-3", children: [_jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Create filter" }), _jsx("a", { href: `/settings/filters?mailboxUid=${encodeURIComponent(mailboxUid)}`, children: _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", children: "Cancel" }) })] })] }))] }) }));
61
67
  }
@@ -33,7 +33,9 @@ function FocusedInboxContent() {
33
33
  useEffect(reload, [mailboxUid]);
34
34
  async function handleAdd(e) {
35
35
  e.preventDefault();
36
- const senderAddress = newSender.trim();
36
+ // Lowercased to match restapi's own `normalizeAddress()` - what gets sent is exactly what gets
37
+ // stored and matched against, rather than relying on the server to silently rewrite it.
38
+ const senderAddress = newSender.trim().toLowerCase();
37
39
  if (!senderAddress) {
38
40
  return;
39
41
  }
@@ -17,21 +17,52 @@ const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]
17
17
  function importFormatFromFilename(filename) {
18
18
  return filename.toLowerCase().endsWith(".pst") ? "pst" : "mbox";
19
19
  }
20
+ /** Request statuses still waiting or running - shown as a neutral in-progress badge, never as success. Includes
21
+ * restapi's `processing` (exports/imports) and `in_progress` (an erasure that is running). */
22
+ const IN_PROGRESS_STATUSES = new Set(["pending", "processing", "in_progress"]);
20
23
  function statusBadgeClass(status) {
21
24
  if (status === "failed" || status === "denied")
22
25
  return "bg-danger-bg text-danger";
23
- if (status === "pending" || status === "processing")
26
+ if (IN_PROGRESS_STATUSES.has(status))
24
27
  return "bg-surface-alt text-text-muted";
25
28
  return "bg-success text-white";
26
29
  }
30
+ /** A status as shown on its badge (`in_progress` -> `in progress`). */
31
+ function statusLabel(status) {
32
+ return status.replace(/_/g, " ");
33
+ }
27
34
  export default function SettingsPrivacyPage(props) {
28
- return (_jsx(SettingsShell, { ...props, active: "privacy", children: _jsx(PrivacyContent, {}) }));
35
+ return (_jsx(SettingsShell, { ...props, active: "privacy", children: _jsx(PrivacyContent, { userUid: props.userUid }) }));
36
+ }
37
+ function PrivacyContent({ userUid }) {
38
+ const { mailboxUid, mailboxes } = useSettingsShell();
39
+ // Data export, account erasure, and (for a non-admin) mail import all act on the *caller's own*
40
+ // mailbox server-side - none of these requests carries a mailbox a regular user may choose, so
41
+ // restapi resolves "own mailbox" itself with an unordered lookup by owner. That is only unambiguous
42
+ // when the caller owns exactly one mailbox, so all three are only offered while the switcher is on
43
+ // that single owned mailbox, and name it explicitly. (Import on any other mailbox would always be
44
+ // rejected: its destination folder wouldn't belong to the mailbox the server resolved.)
45
+ const selected = mailboxes.find((mb) => mb.uid === mailboxUid);
46
+ const ownedMailboxes = mailboxes.filter((mb) => mb.ownerUserUid !== undefined && mb.ownerUserUid === userUid);
47
+ const ownMailbox = ownedMailboxes.length === 1 ? ownedMailboxes[0] : undefined;
48
+ const selectedIsOwn = selected.uid === ownMailbox?.uid;
49
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Privacy & Data" }), _jsx("p", { className: "text-sm text-text-muted", children: "Export or manage the data associated with this mailbox." })] }), selectedIsOwn ? (_jsxs(_Fragment, { children: [_jsx(ExportSection, { mailboxUid: mailboxUid, mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` }), _jsx(ImportSection, { mailboxUid: mailboxUid }), _jsx(ErasureSection, { mailboxLabel: `${selected.displayName} (${selected.primarySmtpAddress})` })] })) : (_jsx(OwnMailboxOnlyNotice, { ownedMailboxCount: ownedMailboxes.length, ownMailboxLabel: ownMailbox && `${ownMailbox.displayName} (${ownMailbox.primarySmtpAddress})` }))] }) }));
29
50
  }
30
- function PrivacyContent() {
31
- const { mailboxUid } = useSettingsShell();
32
- return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Privacy & Data" }), _jsx("p", { className: "text-sm text-text-muted", children: "Export or manage the data associated with this mailbox." })] }), _jsx(ExportSection, { mailboxUid: mailboxUid }), _jsx(ImportSection, { mailboxUid: mailboxUid }), _jsx(ErasureSection, {})] }) }));
51
+ function OwnMailboxOnlyNotice({ ownedMailboxCount, ownMailboxLabel }) {
52
+ let detail;
53
+ if (ownMailboxLabel) {
54
+ detail = `Switch to ${ownMailboxLabel} to manage them.`;
55
+ }
56
+ else if (ownedMailboxCount > 1) {
57
+ detail =
58
+ "You own more than one mailbox, so which one these would apply to can't be determined here. Ask an administrator to export, import, or erase data for a specific mailbox.";
59
+ }
60
+ else {
61
+ detail = "You don't have a mailbox of your own to manage here.";
62
+ }
63
+ return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export, import, or delete my data" }), _jsxs("p", { className: "text-xs text-text-muted", children: ["Exporting your data, importing mail, and deleting your account only apply to your own mailbox, not one shared with you. ", detail] })] }));
33
64
  }
34
- function ExportSection({ mailboxUid }) {
65
+ function ExportSection({ mailboxUid, mailboxLabel }) {
35
66
  const [requests, setRequests] = useState([]);
36
67
  const [loading, setLoading] = useState(true);
37
68
  const [loadError, setLoadError] = useState(null);
@@ -46,17 +77,18 @@ function ExportSection({ mailboxUid }) {
46
77
  const seq = ++loadSeq.current;
47
78
  return listExportRequests()
48
79
  .then((data) => {
49
- if (seq === loadSeq.current)
80
+ if (seq === loadSeq.current) {
50
81
  setRequests(data);
82
+ }
51
83
  })
52
84
  .catch((err) => {
53
- if (seq === loadSeq.current)
85
+ if (seq === loadSeq.current) {
54
86
  setLoadError(err instanceof ApiRequestError ? err.message : "Could not load your export requests.");
87
+ }
55
88
  });
56
89
  }
57
90
  useEffect(() => {
58
- loadRequests().finally(() => setLoading(false));
59
- // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ void loadRequests().finally(() => setLoading(false));
60
92
  }, [mailboxUid]);
61
93
  async function handleCreate(e) {
62
94
  e.preventDefault();
@@ -73,11 +105,12 @@ function ExportSection({ mailboxUid }) {
73
105
  setCreating(false);
74
106
  }
75
107
  }
76
- return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export my data" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Requests a copy of this mailbox\u2019s messages, contacts, calendar, tasks, and notes. This runs in the background and can take a few minutes \u2014 check back here for a download link once it\u2019s ready." }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating, children: "Request export" })] }), createError && _jsx(Alert, { children: createError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: request.status }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }));
108
+ return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Export my data" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Requests a copy of ", _jsx("strong", { children: mailboxLabel }), "\u2019s messages, contacts, calendar, tasks, and notes. This runs in the background and can take a few minutes \u2014 check back here for a download link once it\u2019s ready."] }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-5", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating, children: "Request export" })] }), createError && _jsx(Alert, { children: createError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }));
77
109
  }
78
110
  function ImportSection({ mailboxUid }) {
79
111
  const [folders, setFolders] = useState([]);
80
112
  const [targetFolderUid, setTargetFolderUid] = useState("");
113
+ const [foldersError, setFoldersError] = useState(null);
81
114
  const [requests, setRequests] = useState([]);
82
115
  const [loading, setLoading] = useState(true);
83
116
  const [loadError, setLoadError] = useState(null);
@@ -92,26 +125,29 @@ function ImportSection({ mailboxUid }) {
92
125
  const seq = ++loadSeq.current;
93
126
  return listImportRequests()
94
127
  .then((data) => {
95
- if (seq === loadSeq.current)
128
+ if (seq === loadSeq.current) {
96
129
  setRequests(data);
130
+ }
97
131
  })
98
132
  .catch((err) => {
99
- if (seq === loadSeq.current)
133
+ if (seq === loadSeq.current) {
100
134
  setLoadError(err instanceof ApiRequestError ? err.message : "Could not load your import requests.");
135
+ }
101
136
  });
102
137
  }
103
138
  useEffect(() => {
104
139
  // Only reachable once mailboxUid is resolved - SettingsShell never renders this page's children
105
140
  // until then, the same invariant every other settings page in this codebase already relies on.
106
- Promise.all([
107
- listFolders(mailboxUid).then((all) => {
141
+ void Promise.all([
142
+ listFolders(mailboxUid)
143
+ .then((all) => {
108
144
  const mailFolders = all.filter((f) => !NON_MAIL_FOLDER_TYPES.has(f.type));
109
145
  setFolders(mailFolders);
110
146
  setTargetFolderUid(mailFolders[0]?.uid ?? "");
111
- }),
147
+ })
148
+ .catch((err) => setFoldersError(err instanceof ApiRequestError ? err.message : "Could not load this mailbox's folders.")),
112
149
  loadRequests(),
113
150
  ]).finally(() => setLoading(false));
114
- // eslint-disable-next-line react-hooks/exhaustive-deps
115
151
  }, [mailboxUid]);
116
152
  function handleFileChange(e) {
117
153
  const file = e.target.files?.[0];
@@ -126,9 +162,9 @@ function ImportSection({ mailboxUid }) {
126
162
  .catch((err) => setUploadError(err instanceof ApiRequestError ? err.message : "Could not upload this file."))
127
163
  .finally(() => setUploading(false));
128
164
  }
129
- return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Import mail" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Uploads an Mbox or PST archive and imports every message it contains into the folder you choose below. This runs in the background \u2014 check back here for its progress." }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-3 max-w-xs", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Destination folder", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-3", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange }), uploadError && _jsx(Alert, { children: uploadError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "completed" && (_jsxs("span", { className: "text-text-muted", children: [" ", "\u2014 ", request.importedCount ?? 0, " imported", request.failedCount ? `, ${request.failedCount} failed` : ""] })), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: request.status })] }, request.uid))) }))] }));
165
+ return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Import mail" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Uploads an Mbox or PST archive and imports every message it contains into the folder you choose below. This runs in the background \u2014 check back here for its progress." }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-3 max-w-xs", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Destination folder", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto mb-3", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange }), foldersError && _jsx(Alert, { children: foldersError }), uploadError && _jsx(Alert, { children: uploadError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.format.toUpperCase(), " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "completed" && (_jsxs("span", { className: "text-text-muted", children: [" ", "\u2014 ", request.importedCount ?? 0, " imported", request.failedCount ? `, ${request.failedCount} failed` : ""] })), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }, request.uid))) }))] }));
130
166
  }
131
- function ErasureSection() {
167
+ function ErasureSection({ mailboxLabel }) {
132
168
  const [requests, setRequests] = useState([]);
133
169
  const [loading, setLoading] = useState(true);
134
170
  const [loadError, setLoadError] = useState(null);
@@ -141,8 +177,7 @@ function ErasureSection() {
141
177
  .catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load your erasure requests."));
142
178
  }
143
179
  useEffect(() => {
144
- loadRequests().finally(() => setLoading(false));
145
- // eslint-disable-next-line react-hooks/exhaustive-deps
180
+ void loadRequests().finally(() => setLoading(false));
146
181
  }, []);
147
182
  const hasPending = requests.some((request) => request.status === "pending");
148
183
  function closeConfirm() {
@@ -163,5 +198,5 @@ function ErasureSection() {
163
198
  setCreating(false);
164
199
  }
165
200
  }
166
- return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Delete my account" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Permanently and irreversibly deletes this mailbox and everything in it, once a compliance administrator reviews and approves the request. There is no way to cancel a request or undo the deletion once it runs." }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs(_Fragment, { children: [requests.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2 mb-3", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [new Date(request.dateCreated).toLocaleString(), request.status === "denied" && request.reason && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.reason] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: request.status })] }, request.uid))) })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto !border-danger !text-danger hover:!border-danger hover:!text-danger", disabled: hasPending, onClick: () => setConfirming(true), children: "Request account erasure" })] })), _jsxs(Modal, { open: confirming, onClose: closeConfirm, title: "Delete my account", children: [_jsx("p", { className: "text-sm mb-5", children: "This permanently deletes this mailbox and everything in it \u2014 messages, contacts, calendar, tasks, and notes \u2014 once approved. This cannot be undone and cannot be cancelled once submitted." }), createError && _jsx(Alert, { children: createError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: creating, onClick: closeConfirm, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: creating, disabled: creating, onClick: handleConfirm, children: "Request erasure" })] })] })] }));
201
+ return (_jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "Delete my account" }), _jsxs("p", { className: "text-xs text-text-muted mb-3", children: ["Permanently and irreversibly deletes ", _jsx("strong", { children: mailboxLabel }), " and everything in it, once a compliance administrator reviews and approves the request. There is no way to cancel a request or undo the deletion once it runs."] }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : (_jsxs(_Fragment, { children: [requests.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2 mb-3", children: requests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [new Date(request.dateCreated).toLocaleString(), request.status === "denied" && request.reason && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.reason] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }, request.uid))) })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto !border-danger !text-danger hover:!border-danger hover:!text-danger", disabled: hasPending, onClick: () => setConfirming(true), children: "Request account erasure" })] })), _jsxs(Modal, { open: confirming, onClose: closeConfirm, title: "Delete my account", children: [_jsxs("p", { className: "text-sm mb-5", children: ["This permanently deletes ", mailboxLabel, " and everything in it \u2014 messages, contacts, calendar, tasks, and notes \u2014 once approved. This cannot be undone and cannot be cancelled once submitted."] }), createError && _jsx(Alert, { children: createError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: creating, onClick: closeConfirm, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: creating, disabled: creating, onClick: handleConfirm, children: "Request erasure" })] })] })] }));
167
202
  }
@@ -18,9 +18,13 @@ function ReadReceiptsContent() {
18
18
  // resolved to one of them — same established non-null pattern as `apps/www/settings/auto-reply/index.tsx`.
19
19
  const mailbox = mailboxes.find((mb) => mb.uid === mailboxUid);
20
20
  const [alwaysRequestReceiptInternal, setAlwaysRequestReceiptInternal] = useState(mailbox.alwaysRequestReceiptInternal ?? true);
21
+ const [alwaysRequestReceiptFederated, setAlwaysRequestReceiptFederated] = useState(mailbox.alwaysRequestReceiptFederated ?? false);
21
22
  const [alwaysRequestReceiptExternal, setAlwaysRequestReceiptExternal] = useState(mailbox.alwaysRequestReceiptExternal ?? false);
22
23
  const [autoSendReceiptsInternal, setAutoSendReceiptsInternal] = useState(mailbox.autoSendReceiptsInternal ?? true);
24
+ const [autoSendReceiptsFederated, setAutoSendReceiptsFederated] = useState(mailbox.autoSendReceiptsFederated ?? false);
23
25
  const [autoSendReceiptsExternal, setAutoSendReceiptsExternal] = useState(mailbox.autoSendReceiptsExternal ?? false);
26
+ // See auto-reply/index.tsx's identical note - later saves must carry the version the previous save returned.
27
+ const [version, setVersion] = useState(mailbox.version);
24
28
  const [error, setError] = useState(null);
25
29
  const [saving, setSaving] = useState(false);
26
30
  const [saved, setSaved] = useState(false);
@@ -30,14 +34,17 @@ function ReadReceiptsContent() {
30
34
  setSaved(false);
31
35
  setSaving(true);
32
36
  try {
33
- await updateMailbox({
37
+ const updated = await updateMailbox({
34
38
  uid: mailbox.uid,
35
- version: mailbox.version,
39
+ version,
36
40
  alwaysRequestReceiptInternal,
41
+ alwaysRequestReceiptFederated,
37
42
  alwaysRequestReceiptExternal,
38
43
  autoSendReceiptsInternal,
44
+ autoSendReceiptsFederated,
39
45
  autoSendReceiptsExternal,
40
46
  });
47
+ setVersion(updated.version);
41
48
  setSaved(true);
42
49
  }
43
50
  catch (err) {
@@ -47,5 +54,5 @@ function ReadReceiptsContent() {
47
54
  setSaving(false);
48
55
  }
49
56
  }
50
- return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Read Receipts" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Controls when this mailbox requests a delivery/read receipt on mail it sends, and how it responds to a receipt requested of it. A per-message \u201CRequest a read receipt\u201D checkbox in Compose always overrides the request-side settings below for that one message." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Request receipts when sending" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptInternal, onChange: (e) => setAlwaysRequestReceiptInternal(e.target.checked) }), "From internal recipients (mail to this organization)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptExternal, onChange: (e) => setAlwaysRequestReceiptExternal(e.target.checked) }), "From external recipients"] })] }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Respond automatically to requests" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsInternal, onChange: (e) => setAutoSendReceiptsInternal(e.target.checked) }), "From internal senders"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsExternal, onChange: (e) => setAutoSendReceiptsExternal(e.target.checked) }), "From external senders"] }), _jsx("p", { className: "text-xs text-text-muted", children: "Unchecked means a request from that sender waits in your Inbox for you to send or decline it explicitly." })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
57
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl", children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Read Receipts" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Controls when this mailbox requests a delivery/read receipt on mail it sends, and how it responds to a receipt requested of it. A per-message \u201CRequest a read receipt\u201D checkbox in Compose always overrides the request-side settings below for that one message." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [_jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Request receipts when sending" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptInternal, onChange: (e) => setAlwaysRequestReceiptInternal(e.target.checked) }), "From internal recipients (mail to this organization)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptFederated, onChange: (e) => setAlwaysRequestReceiptFederated(e.target.checked) }), "From federated recipients (partner organizations)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: alwaysRequestReceiptExternal, onChange: (e) => setAlwaysRequestReceiptExternal(e.target.checked) }), "From external recipients"] })] }), _jsxs("fieldset", { className: "flex flex-col gap-2", children: [_jsx("legend", { className: "text-sm font-semibold mb-1", children: "Respond automatically to requests" }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsInternal, onChange: (e) => setAutoSendReceiptsInternal(e.target.checked) }), "From internal senders"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsFederated, onChange: (e) => setAutoSendReceiptsFederated(e.target.checked) }), "From federated senders (partner organizations)"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: autoSendReceiptsExternal, onChange: (e) => setAutoSendReceiptsExternal(e.target.checked) }), "From external senders"] }), _jsx("p", { className: "text-xs text-text-muted", children: "Unchecked means a request from that sender waits in your Inbox for you to send or decline it explicitly." })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] }) }));
51
58
  }
@@ -36,10 +36,10 @@ function SharingContent() {
36
36
  const [pendingRoleChange, setPendingRoleChange] = useState(null);
37
37
  const [removeTarget, setRemoveTarget] = useState(null);
38
38
  const [removing, setRemoving] = useState(false);
39
+ // `SettingsShell` only ever renders its children once `mailboxUid` has resolved (a mailbox-less caller
40
+ // gets `<MailboxProvisioning />` instead) - same established precedent as the Labels/Focused Inbox
41
+ // settings pages, so none of the handlers below need their own `!mailboxUid` guard.
39
42
  function reload() {
40
- if (!mailboxUid) {
41
- return;
42
- }
43
43
  setStatus("loading");
44
44
  setError(null);
45
45
  listMailboxAccess(mailboxUid)
@@ -63,7 +63,7 @@ function SharingContent() {
63
63
  async function handleAdd(e) {
64
64
  e.preventDefault();
65
65
  const email = emailInput.trim();
66
- if (!email || !mailboxUid) {
66
+ if (!email) {
67
67
  return;
68
68
  }
69
69
  setAddError(null);
@@ -87,9 +87,6 @@ function SharingContent() {
87
87
  }
88
88
  }
89
89
  async function handleRoleChange(userOrRoleId, role) {
90
- if (!mailboxUid) {
91
- return;
92
- }
93
90
  setPendingRoleChange(userOrRoleId);
94
91
  setError(null);
95
92
  try {
@@ -107,9 +104,8 @@ function SharingContent() {
107
104
  setRemoveTarget(null);
108
105
  }
109
106
  async function handleRemove() {
110
- if (!mailboxUid || !removeTarget) {
111
- return;
112
- }
107
+ // Only reachable from the confirm modal's own "Remove" button, and `Modal` renders nothing at all
108
+ // unless `removeTarget` is set, so `removeTarget` is always non-null here.
113
109
  setRemoving(true);
114
110
  setError(null);
115
111
  try {
@@ -124,5 +120,5 @@ function SharingContent() {
124
120
  setRemoving(false);
125
121
  }
126
122
  }
127
- return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Sharing" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage who else can access ", mailbox?.displayName ?? "this mailbox", "\u2019s mail, calendar, contacts, and tasks. Granting access here covers all of it at once - there\u2019s nothing else to configure per data type."] })] }), status === "loading" && _jsx(SkeletonList, { count: 3 }), status === "forbidden" && (_jsx(Alert, { children: "You don\u2019t have permission to manage sharing for this mailbox." })), status === "error" && error && _jsx(Alert, { children: error }), status === "ready" && (_jsxs(_Fragment, { children: [error && _jsx(Alert, { children: error }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "People with access" }), members.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No one else has access to this mailbox yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: members.map((member) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsx("span", { className: "truncate", children: member.userOrRoleId }), _jsxs("span", { className: "flex items-center gap-2 shrink-0", children: [_jsx("select", { "aria-label": `Role for ${member.userOrRoleId}`, className: "text-sm border border-border rounded-sm py-1 px-2 bg-surface", value: member.role, disabled: pendingRoleChange === member.userOrRoleId, onChange: (e) => handleRoleChange(member.userOrRoleId, e.target.value), children: Object.keys(ROLE_LABELS).map((role) => (_jsx("option", { value: role, children: ROLE_LABELS[role] }, role))) }), _jsx(Button, { type: "button", variant: "text", className: "!w-auto text-danger", onClick: () => setRemoveTarget(member), children: "Remove" })] })] }, member.userOrRoleId))) }))] }), _jsxs("form", { onSubmit: handleAdd, className: "flex flex-col gap-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Add someone" }), addError && _jsx(Alert, { children: addError }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "email", "aria-label": "Email address", placeholder: "Email address", className: "flex-1 text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: emailInput, onChange: (e) => setEmailInput(e.target.value), disabled: adding }), _jsx("select", { "aria-label": "Role to grant", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: newRole, onChange: (e) => setNewRole(e.target.value), disabled: adding, children: Object.keys(ROLE_LABELS).map((role) => (_jsx("option", { value: role, children: ROLE_LABELS[role] }, role))) }), _jsx(Button, { type: "submit", loading: adding, disabled: adding, className: "!w-auto", children: "Add" })] })] })] })), _jsxs(Modal, { open: !!removeTarget, onClose: closeRemoveModal, title: "Remove access", children: [_jsxs("p", { className: "text-sm mb-5", children: [removeTarget?.userOrRoleId, " will no longer be able to access ", mailbox?.displayName ?? "this mailbox", "."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: removing, onClick: closeRemoveModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: removing, disabled: removing, onClick: handleRemove, children: "Remove" })] })] })] }) }));
123
+ return (_jsx("div", { className: "flex-1 min-w-0 overflow-y-auto p-6", children: _jsxs("div", { className: "max-w-xl flex flex-col gap-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-lg font-bold tracking-tight mb-1", children: "Sharing" }), _jsxs("p", { className: "text-sm text-text-muted", children: ["Manage who else can access ", mailbox?.displayName ?? "this mailbox", "\u2019s mail, calendar, contacts, and tasks. Granting access here covers all of it at once - there\u2019s nothing else to configure per data type."] })] }), status === "loading" && _jsx(SkeletonList, { count: 3 }), status === "forbidden" && (_jsx(Alert, { children: "You don\u2019t have permission to manage sharing for this mailbox." })), status === "error" && error && _jsx(Alert, { children: error }), status === "ready" && (_jsxs(_Fragment, { children: [error && _jsx(Alert, { children: error }), _jsxs("div", { children: [_jsx("h2", { className: "text-sm font-semibold mb-2", children: "People with access" }), members.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No one else has access to this mailbox yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: members.map((member) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsx("span", { className: "truncate", children: member.userOrRoleId }), _jsxs("span", { className: "flex items-center gap-2 shrink-0", children: [_jsxs("select", { "aria-label": `Role for ${member.userOrRoleId}`, className: "text-sm border border-border rounded-sm py-1 px-2 bg-surface", value: member.role, disabled: pendingRoleChange === member.userOrRoleId, onChange: (e) => handleRoleChange(member.userOrRoleId, e.target.value), children: [member.role === "custom" && (_jsx("option", { value: "custom", disabled: true, children: "Custom access" })), Object.keys(ROLE_LABELS).map((role) => (_jsx("option", { value: role, children: ROLE_LABELS[role] }, role)))] }), _jsx(Button, { type: "button", variant: "text", className: "!w-auto text-danger", onClick: () => setRemoveTarget(member), children: "Remove" })] })] }, member.userOrRoleId))) }))] }), _jsxs("form", { onSubmit: handleAdd, className: "flex flex-col gap-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Add someone" }), addError && _jsx(Alert, { children: addError }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { type: "email", "aria-label": "Email address", placeholder: "Email address", className: "flex-1 text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: emailInput, onChange: (e) => setEmailInput(e.target.value), disabled: adding }), _jsx("select", { "aria-label": "Role to grant", className: "text-sm border border-border rounded-sm py-1.5 px-2 bg-surface", value: newRole, onChange: (e) => setNewRole(e.target.value), disabled: adding, children: Object.keys(ROLE_LABELS).map((role) => (_jsx("option", { value: role, children: ROLE_LABELS[role] }, role))) }), _jsx(Button, { type: "submit", loading: adding, disabled: adding, className: "!w-auto", children: "Add" })] })] })] })), _jsxs(Modal, { open: !!removeTarget, onClose: closeRemoveModal, title: "Remove access", children: [_jsxs("p", { className: "text-sm mb-5", children: [removeTarget?.userOrRoleId, " will no longer be able to access ", mailbox?.displayName ?? "this mailbox", "."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: removing, onClick: closeRemoveModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: removing, disabled: removing, onClick: handleRemove, children: "Remove" })] })] })] }) }));
128
124
  }
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useMemo, useState } from "react";
7
- import { endOfWeek, isAfter, isBefore, isToday, startOfDay } from "date-fns";
7
+ import { endOfWeek, isAfter, isBefore, isToday, parseISO, startOfDay } from "date-fns";
8
8
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
9
  import { createTask, deleteTask, listTasks, setTaskCompleted, setTaskMyDay, } from "@rapidmx/react-shared/tasks/tasksApi.js";
10
10
  import { listFlaggedMessages } from "@rapidmx/react-shared/mail/flaggedMessages.js";
@@ -13,18 +13,29 @@ import TasksSidebar from "../../shared/components/tasks/TasksSidebar.js";
13
13
  import TasksToolbar from "../../shared/components/tasks/TasksToolbar.js";
14
14
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
15
15
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
16
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
16
17
  import { findWellKnownFolderUid } from "../../shared/mail/findWellKnownFolderUid.js";
18
+ import { useWritableMailboxes } from "../../shared/components/mail/writableMailboxes.js";
19
+ import { LIST_PAGE_SIZE, MAX_LIST_PAGES, listAllPages } from "../../shared/mail/listAllPages.js";
17
20
  const INPUT_CLASS = "text-sm py-1.5 px-2 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
18
21
  export default function TasksPage(props) {
19
22
  return (_jsx(TasksShell, { ...props, children: _jsx(TasksContent, {}) }));
20
23
  }
24
+ /**
25
+ * Parses a task's `dueDate` - `parseISO()` rather than `new Date()`, since `new Date("2026-06-16")` reads a
26
+ * date-only value as UTC midnight, which is the *previous* local day anywhere west of UTC. A full
27
+ * timestamp parses identically either way.
28
+ */
29
+ function parseDueDate(value) {
30
+ return parseISO(value);
31
+ }
21
32
  const BUCKET_ORDER = ["Overdue", "Today", "This Week", "Later", "No due date"];
22
33
  /** Buckets a task by its due date relative to `now`, matching the Outlook/To-Do convention this list mirrors. */
23
34
  function bucketFor(task, now) {
24
35
  if (!task.dueDate) {
25
36
  return "No due date";
26
37
  }
27
- const due = new Date(task.dueDate);
38
+ const due = parseDueDate(task.dueDate);
28
39
  if (isBefore(due, startOfDay(now))) {
29
40
  return "Overdue";
30
41
  }
@@ -44,6 +55,8 @@ const PRIORITY_CLASS = {
44
55
  };
45
56
  function TasksContent() {
46
57
  const { folderUid, mailboxUid, userUid, mailboxes } = useTasksShell();
58
+ // The quick-add form's Mailbox choices - view-only shares are left out (see writableMailboxes.ts).
59
+ const writableMailboxes = useWritableMailboxes(mailboxes, userUid, mailboxUid);
47
60
  // The quick-add form's chosen mailbox - `undefined` means "the mailbox being viewed".
48
61
  const [targetMailboxUid, setTargetMailboxUid] = useState(undefined);
49
62
  // Set after a task is added to a different mailbox than the one this list shows.
@@ -51,6 +64,8 @@ function TasksContent() {
51
64
  const [tasks, setTasks] = useState([]);
52
65
  const [loading, setLoading] = useState(true);
53
66
  const [error, setError] = useState(null);
67
+ // Set when `listAllPages()` stopped at its page cap - the list shown isn't every task.
68
+ const [truncated, setTruncated] = useState(false);
54
69
  const [title, setTitle] = useState("");
55
70
  const [dueDate, setDueDate] = useState("");
56
71
  const [priority, setPriority] = useState("normal");
@@ -62,16 +77,23 @@ function TasksContent() {
62
77
  const [flaggedMessages, setFlaggedMessages] = useState([]);
63
78
  const [flaggedLoading, setFlaggedLoading] = useState(false);
64
79
  const [flaggedError, setFlaggedError] = useState(null);
80
+ // What the delete confirmation dialog is asking about - one task (a row's delete button), or every
81
+ // checked task (the toolbar's Delete).
82
+ const [pendingDelete, setPendingDelete] = useState(null);
65
83
  function reload() {
66
84
  if (!folderUid) {
67
85
  setTasks([]);
86
+ setTruncated(false);
68
87
  setLoading(false);
69
88
  return Promise.resolve();
70
89
  }
71
90
  setLoading(true);
72
91
  setError(null);
73
- return listTasks(folderUid, { limit: 500 })
74
- .then(setTasks)
92
+ return listAllPages((page) => listTasks(folderUid, { limit: LIST_PAGE_SIZE, page }))
93
+ .then((result) => {
94
+ setTasks(result.items);
95
+ setTruncated(result.truncated);
96
+ })
75
97
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load tasks."))
76
98
  .finally(() => setLoading(false));
77
99
  }
@@ -115,7 +137,8 @@ function TasksContent() {
115
137
  mailboxUid: target,
116
138
  folderUid: targetFolderUid,
117
139
  title: title.trim(),
118
- dueDate: dueDate ? new Date(dueDate).toISOString() : undefined,
140
+ // The date input's "yyyy-MM-dd" is the user's *local* day - see `parseDueDate()`.
141
+ dueDate: dueDate ? parseDueDate(dueDate).toISOString() : undefined,
119
142
  priority,
120
143
  });
121
144
  if (target === mailboxUid) {
@@ -237,6 +260,19 @@ function TasksContent() {
237
260
  await reload();
238
261
  setError(bulkError);
239
262
  }
263
+ function requestDelete(task) {
264
+ setPendingDelete({ type: "single", task });
265
+ }
266
+ function handleConfirmDelete() {
267
+ const pending = pendingDelete;
268
+ setPendingDelete(null);
269
+ if (pending.type === "single") {
270
+ void handleDelete(pending.task);
271
+ }
272
+ else {
273
+ void handleBulkDelete();
274
+ }
275
+ }
240
276
  const grouped = useMemo(() => {
241
277
  const now = new Date();
242
278
  const map = new Map(BUCKET_ORDER.map((b) => [b, []]));
@@ -249,13 +285,17 @@ function TasksContent() {
249
285
  return map;
250
286
  }, [viewFiltered]);
251
287
  const completedTasks = viewFiltered.filter((t) => t.completed);
252
- return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx(TasksSidebar, { mailboxUid: mailboxUid, tasks: tasks, userUid: userUid, active: view, onSelect: handleSelectView }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col overflow-y-auto", children: [_jsx(TasksToolbar, { viewMode: viewMode, onViewModeChange: setViewMode, selectedCount: checkedTasks.length, onComplete: handleBulkComplete, onDelete: handleBulkDelete, onAddToMyDay: handleBulkAddToMyDay }), _jsxs("div", { role: "region", "aria-label": "Tasks list", className: "max-w-2xl mx-auto w-full flex flex-col gap-6 p-6", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Tasks" }), error && _jsx(Alert, { children: error }), view.type === "flagged" ? (_jsx(FlaggedEmailList, { loading: flaggedLoading, error: flaggedError, messages: flaggedMessages })) : (_jsxs(_Fragment, { children: [_jsxs("form", { onSubmit: handleCreate, className: "flex items-center gap-2 bg-surface border border-border rounded-md p-2", children: [_jsx("input", { type: "text", "aria-label": "Add a task", className: `${INPUT_CLASS} flex-1`, value: title, onChange: (e) => setTitle(e.target.value), placeholder: "Add a task" }), _jsx("input", { type: "date", "aria-label": "Due date", className: INPUT_CLASS, value: dueDate, onChange: (e) => setDueDate(e.target.value) }), _jsxs("select", { "aria-label": "Priority", className: INPUT_CLASS, value: priority, onChange: (e) => setPriority(e.target.value), children: [_jsx("option", { value: "low", children: "Low" }), _jsx("option", { value: "normal", children: "Normal" }), _jsx("option", { value: "high", children: "High" })] }), mailboxes.length > 1 && (_jsx("select", { "aria-label": "Task mailbox", className: `${INPUT_CLASS} max-w-40`, value: targetMailboxUid ?? mailboxUid ?? "", onChange: (e) => setTargetMailboxUid(e.target.value), children: mailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid === userUid ? "" : " (shared)"] }, mb.uid))) })), _jsx(Button, { type: "submit", loading: creating, disabled: creating, className: "!w-auto shrink-0", children: "Add" })] }), createError && _jsx(Alert, { children: createError }), addedElsewhere && (_jsxs("p", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["\u201C", addedElsewhere.title, "\u201D was added to", " ", mailboxes.find((mb) => mb.uid === addedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/tasks?mailboxUid=${encodeURIComponent(addedElsewhere.mailboxUid)}`, className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s tasks" })] })), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : viewFiltered.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No tasks yet." })) : viewMode === "grid" ? (_jsx(TaskTable, { tasks: viewFiltered, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: handleDelete })) : (_jsxs(_Fragment, { children: [BUCKET_ORDER.map((bucket) => {
288
+ return (_jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsx(TasksSidebar, { mailboxUid: mailboxUid, tasks: tasks, userUid: userUid, active: view, onSelect: handleSelectView }), _jsxs("div", { className: "flex-1 min-w-0 flex flex-col overflow-y-auto", children: [_jsx(TasksToolbar, { viewMode: viewMode, onViewModeChange: setViewMode, selectedCount: checkedTasks.length, onComplete: handleBulkComplete, onDelete: () => setPendingDelete({ type: "bulk" }), onAddToMyDay: handleBulkAddToMyDay }), _jsxs("div", { role: "region", "aria-label": "Tasks list", className: "max-w-2xl mx-auto w-full flex flex-col gap-6 p-6", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Tasks" }), error && _jsx(Alert, { children: error }), truncated && view.type !== "flagged" && (_jsxs(Alert, { children: ["This folder has more tasks than can be shown at once - only the first ", LIST_PAGE_SIZE * MAX_LIST_PAGES, " are listed."] })), view.type === "flagged" ? (_jsx(FlaggedEmailList, { loading: flaggedLoading, error: flaggedError, messages: flaggedMessages })) : (_jsxs(_Fragment, { children: [_jsxs("form", { onSubmit: handleCreate, className: "flex items-center gap-2 bg-surface border border-border rounded-md p-2", children: [_jsx("input", { type: "text", "aria-label": "Add a task", className: `${INPUT_CLASS} flex-1`, value: title, onChange: (e) => setTitle(e.target.value), placeholder: "Add a task" }), _jsx("input", { type: "date", "aria-label": "Due date", className: INPUT_CLASS, value: dueDate, onChange: (e) => setDueDate(e.target.value) }), _jsxs("select", { "aria-label": "Priority", className: INPUT_CLASS, value: priority, onChange: (e) => setPriority(e.target.value), children: [_jsx("option", { value: "low", children: "Low" }), _jsx("option", { value: "normal", children: "Normal" }), _jsx("option", { value: "high", children: "High" })] }), writableMailboxes.length > 1 && (_jsx("select", { "aria-label": "Task mailbox", className: `${INPUT_CLASS} max-w-40`,
289
+ // `writableMailboxes` only has entries once `mailboxUid` has resolved.
290
+ value: targetMailboxUid ?? mailboxUid, onChange: (e) => setTargetMailboxUid(e.target.value), children: writableMailboxes.map((mb) => (_jsxs("option", { value: mb.uid, children: [mb.displayName, mb.ownerUserUid === userUid ? "" : " (shared)"] }, mb.uid))) })), _jsx(Button, { type: "submit", loading: creating, disabled: creating, className: "!w-auto shrink-0", children: "Add" })] }), createError && _jsx(Alert, { children: createError }), addedElsewhere && (_jsxs("p", { role: "status", className: "text-sm py-2 px-3 rounded-sm bg-surface-alt text-text", children: ["\u201C", addedElsewhere.title, "\u201D was added to", " ", mailboxes.find((mb) => mb.uid === addedElsewhere.mailboxUid)?.displayName ?? "another mailbox", ".", " ", _jsx("a", { href: `/tasks?mailboxUid=${encodeURIComponent(addedElsewhere.mailboxUid)}`, className: "font-medium text-primary-dark hover:underline", children: "View that mailbox\u2019s tasks" })] })), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : viewFiltered.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No tasks yet." })) : viewMode === "grid" ? (_jsx(TaskTable, { tasks: viewFiltered, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: requestDelete })) : (_jsxs(_Fragment, { children: [BUCKET_ORDER.map((bucket) => {
253
291
  const items = grouped.get(bucket);
254
292
  if (items.length === 0) {
255
293
  return null;
256
294
  }
257
- return (_jsx(TaskGroup, { label: bucket, tasks: items, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: handleDelete }, bucket));
258
- }), completedTasks.length > 0 && (_jsx(TaskGroup, { label: "Completed", tasks: completedTasks, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: handleDelete }))] }))] }))] })] })] }));
295
+ return (_jsx(TaskGroup, { label: bucket, tasks: items, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: requestDelete }, bucket));
296
+ }), completedTasks.length > 0 && (_jsx(TaskGroup, { label: "Completed", tasks: completedTasks, checkedUids: checkedUids, onToggleChecked: toggleChecked, onToggle: handleToggle, onDelete: requestDelete }))] }))] }))] })] }), _jsxs(Modal, { open: pendingDelete !== null, onClose: () => setPendingDelete(null), title: "Delete tasks", children: [_jsx("p", { className: "text-sm mb-5", children: pendingDelete?.type === "single"
297
+ ? `Delete "${pendingDelete.task.title}"?`
298
+ : `Delete ${checkedTasks.length} selected ${checkedTasks.length === 1 ? "task" : "tasks"}?` }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setPendingDelete(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: handleConfirmDelete, children: "Delete" })] })] })] }));
259
299
  }
260
300
  function FlaggedEmailList({ loading, error, messages }) {
261
301
  if (error) {
@@ -278,7 +318,7 @@ function CompletionToggle({ task, onToggle }) {
278
318
  ].join(" "), children: "\u2713" }));
279
319
  }
280
320
  function TaskGroup({ label, tasks, checkedUids, onToggleChecked, onToggle, onDelete }) {
281
- return (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-2", children: label }), _jsx("ul", { className: "bg-surface border border-border rounded-md divide-y divide-border", children: tasks.map((task) => (_jsxs("li", { className: "flex items-center gap-3 py-2.5 px-3", children: [_jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }), _jsx(CompletionToggle, { task: task, onToggle: onToggle }), _jsx("span", { className: ["flex-1 text-sm", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), task.priority !== "normal" && (_jsx("span", { className: ["text-xs font-medium", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] })), task.dueDate && (_jsx("span", { className: "text-xs text-text-muted shrink-0", children: new Date(task.dueDate).toLocaleDateString() })), _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" })] }, task.uid))) })] }));
321
+ return (_jsxs("div", { children: [_jsx("h2", { className: "text-xs font-bold uppercase tracking-wide text-text-muted mb-2", children: label }), _jsx("ul", { className: "bg-surface border border-border rounded-md divide-y divide-border", children: tasks.map((task) => (_jsxs("li", { className: "flex items-center gap-3 py-2.5 px-3", children: [_jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }), _jsx(CompletionToggle, { task: task, onToggle: onToggle }), _jsx("span", { className: ["flex-1 text-sm", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), task.priority !== "normal" && (_jsx("span", { className: ["text-xs font-medium", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] })), task.dueDate && (_jsx("span", { className: "text-xs text-text-muted shrink-0", children: parseDueDate(task.dueDate).toLocaleDateString() })), _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" })] }, task.uid))) })] }));
282
322
  }
283
323
  function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete }) {
284
324
  const allChecked = tasks.length > 0 && tasks.every((t) => checkedUids.has(t.uid));
@@ -298,5 +338,5 @@ function TaskTable({ tasks, checkedUids, onToggleChecked, onToggle, onDelete })
298
338
  }
299
339
  }
300
340
  }
301
- return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm bg-surface border border-border rounded-md", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all tasks", checked: allChecked, onChange: toggleAll }) }), _jsx("th", { className: "w-8 px-3 py-2" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Title" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Due Date" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Importance" }), _jsx("th", { className: "w-8 px-3 py-2" })] }) }), _jsx("tbody", { children: tasks.map((task) => (_jsxs("tr", { className: "border-b border-border last:border-0", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(CompletionToggle, { task: task, onToggle: onToggle }) }), _jsx("td", { className: ["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), _jsx("td", { className: "px-3 py-2 text-text-muted", children: task.dueDate ? new Date(task.dueDate).toLocaleDateString() : "" }), _jsx("td", { className: ["px-3 py-2", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" }) })] }, task.uid))) })] }) }));
341
+ return (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm bg-surface border border-border rounded-md", children: [_jsx("thead", { children: _jsxs("tr", { className: "border-b border-border text-left", children: [_jsx("th", { className: "w-8 px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": "Select all tasks", checked: allChecked, onChange: toggleAll }) }), _jsx("th", { className: "w-8 px-3 py-2" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Title" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Due Date" }), _jsx("th", { className: "px-3 py-2 text-xs font-bold uppercase tracking-wide text-text-muted", children: "Importance" }), _jsx("th", { className: "w-8 px-3 py-2" })] }) }), _jsx("tbody", { children: tasks.map((task) => (_jsxs("tr", { className: "border-b border-border last:border-0", children: [_jsx("td", { className: "px-3 py-2", children: _jsx("input", { type: "checkbox", "aria-label": `Select ${task.title}`, checked: checkedUids.has(task.uid), onChange: () => onToggleChecked(task.uid) }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(CompletionToggle, { task: task, onToggle: onToggle }) }), _jsx("td", { className: ["px-3 py-2", task.completed ? "line-through text-text-muted" : ""].join(" "), children: task.title }), _jsx("td", { className: "px-3 py-2 text-text-muted", children: task.dueDate ? parseDueDate(task.dueDate).toLocaleDateString() : "" }), _jsx("td", { className: ["px-3 py-2", PRIORITY_CLASS[task.priority]].join(" "), children: PRIORITY_LABEL[task.priority] }), _jsx("td", { className: "px-3 py-2", children: _jsx("button", { type: "button", onClick: () => onDelete(task), "aria-label": `Delete "${task.title}"`, className: "text-text-muted hover:text-danger text-sm px-1", children: "\u00D7" }) })] }, task.uid))) })] }) }));
302
342
  }