@remit/web-client 0.0.313 → 0.0.315

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/src/components/calendar/CalendarComposePane.tsx +15 -2
  3. package/src/components/calendar/CalendarWaiting.tsx +3 -0
  4. package/src/components/calendar/ChangeCalendarSuggestion.stories.tsx +184 -0
  5. package/src/components/calendar/ChangeCalendarSuggestion.tsx +159 -0
  6. package/src/components/calendar/PendingSuggestions.tsx +35 -5
  7. package/src/components/calendar/calendar-story-server.tsx +5 -0
  8. package/src/components/calendar/change-suggestion-first.render.test.ts +301 -0
  9. package/src/components/calendar/pending-suggestions.render.test.ts +70 -3
  10. package/src/components/mail/BriefPane.tsx +6 -1
  11. package/src/components/mail/FlaggedPane.tsx +6 -1
  12. package/src/components/mail/IntelligenceDrawer.tsx +4 -0
  13. package/src/components/mail/IntelligencePane.tsx +11 -2
  14. package/src/components/mail/MailboxPane.tsx +5 -0
  15. package/src/components/mail/offer-times-reply.render.test.ts +2 -0
  16. package/src/hooks/calendar/draft.ts +1 -1
  17. package/src/hooks/calendar/index.ts +7 -0
  18. package/src/hooks/calendar/suggestion-draft.ts +77 -0
  19. package/src/hooks/calendar/useCalendarSuggestions.ts +59 -1
  20. package/src/hooks/intelligence-calendar.render.test.ts +192 -3
  21. package/src/hooks/useIntelligenceCalendar.ts +46 -5
  22. package/src/lib/calendar-suggestion.test.ts +12 -1
  23. package/src/lib/calendar-suggestion.ts +12 -2
  24. package/src/lib/calendar-suggestion.west.test.ts +2 -0
  25. package/src/routeTree.gen.ts +23 -0
  26. package/src/routes/calendar/$view.$date/suggestion.$suggestionId.tsx +22 -0
  27. package/src/routes/calendar/$view.$date.tsx +9 -3
  28. package/src/routes/calendar/-calendar-tree.test.ts +18 -0
  29. package/src/routing/calendar.ts +36 -1
  30. package/src/routing/index.ts +2 -0
  31. package/src/test-support/thread-router.ts +54 -0
