@remit/ui 0.0.71 → 0.0.73
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/folder-tree-picker.create.test.ts +150 -20
- package/src/components/folder-tree-picker.render.test.ts +3 -2
- package/src/components/folder-tree-picker.stories.tsx +41 -0
- package/src/components/folder-tree-picker.tsx +20 -2
- package/src/components/selection-wizard.render.test.ts +31 -0
- package/src/lib/wizard-steps.test.ts +22 -0
- package/src/lib/wizard-steps.ts +20 -1
package/package.json
CHANGED
|
@@ -158,6 +158,31 @@ const typeName = async (value: string) => {
|
|
|
158
158
|
});
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
+
const filterField = (): HTMLInputElement | null =>
|
|
162
|
+
container.querySelector('input[type="search"]');
|
|
163
|
+
|
|
164
|
+
const typeFilter = async (value: string) => {
|
|
165
|
+
const input = filterField();
|
|
166
|
+
assert.ok(input, "filter field not rendered");
|
|
167
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
168
|
+
dom.window.HTMLInputElement.prototype,
|
|
169
|
+
"value",
|
|
170
|
+
)?.set;
|
|
171
|
+
await act(async () => {
|
|
172
|
+
setter?.call(input, value);
|
|
173
|
+
input.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const press = async (target: Element | null | undefined, key: string) => {
|
|
178
|
+
assert.ok(target, "control not rendered");
|
|
179
|
+
await act(async () => {
|
|
180
|
+
target.dispatchEvent(
|
|
181
|
+
new dom.window.KeyboardEvent("keydown", { key, bubbles: true }),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
161
186
|
const open = async (...labels: string[]) => {
|
|
162
187
|
for (const label of labels) {
|
|
163
188
|
await click(byAriaLabel(`Move to ${label}`));
|
|
@@ -529,32 +554,131 @@ describe("create wait", () => {
|
|
|
529
554
|
});
|
|
530
555
|
});
|
|
531
556
|
|
|
532
|
-
describe("
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
assert.ok(input);
|
|
539
|
-
const setter = Object.getOwnPropertyDescriptor(
|
|
540
|
-
dom.window.HTMLInputElement.prototype,
|
|
541
|
-
"value",
|
|
542
|
-
)?.set;
|
|
543
|
-
await act(async () => {
|
|
544
|
-
setter?.call(input, value);
|
|
545
|
-
input.dispatchEvent(new dom.window.Event("input", { bubbles: true }));
|
|
557
|
+
describe("the form's keys stay in the form", () => {
|
|
558
|
+
it("takes a space in the name without acting on the tree", async () => {
|
|
559
|
+
const selected: string[] = [];
|
|
560
|
+
await mount({
|
|
561
|
+
onSelect: (id) => selected.push(id),
|
|
562
|
+
onCreateFolder: resolving,
|
|
546
563
|
});
|
|
564
|
+
await open("Travel");
|
|
565
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
566
|
+
await press(nameField(), " ");
|
|
567
|
+
assert.deepEqual(selected, ["travel"]);
|
|
568
|
+
assert.ok(byAriaLabel("Move to Hotels"));
|
|
569
|
+
assert.ok(createBlockOf("travel")?.querySelector("input"));
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("closes the form on Escape and leaves the picker open", async () => {
|
|
573
|
+
let cancelled = 0;
|
|
574
|
+
await mount({
|
|
575
|
+
onCancel: () => {
|
|
576
|
+
cancelled += 1;
|
|
577
|
+
},
|
|
578
|
+
onCreateFolder: resolving,
|
|
579
|
+
});
|
|
580
|
+
await open("Travel");
|
|
581
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
582
|
+
await press(nameField(), "Escape");
|
|
583
|
+
assert.equal(nameField(), null);
|
|
584
|
+
assert.equal(cancelled, 0);
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
describe("a draft outlives the rows under it", () => {
|
|
589
|
+
const startDraft = async (props: PickerProps) => {
|
|
590
|
+
await mount(props);
|
|
591
|
+
await open("Travel");
|
|
592
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
593
|
+
await typeName("Car hire");
|
|
547
594
|
};
|
|
548
595
|
|
|
549
|
-
|
|
550
|
-
|
|
596
|
+
it("keeps the form and the typed name when another folder opens", async () => {
|
|
597
|
+
await startDraft({ onCreateFolder: resolving });
|
|
598
|
+
await open("Archive");
|
|
599
|
+
assert.equal(nameField()?.value, "Car hire");
|
|
600
|
+
assert.ok(createBlockOf("top")?.querySelector("input"));
|
|
601
|
+
assert.match(container.textContent ?? "", /Inside\s*Travel/);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("keeps the form when its own folder is closed again", async () => {
|
|
605
|
+
await startDraft({ onCreateFolder: resolving });
|
|
606
|
+
await open("Travel");
|
|
607
|
+
assert.equal(byAriaLabel("Move to Hotels"), null);
|
|
608
|
+
assert.equal(nameField()?.value, "Car hire");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("keeps the form when the filter empties the list", async () => {
|
|
612
|
+
await startDraft({ onCreateFolder: resolving });
|
|
613
|
+
await typeFilter("zzz");
|
|
614
|
+
assert.match(container.textContent ?? "", /No folders match "zzz"/);
|
|
615
|
+
assert.equal(nameField()?.value, "Car hire");
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it("puts the form back among the children when its folder opens again", async () => {
|
|
619
|
+
await startDraft({ onCreateFolder: resolving });
|
|
620
|
+
await open("Archive");
|
|
621
|
+
await open("Travel");
|
|
622
|
+
assert.ok(createBlockOf("travel")?.querySelector("input"));
|
|
623
|
+
assert.equal(createBlockOf("top")?.querySelector("input"), null);
|
|
624
|
+
assert.equal(nameField()?.value, "Car hire");
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it("states a failure that lands after the folder it was opened in closed", async () => {
|
|
628
|
+
let fail: ((error: Error) => void) | undefined;
|
|
629
|
+
await startDraft({
|
|
630
|
+
onCreateFolder: () =>
|
|
631
|
+
new Promise<FolderTreeNode>((_resolve, reject) => {
|
|
632
|
+
fail = reject;
|
|
633
|
+
}),
|
|
634
|
+
});
|
|
635
|
+
await click(byText("Create folder"));
|
|
636
|
+
await open("Archive");
|
|
637
|
+
assert.ok(byText("Creating folder…"), "the wait went off screen");
|
|
551
638
|
await act(async () => {
|
|
552
|
-
|
|
553
|
-
new dom.window.KeyboardEvent("keydown", { key, bubbles: true }),
|
|
554
|
-
);
|
|
639
|
+
fail?.(new Error("The mail server refused that name."));
|
|
555
640
|
});
|
|
556
|
-
|
|
641
|
+
assert.equal(
|
|
642
|
+
container.querySelector('[role="alert"]')?.textContent,
|
|
643
|
+
"The mail server refused that name.",
|
|
644
|
+
);
|
|
645
|
+
assert.equal(nameField()?.value, "Car hire");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("keeps the wait on screen when the filter clears the list", async () => {
|
|
649
|
+
await startDraft({
|
|
650
|
+
onCreateFolder: () => new Promise<FolderTreeNode>(() => undefined),
|
|
651
|
+
});
|
|
652
|
+
await click(byText("Create folder"));
|
|
653
|
+
await typeFilter("zzz");
|
|
654
|
+
assert.ok(byText("Creating folder…"), "the wait went off screen");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("selects what comes back from a form the list moved under", async () => {
|
|
658
|
+
const selected: string[] = [];
|
|
659
|
+
let confirm: ((folder: FolderTreeNode) => void) | undefined;
|
|
660
|
+
await mount({
|
|
661
|
+
onSelect: (id) => selected.push(id),
|
|
662
|
+
onCreateFolder: () =>
|
|
663
|
+
new Promise<FolderTreeNode>((resolve) => {
|
|
664
|
+
confirm = resolve;
|
|
665
|
+
}),
|
|
666
|
+
});
|
|
667
|
+
await open("Travel");
|
|
668
|
+
await click(byAriaLabel("New folder inside Travel"));
|
|
669
|
+
await typeName("Car hire");
|
|
670
|
+
await click(byText("Create folder"));
|
|
671
|
+
await open("Archive");
|
|
672
|
+
assert.ok(byText("Creating folder…"), "the wait went off screen");
|
|
673
|
+
await act(async () => {
|
|
674
|
+
confirm?.(created("Car hire", "Travel"));
|
|
675
|
+
});
|
|
676
|
+
assert.deepEqual(selected, ["travel", "archive", "made"]);
|
|
677
|
+
assert.equal(nameField(), null);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
557
680
|
|
|
681
|
+
describe("filter and keyboard", () => {
|
|
558
682
|
const focused = (): string | null =>
|
|
559
683
|
dom.window.document.activeElement?.getAttribute("aria-label") ?? null;
|
|
560
684
|
|
|
@@ -587,6 +711,12 @@ describe("filter and keyboard", () => {
|
|
|
587
711
|
assert.match(container.textContent ?? "", /No folders match "zzz"/);
|
|
588
712
|
});
|
|
589
713
|
|
|
714
|
+
it("says the list is empty rather than blaming the filter", async () => {
|
|
715
|
+
await mount({ folders: [] });
|
|
716
|
+
await typeFilter("zzz");
|
|
717
|
+
assert.match(container.textContent ?? "", /No folders to show/);
|
|
718
|
+
});
|
|
719
|
+
|
|
590
720
|
it("picks and opens the focused row on Enter", async () => {
|
|
591
721
|
const selected: string[] = [];
|
|
592
722
|
await mount({ onSelect: (id) => selected.push(id) });
|
|
@@ -93,9 +93,10 @@ describe("FolderTreePicker render", () => {
|
|
|
93
93
|
);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it("
|
|
96
|
+
it("says the list is empty when there is nothing to list", () => {
|
|
97
97
|
const html = render({ folders: [] });
|
|
98
|
-
assert.match(html, /No folders
|
|
98
|
+
assert.match(html, /No folders to show/);
|
|
99
|
+
assert.doesNotMatch(html, /No folders match/);
|
|
99
100
|
});
|
|
100
101
|
|
|
101
102
|
it("applies caller-supplied labels", () => {
|
|
@@ -160,6 +160,7 @@ export const LongList: Story = {
|
|
|
160
160
|
render: () => <Picker options={longFolders} />,
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
+
/** An account with nothing to list: the message states that, not a filter. */
|
|
163
164
|
export const Empty: Story = {
|
|
164
165
|
name: "No folders",
|
|
165
166
|
render: () => <Picker options={[]} />,
|
|
@@ -309,6 +310,46 @@ export const CreateFailed: Story = {
|
|
|
309
310
|
},
|
|
310
311
|
};
|
|
311
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Looking somewhere else does not throw the draft away: opening another folder
|
|
315
|
+
* closes the branch the form was in, and the form is pinned above the tree with
|
|
316
|
+
* the typed name and the folder it will be made in.
|
|
317
|
+
*/
|
|
318
|
+
export const CreateWhileTheListMoves: Story = {
|
|
319
|
+
name: "Create — kept while the list moves",
|
|
320
|
+
render: () => <Picker />,
|
|
321
|
+
play: async ({ canvasElement }) => {
|
|
322
|
+
await expand(canvasElement, "Travel");
|
|
323
|
+
clickAriaLabel(canvasElement, "New folder inside Travel");
|
|
324
|
+
await tick();
|
|
325
|
+
typeFolderName(canvasElement, "Car hire");
|
|
326
|
+
await tick();
|
|
327
|
+
await expand(canvasElement, "Finance");
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/** A failure is stated where the user is looking, whatever the list has done. */
|
|
332
|
+
export const CreateFailedAfterTheListMoved: Story = {
|
|
333
|
+
name: "Create — failed after the list moved",
|
|
334
|
+
render: () => (
|
|
335
|
+
<Picker
|
|
336
|
+
onCreateFolder={rejects(
|
|
337
|
+
"The mail server refused the folder name. Try another one.",
|
|
338
|
+
)}
|
|
339
|
+
/>
|
|
340
|
+
),
|
|
341
|
+
play: async ({ canvasElement }) => {
|
|
342
|
+
await expand(canvasElement, "Travel");
|
|
343
|
+
clickAriaLabel(canvasElement, "New folder inside Travel");
|
|
344
|
+
await tick();
|
|
345
|
+
typeFolderName(canvasElement, "Car hire");
|
|
346
|
+
await tick();
|
|
347
|
+
clickText(canvasElement, "Create folder");
|
|
348
|
+
await tick();
|
|
349
|
+
await expand(canvasElement, "Finance");
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
312
353
|
const wizardSteps: StepId[] = ["match", "folder", "rule", "review"];
|
|
313
354
|
|
|
314
355
|
function WizardFolderStep() {
|
|
@@ -53,6 +53,8 @@ export interface FolderTreePickerLabels {
|
|
|
53
53
|
/** Suffix announced for an ancestor held on screen by a match below it. */
|
|
54
54
|
contextSuffix?: string;
|
|
55
55
|
emptyMessage?: (query: string) => string;
|
|
56
|
+
/** Shown when there is no folder to list at all, filter or no filter. */
|
|
57
|
+
noFolders?: string;
|
|
56
58
|
/** Accessible label for a selectable row, e.g. `Move to X`. */
|
|
57
59
|
optionLabel?: (label: string) => string;
|
|
58
60
|
newFolder?: string;
|
|
@@ -110,6 +112,7 @@ const defaultLabels: Required<FolderTreePickerLabels> = {
|
|
|
110
112
|
currentTag: "current",
|
|
111
113
|
contextSuffix: "(containing folder)",
|
|
112
114
|
emptyMessage: (query) => `No folders match "${query}"`,
|
|
115
|
+
noFolders: "No folders to show",
|
|
113
116
|
optionLabel: (label) => `Move to ${label}`,
|
|
114
117
|
newFolder: "New folder",
|
|
115
118
|
newSubfolder: (label) => `New folder inside ${label}`,
|
|
@@ -282,6 +285,11 @@ export const FolderTreePicker = ({
|
|
|
282
285
|
|
|
283
286
|
const handleTreeKeyDown = useCallback(
|
|
284
287
|
(event: ReactKeyboardEvent<HTMLElement>) => {
|
|
288
|
+
// The create form sits inside the tree, so what is typed into its name
|
|
289
|
+
// field reaches here too. Only a row's own keys move the tree.
|
|
290
|
+
const source = event.target;
|
|
291
|
+
if (!(source instanceof Element)) return;
|
|
292
|
+
if (!source.closest('[role="treeitem"]')) return;
|
|
285
293
|
const move = (next: number) => {
|
|
286
294
|
event.preventDefault();
|
|
287
295
|
roving.current = true;
|
|
@@ -337,6 +345,14 @@ export const FolderTreePicker = ({
|
|
|
337
345
|
[rows, focusedIndex, delimiter, activateRow, setExpanded, onCancel],
|
|
338
346
|
);
|
|
339
347
|
|
|
348
|
+
const draftAnchorOnScreen =
|
|
349
|
+
draft !== null &&
|
|
350
|
+
draft.anchorId !== null &&
|
|
351
|
+
(displayRows?.some(
|
|
352
|
+
(entry) => entry.kind === "create" && entry.parent.id === draft.anchorId,
|
|
353
|
+
) ??
|
|
354
|
+
false);
|
|
355
|
+
|
|
340
356
|
const draftForm = draft && (
|
|
341
357
|
<NewFolderForm
|
|
342
358
|
parentLabel={draft.parentLabel}
|
|
@@ -414,8 +430,10 @@ export const FolderTreePicker = ({
|
|
|
414
430
|
/>
|
|
415
431
|
|
|
416
432
|
{onCreateFolder && (
|
|
433
|
+
// Closing or filtering away the folder a draft was opened in leaves
|
|
434
|
+
// the form here instead of unmounting it mid-type or mid-create.
|
|
417
435
|
<div className="shrink-0 border-b border-line" data-create-anchor="top">
|
|
418
|
-
{draft
|
|
436
|
+
{draft && !draftAnchorOnScreen ? (
|
|
419
437
|
draftForm
|
|
420
438
|
) : (
|
|
421
439
|
<NewFolderAction
|
|
@@ -430,7 +448,7 @@ export const FolderTreePicker = ({
|
|
|
430
448
|
|
|
431
449
|
{rows.length === 0 ? (
|
|
432
450
|
<p className="px-3 py-3 text-sm text-fg-muted" aria-live="polite">
|
|
433
|
-
{text.emptyMessage(query)}
|
|
451
|
+
{folders.length === 0 ? text.noFolders : text.emptyMessage(query)}
|
|
434
452
|
</p>
|
|
435
453
|
) : (
|
|
436
454
|
// A flattened tree: `aria-level` carries the nesting the indentation
|
|
@@ -582,6 +582,25 @@ describe("RunStepBody", () => {
|
|
|
582
582
|
assert.match(html, /Nothing has changed\./);
|
|
583
583
|
});
|
|
584
584
|
|
|
585
|
+
// A poll that could not be read is not a run that never started (#526): the
|
|
586
|
+
// screen keeps the counts it has and says what it cannot see.
|
|
587
|
+
it("keeps a run that is going when its progress could not be read", () => {
|
|
588
|
+
const html = renderToString(
|
|
589
|
+
createElement(RunStepBody, {
|
|
590
|
+
...runProps,
|
|
591
|
+
state: "statusUnknown",
|
|
592
|
+
scope: "once",
|
|
593
|
+
matched: 1284,
|
|
594
|
+
applied: 40,
|
|
595
|
+
}),
|
|
596
|
+
);
|
|
597
|
+
assert.match(html, /progress unknown/);
|
|
598
|
+
assert.match(html, /carries on either way/);
|
|
599
|
+
assert.match(html, /role="progressbar"/);
|
|
600
|
+
assert.doesNotMatch(html, /Nothing has changed/);
|
|
601
|
+
assert.doesNotMatch(html, /never started/);
|
|
602
|
+
});
|
|
603
|
+
|
|
585
604
|
it("says a filter saved with nothing to back-apply is live", () => {
|
|
586
605
|
const html = renderToString(
|
|
587
606
|
createElement(RunStepBody, {
|
|
@@ -620,6 +639,18 @@ describe("RunFooter", () => {
|
|
|
620
639
|
assert.match(html, /Not now/);
|
|
621
640
|
});
|
|
622
641
|
|
|
642
|
+
it("offers another look, not another run, when the progress could not be read", () => {
|
|
643
|
+
const html = renderToString(
|
|
644
|
+
createElement(RunFooter, {
|
|
645
|
+
...runProps,
|
|
646
|
+
state: "statusUnknown",
|
|
647
|
+
scope: "once",
|
|
648
|
+
}),
|
|
649
|
+
);
|
|
650
|
+
assert.match(html, /Check again/);
|
|
651
|
+
assert.match(html, /Close/);
|
|
652
|
+
});
|
|
653
|
+
|
|
623
654
|
it("offers only a way out once there is nothing outstanding", () => {
|
|
624
655
|
const html = renderToString(createElement(RunFooter, runProps));
|
|
625
656
|
assert.match(html, /Done/);
|
|
@@ -477,6 +477,7 @@ describe("runCopy", () => {
|
|
|
477
477
|
"backApplyComplete",
|
|
478
478
|
"backApplyFailed",
|
|
479
479
|
"backApplyStartFailed",
|
|
480
|
+
"statusUnknown",
|
|
480
481
|
"filterSaved",
|
|
481
482
|
"runStopped",
|
|
482
483
|
"commitFailed",
|
|
@@ -539,6 +540,25 @@ describe("runCopy", () => {
|
|
|
539
540
|
assert.equal(started.showProgress, false);
|
|
540
541
|
});
|
|
541
542
|
|
|
543
|
+
it("keeps a run that is going when its progress could not be read", () => {
|
|
544
|
+
// A poll that failed says nothing about the job behind it (#526), so the
|
|
545
|
+
// screen never claims the action never started, and the way out of it is a
|
|
546
|
+
// second look rather than a second run.
|
|
547
|
+
const once = outcome("statusUnknown", "once");
|
|
548
|
+
assert.equal(once.title, "Moving — progress unknown");
|
|
549
|
+
assert.match(once.detail, /carries on either way/);
|
|
550
|
+
assert.doesNotMatch(once.detail, /Nothing has changed/);
|
|
551
|
+
assert.equal(once.tone, "warning");
|
|
552
|
+
assert.equal(once.retryLabel, "Check again");
|
|
553
|
+
assert.equal(once.screenTitle, "Move");
|
|
554
|
+
|
|
555
|
+
const standing = outcome("statusUnknown", "standing");
|
|
556
|
+
assert.match(standing.title, /Rule saved/);
|
|
557
|
+
assert.doesNotMatch(standing.detail, /never started/);
|
|
558
|
+
assert.match(standing.detail, /keeps working on new mail/);
|
|
559
|
+
assert.equal(standing.retryLabel, "Check again");
|
|
560
|
+
});
|
|
561
|
+
|
|
542
562
|
it("says a filter saved with nothing to back-apply is still live", () => {
|
|
543
563
|
const saved = outcome("filterSaved", "standing");
|
|
544
564
|
assert.equal(saved.title, "Filter saved");
|
|
@@ -559,6 +579,8 @@ describe("runCopy", () => {
|
|
|
559
579
|
assert.equal(outcome("backApplyRunning", "standing").showProgress, true);
|
|
560
580
|
assert.equal(outcome("backApplyComplete", "once").showProgress, true);
|
|
561
581
|
assert.equal(outcome("backApplyFailed", "once").showProgress, true);
|
|
582
|
+
// The last counts read are still the last counts read.
|
|
583
|
+
assert.equal(outcome("statusUnknown", "once").showProgress, true);
|
|
562
584
|
});
|
|
563
585
|
|
|
564
586
|
it("names the failure list in the verb's own past tense", () => {
|
package/src/lib/wizard-steps.ts
CHANGED
|
@@ -301,6 +301,7 @@ export type RunState =
|
|
|
301
301
|
| "backApplyComplete"
|
|
302
302
|
| "backApplyFailed"
|
|
303
303
|
| "backApplyStartFailed"
|
|
304
|
+
| "statusUnknown"
|
|
304
305
|
| "filterSaved"
|
|
305
306
|
| "runStopped"
|
|
306
307
|
| "commitFailed";
|
|
@@ -374,7 +375,10 @@ export const runCopy = ({
|
|
|
374
375
|
const { label, present, past } = verbCopy(verb);
|
|
375
376
|
const done = past.toLowerCase();
|
|
376
377
|
const standing = scope === "standing" || scope === "until";
|
|
377
|
-
const inFlight =
|
|
378
|
+
const inFlight =
|
|
379
|
+
state === "saving" ||
|
|
380
|
+
state === "backApplyRunning" ||
|
|
381
|
+
state === "statusUnknown";
|
|
378
382
|
const shared = {
|
|
379
383
|
// A create that failed did not finish, so the header does not say it did.
|
|
380
384
|
screenTitle: inFlight || state === "commitFailed" ? label : "Done",
|
|
@@ -382,6 +386,7 @@ export const runCopy = ({
|
|
|
382
386
|
state === "backApplyRunning" ||
|
|
383
387
|
state === "backApplyComplete" ||
|
|
384
388
|
state === "backApplyFailed" ||
|
|
389
|
+
state === "statusUnknown" ||
|
|
385
390
|
state === "runStopped",
|
|
386
391
|
failureListLabel: `Not ${done}`,
|
|
387
392
|
};
|
|
@@ -407,6 +412,20 @@ export const runCopy = ({
|
|
|
407
412
|
cancelLabel: "Stop the run",
|
|
408
413
|
};
|
|
409
414
|
}
|
|
415
|
+
if (state === "statusUnknown") {
|
|
416
|
+
return {
|
|
417
|
+
...shared,
|
|
418
|
+
title: standing
|
|
419
|
+
? "Rule saved. Its progress over your existing mail is unknown"
|
|
420
|
+
: `${present} — progress unknown`,
|
|
421
|
+
detail: standing
|
|
422
|
+
? "The connection dropped, so the bar shows the last that was read. The pass runs on the mail server, and the rule keeps working on new mail either way. This keeps checking."
|
|
423
|
+
: "The connection dropped, so the bar shows the last that was read. The run is on the mail server and carries on either way. This keeps checking.",
|
|
424
|
+
tone: "warning",
|
|
425
|
+
dismissLabel: "Close",
|
|
426
|
+
retryLabel: "Check again",
|
|
427
|
+
};
|
|
428
|
+
}
|
|
410
429
|
if (state === "backApplyComplete") {
|
|
411
430
|
return {
|
|
412
431
|
...shared,
|