Skip to content

feat(import): import multiple CSV files at once - #481

Merged
appflowy merged 4 commits into
mainfrom
feat/multi-csv-import
Aug 18, 2026
Merged

feat(import): import multiple CSV files at once#481
appflowy merged 4 commits into
mainfrom
feat/multi-csv-import

Conversation

@appflowy

@appflowy appflowy commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Importing CSVs on web was single-file end to end, so bringing in a folder of exports meant repeating the whole dialog flow once per file. The block was entirely client-side — the file input had no multiple, the change handler read files[0], and importCsvAsDatabase handled exactly one file per call. The server endpoint is one task per file by design, so the client loops.

Selecting several CSVs now creates one Grid page per file under the same parent.

Files import one at a time on purpose. The server caps pending import tasks per user (MAXIMUM_IMPORT_PENDING_TASK, 3 by default), so a parallel fan-out would fail every file past the cap with TooManyImportTask. A unit test asserts max-in-flight is 1.

One bad file no longer sinks the batch — its error is reported and the remaining files still import. Behaviour for a single selected file is unchanged, including the existing toast wording and navigating to the new grid.

Along the way:

  • Cancelling. A batch can run for minutes, so the close button now cancels it instead of being disabled, and the abort signal reaches the in-flight upload (axios.put gets the signal), so cancelling stops the transfer immediately rather than at the next file boundary. Backdrop click and Esc still never interrupt an import — only the button, which is labelled "Cancel import" while a batch runs. Pages already imported are kept and reported.
  • Reporting. Per-file progress on the CSV button (2/7), and toasts that name which files failed rather than a single generic error.
  • Escaping. Failure text is interpolated with escapeValue: false, so a file named Q1&Q2.csv is not shown as Q1&Q2.csv (i18next escapes interpolated values by default and toasts render plain text).

Notes for reviewers

  • en.json gains 6 importPanel keys; other locales fall back to English until the translation sync runs.
  • Per-file byte progress is still not wired up — ImportCsvInput.onProgress had no caller before this change either, so a large single CSV shows a spinner without a percentage. Out of scope here.
  • Failed-file lists join with a hardcoded ', '. Locale-aware joining needs Intl.ListFormat keyed off i18n.language, which risks a RangeError on tags in this repo's locale set (e.g. ckb-KU), so it was left alone deliberately.
  • Cancelling still takes one click with no confirmation. If you'd prefer a distinct Cancel affordance or a confirm step, happy to add it.

Checklist

General

  • I've included relevant documentation or comments for the changes introduced.
  • I've tested the changes in multiple environments (e.g., different browsers, operating systems).

Testing

  • I've added or updated tests to validate the changes introduced for AppFlowy Web.

22 new unit tests across two suites:

  • import-csv-batch.test.ts — selection order, one-at-a-time execution, progress callbacks, per-file failure continuing the batch, server-side Failed status plus task cancellation, mid-batch abort returning partial results, pre-aborted signal, empty error message left for the caller to translate, and the signal reaching the upload.
  • ImportDialog.multiCsv.test.tsx — multi-select attribute, file forwarding, toast wording per case (single / all / partial / cancelled), failure naming and translated overflow, all-fail keeps the dialog open, progress display, cancel-on-close aborts the signal, and backdrop/Escape not aborting.

Full suite: 309 suites / 2959 tests passing. pnpm type-check and eslint clean.

Feature-Specific

  • For feature additions, I've added a preview (video, screenshot, or demo) in the "Feature Preview" section.
  • I've verified that this feature integrates seamlessly with existing functionality.

Not yet exercised against a live stack — verification so far is unit tests, type-check and lint.

Summary by Sourcery

Enable resilient multi-file CSV imports with sequential processing, cancellation, progress feedback, and clearer failure reporting.

New Features:

  • Support selecting multiple CSV files and importing each as a separate Grid page under the chosen parent.
  • Provide batch progress, per-file failure reporting, partial-result handling, and cancellation for CSV imports.

Bug Fixes:

  • Propagate abort signals through CSV and Notion uploads so cancelling stops in-flight transfers.
  • Prevent relation row pickers from showing premature empty results or indefinite loading states while row data is resolving.

Enhancements:

  • Preserve existing single-file CSV behavior while improving import feedback and accessibility for multi-file batches.
  • Keep CSV imports sequential to respect server-side pending-task limits and stop cleanly on account-level capacity errors.

Tests:

  • Add unit and component coverage for CSV batch ordering, sequential execution, failures, cancellation, progress, reporting, localization, and relation-row loading states.

The CSV import path was single-file end to end: the file input had no
`multiple`, the change handler read `files[0]`, and the service imported
exactly one file per call.

