@remit/web-client 0.0.93 → 0.0.95

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 (41) hide show
  1. package/package.json +1 -1
  2. package/src/components/mail/BriefPane.selection.test.ts +0 -1
  3. package/src/components/mail/BriefPane.tsx +7 -17
  4. package/src/components/mail/DailyBrief.selection.test.ts +21 -3
  5. package/src/components/mail/DailyBrief.tsx +37 -39
  6. package/src/components/mail/FlaggedList.tsx +55 -8
  7. package/src/components/mail/FlaggedPane.tsx +9 -23
  8. package/src/components/mail/MailListHeader.tsx +41 -39
  9. package/src/components/mail/MailboxPane.tsx +24 -11
  10. package/src/components/mail/MessageList.selection.test.ts +44 -30
  11. package/src/components/mail/MessageList.tsx +117 -339
  12. package/src/components/mail/SelectionWizardHost.tsx +265 -48
  13. package/src/components/mail/ThreadListInteraction.test.ts +74 -4
  14. package/src/components/mail/ThreadListInteraction.tsx +58 -50
  15. package/src/components/mail/make-filter-entry.test.ts +63 -0
  16. package/src/components/mail/selection-verbs.test.ts +278 -0
  17. package/src/components/ui/ConfirmDialog.stories.tsx +0 -15
  18. package/src/hooks/useEscalatedActions.ts +2 -2
  19. package/src/hooks/useMatchSample.ts +44 -3
  20. package/src/hooks/useRulePreview.ts +1 -2
  21. package/src/lib/bulk-action-copy.test.ts +0 -19
  22. package/src/lib/bulk-action-copy.ts +0 -8
  23. package/src/lib/bulk-actions.test.ts +0 -60
  24. package/src/lib/bulk-actions.ts +0 -26
  25. package/src/lib/format.test.ts +0 -27
  26. package/src/lib/format.ts +4 -14
  27. package/src/lib/list-header-chrome.ts +10 -0
  28. package/src/lib/organize/organize-model.ts +8 -2
  29. package/src/lib/organize/rule-model.ts +0 -4
  30. package/src/lib/wizard-history.test.ts +31 -0
  31. package/src/lib/wizard-history.ts +97 -9
  32. package/src/routes/mail.tsx +3 -1
  33. package/src/components/mail/organize/SearchFilterDialog.render.test.ts +0 -47
  34. package/src/components/mail/organize/SearchFilterDialog.tsx +0 -90
  35. package/src/components/mail/organize/SearchFilterEditor.render.test.ts +0 -260
  36. package/src/components/mail/organize/SearchFilterEditor.tsx +0 -140
  37. package/src/components/mail/organize/rule-editor-states.stories.tsx +0 -147
  38. package/src/components/mail/organize/rule-editor-states.tsx +0 -139
  39. package/src/hooks/useRuleEditorState.ts +0 -182
  40. package/src/hooks/useSearchFilterSeed.render.test.ts +0 -118
  41. package/src/hooks/useSearchFilterSeed.ts +0 -79
@@ -695,21 +695,24 @@ function MailboxPaneProvider({
695
695
  if (selectedThread) setToolbarComposeRequest("forward");
696
696
  }, [ensureFocusedOpen, selectedThread, setToolbarComposeRequest]);
697
697
 
698
- const triageTargetMessageIds = useCallback((): string[] => {
699
- if (triageSelectedIds.length > 0) return triageSelectedIds;
700
- return messageIdsForFocusedThread(focusedThread);
701
- }, [triageSelectedIds, messageIdsForFocusedThread, focusedThread]);
702
-
703
- // Prefer the list's delete: it confirms the move to Trash and then places the
704
- // cursor on a surviving row. Falls back to the reading pane's message when no
705
- // list is mounted or nothing in it is targeted.
698
+ const triageTargetMessageIds = useCallback(
699
+ (): string[] => messageIdsForFocusedThread(focusedThread),
700
+ [messageIdsForFocusedThread, focusedThread],
701
+ );
702
+
703
+ // Every verb the list can take, it takes: over a selection it opens the
704
+ // wizard, which is where a bulk action is reviewed before it reaches the mail
705
+ // server (#477 1.4, #508). What falls through is aimed at the bare cursor, or
706
+ // at the reading pane when no list is mounted — one message, not a bulk
707
+ // action, and the pane acts on it directly.
706
708
  const triageDeleteAction = useCallback(() => {
707
- if (listCommandsRef.current?.requestDelete()) return;
709
+ if (listCommandsRef.current?.requestVerb("delete")) return;
708
710
  const ids = triageTargetMessageIds();
709
711
  if (ids.length > 0) triageDelete(ids);
710
712
  }, [listCommandsRef, triageTargetMessageIds, triageDelete]);
