@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/ui",
3
- "version": "0.0.148",
3
+ "version": "0.0.150",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -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
- const next: Record<string, RoleAppointment> = {
105
- ...prev,
106
- [role]: mailboxId
107
- ? { mailboxId, source: "Appointed" }
108
- : { mailboxId: null, source: "None" },
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
- Appointing a folder here removes it from any other role. Each row says
305
- where its answer came from — your choice, the mail server's own flag,
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
- "Emptying a folder erases everything in it from the mail server, and that cannot be restored.";
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: `Moving them to ${folderLabel}`,
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 expunges = action.kind === "emptyTrash";
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