@remit/web-client 0.0.94 → 0.0.95

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.
@@ -6,7 +6,6 @@ import {
6
6
  countMatches,
7
7
  type FetchIdsPageResult,
8
8
  honestProgress,
9
- resolveSelectionAfterRun,
10
9
  runChunkedAction,
11
10
  runPredicateAction,
12
11
  } from "./bulk-actions.js";
@@ -355,65 +354,6 @@ describe("countMatches", () => {
355
354
  });
356
355
  });
357
356
 
358
- describe("resolveSelectionAfterRun", () => {
359
- test("a clean run with nothing failed exits selection mode", () => {
360
- assert.deepEqual(
361
- resolveSelectionAfterRun({
362
- done: 3412,
363
- failedIds: [],
364
- cancelled: false,
365
- }),
366
- { exit: true, retryIds: [] },
367
- );
368
- });
369
-
370
- test("unreached ids stay selected for a precise retry, even alongside a clean stop", () => {
371
- assert.deepEqual(
372
- resolveSelectionAfterRun({
373
- done: 3072,
374
- failedIds: ["a", "b"],
375
- cancelled: false,
376
- }),
377
- { exit: false, retryIds: ["a", "b"] },
378
- );
379
- });
380
-
381
- test("a clean cancel with nothing yet confirmed failed leaves nothing to retry, but does not exit", () => {
382
- assert.deepEqual(
383
- resolveSelectionAfterRun({
384
- done: 100,
385
- failedIds: [],
386
- cancelled: true,
387
- }),
388
- { exit: false, retryIds: [] },
389
- );
390
- });
391
-
392
- test("an infra failure with nothing left unreached still keeps selection mode open", () => {
393
- assert.deepEqual(
394
- resolveSelectionAfterRun({
395
- done: 0,
396
- failedIds: [],
397
- cancelled: false,
398
- error: new Error("network blip"),
399
- }),
400
- { exit: false, retryIds: [] },
401
- );
402
- });
403
-
404
- test("failedIds wins over cancelled/error when both are present", () => {
405
- assert.deepEqual(
406
- resolveSelectionAfterRun({
407
- done: 10,
408
- failedIds: ["x"],
409
- cancelled: true,
410
- error: new Error("boom"),
411
- }),
412
- { exit: false, retryIds: ["x"] },
413
- );
414
- });
415
- });
416
-
417
357
  describe("honestProgress", () => {
418
358
  // Regression for #109: `countMatches` and `runPredicateAction` page the
419
359
  // same predicate independently, so the delete can outrun the frozen
@@ -275,29 +275,3 @@ export interface BulkRunOutcome {
275
275
  cancelled: boolean;
276
276
  error?: unknown;
277
277
  }
278
-
279
- export interface SelectionAfterRun {
280
- /** The action reached everything targeted — selection mode should exit. */
281
- exit: boolean;
282
- /** The bounded selection to leave in place, empty when exiting. */
283
- retryIds: string[];
284
- }
285
-
286
- /**
287
- * What a caller does with selection once a run ends, for any reason. Every id
288
- * the action never reached — a chunk the bounded run skipped because it was
289
- * stopped or errored — belongs in `retryIds`: it is exactly what Retry should
290
- * resend, and it is what stays selected so the count on screen never claims
291
- * more was done than actually was (#92 requirement 8).
292
- */
293
- export const resolveSelectionAfterRun = (
294
- outcome: BulkRunOutcome,
295
- ): SelectionAfterRun => {
296
- if (outcome.failedIds.length > 0) {
297
- return { exit: false, retryIds: outcome.failedIds };
298
- }
299
- if (outcome.cancelled || outcome.error) {
300
- return { exit: false, retryIds: [] };
301
- }
302
- return { exit: true, retryIds: [] };
303
- };
@@ -119,31 +119,4 @@ describe("formatDeleteToTrashTitle", () => {
119
119
  "Move 3,412 messages to Trash?",
120
120
  );
121
121
  });
122
-
123
- // #109: an escalated-predicate count is paged once by `countMatches` and
124
- // re-paged independently by the delete itself — never provably the number
125
- // that gets deleted. `isEstimate` says so instead of stating an exact
126
- // number the run may not honour.
127
- describe("isEstimate (#109 — an escalated-predicate count, not a materialized selection)", () => {
128
- test("prefixes 'about' for a plural estimate", () => {
129
- assert.strictEqual(
130
- formatDeleteToTrashTitle(3412, true),
131
- "Move about 3,412 messages to Trash?",
132
- );
133
- });
134
-
135
- test("prefixes 'about' for a singular estimate", () => {
136
- assert.strictEqual(
137
- formatDeleteToTrashTitle(1, true),
138
- "Move about 1 message to Trash?",
139
- );
140
- });
141
-
142
- test("defaults to false — a bounded selection's count stays exact", () => {
143
- assert.strictEqual(
144
- formatDeleteToTrashTitle(3412),
145
- formatDeleteToTrashTitle(3412, false),
146
- );
147
- });
148
- });
149
122
  });
