@rapidmx/web-client 0.3.1 → 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 (202) hide show
  1. package/apps/admin/audit-log/index.tsx +35 -8
  2. package/apps/admin/branding/index.tsx +39 -404
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/domains/[uid].tsx +92 -259
  6. package/apps/admin/encryption-policy/index.tsx +19 -0
  7. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  8. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  9. package/apps/admin/index.tsx +118 -82
  10. package/apps/admin/ingest-queue/index.tsx +44 -5
  11. package/apps/admin/mailbox-policy/index.tsx +19 -0
  12. package/apps/admin/mailboxes/[uid].tsx +211 -184
  13. package/apps/admin/mailboxes/new/index.tsx +28 -291
  14. package/apps/admin/plugins/index.tsx +15 -0
  15. package/apps/admin/quarantine/index.tsx +46 -14
  16. package/apps/admin/retention-policy/index.tsx +39 -132
  17. package/apps/admin/setup/index.tsx +15 -0
  18. package/apps/admin/transport-rules/[uid].tsx +30 -1
  19. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  20. package/apps/admin/transport-rules/index.tsx +24 -6
  21. package/apps/admin/transport-rules/new/index.tsx +30 -1
  22. package/apps/escrow/_layout.tsx +2 -3
  23. package/apps/escrow/audit-log/index.tsx +196 -168
  24. package/apps/escrow/matters/[uid].tsx +621 -521
  25. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  26. package/apps/shared/components/admin/layout/AdminShell.tsx +251 -210
  27. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  28. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  29. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  30. package/apps/shared/components/admin/settings/BrandingForm.tsx +385 -0
  31. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +187 -0
  32. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +112 -0
  33. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
  34. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +308 -0
  35. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +152 -0
  36. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -0
  37. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -0
  38. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +290 -0
  39. package/apps/shared/components/admin/setup/SetupWizard.tsx +416 -0
  40. package/apps/shared/components/admin/signOut.ts +79 -0
  41. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  42. package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
  43. package/apps/shared/components/calendar/EventModal.tsx +717 -513
  44. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  45. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  46. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  47. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  48. package/apps/shared/components/calendar/allDay.ts +124 -0
  49. package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
  50. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  51. package/apps/shared/components/contacts/ContactForm.tsx +377 -314
  52. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  53. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  54. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  55. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  56. package/apps/shared/components/layout/AppShell.tsx +270 -169
  57. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  58. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +150 -26
  59. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  60. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  61. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -0
  62. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  63. package/apps/shared/components/mail/MessageDetailPane.tsx +799 -76
  64. package/apps/shared/components/mail/compose/ComposeContext.tsx +26 -16
  65. package/apps/shared/components/mail/compose/ComposeWindow.tsx +1652 -765
  66. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  67. package/apps/shared/components/mail/layout/MailShell.tsx +418 -302
  68. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  69. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  70. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  71. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  72. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  73. package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
  74. package/apps/shared/mail/listAllPages.ts +39 -0
  75. package/apps/shared/search/LocalIndexLifecycle.tsx +98 -0
  76. package/apps/shared/search/localIndexBlockCipher.ts +83 -0
  77. package/apps/shared/search/localIndexBuilder.ts +481 -0
  78. package/apps/shared/search/localIndexKey.ts +27 -0
  79. package/apps/shared/search/localIndexRpcClient.ts +316 -0
  80. package/apps/shared/search/localIndexSchema.ts +236 -0
  81. package/apps/shared/search/localIndexSizePreference.ts +88 -0
  82. package/apps/shared/search/localIndexStorage.ts +98 -0
  83. package/apps/shared/search/localIndexVFS.ts +266 -0
  84. package/apps/shared/search/localIndexWorker.ts +926 -0
  85. package/apps/shared/search/searchTier2.ts +79 -0
  86. package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
  87. package/apps/www/calendar/index.tsx +438 -407
  88. package/apps/www/contacts/[uid].tsx +21 -2
  89. package/apps/www/contacts/index.tsx +148 -17
  90. package/apps/www/index.tsx +1314 -486
  91. package/apps/www/messages/[uid].tsx +23 -7
  92. package/apps/www/settings/auto-reply/index.tsx +134 -129
  93. package/apps/www/settings/booking-types/[uid].tsx +306 -300
  94. package/apps/www/settings/encryption/index.tsx +800 -215
  95. package/apps/www/settings/filters/[uid].tsx +171 -155
  96. package/apps/www/settings/filters/new/index.tsx +8 -1
  97. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  98. package/apps/www/settings/privacy/index.tsx +494 -436
  99. package/apps/www/settings/read-receipts/index.tsx +148 -125
  100. package/apps/www/settings/sharing/index.tsx +274 -0
  101. package/apps/www/tasks/index.tsx +638 -530
  102. package/dist/apps/admin/audit-log/index.js +38 -8
  103. package/dist/apps/admin/branding/index.js +4 -98
  104. package/dist/apps/admin/data-requests/index.js +52 -72
  105. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  106. package/dist/apps/admin/domains/[uid].js +5 -68
  107. package/dist/apps/admin/encryption-policy/index.js +8 -0
  108. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.js +17 -1
  111. package/dist/apps/admin/ingest-queue/index.js +24 -6
  112. package/dist/apps/admin/mailbox-policy/index.js +8 -0
  113. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  114. package/dist/apps/admin/mailboxes/new/index.js +4 -89
  115. package/dist/apps/admin/plugins/index.js +6 -0
  116. package/dist/apps/admin/quarantine/index.js +20 -11
  117. package/dist/apps/admin/retention-policy/index.js +3 -36
  118. package/dist/apps/admin/setup/index.js +6 -0
  119. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  120. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  121. package/dist/apps/admin/transport-rules/index.js +22 -6
  122. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  123. package/dist/apps/escrow/_layout.js +3 -3
  124. package/dist/apps/escrow/audit-log/index.js +26 -1
  125. package/dist/apps/escrow/matters/[uid].js +77 -42
  126. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  127. package/dist/apps/shared/components/admin/layout/AdminShell.js +37 -4
  128. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  129. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  130. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  131. package/dist/apps/shared/components/admin/settings/BrandingForm.js +116 -0
  132. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +83 -0
  133. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +62 -0
  134. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
  135. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +112 -0
  136. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +74 -0
  137. package/dist/apps/shared/components/admin/settings/PluginsManager.js +572 -0
  138. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +73 -0
  139. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +147 -0
  140. package/dist/apps/shared/components/admin/setup/SetupWizard.js +208 -0
  141. package/dist/apps/shared/components/admin/signOut.js +75 -0
  142. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  143. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
  144. package/dist/apps/shared/components/calendar/EventModal.js +154 -21
  145. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  146. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  147. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  148. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  149. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  150. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
  151. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  152. package/dist/apps/shared/components/contacts/ContactForm.js +45 -12
  153. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  154. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  155. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  156. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  157. package/dist/apps/shared/components/layout/AppShell.js +100 -8
  158. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  159. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +63 -14
  160. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  161. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  162. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +136 -0
  163. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  164. package/dist/apps/shared/components/mail/MessageDetailPane.js +464 -34
  165. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  166. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +807 -116
  167. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  168. package/dist/apps/shared/components/mail/layout/MailShell.js +116 -41
  169. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  170. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  171. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  172. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  173. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  174. package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
  175. package/dist/apps/shared/mail/listAllPages.js +25 -0
  176. package/dist/apps/shared/search/LocalIndexLifecycle.js +80 -0
  177. package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
  178. package/dist/apps/shared/search/localIndexBuilder.js +408 -0
  179. package/dist/apps/shared/search/localIndexKey.js +25 -0
  180. package/dist/apps/shared/search/localIndexRpcClient.js +241 -0
  181. package/dist/apps/shared/search/localIndexSchema.js +185 -0
  182. package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
  183. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  184. package/dist/apps/shared/search/localIndexVFS.js +258 -0
  185. package/dist/apps/shared/search/localIndexWorker.js +687 -0
  186. package/dist/apps/shared/search/searchTier2.js +38 -0
  187. package/dist/apps/www/calendar/index.js +38 -18
  188. package/dist/apps/www/contacts/[uid].js +13 -2
  189. package/dist/apps/www/contacts/index.js +94 -15
  190. package/dist/apps/www/index.js +770 -137
  191. package/dist/apps/www/messages/[uid].js +23 -7
  192. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  193. package/dist/apps/www/settings/booking-types/[uid].js +17 -12
  194. package/dist/apps/www/settings/encryption/index.js +541 -96
  195. package/dist/apps/www/settings/filters/[uid].js +22 -7
  196. package/dist/apps/www/settings/filters/new/index.js +8 -2
  197. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  198. package/dist/apps/www/settings/privacy/index.js +57 -22
  199. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  200. package/dist/apps/www/settings/sharing/index.js +124 -0
  201. package/dist/apps/www/tasks/index.js +76 -14
  202. package/package.json +3 -2
