@remit/web-client 0.0.65 → 0.0.67

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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/src/components/mail/MessageList.tsx +0 -2
  3. package/src/components/mail/organize/MobileOrganizeFlow.render.test.ts +0 -1
  4. package/src/components/mail/organize/MobileOrganizeFlow.tsx +14 -10
  5. package/src/components/mail/organize/OrganizeDialog.render.test.ts +0 -1
  6. package/src/components/mail/organize/OrganizeDialog.tsx +7 -12
  7. package/src/components/mail/organize/OrganizeRuleEditor.render.test.ts +358 -0
  8. package/src/components/mail/organize/OrganizeRuleEditor.tsx +361 -0
  9. package/src/components/mail/organize/smart-organize.stories.tsx +39 -119
  10. package/src/components/settings/FilterEditor.render.test.ts +281 -0
  11. package/src/components/settings/FilterEditor.tsx +338 -0
  12. package/src/components/settings/FilterEditorSurface.tsx +52 -0
  13. package/src/components/settings/FiltersList.render.test.ts +27 -1
  14. package/src/components/settings/FiltersList.tsx +33 -5
  15. package/src/components/settings/settings-filter.stories.tsx +165 -0
  16. package/src/hooks/useRulePreview.ts +91 -0
  17. package/src/hooks/useUpdateFilter.ts +56 -0
  18. package/src/lib/organize/filter-edit-model.test.ts +175 -0
  19. package/src/lib/organize/filter-edit-model.ts +106 -0
  20. package/src/lib/organize/rule-model.test.ts +297 -0
  21. package/src/lib/organize/rule-model.ts +205 -0
  22. package/src/routes/settings/filters.tsx +26 -1
  23. package/src/components/mail/organize/OrganizePanel.progress.test.ts +0 -113
  24. package/src/components/mail/organize/OrganizePanel.render.test.ts +0 -170
  25. package/src/components/mail/organize/OrganizePanel.tsx +0 -420
  26. package/src/lib/organize/organize-copy.test.ts +0 -142
  27. package/src/lib/organize/organize-copy.ts +0 -95
