@remit/web-client 0.0.163 → 0.0.164
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.164",
|
|
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": {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { ThreadRowData } from "@remit/ui";
|
|
4
|
+
import { wizardScopeFor } from "@remit/ui";
|
|
5
|
+
import { resolveBriefSelectionScope } from "./DailyBrief";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The brief spans accounts and folders, so it resolves both from the selection
|
|
9
|
+
* itself. Which of the two a selection spans decides what the wizard's folder
|
|
10
|
+
* and rule steps can say: told to pick a single account, a selection already
|
|
11
|
+
* inside one account has nothing to act on and the flow dead-ends (#525).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const row = (
|
|
15
|
+
id: string,
|
|
16
|
+
accountId: string,
|
|
17
|
+
mailboxId: string,
|
|
18
|
+
): ThreadRowData => ({
|
|
19
|
+
id,
|
|
20
|
+
accountId,
|
|
21
|
+
mailboxId,
|
|
22
|
+
fromName: "Booking.com",
|
|
23
|
+
fromEmail: "noreply@booking.com",
|
|
24
|
+
subject: "Booking confirmation",
|
|
25
|
+
snippet: "Your stay is confirmed",
|
|
26
|
+
timeLabel: "Jul 2",
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const rows: ThreadRowData[] = [
|
|
30
|
+
row("m1", "acc-personal", "mbx-inbox"),
|
|
31
|
+
row("m2", "acc-personal", "mbx-archive"),
|
|
32
|
+
row("m3", "acc-work", "mbx-inbox"),
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const scopeOf = (...ids: string[]) =>
|
|
36
|
+
resolveBriefSelectionScope(rows, new Set(ids));
|
|
37
|
+
|
|
38
|
+
describe("the scope a brief selection resolves to", () => {
|
|
39
|
+
it("carries the folder restriction when one account holds every row", () => {
|
|
40
|
+
const scope = scopeOf("m1", "m2");
|
|
41
|
+
assert.equal(scope.restriction, "spansFolders");
|
|
42
|
+
assert.equal(scope.accountId, "acc-personal");
|
|
43
|
+
assert.equal(scope.mailboxId, undefined);
|
|
44
|
+
assert.match(scope.moveDisabledHint ?? "", /spans 2 folders/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("reaches the folder step told about folders, not accounts", () => {
|
|
48
|
+
const scope = scopeOf("m1", "m2");
|
|
49
|
+
const wizard = wizardScopeFor(scope.accountId, scope.restriction);
|
|
50
|
+
assert.match(wizard.destination ?? "", /within one folder/);
|
|
51
|
+
assert.doesNotMatch(wizard.destination ?? "", /account/);
|
|
52
|
+
// The account is settled, so the preview behind a widened door has one to
|
|
53
|
+
// count against and the match step keeps it.
|
|
54
|
+
assert.equal(wizard.accountId, "acc-personal");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("carries the account restriction when the rows span accounts", () => {
|
|
58
|
+
const scope = scopeOf("m1", "m3");
|
|
59
|
+
assert.equal(scope.restriction, "spansAccounts");
|
|
60
|
+
assert.equal(scope.accountId, undefined);
|
|
61
|
+
assert.match(
|
|
62
|
+
wizardScopeFor(scope.accountId, scope.restriction).destination ?? "",
|
|
63
|
+
/single account/,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("carries no restriction from one account and one folder", () => {
|
|
68
|
+
const scope = scopeOf("m1");
|
|
69
|
+
assert.equal(scope.restriction, undefined);
|
|
70
|
+
assert.equal(scope.mailboxId, "mbx-inbox");
|
|
71
|
+
assert.equal(
|
|
72
|
+
wizardScopeFor(scope.accountId, scope.restriction).destination,
|
|
73
|
+
undefined,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
partitionSpamResults,
|
|
55
55
|
RefreshButton,
|
|
56
56
|
type SearchResult,
|
|
57
|
+
type SelectionRestriction,
|
|
57
58
|
SelectionTopBar,
|
|
58
59
|
SpamResultsOffer,
|
|
59
60
|
setBriefCategoryInQuery,
|
|
@@ -185,6 +186,8 @@ export interface BriefSelectionScope {
|
|
|
185
186
|
accountId?: string;
|
|
186
187
|
/** Source folder, when every selected row shares one. */
|
|
187
188
|
mailboxId?: string;
|
|
189
|
+
/** Which scope the selection spans more of than those verbs can take. */
|
|
190
|
+
restriction?: SelectionRestriction;
|
|
188
191
|
/** Why folder-scoped verbs are withheld, in the toolbar's own words. */
|
|
189
192
|
moveDisabledHint?: string;
|
|
190
193
|
}
|
|
@@ -214,6 +217,7 @@ export const resolveBriefSelectionScope = (
|
|
|
214
217
|
}
|
|
215
218
|
if (accountIds.size > 1) {
|
|
216
219
|
return {
|
|
220
|
+
restriction: "spansAccounts",
|
|
217
221
|
moveDisabledHint:
|
|
218
222
|
"Move only works within one account — clear selection or pick messages from a single account",
|
|
219
223
|
};
|
|
@@ -222,6 +226,7 @@ export const resolveBriefSelectionScope = (
|
|
|
222
226
|
if (mailboxIds.size > 1) {
|
|
223
227
|
return {
|
|
224
228
|
accountId,
|
|
229
|
+
restriction: "spansFolders",
|
|
225
230
|
moveDisabledHint: `Move only works within one folder — this selection spans ${mailboxIds.size} folders`,
|
|
226
231
|
};
|
|
227
232
|
}
|
|
@@ -362,7 +367,7 @@ function BriefSelectionChrome({
|
|
|
362
367
|
accountId={scope.accountId}
|
|
363
368
|
mailboxId={scope.mailboxId}
|
|
364
369
|
selection={wizardSelection}
|
|
365
|
-
|
|
370
|
+
selectionRestriction={scope.restriction}
|
|
366
371
|
onFinished={exitSelection}
|
|
367
372
|
/>
|
|
368
373
|
}
|
|
@@ -1351,7 +1351,7 @@ export const MessageList = ({
|
|
|
1351
1351
|
accountId={accountId}
|
|
1352
1352
|
mailboxId={mailboxId}
|
|
1353
1353
|
selection={wizardSelection}
|
|
1354
|
-
|
|
1354
|
+
selectionRestriction={moveDisabledHint ? "spansAccounts" : undefined}
|
|
1355
1355
|
escalated={escalatedSelection}
|
|
1356
1356
|
escalatedProgress={escalation.progress}
|
|
1357
1357
|
onFinished={exitSelection}
|
|
@@ -2,8 +2,6 @@ import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@t
|
|
|
2
2
|
import {
|
|
3
3
|
type ClauseDraft,
|
|
4
4
|
type ClauseEditState,
|
|
5
|
-
crossAccountDestinationReason,
|
|
6
|
-
crossAccountRuleReason,
|
|
7
5
|
derivePropertyClauses,
|
|
8
6
|
deriveSenderClauses,
|
|
9
7
|
dominantSender,
|
|
@@ -14,6 +12,7 @@ import {
|
|
|
14
12
|
type RuleClause,
|
|
15
13
|
type RunState,
|
|
16
14
|
type SearchConversion,
|
|
15
|
+
type SelectionRestriction,
|
|
17
16
|
SelectionWizard,
|
|
18
17
|
type StepId,
|
|
19
18
|
searchConversionNotice,
|
|
@@ -25,6 +24,7 @@ import {
|
|
|
25
24
|
type Verb,
|
|
26
25
|
type WizardDraft,
|
|
27
26
|
type WizardMessage,
|
|
27
|
+
wizardScopeFor,
|
|
28
28
|
} from "@remit/ui";
|
|
29
29
|
import { useQuery } from "@tanstack/react-query";
|
|
30
30
|
import { useBlocker } from "@tanstack/react-router";
|
|
@@ -125,8 +125,12 @@ export interface SelectionWizardHostProps {
|
|
|
125
125
|
/** Where the selection was made, so a run invalidates the listing it changed. */
|
|
126
126
|
mailboxId?: string;
|
|
127
127
|
selection: readonly WizardSelectionMessage[];
|
|
128
|
-
/**
|
|
129
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Which scope the ticked rows span more of than the folder and rule steps can
|
|
130
|
+
* take. The surface that resolved the selection knows which of the two it is,
|
|
131
|
+
* and the wizard states that one (#525).
|
|
132
|
+
*/
|
|
133
|
+
selectionRestriction?: SelectionRestriction;
|
|
130
134
|
/** Present when the selection is a predicate rather than the ticked rows. */
|
|
131
135
|
escalated?: EscalatedSelection;
|
|
132
136
|
/**
|
|
@@ -257,7 +261,7 @@ function SelectionWizardSession({
|
|
|
257
261
|
accountId,
|
|
258
262
|
mailboxId,
|
|
259
263
|
selection,
|
|
260
|
-
|
|
264
|
+
selectionRestriction,
|
|
261
265
|
escalated: escalatedSelection,
|
|
262
266
|
escalatedProgress,
|
|
263
267
|
searchConversion,
|
|
@@ -451,18 +455,16 @@ function SelectionWizardSession({
|
|
|
451
455
|
|
|
452
456
|
// A filter, a folder, and the preview that counts a widened door all belong to
|
|
453
457
|
// one account, so a selection spanning accounts reaches none of them and is
|
|
454
|
-
// told so on the step that asks (#477 5.5, #523).
|
|
455
|
-
//
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
? undefined
|
|
460
|
-
: crossAccountDestinationReason;
|
|
458
|
+
// told so on the step that asks (#477 5.5, #523). A selection spanning folders
|
|
459
|
+
// of one account is told about folders instead, and keeps its account: the
|
|
460
|
+
// preview behind a widened door has something to count against. The one-off
|
|
461
|
+
// scope and the ticked rows act on the messages themselves and are unaffected.
|
|
462
|
+
const wizardScope = wizardScopeFor(accountId, selectionRestriction);
|
|
461
463
|
const restrictionFor = (step: StepId): string | undefined => {
|
|
462
|
-
if (step === "folder") return
|
|
464
|
+
if (step === "folder") return wizardScope.destination;
|
|
463
465
|
if (step !== "rule") return undefined;
|
|
464
466
|
return named.scope === "standing" || named.scope === "until"
|
|
465
|
-
?
|
|
467
|
+
? wizardScope.rule
|
|
466
468
|
: undefined;
|
|
467
469
|
};
|
|
468
470
|
const blockedReason =
|
|
@@ -940,7 +942,7 @@ function SelectionWizardSession({
|
|
|
940
942
|
match={{
|
|
941
943
|
selectedCount: selection.length,
|
|
942
944
|
mode,
|
|
943
|
-
accountId:
|
|
945
|
+
accountId: wizardScope.accountId,
|
|
944
946
|
onModeChange: changeMode,
|
|
945
947
|
semanticUnavailable,
|
|
946
948
|
semanticErrorDetail:
|
|
@@ -977,13 +979,13 @@ function SelectionWizardSession({
|
|
|
977
979
|
onSelect: (moveMailboxId) =>
|
|
978
980
|
setDraft((held) => ({ ...held, moveMailboxId })),
|
|
979
981
|
onCreateFolder: accountId ? createFolderIn : undefined,
|
|
980
|
-
restriction:
|
|
982
|
+
restriction: wizardScope.destination,
|
|
981
983
|
}}
|
|
982
984
|
rule={{
|
|
983
985
|
draft: named,
|
|
984
986
|
onScopeChange: (scope) => setDraft((held) => ({ ...held, scope })),
|
|
985
987
|
onUntilChange: (until) => setDraft((held) => ({ ...held, until })),
|
|
986
|
-
restriction:
|
|
988
|
+
restriction: wizardScope.rule,
|
|
987
989
|
}}
|
|
988
990
|
name={{
|
|
989
991
|
name: named.name ?? "",
|
|
@@ -1056,7 +1058,7 @@ export function SelectionWizardHost(props: SelectionWizardHostProps) {
|
|
|
1056
1058
|
accounts,
|
|
1057
1059
|
),
|
|
1058
1060
|
selection: EMPTY_SELECTION,
|
|
1059
|
-
|
|
1061
|
+
selectionRestriction: undefined,
|
|
1060
1062
|
escalated: undefined,
|
|
1061
1063
|
searchConversion: conversion,
|
|
1062
1064
|
}
|