Skip to content

feat(drive-integration): async import runs rearchitecture [INTEG-4526]#11092

Open
david-shibley-contentful wants to merge 6 commits into
masterfrom
feat/drive-integration-async-runs-INTEG-4526
Open

feat(drive-integration): async import runs rearchitecture [INTEG-4526]#11092
david-shibley-contentful wants to merge 6 commits into
masterfrom
feat/drive-integration-async-runs-INTEG-4526

Conversation

@david-shibley-contentful

@david-shibley-contentful david-shibley-contentful commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

INTEG-4526

Summary

  • Rearchitects the import flow from synchronous (20-min blocking spinner) to fully async: wizard fires off the agent run, saves to localStorage, redirects immediately to a new Runs page
  • New Runs page becomes the app home screen — lists all imports with live status badges (Running / Needs Review / Completed / Failed / Expired), polls every 10s while any run is in-flight, supports multiple concurrent runs
  • ReviewPage is now reachable directly from the Runs page via a Review CTA (no longer locked inside the wizard flow); entry creation stays synchronous
Screenshot 2026-07-16 at 3 37 09 PM

Key files

  • src/types/runs.tsRunRecord, DisplayStatus, RunWithStatus, AppView types
  • src/hooks/useRunStorage.ts — localStorage-backed run history (max 50, scoped by space+env)
  • src/hooks/useRunsPolling.ts — parallel Promise.all polling, 10s interval while running
  • src/services/workflowService.tsresumeAndPollWorkflow extracted from the hook
  • src/locations/Page/components/runs/RunsPage + RunRow components
  • src/locations/Page/Page.tsxAppView discriminated union state machine replaces binary toggle
  • src/locations/Page/components/mainpage/ModalOrchestrator.tsx — fire-and-forget wizard exit

Test plan

  • 104 unit/component tests pass (npm test across all modified files)
  • TypeScript compiles clean (tsc --noEmit)
  • Vite production build succeeds (npm run build)
  • Start an import → wizard closes immediately, Runs page shows "Running" status
  • Wait for AI analysis → run transitions to "Needs Review", Review CTA appears
  • Click Review → ReviewPage loads with correct mapping payload
  • Create entries → run transitions to "Completed" with entry links
  • Start two concurrent imports → both rows appear independently with correct statuses
  • Dismiss a failed/expired run → row removed from list and localStorage
  • Clear localStorage → empty state with "Start your first import" CTA

Generated with Claude Code

@david-shibley-contentful
david-shibley-contentful requested review from a team as code owners July 16, 2026 21:10
@david-shibley-contentful
david-shibley-contentful force-pushed the feat/drive-integration-async-runs-INTEG-4526 branch from 3ca6d78 to 4bc5d43 Compare July 16, 2026 21:52
david-shibley-contentful and others added 3 commits July 16, 2026 16:00
Rearchitects the drive-integration import flow from synchronous (20-min
blocking spinner) to fully async. Import wizard fires off the agent run,
saves a RunRecord to localStorage, then immediately redirects to a new
Runs page. The Runs page polls agentRun status every 10s for in-flight
imports and supports multiple concurrent runs. ReviewPage is now
reachable directly from the Runs page rather than only from the wizard
flow.

Key changes:
- New RunsPage (home screen) + RunRow components with live status badges
- useRunStorage hook (localStorage-backed RunRecord[] with 50-record cap)
- useRunsPolling hook (parallel Promise.all, 10s interval while running)
- workflowService.ts extracted from useWorkflowAgent (resumeAndPollWorkflow)
- Page.tsx rewritten around AppView discriminated union state machine
- ModalOrchestrator: fire-and-forget startWorkflow → addRun → onRunStarted
- 104 tests passing across all new and modified files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…table after import

Three independent useRunStorage instances (Page, RunsPage, ModalOrchestrator) meant
addRun() only updated local React state — RunsPage wouldn't see the new run without
a page refresh. Lift the hook to Page as the single owner and pass runs/addRun/
removeRun/storageError down as props.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@david-shibley-contentful
david-shibley-contentful force-pushed the feat/drive-integration-async-runs-INTEG-4526 branch from 2734bc2 to 2d23ce1 Compare July 16, 2026 22:00
david-shibley-contentful and others added 3 commits July 16, 2026 16:04
…y ESLint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ixtures from branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…E_ENABLE_MOCK_REVIEW_PAYLOAD branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
const handleCancelReview = async (runId?: string) => {
if (runId) {
try {
await resumeAndPollWorkflow(sdk, runId, { cancelled: true });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚔️ This is missing the entryBlockGraph. I think we want it for logging purposes

Comment on lines +212 to +213
onRunStarted={handleRunStarted}
onResetToMain={() => setAppView({ view: 'runs' })}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 These props do the same thing, setAppView({ view: 'runs' })

const handleExitReview = () => {
modalOrchestratorRef.current?.resetFlow();
handleReturnToMainPage();
setAppView({ view: 'runs' });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Same logic as handleRunStarted(), would replace with that

{run.createdEntryIds.map((entryId) => (
<TextLink
key={entryId}
href={`https://app.contentful.com/spaces/${spaceId}/entries/${entryId}`}

@JuliRossi JuliRossi Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Should this use : sdk.hostnames.webapp instead of https://app.contentful.com ?

default:
return 'expired';
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 This could be a map, instead of s switch. Similar to what we do for FAILURE_REASON_MESSAGES

Comment on lines +301 to +312
const runId = await startWorkflow(contentTypeIds, documentSelection);

if (storageError) {
// Storage unavailable — surface error before proceeding
showWorkflowError(
new WorkflowRunError(
'Unable to track this import: browser storage is unavailable or full.',
WorkflowFailureReason.GENERIC
)
);
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Do we want to start the workflow even if we've got no storage? How is the user going to be able to see the run?

onDismiss: (runId: string) => void;
}

function formatDate(iso: string): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⛏️ This manual format handling could be avoided if use a library like Dayjs

payload: MappingReviewSuspendPayload;
runId?: string;
} | null>(null);
const [appView, setAppView] = useState<AppView>({ view: 'runs' });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 Could we move runId outside AppView type? It makes AppView type a bit inconsistent. This could also make simpler with UseEffect in line 41

Comment on lines +42 to +44
if (appView.view !== 'review') {
setPendingReviewPayload(null);
return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (appView.view !== 'review') {
setPendingReviewPayload(null);
return;
if (appView.view !== 'review') {
setPendingReviewPayload(null);
setIsLoadingReviewPayload(false);
return;

errorMessage?: string;
}

export type AppView = { view: 'runs' } | { view: 'import' } | { view: 'review'; runId: string };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 runs, import and review could be an enum

@JuliRossi JuliRossi Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Should we allow the user to a way clear the runs they have on the page? So they can free space on the localstorage as well

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also if a run stuck in 'running' has no action button. The user can't dismiss it, can't cancel it, can't do anything. It just sits there with a spinner.

@JuliRossi JuliRossi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! They way I review PRs:
⛏️ Nitpicks, non blockers
💭 / 🤔 Highly suggested, but non blockers
❓ Question, may be blocking or not depending on the answer
⚔️ Blockers

After testing it locally: There seems to be a bug with the document title that keep it as Untitled document instead of the actual title. We are also showing the entry id instead of the name in the RunPage
Captura de pantalla 2026-07-20 a las 1 07 36 p  m

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 I think we no longer need the loading step for the ModalOrchestrator

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants