@remit/web-client 0.0.312 → 0.0.314
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.tsx +15 -2
- package/src/components/calendar/CalendarWaiting.tsx +3 -0
- package/src/components/calendar/ChangeCalendarSuggestion.stories.tsx +182 -0
- package/src/components/calendar/ChangeCalendarSuggestion.tsx +159 -0
- package/src/components/calendar/PendingSuggestions.tsx +35 -5
- package/src/components/calendar/calendar-story-server.tsx +5 -0
- package/src/components/calendar/change-suggestion-first.render.test.ts +299 -0
- package/src/components/calendar/pending-suggestions.render.test.ts +68 -3
- package/src/components/compose/ComposeForm.tsx +38 -4
- package/src/components/compose/compose-inline-image.render.test.ts +542 -0
- package/src/hooks/calendar/draft.ts +1 -1
- package/src/hooks/calendar/index.ts +7 -0
- package/src/hooks/calendar/suggestion-draft.ts +77 -0
- package/src/hooks/calendar/useCalendarSuggestions.ts +59 -1
- package/src/hooks/intelligence-calendar.render.test.ts +135 -0
- package/src/hooks/useComposeAttachments.ts +6 -1
- package/src/hooks/useInlineImages.ts +93 -0
- package/src/hooks/useIntelligenceCalendar.ts +36 -5
- package/src/lib/calendar-suggestion.test.ts +10 -1
- package/src/lib/calendar-suggestion.ts +12 -2
- package/src/lib/compose-attachment-controller.test.ts +110 -0
- package/src/lib/compose-attachment-controller.ts +120 -16
- package/src/routeTree.gen.ts +23 -0
- package/src/routes/calendar/$view.$date/suggestion.$suggestionId.tsx +22 -0
- package/src/routes/calendar/$view.$date.tsx +9 -3
- package/src/routes/calendar/-calendar-tree.test.ts +18 -0
- package/src/routing/calendar.ts +36 -1
- package/src/routing/index.ts +2 -0
|
@@ -12,9 +12,26 @@ export type DraftForAttachment =
|
|
|
12
12
|
export interface MintedAttachment {
|
|
13
13
|
outboxAttachmentId: string;
|
|
14
14
|
filename: string;
|
|
15
|
+
contentType: string;
|
|
16
|
+
contentId: string;
|
|
15
17
|
uploadUrl: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
export interface CompletedAttachment {
|
|
21
|
+
contentUrl: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AttachmentPlacement {
|
|
25
|
+
stored: (file: File, contentId: string) => void;
|
|
26
|
+
failed: (file: File) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StoredContent {
|
|
30
|
+
filename: string;
|
|
31
|
+
contentType: string;
|
|
32
|
+
contentUrl: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
/** The four requests a file on a draft is made of. Each throws on failure. */
|
|
19
36
|
export interface AttachmentTransport {
|
|
20
37
|
mint: (outboxMessageId: string, file: File) => Promise<MintedAttachment>;
|
|
@@ -22,7 +39,7 @@ export interface AttachmentTransport {
|
|
|
22
39
|
complete: (
|
|
23
40
|
outboxMessageId: string,
|
|
24
41
|
outboxAttachmentId: string,
|
|
25
|
-
) => Promise<
|
|
42
|
+
) => Promise<CompletedAttachment>;
|
|
26
43
|
keep: (outboxMessageId: string, attachmentIds: string[]) => Promise<void>;
|
|
27
44
|
}
|
|
28
45
|
|
|
@@ -34,6 +51,10 @@ export interface ComposeAttachmentControllerDeps {
|
|
|
34
51
|
interface Entry extends ComposeAttachmentItem {
|
|
35
52
|
serverId: string | null;
|
|
36
53
|
file: File | null;
|
|
54
|
+
placement: AttachmentPlacement | null;
|
|
55
|
+
contentId: string | null;
|
|
56
|
+
contentType: string;
|
|
57
|
+
contentUrl: string | null;
|
|
37
58
|
}
|
|
38
59
|
|
|
39
60
|
interface AttachmentRejection {
|
|
@@ -91,6 +112,10 @@ const fromServer = (attachment: RemitImapOutboxAttachmentResponse): Entry => ({
|
|
|
91
112
|
key: attachment.outboxAttachmentId,
|
|
92
113
|
serverId: attachment.outboxAttachmentId,
|
|
93
114
|
file: null,
|
|
115
|
+
placement: null,
|
|
116
|
+
contentId: attachment.contentId,
|
|
117
|
+
contentType: attachment.contentType,
|
|
118
|
+
contentUrl: attachment.contentUrl,
|
|
94
119
|
filename: attachment.filename,
|
|
95
120
|
sizeBytes: attachment.sizeBytes,
|
|
96
121
|
state:
|
|
@@ -124,6 +149,8 @@ export class ComposeAttachmentController {
|
|
|
124
149
|
private readonly inFlight = new Map<string, AbortController>();
|
|
125
150
|
private readonly mintedAfterRemoval = new Map<string, string>();
|
|
126
151
|
private readonly removing = new Map<string, string>();
|
|
152
|
+
private readonly removedListeners = new Set<(contentId: string) => void>();
|
|
153
|
+
private loaded = false;
|
|
127
154
|
|
|
128
155
|
constructor(private readonly deps: ComposeAttachmentControllerDeps) {}
|
|
129
156
|
|
|
@@ -134,6 +161,47 @@ export class ComposeAttachmentController {
|
|
|
134
161
|
|
|
135
162
|
getSnapshot = (): ComposeAttachmentItem[] => this.snapshot;
|
|
136
163
|
|
|
164
|
+
owns = (contentId: string): boolean =>
|
|
165
|
+
this.entries.some((entry) => entry.contentId === contentId);
|
|
166
|
+
|
|
167
|
+
storedContent = (contentId: string): StoredContent | null => {
|
|
168
|
+
const entry = this.entries.find(
|
|
169
|
+
(candidate) =>
|
|
170
|
+
candidate.contentId === contentId && candidate.contentUrl !== null,
|
|
171
|
+
);
|
|
172
|
+
if (!entry?.contentUrl) return null;
|
|
173
|
+
return {
|
|
174
|
+
filename: entry.filename,
|
|
175
|
+
contentType: entry.contentType,
|
|
176
|
+
contentUrl: entry.contentUrl,
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
isLoaded = (): boolean => this.loaded;
|
|
181
|
+
|
|
182
|
+
refreshLinks = (
|
|
183
|
+
attachments: readonly RemitImapOutboxAttachmentResponse[],
|
|
184
|
+
): void => {
|
|
185
|
+
const links = new Map(
|
|
186
|
+
attachments.map((attachment) => [
|
|
187
|
+
attachment.outboxAttachmentId,
|
|
188
|
+
attachment.contentUrl,
|
|
189
|
+
]),
|
|
190
|
+
);
|
|
191
|
+
this.set(
|
|
192
|
+
this.entries.map((entry) => {
|
|
193
|
+
const contentUrl =
|
|
194
|
+
entry.serverId === null ? undefined : links.get(entry.serverId);
|
|
195
|
+
return contentUrl === undefined ? entry : { ...entry, contentUrl };
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
onRemoved = (listener: (contentId: string) => void): (() => void) => {
|
|
201
|
+
this.removedListeners.add(listener);
|
|
202
|
+
return () => this.removedListeners.delete(listener);
|
|
203
|
+
};
|
|
204
|
+
|
|
137
205
|
blockingReason = (): string | undefined => {
|
|
138
206
|
const [removing] = this.removing.values();
|
|
139
207
|
if (removing !== undefined) return `Removing "${removing}"…`;
|
|
@@ -210,6 +278,8 @@ export class ComposeAttachmentController {
|
|
|
210
278
|
this.patch(generation, key, {
|
|
211
279
|
serverId: minted.outboxAttachmentId,
|
|
212
280
|
filename: minted.filename,
|
|
281
|
+
contentId: minted.contentId,
|
|
282
|
+
contentType: minted.contentType,
|
|
213
283
|
});
|
|
214
284
|
}
|
|
215
285
|
settle();
|
|
@@ -232,19 +302,29 @@ export class ComposeAttachmentController {
|
|
|
232
302
|
|
|
233
303
|
const completed = await transport
|
|
234
304
|
.complete(outboxMessageId, minted.outboxAttachmentId)
|
|
235
|
-
.
|
|
236
|
-
(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
305
|
+
.catch((error: unknown) => {
|
|
306
|
+
this.patch(generation, key, {
|
|
307
|
+
state: refusalOf(minted.filename, error),
|
|
308
|
+
});
|
|
309
|
+
return null;
|
|
310
|
+
});
|
|
311
|
+
if (completed === null || signal.aborted) return;
|
|
312
|
+
this.patch(generation, key, {
|
|
313
|
+
state: ATTACHED,
|
|
314
|
+
contentUrl: completed.contentUrl,
|
|
315
|
+
});
|
|
316
|
+
if (generation !== this.generation) return;
|
|
317
|
+
this.entries
|
|
318
|
+
.find((entry) => entry.key === key)
|
|
319
|
+
?.placement?.stored(file, minted.contentId);
|
|
246
320
|
};
|
|
247
321
|
|
|
322
|
+
private settleFailure(generation: number, key: string, file: File): void {
|
|
323
|
+
if (generation !== this.generation) return;
|
|
324
|
+
const entry = this.entries.find((candidate) => candidate.key === key);
|
|
325
|
+
if (entry?.state.status === "failed") entry.placement?.failed(file);
|
|
326
|
+
}
|
|
327
|
+
|
|
248
328
|
private run = async (
|
|
249
329
|
generation: number,
|
|
250
330
|
key: string,
|
|
@@ -254,6 +334,7 @@ export class ComposeAttachmentController {
|
|
|
254
334
|
): Promise<void> => {
|
|
255
335
|
await this.upload(generation, key, file, outboxMessageId, signal);
|
|
256
336
|
if (this.inFlight.get(key)?.signal === signal) this.inFlight.delete(key);
|
|
337
|
+
this.settleFailure(generation, key, file);
|
|
257
338
|
};
|
|
258
339
|
|
|
259
340
|
/**
|
|
@@ -282,7 +363,10 @@ export class ComposeAttachmentController {
|
|
|
282
363
|
);
|
|
283
364
|
};
|
|
284
365
|
|
|
285
|
-
attach = async (
|
|
366
|
+
attach = async (
|
|
367
|
+
files: File[],
|
|
368
|
+
placement?: AttachmentPlacement,
|
|
369
|
+
): Promise<void> => {
|
|
286
370
|
const generation = this.generation;
|
|
287
371
|
const added = files.map((file) => {
|
|
288
372
|
this.nextKey += 1;
|
|
@@ -290,6 +374,10 @@ export class ComposeAttachmentController {
|
|
|
290
374
|
key: `local-${this.nextKey}`,
|
|
291
375
|
serverId: null,
|
|
292
376
|
file,
|
|
377
|
+
placement: placement ?? null,
|
|
378
|
+
contentId: null,
|
|
379
|
+
contentType: file.type || "application/octet-stream",
|
|
380
|
+
contentUrl: null,
|
|
293
381
|
filename: file.name,
|
|
294
382
|
sizeBytes: file.size,
|
|
295
383
|
state: UPLOADING,
|
|
@@ -302,11 +390,12 @@ export class ComposeAttachmentController {
|
|
|
302
390
|
|
|
303
391
|
const draft = await this.deps.ensureDraft();
|
|
304
392
|
if (draft.outcome === "refused") {
|
|
305
|
-
for (const { entry } of added) {
|
|
393
|
+
for (const { entry, file } of added) {
|
|
306
394
|
this.inFlight.delete(entry.key);
|
|
307
395
|
this.patch(generation, entry.key, {
|
|
308
396
|
state: failed(draft.reason, true),
|
|
309
397
|
});
|
|
398
|
+
this.settleFailure(generation, entry.key, file);
|
|
310
399
|
}
|
|
311
400
|
return;
|
|
312
401
|
}
|
|
@@ -379,7 +468,12 @@ export class ComposeAttachmentController {
|
|
|
379
468
|
this.set(this.entries);
|
|
380
469
|
const serverId = entry.serverId ?? this.mintedAfterRemoval.get(key) ?? null;
|
|
381
470
|
this.mintedAfterRemoval.delete(key);
|
|
382
|
-
if (
|
|
471
|
+
if (generation !== this.generation) return;
|
|
472
|
+
if (failure === null) {
|
|
473
|
+
if (entry.contentId === null) return;
|
|
474
|
+
for (const listener of this.removedListeners) listener(entry.contentId);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
383
477
|
if (serverId === null) return;
|
|
384
478
|
this.set([
|
|
385
479
|
...this.entries,
|
|
@@ -400,7 +494,16 @@ export class ComposeAttachmentController {
|
|
|
400
494
|
attachments: readonly RemitImapOutboxAttachmentResponse[],
|
|
401
495
|
): void => {
|
|
402
496
|
this.draftId = outboxMessageId;
|
|
403
|
-
this.
|
|
497
|
+
this.loaded = true;
|
|
498
|
+
const listed = new Set(
|
|
499
|
+
attachments.map((attachment) => attachment.outboxAttachmentId),
|
|
500
|
+
);
|
|
501
|
+
const attachedHere = this.entries.filter(
|
|
502
|
+
(entry) =>
|
|
503
|
+
entry.file !== null &&
|
|
504
|
+
(entry.serverId === null || !listed.has(entry.serverId)),
|
|
505
|
+
);
|
|
506
|
+
this.set([...attachments.map(fromServer), ...attachedHere]);
|
|
404
507
|
};
|
|
405
508
|
|
|
406
509
|
reset = (outboxMessageId: string | undefined): void => {
|
|
@@ -410,6 +513,7 @@ export class ComposeAttachmentController {
|
|
|
410
513
|
this.mintedAfterRemoval.clear();
|
|
411
514
|
this.removing.clear();
|
|
412
515
|
this.draftId = outboxMessageId;
|
|
516
|
+
this.loaded = false;
|
|
413
517
|
this.set([]);
|
|
414
518
|
};
|
|
415
519
|
}
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { Route as MailOutboxComposeChar123OutboxMessageIdChar125RouteImport } fr
|
|
|
55
55
|
import { Route as MailOutboxDraftOutboxMessageIdRouteImport } from './routes/mail/outbox/draft/$outboxMessageId'
|
|
56
56
|
import { Route as CalendarViewDateCalendarObjectIdIndexRouteImport } from './routes/calendar/$view.$date/$calendarObjectId/index'
|
|
57
57
|
import { Route as CalendarViewDateCalendarObjectIdRecurrenceIdRouteImport } from './routes/calendar/$view.$date/$calendarObjectId/$recurrenceId'
|
|
58
|
+
import { Route as CalendarViewDateSuggestionSuggestionIdRouteImport } from './routes/calendar/$view.$date/suggestion.$suggestionId'
|
|
58
59
|
import { Route as MailMailboxIdThreadIdMessageIdModeChar123OutboxMessageIdChar125RouteImport } from './routes/mail/$mailboxId/$threadId/$messageId/$mode.{-$outboxMessageId}'
|
|
59
60
|
import { Route as MailBriefThreadIdMessageIdModeChar123OutboxMessageIdChar125RouteImport } from './routes/mail/brief/$threadId/$messageId/$mode.{-$outboxMessageId}'
|
|
60
61
|
import { Route as MailFlaggedThreadIdMessageIdModeChar123OutboxMessageIdChar125RouteImport } from './routes/mail/flagged/$threadId/$messageId/$mode.{-$outboxMessageId}'
|
|
@@ -302,6 +303,12 @@ const CalendarViewDateCalendarObjectIdRecurrenceIdRoute =
|
|
|
302
303
|
path: '/$recurrenceId',
|
|
303
304
|
getParentRoute: () => CalendarViewDateCalendarObjectIdRoute,
|
|
304
305
|
} as any)
|
|
306
|
+
const CalendarViewDateSuggestionSuggestionIdRoute =
|
|
307
|
+
CalendarViewDateSuggestionSuggestionIdRouteImport.update({
|
|
308
|
+
id: '/suggestion/$suggestionId',
|
|
309
|
+
path: '/suggestion/$suggestionId',
|
|
310
|
+
getParentRoute: () => CalendarViewDateRoute,
|
|
311
|
+
} as any)
|
|
305
312
|
const MailMailboxIdThreadIdMessageIdModeChar123OutboxMessageIdChar125Route =
|
|
306
313
|
MailMailboxIdThreadIdMessageIdModeChar123OutboxMessageIdChar125RouteImport.update(
|
|
307
314
|
{
|
|
@@ -373,6 +380,7 @@ export interface FileRoutesByFullPath {
|
|
|
373
380
|
'/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
|
|
374
381
|
'/mail/flagged/$threadId/': typeof MailFlaggedThreadIdIndexRoute
|
|
375
382
|
'/calendar/$view/$date/$calendarObjectId/$recurrenceId': typeof CalendarViewDateCalendarObjectIdRecurrenceIdRoute
|
|
383
|
+
'/calendar/$view/$date/suggestion/$suggestionId': typeof CalendarViewDateSuggestionSuggestionIdRoute
|
|
376
384
|
'/calendar/$view/$date/$calendarObjectId/': typeof CalendarViewDateCalendarObjectIdIndexRoute
|
|
377
385
|
'/mail/$mailboxId/$threadId/$messageId/$mode/{-$outboxMessageId}': typeof MailMailboxIdThreadIdMessageIdModeChar123OutboxMessageIdChar125Route
|
|
378
386
|
'/mail/brief/$threadId/$messageId/$mode/{-$outboxMessageId}': typeof MailBriefThreadIdMessageIdModeChar123OutboxMessageIdChar125Route
|
|
@@ -412,6 +420,7 @@ export interface FileRoutesByTo {
|
|
|
412
420
|
'/mail/brief/$threadId': typeof MailBriefThreadIdIndexRoute
|
|
413
421
|
'/mail/flagged/$threadId': typeof MailFlaggedThreadIdIndexRoute
|
|
414
422
|
'/calendar/$view/$date/$calendarObjectId/$recurrenceId': typeof CalendarViewDateCalendarObjectIdRecurrenceIdRoute
|
|
423
|
+
'/calendar/$view/$date/suggestion/$suggestionId': typeof CalendarViewDateSuggestionSuggestionIdRoute
|
|
415
424
|
'/calendar/$view/$date/$calendarObjectId': typeof CalendarViewDateCalendarObjectIdIndexRoute
|
|
416
425
|
'/mail/$mailboxId/$threadId/$messageId/$mode/{-$outboxMessageId}': typeof MailMailboxIdThreadIdMessageIdModeChar123OutboxMessageIdChar125Route
|
|
417
426
|
'/mail/brief/$threadId/$messageId/$mode/{-$outboxMessageId}': typeof MailBriefThreadIdMessageIdModeChar123OutboxMessageIdChar125Route
|
|
@@ -464,6 +473,7 @@ export interface FileRoutesById {
|
|
|
464
473
|
'/mail/brief/$threadId/': typeof MailBriefThreadIdIndexRoute
|
|
465
474
|
'/mail/flagged/$threadId/': typeof MailFlaggedThreadIdIndexRoute
|
|
466
475
|
'/calendar/$view/$date/$calendarObjectId/$recurrenceId': typeof CalendarViewDateCalendarObjectIdRecurrenceIdRoute
|
|
476
|
+
'/calendar/$view/$date/suggestion/$suggestionId': typeof CalendarViewDateSuggestionSuggestionIdRoute
|
|
467
477
|
'/calendar/$view/$date/$calendarObjectId/': typeof CalendarViewDateCalendarObjectIdIndexRoute
|
|
468
478
|
'/mail/$mailboxId/$threadId/$messageId/$mode/{-$outboxMessageId}': typeof MailMailboxIdThreadIdMessageIdModeChar123OutboxMessageIdChar125Route
|
|
469
479
|
'/mail/brief/$threadId/$messageId/$mode/{-$outboxMessageId}': typeof MailBriefThreadIdMessageIdModeChar123OutboxMessageIdChar125Route
|
|
@@ -517,6 +527,7 @@ export interface FileRouteTypes {
|
|
|
517
527
|
| '/mail/brief/$threadId/'
|
|
518
528
|
| '/mail/flagged/$threadId/'
|
|
519
529
|
| '/calendar/$view/$date/$calendarObjectId/$recurrenceId'
|
|
530
|
+
| '/calendar/$view/$date/suggestion/$suggestionId'
|
|
520
531
|
| '/calendar/$view/$date/$calendarObjectId/'
|
|
521
532
|
| '/mail/$mailboxId/$threadId/$messageId/$mode/{-$outboxMessageId}'
|
|
522
533
|
| '/mail/brief/$threadId/$messageId/$mode/{-$outboxMessageId}'
|
|
@@ -556,6 +567,7 @@ export interface FileRouteTypes {
|
|
|
556
567
|
| '/mail/brief/$threadId'
|
|
557
568
|
| '/mail/flagged/$threadId'
|
|
558
569
|
| '/calendar/$view/$date/$calendarObjectId/$recurrenceId'
|
|
570
|
+
| '/calendar/$view/$date/suggestion/$suggestionId'
|
|
559
571
|
| '/calendar/$view/$date/$calendarObjectId'
|
|
560
572
|
| '/mail/$mailboxId/$threadId/$messageId/$mode/{-$outboxMessageId}'
|
|
561
573
|
| '/mail/brief/$threadId/$messageId/$mode/{-$outboxMessageId}'
|
|
@@ -607,6 +619,7 @@ export interface FileRouteTypes {
|
|
|
607
619
|
| '/mail/brief/$threadId/'
|
|
608
620
|
| '/mail/flagged/$threadId/'
|
|
609
621
|
| '/calendar/$view/$date/$calendarObjectId/$recurrenceId'
|
|
622
|
+
| '/calendar/$view/$date/suggestion/$suggestionId'
|
|
610
623
|
| '/calendar/$view/$date/$calendarObjectId/'
|
|
611
624
|
| '/mail/$mailboxId/$threadId/$messageId/$mode/{-$outboxMessageId}'
|
|
612
625
|
| '/mail/brief/$threadId/$messageId/$mode/{-$outboxMessageId}'
|
|
@@ -946,6 +959,13 @@ declare module '@tanstack/react-router' {
|
|
|
946
959
|
preLoaderRoute: typeof CalendarViewDateCalendarObjectIdRecurrenceIdRouteImport
|
|
947
960
|
parentRoute: typeof CalendarViewDateCalendarObjectIdRoute
|
|
948
961
|
}
|
|
962
|
+
'/calendar/$view/$date/suggestion/$suggestionId': {
|
|
963
|
+
id: '/calendar/$view/$date/suggestion/$suggestionId'
|
|
964
|
+
path: '/suggestion/$suggestionId'
|
|
965
|
+
fullPath: '/calendar/$view/$date/suggestion/$suggestionId'
|
|
966
|
+
preLoaderRoute: typeof CalendarViewDateSuggestionSuggestionIdRouteImport
|
|
967
|
+
parentRoute: typeof CalendarViewDateRoute
|
|
968
|
+
}
|
|
949
969
|
'/mail/$mailboxId/$threadId/$messageId/$mode/{-$outboxMessageId}': {
|
|
950
970
|
id: '/mail/$mailboxId/$threadId/$messageId/$mode/{-$outboxMessageId}'
|
|
951
971
|
path: '/$mode/{-$outboxMessageId}'
|
|
@@ -992,6 +1012,7 @@ interface CalendarViewDateRouteChildren {
|
|
|
992
1012
|
CalendarViewDateCalendarObjectIdRoute: typeof CalendarViewDateCalendarObjectIdRouteWithChildren
|
|
993
1013
|
CalendarViewDateNewRoute: typeof CalendarViewDateNewRoute
|
|
994
1014
|
CalendarViewDateIndexRoute: typeof CalendarViewDateIndexRoute
|
|
1015
|
+
CalendarViewDateSuggestionSuggestionIdRoute: typeof CalendarViewDateSuggestionSuggestionIdRoute
|
|
995
1016
|
}
|
|
996
1017
|
|
|
997
1018
|
const CalendarViewDateRouteChildren: CalendarViewDateRouteChildren = {
|
|
@@ -999,6 +1020,8 @@ const CalendarViewDateRouteChildren: CalendarViewDateRouteChildren = {
|
|
|
999
1020
|
CalendarViewDateCalendarObjectIdRouteWithChildren,
|
|
1000
1021
|
CalendarViewDateNewRoute: CalendarViewDateNewRoute,
|
|
1001
1022
|
CalendarViewDateIndexRoute: CalendarViewDateIndexRoute,
|
|
1023
|
+
CalendarViewDateSuggestionSuggestionIdRoute:
|
|
1024
|
+
CalendarViewDateSuggestionSuggestionIdRoute,
|
|
1002
1025
|
}
|
|
1003
1026
|
|
|
1004
1027
|
const CalendarViewDateRouteWithChildren =
|
|
@@ -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 {
|
|
90
|
-
|
|
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",
|
package/src/routing/calendar.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|