@rapidmx/web-client 0.23.0 → 0.25.1
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/apps/admin/mailboxes/[uid].tsx +2 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +39 -4
- package/apps/shared/components/admin/layout/railPreference.ts +25 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +2 -1
- package/apps/shared/components/calendar/AppointmentQuickForm.tsx +2 -1
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +6 -2
- package/apps/shared/components/calendar/EventDescriptionView.tsx +37 -5
- package/apps/shared/components/calendar/EventDetails.tsx +15 -6
- package/apps/shared/components/calendar/EventEditor.tsx +5 -2
- package/apps/shared/components/calendar/EventFormParts.tsx +45 -39
- package/apps/shared/components/calendar/EventQuickForm.tsx +3 -2
- package/apps/shared/components/calendar/FindATime.tsx +3 -2
- package/apps/shared/components/calendar/TimeComboBox.tsx +143 -0
- package/apps/shared/components/calendar/eventFormat.ts +15 -0
- package/apps/shared/components/calendar/timePicker.ts +106 -0
- package/apps/shared/components/layout/ThemeSwitch.tsx +3 -2
- package/apps/shared/components/layout/UserMenu.tsx +30 -10
- package/apps/shared/components/mail/invite/inviteFormat.ts +6 -5
- package/apps/shared/components/mail/layout/MailShell.tsx +13 -5
- package/apps/shared/components/mail/layout/folderIcons.tsx +54 -0
- package/apps/shared/components/settings/layout/SettingsShell.tsx +32 -13
- package/apps/shared/navigation/appHrefs.ts +3 -3
- package/apps/www/calendar/index.tsx +5 -2
- package/apps/www/settings/profile/index.tsx +8 -12
- package/dist/apps/admin/mailboxes/[uid].js +2 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +21 -4
- package/dist/apps/shared/components/admin/layout/railPreference.d.ts +6 -0
- package/dist/apps/shared/components/admin/layout/railPreference.js +24 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +2 -2
- package/dist/apps/shared/components/calendar/AppointmentQuickForm.js +2 -1
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +3 -2
- package/dist/apps/shared/components/calendar/EventDescriptionView.d.ts +6 -1
- package/dist/apps/shared/components/calendar/EventDescriptionView.js +34 -4
- package/dist/apps/shared/components/calendar/EventDetails.js +4 -4
- package/dist/apps/shared/components/calendar/EventEditor.js +5 -2
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +1 -1
- package/dist/apps/shared/components/calendar/EventFormParts.js +9 -3
- package/dist/apps/shared/components/calendar/EventQuickForm.js +3 -2
- package/dist/apps/shared/components/calendar/FindATime.js +3 -2
- package/dist/apps/shared/components/calendar/TimeComboBox.d.ts +25 -0
- package/dist/apps/shared/components/calendar/TimeComboBox.js +77 -0
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +4 -0
- package/dist/apps/shared/components/calendar/eventFormat.js +13 -0
- package/dist/apps/shared/components/calendar/timePicker.d.ts +31 -0
- package/dist/apps/shared/components/calendar/timePicker.js +89 -0
- package/dist/apps/shared/components/layout/ThemeSwitch.js +2 -2
- package/dist/apps/shared/components/layout/UserMenu.d.ts +1 -2
- package/dist/apps/shared/components/layout/UserMenu.js +4 -4
- package/dist/apps/shared/components/mail/invite/inviteFormat.js +6 -5
- package/dist/apps/shared/components/mail/layout/MailShell.js +5 -4
- package/dist/apps/shared/components/mail/layout/folderIcons.d.ts +8 -0
- package/dist/apps/shared/components/mail/layout/folderIcons.js +25 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +16 -15
- package/dist/apps/shared/navigation/appHrefs.d.ts +3 -3
- package/dist/apps/shared/navigation/appHrefs.js +3 -3
- package/dist/apps/www/calendar/index.js +5 -2
- package/dist/apps/www/settings/profile/index.js +6 -5
- package/dist/lib/auth/session.d.ts +14 -1
- package/dist/lib/auth/session.js +40 -3
- package/dist/lib/components/pickers/TimeZonePicker.d.ts +20 -0
- package/dist/lib/components/pickers/TimeZonePicker.js +109 -0
- package/dist/lib/util/api.d.ts +6 -0
- package/dist/lib/util/api.js +22 -1
- package/dist/lib/util/timeZone.d.ts +19 -1
- package/dist/lib/util/timeZone.js +59 -2
- package/lib/auth/session.ts +48 -3
- package/lib/components/pickers/TimeZonePicker.tsx +185 -0
- package/lib/util/api.ts +24 -1
- package/lib/util/timeZone.ts +65 -2
- package/package.json +1 -1
package/lib/util/api.ts
CHANGED
|
@@ -157,10 +157,33 @@ export async function apiFetch<T = unknown>(path: string, init: RequestInit = {}
|
|
|
157
157
|
applyCsrfHeader(headers, init.method);
|
|
158
158
|
const credentials = apiBaseUrl ? "include" : init.credentials;
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
let res = await fetch(apiUrl(path), { ...init, headers, credentials });
|
|
161
|
+
// The access token has run out (a sleeping tab or laptop stops the timer that renews it in time): renew the session, and ask again once.
|
|
162
|
+
if (res.status === 401 && sessionRecovery && (await recoverSession())) {
|
|
163
|
+
res = await fetch(apiUrl(path), { ...init, headers, credentials });
|
|
164
|
+
}
|
|
161
165
|
return decodeApiResponse<T>(res, (error) => unauthorizedObserver?.(error));
|
|
162
166
|
}
|
|
163
167
|
|
|
168
|
+
let sessionRecovery: (() => Promise<boolean>) | undefined;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Registers what `apiFetch()` does when the server answers `401`: try to renew the signed-in session and, when that worked, send the request once
|
|
172
|
+
* more. `recovery` resolves whether the session was renewed. The app frame registers it while a session is open (`useSessionRefresh()`); pass
|
|
173
|
+
* `undefined` to remove it. A rejection counts as "not renewed".
|
|
174
|
+
*/
|
|
175
|
+
export function setApiSessionRecovery(recovery: (() => Promise<boolean>) | undefined): void {
|
|
176
|
+
sessionRecovery = recovery;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function recoverSession(): Promise<boolean> {
|
|
180
|
+
try {
|
|
181
|
+
return await sessionRecovery!();
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
164
187
|
/**
|
|
165
188
|
* `fetch()` against auth-server's API — a *different* origin than this app, unlike `apiFetch()` above. Used
|
|
166
189
|
* only for the handful of actions that must be issued by auth-server itself (e.g. admin impersonation, which
|
package/lib/util/timeZone.ts
CHANGED
|
@@ -19,7 +19,70 @@ export function deviceTimeZone(): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* What `zone`'s clock is called and how far it is from UTC at `at`: `{ abbreviation: "PDT", offset: "-07:00" }`. `null` for a zone the runtime doesn't
|
|
23
|
+
* know. The abbreviation is the runtime's own (`GMT+1` where it has no name for a zone), the offset counts daylight saving on that date.
|
|
24
|
+
*/
|
|
25
|
+
export function zoneClock(zone: string, at: Date = new Date()): { abbreviation: string; offset: string } | null {
|
|
26
|
+
try {
|
|
27
|
+
const part = (style: "short" | "longOffset") =>
|
|
28
|
+
formatterFor(zone, style).formatToParts(at).find((p) => p.type === "timeZoneName")!.value;
|
|
29
|
+
return { abbreviation: part("short"), offset: part("longOffset").replace("GMT", "") || "+00:00" };
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Building an `Intl.DateTimeFormat` is by far the costly part of naming a zone's clock, and a list names hundreds: each is made once. */
|
|
36
|
+
const formatters = new Map<string, Intl.DateTimeFormat>();
|
|
37
|
+
|
|
38
|
+
function formatterFor(zone: string, style: "short" | "longOffset"): Intl.DateTimeFormat {
|
|
39
|
+
const key = `${style}|${zone}`;
|
|
40
|
+
let formatter = formatters.get(key);
|
|
41
|
+
if (!formatter) {
|
|
42
|
+
// Throws for a zone the runtime doesn't know, which is then not remembered.
|
|
43
|
+
formatter = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: style });
|
|
44
|
+
formatters.set(key, formatter);
|
|
45
|
+
}
|
|
46
|
+
return formatter;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A zone id split into its city and the region it is in: "America/Argentina/Buenos_Aires" is "Buenos Aires" in "America/Argentina"; "UTC" is in none. */
|
|
50
|
+
function zoneNames(zone: string): { city: string; region: string } {
|
|
51
|
+
const parts = zone.split("/");
|
|
52
|
+
const city = parts.pop()!.replace(/_/g, " ");
|
|
53
|
+
return { city, region: parts.join("/").replace(/_/g, " ") };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A zone as a list or a line of text names it, city first, with how far its clock is from UTC: "Los Angeles, America (GMT-07:00)". Daylight saving
|
|
58
|
+
* is read at `at` (now, unless a date matters). A zone the runtime doesn't know is just its name.
|
|
59
|
+
*/
|
|
60
|
+
export function describeTimeZone(zone: string, at: Date = new Date()): string {
|
|
61
|
+
const clock = zoneClock(zone, at);
|
|
62
|
+
if (!clock) {
|
|
63
|
+
return zone.replace(/_/g, " ");
|
|
64
|
+
}
|
|
65
|
+
const { city, region } = zoneNames(zone);
|
|
66
|
+
return `${city}${region ? `, ${region}` : ""} (GMT${clock.offset})`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Orders zones for a list: by how far their clocks are from UTC at `at` (the westernmost first), then by city, then by id. A zone the runtime
|
|
71
|
+
* doesn't know comes last.
|
|
72
|
+
*/
|
|
73
|
+
export function sortTimeZones(zones: readonly string[], at: Date = new Date()): string[] {
|
|
74
|
+
// Each zone's offset is read once, not once per comparison.
|
|
75
|
+
const keyed = zones.map((zone) => {
|
|
76
|
+
const offset = zoneClock(zone, at)?.offset;
|
|
77
|
+
const minutes = offset ? (offset.startsWith("-") ? -1 : 1) * (Number(offset.slice(1, 3)) * 60 + Number(offset.slice(4, 6))) : Number.POSITIVE_INFINITY;
|
|
78
|
+
return { zone, minutes, city: zoneNames(zone).city };
|
|
79
|
+
});
|
|
80
|
+
keyed.sort((a, b) => (a.minutes === b.minutes ? 0 : a.minutes < b.minutes ? -1 : 1) || a.city.localeCompare(b.city) || a.zone.localeCompare(b.zone));
|
|
81
|
+
return keyed.map((entry) => entry.zone);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Every time zone the browser can name, for a picker (sorted by offset from UTC as it is now, then by city): `Intl.supportedValuesOf("timeZone")` where there is one (it leaves out `UTC`,
|
|
23
86
|
* which is added), else just `extra` - the zones the caller already holds, such as a mailbox's current one.
|
|
24
87
|
*/
|
|
25
88
|
export function timeZoneOptions(...extra: string[]): string[] {
|
|
@@ -29,5 +92,5 @@ export function timeZoneOptions(...extra: string[]): string[] {
|
|
|
29
92
|
} catch {
|
|
30
93
|
zones = [];
|
|
31
94
|
}
|
|
32
|
-
return [...new Set([DEFAULT_TIME_ZONE, ...zones, ...extra.filter(Boolean)])]
|
|
95
|
+
return sortTimeZones([...new Set([DEFAULT_TIME_ZONE, ...zones, ...extra.filter(Boolean)])]);
|
|
33
96
|
}
|
package/package.json
CHANGED