@@ -3,13 +3,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useState } from "react";
6
+ import { useEffect, useRef, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { listAuditLog } from "@rapidmx/react-shared/admin/auditLogApi.js";
9
9
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
10
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
11
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
12
  const PAGE_SIZE = 25;
13
+ /** How long typing in a filter box has to pause before the log is re-fetched. */
14
+ export const FILTER_DEBOUNCE_MS = 300;
13
15
  const FILTER_KEYS = ["mailboxUid", "actorUserUid", "action", "targetType"];
14
16
  /** Seeds the filter form from the query string on first render — lets a link (e.g. a future "View audit
15
17
  * log" deep link from a mailbox's detail page) land here pre-filtered, same convention as `quarantine`'s
@@ -32,26 +34,54 @@ export default function AuditLogPage(props) {
32
34
  return (_jsx(AdminShell, { ...props, active: "auditLog", children: _jsx(AuditLogContent, {}) }));
33
35
  }
34
36
  function AuditLogContent() {
37
+ // `filters` is what the boxes show; `queryFilters` is what was last fetched - it follows `filters` once typing
38
+ // pauses, and stays `null` until the URL's filters are read so no unfiltered fetch goes out first.
35
39
  const [filters, setFilters] = useState({});
40
+ const [queryFilters, setQueryFilters] = useState(null);
36
41
  const [page, setPage] = useState(0);
37
42
  const [entries, setEntries] = useState([]);
38
43
  const [loading, setLoading] = useState(true);
39
44
  const [error, setError] = useState(null);
45
+ const debounceTimer = useRef(undefined);
40
46
  useEffect(() => {
41
- setFilters(readFiltersFromUrl());
47
+ const initial = readFiltersFromUrl();
48
+ setFilters(initial);
49
+ setQueryFilters(initial);
50
+ return () => clearTimeout(debounceTimer.current);
42
51
  }, []);
43
52
  useEffect(() => {
53
+ if (!queryFilters) {
54
+ return;
55
+ }
56
+ // Only the latest filters/page's response is applied, so a slow earlier fetch can't overwrite it.
57
+ let cancelled = false;
44
58
  setLoading(true);
45
59
  setError(null);
46
- listAuditLog(filters, { page, limit: PAGE_SIZE })
47
- .then(setEntries)
48
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load the audit log."))
49
- .finally(() => setLoading(false));
50
- }, [filters, page]);
60
+ listAuditLog(queryFilters, { page, limit: PAGE_SIZE })
61
+ .then((data) => {
62
+ if (!cancelled)
63
+ setEntries(data);
64
+ })
65
+ .catch((err) => {
66
+ if (!cancelled)
67
+ setError(err instanceof ApiRequestError ? err.message : "Could not load the audit log.");
68
+ })
69
+ .finally(() => {
70
+ if (!cancelled)
71
+ setLoading(false);
72
+ });
73
+ return () => {
74
+ cancelled = true;
75
+ };
76
+ }, [queryFilters, page]);
51
77
  function updateFilter(key, value) {
52
78
  const next = { ...filters, [key]: value || undefined };
53
79
  setFilters(next);
54
- setPage(0);
80
+ clearTimeout(debounceTimer.current);
81
+ debounceTimer.current = setTimeout(() => {
82
+ setQueryFilters(next);
83
+ setPage(0);
84
+ }, FILTER_DEBOUNCE_MS);
55
85
  const params = new URLSearchParams();
56
86
  for (const k of FILTER_KEYS) {
57
87
  const v = next[k];
@@ -1,16 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  ///////////////////////////////////////////////////////////////////////////////
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useRef, useState } from "react";
6
+ import { useEffect, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import { deleteBrandingIcon, deleteBrandingLogo, deleteBrandingStylesheet, getBranding, updateBranding, uploadBrandingIcon, uploadBrandingLogo, uploadBrandingStylesheet, } from "@rapidmx/react-shared/branding/brandingApi.js";
8
+ import { getBranding } from "@rapidmx/react-shared/branding/brandingApi.js";
9
9
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import BrandingForm from "../../shared/components/admin/settings/BrandingForm.js";
10
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
- import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
- const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
13
- const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
14
12
  export default function BrandingPage(props) {
15
13
  return (_jsx(AdminShell, { ...props, active: "branding", children: _jsx(BrandingContent, {}) }));
16
14
  }
@@ -32,95 +30,3 @@ function BrandingContent() {
32
30
  }
33
31
  return _jsx(BrandingForm, { branding: branding, onChange: setBranding });
34
32
  }
35
- function BrandingForm({ branding, onChange }) {
36
- const [companyName, setCompanyName] = useState(branding.companyName);
37
- const [title, setTitle] = useState(branding.title);
38
- const [headerHtml, setHeaderHtml] = useState(branding.headerHtml ?? "");
39
- const [footerHtml, setFooterHtml] = useState(branding.footerHtml ?? "");
40
- const [logoUrlInput, setLogoUrlInput] = useState(branding.logoUrl ?? "");
41
- const [iconUrlInput, setIconUrlInput] = useState(branding.iconUrl ?? "");
42
- const [stylesheetUrlInput, setStylesheetUrlInput] = useState(branding.stylesheetUrl ?? "");
43
- const [error, setError] = useState(null);
44
- const [saving, setSaving] = useState(false);
45
- const [saved, setSaved] = useState(false);
46
- const [assetBusy, setAssetBusy] = useState(null);
47
- const logoFileRef = useRef(null);
48
- const iconFileRef = useRef(null);
49
- const stylesheetFileRef = useRef(null);
50
- function runAsset(name, action) {
51
- setError(null);
52
- setAssetBusy(name);
53
- return action()
54
- .then((updated) => {
55
- onChange(updated);
56
- setLogoUrlInput(updated.logoUrl ?? "");
57
- setIconUrlInput(updated.iconUrl ?? "");
58
- setStylesheetUrlInput(updated.stylesheetUrl ?? "");
59
- })
60
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not update branding."))
61
- .finally(() => setAssetBusy(null));
62
- }
63
- function handleLogoFileChange(e) {
64
- const file = e.target.files?.[0];
65
- if (!file)
66
- return;
67
- if (file.size > MAX_UPLOAD_BYTES) {
68
- setError(`"${file.name}" is too large — logos must be 5MB or smaller.`);
69
- }
70
- else {
71
- void runAsset("logo-upload", () => uploadBrandingLogo(file));
72
- }
73
- e.target.value = "";
74
- }
75
- function handleIconFileChange(e) {
76
- const file = e.target.files?.[0];
77
- if (!file)
78
- return;
79
- if (file.size > MAX_UPLOAD_BYTES) {
80
- setError(`"${file.name}" is too large — icons must be 5MB or smaller.`);
81
- }
82
- else {
83
- void runAsset("icon-upload", () => uploadBrandingIcon(file));
84
- }
85
- e.target.value = "";
86
- }
87
- function handleStylesheetFileChange(e) {
88
- const file = e.target.files?.[0];
89
- if (!file)
90
- return;
91
- if (file.size > MAX_UPLOAD_BYTES) {
92
- setError(`"${file.name}" is too large — stylesheets must be 5MB or smaller.`);
93
- }
94
- else {
95
- void runAsset("stylesheet-upload", () => uploadBrandingStylesheet(file));
96
- }
97
- e.target.value = "";
98
- }
99
- async function handleSubmit(e) {
100
- e.preventDefault();
101
- setError(null);
102
- setSaved(false);
103
- setSaving(true);
104
- try {
105
- const updated = await updateBranding({ companyName, title, headerHtml, footerHtml });
106
- onChange(updated);
107
- setSaved(true);
108
- }
109
- catch (err) {
110
- setError(err instanceof ApiRequestError ? err.message : "Could not save branding.");
111
- }
112
- finally {
113
- setSaving(false);
114
- }
115
- }
116
- return (_jsxs("div", { className: "max-w-2xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Branding" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Customize the logo, nav-header icon, product name, and chrome shown to every visitor of the webmail and admin console \u2014 including anonymous booking-page visitors." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Logo" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.logoUrl || "/images/logo.svg", alt: "Current logo", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "logo-upload", disabled: assetBusy !== null, onClick: () => logoFileRef.current?.click(), children: "Upload logo" }), _jsx("input", { ref: logoFileRef, type: "file", accept: "image/*", "aria-label": "Upload logo", className: "hidden", onChange: handleLogoFileChange }), branding.logoUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "logo-delete", onClick: () => void runAsset("logo-delete", async () => {
117
- await deleteBrandingLogo();
118
- return { ...branding, logoUrl: undefined };
119
- }), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Icon" }), _jsx("p", { className: "text-xs text-text-muted -mt-1", children: "A compact mark for navigation headers, independent of the full logo above. Falls back to the logo, then a default asset, when not set." }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.iconUrl || branding.logoUrl || "/images/logo.svg", alt: "Current icon", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "icon-upload", disabled: assetBusy !== null, onClick: () => iconFileRef.current?.click(), children: "Upload icon" }), _jsx("input", { ref: iconFileRef, type: "file", accept: "image/*", "aria-label": "Upload icon", className: "hidden", onChange: handleIconFileChange }), branding.iconUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "icon-delete", onClick: () => void runAsset("icon-delete", async () => {
120
- await deleteBrandingIcon();
121
- return { ...branding, iconUrl: undefined };
122
- }), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
123
- await deleteBrandingStylesheet();
124
- return { ...branding, stylesheetUrl: undefined };
125
- }), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
126
- }
@@ -10,6 +10,7 @@ import { listImportRequests, uploadMailboxImport, } from "@rapidmx/react-shared/
10
10
  import { approveErasureRequest, denyErasureRequest, listErasureRequests, } from "@rapidmx/react-shared/mail/erasureRequestApi.js";
11
11
  import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
12
12
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
13
+ import { LoadMoreButton, usePagedList } from "../../shared/components/admin/usePagedList.js";
13
14
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
15
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
15
16
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
@@ -18,43 +19,33 @@ const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]
18
19
  function importFormatFromFilename(filename) {
19
20
  return filename.toLowerCase().endsWith(".pst") ? "pst" : "mbox";
20
21
  }
22
+ /** Request statuses still waiting or running - shown as a neutral in-progress badge, never as success. Includes
23
+ * restapi's `processing` (exports/imports) and `in_progress` (an erasure that is running). */
24
+ const IN_PROGRESS_STATUSES = new Set(["pending", "processing", "in_progress"]);
21
25
  function statusBadgeClass(status) {
22
26
  if (status === "failed" || status === "denied")
23
27
  return "bg-danger-bg text-danger";
24
- if (status === "pending" || status === "processing")
28
+ if (IN_PROGRESS_STATUSES.has(status))
25
29
  return "bg-surface-alt text-text-muted";
26
30
  return "bg-success text-white";
27
31
  }
32
+ /** A status as shown on its badge (`in_progress` -> `in progress`). */
33
+ function statusLabel(status) {
34
+ return status.replace(/_/g, " ");
35
+ }
28
36
  export default function DataRequestsPage(props) {
29
- return (_jsx(AdminShell, { ...props, active: "dataRequests", children: _jsxs("div", { className: "flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Data Requests" }), _jsx("p", { className: "text-sm text-text-muted", children: "Every GDPR export, mailbox-import, and account-erasure request across every mailbox." })] }), _jsx(ExportRequestsSection, {}), _jsx(ImportRequestsSection, {}), _jsx(ErasureRequestsSection, {})] }) }));
37
+ return (_jsx(AdminShell, { ...props, active: "dataRequests", children: _jsxs("div", { className: "flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Data Requests" }), _jsx("p", { className: "text-sm text-text-muted", children: "Every GDPR export, mailbox-import, and account-erasure request across every mailbox, newest first." })] }), _jsx(ExportRequestsSection, {}), _jsx(ImportRequestsSection, {}), _jsx(ErasureRequestsSection, {})] }) }));
30
38
  }
