@remit/web-client 0.0.89 → 0.0.91
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/mail/DailyBrief.selection.test.ts +19 -15
- package/src/components/mail/DailyBrief.tsx +70 -89
- package/src/components/mail/FlaggedList.tsx +58 -50
- package/src/components/mail/LabelApplyTrigger.tsx +11 -5
- package/src/components/mail/MailListHeader.tsx +165 -85
- package/src/components/mail/MailViewChrome.tsx +7 -6
- package/src/components/mail/MessageList.selection.test.ts +34 -36
- package/src/components/mail/MessageList.tsx +64 -96
- package/src/components/mail/MoveToTrigger.tsx +1 -1
- package/src/components/mail/ThreadListInteraction.test.ts +8 -2
- package/src/components/mail/ThreadListInteraction.tsx +47 -24
- package/src/hooks/useInitialSyncProgress.render.test.ts +11 -5
- package/src/hooks/useSelectedSubjects.ts +0 -0
- package/src/lib/list-header-chrome.ts +50 -0
- package/src/components/mail/SelectionToolbar.render.test.ts +0 -324
- package/src/components/mail/SelectionToolbar.stories.tsx +0 -199
- package/src/components/mail/SelectionToolbar.tsx +0 -274
- package/src/lib/selection-sheet-mode.test.ts +0 -105
- package/src/lib/selection-sheet-mode.ts +0 -49
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The bulk-action bar. Two rules govern it: actions are never disabled (they
|
|
3
|
-
* no-op and explain — `doc/rules/ux.md`), and Move/Organize only appear when
|
|
4
|
-
* the selection is inside a single account, because neither works across
|
|
5
|
-
* accounts.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { afterEach, describe, it } from "node:test";
|
|
10
|
-
import { labelOperationsListLabelsQueryKey } from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
11
|
-
import { createElement } from "react";
|
|
12
|
-
import { createDomHarness, type DomHarness } from "../../test-support/dom";
|
|
13
|
-
import { SelectionToolbar } from "./SelectionToolbar";
|
|
14
|
-
|
|
15
|
-
let harness: DomHarness | undefined;
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
harness?.close();
|
|
19
|
-
harness = undefined;
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
type ToolbarProps = Parameters<typeof SelectionToolbar>[0];
|
|
23
|
-
|
|
24
|
-
const mount = (
|
|
25
|
-
props: Partial<ToolbarProps> = {},
|
|
26
|
-
options: { labels?: { labelId: string; name: string; color: string }[] } = {},
|
|
27
|
-
): DomHarness => {
|
|
28
|
-
harness = createDomHarness();
|
|
29
|
-
if (props.accountId) {
|
|
30
|
-
harness.queryClient.setQueryData(
|
|
31
|
-
labelOperationsListLabelsQueryKey({
|
|
32
|
-
path: { accountId: props.accountId },
|
|
33
|
-
}),
|
|
34
|
-
{ items: options.labels ?? [] },
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
harness.renderApp(
|
|
38
|
-
createElement(SelectionToolbar, {
|
|
39
|
-
selectedCount: 2,
|
|
40
|
-
onDelete: () => undefined,
|
|
41
|
-
onClearSelection: () => undefined,
|
|
42
|
-
...props,
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
return harness;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
describe("SelectionToolbar", () => {
|
|
49
|
-
it("renders nothing while nothing is selected", () => {
|
|
50
|
-
const dom = mount({ selectedCount: 0 });
|
|
51
|
-
assert.equal(dom.html(), "");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("counts the selection in words the user reads", () => {
|
|
55
|
-
assert.match(mount({ selectedCount: 1 }).text(), /1 message selected/);
|
|
56
|
-
harness?.close();
|
|
57
|
-
harness = undefined;
|
|
58
|
-
assert.match(mount({ selectedCount: 4 }).text(), /4 messages selected/);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("hides Move until both an account and a source mailbox are known", () => {
|
|
62
|
-
const dom = mount({ onMove: () => undefined, accountId: "acc-1" });
|
|
63
|
-
assert.equal(dom.query('[aria-label="Move selected messages"]'), null);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("offers Move and Organize once the selection sits in one account", () => {
|
|
67
|
-
const dom = mount({
|
|
68
|
-
onMove: () => undefined,
|
|
69
|
-
onOrganize: () => undefined,
|
|
70
|
-
accountId: "acc-1",
|
|
71
|
-
currentMailboxId: "mbx-inbox",
|
|
72
|
-
});
|
|
73
|
-
assert.ok(dom.query('[aria-label="Move selected messages"]'));
|
|
74
|
-
assert.ok(dom.query('[aria-label="Organize similar messages"]'));
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("hides the label trigger without a materialized selection (issue #26)", () => {
|
|
78
|
-
const dom = mount({
|
|
79
|
-
accountId: "acc-1",
|
|
80
|
-
currentMailboxId: "mbx-inbox",
|
|
81
|
-
});
|
|
82
|
-
assert.equal(
|
|
83
|
-
dom.query('[aria-label="Apply label to selected messages"]'),
|
|
84
|
-
null,
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("hides the label trigger when the account has no labels yet", () => {
|
|
89
|
-
const dom = mount(
|
|
90
|
-
{
|
|
91
|
-
accountId: "acc-1",
|
|
92
|
-
currentMailboxId: "mbx-inbox",
|
|
93
|
-
selectedMessageIds: ["m1", "m2"],
|
|
94
|
-
},
|
|
95
|
-
{ labels: [] },
|
|
96
|
-
);
|
|
97
|
-
assert.equal(
|
|
98
|
-
dom.query('[aria-label="Apply label to selected messages"]'),
|
|
99
|
-
null,
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("offers the label trigger once a selection, account, and mailbox are all known", () => {
|
|
104
|
-
const dom = mount(
|
|
105
|
-
{
|
|
106
|
-
accountId: "acc-1",
|
|
107
|
-
currentMailboxId: "mbx-inbox",
|
|
108
|
-
selectedMessageIds: ["m1", "m2"],
|
|
109
|
-
},
|
|
110
|
-
{ labels: [{ labelId: "l1", name: "Receipts", color: "Blue" }] },
|
|
111
|
-
);
|
|
112
|
-
assert.ok(dom.query('[aria-label="Apply label to selected messages"]'));
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("withdraws Organize and explains why when the selection spans accounts", () => {
|
|
116
|
-
const dom = mount({
|
|
117
|
-
onMove: () => undefined,
|
|
118
|
-
onOrganize: () => undefined,
|
|
119
|
-
accountId: "acc-1",
|
|
120
|
-
currentMailboxId: "mbx-inbox",
|
|
121
|
-
moveDisabledHint: "Select messages from one account to move them",
|
|
122
|
-
});
|
|
123
|
-
assert.equal(dom.query('[aria-label="Organize similar messages"]'), null);
|
|
124
|
-
assert.match(dom.text(), /Select messages from one account to move them/);
|
|
125
|
-
assert.match(
|
|
126
|
-
dom.query('[role="status"]')?.textContent ?? "",
|
|
127
|
-
/one account/,
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("keeps every action pressable while a mutation is in flight (ux.md)", () => {
|
|
132
|
-
const dom = mount({ isDeleting: true, onMarkAsRead: () => undefined });
|
|
133
|
-
for (const button of dom.queryAll<HTMLButtonElement>("button")) {
|
|
134
|
-
assert.equal(button.disabled, false);
|
|
135
|
-
}
|
|
136
|
-
assert.equal(
|
|
137
|
-
dom.byLabel("Delete selected messages").getAttribute("aria-busy"),
|
|
138
|
-
"true",
|
|
139
|
-
);
|
|
140
|
-
assert.match(dom.text(), /Deleting\.\.\./);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("no-ops rather than firing a second time while busy", () => {
|
|
144
|
-
let deletes = 0;
|
|
145
|
-
let marks = 0;
|
|
146
|
-
const dom = mount({
|
|
147
|
-
isMoving: true,
|
|
148
|
-
onDelete: () => {
|
|
149
|
-
deletes += 1;
|
|
150
|
-
},
|
|
151
|
-
onMarkAsRead: () => {
|
|
152
|
-
marks += 1;
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
dom.click(dom.byLabel("Delete selected messages"));
|
|
156
|
-
dom.click(dom.byLabel("Mark as read"));
|
|
157
|
-
assert.equal(deletes, 0);
|
|
158
|
-
assert.equal(marks, 0);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("fires delete, mark-read and clear when idle", () => {
|
|
162
|
-
let cleared = 0;
|
|
163
|
-
let deletes = 0;
|
|
164
|
-
let marks = 0;
|
|
165
|
-
const dom = mount({
|
|
166
|
-
onDelete: () => {
|
|
167
|
-
deletes += 1;
|
|
168
|
-
},
|
|
169
|
-
onClearSelection: () => {
|
|
170
|
-
cleared += 1;
|
|
171
|
-
},
|
|
172
|
-
onMarkAsRead: () => {
|
|
173
|
-
marks += 1;
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
dom.click(dom.byLabel("Delete selected messages"));
|
|
177
|
-
dom.click(dom.byLabel("Mark as read"));
|
|
178
|
-
dom.click(dom.byLabel("Clear selection"));
|
|
179
|
-
assert.deepEqual([deletes, marks, cleared], [1, 1, 1]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("opens the organize flow for the current selection", () => {
|
|
183
|
-
let organized = 0;
|
|
184
|
-
const dom = mount({
|
|
185
|
-
onMove: () => undefined,
|
|
186
|
-
accountId: "acc-1",
|
|
187
|
-
currentMailboxId: "mbx-inbox",
|
|
188
|
-
onOrganize: () => {
|
|
189
|
-
organized += 1;
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
dom.click(dom.byLabel("Organize similar messages"));
|
|
193
|
-
assert.equal(organized, 1);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* The search-escalation states desktop gains in #212: the same offer →
|
|
199
|
-
* counting → escalated → chunked-run flow the mobile sheet carries, driven by
|
|
200
|
-
* the shared derivations in `MessageList`. These assert the toolbar routes each
|
|
201
|
-
* state, never that it re-implements the engine.
|
|
202
|
-
*/
|
|
203
|
-
describe("SelectionToolbar — search escalation", () => {
|
|
204
|
-
it("renders the select-all-loaded checkbox only when the control is wired", () => {
|
|
205
|
-
assert.equal(mount().query('[aria-label="Select all"]'), null);
|
|
206
|
-
harness?.close();
|
|
207
|
-
harness = undefined;
|
|
208
|
-
const dom = mount({
|
|
209
|
-
selectAll: {
|
|
210
|
-
checked: false,
|
|
211
|
-
indeterminate: true,
|
|
212
|
-
onChange: () => undefined,
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
assert.ok(dom.query('[aria-label="Select all"]'));
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("names the escalated scope through statusLabel instead of a bare count", () => {
|
|
219
|
-
const dom = mount({
|
|
220
|
-
selectedCount: 3412,
|
|
221
|
-
statusLabel: 'All 3,412 matching "npm" selected',
|
|
222
|
-
});
|
|
223
|
-
assert.match(dom.text(), /All 3,412 matching "npm" selected/);
|
|
224
|
-
assert.doesNotMatch(dom.text(), /messages selected/);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("hides Delete while the total is still counting, and offers Stop", () => {
|
|
228
|
-
let stopped = 0;
|
|
229
|
-
const dom = mount({
|
|
230
|
-
isCounting: true,
|
|
231
|
-
statusLabel: "Counting… 1,900 so far",
|
|
232
|
-
notice: {
|
|
233
|
-
tone: "info",
|
|
234
|
-
text: "",
|
|
235
|
-
action: {
|
|
236
|
-
label: "Stop",
|
|
237
|
-
onClick: () => {
|
|
238
|
-
stopped += 1;
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
assert.equal(dom.query('[aria-label="Delete selected messages"]'), null);
|
|
244
|
-
dom.click(dom.byText("button", "Stop"));
|
|
245
|
-
assert.equal(stopped, 1);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("offers the escalation control naming the scope, and escalating fires it", () => {
|
|
249
|
-
let escalated = 0;
|
|
250
|
-
const dom = mount({
|
|
251
|
-
selectAll: { checked: true, onChange: () => undefined },
|
|
252
|
-
notice: {
|
|
253
|
-
tone: "info",
|
|
254
|
-
text: "",
|
|
255
|
-
action: {
|
|
256
|
-
label: 'Select all matching "npm"',
|
|
257
|
-
onClick: () => {
|
|
258
|
-
escalated += 1;
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
dom.click(dom.byText("button", 'Select all matching "npm"'));
|
|
264
|
-
assert.equal(escalated, 1);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("keeps every verb over an escalated selection — not delete-only (#114)", () => {
|
|
268
|
-
const dom = mount({
|
|
269
|
-
selectedCount: 3412,
|
|
270
|
-
statusLabel: 'All 3,412 matching "npm" selected',
|
|
271
|
-
onMarkAsRead: () => undefined,
|
|
272
|
-
onMove: () => undefined,
|
|
273
|
-
onOrganize: () => undefined,
|
|
274
|
-
accountId: "acc-1",
|
|
275
|
-
currentMailboxId: "mbx-inbox",
|
|
276
|
-
notice: {
|
|
277
|
-
tone: "info",
|
|
278
|
-
text: "",
|
|
279
|
-
action: { label: "Clear selection", onClick: () => undefined },
|
|
280
|
-
},
|
|
281
|
-
});
|
|
282
|
-
assert.ok(dom.query('[aria-label="Mark as read"]'));
|
|
283
|
-
assert.ok(dom.query('[aria-label="Move selected messages"]'));
|
|
284
|
-
assert.ok(dom.query('[aria-label="Delete selected messages"]'));
|
|
285
|
-
// Organize has no escalated-predicate path, so it withdraws once the
|
|
286
|
-
// selection names a scope through statusLabel.
|
|
287
|
-
assert.equal(dom.query('[aria-label="Organize similar messages"]'), null);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it("shows a progress bar mid-run and takes the verbs off screen", () => {
|
|
291
|
-
const dom = mount({
|
|
292
|
-
selectedCount: 3412,
|
|
293
|
-
statusLabel: "Moving 1,200 of 3,412…",
|
|
294
|
-
onMarkAsRead: () => undefined,
|
|
295
|
-
progress: { value: 1200, max: 3412, tone: "info" },
|
|
296
|
-
});
|
|
297
|
-
assert.ok(dom.query('[role="progressbar"]'));
|
|
298
|
-
assert.equal(dom.query('[aria-label="Delete selected messages"]'), null);
|
|
299
|
-
assert.equal(dom.query('[aria-label="Mark as read"]'), null);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("names the succeeded count and retries exactly what is left over", () => {
|
|
303
|
-
let retried = 0;
|
|
304
|
-
const dom = mount({
|
|
305
|
-
selectedCount: 340,
|
|
306
|
-
notice: {
|
|
307
|
-
tone: "danger",
|
|
308
|
-
text: "3,072 moved to Trash. 340 couldn't be deleted.",
|
|
309
|
-
action: {
|
|
310
|
-
label: "Retry 340",
|
|
311
|
-
onClick: () => {
|
|
312
|
-
retried += 1;
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
assert.match(
|
|
318
|
-
dom.text(),
|
|
319
|
-
/3,072 moved to Trash\. 340 couldn't be deleted\./,
|
|
320
|
-
);
|
|
321
|
-
dom.click(dom.byText("button", "Retry 340"));
|
|
322
|
-
assert.equal(retried, 1);
|
|
323
|
-
});
|
|
324
|
-
});
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
-
import { SelectionToolbar } from "./SelectionToolbar";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The desktop bulk-action bar. It keeps its bounded verbs (mark-read, move,
|
|
6
|
-
* delete, organize) and, from issue #212, gains the search-escalation states
|
|
7
|
-
* the mobile `SelectionSheet` already carries: the "Select all N matching…"
|
|
8
|
-
* offer, a running count, the escalated predicate, a chunked run's progress,
|
|
9
|
-
* and a partial-failure Retry. Both surfaces read the same derivations in
|
|
10
|
-
* `MessageList`, so these stories and the kit `SelectionTopBar` stories track
|
|
11
|
-
* the one state matrix.
|
|
12
|
-
*
|
|
13
|
-
* The Move verb needs the account-scoped `MoveToTrigger` (its own query and
|
|
14
|
-
* folder data), so it is left out here to keep these stories provider-free —
|
|
15
|
-
* its visual is covered by the kit `SelectionTopBar` stories' move slot. The
|
|
16
|
-
* escalated stories below still assert verb parity through the mark-read and
|
|
17
|
-
* delete verbs remaining available over the predicate.
|
|
18
|
-
*
|
|
19
|
-
* The apply-label verb (issue #26) needs the same account-scoped query
|
|
20
|
-
* (`LabelApplyTrigger` lists the account's labels), so it is withheld here for
|
|
21
|
-
* the identical reason and omitted from `args` — none of these stories pass
|
|
22
|
-
* `selectedMessageIds`, which keeps the trigger from rendering.
|
|
23
|
-
*/
|
|
24
|
-
const meta: Meta<typeof SelectionToolbar> = {
|
|
25
|
-
title: "Screens/WebClient/SelectionToolbar",
|
|
26
|
-
component: SelectionToolbar,
|
|
27
|
-
parameters: { layout: "padded" },
|
|
28
|
-
args: {
|
|
29
|
-
selectedCount: 3,
|
|
30
|
-
onDelete: () => undefined,
|
|
31
|
-
onClearSelection: () => undefined,
|
|
32
|
-
onMarkAsRead: () => undefined,
|
|
33
|
-
},
|
|
34
|
-
render: (args) => (
|
|
35
|
-
<div className="w-full rounded-md border border-line">
|
|
36
|
-
<SelectionToolbar {...args} />
|
|
37
|
-
</div>
|
|
38
|
-
),
|
|
39
|
-
};
|
|
40
|
-
export default meta;
|
|
41
|
-
|
|
42
|
-
type Story = StoryObj<typeof SelectionToolbar>;
|
|
43
|
-
|
|
44
|
-
/** A plain bounded selection: the count in words, mark-read and delete. */
|
|
45
|
-
export const Bounded: Story = {};
|
|
46
|
-
|
|
47
|
-
export const OneSelected: Story = { args: { selectedCount: 1 } };
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Some but not all loaded rows checked while searching: the select-all control
|
|
51
|
-
* renders the tri-state dash.
|
|
52
|
-
*/
|
|
53
|
-
export const SelectAllIndeterminate: Story = {
|
|
54
|
-
args: {
|
|
55
|
-
selectAll: {
|
|
56
|
-
checked: false,
|
|
57
|
-
indeterminate: true,
|
|
58
|
-
onChange: () => undefined,
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Every loaded row checked. The label names the loaded scope — "All 47 loaded
|
|
65
|
-
* selected" — rather than a bare count next to a fully ticked box.
|
|
66
|
-
*/
|
|
67
|
-
export const AllLoadedSelected: Story = {
|
|
68
|
-
args: {
|
|
69
|
-
selectedCount: 47,
|
|
70
|
-
selectAll: { checked: true, onChange: () => undefined },
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Search has more matches than are loaded: an escalation offer naming the
|
|
76
|
-
* scope (a real button, not prose). Tapping it pays for the real count.
|
|
77
|
-
*/
|
|
78
|
-
export const EscalationAvailable: Story = {
|
|
79
|
-
args: {
|
|
80
|
-
selectedCount: 47,
|
|
81
|
-
selectAll: { checked: true, onChange: () => undefined },
|
|
82
|
-
notice: {
|
|
83
|
-
tone: "info",
|
|
84
|
-
text: "",
|
|
85
|
-
action: {
|
|
86
|
-
label: 'Select all matching "npm"',
|
|
87
|
-
onClick: () => undefined,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The result set is still paging to its total: a running count, Delete hidden
|
|
95
|
-
* (the count it would act on isn't known yet), and an explicit Stop.
|
|
96
|
-
*/
|
|
97
|
-
export const Counting: Story = {
|
|
98
|
-
args: {
|
|
99
|
-
selectedCount: 47,
|
|
100
|
-
isCounting: true,
|
|
101
|
-
statusLabel: "Counting… 1,900 so far",
|
|
102
|
-
selectAll: { checked: true, onChange: () => undefined },
|
|
103
|
-
notice: {
|
|
104
|
-
tone: "info",
|
|
105
|
-
text: "",
|
|
106
|
-
action: { label: "Stop", onClick: () => undefined },
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
/** Past ~10s the counting state says so rather than looking stuck. */
|
|
112
|
-
export const CountingLargeResultSet: Story = {
|
|
113
|
-
args: {
|
|
114
|
-
selectedCount: 0,
|
|
115
|
-
isCounting: true,
|
|
116
|
-
statusLabel: "Counting… 12,400 so far. This is a big result set.",
|
|
117
|
-
notice: {
|
|
118
|
-
tone: "info",
|
|
119
|
-
text: "",
|
|
120
|
-
action: { label: "Stop", onClick: () => undefined },
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* The selection is now the search predicate: the count names the query's total,
|
|
127
|
-
* every verb stays available over it (#114 — not delete-only), and the notice
|
|
128
|
-
* offers a way back to the bounded selection.
|
|
129
|
-
*/
|
|
130
|
-
export const Escalated: Story = {
|
|
131
|
-
args: {
|
|
132
|
-
selectedCount: 3412,
|
|
133
|
-
statusLabel: 'All 3,412 matching "npm" selected',
|
|
134
|
-
notice: {
|
|
135
|
-
tone: "info",
|
|
136
|
-
text: "",
|
|
137
|
-
action: { label: "Clear selection", onClick: () => undefined },
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* A chunked delete over the predicate: a running total and a determinate
|
|
144
|
-
* progress bar, the verbs off screen (the bar is the only thing that can act
|
|
145
|
-
* mid-run), toned as destructive.
|
|
146
|
-
*/
|
|
147
|
-
export const DeletingWithProgress: Story = {
|
|
148
|
-
args: {
|
|
149
|
-
selectedCount: 3412,
|
|
150
|
-
statusLabel: "Deleting 1,200 of 3,412…",
|
|
151
|
-
progress: { value: 1200, max: 3412, tone: "danger" },
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
/** A move over the escalated selection: the same run, toned as ordinary
|
|
156
|
-
* progress and worded for the action that is running. */
|
|
157
|
-
export const MovingWithProgress: Story = {
|
|
158
|
-
args: {
|
|
159
|
-
selectedCount: 3412,
|
|
160
|
-
statusLabel: "Moving 1,200 of 3,412…",
|
|
161
|
-
progress: { value: 1200, max: 3412, tone: "info" },
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/** Mark-read over the escalated selection. */
|
|
166
|
-
export const MarkingReadWithProgress: Story = {
|
|
167
|
-
args: {
|
|
168
|
-
selectedCount: 3412,
|
|
169
|
-
statusLabel: "Marking 1,200 of 3,412 as read…",
|
|
170
|
-
progress: { value: 1200, max: 3412, tone: "info" },
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* A run left some messages unreached: the notice names how many landed, the
|
|
176
|
-
* count reflects only what is still selected (the failures), and Retry targets
|
|
177
|
-
* exactly that.
|
|
178
|
-
*/
|
|
179
|
-
export const PartialFailure: Story = {
|
|
180
|
-
args: {
|
|
181
|
-
selectedCount: 340,
|
|
182
|
-
notice: {
|
|
183
|
-
tone: "danger",
|
|
184
|
-
text: "3,072 moved to Trash. 340 couldn't be deleted.",
|
|
185
|
-
action: { label: "Retry 340", onClick: () => undefined },
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* A selection spanning accounts: Move is withdrawn (it only works within one
|
|
192
|
-
* account) and the reason is stated inline.
|
|
193
|
-
*/
|
|
194
|
-
export const CrossAccountHint: Story = {
|
|
195
|
-
args: {
|
|
196
|
-
moveDisabledHint:
|
|
197
|
-
"Move only works within one account — clear selection or pick messages from a single account",
|
|
198
|
-
},
|
|
199
|
-
};
|