@remit/web-client 0.0.321 → 0.0.323
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 +4 -4
- package/src/auth/SignInAgain.tsx +18 -0
- package/src/auth/amplify-config.test.ts +1 -107
- package/src/auth/auth-interceptor.test.ts +0 -6
- package/src/auth/cognito/cognito-token.test.ts +2 -4
- package/src/components/calendar/AgendaStrip.tsx +1 -1
- package/src/components/calendar/CalendarWorkspace.tsx +1 -1
- package/src/components/calendar/ChangeCalendarSuggestion.tsx +1 -1
- package/src/components/calendar/OpenCalendarEvent.tsx +1 -18
- package/src/components/compose/ComposeForm.tsx +11 -6
- package/src/components/compose/FullCompose.tsx +1 -1
- package/src/components/compose/MobileComposeSheet.tsx +1 -2
- package/src/components/compose/compose.stories.tsx +18 -0
- package/src/components/compose/draft-discard-failure.render.test.ts +138 -0
- package/src/components/config-import/config-import.fixtures.ts +8 -9
- package/src/components/config-import/config-import.stories.tsx +2 -2
- package/src/components/layout/MailTopBar.tsx +3 -0
- package/src/components/mail/BriefPane.tsx +0 -3
- package/src/components/mail/ConversationView.tsx +1 -3
- package/src/components/mail/DailyBrief.tsx +5 -28
- package/src/components/mail/FlaggedList.tsx +3 -4
- package/src/components/mail/FlaggedPane.tsx +0 -3
- package/src/components/mail/IntelligenceDrawer.tsx +0 -3
- package/src/components/mail/IntelligencePane.render.test.ts +0 -1
- package/src/components/mail/IntelligencePane.test.ts +1 -95
- package/src/components/mail/IntelligencePane.tsx +3 -93
- package/src/components/mail/MailListHeader.tsx +12 -43
- package/src/components/mail/MailNav.tsx +6 -2
- package/src/components/mail/MailViewChrome.tsx +1 -11
- package/src/components/mail/MailboxPane.tsx +4 -34
- package/src/components/mail/MessageActionMenu.tsx +6 -6
- package/src/components/mail/MessageAttachments.tsx +35 -0
- package/src/components/mail/MessageBody.tsx +1 -0
- package/src/components/mail/MessageBodyErrorBanner.test.ts +0 -30
- package/src/components/mail/MessageCard.tsx +16 -4
- package/src/components/mail/MessageList.run-reporting.test.ts +2 -46
- package/src/components/mail/MessageList.tsx +23 -11
- package/src/components/mail/MessageRow.render.test.ts +0 -7
- package/src/components/mail/MoveToTrigger.tsx +1 -1
- package/src/components/mail/RawMessageView.tsx +1 -1
- package/src/components/mail/SelectionWizardHost.search-entry.test.ts +0 -1
- package/src/components/mail/SelectionWizardHost.tsx +16 -102
- package/src/components/mail/authenticity-warning.stories.tsx +0 -21
- package/src/components/mail/daily-brief.stories.tsx +31 -17
- package/src/components/mail/enter-cursor-late-open.render.test.ts +197 -0
- package/src/components/mail/intelligence-drawer.stories.tsx +0 -2
- package/src/components/mail/message-gone.stories.tsx +44 -0
- package/src/components/mail/offer-times-reply.render.test.ts +25 -1
- package/src/components/mail/reclassify-remove-override.render.test.ts +2 -6
- package/src/components/overlay-escape.render.test.ts +7 -3
- package/src/components/settings/AccountFormPanel.render.test.ts +26 -3
- package/src/components/settings/AccountFormPanel.test.ts +1 -36
- package/src/components/settings/AccountFormPanel.tsx +5 -25
- package/src/components/settings/AccountServices.tsx +1 -1
- package/src/components/settings/AdvancedNavIcon.tsx +3 -3
- package/src/components/settings/CalendarFeedCard.tsx +2 -1
- package/src/components/settings/CalendarSubscribeCard.tsx +1 -1
- package/src/components/settings/CalendarSubscriptionStatus.tsx +1 -2
- package/src/components/settings/FilterActionError.tsx +1 -1
- package/src/components/settings/FilterEditor.render.test.ts +3 -27
- package/src/components/settings/FilterEditor.tsx +4 -24
- package/src/components/settings/FilterEditorSurface.tsx +0 -3
- package/src/components/settings/FiltersList.render.test.ts +5 -24
- package/src/components/settings/FiltersList.stories.tsx +0 -1
- package/src/components/settings/FiltersList.tsx +10 -35
- package/src/components/settings/WarningsNavIcon.tsx +12 -0
- package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts} +64 -5
- package/src/components/settings/WarningsPanel.render.test.ts +56 -0
- package/src/components/settings/WarningsPanel.tsx +244 -0
- package/src/components/settings/accounts.stories.tsx +18 -0
- package/src/components/settings/advanced.stories.tsx +3 -125
- package/src/components/settings/settings-filter.stories.tsx +2 -42
- package/src/components/settings/warnings.stories.tsx +290 -0
- package/src/components/ui/ErrorBannerProvider.tsx +5 -3
- package/src/components/ui/error-banners.test.ts +1 -22
- package/src/components/ui/error-banners.ts +9 -20
- package/src/hooks/calendar/useCalendarCollectionWrites.ts +2 -1
- package/src/hooks/calendar/useCalendarSuggestions.ts +2 -1
- package/src/hooks/calendar/useCalendarWrites.ts +2 -1
- package/src/hooks/intelligence-calendar.render.test.ts +0 -1
- package/src/hooks/useDeleteMessages.test.ts +1 -42
- package/src/hooks/useFilters.test.ts +0 -8
- package/src/hooks/useFollowFocusOpen.render.test.ts +1 -11
- package/src/hooks/useIntelligenceData.test.ts +9 -47
- package/src/hooks/useIntelligenceData.ts +7 -100
- package/src/hooks/useLayoutTier.test.ts +0 -48
- package/src/hooks/useReportSpam.ts +1 -1
- package/src/hooks/useRescueCandidates.test.ts +0 -23
- package/src/hooks/useRulePreview.ts +8 -7
- package/src/hooks/useSearchTokenContext.ts +2 -3
- package/src/hooks/useStaleAccountSync.test.ts +0 -17
- package/src/hooks/useStarredThreads.test.ts +0 -10
- package/src/hooks/useTriggerSync.test.ts +0 -6
- package/src/hooks/useVisualViewport.render.test.ts +63 -0
- package/src/hooks/useWarningsAttention.ts +37 -0
- package/src/lib/account-order.test.ts +0 -10
- package/src/lib/adjacent-message.test.ts +0 -9
- package/src/lib/app-info.ts +0 -3
- package/src/lib/autodiscovery.test.ts +0 -47
- package/src/lib/bug-report.test.ts +38 -0
- package/src/lib/bug-report.ts +10 -7
- package/src/lib/bulk-action-copy.ts +1 -1
- package/src/lib/calendar-route.test.ts +0 -5
- package/src/lib/config-import.test.ts +3 -39
- package/src/lib/dedupe-thread-messages.test.ts +0 -4
- package/src/lib/desktop-breakpoint.test.ts +0 -8
- package/src/lib/drafts.test.ts +0 -23
- package/src/lib/fatal-error.test.ts +0 -19
- package/src/lib/folder-roles.test.ts +0 -4
- package/src/lib/hot-sync-window.test.ts +0 -5
- package/src/lib/list-header-chrome.ts +1 -1
- package/src/lib/mail-context.ts +1 -1
- package/src/lib/message-attachments.test.ts +77 -0
- package/src/lib/message-attachments.ts +31 -15
- package/src/lib/message-body-source.test.ts +21 -4
- package/src/lib/message-body-source.ts +5 -2
- package/src/lib/onboarding-completion.test.ts +0 -16
- package/src/lib/organize/filter-edit-model.test.ts +2 -32
- package/src/lib/organize/filter-edit-model.ts +6 -16
- package/src/lib/organize/organize-model.test.ts +7 -40
- package/src/lib/organize/organize-model.ts +16 -18
- package/src/lib/organize/rule-model.test.ts +24 -202
- package/src/lib/organize/rule-model.ts +19 -166
- package/src/lib/organize/search-to-rule.test.ts +2 -36
- package/src/lib/organize/search-to-rule.ts +1 -16
- package/src/lib/provider-presets.test.ts +1 -77
- package/src/lib/query-error-handler.test.ts +0 -61
- package/src/lib/query-escalation.integration.test.ts +4 -4
- package/src/lib/result-count.ts +1 -1
- package/src/lib/result-folder.ts +1 -1
- package/src/lib/rum-adapter.test.ts +0 -104
- package/src/lib/search-query.test.ts +0 -11
- package/src/lib/search-result.test.ts +2 -142
- package/src/lib/search-result.ts +3 -73
- package/src/lib/search-scope.test.ts +1 -92
- package/src/lib/search-scope.ts +0 -29
- package/src/lib/search-surface.ts +2 -2
- package/src/lib/search-token-index.test.ts +0 -8
- package/src/lib/search-tokens.ts +15 -16
- package/src/lib/starred-rows.test.ts +0 -4
- package/src/lib/sync-warning-entries.ts +33 -0
- package/src/lib/wizard-selection.ts +1 -1
- package/src/mocks/story-frame/mail-handlers.ts +12 -8
- package/src/mocks/story-frame/mail-world.ts +5 -0
- package/src/routeTree.gen.ts +21 -0
- package/src/routes/calendar/$view.$date/index.tsx +1 -1
- package/src/routes/calendar.tsx +2 -1
- package/src/routes/mail/brief.tsx +1 -1
- package/src/routes/mail/flagged.tsx +1 -1
- package/src/routes/mail.tsx +5 -3
- package/src/routes/settings/-accounts.test.ts +0 -5
- package/src/routes/settings/-filters.test.ts +0 -1
- package/src/routes/settings/-shared/help-copy.tsx +15 -0
- package/src/routes/settings/accounts.tsx +1 -1
- package/src/routes/settings/advanced.tsx +1 -4
- package/src/routes/settings/filters.tsx +1 -2
- package/src/routes/settings/folders.tsx +1 -1
- package/src/routes/settings/labels.tsx +1 -1
- package/src/routes/settings/senders.tsx +1 -1
- package/src/routes/settings/warnings.tsx +36 -0
- package/src/routes/settings.tsx +7 -0
- package/src/routing/wizard-history.test.ts +1 -45
- package/src/runtime-config.ts +5 -0
- package/src/test-support/fixtures.ts +1 -1
- package/src/components/compose/draft-discard-banner.test.ts +0 -76
- package/src/components/mail/ReadingPaneEmpty.render.test.ts +0 -42
- package/src/components/mail/SpamRescue.render.test.ts +0 -35
- package/src/components/mail/intelligence-semantic-off.render.test.ts +0 -111
- package/src/components/mail/selection-surface-rollcall.test.ts +0 -270
- package/src/components/settings/QuarantinePanel.render.test.ts +0 -71
- package/src/components/settings/QuarantinePanel.tsx +0 -178
- package/src/components/ui/DropdownMenu.tsx +0 -99
- package/src/components/ui/EmptyState.tsx +0 -11
- package/src/components/ui/ErrorBanner.render.test.ts +0 -104
- package/src/components/ui/ErrorBanner.stories.tsx +0 -132
- package/src/components/ui/ErrorBanner.tsx +0 -106
- package/src/components/ui/ErrorBannerStack.tsx +0 -35
- package/src/components/ui/ErrorState.render.test.ts +0 -82
- package/src/components/ui/ErrorState.tsx +0 -80
- package/src/components/ui/KeyboardShortcutsModal.tsx +0 -113
- package/src/hooks/useMoveMessages.test.ts +0 -63
- package/src/hooks/useOrganizeWiden.test.ts +0 -217
- package/src/hooks/useOrganizeWiden.ts +0 -112
- package/src/hooks/useSemanticSearch.ts +0 -132
- package/src/hooks/useSemanticSearchEnabled.ts +0 -23
- package/src/hooks/useVisualViewport.test.ts +0 -126
- package/src/lib/organize/sender-fallback.test.ts +0 -52
- package/src/lib/organize/sender-fallback.ts +0 -38
- package/src/lib/rescue-telemetry.test.ts +0 -75
- package/src/router.test.ts +0 -81
|
@@ -46,10 +46,8 @@ import { useCreateFilter } from "@/hooks/useFilters";
|
|
|
46
46
|
import { useFolderLabelTranslator } from "@/hooks/useFolderLabelTranslator";
|
|
47
47
|
import { useMatchSample, useSearchMatchSample } from "@/hooks/useMatchSample";
|
|
48
48
|
import { useOrganizeJob } from "@/hooks/useOrganizeJob";
|
|
49
|
-
import { useOrganizeWiden } from "@/hooks/useOrganizeWiden";
|
|
50
49
|
import { useRulePreview } from "@/hooks/useRulePreview";
|
|
51
50
|
import { useSelectedSubjects } from "@/hooks/useSelectedSubjects";
|
|
52
|
-
import { useSemanticSearchEnabled } from "@/hooks/useSemanticSearchEnabled";
|
|
53
51
|
import type {
|
|
54
52
|
BulkActionProgress,
|
|
55
53
|
BulkActionTarget,
|
|
@@ -62,6 +60,7 @@ import { useMailContext } from "@/lib/mail-context";
|
|
|
62
60
|
import { buildMoveOptions, folderDelimiter } from "@/lib/move-options";
|
|
63
61
|
import {
|
|
64
62
|
buildWizardDraft,
|
|
63
|
+
type OrganizeMatchPredicate,
|
|
65
64
|
type OrganizeScope,
|
|
66
65
|
organizeScopeFor,
|
|
67
66
|
} from "@/lib/organize/organize-model";
|
|
@@ -70,7 +69,6 @@ import {
|
|
|
70
69
|
SUPPORTED_CLAUSE_FIELDS,
|
|
71
70
|
} from "@/lib/organize/rule-model";
|
|
72
71
|
import { searchRuleAccountId } from "@/lib/organize/search-to-rule";
|
|
73
|
-
import type { OrganizeMatchPredicate } from "@/lib/organize/sender-fallback";
|
|
74
72
|
import { useSearchConversion } from "@/lib/search-conversion";
|
|
75
73
|
import type { WizardSelectionMessage } from "@/lib/wizard-selection";
|
|
76
74
|
import { useWizardEntryValue, useWizardStep } from "@/routing";
|
|
@@ -186,7 +184,6 @@ const NO_CONVERSION: SearchConversion = {
|
|
|
186
184
|
matchOperator: "all",
|
|
187
185
|
droppedFacets: [],
|
|
188
186
|
keptTerms: false,
|
|
189
|
-
droppedSemantic: false,
|
|
190
187
|
};
|
|
191
188
|
|
|
192
189
|
/** A search entry has nothing ticked; its match is the query's clauses (#477 3.2). */
|
|
@@ -269,7 +266,7 @@ const bulkActionFor = (
|
|
|
269
266
|
};
|
|
270
267
|
|
|
271
268
|
/**
|
|
272
|
-
* Whether
|
|
269
|
+
* Whether the properties match is applied by the server's own pass rather than over
|
|
273
270
|
* the ids it matched. The back-apply job carries a move and nothing else, so
|
|
274
271
|
* Move, Junk and Organize hand it the whole match; Delete and Mark read have no
|
|
275
272
|
* server-side pass and act on the ids the preview matched.
|
|
@@ -290,8 +287,7 @@ const noDestinationReason = (verb: Verb): string =>
|
|
|
290
287
|
|
|
291
288
|
/**
|
|
292
289
|
* Where the wizard meets the app (#483). The steps and their bodies belong to
|
|
293
|
-
* `@remit/ui`; everything that talks to a server is here: the
|
|
294
|
-
* count, the folder create, the bulk call, the back-apply job and the filter.
|
|
290
|
+
* `@remit/ui`; everything that talks to a server is here: the live count, the folder create, the bulk call, the back-apply job and the filter.
|
|
295
291
|
*
|
|
296
292
|
* Every commit routes through `organize-model.ts`, so the four `OrganizeScope`
|
|
297
293
|
* values are reconstructed in one place from the two answers the wizard asks
|
|
@@ -351,7 +347,6 @@ function SelectionWizardSession({
|
|
|
351
347
|
undefined,
|
|
352
348
|
);
|
|
353
349
|
const [nudgedStep, setNudgedStep] = useState<StepId | undefined>(undefined);
|
|
354
|
-
const [semanticFallbackTaken, setSemanticFallbackTaken] = useState(false);
|
|
355
350
|
const [committedScope, setCommittedScope] = useState<
|
|
356
351
|
OrganizeScope | undefined
|
|
357
352
|
>(undefined);
|
|
@@ -386,7 +381,6 @@ function SelectionWizardSession({
|
|
|
386
381
|
() => selection.map((message) => message.email).filter(Boolean),
|
|
387
382
|
[selection],
|
|
388
383
|
);
|
|
389
|
-
const anchorMessageId = messageIds[0];
|
|
390
384
|
const subjects = useSelectedSubjects(messageIds);
|
|
391
385
|
const { junkMailboxId } = useJunkMailbox(accountId);
|
|
392
386
|
const { inboxMailboxId } = useInboxMailbox(accountId);
|
|
@@ -410,26 +404,11 @@ function SelectionWizardSession({
|
|
|
410
404
|
[selection, inboxMailboxId],
|
|
411
405
|
);
|
|
412
406
|
|
|
413
|
-
// No door is offered over an escalated predicate, so nothing here has a widen
|
|
414
|
-
// to probe for.
|
|
415
|
-
const widen = useOrganizeWiden(
|
|
416
|
-
escalated ? undefined : accountId,
|
|
417
|
-
anchorMessageId,
|
|
418
|
-
senders,
|
|
419
|
-
subjects,
|
|
420
|
-
);
|
|
421
|
-
const { preview: probeWiden } = widen;
|
|
422
|
-
// The similar door has to know before it is pressed whether it can run, so
|
|
423
|
-
// the probe fires with the wizard rather than with the door (#477 3.6).
|
|
424
|
-
useEffect(() => {
|
|
425
|
-
probeWiden();
|
|
426
|
-
}, [probeWiden]);
|
|
427
|
-
|
|
428
407
|
// A door the wizard resolves itself, through the preview endpoint. The
|
|
429
408
|
// escalated predicate is resolved by the list before the wizard opens, and
|
|
430
409
|
// the ticked list is its own answer, so neither is previewed.
|
|
431
|
-
const previewed = mode === "
|
|
432
|
-
const
|
|
410
|
+
const previewed = mode === "properties";
|
|
411
|
+
const predicate: OrganizeMatchPredicate = useMemo(
|
|
433
412
|
() => ({
|
|
434
413
|
matchOperator: draft.matchOperator === "all" ? "And" : "Or",
|
|
435
414
|
literalClauses: draft.clauses.map((clause) => ({
|
|
@@ -439,8 +418,6 @@ function SelectionWizardSession({
|
|
|
439
418
|
}),
|
|
440
419
|
[draft.clauses, draft.matchOperator],
|
|
441
420
|
);
|
|
442
|
-
const predicate =
|
|
443
|
-
mode === "similar" ? widen.matchPredicate : literalPredicate;
|
|
444
421
|
// The count and the ids both come from the server, never from paging the list
|
|
445
422
|
// in the browser (#477 5.3). The ticked list is its own count, so no request
|
|
446
423
|
// is made for it.
|
|
@@ -552,11 +529,11 @@ function SelectionWizardSession({
|
|
|
552
529
|
// is how a footer comes to name a screen nobody is looking at.
|
|
553
530
|
const current = steps[stepIndex(steps, step)];
|
|
554
531
|
|
|
555
|
-
// A filter, a folder, and the preview that counts
|
|
556
|
-
// one account, so a selection spanning accounts reaches none of them
|
|
557
|
-
// told so on the step that asks (#477 5.5, #523). A selection spanning
|
|
558
|
-
// of one account is told about folders instead, and keeps its account:
|
|
559
|
-
// preview behind
|
|
532
|
+
// A filter, a folder, and the preview that counts the properties door all
|
|
533
|
+
// belong to one account, so a selection spanning accounts reaches none of them
|
|
534
|
+
// and is told so on the step that asks (#477 5.5, #523). A selection spanning
|
|
535
|
+
// folders of one account is told about folders instead, and keeps its account:
|
|
536
|
+
// the preview behind the properties door has something to count against. The one-off
|
|
560
537
|
// scope and the ticked rows act on the messages themselves and are unaffected.
|
|
561
538
|
const wizardScope = wizardScopeFor(accountId, selectionRestriction);
|
|
562
539
|
// An escalated match is restricted on the same step for its own reason: the
|
|
@@ -581,65 +558,15 @@ function SelectionWizardSession({
|
|
|
581
558
|
const changeMode = useCallback(
|
|
582
559
|
(next: MatchDoor) => {
|
|
583
560
|
setDoor(next);
|
|
584
|
-
setDraft((held) =>
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
: undefined,
|
|
590
|
-
...(next === "properties" && held.clauses.length === 0
|
|
591
|
-
? { clauses: seedPropertyClauses() }
|
|
592
|
-
: {}),
|
|
593
|
-
}));
|
|
561
|
+
setDraft((held) =>
|
|
562
|
+
next === "properties" && held.clauses.length === 0
|
|
563
|
+
? { ...held, clauses: seedPropertyClauses() }
|
|
564
|
+
: held,
|
|
565
|
+
);
|
|
594
566
|
},
|
|
595
|
-
[
|
|
567
|
+
[seedPropertyClauses],
|
|
596
568
|
);
|
|
597
569
|
|
|
598
|
-
// The dimmed similar door, pressed. What the widen would have fallen back to
|
|
599
|
-
// is filled in on the property step instead of standing in for the semantic
|
|
600
|
-
// match without saying so (#477 3.6) — the same agreement the property step
|
|
601
|
-
// itself seeds from (#458), not senders alone: a selection with nothing in
|
|
602
|
-
// common on sender still shares a subject worth matching on.
|
|
603
|
-
const takeSemanticFallback = useCallback(() => {
|
|
604
|
-
setSemanticFallbackTaken(true);
|
|
605
|
-
setDoor("properties");
|
|
606
|
-
setDraft((held) => ({
|
|
607
|
-
...held,
|
|
608
|
-
widen: undefined,
|
|
609
|
-
clauses: seedPropertyClauses(),
|
|
610
|
-
matchOperator: "any",
|
|
611
|
-
}));
|
|
612
|
-
goToStep("properties");
|
|
613
|
-
}, [seedPropertyClauses, goToStep]);
|
|
614
|
-
|
|
615
|
-
// The probe lands after the door is on screen, so it can report the widen
|
|
616
|
-
// unavailable while the user is already holding it. The fallback is taken then
|
|
617
|
-
// rather than left as a door that counts nothing: the property step says, in
|
|
618
|
-
// so many words, that these are the senders it substituted (#477 3.6).
|
|
619
|
-
// Off is a deployment setting, not a probe result, and it reaches the door the
|
|
620
|
-
// same way the runtime failure does: dimmed, pressable, and falling back to
|
|
621
|
-
// the senders. Only the copy differs (#1068).
|
|
622
|
-
const semanticOff = useSemanticSearchEnabled() === false;
|
|
623
|
-
const semanticUnavailable =
|
|
624
|
-
semanticOff || widen.semanticUnavailable || widen.isError;
|
|
625
|
-
// Only while the door is the screen: the probe re-fires when the anchor
|
|
626
|
-
// changes under a background refetch, and a late answer that moved the step
|
|
627
|
-
// from Review would push an entry the wizard does not own and leave the count
|
|
628
|
-
// it rewinds by wrong.
|
|
629
|
-
useEffect(() => {
|
|
630
|
-
if (current !== "match") return;
|
|
631
|
-
if (mode !== "similar" || !semanticUnavailable || semanticFallbackTaken) {
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
takeSemanticFallback();
|
|
635
|
-
}, [
|
|
636
|
-
current,
|
|
637
|
-
mode,
|
|
638
|
-
semanticUnavailable,
|
|
639
|
-
semanticFallbackTaken,
|
|
640
|
-
takeSemanticFallback,
|
|
641
|
-
]);
|
|
642
|
-
|
|
643
570
|
const startAddClause = useCallback(() => {
|
|
644
571
|
setClauseEdit({ mode: "add", draft: { field: "From", value: "" } });
|
|
645
572
|
}, []);
|
|
@@ -757,7 +684,6 @@ function SelectionWizardSession({
|
|
|
757
684
|
const organizeDraft = buildWizardDraft({
|
|
758
685
|
mode: door,
|
|
759
686
|
ruleScope: named.scope,
|
|
760
|
-
anchorMessageId,
|
|
761
687
|
clauses: named.clauses,
|
|
762
688
|
matchOperator: named.matchOperator,
|
|
763
689
|
moveMailboxId: named.moveMailboxId,
|
|
@@ -797,7 +723,6 @@ function SelectionWizardSession({
|
|
|
797
723
|
runEscalated,
|
|
798
724
|
door,
|
|
799
725
|
named,
|
|
800
|
-
anchorMessageId,
|
|
801
726
|
verb,
|
|
802
727
|
accountId,
|
|
803
728
|
bulkTargets,
|
|
@@ -1016,10 +941,6 @@ function SelectionWizardSession({
|
|
|
1016
941
|
messages: sampleMessages,
|
|
1017
942
|
count,
|
|
1018
943
|
label: mode === "selected" ? "Your selection" : "A sample of what matches",
|
|
1019
|
-
emptyReason:
|
|
1020
|
-
mode === "similar" && semanticUnavailable
|
|
1021
|
-
? ("notIndexed" as const)
|
|
1022
|
-
: ("noMatch" as const),
|
|
1023
944
|
loading: escalated
|
|
1024
945
|
? escalatedSample.isPending
|
|
1025
946
|
: previewed && (count.status === "loading" || matchSample.isPending),
|
|
@@ -1044,12 +965,6 @@ function SelectionWizardSession({
|
|
|
1044
965
|
mode,
|
|
1045
966
|
accountId: wizardScope.accountId,
|
|
1046
967
|
onModeChange: changeMode,
|
|
1047
|
-
semanticUnavailable,
|
|
1048
|
-
semanticOff,
|
|
1049
|
-
semanticErrorDetail:
|
|
1050
|
-
widen.error instanceof Error ? widen.error.message : undefined,
|
|
1051
|
-
semanticFallbackTaken,
|
|
1052
|
-
onSemanticFallback: takeSemanticFallback,
|
|
1053
968
|
escalatedScope: escalated?.scope,
|
|
1054
969
|
sample,
|
|
1055
970
|
}}
|
|
@@ -1070,7 +985,6 @@ function SelectionWizardSession({
|
|
|
1070
985
|
conversionNotice: conversion
|
|
1071
986
|
? searchConversionNotice(conversion)
|
|
1072
987
|
: undefined,
|
|
1073
|
-
semanticFallbackTaken,
|
|
1074
988
|
sample: { ...sample, label: "What this matches" },
|
|
1075
989
|
}}
|
|
1076
990
|
folder={{
|
|
@@ -48,30 +48,9 @@ const intelligence: IntelligenceData = {
|
|
|
48
48
|
claimedBrand: "Mondial Relay",
|
|
49
49
|
summary:
|
|
50
50
|
"The display name claims “Mondial Relay”, but the message was sent and signed by a personal gmail.example mailbox — not mondialrelay.fr. Real carriers send from their own domain.",
|
|
51
|
-
similarCount: 15,
|
|
52
51
|
},
|
|
53
52
|
category: { value: "automated" },
|
|
54
53
|
flags: {},
|
|
55
|
-
similar: [
|
|
56
|
-
{
|
|
57
|
-
id: "sim_dhl",
|
|
58
|
-
mailboxId: "mbx_personal_junk",
|
|
59
|
-
threadId: "thr_sim_dhl",
|
|
60
|
-
fromName: "DHL Express",
|
|
61
|
-
subject: "Action required: customs fee outstanding",
|
|
62
|
-
timeLabel: "Thu",
|
|
63
|
-
matched: "body",
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
id: "sim_postnl",
|
|
67
|
-
mailboxId: "mbx_personal_junk",
|
|
68
|
-
threadId: "thr_sim_postnl",
|
|
69
|
-
fromName: "PostNL",
|
|
70
|
-
subject: "Uw pakket kon niet worden bezorgd",
|
|
71
|
-
timeLabel: "28 May",
|
|
72
|
-
matched: "body",
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
54
|
};
|
|
76
55
|
|
|
77
56
|
/**
|
|
@@ -7,6 +7,8 @@ import { longPress } from "@/mocks/story-frame/touch";
|
|
|
7
7
|
|
|
8
8
|
const world = mailWorld();
|
|
9
9
|
|
|
10
|
+
const routeLoad = { timeout: 5000 };
|
|
11
|
+
|
|
10
12
|
const meta = {
|
|
11
13
|
title: "Playground/Shipped/Mail/Daily brief",
|
|
12
14
|
component: AppStory,
|
|
@@ -50,7 +52,9 @@ const pick = async (
|
|
|
50
52
|
export const Default: Story = {
|
|
51
53
|
play: async ({ canvasElement }) => {
|
|
52
54
|
const canvas = within(canvasElement);
|
|
53
|
-
await expect(
|
|
55
|
+
await expect(
|
|
56
|
+
await canvas.findByText("Q3 planning", {}, routeLoad),
|
|
57
|
+
).toBeVisible();
|
|
54
58
|
await expect(canvas.getByText("Autumn sale starts now")).toBeVisible();
|
|
55
59
|
},
|
|
56
60
|
};
|
|
@@ -59,7 +63,9 @@ export const Phone: Story = {
|
|
|
59
63
|
globals: phone,
|
|
60
64
|
play: async ({ canvasElement }) => {
|
|
61
65
|
const canvas = within(canvasElement);
|
|
62
|
-
await expect(
|
|
66
|
+
await expect(
|
|
67
|
+
await canvas.findByText("Q3 planning", {}, routeLoad),
|
|
68
|
+
).toBeVisible();
|
|
63
69
|
},
|
|
64
70
|
};
|
|
65
71
|
|
|
@@ -69,14 +75,16 @@ export const CaughtUp: Story = {
|
|
|
69
75
|
},
|
|
70
76
|
play: async ({ canvasElement }) => {
|
|
71
77
|
const canvas = within(canvasElement);
|
|
72
|
-
await expect(
|
|
78
|
+
await expect(
|
|
79
|
+
await canvas.findByText("You're caught up", {}, routeLoad),
|
|
80
|
+
).toBeVisible();
|
|
73
81
|
},
|
|
74
82
|
};
|
|
75
83
|
|
|
76
84
|
export const FilteredToCategory: Story = {
|
|
77
85
|
play: async ({ canvasElement }) => {
|
|
78
86
|
const canvas = within(canvasElement);
|
|
79
|
-
await canvas.findByText("Q3 planning");
|
|
87
|
+
await canvas.findByText("Q3 planning", {}, routeLoad);
|
|
80
88
|
await openFilters(canvasElement);
|
|
81
89
|
await pick(canvasElement, "Categories", "Newsletter");
|
|
82
90
|
await waitFor(() =>
|
|
@@ -91,13 +99,15 @@ export const FilteredToCategory: Story = {
|
|
|
91
99
|
export const WorkOnly: Story = {
|
|
92
100
|
play: async ({ canvasElement }) => {
|
|
93
101
|
const canvas = within(canvasElement);
|
|
94
|
-
await canvas.findByText("Dinner on Saturday?");
|
|
102
|
+
await canvas.findByText("Dinner on Saturday?", {}, routeLoad);
|
|
95
103
|
await openFilters(canvasElement);
|
|
96
104
|
await pick(canvasElement, "Accounts", "alice.tan");
|
|
97
105
|
await waitFor(() =>
|
|
98
106
|
expect(canvas.queryByText("Dinner on Saturday?")).not.toBeInTheDocument(),
|
|
99
107
|
);
|
|
100
|
-
await expect(
|
|
108
|
+
await expect(
|
|
109
|
+
await canvas.findByText("Q3 planning", {}, routeLoad),
|
|
110
|
+
).toBeVisible();
|
|
101
111
|
},
|
|
102
112
|
};
|
|
103
113
|
|
|
@@ -113,7 +123,7 @@ export const SelectAllThenNarrow: Story = {
|
|
|
113
123
|
},
|
|
114
124
|
play: async ({ canvasElement }) => {
|
|
115
125
|
const canvas = within(canvasElement);
|
|
116
|
-
await canvas.findByText("Q3 planning");
|
|
126
|
+
await canvas.findByText("Q3 planning", {}, routeLoad);
|
|
117
127
|
await expect(
|
|
118
128
|
await canvas.findByRole("button", { name: /^Show (all|\d+ more)/ }),
|
|
119
129
|
).toBeVisible();
|
|
@@ -146,7 +156,7 @@ export const SelectAllThenNarrow: Story = {
|
|
|
146
156
|
export const ShiftArrowRange: Story = {
|
|
147
157
|
play: async ({ canvasElement }) => {
|
|
148
158
|
const canvas = within(canvasElement);
|
|
149
|
-
await canvas.findByText("Q3 planning");
|
|
159
|
+
await canvas.findByText("Q3 planning", {}, routeLoad);
|
|
150
160
|
rows(canvasElement)[0]?.focus();
|
|
151
161
|
await userEvent.keyboard("j");
|
|
152
162
|
await userEvent.keyboard("{Shift>}{ArrowDown}{ArrowDown}{/Shift}");
|
|
@@ -159,7 +169,7 @@ export const ShiftArrowRange: Story = {
|
|
|
159
169
|
export const OpenWithJ: Story = {
|
|
160
170
|
play: async ({ canvasElement }) => {
|
|
161
171
|
const canvas = within(canvasElement);
|
|
162
|
-
await canvas.findByText("Q3 planning");
|
|
172
|
+
await canvas.findByText("Q3 planning", {}, routeLoad);
|
|
163
173
|
rows(canvasElement)[0]?.focus();
|
|
164
174
|
await userEvent.keyboard("j");
|
|
165
175
|
await waitFor(() =>
|
|
@@ -173,7 +183,7 @@ export const OpenWithJ: Story = {
|
|
|
173
183
|
export const MultiSelect: Story = {
|
|
174
184
|
play: async ({ canvasElement }) => {
|
|
175
185
|
const canvas = within(canvasElement);
|
|
176
|
-
await canvas.findByText("Q3 planning");
|
|
186
|
+
await canvas.findByText("Q3 planning", {}, routeLoad);
|
|
177
187
|
const [first, , third] = rows(canvasElement);
|
|
178
188
|
for (const row of [first, third]) {
|
|
179
189
|
const toggle = row?.querySelector<HTMLElement>(
|
|
@@ -189,7 +199,7 @@ export const MultiSelectPhone: Story = {
|
|
|
189
199
|
globals: phone,
|
|
190
200
|
play: async ({ canvasElement }) => {
|
|
191
201
|
const canvas = within(canvasElement);
|
|
192
|
-
await canvas.findByText("Q3 planning");
|
|
202
|
+
await canvas.findByText("Q3 planning", {}, routeLoad);
|
|
193
203
|
const first = rows(canvasElement)[0];
|
|
194
204
|
if (first) await longPress(first);
|
|
195
205
|
await expect(await canvas.findByText("1 message selected")).toBeVisible();
|
|
@@ -201,7 +211,7 @@ export const Search: Story = {
|
|
|
201
211
|
play: async ({ canvasElement }) => {
|
|
202
212
|
const canvas = within(canvasElement);
|
|
203
213
|
await expect(
|
|
204
|
-
await canvas.findByText("Signed lease attached"),
|
|
214
|
+
await canvas.findByText("Signed lease attached", {}, routeLoad),
|
|
205
215
|
).toBeVisible();
|
|
206
216
|
await expect(canvas.queryByText("Dinner on Saturday?")).toBeNull();
|
|
207
217
|
},
|
|
@@ -211,7 +221,7 @@ export const SearchWithChips: Story = {
|
|
|
211
221
|
args: { url: "/mail/brief?q=your" },
|
|
212
222
|
play: async ({ canvasElement }) => {
|
|
213
223
|
const canvas = within(canvasElement);
|
|
214
|
-
await canvas.findByText("Signed lease attached");
|
|
224
|
+
await canvas.findByText("Signed lease attached", {}, routeLoad);
|
|
215
225
|
await openFilters(canvasElement);
|
|
216
226
|
await pick(canvasElement, "Attributes", "Unread");
|
|
217
227
|
await waitFor(() =>
|
|
@@ -226,9 +236,11 @@ export const SearchOfChipsOnly: Story = {
|
|
|
226
236
|
args: { url: "/mail/brief?q=is:unread" },
|
|
227
237
|
play: async ({ canvasElement }) => {
|
|
228
238
|
const canvas = within(canvasElement);
|
|
229
|
-
const offer = await canvas.findByRole(
|
|
230
|
-
|
|
231
|
-
|
|
239
|
+
const offer = await canvas.findByRole(
|
|
240
|
+
"button",
|
|
241
|
+
{ name: "Make this a filter" },
|
|
242
|
+
routeLoad,
|
|
243
|
+
);
|
|
232
244
|
await userEvent.click(offer);
|
|
233
245
|
await expect(
|
|
234
246
|
await canvas.findByText(/isn't a filter condition/, { selector: "p" }),
|
|
@@ -240,7 +252,9 @@ export const SearchHitOutsideTheBrief: Story = {
|
|
|
240
252
|
args: { url: "/mail/brief?q=outline" },
|
|
241
253
|
play: async ({ canvasElement }) => {
|
|
242
254
|
const canvas = within(canvasElement);
|
|
243
|
-
await userEvent.click(
|
|
255
|
+
await userEvent.click(
|
|
256
|
+
await canvas.findByText("Conference talk outline", {}, routeLoad),
|
|
257
|
+
);
|
|
244
258
|
await waitFor(() =>
|
|
245
259
|
expect(
|
|
246
260
|
canvas.getAllByText("Here is the rough structure for the keynote."),
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enter opens the row the keyboard cursor is on, even while the reading pane is
|
|
3
|
+
* still catching up to an earlier cursor move.
|
|
4
|
+
*
|
|
5
|
+
* On desktop the pane follows the cursor: a `j` opens its row a beat later. That
|
|
6
|
+
* open is a navigation, and the list only learns of it when the route hands the
|
|
7
|
+
* opened row back as `selectedMessageId`. If the user pressed `j` again in
|
|
8
|
+
* between, the late arrival used to drag the cursor back to the row it opened,
|
|
9
|
+
* so Enter opened the earlier row instead of the one under the cursor.
|
|
10
|
+
*
|
|
11
|
+
* The host below holds `selectedMessageId` back on purpose, which is what makes
|
|
12
|
+
* that window as wide as the test needs: the URL moves at once, the list hears
|
|
13
|
+
* about it only when the test releases it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { afterEach, describe, it } from "node:test";
|
|
18
|
+
import {
|
|
19
|
+
type AnyRouter,
|
|
20
|
+
createMemoryHistory,
|
|
21
|
+
createRootRoute,
|
|
22
|
+
createRoute,
|
|
23
|
+
createRouter,
|
|
24
|
+
Outlet,
|
|
25
|
+
RouterProvider,
|
|
26
|
+
} from "@tanstack/react-router";
|
|
27
|
+
import { act, createElement, createRef, useState } from "react";
|
|
28
|
+
import { createDomHarness, type DomHarness } from "@/test-support/dom";
|
|
29
|
+
import { makeAccount, makeThreadMessage } from "@/test-support/fixtures";
|
|
30
|
+
import { type HttpMock, mockFetch } from "@/test-support/http";
|
|
31
|
+
import { MessageList, type MessageListCommands } from "./MessageList";
|
|
32
|
+
|
|
33
|
+
const MAILBOX_ID = "mbx-inbox";
|
|
34
|
+
const LIST_PATH = `/mail/${MAILBOX_ID}`;
|
|
35
|
+
|
|
36
|
+
const threads = ["msg-a", "msg-b", "msg-c"].map((messageId) =>
|
|
37
|
+
makeThreadMessage({
|
|
38
|
+
messageId,
|
|
39
|
+
threadId: `thread-${messageId}`,
|
|
40
|
+
mailboxId: MAILBOX_ID,
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
const [FIRST, SECOND] = threads;
|
|
44
|
+
|
|
45
|
+
const openPath = (messageId: string): string =>
|
|
46
|
+
`${LIST_PATH}/thread-${messageId}/${messageId}`;
|
|
47
|
+
|
|
48
|
+
let harness: DomHarness | undefined;
|
|
49
|
+
let http: HttpMock | undefined;
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
harness?.close();
|
|
53
|
+
harness = undefined;
|
|
54
|
+
http?.restore();
|
|
55
|
+
http = undefined;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// The router reads `self` at construction; the shared jsdom globals stop at
|
|
59
|
+
// `window`.
|
|
60
|
+
(globalThis as { self?: typeof globalThis }).self ??= globalThis;
|
|
61
|
+
|
|
62
|
+
interface Mounted {
|
|
63
|
+
mounted: DomHarness;
|
|
64
|
+
router: AnyRouter;
|
|
65
|
+
commands: () => MessageListCommands;
|
|
66
|
+
cursor: () => string | undefined;
|
|
67
|
+
releaseOpen: (messageId: string) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mount = async (): Promise<Mounted> => {
|
|
71
|
+
http = mockFetch((call) => {
|
|
72
|
+
if (call.path.endsWith("/config")) {
|
|
73
|
+
return { accounts: [makeAccount({ accountId: "acc-1" })] };
|
|
74
|
+
}
|
|
75
|
+
return { items: [] };
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const commandsRef = createRef<MessageListCommands | null>();
|
|
79
|
+
let cursor: string | undefined;
|
|
80
|
+
let setSelected: ((messageId: string | undefined) => void) | undefined;
|
|
81
|
+
|
|
82
|
+
const Host = () => {
|
|
83
|
+
const [selected, set] = useState<string | undefined>(undefined);
|
|
84
|
+
setSelected = set;
|
|
85
|
+
return createElement(
|
|
86
|
+
"div",
|
|
87
|
+
null,
|
|
88
|
+
createElement(MessageList, {
|
|
89
|
+
mailboxId: MAILBOX_ID,
|
|
90
|
+
accountId: "acc-1",
|
|
91
|
+
threads,
|
|
92
|
+
selectedMessageId: selected,
|
|
93
|
+
isLoading: false,
|
|
94
|
+
listTitle: "Inbox",
|
|
95
|
+
commandsRef,
|
|
96
|
+
onTriageContextChange: (context) => {
|
|
97
|
+
cursor = context.focusedMessageId;
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
createElement(Outlet),
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const rootRoute = createRootRoute({ component: Outlet });
|
|
105
|
+
const mailRoute = createRoute({
|
|
106
|
+
getParentRoute: () => rootRoute,
|
|
107
|
+
path: "/mail",
|
|
108
|
+
validateSearch: (search: Record<string, unknown>) => search,
|
|
109
|
+
component: Outlet,
|
|
110
|
+
});
|
|
111
|
+
const mailboxRoute = createRoute({
|
|
112
|
+
getParentRoute: () => mailRoute,
|
|
113
|
+
path: "$mailboxId",
|
|
114
|
+
component: Host,
|
|
115
|
+
});
|
|
116
|
+
const listRoute = createRoute({
|
|
117
|
+
getParentRoute: () => mailboxRoute,
|
|
118
|
+
path: "/",
|
|
119
|
+
});
|
|
120
|
+
const threadRoute = createRoute({
|
|
121
|
+
getParentRoute: () => mailboxRoute,
|
|
122
|
+
path: "$threadId",
|
|
123
|
+
});
|
|
124
|
+
const messageRoute = createRoute({
|
|
125
|
+
getParentRoute: () => threadRoute,
|
|
126
|
+
path: "$messageId",
|
|
127
|
+
});
|
|
128
|
+
const router = createRouter({
|
|
129
|
+
routeTree: rootRoute.addChildren([
|
|
130
|
+
mailRoute.addChildren([
|
|
131
|
+
mailboxRoute.addChildren([
|
|
132
|
+
listRoute,
|
|
133
|
+
threadRoute.addChildren([messageRoute]),
|
|
134
|
+
]),
|
|
135
|
+
]),
|
|
136
|
+
]),
|
|
137
|
+
history: createMemoryHistory({ initialEntries: [LIST_PATH] }),
|
|
138
|
+
}) as unknown as AnyRouter;
|
|
139
|
+
await router.load();
|
|
140
|
+
|
|
141
|
+
const mounted = createDomHarness({ viewportWidth: 1440 });
|
|
142
|
+
harness = mounted;
|
|
143
|
+
mounted.renderApp(createElement(RouterProvider, { router }));
|
|
144
|
+
await mounted.waitFor(
|
|
145
|
+
() => commandsRef.current !== null,
|
|
146
|
+
"the list published its commands",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
mounted,
|
|
151
|
+
router,
|
|
152
|
+
commands: () => {
|
|
153
|
+
const commands = commandsRef.current;
|
|
154
|
+
assert.ok(commands, "the list is mounted");
|
|
155
|
+
return commands;
|
|
156
|
+
},
|
|
157
|
+
cursor: () => cursor,
|
|
158
|
+
releaseOpen: (messageId) => act(() => setSelected?.(messageId)),
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
describe("Enter after a cursor move the reading pane has not caught up to", () => {
|
|
163
|
+
it("keeps the cursor on the newer row and opens that row", async () => {
|
|
164
|
+
const { mounted, router, commands, cursor, releaseOpen } = await mount();
|
|
165
|
+
|
|
166
|
+
act(() => commands().focusNext());
|
|
167
|
+
await mounted.waitFor(
|
|
168
|
+
() => router.state.location.pathname === openPath(FIRST.messageId),
|
|
169
|
+
"the pane followed the cursor onto the first row",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
act(() => commands().focusNext());
|
|
173
|
+
await mounted.flush();
|
|
174
|
+
assert.equal(cursor(), SECOND.messageId, "j moved the cursor on");
|
|
175
|
+
|
|
176
|
+
releaseOpen(FIRST.messageId);
|
|
177
|
+
await mounted.flush();
|
|
178
|
+
|
|
179
|
+
assert.equal(
|
|
180
|
+
cursor(),
|
|
181
|
+
SECOND.messageId,
|
|
182
|
+
"the late open dragged the cursor back to the row it opened",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const pushesBefore = router.history.length;
|
|
186
|
+
act(() => commands().openFocused());
|
|
187
|
+
await mounted.waitFor(
|
|
188
|
+
() => router.history.length > pushesBefore,
|
|
189
|
+
"Enter opened a row",
|
|
190
|
+
);
|
|
191
|
+
assert.equal(
|
|
192
|
+
router.state.location.pathname,
|
|
193
|
+
openPath(SECOND.messageId),
|
|
194
|
+
"Enter opened a row other than the one under the cursor",
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -56,10 +56,8 @@ const intelligence: IntelligenceData = {
|
|
|
56
56
|
claimedBrand: "Mondial Relay",
|
|
57
57
|
summary:
|
|
58
58
|
"The display name claims “Mondial Relay”, but the message was sent and signed by a personal gmail.example mailbox — not mondialrelay.fr.",
|
|
59
|
-
similarCount: 15,
|
|
60
59
|
},
|
|
61
60
|
category: { value: "automated" },
|
|
62
|
-
similar: [],
|
|
63
61
|
};
|
|
64
62
|
|
|
65
63
|
interface Message {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { HttpResponse, http } from "msw";
|
|
3
|
+
import { expect, within } from "storybook/test";
|
|
4
|
+
import { AppStory } from "@/mocks/story-frame/AppStory";
|
|
5
|
+
import { composeWorld, messageUrl } from "@/mocks/story-frame/compose-world";
|
|
6
|
+
import { mailHandlers } from "@/mocks/story-frame/mail-handlers";
|
|
7
|
+
|
|
8
|
+
const world = composeWorld();
|
|
9
|
+
|
|
10
|
+
const goneFromServer = http.get("/api/messages/msg-q3-planning", () =>
|
|
11
|
+
HttpResponse.json(
|
|
12
|
+
{
|
|
13
|
+
code: "message_gone_from_server",
|
|
14
|
+
message: "This message is no longer on the mail server.",
|
|
15
|
+
details: { messageId: "msg-q3-planning" },
|
|
16
|
+
},
|
|
17
|
+
{ status: 404 },
|
|
18
|
+
),
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const meta = {
|
|
22
|
+
title: "Playground/Shipped/Mail/Message gone from server",
|
|
23
|
+
component: AppStory,
|
|
24
|
+
args: { url: messageUrl("q3-planning") },
|
|
25
|
+
parameters: {
|
|
26
|
+
layout: "fullscreen",
|
|
27
|
+
msw: { handlers: [goneFromServer, ...mailHandlers(world)] },
|
|
28
|
+
},
|
|
29
|
+
} satisfies Meta<typeof AppStory>;
|
|
30
|
+
export default meta;
|
|
31
|
+
|
|
32
|
+
type Story = StoryObj<typeof meta>;
|
|
33
|
+
|
|
34
|
+
export const MovedOrDeletedElsewhere: Story = {
|
|
35
|
+
play: async () => {
|
|
36
|
+
const page = within(document.body);
|
|
37
|
+
await page.findAllByText("Q3 planning");
|
|
38
|
+
await expect(
|
|
39
|
+
await page.findByText(
|
|
40
|
+
"This message is no longer on the mail server. It was moved or deleted in another mail app.",
|
|
41
|
+
),
|
|
42
|
+
).toBeVisible();
|
|
43
|
+
},
|
|
44
|
+
};
|