@@ -0,0 +1,22 @@
1
+ // biome-ignore lint/style/useFilenamingConvention: TanStack Router convention
2
+ import { createFileRoute } from "@tanstack/react-router";
3
+ import { ChangeCalendarSuggestion } from "@/components/calendar/ChangeCalendarSuggestion";
4
+ import { useCalendarNavigation, useChangingSuggestionId } from "@/routing";
5
+
6
+ function ChangeSuggestionRoute() {
7
+ const suggestionId = useChangingSuggestionId();
8
+ const { closeEvent } = useCalendarNavigation();
9
+ if (suggestionId === undefined) return null;
10
+ return (
11
+ <ChangeCalendarSuggestion
12
+ suggestionId={suggestionId}
13
+ onClose={closeEvent}
14
+ />
15
+ );
16
+ }
17
+
18
+ export const Route = createFileRoute(
19
+ "/calendar/$view/$date/suggestion/$suggestionId",
20
+ )({
21
+ component: ChangeSuggestionRoute,
22
+ });
@@ -86,8 +86,14 @@ function CalendarView({ isPhone }: { isPhone: boolean }) {
86
86
  const drawsGrid = !calendarViewMountsAgenda(view);
87
87
  const { events, colorByCalendarId, isLoading, error, retry, instanceOf } =
88
88
  useCalendarData({ view, date, calendarIds, enabled: drawsGrid });
89
- const { goToView, goToToday, step, openEvent, openComposer } =
90
- useCalendarNavigation();
89
+ const {
90
+ goToView,
91
+ goToToday,
92
+ step,
93
+ openEvent,
94
+ openComposer,
95
+ changeSuggestionFirst,
96
+ } = useCalendarNavigation();
91
97
  const openedEvent = useOpenCalendarEvent();
92
98
  const isWriting = useIsWritingEvent();
93
99
  // Held in state rather than read back from storage each render: how much of
@@ -123,7 +129,7 @@ function CalendarView({ isPhone }: { isPhone: boolean }) {
123
129
  events={events}
124
130
  colorByCalendarId={colorByCalendarId}
125
131
  agenda={<AgendaView density={density} onPickSlot={pickSlot} />}
126
- waiting={<PendingSuggestions />}
132
+ waiting={<PendingSuggestions onChangeFirst={changeSuggestionFirst} />}
127
133
  isLoading={isLoading}
128
134
  error={error}
129
135
  onRetry={retry}
@@ -31,6 +31,7 @@ import { Route as EventIndexRoute } from "./$view.$date/$calendarObjectId/index.
31
31
  import { Route as EventRoute } from "./$view.$date/$calendarObjectId.js";
32
32
  import { Route as ReadingPaneRoute } from "./$view.$date/index.js";
33
33
  import { Route as ComposeRoute } from "./$view.$date/new.js";
34
+ import { Route as SuggestionRoute } from "./$view.$date/suggestion.$suggestionId.js";
34
35
  import { Route as ViewRoute } from "./$view.$date.js";
35
36
  import { Route as CalendarIndexRoute } from "./index.js";
36
37
 
@@ -68,6 +69,10 @@ eventRoute.addChildren([
68
69
  viewRoute.addChildren([
69
70
  attach(ReadingPaneRoute, { path: "/", getParentRoute: () => viewRoute }),
70
71
  attach(ComposeRoute, { path: "new", getParentRoute: () => viewRoute }),
72
+ attach(SuggestionRoute, {
73
+ path: "suggestion/$suggestionId",
74
+ getParentRoute: () => viewRoute,
75
+ }),
71
76
  eventRoute,
72
77
  ]);
73
78
  const routeTree = rootRoute.addChildren([
@@ -253,6 +258,19 @@ describe("the composer and the open event cannot both be addressed", () => {
253
258
  assert.equal(mounted.routeIds.includes("/calendar/$view/$date/new"), false);
254
259
  });
255
260
 
261
+ it("mounts the editor for a suggestion, and nothing of the event route", () => {
262
+ const mounted = match("/calendar/day/2026-06-11/suggestion/sug_1");
263
+ assert.equal(
264
+ mounted.routeIds.at(-1),
265
+ "/calendar/$view/$date/suggestion/$suggestionId",
266
+ );
267
+ assert.equal(mounted.params.suggestionId, "sug_1");
268
+ assert.equal(
269
+ mounted.routeIds.some((id) => id.includes("$calendarObjectId")),
270
+ false,
271
+ );
272
+ });
273
+
256
274
  it("keeps the series matched under one of its occurrences", () => {
257
275
  const mounted = match(
258
276
  "/calendar/week/2026-06-10/evt_1/2026-06-11T09%3A15%3A00%2B02%3A00",
@@ -9,6 +9,8 @@ import {
9
9
  import { useRetainOpenPanels } from "./fragment";
10
10
 
11
11
  const VIEW_ROUTE = "/calendar/$view/$date" as const;
12
+ const SUGGESTION_ROUTE =
13
+ "/calendar/$view/$date/suggestion/$suggestionId" as const;
12
14
 
13
15
  /** Which zoom the calendar is at, on which day, over which calendars. */
14
16
  export interface CalendarAddress {
@@ -67,12 +69,21 @@ export function useOpenCalendarEvent(): OpenCalendarEvent | undefined {
67
69
  }
68
70
 
69
71
  /** Whether the composer is up, which is a route rather than a flag. */
72
+ export function useChangingSuggestionId(): string | undefined {
73
+ return useParams({ from: SUGGESTION_ROUTE, shouldThrow: false })
74
+ ?.suggestionId;
75
+ }
76
+
70
77
  export function useIsWritingEvent(): boolean {
71
78
  const composer = useParams({
72
79
  from: "/calendar/$view/$date/new",
73
80
  shouldThrow: false,
74
81
  });
75
- return composer !== undefined;
82
+ const changing = useParams({
83
+ from: SUGGESTION_ROUTE,
84
+ shouldThrow: false,
85
+ });
86
+ return composer !== undefined || changing !== undefined;
76
87
  }
77
88
 
78
89
  export interface CalendarNavigation {
@@ -97,6 +108,7 @@ export interface CalendarNavigation {
97
108
  openEvent: (calendarObjectId: string, recurrenceId?: string) => void;
98
109
  closeEvent: () => void;
99
110
  openComposer: () => void;
111
+ changeSuggestionFirst: (suggestionId: string, date: string) => void;
100
112
  }
101
113
 
102
114
  /**
@@ -161,6 +173,13 @@ export function useCalendarNavigation(): CalendarNavigation {
161
173
  search: true,
162
174
  hash: retainPanels,
163
175
  }),
176
+ changeSuggestionFirst: (suggestionId: string, on: string) =>
177
+ navigate({
178
+ to: SUGGESTION_ROUTE,
179
+ params: { view, date: on, suggestionId },
180
+ search: true,
181
+ hash: retainPanels,
182
+ }),
164
183
  }),
165
184
  [goTo, navigate, retainPanels, view, date],
166
185
  );
@@ -194,3 +213,19 @@ export function useOpenEventOnCalendar(): (
194
213
  [navigate],
195
214
  );
196
215
  }
216
+
217
+ export function useChangeSuggestionOnCalendar(): (
218
+ date: string,
219
+ suggestionId: string,
220
+ ) => void {
221
+ const navigate = useNavigate();
222
+ return useCallback(
223
+ (date: string, suggestionId: string) => {
224
+ navigate({
225
+ to: SUGGESTION_ROUTE,
226
+ params: { view: "day", date, suggestionId },
227
+ });
228
+ },
229
+ [navigate],
230
+ );
231
+ }
@@ -9,6 +9,8 @@ export {
9
9
  type OpenCalendarEvent,
10
10
  useCalendarAddress,
11
11
  useCalendarNavigation,
12
+ useChangeSuggestionOnCalendar,
13
+ useChangingSuggestionId,
12
14
  useIsWritingEvent,
13
15
  useOpenCalendarEvent,
14
16
  useOpenEventOnCalendar,
@@ -0,0 +1,54 @@
1
+ import {
2
+ type AnyRouter,
3
+ createMemoryHistory,
4
+ createRootRoute,
5
+ createRoute,
6
+ createRouter,
7
+ Outlet,
8
+ } from "@tanstack/react-router";
9
+ import { createElement, type FunctionComponent } from "react";
10
+
11
+ (globalThis as { self?: typeof globalThis }).self ??= globalThis;
12
+
13
+ /**
14
+ * The brief's thread routes, with `surface` mounted above them so it stays on
15
+ * screen while the address moves. A surface that opens a conversation is
16
+ * asserted by where the router lands, not by the callback it was handed.
17
+ */
18
+ export const threadRouter = (
19
+ surface: FunctionComponent,
20
+ path: string,
21
+ ): AnyRouter => {
22
+ const rootRoute = createRootRoute({
23
+ component: () =>
24
+ createElement("div", null, createElement(surface), createElement(Outlet)),
25
+ });
26
+ const mailRoute = createRoute({
27
+ getParentRoute: () => rootRoute,
28
+ path: "/mail",
29
+ component: Outlet,
30
+ });
31
+ const briefRoute = createRoute({
32
+ getParentRoute: () => mailRoute,
33
+ path: "/brief",
34
+ component: Outlet,
35
+ });
36
+ const threadRoute = createRoute({
37
+ getParentRoute: () => briefRoute,
38
+ path: "$threadId",
39
+ component: Outlet,
40
+ });
41
+ const messageRoute = createRoute({
42
+ getParentRoute: () => threadRoute,
43
+ path: "$messageId",
44
+ component: Outlet,
45
+ });
46
+ return createRouter({
47
+ routeTree: rootRoute.addChildren([
48
+ mailRoute.addChildren([
49
+ briefRoute.addChildren([threadRoute.addChildren([messageRoute])]),
50
+ ]),
51
+ ]),
52
+ history: createMemoryHistory({ initialEntries: [path] }),
53
+ }) as unknown as AnyRouter;
54
+ };