Component contract
importKind: ImportKind— discriminator: "address-book" | "guest-list" | "salesforce-campaign" | other; drives field schemafields: FieldDef[]— target schema for mapping; passed through to ui-csv-import-previewonParseFile: (file) => Promise<ParsedCsv>— parent-supplied parser; returns headers + rowsonCommit: (validRows, idempotencyKey) => Promise<CommitJobRef>— kicks off the async job; returns job reference for the trackeronCancelJob?: (jobId) => PromisefetchJobStatus: (jobId) => Promise<JobStatus>— for the tracker phaseidempotencyKeyPrefix: string— e.g., "address-book-import-", "guest-list-import-"maxFileSizeBytes?: number— upload cap; passed to ui-file-uploadermaxPreviewRows?: 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 + parseui-csv-import-preview— phase 2: header→field mapping + per-row validation + commit confirmationui-async-job-tracker— phase 3: monitor commit progress until terminal stateui-toast— phase transitions + error notificationsui-stepper— visual phase indicator (Upload → Preview → Importing → Done) with URL state per the SPA mandate
Interaction surface
-
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.
-
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.
-
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).
-
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.
-
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-test | Where | Purpose |
|---|---|---|
ui-csv-import-flow | Outer wrapper | data-import-kind + data-phase attrs |
ui-csv-import-flow-stepper | Top phase indicator | ui-stepper instance |
ui-csv-import-flow-uploader | Phase 1 surface | ui-file-uploader instance |
ui-csv-import-flow-preview | Phase 2 surface | ui-csv-import-preview instance |
ui-csv-import-flow-tracker | Phase 3 surface | ui-async-job-tracker instance |
ui-csv-import-flow-results | Phase 4 panel | imported + failed + error-report |
ui-csv-import-flow-error-report | Download CTA | Visible only when failed > 0 |
ui-csv-import-flow-start-new | Phase 4 CTA | Resets to phase 1 with new idempotency key |
ui-csv-import-flow-back-prompt | Backwards-from-importing prompt | Visible 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
| Branch | importKind |
|---|---|
| EF-005 address-book-groups | address-book |
| EF-044 group-invite-and-upload | guest-list |
| EF-045 upload-template | guest-list (template-mode) |
| EF-093 salesforce-campaign-import-export | salesforce-campaign |