diff --git a/frontend/src/lib/components/ImportWizardSteps.svelte b/frontend/src/lib/components/ImportWizardSteps.svelte
index dd86aecebc..5712ede180 100644
--- a/frontend/src/lib/components/ImportWizardSteps.svelte
+++ b/frontend/src/lib/components/ImportWizardSteps.svelte
@@ -17,9 +17,17 @@
step: WizardStep
/** Whether this import has a setup step at all — most projects do not. */
hasSetup?: boolean
+ /**
+ * The lowest step still worth returning to. Defaults to the first. The page raises it
+ * past the import once that import has landed and the run behind it can no longer be
+ * recovered — after a reload on the setup step, where the executor was in memory and
+ * the parking a clean finish cleared. Step 3 would otherwise mount with nothing to
+ * resume and offer to run the whole bundle again.
+ */
+ lowestStep?: number
}
- let { step, hasSetup = false }: Props = $props()
+ let { step, hasSetup = false, lowestStep = 1 }: Props = $props()
// Most projects ship no data table migrations, so the wizard is three steps and
// says so. A fourth appears only once there is something to configure.
@@ -32,6 +40,10 @@
// skip ahead, since each step decides what the next one asks.
function onStepClick(index: number) {
if (index >= step - 1) return
+ if (index + 1 < lowestStep) {
+ sendUserToast('The project is already imported. There is nothing to go back to.', true)
+ return
+ }
// An import in flight owns the page: stepping back unmounts the step that is
// awaiting the migration review, which would leave the run with no controls
// and no way to resolve.
diff --git a/frontend/src/lib/importWizard/abandon.test.ts b/frontend/src/lib/importWizard/abandon.test.ts
index 8129888e7f..942788c66f 100644
--- a/frontend/src/lib/importWizard/abandon.test.ts
+++ b/frontend/src/lib/importWizard/abandon.test.ts
@@ -104,6 +104,19 @@ describe('abandoning mid-import', () => {
expect(resumableImport('calendly', 'ws-a')).toBe(true)
})
+ it('stops the migrate row spinning when it is abandoned mid-migration', async () => {
+ const run = new ImportExecution(PLAN, deps)
+ // Abandon once the write loop has started, which is where `onMigrationsStart` has
+ // already flipped the row to running in a real run.
+ hooks.afterFirstItem = () => {
+ run.abandon()
+ }
+ await run.run()
+ // A row left on `running` reads as work still in progress on a run that has stopped.
+ expect(run.tasks.some((t) => t.status === 'running')).toBe(false)
+ expect(run.done).toBe(false)
+ })
+
it('clears it on a clean finish, so a later import reaches its own create', async () => {
parkImport({ slug: 'calendly', workspaceId: 'ws-a' })
const run = new ImportExecution(PLAN, deps)
diff --git a/frontend/src/routes/(root)/(logged)/projects/import/+page@(root).svelte b/frontend/src/routes/(root)/(logged)/projects/import/+page@(root).svelte
index 0486e5259c..a1bc2ab970 100644
--- a/frontend/src/routes/(root)/(logged)/projects/import/+page@(root).svelte
+++ b/frontend/src/routes/(root)/(logged)/projects/import/+page@(root).svelte
@@ -328,7 +328,15 @@
{/if}
{/snippet}
-