@remit/ui 0.0.40 → 0.0.42
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/filter-clause-chip.tsx +211 -0
- package/src/components/filter-preview-count.tsx +57 -0
- package/src/components/filter-rule-editor.stories.tsx +336 -0
- package/src/components/filter-rule-editor.tsx +303 -0
- package/src/components/filter-rule.render.test.ts +547 -0
- package/src/components/filter-rule.ts +230 -0
- package/src/components/self-update-confirm-dialog.tsx +17 -0
- package/src/components/self-update-section.tsx +102 -1
- package/src/components/self-update.render.test.ts +54 -0
- package/src/components/self-update.stories.tsx +51 -0
- package/src/components/self-update.ts +21 -0
- package/src/index.ts +47 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filter rule as the user edits it (RFC 038).
|
|
3
|
+
*
|
|
4
|
+
* A rule is literal clauses combined under one match operator, an optional
|
|
5
|
+
* semantic widen, an action, and a scope. This module is the vocabulary the
|
|
6
|
+
* chip editor renders — a design model driven from fixtures, not the API.
|
|
7
|
+
* Naming tracks the RFC: rule, clause, widen, scope.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The clause fields (RFC 038 D2). `From`, `Subject`, `HasWords` ship now;
|
|
12
|
+
* `ListId` and `FromDomain` arrive with the vocabulary ticket. Every variant
|
|
13
|
+
* renders here so that ticket slots its fields in without touching the chip.
|
|
14
|
+
*/
|
|
15
|
+
export type ClauseField =
|
|
16
|
+
| "From"
|
|
17
|
+
| "Subject"
|
|
18
|
+
| "HasWords"
|
|
19
|
+
| "ListId"
|
|
20
|
+
| "FromDomain";
|
|
21
|
+
|
|
22
|
+
export interface RuleClause {
|
|
23
|
+
id: string;
|
|
24
|
+
field: ClauseField;
|
|
25
|
+
value: string;
|
|
26
|
+
/**
|
|
27
|
+
* A `From` clause the server derived from the selection because the widen
|
|
28
|
+
* could not run (#251). It is an ordinary visible, editable chip — this flag
|
|
29
|
+
* only annotates where it came from.
|
|
30
|
+
*/
|
|
31
|
+
derived?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** How the clauses combine. Maps to the API's `And` / `Or`. */
|
|
35
|
+
export type MatchOperator = "all" | "any";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* When the rule stops (RFC 038 D1). `once` is a one-time action, `standing`
|
|
39
|
+
* persists, `until` expires on a picked date.
|
|
40
|
+
*/
|
|
41
|
+
export type RuleScope = "once" | "standing" | "until";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The semantic widen (RFC 038 D3): one chip backed by the anchor mechanism.
|
|
45
|
+
* Absent from a rule means the chip is not present; a deployment that cannot
|
|
46
|
+
* serve the widen never offers it at all.
|
|
47
|
+
*/
|
|
48
|
+
export interface RuleWiden {
|
|
49
|
+
/** Anchors the similarity rides on — "similar to these N". */
|
|
50
|
+
anchorCount: number;
|
|
51
|
+
/**
|
|
52
|
+
* The rule carries an anchor this deployment cannot evaluate — created
|
|
53
|
+
* elsewhere, capability since lost (RFC 038 D4). The chip lists as inactive,
|
|
54
|
+
* the rule matches by its literal clauses only, and nothing claims otherwise.
|
|
55
|
+
*/
|
|
56
|
+
inactive?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FilterRule {
|
|
60
|
+
clauses: RuleClause[];
|
|
61
|
+
matchOperator: MatchOperator;
|
|
62
|
+
widen?: RuleWiden;
|
|
63
|
+
/** The move-to-folder action's destination. Absent leaves mail in place. */
|
|
64
|
+
moveMailboxId?: string;
|
|
65
|
+
scope: RuleScope;
|
|
66
|
+
/** ISO 8601 civil date (`YYYY-MM-DD`) for the `until` scope. */
|
|
67
|
+
until?: string;
|
|
68
|
+
/** Names a standing or timed rule so it can be found in Settings › Filters. */
|
|
69
|
+
name?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface FolderOption {
|
|
73
|
+
id: string;
|
|
74
|
+
label: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The live match count (RFC 038 D1). `stale` marks a count the editor already
|
|
79
|
+
* moved past — a clause changed after it was counted, so the number on screen
|
|
80
|
+
* is the previous rule's until the next preview lands.
|
|
81
|
+
*/
|
|
82
|
+
export type PreviewCount =
|
|
83
|
+
| { status: "loading" }
|
|
84
|
+
| { status: "ready"; count: number; stale?: boolean }
|
|
85
|
+
| { status: "error"; reason: string };
|
|
86
|
+
|
|
87
|
+
const clauseFieldLabels: Record<ClauseField, string> = {
|
|
88
|
+
From: "From",
|
|
89
|
+
Subject: "Subject",
|
|
90
|
+
HasWords: "Has the words",
|
|
91
|
+
ListId: "List",
|
|
92
|
+
FromDomain: "Domain",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export function clauseFieldLabel(field: ClauseField): string {
|
|
96
|
+
return clauseFieldLabels[field];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The fields a new clause can be added as, in menu order. */
|
|
100
|
+
export const clauseFieldOrder: ClauseField[] = [
|
|
101
|
+
"From",
|
|
102
|
+
"Subject",
|
|
103
|
+
"HasWords",
|
|
104
|
+
"ListId",
|
|
105
|
+
"FromDomain",
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export function widenChipLabel(widen: RuleWiden): string {
|
|
109
|
+
return `Similar to these ${widen.anchorCount}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const matchOperatorLabels: Record<MatchOperator, string> = {
|
|
113
|
+
all: "Match all",
|
|
114
|
+
any: "Match any",
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export function matchOperatorLabel(operator: MatchOperator): string {
|
|
118
|
+
return matchOperatorLabels[operator];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The join word between chips — "all" reads as "and", "any" as "or". Rendered
|
|
123
|
+
* between clauses so the operator is legible in the rule itself, not only the
|
|
124
|
+
* toggle.
|
|
125
|
+
*/
|
|
126
|
+
export function matchJoinWord(operator: MatchOperator): string {
|
|
127
|
+
return operator === "all" ? "and" : "or";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function previewCountSummary(preview: PreviewCount): string {
|
|
131
|
+
if (preview.status === "loading") return "Counting matches…";
|
|
132
|
+
if (preview.status === "error") return preview.reason;
|
|
133
|
+
if (preview.count === 0) return "No mail matches yet";
|
|
134
|
+
const noun = preview.count === 1 ? "message" : "messages";
|
|
135
|
+
const base = `${preview.count} ${noun} match`;
|
|
136
|
+
return preview.stale ? `${base} — recounting` : base;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const scopeLabels: Record<RuleScope, string> = {
|
|
140
|
+
once: "Just once",
|
|
141
|
+
standing: "Keep doing this",
|
|
142
|
+
until: "Until a date",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export function scopeLabel(scope: RuleScope): string {
|
|
146
|
+
return scopeLabels[scope];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function commitLabel(scope: RuleScope): string {
|
|
150
|
+
if (scope === "once") return "Apply now";
|
|
151
|
+
if (scope === "standing") return "Save rule";
|
|
152
|
+
return "Save until then";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Why the rule cannot be saved yet, or `undefined` when it is ready. A rule
|
|
157
|
+
* needs at least one live way to match, a folder to move into (the only wired
|
|
158
|
+
* action), and — for the two persisted scopes — a name and, for `until`, a
|
|
159
|
+
* date. It also needs a settled preview: the rule is committable only when the
|
|
160
|
+
* count on screen is the count that will be applied. That makes RFC 038's
|
|
161
|
+
* previewed-set-equals-applied-set contract structural — a consumer cannot save
|
|
162
|
+
* a rule whose match count is still moving. Never disable a control without
|
|
163
|
+
* saying why (ux.md).
|
|
164
|
+
*/
|
|
165
|
+
export function commitBlockedReason(
|
|
166
|
+
rule: FilterRule,
|
|
167
|
+
preview: PreviewCount,
|
|
168
|
+
): string | undefined {
|
|
169
|
+
const hasMatch =
|
|
170
|
+
rule.clauses.length > 0 ||
|
|
171
|
+
(rule.widen !== undefined && !rule.widen.inactive);
|
|
172
|
+
if (!hasMatch) return "Add a clause so the rule has something to match.";
|
|
173
|
+
if (!rule.moveMailboxId)
|
|
174
|
+
return "Pick a folder to move matches into — labeling isn't available yet.";
|
|
175
|
+
if (
|
|
176
|
+
(rule.scope === "standing" || rule.scope === "until") &&
|
|
177
|
+
(rule.name ?? "").trim() === ""
|
|
178
|
+
)
|
|
179
|
+
return "Name this rule so you can find it later.";
|
|
180
|
+
if (rule.scope === "until" && !rule.until)
|
|
181
|
+
return "Pick the date this rule should stop on.";
|
|
182
|
+
if (preview.status === "loading")
|
|
183
|
+
return "Counting matches — save once the count settles.";
|
|
184
|
+
if (preview.status === "ready" && preview.stale)
|
|
185
|
+
return "Recounting matches — save once the count settles.";
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const demoFolders: FolderOption[] = [
|
|
190
|
+
{ id: "mbx-inbox", label: "Inbox" },
|
|
191
|
+
{ id: "mbx-archive", label: "Archive" },
|
|
192
|
+
{ id: "mbx-receipts", label: "Receipts" },
|
|
193
|
+
{ id: "mbx-travel", label: "Travel" },
|
|
194
|
+
{ id: "mbx-junk", label: "Junk" },
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
export const demoRule: FilterRule = {
|
|
198
|
+
clauses: [
|
|
199
|
+
{ id: "c1", field: "From", value: "notifications@github.com" },
|
|
200
|
+
{ id: "c2", field: "Subject", value: "pull request" },
|
|
201
|
+
],
|
|
202
|
+
matchOperator: "all",
|
|
203
|
+
widen: { anchorCount: 2 },
|
|
204
|
+
moveMailboxId: "mbx-archive",
|
|
205
|
+
scope: "standing",
|
|
206
|
+
name: "GitHub notifications",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const demoVocabularyRule: FilterRule = {
|
|
210
|
+
clauses: [
|
|
211
|
+
{ id: "c1", field: "ListId", value: "python-dev.python.org" },
|
|
212
|
+
{ id: "c2", field: "FromDomain", value: "python.org" },
|
|
213
|
+
{ id: "c3", field: "HasWords", value: "nightly build" },
|
|
214
|
+
],
|
|
215
|
+
matchOperator: "any",
|
|
216
|
+
moveMailboxId: "mbx-archive",
|
|
217
|
+
scope: "standing",
|
|
218
|
+
name: "Python lists",
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const demoSenderFallbackRule: FilterRule = {
|
|
222
|
+
clauses: [
|
|
223
|
+
{ id: "c1", field: "From", value: "receipts@stripe.com", derived: true },
|
|
224
|
+
{ id: "c2", field: "From", value: "receipts@lyft.com", derived: true },
|
|
225
|
+
],
|
|
226
|
+
matchOperator: "any",
|
|
227
|
+
moveMailboxId: "mbx-receipts",
|
|
228
|
+
scope: "standing",
|
|
229
|
+
name: "Receipts",
|
|
230
|
+
};
|
|
@@ -7,6 +7,12 @@ export interface SelfUpdateConfirmDialogProps {
|
|
|
7
7
|
open: boolean;
|
|
8
8
|
currentVersion: string;
|
|
9
9
|
release: ReleaseInfo;
|
|
10
|
+
/**
|
|
11
|
+
* Whether installing this release runs a database migration during the
|
|
12
|
+
* offline window. Stated only when the surface reports both schema versions
|
|
13
|
+
* and the new one is higher; silence otherwise.
|
|
14
|
+
*/
|
|
15
|
+
appliesSchemaMigration?: boolean;
|
|
10
16
|
onClose: () => void;
|
|
11
17
|
onConfirm: () => void;
|
|
12
18
|
}
|
|
@@ -26,6 +32,7 @@ export function SelfUpdateConfirmDialog({
|
|
|
26
32
|
open,
|
|
27
33
|
currentVersion,
|
|
28
34
|
release,
|
|
35
|
+
appliesSchemaMigration = false,
|
|
29
36
|
onClose,
|
|
30
37
|
onConfirm,
|
|
31
38
|
}: SelfUpdateConfirmDialogProps) {
|
|
@@ -53,6 +60,16 @@ export function SelfUpdateConfirmDialog({
|
|
|
53
60
|
<span>{line}</span>
|
|
54
61
|
</li>
|
|
55
62
|
))}
|
|
63
|
+
{appliesSchemaMigration && (
|
|
64
|
+
<li className="flex gap-2 text-sm text-fg-muted">
|
|
65
|
+
<span className="mt-1.5 size-1.5 shrink-0 rounded-full bg-fg-subtle" />
|
|
66
|
+
<span>
|
|
67
|
+
This version updates the database while Remit is offline. The
|
|
68
|
+
step forward cannot be undone by hand, but a failed start is
|
|
69
|
+
still rolled back to the version and data you have now.
|
|
70
|
+
</span>
|
|
71
|
+
</li>
|
|
72
|
+
)}
|
|
56
73
|
</ul>
|
|
57
74
|
<p className="text-xs text-fg-subtle">
|
|
58
75
|
Good moment for this: when you are not waiting on a message.
|
|
@@ -82,7 +82,11 @@ export function SelfUpdateSection({
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
const handleInstall = () => {
|
|
85
|
-
if (
|
|
85
|
+
if (
|
|
86
|
+
!installable &&
|
|
87
|
+
state.status !== "rolledBack" &&
|
|
88
|
+
state.status !== "abandoned"
|
|
89
|
+
) {
|
|
86
90
|
setNotice(
|
|
87
91
|
"There is no update to install. Check for updates first — if one is found it appears here.",
|
|
88
92
|
);
|
|
@@ -348,6 +352,103 @@ export function SelfUpdateSection({
|
|
|
348
352
|
</SectionRow>
|
|
349
353
|
);
|
|
350
354
|
|
|
355
|
+
case "rollbackFailed":
|
|
356
|
+
return (
|
|
357
|
+
<SectionRow tone="danger">
|
|
358
|
+
<div className="space-y-3">
|
|
359
|
+
<div className="flex items-start gap-2">
|
|
360
|
+
<TriangleAlert
|
|
361
|
+
className="mt-0.5 size-4 shrink-0 text-danger"
|
|
362
|
+
aria-hidden
|
|
363
|
+
/>
|
|
364
|
+
<div className="min-w-0 space-y-1">
|
|
365
|
+
<p className="text-sm font-semibold text-fg">
|
|
366
|
+
Remit {state.attemptedVersion} did not start, and Remit
|
|
367
|
+
could not put {state.previousVersion} back.
|
|
368
|
+
</p>
|
|
369
|
+
<p className="text-sm text-fg-muted">
|
|
370
|
+
This is the one outcome Remit cannot resolve on its own. The
|
|
371
|
+
server is in a half-changed state and needs you at a shell.
|
|
372
|
+
The log below is the only account of where it stopped.
|
|
373
|
+
</p>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
<div className="space-y-1">
|
|
377
|
+
<p className="text-xs text-fg-subtle">
|
|
378
|
+
What Remit reported as the failure
|
|
379
|
+
</p>
|
|
380
|
+
<code className="block rounded-xs bg-danger-soft px-2 py-1 text-2xs text-danger">
|
|
381
|
+
{state.reason}
|
|
382
|
+
</code>
|
|
383
|
+
</div>
|
|
384
|
+
<div className="space-y-1">
|
|
385
|
+
<p className="text-xs text-fg-subtle">
|
|
386
|
+
Read the full log on the server:
|
|
387
|
+
</p>
|
|
388
|
+
<code className="block rounded-xs bg-surface-sunken px-2 py-1 text-2xs text-fg-muted">
|
|
389
|
+
{state.logsCommand}
|
|
390
|
+
</code>
|
|
391
|
+
</div>
|
|
392
|
+
<div className="flex justify-end">
|
|
393
|
+
<Button variant="ghost" size="sm" onClick={onDismissResult}>
|
|
394
|
+
Dismiss
|
|
395
|
+
</Button>
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
</SectionRow>
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
case "abandoned":
|
|
402
|
+
return (
|
|
403
|
+
<SectionRow tone="danger">
|
|
404
|
+
<div className="space-y-3">
|
|
405
|
+
<div className="flex items-start gap-2">
|
|
406
|
+
<TriangleAlert
|
|
407
|
+
className="mt-0.5 size-4 shrink-0 text-fg-subtle"
|
|
408
|
+
aria-hidden
|
|
409
|
+
/>
|
|
410
|
+
<div className="min-w-0 space-y-1">
|
|
411
|
+
<p className="text-sm font-semibold text-fg">
|
|
412
|
+
Remit {state.attemptedVersion} was not installed. Nothing
|
|
413
|
+
changed.
|
|
414
|
+
</p>
|
|
415
|
+
<p className="text-sm text-fg-muted">
|
|
416
|
+
The update stopped before it altered anything. You are still
|
|
417
|
+
running {state.version}.
|
|
418
|
+
</p>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
<div className="space-y-1">
|
|
422
|
+
<p className="text-xs text-fg-subtle">What Remit reported</p>
|
|
423
|
+
<code className="block rounded-xs bg-surface-sunken px-2 py-1 text-2xs text-fg-muted">
|
|
424
|
+
{state.reason}
|
|
425
|
+
</code>
|
|
426
|
+
</div>
|
|
427
|
+
<div className="space-y-1">
|
|
428
|
+
<p className="text-xs text-fg-subtle">
|
|
429
|
+
Read the full log before trying again:
|
|
430
|
+
</p>
|
|
431
|
+
<code className="block rounded-xs bg-surface-sunken px-2 py-1 text-2xs text-fg-muted">
|
|
432
|
+
{state.logsCommand}
|
|
433
|
+
</code>
|
|
434
|
+
</div>
|
|
435
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
436
|
+
<Button
|
|
437
|
+
variant="secondary"
|
|
438
|
+
size="sm"
|
|
439
|
+
icon={<RotateCcw className="size-3.5" />}
|
|
440
|
+
onClick={handleInstall}
|
|
441
|
+
>
|
|
442
|
+
Try {state.attemptedVersion} again
|
|
443
|
+
</Button>
|
|
444
|
+
<Button variant="ghost" size="sm" onClick={onDismissResult}>
|
|
445
|
+
Stay on {state.version}
|
|
446
|
+
</Button>
|
|
447
|
+
</div>
|
|
448
|
+
</div>
|
|
449
|
+
</SectionRow>
|
|
450
|
+
);
|
|
451
|
+
|
|
351
452
|
default: {
|
|
352
453
|
const exhaustive: never = state;
|
|
353
454
|
return exhaustive;
|
|
@@ -164,6 +164,41 @@ describe("SelfUpdateSection", () => {
|
|
|
164
164
|
assert.match(html, /Check again/);
|
|
165
165
|
assert.doesNotMatch(html, /disabled=/);
|
|
166
166
|
});
|
|
167
|
+
|
|
168
|
+
it("renders a failed rollback verbatim and never claims a version is running", () => {
|
|
169
|
+
const html = section({
|
|
170
|
+
status: "rollbackFailed",
|
|
171
|
+
runId: demoRunId,
|
|
172
|
+
attemptedVersion: "0.9.4",
|
|
173
|
+
previousVersion: CURRENT,
|
|
174
|
+
reason: "migration 0042 failed and the snapshot restore errored",
|
|
175
|
+
logsCommand: demoLogsCommand,
|
|
176
|
+
});
|
|
177
|
+
assert.match(html, /could not put 0\.9\.3 back/);
|
|
178
|
+
assert.match(html, /needs you at a shell/);
|
|
179
|
+
assert.match(
|
|
180
|
+
html,
|
|
181
|
+
/migration 0042 failed and the snapshot restore errored/,
|
|
182
|
+
);
|
|
183
|
+
assert.match(html, /remit logs --since 10m/);
|
|
184
|
+
// The rollback itself failed — the pane must not assert a running version.
|
|
185
|
+
assert.doesNotMatch(html, /running 0\.9\.3 again/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("renders an abandoned run as a no-op that changed nothing", () => {
|
|
189
|
+
const html = section({
|
|
190
|
+
status: "abandoned",
|
|
191
|
+
runId: demoRunId,
|
|
192
|
+
version: CURRENT,
|
|
193
|
+
attemptedVersion: "0.9.4",
|
|
194
|
+
reason: "manifest fetch timed out before anything was pulled",
|
|
195
|
+
logsCommand: demoLogsCommand,
|
|
196
|
+
});
|
|
197
|
+
assert.match(html, /Nothing changed/);
|
|
198
|
+
assert.match(html, /still\s+running 0\.9\.3/);
|
|
199
|
+
assert.match(html, /manifest fetch timed out/);
|
|
200
|
+
assert.match(html, /Try 0\.9\.4 again/);
|
|
201
|
+
});
|
|
167
202
|
});
|
|
168
203
|
|
|
169
204
|
describe("SelfUpdateConfirmDialog", () => {
|
|
@@ -201,6 +236,25 @@ describe("SelfUpdateConfirmDialog", () => {
|
|
|
201
236
|
"",
|
|
202
237
|
);
|
|
203
238
|
});
|
|
239
|
+
|
|
240
|
+
it("says nothing about a migration by default", () => {
|
|
241
|
+
assert.doesNotMatch(html, /updates the database/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("warns about a migration only when the release carries one", () => {
|
|
245
|
+
const withMigration = render(
|
|
246
|
+
createElement(SelfUpdateConfirmDialog, {
|
|
247
|
+
open: true,
|
|
248
|
+
currentVersion: CURRENT,
|
|
249
|
+
release: demoRelease,
|
|
250
|
+
appliesSchemaMigration: true,
|
|
251
|
+
onClose: noop,
|
|
252
|
+
onConfirm: noop,
|
|
253
|
+
}),
|
|
254
|
+
);
|
|
255
|
+
assert.match(withMigration, /updates the database while Remit is offline/);
|
|
256
|
+
assert.match(withMigration, /rolled back to the version and data you have/);
|
|
257
|
+
});
|
|
204
258
|
});
|
|
205
259
|
|
|
206
260
|
describe("SelfUpdateProgressOverlay", () => {
|
|
@@ -111,6 +111,39 @@ export const RolledBack: Story = {
|
|
|
111
111
|
}),
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The new version did not start and the rollback to the old one failed too.
|
|
116
|
+
* The one outcome Remit cannot resolve on its own: it names the failure and the
|
|
117
|
+
* log verbatim, claims no running version, and sends the operator to a shell.
|
|
118
|
+
*/
|
|
119
|
+
export const RollbackFailed: Story = {
|
|
120
|
+
args: withState({
|
|
121
|
+
status: "rollbackFailed",
|
|
122
|
+
runId: demoRunId,
|
|
123
|
+
attemptedVersion: demoRelease.version,
|
|
124
|
+
previousVersion: CURRENT,
|
|
125
|
+
reason:
|
|
126
|
+
"migration 0042_add_thread_index failed and the snapshot restore errored: database is locked",
|
|
127
|
+
logsCommand: demoLogsCommand,
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The run stopped before it changed anything — a manifest that could not be
|
|
133
|
+
* fetched, a preflight that refused. Nothing was installed and nothing was
|
|
134
|
+
* touched, so the pane says exactly that and offers the retry.
|
|
135
|
+
*/
|
|
136
|
+
export const Abandoned: Story = {
|
|
137
|
+
args: withState({
|
|
138
|
+
status: "abandoned",
|
|
139
|
+
runId: demoRunId,
|
|
140
|
+
version: CURRENT,
|
|
141
|
+
attemptedVersion: demoRelease.version,
|
|
142
|
+
reason: "manifest fetch timed out before anything was pulled",
|
|
143
|
+
logsCommand: demoLogsCommand,
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
|
|
114
147
|
/**
|
|
115
148
|
* An update is running. The blocking screen owns the window; the pane behind it
|
|
116
149
|
* still says what is going on rather than going blank.
|
|
@@ -158,6 +191,24 @@ export const ConfirmBeforeInstalling: Story = {
|
|
|
158
191
|
),
|
|
159
192
|
};
|
|
160
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Consent for a release that also migrates the database. The extra line states
|
|
196
|
+
* the forward step is one-way but a failed start is still rolled back — shown
|
|
197
|
+
* only when the surface reports a higher schema version than the running one.
|
|
198
|
+
*/
|
|
199
|
+
export const ConfirmWithSchemaMigration: Story = {
|
|
200
|
+
render: () => (
|
|
201
|
+
<SelfUpdateConfirmDialog
|
|
202
|
+
open
|
|
203
|
+
currentVersion={CURRENT}
|
|
204
|
+
release={demoRelease}
|
|
205
|
+
appliesSchemaMigration
|
|
206
|
+
onClose={() => {}}
|
|
207
|
+
onConfirm={() => {}}
|
|
208
|
+
/>
|
|
209
|
+
),
|
|
210
|
+
};
|
|
211
|
+
|
|
161
212
|
/**
|
|
162
213
|
* Consent reached from the pane, and declined. The offer stays exactly where
|
|
163
214
|
* it was; declining costs nothing and is not asked about again.
|
|
@@ -69,6 +69,27 @@ export type SelfUpdateState =
|
|
|
69
69
|
attemptedVersion: string;
|
|
70
70
|
elapsedSeconds: number;
|
|
71
71
|
logsCommand: string;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
status: "rollbackFailed";
|
|
75
|
+
runId: UpdateRunId;
|
|
76
|
+
attemptedVersion: string;
|
|
77
|
+
previousVersion: string;
|
|
78
|
+
/** The server's own account of the failure, shown verbatim. */
|
|
79
|
+
reason: string;
|
|
80
|
+
/** The command to read the full log, shown verbatim. */
|
|
81
|
+
logsCommand: string;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
status: "abandoned";
|
|
85
|
+
runId: UpdateRunId;
|
|
86
|
+
/** The version still running — the run changed nothing. */
|
|
87
|
+
version: string;
|
|
88
|
+
attemptedVersion: string;
|
|
89
|
+
/** The server's own account of why it stopped, shown verbatim. */
|
|
90
|
+
reason: string;
|
|
91
|
+
/** The command to read the full log, shown verbatim. */
|
|
92
|
+
logsCommand: string;
|
|
72
93
|
};
|
|
73
94
|
|
|
74
95
|
export type SelfUpdateStatus = SelfUpdateState["status"];
|
package/src/index.ts
CHANGED
|
@@ -121,6 +121,53 @@ export {
|
|
|
121
121
|
FieldLabel,
|
|
122
122
|
type FieldLabelProps,
|
|
123
123
|
} from "./components/field-label.js";
|
|
124
|
+
export {
|
|
125
|
+
AddChipButton,
|
|
126
|
+
type AddChipButtonProps,
|
|
127
|
+
ClauseChip,
|
|
128
|
+
type ClauseChipProps,
|
|
129
|
+
type ClauseDraft,
|
|
130
|
+
ClauseEditor,
|
|
131
|
+
type ClauseEditorProps,
|
|
132
|
+
WidenChip,
|
|
133
|
+
type WidenChipProps,
|
|
134
|
+
} from "./components/filter-clause-chip.js";
|
|
135
|
+
export {
|
|
136
|
+
FilterPreviewCount,
|
|
137
|
+
type FilterPreviewCountProps,
|
|
138
|
+
} from "./components/filter-preview-count.js";
|
|
139
|
+
export {
|
|
140
|
+
type ClauseField,
|
|
141
|
+
clauseFieldLabel,
|
|
142
|
+
clauseFieldOrder,
|
|
143
|
+
commitBlockedReason,
|
|
144
|
+
commitLabel,
|
|
145
|
+
demoFolders,
|
|
146
|
+
demoRule,
|
|
147
|
+
demoSenderFallbackRule,
|
|
148
|
+
demoVocabularyRule,
|
|
149
|
+
type FilterRule,
|
|
150
|
+
type FolderOption,
|
|
151
|
+
type MatchOperator,
|
|
152
|
+
matchJoinWord,
|
|
153
|
+
matchOperatorLabel,
|
|
154
|
+
type PreviewCount,
|
|
155
|
+
previewCountSummary,
|
|
156
|
+
type RuleClause,
|
|
157
|
+
type RuleScope,
|
|
158
|
+
type RuleWiden,
|
|
159
|
+
scopeLabel,
|
|
160
|
+
widenChipLabel,
|
|
161
|
+
} from "./components/filter-rule.js";
|
|
162
|
+
export {
|
|
163
|
+
type ClauseEditState,
|
|
164
|
+
FilterRuleDialog,
|
|
165
|
+
type FilterRuleDialogProps,
|
|
166
|
+
FilterRuleEditor,
|
|
167
|
+
type FilterRuleEditorProps,
|
|
168
|
+
FilterRuleSheet,
|
|
169
|
+
type FilterRuleSheetProps,
|
|
170
|
+
} from "./components/filter-rule-editor.js";
|
|
124
171
|
export {
|
|
125
172
|
FilterSheet,
|
|
126
173
|
type FilterSheetCategory,
|