@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.
- package/package.json +1 -1
- package/src/components/mail/BriefPane.selection.test.ts +0 -1
- package/src/components/mail/BriefPane.tsx +7 -17
- package/src/components/mail/DailyBrief.selection.test.ts +21 -3
- package/src/components/mail/DailyBrief.tsx +37 -39
- package/src/components/mail/FlaggedList.tsx +55 -8
- package/src/components/mail/FlaggedPane.tsx +9 -23
- package/src/components/mail/MailListHeader.tsx +41 -39
- package/src/components/mail/MailboxPane.tsx +24 -11
- package/src/components/mail/MessageList.selection.test.ts +44 -30
- package/src/components/mail/MessageList.tsx +117 -339
- package/src/components/mail/SelectionWizardHost.tsx +265 -48
- package/src/components/mail/ThreadListInteraction.test.ts +74 -4
- package/src/components/mail/ThreadListInteraction.tsx +58 -50
- package/src/components/mail/make-filter-entry.test.ts +63 -0
- package/src/components/mail/selection-verbs.test.ts +278 -0
- package/src/components/ui/ConfirmDialog.stories.tsx +0 -15
- package/src/hooks/useEscalatedActions.ts +2 -2
- package/src/hooks/useMatchSample.ts +44 -3
- package/src/hooks/useRulePreview.ts +1 -2
- package/src/lib/bulk-action-copy.test.ts +0 -19
- package/src/lib/bulk-action-copy.ts +0 -8
- package/src/lib/bulk-actions.test.ts +0 -60
- package/src/lib/bulk-actions.ts +0 -26
- package/src/lib/format.test.ts +0 -27
- package/src/lib/format.ts +4 -14
- package/src/lib/list-header-chrome.ts +10 -0
- package/src/lib/organize/organize-model.ts +8 -2
- package/src/lib/organize/rule-model.ts +0 -4
- package/src/lib/wizard-history.test.ts +31 -0
- package/src/lib/wizard-history.ts +97 -9
- package/src/routes/mail.tsx +3 -1
- package/src/components/mail/organize/SearchFilterDialog.render.test.ts +0 -47
- package/src/components/mail/organize/SearchFilterDialog.tsx +0 -90
- package/src/components/mail/organize/SearchFilterEditor.render.test.ts +0 -260
- package/src/components/mail/organize/SearchFilterEditor.tsx +0 -140
- package/src/components/mail/organize/rule-editor-states.stories.tsx +0 -147
- package/src/components/mail/organize/rule-editor-states.tsx +0 -139
- package/src/hooks/useRuleEditorState.ts +0 -182
- package/src/hooks/useSearchFilterSeed.render.test.ts +0 -118
- 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(
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
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?.
|
|
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
|
-
}, [
|
|
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,
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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("
|
|
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"
|
|
71
|
+
/const escalatedSelection: EscalatedSelection \| undefined =\s*escalation\.phase\.kind === "escalated"/,
|
|
72
72
|
);
|
|
73
73
|
assert.match(
|
|
74
74
|
source,
|
|
75
|
-
/
|
|
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
|
-
/
|
|
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("
|
|
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("
|
|
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
|
|
149
|
-
|
|
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
|
|
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
|
|
165
|
-
* random message" rather than "the
|
|
166
|
-
*
|
|
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
|
|
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,
|