@remit/ui 0.0.58 → 0.0.59

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/src/index.ts CHANGED
@@ -167,10 +167,12 @@ export {
167
167
  matchOperatorLabel,
168
168
  type PreviewCount,
169
169
  previewCountSummary,
170
+ previewSettledReason,
170
171
  type RuleClause,
171
172
  type RuleMatchMode,
172
173
  type RuleScope,
173
174
  type RuleWiden,
175
+ ruleBlockedCopy,
174
176
  scopeLabel,
175
177
  unreadableBodyClauses,
176
178
  widenChipLabel,
@@ -483,6 +485,28 @@ export {
483
485
  type SelectionTopBarNoticeAction,
484
486
  type SelectionTopBarProps,
485
487
  } from "./components/selection-top-bar.js";
488
+ export {
489
+ FolderStepBody,
490
+ type FolderStepProps,
491
+ MatchStepBody,
492
+ type MatchStepProps,
493
+ NameStepBody,
494
+ type NameStepProps,
495
+ PropertiesStepBody,
496
+ type PropertiesStepProps,
497
+ ReviewStepBody,
498
+ type ReviewStepProps,
499
+ RuleStepBody,
500
+ type RuleStepProps,
501
+ RunFooter,
502
+ RunStepBody,
503
+ type RunStepProps,
504
+ SelectionSample,
505
+ type SelectionSampleProps,
506
+ SelectionWizard,
507
+ type SelectionWizardProps,
508
+ type WizardMessage,
509
+ } from "./components/selection-wizard.js";
486
510
  export {
487
511
  demoLogsCommand,
488
512
  demoRelease,
@@ -611,6 +635,7 @@ export {
611
635
  type UseRovingFocusOptions,
612
636
  useRovingFocus,
613
637
  } from "./lib/roving-focus.js";
638
+ export { type RuleNameParts, suggestRuleName } from "./lib/rule-name.js";
614
639
  export {
615
640
  buildSearchRule,
616
641
  type DroppedFacet,
@@ -622,7 +647,9 @@ export {
622
647
  collapsibleDomain,
623
648
  deriveSenderClauses,
624
649
  distinctSenders,
650
+ dominantSender,
625
651
  senderDomain,
652
+ senderLabel,
626
653
  } from "./lib/sender-derivation.js";
627
654
  export {
628
655
  type SuggestAction,
@@ -640,3 +667,32 @@ export {
640
667
  type UseSuggestListInput,
641
668
  useSuggestList,
642
669
  } from "./lib/use-suggest-list.js";
670
+ export {
671
+ backExits,
672
+ clauseSentence,
673
+ clauseWords,
674
+ type MatchCount,
675
+ type MatchDescription,
676
+ type MatchMode,
677
+ matchDoorHint,
678
+ matchDoorLabel,
679
+ matchPhrase,
680
+ matchSummary,
681
+ type RunCopy,
682
+ type RunOutcome,
683
+ type RunState,
684
+ runCopy,
685
+ type SampleEmptyReason,
686
+ type StepId,
687
+ sampleEmptyCopy,
688
+ stepBlockedReason,
689
+ stepIndex,
690
+ stepLabel,
691
+ stepsFor,
692
+ unreadableDraftClauses,
693
+ type Verb,
694
+ type VerbCopy,
695
+ verbCopy,
696
+ type WizardAnswers,
697
+ type WizardDraft,
698
+ } from "./lib/wizard-steps.js";
@@ -0,0 +1,28 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { suggestRuleName } from "./rule-name.js";
4
+
5
+ describe("suggestRuleName", () => {
6
+ it("names the match and its destination when it knows both", () => {
7
+ assert.equal(
8
+ suggestRuleName({ match: "Your receipt", folder: "Receipts" }),
9
+ "Your receipt → Receipts",
10
+ );
11
+ });
12
+
13
+ it("falls back through match, then sender, then destination", () => {
14
+ assert.equal(
15
+ suggestRuleName({ match: "Your receipt" }),
16
+ "Mail matching Your receipt",
17
+ );
18
+ assert.equal(
19
+ suggestRuleName({ sender: "Booking.com" }),
20
+ "Mail from Booking.com",
21
+ );
22
+ assert.equal(suggestRuleName({ folder: "Travel" }), "Mail to Travel");
23
+ });
24
+
25
+ it("suggests nothing when it knows nothing", () => {
26
+ assert.equal(suggestRuleName({}), "");
27
+ });
28
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The name a rule is offered under before the user writes their own (#477 4.10).
3
+ * A suggestion and never a decision: the field it fills is editable and
4
+ * clearable, and a rule with no name is held by `ruleBlockedCopy.unnamed`, not
5
+ * by a guess.
6
+ */
7
+
8
+ export interface RuleNameParts {
9
+ /** What a property match is anchored on — the leading clause value. */
10
+ match?: string;
11
+ /** The sender carrying most of the selection, as it reads on screen. */
12
+ sender?: string;
13
+ /** The destination, once one has been chosen. */
14
+ folder?: string;
15
+ }
16
+
17
+ export const suggestRuleName = ({
18
+ match,
19
+ sender,
20
+ folder,
21
+ }: RuleNameParts): string => {
22
+ const subject = match ?? sender;
23
+ if (!subject) return folder ? `Mail to ${folder}` : "";
24
+ if (folder) return `${subject} → ${folder}`;
25
+ if (match) return `Mail matching ${match}`;
26
+ return `Mail from ${subject}`;
27
+ };
@@ -4,6 +4,8 @@ import {
4
4
  collapsibleDomain,
5
5
  deriveSenderClauses,
6
6
  distinctSenders,
7
+ dominantSender,
8
+ senderLabel,
7
9
  } from "./sender-derivation.js";
8
10
 
9
11
  describe("distinctSenders", () => {
@@ -104,3 +106,54 @@ describe("deriveSenderClauses", () => {
104
106
  ]);
105
107
  });
106
108
  });
109
+
110
+ describe("dominantSender", () => {
111
+ const envelope = (normalizedEmail: string, displayName?: string) => ({
112
+ normalizedEmail,
113
+ displayName,
114
+ });
115
+
116
+ it("takes the sender carrying most of the selection", () => {
117
+ assert.deepEqual(
118
+ dominantSender([
119
+ envelope("noreply@booking.com", "Booking.com"),
120
+ envelope("automated@airbnb.com", "Airbnb"),
121
+ envelope("noreply@booking.com", "Booking.com"),
122
+ ]),
123
+ envelope("noreply@booking.com", "Booking.com"),
124
+ );
125
+ });
126
+
127
+ it("counts one address writing under two names as one sender", () => {
128
+ assert.deepEqual(
129
+ dominantSender([
130
+ envelope("info@klm.com", "KLM"),
131
+ envelope("INFO@klm.com", "KLM Royal Dutch Airlines"),
132
+ envelope("noreply@sixt.com", "Sixt"),
133
+ ]),
134
+ envelope("info@klm.com", "KLM"),
135
+ );
136
+ });
137
+
138
+ it("has no answer for an empty selection", () => {
139
+ assert.equal(dominantSender([]), undefined);
140
+ assert.equal(dominantSender([envelope(" ")]), undefined);
141
+ });
142
+ });
143
+
144
+ describe("senderLabel", () => {
145
+ it("reads the display name, and the address when there is none", () => {
146
+ assert.equal(
147
+ senderLabel({ normalizedEmail: "a@b.example", displayName: "Ada" }),
148
+ "Ada",
149
+ );
150
+ assert.equal(
151
+ senderLabel({ normalizedEmail: "a@b.example", displayName: " " }),
152
+ "a@b.example",
153
+ );
154
+ assert.equal(
155
+ senderLabel({ normalizedEmail: "a@b.example" }),
156
+ "a@b.example",
157
+ );
158
+ });
159
+ });
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { getDomain } from "tldts";
13
+ import type { EnvelopeAddress } from "../components/address-display.js";
13
14
  import type { RuleClause } from "../components/filter-rule.js";
14
15
 
15
16
  /**
@@ -83,3 +84,37 @@ export const deriveSenderClauses = (
83
84
  (value): Omit<RuleClause, "id"> => ({ field: "From", value }),
84
85
  );
85
86
  };
87
+
88
+ /**
89
+ * The sender carrying most of a selection, or `undefined` when it carries none.
90
+ * Whole envelope addresses rather than bare strings: this reads the display name
91
+ * a person recognises, where the clause derivations above read the address a
92
+ * matcher compares, and the two are not interchangeable.
93
+ *
94
+ * Counted on the normalized address, so one sender writing under two display
95
+ * names is one sender; the answer is the first envelope seen for the winner.
96
+ */
97
+ export const dominantSender = (
98
+ senders: readonly EnvelopeAddress[],
99
+ ): EnvelopeAddress | undefined => {
100
+ const counts = new Map<string, { sender: EnvelopeAddress; count: number }>();
101
+ for (const sender of senders) {
102
+ const key = sender.normalizedEmail.trim().toLowerCase();
103
+ if (key === "") continue;
104
+ const seen = counts.get(key);
105
+ if (seen) seen.count += 1;
106
+ else counts.set(key, { sender, count: 1 });
107
+ }
108
+ let best: EnvelopeAddress | undefined;
109
+ let bestCount = 0;
110
+ for (const { sender, count } of counts.values()) {
111
+ if (count <= bestCount) continue;
112
+ best = sender;
113
+ bestCount = count;
114
+ }
115
+ return best;
116
+ };
117
+
118
+ /** What a sender is called on screen — the display name, or the address when it has none. */
119
+ export const senderLabel = (sender: EnvelopeAddress): string =>
120
+ sender.displayName?.trim() || sender.normalizedEmail;