711
713
 
712
714
  const triageMarkJunk = useCallback(() => {
715
+ if (listCommandsRef.current?.requestVerb("junk")) return;
713
716
  if (!junkMailboxId) return;
714
717
  const ids = triageTargetMessageIds();
715
718
  if (ids.length === 0) return;
@@ -720,6 +723,7 @@ function MailboxPaneProvider({
720
723
  });
721
724
  triageMove(ids, junkMailboxId);
722
725
  }, [
726
+ listCommandsRef,
723
727
  junkMailboxId,
724
728
  triageTargetMessageIds,
725
729
  triageMove,
@@ -727,6 +731,10 @@ function MailboxPaneProvider({
727
731
  focusedThread,
728
732
  ]);
729
733
 
734
+ // Star is the one verb that acts on a selection from here. It is not on the
735
+ // selection bar and not one of the five the wizard walks: it sets a flag on
736
+ // mail that is already in front of the user and unsets it the same way, so
737
+ // there is nothing for a review screen to name and nothing to undo.
730
738
  const triageStar = useCallback(() => {
731
739
  if (triageSelectedIds.length > 0) {
732
740
  const nextStarred = !(focusedThread?.hasStars ?? false);
@@ -743,11 +751,17 @@ function MailboxPaneProvider({
743
751
  }, [triageSelectedIds, threads, focusedThread, focusedToggleStar]);
744
752
 
745
753
  const triageToggleRead = useCallback(() => {
754
+ if (listCommandsRef.current?.requestVerb("markRead")) return;
746
755
  const ids = triageTargetMessageIds();
747
756
  if (ids.length === 0) return;
748
757
  const nextRead = !(focusedThread?.isRead ?? false);
749
758
  triageToggleReadFor(ids, nextRead);
750
- }, [triageTargetMessageIds, focusedThread, triageToggleReadFor]);
759
+ }, [
760
+ listCommandsRef,
761
+ triageTargetMessageIds,
762
+ focusedThread,
763
+ triageToggleReadFor,
764
+ ]);
751
765
 
752
766
  const triageMute = useCallback(() => {
753
767
  if (!focusedAddressId) return;
@@ -1013,7 +1027,6 @@ function MailboxList() {
1013
1027
  searchQuery={searchQuery}
1014
1028
  searchPredicate={searchPredicate}
1015
1029
  onDeleteMessages={onDeleteMessages}
1016
- onMoveMessages={onMoveMessages}
1017
1030
  isDeleting={isDeleting}
1018
1031
  isMoving={isMoving}
1019
1032
  onLoadMore={onLoadMore}
@@ -59,45 +59,43 @@ describe("MessageList selection mode", () => {
59
59
  });
60
60
 
61
61
  /**
62
- * An escalated selection is a predicate, so it has no id list to hand the
63
- * optimistic move/mark-read mutations before #114 the bar simply dropped
64
- * both, leaving "select all 1,284 matching npm" able to delete and nothing
65
- * else. All three actions now page the predicate through the same run.
62
+ * An escalated selection is a predicate, and it walks the same wizard every
63
+ * other selection walks (#508). The list hands the wizard the predicate — the
64
+ * words it names it with, the count it was escalated to, and the chunked runner
65
+ * that re-resolves it and keeps no second confirmation of its own.
66
66
  */
67
67
  describe("MessageList escalated actions", () => {
68
- it("routes move and mark-read through the predicate run when escalated", () => {
68
+ it("hands the escalated predicate to the wizard rather than running it here", () => {
69
69
  assert.match(
70
70
  source,
71
- /escalation\.phase\.kind === "escalated"\)\s*\{\s*void runEscalatedAction\(MARK_READ_ACTION\);/,
71
+ /const escalatedSelection: EscalatedSelection \| undefined =\s*escalation\.phase\.kind === "escalated"/,
72
72
  );
73
73
  assert.match(
74
74
  source,
75
- /escalation\.phase\.kind === "escalated"\)\s*\{\s*void runEscalatedAction\(\{ kind: "move", destinationMailboxId \}\);/,
75
+ /scope: describeSearchScope\(searchPredicate \?\? \{\}\)/,
76
76
  );
77
- });
78
-
79
- it("keeps mark-read and the move slot on an escalated selection", () => {
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 — it falls
82
- // back to the predicate run, which the wizard cannot take.
83
- assert.match(source, /: handleMarkAsRead\s*\}/);
84
- // The move slot is offered for a bounded selection or an escalated one —
85
- // #114's rule that an escalated selection is never delete-only.
77
+ assert.match(source, /total: escalation\.phase\.total/);
86
78
  assert.match(
87
79
  source,
88
- /onMoveMessages \|\| escalation\.phase\.kind === "escalated"/,
80
+ /run: \(action: EscalatedAction\) => escalation\.runAction\(action\)/,
89
81
  );
82
+ const host = source.match(/<SelectionWizardHost[\s\S]*?\/>/)?.[0] ?? "";
83
+ assert.match(host, /escalated=\{escalatedSelection\}/);
90
84
  });
91
85
 
92
- it("words progress and completion per action instead of per delete", () => {
86
+ it("keeps no predicate confirmation of its own", () => {
87
+ // The review screen names what the predicate covers; a second dialog
88
+ // asking the same question is the drift the wizard exists to end.
89
+ assert.doesNotMatch(source, /source: "predicate"/);
90
+ assert.doesNotMatch(source, /requestEscalatedDelete/);
91
+ assert.doesNotMatch(source, /pendingDeleteIsEstimate/);
92
+ });
93
+
94
+ it("words the bar's progress per action instead of per delete", () => {
93
95
  assert.match(
94
96
  source,
95
97
  /bulkActionProgressLabel\(\s*escalation\.runningAction\.kind,/,
96
98
  );
97
- assert.match(
98
- source,
99
- /bulkActionCompletionText\(action\.kind, outcome\.done\)/,
100
- );
101
99
  });
102
100
  });
103
101
 
@@ -143,29 +141,45 @@ describe("MessageList escalation reaches desktop (#212)", () => {
143
141
  assert.match(source, /navSlot=\{listHeaderChrome\.navSlot\}/);
144
142
  });
145
143
 
146
- it("sends every verb on the bar into the wizard (#477 1.4)", () => {
144
+ it("carries every verb the bar can offer (#477 1.4)", () => {
145
+ // That each of them opens the wizard is `selection-verbs.test.ts`, over
146
+ // every surface at once. What this pins is that none of them was dropped
147
+ // from the one bar the mailbox raises.
147
148
  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}"\\)`));
149
+ for (const prop of [
150
+ "onDelete",
151
+ "onMove",
152
+ "onOrganize",
153
+ "onJunk",
154
+ "onMarkRead",
155
+ ]) {
156
+ assert.match(bar, new RegExp(`${prop}=`));
150
157
  }
151
158
  // The one selection surface has no second organize entry beside them.
152
159
  assert.doesNotMatch(source, /onSomethingElse/);
153
160
  });
154
161
 
162
+ it("keeps Organize reachable over a predicate rather than dropping it", () => {
163
+ // A verb that vanishes when a selection escalates leaves the user no route
164
+ // at all: the make-filter affordance it defers to is hidden while rows are
165
+ // ticked (#477 1.7).
166
+ assert.match(source, /if \(escalatedSelection\) \{\s*startFromSearch\(\);/);
167
+ });
168
+
155
169
  it("offers the label picker only when the account has labels", () => {
156
170
  assert.match(source, /labels\.length > 0 \? \(/);
157
171
  });
158
172
  });
159
173
 
160
174
  /**
161
- * A bounded confirm-delete used to open the surviving neighbour by writing
175
+ * A confirmed delete used to open the surviving neighbour by writing
162
176
  * `selectedMessageId` into the URL. On desktop that fills the reading pane
163
177
  * beside the list; on a single-pane mobile layout the same navigation replaced
164
- * the list with a full-screen message, so a bulk delete read as "jumped into a
165
- * random message" rather than "the rows are gone" (#202). Mobile now stays on
166
- * the list and raises the same completion banner a chunked run does.
178
+ * the list with a full-screen message, so the delete read as "jumped into a
179
+ * random message" rather than "the row is gone" (#202). Mobile now stays on the
180
+ * list and raises a completion banner instead.
167
181
  */
168
- describe("MessageList bounded delete stays on the list on mobile (#202)", () => {
182
+ describe("MessageList confirmed delete stays on the list on mobile (#202)", () => {
169
183
  it("only the desktop two-pane opens the surviving neighbour after a delete", () => {
170
184
  assert.match(
171
185
  source,