@rapidmx/web-client 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -2
- package/apps/admin/_layout.tsx +8 -2
- package/apps/admin/index.tsx +7 -1
- package/apps/admin/ingest-queue/index.tsx +2 -1
- package/apps/admin/mailboxes/[uid].tsx +23 -10
- package/apps/admin/quarantine/index.tsx +2 -1
- package/apps/admin/signing-certificates/index.tsx +343 -0
- package/apps/escrow/_layout.tsx +8 -2
- package/apps/shared/appearance/AppearanceForm.tsx +392 -0
- package/apps/shared/appearance/AppearanceHead.tsx +49 -0
- package/apps/shared/appearance/AppearanceProvider.tsx +470 -0
- package/apps/shared/appearance/ColorField.tsx +107 -0
- package/apps/shared/appearance/appearanceCache.ts +106 -0
- package/apps/shared/appearance/appearanceContext.ts +77 -0
- package/apps/shared/appearance/bootScript.ts +35 -0
- package/apps/shared/appearance/color.ts +169 -0
- package/apps/shared/appearance/photo.ts +88 -0
- package/apps/shared/appearance/resolvedTheme.ts +67 -0
- package/apps/shared/appearance/theme.ts +355 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +22 -10
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +107 -60
- package/apps/shared/components/admin/settings/BrandingForm.tsx +24 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +228 -25
- package/apps/shared/components/admin/setup/SetupWizard.tsx +2 -1
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +4 -5
- package/apps/shared/components/contacts/ContactForm.tsx +11 -5
- package/apps/shared/components/contacts/ContactsSidebar.tsx +5 -3
- package/apps/shared/components/contacts/ContactsToolbar.tsx +25 -52
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +10 -5
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +13 -6
- package/apps/shared/components/layout/AppShell.tsx +98 -32
- package/apps/shared/components/layout/BrandingChrome.tsx +200 -13
- package/apps/shared/components/layout/RailIcon.tsx +100 -0
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -0
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -0
- package/apps/shared/components/layout/UserMenu.tsx +75 -6
- package/apps/shared/components/mail/ConversationList.tsx +4 -2
- package/apps/shared/components/mail/ConversationThreadPane.tsx +487 -475
- package/apps/shared/components/mail/LazyReadingPane.tsx +14 -16
- package/apps/shared/components/mail/MailSelectionBar.tsx +2 -10
- package/apps/shared/components/mail/MessageDetailPane.tsx +366 -287
- package/apps/shared/components/mail/OutboxBadge.tsx +44 -0
- package/apps/shared/components/mail/OutboxRowStatus.tsx +31 -0
- package/apps/shared/components/mail/compose/ComposeContext.tsx +35 -2
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +239 -437
- package/apps/shared/components/mail/compose/encryptionRequirement.ts +86 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +39 -25
- package/apps/shared/components/mail/reading/EncryptedBody.tsx +65 -0
- package/apps/shared/components/mail/reading/EncryptedPreview.tsx +35 -0
- package/apps/shared/components/mail/reading/MessageBody.tsx +240 -0
- package/apps/shared/components/mail/reading/MessageCard.tsx +174 -0
- package/apps/shared/components/mail/reading/bodyContent.ts +100 -0
- package/apps/shared/components/mail/reading/bodyHtml.ts +298 -0
- package/apps/shared/components/mail/reading/color.ts +245 -0
- package/apps/shared/components/mail/reading/frameControl.ts +142 -0
- package/apps/shared/components/mail/reading/frameDocument.ts +123 -0
- package/apps/shared/components/mail/reading/safeDocument.ts +24 -0
- package/apps/shared/components/mail/reading/themeAdaptation.ts +221 -0
- package/apps/shared/components/mail/reading/themeSurface.ts +90 -0
- package/apps/shared/components/mail/reading/viewOriginal.ts +38 -0
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/components/sharing/PrincipalPicker.tsx +145 -0
- package/apps/shared/components/tasks/TasksSidebar.tsx +5 -3
- package/apps/shared/components/tasks/layout/TasksShell.tsx +9 -5
- package/apps/shared/mail/folderCounts.ts +9 -3
- package/apps/shared/mail/folderTree.ts +143 -0
- package/apps/shared/mail/outbox/UnlockBridge.tsx +23 -0
- package/apps/shared/mail/outbox/composeBridge.ts +69 -0
- package/apps/shared/mail/outbox/outboxState.ts +88 -0
- package/apps/shared/mail/outbox/pendingSends.ts +146 -0
- package/apps/shared/mail/outbox/sendDecision.ts +157 -0
- package/apps/shared/mail/outbox/sendJob.ts +427 -0
- package/apps/shared/mail/outbox/sendOutcomes.ts +146 -0
- package/apps/shared/mail/outbox/sendState.ts +35 -0
- package/apps/shared/mail/outbox/useOutboxStatus.ts +61 -0
- package/apps/shared/mail/useMailConnection.ts +81 -28
- package/apps/shared/mail/useMailLiveUpdates.ts +55 -10
- package/apps/shared/mail/useNewMailNotifications.ts +47 -32
- package/apps/shared/navigation/AppRouter.tsx +2 -2
- package/apps/shared/notifications/NotificationCenter.tsx +215 -0
- package/apps/shared/notifications/NotificationHistoryDialog.tsx +90 -0
- package/apps/shared/notifications/apiErrors.ts +98 -0
- package/apps/shared/notifications/headerOffset.ts +35 -0
- package/apps/shared/notifications/pushStatus.ts +56 -0
- package/apps/shared/notifications/store.ts +549 -0
- package/apps/shared/notifications/systemErrors.ts +63 -0
- package/apps/shared/notifications/useNotifications.ts +41 -0
- package/apps/shared/signing/enrollmentStorage.ts +33 -0
- package/apps/shared/signing/enrollmentTracker.ts +385 -0
- package/apps/shared/signing/enrollmentView.ts +251 -0
- package/apps/shared/signing/signingInfo.ts +58 -0
- package/apps/shared/signing/useNow.ts +19 -0
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -0
- package/apps/shared/styles/app.css +80 -0
- package/apps/www/_layout.tsx +8 -2
- package/apps/www/_routes.ts +1 -0
- package/apps/www/calendar/index.tsx +9 -14
- package/apps/www/contacts/[uid].tsx +5 -8
- package/apps/www/contacts/index.tsx +587 -584
- package/apps/www/index.tsx +38 -27
- package/apps/www/messages/[uid].tsx +3 -1
- package/apps/www/settings/appearance/index.tsx +21 -0
- package/apps/www/settings/auto-reply/index.tsx +3 -7
- package/apps/www/settings/encryption/index.tsx +151 -116
- package/apps/www/settings/filters/[uid].tsx +3 -1
- package/apps/www/settings/filters/new/index.tsx +3 -1
- package/apps/www/settings/privacy/index.tsx +3 -8
- package/apps/www/settings/read-receipts/index.tsx +3 -7
- package/apps/www/settings/sharing/index.tsx +44 -65
- package/apps/www/settings/signatures/[uid].tsx +3 -1
- package/apps/www/settings/signatures/new/index.tsx +3 -2
- package/apps/www/tasks/index.tsx +11 -16
- package/dist/apps/admin/_layout.d.ts +5 -1
- package/dist/apps/admin/_layout.js +3 -2
- package/dist/apps/admin/index.js +3 -2
- package/dist/apps/admin/ingest-queue/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +9 -6
- package/dist/apps/admin/quarantine/index.js +2 -1
- package/dist/apps/admin/signing-certificates/index.d.ts +3 -0
- package/dist/apps/admin/signing-certificates/index.js +161 -0
- package/dist/apps/escrow/_layout.d.ts +5 -1
- package/dist/apps/escrow/_layout.js +3 -2
- package/dist/apps/shared/appearance/AppearanceForm.d.ts +7 -0
- package/dist/apps/shared/appearance/AppearanceForm.js +131 -0
- package/dist/apps/shared/appearance/AppearanceHead.d.ts +15 -0
- package/dist/apps/shared/appearance/AppearanceHead.js +31 -0
- package/dist/apps/shared/appearance/AppearanceProvider.d.ts +32 -0
- package/dist/apps/shared/appearance/AppearanceProvider.js +390 -0
- package/dist/apps/shared/appearance/ColorField.d.ts +22 -0
- package/dist/apps/shared/appearance/ColorField.js +40 -0
- package/dist/apps/shared/appearance/appearanceCache.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceCache.js +80 -0
- package/dist/apps/shared/appearance/appearanceContext.d.ts +45 -0
- package/dist/apps/shared/appearance/appearanceContext.js +29 -0
- package/dist/apps/shared/appearance/bootScript.d.ts +16 -0
- package/dist/apps/shared/appearance/bootScript.js +33 -0
- package/dist/apps/shared/appearance/color.d.ts +65 -0
- package/dist/apps/shared/appearance/color.js +132 -0
- package/dist/apps/shared/appearance/photo.d.ts +17 -0
- package/dist/apps/shared/appearance/photo.js +79 -0
- package/dist/apps/shared/appearance/resolvedTheme.d.ts +21 -0
- package/dist/apps/shared/appearance/resolvedTheme.js +55 -0
- package/dist/apps/shared/appearance/theme.d.ts +111 -0
- package/dist/apps/shared/appearance/theme.js +251 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +16 -6
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +10 -5
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +49 -34
- package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +2 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +7 -1
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +106 -20
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +2 -1
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +8 -8
- package/dist/apps/shared/components/contacts/ContactForm.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +4 -2
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +24 -9
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -1
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +7 -4
- package/dist/apps/shared/components/layout/AppShell.d.ts +6 -1
- package/dist/apps/shared/components/layout/AppShell.js +59 -13
- package/dist/apps/shared/components/layout/BrandingChrome.d.ts +69 -10
- package/dist/apps/shared/components/layout/BrandingChrome.js +140 -12
- package/dist/apps/shared/components/layout/RailIcon.d.ts +21 -0
- package/dist/apps/shared/components/layout/RailIcon.js +85 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.d.ts +55 -0
- package/dist/apps/shared/components/layout/ResponsiveToolbar.js +199 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.d.ts +9 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.js +54 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +7 -1
- package/dist/apps/shared/components/layout/UserMenu.js +41 -7
- package/dist/apps/shared/components/mail/ConversationList.js +3 -2
- package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +2 -1
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +42 -20
- package/dist/apps/shared/components/mail/LazyReadingPane.js +9 -8
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +3 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +2 -3
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +18 -7
- package/dist/apps/shared/components/mail/MessageDetailPane.js +86 -41
- package/dist/apps/shared/components/mail/OutboxBadge.d.ts +17 -0
- package/dist/apps/shared/components/mail/OutboxBadge.js +18 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.d.ts +10 -0
- package/dist/apps/shared/components/mail/OutboxRowStatus.js +19 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +8 -0
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +28 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +2 -1
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +229 -328
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +49 -0
- package/dist/apps/shared/components/mail/compose/encryptionRequirement.js +35 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +2 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +20 -14
- package/dist/apps/shared/components/mail/reading/EncryptedBody.d.ts +21 -0
- package/dist/apps/shared/components/mail/reading/EncryptedBody.js +22 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.d.ts +18 -0
- package/dist/apps/shared/components/mail/reading/EncryptedPreview.js +22 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.d.ts +32 -0
- package/dist/apps/shared/components/mail/reading/MessageBody.js +136 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.d.ts +62 -0
- package/dist/apps/shared/components/mail/reading/MessageCard.js +58 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.d.ts +38 -0
- package/dist/apps/shared/components/mail/reading/bodyContent.js +84 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +47 -0
- package/dist/apps/shared/components/mail/reading/bodyHtml.js +267 -0
- package/dist/apps/shared/components/mail/reading/color.d.ts +63 -0
- package/dist/apps/shared/components/mail/reading/color.js +201 -0
- package/dist/apps/shared/components/mail/reading/frameControl.d.ts +33 -0
- package/dist/apps/shared/components/mail/reading/frameControl.js +120 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.d.ts +52 -0
- package/dist/apps/shared/components/mail/reading/frameDocument.js +98 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.d.ts +3 -0
- package/dist/apps/shared/components/mail/reading/safeDocument.js +12 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/themeAdaptation.js +144 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.d.ts +17 -0
- package/dist/apps/shared/components/mail/reading/themeSurface.js +83 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.d.ts +9 -0
- package/dist/apps/shared/components/mail/reading/viewOriginal.js +32 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.d.ts +32 -0
- package/dist/apps/shared/components/settings/SigningCertificateCard.js +98 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.d.ts +26 -0
- package/dist/apps/shared/components/sharing/PrincipalPicker.js +77 -0
- package/dist/apps/shared/components/tasks/TasksSidebar.js +4 -2
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -1
- package/dist/apps/shared/mail/folderCounts.d.ts +1 -1
- package/dist/apps/shared/mail/folderCounts.js +9 -3
- package/dist/apps/shared/mail/folderTree.d.ts +55 -0
- package/dist/apps/shared/mail/folderTree.js +116 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.d.ts +5 -0
- package/dist/apps/shared/mail/outbox/UnlockBridge.js +18 -0
- package/dist/apps/shared/mail/outbox/composeBridge.d.ts +34 -0
- package/dist/apps/shared/mail/outbox/composeBridge.js +32 -0
- package/dist/apps/shared/mail/outbox/outboxState.d.ts +41 -0
- package/dist/apps/shared/mail/outbox/outboxState.js +42 -0
- package/dist/apps/shared/mail/outbox/pendingSends.d.ts +39 -0
- package/dist/apps/shared/mail/outbox/pendingSends.js +109 -0
- package/dist/apps/shared/mail/outbox/sendDecision.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/sendDecision.js +94 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +68 -0
- package/dist/apps/shared/mail/outbox/sendJob.js +319 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +3 -0
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +141 -0
- package/dist/apps/shared/mail/outbox/sendState.d.ts +25 -0
- package/dist/apps/shared/mail/outbox/sendState.js +8 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.d.ts +11 -0
- package/dist/apps/shared/mail/outbox/useOutboxStatus.js +49 -0
- package/dist/apps/shared/mail/useMailConnection.d.ts +9 -8
- package/dist/apps/shared/mail/useMailConnection.js +64 -22
- package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +17 -4
- package/dist/apps/shared/mail/useMailLiveUpdates.js +36 -10
- package/dist/apps/shared/mail/useNewMailNotifications.d.ts +4 -10
- package/dist/apps/shared/mail/useNewMailNotifications.js +43 -23
- package/dist/apps/shared/navigation/AppRouter.js +2 -2
- package/dist/apps/shared/notifications/NotificationCenter.d.ts +19 -0
- package/dist/apps/shared/notifications/NotificationCenter.js +90 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.d.ts +10 -0
- package/dist/apps/shared/notifications/NotificationHistoryDialog.js +36 -0
- package/dist/apps/shared/notifications/apiErrors.d.ts +19 -0
- package/dist/apps/shared/notifications/apiErrors.js +86 -0
- package/dist/apps/shared/notifications/headerOffset.d.ts +8 -0
- package/dist/apps/shared/notifications/headerOffset.js +33 -0
- package/dist/apps/shared/notifications/pushStatus.d.ts +9 -0
- package/dist/apps/shared/notifications/pushStatus.js +54 -0
- package/dist/apps/shared/notifications/store.d.ts +136 -0
- package/dist/apps/shared/notifications/store.js +416 -0
- package/dist/apps/shared/notifications/systemErrors.d.ts +9 -0
- package/dist/apps/shared/notifications/systemErrors.js +58 -0
- package/dist/apps/shared/notifications/useNotifications.d.ts +17 -0
- package/dist/apps/shared/notifications/useNotifications.js +18 -0
- package/dist/apps/shared/signing/enrollmentStorage.d.ts +10 -0
- package/dist/apps/shared/signing/enrollmentStorage.js +33 -0
- package/dist/apps/shared/signing/enrollmentTracker.d.ts +73 -0
- package/dist/apps/shared/signing/enrollmentTracker.js +307 -0
- package/dist/apps/shared/signing/enrollmentView.d.ts +57 -0
- package/dist/apps/shared/signing/enrollmentView.js +214 -0
- package/dist/apps/shared/signing/signingInfo.d.ts +11 -0
- package/dist/apps/shared/signing/signingInfo.js +48 -0
- package/dist/apps/shared/signing/useNow.d.ts +2 -0
- package/dist/apps/shared/signing/useNow.js +18 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.d.ts +16 -0
- package/dist/apps/shared/signing/useSigningEnrollmentWatcher.js +87 -0
- package/dist/apps/shared/styles/app.css +80 -0
- package/dist/apps/www/_layout.d.ts +5 -1
- package/dist/apps/www/_layout.js +3 -2
- package/dist/apps/www/_routes.js +1 -0
- package/dist/apps/www/calendar/index.js +3 -4
- package/dist/apps/www/contacts/[uid].js +5 -7
- package/dist/apps/www/contacts/index.js +14 -21
- package/dist/apps/www/index.js +37 -19
- package/dist/apps/www/messages/[uid].js +3 -1
- package/dist/apps/www/settings/appearance/index.d.ts +4 -0
- package/dist/apps/www/settings/appearance/index.js +13 -0
- package/dist/apps/www/settings/auto-reply/index.js +3 -6
- package/dist/apps/www/settings/encryption/index.js +103 -95
- package/dist/apps/www/settings/filters/[uid].js +3 -1
- package/dist/apps/www/settings/filters/new/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +5 -8
- package/dist/apps/www/settings/read-receipts/index.js +3 -6
- package/dist/apps/www/settings/sharing/index.js +19 -33
- package/dist/apps/www/settings/signatures/[uid].js +3 -1
- package/dist/apps/www/settings/signatures/new/index.js +3 -2
- package/dist/apps/www/tasks/index.js +8 -14
- package/package.json +2 -2
- package/apps/shared/components/mail/NewMailToasts.tsx +0 -141
- package/apps/shared/components/mail/compose/SendFailureAlert.tsx +0 -48
- package/dist/apps/shared/components/mail/NewMailToasts.d.ts +0 -19
- package/dist/apps/shared/components/mail/NewMailToasts.js +0 -55
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.d.ts +0 -14
- package/dist/apps/shared/components/mail/compose/SendFailureAlert.js +0 -11
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
+
import React, { FormEvent, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
6
6
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
7
|
import {
|
|
8
8
|
addPlugin,
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
PluginChangePlan,
|
|
18
18
|
PluginInstanceStatus,
|
|
19
19
|
PluginNamespace,
|
|
20
|
+
PluginPurgeInfo,
|
|
21
|
+
PluginPurgeState,
|
|
20
22
|
PluginRegistryLookup,
|
|
21
23
|
PluginSearchResult,
|
|
22
24
|
PluginSettingDefinition,
|
|
@@ -24,12 +26,16 @@ import {
|
|
|
24
26
|
PluginStatus,
|
|
25
27
|
PluginUpdateInfo,
|
|
26
28
|
removePlugin,
|
|
29
|
+
RemovePluginResult,
|
|
30
|
+
retryPluginPurge,
|
|
27
31
|
searchPlugins,
|
|
28
32
|
updatePlugin,
|
|
29
33
|
} from "@rapidmx/react-shared/admin/pluginsApi.js";
|
|
30
34
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
31
35
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
32
36
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
37
|
+
import { notify } from "../../../notifications/store.js";
|
|
38
|
+
import { isElevationRequired } from "../elevation.js";
|
|
33
39
|
|
|
34
40
|
const INPUT_CLASS =
|
|
35
41
|
"w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
@@ -57,6 +63,23 @@ async function attempt(action: () => Promise<void>, failure: string): Promise<st
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
/** The classes of a confirm button that deletes data, the same as the app's other destructive buttons. */
|
|
67
|
+
const DANGER_BUTTON_CLASS = "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger";
|
|
68
|
+
|
|
69
|
+
/** Shown when the server wants the administrator to have confirmed their identity recently (`api-104`). */
|
|
70
|
+
const ELEVATION_MESSAGE = "Deleting data needs you to have recently confirmed your identity. Reload this page, or sign in again, then try once more.";
|
|
71
|
+
|
|
72
|
+
/** What an administrator sees an uninstalled plugin's data deletion as, without the display name of a plugin that's gone. */
|
|
73
|
+
const purgeName = (purge: PluginPurgeInfo): string => purge.displayName ?? purge.name;
|
|
74
|
+
|
|
75
|
+
/** Why a data deletion failed: what the server recorded, else the steps that failed. */
|
|
76
|
+
function purgeFailure(purge: PluginPurgeInfo): string {
|
|
77
|
+
return purge.error ?? (purge.steps.filter((step) => !step.ok).map((step) => step.error ?? step.step).join("; ") || "The reason wasn't recorded.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A day, in the administrator's own format. */
|
|
81
|
+
const formatDate = (iso: string): string => new Date(iso).toLocaleDateString(undefined, { dateStyle: "medium" });
|
|
82
|
+
|
|
60
83
|
/** A change waiting for the administrator to confirm the other plugins it also installs or enables. */
|
|
61
84
|
interface PendingChange {
|
|
62
85
|
displayName: string;
|
|
@@ -116,6 +139,8 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
116
139
|
/** How many times status has been retried while none has ever been read. */
|
|
117
140
|
const [statusRetries, setStatusRetries] = useState(0);
|
|
118
141
|
const statusRequest = useRef<Promise<void> | null>(null);
|
|
142
|
+
/** The state each data deletion was last seen in, so a notification is raised when one that was under way ends. */
|
|
143
|
+
const seenPurges = useRef<Map<string, PluginPurgeState>>(new Map());
|
|
119
144
|
|
|
120
145
|
const setBusy = (uid: string, busy: boolean) =>
|
|
121
146
|
setBusyUids((prev) => {
|
|
@@ -171,8 +196,12 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
171
196
|
}, [refreshStatus, refreshUpdates]);
|
|
172
197
|
|
|
173
198
|
const pending: boolean = !!status && status.instances.some((instance) => instance.hash !== status.hash);
|
|
199
|
+
// A plugin's data deletion in the status that isn't over yet, for a plugin that's uninstalled (one added again is hidden).
|
|
200
|
+
const installedNames: Set<string> = new Set(plugins.map((plugin) => plugin.name));
|
|
201
|
+
const purges: PluginPurgeInfo[] = (status?.purges ?? []).filter((purge) => !installedNames.has(purge.name));
|
|
202
|
+
const purging: boolean = purges.some((purge) => purge.state === "pending" || purge.state === "running");
|
|
174
203
|
useEffect(() => {
|
|
175
|
-
if (!pending && pollUntil === 0) {
|
|
204
|
+
if (!pending && !purging && pollUntil === 0) {
|
|
176
205
|
return;
|
|
177
206
|
}
|
|
178
207
|
const timer = setInterval(() => void refreshStatus(), PENDING_POLL_MS);
|
|
@@ -182,7 +211,29 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
182
211
|
clearInterval(timer);
|
|
183
212
|
clearTimeout(stop);
|
|
184
213
|
};
|
|
185
|
-
}, [pending, pollUntil, refreshStatus]);
|
|
214
|
+
}, [pending, purging, pollUntil, refreshStatus]);
|
|
215
|
+
|
|
216
|
+
// A data deletion seen under way that has now ended is worth a notification: it happens after the servers restart,
|
|
217
|
+
// long after the administrator asked for it.
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
for (const purge of status?.purges ?? []) {
|
|
220
|
+
const before = seenPurges.current.get(purge.uid);
|
|
221
|
+
seenPurges.current.set(purge.uid, purge.state);
|
|
222
|
+
if (before !== "pending" && before !== "running") {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (purge.state === "done") {
|
|
226
|
+
notify({ kind: "success", title: `${purgeName(purge)}'s data was deleted`, dedupeKey: `plugin-purge-${purge.uid}-done` });
|
|
227
|
+
} else if (purge.state === "failed") {
|
|
228
|
+
notify({
|
|
229
|
+
kind: "error",
|
|
230
|
+
title: `Deleting ${purgeName(purge)}'s data failed`,
|
|
231
|
+
message: purgeFailure(purge),
|
|
232
|
+
dedupeKey: `plugin-purge-${purge.uid}-failed`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}, [status]);
|
|
186
237
|
|
|
187
238
|
// With no status read at all there's nothing pending to poll for, so keep retrying - less often each time.
|
|
188
239
|
const neverRead: boolean = !status && statusStale;
|
|
@@ -298,6 +349,10 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
298
349
|
async (plan) => {
|
|
299
350
|
const result = await addPlugin(name, packageVersion, expectedPlanOf(plan));
|
|
300
351
|
applied(...result.dependencies, result.plugin);
|
|
352
|
+
// Adding a plugin cancels the deletion of its data that was waiting for the servers to stop running it.
|
|
353
|
+
for (const warning of result.warnings ?? []) {
|
|
354
|
+
notify({ kind: "warning", title: "Data deletion cancelled", message: warning });
|
|
355
|
+
}
|
|
301
356
|
},
|
|
302
357
|
`Could not install ${displayName}.`,
|
|
303
358
|
);
|
|
@@ -328,6 +383,22 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
328
383
|
return onRow(plugin, () => changeVersion(plugin, packageVersion));
|
|
329
384
|
}
|
|
330
385
|
|
|
386
|
+
/** Runs the steps of a failed data deletion that failed, again. */
|
|
387
|
+
async function retryPurge(purge: PluginPurgeInfo) {
|
|
388
|
+
setBusy(purge.uid, true);
|
|
389
|
+
setError(null);
|
|
390
|
+
setRetryEnableUid(null);
|
|
391
|
+
try {
|
|
392
|
+
await retryPluginPurge(purge.uid);
|
|
393
|
+
// The retry starts on a server shortly; keep reading status until it ends.
|
|
394
|
+
setPollUntil(Date.now() + AFTER_CHANGE_POLL_MS);
|
|
395
|
+
await refreshStatus();
|
|
396
|
+
} catch (err) {
|
|
397
|
+
setError(isElevationRequired(err) ? ELEVATION_MESSAGE : errorMessage(err, `Could not retry deleting ${purgeName(purge)}'s data.`));
|
|
398
|
+
}
|
|
399
|
+
setBusy(purge.uid, false);
|
|
400
|
+
}
|
|
401
|
+
|
|
331
402
|
// Retried against the plugin as it's listed now (a newer version, or already enabled elsewhere, hides the retry).
|
|
332
403
|
const retryEnable: Plugin | undefined = plugins.find((plugin) => plugin.uid === retryEnableUid && !plugin.enabled);
|
|
333
404
|
|
|
@@ -398,7 +469,7 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
398
469
|
</div>
|
|
399
470
|
{loading ? (
|
|
400
471
|
<p className="text-sm text-text-muted">Loading…</p>
|
|
401
|
-
) : plugins.length === 0 ? (
|
|
472
|
+
) : plugins.length === 0 && purges.length === 0 ? (
|
|
402
473
|
<p className="text-sm text-text-muted">No plugins installed.</p>
|
|
403
474
|
) : (
|
|
404
475
|
<div className="overflow-x-auto">
|
|
@@ -492,6 +563,9 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
492
563
|
</tr>
|
|
493
564
|
);
|
|
494
565
|
})}
|
|
566
|
+
{purges.map((purge) => (
|
|
567
|
+
<UninstalledPluginRow key={purge.uid} purge={purge} busy={busyUids.has(purge.uid)} onRetry={() => void retryPurge(purge)} />
|
|
568
|
+
))}
|
|
495
569
|
</tbody>
|
|
496
570
|
</table>
|
|
497
571
|
</div>
|
|
@@ -551,10 +625,23 @@ export default function PluginsManager({ embedded = false }: PluginsManagerProps
|
|
|
551
625
|
<RemoveModal
|
|
552
626
|
plugin={removing}
|
|
553
627
|
onClose={() => setRemoving(null)}
|
|
554
|
-
onRemoved={() => {
|
|
628
|
+
onRemoved={(result) => {
|
|
555
629
|
const uid = removing.uid;
|
|
630
|
+
const displayName = removing.manifest.displayName;
|
|
556
631
|
setRemoving(null);
|
|
557
632
|
setPlugins((prev) => prev.filter((plugin) => plugin.uid !== uid));
|
|
633
|
+
if (result.purgeScheduled) {
|
|
634
|
+
// Shown at once; the status polling that follows replaces it with what the servers report.
|
|
635
|
+
const scheduled = result.purge;
|
|
636
|
+
setStatus((prev) =>
|
|
637
|
+
prev && scheduled ? { ...prev, purges: [...(prev.purges ?? []).filter((purge) => purge.uid !== scheduled.uid), scheduled] } : prev,
|
|
638
|
+
);
|
|
639
|
+
notify({
|
|
640
|
+
kind: "info",
|
|
641
|
+
title: `${displayName} uninstalled`,
|
|
642
|
+
message: "Its data is deleted once every server has stopped running it. Adding the plugin again before then cancels the deletion.",
|
|
643
|
+
});
|
|
644
|
+
}
|
|
558
645
|
watchRollout();
|
|
559
646
|
}}
|
|
560
647
|
/>
|
|
@@ -811,6 +898,68 @@ function PluginServers({ plugin, status }: { plugin: Plugin; status: PluginStatu
|
|
|
811
898
|
);
|
|
812
899
|
}
|
|
813
900
|
|
|
901
|
+
/** The row of a plugin that was uninstalled but whose data deletion is still listed: what became of its data. */
|
|
902
|
+
function UninstalledPluginRow({ purge, busy, onRetry }: { purge: PluginPurgeInfo; busy: boolean; onRetry: () => void }) {
|
|
903
|
+
const failedSteps = purge.steps.filter((step) => !step.ok);
|
|
904
|
+
return (
|
|
905
|
+
<tr role="row" className={INSTALLED_ROW_CLASS}>
|
|
906
|
+
<td role="cell" className={`${INSTALLED_TD_CLASS} basis-full pt-3 pb-2`}>
|
|
907
|
+
<div className="font-semibold">{purgeName(purge)}</div>
|
|
908
|
+
<div className="text-xs text-text-muted">{purge.name}</div>
|
|
909
|
+
</td>
|
|
910
|
+
<td role="cell" className={`${INSTALLED_TD_CLASS} hidden lg:table-cell`} />
|
|
911
|
+
<td role="cell" className={`${INSTALLED_TD_CLASS} pt-1 pb-3 basis-full lg:basis-auto`}>
|
|
912
|
+
<div className="flex flex-col items-start gap-1 text-xs max-w-sm">
|
|
913
|
+
<span className={`${BADGE_CLASS} bg-surface-alt text-text-muted`}>Uninstalled</span>
|
|
914
|
+
{purge.state === "pending" && (
|
|
915
|
+
<>
|
|
916
|
+
<span>Uninstalled - data will be deleted after servers restart</span>
|
|
917
|
+
{!!purge.serversRunning && (
|
|
918
|
+
<span className="text-text-muted">
|
|
919
|
+
{purge.serversRunning} of {purge.serversTotal} {purge.serversTotal === 1 ? "server" : "servers"} still running it
|
|
920
|
+
</span>
|
|
921
|
+
)}
|
|
922
|
+
</>
|
|
923
|
+
)}
|
|
924
|
+
{purge.state === "running" && <span>Uninstalled - deleting its data now</span>}
|
|
925
|
+
{purge.state === "done" && <span className="text-success font-medium">Data deleted{purge.completedAt ? ` ${formatDate(purge.completedAt)}` : ""}</span>}
|
|
926
|
+
{purge.state === "failed" && (
|
|
927
|
+
<>
|
|
928
|
+
<span className="text-danger break-words">Data deletion failed: {purgeFailure(purge)}</span>
|
|
929
|
+
{failedSteps.length > 0 && (
|
|
930
|
+
<ul className="list-disc pl-4 text-text-muted break-words">
|
|
931
|
+
{failedSteps.map((step) => (
|
|
932
|
+
<li key={step.step}>
|
|
933
|
+
{step.step}: {step.error ?? "failed"}
|
|
934
|
+
</li>
|
|
935
|
+
))}
|
|
936
|
+
</ul>
|
|
937
|
+
)}
|
|
938
|
+
</>
|
|
939
|
+
)}
|
|
940
|
+
</div>
|
|
941
|
+
</td>
|
|
942
|
+
<td role="cell" className={`${INSTALLED_TD_CLASS} basis-full pt-1 pb-3`}>
|
|
943
|
+
{purge.state === "failed" && (
|
|
944
|
+
<div className="flex lg:justify-end">
|
|
945
|
+
<Button
|
|
946
|
+
type="button"
|
|
947
|
+
variant="secondary"
|
|
948
|
+
className="!w-auto"
|
|
949
|
+
aria-label={`Retry deleting the data of ${purgeName(purge)}`}
|
|
950
|
+
disabled={busy}
|
|
951
|
+
loading={busy}
|
|
952
|
+
onClick={onRetry}
|
|
953
|
+
>
|
|
954
|
+
Retry
|
|
955
|
+
</Button>
|
|
956
|
+
</div>
|
|
957
|
+
)}
|
|
958
|
+
</td>
|
|
959
|
+
</tr>
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
814
963
|
function AddPluginModal({
|
|
815
964
|
open,
|
|
816
965
|
onClose,
|
|
@@ -1252,37 +1401,91 @@ function SettingField({
|
|
|
1252
1401
|
);
|
|
1253
1402
|
}
|
|
1254
1403
|
|
|
1255
|
-
function RemoveModal({ plugin, onClose, onRemoved }: { plugin: Plugin; onClose: () => void; onRemoved: () => void }) {
|
|
1404
|
+
function RemoveModal({ plugin, onClose, onRemoved }: { plugin: Plugin; onClose: () => void; onRemoved: (result: RemovePluginResult) => void }) {
|
|
1256
1405
|
const [error, setError] = useState<string | null>(null);
|
|
1257
1406
|
const [busy, setBusy] = useState(false);
|
|
1258
|
-
|
|
1259
|
-
|
|
1407
|
+
const [purgeData, setPurgeData] = useState(false);
|
|
1408
|
+
const [typed, setTyped] = useState("");
|
|
1409
|
+
const helpId = useId();
|
|
1410
|
+
const displayName = plugin.manifest.displayName;
|
|
1411
|
+
// Deleting data is destructive and permanent, so it also takes the plugin's name typed out.
|
|
1412
|
+
const confirmed = !purgeData || typed.trim().toLowerCase() === displayName.trim().toLowerCase();
|
|
1413
|
+
|
|
1414
|
+
async function remove(event: FormEvent) {
|
|
1415
|
+
event.preventDefault();
|
|
1260
1416
|
setBusy(true);
|
|
1261
1417
|
setError(null);
|
|
1262
1418
|
try {
|
|
1263
|
-
await removePlugin(plugin.uid);
|
|
1264
|
-
onRemoved();
|
|
1419
|
+
onRemoved(await removePlugin(plugin.uid, { purgeData }));
|
|
1265
1420
|
} catch (err) {
|
|
1266
|
-
setError(errorMessage(err, "Could not uninstall the plugin."));
|
|
1421
|
+
setError(isElevationRequired(err) ? ELEVATION_MESSAGE : errorMessage(err, "Could not uninstall the plugin."));
|
|
1267
1422
|
setBusy(false);
|
|
1268
1423
|
}
|
|
1269
1424
|
}
|
|
1270
1425
|
|
|
1271
1426
|
return (
|
|
1272
|
-
<Modal open onClose={onClose} title={`Uninstall ${
|
|
1273
|
-
{
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1427
|
+
<Modal open onClose={onClose} title={`Uninstall ${displayName}?`}>
|
|
1428
|
+
<form onSubmit={(event) => void remove(event)}>
|
|
1429
|
+
{error && <Alert>{error}</Alert>}
|
|
1430
|
+
<p className="text-sm mb-4">
|
|
1431
|
+
{purgeData
|
|
1432
|
+
? "The servers stop running this plugin after they restart, and then all the data it stored is deleted."
|
|
1433
|
+
: "The servers stop running this plugin after they restart. Data it stored stays in the database, and adding the plugin again brings it back."}
|
|
1434
|
+
</p>
|
|
1435
|
+
<div className="mb-4">
|
|
1436
|
+
<label className="flex items-start gap-2 text-sm font-semibold cursor-pointer">
|
|
1437
|
+
<input
|
|
1438
|
+
type="checkbox"
|
|
1439
|
+
className="mt-0.5 h-4 w-4 shrink-0 accent-danger"
|
|
1440
|
+
checked={purgeData}
|
|
1441
|
+
disabled={busy}
|
|
1442
|
+
aria-describedby={helpId}
|
|
1443
|
+
onChange={(e) => setPurgeData(e.target.checked)}
|
|
1444
|
+
/>
|
|
1445
|
+
<span>Also delete all data this plugin stored</span>
|
|
1446
|
+
</label>
|
|
1447
|
+
<div id={helpId} className="mt-2 pl-6 text-xs text-text-muted">
|
|
1448
|
+
<p>What is deleted:</p>
|
|
1449
|
+
<ul className="list-disc pl-5 mt-1 space-y-0.5">
|
|
1450
|
+
<li>the database collections and tables the plugin's features use, with everything in them</li>
|
|
1451
|
+
<li>the plugin's saved settings</li>
|
|
1452
|
+
<li>its downloaded package and cached pages on the servers</li>
|
|
1453
|
+
<li>whatever else the plugin cleans up itself, such as files it stored or data kept outside the database</li>
|
|
1454
|
+
</ul>
|
|
1455
|
+
<p className="mt-2 text-danger font-semibold">This can't be undone.</p>
|
|
1456
|
+
</div>
|
|
1457
|
+
</div>
|
|
1458
|
+
{purgeData && (
|
|
1459
|
+
<>
|
|
1460
|
+
<p className="text-xs text-text-muted mb-3">
|
|
1461
|
+
Nothing is deleted while a server is still running the plugin. Once every server has restarted without it, usually
|
|
1462
|
+
within a few minutes, its data is deleted. Adding the plugin again before then cancels the deletion.
|
|
1463
|
+
</p>
|
|
1464
|
+
<label className="block text-sm mb-4">
|
|
1465
|
+
<span>
|
|
1466
|
+
Type <strong>{displayName}</strong> to confirm
|
|
1467
|
+
</span>
|
|
1468
|
+
<input
|
|
1469
|
+
type="text"
|
|
1470
|
+
className={`${INPUT_CLASS} mt-1.5`}
|
|
1471
|
+
value={typed}
|
|
1472
|
+
autoComplete="off"
|
|
1473
|
+
spellCheck={false}
|
|
1474
|
+
disabled={busy}
|
|
1475
|
+
onChange={(e) => setTyped(e.target.value)}
|
|
1476
|
+
/>
|
|
1477
|
+
</label>
|
|
1478
|
+
</>
|
|
1479
|
+
)}
|
|
1480
|
+
<div className="flex gap-2 justify-end">
|
|
1481
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={onClose}>
|
|
1482
|
+
Cancel
|
|
1483
|
+
</Button>
|
|
1484
|
+
<Button type="submit" className={purgeData ? DANGER_BUTTON_CLASS : "!w-auto"} loading={busy} disabled={busy || !confirmed}>
|
|
1485
|
+
{purgeData ? "Uninstall and delete data" : "Uninstall"}
|
|
1486
|
+
</Button>
|
|
1487
|
+
</div>
|
|
1488
|
+
</form>
|
|
1286
1489
|
</Modal>
|
|
1287
1490
|
);
|
|
1288
1491
|
}
|
|
@@ -391,7 +391,8 @@ function MailboxesStep({ userUid, domain }: { userUid: string; domain?: string }
|
|
|
391
391
|
const [formKey, setFormKey] = useState(0);
|
|
392
392
|
|
|
393
393
|
useEffect(() => {
|
|
394
|
-
|
|
394
|
+
// The administration scope: every mailbox that exists (administrative metadata), not only the administrator's own.
|
|
395
|
+
listMailboxes({ limit: 100, scope: "admin" })
|
|
395
396
|
.then((list) =>
|
|
396
397
|
// Keep any mailbox created here while the list was loading.
|
|
397
398
|
setMailboxes((prev) => [...list, ...prev.filter((mine) => !list.some((mailbox) => mailbox.uid === mine.uid))]),
|
|
@@ -6,6 +6,7 @@ import React, { FormEvent, useState } from "react";
|
|
|
6
6
|
import { CALENDAR_COLOR_PALETTE, accentColorForMailbox, colorForFolder } from "@rapidmx/react-shared/calendar/calendarColors.js";
|
|
7
7
|
import { Folder, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
8
8
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
9
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
9
10
|
|
|
10
11
|
export interface CalendarListSidebarMailbox {
|
|
11
12
|
mailbox: Mailbox;
|
|
@@ -42,7 +43,6 @@ export default function CalendarListSidebar({
|
|
|
42
43
|
const [addingFor, setAddingFor] = useState<string | null>(null);
|
|
43
44
|
const [name, setName] = useState("");
|
|
44
45
|
const [color, setColor] = useState(CALENDAR_COLOR_PALETTE[0]);
|
|
45
|
-
const [error, setError] = useState<string | null>(null);
|
|
46
46
|
const [saving, setSaving] = useState(false);
|
|
47
47
|
const multiple = mailboxCalendars.length > 1;
|
|
48
48
|
|
|
@@ -57,15 +57,15 @@ export default function CalendarListSidebar({
|
|
|
57
57
|
if (!name.trim() || !addingFor) {
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
|
-
setError(null);
|
|
61
60
|
setSaving(true);
|
|
62
61
|
try {
|
|
63
62
|
await onAddCalendar(addingFor, name.trim(), color);
|
|
64
63
|
setName("");
|
|
65
64
|
setColor(CALENDAR_COLOR_PALETTE[0]);
|
|
66
65
|
setAddingFor(null);
|
|
67
|
-
} catch {
|
|
68
|
-
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// A pop-up; the form stays open, with what was typed, for another try.
|
|
68
|
+
notifyApiError(err, "Couldn't create the calendar");
|
|
69
69
|
} finally {
|
|
70
70
|
setSaving(false);
|
|
71
71
|
}
|
|
@@ -107,7 +107,6 @@ export default function CalendarListSidebar({
|
|
|
107
107
|
|
|
108
108
|
return (
|
|
109
109
|
<nav aria-label="My calendars" className="p-3 border-t border-border flex flex-col gap-3">
|
|
110
|
-
{error && <Alert>{error}</Alert>}
|
|
111
110
|
{mailboxCalendars.map(({ mailbox, calendarFolders, error: mailboxError }) => {
|
|
112
111
|
const title = multiple ? `${mailbox.displayName}${mailbox.ownerUserUid ? "" : " (shared)"}` : "My calendars";
|
|
113
112
|
return (
|
|
@@ -144,7 +144,11 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
return (
|
|
147
|
-
|
|
147
|
+
// The form fills the pane it is in: the fields scroll in their own area and Save / Cancel stay in view under them (`rr-solid`: opaque over a
|
|
148
|
+
// background image). Outside a bounded pane (the phone's page, which scrolls as a whole) the footer sticks above the bottom tab bar instead.
|
|
149
|
+
<form onSubmit={handleSubmit} className="@container flex min-h-0 flex-1 flex-col">
|
|
150
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-6">
|
|
151
|
+
<div className="flex max-w-xl flex-col gap-1">
|
|
148
152
|
<h1 className="text-xl font-bold uppercase tracking-wide mb-3">{contact ? "Edit contact" : "New contact"}</h1>
|
|
149
153
|
|
|
150
154
|
{error && <Alert>{error}</Alert>}
|
|
@@ -177,7 +181,7 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
177
181
|
/>
|
|
178
182
|
</FormField>
|
|
179
183
|
|
|
180
|
-
<div className="grid grid-cols-1
|
|
184
|
+
<div className="grid grid-cols-1 @md:grid-cols-2 gap-3">
|
|
181
185
|
<FormField label="First name" htmlFor="contact-givenName">
|
|
182
186
|
<input id="contact-givenName" type="text" className={INPUT_CLASS} value={givenName} onChange={(e) => setGivenName(e.target.value)} />
|
|
183
187
|
</FormField>
|
|
@@ -186,7 +190,7 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
186
190
|
</FormField>
|
|
187
191
|
</div>
|
|
188
192
|
|
|
189
|
-
<div className="grid grid-cols-1
|
|
193
|
+
<div className="grid grid-cols-1 @md:grid-cols-2 gap-3">
|
|
190
194
|
<FormField label="Company" htmlFor="contact-company">
|
|
191
195
|
<input id="contact-company" type="text" className={INPUT_CLASS} value={company} onChange={(e) => setCompany(e.target.value)} />
|
|
192
196
|
</FormField>
|
|
@@ -291,7 +295,7 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
291
295
|
<div className="flex flex-col gap-3">
|
|
292
296
|
{addresses.map((address, i) => (
|
|
293
297
|
<div key={i} className="flex flex-col gap-2">
|
|
294
|
-
<div className="grid grid-cols-1
|
|
298
|
+
<div className="grid grid-cols-1 @md:grid-cols-2 gap-2">
|
|
295
299
|
<input
|
|
296
300
|
type="text"
|
|
297
301
|
placeholder="Street"
|
|
@@ -364,7 +368,9 @@ export default function ContactForm({ contact, mailboxUid, folderUid, mailboxes,
|
|
|
364
368
|
<textarea id="contact-notes" rows={3} className={INPUT_CLASS} value={notes} onChange={(e) => setNotes(e.target.value)} />
|
|
365
369
|
</FormField>
|
|
366
370
|
|
|
367
|
-
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
<div className="rr-solid sticky bottom-14 z-10 flex shrink-0 gap-3 border-t border-border bg-surface px-6 py-3 md:bottom-0">
|
|
368
374
|
<Button type="submit" loading={saving} disabled={saving} className="!w-auto">
|
|
369
375
|
Save
|
|
370
376
|
</Button>
|
|
@@ -7,6 +7,7 @@ import { HiOutlineBars3 } from "react-icons/hi2";
|
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
8
|
import { Contact, ContactList, createContactList, listContactLists } from "@rapidmx/react-shared/contacts/contactsApi.js";
|
|
9
9
|
import Drawer from "@rapidmx/react-shared/components/overlays/Drawer.js";
|
|
10
|
+
import { notifyApiError } from "../../notifications/apiErrors.js";
|
|
10
11
|
|
|
11
12
|
export type ContactsView =
|
|
12
13
|
| { type: "all" }
|
|
@@ -144,7 +145,8 @@ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect
|
|
|
144
145
|
setNewListName("");
|
|
145
146
|
setAddingList(false);
|
|
146
147
|
} catch (err) {
|
|
147
|
-
|
|
148
|
+
// A pop-up: `listsError` is only the lists' own load failure.
|
|
149
|
+
notifyApiError(err, "Couldn't create the list");
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
152
|
|
|
@@ -229,13 +231,13 @@ export default function ContactsSidebar({ mailboxUid, contacts, active, onSelect
|
|
|
229
231
|
<>
|
|
230
232
|
<button
|
|
231
233
|
type="button"
|
|
232
|
-
className="
|
|
234
|
+
className="lg:hidden m-3 mb-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
|
|
233
235
|
aria-label="Open contacts menu"
|
|
234
236
|
onClick={() => setDrawerOpen(true)}
|
|
235
237
|
>
|
|
236
238
|
<HiOutlineBars3 size={20} aria-hidden="true" />
|
|
237
239
|
</button>
|
|
238
|
-
<nav aria-label="Contacts" className="hidden
|
|
240
|
+
<nav aria-label="Contacts" className="hidden lg:flex w-56 shrink-0 border-r border-border flex-col gap-4 p-3 overflow-y-auto">
|
|
239
241
|
{navContent("desktop")}
|
|
240
242
|
</nav>
|
|
241
243
|
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Contacts">
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { ChangeEvent, useRef } from "react";
|
|
6
|
-
import type { IconType } from "react-icons";
|
|
7
6
|
import {
|
|
8
7
|
HiOutlineArrowDownTray,
|
|
9
8
|
HiOutlineArrowUpTray,
|
|
@@ -17,6 +16,7 @@ import {
|
|
|
17
16
|
} from "react-icons/hi2";
|
|
18
17
|
import { SHORTCUTS } from "../../keyboard/keymap.js";
|
|
19
18
|
import { useShortcutProps } from "../../keyboard/useShortcutProps.js";
|
|
19
|
+
import ResponsiveToolbar, { ToolbarAction } from "../layout/ResponsiveToolbar.js";
|
|
20
20
|
|
|
21
21
|
export interface ContactsToolbarProps {
|
|
22
22
|
selectedCount: number;
|
|
@@ -37,40 +37,10 @@ export interface ContactsToolbarProps {
|
|
|
37
37
|
shortcuts?: boolean;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function ToolbarButton({
|
|
41
|
-
label,
|
|
42
|
-
icon: Icon,
|
|
43
|
-
disabled,
|
|
44
|
-
onClick,
|
|
45
|
-
hint,
|
|
46
|
-
}: {
|
|
47
|
-
label: string;
|
|
48
|
-
icon: IconType;
|
|
49
|
-
disabled?: boolean;
|
|
50
|
-
onClick: () => void;
|
|
51
|
-
/** `title` and `aria-keyshortcuts` for a button with a shortcut. */
|
|
52
|
-
hint?: { title: string; "aria-keyshortcuts"?: string };
|
|
53
|
-
}) {
|
|
54
|
-
return (
|
|
55
|
-
<button
|
|
56
|
-
type="button"
|
|
57
|
-
onClick={onClick}
|
|
58
|
-
disabled={disabled}
|
|
59
|
-
{...hint}
|
|
60
|
-
className="flex flex-col items-center gap-1 text-xs text-text-muted hover:text-text disabled:opacity-40 disabled:cursor-not-allowed px-2.5 py-1.5 rounded-sm hover:not-disabled:bg-surface-alt"
|
|
61
|
-
>
|
|
62
|
-
<Icon size={18} aria-hidden="true" />
|
|
63
|
-
<span>{label}</span>
|
|
64
|
-
</button>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function Divider() {
|
|
69
|
-
return <div className="w-px self-stretch my-1 bg-border" aria-hidden="true" />;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
40
|
/**
|
|
73
|
-
* Contacts' Outlook-style ribbon toolbar
|
|
41
|
+
* Contacts' Outlook-style ribbon toolbar, fitted to its column (`ResponsiveToolbar`): captions under the icons while there is room, then icons
|
|
42
|
+
* alone, then the rarely used actions - Import, Export, Add category, and so on up - in a "More" menu; the bar can never spill into the pane
|
|
43
|
+
* beside it. Every button keeps its tooltip (New contact its shortcut's) and accessible name in every layout. Deliberately presentational — every action is a callback the
|
|
74
44
|
* page (`apps/www/contacts/index.tsx`) implements, since it owns the actual contact data, the current
|
|
75
45
|
* selection, and the mailbox/folder context every real action (create/delete/email/favorite/category)
|
|
76
46
|
* needs. "New contact list" isn't duplicated here — `ContactsSidebar` already has its own inline
|
|
@@ -101,25 +71,28 @@ export default function ContactsToolbar({
|
|
|
101
71
|
}
|
|
102
72
|
}
|
|
103
73
|
|
|
74
|
+
const actions: ToolbarAction[] = [
|
|
75
|
+
{ id: "new", label: "New contact", icon: HiOutlineUserPlus, onClick: onNewContact, hint: shortcuts ? newContactHint : undefined, group: 0, rank: 99, essential: true },
|
|
76
|
+
{ id: "edit", label: "Edit", icon: HiOutlinePencil, onClick: onEdit, disabled: selectedCount !== 1, group: 1, rank: 6 },
|
|
77
|
+
{ id: "delete", label: "Delete", icon: HiOutlineTrash, onClick: onDelete, disabled: !hasSelection, group: 1, rank: 5 },
|
|
78
|
+
{ id: "email", label: "Email", icon: HiOutlineEnvelope, onClick: onEmail, disabled: !hasSelection, group: 2, rank: 4 },
|
|
79
|
+
{
|
|
80
|
+
id: "favorite",
|
|
81
|
+
label: allSelectedFavorited ? "Unfavorite" : "Favorite",
|
|
82
|
+
icon: allSelectedFavorited ? HiStar : HiOutlineStar,
|
|
83
|
+
onClick: onToggleFavorite,
|
|
84
|
+
disabled: !hasSelection,
|
|
85
|
+
group: 2,
|
|
86
|
+
rank: 3,
|
|
87
|
+
},
|
|
88
|
+
{ id: "category", label: "Add category", icon: HiOutlineTag, onClick: onAddCategory, disabled: !hasSelection, group: 2, rank: 2 },
|
|
89
|
+
{ id: "export", label: "Export", icon: HiOutlineArrowUpTray, onClick: onExportVCard, disabled: !hasSelection, group: 3, rank: 1 },
|
|
90
|
+
{ id: "import", label: "Import", icon: HiOutlineArrowDownTray, onClick: () => importInputRef.current?.click(), group: 3, rank: 0 },
|
|
91
|
+
];
|
|
92
|
+
|
|
104
93
|
return (
|
|
105
|
-
<
|
|
106
|
-
<ToolbarButton label="New contact" icon={HiOutlineUserPlus} onClick={onNewContact} hint={shortcuts ? newContactHint : undefined} />
|
|
107
|
-
<Divider />
|
|
108
|
-
<ToolbarButton label="Edit" icon={HiOutlinePencil} disabled={selectedCount !== 1} onClick={onEdit} />
|
|
109
|
-
<ToolbarButton label="Delete" icon={HiOutlineTrash} disabled={!hasSelection} onClick={onDelete} />
|
|
110
|
-
<Divider />
|
|
111
|
-
<ToolbarButton label="Email" icon={HiOutlineEnvelope} disabled={!hasSelection} onClick={onEmail} />
|
|
112
|
-
<ToolbarButton
|
|
113
|
-
label={allSelectedFavorited ? "Unfavorite" : "Favorite"}
|
|
114
|
-
icon={allSelectedFavorited ? HiStar : HiOutlineStar}
|
|
115
|
-
disabled={!hasSelection}
|
|
116
|
-
onClick={onToggleFavorite}
|
|
117
|
-
/>
|
|
118
|
-
<ToolbarButton label="Add category" icon={HiOutlineTag} disabled={!hasSelection} onClick={onAddCategory} />
|
|
119
|
-
<Divider />
|
|
120
|
-
<ToolbarButton label="Export" icon={HiOutlineArrowUpTray} disabled={!hasSelection} onClick={onExportVCard} />
|
|
121
|
-
<ToolbarButton label="Import" icon={HiOutlineArrowDownTray} onClick={() => importInputRef.current?.click()} />
|
|
94
|
+
<ResponsiveToolbar label="Contacts actions" actions={actions}>
|
|
122
95
|
<input ref={importInputRef} type="file" accept=".vcf" className="sr-only" aria-label="Import contacts file" onChange={handleImportFileChosen} />
|
|
123
|
-
</
|
|
96
|
+
</ResponsiveToolbar>
|
|
124
97
|
);
|
|
125
98
|
}
|
|
@@ -169,19 +169,24 @@ export default function ContactsShell({
|
|
|
169
169
|
</>
|
|
170
170
|
);
|
|
171
171
|
|
|
172
|
+
// The mailbox switcher (or an error) is all this column holds: with a single mailbox and no error there is nothing to put in it, and an empty
|
|
173
|
+
// 224 px column beside the contacts' own menu is wasted room.
|
|
174
|
+
const hasSidebar = mailboxes.length > 1 || !!folderError;
|
|
172
175
|
inner = (
|
|
173
176
|
<>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
{hasSidebar && (
|
|
178
|
+
<aside className="hidden lg:flex w-56 shrink-0 bg-surface border-r border-border flex-col p-3 gap-3">
|
|
179
|
+
{sidebarContent("desktop")}
|
|
180
|
+
</aside>
|
|
181
|
+
)}
|
|
177
182
|
<Drawer open={drawerOpen} onClose={() => setDrawerOpen(false)} title="Mailbox">
|
|
178
183
|
<div className="flex flex-col gap-3">{sidebarContent("mobile")}</div>
|
|
179
184
|
</Drawer>
|
|
180
185
|
<main className="flex-1 min-w-0 flex flex-col">
|
|
181
|
-
{
|
|
186
|
+
{hasSidebar && (
|
|
182
187
|
<button
|
|
183
188
|
type="button"
|
|
184
|
-
className="
|
|
189
|
+
className="lg:hidden m-3 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text"
|
|
185
190
|
aria-label="Open mailbox switcher"
|
|
186
191
|
onClick={() => setDrawerOpen(true)}
|
|
187
192
|
>
|