@remit/ui 0.0.149 → 0.0.151

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.149",
3
+ "version": "0.0.151",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src"
@@ -258,6 +258,29 @@ function RoleAppointmentRow({
258
258
  );
259
259
  }
260
260
 
261
+ export function FolderRolesHelp() {
262
+ return (
263
+ <div className="space-y-3">
264
+ <p>
265
+ Each canonical role — Inbox, Drafts, Sent, Archive, Spam, Trash — points
266
+ at one of your account's real folders. Pick the one that actually holds
267
+ the mail; the message counts tell real folders from empty look-alikes.
268
+ </p>
269
+ <p>
270
+ Appointing a folder to a role here doesn't touch any other role, and
271
+ doesn't move or rename anything on the server — it just tells Remit
272
+ which folder to treat as e.g. "Drafts" everywhere (sidebar, unread
273
+ badges, the compose flow).
274
+ </p>
275
+ <p>
276
+ <strong className="text-fg">Your folders</strong> is the account's real
277
+ hierarchy. Open a folder to see what's inside it, make a new one where
278
+ you're looking, and rename or delete any of them from its row.
279
+ </p>
280
+ </div>
281
+ );
282
+ }
283
+
261
284
  export interface RoleAppointmentListProps {
262
285
  accountEmail: string;
263
286
  /** Every folder the account exposes (candidates for any role). */
@@ -299,10 +322,8 @@ export function RoleAppointmentList({
299
322
  Folder roles — {accountEmail}
300
323
  </h2>
301
324
  <p className="text-xs text-fg-muted">
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. Each row
304
- says where its answer came from — your choice, the mail server's own
305
- flag, or a name reader matched.
325
+ Each row says where its answer came from your choice, the mail
326
+ server's own flag, or a name reader matched.
306
327
  </p>
307
328
  </header>
308
329
  <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
package/src/index.ts CHANGED
@@ -591,6 +591,7 @@ export {
591
591
  export {
592
592
  APPOINTABLE_ROLES,
593
593
  type CandidateFolder,
594
+ FolderRolesHelp,
594
595
  type RoleAppointment,
595
596
  RoleAppointmentList,
596
597
  type RoleAppointmentListProps,