@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.
- package/package.json +1 -1
- package/src/components/mail/DailyBrief.selection.test.ts +6 -2
- package/src/components/mail/DailyBrief.tsx +57 -112
- package/src/components/mail/FlaggedList.tsx +42 -0
- package/src/components/mail/MailListHeader.tsx +41 -39
- package/src/components/mail/MessageList.selection.test.ts +10 -5
- package/src/components/mail/MessageList.tsx +78 -82
- package/src/components/mail/SelectionWizardHost.tsx +721 -64
- package/src/components/mail/ThreadListInteraction.tsx +0 -9
- package/src/components/mail/make-filter-entry.test.ts +63 -0
- package/src/components/settings/FilterEditor.tsx +1 -1
- package/src/hooks/useCreateMailbox.ts +16 -4
- package/src/hooks/useFilters.ts +30 -1
- package/src/hooks/useMatchSample.ts +63 -0
- package/src/hooks/useRulePreview.ts +24 -5
- package/src/lib/list-header-chrome.ts +10 -0
- package/src/lib/mail-context.ts +0 -9
- package/src/lib/organize/organize-model.test.ts +110 -0
- package/src/lib/organize/organize-model.ts +69 -0
- package/src/lib/organize/rule-model.ts +2 -4
- package/src/lib/wizard-history.test.ts +31 -0
- package/src/lib/wizard-history.ts +65 -2
- package/src/routes/mail.tsx +3 -8
- package/src/components/mail/organize/MobileOrganizeFlow.render.test.ts +0 -45
- package/src/components/mail/organize/MobileOrganizeFlow.tsx +0 -157
- package/src/components/mail/organize/OrganizeDialog.render.test.ts +0 -37
- package/src/components/mail/organize/OrganizeDialog.tsx +0 -91
- package/src/components/mail/organize/OrganizeRuleEditor.render.test.ts +0 -498
- package/src/components/mail/organize/OrganizeRuleEditor.tsx +0 -285
- package/src/components/mail/organize/SearchFilterDialog.render.test.ts +0 -47
- package/src/components/mail/organize/SearchFilterDialog.tsx +0 -90
- package/src/components/mail/organize/SearchFilterEditor.render.test.ts +0 -260
- package/src/components/mail/organize/SearchFilterEditor.tsx +0 -136
- package/src/components/mail/organize/SomethingElsePanel.render.test.ts +0 -54
- package/src/components/mail/organize/SomethingElsePanel.tsx +0 -159
- package/src/components/mail/organize/rule-editor-states.stories.tsx +0 -147
- package/src/components/mail/organize/rule-editor-states.tsx +0 -139
- package/src/components/mail/organize/smart-organize.stories.tsx +0 -342
- package/src/hooks/useRuleEditorState.ts +0 -182
- package/src/hooks/useSearchFilterSeed.render.test.ts +0 -118
- package/src/hooks/useSearchFilterSeed.ts +0 -79
- package/src/lib/organize/mobile-organize-flow.test.ts +0 -96
- 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
|
-
};
|