31
39
  function ExportRequestsSection() {
32
- const [requests, setRequests] = useState([]);
33
- const [loading, setLoading] = useState(true);
34
- const [loadError, setLoadError] = useState(null);
40
+ // The create form below isn't gated behind `loading`, so a fast create-then-reload can resolve before the
41
+ // initial mount fetch does - `usePagedList()` only ever applies the most recently started load.
42
+ const list = usePagedList(listExportRequests, "Could not load export requests.");
35
43
  const [mailboxUid, setMailboxUid] = useState("");
36
44
  const [format, setFormat] = useState("json");
37
45
  const [creating, setCreating] = useState(false);
38
46
  const [createError, setCreateError] = useState(null);
39
- // The create form below isn't gated behind `loading`, so a fast create-then-reload can resolve before
40
- // the initial mount fetch does; without this guard the mount fetch's now-stale response would land
41
- // last and silently revert the list. Only the most-recently-issued load's response is ever applied.
42
- const loadSeq = useRef(0);
43
- function loadRequests() {
44
- const seq = ++loadSeq.current;
45
- return listExportRequests()
46
- .then((data) => {
47
- if (seq === loadSeq.current)
48
- setRequests(data);
49
- })
50
- .catch((err) => {
51
- if (seq === loadSeq.current)
52
- setLoadError(err instanceof ApiRequestError ? err.message : "Could not load export requests.");
53
- });
54
- }
55
47
  useEffect(() => {
56
- loadRequests().finally(() => setLoading(false));
57
- // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ void list.reload();
58
49
  }, []);
59
50
  async function handleCreate(e) {
60
51
  e.preventDefault();
@@ -63,7 +54,7 @@ function ExportRequestsSection() {
63
54
  try {
64
55
  await createExportRequest({ format, mailboxUid: mailboxUid.trim() });
65
56
  setMailboxUid("");
66
- await loadRequests();
57
+ await list.reload();
67
58
  }
68
59
  catch (err) {
69
60
  setCreateError(err instanceof ApiRequestError ? err.message : "Could not start this export.");
@@ -72,12 +63,12 @@ function ExportRequestsSection() {
72
63
  setCreating(false);
73
64
  }
74
65
  }
75
- return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Export requests" }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-4 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Export mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value) })] }), _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: INPUT_CLASS, 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 || !mailboxUid.trim(), children: "Create 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." })) : (_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.mailboxUid, " \u00B7 ", 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))) }))] }));
66
+ return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Export requests" }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-4 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Export mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value) })] }), _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: INPUT_CLASS, 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 || !mailboxUid.trim(), children: "Create export" })] }), createError && _jsx(Alert, { children: createError }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.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.mailboxUid, " \u00B7 ", 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))) })), _jsx(LoadMoreButton, { list: list, label: "Load more export requests" })] }));
76
67
  }
77
68
  function ImportRequestsSection() {
78
- const [requests, setRequests] = useState([]);
79
- const [loading, setLoading] = useState(true);
80
- const [loadError, setLoadError] = useState(null);
69
+ // Same stale-response guard as ExportRequestsSection - the Upload button is enabled independently of this
70
+ // section's own `loading`.
71
+ const list = usePagedList(listImportRequests, "Could not load import requests.");
81
72
  const [mailboxUid, setMailboxUid] = useState("");
82
73
  const [folders, setFolders] = useState([]);
83
74
  const [targetFolderUid, setTargetFolderUid] = useState("");
@@ -85,25 +76,8 @@ function ImportRequestsSection() {
85
76
  const [uploading, setUploading] = useState(false);
86
77
  const [uploadError, setUploadError] = useState(null);
87
78
  const fileInputRef = useRef(null);
88
- // Same stale-response guard as ExportRequestsSection's loadRequests() - the Upload button is enabled
89
- // independently of this section's own `loading`, so a fast upload-then-reload can resolve before the
90
- // initial mount fetch does.
91
- const loadSeq = useRef(0);
92
- function loadRequests() {
93
- const seq = ++loadSeq.current;
94
- return listImportRequests()
95
- .then((data) => {
96
- if (seq === loadSeq.current)
97
- setRequests(data);
98
- })
99
- .catch((err) => {
100
- if (seq === loadSeq.current)
101
- setLoadError(err instanceof ApiRequestError ? err.message : "Could not load import requests.");
102
- });
103
- }
104
79
  useEffect(() => {
105
- loadRequests().finally(() => setLoading(false));
106
- // eslint-disable-next-line react-hooks/exhaustive-deps
80
+ void list.reload();
107
81
  }, []);
108
82
  function handleLoadFolders() {
109
83
  const uid = mailboxUid.trim();
@@ -130,46 +104,48 @@ function ImportRequestsSection() {
130
104
  setUploadError(null);
131
105
  setUploading(true);
132
106
  uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid.trim() })
133
- .then(() => loadRequests())
107
+ .then(() => list.reload())
134
108
  .catch((err) => setUploadError(err instanceof ApiRequestError ? err.message : "Could not upload this file."))
135
109
  .finally(() => setUploading(false));
136
110
  }
137
- return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Import requests" }), _jsxs("div", { className: "flex items-end gap-3 mb-2 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Import mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value), onBlur: handleLoadFolders })] }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Import destination folder", className: INPUT_CLASS, 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", 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 })] }), folderError && _jsx(Alert, { children: folderError }), 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." })) : (_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.mailboxUid, " \u00B7 ", 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))) }))] }));
111
+ return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Import requests" }), _jsxs("div", { className: "flex items-end gap-3 mb-2 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Import mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value), onBlur: handleLoadFolders })] }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Import destination folder", className: INPUT_CLASS, 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", 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 })] }), folderError && _jsx(Alert, { children: folderError }), uploadError && _jsx(Alert, { children: uploadError }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.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.mailboxUid, " \u00B7 ", 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))) })), _jsx(LoadMoreButton, { list: list, label: "Load more import requests" })] }));
138
112
  }
