@remit/web-client 0.0.321 → 0.0.323

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 (190) hide show
  1. package/package.json +4 -4
  2. package/src/auth/SignInAgain.tsx +18 -0
  3. package/src/auth/amplify-config.test.ts +1 -107
  4. package/src/auth/auth-interceptor.test.ts +0 -6
  5. package/src/auth/cognito/cognito-token.test.ts +2 -4
  6. package/src/components/calendar/AgendaStrip.tsx +1 -1
  7. package/src/components/calendar/CalendarWorkspace.tsx +1 -1
  8. package/src/components/calendar/ChangeCalendarSuggestion.tsx +1 -1
  9. package/src/components/calendar/OpenCalendarEvent.tsx +1 -18
  10. package/src/components/compose/ComposeForm.tsx +11 -6
  11. package/src/components/compose/FullCompose.tsx +1 -1
  12. package/src/components/compose/MobileComposeSheet.tsx +1 -2
  13. package/src/components/compose/compose.stories.tsx +18 -0
  14. package/src/components/compose/draft-discard-failure.render.test.ts +138 -0
  15. package/src/components/config-import/config-import.fixtures.ts +8 -9
  16. package/src/components/config-import/config-import.stories.tsx +2 -2
  17. package/src/components/layout/MailTopBar.tsx +3 -0
  18. package/src/components/mail/BriefPane.tsx +0 -3
  19. package/src/components/mail/ConversationView.tsx +1 -3
  20. package/src/components/mail/DailyBrief.tsx +5 -28
  21. package/src/components/mail/FlaggedList.tsx +3 -4
  22. package/src/components/mail/FlaggedPane.tsx +0 -3
  23. package/src/components/mail/IntelligenceDrawer.tsx +0 -3
  24. package/src/components/mail/IntelligencePane.render.test.ts +0 -1
  25. package/src/components/mail/IntelligencePane.test.ts +1 -95
  26. package/src/components/mail/IntelligencePane.tsx +3 -93
  27. package/src/components/mail/MailListHeader.tsx +12 -43
  28. package/src/components/mail/MailNav.tsx +6 -2
  29. package/src/components/mail/MailViewChrome.tsx +1 -11
  30. package/src/components/mail/MailboxPane.tsx +4 -34
  31. package/src/components/mail/MessageActionMenu.tsx +6 -6
  32. package/src/components/mail/MessageAttachments.tsx +35 -0
  33. package/src/components/mail/MessageBody.tsx +1 -0
  34. package/src/components/mail/MessageBodyErrorBanner.test.ts +0 -30
  35. package/src/components/mail/MessageCard.tsx +16 -4
  36. package/src/components/mail/MessageList.run-reporting.test.ts +2 -46
  37. package/src/components/mail/MessageList.tsx +23 -11
  38. package/src/components/mail/MessageRow.render.test.ts +0 -7
  39. package/src/components/mail/MoveToTrigger.tsx +1 -1
  40. package/src/components/mail/RawMessageView.tsx +1 -1
  41. package/src/components/mail/SelectionWizardHost.search-entry.test.ts +0 -1
  42. package/src/components/mail/SelectionWizardHost.tsx +16 -102
  43. package/src/components/mail/authenticity-warning.stories.tsx +0 -21
  44. package/src/components/mail/daily-brief.stories.tsx +31 -17
  45. package/src/components/mail/enter-cursor-late-open.render.test.ts +197 -0
  46. package/src/components/mail/intelligence-drawer.stories.tsx +0 -2
  47. package/src/components/mail/message-gone.stories.tsx +44 -0
  48. package/src/components/mail/offer-times-reply.render.test.ts +25 -1
  49. package/src/components/mail/reclassify-remove-override.render.test.ts +2 -6
  50. package/src/components/overlay-escape.render.test.ts +7 -3
  51. package/src/components/settings/AccountFormPanel.render.test.ts +26 -3
  52. package/src/components/settings/AccountFormPanel.test.ts +1 -36
  53. package/src/components/settings/AccountFormPanel.tsx +5 -25
  54. package/src/components/settings/AccountServices.tsx +1 -1
  55. package/src/components/settings/AdvancedNavIcon.tsx +3 -3
  56. package/src/components/settings/CalendarFeedCard.tsx +2 -1
  57. package/src/components/settings/CalendarSubscribeCard.tsx +1 -1
  58. package/src/components/settings/CalendarSubscriptionStatus.tsx +1 -2
  59. package/src/components/settings/FilterActionError.tsx +1 -1
  60. package/src/components/settings/FilterEditor.render.test.ts +3 -27
  61. package/src/components/settings/FilterEditor.tsx +4 -24
  62. package/src/components/settings/FilterEditorSurface.tsx +0 -3
  63. package/src/components/settings/FiltersList.render.test.ts +5 -24
  64. package/src/components/settings/FiltersList.stories.tsx +0 -1
  65. package/src/components/settings/FiltersList.tsx +10 -35
  66. package/src/components/settings/WarningsNavIcon.tsx +12 -0
  67. package/src/components/settings/{QuarantinePanel.delimiter.test.ts → WarningsPanel.dom.test.ts} +64 -5
  68. package/src/components/settings/WarningsPanel.render.test.ts +56 -0
  69. package/src/components/settings/WarningsPanel.tsx +244 -0
  70. package/src/components/settings/accounts.stories.tsx +18 -0
  71. package/src/components/settings/advanced.stories.tsx +3 -125
  72. package/src/components/settings/settings-filter.stories.tsx +2 -42
  73. package/src/components/settings/warnings.stories.tsx +290 -0
  74. package/src/components/ui/ErrorBannerProvider.tsx +5 -3
  75. package/src/components/ui/error-banners.test.ts +1 -22
  76. package/src/components/ui/error-banners.ts +9 -20
  77. package/src/hooks/calendar/useCalendarCollectionWrites.ts +2 -1
  78. package/src/hooks/calendar/useCalendarSuggestions.ts +2 -1
  79. package/src/hooks/calendar/useCalendarWrites.ts +2 -1
  80. package/src/hooks/intelligence-calendar.render.test.ts +0 -1
  81. package/src/hooks/useDeleteMessages.test.ts +1 -42
  82. package/src/hooks/useFilters.test.ts +0 -8
  83. package/src/hooks/useFollowFocusOpen.render.test.ts +1 -11
  84. package/src/hooks/useIntelligenceData.test.ts +9 -47
  85. package/src/hooks/useIntelligenceData.ts +7 -100
  86. package/src/hooks/useLayoutTier.test.ts +0 -48
  87. package/src/hooks/useReportSpam.ts +1 -1
  88. package/src/hooks/useRescueCandidates.test.ts +0 -23
  89. package/src/hooks/useRulePreview.ts +8 -7
  90. package/src/hooks/useSearchTokenContext.ts +2 -3
  91. package/src/hooks/useStaleAccountSync.test.ts +0 -17
  92. package/src/hooks/useStarredThreads.test.ts +0 -10
  93. package/src/hooks/useTriggerSync.test.ts +0 -6
  94. package/src/hooks/useVisualViewport.render.test.ts +63 -0
  95. package/src/hooks/useWarningsAttention.ts +37 -0
  96. package/src/lib/account-order.test.ts +0 -10
  97. package/src/lib/adjacent-message.test.ts +0 -9
  98. package/src/lib/app-info.ts +0 -3
  99. package/src/lib/autodiscovery.test.ts +0 -47
  100. package/src/lib/bug-report.test.ts +38 -0
  101. package/src/lib/bug-report.ts +10 -7
  102. package/src/lib/bulk-action-copy.ts +1 -1
  103. package/src/lib/calendar-route.test.ts +0 -5
  104. package/src/lib/config-import.test.ts +3 -39
  105. package/src/lib/dedupe-thread-messages.test.ts +0 -4
  106. package/src/lib/desktop-breakpoint.test.ts +0 -8
  107. package/src/lib/drafts.test.ts +0 -23
  108. package/src/lib/fatal-error.test.ts +0 -19
  109. package/src/lib/folder-roles.test.ts +0 -4
  110. package/src/lib/hot-sync-window.test.ts +0 -5
  111. package/src/lib/list-header-chrome.ts +1 -1
  112. package/src/lib/mail-context.ts +1 -1
  113. package/src/lib/message-attachments.test.ts +77 -0
  114. package/src/lib/message-attachments.ts +31 -15
  115. package/src/lib/message-body-source.test.ts +21 -4
  116. package/src/lib/message-body-source.ts +5 -2
  117. package/src/lib/onboarding-completion.test.ts +0 -16
  118. package/src/lib/organize/filter-edit-model.test.ts +2 -32
  119. package/src/lib/organize/filter-edit-model.ts +6 -16
  120. package/src/lib/organize/organize-model.test.ts +7 -40
  121. package/src/lib/organize/organize-model.ts +16 -18
  122. package/src/lib/organize/rule-model.test.ts +24 -202
  123. package/src/lib/organize/rule-model.ts +19 -166
  124. package/src/lib/organize/search-to-rule.test.ts +2 -36
  125. package/src/lib/organize/search-to-rule.ts +1 -16
  126. package/src/lib/provider-presets.test.ts +1 -77
  127. package/src/lib/query-error-handler.test.ts +0 -61
  128. package/src/lib/query-escalation.integration.test.ts +4 -4
  129. package/src/lib/result-count.ts +1 -1
  130. package/src/lib/result-folder.ts +1 -1
  131. package/src/lib/rum-adapter.test.ts +0 -104
  132. package/src/lib/search-query.test.ts +0 -11
  133. package/src/lib/search-result.test.ts +2 -142
  134. package/src/lib/search-result.ts +3 -73
  135. package/src/lib/search-scope.test.ts +1 -92
  136. package/src/lib/search-scope.ts +0 -29
  137. package/src/lib/search-surface.ts +2 -2
  138. package/src/lib/search-token-index.test.ts +0 -8
  139. package/src/lib/search-tokens.ts +15 -16
  140. package/src/lib/starred-rows.test.ts +0 -4
  141. package/src/lib/sync-warning-entries.ts +33 -0
  142. package/src/lib/wizard-selection.ts +1 -1
  143. package/src/mocks/story-frame/mail-handlers.ts +12 -8
  144. package/src/mocks/story-frame/mail-world.ts +5 -0
  145. package/src/routeTree.gen.ts +21 -0
  146. package/src/routes/calendar/$view.$date/index.tsx +1 -1
  147. package/src/routes/calendar.tsx +2 -1
  148. package/src/routes/mail/brief.tsx +1 -1
  149. package/src/routes/mail/flagged.tsx +1 -1
  150. package/src/routes/mail.tsx +5 -3
  151. package/src/routes/settings/-accounts.test.ts +0 -5
  152. package/src/routes/settings/-filters.test.ts +0 -1
  153. package/src/routes/settings/-shared/help-copy.tsx +15 -0
  154. package/src/routes/settings/accounts.tsx +1 -1
  155. package/src/routes/settings/advanced.tsx +1 -4
  156. package/src/routes/settings/filters.tsx +1 -2
  157. package/src/routes/settings/folders.tsx +1 -1
  158. package/src/routes/settings/labels.tsx +1 -1
  159. package/src/routes/settings/senders.tsx +1 -1
  160. package/src/routes/settings/warnings.tsx +36 -0
  161. package/src/routes/settings.tsx +7 -0
  162. package/src/routing/wizard-history.test.ts +1 -45
  163. package/src/runtime-config.ts +5 -0
  164. package/src/test-support/fixtures.ts +1 -1
  165. package/src/components/compose/draft-discard-banner.test.ts +0 -76
  166. package/src/components/mail/ReadingPaneEmpty.render.test.ts +0 -42
  167. package/src/components/mail/SpamRescue.render.test.ts +0 -35
  168. package/src/components/mail/intelligence-semantic-off.render.test.ts +0 -111
  169. package/src/components/mail/selection-surface-rollcall.test.ts +0 -270
  170. package/src/components/settings/QuarantinePanel.render.test.ts +0 -71
  171. package/src/components/settings/QuarantinePanel.tsx +0 -178
  172. package/src/components/ui/DropdownMenu.tsx +0 -99
  173. package/src/components/ui/EmptyState.tsx +0 -11
  174. package/src/components/ui/ErrorBanner.render.test.ts +0 -104
  175. package/src/components/ui/ErrorBanner.stories.tsx +0 -132
  176. package/src/components/ui/ErrorBanner.tsx +0 -106
  177. package/src/components/ui/ErrorBannerStack.tsx +0 -35
  178. package/src/components/ui/ErrorState.render.test.ts +0 -82
  179. package/src/components/ui/ErrorState.tsx +0 -80
  180. package/src/components/ui/KeyboardShortcutsModal.tsx +0 -113
  181. package/src/hooks/useMoveMessages.test.ts +0 -63
  182. package/src/hooks/useOrganizeWiden.test.ts +0 -217
  183. package/src/hooks/useOrganizeWiden.ts +0 -112
  184. package/src/hooks/useSemanticSearch.ts +0 -132
  185. package/src/hooks/useSemanticSearchEnabled.ts +0 -23
  186. package/src/hooks/useVisualViewport.test.ts +0 -126
  187. package/src/lib/organize/sender-fallback.test.ts +0 -52
  188. package/src/lib/organize/sender-fallback.ts +0 -38
  189. package/src/lib/rescue-telemetry.test.ts +0 -75
  190. package/src/router.test.ts +0 -81
