#83 — CSV Import and Export for Todo Lists and Shopping Lists #83
Labels
No labels
priority/could
priority/must
priority/should
priority/wont
status/blocked
status/claimed
status/done-migrated
type/bug
type/feature
type/infra
type/tech-debt
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/todo#83
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
#83— CSV Import and Export for Todo Lists and Shopping ListsProblem
There is no way to bulk-add items to a list or extract list data for use in other tools. Users who maintain lists in spreadsheets, want to seed a new list from an existing file, or need to share list contents outside the app have no supported path.
Acceptance Criteria
Entry Points
Export Flow
.csvfile.File format
;).Title;Category;DueDate).YYYY-MM-DD).true/false."".Import Flow
.csvfile from their device.Import behaviour
Available Fields
Todo List
TitleDescriptionCategoryDueDatePriorityNone/Low/Normal/HighDoneLabelsShopping List — Master List
NameCategoryLastQuantityShopping List — Shopping List (active items)
NameCategoryQuantityOut of Scope
Priority
Should — Standalone utility feature; no dependency on
#80–#82.Decisions
EF BB BF) causes Excel to recognise the encoding automatically, preventing garbled umlauts.design (
83_csv_import_export_design.md)Design:
#83CSV Import and Export for Todo Lists and Shopping ListsArchitect note, 2026-07-25
Backend
needs is already reachable client-side — todos via the live-synced Zustand store (WS-kept
current), categories/products via the existing
Get*ForListQueryhandlers this feature's dialogfetches directly. CSV generation (column selection/order, quoting, BOM, delimiter) is pure string
formatting with no server-side business logic, so building a new query that just re-serves data
the frontend already has would be pure duplication.
docs/features/done/49_typed_callapi...'sprecedent of "no handler without an AC" cuts the same way here in reverse: no new handler where
an existing one (or the store) already answers the question.
ImportTodosFromCsvCommand/ImportShoppingProductsFromCsvCommand, each wrapped in oneDbTransactionDecorator, loop overtheir rows calling
CreateTodoCommand/SetTodoPriorityCommand/CheckTodoCommand/TagTodoCommand(Todos) orCreateShoppingProductCommand/ActivateShoppingProductCommand(Shopping) — the same "seeded through the normal pipeline" precedent
CreateTodoListCommandHandler's template seeding established (#63), so Nr/SortOrder assignment,WS broadcast, and activity-feed events are correct for free instead of reimplemented as a raw
bulk insert.
CreateTodoCommand.NotifyEmail: falsefor every imported row — a bulk importshouldn't email every Instant-subscribed list member once per row.
Dictionary<string, TCategoryId>seeded from the list's existing categories, updated as new onesare created) so 50 rows sharing one new category name only create it once.
category auto-creation; a label also carries a colour an import row has no way to specify, so an
unmatched label name is silently skipped rather than invented with an arbitrary colour.
ActivateOnImport: bool), not two, sincethe only difference is whether a matched/created product is also activated with the row's
quantity — Master List target additionally treats an already-catalogued name as "no existing
items are modified" (skip, don't touch), where Shopping List target activates it instead (mirrors
AddOrActivateShoppingProductCommand's find-or-create-then-activate shape, extended to alsocarry a
CategoryandQuantityfrom the row, which that command doesn't accept).Donenot literally"true") is silentlyimported as empty/default rather than failing or skipping the row, per the AC —
DateOnly.TryParse/
Enum.TryParsefailures just leave the field atCreateTodoCommand's own default.Frontend
CsvExportDialog/CsvImportDialogare shared across both domains via akind: 'todo' | 'shopping'prop and a sharedCSV_FIELDSfield-spec table (utils/csvFields.ts) rather thanthree near-duplicate dialog components — the two dialogs' UI structure (column
selector/mapping, file picker, preview) doesn't vary by domain, only the field list and the
API call at the end do.
#80/#81'sCategoryManagermade and documented (afireEvent-testable equivalent reachingthe same end state, built in a sandbox with no live browser to ever watch a drag gesture work).
utils/csv.ts,parseCsv) — a quoted field's embedded newline would otherwise be mistaken for a row boundary,the one case a naive split gets wrong and CSV files legitimately contain (a multi-line
Description).
rows) rather than pre-filtering client-side — the backend already computes and returns the
authoritative
Imported/Skipped/CategoriesCreatedcounts, so duplicating that logicclient-side would risk the two disagreeing. The frontend only shows a rough pre-confirm row
count, not a predicted skip/create count.
=,+,-,@, tab, or CR is prefixed with a'before being written, since exported cells areuser-entered todo/product text opened directly in Excel/Sheets/LibreOffice, which would
otherwise evaluate a leading
=...as a formula. Header labels (our own fixed strings) are neverneutralized, only data cells.
Known limitation (not fixed this cycle)
Todo category auto-creation during import doesn't live-refresh an already-open
TodoListview'scategory grouping —
OwnerActionsMenu(where the CSV menu items live) andTodoList(which ownsthe categories fetch) are separate branches of the component tree with no shared invalidation
signal, unlike Shopping's
ShoppingListPage, which owns both the menu and the refetch and so wiresonImporteddirectly. Newly-imported todos still appear immediately (WS-synced), just notregrouped under a newly-created category until the list is reselected or the page reloads. Judged
out of proportion to fix this cycle (would need lifting category state up or a pub/sub mechanism);
flagged here rather than left as a silent gap.
Security
AuthorizeTodoListAccessFor CurrentUserQuery,AuthorizeTodoListIsNotArchivedQuery,AuthorizeShoppingListAccessForCurrent UserQuery) — no new authorization surface, since nothing here bypasses the normal per-itemcommands' own checks.
.csventirely client-side and sendsonly the already-mapped, already-typed row data over the wire.
Blockers
None (standalone utility feature, no dependency on
#80–#82, per the story).