← All stories

COMPONENT (Tier 3) · ui-csv-import-flow

ui-csv-import-flow

Component · Tier 3 (compound) Recurrence: 4 stories Composes: ui-file-uploader, ui-csv-import-preview, ui-async-job-tracker

The full upload-preview-commit-monitor sequence wrapped as a single guided flow. Three handoff seams (upload → preview, preview → commit, commit → tracker) become one component contract. Inherits idempotency-key for retry-safe commits, partial-failure semantics, and post-commit error-report download.

Component contract

  • importKind: ImportKind — discriminator: "address-book" | "guest-list" | "salesforce-campaign" | other; drives field schema
  • fields: FieldDef[] — target schema for mapping; passed through to ui-csv-import-preview
  • onParseFile: (file) => Promise<ParsedCsv> — parent-supplied parser; returns headers + rows
  • onCommit: (validRows, idempotencyKey) => Promise<CommitJobRef> — kicks off the async job; returns job reference for the tracker
  • onCancelJob?: (jobId) => Promise
  • fetchJobStatus: (jobId) => Promise<JobStatus> — for the tracker phase
  • idempotencyKeyPrefix: string — e.g., "address-book-import-", "guest-list-import-"
  • maxFileSizeBytes?: number — upload cap; passed to ui-file-uploader
  • maxPreviewRows?: number — preview cap; passed to ui-csv-import-preview (commit always sends ALL rows, not just preview)
  • onComplete?: (result) => void — fires when the async job reaches a terminal state

Composition

  • ui-file-uploader — phase 1: upload + parse
  • ui-csv-import-preview — phase 2: header→field mapping + per-row validation + commit confirmation
  • ui-async-job-tracker — phase 3: monitor commit progress until terminal state
  • ui-toast — phase transitions + error notifications
  • ui-stepper — visual phase indicator (Upload → Preview → Importing → Done) with URL state per the SPA mandate

Interaction surface

  1. Phase 1: Upload.

    ui-file-uploader visible. User selects/drops a CSV. Parser runs (parent-owned). On success, transitions to phase 2 with parsed headers + rows. On failure, ui-toast surfaces parse error; user re-uploads.

  2. Phase 2: Preview & map.

    ui-csv-import-preview takes over. User confirms or adjusts header→field mapping, sees per-row validation, reviews commit summary. Click Commit → confirmation modal (per ui-csv-import-preview contract) → transitions to phase 3 with idempotency key.

  3. Phase 3: Importing (async job tracker).

    ui-async-job-tracker monitors. Pending → running → terminal state. Progress updates per ui-async-job-tracker's poll cadence (2s default). User can cancel mid-flight (per onCancelJob prop).

  4. Phase 4: Done — results panel.

    After terminal state: imported count + failed count + download error report (when failures exist). "Start new import" CTA returns to phase 1 with fresh idempotency key.

  5. Phase navigation: backwards is allowed; forwards requires phase completion.

    Inherits ui-stepper contract. URL state syncs (?phase=upload|preview|importing|done). Backwards from preview to upload re-enters with prior file cleared (avoid stale state). Backwards from importing prompts for cancel-job confirmation.

Failure modes

URL refresh mid-import recovers correctly

Trigger: user is in phase 3 (importing), refreshes browser.

?phase=importing&jobId=X URL state restores the tracker on reload. ui-async-job-tracker resumes polling. No duplicate commit fires (idempotency-key is in the URL or stored per session). Harness: phase=3 + refresh, tracker resumes for the same jobId.

Backwards-from-importing prompts for cancel-job

Trigger: user navigates back from phase 3 to phase 2.

Confirmation modal: "Cancel the import in progress?" Yes → onCancelJob fires + transitions to phase 2 with prior preview state. No → stays in phase 3. Harness: navigate back from phase 3, prompt visible.

Idempotent commit retry

Trigger: phase 2 commit submit is fired; network blip; user retries.

Same idempotency-key sent on both attempts. Server dedupes. Only one job created. Harness: 2 commit POSTs in 100ms, exactly 1 server-side job record.

Large file commits ALL rows (not just preview)

Trigger: 10,000-row CSV with maxPreviewRows=50.

