@remit/web-client 0.0.116 → 0.0.118
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/layout/MailTopBar.tsx +1 -2
- package/src/components/mail/MessageToolbar.tsx +1 -1
- package/src/components/mail/ThreadListInteraction.tsx +1 -2
- package/src/components/self-update/SelfUpdateOverlay.tsx +2 -2
- package/src/components/ui/KeyboardShortcutsModal.tsx +2 -3
- package/src/hooks/use-system-update.test.ts +179 -92
- package/src/hooks/use-system-update.ts +16 -20
- package/src/hooks/useTriageLayer.ts +1 -4
- package/src/lib/self-update-state.test.ts +24 -65
- package/src/lib/self-update-state.ts +66 -165
- package/src/hooks/useListCursor.test.ts +0 -273
- package/src/hooks/useListCursor.ts +0 -259
- package/src/hooks/useTriageKeyboard.ts +0 -108
- package/src/lib/keymap-dispatch.test.ts +0 -255
- package/src/lib/keymap-dispatch.ts +0 -244
- package/src/lib/keymap.test.ts +0 -68
- package/src/lib/keymap.ts +0 -210
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.118",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -7,13 +7,12 @@
|
|
|
7
7
|
* what wording is not decided here.
|
|
8
8
|
*/
|
|
9
9
|
import type { RemitImapAccountResponse } from "@remit/api-http-client/types.gen.ts";
|
|
10
|
-
import { ShellTopBar } from "@remit/ui";
|
|
10
|
+
import { ShellTopBar, shortcutHintForAction } from "@remit/ui";
|
|
11
11
|
import { useNavigate } from "@tanstack/react-router";
|
|
12
12
|
import { AccountMenu } from "@/auth/AccountMenu";
|
|
13
13
|
import { useGlobalCompose } from "@/hooks/useComposeTarget";
|
|
14
14
|
import { useSearchScope } from "@/hooks/useSearchScope";
|
|
15
15
|
import { openBugReport } from "@/lib/bug-report";
|
|
16
|
-
import { shortcutHintForAction } from "@/lib/keymap";
|
|
17
16
|
import { useMailContext } from "@/lib/mail-context";
|
|
18
17
|
|
|
19
18
|
interface MailTopBarProps {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* are not rendered: focus stops moving, the highlight disappears, and the next
|
|
17
17
|
* verb acts on a message the user cannot see.
|
|
18
18
|
*/
|
|
19
|
-
import { SelectionTopBar, type Verb } from "@remit/ui";
|
|
19
|
+
import { SelectionTopBar, useListCursor, type Verb } from "@remit/ui";
|
|
20
20
|
import {
|
|
21
21
|
createContext,
|
|
22
22
|
type ReactNode,
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
} from "react";
|
|
31
31
|
import { ConfirmDialog } from "@/components/ui/ConfirmDialog";
|
|
32
32
|
import { useFollowFocusOpen } from "@/hooks/useFollowFocusOpen";
|
|
33
|
-
import { useListCursor } from "@/hooks/useListCursor";
|
|
34
33
|
import { useIsDesktop } from "@/hooks/useMediaQuery";
|
|
35
34
|
import type { TriageContextUpdate } from "@/hooks/useTriageLayer";
|
|
36
35
|
import { formatDeleteToTrashTitle } from "@/lib/format";
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Once consent is given the server is genuinely going away, so a quiet spinner
|
|
5
5
|
* over a mailbox that cannot load would let a broken system look healthy. This
|
|
6
6
|
* mounts at the app root and owns the window for the whole apply, and for the
|
|
7
|
-
* "server never came back" verdict — from any route,
|
|
8
|
-
*
|
|
7
|
+
* "server never came back" verdict — from any route, and in any tab the server
|
|
8
|
+
* tells about the run.
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
11
|
SelfUpdateProgressOverlay,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { Kbd } from "@remit/ui";
|
|
1
|
+
import { Kbd, KEY_HINT_GROUPS } from "@remit/ui";
|
|
2
2
|
import { X } from "lucide-react";
|
|
3
3
|
import { Fragment, useCallback, useEffect } from "react";
|
|
4
|
-
import { KEY_HINT_GROUPS } from "@/lib/keymap";
|
|
5
4
|
import { cn } from "@/lib/utils";
|
|
6
5
|
|
|
7
6
|
interface KeyboardShortcutsModalProps {
|
|
@@ -11,7 +10,7 @@ interface KeyboardShortcutsModalProps {
|
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* The `?` help overlay (#429). Renders the full triage key map from the single
|
|
14
|
-
* source of truth (
|
|
13
|
+
* source of truth (`@remit/ui`'s keymap) using the `Kbd` component, so the
|
|
15
14
|
* displayed bindings can never drift from what the dispatcher actually routes.
|
|
16
15
|
*/
|
|
17
16
|
export const KeyboardShortcutsModal = ({
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The self-update hook, wired end to end against the generated client through a
|
|
3
3
|
* mocked fetch. The pure state machine is covered in `lib/self-update-state.test.ts`;
|
|
4
|
-
* these exercise the parts only the live hook has:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* these exercise the parts only the live hook has: what a page claims before the
|
|
5
|
+
* server has answered, the run this page starts and holds, and the poll that
|
|
6
|
+
* survives a dead server.
|
|
7
7
|
*/
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
9
|
import { afterEach, beforeEach, describe, test } from "node:test";
|
|
@@ -17,9 +17,8 @@ import { SelfUpdateOverlay } from "../components/self-update/SelfUpdateOverlay";
|
|
|
17
17
|
import { AdvancedNavIcon } from "../components/settings/AdvancedNavIcon";
|
|
18
18
|
import { SelfUpdatePanel } from "../components/settings/SelfUpdatePanel";
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
saveHeldRun,
|
|
20
|
+
APPLY_BUDGET_SECONDS,
|
|
21
|
+
NEVER_CAME_BACK_MARGIN_SECONDS,
|
|
23
22
|
} from "../lib/self-update-state";
|
|
24
23
|
import { createDomHarness, type DomHarness } from "../test-support/dom";
|
|
25
24
|
import { type HttpMock, httpError, mockFetch } from "../test-support/http";
|
|
@@ -32,19 +31,12 @@ import {
|
|
|
32
31
|
let harness: DomHarness | undefined;
|
|
33
32
|
let http: HttpMock | undefined;
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
clear: () => store.clear(),
|
|
42
|
-
key: () => null,
|
|
43
|
-
length: 0,
|
|
44
|
-
} as Storage;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
beforeEach(installMemoryStorage);
|
|
34
|
+
// The browser global, as the app would find it. Present so that "a reload
|
|
35
|
+
// resumes nothing" is a claim about the app rather than about the harness.
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
globalThis.localStorage = globalThis.window.localStorage;
|
|
38
|
+
localStorage.clear();
|
|
39
|
+
});
|
|
48
40
|
|
|
49
41
|
afterEach(() => {
|
|
50
42
|
http?.restore();
|
|
@@ -84,6 +76,8 @@ const available: RemitImapSystemUpdateResponse = {
|
|
|
84
76
|
};
|
|
85
77
|
|
|
86
78
|
const updateKey = systemOperationsGetSystemUpdateQueryKey();
|
|
79
|
+
const BUDGET_MS =
|
|
80
|
+
(APPLY_BUDGET_SECONDS + NEVER_CAME_BACK_MARGIN_SECONDS) * 1000;
|
|
87
81
|
|
|
88
82
|
async function settle(dom: DomHarness): Promise<void> {
|
|
89
83
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
@@ -105,6 +99,55 @@ async function settle(dom: DomHarness): Promise<void> {
|
|
|
105
99
|
}
|
|
106
100
|
}
|
|
107
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Mount the provider with a probe on the hook, so a test can drive `install`
|
|
104
|
+
* and read the surface back. The POST answers with a run in flight, which is
|
|
105
|
+
* what the real seam returns the moment it accepts the request.
|
|
106
|
+
*/
|
|
107
|
+
function mountApi(
|
|
108
|
+
getResponse: () => unknown,
|
|
109
|
+
children: ReactNode = null,
|
|
110
|
+
): { dom: DomHarness; api: () => SelfUpdateApi } {
|
|
111
|
+
http = mockFetch((call) => {
|
|
112
|
+
if (call.path.endsWith("/system/update") && call.method === "GET") {
|
|
113
|
+
return getResponse();
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
currentVersion: "0.9.3",
|
|
117
|
+
check: { status: "ok", updateAvailable: true, latestVersion: "0.9.4" },
|
|
118
|
+
run: run({ outcome: null }),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
let captured: SelfUpdateApi | undefined;
|
|
122
|
+
const Probe = () => {
|
|
123
|
+
captured = useSelfUpdate();
|
|
124
|
+
return null;
|
|
125
|
+
};
|
|
126
|
+
harness = createDomHarness();
|
|
127
|
+
harness.renderApp(
|
|
128
|
+
createElement(SelfUpdateProvider, null, createElement(Probe), children),
|
|
129
|
+
);
|
|
130
|
+
return {
|
|
131
|
+
dom: harness,
|
|
132
|
+
api: () => {
|
|
133
|
+
if (!captured) throw new Error("hook not mounted");
|
|
134
|
+
return captured;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function startUpdate(
|
|
140
|
+
dom: DomHarness,
|
|
141
|
+
api: () => SelfUpdateApi,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
await act(async () => {
|
|
144
|
+
api().install("0.9.4");
|
|
145
|
+
await dom.flush();
|
|
146
|
+
await dom.wait(1);
|
|
147
|
+
await dom.flush();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
108
151
|
async function renderSurface(
|
|
109
152
|
getResponse: () => unknown,
|
|
110
153
|
children: ReactNode,
|
|
@@ -209,41 +252,122 @@ describe("SelfUpdateOverlay — the blocking screen", () => {
|
|
|
209
252
|
assert.match(dom.html(), /Installing Remit 0\.9\.4/);
|
|
210
253
|
});
|
|
211
254
|
|
|
212
|
-
test("a
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
test("a page that has heard nothing yet says nothing at all", async () => {
|
|
256
|
+
http = mockFetch(() => new Promise(() => {}));
|
|
257
|
+
harness = createDomHarness();
|
|
258
|
+
harness.renderApp(
|
|
259
|
+
createElement(
|
|
260
|
+
SelfUpdateProvider,
|
|
261
|
+
null,
|
|
262
|
+
createElement(SelfUpdateOverlay),
|
|
263
|
+
createElement(SelfUpdatePanel),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
await harness.flush();
|
|
267
|
+
await harness.wait(1);
|
|
268
|
+
await harness.flush();
|
|
269
|
+
|
|
270
|
+
assert.equal(harness.html(), "");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("the page that asked for the update rides out a failed request", async () => {
|
|
274
|
+
let failing = false;
|
|
275
|
+
const { dom, api } = mountApi(() => {
|
|
276
|
+
if (failing) throw new Error("connection refused");
|
|
277
|
+
return available;
|
|
221
278
|
}, createElement(SelfUpdateOverlay));
|
|
279
|
+
await settle(dom);
|
|
280
|
+
await startUpdate(dom, api);
|
|
281
|
+
|
|
282
|
+
failing = true;
|
|
283
|
+
await act(async () => {
|
|
284
|
+
api().onRetryConnection();
|
|
285
|
+
await dom.flush();
|
|
286
|
+
await dom.wait(1);
|
|
287
|
+
await dom.flush();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const surface = api().surface;
|
|
291
|
+
assert.equal(
|
|
292
|
+
surface.status === "ready" &&
|
|
293
|
+
surface.section.status === "applying" &&
|
|
294
|
+
surface.section.phase,
|
|
295
|
+
"reconnecting",
|
|
296
|
+
);
|
|
222
297
|
assert.match(dom.html(), /Installing Remit 0\.9\.4/);
|
|
223
298
|
assert.doesNotMatch(dom.html(), /has not answered since the restart/);
|
|
224
299
|
});
|
|
225
300
|
|
|
226
|
-
test("the budget
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
startedAt: Date.now() - 60 * 60_000,
|
|
232
|
-
});
|
|
233
|
-
const dom = await renderSurface(() => {
|
|
234
|
-
throw new Error("connection refused");
|
|
301
|
+
test("a silence past the apply budget stops claiming an install is running", async () => {
|
|
302
|
+
let failing = false;
|
|
303
|
+
const { dom, api } = mountApi(() => {
|
|
304
|
+
if (failing) throw new Error("connection refused");
|
|
305
|
+
return available;
|
|
235
306
|
}, createElement(SelfUpdateOverlay));
|
|
236
|
-
|
|
237
|
-
|
|
307
|
+
await settle(dom);
|
|
308
|
+
await startUpdate(dom, api);
|
|
309
|
+
|
|
310
|
+
failing = true;
|
|
311
|
+
const realNow = Date.now;
|
|
312
|
+
Date.now = () => realNow() + BUDGET_MS + 60_000;
|
|
313
|
+
try {
|
|
314
|
+
await act(async () => {
|
|
315
|
+
api().onRetryConnection();
|
|
316
|
+
await dom.flush();
|
|
317
|
+
await dom.wait(1);
|
|
318
|
+
await dom.flush();
|
|
319
|
+
});
|
|
320
|
+
assert.doesNotMatch(dom.html(), /Installing Remit 0\.9\.4/);
|
|
321
|
+
assert.match(dom.html(), /has not answered since the restart/);
|
|
322
|
+
assert.match(dom.html(), /remit logs/);
|
|
323
|
+
} finally {
|
|
324
|
+
Date.now = realNow;
|
|
325
|
+
}
|
|
238
326
|
});
|
|
239
327
|
|
|
240
|
-
test("a
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
328
|
+
test("a run the server reports finished ends the wait", async () => {
|
|
329
|
+
let finished = false;
|
|
330
|
+
const { dom, api } = mountApi(
|
|
331
|
+
() =>
|
|
332
|
+
finished
|
|
333
|
+
? {
|
|
334
|
+
currentVersion: "0.9.4",
|
|
335
|
+
check: { status: "ok", updateAvailable: false },
|
|
336
|
+
run: run({ outcome: "succeeded" }),
|
|
337
|
+
}
|
|
338
|
+
: available,
|
|
339
|
+
createElement(SelfUpdateOverlay),
|
|
340
|
+
);
|
|
341
|
+
await settle(dom);
|
|
342
|
+
await startUpdate(dom, api);
|
|
343
|
+
|
|
344
|
+
finished = true;
|
|
345
|
+
await act(async () => {
|
|
346
|
+
api().onRetryConnection();
|
|
347
|
+
await dom.flush();
|
|
348
|
+
await dom.wait(1);
|
|
349
|
+
await dom.flush();
|
|
246
350
|
});
|
|
351
|
+
|
|
352
|
+
const surface = api().surface;
|
|
353
|
+
assert.equal(
|
|
354
|
+
surface.status === "ready" && surface.section.status,
|
|
355
|
+
"succeeded",
|
|
356
|
+
);
|
|
357
|
+
assert.equal(dom.html(), "");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("a reload during the silence resumes nothing", async () => {
|
|
361
|
+
const { dom, api } = mountApi(
|
|
362
|
+
() => available,
|
|
363
|
+
createElement(SelfUpdateOverlay),
|
|
364
|
+
);
|
|
365
|
+
await settle(dom);
|
|
366
|
+
await startUpdate(dom, api);
|
|
367
|
+
assert.match(dom.html(), /Installing Remit 0\.9\.4/);
|
|
368
|
+
|
|
369
|
+
http?.restore();
|
|
370
|
+
harness?.close();
|
|
247
371
|
http = mockFetch(() => new Promise(() => {}));
|
|
248
372
|
harness = createDomHarness();
|
|
249
373
|
harness.renderApp(
|
|
@@ -253,65 +377,28 @@ describe("SelfUpdateOverlay — the blocking screen", () => {
|
|
|
253
377
|
await harness.wait(1);
|
|
254
378
|
await harness.flush();
|
|
255
379
|
|
|
256
|
-
assert.
|
|
257
|
-
assert.match(harness.html(), /has not answered since the restart/);
|
|
258
|
-
assert.match(harness.html(), /remit logs/);
|
|
259
|
-
assert.equal(loadHeldRun(), null);
|
|
380
|
+
assert.equal(harness.html(), "");
|
|
260
381
|
});
|
|
261
382
|
});
|
|
262
383
|
|
|
263
384
|
describe("useSystemUpdate — actions", () => {
|
|
264
|
-
|
|
265
|
-
dom: DomHarness;
|
|
266
|
-
api: () => SelfUpdateApi;
|
|
267
|
-
} {
|
|
268
|
-
http = mockFetch((call) => {
|
|
269
|
-
if (call.path.endsWith("/system/update") && call.method === "GET") {
|
|
270
|
-
return getResponse();
|
|
271
|
-
}
|
|
272
|
-
return {
|
|
273
|
-
currentVersion: "0.9.3",
|
|
274
|
-
check: { status: "ok", updateAvailable: true, latestVersion: "0.9.4" },
|
|
275
|
-
run: run({ outcome: null }),
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
let captured: SelfUpdateApi | undefined;
|
|
279
|
-
const Probe = () => {
|
|
280
|
-
captured = useSelfUpdate();
|
|
281
|
-
return null;
|
|
282
|
-
};
|
|
283
|
-
harness = createDomHarness();
|
|
284
|
-
harness.renderApp(
|
|
285
|
-
createElement(SelfUpdateProvider, null, createElement(Probe)),
|
|
286
|
-
);
|
|
287
|
-
return {
|
|
288
|
-
dom: harness,
|
|
289
|
-
api: () => {
|
|
290
|
-
if (!captured) throw new Error("hook not mounted");
|
|
291
|
-
return captured;
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
test("install persists the run id the surface returns", async () => {
|
|
385
|
+
test("install takes the run from the surface and writes nothing down", async () => {
|
|
297
386
|
const { dom, api } = mountApi(() => available);
|
|
298
387
|
await settle(dom);
|
|
388
|
+
await startUpdate(dom, api);
|
|
299
389
|
|
|
300
|
-
await act(async () => {
|
|
301
|
-
api().install("0.9.4");
|
|
302
|
-
await dom.flush();
|
|
303
|
-
await dom.wait(1);
|
|
304
|
-
await dom.flush();
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const held = loadHeldRun();
|
|
308
|
-
assert.equal(held?.runId, "upd_1");
|
|
309
|
-
assert.equal(localStorage.getItem(SELF_UPDATE_RUN_KEY) !== null, true);
|
|
310
390
|
const surface = api().surface;
|
|
311
391
|
assert.equal(
|
|
312
392
|
surface.status === "ready" && surface.overlay.kind,
|
|
313
393
|
"applying",
|
|
314
394
|
);
|
|
395
|
+
assert.equal(
|
|
396
|
+
surface.status === "ready" &&
|
|
397
|
+
surface.section.status === "applying" &&
|
|
398
|
+
surface.section.runId,
|
|
399
|
+
"upd_1",
|
|
400
|
+
);
|
|
401
|
+
assert.equal(localStorage.length, 0);
|
|
315
402
|
});
|
|
316
403
|
|
|
317
404
|
test("dismissing a finished result clears the pane", async () => {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The self-update surface, wired to the generated client.
|
|
3
3
|
*
|
|
4
|
-
* A single instance owns the query, the mutation, and the run
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* A single instance owns the query, the mutation, and the run this page asked
|
|
5
|
+
* for, and hands the folded `SelfUpdateState` plus the blocking-overlay state to
|
|
6
|
+
* every consumer through context — so the Advanced pane, the root overlay, and
|
|
7
|
+
* the nav dot all read one source of truth. The state machine itself lives in
|
|
8
|
+
* `lib/self-update-state.ts`.
|
|
9
|
+
*
|
|
10
|
+
* The held run lives here and nowhere else. A restart does not reload the page,
|
|
11
|
+
* so this state spans the whole window; a page that loads afterwards holds
|
|
12
|
+
* nothing and takes the server's answer as it finds it.
|
|
9
13
|
*
|
|
10
14
|
* Polling follows the run: every 30 seconds while idle, every 5 seconds while a
|
|
11
|
-
* run is in flight or
|
|
15
|
+
* run is in flight or this page is waiting on one it started.
|
|
12
16
|
*/
|
|
13
17
|
import {
|
|
14
18
|
systemOperationsApplySystemUpdateMutation,
|
|
@@ -33,13 +37,11 @@ import {
|
|
|
33
37
|
} from "react";
|
|
34
38
|
import {
|
|
35
39
|
appliesSchemaMigration,
|
|
36
|
-
clearStoredRun,
|
|
37
40
|
deriveUpdateSurface,
|
|
38
41
|
type HeldRun,
|
|
39
42
|
isSurfaceAbsent,
|
|
40
|
-
|
|
43
|
+
mapUpdatePhase,
|
|
41
44
|
releaseFromCheck,
|
|
42
|
-
saveHeldRun,
|
|
43
45
|
type UpdateSurface,
|
|
44
46
|
} from "@/lib/self-update-state";
|
|
45
47
|
|
|
@@ -76,7 +78,7 @@ function pollInterval(
|
|
|
76
78
|
|
|
77
79
|
export function useSystemUpdate(): SelfUpdateApi {
|
|
78
80
|
const queryClient = useQueryClient();
|
|
79
|
-
const [held, setHeld] = useState<HeldRun | null>(
|
|
81
|
+
const [held, setHeld] = useState<HeldRun | null>(null);
|
|
80
82
|
const [dismissedRunId, setDismissedRunId] = useState<string | null>(null);
|
|
81
83
|
const [checkRequested, setCheckRequested] = useState(false);
|
|
82
84
|
|
|
@@ -112,11 +114,7 @@ export function useSystemUpdate(): SelfUpdateApi {
|
|
|
112
114
|
? derived.surface.section.runId
|
|
113
115
|
: null;
|
|
114
116
|
|
|
115
|
-
const {
|
|
116
|
-
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
if (shouldClearStored) clearStoredRun();
|
|
119
|
-
}, [shouldClearStored]);
|
|
117
|
+
const { releaseHeld } = derived;
|
|
120
118
|
|
|
121
119
|
useEffect(() => {
|
|
122
120
|
if (releaseHeld) setHeld((current) => (current === null ? current : null));
|
|
@@ -143,14 +141,13 @@ export function useSystemUpdate(): SelfUpdateApi {
|
|
|
143
141
|
onSuccess: (response: RemitImapSystemUpdateResponse) => {
|
|
144
142
|
const run = response.run;
|
|
145
143
|
if (run !== null) {
|
|
146
|
-
|
|
144
|
+
setHeld({
|
|
147
145
|
runId: run.runId,
|
|
148
146
|
attemptedVersion: run.targetVersion,
|
|
149
147
|
previousVersion: run.fromVersion,
|
|
148
|
+
phase: mapUpdatePhase(run.phase),
|
|
150
149
|
startedAt: Date.now(),
|
|
151
|
-
};
|
|
152
|
-
saveHeldRun(record);
|
|
153
|
-
setHeld(record);
|
|
150
|
+
});
|
|
154
151
|
setDismissedRunId(null);
|
|
155
152
|
}
|
|
156
153
|
queryClient.setQueryData(
|
|
@@ -174,7 +171,6 @@ export function useSystemUpdate(): SelfUpdateApi {
|
|
|
174
171
|
const onDismissResult = useCallback(() => {
|
|
175
172
|
if (shownRunIdRef.current !== null)
|
|
176
173
|
setDismissedRunId(shownRunIdRef.current);
|
|
177
|
-
clearStoredRun();
|
|
178
174
|
setHeld(null);
|
|
179
175
|
}, []);
|
|
180
176
|
|
|
@@ -14,12 +14,9 @@
|
|
|
14
14
|
* context comes first, the pane builds its handlers from it, and the keyboard
|
|
15
15
|
* registration comes last.
|
|
16
16
|
*/
|
|
17
|
+
import { type TriageHandlers, useTriageKeyboard } from "@remit/ui";
|
|
17
18
|
import { type RefObject, useCallback, useRef, useState } from "react";
|
|
18
19
|
import type { MessageListCommands } from "@/components/mail/MessageList";
|
|
19
|
-
import {
|
|
20
|
-
type TriageHandlers,
|
|
21
|
-
useTriageKeyboard,
|
|
22
|
-
} from "@/hooks/useTriageKeyboard";
|
|
23
20
|
import { adjacentMessageId } from "@/lib/adjacent-message";
|
|
24
21
|
|
|
25
22
|
export interface TriageContextUpdate {
|