@remit/web-client 0.0.176 → 0.0.178
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.178",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -21,7 +21,7 @@ import type { RefObject } from "react";
|
|
|
21
21
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
22
22
|
import { useErrorBanners } from "@/components/ui/ErrorBannerProvider";
|
|
23
23
|
import { formatErrorMessage } from "@/components/ui/ErrorState";
|
|
24
|
-
import { useJunkMailbox } from "@/hooks/useArchiveMailbox";
|
|
24
|
+
import { useJunkMailbox, useTrashMailbox } from "@/hooks/useArchiveMailbox";
|
|
25
25
|
import {
|
|
26
26
|
type EscalatedAction,
|
|
27
27
|
type EscalationSearchQuery,
|
|
@@ -46,7 +46,11 @@ import {
|
|
|
46
46
|
escalatedStatusLabel,
|
|
47
47
|
escalationActionLabel,
|
|
48
48
|
} from "@/lib/escalation-label";
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
type DeleteOutcome,
|
|
51
|
+
deleteConfirmationCopy,
|
|
52
|
+
formatEmailDate,
|
|
53
|
+
} from "@/lib/format";
|
|
50
54
|
import { junkDestination } from "@/lib/junk-destination";
|
|
51
55
|
import { tabStopId } from "@/lib/list-focus";
|
|
52
56
|
import { useListHeaderChrome } from "@/lib/list-header-chrome";
|
|
@@ -296,6 +300,18 @@ export const MessageList = ({
|
|
|
296
300
|
const { junkMailboxId } = useJunkMailbox(accountId);
|
|
297
301
|
const junkDestinationId = junkDestination(junkMailboxId, mailboxId);
|
|
298
302
|
|
|
303
|
+
// Deleting inside Trash is an expunge on the mail server, not a move, so the
|
|
304
|
+
// confirmation has to ask that question instead of "move to Trash?" (#845).
|
|
305
|
+
// Until the appointment resolves the outcome is genuinely unknown, and the
|
|
306
|
+
// dialog says so rather than guessing the reversible half of the answer.
|
|
307
|
+
const { trashMailboxId, isLoading: isTrashRoleLoading } =
|
|
308
|
+
useTrashMailbox(accountId);
|
|
309
|
+
const deleteOutcome: DeleteOutcome = isTrashRoleLoading
|
|
310
|
+
? "unknown"
|
|
311
|
+
: trashMailboxId === mailboxId
|
|
312
|
+
? "permanent"
|
|
313
|
+
: "trash";
|
|
314
|
+
|
|
299
315
|
// Selection state
|
|
300
316
|
const {
|
|
301
317
|
selectedIds,
|
|
@@ -1339,11 +1355,9 @@ export const MessageList = ({
|
|
|
1339
1355
|
/>
|
|
1340
1356
|
<ConfirmDialog
|
|
1341
1357
|
isOpen={pendingDelete !== null}
|
|
1342
|
-
|
|
1343
|
-
description="You can restore them from Trash later."
|
|
1344
|
-
confirmLabel="Move to Trash"
|
|
1358
|
+
{...deleteConfirmationCopy(pendingDelete?.length ?? 0, deleteOutcome)}
|
|
1345
1359
|
destructive
|
|
1346
|
-
isBusy={isDeleting}
|
|
1360
|
+
isBusy={isDeleting || deleteOutcome === "unknown"}
|
|
1347
1361
|
onConfirm={handleConfirmDelete}
|
|
1348
1362
|
onCancel={handleCancelDelete}
|
|
1349
1363
|
/>
|
|
@@ -80,6 +80,18 @@ export const useJunkMailbox = (
|
|
|
80
80
|
return { junkMailboxId: mailboxId, isLoading };
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Returns the account's appointed Trash mailbox id. The delete confirmation
|
|
85
|
+
* needs it to tell a move-to-Trash apart from a delete inside Trash, which is
|
|
86
|
+
* an unrecoverable expunge and has to be asked as one (#845).
|
|
87
|
+
*/
|
|
88
|
+
export const useTrashMailbox = (
|
|
89
|
+
accountId: string | undefined,
|
|
90
|
+
): { trashMailboxId: string | undefined; isLoading: boolean } => {
|
|
91
|
+
const { mailboxId, isLoading } = useFolderRoleMailbox(accountId, "Trash");
|
|
92
|
+
return { trashMailboxId: mailboxId, isLoading };
|
|
93
|
+
};
|
|
94
|
+
|
|
83
95
|
/**
|
|
84
96
|
* Returns an account's full role→mailbox appointment map, for callers that
|
|
85
97
|
* need more than one role at once (e.g. the Move-to picker excluding both
|
package/src/lib/format.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import {
|
|
4
|
+
deleteConfirmationCopy,
|
|
4
5
|
formatDate,
|
|
5
6
|
formatDatePreset,
|
|
6
7
|
formatDeleteToTrashTitle,
|
|
@@ -120,3 +121,37 @@ describe("formatDeleteToTrashTitle", () => {
|
|
|
120
121
|
);
|
|
121
122
|
});
|
|
122
123
|
});
|
|
124
|
+
|
|
125
|
+
describe("deleteConfirmationCopy", () => {
|
|
126
|
+
test("asks to move when the delete files the mail in Trash", () => {
|
|
127
|
+
assert.deepStrictEqual(deleteConfirmationCopy(3, "trash"), {
|
|
128
|
+
title: "Move 3 messages to Trash?",
|
|
129
|
+
description: "You can restore them from Trash later.",
|
|
130
|
+
confirmLabel: "Move to Trash",
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("asks about destruction when the delete expunges", () => {
|
|
135
|
+
assert.deepStrictEqual(deleteConfirmationCopy(3, "permanent"), {
|
|
136
|
+
title: "Permanently delete 3 messages?",
|
|
137
|
+
description:
|
|
138
|
+
"They are erased from the mail server and cannot be restored.",
|
|
139
|
+
confirmLabel: "Delete permanently",
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("uses the singular noun for one message on the permanent path", () => {
|
|
144
|
+
assert.strictEqual(
|
|
145
|
+
deleteConfirmationCopy(1, "permanent").title,
|
|
146
|
+
"Permanently delete 1 message?",
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("commits to neither wording until the Trash appointment resolves", () => {
|
|
151
|
+
assert.deepStrictEqual(deleteConfirmationCopy(3, "unknown"), {
|
|
152
|
+
title: "Delete 3 messages?",
|
|
153
|
+
description: "Checking where this account files deleted mail…",
|
|
154
|
+
confirmLabel: "Delete",
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
package/src/lib/format.ts
CHANGED
|
@@ -160,3 +160,53 @@ export const formatDeleteToTrashTitle = (count: number): string => {
|
|
|
160
160
|
const noun = count === 1 ? "message" : "messages";
|
|
161
161
|
return `Move ${quantity} ${noun} to Trash?`;
|
|
162
162
|
};
|
|
163
|
+
|
|
164
|
+
export interface DeleteConfirmationCopy {
|
|
165
|
+
title: string;
|
|
166
|
+
description: string;
|
|
167
|
+
confirmLabel: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* What a delete will actually do to the selected mail. `unknown` is the state
|
|
172
|
+
* before the account's Trash appointment has resolved — a real state on a cold
|
|
173
|
+
* open, and the one the copy must not guess at, because guessing "move to
|
|
174
|
+
* Trash" over an expunge is the dishonesty this whole flow exists to remove.
|
|
175
|
+
*/
|
|
176
|
+
export type DeleteOutcome = "trash" | "permanent" | "unknown";
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The confirmation for a delete, worded for what the delete actually does.
|
|
180
|
+
* Deleting mail that already sits in Trash expunges it on the server and
|
|
181
|
+
* nothing survives that, so it is asked as a permanent delete — a dialog that
|
|
182
|
+
* says "Move to Trash" over an expunge collects an answer to a question the
|
|
183
|
+
* user was never asked.
|
|
184
|
+
*/
|
|
185
|
+
export const deleteConfirmationCopy = (
|
|
186
|
+
count: number,
|
|
187
|
+
outcome: DeleteOutcome,
|
|
188
|
+
): DeleteConfirmationCopy => {
|
|
189
|
+
const quantity = count === 1 ? "1" : formatNumber(count);
|
|
190
|
+
const noun = count === 1 ? "message" : "messages";
|
|
191
|
+
|
|
192
|
+
if (outcome === "unknown") {
|
|
193
|
+
return {
|
|
194
|
+
title: `Delete ${quantity} ${noun}?`,
|
|
195
|
+
description: "Checking where this account files deleted mail…",
|
|
196
|
+
confirmLabel: "Delete",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (outcome === "permanent") {
|
|
200
|
+
return {
|
|
201
|
+
title: `Permanently delete ${quantity} ${noun}?`,
|
|
202
|
+
description:
|
|
203
|
+
"They are erased from the mail server and cannot be restored.",
|
|
204
|
+
confirmLabel: "Delete permanently",
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
title: formatDeleteToTrashTitle(count),
|
|
209
|
+
description: "You can restore them from Trash later.",
|
|
210
|
+
confirmLabel: "Move to Trash",
|
|
211
|
+
};
|
|
212
|
+
};
|
|
@@ -31,7 +31,7 @@ describe("junkWithheldReason", () => {
|
|
|
31
31
|
|
|
32
32
|
it("names the setting that appoints a folder", () => {
|
|
33
33
|
assert.equal(junkWithheldReason(undefined, INBOX), NO_JUNK_FOLDER_REASON);
|
|
34
|
-
assert.match(NO_JUNK_FOLDER_REASON, /Settings ›
|
|
34
|
+
assert.match(NO_JUNK_FOLDER_REASON, /Settings › Folder roles/);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it("says the mail is already where the verb would put it", () => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export const NO_JUNK_FOLDER_REASON =
|
|
9
|
-
"This account has no Junk folder appointed, so there is nowhere to file these. Appoint one under Settings ›
|
|
9
|
+
"This account has no Junk folder appointed, so there is nowhere to file these. Appoint one under Settings › Folder roles.";
|
|
10
10
|
|
|
11
11
|
export const ALREADY_IN_JUNK_REASON =
|
|
12
12
|
"These are already in Junk, so there is nowhere to file them.";
|