Selecting several CSVs now creates one Grid page per file under the same
parent. Files import one at a time on purpose — the server caps pending
import tasks per user (MAXIMUM_IMPORT_PENDING_TASK, 3 by default), so a
parallel fan-out would fail every file past the cap. One bad file no longer
sinks the batch: its error is reported and the rest still import.

Also:
- the close button cancels a running batch (a batch can run for minutes),
  and the abort signal now reaches the in-flight upload so cancelling stops
  it immediately; backdrop click and Escape still never interrupt an import
- per-file progress on the CSV button, and toasts that say which files failed
- failure text is interpolated unescaped, so `Q1&Q2.csv` is not shown as
  `Q1&Q2.csv`

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds client-side support for importing multiple CSV files in a single batch, executing them sequentially with improved cancellation, progress reporting, and per-file error handling, while wiring abort signals through to uploads and expanding test coverage for the new behavior.

Sequence diagram for multi-file CSV import with cancellation

sequenceDiagram
  actor User
  participant ImportDialog
  participant importCsvFilesAsDatabases
  participant importCsvAsDatabase
  participant uploadDatabaseCsvImportFile
  participant axios

  User->>ImportDialog: click csvInputRef (multiple files)
  ImportDialog->>ImportDialog: onCsvPicked
  ImportDialog->>ImportDialog: handleCsv(files)
  ImportDialog->>ImportDialog: AbortController()
  ImportDialog->>importCsvFilesAsDatabases: importCsvFilesAsDatabases({ workspaceId, parentViewId, files, onFileStart, signal })

  loop for each file
    importCsvFilesAsDatabases->>ImportDialog: onFileStart(index, total)
    importCsvFilesAsDatabases->>importCsvAsDatabase: importCsvAsDatabase({ workspaceId, parentViewId, file, signal })
    importCsvAsDatabase->>uploadDatabaseCsvImportFile: uploadDatabaseCsvImportFile(presignedUrl, file, onProgress, signal)
    uploadDatabaseCsvImportFile->>axios: put(presignedUrl, file, { onUploadProgress, signal })
    axios-->>uploadDatabaseCsvImportFile: response 200/204
    uploadDatabaseCsvImportFile-->>importCsvAsDatabase: void
    importCsvAsDatabase-->>importCsvFilesAsDatabases: { viewId }
    importCsvFilesAsDatabases-->>importCsvFilesAsDatabases: items.push({ fileName, viewId })
  end

  importCsvFilesAsDatabases-->>ImportDialog: { items, aborted: false }
  ImportDialog->>ImportDialog: toast.success / toast.error
  ImportDialog->>ImportDialog: close()
  ImportDialog->>ImportDialog: toView(importedViewIds[0])

  alt user cancels mid-batch
    User->>ImportDialog: click close button
    ImportDialog->>ImportDialog: handleCloseClick
    ImportDialog->>ImportDialog: abortRef.current.abort()
    importCsvFilesAsDatabases-->>ImportDialog: { items, aborted: true }
    ImportDialog->>ImportDialog: toast.success (partial)
  end
Loading

File-Level Changes

Change Details Files
Support batch CSV imports in the Import dialog with sequential processing, progress display, and cancel behavior.
  • Refactor CSV import handler in ImportDialog to accept an array of files and call a new batch import service.
  • Track CSV batch progress in component state and show per-file progress text alongside the spinner.
  • Change dialog close behavior so backdrop/Escape never cancel imports, while the close button doubles as a cancel control for CSV batches.
  • Adjust toast logic to handle full success, partial success, cancellations, and per-file failure messages, including overflow handling and raw interpolation for file names.
src/components/app/import/ImportDialog.tsx
src/@types/translations/en.json
Introduce a batch CSV import service that imports files one-by-one and surfaces per-file outcomes and aborts.
  • Extend existing single-file import to pass AbortSignal into the upload and normalize axios cancellation into ImportAbortError.
  • Add importCsvFilesAsDatabases to loop over files sequentially, honoring AbortSignal and collecting per-file results.
  • Define ImportCsvBatchInput/Result types and ImportCsvBatchItem structure for view IDs and error messages, using getErrorMessage to normalize failures.
src/components/app/import/import-service.ts
Propagate abort signals to CSV upload HTTP calls.
  • Extend uploadDatabaseCsvImportFile to accept an AbortSignal and pass it to axios.put to support immediate cancellation of in-flight uploads.
src/application/services/js-services/http/import-api.ts
Add unit tests for CSV batch import logic and multi-file Import dialog behavior.
  • Add import-csv-batch tests covering selection order, sequential execution, progress callbacks, mixed success/failure, server-side failures, abort semantics, and pre-aborted signals.
  • Add ImportDialog.multiCsv tests covering multi-select, wiring to batch import, toast wording, failure naming and overflow, progress UI, cancel behavior, and non-interrupting backdrop/Escape behavior.
