@rapidmx/web-client 0.16.0 → 0.17.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 +36 -28
- package/apps/admin/distribution-lists/new/index.tsx +3 -1
- package/apps/admin/domains/[uid].tsx +3 -1
- package/apps/admin/domains/new/index.tsx +3 -1
- package/apps/admin/escrow-scopes/[uid].tsx +3 -1
- package/apps/admin/escrow-scopes/new/index.tsx +3 -1
- package/apps/admin/index.tsx +3 -1
- package/apps/admin/mailboxes/[uid].tsx +3 -1
- package/apps/admin/mailboxes/new/index.tsx +3 -1
- package/apps/admin/transport-rules/new/index.tsx +3 -1
- package/apps/escrow/matters/new/index.tsx +3 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +4 -1
- package/apps/shared/components/admin/setup/SetupWizard.tsx +3 -1
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +2 -2
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +3 -2
- package/apps/shared/components/layout/AppShell.tsx +24 -43
- package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -0
- package/apps/shared/components/mail/EmptyFolderBar.tsx +41 -0
- package/apps/shared/components/mail/MailSelectionBar.tsx +19 -15
- package/apps/shared/components/mail/MenuButton.tsx +31 -11
- package/apps/shared/components/mail/MessageDetailPane.tsx +202 -4
- package/apps/shared/components/mail/PermanentDeleteDialog.tsx +47 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +37 -34
- package/apps/shared/components/mail/reading/MessageMoreMenu.tsx +258 -0
- package/apps/shared/components/mail/reading/MessageSourceDialog.tsx +79 -0
- package/apps/shared/components/mail/reading/messageExport.ts +59 -0
- package/apps/shared/components/mail/reading/printMessage.ts +99 -0
- package/apps/shared/components/mail/reading/useMessageActions.ts +443 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +4 -2
- package/apps/shared/components/tasks/layout/TasksShell.tsx +3 -2
- package/apps/shared/keyboard/GlobalShortcuts.tsx +1 -1
- package/apps/shared/mail/folderOfType.ts +49 -0
- package/apps/shared/mail/permanentDelete.ts +160 -0
- package/apps/shared/mail/reportNotices.ts +66 -0
- package/apps/shared/mail/senderBlocking.ts +141 -0
- package/apps/shared/mail/useMailboxUpdateAccess.ts +60 -0
- package/apps/shared/mail/usePermanentDelete.tsx +160 -0
- package/apps/shared/navigation/frameContext.tsx +1 -1
- package/apps/shared/navigation/index.ts +43 -0
- package/apps/shared/navigation/pageTitle.ts +19 -0
- package/apps/www/_shell.tsx +82 -0
- package/apps/www/calendar/index.tsx +5 -2
- package/apps/www/contacts/[uid].tsx +6 -3
- package/apps/www/contacts/index.tsx +8 -3
- package/apps/www/index.tsx +146 -31
- package/apps/www/messages/[uid].tsx +8 -4
- package/apps/www/settings/appearance/index.tsx +5 -2
- package/apps/www/settings/auto-reply/index.tsx +5 -2
- package/apps/www/settings/blocked-senders/index.tsx +303 -0
- package/apps/www/settings/encryption/index.tsx +5 -2
- package/apps/www/settings/filters/[uid].tsx +5 -2
- package/apps/www/settings/filters/index.tsx +5 -2
- package/apps/www/settings/filters/new/index.tsx +30 -5
- package/apps/www/settings/labels/index.tsx +5 -2
- package/apps/www/settings/privacy/index.tsx +5 -2
- package/apps/www/settings/profile/index.tsx +5 -2
- package/apps/www/settings/read-receipts/index.tsx +5 -2
- package/apps/www/settings/sharing/index.tsx +5 -2
- package/apps/www/settings/signatures/[uid].tsx +5 -2
- package/apps/www/settings/signatures/index.tsx +5 -2
- package/apps/www/settings/signatures/new/index.tsx +6 -3
- package/apps/www/tasks/index.tsx +7 -2
- package/dist/apps/admin/distribution-lists/new/index.js +3 -1
- package/dist/apps/admin/domains/[uid].js +3 -1
- package/dist/apps/admin/domains/new/index.js +3 -1
- package/dist/apps/admin/escrow-scopes/[uid].js +3 -1
- package/dist/apps/admin/escrow-scopes/new/index.js +3 -1
- package/dist/apps/admin/index.js +3 -1
- package/dist/apps/admin/mailboxes/[uid].js +3 -1
- package/dist/apps/admin/mailboxes/new/index.js +3 -1
- package/dist/apps/admin/transport-rules/new/index.js +3 -1
- package/dist/apps/escrow/matters/new/index.js +3 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +4 -1
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +3 -1
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +2 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +3 -2
- package/dist/apps/shared/components/layout/AppShell.d.ts +8 -11
- package/dist/apps/shared/components/layout/AppShell.js +24 -33
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
- package/dist/apps/shared/components/mail/EmptyFolderBar.d.ts +18 -0
- package/dist/apps/shared/components/mail/EmptyFolderBar.js +10 -0
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +9 -4
- package/dist/apps/shared/components/mail/MailSelectionBar.js +10 -12
- package/dist/apps/shared/components/mail/MenuButton.d.ts +9 -2
- package/dist/apps/shared/components/mail/MenuButton.js +15 -11
- package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +5 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +147 -7
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.d.ts +19 -0
- package/dist/apps/shared/components/mail/PermanentDeleteDialog.js +11 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +13 -9
- package/dist/apps/shared/components/mail/layout/MailShell.js +27 -33
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.d.ts +56 -0
- package/dist/apps/shared/components/mail/reading/MessageMoreMenu.js +153 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.d.ts +27 -0
- package/dist/apps/shared/components/mail/reading/MessageSourceDialog.js +27 -0
- package/dist/apps/shared/components/mail/reading/messageExport.d.ts +19 -0
- package/dist/apps/shared/components/mail/reading/messageExport.js +51 -0
- package/dist/apps/shared/components/mail/reading/printMessage.d.ts +30 -0
- package/dist/apps/shared/components/mail/reading/printMessage.js +70 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.d.ts +57 -0
- package/dist/apps/shared/components/mail/reading/useMessageActions.js +366 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +4 -2
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +3 -2
- package/dist/apps/shared/keyboard/GlobalShortcuts.js +1 -1
- package/dist/apps/shared/mail/folderOfType.d.ts +13 -0
- package/dist/apps/shared/mail/folderOfType.js +40 -0
- package/dist/apps/shared/mail/permanentDelete.d.ts +62 -0
- package/dist/apps/shared/mail/permanentDelete.js +127 -0
- package/dist/apps/shared/mail/reportNotices.d.ts +21 -0
- package/dist/apps/shared/mail/reportNotices.js +47 -0
- package/dist/apps/shared/mail/senderBlocking.d.ts +26 -0
- package/dist/apps/shared/mail/senderBlocking.js +92 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.d.ts +11 -0
- package/dist/apps/shared/mail/useMailboxUpdateAccess.js +52 -0
- package/dist/apps/shared/mail/usePermanentDelete.d.ts +35 -0
- package/dist/apps/shared/mail/usePermanentDelete.js +96 -0
- package/dist/apps/shared/navigation/frameContext.d.ts +1 -1
- package/dist/apps/shared/navigation/index.d.ts +16 -0
- package/dist/apps/shared/navigation/index.js +34 -0
- package/dist/apps/shared/navigation/pageTitle.d.ts +11 -0
- package/dist/apps/shared/navigation/pageTitle.js +12 -0
- package/dist/apps/www/_shell.d.ts +26 -0
- package/dist/apps/www/_shell.js +31 -0
- package/dist/apps/www/calendar/index.d.ts +7 -2
- package/dist/apps/www/calendar/index.js +4 -2
- package/dist/apps/www/contacts/[uid].d.ts +14 -3
- package/dist/apps/www/contacts/[uid].js +5 -3
- package/dist/apps/www/contacts/index.d.ts +7 -2
- package/dist/apps/www/contacts/index.js +6 -4
- package/dist/apps/www/index.d.ts +7 -2
- package/dist/apps/www/index.js +117 -19
- package/dist/apps/www/messages/[uid].d.ts +14 -3
- package/dist/apps/www/messages/[uid].js +7 -5
- package/dist/apps/www/settings/appearance/index.d.ts +8 -2
- package/dist/apps/www/settings/appearance/index.js +4 -2
- package/dist/apps/www/settings/auto-reply/index.d.ts +7 -2
- package/dist/apps/www/settings/auto-reply/index.js +4 -2
- package/dist/apps/www/settings/blocked-senders/index.d.ts +9 -0
- package/dist/apps/www/settings/blocked-senders/index.js +134 -0
- package/dist/apps/www/settings/encryption/index.d.ts +7 -2
- package/dist/apps/www/settings/encryption/index.js +4 -2
- package/dist/apps/www/settings/filters/[uid].d.ts +7 -2
- package/dist/apps/www/settings/filters/[uid].js +4 -2
- package/dist/apps/www/settings/filters/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/index.js +4 -2
- package/dist/apps/www/settings/filters/new/index.d.ts +7 -2
- package/dist/apps/www/settings/filters/new/index.js +28 -5
- package/dist/apps/www/settings/labels/index.d.ts +7 -2
- package/dist/apps/www/settings/labels/index.js +4 -2
- package/dist/apps/www/settings/privacy/index.d.ts +7 -2
- package/dist/apps/www/settings/privacy/index.js +4 -2
- package/dist/apps/www/settings/profile/index.d.ts +7 -2
- package/dist/apps/www/settings/profile/index.js +4 -2
- package/dist/apps/www/settings/read-receipts/index.d.ts +7 -2
- package/dist/apps/www/settings/read-receipts/index.js +4 -2
- package/dist/apps/www/settings/sharing/index.d.ts +7 -2
- package/dist/apps/www/settings/sharing/index.js +4 -2
- package/dist/apps/www/settings/signatures/[uid].d.ts +7 -2
- package/dist/apps/www/settings/signatures/[uid].js +4 -2
- package/dist/apps/www/settings/signatures/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/index.js +4 -2
- package/dist/apps/www/settings/signatures/new/index.d.ts +7 -2
- package/dist/apps/www/settings/signatures/new/index.js +5 -3
- package/dist/apps/www/tasks/index.d.ts +7 -2
- package/dist/apps/www/tasks/index.js +5 -3
- package/package.json +4 -2
- package/apps/shared/navigation/AppRouter.tsx +0 -300
- package/apps/shared/navigation/idle.ts +0 -45
- package/apps/shared/navigation/routerContext.tsx +0 -83
- package/apps/shared/navigation/routes.ts +0 -72
- package/apps/www/_routedPage.tsx +0 -24
- package/apps/www/_routes.ts +0 -37
- package/dist/apps/shared/navigation/AppRouter.d.ts +0 -52
- package/dist/apps/shared/navigation/AppRouter.js +0 -242
- package/dist/apps/shared/navigation/idle.d.ts +0 -14
- package/dist/apps/shared/navigation/idle.js +0 -43
- package/dist/apps/shared/navigation/routerContext.d.ts +0 -37
- package/dist/apps/shared/navigation/routerContext.js +0 -56
- package/dist/apps/shared/navigation/routes.d.ts +0 -32
- package/dist/apps/shared/navigation/routes.js +0 -37
- package/dist/apps/www/_routedPage.d.ts +0 -12
- package/dist/apps/www/_routedPage.js +0 -19
- package/dist/apps/www/_routes.d.ts +0 -11
- package/dist/apps/www/_routes.js +0 -31
package/README.md
CHANGED
|
@@ -21,39 +21,47 @@ import SettingsShell from "@rapidmx/web-client/shared/components/settings/layout
|
|
|
21
21
|
|
|
22
22
|
## Navigation without page loads
|
|
23
23
|
|
|
24
|
-
`@rapidrest/react`
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
24
|
+
All three apps use `@rapidrest/react`'s client-side router (`router = true` on the server's route, the client built with
|
|
25
|
+
`createViteConfig({ router })`; `@rapidrest/react` is a **peer dependency** here, `>=2.1.0 <3`, because the router's state lives
|
|
26
|
+
in a React context that this package must share with the server's one copy). The server still renders every URL, so first
|
|
27
|
+
loads, reloads and crawlers are unchanged; after the first load a click on an ordinary `<a href>` between two pages of an app
|
|
28
|
+
fetches the next page's props as JSON and swaps the page in. Back and forward work, the address bar always holds the real,
|
|
29
|
+
shareable URL, and anything the router cannot be sure of (a modified click, `target`, `download`, `data-router-ignore`, another
|
|
30
|
+
app's page - the consoles, plugin pages, other sites - or a failure) is left to the browser as a page load.
|
|
31
|
+
|
|
32
|
+
The admin and escrow pages each render their own shell, so the router just swaps the page. The webmail (`apps/www`) has an **app
|
|
33
|
+
shell**, `apps/www/_shell.tsx` (the router's persistent client layout), which the server renders around every page and the
|
|
34
|
+
browser hydrates with it as one root:
|
|
35
|
+
|
|
36
|
+
- the one `AppChrome` (the app rail, header, user menu, impersonation banner, compose windows in progress, the unlock prompt,
|
|
37
|
+
the idle-key timer, the mail connection and the notification pop-ups) stays mounted while only the page inside it is replaced.
|
|
38
|
+
A page's own shell (`MailShell`, `CalendarShell`, ...) still renders `AppShell`; inside the frame that is only its children;
|
|
39
|
+
- the shell gets the current page's props - `WwwRoute.fetchProps()`'s, the same for every page - so the rail's highlighted app
|
|
40
|
+
(from the route), the branding, the plugin navigation and the impersonation banner follow the server on every navigation. That is
|
|
41
|
+
one request to `fetchProps()` per page change (warm for most, since the router prefetches a plain link on hover and the four rail
|
|
42
|
+
pages' code when the browser is idle);
|
|
43
|
+
- **a folder is a shallow navigation.** `<a data-router-shallow>` (the Mail folder links), `navigate()` to the same path with another
|
|
44
|
+
query (`useNavigate()`, below) and `useSearchParams()`' setter keep the page instance and its state and only change the location;
|
|
45
|
+
- after a page change the router moves focus to the content region (`#app-content`), scrolls to the top and announces the page's
|
|
46
|
+
title in a polite live region (all set by the shell with `useNavigationEffects()`); the tab's title is each page's `title`
|
|
47
|
+
export (`pageTitle("Calendar")`: `Acme Mail: Calendar`), rendered by the server and sent with every navigation, with the unread
|
|
48
|
+
count kept in front of it; the content region is `aria-busy` while the next page loads.
|
|
49
|
+
|
|
50
|
+
Code that decides where to go uses `useNavigate()` from `shared/navigation/index.js` and reads the location with the library's
|
|
51
|
+
hooks:
|
|
45
52
|
|
|
46
53
|
```tsx
|
|
47
|
-
import { useLocation,
|
|
54
|
+
import { useLocation, useSearchParams } from "@rapidrest/react/client";
|
|
55
|
+
import { useNavigate } from "@rapidmx/web-client/shared/navigation/index.js";
|
|
48
56
|
|
|
49
|
-
const navigate = useNavigate(); // navigate("/contacts")
|
|
50
|
-
const { pathname, search, hash } = useLocation(); //
|
|
57
|
+
const navigate = useNavigate(); // navigate("/contacts"); navigate("/?mailboxUid=a&folderUid=b") keeps the page (same path)
|
|
58
|
+
const { pathname, search, hash } = useLocation(); // the request's on the server and while hydrating; `hash` is empty there
|
|
59
|
+
const [params, setParams] = useSearchParams(); // setParams({ folderUid: "x" }) is shallow by default
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
`navigate()` changes the page without a load when the URL is a
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
`AppShell` chrome) and are opened with a page load.
|
|
62
|
+
`navigate()` changes the page without a load when the URL is a page of the app, and is an ordinary navigation otherwise (outside a
|
|
63
|
+
router too). An admin or escrow page that leads to another page of its console after a save goes there with `useRouter().navigate`.
|
|
64
|
+
Plugin pages are not part of the router (they render their own `AppShell` chrome) and are opened with a page load.
|
|
57
65
|
|
|
58
66
|
The compose window, the reading pane, S/MIME and the emoji list are also chunks of their own, loaded on demand or fetched
|
|
59
67
|
when the browser is idle, so a page's first JavaScript is React and what the first screen draws.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { FormEvent, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { createDistributionList } from "@rapidmx/react-shared/admin/distributionListsApi.js";
|
|
8
9
|
import AdminShell, { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -22,6 +23,7 @@ export default function NewDistributionListPage(props: Omit<AdminShellProps, "ac
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function NewDistributionListForm() {
|
|
26
|
+
const { navigate } = useRouter();
|
|
25
27
|
const [primarySmtpAddress, setPrimarySmtpAddress] = useState("");
|
|
26
28
|
const [name, setName] = useState("");
|
|
27
29
|
const [description, setDescription] = useState("");
|
|
@@ -48,7 +50,7 @@ function NewDistributionListForm() {
|
|
|
48
50
|
name: name.trim(),
|
|
49
51
|
description: description.trim() || undefined,
|
|
50
52
|
});
|
|
51
|
-
|
|
53
|
+
void navigate(`/admin/distribution-lists/${encodeURIComponent(list.uid)}`);
|
|
52
54
|
} catch (err) {
|
|
53
55
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the distribution list.");
|
|
54
56
|
} finally {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { deleteDomain, Domain, updateDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
|
|
8
9
|
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -24,6 +25,7 @@ export default function DomainDetailPage(props: Omit<AdminShellProps, "active">
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function DomainDetailContent({ uid }: { uid: string }) {
|
|
28
|
+
const { navigate } = useRouter();
|
|
27
29
|
const [domain, setDomain] = useState<Domain | null>(null);
|
|
28
30
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
29
31
|
const [deleting, setDeleting] = useState(false);
|
|
@@ -48,7 +50,7 @@ function DomainDetailContent({ uid }: { uid: string }) {
|
|
|
48
50
|
setDeleteError(null);
|
|
49
51
|
try {
|
|
50
52
|
await deleteDomain(domain!.uid, domain!.version);
|
|
51
|
-
|
|
53
|
+
void navigate("/admin/domains");
|
|
52
54
|
} catch (err) {
|
|
53
55
|
setDeleteError(err instanceof ApiRequestError ? err.message : "Could not delete this domain.");
|
|
54
56
|
setDeleting(false);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { FormEvent, useEffect, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { createDomain, Domain, listDomains } from "@rapidmx/react-shared/admin/domainsApi.js";
|
|
8
9
|
import AdminShell, { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -22,6 +23,7 @@ export default function NewDomainPage(props: Omit<AdminShellProps, "active">) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function NewDomainForm() {
|
|
26
|
+
const { navigate } = useRouter();
|
|
25
27
|
const [name, setName] = useState("");
|
|
26
28
|
const [aliasOf, setAliasOf] = useState("");
|
|
27
29
|
// Only an existing, non-alias domain can be aliased (no chains) - see `Domain.aliasOf`'s own doc comment
|
|
@@ -48,7 +50,7 @@ function NewDomainForm() {
|
|
|
48
50
|
setSaving(true);
|
|
49
51
|
try {
|
|
50
52
|
const domain = await createDomain({ name: name.trim(), aliasOf: aliasOf || undefined });
|
|
51
|
-
|
|
53
|
+
void navigate(`/admin/domains/${encodeURIComponent(domain.uid)}`);
|
|
52
54
|
} catch (err) {
|
|
53
55
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the domain.");
|
|
54
56
|
} finally {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { FormEvent, useEffect, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
8
9
|
import {
|
|
@@ -118,6 +119,7 @@ export default function EscrowScopeDetailPage(props: Omit<AdminShellProps, "acti
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
function EscrowScopeDetailContent({ uid, adminUid }: { uid: string; adminUid?: string }) {
|
|
122
|
+
const { navigate } = useRouter();
|
|
121
123
|
const [original, setOriginal] = useState<EscrowScope | null>(null);
|
|
122
124
|
const [name, setName] = useState("");
|
|
123
125
|
const [description, setDescription] = useState("");
|
|
@@ -225,7 +227,7 @@ function EscrowScopeDetailContent({ uid, adminUid }: { uid: string; adminUid?: s
|
|
|
225
227
|
setDeleteError(null);
|
|
226
228
|
try {
|
|
227
229
|
await deleteEscrowScope(original!.uid, original!.version);
|
|
228
|
-
|
|
230
|
+
void navigate("/admin/escrow-scopes");
|
|
229
231
|
} catch (err) {
|
|
230
232
|
// Most commonly a 409 ("referenced by an existing Matter") — see `deleteEscrowScope()`'s own
|
|
231
233
|
// doc comment - surfaced as-is rather than special-cased.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { FormEvent, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { createEscrowScope } from "@rapidmx/react-shared/admin/escrowScopesApi.js";
|
|
8
9
|
import AdminShell, { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -26,6 +27,7 @@ export default function NewEscrowScopePage(props: Omit<AdminShellProps, "active"
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function NewEscrowScopeForm({ adminUid }: { adminUid?: string }) {
|
|
30
|
+
const { navigate } = useRouter();
|
|
29
31
|
const [name, setName] = useState("");
|
|
30
32
|
const [description, setDescription] = useState("");
|
|
31
33
|
const [keyAndHolders, setKeyAndHolders] = useState(emptyEscrowScopeKeyAndHoldersValue());
|
|
@@ -74,7 +76,7 @@ function NewEscrowScopeForm({ adminUid }: { adminUid?: string }) {
|
|
|
74
76
|
requiredHolders: keyAndHolders.requiredHolders,
|
|
75
77
|
notifySubjectOnAccess: keyAndHolders.notifySubjectOnAccess,
|
|
76
78
|
});
|
|
77
|
-
|
|
79
|
+
void navigate(`/admin/escrow-scopes/${encodeURIComponent(created.uid)}`);
|
|
78
80
|
} catch (err) {
|
|
79
81
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the escrow scope.");
|
|
80
82
|
} finally {
|
package/apps/admin/index.tsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useEffect, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { listMailboxes, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
8
9
|
import { reopenSetup } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
@@ -24,6 +25,7 @@ export default function MailboxesListPage(props: Omit<AdminShellProps, "active">
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function MailboxesListContent() {
|
|
28
|
+
const { navigate } = useRouter();
|
|
27
29
|
const [page, setPage] = useState(0);
|
|
28
30
|
const [mailboxes, setMailboxes] = useState<Mailbox[]>([]);
|
|
29
31
|
const [loading, setLoading] = useState(true);
|
|
@@ -47,7 +49,7 @@ function MailboxesListContent() {
|
|
|
47
49
|
setReopening(true);
|
|
48
50
|
try {
|
|
49
51
|
await reopenSetup();
|
|
50
|
-
|
|
52
|
+
void navigate("/admin/setup");
|
|
51
53
|
} catch (err) {
|
|
52
54
|
setError(err instanceof ApiRequestError ? err.message : "Could not reopen setup.");
|
|
53
55
|
setReopening(false);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useEffect, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { deleteMailbox, getMailbox, impersonateUser, Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
8
9
|
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
@@ -34,6 +35,7 @@ function MailboxDetailContent({
|
|
|
34
35
|
currentUserUid,
|
|
35
36
|
}: { uid: string; currentUserUid?: string } & Pick<AdminShellProps, "impersonationBaseUrl">) {
|
|
36
37
|
const [mailbox, setMailbox] = useState<Mailbox | null>(null);
|
|
38
|
+
const { navigate } = useRouter();
|
|
37
39
|
const [loading, setLoading] = useState(true);
|
|
38
40
|
const [error, setError] = useState<string | null>(null);
|
|
39
41
|
const [confirmingAccess, setConfirmingAccess] = useState(false);
|
|
@@ -84,7 +86,7 @@ function MailboxDetailContent({
|
|
|
84
86
|
try {
|
|
85
87
|
await deleteMailbox(mailbox!.uid, mailbox!.version, { erase: eraseData });
|
|
86
88
|
// The Mailboxes page it goes to lists what a delete without `erase` kept, and how far an erasure has got.
|
|
87
|
-
|
|
89
|
+
void navigate("/admin");
|
|
88
90
|
} catch (err) {
|
|
89
91
|
// Most commonly a 409 if this mailbox is a custodian on an open legal hold (restapi's own
|
|
90
92
|
// `assertNotOnLegalHold()`, naming the blocking Matter uid(s)) - surfaced as-is, same as
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import AdminShell, { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
7
8
|
import MailboxCreateForm from "../../../shared/components/admin/settings/MailboxCreateForm.js";
|
|
8
9
|
|
|
9
10
|
export default function NewMailboxPage(props: Omit<AdminShellProps, "active">) {
|
|
11
|
+
const { navigate } = useRouter();
|
|
10
12
|
return (
|
|
11
13
|
<AdminShell {...props} active="mailboxes">
|
|
12
14
|
<div className="max-w-xl">
|
|
@@ -19,7 +21,7 @@ export default function NewMailboxPage(props: Omit<AdminShellProps, "active">) {
|
|
|
19
21
|
<MailboxCreateForm
|
|
20
22
|
cancelHref="/admin"
|
|
21
23
|
onCreated={(mailbox) => {
|
|
22
|
-
|
|
24
|
+
void navigate(`/admin/mailboxes/${encodeURIComponent(mailbox.uid)}`);
|
|
23
25
|
}}
|
|
24
26
|
/>
|
|
25
27
|
</div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { FormEvent, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import {
|
|
8
9
|
createTransportRule,
|
|
@@ -34,6 +35,7 @@ export default function NewTransportRulePage(props: Omit<AdminShellProps, "activ
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function NewTransportRuleForm() {
|
|
38
|
+
const { navigate } = useRouter();
|
|
37
39
|
const [name, setName] = useState("");
|
|
38
40
|
const [rule, setRule] = useState<RuleBuilderValue<TransportRuleConditions, TransportRuleAction>>({
|
|
39
41
|
enabled: true,
|
|
@@ -73,7 +75,7 @@ function NewTransportRuleForm() {
|
|
|
73
75
|
setSaving(true);
|
|
74
76
|
try {
|
|
75
77
|
const created = await createTransportRule({ name: name.trim(), ...rule });
|
|
76
|
-
|
|
78
|
+
void navigate(`/admin/transport-rules/${encodeURIComponent(created.uid)}`);
|
|
77
79
|
} catch (err) {
|
|
78
80
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the transport rule.");
|
|
79
81
|
} finally {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { FormEvent, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
7
8
|
import { toDatetimeLocal } from "@rapidmx/react-shared/util/dateInput.js";
|
|
8
9
|
import { createMatter } from "@rapidmx/react-shared/admin/mattersApi.js";
|
|
@@ -24,6 +25,7 @@ export default function NewMatterPage(props: Omit<EscrowShellProps, "active">) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function NewMatterForm() {
|
|
28
|
+
const { navigate } = useRouter();
|
|
27
29
|
const [name, setName] = useState("");
|
|
28
30
|
const [description, setDescription] = useState("");
|
|
29
31
|
// No dropdown here: `EscrowScope` (BaseEscrowScopeRoute) is trusted-admin-only end to end, so this
|
|
@@ -72,7 +74,7 @@ function NewMatterForm() {
|
|
|
72
74
|
dateRangeStart: new Date(dateRangeStart).toISOString(),
|
|
73
75
|
dateRangeEnd: new Date(dateRangeEnd).toISOString(),
|
|
74
76
|
});
|
|
75
|
-
|
|
77
|
+
void navigate(`/escrow/matters/${encodeURIComponent(created.uid)}`);
|
|
76
78
|
} catch (err) {
|
|
77
79
|
setError(err instanceof ApiRequestError ? err.message : "Could not create the matter.");
|
|
78
80
|
} finally {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import "../../../styles/app.css";
|
|
6
6
|
import React, { PropsWithChildren, ReactNode, useEffect, useState } from "react";
|
|
7
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
8
|
import {
|
|
8
9
|
HiOutlineClipboardDocumentList,
|
|
9
10
|
HiOutlineClock,
|
|
@@ -184,6 +185,7 @@ export function adminNavItems(pluginNav?: PluginNav): NavItem[] {
|
|
|
184
185
|
* 403 `api-103` (elevated, but not an administrator), any other 403, and 401 mean "no administrator access".
|
|
185
186
|
*/
|
|
186
187
|
export default function AdminShell({ active, userUid, authServerUrl, pluginNav, children }: PropsWithChildren<AdminShellProps>) {
|
|
188
|
+
const { navigate } = useRouter();
|
|
187
189
|
const [status, setStatus] = useState<Status>("checking");
|
|
188
190
|
const [error, setError] = useState<string | null>(null);
|
|
189
191
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
@@ -207,7 +209,8 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
|
|
|
207
209
|
if (active !== "setup") {
|
|
208
210
|
try {
|
|
209
211
|
if ((await getSetupStatus()).required) {
|
|
210
|
-
|
|
212
|
+
// Replacing this page in the history, which would otherwise send the back button round again.
|
|
213
|
+
void navigate(SETUP_ITEM.href, { replace: true });
|
|
211
214
|
return;
|
|
212
215
|
}
|
|
213
216
|
} catch {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { FormEvent, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
6
7
|
import { HiCheck } from "react-icons/hi2";
|
|
7
8
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
9
|
import { getRetentionPolicy } from "@rapidmx/react-shared/admin/retentionPolicyApi.js";
|
|
@@ -61,6 +62,7 @@ export interface SetupWizardProps {
|
|
|
61
62
|
* except the domain can be passed over and configured later from the admin console.
|
|
62
63
|
*/
|
|
63
64
|
export default function SetupWizard({ userUid }: SetupWizardProps) {
|
|
65
|
+
const { navigate } = useRouter();
|
|
64
66
|
const [step, setStep] = useState<SetupStepId | null>(null);
|
|
65
67
|
const [status, setStatus] = useState<SetupStatus | undefined>();
|
|
66
68
|
const [domains, setDomains] = useState<Domain[]>([]);
|
|
@@ -162,7 +164,7 @@ export default function SetupWizard({ userUid }: SetupWizardProps) {
|
|
|
162
164
|
// Let a step still being recorded land first, so it can't arrive after setup is marked finished.
|
|
163
165
|
await progress.current.running;
|
|
164
166
|
await completeSetup();
|
|
165
|
-
|
|
167
|
+
void navigate("/admin");
|
|
166
168
|
} catch (err) {
|
|
167
169
|
setError(errorMessage(err, "Could not finish setup."));
|
|
168
170
|
setFinishing(false);
|
|
@@ -9,7 +9,7 @@ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shar
|
|
|
9
9
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
10
10
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
11
11
|
import AppShell, { AppShellProps } from "../../layout/AppShell.js";
|
|
12
|
-
import {
|
|
12
|
+
import { useLocation } from "@rapidrest/react/client";
|
|
13
13
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
14
14
|
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
15
15
|
|
|
@@ -84,7 +84,7 @@ export default function CalendarShell({
|
|
|
84
84
|
|
|
85
85
|
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
86
86
|
// effect, not during render, so the server render and the hydrating render agree.
|
|
87
|
-
const search =
|
|
87
|
+
const { search } = useLocation();
|
|
88
88
|
useEffect(() => {
|
|
89
89
|
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
90
90
|
}, [search]);
|
|
@@ -10,7 +10,8 @@ import { Folder, Mailbox, listFolders, listMailboxes } from "@rapidmx/react-shar
|
|
|
10
10
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
11
|
import Skeleton, { SkeletonList } from "@rapidmx/react-shared/components/feedback/Skeleton.js";
|
|
12
12
|
import AppShell, { AppShellProps } from "../../layout/AppShell.js";
|
|
13
|
-
import {
|
|
13
|
+
import { useLocation } from "@rapidrest/react/client";
|
|
14
|
+
import { useNavigate } from "../../../navigation/index.js";
|
|
14
15
|
import MailboxProvisioning from "../../layout/MailboxProvisioning.js";
|
|
15
16
|
import { orderMailboxes, primaryMailboxUid } from "../../../mail/primaryMailbox.js";
|
|
16
17
|
|
|
@@ -60,7 +61,7 @@ export default function ContactsShell({
|
|
|
60
61
|
|
|
61
62
|
// Read from the router's location, so a mailbox change (a link, or `navigate()`) takes effect without a page load - and in an
|
|
62
63
|
// effect, not during render, so the server render and the hydrating render agree.
|
|
63
|
-
const search =
|
|
64
|
+
const { search } = useLocation();
|
|
64
65
|
const navigate = useNavigate();
|
|
65
66
|
useEffect(() => {
|
|
66
67
|
setRequestedMailboxUid(new URLSearchParams(search).get("mailboxUid"));
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: MPL-2.0
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import "../../styles/app.css";
|
|
6
|
-
import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
|
|
6
|
+
import React, { PropsWithChildren, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
7
7
|
import type { IconType } from "react-icons";
|
|
8
8
|
import {
|
|
9
9
|
HiOutlineCalendarDays,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
HiOutlinePuzzlePiece,
|
|
13
13
|
HiOutlineUsers,
|
|
14
14
|
} from "react-icons/hi2";
|
|
15
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
15
16
|
import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
|
|
16
17
|
import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
|
|
17
18
|
import { stopImpersonating } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
@@ -34,7 +35,7 @@ import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
|
|
|
34
35
|
import { mergePluginNavItems, PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
|
|
35
36
|
import { useInAppFrame } from "../../navigation/frameContext.js";
|
|
36
37
|
import { APP_HREFS } from "../../navigation/appHrefs.js";
|
|
37
|
-
import { useNavigate } from "../../navigation/
|
|
38
|
+
import { useNavigate } from "../../navigation/index.js";
|
|
38
39
|
import { GlobalShortcuts } from "../../keyboard/GlobalShortcuts.js";
|
|
39
40
|
import { ShortcutProvider } from "../../keyboard/ShortcutProvider.js";
|
|
40
41
|
import ShortcutsDialog from "../../keyboard/ShortcutsDialog.js";
|
|
@@ -138,12 +139,9 @@ export function appRailItems(pluginNav?: PluginNav): NavItem[] {
|
|
|
138
139
|
);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
/** What only the persistent frame (`
|
|
142
|
+
/** What only the persistent frame (`apps/www/_shell.tsx`) passes to the chrome it keeps mounted. */
|
|
142
143
|
export interface AppChromeProps extends AppShellProps {
|
|
143
|
-
/**
|
|
144
|
-
* sets the document title and announces the page. Absent outside the router, when nothing does. */
|
|
145
|
-
routeKey?: string;
|
|
146
|
-
/** The router is loading the next page's code - the content is `aria-busy`. */
|
|
144
|
+
/** The router is loading the next page - the content is `aria-busy`. */
|
|
147
145
|
busy?: boolean;
|
|
148
146
|
/** A screen that takes over the window (`FrameTakeover`) is showing: the rail, header, banner and footer are hidden - not
|
|
149
147
|
* unmounted, so what is inside them (and the page in the content region) keeps its state. */
|
|
@@ -156,8 +154,8 @@ export interface AppChromeProps extends AppShellProps {
|
|
|
156
154
|
* Each app's own shell (e.g. `MailShell`) renders its own contextual sidebar + content as `children`, inside
|
|
157
155
|
* the area to the right of the icon rail and below the header.
|
|
158
156
|
*
|
|
159
|
-
* Rendered by
|
|
160
|
-
* `AppShell` below for what a page's own shell renders instead.
|
|
157
|
+
* Rendered by the webmail's app shell (`apps/www/_shell.tsx`, the router's persistent client layout), once, for the life of the
|
|
158
|
+
* page, so that moving between apps replaces only `children` - see `AppShell` below for what a page's own shell renders instead.
|
|
161
159
|
*/
|
|
162
160
|
export function AppChrome({
|
|
163
161
|
active,
|
|
@@ -170,7 +168,6 @@ export function AppChrome({
|
|
|
170
168
|
branding: initialBranding,
|
|
171
169
|
appearance,
|
|
172
170
|
pluginNav,
|
|
173
|
-
routeKey,
|
|
174
171
|
busy,
|
|
175
172
|
hideChrome,
|
|
176
173
|
children,
|
|
@@ -198,6 +195,7 @@ export function AppChrome({
|
|
|
198
195
|
// the router (`AppShell` rendered as a page's own chrome: tests, plugin pages) it stays off, and a Mail shell owns its connection itself.
|
|
199
196
|
const inFrame = useInAppFrame();
|
|
200
197
|
const navigate = useNavigate();
|
|
198
|
+
const { pathname } = useRouter();
|
|
201
199
|
const mail = useMailConnection({ userUid, enabled: inFrame, open: navigate });
|
|
202
200
|
// A signing certificate the user asked for is announced when it is issued (or fails), on whichever page they are - see the hook.
|
|
203
201
|
useSigningEnrollmentWatcher({ userUid, mailboxes: mail.mailboxes, enabled: inFrame });
|
|
@@ -301,32 +299,21 @@ export function AppChrome({
|
|
|
301
299
|
}
|
|
302
300
|
}
|
|
303
301
|
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
302
|
+
// The tab's title in the frame is each page's own (its `title` export: rendered by the server, set again by the router on every navigation),
|
|
303
|
+
// but `useBranding()` above sets it to the branding's title once its fetch answers - a title for a page that has none. The page's is put back:
|
|
304
|
+
// the layout effect reads it before that hook's effect runs, this one (declared after it) writes it back.
|
|
305
|
+
const pageTitleRef = useRef("");
|
|
306
|
+
useLayoutEffect(() => {
|
|
307
|
+
pageTitleRef.current = document.title;
|
|
308
|
+
}, [fetchedBranding?.title]);
|
|
311
309
|
useEffect(() => {
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
document.title = pageLabel ? `${brand}: ${pageLabel}` : brand;
|
|
310
|
+
if (inFrame) {
|
|
311
|
+
document.title = pageTitleRef.current;
|
|
315
312
|
}
|
|
316
|
-
}, [
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
resetKey: `${routeKey}|${pageLabel}|${branding?.title}|${branding?.companyName}`,
|
|
321
|
-
});
|
|
322
|
-
useEffect(() => {
|
|
323
|
-
if (routeKey === undefined || routeKey === firstRouteKeyRef.current) {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
setAnnouncement(pageLabel ?? "");
|
|
327
|
-
contentRef.current?.focus({ preventScroll: true });
|
|
328
|
-
window.scrollTo(0, 0);
|
|
329
|
-
}, [routeKey]);
|
|
313
|
+
}, [fetchedBranding?.title, inFrame]);
|
|
314
|
+
// The router sets the document's title to the page's own whenever the page changes, which drops the unread count this puts in front
|
|
315
|
+
// of it, so a new pathname is what puts the count back. (A folder change is shallow and keeps the title.)
|
|
316
|
+
useUnreadTitle(inboxUnreadTotal(mail.mailboxFolders, mail.folderCounts.counts), { enabled: inFrame, resetKey: pathname });
|
|
330
317
|
|
|
331
318
|
if (!userUid) {
|
|
332
319
|
return <div className="min-h-screen" />;
|
|
@@ -442,7 +429,6 @@ export function AppChrome({
|
|
|
442
429
|
{/* The one pop-up stack for the whole app: right under the header row, so it never covers the account menu. */}
|
|
443
430
|
<NotificationCenter />
|
|
444
431
|
<div
|
|
445
|
-
ref={contentRef}
|
|
446
432
|
id="app-content"
|
|
447
433
|
tabIndex={-1}
|
|
448
434
|
aria-busy={busy || undefined}
|
|
@@ -452,11 +438,6 @@ export function AppChrome({
|
|
|
452
438
|
</div>
|
|
453
439
|
</div>
|
|
454
440
|
</div>
|
|
455
|
-
{routeKey !== undefined && (
|
|
456
|
-
<div role="status" aria-live="polite" className="sr-only">
|
|
457
|
-
{announcement}
|
|
458
|
-
</div>
|
|
459
|
-
)}
|
|
460
441
|
{!hideChrome && <FrameBrandingFooter parsed={footer} userMenu={menuInFooter ? renderUserMenu("up") : undefined} appTitle={activeLabel} />}
|
|
461
442
|
</div>
|
|
462
443
|
</ComposeProvider>
|
|
@@ -470,9 +451,9 @@ export function AppChrome({
|
|
|
470
451
|
|
|
471
452
|
/**
|
|
472
453
|
* What a page's own shell (`MailShell`, `CalendarShell`, ...) renders around its content. Outside the client-side router it
|
|
473
|
-
* is the whole `AppChrome`, as it always was. Inside it (`
|
|
474
|
-
* mounted as the page is replaced - so this is only its children; everything it would have been given comes from
|
|
475
|
-
*
|
|
454
|
+
* is the whole `AppChrome`, as it always was. Inside it (the router's app shell, `apps/www/_shell.tsx`) the chrome is already mounted above the
|
|
455
|
+
* page - and stays mounted as the page is replaced - so this is only its children; everything it would have been given comes from
|
|
456
|
+
* the shell instead (the props are the same for every page, and `active` is the route's).
|
|
476
457
|
*/
|
|
477
458
|
export default function AppShell(props: PropsWithChildren<AppShellProps>) {
|
|
478
459
|
const inFrame = useInAppFrame();
|
|
@@ -577,6 +577,7 @@ export default function ConversationThreadPane({
|
|
|
577
577
|
onReceiptHandled={patchMessage}
|
|
578
578
|
onScheduledSendCanceled={removeMessage}
|
|
579
579
|
onArchived={removeMessage}
|
|
580
|
+
onChanged={patchMessage}
|
|
580
581
|
labels={labels}
|
|
581
582
|
onLabelsChanged={patchMessage}
|
|
582
583
|
onLabelCreated={onLabelCreated}
|