@@ -1,142 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import {
4
- commitButtonLabel,
5
- commitDisabledReason,
6
- formatSenderList,
7
- scopeActionCount,
8
- senderFallbackSummary,
9
- } from "./organize-copy";
10
- import type { OrganizeDraft } from "./organize-model";
11
-
12
- const draft = (overrides: Partial<OrganizeDraft> = {}): OrganizeDraft => ({
13
- matchOperator: "And",
14
- literalClauses: [],
15
- ...overrides,
16
- });
17
-
18
- describe("commitDisabledReason", () => {
19
- it("blocks with a labeling explanation when no folder is chosen", () => {
20
- const reason = commitDisabledReason({
21
- draft: draft(),
22
- scope: "just-these",
23
- name: "",
24
- pickedDate: "",
25
- });
26
- assert.match(reason ?? "", /Pick a folder/);
27
- });
28
-
29
- it("requires a name for a standing filter", () => {
30
- const reason = commitDisabledReason({
31
- draft: draft({ moveMailboxId: "mbx-1" }),
32
- scope: "standing",
33
- name: " ",
34
- pickedDate: "",
35
- });
36
- assert.match(reason ?? "", /Name this filter/);
37
- });
38
-
39
- it("requires a date for a temporary filter", () => {
40
- const reason = commitDisabledReason({
41
- draft: draft({ moveMailboxId: "mbx-1" }),
42
- scope: "temporary",
43
- name: "Trip",
44
- pickedDate: "",
45
- });
46
- assert.match(reason ?? "", /Pick the date/);
47
- });
48
-
49
- it("is undefined when a move target is set for a one-time scope", () => {
50
- assert.equal(
51
- commitDisabledReason({
52
- draft: draft({ moveMailboxId: "mbx-1" }),
53
- scope: "all-like-these",
54
- name: "",
55
- pickedDate: "",
56
- }),
57
- undefined,
58
- );
59
- });
60
-
61
- it("is undefined for a fully-specified temporary filter", () => {
62
- assert.equal(
63
- commitDisabledReason({
64
- draft: draft({ moveMailboxId: "mbx-1" }),
65
- scope: "temporary",
66
- name: "Trip",
67
- pickedDate: "2026-07-16",
68
- }),
69
- undefined,
70
- );
71
- });
72
- });
73
-
74
- describe("commitButtonLabel", () => {
75
- it("pluralizes the one-time labels", () => {
76
- assert.equal(commitButtonLabel("just-these", 1), "Move 1 message");
77
- assert.equal(
78
- commitButtonLabel("all-like-these", 48),
79
- "Organize 48 messages",
80
- );
81
- });
82
-
83
- it("uses standing copy for the persisted scopes", () => {
84
- assert.equal(commitButtonLabel("standing", 48), "Always do this");
85
- assert.equal(commitButtonLabel("temporary", 48), "Do this until then");
86
- });
87
- });
88
-
89
- describe("scopeActionCount", () => {
90
- it("uses the selection for just-these and the match set otherwise", () => {
91
- assert.equal(scopeActionCount("just-these", 3, 48), 3);
92
- assert.equal(scopeActionCount("all-like-these", 3, 48), 48);
93
- assert.equal(scopeActionCount("standing", 3, 48), 48);
94
- });
95
- });
96
-
97
- describe("formatSenderList", () => {
98
- it("names a single sender bare", () => {
99
- assert.equal(formatSenderList(["npm@github.com"]), "npm@github.com");
100
- });
101
-
102
- it("joins a couple with 'and'", () => {
103
- assert.equal(
104
- formatSenderList(["a@x.com", "b@y.com"]),
105
- "a@x.com and b@y.com",
106
- );
107
- });
108
-
109
- it("lists up to three in full", () => {
110
- assert.equal(
111
- formatSenderList(["a@x.com", "b@y.com", "c@z.com"]),
112
- "a@x.com, b@y.com and c@z.com",
113
- );
114
- });
115
-
116
- it("truncates past three, summing the rest", () => {
117
- assert.equal(
118
- formatSenderList(["a@x.com", "b@y.com", "c@z.com", "d@w.com"]),
119
- "a@x.com, b@y.com, c@z.com and 1 other",
120
- );
121
- assert.equal(
122
- formatSenderList(["a@x.com", "b@y.com", "c@z.com", "d@w.com", "e@v.com"]),
123
- "a@x.com, b@y.com, c@z.com and 2 others",
124
- );
125
- });
126
- });
127
-
128
- describe("senderFallbackSummary", () => {
129
- it("states it is matching all mail from the senders, never 'similar'", () => {
130
- const summary = senderFallbackSummary(["npm@github.com"]);
131
- assert.match(summary, /isn't available on this server/);
132
- assert.match(summary, /matching all mail from npm@github\.com instead/);
133
- });
134
-
135
- it("names the shared domain when the senders collapse to one FromDomain", () => {
136
- const summary = senderFallbackSummary(["npm@github.com", "ci@github.com"]);
137
- assert.match(
138
- summary,
139
- /matching all mail from anyone at github\.com instead/,
140
- );
141
- });
142
- });
@@ -1,95 +0,0 @@
1
- import type { OrganizeScope } from "./organize-model";
2
- import { hasCommittableAction, type OrganizeDraft } from "./organize-model";
3
- import { collapsibleDomain } from "./sender-fallback";
4
-
5
- export interface CommitContext {
6
- draft: OrganizeDraft;
7
- scope: OrganizeScope;
8
- /** The filter name, required for the two standing scopes. */
9
- name: string;
10
- /** The raw picked date (`YYYY-MM-DD`), required for the temporary scope. */
11
- pickedDate: string;
12
- }
13
-
14
- /**
15
- * Why the commit button is disabled, or `undefined` when it is actionable.
16
- * Never disable a control without saying why (ux.md), so the caller renders
17
- * this string next to the button.
18
- */
19
- export const commitDisabledReason = ({
20
- draft,
21
- scope,
22
- name,
23
- pickedDate,
24
- }: CommitContext): string | undefined => {
25
- if (!hasCommittableAction(draft)) {
26
- return "Pick a folder to move these into — labeling isn't available yet.";
27
- }
28
- if ((scope === "standing" || scope === "temporary") && name.trim() === "") {
29
- return "Name this filter so you can find it later.";
30
- }
31
- if (scope === "temporary" && pickedDate === "") {
32
- return "Pick the date this should stop on.";
33
- }
34
- return undefined;
35
- };
36
-
37
- /** The commit button label for each scope. */
38
- export const commitButtonLabel = (
39
- scope: OrganizeScope,
40
- matchedTotal: number,
41
- ): string => {
42
- switch (scope) {
43
- case "just-these":
44
- return `Move ${matchedTotal} message${matchedTotal === 1 ? "" : "s"}`;
45
- case "all-like-these":
46
- return `Organize ${matchedTotal} message${matchedTotal === 1 ? "" : "s"}`;
47
- case "standing":
48
- return "Always do this";
49
- case "temporary":
50
- return "Do this until then";
51
- }
52
- };
53
-
54
- /**
55
- * The count a scope acts on: the current selection for "just these", the
56
- * widened match set for every scope that reaches similar mail.
57
- */
58
- export const scopeActionCount = (
59
- scope: OrganizeScope,
60
- selectionCount: number,
61
- matchedCount: number,
62
- ): number => (scope === "just-these" ? selectionCount : matchedCount);
63
-
64
- /** How many sender addresses are named before the rest are summed as "N others". */
65
- const MAX_SENDERS_SHOWN = 3;
66
-
67
- /**
68
- * The sender addresses read out in prose: all of them when there are a few, or
69
- * the first {@link MAX_SENDERS_SHOWN} and a count of the rest so a long selection
70
- * stays legible in the sheet.
71
- */
72
- export const formatSenderList = (senders: readonly string[]): string => {
73
- if (senders.length === 0) return "these senders";
74
- if (senders.length === 1) return senders[0];
75
- if (senders.length <= MAX_SENDERS_SHOWN) {
76
- return `${senders.slice(0, -1).join(", ")} and ${senders[senders.length - 1]}`;
77
- }
78
- const shown = senders.slice(0, MAX_SENDERS_SHOWN);
79
- const rest = senders.length - MAX_SENDERS_SHOWN;
80
- return `${shown.join(", ")} and ${rest} other${rest === 1 ? "" : "s"}`;
81
- };
82
-
83
- /**
84
- * The heading when the widen fell back to sender matching (no vector pipeline on
85
- * this server). States the actual semantics — matching every mail from these
86
- * senders, or from anyone at their shared domain when the clauses collapsed to a
87
- * single `FromDomain` (RFC 038 D2) — and never claims semantic similarity.
88
- */
89
- export const senderFallbackSummary = (senders: readonly string[]): string => {
90
- const domain = collapsibleDomain(senders);
91
- const target = domain
92
- ? `anyone at ${domain}`
93
- : `${formatSenderList(senders)}`;
94
- return `Similar-mail matching isn't available on this server — matching all mail from ${target} instead.`;
95
- };