@remit/web-client 0.0.229 → 0.0.231
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/package.json +1 -1
- package/src/components/calendar/CalendarComposePane.stories.tsx +25 -0
- package/src/components/calendar/CalendarComposePane.tsx +34 -0
- package/src/components/calendar/CalendarEventPane.stories.tsx +81 -0
- package/src/components/calendar/CalendarEventPane.tsx +67 -0
- package/src/components/calendar/CalendarShell.tsx +83 -0
- package/src/components/calendar/CalendarViewPlaceholder.tsx +37 -0
- package/src/components/calendar/CalendarWorkspace.stories.tsx +197 -0
- package/src/components/calendar/CalendarWorkspace.tsx +162 -0
- package/src/components/calendar/OpenCalendarEvent.tsx +39 -0
- package/src/components/calendar/calendar-workspace.render.test.ts +193 -0
- package/src/components/config-import/ConfigImportWizard.flow.test.ts +338 -0
- package/src/components/config-import/ConfigImportWizard.tsx +720 -0
- package/src/components/config-import/config-import.fixtures.ts +229 -0
- package/src/components/config-import/config-import.stories.tsx +449 -0
- package/src/components/config-import/steps.tsx +772 -0
- package/src/components/mail/MailSidebarAdapter.tsx +17 -0
- package/src/components/onboarding/OnboardingWizard.tsx +64 -390
- package/src/components/onboarding/credential-steps.tsx +441 -0
- package/src/components/settings/ConfigExportCard.tsx +114 -0
- package/src/components/settings/ConfigTransferPanel.tsx +72 -0
- package/src/hooks/useCalendarData.ts +72 -0
- package/src/lib/calendar-density.ts +34 -0
- package/src/lib/calendar-fixtures.ts +207 -0
- package/src/lib/calendar-format.ts +29 -0
- package/src/lib/calendar-route.test.ts +210 -0
- package/src/lib/calendar-route.ts +183 -0
- package/src/lib/config-import.test.ts +306 -0
- package/src/lib/config-import.ts +492 -0
- package/src/lib/json.ts +11 -0
- package/src/lib/search-params.test.ts +77 -0
- package/src/lib/search-params.ts +59 -0
- package/src/routeTree.gen.ts +220 -0
- package/src/router.tsx +5 -0
- package/src/routes/calendar/$view.$date/$calendarObjectId/$recurrenceId.tsx +26 -0
- package/src/routes/calendar/$view.$date/$calendarObjectId/index.tsx +18 -0
- package/src/routes/calendar/$view.$date/$calendarObjectId.tsx +15 -0
- package/src/routes/calendar/$view.$date/index.tsx +25 -0
- package/src/routes/calendar/$view.$date/new.tsx +20 -0
- package/src/routes/calendar/$view.$date.tsx +100 -0
- package/src/routes/calendar/-calendar-tree.test.ts +267 -0
- package/src/routes/calendar/index.tsx +27 -0
- package/src/routes/calendar.tsx +98 -0
- package/src/routes/import.tsx +51 -0
- package/src/routes/onboarding.tsx +9 -2
- package/src/routes/settings/accounts.tsx +30 -8
- package/src/routes/settings/advanced.tsx +5 -1
- package/src/routing/calendar-fragment.test.ts +66 -0
- package/src/routing/calendar.ts +139 -0
- package/src/routing/fragment.ts +19 -0
- package/src/routing/index.ts +10 -0
- package/src/routing/nav-selection.ts +2 -0
- package/src/routing/sections.ts +3 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fragment across the mail↔calendar switch.
|
|
3
|
+
*
|
|
4
|
+
* The calendar is a sibling of mail in the shell, not a different app, so the
|
|
5
|
+
* chrome a reader has up travels with them: the rail is a pane they keep, the
|
|
6
|
+
* slide-over and the sheet are overlays that going somewhere dismisses. The
|
|
7
|
+
* calendar owns no panel of its own — nothing calendar-specific joins the union
|
|
8
|
+
* (R3) — so the only thing it does to the fragment is carry it.
|
|
9
|
+
*/
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { describe, it } from "node:test";
|
|
12
|
+
import {
|
|
13
|
+
formatOpenPanels,
|
|
14
|
+
panelFragments,
|
|
15
|
+
panelsWithOverlay,
|
|
16
|
+
parseOpenPanels,
|
|
17
|
+
retainOpenPanelsAtTier,
|
|
18
|
+
} from "./fragment.js";
|
|
19
|
+
|
|
20
|
+
const retainOnDesktop = retainOpenPanelsAtTier(true);
|
|
21
|
+
const retainOnPhone = retainOpenPanelsAtTier(false);
|
|
22
|
+
|
|
23
|
+
describe("the calendar names no panel of its own", () => {
|
|
24
|
+
it("leaves the union exactly as mail wrote it", () => {
|
|
25
|
+
assert.deepEqual(panelFragments, ["intelligence", "nav", "shortcuts"]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("crossing between mail and the calendar", () => {
|
|
30
|
+
// The nav entry is a `NavLink`, so both directions go through the same
|
|
31
|
+
// retention the mail lists already navigate under.
|
|
32
|
+
it("carries the rail into the calendar and back out again", () => {
|
|
33
|
+
assert.equal(retainOnDesktop("intelligence"), "intelligence");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("leaves the slide-over the reader picked the entry from behind", () => {
|
|
37
|
+
assert.equal(retainOnDesktop("nav"), "");
|
|
38
|
+
assert.equal(retainOnDesktop("intelligence,nav"), "intelligence");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("leaves the drawer behind on a tier that has no rail", () => {
|
|
42
|
+
assert.equal(retainOnPhone("intelligence"), "");
|
|
43
|
+
assert.equal(retainOnPhone("intelligence,nav"), "");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("reaching for the folders while on the calendar", () => {
|
|
48
|
+
const opened = (hash: string): string =>
|
|
49
|
+
formatOpenPanels(panelsWithOverlay(parseOpenPanels(hash), "nav"));
|
|
50
|
+
const closed = (hash: string): string =>
|
|
51
|
+
formatOpenPanels(panelsWithOverlay(parseOpenPanels(hash), undefined));
|
|
52
|
+
|
|
53
|
+
it("keeps the rail the reader arrived with", () => {
|
|
54
|
+
assert.equal(opened("intelligence"), "intelligence,nav");
|
|
55
|
+
assert.equal(closed("intelligence,nav"), "intelligence");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("puts the slide-over up over nothing else, and takes it down again", () => {
|
|
59
|
+
assert.equal(opened(""), "nav");
|
|
60
|
+
assert.equal(closed("nav"), "");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("replaces whichever overlay was up, because two cannot be", () => {
|
|
64
|
+
assert.equal(opened("intelligence,shortcuts"), "intelligence,nav");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { CalendarViewId } from "@remit/ui";
|
|
2
|
+
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
canonicalCalendarParams,
|
|
6
|
+
isoDate,
|
|
7
|
+
stepCalendarDate,
|
|
8
|
+
} from "@/lib/calendar-route";
|
|
9
|
+
import { useRetainOpenPanels } from "./fragment";
|
|
10
|
+
|
|
11
|
+
const VIEW_ROUTE = "/calendar/$view/$date" as const;
|
|
12
|
+
|
|
13
|
+
/** Which zoom the calendar is at, on which day, over which calendars. */
|
|
14
|
+
export interface CalendarAddress {
|
|
15
|
+
view: CalendarViewId;
|
|
16
|
+
/** `YYYY-MM-DD`. */
|
|
17
|
+
date: string;
|
|
18
|
+
/** The calendars ticked in the query; empty means every one of them. */
|
|
19
|
+
calendarIds: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The calendar the address names.
|
|
24
|
+
*
|
|
25
|
+
* The route canonicalises its own segments before anything mounts, so what
|
|
26
|
+
* arrives here is always a view the ladder has and a day the calendar has —
|
|
27
|
+
* the fallbacks below only cover the frame before the router has resolved
|
|
28
|
+
* the params.
|
|
29
|
+
*/
|
|
30
|
+
export function useCalendarAddress(): CalendarAddress {
|
|
31
|
+
const params = useParams({ from: VIEW_ROUTE, shouldThrow: false });
|
|
32
|
+
const search = useSearch({ from: VIEW_ROUTE, shouldThrow: false });
|
|
33
|
+
const view = params?.view ?? "";
|
|
34
|
+
const date = params?.date ?? "";
|
|
35
|
+
const calendarIds = search?.calendarId;
|
|
36
|
+
return useMemo(() => {
|
|
37
|
+
const canonical = canonicalCalendarParams(
|
|
38
|
+
{ view, date },
|
|
39
|
+
isoDate(new Date()),
|
|
40
|
+
);
|
|
41
|
+
return { ...canonical, calendarIds: calendarIds ?? [] };
|
|
42
|
+
}, [view, date, calendarIds]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The event the reading pane has open, and the instance of it, if any. */
|
|
46
|
+
export interface OpenCalendarEvent {
|
|
47
|
+
calendarObjectId: string;
|
|
48
|
+
/** One instance of a series; absent on the series itself. */
|
|
49
|
+
recurrenceId: string | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useOpenCalendarEvent(): OpenCalendarEvent | undefined {
|
|
53
|
+
const event = useParams({
|
|
54
|
+
from: "/calendar/$view/$date/$calendarObjectId",
|
|
55
|
+
shouldThrow: false,
|
|
56
|
+
});
|
|
57
|
+
const instance = useParams({
|
|
58
|
+
from: "/calendar/$view/$date/$calendarObjectId/$recurrenceId",
|
|
59
|
+
shouldThrow: false,
|
|
60
|
+
});
|
|
61
|
+
const calendarObjectId = event?.calendarObjectId;
|
|
62
|
+
const recurrenceId = instance?.recurrenceId;
|
|
63
|
+
return useMemo(
|
|
64
|
+
() => (calendarObjectId ? { calendarObjectId, recurrenceId } : undefined),
|
|
65
|
+
[calendarObjectId, recurrenceId],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Whether the composer is up, which is a route rather than a flag. */
|
|
70
|
+
export function useIsWritingEvent(): boolean {
|
|
71
|
+
const composer = useParams({
|
|
72
|
+
from: "/calendar/$view/$date/new",
|
|
73
|
+
shouldThrow: false,
|
|
74
|
+
});
|
|
75
|
+
return composer !== undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CalendarNavigation {
|
|
79
|
+
/** Change zoom, keeping the day on screen. */
|
|
80
|
+
goToView: (view: CalendarViewId) => void;
|
|
81
|
+
/** The previous or next screenful, measured in the view's own unit. */
|
|
82
|
+
step: (direction: -1 | 1) => void;
|
|
83
|
+
/** Home, from every view and every distance away. */
|
|
84
|
+
goToToday: () => void;
|
|
85
|
+
openEvent: (calendarObjectId: string) => void;
|
|
86
|
+
closeEvent: () => void;
|
|
87
|
+
openComposer: () => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Every move the calendar makes, as an address.
|
|
92
|
+
*
|
|
93
|
+
* Prev, next, today and the zoom switch are navigations rather than state
|
|
94
|
+
* changes, so the address always names what is on screen and a link to it
|
|
95
|
+
* reopens exactly that. Each one carries the open panes across the way the nav
|
|
96
|
+
* does, so the reader keeps their chrome moving through the week.
|
|
97
|
+
*/
|
|
98
|
+
export function useCalendarNavigation(): CalendarNavigation {
|
|
99
|
+
const navigate = useNavigate();
|
|
100
|
+
const retainPanels = useRetainOpenPanels();
|
|
101
|
+
const { view, date } = useCalendarAddress();
|
|
102
|
+
|
|
103
|
+
const goTo = useCallback(
|
|
104
|
+
(params: { view: CalendarViewId; date: string }) => {
|
|
105
|
+
navigate({
|
|
106
|
+
to: VIEW_ROUTE,
|
|
107
|
+
params,
|
|
108
|
+
search: true,
|
|
109
|
+
hash: retainPanels,
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
[navigate, retainPanels],
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return useMemo(
|
|
116
|
+
() => ({
|
|
117
|
+
goToView: (next: CalendarViewId) => goTo({ view: next, date }),
|
|
118
|
+
step: (direction: -1 | 1) =>
|
|
119
|
+
goTo({ view, date: stepCalendarDate(date, view, direction) }),
|
|
120
|
+
goToToday: () => goTo({ view, date: isoDate(new Date()) }),
|
|
121
|
+
openEvent: (calendarObjectId: string) =>
|
|
122
|
+
navigate({
|
|
123
|
+
to: "/calendar/$view/$date/$calendarObjectId",
|
|
124
|
+
params: { view, date, calendarObjectId },
|
|
125
|
+
search: true,
|
|
126
|
+
hash: retainPanels,
|
|
127
|
+
}),
|
|
128
|
+
closeEvent: () => goTo({ view, date }),
|
|
129
|
+
openComposer: () =>
|
|
130
|
+
navigate({
|
|
131
|
+
to: "/calendar/$view/$date/new",
|
|
132
|
+
params: { view, date },
|
|
133
|
+
search: true,
|
|
134
|
+
hash: retainPanels,
|
|
135
|
+
}),
|
|
136
|
+
}),
|
|
137
|
+
[goTo, navigate, retainPanels, view, date],
|
|
138
|
+
);
|
|
139
|
+
}
|
package/src/routing/fragment.ts
CHANGED
|
@@ -27,6 +27,25 @@ export type OverlayPanel = (typeof overlayPanels)[number];
|
|
|
27
27
|
export const isOverlayPanel = (panel: PanelFragment): panel is OverlayPanel =>
|
|
28
28
|
(overlayPanels as readonly PanelFragment[]).includes(panel);
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The set an overlay opening or closing leaves behind it.
|
|
32
|
+
*
|
|
33
|
+
* A write states the whole set, so raising the nav slide-over has to say what
|
|
34
|
+
* else is up or it takes it down. The panes stay and the previous overlay goes,
|
|
35
|
+
* because two overlays cannot both be up — which is how a reader crossing from
|
|
36
|
+
* their mail into the calendar and reaching for the folders keeps the rail they
|
|
37
|
+
* arrived with.
|
|
38
|
+
*/
|
|
39
|
+
export function panelsWithOverlay(
|
|
40
|
+
open: readonly PanelFragment[],
|
|
41
|
+
overlay: OverlayPanel | undefined,
|
|
42
|
+
): PanelFragment[] {
|
|
43
|
+
return [
|
|
44
|
+
...open.filter((panel) => !isOverlayPanel(panel)),
|
|
45
|
+
...(overlay ? [overlay] : []),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
const panelFragmentSchema = z.enum(panelFragments);
|
|
31
50
|
|
|
32
51
|
/**
|
package/src/routing/index.ts
CHANGED
|
@@ -3,6 +3,15 @@ export {
|
|
|
3
3
|
useBrowsedList,
|
|
4
4
|
useOpensDetail,
|
|
5
5
|
} from "./browsed-list";
|
|
6
|
+
export {
|
|
7
|
+
type CalendarAddress,
|
|
8
|
+
type CalendarNavigation,
|
|
9
|
+
type OpenCalendarEvent,
|
|
10
|
+
useCalendarAddress,
|
|
11
|
+
useCalendarNavigation,
|
|
12
|
+
useIsWritingEvent,
|
|
13
|
+
useOpenCalendarEvent,
|
|
14
|
+
} from "./calendar";
|
|
6
15
|
export {
|
|
7
16
|
useAdoptComposeDraft,
|
|
8
17
|
useCloseCompose,
|
|
@@ -18,6 +27,7 @@ export {
|
|
|
18
27
|
overlayPanels,
|
|
19
28
|
type PanelFragment,
|
|
20
29
|
panelFragments,
|
|
30
|
+
panelsWithOverlay,
|
|
21
31
|
parseOpenPanels,
|
|
22
32
|
retainOpenPanelsAtTier,
|
|
23
33
|
useOpenPanels,
|
|
@@ -5,6 +5,7 @@ import { locationIsOnList } from "@/lib/mail-route";
|
|
|
5
5
|
* The nav entry the address selects, or `""` where none does.
|
|
6
6
|
*
|
|
7
7
|
* - `/settings/*` → "settings"
|
|
8
|
+
* - `/calendar/*` → "calendar"
|
|
8
9
|
* - `/mail/outbox`, `/mail/flagged`, `/mail/brief` → that list
|
|
9
10
|
* - `/mail/$mailboxId` → the mailbox id, which is what the sidebar keys on
|
|
10
11
|
*
|
|
@@ -17,6 +18,7 @@ export function useSelectedNavId(): string {
|
|
|
17
18
|
const mailbox = useParams({ from: "/mail/$mailboxId", shouldThrow: false });
|
|
18
19
|
|
|
19
20
|
if (locationIsOnList(pathname, "/settings")) return "settings";
|
|
21
|
+
if (locationIsOnList(pathname, "/calendar")) return "calendar";
|
|
20
22
|
if (locationIsOnList(pathname, "/mail/outbox")) return "outbox";
|
|
21
23
|
if (locationIsOnList(pathname, "/mail/flagged")) return "flagged";
|
|
22
24
|
if (locationIsOnList(pathname, "/mail/brief")) return "brief";
|
package/src/routing/sections.ts
CHANGED
|
@@ -11,7 +11,8 @@ export type AppSection =
|
|
|
11
11
|
| "flagged"
|
|
12
12
|
| "outbox"
|
|
13
13
|
| "settings"
|
|
14
|
-
| "accounts"
|
|
14
|
+
| "accounts"
|
|
15
|
+
| "importConfig";
|
|
15
16
|
|
|
16
17
|
const sectionRoute = {
|
|
17
18
|
brief: "/mail/brief",
|
|
@@ -19,6 +20,7 @@ const sectionRoute = {
|
|
|
19
20
|
outbox: "/mail/outbox",
|
|
20
21
|
settings: "/settings",
|
|
21
22
|
accounts: "/settings/accounts",
|
|
23
|
+
importConfig: "/import",
|
|
22
24
|
} as const satisfies Record<AppSection, string>;
|
|
23
25
|
|
|
24
26
|
/** Go to a top-level section by name, so no caller writes a path. */
|