feat(web): export all project cards as CSV (PE-3) - #524
prakruthi2411 wants to merge 2 commits into
Conversation
Add an Export CSV action on interaction boards so teams can download all project cards for reporting, and default board columns to 15 cards with scroll-to-load-more batches of 15. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
ℹ️ No critical issues — minor suggestions inline.
Reviewed changes
- CSV export utility (
apps/web/src/lib/export-tasks-csv.ts) — newfetchAllTasksForExport(cursor-paginates the full project task list),tasksToCsv(UTF-8 BOM + RFC-4180 quoting, ID→name resolution, custom fields),downloadCsv, andbuildExportFilename. - Export button wired up (
interaction-layout.tsx) —handleExportCsvassembles project lookups and adds a header button with spinner and inline error alert. - Board pagination defaults (
view-utils.ts) — Board initial/per-page defaults move 20 → 15 and"15"is added toPAGE_SIZE_OPTIONS. - i18n —
layout.shell.export*andexportColumnskeys added across all 9 locales. - Tests —
export-tasks-csv.test.tscovers BOM/headers/resolved names, escaping, parent resolution, and filename sanitization.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
|
|
||
| function escapeCsvCell(value: string): string { | ||
| if (/[",\r\n]/.test(value)) { | ||
| return `"${value.replaceAll('"', '""')}"`; |
There was a problem hiding this comment.
User-controlled cells (task titles, tags, custom-field text) are emitted verbatim, so a title like =HYPERLINK(...) or =cmd|... will be evaluated as a formula when the CSV is opened in Excel/Sheets (CSV injection, CWE-1236). Consider neutralizing cells whose first non-space char is one of = + - @ (e.g. prefix with a single quote or tab) inside this escaper.
Technical details
# CSV formula injection
## Affected sites
- apps/web/src/lib/export-tasks-csv.ts:76-81 — `escapeCsvCell` only quotes when the value contains `"`, `,`, `\r`, or `\n`; leading `=`, `+`, `-`, `@` pass through unchanged.
## Required outcome
- Exported cells cannot be interpreted as spreadsheet formulas, while preserving the visible value for legitimate data.
## Suggested approach (optional)
- In `escapeCsvCell`, after determining the string, prepend a guard (commonly `'`) when `/^[\t\r ]*[=+\-@]/` matches, then run the normal quoting logic.| ): string { | ||
| const safe = (projectSlug || "project") | ||
| .trim() | ||
| .replace(/[^\w.-]+/g, "-") |
There was a problem hiding this comment.
\w is ASCII-only, so a project named e.g. プロジェクト or Café collapses to an empty slug and the download falls back to the literal project-tasks-....csv. Using a Unicode-aware class (e.g. /[^\p{L}\p{N}._-]+/gu) would preserve those names.
Technical details
# Non-ASCII project names lose the filename slug
## Affected sites
- apps/web/src/lib/export-tasks-csv.ts:212-217 — `replace(/[^\w.-]+/g, "-")` strips every non-ASCII letter.
## Required outcome
- Filenames remain meaningful for non-ASCII project names while still stripping path/quote-unsafe characters.
## Open questions for the human (optional)
- Some filesystems/servers may prefer ASCII-only filenames; if so, this is acceptable as-is, but the current fallback is `project` rather than a transliteration.| /** Task list API caps page_size at 200. */ | ||
| const PAGE_SIZE = 200; | ||
| /** Safety cap so a runaway cursor loop cannot hang the browser. */ | ||
| const MAX_PAGES = 100; |
There was a problem hiding this comment.
The MAX_PAGES guard silently drops cards once a project exceeds 20,000 tasks — the CSV will look complete while missing data. Consider surfacing a warning (or including the truncation in exportFailed/the alert) when the loop hits the cap.
Technical details
# Silent truncation at the pagination cap
## Affected sites
- apps/web/src/lib/export-tasks-csv.ts:20-21, 60-73 — loop stops after `MAX_PAGES` iterations with no signal that `next_cursor` was still non-null.
## Required outcome
- Callers can distinguish a complete export from a capped one; the user is told when the file is partial.
## Suggested approach (optional)
- Return `{ tasks, truncated: boolean }` (or throw) when the loop exits on the cap, and have `handleExportCsv` surface a message instead of writing a partial file silently.PE-3 only covers exporting project cards as CSV; revert the unrelated board pagination default change so this PR stays scoped. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Hi @prakruthi2411 , Exporting CSV is a great feature idea! However, we shouldn't loop API calls from the web app to fetch the full task data. Instead, it would be better to create an API endpoint that handles export requests by publishing a message to a Valkey stream. A background job (acting as a Valkey stream consumer in the API service) can then fetch all the task data, generate the CSV file, and upload it to storage (Rustfs or S3, depending on the setup) for the user to download Also, this feature should be placed under Project Settings and protected by a separate permission (e.g., project.export). Thank you so much for your effort and for proposing this feature! If you'd like to continue working on this PR, feel free to update it following this approach. If you're busy, just let me know and I can take over |

Summary
Implements PE-3 — Export All PACA Cards by Project.
Test plan
.csvwith all cardsbun --bun run test src/lib/export-tasks-csv.test.ts