@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
@@ -0,0 +1,361 @@
1
+ import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
2
+ import {
3
+ Button,
4
+ type ClauseEditState,
5
+ type ClauseField,
6
+ type FilterRule,
7
+ FilterRuleEditor,
8
+ type FolderOption,
9
+ type MatchOperator,
10
+ type RuleScope,
11
+ } from "@remit/ui";
12
+ import { useQuery } from "@tanstack/react-query";
13
+ import { CheckCircle2, Loader2 } from "lucide-react";
14
+ import { useMemo, useRef, useState } from "react";
15
+ import { useCreateFilter } from "@/hooks/useFilters";
16
+ import { useOrganizeJob } from "@/hooks/useOrganizeJob";
17
+ import { useRulePreview } from "@/hooks/useRulePreview";
18
+ import { getMailboxDisplayName } from "@/lib/folder-roles";
19
+ import { buildMoveTargets } from "@/lib/move-targets";
20
+ import {
21
+ buildInitialRule,
22
+ normalizeClauseValue,
23
+ rulePredicate,
24
+ ruleToDraft,
25
+ SUPPORTED_CLAUSE_FIELDS,
26
+ } from "@/lib/organize/rule-model";
27
+
28
+ interface OrganizeRuleEditorProps {
29
+ accountId: string;
30
+ selectedMessageIds: string[];
31
+ /** The widen probe's matched total — seeds the live count without a re-fetch. */
32
+ seedCount: number;
33
+ /**
34
+ * The deployment ships no vector pipeline, so the widen cannot run. The rule
35
+ * opens on the sender-fallback `From` chips instead of the anchor.
36
+ */
37
+ semanticUnavailable?: boolean;
38
+ /** Distinct sender addresses, for the fallback clauses and the progress copy. */
39
+ senders?: string[];
40
+ /** A folder a "Something else" shortcut pre-picked. */
41
+ seedMailboxId?: string;
42
+ /** A scope a "Something else" shortcut pre-picked. */
43
+ seedScope?: RuleScope;
44
+ onClose: () => void;
45
+ }
46
+
47
+ /**
48
+ * The Organize surface as the chip editor (RFC 038 D1). The rule is rendered and
49
+ * edited over the existing preview/apply endpoints: clause chips, a
50
+ * match-operator toggle, a move action, and a scope that maps one-time apply to
51
+ * a back-apply job and standing/until to a `Filter`. The count is live and the
52
+ * commit gate holds apply until it settles, so the set the editor shows is the
53
+ * set a commit acts on. Rendered inside the desktop dialog and the mobile sheet
54
+ * alike, so the two cannot drift.
55
+ */
56
+ export function OrganizeRuleEditor({
57
+ accountId,
58
+ selectedMessageIds,
59
+ seedCount,
60
+ semanticUnavailable = false,
61
+ senders = [],
62
+ seedMailboxId,
63
+ seedScope,
64
+ onClose,
65
+ }: OrganizeRuleEditorProps) {
66
+ const anchorMessageId = selectedMessageIds[0];
67
+ const senderFallback = semanticUnavailable && senders.length > 0;
68
+
69
+ const [rule, setRule] = useState<FilterRule>(() =>
70
+ buildInitialRule({
71
+ anchorMessageId,
72
+ semanticUnavailable,
73
+ senders,
74
+ selectionCount: selectedMessageIds.length,
75
+ seedMailboxId,
76
+ seedScope,
77
+ }),
78
+ );
79
+ const [clauseEdit, setClauseEdit] = useState<ClauseEditState | undefined>();
80
+ const nextClauseId = useRef(0);
81
+
82
+ const { data: mailboxesData } = useQuery({
83
+ ...mailboxOperationsListMailboxesOptions({ path: { accountId } }),
84
+ staleTime: Number.POSITIVE_INFINITY,
85
+ });
86
+
87
+ const folders: FolderOption[] = useMemo(
88
+ () =>
89
+ buildMoveTargets(mailboxesData?.items ?? []).map((mailbox) => ({
90
+ id: mailbox.mailboxId,
91
+ label: getMailboxDisplayName(mailbox.fullPath),
92
+ })),
93
+ [mailboxesData?.items],
94
+ );
95
+
96
+ const preview = useRulePreview(
97
+ accountId,
98
+ rulePredicate(rule, anchorMessageId),
99
+ seedCount,
100
+ );
101
+
102
+ const organizeJob = useOrganizeJob(accountId);
103
+ const createFilter = useCreateFilter(accountId);
104
+
105
+ const startAddClause = () =>
106
+ setClauseEdit({
107
+ mode: "add",
108
+ draft: { field: SUPPORTED_CLAUSE_FIELDS[0], value: "" },
109
+ });
110
+
111
+ const startEditClause = (clauseId: string) => {
112
+ const clause = rule.clauses.find((entry) => entry.id === clauseId);
113
+ if (!clause) return;
114
+ setClauseEdit({
115
+ mode: "edit",
116
+ clauseId,
117
+ draft: { field: clause.field, value: clause.value },
118
+ });
119
+ };
120
+
121
+ const changeDraftField = (field: ClauseField) =>
122
+ setClauseEdit((edit) =>
123
+ edit ? { ...edit, draft: { ...edit.draft, field } } : edit,
124
+ );
125
+
126
+ const changeDraftValue = (value: string) =>
127
+ setClauseEdit((edit) =>
128
+ edit ? { ...edit, draft: { ...edit.draft, value } } : edit,
129
+ );
130
+
131
+ const submitClause = () => {
132
+ if (!clauseEdit) return;
133
+ const field = clauseEdit.draft.field;
134
+ const value = normalizeClauseValue(field, clauseEdit.draft.value);
135
+ if (value === "") return;
136
+ setRule((current) => {
137
+ if (clauseEdit.mode === "edit" && clauseEdit.clauseId) {
138
+ return {
139
+ ...current,
140
+ clauses: current.clauses.map((clause) =>
141
+ clause.id === clauseEdit.clauseId
142
+ ? { id: clause.id, field, value }
143
+ : clause,
144
+ ),
145
+ };
146
+ }
147
+ nextClauseId.current += 1;
148
+ return {
149
+ ...current,
150
+ clauses: [
151
+ ...current.clauses,
152
+ { id: `clause-${nextClauseId.current}`, field, value },
153
+ ],
154
+ };
155
+ });
156
+ setClauseEdit(undefined);
157
+ };
158
+
159
+ const removeClause = (clauseId: string) =>
160
+ setRule((current) => ({
161
+ ...current,
162
+ clauses: current.clauses.filter((clause) => clause.id !== clauseId),
163
+ }));
164
+
165
+ const addWiden = () =>
166
+ setRule((current) => ({
167
+ ...current,
168
+ widen: { anchorCount: Math.max(selectedMessageIds.length, 1) },
169
+ }));
170
+
171
+ const removeWiden = () =>
172
+ setRule((current) => ({ ...current, widen: undefined }));
173
+
174
+ const changeMatchOperator = (matchOperator: MatchOperator) =>
175
+ setRule((current) => ({ ...current, matchOperator }));
176
+
177
+ const changeMove = (mailboxId: string) =>
178
+ setRule((current) => ({
179
+ ...current,
180
+ moveMailboxId: mailboxId || undefined,
181
+ }));
182
+
183
+ const changeScope = (scope: RuleScope) =>
184
+ setRule((current) => ({ ...current, scope }));
185
+
186
+ const changeName = (name: string) =>
187
+ setRule((current) => ({ ...current, name }));
188
+
189
+ const changeUntil = (until: string) =>
190
+ setRule((current) => ({ ...current, until }));
191
+
192
+ const commit = () => {
193
+ const draft = ruleToDraft(rule, anchorMessageId);
194
+ if (rule.scope === "once") {
195
+ organizeJob.start(draft);
196
+ return;
197
+ }
198
+ createFilter.createFilter(
199
+ draft,
200
+ rule.scope === "standing" ? "standing" : "temporary",
201
+ (rule.name ?? "").trim(),
202
+ );
203
+ };
204
+
205
+ if (organizeJob.isStarting || organizeJob.isRunning || organizeJob.isDone) {
206
+ return (
207
+ <JobProgress
208
+ progress={organizeJob.progress}
209
+ isDone={organizeJob.isDone}
210
+ senderFallback={senderFallback}
211
+ onClose={onClose}
212
+ />
213
+ );
214
+ }
215
+
216
+ if (createFilter.isPending) {
217
+ return <SavingState />;
218
+ }
219
+
220
+ if (createFilter.isSuccess) {
221
+ return <FilterSaved onClose={onClose} />;
222
+ }
223
+
224
+ if (createFilter.isError) {
225
+ return <CommitError onRetry={createFilter.reset} onClose={onClose} />;
226
+ }
227
+
228
+ return (
229
+ <FilterRuleEditor
230
+ rule={rule}
231
+ folders={folders}
232
+ preview={preview}
233
+ semanticAvailable={!semanticUnavailable}
234
+ clauseFields={SUPPORTED_CLAUSE_FIELDS}
235
+ clauseEdit={clauseEdit}
236
+ onStartAddClause={startAddClause}
237
+ onStartEditClause={startEditClause}
238
+ onRemoveClause={removeClause}
239
+ onChangeDraftField={changeDraftField}
240
+ onChangeDraftValue={changeDraftValue}
241
+ onSubmitClause={submitClause}
242
+ onCancelClause={() => setClauseEdit(undefined)}
243
+ onAddWiden={addWiden}
244
+ onRemoveWiden={removeWiden}
245
+ onChangeMatchOperator={changeMatchOperator}
246
+ onChangeMove={changeMove}
247
+ onChangeScope={changeScope}
248
+ onChangeName={changeName}
249
+ onChangeUntil={changeUntil}
250
+ onCommit={commit}
251
+ onCancel={onClose}
252
+ />
253
+ );
254
+ }
255
+
256
+ function JobProgress({
257
+ progress,
258
+ isDone,
259
+ senderFallback,
260
+ onClose,
261
+ }: {
262
+ progress: ReturnType<typeof useOrganizeJob>["progress"];
263
+ isDone: boolean;
264
+ senderFallback: boolean;
265
+ onClose: () => void;
266
+ }) {
267
+ const failed = progress.state === "Failed";
268
+ return (
269
+ <div className="flex flex-col items-center gap-3 px-5 py-8 text-center">
270
+ {!isDone ? (
271
+ <Loader2 className="size-8 animate-spin text-accent-2" />
272
+ ) : failed ? (
273
+ <span className="text-sm font-semibold text-danger">
274
+ Organize failed
275
+ </span>
276
+ ) : (
277
+ <CheckCircle2 className="size-8 text-positive" />
278
+ )}
279
+
280
+ {!isDone && (
281
+ <p className="text-sm font-medium text-fg">
282
+ {senderFallback
283
+ ? "Organizing mail from these senders…"
284
+ : "Organizing similar mail…"}
285
+ </p>
286
+ )}
287
+
288
+ {isDone && !failed && (
289
+ <div className="text-sm text-fg">
290
+ <p className="font-medium">Done</p>
291
+ <p className="mt-1 text-xs text-fg-muted">
292
+ {progress.appliedCount} of {progress.matchedCount} moved
293
+ {progress.failedCount > 0
294
+ ? ` · ${progress.failedCount} failed`
295
+ : ""}
296
+ .
297
+ </p>
298
+ </div>
299
+ )}
300
+
301
+ {isDone && failed && (
302
+ <p className="max-w-xs text-xs text-fg-muted">
303
+ {progress.errorMessage || "Something went wrong. Please try again."}
304
+ </p>
305
+ )}
306
+
307
+ {isDone && (
308
+ <Button variant="primary" onClick={onClose} className="mt-2">
309
+ Done
310
+ </Button>
311
+ )}
312
+ </div>
313
+ );
314
+ }
315
+
316
+ function SavingState() {
317
+ return (
318
+ <div className="flex flex-col items-center gap-3 px-5 py-10 text-center">
319
+ <Loader2 className="size-8 animate-spin text-accent-2" />
320
+ <p className="text-sm font-medium text-fg">Saving rule…</p>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ function FilterSaved({ onClose }: { onClose: () => void }) {
326
+ return (
327
+ <div className="flex flex-col items-center gap-3 px-5 py-8 text-center">
328
+ <CheckCircle2 className="size-8 text-positive" />
329
+ <p className="text-sm font-medium text-fg">Filter saved</p>
330
+ <p className="max-w-xs text-xs text-fg-muted">
331
+ You can see it, and when it expires, under Settings › Filters.
332
+ </p>
333
+ <Button variant="primary" onClick={onClose} className="mt-2">
334
+ Done
335
+ </Button>
336
+ </div>
337
+ );
338
+ }
339
+
340
+ function CommitError({
341
+ onRetry,
342
+ onClose,
343
+ }: {
344
+ onRetry: () => void;
345
+ onClose: () => void;
346
+ }) {
347
+ return (
348
+ <div className="flex flex-col items-center gap-3 px-5 py-8 text-center">
349
+ <p className="text-sm font-medium text-danger">Couldn't save the rule</p>
350
+ <p className="max-w-xs text-xs text-fg-muted">Please try again.</p>
351
+ <div className="mt-2 flex gap-2">
352
+ <Button variant="primary" onClick={onRetry}>
353
+ Try again
354
+ </Button>
355
+ <Button variant="ghost" onClick={onClose}>
356
+ Not now
357
+ </Button>
358
+ </div>
359
+ </div>
360
+ );
361
+ }
@@ -5,18 +5,17 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
5
5
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
6
  import type { ReactNode } from "react";
7
7
  import { ErrorBannerProvider } from "@/components/ui/ErrorBannerProvider";
8
- import { OrganizePanel } from "./OrganizePanel";
8
+ import { OrganizeRuleEditor } from "./OrganizeRuleEditor";
9
9
  import type { FolderOption } from "./SomethingElsePanel";
10
10
  import { SomethingElsePanel } from "./SomethingElsePanel";
11
11
 
12
12
  /**
13
- * `Flows/Smart Organize` — the guided select-similar organize flow (issue
14
- * #211), rendered from the real web-client components, not a prototype copy:
15
- * the same {@link OrganizePanel} the desktop dialog and the mobile sheet use,
16
- * and the same {@link SomethingElsePanel} the flow seeds from. The prototype's
17
- * interactive `Inbox` / `Walkthrough` stories live alongside these in the
18
- * workbench; these show each in-sheet panel in isolation so they can't drift
19
- * from what ships.
13
+ * `Flows/Smart Organize` — the Organize surface as the chip editor (RFC 038 D1),
14
+ * rendered from the real web-client component the desktop dialog and the mobile
15
+ * sheet both mount, not a prototype copy. Each story opens the editor on a
16
+ * seeded state a widen-anchored rule, the sender-fallback clauses (#251), a
17
+ * pre-seeded standing rule, and the capability-absent cases so the surface
18
+ * can't drift from what ships.
20
19
  */
21
20
 
22
21
  const ACCOUNT_ID = "acc-1";
@@ -57,8 +56,8 @@ const FOLDER_OPTIONS: FolderOption[] = [
57
56
  ];
58
57
 
59
58
  /**
60
- * A QueryClient pre-seeded with the account's mailboxes, so the sentence's
61
- * folder picker renders its real options without a network round-trip.
59
+ * A QueryClient pre-seeded with the account's mailboxes, so the editor's folder
60
+ * picker renders its real options without a network round-trip.
62
61
  */
63
62
  function seededClient(): QueryClient {
64
63
  const client = new QueryClient({
@@ -71,7 +70,7 @@ function seededClient(): QueryClient {
71
70
  return client;
72
71
  }
73
72
 
74
- /** Phone frame + a bottom sheet holding the panel, mirroring the mobile home. */
73
+ /** Phone frame + a bottom sheet holding the editor, mirroring the mobile home. */
75
74
  function SheetStage({ children }: { children: ReactNode }) {
76
75
  return (
77
76
  <QueryClientProvider client={seededClient()}>
@@ -109,44 +108,29 @@ export default meta;
109
108
 
110
109
  type Story = StoryObj;
111
110
 
112
- /** The organize sentence on a widened selection — folder picker + four scopes. */
111
+ /** The rule editor on a widened selection — the widen chip and a live count. */
113
112
  export const Organize: Story = {
114
113
  render: () => (
115
114
  <SheetStage>
116
- <OrganizePanel
115
+ <OrganizeRuleEditor
117
116
  accountId={ACCOUNT_ID}
118
- mailboxId="mbx-inbox"
119
117
  selectedMessageIds={["msg-1", "msg-2", "msg-3"]}
120
- matchPredicate={{
121
- anchorMessageId: "msg-1",
122
- matchOperator: "And",
123
- literalClauses: [],
124
- }}
125
- matchedCount={47}
118
+ seedCount={47}
126
119
  onClose={() => undefined}
127
120
  />
128
121
  </SheetStage>
129
122
  ),
130
123
  };
131
124
 
132
- /**
133
- * A larger widen — the same sentence over a broad match set, as when the anchor
134
- * is a common sender.
135
- */
125
+ /** A larger widen — the same editor over a broad match set. */
136
126
  export const FromSearch: Story = {
137
127
  name: "From Search",
138
128
  render: () => (
139
129
  <SheetStage>
140
- <OrganizePanel
130
+ <OrganizeRuleEditor
141
131
  accountId={ACCOUNT_ID}
142
- mailboxId="mbx-inbox"
143
132
  selectedMessageIds={["msg-1"]}
144
- matchPredicate={{
145
- anchorMessageId: "msg-1",
146
- matchOperator: "And",
147
- literalClauses: [],
148
- }}
149
- matchedCount={412}
133
+ seedCount={412}
150
134
  seedMailboxId="mbx-archive"
151
135
  onClose={() => undefined}
152
136
  />
@@ -154,22 +138,16 @@ export const FromSearch: Story = {
154
138
  ),
155
139
  };
156
140
 
157
- /** The standing scope pre-selected — the "Always keep…" sentence + filter name. */
141
+ /** The standing scope pre-selected — the rule that keeps working on new mail. */
158
142
  export const AlwaysRule: Story = {
159
143
  name: "Always Rule",
160
144
  render: () => (
161
145
  <SheetStage>
162
- <OrganizePanel
146
+ <OrganizeRuleEditor
163
147
  accountId={ACCOUNT_ID}
164
- mailboxId="mbx-inbox"
165
148
  selectedMessageIds={["msg-1", "msg-2"]}
166
- matchPredicate={{
167
- anchorMessageId: "msg-1",
168
- matchOperator: "And",
169
- literalClauses: [],
170
- }}
171
- matchedCount={47}
172
- initialScope="standing"
149
+ seedCount={47}
150
+ seedScope="standing"
173
151
  seedMailboxId="mbx-travel"
174
152
  onClose={() => undefined}
175
153
  />
@@ -177,47 +155,19 @@ export const AlwaysRule: Story = {
177
155
  ),
178
156
  };
179
157
 
180
- /** The widen matched nothing — the sentence organizes just the selection. */
181
- export const NoSimilarFound: Story = {
182
- name: "No Similar Found",
183
- render: () => (
184
- <SheetStage>
185
- <OrganizePanel
186
- accountId={ACCOUNT_ID}
187
- mailboxId="mbx-inbox"
188
- selectedMessageIds={["msg-1", "msg-2"]}
189
- matchPredicate={{
190
- anchorMessageId: "msg-1",
191
- matchOperator: "And",
192
- literalClauses: [],
193
- }}
194
- matchedCount={0}
195
- fallback
196
- onClose={() => undefined}
197
- />
198
- </SheetStage>
199
- ),
200
- };
201
-
202
158
  /**
203
- * The deployment ships no vector pipeline, so the server ran no semantic widen —
204
- * the sentence organizes just the selection and the heading names the real
205
- * reason (#226/#201).
159
+ * The deployment ships no vector pipeline and the selection has no senders to
160
+ * match on — the widen is not offered and the rule opens empty, awaiting a
161
+ * clause (#226/#201).
206
162
  */
207
163
  export const SemanticUnavailable: Story = {
208
164
  name: "Semantic Unavailable",
209
165
  render: () => (
210
166
  <SheetStage>
211
- <OrganizePanel
167
+ <OrganizeRuleEditor
212
168
  accountId={ACCOUNT_ID}
213
- mailboxId="mbx-inbox"
214
169
  selectedMessageIds={["msg-1", "msg-2"]}
215
- matchPredicate={{
216
- anchorMessageId: "msg-1",
217
- matchOperator: "And",
218
- literalClauses: [],
219
- }}
220
- matchedCount={0}
170
+ seedCount={0}
221
171
  semanticUnavailable
222
172
  onClose={() => undefined}
223
173
  />
@@ -226,29 +176,19 @@ export const SemanticUnavailable: Story = {
226
176
  };
227
177
 
228
178
  /**
229
- * No vector pipeline, but the selection has senders to match on — the widen
230
- * fell back to matching all mail from those senders. The senders span different
231
- * domains, so the fallback keeps one `From` clause per address; the heading
232
- * names the senders and states the real semantics, and every commit scope
233
- * (including the standing filter) carries those clauses, so it reaches the
234
- * widened set and keeps working on future mail.
179
+ * No vector pipeline, but the selection has senders to match on — the widen fell
180
+ * back to the sender `From` clauses (#251). The senders span different domains,
181
+ * so the fallback keeps one editable `From` chip per address, combined with
182
+ * "or". Every commit carries exactly these clauses.
235
183
  */
236
184
  export const SenderFallback: Story = {
237
185
  name: "Sender Fallback",
238
186
  render: () => (
239
187
  <SheetStage>
240
- <OrganizePanel
188
+ <OrganizeRuleEditor
241
189
  accountId={ACCOUNT_ID}
242
- mailboxId="mbx-inbox"
243
190
  selectedMessageIds={["msg-1", "msg-2"]}
244
- matchPredicate={{
245
- matchOperator: "Or",
246
- literalClauses: [
247
- { field: "From", value: "npm@github.com" },
248
- { field: "From", value: "noreply@medium.com" },
249
- ],
250
- }}
251
- matchedCount={128}
191
+ seedCount={128}
252
192
  semanticUnavailable
253
193
  senders={["npm@github.com", "noreply@medium.com"]}
254
194
  onClose={() => undefined}
@@ -260,23 +200,17 @@ export const SenderFallback: Story = {
260
200
  /**
261
201
  * The sender fallback where every selected sender shares one registrable domain
262
202
  * (RFC 038 D2): the per-address `From` chips collapse to a single `FromDomain`
263
- * clause, and the heading names the domain — "anyone at github.com" — rather
264
- * than listing addresses. Matches `sub.github.com` too, never a look-alike like
265
- * `github.com.evil.example`.
203
+ * chip — "anyone at github.com" — rather than listing addresses. Matches
204
+ * `sub.github.com` too, never a look-alike like `github.com.evil.example`.
266
205
  */
267
206
  export const SenderFallbackDomain: Story = {
268
207
  name: "Sender Fallback (domain)",
269
208
  render: () => (
270
209
  <SheetStage>
271
- <OrganizePanel
210
+ <OrganizeRuleEditor
272
211
  accountId={ACCOUNT_ID}
273
- mailboxId="mbx-inbox"
274
212
  selectedMessageIds={["msg-1", "msg-2", "msg-3"]}
275
- matchPredicate={{
276
- matchOperator: "Or",
277
- literalClauses: [{ field: "FromDomain", value: "github.com" }],
278
- }}
279
- matchedCount={342}
213
+ seedCount={342}
280
214
  semanticUnavailable
281
215
  senders={[
282
216
  "npm@github.com",
@@ -289,29 +223,15 @@ export const SenderFallbackDomain: Story = {
289
223
  ),
290
224
  };
291
225
 
292
- /**
293
- * The sender fallback with a standing rule pre-selected — the "Always keep mail
294
- * from these senders" sentence, the filter that will match future mail at index
295
- * time.
296
- */
226
+ /** The sender fallback with a standing rule pre-selected. */
297
227
  export const SenderFallbackStanding: Story = {
298
228
  name: "Sender Fallback (standing)",
299
229
  render: () => (
300
230
  <SheetStage>
301
- <OrganizePanel
231
+ <OrganizeRuleEditor
302
232
  accountId={ACCOUNT_ID}
303
- mailboxId="mbx-inbox"
304
233
  selectedMessageIds={["msg-1", "msg-2", "msg-3", "msg-4"]}
305
- matchPredicate={{
306
- matchOperator: "Or",
307
- literalClauses: [
308
- { field: "From", value: "npm@github.com" },
309
- { field: "From", value: "notifications@github.com" },
310
- { field: "From", value: "noreply@medium.com" },
311
- { field: "From", value: "digest@substack.com" },
312
- ],
313
- }}
314
- matchedCount={412}
234
+ seedCount={412}
315
235
  semanticUnavailable
316
236
  senders={[
317
237
  "npm@github.com",
@@ -319,7 +239,7 @@ export const SenderFallbackStanding: Story = {
319
239
  "noreply@medium.com",
320
240
  "digest@substack.com",
321
241
  ]}
322
- initialScope="standing"
242
+ seedScope="standing"
323
243
  seedMailboxId="mbx-archive"
324
244
  onClose={() => undefined}
325
245
  />