@remit/ui 0.0.16 → 0.0.18
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/quarantine-entry-row.tsx +5 -1
- package/src/components/quarantine-fixtures.ts +48 -21
- package/src/components/quarantine-report.test.ts +63 -14
- package/src/components/quarantine-report.ts +61 -32
- package/src/components/quarantine-section.render.test.ts +7 -1
- package/src/components/quarantine-section.stories.tsx +17 -1
- package/src/components/selection-top-bar.stories.tsx +10 -4
package/package.json
CHANGED
|
@@ -41,7 +41,11 @@ export function QuarantineEntryRow({
|
|
|
41
41
|
{entry.failureMessage}
|
|
42
42
|
</p>
|
|
43
43
|
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-2xs text-fg-subtle">
|
|
44
|
-
|
|
44
|
+
{entry.mailboxRole && (
|
|
45
|
+
<Badge tone="warning">
|
|
46
|
+
{canonicalRoleLabel(entry.mailboxRole)}
|
|
47
|
+
</Badge>
|
|
48
|
+
)}
|
|
45
49
|
<span className="truncate" title={entry.mailboxPath}>
|
|
46
50
|
{providerLeaf(entry.mailboxPath)}
|
|
47
51
|
</span>
|
|
@@ -3,43 +3,42 @@ import type { QuarantineEntry } from "./quarantine-report.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Demo entries backing the quarantine stories. Exported so the kit's own
|
|
5
5
|
* stories and the workbench settings screen render the same data — they had
|
|
6
|
-
* drifted on
|
|
6
|
+
* drifted on the worker build when each file carried its own copy.
|
|
7
7
|
*/
|
|
8
8
|
export const quarantineDemoEntries: readonly QuarantineEntry[] = [
|
|
9
9
|
{
|
|
10
10
|
quarantineId: "q-1",
|
|
11
11
|
accountId: "acct-1",
|
|
12
12
|
mailboxId: "mbx-inbox",
|
|
13
|
+
uidValidity: 1_712_000_000,
|
|
13
14
|
uid: 40217,
|
|
14
15
|
mailboxRole: "inbox",
|
|
15
16
|
mailboxPath: "INBOX",
|
|
16
17
|
failureStage: "BodyParse",
|
|
17
18
|
failureCode: "UnterminatedMultipartBoundary",
|
|
18
19
|
failureMessage: "multipart boundary was never closed",
|
|
19
|
-
failurePartPath:
|
|
20
|
+
failurePartPath: undefined,
|
|
20
21
|
quarantinedAt: Date.parse("2026-07-18T09:12:00Z"),
|
|
21
22
|
attempts: 3,
|
|
22
23
|
sizeBytes: 184_233,
|
|
23
24
|
contentType: "multipart/mixed",
|
|
24
25
|
transferEncoding: "7bit",
|
|
25
26
|
charset: "utf-8",
|
|
26
|
-
structure:
|
|
27
|
-
contentType: "multipart/mixed",
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{ contentType: "application/pdf" },
|
|
34
|
-
],
|
|
35
|
-
},
|
|
27
|
+
structure: [
|
|
28
|
+
{ depth: 0, contentType: "multipart/mixed" },
|
|
29
|
+
{ depth: 1, contentType: "multipart/alternative" },
|
|
30
|
+
{ depth: 2, contentType: "text/plain" },
|
|
31
|
+
{ depth: 2, contentType: "text/html" },
|
|
32
|
+
{ depth: 1, contentType: "application/pdf" },
|
|
33
|
+
],
|
|
36
34
|
messageIdHash: "sha256:6f1c4a9d20",
|
|
37
|
-
|
|
35
|
+
workerVersion: "worker 1.0.0",
|
|
38
36
|
},
|
|
39
37
|
{
|
|
40
38
|
quarantineId: "q-2",
|
|
41
39
|
accountId: "acct-1",
|
|
42
40
|
mailboxId: "mbx-archive",
|
|
41
|
+
uidValidity: 1_712_000_100,
|
|
43
42
|
uid: 40219,
|
|
44
43
|
mailboxRole: "archive",
|
|
45
44
|
mailboxPath: "Archive/2026",
|
|
@@ -53,29 +52,57 @@ export const quarantineDemoEntries: readonly QuarantineEntry[] = [
|
|
|
53
52
|
contentType: "text/plain",
|
|
54
53
|
transferEncoding: "quoted-printable",
|
|
55
54
|
charset: "x-user-defined",
|
|
56
|
-
structure: { contentType: "text/plain" },
|
|
55
|
+
structure: [{ depth: 0, contentType: "text/plain" }],
|
|
57
56
|
messageIdHash: "sha256:b31e0744af",
|
|
58
|
-
|
|
57
|
+
workerVersion: "worker 1.0.0",
|
|
59
58
|
},
|
|
60
59
|
{
|
|
61
60
|
quarantineId: "q-3",
|
|
62
61
|
accountId: "acct-1",
|
|
63
62
|
mailboxId: "mbx-junk",
|
|
63
|
+
uidValidity: 1_712_000_200,
|
|
64
64
|
uid: 40251,
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// A folder the user never appointed a role to — the ordinary state of a
|
|
66
|
+
// plain folder, and the reason mailboxRole is optional.
|
|
67
|
+
mailboxRole: undefined,
|
|
68
|
+
mailboxPath: "Clients/Acme Holdings",
|
|
67
69
|
failureStage: "BodyParse",
|
|
68
70
|
failureCode: "TruncatedBody",
|
|
69
71
|
failureMessage: "stream ended before the declared body length",
|
|
70
|
-
failurePartPath:
|
|
72
|
+
failurePartPath: undefined,
|
|
71
73
|
quarantinedAt: Date.parse("2026-07-19T06:03:00Z"),
|
|
72
74
|
attempts: 1,
|
|
73
75
|
sizeBytes: 2_140,
|
|
74
76
|
contentType: "text/html",
|
|
75
77
|
transferEncoding: "base64",
|
|
76
|
-
charset:
|
|
77
|
-
structure: { contentType: "text/html" },
|
|
78
|
+
charset: undefined,
|
|
79
|
+
structure: [{ depth: 0, contentType: "text/html" }],
|
|
78
80
|
messageIdHash: "sha256:0a77de1c05",
|
|
79
|
-
|
|
81
|
+
workerVersion: "worker 1.0.0",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
// The message shape is absent as a set: a message that failed before its
|
|
85
|
+
// BODYSTRUCTURE was read carries no content type, encoding, size or
|
|
86
|
+
// tree, and one that declared no Message-ID carries no hash either.
|
|
87
|
+
quarantineId: "q-4",
|
|
88
|
+
accountId: "acct-1",
|
|
89
|
+
mailboxId: "mbx-inbox",
|
|
90
|
+
uidValidity: 1_712_000_000,
|
|
91
|
+
uid: 40263,
|
|
92
|
+
mailboxRole: "inbox",
|
|
93
|
+
mailboxPath: "INBOX",
|
|
94
|
+
failureStage: "BodyParse",
|
|
95
|
+
failureCode: "TruncatedBody",
|
|
96
|
+
failureMessage: "connection closed mid-body",
|
|
97
|
+
failurePartPath: undefined,
|
|
98
|
+
quarantinedAt: Date.parse("2026-07-19T11:27:00Z"),
|
|
99
|
+
attempts: 2,
|
|
100
|
+
sizeBytes: undefined,
|
|
101
|
+
contentType: undefined,
|
|
102
|
+
transferEncoding: undefined,
|
|
103
|
+
charset: undefined,
|
|
104
|
+
structure: [],
|
|
105
|
+
messageIdHash: undefined,
|
|
106
|
+
workerVersion: "worker 1.0.0",
|
|
80
107
|
},
|
|
81
108
|
];
|
|
@@ -48,15 +48,13 @@ describe("formatQuarantineReport", () => {
|
|
|
48
48
|
const report = formatQuarantineReport({
|
|
49
49
|
...entry,
|
|
50
50
|
contentType: 'multipart/mixed; boundary="=_a1b2"',
|
|
51
|
-
structure:
|
|
52
|
-
contentType: "multipart/mixed",
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
],
|
|
59
|
-
},
|
|
51
|
+
structure: [
|
|
52
|
+
{ depth: 0, contentType: "multipart/mixed" },
|
|
53
|
+
{
|
|
54
|
+
depth: 1,
|
|
55
|
+
contentType: 'application/octet-stream; name="Q3 payroll — Acme.pdf"',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
60
58
|
});
|
|
61
59
|
assert.doesNotMatch(report, /payroll/);
|
|
62
60
|
assert.doesNotMatch(report, /boundary/);
|
|
@@ -71,10 +69,39 @@ describe("formatQuarantineReport", () => {
|
|
|
71
69
|
);
|
|
72
70
|
});
|
|
73
71
|
|
|
72
|
+
it("says no role was appointed rather than inventing one", () => {
|
|
73
|
+
const report = formatQuarantineReport({
|
|
74
|
+
...entry,
|
|
75
|
+
mailboxRole: undefined,
|
|
76
|
+
});
|
|
77
|
+
assert.match(report, /Folder role.*none appointed/);
|
|
78
|
+
});
|
|
79
|
+
|
|
74
80
|
it("marks an undeclared charset rather than omitting it", () => {
|
|
75
|
-
const report = formatQuarantineReport({ ...entry, charset:
|
|
81
|
+
const report = formatQuarantineReport({ ...entry, charset: undefined });
|
|
76
82
|
assert.match(report, /Charset.*not declared/);
|
|
77
83
|
});
|
|
84
|
+
|
|
85
|
+
// Absent is the wire shape: the API omits an optional diagnostic and the
|
|
86
|
+
// repo maps a null column to undefined. A report that printed `undefined`
|
|
87
|
+
// into a public issue would do it on the common case — a whole-body parse
|
|
88
|
+
// failure has no failing part, which is every failure today.
|
|
89
|
+
it("never prints undefined for a diagnostic the message did not carry", () => {
|
|
90
|
+
const report = formatQuarantineReport({
|
|
91
|
+
...entry,
|
|
92
|
+
failurePartPath: undefined,
|
|
93
|
+
charset: undefined,
|
|
94
|
+
contentType: undefined,
|
|
95
|
+
transferEncoding: undefined,
|
|
96
|
+
sizeBytes: undefined,
|
|
97
|
+
messageIdHash: undefined,
|
|
98
|
+
structure: [],
|
|
99
|
+
});
|
|
100
|
+
assert.doesNotMatch(report, /undefined|null/);
|
|
101
|
+
assert.match(report, /Failing part.*whole message/);
|
|
102
|
+
assert.match(report, /Message-ID hash.*none declared/);
|
|
103
|
+
assert.match(report, /no MIME structure was read/);
|
|
104
|
+
});
|
|
78
105
|
});
|
|
79
106
|
|
|
80
107
|
describe("sender-controlled BODYSTRUCTURE strings", () => {
|
|
@@ -109,16 +136,38 @@ describe("sender-controlled BODYSTRUCTURE strings", () => {
|
|
|
109
136
|
it("keeps a hostile node type from closing the MIME fence", () => {
|
|
110
137
|
const report = formatQuarantineReport({
|
|
111
138
|
...entry,
|
|
112
|
-
structure:
|
|
113
|
-
contentType: "multipart/mixed",
|
|
114
|
-
|
|
115
|
-
|
|
139
|
+
structure: [
|
|
140
|
+
{ depth: 0, contentType: "multipart/mixed" },
|
|
141
|
+
{ depth: 1, contentType: "```\n## Injected heading" },
|
|
142
|
+
],
|
|
116
143
|
});
|
|
117
144
|
assert.equal((report.match(/^```/gm) ?? []).length, 2);
|
|
118
145
|
assert.doesNotMatch(report, /^## Injected heading/m);
|
|
119
146
|
});
|
|
120
147
|
});
|
|
121
148
|
|
|
149
|
+
describe("a message that failed before its shape was read", () => {
|
|
150
|
+
const shapeless: QuarantineEntry = {
|
|
151
|
+
...entry,
|
|
152
|
+
contentType: undefined,
|
|
153
|
+
transferEncoding: undefined,
|
|
154
|
+
sizeBytes: undefined,
|
|
155
|
+
structure: [],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
it("says the shape was never read rather than guessing one", () => {
|
|
159
|
+
const report = formatQuarantineReport(shapeless);
|
|
160
|
+
assert.match(report, /Content-Type.*not read/);
|
|
161
|
+
assert.match(report, /Content-Transfer-Encoding.*not read/);
|
|
162
|
+
assert.match(report, /Size.*not read/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("keeps the fence balanced with an empty tree", () => {
|
|
166
|
+
const report = formatQuarantineReport(shapeless);
|
|
167
|
+
assert.equal((report.match(/^```/gm) ?? []).length, 2);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
122
171
|
describe("quarantineReportSections", () => {
|
|
123
172
|
it("hands the MIME tree over unfenced, so it can be truncated safely", () => {
|
|
124
173
|
const { head, structure, disclaimer } = quarantineReportSections(entry);
|
|
@@ -45,7 +45,12 @@ export type QuarantineFailureCode =
|
|
|
45
45
|
| "TruncatedBody";
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* One node of the message's MIME tree, in a pre-order walk.
|
|
49
|
+
*
|
|
50
|
+
* The tree arrives flat with an explicit `depth` rather than as nested
|
|
51
|
+
* children: a self-referential model is not expressible in the schema this
|
|
52
|
+
* comes from, and the tree is only ever rendered as an indented list, which a
|
|
53
|
+
* pre-order walk reproduces exactly.
|
|
49
54
|
*
|
|
50
55
|
* `contentType` is `type/subtype` only. BODYSTRUCTURE hands the type and its
|
|
51
56
|
* parameters over separately, so a node is built from `type`/`subtype` and
|
|
@@ -53,8 +58,8 @@ export type QuarantineFailureCode =
|
|
|
53
58
|
* `filename=`, which name the user's attachments.
|
|
54
59
|
*/
|
|
55
60
|
export interface QuarantineMimeNode {
|
|
61
|
+
depth: number;
|
|
56
62
|
contentType: string;
|
|
57
|
-
parts?: readonly QuarantineMimeNode[];
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
@@ -70,10 +75,19 @@ export interface QuarantineEntry {
|
|
|
70
75
|
quarantineId: string;
|
|
71
76
|
accountId: string;
|
|
72
77
|
mailboxId: string;
|
|
78
|
+
/**
|
|
79
|
+
* UIDVALIDITY of the mailbox when the message was set aside. A uid names a
|
|
80
|
+
* message only alongside it.
|
|
81
|
+
*/
|
|
82
|
+
uidValidity: number;
|
|
73
83
|
/** IMAP uid of the message that was not written. */
|
|
74
84
|
uid: number;
|
|
75
|
-
/**
|
|
76
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Canonical role of the folder it arrived in — travels in the report.
|
|
87
|
+
* Absent when the user appointed no role to that folder, which is the
|
|
88
|
+
* ordinary state of a plain folder.
|
|
89
|
+
*/
|
|
90
|
+
mailboxRole?: FolderRole;
|
|
77
91
|
/** The user's own folder name. Shown on screen, withheld from the report. */
|
|
78
92
|
mailboxPath: string;
|
|
79
93
|
failureStage: QuarantineFailureStage;
|
|
@@ -81,26 +95,36 @@ export interface QuarantineEntry {
|
|
|
81
95
|
/** Parser error text. Shown on screen, never in the report. */
|
|
82
96
|
failureMessage: string;
|
|
83
97
|
/**
|
|
84
|
-
* Dot-numbered part path the failure is attributable to
|
|
85
|
-
* not attributable to one node — the case for a whole-body parse failure
|
|
98
|
+
* Dot-numbered part path the failure is attributable to. Absent when it is
|
|
99
|
+
* not attributable to one node — the case for a whole-body parse failure,
|
|
100
|
+
* which is every failure today.
|
|
86
101
|
*/
|
|
87
|
-
failurePartPath
|
|
102
|
+
failurePartPath?: string;
|
|
88
103
|
/** Epoch millis the message was quarantined. */
|
|
89
104
|
quarantinedAt: number;
|
|
90
105
|
/** Rounds attempted before the message was set aside. */
|
|
91
106
|
attempts: number;
|
|
92
|
-
|
|
107
|
+
/**
|
|
108
|
+
* The message-shape fields all come off BODYSTRUCTURE and RFC822.SIZE in
|
|
109
|
+
* the same FETCH, so a message that failed before either was read carries
|
|
110
|
+
* none of them — they are absent together, and `structure` is empty.
|
|
111
|
+
*/
|
|
112
|
+
sizeBytes?: number;
|
|
93
113
|
/** Top-level Content-Type, `type/subtype` only. */
|
|
94
|
-
contentType
|
|
95
|
-
transferEncoding
|
|
96
|
-
/** Declared charset
|
|
97
|
-
charset
|
|
98
|
-
/** The MIME tree, structure only. */
|
|
99
|
-
structure: QuarantineMimeNode;
|
|
100
|
-
/**
|
|
101
|
-
|
|
114
|
+
contentType?: string;
|
|
115
|
+
transferEncoding?: string;
|
|
116
|
+
/** Declared charset. Absent when the message declared none. */
|
|
117
|
+
charset?: string;
|
|
118
|
+
/** The MIME tree, structure only, as a pre-order walk. */
|
|
119
|
+
structure: readonly QuarantineMimeNode[];
|
|
120
|
+
/**
|
|
121
|
+
* SHA-256 of the Message-ID, `sha256:` prefixed. Absent when the message
|
|
122
|
+
* declared no Message-ID — a shared hash of nothing would correlate
|
|
123
|
+
* unrelated reports.
|
|
124
|
+
*/
|
|
125
|
+
messageIdHash?: string;
|
|
102
126
|
/** Build of the worker that failed to parse — a parse bug belongs to it. */
|
|
103
|
-
|
|
127
|
+
workerVersion: string;
|
|
104
128
|
}
|
|
105
129
|
|
|
106
130
|
const stageSummaries: Record<QuarantineFailureStage, string> = {
|
|
@@ -146,13 +170,18 @@ function renderNodeType(contentType: string): string {
|
|
|
146
170
|
return JSON.stringify(stripped);
|
|
147
171
|
}
|
|
148
172
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
/**
|
|
174
|
+
* What a message-shape field reads as when the message failed before its
|
|
175
|
+
* BODYSTRUCTURE was read. Distinct from "not declared", which is a statement
|
|
176
|
+
* about the message rather than about how far sync got.
|
|
177
|
+
*/
|
|
178
|
+
const NOT_READ = "_not read_";
|
|
179
|
+
|
|
180
|
+
function renderStructure(nodes: readonly QuarantineMimeNode[]): string[] {
|
|
181
|
+
if (nodes.length === 0) return ["(no MIME structure was read)"];
|
|
182
|
+
return nodes.map(
|
|
183
|
+
(node) => `${" ".repeat(node.depth)}- ${renderNodeType(node.contentType)}`,
|
|
184
|
+
);
|
|
156
185
|
}
|
|
157
186
|
|
|
158
187
|
export const QUARANTINE_REPORT_DISCLAIMER =
|
|
@@ -182,18 +211,18 @@ export function quarantineReportSections(
|
|
|
182
211
|
`### Message quarantined at \`${entry.failureStage}\``,
|
|
183
212
|
"",
|
|
184
213
|
`- **Failure**: \`${entry.failureCode}\``,
|
|
185
|
-
`- **Folder role**: ${entry.mailboxRole}`,
|
|
186
|
-
`- **Failing part**: ${entry.failurePartPath
|
|
214
|
+
`- **Folder role**: ${entry.mailboxRole ?? "_none appointed_"}`,
|
|
215
|
+
`- **Failing part**: ${entry.failurePartPath == null ? "_whole message_" : `\`${entry.failurePartPath}\``}`,
|
|
187
216
|
`- **Attempts before quarantine**: ${entry.attempts}`,
|
|
188
|
-
`- **Worker build**: ${entry.
|
|
189
|
-
`- **Message-ID hash**:
|
|
217
|
+
`- **Worker build**: ${entry.workerVersion}`,
|
|
218
|
+
`- **Message-ID hash**: ${entry.messageIdHash == null ? "_none declared_" : `\`${entry.messageIdHash}\``}`,
|
|
190
219
|
"",
|
|
191
220
|
"#### Message shape",
|
|
192
221
|
"",
|
|
193
|
-
`- **Content-Type**: ${renderToken(stripParameters(entry.contentType), MEDIA_TYPE)}`,
|
|
194
|
-
`- **Content-Transfer-Encoding**: ${renderToken(entry.transferEncoding, TOKEN)}`,
|
|
195
|
-
`- **Charset**: ${entry.charset
|
|
196
|
-
`- **Size**: ${entry.sizeBytes} bytes`,
|
|
222
|
+
`- **Content-Type**: ${entry.contentType == null ? NOT_READ : renderToken(stripParameters(entry.contentType), MEDIA_TYPE)}`,
|
|
223
|
+
`- **Content-Transfer-Encoding**: ${entry.transferEncoding == null ? NOT_READ : renderToken(entry.transferEncoding, TOKEN)}`,
|
|
224
|
+
`- **Charset**: ${entry.charset == null ? "_not declared_" : renderToken(entry.charset, TOKEN)}`,
|
|
225
|
+
`- **Size**: ${entry.sizeBytes == null ? NOT_READ : `${entry.sizeBytes} bytes`}`,
|
|
197
226
|
"",
|
|
198
227
|
"MIME structure:",
|
|
199
228
|
].join("\n");
|
|
@@ -8,7 +8,7 @@ import type { QuarantineEntry } from "./quarantine-report.js";
|
|
|
8
8
|
import { QuarantineSection } from "./quarantine-section.js";
|
|
9
9
|
|
|
10
10
|
const noop = () => {};
|
|
11
|
-
const [base, second] = quarantineDemoEntries;
|
|
11
|
+
const [base, second, unappointedFolder] = quarantineDemoEntries;
|
|
12
12
|
const ISSUE_URL = "https://github.com/remit-mail/reader/issues/new?title=x";
|
|
13
13
|
|
|
14
14
|
const render = (entries: readonly QuarantineEntry[]) =>
|
|
@@ -38,6 +38,12 @@ describe("QuarantineSection", () => {
|
|
|
38
38
|
assert.match(html, /multipart boundary was never closed/);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
it("names no role for a folder the user never appointed one to", () => {
|
|
42
|
+
const html = render([unappointedFolder]);
|
|
43
|
+
assert.doesNotMatch(html, /Inbox|Spam|Archive/);
|
|
44
|
+
assert.match(html, /Acme Holdings/);
|
|
45
|
+
});
|
|
46
|
+
|
|
41
47
|
it("offers reporting as the only per-row action", () => {
|
|
42
48
|
const html = render([base]);
|
|
43
49
|
assert.match(html, /Cut a bug/);
|
|
@@ -5,7 +5,7 @@ import { quarantineDemoEntries } from "./quarantine-fixtures.js";
|
|
|
5
5
|
import type { QuarantineEntry } from "./quarantine-report.js";
|
|
6
6
|
import { QuarantineSection } from "./quarantine-section.js";
|
|
7
7
|
|
|
8
|
-
const [unterminatedBoundary, unknownCharset, truncatedBody] =
|
|
8
|
+
const [unterminatedBoundary, unknownCharset, truncatedBody, shapeUnread] =
|
|
9
9
|
quarantineDemoEntries;
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -62,6 +62,22 @@ export const CutABugFlow: Story = {
|
|
|
62
62
|
},
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A message that failed before its BODYSTRUCTURE was read carries no content
|
|
67
|
+
* type, encoding, size or MIME tree, and one that declared no Message-ID
|
|
68
|
+
* carries no hash. The report says so rather than printing an empty value.
|
|
69
|
+
*/
|
|
70
|
+
export const BugReportWithoutMessageShape: Story = {
|
|
71
|
+
render: () => (
|
|
72
|
+
<QuarantineBugDialog
|
|
73
|
+
entry={shapeUnread}
|
|
74
|
+
issueUrl={demoIssueUrl}
|
|
75
|
+
onClose={() => {}}
|
|
76
|
+
onCopy={() => {}}
|
|
77
|
+
/>
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
|
|
65
81
|
export const BugReport: Story = {
|
|
66
82
|
render: () => (
|
|
67
83
|
<QuarantineBugDialog
|
|
@@ -74,9 +74,15 @@ export const AllSelected: Story = {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* The search has more matches than are loaded: an escalation notice offers a
|
|
77
|
-
* real button (not prose) naming the
|
|
78
|
-
* selection's identity from an id set to the search query (
|
|
79
|
-
*
|
|
77
|
+
* real button (not prose) naming the scope. Tapping it is what flips the
|
|
78
|
+
* selection's identity from an id set to the search query (`useEscalatedDelete`
|
|
79
|
+
* in web-client). No count in the label yet — the real client's own read path
|
|
80
|
+
* (`ThreadOperations.searchThreads`) only counts within a capped recency
|
|
81
|
+
* window short of paging the whole result set, and paging it just to seed a
|
|
82
|
+
* button label the user hasn't asked for yet would burn a request on every
|
|
83
|
+
* render of "all loaded selected" for a number that goes stale the moment new
|
|
84
|
+
* mail arrives. Tapping the button is what pays for the real count, via the
|
|
85
|
+
* counting state below.
|
|
80
86
|
*/
|
|
81
87
|
export const EscalationAvailable: Story = {
|
|
82
88
|
args: {
|
|
@@ -90,7 +96,7 @@ export const EscalationAvailable: Story = {
|
|
|
90
96
|
tone: "info",
|
|
91
97
|
text: "",
|
|
92
98
|
action: {
|
|
93
|
-
label: 'Select all
|
|
99
|
+
label: 'Select all matching "npm"',
|
|
94
100
|
onClick: () => undefined,
|
|
95
101
|
},
|
|
96
102
|
},
|