@remit/web-client 0.0.91 → 0.0.93
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 +47 -110
- package/src/components/mail/MessageList.selection.test.ts +18 -6
- package/src/components/mail/MessageList.tsx +83 -83
- package/src/components/mail/SelectionWizardHost.tsx +779 -0
- package/src/components/mail/organize/SearchFilterEditor.tsx +5 -1
- 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 +23 -3
- 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 -0
- package/src/lib/selection-mode.test.ts +20 -6
- package/src/lib/selection-mode.ts +8 -1
- package/src/lib/wizard-history.test.ts +176 -0
- package/src/lib/wizard-history.ts +131 -0
- package/src/routes/mail.tsx +4 -0
- 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/SomethingElsePanel.render.test.ts +0 -54
- package/src/components/mail/organize/SomethingElsePanel.tsx +0 -159
- package/src/components/mail/organize/smart-organize.stories.tsx +0 -342
- package/src/lib/organize/mobile-organize-flow.test.ts +0 -96
- package/src/lib/organize/mobile-organize-flow.ts +0 -73
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selection wizard's step in the URL, and the history entries it owns
|
|
3
|
+
* (#477 clause 1.6). TanStack Router owns history here: the step is a validated
|
|
4
|
+
* `wizard` search param on /mail, pushed with `navigate({ search })`, so
|
|
5
|
+
* `router.state.location` and `window.location` never disagree.
|
|
6
|
+
*
|
|
7
|
+
* The wizard is rooted on an entry that carries no step, so the entries it owns
|
|
8
|
+
* are the pushes that reached the current step: one for the opening step, one
|
|
9
|
+
* per advance. A document that loads straight into a step has no such root, and
|
|
10
|
+
* no answers behind the step either, so it is re-rooted at the opening step
|
|
11
|
+
* before anything else — otherwise the number of entries to rewind is a guess
|
|
12
|
+
* about a tab this code never saw.
|
|
13
|
+
*
|
|
14
|
+
* Which steps Back leaves the wizard from is `backExits` in the step model, and
|
|
15
|
+
* the shell reads it to route Back to exit rather than to a step. These are the
|
|
16
|
+
* two movements it routes between: `goBack` pops one entry, `closeWizard`
|
|
17
|
+
* rewinds every entry the wizard owns.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { type StepId, stepIndex } from "@remit/ui";
|
|
21
|
+
import { useNavigate, useRouter, useSearch } from "@tanstack/react-router";
|
|
22
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
|
|
25
|
+
const stepId = z.enum([
|
|
26
|
+
"match",
|
|
27
|
+
"properties",
|
|
28
|
+
"folder",
|
|
29
|
+
"rule",
|
|
30
|
+
"name",
|
|
31
|
+
"review",
|
|
32
|
+
"run",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const wizardStepFromParam = (value: unknown): StepId | undefined => {
|
|
36
|
+
const parsed = stepId.safeParse(value);
|
|
37
|
+
return parsed.success ? parsed.data : undefined;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The route's `wizard` field. A value the wizard cannot be on reads as no step
|
|
42
|
+
* rather than as a validation failure, so a truncated or hand-typed link lands
|
|
43
|
+
* on the mail list instead of the router's error screen.
|
|
44
|
+
*/
|
|
45
|
+
export const wizardStepValue = z.unknown().transform(wizardStepFromParam);
|
|
46
|
+
|
|
47
|
+
export const ownedHistoryEntries = (
|
|
48
|
+
steps: readonly StepId[],
|
|
49
|
+
step: StepId,
|
|
50
|
+
): number => stepIndex(steps, step) + 1;
|
|
51
|
+
|
|
52
|
+
export const useWizardStepValue = (): StepId | undefined =>
|
|
53
|
+
useSearch({ from: "/mail", select: (search) => search.wizard });
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Opens the wizard on a step, from a surface that does not drive it — a verb on
|
|
57
|
+
* the selection bar. The push is the wizard's first owned entry, so the back
|
|
58
|
+
* that leaves it lands on the list with the selection still ticked.
|
|
59
|
+
*/
|
|
60
|
+
export const useOpenWizard = (): ((step: StepId) => void) => {
|
|
61
|
+
const navigate = useNavigate();
|
|
62
|
+
return useCallback(
|
|
63
|
+
(step: StepId) => {
|
|
64
|
+
navigate({ to: ".", search: (prev) => ({ ...prev, wizard: step }) });
|
|
65
|
+
},
|
|
66
|
+
[navigate],
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export interface WizardStepNavigation {
|
|
71
|
+
step: StepId | undefined;
|
|
72
|
+
goToStep: (step: StepId) => void;
|
|
73
|
+
goBack: () => void;
|
|
74
|
+
closeWizard: (steps: readonly StepId[], step: StepId) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const useWizardStep = (openingStep: StepId): WizardStepNavigation => {
|
|
78
|
+
const router = useRouter();
|
|
79
|
+
const navigate = useNavigate();
|
|
80
|
+
const step = useWizardStepValue();
|
|
81
|
+
// Whether this document loaded already holding a step, which is the one
|
|
82
|
+
// entrance that leaves the wizard unrooted. A step the app itself pushed
|
|
83
|
+
// arrives rooted, so re-rooting it would duplicate the entry underneath it.
|
|
84
|
+
const loadedHoldingStep = useRef(step !== undefined);
|
|
85
|
+
const pushedTo = useRef<StepId | undefined>(undefined);
|
|
86
|
+
|
|
87
|
+
// Two taps on Continue land before the URL settles, and both would push the
|
|
88
|
+
// same step — leaving a back that appears to do nothing.
|
|
89
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: the guard is cleared because the step changed, which is only knowable from the dependency.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
pushedTo.current = undefined;
|
|
92
|
+
}, [step]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!loadedHoldingStep.current) return;
|
|
96
|
+
loadedHoldingStep.current = false;
|
|
97
|
+
void (async () => {
|
|
98
|
+
await navigate({
|
|
99
|
+
to: ".",
|
|
100
|
+
search: (prev) => ({ ...prev, wizard: undefined }),
|
|
101
|
+
replace: true,
|
|
102
|
+
});
|
|
103
|
+
await navigate({
|
|
104
|
+
to: ".",
|
|
105
|
+
search: (prev) => ({ ...prev, wizard: openingStep }),
|
|
106
|
+
});
|
|
107
|
+
})();
|
|
108
|
+
}, [openingStep, navigate]);
|
|
109
|
+
|
|
110
|
+
const goToStep = useCallback(
|
|
111
|
+
(next: StepId) => {
|
|
112
|
+
if (next === step || pushedTo.current === next) return;
|
|
113
|
+
pushedTo.current = next;
|
|
114
|
+
navigate({ to: ".", search: (prev) => ({ ...prev, wizard: next }) });
|
|
115
|
+
},
|
|
116
|
+
[navigate, step],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const goBack = useCallback(() => {
|
|
120
|
+
router.history.back();
|
|
121
|
+
}, [router]);
|
|
122
|
+
|
|
123
|
+
const closeWizard = useCallback(
|
|
124
|
+
(steps: readonly StepId[], current: StepId) => {
|
|
125
|
+
router.history.go(-ownedHistoryEntries(steps, current));
|
|
126
|
+
},
|
|
127
|
+
[router],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return { step, goToStep, goBack, closeWizard };
|
|
131
|
+
};
|
package/src/routes/mail.tsx
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
searchInputForView,
|
|
44
44
|
shouldMirrorQuery,
|
|
45
45
|
} from "@/lib/search-view";
|
|
46
|
+
import { wizardStepValue } from "@/lib/wizard-history";
|
|
46
47
|
import "@/lib/client";
|
|
47
48
|
|
|
48
49
|
// `MailContext` / `useMailContext` live in `@/lib/mail-context` so the provider
|
|
@@ -53,6 +54,9 @@ export { useMailContext } from "@/lib/mail-context";
|
|
|
53
54
|
|
|
54
55
|
const mailSearchSchema = z.object({
|
|
55
56
|
q: z.string().optional(),
|
|
57
|
+
// The selection wizard's step (#477 clause 1.6). The router owns history, so
|
|
58
|
+
// the step is a validated search param rather than a raw pushState entry.
|
|
59
|
+
wizard: wizardStepValue,
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
export const Route = createFileRoute("/mail")({
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
-
import React, { createElement } from "react";
|
|
5
|
-
import { renderToString } from "react-dom/server";
|
|
6
|
-
import { ErrorBannerProvider } from "@/components/ui/ErrorBannerProvider";
|
|
7
|
-
import type { OrganizeEntry } from "@/lib/organize/mobile-organize-flow";
|
|
8
|
-
import { MobileOrganizeFlow } from "./MobileOrganizeFlow";
|
|
9
|
-
|
|
10
|
-
// The node test loader transpiles remit-ui's `.tsx` with the classic JSX
|
|
11
|
-
// runtime, which references a global `React`. Vite uses the automatic runtime,
|
|
12
|
-
// so this shim only exists for the SSR test harness.
|
|
13
|
-
(globalThis as { React?: typeof React }).React = React;
|
|
14
|
-
|
|
15
|
-
const render = (entry: OrganizeEntry) =>
|
|
16
|
-
renderToString(
|
|
17
|
-
createElement(
|
|
18
|
-
QueryClientProvider,
|
|
19
|
-
{ client: new QueryClient() },
|
|
20
|
-
createElement(
|
|
21
|
-
ErrorBannerProvider,
|
|
22
|
-
null,
|
|
23
|
-
createElement(MobileOrganizeFlow, {
|
|
24
|
-
entry,
|
|
25
|
-
accountId: "acc-1",
|
|
26
|
-
selectedMessageIds: ["msg-1", "msg-2"],
|
|
27
|
-
selectedSenders: ["npm@github.com"],
|
|
28
|
-
onClose: () => undefined,
|
|
29
|
-
}),
|
|
30
|
-
),
|
|
31
|
-
) as never,
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
describe("MobileOrganizeFlow", () => {
|
|
35
|
-
it("select-similar opens on the widening state before the preview resolves", () => {
|
|
36
|
-
const html = render("select-similar");
|
|
37
|
-
assert.match(html, /Finding similar messages/);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("something-else opens on the shortcuts + plain-language input", () => {
|
|
41
|
-
const html = render("something-else");
|
|
42
|
-
assert.match(html, /What should Remit do\?/);
|
|
43
|
-
assert.match(html, /Tell Remit what to do/);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { mailboxOperationsListMailboxesOptions } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
2
|
-
import type { RuleScope } from "@remit/ui";
|
|
3
|
-
import { BottomSheet, Button } from "@remit/ui";
|
|
4
|
-
import { useQuery } from "@tanstack/react-query";
|
|
5
|
-
import { Loader2 } from "lucide-react";
|
|
6
|
-
import { useEffect, useMemo, useState } from "react";
|
|
7
|
-
import { useOrganizeWiden } from "@/hooks/useOrganizeWiden";
|
|
8
|
-
import { getMailboxDisplayName } from "@/lib/folder-roles";
|
|
9
|
-
import { buildMoveTargets } from "@/lib/move-targets";
|
|
10
|
-
import {
|
|
11
|
-
type OrganizeEntry,
|
|
12
|
-
type OrganizeSeed,
|
|
13
|
-
type PreviewStatus,
|
|
14
|
-
resolveOrganizeStage,
|
|
15
|
-
} from "@/lib/organize/mobile-organize-flow";
|
|
16
|
-
import { OrganizeRuleEditor } from "./OrganizeRuleEditor";
|
|
17
|
-
import { SomethingElsePanel } from "./SomethingElsePanel";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Map a "Something else" seed's scope onto the rule editor's scope. Only the
|
|
21
|
-
* standing shortcut carries one; everything else lets the editor default.
|
|
22
|
-
*/
|
|
23
|
-
const seedRuleScope = (
|
|
24
|
-
seed: OrganizeSeed | undefined,
|
|
25
|
-
): RuleScope | undefined =>
|
|
26
|
-
seed?.scope === "standing" ? "standing" : undefined;
|
|
27
|
-
|
|
28
|
-
interface MobileOrganizeFlowProps {
|
|
29
|
-
entry: OrganizeEntry;
|
|
30
|
-
accountId: string;
|
|
31
|
-
selectedMessageIds: string[];
|
|
32
|
-
/**
|
|
33
|
-
* Sender addresses of the selected messages, driving the literal fallback on
|
|
34
|
-
* a deployment without the vector pipeline (semantic-capability.ts).
|
|
35
|
-
*/
|
|
36
|
-
selectedSenders: string[];
|
|
37
|
-
junkMailboxId?: string;
|
|
38
|
-
/** Close the flow and return to the list — dismiss, "Not now", and Done all use it. */
|
|
39
|
-
onClose: () => void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const previewStatusOf = (
|
|
43
|
-
isError: boolean,
|
|
44
|
-
isPending: boolean,
|
|
45
|
-
matchedCount: number | undefined,
|
|
46
|
-
): PreviewStatus => {
|
|
47
|
-
if (isError) return "error";
|
|
48
|
-
if (isPending) return "pending";
|
|
49
|
-
return matchedCount !== undefined ? "success" : "idle";
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* The guided select-similar → organize flow, the mobile home for organizing
|
|
54
|
-
* (issue #211). Entered from the selection sheet, it widens the selection once
|
|
55
|
-
* with the read-only matcher (POST /organize/preview), shows a brief widening
|
|
56
|
-
* state, and renders the organize sentence inside a bottom sheet on that
|
|
57
|
-
* widened set — the same {@link OrganizePanel} the desktop dialog uses, so the
|
|
58
|
-
* two never drift. "Something else" collects a folder/scope seed first; a widen
|
|
59
|
-
* that matches nothing falls back to organizing the selection. Desktop keeps
|
|
60
|
-
* its `OrganizeDialog` — this is the touch surface only.
|
|
61
|
-
*/
|
|
62
|
-
export function MobileOrganizeFlow({
|
|
63
|
-
entry,
|
|
64
|
-
accountId,
|
|
65
|
-
selectedMessageIds,
|
|
66
|
-
selectedSenders,
|
|
67
|
-
junkMailboxId,
|
|
68
|
-
onClose,
|
|
69
|
-
}: MobileOrganizeFlowProps) {
|
|
70
|
-
const anchorMessageId = selectedMessageIds[0];
|
|
71
|
-
const [seed, setSeed] = useState<OrganizeSeed | undefined>();
|
|
72
|
-
|
|
73
|
-
const {
|
|
74
|
-
preview,
|
|
75
|
-
matchedCount,
|
|
76
|
-
semanticUnavailable,
|
|
77
|
-
senders,
|
|
78
|
-
isPending,
|
|
79
|
-
isError,
|
|
80
|
-
error,
|
|
81
|
-
} = useOrganizeWiden(accountId, anchorMessageId, selectedSenders);
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
preview();
|
|
85
|
-
}, [preview]);
|
|
86
|
-
|
|
87
|
-
const { data: mailboxesData } = useQuery({
|
|
88
|
-
...mailboxOperationsListMailboxesOptions({ path: { accountId } }),
|
|
89
|
-
staleTime: Number.POSITIVE_INFINITY,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const folderOptions = useMemo(
|
|
93
|
-
() =>
|
|
94
|
-
buildMoveTargets(mailboxesData?.items ?? []).map((mailbox) => ({
|
|
95
|
-
id: mailbox.mailboxId,
|
|
96
|
-
label: getMailboxDisplayName(mailbox.fullPath),
|
|
97
|
-
})),
|
|
98
|
-
[mailboxesData?.items],
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const stage = resolveOrganizeStage({
|
|
102
|
-
entry,
|
|
103
|
-
hasSeed: seed !== undefined,
|
|
104
|
-
previewStatus: previewStatusOf(isError, isPending, matchedCount),
|
|
105
|
-
matchedCount,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<BottomSheet open onClose={onClose} dismissLabel="Dismiss organize">
|
|
110
|
-
{stage.kind === "something-else" && (
|
|
111
|
-
<SomethingElsePanel
|
|
112
|
-
folderOptions={folderOptions}
|
|
113
|
-
junkMailboxId={junkMailboxId}
|
|
114
|
-
onSeed={setSeed}
|
|
115
|
-
/>
|
|
116
|
-
)}
|
|
117
|
-
|
|
118
|
-
{stage.kind === "widening" && <WideningState />}
|
|
119
|
-
|
|
120
|
-
{stage.kind === "error" && (
|
|
121
|
-
<div className="flex flex-col items-center gap-3 px-5 py-10 text-center">
|
|
122
|
-
<p className="text-sm font-medium text-danger">
|
|
123
|
-
Couldn't find similar messages
|
|
124
|
-
</p>
|
|
125
|
-
<p className="max-w-xs text-xs text-fg-muted">
|
|
126
|
-
{error instanceof Error ? error.message : "Please try again."}
|
|
127
|
-
</p>
|
|
128
|
-
<Button variant="ghost" onClick={onClose} className="mt-2">
|
|
129
|
-
Close
|
|
130
|
-
</Button>
|
|
131
|
-
</div>
|
|
132
|
-
)}
|
|
133
|
-
|
|
134
|
-
{stage.kind === "organize" && (
|
|
135
|
-
<OrganizeRuleEditor
|
|
136
|
-
accountId={accountId}
|
|
137
|
-
selectedMessageIds={selectedMessageIds}
|
|
138
|
-
seedCount={stage.matchedCount}
|
|
139
|
-
seedScope={seedRuleScope(seed)}
|
|
140
|
-
seedMailboxId={seed?.moveMailboxId}
|
|
141
|
-
semanticUnavailable={semanticUnavailable}
|
|
142
|
-
senders={senders}
|
|
143
|
-
onClose={onClose}
|
|
144
|
-
/>
|
|
145
|
-
)}
|
|
146
|
-
</BottomSheet>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function WideningState() {
|
|
151
|
-
return (
|
|
152
|
-
<div className="flex flex-col items-center gap-3 px-5 py-10 text-center">
|
|
153
|
-
<Loader2 className="size-8 animate-spin text-accent-2" />
|
|
154
|
-
<p className="text-sm font-medium text-fg">Finding similar messages…</p>
|
|
155
|
-
</div>
|
|
156
|
-
);
|
|
157
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
-
import React, { createElement } from "react";
|
|
5
|
-
import { renderToString } from "react-dom/server";
|
|
6
|
-
import { OrganizeDialog } from "./OrganizeDialog";
|
|
7
|
-
|
|
8
|
-
// The node test loader transpiles remit-ui's `.tsx` with the classic JSX
|
|
9
|
-
// runtime, which references a global `React`. Vite uses the automatic runtime,
|
|
10
|
-
// so this shim only exists for the SSR test harness.
|
|
11
|
-
(globalThis as { React?: typeof React }).React = React;
|
|
12
|
-
|
|
13
|
-
const render = (open: boolean) =>
|
|
14
|
-
renderToString(
|
|
15
|
-
createElement(
|
|
16
|
-
QueryClientProvider,
|
|
17
|
-
{ client: new QueryClient() },
|
|
18
|
-
createElement(OrganizeDialog, {
|
|
19
|
-
open,
|
|
20
|
-
accountId: "acc-1",
|
|
21
|
-
selectedMessageIds: ["msg-1", "msg-2"],
|
|
22
|
-
selectedSenders: ["npm@github.com"],
|
|
23
|
-
onClose: () => undefined,
|
|
24
|
-
}),
|
|
25
|
-
) as never,
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
describe("OrganizeDialog", () => {
|
|
29
|
-
it("renders nothing when closed", () => {
|
|
30
|
-
assert.equal(render(false), "");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("shows the widen step while the preview is in flight", () => {
|
|
34
|
-
const html = render(true);
|
|
35
|
-
assert.match(html, /Finding similar messages/);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { Button, Dialog } from "@remit/ui";
|
|
2
|
-
import { Loader2 } from "lucide-react";
|
|
3
|
-
import { useEffect } from "react";
|
|
4
|
-
import { useOrganizeWiden } from "@/hooks/useOrganizeWiden";
|
|
5
|
-
import { OrganizeRuleEditor } from "./OrganizeRuleEditor";
|
|
6
|
-
|
|
7
|
-
interface OrganizeDialogProps {
|
|
8
|
-
open: boolean;
|
|
9
|
-
accountId: string;
|
|
10
|
-
selectedMessageIds: string[];
|
|
11
|
-
/**
|
|
12
|
-
* Sender addresses of the selected messages, driving the literal fallback on
|
|
13
|
-
* a deployment without the vector pipeline (semantic-capability.ts).
|
|
14
|
-
*/
|
|
15
|
-
selectedSenders: string[];
|
|
16
|
-
onClose: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Smart-organize flow entered from the selection toolbar. Widens the selection
|
|
21
|
-
* once (POST /organize/preview) to seed the rule, then hands off to the chip
|
|
22
|
-
* editor (RFC 038 D1), which counts and commits over the same endpoints. The
|
|
23
|
-
* widen is only the opening count; the editor re-previews every edit.
|
|
24
|
-
*/
|
|
25
|
-
export function OrganizeDialog({
|
|
26
|
-
open,
|
|
27
|
-
accountId,
|
|
28
|
-
selectedMessageIds,
|
|
29
|
-
selectedSenders,
|
|
30
|
-
onClose,
|
|
31
|
-
}: OrganizeDialogProps) {
|
|
32
|
-
const anchorMessageId = selectedMessageIds[0];
|
|
33
|
-
const {
|
|
34
|
-
preview,
|
|
35
|
-
reset,
|
|
36
|
-
matchedCount,
|
|
37
|
-
semanticUnavailable,
|
|
38
|
-
senders,
|
|
39
|
-
isPending,
|
|
40
|
-
isError,
|
|
41
|
-
error,
|
|
42
|
-
} = useOrganizeWiden(accountId, anchorMessageId, selectedSenders);
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (!open) return;
|
|
46
|
-
preview();
|
|
47
|
-
}, [open, preview]);
|
|
48
|
-
|
|
49
|
-
const handleClose = () => {
|
|
50
|
-
reset();
|
|
51
|
-
onClose();
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
if (!open) return null;
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<Dialog open={open} onClose={handleClose} title="Filter rule">
|
|
58
|
-
{isPending || matchedCount === undefined ? (
|
|
59
|
-
isError ? (
|
|
60
|
-
<div className="flex flex-col items-center gap-3 px-5 py-8 text-center">
|
|
61
|
-
<p className="text-sm font-medium text-danger">
|
|
62
|
-
Couldn't find similar messages
|
|
63
|
-
</p>
|
|
64
|
-
<p className="max-w-xs text-xs text-fg-muted">
|
|
65
|
-
{error instanceof Error ? error.message : "Please try again."}
|
|
66
|
-
</p>
|
|
67
|
-
<Button variant="ghost" onClick={handleClose} className="mt-2">
|
|
68
|
-
Close
|
|
69
|
-
</Button>
|
|
70
|
-
</div>
|
|
71
|
-
) : (
|
|
72
|
-
<div className="flex flex-col items-center gap-3 px-5 py-10 text-center">
|
|
73
|
-
<Loader2 className="size-8 animate-spin text-accent-2" />
|
|
74
|
-
<p className="text-sm font-medium text-fg">
|
|
75
|
-
Finding similar messages…
|
|
76
|
-
</p>
|
|
77
|
-
</div>
|
|
78
|
-
)
|
|
79
|
-
) : (
|
|
80
|
-
<OrganizeRuleEditor
|
|
81
|
-
accountId={accountId}
|
|
82
|
-
selectedMessageIds={selectedMessageIds}
|
|
83
|
-
seedCount={matchedCount}
|
|
84
|
-
semanticUnavailable={semanticUnavailable}
|
|
85
|
-
senders={senders}
|
|
86
|
-
onClose={handleClose}
|
|
87
|
-
/>
|
|
88
|
-
)}
|
|
89
|
-
</Dialog>
|
|
90
|
-
);
|
|
91
|
-
}
|