@@ -6,6 +6,7 @@ import {
6
6
  } from "./message-body-source";
7
7
 
8
8
  const make = (overrides: Partial<RenderableBodyPart>): RenderableBodyPart => ({
9
+ partPath: "1",
9
10
  mediaType: "TEXT",
10
11
  mediaSubtype: "PLAIN",
11
12
  contentUrl: "https://cdn.test/content/x",
@@ -75,10 +76,6 @@ describe("pickRenderablePart", () => {
75
76
  assert.equal(pickRenderablePart([image]), null);
76
77
  });
77
78
 
78
- it("returns null on an empty list", () => {
79
- assert.equal(pickRenderablePart([]), null);
80
- });
81
-
82
79
  it("treats casing variants of the subtype as equivalent (mailers are inconsistent)", () => {
83
80
  const html = make({
84
81
  mediaSubtype: "html",
@@ -95,4 +92,24 @@ describe("pickRenderablePart", () => {
95
92
  const broken = make({ mediaSubtype: "HTML", contentUrl: "" });
96
93
  assert.equal(pickRenderablePart([broken]), null);
97
94
  });
95
+
96
+ it("never renders a forwarded message's body as the body carrying it", () => {
97
+ const outer = make({ partPath: "1", contentUrl: "https://cdn.test/outer" });
98
+ const forwarded = make({
99
+ partPath: "2",
100
+ mediaType: "MESSAGE",
101
+ mediaSubtype: "rfc822",
102
+ disposition: "attachment",
103
+ contentUrl: "https://cdn.test/forwarded",
104
+ });
105
+ const innerHtml = make({
106
+ partPath: "2.1",
107
+ mediaSubtype: "HTML",
108
+ contentUrl: "https://cdn.test/inner",
109
+ });
110
+ assert.deepEqual(pickRenderablePart([outer, forwarded, innerHtml]), {
111
+ kind: "text",
112
+ contentUrl: "https://cdn.test/outer",
113
+ });
114
+ });
98
115
  });
@@ -8,9 +8,12 @@
8
8
  * type without standing up jsdom or DOMPurify.
9
9
  */
10
10
 
11
+ import { ownParts } from "@remit/data-ports/message-parts";
12
+
11
13
  export type BodyContentKind = "html" | "text";
12
14
 
13
15
  export interface RenderableBodyPart {
16
+ partPath: string;
14
17
  mediaType: string;
15
18
  mediaSubtype: string;
16
19
  disposition?: string;
@@ -27,13 +30,13 @@ export interface PickedBodyPart {
27
30
  * Pick the best body part to render: prefer the first inline `text/html`,
28
31
  * fall back to the first inline `text/plain`. Multipart container rows and
29
32
  * attachment-disposition parts are ignored — they carry no renderable
30
- * payload at this position. Returns `null` when nothing renderable is
33
+ * payload at this position — and so are the parts of a forwarded message. Returns `null` when nothing renderable is
31
34
  * available so callers can show an empty-body affordance.
32
35
  */
33
36
  export const pickRenderablePart = (
34
37
  parts: readonly RenderableBodyPart[],
35
38
  ): PickedBodyPart | null => {
36
- const renderable = parts.filter(
39
+ const renderable = ownParts(parts).filter(
37
40
  (p) =>
38
41
  !p.isMultipart &&
39
42
  p.disposition !== "attachment" &&
@@ -45,20 +45,4 @@ describe("completeOnboarding", () => {
45
45
  "navigate",
46
46
  ]);
47
47
  });
48
-
49
- it("refreshes the config query", async () => {
50
- let refetchedKey: unknown;
51
- await completeOnboarding({
52
- queryClient: {
53
- refetchQueries: (filters) => {
54
- refetchedKey = (filters as { queryKey?: unknown }).queryKey;
55
- return Promise.resolve();
56
- },
57
- },
58
- recordCompleted: () => {},
59
- navigateToInbox: () => {},
60
- });
61
-
62
- assert.ok(refetchedKey, "refetchQueries called with a query key");
63
- });
64
48
  });
@@ -19,7 +19,6 @@ const filter = (
19
19
  scope: "Standing",
20
20
  state: "Active",
21
21
  disabledReason: "None",
22
- hasAnchor: false,
23
22
  ruleChangedAt: 0,
24
23
  actionChangedAt: 0,
25
24
  matchOperator: "And",
@@ -77,21 +76,6 @@ describe("filterToRule", () => {
77
76
  assert.equal(rule.scope, "until");
78
77
  assert.match(rule.until ?? "", /^\d{4}-\d{2}-\d{2}$/);
79
78
  });
80
-
81
- it("shows a semantic anchor as an active widen chip when the deployment can serve it", () => {
82
- const rule = filterToRule(filter({ hasAnchor: true }));
83
- assert.ok(rule.widen);
84
- assert.equal(rule.widen?.inactive, undefined);
85
- });
86
-
87
- it("marks the widen inactive on a deployment that cannot evaluate it (D4)", () => {
88
- const rule = filterToRule(filter({ hasAnchor: true }), true);
89
- assert.equal(rule.widen?.inactive, true);
90
- });
91
-
92
- it("carries no widen when the filter has no anchor", () => {
93
- assert.equal(filterToRule(filter({ hasAnchor: false })).widen, undefined);
94
- });
95
79
  });
96
80
 
97
81
  describe("expiresAtToPickedDate", () => {
@@ -109,7 +93,7 @@ describe("expiresAtToPickedDate", () => {
109
93
  });
110
94
 
111
95
  describe("ruleChangesPredicateOrAction", () => {
112
- const base = filterToRule(filter({ hasAnchor: true }));
96
+ const base = filterToRule(filter());
113
97
 
114
98
  it("is false for an identical rule and for a rename only", () => {
115
99
  assert.equal(ruleChangesPredicateOrAction(base, base), false);
@@ -119,7 +103,7 @@ describe("ruleChangesPredicateOrAction", () => {
119
103
  );
120
104
  });
121
105
 
122
- it("is true when a clause, operator, move target or widen changes", () => {
106
+ it("is true when a clause, operator or move target changes", () => {
123
107
  assert.equal(
124
108
  ruleChangesPredicateOrAction({ ...base, matchOperator: "any" }, base),
125
109
  true,
@@ -131,10 +115,6 @@ describe("ruleChangesPredicateOrAction", () => {
131
115
  ),
132
116
  true,
133
117
  );
134
- assert.equal(
135
- ruleChangesPredicateOrAction({ ...base, widen: undefined }, base),
136
- true,
137
- );
138
118
  assert.equal(
139
119
  ruleChangesPredicateOrAction(
140
120
  { ...base, clauses: [{ id: "c", field: "Subject", value: "x" }] },
@@ -288,14 +268,4 @@ describe("buildUpdateFilterInput", () => {
288
268
  assert.equal(body.scope, "Temporary");
289
269
  assert.match(body.expiresAt ?? "", /^2027-06-01T/);
290
270
  });
291
-
292
- it("carries no anchor field — the editor never has one to send (reader #266)", () => {
293
- const changed: FilterRule = {
294
- ...original,
295
- scope: "until",
296
- until: "2027-03-04",
297
- };
298
- const body = buildUpdateFilterInput(changed, original);
299
- assert.equal("anchorMessageId" in body, false);
300
- });
301
271
  });
@@ -8,20 +8,15 @@ import { NO_ACTION } from "./organize-model";
8
8
 
9
9
  /**
10
10
  * The persisted filter as the chip editor's rule (RFC 038 D6). Loads the whole
11
- * rule the user edits: literal clauses, the match operator, the move action, the
12
- * scope (and, for a Temporary filter, the date it stops on), and the semantic
13
- * anchor as a widen chip. The anchor renders inactive when this deployment
14
- * cannot evaluate it (D4) — the rule then matches by its literal clauses only.
11
+ * rule the user edits: literal clauses, the match operator, the move action, and
12
+ * the scope (and, for a Temporary filter, the date it stops on).
15
13
  *
16
14
  * The scope maps `Standing` → "standing" and `Temporary` → "until"; a persisted
17
15
  * filter is never the one-time "once" scope. `moveMailboxId` and `labelId` both
18
16
  * drop the `"None"` sentinel to `undefined` so an empty select reads as "no
19
17
  * action" (issue #26).
20
18
  */
21
- export const filterToRule = (
22
- filter: RemitImapFilterResponse,
23
- semanticUnavailable = false,
24
- ): FilterRule => {
19
+ export const filterToRule = (filter: RemitImapFilterResponse): FilterRule => {
25
20
  const clauses: RuleClause[] = filter.literalClauses.map((clause, index) => ({
26
21
  id: `clause-${index}`,
27
22
  field: clause.field,
@@ -30,9 +25,6 @@ export const filterToRule = (
30
25
  return {
31
26
  clauses,
32
27
  matchOperator: filter.matchOperator === "And" ? "all" : "any",
33
- widen: filter.hasAnchor
34
- ? { anchorCount: 1, ...(semanticUnavailable ? { inactive: true } : {}) }
35
- : undefined,
36
28
  moveMailboxId:
37
29
  filter.actionMailboxId !== NO_ACTION ? filter.actionMailboxId : undefined,
38
30
  labelId:
@@ -69,13 +61,12 @@ const predicateActionKey = (rule: FilterRule): string =>
69
61
  operator: rule.matchOperator,
70
62
  move: rule.moveMailboxId ?? null,
71
63
  label: rule.labelId ?? null,
72
- widen: rule.widen ? (rule.widen.inactive ? "inactive" : "active") : "none",
73
64
  });
74
65
 
75
66
  /**
76
67
  * Whether the edited rule changes the predicate or the action versus the one it
77
68
  * was loaded from — the clauses, the match operator, the move target, the
78
- * label target, or the widen's presence. A change here is what bumps
69
+ * or the label target. A change here is what bumps
79
70
  * `ruleChangedAt` and offers the re-back-apply (RFC 034 Decision 3.2); the name
80
71
  * is deliberately excluded, so a cosmetic rename is not a rule change.
81
72
  */
@@ -93,7 +84,7 @@ const scopeExpiryKey = (rule: FilterRule): string =>
93
84
  /**
94
85
  * Whether the edited rule changes scope (standing ↔ until-a-date) or the date
95
86
  * itself, versus the one it was loaded from (reader #266). Scope and expiry
96
- * are mutable on an existing filter, unlike the anchor, and a change here
87
+ * are mutable on an existing filter, and a change here
97
88
  * bumps `ruleChangedAt` the same way a predicate/action change does — moving a
98
89
  * lapsed filter back to Standing (or extending its date) is the same kind of
99
90
  * "the user just reasserted this rule" moment, and re-offers the back-apply so
@@ -110,8 +101,7 @@ export const ruleChangesScopeOrExpiry = (
110
101
  * untouched (RFC 034 Decision 3.2). A predicate or action change sends the
111
102
  * operator, clauses, move target, and label target; a scope or expiry change
112
103
  * sends `scope` and, for the `until` scope, `expiresAt` (reader #266) —
113
- * either bumps `ruleChangedAt`. The anchor is never in the editor's gift, so
114
- * it never enters the patch; the partial update preserves it.
104
+ * either bumps `ruleChangedAt`.
115
105
  */
116
106
  export const buildUpdateFilterInput = (
117
107
  rule: FilterRule,
@@ -53,31 +53,17 @@ describe("hasCommittableAction", () => {
53
53
  });
54
54
 
55
55
  describe("buildOrganizeInput", () => {
56
- it("carries the anchor and defaults both actions to None when neither is set", () => {
57
- const input = buildOrganizeInput(baseDraft({ anchorMessageId: "msg-1" }));
58
- assert.equal(input.anchorMessageId, "msg-1");
56
+ it("defaults both actions to None when neither is set", () => {
57
+ const input = buildOrganizeInput(baseDraft());
59
58
  assert.equal(input.actionMailboxId, NO_ACTION);
60
59
  assert.equal(input.actionLabelId, NO_ACTION);
61
60
  assert.equal(input.matchOperator, "And");
62
61
  assert.deepEqual(input.literalClauses, []);
63
62
  });
64
63
 
65
- it("omits anchorMessageId entirely for a purely-literal input", () => {
66
- const input = buildOrganizeInput(
67
- baseDraft({
68
- literalClauses: [{ field: "From", value: "airbnb.com" }],
69
- }),
70
- );
71
- assert.equal("anchorMessageId" in input, false);
72
- });
73
-
74
64
  it("carries the move and label actions independently (issue #26)", () => {
75
65
  const input = buildOrganizeInput(
76
- baseDraft({
77
- anchorMessageId: "msg-1",
78
- moveMailboxId: "mbx-9",
79
- labelId: "lbl-9",
80
- }),
66
+ baseDraft({ moveMailboxId: "mbx-9", labelId: "lbl-9" }),
81
67
  );
82
68
  assert.equal(input.actionMailboxId, "mbx-9");
83
69
  assert.equal(input.actionLabelId, "lbl-9");
@@ -87,7 +73,7 @@ describe("buildOrganizeInput", () => {
87
73
  describe("buildCreateFilterInput", () => {
88
74
  it("builds a Standing filter with no expiresAt", () => {
89
75
  const input = buildCreateFilterInput(
90
- baseDraft({ anchorMessageId: "msg-1", moveMailboxId: "mbx-2" }),
76
+ baseDraft({ moveMailboxId: "mbx-2" }),
91
77
  "standing",
92
78
  "Travel",
93
79
  );
@@ -96,7 +82,6 @@ describe("buildCreateFilterInput", () => {
96
82
  assert.equal(input.name, "Travel");
97
83
  assert.equal(input.actionMailboxId, "mbx-2");
98
84
  assert.equal(input.actionLabelId, NO_ACTION);
99
- assert.equal(input.anchorMessageId, "msg-1");
100
85
  });
101
86
 
102
87
  it("builds a Temporary filter that carries expiresAt", () => {
@@ -143,8 +128,8 @@ describe("organizeScopeFor", () => {
143
128
  );
144
129
  });
145
130
 
146
- it("reads either widened door at scope once as all-like-these", () => {
147
- assert.equal(organizeScopeFor({ mode: "similar" }), "all-like-these");
131
+ it("reads the properties door at scope once as all-like-these", () => {
132
+ assert.equal(organizeScopeFor({ mode: "properties" }), "all-like-these");
148
133
  assert.equal(
149
134
  organizeScopeFor({ mode: "properties", ruleScope: "once" }),
150
135
  "all-like-these",
@@ -152,7 +137,7 @@ describe("organizeScopeFor", () => {
152
137
  });
153
138
 
154
139
  it("reads the two persisting scopes off the scope answer alone", () => {
155
- for (const mode of ["selected", "similar", "properties"] as const) {
140
+ for (const mode of ["selected", "properties"] as const) {
156
141
  assert.equal(
157
142
  organizeScopeFor({ mode, ruleScope: "standing" }),
158
143
  "standing",
@@ -196,24 +181,6 @@ describe("buildWizardDraft", () => {
196
181
  }
197
182
  });
198
183
 
199
- it("anchors on the ticked messages only while the similar door is matching", () => {
200
- const anchored = buildWizardDraft({
201
- mode: "similar",
202
- anchorMessageId: "msg-1",
203
- clauses: [],
204
- matchOperator: "all",
205
- });
206
- assert.equal(anchored.anchorMessageId, "msg-1");
207
-
208
- const literal = buildWizardDraft({
209
- mode: "properties",
210
- anchorMessageId: "msg-1",
211
- clauses,
212
- matchOperator: "any",
213
- });
214
- assert.equal("anchorMessageId" in literal, false);
215
- });
216
-
217
184
  it("maps the wizard's operator onto the API's", () => {
218
185
  assert.equal(
219
186
  buildWizardDraft({ mode: "properties", clauses, matchOperator: "all" })
@@ -32,16 +32,23 @@ export type OrganizeScope =
32
32
  /** The `"None"` sentinel the API uses for an absent label / move action. */
33
33
  export const NO_ACTION = "None";
34
34
 
35
+ /**
36
+ * The predicate a preview counts and a commit acts on: the operator and the
37
+ * literal clauses. The set it previews equals the set every commit scope acts on.
38
+ */
39
+ export type OrganizeMatchPredicate = Pick<
40
+ RemitImapOrganizeInput,
41
+ "matchOperator" | "literalClauses"
42
+ >;
43
+
35
44
  /**
36
45
  * The user's in-progress organize decision, independent of which scope they
37
- * land on. The anchor and predicate drive the match set; `moveMailboxId` and
46
+ * land on. The predicate drives the match set; `moveMailboxId` and
38
47
  * `labelId` are the two committable actions (issue #26) — independent of each
39
48
  * other, since a move is exclusive and a label is additive (RFC 034 Decision
40
49
  * 3.1).
41
50
  */
42
51
  export interface OrganizeDraft {
43
- /** Semantic anchor — "mail like this one". The first selected message. */
44
- anchorMessageId?: string;
45
52
  /** How `literalClauses` combine. Ignored when there are none. */
46
53
  matchOperator: RemitImapFilterMatchOperator;
47
54
  /** Literal from/subject/has-words clauses (RFC 031). */
@@ -90,13 +97,12 @@ export const buildRunFilterInput = (
90
97
  /**
91
98
  * Build the read-only preview / back-apply matcher input. The action fields do
92
99
  * not affect which messages match — the preview returns exactly the set a job
93
- * with the same predicate would apply to — so a widen preview can pass this
94
- * before the user has chosen a folder or a label.
100
+ * with the same predicate would apply to — so a preview can pass this before
101
+ * the user has chosen a folder or a label.
95
102
  */
96
103
  export const buildOrganizeInput = (
97
104
  draft: OrganizeDraft,
98
105
  ): RemitImapOrganizeInput => ({
99
- ...(draft.anchorMessageId ? { anchorMessageId: draft.anchorMessageId } : {}),
100
106
  matchOperator: draft.matchOperator,
101
107
  literalClauses: draft.literalClauses,
102
108
  actionLabelId: draft.labelId ?? NO_ACTION,
@@ -111,9 +117,9 @@ export const buildOrganizeInput = (
111
117
  export interface WizardCommitAnswers {
112
118
  /**
113
119
  * The door the match came through. An escalated predicate is not one: it has
114
- * no clauses to build a rule from and no anchor to widen, so a scope
115
- * reconstructed for it would be a rule matching everything. Typed to the
116
- * three doors so that cannot be written rather than merely not written.
120
+ * no clauses to build a rule from, so a scope reconstructed for it would be a
121
+ * rule matching everything. Typed to the doors so that cannot be written
122
+ * rather than merely not written.
117
123
  */
118
124
  mode: MatchDoor;
119
125
  /** Absent on the verbs that never reach the scope step — those act once. */
@@ -123,7 +129,7 @@ export interface WizardCommitAnswers {
123
129
  /**
124
130
  * Which of the four commit scopes the wizard's answers add up to. Reconstructed
125
131
  * here and nowhere else, so a driver cannot invent a fifth: the ticked list at
126
- * scope once is `just-these`, a widened door at scope once is `all-like-these`,
132
+ * scope once is `just-these`, the properties door at scope once is `all-like-these`,
127
133
  * and the two persisting scopes are `standing` and `temporary`.
128
134
  */
129
135
  export const organizeScopeFor = ({
@@ -136,8 +142,6 @@ export const organizeScopeFor = ({
136
142
  };
137
143
 
138
144
  export interface WizardDraftInput extends WizardCommitAnswers {
139
- /** The semantic anchor, present only while the similar door is the matcher. */
140
- anchorMessageId?: string;
141
145
  clauses: readonly RuleClause[];
142
146
  matchOperator: MatchOperator;
143
147
  moveMailboxId?: string;
@@ -152,15 +156,12 @@ export interface WizardDraftInput extends WizardCommitAnswers {
152
156
  * instant once there is a draft to carry it.
153
157
  */
154
158
  export const buildWizardDraft = ({
155
- mode,
156
159
  ruleScope,
157
- anchorMessageId,
158
160
  clauses,
159
161
  matchOperator,
160
162
  moveMailboxId,
161
163
  until,
162
164
  }: WizardDraftInput): OrganizeDraft => ({
163
- ...(mode === "similar" && anchorMessageId ? { anchorMessageId } : {}),
164
165
  matchOperator: matchOperator === "all" ? "And" : "Or",
165
166
  literalClauses: clauses.map((clause) => ({
166
167
  field: clause.field,
@@ -193,8 +194,5 @@ export const buildCreateFilterInput = (
193
194
  literalClauses: draft.literalClauses,
194
195
  actionLabelId: draft.labelId ?? NO_ACTION,
195
196
  actionMailboxId: draft.moveMailboxId ?? NO_ACTION,
196
- ...(draft.anchorMessageId
197
- ? { anchorMessageId: draft.anchorMessageId }
198
- : {}),
199
197
  };
200
198
  };