package/src/lib/format.ts CHANGED
@@ -152,21 +152,11 @@ export const formatEmailDate = (date: Date | string | number): string => {
152
152
  /**
153
153
  * Confirmation title for the move-to-Trash delete flow. Reflects that delete
154
154
  * moves messages to Trash (not a permanent delete) and pluralizes on count.
155
- * Thousands-separated: at escalated-selection scale this is exactly the digit
156
- * count someone checks against what they meant to select.
157
- *
158
- * `isEstimate` marks an escalated-predicate count (#109): it was paged to a
159
- * total once, and the delete itself re-pages the same predicate independently
160
- * — mail arriving or leaving between the two can make them differ. "about"
161
- * says so up front instead of stating a number the run may not honour; a
162
- * materialized (bounded) selection's count is exact and never passes it.
155
+ * The count is always a concrete list of ids — every predicate delete ends on
156
+ * the wizard's review screen instead, which states what the predicate covers.
163
157
  */
164
- export const formatDeleteToTrashTitle = (
165
- count: number,
166
- isEstimate = false,
167
- ): string => {
158
+ export const formatDeleteToTrashTitle = (count: number): string => {
168
159
  const quantity = count === 1 ? "1" : formatNumber(count);
169
160
  const noun = count === 1 ? "message" : "messages";
170
- const prefix = isEstimate ? "about " : "";
171
- return `Move ${prefix}${quantity} ${noun} to Trash?`;
161
+ return `Move ${quantity} ${noun} to Trash?`;
172
162
  };
@@ -6,7 +6,7 @@ import type {
6
6
  RemitImapOrganizeInput,
7
7
  } from "@remit/api-http-client/types.gen.ts";
8
8
  import type {
9
- MatchMode,
9
+ MatchDoor,
10
10
  MatchOperator,
11
11
  RuleClause,
12
12
  RuleScope,
@@ -105,7 +105,13 @@ export const buildOrganizeInput = (
105
105
  * it holds, and the four `OrganizeScope` values fall out of the pair.
106
106
  */
107
107
  export interface WizardCommitAnswers {
108
- mode: MatchMode;
108
+ /**
109
+ * The door the match came through. An escalated predicate is not one: it has
110
+ * no clauses to build a rule from and no anchor to widen, so a scope
111
+ * reconstructed for it would be a rule matching everything. Typed to the
112
+ * three doors so that cannot be written rather than merely not written.
113
+ */
114
+ mode: MatchDoor;
109
115
  /** Absent on the verbs that never reach the scope step — those act once. */
110
116
  ruleScope?: RuleScope;
111
117
  }
@@ -17,9 +17,9 @@
17
17
  * rewinds every entry the wizard owns.
18
18
  */
19
19
 
20
- import { type StepId, stepIndex } from "@remit/ui";
20
+ import { type StepId, stepIndex, type Verb } from "@remit/ui";
21
21
  import { useNavigate, useRouter, useSearch } from "@tanstack/react-router";
22
- import { useCallback, useEffect, useRef } from "react";
22
+ import { useCallback, useEffect, useRef, useState } from "react";
23
23
  import { z } from "zod";
24
24
 
25
25
  const stepId = z.enum([
@@ -101,6 +101,46 @@ export const useOpenWizard = (): ((
101
101
  );
102
102
  };
103
103
 
104
+ export interface SelectionWizardControl {
105
+ /** The verb the wizard was opened for, for the host that renders it. */
106
+ verb: Verb;
107
+ /** Opens the wizard on a verb, from the match step — every bar verb, and
108
+ * every keyboard verb aimed at a selection, comes through here. */
109
+ start: (verb: Verb) => void;
110
+ /** Opens it on the search entry instead: the property step, with the query
111
+ * converted (#477 1.8). The route for a verb whose match is a predicate
112
+ * rather than a set of clauses. */
113
+ startFromSearch: () => void;
114
+ /**
115
+ * A step is held, so the wizard owns the screen. Every selection surface
116
+ * suspends its keyboard layer on this: a shortcut acting behind the screen
117
+ * already asking about an action is a second flow nobody asked for.
118
+ */
119
+ isOpen: boolean;
120
+ }
121
+
122
+ /**
123
+ * The wizard as a selection surface drives it. The step lives in the URL and
124
+ * the verb beside it in state, in one place rather than one copy per surface —
125
+ * three of them had drifted apart on which verbs they even offered.
126
+ */
127
+ export const useSelectionWizard = (): SelectionWizardControl => {
128
+ const step = useWizardStepValue();
129
+ const openWizard = useOpenWizard();
130
+ const [verb, setVerb] = useState<Verb>("organize");
131
+ const start = useCallback(
132
+ (next: Verb) => {
133
+ setVerb(next);
134
+ openWizard("match");
135
+ },
136
+ [openWizard],
137
+ );
138
+ const startFromSearch = useCallback(() => {
139
+ openWizard("properties", "search");
140
+ }, [openWizard]);
141
+ return { verb, start, startFromSearch, isOpen: step !== undefined };
142
+ };
143
+
104
144
  export interface WizardStepNavigation {
105
145
  step: StepId | undefined;
106
146
  goToStep: (step: StepId) => void;