@remit/ui 0.0.148 → 0.0.150
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/role-appointment-list.stories.tsx +6 -17
- package/src/components/role-appointment-list.tsx +3 -4
- package/src/components/role-appointment-prompt.render.test.ts +52 -0
- package/src/components/role-appointment-prompt.stories.tsx +49 -0
- package/src/components/role-appointment-prompt.tsx +34 -4
package/package.json
CHANGED
|
@@ -100,23 +100,12 @@ function Harness({
|
|
|
100
100
|
const [displayNames, setDisplayNames] = useState<Record<string, string>>({});
|
|
101
101
|
|
|
102
102
|
const handleAppoint = (role: FolderRole, mailboxId: string | null) => {
|
|
103
|
-
setAppointments((prev) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
};
|
|
110
|
-
// Exclusivity: appointing a folder to one role clears it from any other.
|
|
111
|
-
if (mailboxId) {
|
|
112
|
-
for (const other of Object.keys(next)) {
|
|
113
|
-
if (other === role) continue;
|
|
114
|
-
if (next[other]?.mailboxId !== mailboxId) continue;
|
|
115
|
-
next[other] = { mailboxId: null, source: "None" };
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return next;
|
|
119
|
-
});
|
|
103
|
+
setAppointments((prev) => ({
|
|
104
|
+
...prev,
|
|
105
|
+
[role]: mailboxId
|
|
106
|
+
? { mailboxId, source: "Appointed" }
|
|
107
|
+
: { mailboxId: null, source: "None" },
|
|
108
|
+
}));
|
|
120
109
|
};
|
|
121
110
|
|
|
122
111
|
const handleRename = (mailboxId: string, name: string) =>
|
|
@@ -300,10 +300,9 @@ export function RoleAppointmentList({
|
|
|
300
300
|
</h2>
|
|
301
301
|
<p className="text-xs text-fg-muted">
|
|
302
302
|
Each role points to one folder. Pick the folder that holds the mail —
|
|
303
|
-
the counts help you tell real folders from empty look-alikes.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
or a name reader matched.
|
|
303
|
+
the counts help you tell real folders from empty look-alikes. Each row
|
|
304
|
+
says where its answer came from — your choice, the mail server's own
|
|
305
|
+
flag, or a name reader matched.
|
|
307
306
|
</p>
|
|
308
307
|
</header>
|
|
309
308
|
<div className="rounded-sm border border-line bg-surface">
|
|
@@ -27,6 +27,14 @@ const folders: FolderTreeNode[] = [
|
|
|
27
27
|
|
|
28
28
|
const noop = () => {};
|
|
29
29
|
|
|
30
|
+
/** A delete refused because its rows already sit in the folder reader guessed. */
|
|
31
|
+
const refusedInPlace: Partial<RoleAppointmentPromptProps> = {
|
|
32
|
+
reason: "unconfirmed",
|
|
33
|
+
action: { kind: "delete", count: 3 },
|
|
34
|
+
trashFolderLabel: "Deleted Messages",
|
|
35
|
+
guessedMailboxId: "mb-deleted",
|
|
36
|
+
};
|
|
37
|
+
|
|
30
38
|
const render = (props: Partial<RoleAppointmentPromptProps>) =>
|
|
31
39
|
renderToString(
|
|
32
40
|
createElement(RoleAppointmentPrompt, {
|
|
@@ -127,6 +135,19 @@ describe("roleAppointmentPromptCopy", () => {
|
|
|
127
135
|
);
|
|
128
136
|
assert.equal(copy.confirmLabel, "Set as Trash and empty it");
|
|
129
137
|
});
|
|
138
|
+
|
|
139
|
+
it("words the irreversibility as a delete of these rows, not a folder emptying (#876)", () => {
|
|
140
|
+
const copy = roleAppointmentPromptCopy(
|
|
141
|
+
"unconfirmed",
|
|
142
|
+
{ kind: "delete", count: 3 },
|
|
143
|
+
{ trashFolderLabel: "Deleted Messages" },
|
|
144
|
+
);
|
|
145
|
+
assert.equal(
|
|
146
|
+
copy.description,
|
|
147
|
+
"Nothing has been deleted. reader files this account's deleted mail in Deleted Messages because of its name — you never chose it, and the mail server doesn't mark it as Trash. Deleting 3 messages there erases them from the mail server, and that cannot be restored.",
|
|
148
|
+
);
|
|
149
|
+
assert.equal(copy.confirmLabel, "Set as Trash and delete 3 messages");
|
|
150
|
+
});
|
|
130
151
|
});
|
|
131
152
|
|
|
132
153
|
describe("RoleAppointmentPrompt", () => {
|
|
@@ -214,6 +235,37 @@ describe("RoleAppointmentPrompt", () => {
|
|
|
214
235
|
);
|
|
215
236
|
});
|
|
216
237
|
|
|
238
|
+
it("narrates a delete inside the confirmed folder as an erase, not a move (#876)", () => {
|
|
239
|
+
const inPlace = render({
|
|
240
|
+
...refusedInPlace,
|
|
241
|
+
selectedId: "mb-deleted",
|
|
242
|
+
phase: { kind: "acting" },
|
|
243
|
+
});
|
|
244
|
+
assert.match(inPlace, /Erasing them from Deleted Messages/);
|
|
245
|
+
assert.doesNotMatch(inPlace, /Moving them to/);
|
|
246
|
+
|
|
247
|
+
// The rows are in the guessed folder; into any other folder they move.
|
|
248
|
+
assert.match(
|
|
249
|
+
render({
|
|
250
|
+
...refusedInPlace,
|
|
251
|
+
selectedId: "mb-prullenbak",
|
|
252
|
+
phase: { kind: "acting" },
|
|
253
|
+
}),
|
|
254
|
+
/Moving them to Prullenbak/,
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("marks the confirm that erases in place as the destructive one", () => {
|
|
259
|
+
assert.match(
|
|
260
|
+
render({ ...refusedInPlace, selectedId: "mb-deleted" }),
|
|
261
|
+
/text-danger/,
|
|
262
|
+
);
|
|
263
|
+
assert.match(
|
|
264
|
+
render({ ...refusedInPlace, selectedId: "mb-prullenbak" }),
|
|
265
|
+
/text-warning/,
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
217
269
|
it("keeps the picker and the selection through a failed appointment", () => {
|
|
218
270
|
const html = render({
|
|
219
271
|
selectedId: "mb-prullenbak",
|
|
@@ -129,6 +129,55 @@ export const Unconfirmed: Story = {
|
|
|
129
129
|
},
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* A delete refused because the rows are already inside the folder reader only
|
|
134
|
+
* matched by name: confirming it expunges them where they sit, so the copy says
|
|
135
|
+
* "delete", never "empty", and the confirm is the danger variant.
|
|
136
|
+
*/
|
|
137
|
+
export const UnconfirmedDelete: Story = {
|
|
138
|
+
name: "unconfirmed — delete in place",
|
|
139
|
+
args: {
|
|
140
|
+
reason: "unconfirmed",
|
|
141
|
+
action: { kind: "delete", count: 3 },
|
|
142
|
+
trashFolderLabel: "Deleted Messages",
|
|
143
|
+
guessedMailboxId: "mb-deleted",
|
|
144
|
+
initialSelectedId: "mb-deleted",
|
|
145
|
+
phase: choosing,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The same refusal answered with a different folder. The rows are not in that
|
|
151
|
+
* one, so the delete moves them there and the confirm is not destructive.
|
|
152
|
+
*/
|
|
153
|
+
export const UnconfirmedDeleteElsewhere: Story = {
|
|
154
|
+
name: "unconfirmed — delete into another folder",
|
|
155
|
+
args: {
|
|
156
|
+
reason: "unconfirmed",
|
|
157
|
+
action: { kind: "delete", count: 3 },
|
|
158
|
+
trashFolderLabel: "Deleted Messages",
|
|
159
|
+
guessedMailboxId: "mb-deleted",
|
|
160
|
+
initialSelectedId: "mb-prullenbak",
|
|
161
|
+
phase: choosing,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* That confirm in flight. It says the rows are being erased where they sit —
|
|
167
|
+
* "moving them to Deleted Messages" would narrate the opposite.
|
|
168
|
+
*/
|
|
169
|
+
export const UnconfirmedDeleteInFlight: Story = {
|
|
170
|
+
name: "unconfirmed — delete in place, acting",
|
|
171
|
+
args: {
|
|
172
|
+
reason: "unconfirmed",
|
|
173
|
+
action: { kind: "delete", count: 3 },
|
|
174
|
+
trashFolderLabel: "Deleted Messages",
|
|
175
|
+
guessedMailboxId: "mb-deleted",
|
|
176
|
+
initialSelectedId: "mb-deleted",
|
|
177
|
+
phase: { kind: "acting" },
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
132
181
|
/**
|
|
133
182
|
* Two writes behind one press, and the story steps through both: the
|
|
134
183
|
* appointment, then the delete it unblocks. Neither has a way out — the write
|
|
@@ -102,7 +102,9 @@ export const roleAppointmentPromptCopy = (
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
const irreversible =
|
|
105
|
-
|
|
105
|
+
action.kind === "delete"
|
|
106
|
+
? `Deleting ${quantified(action.count)} there erases them from the mail server, and that cannot be restored.`
|
|
107
|
+
: "Emptying a folder erases everything in it from the mail server, and that cannot be restored.";
|
|
106
108
|
return {
|
|
107
109
|
title: "Confirm this account's Trash folder",
|
|
108
110
|
description: trashFolderLabel
|
|
@@ -125,6 +127,7 @@ const pendingCopy = (
|
|
|
125
127
|
phase: PromptPhase,
|
|
126
128
|
action: PromptAction,
|
|
127
129
|
folderLabel: string,
|
|
130
|
+
inPlace: boolean,
|
|
128
131
|
): PendingCopy => {
|
|
129
132
|
if (phase.kind === "appointing") {
|
|
130
133
|
return { headline: "Setting the Trash folder…", status: folderLabel };
|
|
@@ -132,7 +135,9 @@ const pendingCopy = (
|
|
|
132
135
|
if (action.kind === "delete") {
|
|
133
136
|
return {
|
|
134
137
|
headline: `Deleting ${quantified(action.count)}…`,
|
|
135
|
-
status:
|
|
138
|
+
status: inPlace
|
|
139
|
+
? `Erasing them from ${folderLabel}. This can't be undone.`
|
|
140
|
+
: `Moving them to ${folderLabel}`,
|
|
136
141
|
};
|
|
137
142
|
}
|
|
138
143
|
return {
|
|
@@ -141,6 +146,24 @@ const pendingCopy = (
|
|
|
141
146
|
};
|
|
142
147
|
};
|
|
143
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The confirm expunges rather than moves. A delete is only refused as
|
|
151
|
+
* `unconfirmed` because its rows are already inside the folder reader guessed
|
|
152
|
+
* (#876), so confirming that same folder deletes them where they sit — and
|
|
153
|
+
* "moving them there" would narrate the opposite of what happens. Any other
|
|
154
|
+
* folder still moves them.
|
|
155
|
+
*/
|
|
156
|
+
const deletesInPlace = (
|
|
157
|
+
reason: PromptReason,
|
|
158
|
+
action: PromptAction,
|
|
159
|
+
guessedMailboxId: string | undefined,
|
|
160
|
+
selectedId: string | undefined,
|
|
161
|
+
): boolean =>
|
|
162
|
+
reason === "unconfirmed" &&
|
|
163
|
+
action.kind === "delete" &&
|
|
164
|
+
guessedMailboxId !== undefined &&
|
|
165
|
+
selectedId === guessedMailboxId;
|
|
166
|
+
|
|
144
167
|
const failureCopy = (
|
|
145
168
|
cause: "generic" | "mailbox-pending",
|
|
146
169
|
action: PromptAction,
|
|
@@ -159,6 +182,11 @@ export interface RoleAppointmentPromptProps {
|
|
|
159
182
|
delimiter: string;
|
|
160
183
|
/** `unconfirmed`: the folder reader guessed from its name. */
|
|
161
184
|
trashFolderLabel?: string;
|
|
185
|
+
/**
|
|
186
|
+
* `unconfirmed`: that same folder, by id. Confirming it completes a delete of
|
|
187
|
+
* rows already inside it, which erases them rather than moving them.
|
|
188
|
+
*/
|
|
189
|
+
guessedMailboxId?: string;
|
|
162
190
|
/** `stale`: the folder that vanished from the mail server. */
|
|
163
191
|
staleFolderLabel?: string;
|
|
164
192
|
/** Only on instances holding more than one account. */
|
|
@@ -188,6 +216,7 @@ export const RoleAppointmentPrompt = ({
|
|
|
188
216
|
folders,
|
|
189
217
|
delimiter,
|
|
190
218
|
trashFolderLabel,
|
|
219
|
+
guessedMailboxId,
|
|
191
220
|
staleFolderLabel,
|
|
192
221
|
accountEmail,
|
|
193
222
|
phase,
|
|
@@ -214,12 +243,13 @@ export const RoleAppointmentPrompt = ({
|
|
|
214
243
|
staleFolderLabel,
|
|
215
244
|
selectedCount: selected?.messageCount,
|
|
216
245
|
});
|
|
217
|
-
const
|
|
246
|
+
const inPlace = deletesInPlace(reason, action, guessedMailboxId, selectedId);
|
|
247
|
+
const expunges = action.kind === "emptyTrash" || inPlace;
|
|
218
248
|
|
|
219
249
|
if (!open) return null;
|
|
220
250
|
|
|
221
251
|
if (pending) {
|
|
222
|
-
const words = pendingCopy(phase, action, selected?.label ?? "");
|
|
252
|
+
const words = pendingCopy(phase, action, selected?.label ?? "", inPlace);
|
|
223
253
|
return (
|
|
224
254
|
<Dialog open={open} onClose={() => {}} title={copy.title}>
|
|
225
255
|
<div
|