139
113
  function ErasureRequestsSection() {
140
- const [requests, setRequests] = useState([]);
141
- const [loading, setLoading] = useState(true);
142
- const [loadError, setLoadError] = useState(null);
143
- const [actionErrors, setActionErrors] = useState({});
144
- const [actingOn, setActingOn] = useState({});
114
+ const list = usePagedList(listErasureRequests, "Could not load erasure requests.");
115
+ const [approveTarget, setApproveTarget] = useState(null);
116
+ const [approving, setApproving] = useState(false);
117
+ const [approveError, setApproveError] = useState(null);
145
118
  const [denyTarget, setDenyTarget] = useState(null);
146
119
  const [denyReason, setDenyReason] = useState("");
147
120
  const [denying, setDenying] = useState(false);
148
121
  const [denyError, setDenyError] = useState(null);
149
- function loadRequests() {
150
- return listErasureRequests()
151
- .then(setRequests)
152
- .catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load erasure requests."));
153
- }
154
122
  useEffect(() => {
155
- loadRequests().finally(() => setLoading(false));
156
- // eslint-disable-next-line react-hooks/exhaustive-deps
123
+ void list.reload();
157
124
  }, []);
158
- async function handleApprove(request) {
159
- setActingOn((prev) => ({ ...prev, [request.uid]: true }));
160
- setActionErrors((prev) => ({ ...prev, [request.uid]: "" }));
125
+ function closeApproveModal() {
126
+ setApproveTarget(null);
127
+ setApproveError(null);
128
+ }
129
+ // Escape/Close are ignored while a decision is being sent, so its result (or error) can't land on a modal
130
+ // reopened for a different request.
131
+ function dismissApproveModal() {
132
+ if (!approving)
133
+ closeApproveModal();
134
+ }
135
+ async function handleApprove() {
136
+ setApproveError(null);
137
+ setApproving(true);
161
138
  try {
162
- await approveErasureRequest(request.uid);
163
- await loadRequests();
139
+ // Only invoked from the approve modal below, which only renders once approveTarget is set.
140
+ await approveErasureRequest(approveTarget.uid);
141
+ closeApproveModal();
142
+ await list.reload();
164
143
  }
165
144
  catch (err) {
166
- setActionErrors((prev) => ({
167
- ...prev,
168
- [request.uid]: err instanceof ApiRequestError ? err.message : "Could not approve this request.",
169
- }));
145
+ setApproveError(err instanceof ApiRequestError ? err.message : "Could not approve this request.");
170
146
  }
171
147
  finally {
172
- setActingOn((prev) => ({ ...prev, [request.uid]: false }));
148
+ setApproving(false);
173
149
  }
174
150
  }
175
151
  function closeDenyModal() {
@@ -177,6 +153,10 @@ function ErasureRequestsSection() {
177
153
  setDenyReason("");
178
154
  setDenyError(null);
179
155
  }
156
+ function dismissDenyModal() {
157
+ if (!denying)
158
+ closeDenyModal();
159
+ }
180
160
  async function handleDeny() {
181
161
  setDenyError(null);
182
162
  setDenying(true);
@@ -184,7 +164,7 @@ function ErasureRequestsSection() {
184
164
  // Only invoked from the deny modal below, which only renders once denyTarget is set.
185
165
  await denyErasureRequest(denyTarget.uid, denyReason);
186
166
  closeDenyModal();
187
- await loadRequests();
167
+ await list.reload();
188
168
  }
189
169
  catch (err) {
190
170
  setDenyError(err instanceof ApiRequestError ? err.message : "Could not deny this request.");
@@ -193,5 +173,5 @@ function ErasureRequestsSection() {
193
173
  setDenying(false);
194
174
  }
195
175
  }
196
- return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Erasure requests" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Self-service only \u2014 a mailbox owner requests their own account's erasure; there is no admin-initiated path here, only review." }), 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 erasure requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: requests.map((request) => (_jsxs("li", { className: "border border-border rounded-sm p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "text-sm", children: [request.mailboxUid, " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "denied" && request.reason && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.reason] })), request.status === "completed" && request.purgedCount !== undefined && (_jsxs("span", { className: "text-text-muted", children: [" \u2014 ", request.purgedCount, " records purged"] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: request.status })] }), actionErrors[request.uid] && _jsx(Alert, { children: actionErrors[request.uid] }), request.status === "pending" && (_jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "button", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => handleApprove(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: actingOn[request.uid], onClick: () => setDenyTarget(request), children: "Deny" })] }))] }, request.uid))) })), _jsxs(Modal, { open: denyTarget !== null, onClose: closeDenyModal, title: "Deny erasure request", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-4", children: [_jsx("span", { className: "font-semibold", children: "Reason" }), _jsx("input", { "aria-label": "Denial reason", className: INPUT_CLASS, value: denyReason, onChange: (e) => setDenyReason(e.target.value) })] }), denyError && _jsx(Alert, { children: denyError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: denying, onClick: closeDenyModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: denying, disabled: denying || !denyReason.trim(), onClick: handleDeny, children: "Deny" })] })] })] }));
176
+ return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Erasure requests" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Self-service only \u2014 a mailbox owner requests their own account's erasure; there is no admin-initiated path here, only review." }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No erasure requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.map((request) => (_jsxs("li", { className: "border border-border rounded-sm p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "text-sm", children: [request.mailboxUid, " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "denied" && request.reason && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.reason] })), request.status === "completed" && request.purgedCount !== undefined && (_jsxs("span", { className: "text-text-muted", children: [" \u2014 ", request.purgedCount, " records purged"] }))] }), _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 === "pending" && (_jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setApproveTarget(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(request), children: "Deny" })] }))] }, request.uid))) })), _jsx(LoadMoreButton, { list: list, label: "Load more erasure requests" }), _jsxs(Modal, { open: approveTarget !== null, onClose: dismissApproveModal, title: "Approve erasure request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Approving permanently erases the mailbox ", _jsx("strong", { className: "break-all", children: approveTarget?.mailboxUid }), " ", "and everything in it - mail, contacts, calendars, tasks, and notes."] }), _jsx("p", { className: "text-sm font-semibold text-danger mb-4", children: "This is irreversible and cannot be undone." }), approveError && _jsx(Alert, { children: approveError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: approving, onClick: closeApproveModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: approving, disabled: approving, onClick: handleApprove, children: "Erase mailbox" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: dismissDenyModal, title: "Deny erasure request", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-4", children: [_jsx("span", { className: "font-semibold", children: "Reason" }), _jsx("input", { "aria-label": "Denial reason", className: INPUT_CLASS, value: denyReason, onChange: (e) => setDenyReason(e.target.value) })] }), denyError && _jsx(Alert, { children: denyError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: denying, onClick: closeDenyModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: denying, disabled: denying || !denyReason.trim(), onClick: handleDeny, children: "Deny" })] })] })] }));
197
177
  }
@@ -30,5 +30,5 @@ function DistributionListDetailContent({ uid }) {
30
30
  if (error || !list) {
31
31
  return _jsx(Alert, { children: error ?? "Distribution list not found." });
32
32
  }
33
- return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/distribution-lists", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All distribution lists" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: list.primarySmtpAddress })] }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Name" }), _jsx("dd", { children: list.name }), _jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: list.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: list.ownerUserUid ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: list.aliasAddresses && list.aliasAddresses.length > 0 ? list.aliasAddresses.join(", ") : "None" }), _jsx("dt", { className: "text-text-muted", children: "Restrict senders" }), _jsx("dd", { children: list.restrictSenders ? "Only members may send" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Created" }), _jsx("dd", { children: new Date(list.dateCreated).toLocaleString() })] }) }), _jsx(MemberListCard, { list: list, onUpdate: setList })] }));
33
+ return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/distribution-lists", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All distribution lists" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: list.primarySmtpAddress })] }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Name" }), _jsx("dd", { children: list.name }), _jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: list.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: list.ownerUserUid ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: list.aliasAddresses && list.aliasAddresses.length > 0 ? list.aliasAddresses.join(", ") : "None" }), _jsx("dt", { className: "text-text-muted", children: "Restrict senders" }), _jsxs("dd", { children: [list.restrictSenders ? "Only members may send" : "No", list.restrictSenders ? (_jsxs("p", { role: "status", className: "mt-1.5 py-2 px-3 rounded-sm text-xs bg-warning/15 text-text", children: ["Members are recognized only by a verified (DKIM) signature, which requires the mail server\u2019s Authentication-Results configuration (the trusted authserv id,", " ", _jsx("code", { children: "mail:security:trusted_authserv_id" }), "). If that isn\u2019t configured, all mail sent to this list is dropped."] })) : (_jsx("p", { className: "mt-1 text-xs text-text-muted", children: "Restricting senders to members requires the mail server\u2019s Authentication-Results configuration (the trusted authserv id); without it, all mail to the list is dropped." }))] }), _jsx("dt", { className: "text-text-muted", children: "Created" }), _jsx("dd", { children: new Date(list.dateCreated).toLocaleString() })] }) }), _jsx(MemberListCard, { list: list, onUpdate: setList })] }));
34
34
  }
