@remit/web-client 0.0.251 → 0.0.252
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/config-import/ConfigImportWizard.tsx +1 -3
- package/src/components/config-import/config-import.fixtures.ts +4 -4
- package/src/components/config-import/config-import.stories.tsx +11 -13
- package/src/components/config-import/steps.import-failure.test.ts +91 -0
- package/src/components/config-import/steps.tsx +17 -14
- package/src/lib/config-import.test.ts +29 -45
- package/src/lib/config-import.ts +29 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.252",
|
|
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": {
|
|
@@ -565,9 +565,7 @@ export function ConfigImportWizard({ onDone }: ConfigImportWizardProps) {
|
|
|
565
565
|
results={sectionResults(report)}
|
|
566
566
|
message={failed?.message ?? "The import stopped before it finished."}
|
|
567
567
|
raw={`${failed?.code ?? WRITE_FAILURE_CODE}: ${failed?.message ?? "no message"}`}
|
|
568
|
-
|
|
569
|
-
// write was undone whole, and no section may read as landed.
|
|
570
|
-
undone={!report.applied}
|
|
568
|
+
nothingLanded={!report.applied}
|
|
571
569
|
onBack={goToFile}
|
|
572
570
|
onNext={() => runImport("apply", "merge")}
|
|
573
571
|
/>
|
|
@@ -182,11 +182,11 @@ export const partialImportReport: RemitImapConfigImportReport = report({
|
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
|
-
* The same stop
|
|
186
|
-
*
|
|
187
|
-
*
|
|
185
|
+
* The same stop with no section written: a store that holds the import in one
|
|
186
|
+
* transaction undoes the earlier writes, so the report carries no landed item
|
|
187
|
+
* and `applied` is false. Warnings go with them.
|
|
188
188
|
*/
|
|
189
|
-
export const
|
|
189
|
+
export const nothingLandedImportReport: RemitImapConfigImportReport = report({
|
|
190
190
|
applied: false,
|
|
191
191
|
items: [
|
|
192
192
|
item(
|
|
@@ -19,9 +19,9 @@ import {
|
|
|
19
19
|
cleanRunReport,
|
|
20
20
|
dryRunReport,
|
|
21
21
|
importedAccounts,
|
|
22
|
+
nothingLandedImportReport,
|
|
22
23
|
partialImportReport,
|
|
23
24
|
rejectedReports,
|
|
24
|
-
rolledBackImportReport,
|
|
25
25
|
} from "./config-import.fixtures";
|
|
26
26
|
import {
|
|
27
27
|
AccountCredentialsList,
|
|
@@ -417,6 +417,7 @@ export const PartiallyLandedImportUnnamed: Story = {
|
|
|
417
417
|
results={sectionResults(report)}
|
|
418
418
|
message={writeFailure(report)?.message ?? ""}
|
|
419
419
|
raw="import_write_failed: the store refused the write"
|
|
420
|
+
nothingLanded={!report.applied}
|
|
420
421
|
/>
|
|
421
422
|
);
|
|
422
423
|
},
|
|
@@ -429,26 +430,23 @@ export const PartiallyLandedImport: Story = {
|
|
|
429
430
|
results={sectionResults(partialImportReport)}
|
|
430
431
|
message={writeFailure(partialImportReport)?.message ?? ""}
|
|
431
432
|
raw={`import_write_failed: ${writeFailure(partialImportReport)?.message ?? ""}`}
|
|
433
|
+
nothingLanded={!partialImportReport.applied}
|
|
432
434
|
/>
|
|
433
435
|
),
|
|
434
436
|
};
|
|
435
437
|
|
|
436
438
|
/**
|
|
437
|
-
* The same stop
|
|
438
|
-
*
|
|
439
|
+
* The same stop with no section written — a transaction undid the earlier
|
|
440
|
+
* writes, or the first write was the one that failed. No section may carry a
|
|
441
|
+
* tick, and the screen counts nothing as landed.
|
|
439
442
|
*/
|
|
440
|
-
export const
|
|
443
|
+
export const NothingLandedImport: Story = {
|
|
441
444
|
render: () => (
|
|
442
445
|
<StepPartialImport
|
|
443
|
-
results={sectionResults(
|
|
444
|
-
message={
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
raw={`import_write_failed: ${
|
|
449
|
-
writeFailure(rolledBackImportReport)?.message ?? "no message"
|
|
450
|
-
}`}
|
|
451
|
-
undone={!rolledBackImportReport.applied}
|
|
446
|
+
results={sectionResults(nothingLandedImportReport)}
|
|
447
|
+
message={writeFailure(nothingLandedImportReport)?.message ?? ""}
|
|
448
|
+
raw={`import_write_failed: ${writeFailure(nothingLandedImportReport)?.message ?? ""}`}
|
|
449
|
+
nothingLanded={!nothingLandedImportReport.applied}
|
|
452
450
|
/>
|
|
453
451
|
),
|
|
454
452
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stopped-import screen tells the reader one story (#1147).
|
|
3
|
+
*
|
|
4
|
+
* Title, subtitle and primary action were written three times over and merged
|
|
5
|
+
* out of order, leaving a screen that said the import stopped part-way, then
|
|
6
|
+
* that nothing was written, and offered to retry "the rest". Whichever half a
|
|
7
|
+
* reader believed, the other one was there to contradict it. What is pinned
|
|
8
|
+
* here is agreement: the three strings answer the same question the same way,
|
|
9
|
+
* and no section carries a tick the report cannot vouch for.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { afterEach, describe, it } from "node:test";
|
|
14
|
+
import type { RemitImapConfigImportReport } from "@remit/api-http-client/types.gen.ts";
|
|
15
|
+
import { createElement } from "react";
|
|
16
|
+
import { sectionResults, writeFailure } from "../../lib/config-import";
|
|
17
|
+
import { createDomHarness, type DomHarness } from "../../test-support/dom";
|
|
18
|
+
import {
|
|
19
|
+
nothingLandedImportReport,
|
|
20
|
+
partialImportReport,
|
|
21
|
+
} from "./config-import.fixtures";
|
|
22
|
+
import { StepPartialImport } from "./steps";
|
|
23
|
+
|
|
24
|
+
let harness: DomHarness | undefined;
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
harness?.close();
|
|
28
|
+
harness = undefined;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const renderReport = (report: RemitImapConfigImportReport): DomHarness => {
|
|
32
|
+
const mounted = createDomHarness();
|
|
33
|
+
mounted.render(
|
|
34
|
+
createElement(StepPartialImport, {
|
|
35
|
+
results: sectionResults(report),
|
|
36
|
+
message: writeFailure(report)?.message ?? "",
|
|
37
|
+
raw: `import_write_failed: ${writeFailure(report)?.message ?? ""}`,
|
|
38
|
+
nothingLanded: !report.applied,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
return mounted;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const heading = (mounted: DomHarness): string =>
|
|
45
|
+
mounted.query("h1")?.textContent ?? "";
|
|
46
|
+
|
|
47
|
+
const subtitle = (mounted: DomHarness): string =>
|
|
48
|
+
mounted.query("h1 + p")?.textContent ?? "";
|
|
49
|
+
|
|
50
|
+
const primaryAction = (mounted: DomHarness): string => {
|
|
51
|
+
const footer = mounted.query("[data-testid=wizard-footer]");
|
|
52
|
+
assert.ok(footer, "the step renders a footer");
|
|
53
|
+
const buttons = [...footer.querySelectorAll("button")];
|
|
54
|
+
const last = buttons[buttons.length - 1];
|
|
55
|
+
assert.ok(last, "the footer offers an action");
|
|
56
|
+
return last.textContent ?? "";
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ticked = (mounted: DomHarness): number => {
|
|
60
|
+
const body = mounted.query("[data-testid=wizard-body]");
|
|
61
|
+
assert.ok(body, "the step renders a report body");
|
|
62
|
+
return body.querySelectorAll('[class*="bg-positive"]').length;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe("the screen for an import that stopped", () => {
|
|
66
|
+
it("says nothing landed in all three places, and never part-way", () => {
|
|
67
|
+
harness = renderReport(nothingLandedImportReport);
|
|
68
|
+
|
|
69
|
+
assert.equal(heading(harness), "The import stopped, and nothing landed");
|
|
70
|
+
assert.equal(subtitle(harness), "No section of the file was written.");
|
|
71
|
+
assert.equal(primaryAction(harness), "Try the import again");
|
|
72
|
+
assert.ok(
|
|
73
|
+
!harness.text().includes("part-way"),
|
|
74
|
+
"a screen that says nothing landed cannot also say the import got part of the way",
|
|
75
|
+
);
|
|
76
|
+
assert.ok(
|
|
77
|
+
!harness.text().includes("Retry the rest"),
|
|
78
|
+
'"the rest" claims the file has a part already here',
|
|
79
|
+
);
|
|
80
|
+
assert.equal(ticked(harness), 0, "no section may carry a tick");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("counts what landed when the server says some of it did", () => {
|
|
84
|
+
harness = renderReport(partialImportReport);
|
|
85
|
+
|
|
86
|
+
assert.equal(heading(harness), "The import stopped part-way");
|
|
87
|
+
assert.equal(subtitle(harness), "2 of 5 sections landed.");
|
|
88
|
+
assert.equal(primaryAction(harness), "Retry the rest");
|
|
89
|
+
assert.equal(ticked(harness), 2, "the landed sections keep their ticks");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -704,16 +704,15 @@ export function StepFileRejected({
|
|
|
704
704
|
}
|
|
705
705
|
|
|
706
706
|
/**
|
|
707
|
-
* A section nobody can vouch for reads as failed, never as pending
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
707
|
+
* A section nobody can vouch for reads as failed, never as pending: it is
|
|
708
|
+
* finished, and an outcome the reader has to go and check for themselves is
|
|
709
|
+
* not a quiet empty row. Pending is the empty row — nothing from the file went
|
|
710
|
+
* into this section, whether the import never reached it or reached it and
|
|
711
|
+
* wrote none of it — and it is the state that carries no tick.
|
|
712
712
|
*/
|
|
713
713
|
function resultState(state: SectionResult["state"]) {
|
|
714
714
|
if (state === "landed") return "ok" as const;
|
|
715
|
-
if (state === "failed" || state === "unknown"
|
|
716
|
-
return "failed" as const;
|
|
715
|
+
if (state === "failed" || state === "unknown") return "failed" as const;
|
|
717
716
|
return "pending" as const;
|
|
718
717
|
}
|
|
719
718
|
|
|
@@ -721,7 +720,7 @@ export function StepPartialImport({
|
|
|
721
720
|
results,
|
|
722
721
|
message,
|
|
723
722
|
raw,
|
|
724
|
-
|
|
723
|
+
nothingLanded,
|
|
725
724
|
onBack,
|
|
726
725
|
onNext,
|
|
727
726
|
}: {
|
|
@@ -729,8 +728,8 @@ export function StepPartialImport({
|
|
|
729
728
|
/** The server's own account of what survived, which only it knows. */
|
|
730
729
|
message: string;
|
|
731
730
|
raw: string;
|
|
732
|
-
/**
|
|
733
|
-
|
|
731
|
+
/** No section of the file was written — not that the instance is unchanged. */
|
|
732
|
+
nothingLanded: boolean;
|
|
734
733
|
} & StepNav) {
|
|
735
734
|
const landed = results.filter((result) => result.state === "landed").length;
|
|
736
735
|
const unknown = results.some((result) => result.state === "unknown");
|
|
@@ -738,10 +737,14 @@ export function StepPartialImport({
|
|
|
738
737
|
<WizardShell
|
|
739
738
|
steps={IMPORT_STEPS}
|
|
740
739
|
activeStep={1}
|
|
741
|
-
title=
|
|
740
|
+
title={
|
|
741
|
+
nothingLanded
|
|
742
|
+
? "The import stopped, and nothing landed"
|
|
743
|
+
: "The import stopped part-way"
|
|
744
|
+
}
|
|
742
745
|
subtitle={
|
|
743
|
-
|
|
744
|
-
? "
|
|
746
|
+
nothingLanded
|
|
747
|
+
? "No section of the file was written."
|
|
745
748
|
: unknown
|
|
746
749
|
? "It did not say how far it got, so check Settings before importing again."
|
|
747
750
|
: `${landed} of ${results.length} sections landed.`
|
|
@@ -752,7 +755,7 @@ export function StepPartialImport({
|
|
|
752
755
|
Choose another file
|
|
753
756
|
</Button>
|
|
754
757
|
<Button variant="primary" onClick={onNext}>
|
|
755
|
-
Retry the rest
|
|
758
|
+
{nothingLanded ? "Try the import again" : "Retry the rest"}
|
|
756
759
|
</Button>
|
|
757
760
|
</>
|
|
758
761
|
}
|
|
@@ -151,13 +151,13 @@ test("a stopped import splits the write order into landed, failed and never reac
|
|
|
151
151
|
assert.equal(byId.get("settings")?.state, "not-attempted");
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
test("
|
|
155
|
-
|
|
156
|
-
// only the rejected entry, because the rollback emptied the rest.
|
|
157
|
-
const rolled = sectionResults(
|
|
154
|
+
test("an import the server says wrote nothing marks no section landed, whatever ran before the stop", () => {
|
|
155
|
+
const results = sectionResults(
|
|
158
156
|
reportOf({
|
|
159
157
|
applied: false,
|
|
160
|
-
items: [
|
|
158
|
+
items: [
|
|
159
|
+
item("filters", "Receipts", "rejected", "the store refused the write"),
|
|
160
|
+
],
|
|
161
161
|
errors: [
|
|
162
162
|
{
|
|
163
163
|
code: "import_write_failed",
|
|
@@ -169,71 +169,55 @@ test("a rolled-back import lands nothing, however far the items say it got", ()
|
|
|
169
169
|
}),
|
|
170
170
|
);
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
const byId = new Map(results.map((result) => [result.section, result]));
|
|
173
|
+
assert.equal(byId.get("accounts")?.state, "not-landed");
|
|
174
|
+
assert.match(
|
|
175
|
+
byId.get("accounts")?.detail ?? "",
|
|
176
|
+
/Nothing from this section was written/,
|
|
175
177
|
);
|
|
176
|
-
|
|
177
|
-
assert.equal(byId.get("accounts")?.state, "rolled-back");
|
|
178
|
-
assert.match(byId.get("accounts")?.detail ?? "", /Nothing was written/);
|
|
179
|
-
assert.equal(byId.get("labels")?.state, "rolled-back");
|
|
178
|
+
assert.equal(byId.get("labels")?.state, "not-landed");
|
|
180
179
|
assert.equal(byId.get("filters")?.state, "failed");
|
|
181
|
-
assert.
|
|
182
|
-
assert.equal(byId.get("
|
|
183
|
-
assert.equal(
|
|
180
|
+
assert.equal(byId.get("addressFlags")?.state, "not-attempted");
|
|
181
|
+
assert.equal(byId.get("settings")?.state, "not-attempted");
|
|
182
|
+
assert.equal(results.filter((result) => result.state === "landed").length, 0);
|
|
184
183
|
});
|
|
185
184
|
|
|
186
|
-
test("
|
|
187
|
-
|
|
188
|
-
// report that claims writes while saying nothing was applied still lands
|
|
189
|
-
// nothing, because "your accounts are in" is the one answer this screen
|
|
190
|
-
// must never give without the server saying so.
|
|
191
|
-
const rolled = sectionResults(
|
|
185
|
+
test("a first write that fails on a store with no transaction lands nothing either", () => {
|
|
186
|
+
const results = sectionResults(
|
|
192
187
|
reportOf({
|
|
193
188
|
applied: false,
|
|
194
189
|
items: [
|
|
195
|
-
item("accounts", "a@example.test", "
|
|
196
|
-
item("labels", "Receipts", "created"),
|
|
190
|
+
item("accounts", "a@example.test", "rejected", "the store refused"),
|
|
197
191
|
],
|
|
198
192
|
errors: [
|
|
199
193
|
{
|
|
200
194
|
code: "import_write_failed",
|
|
201
|
-
message:
|
|
202
|
-
|
|
195
|
+
message:
|
|
196
|
+
"the store refused The items above it were written and remain; the import stopped here.",
|
|
197
|
+
details: { section: "accounts", key: "a@example.test" },
|
|
203
198
|
},
|
|
204
199
|
],
|
|
205
200
|
}),
|
|
206
201
|
);
|
|
207
202
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
212
|
-
assert.deepEqual(
|
|
213
|
-
rolled
|
|
214
|
-
.filter((result) => result.section !== "filters")
|
|
215
|
-
.map((result) => result.state),
|
|
216
|
-
["rolled-back", "rolled-back", "rolled-back", "rolled-back"],
|
|
217
|
-
);
|
|
203
|
+
const byId = new Map(results.map((result) => [result.section, result]));
|
|
204
|
+
assert.equal(byId.get("accounts")?.state, "failed");
|
|
205
|
+
assert.equal(byId.get("labels")?.state, "not-attempted");
|
|
206
|
+
assert.equal(results.filter((result) => result.state === "landed").length, 0);
|
|
218
207
|
});
|
|
219
208
|
|
|
220
|
-
test("
|
|
221
|
-
const
|
|
209
|
+
test("nothing written and no section named is a known outcome, not an unknown one", () => {
|
|
210
|
+
const results = sectionResults(
|
|
222
211
|
reportOf({
|
|
223
212
|
applied: false,
|
|
224
213
|
errors: [{ code: "import_write_failed", message: "the store refused" }],
|
|
225
214
|
}),
|
|
226
215
|
);
|
|
227
216
|
|
|
228
|
-
assert.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
);
|
|
232
|
-
assert.equal(
|
|
233
|
-
rolled.some((result) => result.state === "unknown"),
|
|
234
|
-
false,
|
|
217
|
+
assert.deepEqual(
|
|
218
|
+
[...new Set(results.map((result) => result.state))],
|
|
219
|
+
["not-landed"],
|
|
235
220
|
);
|
|
236
|
-
assert.match(rolled[0].detail, /Nothing was written/);
|
|
237
221
|
});
|
|
238
222
|
|
|
239
223
|
test("a pending folder names the setting waiting on it, and repeats are folded", () => {
|
package/src/lib/config-import.ts
CHANGED
|
@@ -287,9 +287,9 @@ export const writeFailure = (
|
|
|
287
287
|
|
|
288
288
|
export type SectionOutcome =
|
|
289
289
|
| "landed"
|
|
290
|
+
| "not-landed"
|
|
290
291
|
| "failed"
|
|
291
292
|
| "not-attempted"
|
|
292
|
-
| "rolled-back"
|
|
293
293
|
| "unknown";
|
|
294
294
|
|
|
295
295
|
export interface SectionResult {
|
|
@@ -299,23 +299,24 @@ export interface SectionResult {
|
|
|
299
299
|
detail: string;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
const NOT_LANDED_DETAIL = "Nothing from this section was written.";
|
|
303
|
+
|
|
302
304
|
/**
|
|
303
|
-
* What a stopped import
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
* the
|
|
305
|
+
* What a stopped import wrote, section by section. The write order is fixed, so
|
|
306
|
+
* the section the failure names splits the document in three: what is before it
|
|
307
|
+
* ran, it did not, and what is after it was never reached.
|
|
308
|
+
*
|
|
309
|
+
* `applied` is the server's answer on whether any of it was written, because
|
|
310
|
+
* only the backend knows whether it had a transaction: false means no section
|
|
311
|
+
* of the file was written, however far the write order got. It does not say
|
|
312
|
+
* why — a transaction undid the earlier writes, or the first one failed — and
|
|
313
|
+
* the report carries no flag that does, so this says what the file put in and
|
|
314
|
+
* never what the instance now holds.
|
|
308
315
|
*
|
|
309
|
-
* A failure that names no section leaves
|
|
316
|
+
* A failure that names no section leaves the split undecidable, and every
|
|
310
317
|
* section reads `unknown` rather than `landed`. Telling someone their accounts
|
|
311
318
|
* are in when nothing said so is the one answer this screen must never give:
|
|
312
319
|
* they would stop looking.
|
|
313
|
-
*
|
|
314
|
-
* `applied` says so when the import ran in a transaction: `false` means the
|
|
315
|
-
* write was undone whole, and the item counts — empty precisely because the
|
|
316
|
-
* rollback emptied them — must not turn sections before the failure back into
|
|
317
|
-
* `landed`. Every section then reads `rolled-back` except the one the failure
|
|
318
|
-
* named, which keeps its `failed` row and the server's own sentence.
|
|
319
320
|
*/
|
|
320
321
|
export const sectionResults = (
|
|
321
322
|
report: RemitImapConfigImportReport,
|
|
@@ -323,6 +324,7 @@ export const sectionResults = (
|
|
|
323
324
|
const failure = writeFailure(report);
|
|
324
325
|
const named = failure?.details?.section;
|
|
325
326
|
const failedSection = named ? asSection(named) : undefined;
|
|
327
|
+
const nothingLanded = !report.applied;
|
|
326
328
|
const counts = new Map<ImportSection, number>();
|
|
327
329
|
for (const item of report.items) {
|
|
328
330
|
if (asVerdict(item.verdict) === "rejected") continue;
|
|
@@ -331,31 +333,20 @@ export const sectionResults = (
|
|
|
331
333
|
counts.set(section, (counts.get(section) ?? 0) + 1);
|
|
332
334
|
}
|
|
333
335
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
const everySection = (state: SectionOutcome, detail: string) =>
|
|
337
|
+
IMPORT_SECTION_ORDER.map((section) => ({
|
|
336
338
|
section,
|
|
337
339
|
title: SECTION_TITLES[section],
|
|
338
|
-
state
|
|
339
|
-
|
|
340
|
-
? ("failed" as const)
|
|
341
|
-
: ("rolled-back" as const),
|
|
342
|
-
detail:
|
|
343
|
-
section === failedSection
|
|
344
|
-
? (failure.message ?? "This section could not be written.")
|
|
345
|
-
: "Nothing was written — the import ran as one transaction and was undone.",
|
|
340
|
+
state,
|
|
341
|
+
detail,
|
|
346
342
|
}));
|
|
347
|
-
}
|
|
348
343
|
|
|
349
344
|
if (!failedSection) {
|
|
345
|
+
if (nothingLanded) return everySection("not-landed", NOT_LANDED_DETAIL);
|
|
350
346
|
const detail = named
|
|
351
347
|
? `The import stopped in "${named}", which this instance does not recognise, so it cannot say whether this section was written.`
|
|
352
348
|
: "The import stopped without naming where, so it cannot say whether this section was written. Open Settings to see what is here.";
|
|
353
|
-
return
|
|
354
|
-
section,
|
|
355
|
-
title: SECTION_TITLES[section],
|
|
356
|
-
state: "unknown" as const,
|
|
357
|
-
detail,
|
|
358
|
-
}));
|
|
349
|
+
return everySection("unknown", detail);
|
|
359
350
|
}
|
|
360
351
|
|
|
361
352
|
const failedAt = IMPORT_SECTION_ORDER.indexOf(failedSection);
|
|
@@ -378,6 +369,14 @@ export const sectionResults = (
|
|
|
378
369
|
detail: "Not attempted — the import stopped before this section.",
|
|
379
370
|
};
|
|
380
371
|
}
|
|
372
|
+
if (nothingLanded) {
|
|
373
|
+
return {
|
|
374
|
+
section,
|
|
375
|
+
title,
|
|
376
|
+
state: "not-landed" as const,
|
|
377
|
+
detail: NOT_LANDED_DETAIL,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
381
380
|
const written = counts.get(section) ?? 0;
|
|
382
381
|
return {
|
|
383
382
|
section,
|