@remit/ui 0.0.121 → 0.0.122
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
|
@@ -3,7 +3,11 @@ import { describe, it } from "node:test";
|
|
|
3
3
|
import { createElement } from "react";
|
|
4
4
|
import { renderToString } from "react-dom/server";
|
|
5
5
|
import type { MatchCount, StepId, WizardDraft } from "../lib/wizard-steps.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
matchDoorsFor,
|
|
8
|
+
stepBlockedReason,
|
|
9
|
+
stepsFor,
|
|
10
|
+
} from "../lib/wizard-steps.js";
|
|
7
11
|
import {
|
|
8
12
|
type RuleClause,
|
|
9
13
|
UNCOUNTABLE_PREDICATE_REASON,
|
|
@@ -57,6 +61,7 @@ const sample = { messages, count: counted(2), label: "Your selection" };
|
|
|
57
61
|
const matchProps = {
|
|
58
62
|
selectedCount: 2,
|
|
59
63
|
mode: "selected" as const,
|
|
64
|
+
accountId: "acc-personal",
|
|
60
65
|
onModeChange: noop,
|
|
61
66
|
onSemanticFallback: noop,
|
|
62
67
|
sample,
|
|
@@ -291,6 +296,42 @@ describe("MatchStepBody", () => {
|
|
|
291
296
|
});
|
|
292
297
|
});
|
|
293
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Starred, and any other surface whose rows span accounts (#523). Both widened
|
|
301
|
+
* doors are counted through a preview one account answers, so offering them
|
|
302
|
+
* walks the user to a review waiting on a count nobody can take, with Back the
|
|
303
|
+
* only way on. They are withheld, and the step says why.
|
|
304
|
+
*/
|
|
305
|
+
describe("MatchStepBody with no single account", () => {
|
|
306
|
+
const spanning = { ...matchProps, accountId: undefined };
|
|
307
|
+
|
|
308
|
+
it("offers the ticked rows alone, and states the restriction", () => {
|
|
309
|
+
const html = text(renderToString(createElement(MatchStepBody, spanning)));
|
|
310
|
+
assert.match(html, /These 2 messages/);
|
|
311
|
+
assert.doesNotMatch(html, /Similar to these/);
|
|
312
|
+
assert.doesNotMatch(html, /Its properties/);
|
|
313
|
+
assert.match(html, /only works within one account/);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("leaves a review that commits rather than one pinned on the count", () => {
|
|
317
|
+
assert.deepEqual([...matchDoorsFor(undefined)], ["selected"]);
|
|
318
|
+
// The only door left is its own count, so nothing about the match is still
|
|
319
|
+
// settling by the time the review is reached.
|
|
320
|
+
const blockedReason = stepBlockedReason("review", draft(), counted(2));
|
|
321
|
+
assert.equal(blockedReason, undefined);
|
|
322
|
+
const html = text(
|
|
323
|
+
renderToString(
|
|
324
|
+
createElement(
|
|
325
|
+
SelectionWizard,
|
|
326
|
+
wizardProps({ step: "review", match: spanning, blockedReason }),
|
|
327
|
+
),
|
|
328
|
+
),
|
|
329
|
+
);
|
|
330
|
+
assert.doesNotMatch(html, /Counting matches/);
|
|
331
|
+
assert.match(html, /2 messages/);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
294
335
|
describe("PropertiesStepBody", () => {
|
|
295
336
|
it("renders the rule as the shipped chips, with the join between them", () => {
|
|
296
337
|
const html = renderToString(
|
|
@@ -21,6 +21,7 @@ import { Fragment, type ReactNode, useEffect, useId, useRef } from "react";
|
|
|
21
21
|
import { cn } from "../lib/cn.js";
|
|
22
22
|
import {
|
|
23
23
|
backExits,
|
|
24
|
+
crossAccountMatchReason,
|
|
24
25
|
ESCALATED_MATCH_HINT,
|
|
25
26
|
ESCALATED_REVIEW_WARNING,
|
|
26
27
|
ESCALATED_SCOPE_FALLBACK,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
type MatchMode,
|
|
31
32
|
matchDoorHint,
|
|
32
33
|
matchDoorLabel,
|
|
34
|
+
matchDoorsFor,
|
|
33
35
|
matchPhrase,
|
|
34
36
|
matchSummary,
|
|
35
37
|
type RunCopy,
|
|
@@ -364,6 +366,13 @@ export function FooterNav({
|
|
|
364
366
|
export interface MatchStepProps {
|
|
365
367
|
selectedCount: number;
|
|
366
368
|
mode: MatchMode;
|
|
369
|
+
/**
|
|
370
|
+
* The one account this selection belongs to, absent when it spans several
|
|
371
|
+
* (#523). Both widened doors are counted through a preview that account
|
|
372
|
+
* answers, so without one they are withheld and the step states the
|
|
373
|
+
* restriction — the ticked rows are their own match and stay on offer.
|
|
374
|
+
*/
|
|
375
|
+
accountId?: string;
|
|
367
376
|
/**
|
|
368
377
|
* Answers the door. Typed to the three doors rather than to every mode, so
|
|
369
378
|
* no driver can set `escalated` from a screen — the list escalates a
|
|
@@ -396,6 +405,7 @@ export interface MatchStepProps {
|
|
|
396
405
|
export function MatchStepBody({
|
|
397
406
|
selectedCount,
|
|
398
407
|
mode,
|
|
408
|
+
accountId,
|
|
399
409
|
onModeChange,
|
|
400
410
|
semanticUnavailable,
|
|
401
411
|
semanticErrorDetail,
|
|
@@ -419,44 +429,55 @@ export function MatchStepBody({
|
|
|
419
429
|
</>
|
|
420
430
|
);
|
|
421
431
|
}
|
|
432
|
+
const doors = matchDoorsFor(accountId);
|
|
433
|
+
const widened = doors.length > 1;
|
|
422
434
|
return (
|
|
423
435
|
<>
|
|
424
436
|
<div className="space-y-2">
|
|
437
|
+
{!widened && (
|
|
438
|
+
<p className="px-1 text-2xs text-warning">
|
|
439
|
+
{crossAccountMatchReason}
|
|
440
|
+
</p>
|
|
441
|
+
)}
|
|
425
442
|
<ChoiceCard
|
|
426
443
|
selected={mode === "selected"}
|
|
427
444
|
onSelect={() => onModeChange("selected")}
|
|
428
445
|
title={matchDoorLabel("selected", selectedCount)}
|
|
429
446
|
description={matchDoorHint("selected")}
|
|
430
447
|
/>
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
448
|
+
{widened && (
|
|
449
|
+
<ChoiceCard
|
|
450
|
+
selected={mode === "similar"}
|
|
451
|
+
unavailable={semanticUnavailable}
|
|
452
|
+
onSelect={
|
|
453
|
+
semanticUnavailable
|
|
454
|
+
? onSemanticFallback
|
|
455
|
+
: () => onModeChange("similar")
|
|
456
|
+
}
|
|
457
|
+
title={matchDoorLabel("similar", selectedCount)}
|
|
458
|
+
description={matchDoorHint("similar")}
|
|
459
|
+
>
|
|
460
|
+
{semanticErrorDetail && (
|
|
461
|
+
<p role="status" className="px-1 text-2xs text-danger">
|
|
462
|
+
Couldn't find similar messages: {semanticErrorDetail}
|
|
463
|
+
</p>
|
|
464
|
+
)}
|
|
465
|
+
{semanticFallbackTaken && (
|
|
466
|
+
<p role="status" className="px-1 text-2xs text-fg-subtle">
|
|
467
|
+
Similar-mail matching is unavailable right now — matching on the
|
|
468
|
+
senders instead.
|
|
469
|
+
</p>
|
|
470
|
+
)}
|
|
471
|
+
</ChoiceCard>
|
|
472
|
+
)}
|
|
473
|
+
{widened && (
|
|
474
|
+
<ChoiceCard
|
|
475
|
+
selected={mode === "properties"}
|
|
476
|
+
onSelect={() => onModeChange("properties")}
|
|
477
|
+
title={matchDoorLabel("properties", selectedCount)}
|
|
478
|
+
description={matchDoorHint("properties")}
|
|
479
|
+
/>
|
|
480
|
+
)}
|
|
460
481
|
</div>
|
|
461
482
|
<div className="mt-4">
|
|
462
483
|
<SelectionSample {...sample} />
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
backExits,
|
|
10
10
|
clauseSentence,
|
|
11
11
|
clauseWords,
|
|
12
|
+
crossAccountMatchReason,
|
|
12
13
|
ESCALATED_MATCH_HINT,
|
|
13
14
|
ESCALATED_REVIEW_WARNING,
|
|
14
15
|
escalatedMatchLabel,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
type MatchMode,
|
|
17
18
|
matchDoorHint,
|
|
18
19
|
matchDoorLabel,
|
|
20
|
+
matchDoorsFor,
|
|
19
21
|
matchPhrase,
|
|
20
22
|
matchSummary,
|
|
21
23
|
type RunState,
|
|
@@ -683,6 +685,15 @@ describe("sample and door vocabulary", () => {
|
|
|
683
685
|
assert.match(ESCALATED_REVIEW_WARNING, /by the time it runs/);
|
|
684
686
|
});
|
|
685
687
|
|
|
688
|
+
it("withholds the widened doors where there is no single account", () => {
|
|
689
|
+
assert.deepEqual(
|
|
690
|
+
[...matchDoorsFor("acc-personal")],
|
|
691
|
+
["selected", "similar", "properties"],
|
|
692
|
+
);
|
|
693
|
+
assert.deepEqual([...matchDoorsFor(undefined)], ["selected"]);
|
|
694
|
+
assert.match(crossAccountMatchReason, /within one account/);
|
|
695
|
+
});
|
|
696
|
+
|
|
686
697
|
it("names each door and what it does", () => {
|
|
687
698
|
assert.equal(matchDoorLabel("selected", 3), "These 3 messages");
|
|
688
699
|
assert.equal(matchDoorLabel("similar", 3), "Similar to these 3");
|
package/src/lib/wizard-steps.ts
CHANGED
|
@@ -223,6 +223,27 @@ export const crossAccountRuleReason =
|
|
|
223
223
|
export const crossAccountDestinationReason =
|
|
224
224
|
"A destination only works within one account — clear the selection, or pick messages from a single account.";
|
|
225
225
|
|
|
226
|
+
/**
|
|
227
|
+
* The same restriction on the step that asks what the action applies to (#523).
|
|
228
|
+
* Both widened doors are resolved by a preview one account answers, so a
|
|
229
|
+
* selection with no single account has nothing to count them against. The ticked
|
|
230
|
+
* rows are their own match and are unaffected.
|
|
231
|
+
*/
|
|
232
|
+
export const crossAccountMatchReason =
|
|
233
|
+
"Matching beyond the messages you picked only works within one account — clear the selection, or pick messages from a single account.";
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The doors the match step offers. Withholding the two widened ones is what
|
|
237
|
+
* keeps a selection spanning accounts off a review that waits on a count nobody
|
|
238
|
+
* can take; the step says why in `crossAccountMatchReason`.
|
|
239
|
+
*/
|
|
240
|
+
export const matchDoorsFor = (
|
|
241
|
+
accountId: string | undefined,
|
|
242
|
+
): readonly MatchDoor[] =>
|
|
243
|
+
accountId === undefined
|
|
244
|
+
? ["selected"]
|
|
245
|
+
: ["selected", "similar", "properties"];
|
|
246
|
+
|
|
226
247
|
/**
|
|
227
248
|
* The match reached nothing, so committing it would report a bulk action that
|
|
228
249
|
* touched no mail. Reachable both from a predicate that matches nothing and from
|