@remit/web-client 0.0.92 → 0.0.93

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/components/mail/DailyBrief.selection.test.ts +6 -2
  3. package/src/components/mail/DailyBrief.tsx +47 -110
  4. package/src/components/mail/MessageList.selection.test.ts +10 -5
  5. package/src/components/mail/MessageList.tsx +78 -82
  6. package/src/components/mail/SelectionWizardHost.tsx +648 -60
  7. package/src/components/mail/ThreadListInteraction.tsx +0 -9
  8. package/src/components/mail/organize/SearchFilterEditor.tsx +5 -1
  9. package/src/components/settings/FilterEditor.tsx +1 -1
  10. package/src/hooks/useCreateMailbox.ts +16 -4
  11. package/src/hooks/useFilters.ts +30 -1
  12. package/src/hooks/useMatchSample.ts +63 -0
  13. package/src/hooks/useRulePreview.ts +23 -3
  14. package/src/lib/mail-context.ts +0 -9
  15. package/src/lib/organize/organize-model.test.ts +110 -0
  16. package/src/lib/organize/organize-model.ts +69 -0
  17. package/src/lib/organize/rule-model.ts +2 -0
  18. package/src/lib/wizard-history.ts +15 -0
  19. package/src/routes/mail.tsx +0 -7
  20. package/src/components/mail/organize/MobileOrganizeFlow.render.test.ts +0 -45
  21. package/src/components/mail/organize/MobileOrganizeFlow.tsx +0 -157
  22. package/src/components/mail/organize/OrganizeDialog.render.test.ts +0 -37
  23. package/src/components/mail/organize/OrganizeDialog.tsx +0 -91
  24. package/src/components/mail/organize/OrganizeRuleEditor.render.test.ts +0 -498
  25. package/src/components/mail/organize/OrganizeRuleEditor.tsx +0 -285
  26. package/src/components/mail/organize/SomethingElsePanel.render.test.ts +0 -54
  27. package/src/components/mail/organize/SomethingElsePanel.tsx +0 -159
  28. package/src/components/mail/organize/smart-organize.stories.tsx +0 -342
  29. package/src/lib/organize/mobile-organize-flow.test.ts +0 -96
  30. package/src/lib/organize/mobile-organize-flow.ts +0 -73
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/web-client",
3
- "version": "0.0.92",
3
+ "version": "0.0.93",
4
4
  "type": "module",
5
5
  "description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
6
6
  "exports": {
@@ -47,7 +47,11 @@ describe("the brief's select-all", () => {
47
47
  );
48
48
  });
49
49
 
50
- it("keeps the free-text organize entry reachable, in the overflow", () => {
51
- assert.match(chromeBody(), /onSomethingElse=\{/);
50
+ it("sends every verb on the bar into the wizard (#477 1.4)", () => {
51
+ const body = chromeBody();
52
+ for (const verb of ["delete", "move", "junk", "markRead", "organize"]) {
53
+ assert.match(body, new RegExp(`startWizard\\("${verb}"\\)`));
54
+ }
55
+ assert.doesNotMatch(body, /onSomethingElse/);
52
56
  });
53
57
  });
@@ -46,6 +46,7 @@ import {
46
46
  SelectionTopBar,
47
47
  type ThreadRowData,
48
48
  type ThreadSection,
49
+ type Verb,
49
50
  } from "@remit/ui";
50
51
  import { useQueries, useQuery } from "@tanstack/react-query";
51
52
  import { useNavigate } from "@tanstack/react-router";
@@ -54,7 +55,6 @@ import {
54
55
  type ReactNode,
55
56
  type RefObject,
56
57
  useCallback,
57
- useEffect,
58
58
  useMemo,
59
59
  useState,
60
60
  } from "react";
@@ -65,7 +65,6 @@ import {
65
65
  } from "@/hooks/useInitialSyncProgress";
66
66
  import { useLabelList } from "@/hooks/useLabels";
67
67
  import { useIsDesktop } from "@/hooks/useMediaQuery";
68
- import { useMoveMessages } from "@/hooks/useMoveMessages";
69
68
  import { useSearchTokenContext } from "@/hooks/useSearchTokenContext";
70
69
  import { useSemanticSearch } from "@/hooks/useSemanticSearch";
71
70
  import type { TriageContextUpdate } from "@/hooks/useTriageLayer";
@@ -83,13 +82,15 @@ import type { ListHeaderChrome } from "@/lib/list-header-chrome";
83
82
  import { useMailContext } from "@/lib/mail-context";
84
83
  import { relatedSearchResults, rowToSearchResult } from "@/lib/search-result";
85
84
  import { parseSearchTokens } from "@/lib/search-tokens";
85
+ import { useOpenWizard } from "@/lib/wizard-history";
86
86
  import { LabelApplyTrigger } from "./LabelApplyTrigger";
87
87
  import { MailListHeader, type MailListHeaderProps } from "./MailListHeader";
88
88
  import type { MessageListCommands } from "./MessageList";
89
89
  import { MessageRow } from "./MessageRow";
90
- import { MoveToTrigger } from "./MoveToTrigger";
91
- import { MobileOrganizeFlow } from "./organize/MobileOrganizeFlow";
92
- import { OrganizeDialog } from "./organize/OrganizeDialog";
90
+ import {
91
+ SelectionWizardHost,
92
+ type WizardSelectionMessage,
93
+ } from "./SelectionWizardHost";
93
94
  import {
94
95
  type OpenMessageOptions,
95
96
  ThreadListInteraction,
@@ -293,7 +294,6 @@ interface BriefSelectionChromeProps {
293
294
  >;
294
295
  /** The rows the list is showing, for resolving the selection's scope. */
295
296
  rows: readonly ThreadRowData[];
296
- isDesktop: boolean;
297
297
  onMarkMessagesRead?: (messageIds: string[]) => void;
298
298
  children: ReactNode;
299
299
  }
@@ -307,7 +307,6 @@ interface BriefSelectionChromeProps {
307
307
  function BriefSelectionChrome({
308
308
  header,
309
309
  rows,
310
- isDesktop,
311
310
  onMarkMessagesRead,
312
311
  children,
313
312
  }: BriefSelectionChromeProps) {
@@ -315,7 +314,6 @@ function BriefSelectionChrome({
315
314
  selectedIds,
316
315
  selectedCount,
317
316
  exitSelection,
318
- requestDeleteSelection,
319
317
  orderedIds,
320
318
  allSelected,
321
319
  toggleAllLoaded,
@@ -327,57 +325,40 @@ function BriefSelectionChrome({
327
325
  );
328
326
  const { junkMailboxId } = useJunkMailbox(scope.accountId);
329
327
  const { labels } = useLabelList(scope.accountId);
330
- const { moveMessages, isPending: isMoving } = useMoveMessages({
331
- mailboxId: scope.mailboxId ?? "",
332
- accountId: scope.accountId,
333
- });
334
-
335
- const [organizeOpen, setOrganizeOpen] = useState(false);
336
- const [mobileOrganizeEntry, setMobileOrganizeEntry] = useState<
337
- "select-similar" | "something-else" | null
338
- >(null);
339
328
 
340
- // An emptied selection takes the flows it opened with it, so a later
341
- // re-selection can't reopen one on a stale entry.
342
- useEffect(() => {
343
- if (selectedCount > 0) return;
344
- setOrganizeOpen(false);
345
- setMobileOrganizeEntry(null);
346
- }, [selectedCount]);
329
+ const openWizard = useOpenWizard();
330
+ // The verb the bar was pressed for. The wizard is open for as long as the URL
331
+ // holds a step, so a back that pops the last one closes it.
332
+ const [wizardVerb, setWizardVerb] = useState<Verb>("organize");
333
+ const startWizard = useCallback(
334
+ (verb: Verb) => {
335
+ setWizardVerb(verb);
336
+ openWizard("match");
337
+ },
338
+ [openWizard],
339
+ );
347
340
 
348
341
  const selectedMessageIds = useMemo(
349
342
  () => Array.from(selectedIds),
350
343
  [selectedIds],
351
344
  );
352
- const selectedSenders = useMemo(() => {
353
- const emails: string[] = [];
354
- for (const row of rows) {
355
- if (selectedIds.has(row.id) && row.fromEmail) emails.push(row.fromEmail);
356
- }
357
- return emails;
358
- }, [rows, selectedIds]);
359
-
360
- const handleMarkAsRead = useCallback(() => {
361
- onMarkMessagesRead?.(selectedMessageIds);
362
- exitSelection();
363
- }, [onMarkMessagesRead, selectedMessageIds, exitSelection]);
364
-
365
- const handleMove = useCallback(
366
- (destinationMailboxId: string) => {
367
- if (selectedMessageIds.length === 0) return;
368
- moveMessages(selectedMessageIds, destinationMailboxId);
369
- exitSelection();
370
- },
371
- [moveMessages, selectedMessageIds, exitSelection],
345
+ // The ticked rows as the wizard reads them — the sample under every screen
346
+ // that names a match, and the senders its widen falls back to.
347
+ const wizardSelection = useMemo<WizardSelectionMessage[]>(
348
+ () =>
349
+ rows
350
+ .filter((row) => selectedIds.has(row.id))
351
+ .map((row) => ({
352
+ id: row.id,
353
+ sender: row.fromName,
354
+ email: row.fromEmail,
355
+ subject: row.subject,
356
+ date: row.timeLabel,
357
+ })),
358
+ [rows, selectedIds],
372
359
  );
373
360
 
374
- const handleJunk = useCallback(() => {
375
- if (junkMailboxId) handleMove(junkMailboxId);
376
- }, [junkMailboxId, handleMove]);
377
-
378
- const scoped = !!scope.accountId && !!scope.mailboxId;
379
- const canJunk =
380
- scoped && !!junkMailboxId && junkMailboxId !== scope.mailboxId;
361
+ const canJunk = !!junkMailboxId && junkMailboxId !== scope.mailboxId;
381
362
 
382
363
  // One select-all for both surfaces: the desktop toolbar and the touch sheet
383
364
  // offer the same control over the same rendered rows, so the verb a phone
@@ -410,31 +391,12 @@ function BriefSelectionChrome({
410
391
  idleSlot={chrome.makeFilterSlot}
411
392
  count={selectedCount}
412
393
  onCancel={exitSelection}
413
- onDelete={requestDeleteSelection}
414
- onOrganize={
415
- scoped
416
- ? () =>
417
- isDesktop
418
- ? setOrganizeOpen(true)
419
- : setMobileOrganizeEntry("select-similar")
420
- : undefined
421
- }
422
- onJunk={canJunk ? handleJunk : undefined}
423
- onMarkRead={onMarkMessagesRead ? handleMarkAsRead : undefined}
424
- onSomethingElse={
425
- scoped && !isDesktop
426
- ? () => setMobileOrganizeEntry("something-else")
427
- : undefined
428
- }
429
- moveSlot={
430
- scoped && scope.accountId && scope.mailboxId ? (
431
- <MoveToTrigger
432
- accountId={scope.accountId}
433
- currentMailboxId={scope.mailboxId}
434
- onMove={isMoving ? () => {} : handleMove}
435
- label="Move selected messages"
436
- />
437
- ) : undefined
394
+ onDelete={() => startWizard("delete")}
395
+ onMove={() => startWizard("move")}
396
+ onOrganize={() => startWizard("organize")}
397
+ onJunk={canJunk ? () => startWizard("junk") : undefined}
398
+ onMarkRead={
399
+ onMarkMessagesRead ? () => startWizard("markRead") : undefined
438
400
  }
439
401
  overflowSlot={
440
402
  scope.accountId &&
@@ -448,48 +410,24 @@ function BriefSelectionChrome({
448
410
  />
449
411
  ) : undefined
450
412
  }
451
- isBusy={isMoving}
452
413
  selectAll={selectAll}
453
414
  notice={notice}
454
415
  />
455
416
  );
456
417
 
457
- const organizeFlow =
458
- mobileOrganizeEntry &&
459
- scope.accountId &&
460
- !isDesktop &&
461
- selectedCount > 0 ? (
462
- <MobileOrganizeFlow
463
- entry={mobileOrganizeEntry}
464
- accountId={scope.accountId}
465
- selectedMessageIds={selectedMessageIds}
466
- selectedSenders={selectedSenders}
467
- junkMailboxId={junkMailboxId}
468
- onClose={() => {
469
- setMobileOrganizeEntry(null);
470
- exitSelection();
471
- }}
472
- />
473
- ) : undefined;
474
-
475
418
  return (
476
419
  <>
477
- <MailListHeader
478
- {...header}
479
- selectionBar={selectionBar}
480
- paneOverlay={organizeFlow}
481
- >
420
+ <MailListHeader {...header} selectionBar={selectionBar}>
482
421
  {children}
483
422
  </MailListHeader>
484
- {organizeOpen && scope.accountId && (
485
- <OrganizeDialog
486
- open={organizeOpen}
487
- accountId={scope.accountId}
488
- selectedMessageIds={selectedMessageIds}
489
- selectedSenders={selectedSenders}
490
- onClose={() => setOrganizeOpen(false)}
491
- />
492
- )}
423
+ <SelectionWizardHost
424
+ verb={wizardVerb}
425
+ accountId={scope.accountId}
426
+ mailboxId={scope.mailboxId}
427
+ selection={wizardSelection}
428
+ crossAccount={scope.moveDisabledHint !== undefined}
429
+ onFinished={exitSelection}
430
+ />
493
431
  </>
494
432
  );
495
433
  }
@@ -850,7 +788,6 @@ export function DailyBrief({
850
788
  onSelectSearchResult,
851
789
  }}
852
790
  rows={filteredRows}
853
- isDesktop={isDesktop}
854
791
  onMarkMessagesRead={onMarkMessagesRead}
855
792
  >
856
793
  <div className="flex h-full flex-col">
@@ -78,8 +78,9 @@ describe("MessageList escalated actions", () => {
78
78
 
79
79
  it("keeps mark-read and the move slot on an escalated selection", () => {
80
80
  // The bar always carries the mark-read verb (it drops it itself while
81
- // counting or busy); an escalated selection must never lose it.
82
- assert.match(source, /onMarkRead=\{handleMarkAsRead\}/);
81
+ // counting or busy); an escalated selection must never lose it — it falls
82
+ // back to the predicate run, which the wizard cannot take.
83
+ assert.match(source, /: handleMarkAsRead\s*\}/);
83
84
  // The move slot is offered for a bounded selection or an escalated one —
84
85
  // #114's rule that an escalated selection is never delete-only.
85
86
  assert.match(
@@ -142,9 +143,13 @@ describe("MessageList escalation reaches desktop (#212)", () => {
142
143
  assert.match(source, /navSlot=\{listHeaderChrome\.navSlot\}/);
143
144
  });
144
145
 
145
- it("keeps the free-text organize entry reachable, in the overflow", () => {
146
- assert.match(source, /onSomethingElse=\{/);
147
- assert.match(source, /setMobileOrganizeEntry\("something-else"\)/);
146
+ it("sends every verb on the bar into the wizard (#477 1.4)", () => {
147
+ const bar = source.match(/<SelectionTopBar[\s\S]*?\n\t\t\/>/)?.[0] ?? "";
148
+ for (const verb of ["delete", "move", "junk", "markRead", "organize"]) {
149
+ assert.match(bar, new RegExp(`startWizard\\("${verb}"\\)`));
150
+ }
151
+ // The one selection surface has no second organize entry beside them.
152
+ assert.doesNotMatch(source, /onSomethingElse/);
148
153
  });
149
154
 
150
155
  it("offers the label picker only when the account has labels", () => {
@@ -6,6 +6,7 @@ import {
6
6
  MessageListLoadingMore,
7
7
  MessageListPane,
8
8
  SelectionTopBar,
9
+ type Verb,
9
10
  } from "@remit/ui";
10
11
  import { useBlocker, useNavigate } from "@tanstack/react-router";
11
12
  import { useVirtualizer } from "@tanstack/react-virtual";
@@ -44,7 +45,11 @@ import {
44
45
  escalatedStatusLabel,
45
46
  escalationActionLabel,
46
47
  } from "@/lib/escalation-label";
47
- import { formatDeleteToTrashTitle, formatNumber } from "@/lib/format";
48
+ import {
49
+ formatDeleteToTrashTitle,
50
+ formatEmailDate,
51
+ formatNumber,
52
+ } from "@/lib/format";
48
53
  import { tabStopId } from "@/lib/list-focus";
49
54
  import { useListHeaderChrome } from "@/lib/list-header-chrome";
50
55
  import {
@@ -52,11 +57,13 @@ import {
52
57
  shouldExitSelectionOnNavigate,
53
58
  } from "@/lib/selection-mode";
54
59
  import { cn } from "@/lib/utils";
55
- import { useWizardStepValue } from "@/lib/wizard-history";
60
+ import { useOpenWizard, useWizardStepValue } from "@/lib/wizard-history";
56
61
  import { LabelApplyTrigger } from "./LabelApplyTrigger";
57
62
  import { MoveToTrigger } from "./MoveToTrigger";
58
- import { MobileOrganizeFlow } from "./organize/MobileOrganizeFlow";
59
- import { OrganizeDialog } from "./organize/OrganizeDialog";
63
+ import {
64
+ SelectionWizardHost,
65
+ type WizardSelectionMessage,
66
+ } from "./SelectionWizardHost";
60
67
  import { SwipeableMessageRow } from "./SwipeableMessageRow";
61
68
 
62
69
  /**
@@ -234,13 +241,11 @@ export const MessageList = ({
234
241
  const navigate = useNavigate();
235
242
  const isDesktop = useIsDesktop();
236
243
  const wizardStep = useWizardStepValue();
237
- const [organizeOpen, setOrganizeOpen] = useState(false);
238
- // Mobile only: which selection-sheet entry opened the guided organize flow
239
- // (`null` when closed). Desktop uses `organizeOpen` + `OrganizeDialog`
240
- // instead a sheet is a touch pattern (issue #211).
241
- const [mobileOrganizeEntry, setMobileOrganizeEntry] = useState<
242
- "select-similar" | "something-else" | null
243
- >(null);
244
+ const openWizard = useOpenWizard();
245
+ // The verb the bar was pressed for. The wizard itself is open for as long as
246
+ // the URL holds a step, so a back that pops the last one closes it without
247
+ // anything here having to hear about it.
248
+ const [wizardVerb, setWizardVerb] = useState<Verb>("organize");
244
249
  const isSearching = !!searchQuery?.trim();
245
250
  const listHeaderChrome = useListHeaderChrome();
246
251
  const { labels } = useLabelList(accountId);
@@ -317,13 +322,6 @@ export const MessageList = ({
317
322
  // it back to the count, and across that render the two disagree.
318
323
  const isMultiSelectMode = deriveIsMultiSelectMode(selectedCount, isDesktop);
319
324
 
320
- // When the selection empties — cancel, a completed action, a mailbox switch —
321
- // the guided organize entry must reset too, so a later re-selection doesn't
322
- // reopen the flow on a stale entry (issue #211).
323
- useEffect(() => {
324
- if (selectedCount === 0) setMobileOrganizeEntry(null);
325
- }, [selectedCount]);
326
-
327
325
  // Pending delete, awaiting confirmation. `null` means the dialog is closed.
328
326
  // `source: "ids"` snapshots the concrete ids at request time so a selection
329
327
  // change behind the dialog can't retarget the delete — every keyboard/desktop
@@ -511,17 +509,23 @@ export const MessageList = ({
511
509
  // false for a plain click (caller lets the Link navigate).
512
510
  const orderedIds = useMemo(() => threads.map((t) => t.messageId), [threads]);
513
511
 
514
- // Sender addresses of the checked rows, for the organize widen's literal
515
- // fallback when this deployment ships no vector pipeline (the sheet matches
516
- // all mail from these senders). Read off the already-loaded thread rows, so
517
- // no extra round-trip; the widen hook dedupes.
518
- const selectedSenders = useMemo(() => {
519
- const emails: string[] = [];
512
+ // The checked rows as the wizard reads them: the sample it shows under every
513
+ // screen that names a match, and the senders its widen falls back to on a
514
+ // deployment with no vector pipeline. Read off the already-loaded thread rows,
515
+ // so no extra round-trip.
516
+ const wizardSelection = useMemo<WizardSelectionMessage[]>(() => {
517
+ const rows: WizardSelectionMessage[] = [];
520
518
  for (const thread of threads) {
521
519
  if (!selectedIds.has(thread.messageId)) continue;
522
- if (thread.fromEmail) emails.push(thread.fromEmail);
520
+ rows.push({
521
+ id: thread.messageId,
522
+ sender: thread.fromName ?? thread.fromEmail ?? "Unknown",
523
+ email: thread.fromEmail ?? "",
524
+ subject: thread.subject ?? "(No subject)",
525
+ date: formatEmailDate(thread.sentDate),
526
+ });
523
527
  }
524
- return emails;
528
+ return rows;
525
529
  }, [threads, selectedIds]);
526
530
  const handleRowSelect = useCallback(
527
531
  (messageId: string, modifiers: SelectionModifiers): boolean => {
@@ -877,6 +881,23 @@ export const MessageList = ({
877
881
  return undefined;
878
882
  }, [selectedCount, selectedIds, threads]);
879
883
 
884
+ // Every verb on the bar opens the wizard on the ticked rows (#477 1.4),
885
+ // including a selection spanning accounts — the wizard is where that
886
+ // restriction is stated, on the step that needs one account (#477 5.5). It is
887
+ // withdrawn only once the selection escalates to a predicate or a run takes
888
+ // over: an escalated selection is a predicate, which no bounded list of ids
889
+ // stands in for (#477 3.1), and keeps the bar's own chunked runner.
890
+ const canOpenWizard =
891
+ escalation.phase.kind === "idle" && !escalation.isRunning;
892
+
893
+ const startWizard = useCallback(
894
+ (verb: Verb) => {
895
+ setWizardVerb(verb);
896
+ openWizard("match");
897
+ },
898
+ [openWizard],
899
+ );
900
+
880
901
  // Swipe-to-delete single message
881
902
  const handleSwipeDelete = useCallback(
882
903
  (messageId: string) => {
@@ -1285,15 +1306,10 @@ export const MessageList = ({
1285
1306
  }
1286
1307
  : undefined;
1287
1308
 
1288
- // The bar shows one notice at a time, so the cross-account move restriction
1289
- // rides in behind the escalation states.
1290
- const selectionNotice =
1291
- escalationNotice ??
1292
- (moveDisabledHint
1293
- ? { tone: "warning" as const, text: moveDisabledHint }
1294
- : undefined);
1295
-
1309
+ // An escalated selection is a predicate the wizard cannot take, so Move there
1310
+ // keeps the caller's own folder picker and the chunked predicate run (#114).
1296
1311
  const selectionMoveSlot =
1312
+ !canOpenWizard &&
1297
1313
  (onMoveMessages || escalation.phase.kind === "escalated") &&
1298
1314
  accountId &&
1299
1315
  mailboxId ? (
@@ -1306,28 +1322,13 @@ export const MessageList = ({
1306
1322
  />
1307
1323
  ) : undefined;
1308
1324
 
1309
- // The guided organize flow (issue #211) covers the pane, which is its
1310
- // positioned ancestor.
1311
- const mobileOrganizeFlow =
1312
- mobileOrganizeEntry && accountId && !isDesktop && selectedCount > 0 ? (
1313
- <MobileOrganizeFlow
1314
- entry={mobileOrganizeEntry}
1315
- accountId={accountId}
1316
- selectedMessageIds={Array.from(selectedIds)}
1317
- selectedSenders={selectedSenders}
1318
- junkMailboxId={junkMailboxId}
1319
- onClose={() => {
1320
- setMobileOrganizeEntry(null);
1321
- exitSelection();
1322
- }}
1323
- />
1324
- ) : undefined;
1325
-
1326
- // Organize acts on the materialized selection within one account, so it is
1327
- // withdrawn the moment the selection escalates to a predicate or a run
1328
- // takes over — both of which name themselves through `statusLabel`.
1329
- const canOrganize =
1330
- !!accountId && !!mailboxId && !moveDisabledHint && !selectionStatusLabel;
1325
+ // The bar shows one notice at a time, so the cross-account move restriction
1326
+ // rides in behind the escalation states.
1327
+ const selectionNotice =
1328
+ escalationNotice ??
1329
+ (moveDisabledHint
1330
+ ? { tone: "warning" as const, text: moveDisabledHint }
1331
+ : undefined);
1331
1332
 
1332
1333
  // One selection surface at every width, always mounted: it is the list
1333
1334
  // header. With nothing ticked it names the mailbox and carries the header's
@@ -1345,27 +1346,24 @@ export const MessageList = ({
1345
1346
  idleSlot={listHeaderChrome.makeFilterSlot}
1346
1347
  count={selectionCount}
1347
1348
  onCancel={handleSelectionCancel}
1348
- onDelete={handleSelectionDelete}
1349
- onOrganize={
1350
- canOrganize
1351
- ? () =>
1352
- isDesktop
1353
- ? setOrganizeOpen(true)
1354
- : setMobileOrganizeEntry("select-similar")
1355
- : undefined
1349
+ onDelete={
1350
+ canOpenWizard ? () => startWizard("delete") : handleSelectionDelete
1356
1351
  }
1352
+ onMove={canOpenWizard ? () => startWizard("move") : undefined}
1353
+ moveSlot={selectionMoveSlot}
1354
+ onOrganize={canOpenWizard ? () => startWizard("organize") : undefined}
1357
1355
  onJunk={
1358
- junkMailboxId && junkMailboxId !== mailboxId && !moveDisabledHint
1359
- ? handleJunk
1356
+ junkMailboxId && junkMailboxId !== mailboxId
1357
+ ? canOpenWizard
1358
+ ? () => startWizard("junk")
1359
+ : !moveDisabledHint
1360
+ ? handleJunk
1361
+ : undefined
1360
1362
  : undefined
1361
1363
  }
1362
- onMarkRead={handleMarkAsRead}
1363
- onSomethingElse={
1364
- canOrganize && !isDesktop
1365
- ? () => setMobileOrganizeEntry("something-else")
1366
- : undefined
1364
+ onMarkRead={
1365
+ canOpenWizard ? () => startWizard("markRead") : handleMarkAsRead
1367
1366
  }
1368
- moveSlot={selectionMoveSlot}
1369
1367
  overflowSlot={
1370
1368
  accountId && mailboxId && selectedCount > 0 && labels.length > 0 ? (
1371
1369
  <LabelApplyTrigger
@@ -1520,7 +1518,6 @@ export const MessageList = ({
1520
1518
  isDesktop={isDesktop}
1521
1519
  hideHeader={hideHeader}
1522
1520
  selectionBar={activeSelectionBar}
1523
- paneOverlay={mobileOrganizeFlow}
1524
1521
  listBody={
1525
1522
  listHeaderChrome.searchResults ??
1526
1523
  (listState === "ready" ? virtualBody : undefined)
@@ -1543,15 +1540,14 @@ export const MessageList = ({
1543
1540
  onConfirm={handleConfirmDelete}
1544
1541
  onCancel={handleCancelDelete}
1545
1542
  />
1546
- {organizeOpen && accountId && (
1547
- <OrganizeDialog
1548
- open={organizeOpen}
1549
- accountId={accountId}
1550
- selectedMessageIds={Array.from(selectedIds)}
1551
- selectedSenders={selectedSenders}
1552
- onClose={() => setOrganizeOpen(false)}
1553
- />
1554
- )}
1543
+ <SelectionWizardHost
1544
+ verb={wizardVerb}
1545
+ accountId={accountId}
1546
+ mailboxId={mailboxId}
1547
+ selection={wizardSelection}
1548
+ crossAccount={moveDisabledHint !== undefined}
1549
+ onFinished={exitSelection}
1550
+ />
1555
1551
  </>
1556
1552
  );
1557
1553
  };