@remit/ui 0.0.69 → 0.0.71
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/auto-moved-badge.tsx +1 -3
- package/src/components/brief-sections.tsx +0 -13
- package/src/components/dialog.tsx +2 -5
- package/src/components/filter-rule-editor.tsx +0 -43
- package/src/components/filter-rule.render.test.ts +0 -49
- package/src/components/folder-tree-picker.stories.tsx +12 -25
- package/src/components/primitives.stories.tsx +0 -30
- package/src/components/rescue-from-spam-flow.render.test.ts +4 -4
- package/src/components/rescue-from-spam-flow.stories.tsx +9 -7
- package/src/components/rescue-from-spam-flow.tsx +20 -16
- package/src/components/search-chip-input.tsx +3 -15
- package/src/components/selection-wizard.render.test.ts +14 -7
- package/src/components/selection-wizard.tsx +16 -18
- package/src/index.ts +0 -15
- package/src/components/list-item.tsx +0 -39
- package/src/components/message-header.render.test.ts +0 -58
- package/src/components/message-header.stories.tsx +0 -79
- package/src/components/message-header.tsx +0 -59
- package/src/components/move-mailbox-picker.create.test.ts +0 -210
- package/src/components/move-mailbox-picker.render.test.ts +0 -113
- package/src/components/move-mailbox-picker.stories.tsx +0 -313
- package/src/components/move-mailbox-picker.tsx +0 -404
package/package.json
CHANGED
|
@@ -17,7 +17,6 @@ export interface AutoMovedBadgeProps {
|
|
|
17
17
|
* way to reach the filter that keeps moving mail. Omit for a classifier move.
|
|
18
18
|
*/
|
|
19
19
|
filtersHref?: string;
|
|
20
|
-
manageLabel?: string;
|
|
21
20
|
className?: string;
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -34,7 +33,6 @@ export function AutoMovedBadge({
|
|
|
34
33
|
onUndo,
|
|
35
34
|
undoLabel = "Undo",
|
|
36
35
|
filtersHref,
|
|
37
|
-
manageLabel = "Manage filter",
|
|
38
36
|
className,
|
|
39
37
|
}: AutoMovedBadgeProps) {
|
|
40
38
|
return (
|
|
@@ -61,7 +59,7 @@ export function AutoMovedBadge({
|
|
|
61
59
|
}}
|
|
62
60
|
className="font-semibold underline decoration-dotted underline-offset-2 hover:decoration-solid"
|
|
63
61
|
>
|
|
64
|
-
|
|
62
|
+
Manage filter
|
|
65
63
|
</a>
|
|
66
64
|
)}
|
|
67
65
|
</Badge>
|
|
@@ -75,17 +75,6 @@ export interface BriefSectionsProps {
|
|
|
75
75
|
onSelectSource?: (id: string) => void;
|
|
76
76
|
/** Seeds the filter panel open on first render (stories / deep links). */
|
|
77
77
|
defaultExpanded?: boolean;
|
|
78
|
-
/**
|
|
79
|
-
* Whether this component owns arrow-key traversal and the roving tabindex
|
|
80
|
-
* over its rows. On by default so a consumer that only passes rows (the
|
|
81
|
-
* Storybook prototype) still has a keyboard.
|
|
82
|
-
*
|
|
83
|
-
* The web client turns it off: its triage layer routes ↑/↓/Home/End through
|
|
84
|
-
* one window-level dispatcher, and a container handler here would swallow
|
|
85
|
-
* them first — taking Shift+↑/↓ with them, which the dispatcher needs for
|
|
86
|
-
* range selection and `rovingNextIndex` ignores.
|
|
87
|
-
*/
|
|
88
|
-
manageFocus?: boolean;
|
|
89
78
|
}
|
|
90
79
|
|
|
91
80
|
/**
|
|
@@ -107,7 +96,6 @@ export function BriefSections({
|
|
|
107
96
|
sourcesNote,
|
|
108
97
|
onSelectSource,
|
|
109
98
|
defaultExpanded = false,
|
|
110
|
-
manageFocus = true,
|
|
111
99
|
}: BriefSectionsProps) {
|
|
112
100
|
const [active, setActive] = useState<ReadonlySet<BriefFilterId>>(new Set());
|
|
113
101
|
const [sheetExpanded, setSheetExpanded] = useState(defaultExpanded);
|
|
@@ -115,7 +103,6 @@ export function BriefSections({
|
|
|
115
103
|
useRovingFocus({
|
|
116
104
|
containerRef: listRef,
|
|
117
105
|
itemSelector: LIST_ROW_SELECTOR,
|
|
118
|
-
enabled: manageFocus,
|
|
119
106
|
});
|
|
120
107
|
|
|
121
108
|
const toggleFilter = (id: BriefFilterId) => {
|
|
@@ -5,8 +5,6 @@ export interface DialogProps {
|
|
|
5
5
|
open: boolean;
|
|
6
6
|
onClose: () => void;
|
|
7
7
|
title: string;
|
|
8
|
-
/** Unique ID for aria-labelledby. Defaults to "dialog-title". */
|
|
9
|
-
titleId?: string;
|
|
10
8
|
children?: ReactNode;
|
|
11
9
|
className?: string;
|
|
12
10
|
/**
|
|
@@ -22,7 +20,6 @@ export function Dialog({
|
|
|
22
20
|
open,
|
|
23
21
|
onClose,
|
|
24
22
|
title,
|
|
25
|
-
titleId = "dialog-title",
|
|
26
23
|
children,
|
|
27
24
|
className,
|
|
28
25
|
anchor = "center",
|
|
@@ -86,7 +83,7 @@ export function Dialog({
|
|
|
86
83
|
ref={dialogRef}
|
|
87
84
|
role="dialog"
|
|
88
85
|
aria-modal="true"
|
|
89
|
-
aria-labelledby=
|
|
86
|
+
aria-labelledby="dialog-title"
|
|
90
87
|
className={cn(
|
|
91
88
|
"relative z-10 overflow-hidden border-line bg-surface shadow-xl",
|
|
92
89
|
isLeft
|
|
@@ -99,7 +96,7 @@ export function Dialog({
|
|
|
99
96
|
onClick={(e) => e.stopPropagation()}
|
|
100
97
|
onKeyDown={(e) => e.stopPropagation()}
|
|
101
98
|
>
|
|
102
|
-
<h2 id=
|
|
99
|
+
<h2 id="dialog-title" className="sr-only">
|
|
103
100
|
{title}
|
|
104
101
|
</h2>
|
|
105
102
|
{children}
|
|
@@ -7,9 +7,7 @@ import {
|
|
|
7
7
|
useState,
|
|
8
8
|
} from "react";
|
|
9
9
|
import { isAbortError } from "../lib/abort.js";
|
|
10
|
-
import { BottomSheet } from "./bottom-sheet.js";
|
|
11
10
|
import { Button } from "./button.js";
|
|
12
|
-
import { Dialog } from "./dialog.js";
|
|
13
11
|
import {
|
|
14
12
|
AddChipButton,
|
|
15
13
|
ClauseChip,
|
|
@@ -699,44 +697,3 @@ export function FilterRuleEditor({
|
|
|
699
697
|
</div>
|
|
700
698
|
);
|
|
701
699
|
}
|
|
702
|
-
|
|
703
|
-
export interface FilterRuleDialogProps extends FilterRuleEditorProps {
|
|
704
|
-
open: boolean;
|
|
705
|
-
onClose: () => void;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/** Desktop home for the rule editor — the centered modal (RFC 038 D1). */
|
|
709
|
-
export function FilterRuleDialog({
|
|
710
|
-
open,
|
|
711
|
-
onClose,
|
|
712
|
-
...editor
|
|
713
|
-
}: FilterRuleDialogProps) {
|
|
714
|
-
if (!open) return null;
|
|
715
|
-
return (
|
|
716
|
-
<Dialog open={open} onClose={onClose} title="Filter rule">
|
|
717
|
-
<FilterRuleEditor {...editor} onCancel={onClose} />
|
|
718
|
-
</Dialog>
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
export interface FilterRuleSheetProps extends FilterRuleEditorProps {
|
|
723
|
-
open: boolean;
|
|
724
|
-
onClose: () => void;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/** Mobile home for the rule editor — the bottom sheet (RFC 038 D1). */
|
|
728
|
-
export function FilterRuleSheet({
|
|
729
|
-
open,
|
|
730
|
-
onClose,
|
|
731
|
-
...editor
|
|
732
|
-
}: FilterRuleSheetProps): ReactNode {
|
|
733
|
-
return (
|
|
734
|
-
<BottomSheet
|
|
735
|
-
open={open}
|
|
736
|
-
onClose={onClose}
|
|
737
|
-
dismissLabel="Dismiss filter rule"
|
|
738
|
-
>
|
|
739
|
-
<FilterRuleEditor {...editor} onCancel={onClose} />
|
|
740
|
-
</BottomSheet>
|
|
741
|
-
);
|
|
742
|
-
}
|
|
@@ -31,10 +31,8 @@ import {
|
|
|
31
31
|
widenChipLabel,
|
|
32
32
|
} from "./filter-rule.js";
|
|
33
33
|
import {
|
|
34
|
-
FilterRuleDialog,
|
|
35
34
|
FilterRuleEditor,
|
|
36
35
|
type FilterRuleEditorProps,
|
|
37
|
-
FilterRuleSheet,
|
|
38
36
|
} from "./filter-rule-editor.js";
|
|
39
37
|
|
|
40
38
|
/** SSR splits interpolations with comment markers; sentences read across them. */
|
|
@@ -671,50 +669,3 @@ describe("FilterRuleEditor", () => {
|
|
|
671
669
|
assert.doesNotMatch(html, /…and similar/);
|
|
672
670
|
});
|
|
673
671
|
});
|
|
674
|
-
|
|
675
|
-
describe("FilterRuleDialog", () => {
|
|
676
|
-
it("renders the editor when open", () => {
|
|
677
|
-
const html = render(
|
|
678
|
-
createElement(FilterRuleDialog, {
|
|
679
|
-
open: true,
|
|
680
|
-
onClose: () => {},
|
|
681
|
-
rule: demoRule,
|
|
682
|
-
folders: FOLDERS,
|
|
683
|
-
preview: READY,
|
|
684
|
-
}),
|
|
685
|
-
);
|
|
686
|
-
assert.match(html, /role="dialog"/);
|
|
687
|
-
assert.match(html, /Filter rule/);
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
it("renders nothing when closed", () => {
|
|
691
|
-
assert.equal(
|
|
692
|
-
renderToString(
|
|
693
|
-
createElement(FilterRuleDialog, {
|
|
694
|
-
open: false,
|
|
695
|
-
onClose: () => {},
|
|
696
|
-
rule: demoRule,
|
|
697
|
-
folders: FOLDERS,
|
|
698
|
-
preview: READY,
|
|
699
|
-
}),
|
|
700
|
-
),
|
|
701
|
-
"",
|
|
702
|
-
);
|
|
703
|
-
});
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
describe("FilterRuleSheet", () => {
|
|
707
|
-
it("renders the editor inside a dismissible sheet", () => {
|
|
708
|
-
const html = render(
|
|
709
|
-
createElement(FilterRuleSheet, {
|
|
710
|
-
open: true,
|
|
711
|
-
onClose: () => {},
|
|
712
|
-
rule: demoRule,
|
|
713
|
-
folders: FOLDERS,
|
|
714
|
-
preview: READY,
|
|
715
|
-
}),
|
|
716
|
-
);
|
|
717
|
-
assert.match(html, /Dismiss filter rule/);
|
|
718
|
-
assert.match(html, /Filter rule/);
|
|
719
|
-
});
|
|
720
|
-
});
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|
|
2
2
|
import { type ReactNode, useState } from "react";
|
|
3
3
|
import type { StepId } from "../lib/wizard-steps.js";
|
|
4
4
|
import { type FolderTreeNode, FolderTreePicker } from "./folder-tree-picker.js";
|
|
5
|
-
import { FooterNav, WizardScreen } from "./selection-wizard.js";
|
|
5
|
+
import { FolderStepBody, FooterNav, WizardScreen } from "./selection-wizard.js";
|
|
6
6
|
|
|
7
7
|
const folders: FolderTreeNode[] = [
|
|
8
8
|
{ id: "mbx-inbox", label: "Inbox", path: "INBOX", isCurrent: true },
|
|
@@ -332,30 +332,17 @@ function WizardFolderStep() {
|
|
|
332
332
|
/>
|
|
333
333
|
}
|
|
334
334
|
>
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
onCreateFolder={(name, parentPath) =>
|
|
347
|
-
createFolder(name, parentPath).then((created) => {
|
|
348
|
-
setKnown((current) => [...current, created]);
|
|
349
|
-
return created;
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
labels={{
|
|
353
|
-
createPending:
|
|
354
|
-
"Waiting for the mail server to confirm the folder…",
|
|
355
|
-
}}
|
|
356
|
-
/>
|
|
357
|
-
</div>
|
|
358
|
-
</div>
|
|
335
|
+
<FolderStepBody
|
|
336
|
+
folders={known}
|
|
337
|
+
mailboxId={selected}
|
|
338
|
+
onSelect={setSelected}
|
|
339
|
+
onCreateFolder={(name, parentPath) =>
|
|
340
|
+
createFolder(name, parentPath).then((created) => {
|
|
341
|
+
setKnown((current) => [...current, created]);
|
|
342
|
+
return created;
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
/>
|
|
359
346
|
</WizardScreen>
|
|
360
347
|
);
|
|
361
348
|
}
|
|
@@ -6,7 +6,6 @@ import { Button } from "./button.js";
|
|
|
6
6
|
import { Card, CardBody, CardHeader, CardTitle } from "./card.js";
|
|
7
7
|
import { FieldLabel } from "./field-label.js";
|
|
8
8
|
import { Input } from "./input.js";
|
|
9
|
-
import { ListItem } from "./list-item.js";
|
|
10
9
|
import { PasswordInput } from "./password-input.js";
|
|
11
10
|
import { SecuritySelect } from "./security-select.js";
|
|
12
11
|
import { Select } from "./select.js";
|
|
@@ -176,32 +175,3 @@ export const Cards: Story = {
|
|
|
176
175
|
</div>
|
|
177
176
|
),
|
|
178
177
|
};
|
|
179
|
-
|
|
180
|
-
export const ListItems: Story = {
|
|
181
|
-
render: () => (
|
|
182
|
-
<div className="max-w-md divide-y divide-line p-8">
|
|
183
|
-
<ListItem
|
|
184
|
-
unread
|
|
185
|
-
leading={<Avatar name="Priya Natarajan" size="md" />}
|
|
186
|
-
trailing="08:52"
|
|
187
|
-
>
|
|
188
|
-
<div className="text-sm font-semibold text-fg">Priya Natarajan</div>
|
|
189
|
-
<div className="text-sm text-fg">Q3 roadmap review</div>
|
|
190
|
-
<p className="line-clamp-1 text-xs text-fg-subtle">
|
|
191
|
-
Sharing the agenda ahead of Thursday…
|
|
192
|
-
</p>
|
|
193
|
-
</ListItem>
|
|
194
|
-
<ListItem
|
|
195
|
-
active
|
|
196
|
-
leading={<Avatar name="Marcus Webb" size="md" />}
|
|
197
|
-
trailing="Wed"
|
|
198
|
-
>
|
|
199
|
-
<div className="text-sm font-medium text-fg-muted">Marcus Webb</div>
|
|
200
|
-
<div className="text-sm text-fg-muted">Re: Reading pane density</div>
|
|
201
|
-
<p className="line-clamp-1 text-xs text-fg-subtle">
|
|
202
|
-
Strong +1 on tightening the rows…
|
|
203
|
-
</p>
|
|
204
|
-
</ListItem>
|
|
205
|
-
</div>
|
|
206
|
-
),
|
|
207
|
-
};
|
|
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import { createElement } from "react";
|
|
4
4
|
import { renderToString } from "react-dom/server";
|
|
5
|
-
import type {
|
|
5
|
+
import type { FolderTreeNode } from "./folder-tree-picker.js";
|
|
6
6
|
import type { RescueCandidate } from "./rescue-candidate-row.js";
|
|
7
7
|
import {
|
|
8
8
|
RescueFromSpamFlow,
|
|
@@ -34,9 +34,9 @@ const candidates: RescueCandidate[] = [
|
|
|
34
34
|
},
|
|
35
35
|
];
|
|
36
36
|
|
|
37
|
-
const folders:
|
|
38
|
-
{ id: "inbox", label: "Inbox" },
|
|
39
|
-
{ id: "spam", label: "Spam", isCurrent: true },
|
|
37
|
+
const folders: FolderTreeNode[] = [
|
|
38
|
+
{ id: "inbox", label: "Inbox", path: "INBOX" },
|
|
39
|
+
{ id: "spam", label: "Spam", path: "Junk", isCurrent: true },
|
|
40
40
|
];
|
|
41
41
|
|
|
42
42
|
const renderFlow = (open: boolean): string =>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { Button } from "./button.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { FolderTreeNode } from "./folder-tree-picker.js";
|
|
5
5
|
import type { RescueCandidate } from "./rescue-candidate-row.js";
|
|
6
6
|
import { RescueFromSpamFlow } from "./rescue-from-spam-flow.js";
|
|
7
7
|
|
|
@@ -54,12 +54,14 @@ const CANDIDATES: RescueCandidate[] = [
|
|
|
54
54
|
},
|
|
55
55
|
];
|
|
56
56
|
|
|
57
|
-
const FOLDERS:
|
|
58
|
-
{ id: "inbox", label: "Inbox" },
|
|
59
|
-
{ id: "spam", label: "Spam", isCurrent: true },
|
|
60
|
-
{ id: "receipts", label: "Receipts" },
|
|
61
|
-
{ id: "family", label: "Family" },
|
|
62
|
-
{ id: "work", label: "Work",
|
|
57
|
+
const FOLDERS: FolderTreeNode[] = [
|
|
58
|
+
{ id: "inbox", label: "Inbox", path: "INBOX" },
|
|
59
|
+
{ id: "spam", label: "Spam", path: "Junk", isCurrent: true },
|
|
60
|
+
{ id: "receipts", label: "Receipts", path: "Receipts" },
|
|
61
|
+
{ id: "family", label: "Family", path: "Family" },
|
|
62
|
+
{ id: "work", label: "Work", path: "Work" },
|
|
63
|
+
{ id: "work-clients", label: "Clients", path: "Work/Clients" },
|
|
64
|
+
{ id: "work-projects", label: "Projects", path: "Work/Projects" },
|
|
63
65
|
];
|
|
64
66
|
|
|
65
67
|
function Demo() {
|
|
@@ -3,10 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
3
3
|
import { Banner } from "./banner.js";
|
|
4
4
|
import { BottomSheet } from "./bottom-sheet.js";
|
|
5
5
|
import { Button } from "./button.js";
|
|
6
|
-
import {
|
|
7
|
-
type MoveMailboxOption,
|
|
8
|
-
MoveMailboxPicker,
|
|
9
|
-
} from "./move-mailbox-picker.js";
|
|
6
|
+
import { type FolderTreeNode, FolderTreePicker } from "./folder-tree-picker.js";
|
|
10
7
|
import type { RescueCandidate } from "./rescue-candidate-row.js";
|
|
11
8
|
import {
|
|
12
9
|
groupRescueCandidatesBySender,
|
|
@@ -23,7 +20,9 @@ export interface RescueFromSpamFlowProps {
|
|
|
23
20
|
/** Where the move lands unless the user picks another folder. */
|
|
24
21
|
defaultDestinationId: string;
|
|
25
22
|
/** Folders the user can send the rescued mail to. */
|
|
26
|
-
availableFolders:
|
|
23
|
+
availableFolders: FolderTreeNode[];
|
|
24
|
+
/** The account's hierarchy separator, which the tree nests on. */
|
|
25
|
+
delimiter?: string;
|
|
27
26
|
/** Run the move. The flow shows the done state right after. */
|
|
28
27
|
onConfirmMove: (messageIds: string[], destinationId: string) => void;
|
|
29
28
|
/** Close the sheet without moving anything. */
|
|
@@ -42,7 +41,7 @@ export const rescueMoveConsequence = (
|
|
|
42
41
|
): string =>
|
|
43
42
|
`Moves ${count === 1 ? "this message" : `these ${count} ${plural(count)}`} out of Spam to ${destinationLabel} now. Nothing later.`;
|
|
44
43
|
|
|
45
|
-
const folderLabel = (folders:
|
|
44
|
+
const folderLabel = (folders: FolderTreeNode[], id: string): string =>
|
|
46
45
|
folders.find((f) => f.id === id)?.label ?? "Inbox";
|
|
47
46
|
|
|
48
47
|
function ExplainWhy() {
|
|
@@ -157,13 +156,15 @@ function ReviewStep({
|
|
|
157
156
|
function DestinationStep({
|
|
158
157
|
count,
|
|
159
158
|
folders,
|
|
159
|
+
delimiter,
|
|
160
160
|
destination,
|
|
161
161
|
onPick,
|
|
162
162
|
onMove,
|
|
163
163
|
onBack,
|
|
164
164
|
}: {
|
|
165
165
|
count: number;
|
|
166
|
-
folders:
|
|
166
|
+
folders: FolderTreeNode[];
|
|
167
|
+
delimiter?: string;
|
|
167
168
|
destination: string;
|
|
168
169
|
onPick: (id: string) => void;
|
|
169
170
|
onMove: () => void;
|
|
@@ -180,16 +181,17 @@ function DestinationStep({
|
|
|
180
181
|
</p>
|
|
181
182
|
</div>
|
|
182
183
|
|
|
183
|
-
<div className="min-h-0 flex-1 overflow-y-auto px-row-inset py-2">
|
|
184
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-row-inset py-2">
|
|
184
185
|
{picking ? (
|
|
185
|
-
<div className="overflow-hidden rounded-xl border border-line bg-surface">
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
186
|
+
<div className="flex min-h-0 flex-1 overflow-hidden rounded-xl border border-line bg-surface">
|
|
187
|
+
{/* Picking a folder also opens it, so the tree stays up: closing
|
|
188
|
+
on the first tap would put anything nested out of reach. The
|
|
189
|
+
consequence line below states the destination as it changes. */}
|
|
190
|
+
<FolderTreePicker
|
|
191
|
+
folders={folders}
|
|
192
|
+
selectedId={destination}
|
|
193
|
+
delimiter={delimiter}
|
|
194
|
+
onSelect={onPick}
|
|
193
195
|
onCancel={() => setPicking(false)}
|
|
194
196
|
/>
|
|
195
197
|
</div>
|
|
@@ -277,6 +279,7 @@ export function RescueFromSpamFlow({
|
|
|
277
279
|
candidates,
|
|
278
280
|
defaultDestinationId,
|
|
279
281
|
availableFolders,
|
|
282
|
+
delimiter,
|
|
280
283
|
onConfirmMove,
|
|
281
284
|
onCancel,
|
|
282
285
|
}: RescueFromSpamFlowProps) {
|
|
@@ -345,6 +348,7 @@ export function RescueFromSpamFlow({
|
|
|
345
348
|
<DestinationStep
|
|
346
349
|
count={selectedCount}
|
|
347
350
|
folders={availableFolders}
|
|
351
|
+
delimiter={delimiter}
|
|
348
352
|
destination={destination}
|
|
349
353
|
onPick={setDestination}
|
|
350
354
|
onMove={move}
|
|
@@ -65,8 +65,6 @@ export interface SearchChipInputProps {
|
|
|
65
65
|
*/
|
|
66
66
|
chips?: readonly SearchChip[];
|
|
67
67
|
onRemoveChip?: (id: string) => void;
|
|
68
|
-
/** Opens a chip's own value editor, where the host offers one. */
|
|
69
|
-
onActivateChip?: (id: string) => void;
|
|
70
68
|
/** The free text alongside the chips. */
|
|
71
69
|
value: string;
|
|
72
70
|
onChange: (value: string) => void;
|
|
@@ -105,8 +103,6 @@ export interface SearchChipInputProps {
|
|
|
105
103
|
* when something outside needs to address the field by a stable id.
|
|
106
104
|
*/
|
|
107
105
|
inputId?: string;
|
|
108
|
-
/** Accessible name for the chip grid. */
|
|
109
|
-
chipsLabel?: string;
|
|
110
106
|
/**
|
|
111
107
|
* Completions for what is being typed. Omit for a field that offers none;
|
|
112
108
|
* see {@link SearchFieldSuggest} for what the field and the host each own.
|
|
@@ -165,7 +161,6 @@ const isEditableTarget = (target: EventTarget | null): boolean => {
|
|
|
165
161
|
export const SearchChipInput = ({
|
|
166
162
|
chips = [],
|
|
167
163
|
onRemoveChip,
|
|
168
|
-
onActivateChip,
|
|
169
164
|
value,
|
|
170
165
|
onChange,
|
|
171
166
|
onClear,
|
|
@@ -175,7 +170,6 @@ export const SearchChipInput = ({
|
|
|
175
170
|
showClearButton = true,
|
|
176
171
|
size = "sm",
|
|
177
172
|
inputId,
|
|
178
|
-
chipsLabel = "Search filters",
|
|
179
173
|
suggest,
|
|
180
174
|
className,
|
|
181
175
|
}: SearchChipInputProps) => {
|
|
@@ -313,16 +307,13 @@ export const SearchChipInput = ({
|
|
|
313
307
|
case "focusInput":
|
|
314
308
|
moveFocus(null);
|
|
315
309
|
return;
|
|
316
|
-
case "activateChip":
|
|
317
|
-
const chip = chips[action.index];
|
|
318
|
-
if (chip && onActivateChip) onActivateChip(chip.id);
|
|
310
|
+
case "activateChip":
|
|
319
311
|
return;
|
|
320
|
-
}
|
|
321
312
|
case "none":
|
|
322
313
|
return;
|
|
323
314
|
}
|
|
324
315
|
},
|
|
325
|
-
[chips, removeChipAt, moveFocus
|
|
316
|
+
[chips, removeChipAt, moveFocus],
|
|
326
317
|
);
|
|
327
318
|
|
|
328
319
|
useEffect(() => {
|
|
@@ -371,7 +362,7 @@ export const SearchChipInput = ({
|
|
|
371
362
|
// biome-ignore lint/a11y/useSemanticElements: see above
|
|
372
363
|
<div
|
|
373
364
|
role="grid"
|
|
374
|
-
aria-label=
|
|
365
|
+
aria-label="Search filters"
|
|
375
366
|
className="flex min-w-0 flex-wrap items-center gap-1"
|
|
376
367
|
>
|
|
377
368
|
{chips.map((chip, index) => (
|
|
@@ -386,9 +377,6 @@ export const SearchChipInput = ({
|
|
|
386
377
|
onFocusLabel={() => setFocusedChip(index)}
|
|
387
378
|
onKeyDown={handleChipKeyDown(index)}
|
|
388
379
|
onRemove={() => removeChipAt(index)}
|
|
389
|
-
onActivate={
|
|
390
|
-
onActivateChip ? () => onActivateChip(chip.id) : undefined
|
|
391
|
-
}
|
|
392
380
|
/>
|
|
393
381
|
))}
|
|
394
382
|
</div>
|
|
@@ -76,14 +76,16 @@ const propertiesProps = {
|
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
const folderProps = {
|
|
79
|
-
|
|
80
|
-
{ id: "mbx-archive", label: "Archive" },
|
|
81
|
-
{ id: "mbx-travel", label: "Travel" },
|
|
82
|
-
{ id: "mbx-
|
|
79
|
+
folders: [
|
|
80
|
+
{ id: "mbx-archive", label: "Archive", path: "Archive" },
|
|
81
|
+
{ id: "mbx-travel", label: "Travel", path: "Travel" },
|
|
82
|
+
{ id: "mbx-travel-2026", label: "2026", path: "Travel/2026" },
|
|
83
|
+
{ id: "mbx-inbox", label: "Inbox", path: "INBOX", isCurrent: true },
|
|
83
84
|
],
|
|
84
85
|
mailboxId: "mbx-travel",
|
|
85
86
|
onSelect: noop,
|
|
86
|
-
onCreateFolder: () =>
|
|
87
|
+
onCreateFolder: () =>
|
|
88
|
+
Promise.resolve({ id: "mbx-new", label: "Hotels", path: "Travel/Hotels" }),
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
const draft = (over: Partial<WizardDraft> = {}): WizardDraft => ({
|
|
@@ -359,11 +361,16 @@ describe("FolderStepBody", () => {
|
|
|
359
361
|
assert.match(html, /Inbox \(current folder\)/);
|
|
360
362
|
});
|
|
361
363
|
|
|
362
|
-
it("
|
|
364
|
+
it("starts at the top level, with nested folders behind the one holding them", () => {
|
|
365
|
+
const html = renderToString(createElement(FolderStepBody, folderProps));
|
|
366
|
+
assert.doesNotMatch(html, /Move to 2026/);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("says how to reach a folder when none is chosen yet", () => {
|
|
363
370
|
const html = renderToString(
|
|
364
371
|
createElement(FolderStepBody, { ...folderProps, mailboxId: undefined }),
|
|
365
372
|
);
|
|
366
|
-
assert.match(text(html), /
|
|
373
|
+
assert.match(text(html), /Tap a folder to open it/);
|
|
367
374
|
});
|
|
368
375
|
});
|
|
369
376
|
|
|
@@ -69,11 +69,8 @@ import {
|
|
|
69
69
|
scopeLabel,
|
|
70
70
|
} from "./filter-rule.js";
|
|
71
71
|
import type { ClauseEditState } from "./filter-rule-editor.js";
|
|
72
|
+
import { type FolderTreeNode, FolderTreePicker } from "./folder-tree-picker.js";
|
|
72
73
|
import { Input } from "./input.js";
|
|
73
|
-
import {
|
|
74
|
-
type MoveMailboxOption,
|
|
75
|
-
MoveMailboxPicker,
|
|
76
|
-
} from "./move-mailbox-picker.js";
|
|
77
74
|
import { ProgressBar } from "./progress-bar.js";
|
|
78
75
|
import type { SearchConversionNotice } from "./search-conversion.js";
|
|
79
76
|
import { SearchConversionNoticeView } from "./search-conversion-notice.js";
|
|
@@ -354,7 +351,6 @@ export function SelectionSample({
|
|
|
354
351
|
}
|
|
355
352
|
|
|
356
353
|
export interface FooterNavProps {
|
|
357
|
-
backLabel?: string;
|
|
358
354
|
onBack: () => void;
|
|
359
355
|
nextLabel: string;
|
|
360
356
|
onNext: () => void;
|
|
@@ -372,7 +368,6 @@ export interface FooterNavProps {
|
|
|
372
368
|
* carries the two ways that reason reaches the user.
|
|
373
369
|
*/
|
|
374
370
|
export function FooterNav({
|
|
375
|
-
backLabel = "Back",
|
|
376
371
|
onBack,
|
|
377
372
|
nextLabel,
|
|
378
373
|
onNext,
|
|
@@ -399,7 +394,7 @@ export function FooterNav({
|
|
|
399
394
|
icon={<ArrowLeft className="size-4" />}
|
|
400
395
|
className="shrink-0"
|
|
401
396
|
>
|
|
402
|
-
|
|
397
|
+
Back
|
|
403
398
|
</Button>
|
|
404
399
|
<Button
|
|
405
400
|
variant={nextVariant}
|
|
@@ -651,7 +646,7 @@ export interface FolderStepProps {
|
|
|
651
646
|
* (`buildMoveTargets`). The wizard does not reorder them: a second ordering
|
|
652
647
|
* beside the one the Move picker already applies would fight it.
|
|
653
648
|
*/
|
|
654
|
-
|
|
649
|
+
folders: readonly FolderTreeNode[];
|
|
655
650
|
/** The destination chosen so far. */
|
|
656
651
|
mailboxId?: string;
|
|
657
652
|
onSelect: (mailboxId: string) => void;
|
|
@@ -664,8 +659,11 @@ export interface FolderStepProps {
|
|
|
664
659
|
*/
|
|
665
660
|
onCreateFolder?: (
|
|
666
661
|
name: string,
|
|
662
|
+
parentPath: string,
|
|
667
663
|
signal?: AbortSignal,
|
|
668
|
-
) => Promise<
|
|
664
|
+
) => Promise<FolderTreeNode>;
|
|
665
|
+
/** The account's hierarchy separator, which the tree nests on. */
|
|
666
|
+
delimiter?: string;
|
|
669
667
|
/**
|
|
670
668
|
* Why there is no folder list to choose from — a selection spanning accounts
|
|
671
669
|
* has no single account whose folders these could be (#477 5.5). Said here
|
|
@@ -675,13 +673,14 @@ export interface FolderStepProps {
|
|
|
675
673
|
}
|
|
676
674
|
|
|
677
675
|
export function FolderStepBody({
|
|
678
|
-
|
|
676
|
+
folders,
|
|
679
677
|
mailboxId,
|
|
680
678
|
onSelect,
|
|
681
679
|
onCreateFolder,
|
|
680
|
+
delimiter,
|
|
682
681
|
restriction,
|
|
683
682
|
}: FolderStepProps) {
|
|
684
|
-
const chosen =
|
|
683
|
+
const chosen = folders.find((folder) => folder.id === mailboxId);
|
|
685
684
|
return (
|
|
686
685
|
<div className="flex min-h-0 flex-col gap-3">
|
|
687
686
|
<p
|
|
@@ -693,17 +692,16 @@ export function FolderStepBody({
|
|
|
693
692
|
{restriction ??
|
|
694
693
|
(chosen
|
|
695
694
|
? `Moving to ${chosen.label}.`
|
|
696
|
-
: "
|
|
695
|
+
: "Tap a folder to open it, or make a new one where you want it.")}
|
|
697
696
|
</p>
|
|
698
|
-
<div className="overflow-hidden rounded-lg border border-line bg-surface">
|
|
699
|
-
<
|
|
700
|
-
|
|
697
|
+
<div className="flex min-h-0 flex-1 overflow-hidden rounded-lg border border-line bg-surface">
|
|
698
|
+
<FolderTreePicker
|
|
699
|
+
folders={folders}
|
|
700
|
+
selectedId={mailboxId}
|
|
701
701
|
onSelect={onSelect}
|
|
702
702
|
onCreateFolder={onCreateFolder}
|
|
703
|
-
|
|
703
|
+
delimiter={delimiter}
|
|
704
704
|
labels={{
|
|
705
|
-
searchPlaceholder: "Move to…",
|
|
706
|
-
optionLabel: (label) => `Move to ${label}`,
|
|
707
705
|
createPending: "Waiting for the mail server to confirm the folder…",
|
|
708
706
|
}}
|
|
709
707
|
/>
|