@remit/ui 0.0.68 → 0.0.70
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/folder-manage-actions.render.test.ts +51 -0
- package/src/components/folder-manage-actions.stories.tsx +94 -0
- package/src/components/folder-manage-actions.tsx +54 -0
- package/src/components/folder-manager.render.test.ts +80 -0
- package/src/components/folder-manager.stories.tsx +184 -0
- package/src/components/folder-manager.tsx +77 -0
- package/src/components/folder-rename-dialog.render.test.ts +51 -0
- package/src/components/folder-rename-dialog.stories.tsx +66 -0
- package/src/components/folder-rename-dialog.tsx +95 -0
- package/src/components/folder-row.render.test.ts +7 -0
- package/src/components/folder-row.tsx +10 -1
- package/src/components/folder-tree-picker.stories.tsx +12 -25
- package/src/components/folder-tree-picker.tsx +13 -4
- 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/selection-wizard.render.test.ts +14 -7
- package/src/components/selection-wizard.tsx +15 -15
- package/src/index.ts +14 -6
- 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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { Button } from "./button.js";
|
|
4
|
+
import { Dialog } from "./dialog.js";
|
|
5
|
+
import { FieldLabel } from "./field-label.js";
|
|
6
|
+
import { Input } from "./input.js";
|
|
7
|
+
|
|
8
|
+
export interface FolderRenameDialogProps {
|
|
9
|
+
open: boolean;
|
|
10
|
+
/** The folder as it reads now. */
|
|
11
|
+
folderLabel: string;
|
|
12
|
+
/** What the folder falls back to once the name is cleared. */
|
|
13
|
+
defaultLabel: string;
|
|
14
|
+
name: string;
|
|
15
|
+
onNameChange: (name: string) => void;
|
|
16
|
+
onSubmit: () => void;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
pending?: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Renames a folder for this account. The name is Remit's own — the folder keeps
|
|
24
|
+
* its place and its path on the mail server — so clearing it puts the folder
|
|
25
|
+
* back to what the server calls it.
|
|
26
|
+
*/
|
|
27
|
+
export const FolderRenameDialog = ({
|
|
28
|
+
open,
|
|
29
|
+
folderLabel,
|
|
30
|
+
defaultLabel,
|
|
31
|
+
name,
|
|
32
|
+
onNameChange,
|
|
33
|
+
onSubmit,
|
|
34
|
+
onClose,
|
|
35
|
+
pending = false,
|
|
36
|
+
error,
|
|
37
|
+
}: FolderRenameDialogProps) => {
|
|
38
|
+
const fieldId = useId();
|
|
39
|
+
const title = `Rename ${folderLabel}`;
|
|
40
|
+
|
|
41
|
+
if (!open) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Dialog open={open} onClose={onClose} title={title}>
|
|
45
|
+
<header className="flex items-center gap-2 border-b border-line px-5 py-3">
|
|
46
|
+
<span className="flex-1 text-sm font-semibold text-fg">{title}</span>
|
|
47
|
+
<Button
|
|
48
|
+
variant="ghost"
|
|
49
|
+
size="sm"
|
|
50
|
+
icon={<X className="size-3.5" />}
|
|
51
|
+
onClick={onClose}
|
|
52
|
+
aria-label="Cancel"
|
|
53
|
+
/>
|
|
54
|
+
</header>
|
|
55
|
+
<div className="space-y-3 px-5 py-4">
|
|
56
|
+
<div>
|
|
57
|
+
<FieldLabel htmlFor={fieldId}>Folder name</FieldLabel>
|
|
58
|
+
<Input
|
|
59
|
+
id={fieldId}
|
|
60
|
+
value={name}
|
|
61
|
+
placeholder={defaultLabel}
|
|
62
|
+
onChange={(event) => onNameChange(event.target.value)}
|
|
63
|
+
onKeyDown={(event) => {
|
|
64
|
+
if (event.key !== "Enter") return;
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
onSubmit();
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<p className="text-xs text-fg-muted">
|
|
71
|
+
Shown everywhere in Remit. Leave it blank to use{" "}
|
|
72
|
+
<span className="font-medium text-fg">{defaultLabel}</span>.
|
|
73
|
+
</p>
|
|
74
|
+
{error && (
|
|
75
|
+
<p className="text-xs text-danger" role="alert">
|
|
76
|
+
{error}
|
|
77
|
+
</p>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
<footer className="flex items-center justify-end gap-2 border-t border-line px-5 py-3">
|
|
81
|
+
<Button variant="secondary" size="sm" onClick={onClose}>
|
|
82
|
+
Cancel
|
|
83
|
+
</Button>
|
|
84
|
+
<Button
|
|
85
|
+
variant="primary"
|
|
86
|
+
size="sm"
|
|
87
|
+
onClick={onSubmit}
|
|
88
|
+
disabled={pending}
|
|
89
|
+
>
|
|
90
|
+
{pending ? "Saving…" : "Save name"}
|
|
91
|
+
</Button>
|
|
92
|
+
</footer>
|
|
93
|
+
</Dialog>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -74,6 +74,13 @@ describe("FolderRow", () => {
|
|
|
74
74
|
assert.doesNotMatch(render({ depth: 1 }), /left:74px/);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it("keeps its actions beside the tree item rather than inside it", () => {
|
|
78
|
+
const html = render({
|
|
79
|
+
actions: createElement("button", { type: "button" }, "Delete"),
|
|
80
|
+
});
|
|
81
|
+
assert.ok(html.indexOf(">Delete<") > html.indexOf("</button>"));
|
|
82
|
+
});
|
|
83
|
+
|
|
77
84
|
it("takes its place in a roving tab order", () => {
|
|
78
85
|
assert.match(render({ tabIndex: 0 }), /tabindex="0"/);
|
|
79
86
|
assert.match(render({ tabIndex: -1 }), /tabindex="-1"/);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Check, ChevronRight, Folder } from "lucide-react";
|
|
2
|
-
import type { Ref } from "react";
|
|
2
|
+
import type { ReactNode, Ref } from "react";
|
|
3
3
|
import { cn } from "../lib/cn.js";
|
|
4
4
|
|
|
5
5
|
export const FOLDER_ROW_BASE =
|
|
@@ -52,6 +52,12 @@ export interface FolderRowProps {
|
|
|
52
52
|
currentTag?: string;
|
|
53
53
|
/** A hairline under the label, drawn for every row but the last. */
|
|
54
54
|
separated?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Controls that operate on the folder rather than open it. They sit beside
|
|
57
|
+
* the row instead of inside it — a button nested in a button is invalid, and
|
|
58
|
+
* a tree item that swallows its own actions cannot be reached by keyboard.
|
|
59
|
+
*/
|
|
60
|
+
actions?: ReactNode;
|
|
55
61
|
tabIndex?: number;
|
|
56
62
|
onActivate?: () => void;
|
|
57
63
|
onFocus?: () => void;
|
|
@@ -72,6 +78,7 @@ export const FolderRow = ({
|
|
|
72
78
|
current = false,
|
|
73
79
|
currentTag,
|
|
74
80
|
separated = false,
|
|
81
|
+
actions,
|
|
75
82
|
tabIndex,
|
|
76
83
|
onActivate,
|
|
77
84
|
onFocus,
|
|
@@ -106,6 +113,7 @@ export const FolderRow = ({
|
|
|
106
113
|
{icon}
|
|
107
114
|
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
108
115
|
</div>
|
|
116
|
+
{actions}
|
|
109
117
|
{separated && <FolderRowSeparator depth={depth} />}
|
|
110
118
|
</div>
|
|
111
119
|
);
|
|
@@ -141,6 +149,7 @@ export const FolderRow = ({
|
|
|
141
149
|
<Check className="size-4 shrink-0 text-accent" aria-hidden="true" />
|
|
142
150
|
)}
|
|
143
151
|
</button>
|
|
152
|
+
{actions}
|
|
144
153
|
{separated && <FolderRowSeparator depth={depth} />}
|
|
145
154
|
</div>
|
|
146
155
|
);
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Search } from "lucide-react";
|
|
2
2
|
import {
|
|
3
3
|
type KeyboardEvent as ReactKeyboardEvent,
|
|
4
|
+
type ReactNode,
|
|
4
5
|
useCallback,
|
|
5
6
|
useEffect,
|
|
6
7
|
useMemo,
|
|
@@ -72,8 +73,14 @@ export interface FolderTreePickerProps {
|
|
|
72
73
|
folders: readonly FolderTreeNode[];
|
|
73
74
|
/** The destination chosen so far. */
|
|
74
75
|
selectedId?: string;
|
|
75
|
-
/**
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Marks the row. Choosing a destination advances nothing on its own. Absent
|
|
78
|
+
* means the tree is browsed rather than chosen from, and a row does nothing
|
|
79
|
+
* beyond opening and closing.
|
|
80
|
+
*/
|
|
81
|
+
onSelect?: (folderId: string) => void;
|
|
82
|
+
/** Controls beside each row, for a surface that acts on folders themselves. */
|
|
83
|
+
rowActions?: (folder: FolderTreeNode) => ReactNode;
|
|
77
84
|
/**
|
|
78
85
|
* Creating a folder is an IMAP mutation, so this resolves only once the mail
|
|
79
86
|
* server confirms the folder (docs/architecture/imap-mutations.md). The form
|
|
@@ -128,6 +135,7 @@ export const FolderTreePicker = ({
|
|
|
128
135
|
folders,
|
|
129
136
|
selectedId,
|
|
130
137
|
onSelect,
|
|
138
|
+
rowActions,
|
|
131
139
|
onCreateFolder,
|
|
132
140
|
onCancel,
|
|
133
141
|
delimiter = "/",
|
|
@@ -204,7 +212,7 @@ export const FolderTreePicker = ({
|
|
|
204
212
|
|
|
205
213
|
const activateRow = useCallback(
|
|
206
214
|
(row: FolderTreeRow) => {
|
|
207
|
-
if (isSelectable(row)) onSelect(row.folder.id);
|
|
215
|
+
if (isSelectable(row)) onSelect?.(row.folder.id);
|
|
208
216
|
setExpanded(row.folder.path, !row.expanded);
|
|
209
217
|
},
|
|
210
218
|
[onSelect, setExpanded],
|
|
@@ -252,7 +260,7 @@ export const FolderTreePicker = ({
|
|
|
252
260
|
setDraft(null);
|
|
253
261
|
setDraftName("");
|
|
254
262
|
if (parentPath) setExpanded(parentPath, true);
|
|
255
|
-
onSelect(created.id);
|
|
263
|
+
onSelect?.(created.id);
|
|
256
264
|
})
|
|
257
265
|
.catch((error: unknown) => {
|
|
258
266
|
if (isAbortError(error)) return;
|
|
@@ -367,6 +375,7 @@ export const FolderTreePicker = ({
|
|
|
367
375
|
currentTag={text.currentTag}
|
|
368
376
|
selected={selectable && folder.id === selectedId}
|
|
369
377
|
separated={separated}
|
|
378
|
+
actions={rowActions?.(folder)}
|
|
370
379
|
ariaLabel={rowAriaLabel(row)}
|
|
371
380
|
tabIndex={index === focusedIndex ? 0 : -1}
|
|
372
381
|
onActivate={() => activateRow(row)}
|
|
@@ -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}
|
|
@@ -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";
|
|
@@ -651,7 +648,7 @@ export interface FolderStepProps {
|
|
|
651
648
|
* (`buildMoveTargets`). The wizard does not reorder them: a second ordering
|
|
652
649
|
* beside the one the Move picker already applies would fight it.
|
|
653
650
|
*/
|
|
654
|
-
|
|
651
|
+
folders: readonly FolderTreeNode[];
|
|
655
652
|
/** The destination chosen so far. */
|
|
656
653
|
mailboxId?: string;
|
|
657
654
|
onSelect: (mailboxId: string) => void;
|
|
@@ -664,8 +661,11 @@ export interface FolderStepProps {
|
|
|
664
661
|
*/
|
|
665
662
|
onCreateFolder?: (
|
|
666
663
|
name: string,
|
|
664
|
+
parentPath: string,
|
|
667
665
|
signal?: AbortSignal,
|
|
668
|
-
) => Promise<
|
|
666
|
+
) => Promise<FolderTreeNode>;
|
|
667
|
+
/** The account's hierarchy separator, which the tree nests on. */
|
|
668
|
+
delimiter?: string;
|
|
669
669
|
/**
|
|
670
670
|
* Why there is no folder list to choose from — a selection spanning accounts
|
|
671
671
|
* has no single account whose folders these could be (#477 5.5). Said here
|
|
@@ -675,13 +675,14 @@ export interface FolderStepProps {
|
|
|
675
675
|
}
|
|
676
676
|
|
|
677
677
|
export function FolderStepBody({
|
|
678
|
-
|
|
678
|
+
folders,
|
|
679
679
|
mailboxId,
|
|
680
680
|
onSelect,
|
|
681
681
|
onCreateFolder,
|
|
682
|
+
delimiter,
|
|
682
683
|
restriction,
|
|
683
684
|
}: FolderStepProps) {
|
|
684
|
-
const chosen =
|
|
685
|
+
const chosen = folders.find((folder) => folder.id === mailboxId);
|
|
685
686
|
return (
|
|
686
687
|
<div className="flex min-h-0 flex-col gap-3">
|
|
687
688
|
<p
|
|
@@ -693,17 +694,16 @@ export function FolderStepBody({
|
|
|
693
694
|
{restriction ??
|
|
694
695
|
(chosen
|
|
695
696
|
? `Moving to ${chosen.label}.`
|
|
696
|
-
: "
|
|
697
|
+
: "Tap a folder to open it, or make a new one where you want it.")}
|
|
697
698
|
</p>
|
|
698
|
-
<div className="overflow-hidden rounded-lg border border-line bg-surface">
|
|
699
|
-
<
|
|
700
|
-
|
|
699
|
+
<div className="flex min-h-0 flex-1 overflow-hidden rounded-lg border border-line bg-surface">
|
|
700
|
+
<FolderTreePicker
|
|
701
|
+
folders={folders}
|
|
702
|
+
selectedId={mailboxId}
|
|
701
703
|
onSelect={onSelect}
|
|
702
704
|
onCreateFolder={onCreateFolder}
|
|
703
|
-
|
|
705
|
+
delimiter={delimiter}
|
|
704
706
|
labels={{
|
|
705
|
-
searchPlaceholder: "Move to…",
|
|
706
|
-
optionLabel: (label) => `Move to ${label}`,
|
|
707
707
|
createPending: "Waiting for the mail server to confirm the folder…",
|
|
708
708
|
}}
|
|
709
709
|
/>
|
package/src/index.ts
CHANGED
|
@@ -198,6 +198,20 @@ export {
|
|
|
198
198
|
type FilterSheetProps,
|
|
199
199
|
type FilterSheetSource,
|
|
200
200
|
} from "./components/filter-sheet.js";
|
|
201
|
+
export {
|
|
202
|
+
FolderManageActions,
|
|
203
|
+
type FolderManageActionsProps,
|
|
204
|
+
} from "./components/folder-manage-actions.js";
|
|
205
|
+
export {
|
|
206
|
+
FolderManager,
|
|
207
|
+
type FolderManagerLabels,
|
|
208
|
+
type FolderManagerProps,
|
|
209
|
+
type ManagedFolder,
|
|
210
|
+
} from "./components/folder-manager.js";
|
|
211
|
+
export {
|
|
212
|
+
FolderRenameDialog,
|
|
213
|
+
type FolderRenameDialogProps,
|
|
214
|
+
} from "./components/folder-rename-dialog.js";
|
|
201
215
|
export {
|
|
202
216
|
canonicalRoleLabel,
|
|
203
217
|
type FolderRole,
|
|
@@ -316,12 +330,6 @@ export {
|
|
|
316
330
|
MobileSearchView,
|
|
317
331
|
type MobileSearchViewProps,
|
|
318
332
|
} from "./components/mobile-search-view.js";
|
|
319
|
-
export {
|
|
320
|
-
type MoveMailboxOption,
|
|
321
|
-
MoveMailboxPicker,
|
|
322
|
-
type MoveMailboxPickerLabels,
|
|
323
|
-
type MoveMailboxPickerProps,
|
|
324
|
-
} from "./components/move-mailbox-picker.js";
|
|
325
333
|
export {
|
|
326
334
|
NavSidebar,
|
|
327
335
|
type NavSidebarProps,
|