@@ -3,72 +3,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useState } from "react";
6
+ import { useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
- import { deleteDomain, getDnsSetup, getDomain, verifyDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
8
+ import { deleteDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
9
9
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
+ import DomainDnsSetup from "../../shared/components/admin/settings/DomainDnsSetup.js";
10
11
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
11
12
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
12
13
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
13
- const RECORD_TYPE_LABELS = {
14
- ownership: "Ownership (TXT)",
15
- mx: "MX",
16
- spf: "SPF",
17
- dkim: "DKIM",
18
- dmarc: "DMARC",
19
- };
20
14
  export default function DomainDetailPage(props) {
21
15
  return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(DomainDetailContent, { uid: props.params.uid }) }));
22
16
  }
23
17
  function DomainDetailContent({ uid }) {
24
18
  const [domain, setDomain] = useState(null);
25
- const [dnsSetup, setDnsSetup] = useState([]);
26
- const [loading, setLoading] = useState(true);
27
- const [error, setError] = useState(null);
28
- const [verifying, setVerifying] = useState(false);
29
- const [copied, setCopied] = useState(false);
30
19
  const [confirmingDelete, setConfirmingDelete] = useState(false);
31
20
  const [deleting, setDeleting] = useState(false);
32
21
  const [deleteError, setDeleteError] = useState(null);
33
- function reload(id) {
34
- setLoading(true);
35
- setError(null);
36
- Promise.all([getDomain(id), getDnsSetup(id)])
37
- .then(([d, checks]) => {
38
- setDomain(d);
39
- setDnsSetup(checks);
40
- })
41
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this domain."))
42
- .finally(() => setLoading(false));
43
- }
44
- useEffect(() => {
45
- reload(uid);
46
- }, [uid]);
47
- // Only ever invoked from the "Verify now" button below, which itself only renders once `domain` is
48
- // resolved (children only render once loaded — see the early returns above) — the non-null assertion
49
- // reflects that real invariant, not an unchecked assumption. Matches `QuarantineContent.handleRelease`'s
50
- // identical pattern.
51
- async function handleVerify() {
52
- setVerifying(true);
53
- setError(null);
54
- try {
55
- await verifyDomain(domain.uid);
56
- reload(domain.uid);
57
- }
58
- catch (err) {
59
- setError(err instanceof ApiRequestError ? err.message : "Could not verify this domain.");
60
- }
61
- finally {
62
- setVerifying(false);
63
- }
64
- }
65
- // Only ever invoked from the "Delete domain" confirmation modal below, which itself only renders once
66
- // `domain` is resolved (children only render once loaded — see the early returns above) — the non-null
67
- // assertion reflects that real invariant, not an unchecked assumption. Matches `handleVerify`'s identical
68
- // pattern above.
69
22
  function closeDeleteModal() {
70
23
  setConfirmingDelete(false);
71
24
  }
25
+ // Only ever invoked from the "Delete domain" confirmation modal, which only renders once `domain` is loaded.
72
26
  async function handleDelete() {
73
27
  setDeleting(true);
74
28
  setDeleteError(null);
@@ -81,22 +35,5 @@ function DomainDetailContent({ uid }) {
81
35
  setDeleting(false);
82
36
  }
83
37
  }
84
- async function handleCopy(value) {
85
- try {
86
- await navigator.clipboard.writeText(value);
87
- setCopied(true);
88
- setTimeout(() => setCopied(false), 2000);
89
- }
90
- catch {
91
- // Clipboard access can be denied by the browser — the value is still selectable/copyable by hand.
92
- }
93
- }
94
- if (loading) {
95
- return _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" });
96
- }
97
- if (error || !domain) {
98
- return _jsx(Alert, { children: error ?? "Domain not found." });
99
- }
100
- const ownershipCheck = dnsSetup.find((c) => c.type === "ownership");
101
- return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/domains", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All domains" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: domain.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete domain" })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "flex-1 text-xs bg-surface-alt border border-border rounded-sm py-2 px-3 overflow-x-auto whitespace-nowrap", children: ownershipCheck?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}` }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => handleCopy(ownershipCheck?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`), children: copied ? "Copied" : "Copy" })] }), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Recommended value"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: RECORD_TYPE_LABELS[check.type] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: _jsx("code", { className: "text-xs", children: check.recommendedValue ?? "—" }) })] }, check.type))) })] }) })] })), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete domain", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: domain.name }), "? This cannot be undone."] }), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: closeDeleteModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] })] }));
38
+ return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [domain && (_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/domains", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All domains" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: domain.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete domain" })] })), _jsx(DomainDnsSetup, { uid: uid, onLoaded: setDomain }), domain && (_jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete domain", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: domain.name }), "? This cannot be undone."] }), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: closeDeleteModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] }))] }));
102
39
  }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getEncryptionPolicy } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
3
+ import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
4
+ import EncryptionPolicyForm from "../../shared/components/admin/settings/EncryptionPolicyForm.js";
5
+ import LoadedSettingsForm from "../../shared/components/admin/settings/LoadedSettingsForm.js";
6
+ export default function EncryptionPolicyPage(props) {
7
+ return (_jsx(AdminShell, { ...props, active: "encryptionPolicy", children: _jsx(LoadedSettingsForm, { load: getEncryptionPolicy, loadErrorMessage: "Could not load the encryption policy.", children: (policy, onChange) => _jsx(EncryptionPolicyForm, { policy: policy, onChange: onChange }) }) }));
8
+ }