@remit/ui 0.0.124 → 0.0.125

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/ui",
3
- "version": "0.0.124",
3
+ "version": "0.0.125",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -7,6 +7,7 @@ import {
7
7
  matchDoorsFor,
8
8
  stepBlockedReason,
9
9
  stepsFor,
10
+ wizardScopeFor,
10
11
  } from "../lib/wizard-steps.js";
11
12
  import {
12
13
  type RuleClause,
@@ -413,6 +414,27 @@ describe("FolderStepBody", () => {
413
414
  );
414
415
  assert.match(text(html), /Tap a folder to open it/);
415
416
  });
417
+
418
+ it("states the restriction the selection actually carries (#525)", () => {
419
+ // Told to pick a single account, a selection already inside one account has
420
+ // nothing to act on and the flow dead-ends here.
421
+ const spansFolders = renderToString(
422
+ createElement(FolderStepBody, {
423
+ ...folderProps,
424
+ restriction: wizardScopeFor("acc-personal", "spansFolders").destination,
425
+ }),
426
+ );
427
+ assert.match(text(spansFolders), /within one folder/);
428
+ assert.doesNotMatch(text(spansFolders), /single account/);
429
+
430
+ const spansAccounts = renderToString(
431
+ createElement(FolderStepBody, {
432
+ ...folderProps,
433
+ restriction: wizardScopeFor(undefined, "spansAccounts").destination,
434
+ }),
435
+ );
436
+ assert.match(text(spansAccounts), /single account/);
437
+ });
416
438
  });
417
439
 
418
440
  describe("RuleStepBody", () => {
@@ -636,9 +636,10 @@ export interface FolderStepProps {
636
636
  /** The account's hierarchy separator, which the tree nests on. */
637
637
  delimiter?: string;
638
638
  /**
639
- * Why there is no folder list to choose from a selection spanning accounts
640
- * has no single account whose folders these could be (#477 5.5). Said here
641
- * rather than left as an empty picker.
639
+ * Why no destination can be chosenthe selection spans more accounts, or
640
+ * more folders, than a move can take (#477 5.5, #525). In the words of the
641
+ * scope it actually spans, so the instruction is one the user can follow. Said
642
+ * here rather than left as an empty picker.
642
643
  */
643
644
  restriction?: string;
644
645
  }
@@ -692,9 +693,10 @@ export interface RuleStepProps {
692
693
  onScopeChange: (scope: RuleScope) => void;
693
694
  onUntilChange: (until: string) => void;
694
695
  /**
695
- * Why the two persisting scopes cannot be reached from here — a selection
696
- * spanning accounts has no single account to create the rule for (#477 5.5).
697
- * They stay pressable and say this; the one-off scope is unaffected.
696
+ * Why the two persisting scopes cannot be reached from here — the selection
697
+ * spans more accounts, or more folders, than a rule can take (#477 5.5, #525),
698
+ * in the words of the scope it actually spans. They stay pressable and say
699
+ * this; the one-off scope is unaffected.
698
700
  */
699
701
  restriction?: string;
700
702
  }
package/src/index.ts CHANGED
@@ -989,6 +989,8 @@ export {
989
989
  clauseWords,
990
990
  crossAccountDestinationReason,
991
991
  crossAccountRuleReason,
992
+ crossFolderDestinationReason,
993
+ crossFolderRuleReason,
992
994
  ESCALATED_MATCH_HINT,
993
995
  ESCALATED_REVIEW_WARNING,
994
996
  ESCALATED_SCOPE_FALLBACK,
@@ -1006,6 +1008,7 @@ export {
1006
1008
  type RunState,
1007
1009
  runCopy,
1008
1010
  type SampleEmptyReason,
1011
+ type SelectionRestriction,
1009
1012
  type StepId,
1010
1013
  sampleEmptyCopy,
1011
1014
  stepBlockedReason,
@@ -1018,4 +1021,6 @@ export {
1018
1021
  verbCopy,
1019
1022
  type WizardAnswers,
1020
1023
  type WizardDraft,
1024
+ type WizardScope,
1025
+ wizardScopeFor,
1021
1026
  } from "./lib/wizard-steps.js";
@@ -10,6 +10,8 @@ import {
10
10
  clauseSentence,
11
11
  clauseWords,
12
12
  crossAccountMatchReason,
13
+ crossFolderDestinationReason,
14
+ crossFolderRuleReason,
13
15
  ESCALATED_MATCH_HINT,
14
16
  ESCALATED_REVIEW_WARNING,
15
17
  escalatedMatchLabel,
@@ -32,6 +34,7 @@ import {
32
34
  type Verb,
33
35
  verbCopy,
34
36
  type WizardDraft,
37
+ wizardScopeFor,
35
38
  } from "./wizard-steps.js";
36
39
 
37
40
  const VERBS: Verb[] = ["delete", "move", "junk", "markRead", "organize"];
@@ -858,3 +861,50 @@ describe("the match as words", () => {
858
861
  );
859
862
  });
860
863
  });
864
+
865
+ /**
866
+ * A selection spanning folders of one account was told to pick a single account
867
+ * (#525) — an instruction a user holding one account's mail cannot follow, so
868
+ * the flow dead-ended on the folder step with nowhere to go.
869
+ */
870
+ describe("the restriction a selection walks the wizard with", () => {
871
+ it("tells a selection spanning folders about folders, not accounts", () => {
872
+ const scope = wizardScopeFor("acc-personal", "spansFolders");
873
+ assert.equal(scope.destination, crossFolderDestinationReason);
874
+ assert.equal(scope.rule, crossFolderRuleReason);
875
+ assert.match(scope.destination ?? "", /within one folder/);
876
+ assert.doesNotMatch(scope.destination ?? "", /account/);
877
+ assert.doesNotMatch(scope.rule ?? "", /account/);
878
+ });
879
+
880
+ it("keeps the account a folder-spanning selection has", () => {
881
+ // The widened doors are counted through a preview that account answers, so
882
+ // there is nothing to withhold them for.
883
+ const scope = wizardScopeFor("acc-personal", "spansFolders");
884
+ assert.equal(scope.accountId, "acc-personal");
885
+ assert.deepEqual(
886
+ [...matchDoorsFor(scope.accountId)],
887
+ ["selected", "similar", "properties"],
888
+ );
889
+ });
890
+
891
+ it("tells a selection spanning accounts about accounts", () => {
892
+ const scope = wizardScopeFor(undefined, "spansAccounts");
893
+ assert.equal(scope.accountId, undefined);
894
+ assert.match(scope.destination ?? "", /within one account/);
895
+ assert.match(scope.rule ?? "", /within one account/);
896
+ assert.deepEqual([...matchDoorsFor(scope.accountId)], ["selected"]);
897
+ });
898
+
899
+ it("restricts a selection with no account whatever it was handed", () => {
900
+ const scope = wizardScopeFor(undefined, undefined);
901
+ assert.match(scope.destination ?? "", /within one account/);
902
+ assert.match(scope.rule ?? "", /within one account/);
903
+ });
904
+
905
+ it("restricts nothing when one account and one folder answer for it", () => {
906
+ assert.deepEqual(wizardScopeFor("acc-personal", undefined), {
907
+ accountId: "acc-personal",
908
+ });
909
+ });
910
+ });
@@ -232,6 +232,74 @@ export const crossAccountDestinationReason =
232
232
  export const crossAccountMatchReason =
233
233
  "Matching beyond the messages you picked only works within one account — clear the selection, or pick messages from a single account.";
234
234
 
235
+ /**
236
+ * Why a selection spanning folders of one account cannot become a rule (#525).
237
+ * The account is settled and the source folder is not, so the restriction the
238
+ * wizard states is the folder one — telling a single-account selection to pick
239
+ * one account is an instruction it cannot follow.
240
+ */
241
+ export const crossFolderRuleReason =
242
+ "A rule only works within one folder — clear the selection, or pick messages from a single folder.";
243
+
244
+ /** The same restriction on the step that asks for a folder. */
245
+ export const crossFolderDestinationReason =
246
+ "A destination only works within one folder — clear the selection, or pick messages from a single folder.";
247
+
248
+ /**
249
+ * Which scope a selection spans more of than the wizard's folder-scoped steps
250
+ * can take. The surface that resolved the selection knows which one it is, so it
251
+ * hands the case over rather than a flag both cases collapse into.
252
+ */
253
+ export type SelectionRestriction = "spansAccounts" | "spansFolders";
254
+
255
+ const RESTRICTION_COPY: Record<
256
+ SelectionRestriction,
257
+ { rule: string; destination: string }
258
+ > = {
259
+ spansAccounts: {
260
+ rule: crossAccountRuleReason,
261
+ destination: crossAccountDestinationReason,
262
+ },
263
+ spansFolders: {
264
+ rule: crossFolderRuleReason,
265
+ destination: crossFolderDestinationReason,
266
+ },
267
+ };
268
+
269
+ /** What a restricted selection leaves the wizard's account-scoped steps to work with. */
270
+ export interface WizardScope {
271
+ /**
272
+ * The account a filter, a folder create and a widened door's preview all
273
+ * belong to. Absent only when the selection has no single account — a
274
+ * selection spanning folders of one account keeps it, so its widened doors
275
+ * have something to count against.
276
+ */
277
+ accountId?: string;
278
+ /** What the rule step states, when the selection cannot reach a saved scope. */
279
+ rule?: string;
280
+ /** What the folder step states, when the selection cannot reach a destination. */
281
+ destination?: string;
282
+ }
283
+
284
+ /**
285
+ * The scope a selection walks the wizard with. A selection with no single
286
+ * account is account-restricted whatever it was handed, since the account is
287
+ * what the filter, the folder create and the preview all hang off.
288
+ */
289
+ export const wizardScopeFor = (
290
+ accountId: string | undefined,
291
+ restriction: SelectionRestriction | undefined,
292
+ ): WizardScope => {
293
+ const applied = accountId === undefined ? "spansAccounts" : restriction;
294
+ if (!applied) return { accountId };
295
+ const copy = RESTRICTION_COPY[applied];
296
+ return {
297
+ accountId: applied === "spansAccounts" ? undefined : accountId,
298
+ rule: copy.rule,
299
+ destination: copy.destination,
300
+ };
301
+ };
302
+
235
303
  /**
236
304
  * The doors the match step offers. Withholding the two widened ones is what
237
305
  * keeps a selection spanning accounts off a review that waits on a count nobody