@remit/web-client 0.0.92 → 0.0.94

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/components/mail/DailyBrief.selection.test.ts +6 -2
  3. package/src/components/mail/DailyBrief.tsx +57 -112
  4. package/src/components/mail/FlaggedList.tsx +42 -0
  5. package/src/components/mail/MailListHeader.tsx +41 -39
  6. package/src/components/mail/MessageList.selection.test.ts +10 -5
  7. package/src/components/mail/MessageList.tsx +78 -82
  8. package/src/components/mail/SelectionWizardHost.tsx +721 -64
  9. package/src/components/mail/ThreadListInteraction.tsx +0 -9
  10. package/src/components/mail/make-filter-entry.test.ts +63 -0
  11. package/src/components/settings/FilterEditor.tsx +1 -1
  12. package/src/hooks/useCreateMailbox.ts +16 -4
  13. package/src/hooks/useFilters.ts +30 -1
  14. package/src/hooks/useMatchSample.ts +63 -0
  15. package/src/hooks/useRulePreview.ts +24 -5
  16. package/src/lib/list-header-chrome.ts +10 -0
  17. package/src/lib/mail-context.ts +0 -9
  18. package/src/lib/organize/organize-model.test.ts +110 -0
  19. package/src/lib/organize/organize-model.ts +69 -0
  20. package/src/lib/organize/rule-model.ts +2 -4
  21. package/src/lib/wizard-history.test.ts +31 -0
  22. package/src/lib/wizard-history.ts +65 -2
  23. package/src/routes/mail.tsx +3 -8
  24. package/src/components/mail/organize/MobileOrganizeFlow.render.test.ts +0 -45
  25. package/src/components/mail/organize/MobileOrganizeFlow.tsx +0 -157
  26. package/src/components/mail/organize/OrganizeDialog.render.test.ts +0 -37
  27. package/src/components/mail/organize/OrganizeDialog.tsx +0 -91
  28. package/src/components/mail/organize/OrganizeRuleEditor.render.test.ts +0 -498
  29. package/src/components/mail/organize/OrganizeRuleEditor.tsx +0 -285
  30. package/src/components/mail/organize/SearchFilterDialog.render.test.ts +0 -47
  31. package/src/components/mail/organize/SearchFilterDialog.tsx +0 -90
  32. package/src/components/mail/organize/SearchFilterEditor.render.test.ts +0 -260
  33. package/src/components/mail/organize/SearchFilterEditor.tsx +0 -136
  34. package/src/components/mail/organize/SomethingElsePanel.render.test.ts +0 -54
  35. package/src/components/mail/organize/SomethingElsePanel.tsx +0 -159
  36. package/src/components/mail/organize/rule-editor-states.stories.tsx +0 -147
  37. package/src/components/mail/organize/rule-editor-states.tsx +0 -139
  38. package/src/components/mail/organize/smart-organize.stories.tsx +0 -342
  39. package/src/hooks/useRuleEditorState.ts +0 -182
  40. package/src/hooks/useSearchFilterSeed.render.test.ts +0 -118
  41. package/src/hooks/useSearchFilterSeed.ts +0 -79
  42. package/src/lib/organize/mobile-organize-flow.test.ts +0 -96
  43. package/src/lib/organize/mobile-organize-flow.ts +0 -73
