fix(ui): lock structural package controls while an import runs

Structural configuration edits (URL, source mode, target, collision
strategy) clear the rendered preview, and the import status panels live
inside the preview section — so an edit landing mid-import unmounted the
running import's progress panel and its eventual failure state. Disable
the structural controls while an import is pending, mirroring the preview
lockout and its inline explanation; the name and pause fields stay
editable since they never unmount the preview. Component tests assert the
lockout while pending and the unlock after settle.
This commit is contained in:
Devin Foley committed 2026-07-30 13:32:53 -07:00
1 parent 6cffe2f085
commit a4ab7fbdf3
2 files changed
+30 -5

No files matched your search

+20 -3
View File
@@ -396,10 +396,22 @@ describe("CompanyImport", () => {
expect(container.textContent).toContain("Uploading and importing");
expect(container.textContent).toContain("Keep this page open.");
// While the import runs, previewing is locked (and explained) so a
// concurrent preview cannot replace the plan the import started from.
// While the import runs, previewing and the structural package/settings
// controls are locked (and explained), so nothing can replace or unmount
// the plan the import started from.
expect(findButton((text) => text === "Preview import")?.disabled).toBe(true);
expect(container.textContent).toContain("Import in progress — previewing unlocks when it finishes.");
expect(container.textContent).toContain(
"Import in progress — the package and settings unlock when it finishes.",
);
const lockedUrlInput = container.querySelector<HTMLInputElement>(
'input[placeholder="https://github.com/owner/repo/tree/main/company"]',
);
expect(lockedUrlInput?.disabled).toBe(true);
const lockedSelects = Array.from(container.querySelectorAll("select")).filter(
(select) => select.value === "new" || select.value === "rename",
);
expect(lockedSelects).toHaveLength(2);
expect(lockedSelects.every((select) => select.disabled)).toBe(true);
// Config edits while the import is in flight must not detach it from
// the UI: the progress panel keeps reporting it until it settles.
@@ -440,6 +452,11 @@ describe("CompanyImport", () => {
expect(container.textContent).not.toContain("Import failed:");
expect(findButton((text) => text.startsWith("Import 3 file"))).toBeTruthy();
expect(findButton((text) => text === "Preview import")?.disabled).toBe(false);
expect(
container.querySelector<HTMLInputElement>(
'input[placeholder="https://github.com/owner/repo/tree/main/company"]',
)?.disabled,
).toBe(false);
// The pause toggle feeds the request payload too: after another failed
// attempt, toggling it also supersedes the request and clears the panel.
+10 -2
View File
@@ -943,7 +943,9 @@ export function CompanyImport() {
// pending request is never detached: its panel keeps reporting it (and the
// action buttons stay disabled) until it settles. The new-company name and
// pause toggle are set against a rendered preview, so they reset only the
// panels; structural changes also discard the preview itself.
// panels; structural changes also discard the preview itself. Structural
// controls are locked while an import runs, so this can never unmount the
// preview section that hosts a running import's status panels.
function resetMutationState() {
if (!previewMutation.isPending) previewMutation.reset();
if (!importMutation.isPending) importMutation.reset();
@@ -1336,7 +1338,9 @@ export function CompanyImport() {
sourceMode === key
? "border-foreground bg-accent"
: "border-border hover:bg-accent/50",
importMutation.isPending && "cursor-not-allowed opacity-50",
)}
disabled={importMutation.isPending}
onClick={() => {
setSourceMode(key);
resetImportFlowState();
@@ -1364,6 +1368,7 @@ export function CompanyImport() {
size="sm"
variant="outline"
onClick={() => packageInputRef.current?.click()}
disabled={importMutation.isPending}
>
Choose zip
</Button>
@@ -1405,6 +1410,7 @@ export function CompanyImport() {
type="text"
value={importUrl}
placeholder="https://github.com/owner/repo/tree/main/company"
disabled={importMutation.isPending}
onChange={(e) => {
setImportUrl(e.target.value);
resetImportFlowState();
@@ -1417,6 +1423,7 @@ export function CompanyImport() {
<select
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
value={targetMode}
disabled={importMutation.isPending}
onChange={(e) => {
setTargetMode(e.target.value as "existing" | "new");
resetImportFlowState();
@@ -1454,6 +1461,7 @@ export function CompanyImport() {
<select
className="w-full rounded-md border border-border bg-transparent px-2.5 py-1.5 text-sm outline-none"
value={collisionStrategy}
disabled={importMutation.isPending}
onChange={(e) => {
setCollisionStrategy(e.target.value as CompanyPortabilityCollisionStrategy);
resetImportFlowState();
@@ -1483,7 +1491,7 @@ export function CompanyImport() {
)}
{importMutation.isPending && (
<span className="text-xs text-muted-foreground">
Import in progress — previewing unlocks when it finishes.
Import in progress — the package and settings unlock when it finishes.
</span>
)}
{inlineImportBlocked && (