mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
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:
1 parent
6cffe2f085
commit
a4ab7fbdf3
2 files changed
+30
-5
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in new issue
Block a user