src/components/app/import/__tests__/import-csv-batch.test.ts
src/components/app/import/__tests__/ImportDialog.multiCsv.test.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

🥷 Ninja i18n – 🛎️ Translations need to be updated

Project /project.inlang

lint rule new reports level link
Missing translation 308 warning contribute (via Fink 🐦)

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The dialog close/cancel flow now has three separate handlers (close, handleDismiss, handleCloseClick) that partially duplicate logic; consider consolidating around a single helper that consistently resets state, aborts as needed, and calls onOpenChange(false) to reduce the chance of future divergence.
  • In handleCsv, files.length is used as the denominator for success/partial-success toasts even when some files may never be attempted due to an aborted signal; you might consider distinguishing between files.length and the number of attempted files so the messaging more precisely reflects what actually ran.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The dialog close/cancel flow now has three separate handlers (`close`, `handleDismiss`, `handleCloseClick`) that partially duplicate logic; consider consolidating around a single helper that consistently resets state, aborts as needed, and calls `onOpenChange(false)` to reduce the chance of future divergence.
- In `handleCsv`, `files.length` is used as the denominator for success/partial-success toasts even when some files may never be attempted due to an aborted signal; you might consider distinguishing between `files.length` and the number of attempted files so the messaging more precisely reflects what actually ran.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

appflowy and others added 3 commits August 18, 2026 21:13
Four issues from a pass with the Vercel React rules.

The count strings hand-rolled their pluralisation: `successCount` baked in
"Imported {{total}} files" and the component branched on `=== 1` around it.
English is the only language that works for — `en.json` already carries 39
`_one`/`_other` keys, and the repo ships `ar-SA`, `cs-CZ` and friends, where
2-4 and 5+ take different forms. Moved the three counted strings onto
i18next plurals keyed on `count`, which also collapses the ternaries. Added
`import-i18n.test.ts`, since the component tests stub `t` and would not
notice `t()` handing back a raw key after a rename.

The branch plumbed the abort signal into the CSV upload but left the same
gap on the Notion path, which handles the largest files: `importNotionZipToView`
took a `signal`, checked it before uploading, then dropped it. Cancelling did
nothing until a multi-hundred-MB zip finished, and with the dialog refusing
backdrop and Escape there was no way out at all. Signal now reaches both the
single-part and multipart uploads, the multipart workers stop claiming parts
once it fires, and an incomplete upload is never finalised. A cancelled upload
is normalised to `ImportAbortError` so it reads as a cancel, not a failure.

`TooManyImportTask` describes the account, not the file — the server counts
pending tasks per user in `ensure_import_task_capacity`. Retrying it per file
spent a round trip each and then blamed all N files for a queue the user only
had to wait out. The batch now stops on it, and the dialog derives its
denominator from the files actually attempted, so a batch that stopped early
never reports "Imported 1 of 20". Those runs keep the dialog open, since the
untried files are still worth retrying.

The progress counter lives inside a disabled button, which assistive tech
skips, so batch progress is also announced from a live region — as words
rather than the bare "3/7" ratio a screen reader would read out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Opening a relation cell painted every row as "Untitled" until its title
arrived. The row list lands as soon as the target view doc resolves, but the
titles come from a separate pass that fetches one row doc at a time, and
`rowContents.get(id) || ''` collapsed "not fetched yet" and "fetched, and the
primary cell really is empty" into the same falsy value — so a picker mid-load
was indistinguishable from a database full of unnamed rows.

The distinction was already available as `rowContents.has(id)`; it was just
being discarded. Rows still waiting on a title now render a pulsing placeholder
instead, and deleted rows are excluded: their wording is final and no doc is
ever coming.

That made error handling load-bearing rather than optional. Presence in
`rowContents` is what ends a placeholder, and the fetch loop had none at all, so
one rejected `createRow` would leave that row — and every row queued behind it —
pulsing forever. A row that cannot be read now falls back to the "Untitled"
wording and the batch continues.

Also stop announcing "no result" off the back of a row list that has not
arrived. An empty list mid-load reads as a terminal answer; it now waits for the
load to finish before concluding anything.

The fetch loop is still sequential, unlike the cell renderer in
`RelationItems`, which already fans out with Promise.all. The placeholders make
that visible rather than hiding it behind text, but parallelising it wants a
concurrency cap and is left for its own change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@appflowy
appflowy merged commit fa542b8 into main Aug 18, 2026
14 of 16 checks passed
@appflowy
appflowy deleted the feat/multi-csv-import branch August 18, 2026 14:34
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.

1 participant