Preview shows 50 rows; commit sends all 10,000 (per ui-csv-import-preview contract). Harness: large CSV, preview has 50, commit's body has 10,000.

Partial-failure result distinguishable from total-success

Trigger: 100-row commit, 3 fail server-side, 97 succeed.

Phase 4 results panel surfaces both counts. Error report download CTA visible. ui-status-pill shows "Partial" tone (not just green "Done"). Harness: stub partial result, distinct UI vs total-success.

Phase 3 stuck-job alert

Trigger: import job sits queued for 5+ min.

Inherits ui-async-job-tracker's stuck-banner. Surfaces in phase 3 UI with support contact. Harness: stub job stuck, banner visible inline within phase 3.

Permission-gated commit

Trigger: user without import-write permission tries to commit.

Commit button disabled in phase 2; if bypassed, server returns 403. UI inherits the permission-gated catalog pattern. Harness: stub no-permission, commit disabled + 403 on direct API.

Cross-tenant import attempt

Trigger: forged tenant_id in commit payload.

Server returns 404 (anti-probing). No job created. Harness: forge tenant context, 404, no job row.

Audit row per import phase

Trigger: complete a successful import.

Audit log gets rows for: import_started (phase 1 → 2 transition with file metadata), import_committed (phase 2 → 3 transition with row counts), import_completed (phase 3 → 4 with results). Three rows per import. Harness: complete import, 3 audit rows in order.

Start-new-import resets idempotency key

Trigger: complete an import, click "Start new import" CTA.

Returns to phase 1. New idempotency key generated. Old jobId cleared from URL. Harness: complete + start-new, new idempotency key in next commit.

Accessibility

  • Inherits ui-file-uploader, ui-csv-import-preview, ui-async-job-tracker, ui-stepper a11y contracts.
  • Phase indicator announces phase changes via aria-live=polite.
  • Phase 4 results announce via role=status.
  • Backwards-navigation prompt inherits ui-modal focus-trap.
  • axe-clean ≥ serious across all 4 phases.

Stable test attributes

data-testWherePurpose
ui-csv-import-flowOuter wrapperdata-import-kind + data-phase attrs
ui-csv-import-flow-stepperTop phase indicatorui-stepper instance
ui-csv-import-flow-uploaderPhase 1 surfaceui-file-uploader instance
ui-csv-import-flow-previewPhase 2 surfaceui-csv-import-preview instance
ui-csv-import-flow-trackerPhase 3 surfaceui-async-job-tracker instance
ui-csv-import-flow-resultsPhase 4 panelimported + failed + error-report
ui-csv-import-flow-error-reportDownload CTAVisible only when failed > 0
ui-csv-import-flow-start-newPhase 4 CTAResets to phase 1 with new idempotency key
ui-csv-import-flow-back-promptBackwards-from-importing promptVisible when navigating back from phase 3

Agent test plan

Probe list
- phase-1-uploader-visible: data-phase=upload, ui-file-uploader visible
- phase-transition-to-preview: upload + parse success → phase=preview
- phase-transition-to-importing: commit + confirm → phase=importing
- phase-transition-to-done: terminal state → phase=done
- url-refresh-mid-import-recovers: phase=importing + refresh, tracker resumes
- backwards-from-importing-prompts: navigate back, ui-csv-import-flow-back-prompt visible
- idempotent-commit: 2 POSTs same key, 1 job created
- large-file-commits-all: 10000 rows / preview 50, commit body has 10000
- partial-failure-distinguishable: stub 97 succ + 3 fail, error-report visible
- phase-3-stuck-banner: stub job stuck 5min, banner visible
- permission-gated-commit-disabled: stub no permission, commit disabled
- cross-tenant-404: forged tenant, 404
- audit-row-per-phase: complete import, 3 audit rows
- start-new-resets-key: start-new, new idempotency key
- aria-live-phase-changes: phase indicator announces changes
- axe-clean-each-phase: 4 phases pass axe ≥ serious

Current consumers

BranchimportKind
EF-005 address-book-groupsaddress-book
EF-044 group-invite-and-uploadguest-list
EF-045 upload-templateguest-list (template-mode)
EF-093 salesforce-campaign-import-exportsalesforce-campaign