@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.
- package/package.json +1 -1
- package/src/components/mail/DailyBrief.selection.test.ts +6 -2
- package/src/components/mail/DailyBrief.tsx +47 -110
- package/src/components/mail/MessageList.selection.test.ts +10 -5
- package/src/components/mail/MessageList.tsx +78 -82
- package/src/components/mail/SelectionWizardHost.tsx +648 -60
- package/src/components/mail/ThreadListInteraction.tsx +0 -9
- package/src/components/mail/organize/SearchFilterEditor.tsx +5 -1
- package/src/components/settings/FilterEditor.tsx +1 -1
- package/src/hooks/useCreateMailbox.ts +16 -4
- package/src/hooks/useFilters.ts +30 -1
- package/src/hooks/useMatchSample.ts +63 -0
- package/src/hooks/useRulePreview.ts +23 -3
- package/src/lib/mail-context.ts +0 -9
- package/src/lib/organize/organize-model.test.ts +110 -0
- package/src/lib/organize/organize-model.ts +69 -0
- package/src/lib/organize/rule-model.ts +2 -0
- package/src/lib/wizard-history.ts +15 -0
- package/src/routes/mail.tsx +0 -7
- package/src/components/mail/organize/MobileOrganizeFlow.render.test.ts +0 -45
- package/src/components/mail/organize/MobileOrganizeFlow.tsx +0 -157
- package/src/components/mail/organize/OrganizeDialog.render.test.ts +0 -37
- package/src/components/mail/organize/OrganizeDialog.tsx +0 -91
- package/src/components/mail/organize/OrganizeRuleEditor.render.test.ts +0 -498
- package/src/components/mail/organize/OrganizeRuleEditor.tsx +0 -285
- package/src/components/mail/organize/SomethingElsePanel.render.test.ts +0 -54
- package/src/components/mail/organize/SomethingElsePanel.tsx +0 -159
- package/src/components/mail/organize/smart-organize.stories.tsx +0 -342
- package/src/lib/organize/mobile-organize-flow.test.ts +0 -96
- 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.
|
|
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("
|
|
51
|
-
|
|
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 {
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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={
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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("
|
|
146
|
-
|
|
147
|
-
|
|
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 {
|
|
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 {
|
|
59
|
-
|
|
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
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
const [
|
|
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
|
-
//
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
// no extra round-trip
|
|
518
|
-
const
|
|
519
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
1289
|
-
//
|
|
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
|
|
1310
|
-
//
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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={
|
|
1349
|
-
|
|
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
|
|
1359
|
-
?
|
|
1356
|
+
junkMailboxId && junkMailboxId !== mailboxId
|
|
1357
|
+
? canOpenWizard
|
|
1358
|
+
? () => startWizard("junk")
|
|
1359
|
+
: !moveDisabledHint
|
|
1360
|
+
? handleJunk
|
|
1361
|
+
: undefined
|
|
1360
1362
|
: undefined
|
|
1361
1363
|
}
|
|
1362
|
-
onMarkRead={
|
|
1363
|
-
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
};
|