@@ -1,79 +0,0 @@
1
- import { organizeOperationsPreviewOrganizeMutation } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
2
- import { useMutation } from "@tanstack/react-query";
3
- import { useCallback, useEffect } from "react";
4
- import { buildOrganizeInput } from "@/lib/organize/organize-model";
5
- import { isEvaluablePredicate } from "@/lib/organize/rule-model";
6
- import type { OrganizeMatchPredicate } from "@/lib/organize/sender-fallback";
7
-
8
- interface SearchFilterSeed {
9
- /** The live count for the converted literal predicate, seeding the editor. */
10
- seedCount?: number;
11
- /**
12
- * The converted predicate is one the vector-free matcher refuses — a free-text
13
- * search kept as a `HasWords` clause. There is no count to seed and no request
14
- * to make.
15
- */
16
- uncountable: boolean;
17
- isPending: boolean;
18
- isError: boolean;
19
- error: unknown;
20
- retry: () => void;
21
- }
22
-
23
- /**
24
- * Open the filter-from-search editor: seed the live count from the converted
25
- * literal predicate. One `POST /organize/preview` under the account the filter
26
- * targets — the count on screen is the set a literal-only filter applies to.
27
- *
28
- * A search whose terms convert to a `HasWords` clause has no count to seed: the
29
- * vector-free matcher reads no message bodies and rejects the predicate outright
30
- * (`assertNoBodyContentClause`), so asking is a 500 and a 500 is not a count.
31
- * That search is still a legitimate standing filter — the index-time matcher
32
- * does read bodies — so the editor opens on the uncountable reason and holds only
33
- * the one-time apply, the same way {@link useRulePreview} handles the clause
34
- * being added by hand.
35
- *
36
- * The deployment's semantic reach is not probed here; it is read from the
37
- * search's own "Related" results on the surface that opens the editor (RFC 038
38
- * D5), a direct signal that needs no request and cannot hit the wrong account.
39
- */
40
- export const useSearchFilterSeed = (
41
- accountId: string | undefined,
42
- literalPredicate: OrganizeMatchPredicate,
43
- ): SearchFilterSeed => {
44
- const seed = useMutation(organizeOperationsPreviewOrganizeMutation());
45
- const { mutate, reset } = seed;
46
- const countable = isEvaluablePredicate(literalPredicate);
47
-
48
- const run = useCallback(() => {
49
- if (!accountId || !countable) return;
50
- reset();
51
- mutate({
52
- path: { accountId },
53
- body: buildOrganizeInput({
54
- matchOperator: literalPredicate.matchOperator,
55
- literalClauses: literalPredicate.literalClauses,
56
- }),
57
- });
58
- }, [
59
- accountId,
60
- countable,
61
- literalPredicate.matchOperator,
62
- literalPredicate.literalClauses,
63
- mutate,
64
- reset,
65
- ]);
66
-
67
- useEffect(() => {
68
- run();
69
- }, [run]);
70
-
71
- return {
72
- seedCount: seed.data?.matchedCount,
73
- uncountable: !countable,
74
- isPending: countable && (seed.isPending || seed.data === undefined),
75
- isError: countable && seed.isError,
76
- error: seed.error,
77
- retry: run,
78
- };
79
- };
@@ -1,96 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import {
4
- type OrganizeStageInput,
5
- resolveOrganizeStage,
6
- } from "./mobile-organize-flow";
7
-
8
- const base: OrganizeStageInput = {
9
- entry: "select-similar",
10
- hasSeed: true,
11
- previewStatus: "idle",
12
- matchedCount: undefined,
13
- };
14
-
15
- describe("resolveOrganizeStage — the guided mobile flow's state machine", () => {
16
- it("select-similar shows the widening state before the preview resolves", () => {
17
- assert.deepEqual(resolveOrganizeStage({ ...base, previewStatus: "idle" }), {
18
- kind: "widening",
19
- });
20
- assert.deepEqual(
21
- resolveOrganizeStage({ ...base, previewStatus: "pending" }),
22
- { kind: "widening" },
23
- );
24
- });
25
-
26
- it("opens the organize sentence on the widened set once the preview matches", () => {
27
- assert.deepEqual(
28
- resolveOrganizeStage({
29
- ...base,
30
- previewStatus: "success",
31
- matchedCount: 47,
32
- }),
33
- { kind: "organize", matchedCount: 47, fallback: false },
34
- );
35
- });
36
-
37
- it("falls back to organizing the selection when the widen matches nothing — no dead end", () => {
38
- assert.deepEqual(
39
- resolveOrganizeStage({
40
- ...base,
41
- previewStatus: "success",
42
- matchedCount: 0,
43
- }),
44
- { kind: "organize", matchedCount: 0, fallback: true },
45
- );
46
- // A success with an undefined count is treated as zero, not a crash.
47
- assert.deepEqual(
48
- resolveOrganizeStage({
49
- ...base,
50
- previewStatus: "success",
51
- matchedCount: undefined,
52
- }),
53
- { kind: "organize", matchedCount: 0, fallback: true },
54
- );
55
- });
56
-
57
- it("surfaces the error branch when the widen fails", () => {
58
- assert.deepEqual(
59
- resolveOrganizeStage({ ...base, previewStatus: "error" }),
60
- { kind: "error" },
61
- );
62
- });
63
-
64
- it("something-else shows the shortcuts + input until a seed is chosen", () => {
65
- assert.deepEqual(
66
- resolveOrganizeStage({
67
- entry: "something-else",
68
- hasSeed: false,
69
- previewStatus: "success",
70
- matchedCount: 47,
71
- }),
72
- { kind: "something-else" },
73
- );
74
- });
75
-
76
- it("something-else widens after the seed, then opens the seeded sentence", () => {
77
- assert.deepEqual(
78
- resolveOrganizeStage({
79
- entry: "something-else",
80
- hasSeed: true,
81
- previewStatus: "pending",
82
- matchedCount: undefined,
83
- }),
84
- { kind: "widening" },
85
- );
86
- assert.deepEqual(
87
- resolveOrganizeStage({
88
- entry: "something-else",
89
- hasSeed: true,
90
- previewStatus: "success",
91
- matchedCount: 12,
92
- }),
93
- { kind: "organize", matchedCount: 12, fallback: false },
94
- );
95
- });
96
- });
@@ -1,73 +0,0 @@
1
- import type { OrganizeScope } from "./organize-model";
2
-
3
- /**
4
- * How the guided mobile flow was entered from the selection sheet:
5
- *
6
- * - `select-similar` — widen straight into the organize sentence.
7
- * - `something-else` — collect a folder/scope seed from shortcuts or a
8
- * plain-language input first, then widen into the seeded sentence.
9
- */
10
- export type OrganizeEntry = "select-similar" | "something-else";
11
-
12
- /** The read-only widen preview's lifecycle (POST /organize/preview). */
13
- export type PreviewStatus = "idle" | "pending" | "success" | "error";
14
-
15
- /** A folder/scope the "Something else" panel pre-fills the sentence with. */
16
- export interface OrganizeSeed {
17
- moveMailboxId?: string;
18
- scope?: OrganizeScope;
19
- }
20
-
21
- /**
22
- * The stage the guided flow renders, resolved from the entry, whether a
23
- * "Something else" seed has been chosen yet, and where the widen preview is:
24
- *
25
- * - `something-else` — the shortcuts + input, shown until a seed is picked.
26
- * - `widening` — the brief widening state between the tap and the sentence.
27
- * - `error` — the widen failed; a dead end only in the sense that it offers a
28
- * close, never a broken sentence.
29
- * - `organize` — the committed sentence, on the widened set. `fallback` is true
30
- * when the widen matched nothing, so the sentence organizes just the
31
- * selection instead of a dead end (issue #211).
32
- */
33
- export type OrganizeStage =
34
- | { kind: "something-else" }
35
- | { kind: "widening" }
36
- | { kind: "error" }
37
- | { kind: "organize"; matchedCount: number; fallback: boolean };
38
-
39
- export interface OrganizeStageInput {
40
- entry: OrganizeEntry;
41
- /** "Something else" only: whether the user has chosen a seed yet. */
42
- hasSeed: boolean;
43
- previewStatus: PreviewStatus;
44
- /** The widen's matched total; defined once `previewStatus` is `success`. */
45
- matchedCount: number | undefined;
46
- }
47
-
48
- /**
49
- * The pure state machine behind the guided organize sheet:
50
- * `idle → widening → organize` for select-similar, with a `something-else`
51
- * seeding step in front for that entry, an `error` branch when the widen
52
- * fails, and the zero-match `fallback` that keeps the organize sentence usable
53
- * on the selection alone. Kept pure so every transition is testable without a
54
- * DOM, React Query, or a network — the component only reads the stage back.
55
- */
56
- export const resolveOrganizeStage = ({
57
- entry,
58
- hasSeed,
59
- previewStatus,
60
- matchedCount,
61
- }: OrganizeStageInput): OrganizeStage => {
62
- if (entry === "something-else" && !hasSeed) {
63
- return { kind: "something-else" };
64
- }
65
- if (previewStatus === "error") {
66
- return { kind: "error" };
67
- }
68
- if (previewStatus !== "success") {
69
- return { kind: "widening" };
70
- }
71
- const matched = matchedCount ?? 0;
72
- return { kind: "organize", matchedCount: matched, fallback: matched === 0 };
73
- };