@remit/web-client 0.0.134 → 0.0.135
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/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.135",
|
|
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": {
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
outboxDetailOperationsDeleteOutboxMessageMutation,
|
|
4
4
|
outboxDetailOperationsGetOutboxMessageOptions,
|
|
5
5
|
outboxDetailOperationsSendOutboxMessageMutation,
|
|
6
|
-
outboxOperationsCreateOutboxMessageMutation,
|
|
7
6
|
} from "@remit/api-http-client/@tanstack/react-query.gen.ts";
|
|
8
7
|
import type {
|
|
9
8
|
RemitImapAccountResponse,
|
|
@@ -442,10 +441,11 @@ export const ComposeForm = ({
|
|
|
442
441
|
[setOutboxMessageId],
|
|
443
442
|
);
|
|
444
443
|
|
|
445
|
-
const { saveStatus, saveError, saveDraft, stopAutoSave } =
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
444
|
+
const { saveStatus, saveError, saveDraft, saveImmediately, stopAutoSave } =
|
|
445
|
+
useSaveDraft({
|
|
446
|
+
outboxMessageId,
|
|
447
|
+
onDraftCreated: adoptCreatedDraft,
|
|
448
|
+
});
|
|
449
449
|
|
|
450
450
|
// Auto-save runs on a debounce, so a failure has no inline call site to
|
|
451
451
|
// surface it. Push the real error detail to a banner instead of leaving only
|
|
@@ -460,10 +460,6 @@ export const ComposeForm = ({
|
|
|
460
460
|
});
|
|
461
461
|
}, [saveError, pushError]);
|
|
462
462
|
|
|
463
|
-
const createMutation = useMutation(
|
|
464
|
-
outboxOperationsCreateOutboxMessageMutation(),
|
|
465
|
-
);
|
|
466
|
-
|
|
467
463
|
const sendMutation = useMutation(
|
|
468
464
|
outboxDetailOperationsSendOutboxMessageMutation(),
|
|
469
465
|
);
|
|
@@ -496,7 +492,11 @@ export const ComposeForm = ({
|
|
|
496
492
|
? accountIsMissingSmtp(selectedAccount)
|
|
497
493
|
: false;
|
|
498
494
|
|
|
499
|
-
|
|
495
|
+
// The action bar refuses a second press while one is in flight, but the
|
|
496
|
+
// editor's own Cmd+Enter goes straight to `handleSend`, and the write that
|
|
497
|
+
// now precedes the request widens the window a second press lands in.
|
|
498
|
+
const sendInFlightRef = useRef(false);
|
|
499
|
+
const [isSending, setIsSending] = useState(false);
|
|
500
500
|
const canSend =
|
|
501
501
|
toAddresses.length > 0 &&
|
|
502
502
|
!!selectedAccountId &&
|
|
@@ -538,76 +538,79 @@ export const ComposeForm = ({
|
|
|
538
538
|
]);
|
|
539
539
|
|
|
540
540
|
const handleSend = useCallback(async () => {
|
|
541
|
+
if (sendInFlightRef.current) return;
|
|
541
542
|
if (!selectedAccountId || toAddresses.length === 0) return;
|
|
542
543
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
? getReferences(sourceMessage)
|
|
548
|
-
: {};
|
|
544
|
+
sendInFlightRef.current = true;
|
|
545
|
+
setIsSending(true);
|
|
546
|
+
try {
|
|
547
|
+
stopAutoSave();
|
|
549
548
|
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
const replyData =
|
|
550
|
+
sourceMessage && (mode === "reply" || mode === "reply_all")
|
|
551
|
+
? getReferences(sourceMessage)
|
|
552
|
+
: {};
|
|
552
553
|
|
|
553
|
-
if (!messageId) {
|
|
554
554
|
const { html: htmlBody, text: textBody } = body;
|
|
555
|
+
const createdThisAttempt = !outboxMessageId;
|
|
556
|
+
|
|
557
|
+
// The debounce dropped above may have been holding the last two seconds
|
|
558
|
+
// of typing, and an existing entry would otherwise go out as the server
|
|
559
|
+
// last saw it (#674). What is on screen is written first, and a write
|
|
560
|
+
// that fails stops the send rather than transmitting the older copy.
|
|
561
|
+
const flushed = await saveImmediately({
|
|
562
|
+
accountId: selectedAccountId,
|
|
563
|
+
toAddresses: toAddresses.map((a) => a.email),
|
|
564
|
+
ccAddresses:
|
|
565
|
+
ccAddresses.length > 0 ? ccAddresses.map((a) => a.email) : undefined,
|
|
566
|
+
bccAddresses:
|
|
567
|
+
bccAddresses.length > 0
|
|
568
|
+
? bccAddresses.map((a) => a.email)
|
|
569
|
+
: undefined,
|
|
570
|
+
subject: subject || undefined,
|
|
571
|
+
textBody: textBody || undefined,
|
|
572
|
+
htmlBody: htmlBody || undefined,
|
|
573
|
+
...replyData,
|
|
574
|
+
});
|
|
555
575
|
|
|
556
|
-
|
|
576
|
+
if (flushed.outcome === "failed") {
|
|
577
|
+
pushError({
|
|
578
|
+
title: "Couldn't send message",
|
|
579
|
+
detail:
|
|
580
|
+
formatErrorDetail(flushed.error) ??
|
|
581
|
+
"Saving the message failed, so nothing was sent. Try again.",
|
|
582
|
+
error: flushed.error,
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const messageId = flushed.outboxMessageId;
|
|
588
|
+
|
|
589
|
+
const sent = await sendMutation
|
|
557
590
|
.mutateAsync({
|
|
558
|
-
|
|
559
|
-
accountId: selectedAccountId,
|
|
560
|
-
toAddresses: toAddresses.map((a) => a.email),
|
|
561
|
-
ccAddresses:
|
|
562
|
-
ccAddresses.length > 0
|
|
563
|
-
? ccAddresses.map((a) => a.email)
|
|
564
|
-
: undefined,
|
|
565
|
-
bccAddresses:
|
|
566
|
-
bccAddresses.length > 0
|
|
567
|
-
? bccAddresses.map((a) => a.email)
|
|
568
|
-
: undefined,
|
|
569
|
-
subject: subject || undefined,
|
|
570
|
-
textBody: textBody || undefined,
|
|
571
|
-
htmlBody: htmlBody || undefined,
|
|
572
|
-
sendImmediately: false,
|
|
573
|
-
...replyData,
|
|
574
|
-
},
|
|
591
|
+
path: { outboxMessageId: messageId },
|
|
575
592
|
})
|
|
576
593
|
.catch((error: unknown) => {
|
|
577
594
|
pushError({
|
|
578
595
|
title: "Couldn't send message",
|
|
579
|
-
detail:
|
|
596
|
+
detail:
|
|
597
|
+
formatErrorDetail(error) ??
|
|
598
|
+
(createdThisAttempt
|
|
599
|
+
? "The draft was saved but the send request failed. Try again from the Outbox."
|
|
600
|
+
: "The send request failed. Try again."),
|
|
580
601
|
error,
|
|
581
602
|
});
|
|
582
603
|
return null;
|
|
583
604
|
});
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
605
|
+
if (sent === null) return;
|
|
606
|
+
|
|
607
|
+
stopAutoSave(messageId);
|
|
608
|
+
startSendPolling(messageId);
|
|
609
|
+
onClose();
|
|
610
|
+
} finally {
|
|
611
|
+
sendInFlightRef.current = false;
|
|
612
|
+
setIsSending(false);
|
|
588
613
|
}
|
|
589
|
-
|
|
590
|
-
const sent = await sendMutation
|
|
591
|
-
.mutateAsync({
|
|
592
|
-
path: { outboxMessageId: messageId },
|
|
593
|
-
})
|
|
594
|
-
.catch((error: unknown) => {
|
|
595
|
-
pushError({
|
|
596
|
-
title: "Couldn't send message",
|
|
597
|
-
detail:
|
|
598
|
-
formatErrorDetail(error) ??
|
|
599
|
-
(createdThisAttempt
|
|
600
|
-
? "The draft was saved but the send request failed. Try again from the Outbox."
|
|
601
|
-
: "The send request failed. Try again."),
|
|
602
|
-
error,
|
|
603
|
-
});
|
|
604
|
-
return null;
|
|
605
|
-
});
|
|
606
|
-
if (sent === null) return;
|
|
607
|
-
|
|
608
|
-
stopAutoSave(messageId);
|
|
609
|
-
startSendPolling(messageId);
|
|
610
|
-
onClose();
|
|
611
614
|
}, [
|
|
612
615
|
selectedAccountId,
|
|
613
616
|
toAddresses,
|
|
@@ -618,11 +621,10 @@ export const ComposeForm = ({
|
|
|
618
621
|
mode,
|
|
619
622
|
sourceMessage,
|
|
620
623
|
outboxMessageId,
|
|
621
|
-
|
|
624
|
+
saveImmediately,
|
|
622
625
|
sendMutation,
|
|
623
626
|
stopAutoSave,
|
|
624
627
|
startSendPolling,
|
|
625
|
-
setOutboxMessageId,
|
|
626
628
|
pushError,
|
|
627
629
|
onClose,
|
|
628
630
|
]);
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* then sends it — and creating it as already-queued makes that send a second
|
|
15
15
|
* dispatch, which the server refuses.
|
|
16
16
|
*
|
|
17
|
+
* Issue #674 is the boundary between the two: pressing Send inside the two
|
|
18
|
+
* seconds the debounce is still counting down. The cancelled timer was carrying
|
|
19
|
+
* the last edits, and the entry went out as the server last saw it. So the send
|
|
20
|
+
* writes what is on screen first, and a write that fails takes the send with it
|
|
21
|
+
* rather than transmitting the older copy behind the user's back.
|
|
22
|
+
*
|
|
17
23
|
* The stub holds the outbox rule the API holds: only a draft accepts a send.
|
|
18
24
|
*/
|
|
19
25
|
|
|
@@ -81,8 +87,18 @@ afterEach(() => {
|
|
|
81
87
|
const callsTo = (suffix: string) =>
|
|
82
88
|
(http?.calls ?? []).filter((call) => call.path.endsWith(suffix));
|
|
83
89
|
|
|
84
|
-
const
|
|
85
|
-
(http?.calls ?? []).filter((call) => call.method === "PATCH")
|
|
90
|
+
const patches = () =>
|
|
91
|
+
(http?.calls ?? []).filter((call) => call.method === "PATCH");
|
|
92
|
+
|
|
93
|
+
const patchCount = (): number => patches().length;
|
|
94
|
+
|
|
95
|
+
const patchesAfterTheSend = (): number => {
|
|
96
|
+
const calls = http?.calls ?? [];
|
|
97
|
+
const sendIndex = calls.findIndex((call) => call.path.endsWith("/send"));
|
|
98
|
+
if (sendIndex === -1) throw new Error("the send never went out");
|
|
99
|
+
return calls.slice(sendIndex + 1).filter((call) => call.method === "PATCH")
|
|
100
|
+
.length;
|
|
101
|
+
};
|
|
86
102
|
|
|
87
103
|
const Opened = ({ outboxMessageId }: { outboxMessageId?: string }) => {
|
|
88
104
|
const { state, openCompose } = useCompose();
|
|
@@ -108,6 +124,8 @@ interface MountOptions {
|
|
|
108
124
|
outboxMessageId?: string;
|
|
109
125
|
/** Hold the PATCH response open so a save is still in flight at send time. */
|
|
110
126
|
gatePatch?: boolean;
|
|
127
|
+
/** Refuse every PATCH, so the write compose makes before sending fails. */
|
|
128
|
+
failPatch?: boolean;
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
const mount = async (
|
|
@@ -140,6 +158,7 @@ const mount = async (
|
|
|
140
158
|
|
|
141
159
|
if (call.method === "PATCH") {
|
|
142
160
|
await patchGate;
|
|
161
|
+
if (options.failPatch) return httpError(409, "The draft moved on.");
|
|
143
162
|
return outboxEntry(status);
|
|
144
163
|
}
|
|
145
164
|
|
|
@@ -172,7 +191,7 @@ const sendButton = (): HTMLElement => {
|
|
|
172
191
|
return button;
|
|
173
192
|
};
|
|
174
193
|
|
|
175
|
-
describe("compose and the outbox entry it is sending (#604)", () => {
|
|
194
|
+
describe("compose and the outbox entry it is sending (#604, #674)", () => {
|
|
176
195
|
it("writes no further PATCH to an entry once it has been sent", async () => {
|
|
177
196
|
const { releasePatch } = await mount({
|
|
178
197
|
outboxMessageId: OUTBOX_MESSAGE_ID,
|
|
@@ -193,7 +212,76 @@ describe("compose and the outbox entry it is sending (#604)", () => {
|
|
|
193
212
|
await harness?.wait(AUTOSAVE_DEBOUNCE_MS + 200);
|
|
194
213
|
|
|
195
214
|
assert.equal(callsTo("/send").length, 1, "the send went out");
|
|
196
|
-
assert.equal(
|
|
215
|
+
assert.equal(patchesAfterTheSend(), 0, "no write followed the send");
|
|
216
|
+
// The in-flight autosave and the write before the send are the two, in
|
|
217
|
+
// that order: a second write issued alongside the first could land behind
|
|
218
|
+
// it and put the older body back.
|
|
219
|
+
assert.equal(
|
|
220
|
+
patchCount(),
|
|
221
|
+
2,
|
|
222
|
+
"the two writes went out one after the other",
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("writes the edit made inside the debounce window before it sends", async () => {
|
|
227
|
+
await mount({ outboxMessageId: OUTBOX_MESSAGE_ID });
|
|
228
|
+
|
|
229
|
+
harness?.type(subjectField(), "Re: Lunch tomorrow");
|
|
230
|
+
await harness?.wait(AUTOSAVE_DEBOUNCE_MS + 100);
|
|
231
|
+
assert.equal(patchCount(), 1, "the typing burst autosaves once");
|
|
232
|
+
|
|
233
|
+
// Inside the two seconds the next autosave would have waited.
|
|
234
|
+
harness?.type(subjectField(), "Re: Lunch on Thursday");
|
|
235
|
+
harness?.click(sendButton());
|
|
236
|
+
await harness?.flush();
|
|
237
|
+
await harness?.wait(100);
|
|
238
|
+
|
|
239
|
+
assert.equal(callsTo("/send").length, 1, "the send went out");
|
|
240
|
+
const written = patches().at(-1);
|
|
241
|
+
assert.equal(
|
|
242
|
+
written?.body?.subject,
|
|
243
|
+
"Re: Lunch on Thursday",
|
|
244
|
+
"the entry carries the edit the debounce was still holding",
|
|
245
|
+
);
|
|
246
|
+
// Autosave never writes these, so an entry autosaved before Send used to
|
|
247
|
+
// go out unchained from the message it replies to.
|
|
248
|
+
assert.equal(written?.body?.inReplyTo, "<m1@example.com>");
|
|
249
|
+
assert.deepEqual(written?.body?.references, ["<m1@example.com>"]);
|
|
250
|
+
assert.equal(closed, 1, "compose closed on a successful send");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("sends nothing, and says so, when that write fails", async () => {
|
|
254
|
+
await mount({ outboxMessageId: OUTBOX_MESSAGE_ID, failPatch: true });
|
|
255
|
+
|
|
256
|
+
harness?.type(subjectField(), "Re: Lunch on Thursday");
|
|
257
|
+
harness?.click(sendButton());
|
|
258
|
+
await harness?.flush();
|
|
259
|
+
await harness?.wait(100);
|
|
260
|
+
|
|
261
|
+
assert.equal(patchCount(), 1, "the write was attempted");
|
|
262
|
+
assert.equal(callsTo("/send").length, 0, "the older copy stayed put");
|
|
263
|
+
assert.equal(closed, 0, "compose stayed open");
|
|
264
|
+
assert.match(harness?.text() ?? "", /Couldn't send message/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("dispatches once when Send is pressed twice over the write", async () => {
|
|
268
|
+
const { releasePatch } = await mount({
|
|
269
|
+
outboxMessageId: OUTBOX_MESSAGE_ID,
|
|
270
|
+
gatePatch: true,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
harness?.type(subjectField(), "Re: Lunch on Thursday");
|
|
274
|
+
harness?.click(sendButton());
|
|
275
|
+
harness?.click(sendButton());
|
|
276
|
+
await harness?.flush();
|
|
277
|
+
|
|
278
|
+
releasePatch();
|
|
279
|
+
await harness?.flush();
|
|
280
|
+
await harness?.wait(100);
|
|
281
|
+
|
|
282
|
+
assert.equal(patchCount(), 1, "the entry was written once");
|
|
283
|
+
assert.equal(callsTo("/send").length, 1, "the message went out once");
|
|
284
|
+
assert.equal(closed, 1, "compose closed on a successful send");
|
|
197
285
|
});
|
|
198
286
|
|
|
199
287
|
it("sends a reply pressed before the first autosave, in one dispatch", async () => {
|
|
@@ -8,6 +8,10 @@ import { useCallback, useRef, useState } from "react";
|
|
|
8
8
|
|
|
9
9
|
export type SaveStatus = "idle" | "saving" | "saved" | "error";
|
|
10
10
|
|
|
11
|
+
export type ImmediateSave =
|
|
12
|
+
| { outcome: "saved"; outboxMessageId: string }
|
|
13
|
+
| { outcome: "failed"; error: unknown };
|
|
14
|
+
|
|
11
15
|
interface DraftData {
|
|
12
16
|
accountId: string;
|
|
13
17
|
toAddresses: string[];
|
|
@@ -25,6 +29,12 @@ interface UseSaveDraftOptions {
|
|
|
25
29
|
onDraftCreated: (id: string) => void;
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
const settled = (promise: Promise<unknown>): Promise<void> =>
|
|
33
|
+
promise.then(
|
|
34
|
+
() => undefined,
|
|
35
|
+
() => undefined,
|
|
36
|
+
);
|
|
37
|
+
|
|
28
38
|
export const useSaveDraft = ({
|
|
29
39
|
outboxMessageId,
|
|
30
40
|
onDraftCreated,
|
|
@@ -35,6 +45,17 @@ export const useSaveDraft = ({
|
|
|
35
45
|
const closedIdsRef = useRef<Set<string>>(new Set());
|
|
36
46
|
const queryClient = useQueryClient();
|
|
37
47
|
|
|
48
|
+
// The entry a save writes to. That is the prop, except between a save
|
|
49
|
+
// creating the draft and the id arriving back as the prop — reading the prop
|
|
50
|
+
// in that window creates the same draft a second time and strands one of the
|
|
51
|
+
// two in the outbox.
|
|
52
|
+
const propIdRef = useRef(outboxMessageId);
|
|
53
|
+
const targetIdRef = useRef(outboxMessageId);
|
|
54
|
+
if (propIdRef.current !== outboxMessageId) {
|
|
55
|
+
propIdRef.current = outboxMessageId;
|
|
56
|
+
targetIdRef.current = outboxMessageId;
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
const createMutation = useMutation(
|
|
39
60
|
outboxOperationsCreateOutboxMessageMutation(),
|
|
40
61
|
);
|
|
@@ -47,9 +68,10 @@ export const useSaveDraft = ({
|
|
|
47
68
|
setSaveStatus("saving");
|
|
48
69
|
setSaveError(null);
|
|
49
70
|
|
|
50
|
-
|
|
71
|
+
const targetId = targetIdRef.current;
|
|
72
|
+
if (targetId) {
|
|
51
73
|
const result = await updateMutation.mutateAsync({
|
|
52
|
-
path: { outboxMessageId },
|
|
74
|
+
path: { outboxMessageId: targetId },
|
|
53
75
|
body: {
|
|
54
76
|
toAddresses: data.toAddresses,
|
|
55
77
|
ccAddresses: data.ccAddresses,
|
|
@@ -71,6 +93,7 @@ export const useSaveDraft = ({
|
|
|
71
93
|
sendImmediately: false,
|
|
72
94
|
},
|
|
73
95
|
});
|
|
96
|
+
targetIdRef.current = result.outboxMessageId;
|
|
74
97
|
onDraftCreated(result.outboxMessageId);
|
|
75
98
|
setSaveStatus("saved");
|
|
76
99
|
queryClient.invalidateQueries({
|
|
@@ -78,38 +101,60 @@ export const useSaveDraft = ({
|
|
|
78
101
|
});
|
|
79
102
|
return result;
|
|
80
103
|
},
|
|
81
|
-
[
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
[createMutation, updateMutation, onDraftCreated, queryClient],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// One entry takes one write at a time. Overlapping writes settle in whatever
|
|
108
|
+
// order the network gives them, so an older body can land last — and two of
|
|
109
|
+
// them racing while the draft has no id yet each create one.
|
|
110
|
+
const writesRef = useRef<Promise<void>>(Promise.resolve());
|
|
111
|
+
const enqueueSave = useCallback(
|
|
112
|
+
(data: DraftData) => {
|
|
113
|
+
const write = writesRef.current.then(() => executeSave(data));
|
|
114
|
+
writesRef.current = settled(write);
|
|
115
|
+
return write;
|
|
116
|
+
},
|
|
117
|
+
[executeSave],
|
|
88
118
|
);
|
|
89
119
|
|
|
90
120
|
const saveDraft = useCallback(
|
|
91
121
|
(data: DraftData) => {
|
|
92
|
-
if (outboxMessageId && closedIdsRef.current.has(outboxMessageId)) return;
|
|
93
122
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
94
123
|
timerRef.current = setTimeout(() => {
|
|
124
|
+
const targetId = targetIdRef.current;
|
|
125
|
+
if (targetId && closedIdsRef.current.has(targetId)) return;
|
|
95
126
|
// Keep the real error, not just a vague "error" status — the caller
|
|
96
127
|
// surfaces its detail in a banner. A fatal 5xx additionally escalates
|
|
97
128
|
// through the global MutationCache.onError sink.
|
|
98
|
-
|
|
129
|
+
enqueueSave(data).catch((error: unknown) => {
|
|
99
130
|
setSaveError(error);
|
|
100
131
|
setSaveStatus("error");
|
|
101
132
|
});
|
|
102
133
|
}, 2000);
|
|
103
134
|
},
|
|
104
|
-
[
|
|
135
|
+
[enqueueSave],
|
|
105
136
|
);
|
|
106
137
|
|
|
138
|
+
// Whoever asks for this is acting on the draft right now and owns the
|
|
139
|
+
// outcome, so the failure is returned rather than thrown and `saveError` is
|
|
140
|
+
// left alone — the caller's own message is the accurate one, and setting
|
|
141
|
+
// `saveError` would raise a second "Couldn't save draft" banner beside it.
|
|
107
142
|
const saveImmediately = useCallback(
|
|
108
|
-
(data: DraftData) => {
|
|
143
|
+
(data: DraftData): Promise<ImmediateSave> => {
|
|
109
144
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
110
|
-
return
|
|
145
|
+
return enqueueSave(data)
|
|
146
|
+
.then(
|
|
147
|
+
(result): ImmediateSave => ({
|
|
148
|
+
outcome: "saved",
|
|
149
|
+
outboxMessageId: result.outboxMessageId,
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
.catch((error: unknown): ImmediateSave => {
|
|
153
|
+
setSaveStatus("error");
|
|
154
|
+
return { outcome: "failed", error };
|
|
155
|
+
});
|
|
111
156
|
},
|
|
112
|
-
[
|
|
157
|
+
[enqueueSave],
|
|
113
158
|
);
|
|
114
159
|
|
|
115
160
|
// Called with an id, the entry is closed to autosave for good. Sending and
|