#52 — Multi-line Paste → Split Into Multiple Todos (Confirmation Prompt) #52

Closed
opened 2026-08-18 13:13:17 +02:00 by lena · 3 comments
lena commented 2026-08-18 13:13:17 +02:00 (Migrated from git.butzei.de)

Story: Multi-line Paste → Split Into Multiple Todos (Confirmation Prompt)

As a list member,
I want to be asked whether a multi-line string I'm adding as a new todo should become one todo or several,
so that I can quickly add a batch of items (e.g. a pasted shopping list) without creating them one at a time.

Acceptance criteria:

  • When the text entered into the "new todo" input contains 2 or more non-blank lines, creation is intercepted
    and a confirmation dialog is shown before anything is created
  • Dialog title: "Create N todos?" where N is the count of non-blank lines
  • Dialog body, second line: "One todo will be created per line."
  • Confirming creates one todo per non-blank line, in order, each as its own todo with that line (trimmed) as
    the title
  • Cancelling/declining creates a single todo whose title is the full original multi-line text, unchanged —
    i.e. today's behavior
  • Blank lines are ignored both when counting N and when splitting (no empty todos are ever created)
  • Single-line input (typed or pasted) never triggers the popup — a todo is created immediately, as today
  • Dialog reuses the existing Dialog / DialogContent pattern (ReactUi/src/components/ui/dialog.tsx),
    consistent with other confirmation modals in the app (see OwnerActionsMenu.tsx)
  • Works on both desktop and mobile/touch viewports

Known technical constraint — needs an Architect decision before implementation:

Today's "new todo" field (TodoInput.tsx) is a plain single-line <input type="text">. A native <input>
cannot hold newline characters at all — pasting multi-line text into it collapses to a single line before this
feature could ever detect multiple lines. The Architect must decide how the multi-line string is actually
captured, e.g.:

  • Switch the field to an auto-growing <textarea> (Enter still submits single-line entries; needs a decision
    on how multi-line entries are then produced — paste only, or also Shift+Enter for manual multi-line typing)
  • Or keep the <input> but intercept the onPaste event and read clipboardData directly, before the browser
    normalizes it, then branch into this flow without changing the field's normal single-line typing behavior

Either approach must preserve todo creation via Enter for the common single-line case without adding friction.

Out of scope for this story:

  • Editing or removing individual lines from within the confirmation dialog before creating (all-or-nothing split)
  • A configurable split delimiter other than newline
  • Applying this behavior to editing an existing todo's title (create/new-todo flow only)

Blockers: None — independent of other in-flight work.

Priority: Should — flagged directly by the human Product Owner as important for a specific user's workflow
(2026-07-19); queued straight into ready/ to steer it ahead of the open Could-tier backlog, per the human
override note in ai/roles/00_team_overview.md ("Autonomous loop" section).

# Story: Multi-line Paste → Split Into Multiple Todos (Confirmation Prompt) **As a** list member, **I want to** be asked whether a multi-line string I'm adding as a new todo should become one todo or several, **so that** I can quickly add a batch of items (e.g. a pasted shopping list) without creating them one at a time. **Acceptance criteria:** - [ ] When the text entered into the "new todo" input contains 2 or more non-blank lines, creation is intercepted and a confirmation dialog is shown before anything is created - [ ] Dialog title: **"Create N todos?"** where N is the count of non-blank lines - [ ] Dialog body, second line: **"One todo will be created per line."** - [ ] Confirming creates one todo per non-blank line, in order, each as its own todo with that line (trimmed) as the title - [ ] Cancelling/declining creates a single todo whose title is the full original multi-line text, unchanged — i.e. today's behavior - [ ] Blank lines are ignored both when counting N and when splitting (no empty todos are ever created) - [ ] Single-line input (typed or pasted) never triggers the popup — a todo is created immediately, as today - [ ] Dialog reuses the existing `Dialog` / `DialogContent` pattern (`ReactUi/src/components/ui/dialog.tsx`), consistent with other confirmation modals in the app (see `OwnerActionsMenu.tsx`) - [ ] Works on both desktop and mobile/touch viewports **Known technical constraint — needs an Architect decision before implementation:** Today's "new todo" field (`TodoInput.tsx`) is a plain single-line `<input type="text">`. A native `<input>` cannot hold newline characters at all — pasting multi-line text into it collapses to a single line before this feature could ever detect multiple lines. The Architect must decide how the multi-line string is actually captured, e.g.: - Switch the field to an auto-growing `<textarea>` (Enter still submits single-line entries; needs a decision on how multi-line entries are then produced — paste only, or also Shift+Enter for manual multi-line typing) - Or keep the `<input>` but intercept the `onPaste` event and read `clipboardData` directly, before the browser normalizes it, then branch into this flow without changing the field's normal single-line typing behavior Either approach must preserve todo creation via Enter for the common single-line case without adding friction. **Out of scope for this story:** - Editing or removing individual lines from within the confirmation dialog before creating (all-or-nothing split) - A configurable split delimiter other than newline - Applying this behavior to editing an existing todo's title (create/new-todo flow only) **Blockers:** None — independent of other in-flight work. **Priority:** Should — flagged directly by the human Product Owner as important for a specific user's workflow (2026-07-19); queued straight into `ready/` to steer it ahead of the open Could-tier backlog, per the human override note in `ai/roles/00_team_overview.md` ("Autonomous loop" section).
lena commented 2026-08-18 13:13:17 +02:00 (Migrated from git.butzei.de)

security (52_multiline_todo_split_confirmation_security.md)

Security Review — #52 Multi-line Paste → Split Confirmation

Author: Security Agent
Date: 2026-07-19

Review

Frontend-only change. No new backend endpoint, no new authorization surface: every created
todo — whether from the "confirm split" path or the "decline" fallback — goes through the
existing CreateTodoCommand, already gated by
AuthorizeTodoListAccessForCurrentUserQuery/AuthorizeTodoListIsNotArchivedQuery exactly as
before this story. Splitting client-side into N calls instead of 1 doesn't change what data
crosses the wire per call or who's allowed to make it — same shape, same guard, just more of them.

Clipboard access (event.clipboardData.getData('text')) is a standard, user-initiated paste
event handler — no clipboard read happens outside of an actual paste the user performed, and
nothing is sent anywhere the browser's own paste gesture didn't already imply.

No new dependency added. No secrets, tokens, or credential handling involved.

Verdict: Approved.

**security** (`52_multiline_todo_split_confirmation_security.md`) # Security Review — `#52` Multi-line Paste → Split Confirmation **Author:** Security Agent **Date:** 2026-07-19 ## Review Frontend-only change. No new backend endpoint, no new authorization surface: every created todo — whether from the "confirm split" path or the "decline" fallback — goes through the existing `CreateTodoCommand`, already gated by `AuthorizeTodoListAccessForCurrentUserQuery`/`AuthorizeTodoListIsNotArchivedQuery` exactly as before this story. Splitting client-side into N calls instead of 1 doesn't change what data crosses the wire per call or who's allowed to make it — same shape, same guard, just more of them. Clipboard access (`event.clipboardData.getData('text')`) is a standard, user-initiated paste event handler — no clipboard read happens outside of an actual paste the user performed, and nothing is sent anywhere the browser's own paste gesture didn't already imply. No new dependency added. No secrets, tokens, or credential handling involved. **Verdict:** Approved.
lena commented 2026-08-18 13:13:18 +02:00 (Migrated from git.butzei.de)

design (52_multiline_todo_split_confirmation_design.md)

Design Note — #52 Multi-line Paste → Split Confirmation

Author: Software Architect
Date: 2026-07-19

Resolving the flagged technical constraint

The story flags that TodoInput.tsx's field is a native <input type="text">, which cannot hold
newline characters at all, and asks for a decision between switching to a <textarea> or
intercepting onPaste directly.

Decision: keep the <input>, intercept onPaste.

  • onPaste's event.clipboardData.getData('text') gives the raw pasted string, newlines intact,
    before the browser inserts/normalizes it into the input's value — so no field-type change is
    needed to observe multi-line content.
  • Switching to a <textarea> would reopen the exact ambiguity the story's own note raises (does
    Enter still submit, or does it need Shift+Enter, and does that change apply to every
    single-line-typing user going forward) for a feature that, per the story's title and primary use
    case, is specifically about paste, not manual multi-line typing. onPaste interception adds
    the behavior with zero change to how typing/Enter-to-submit already works — satisfies "must
    preserve todo creation via Enter for the common single-line case without adding friction" with
    no new code path for that case at all.
  • A plain <input> physically cannot contain \n from typing (Enter never inserts a newline,
    it only fires the existing onKeyDown submit handler), so "single-line input... never triggers
    the popup" holds trivially for typed input — only paste can ever produce multi-line text here,
    which is exactly the surface onPaste interception covers.

Scope decision: only intercept when the field is currently empty

The story doesn't specify what happens when a user pastes multi-line text into a field that
already has some typed content (splicing at cursor position). Given the story's own framing
("quickly add a batch of items e.g. a pasted shopping list") describes pasting into a fresh input,
and implementing correct cursor-position splicing adds real complexity for a case the acceptance
criteria never describes, this only triggers when value === '' at paste time. If the field
already has content, the paste falls through to native browser behavior (newlines stripped, same
as today) — unchanged from current behavior, not a regression.

Flow

  • onPaste: if value === '', read clipboardData.getData('text'), split on \n, filter blank
    (trim() !== '') lines. If the resulting count is >= 2: event.preventDefault() (block the
    default paste so nothing lands in the input), store the raw pasted text in new state
    (pendingPaste: string | null), open a confirmation dialog. Otherwise (0 or 1 non-blank lines):
    let the paste proceed natively — same as today.
  • New MultilinePasteConfirmDialog component (matching the existing pattern of
    TransferOwnershipDialog.tsx/AccountDeletionDialog.tsx — small dialog bodies extracted to
    their own file, rendered inside <Dialog><DialogContent> by the parent), title Create N todos? (N = non-blank line count), body One todo will be created per line.
    • Confirm: for each non-blank line (trimmed), await callApi('CreateTodoCommand', {todoListId, title: line, description: '', dueDate: null}) sequentially (not Promise.all) — the story requires "in order," and while the backend's per-list insert trigger serializes concurrent inserts via a row lock regardless, sequential awaits are what actually guarantee completion order matches line order (a Promise.all batch has no ordering guarantee on which request's lock-acquisition/insert completes first).
    • Cancel: await callApi('CreateTodoCommand', {todoListId, title: pendingPaste, description: '', dueDate: null}) — the raw, unmodified pasted text, matching "today's behavior" (a single todo with the full original text as its title, not trimmed/altered).
    • Either path clears pendingPaste and closes the dialog.

Out of scope (per story)

Editing individual lines before creating, a configurable delimiter, and applying this to editing
an existing todo's title — none of the above touches TodoItem.tsx's edit paths.

Backend

No backend changes. Each split line becomes one CreateTodoCommand call — an existing command,
already authorized/validated (list access + not-archived) per-call, same as any other todo
creation.

**design** (`52_multiline_todo_split_confirmation_design.md`) # Design Note — `#52` Multi-line Paste → Split Confirmation **Author:** Software Architect **Date:** 2026-07-19 ## Resolving the flagged technical constraint The story flags that `TodoInput.tsx`'s field is a native `<input type="text">`, which cannot hold newline characters at all, and asks for a decision between switching to a `<textarea>` or intercepting `onPaste` directly. **Decision: keep the `<input>`, intercept `onPaste`.** - `onPaste`'s `event.clipboardData.getData('text')` gives the raw pasted string, newlines intact, *before* the browser inserts/normalizes it into the input's value — so no field-type change is needed to observe multi-line content. - Switching to a `<textarea>` would reopen the exact ambiguity the story's own note raises (does Enter still submit, or does it need Shift+Enter, and does that change apply to every single-line-typing user going forward) for a feature that, per the story's title and primary use case, is specifically about *paste*, not manual multi-line typing. `onPaste` interception adds the behavior with zero change to how typing/Enter-to-submit already works — satisfies "must preserve todo creation via Enter for the common single-line case without adding friction" with no new code path for that case at all. - A plain `<input>` physically cannot contain `\n` from typing (`Enter` never inserts a newline, it only fires the existing `onKeyDown` submit handler), so "single-line input... never triggers the popup" holds trivially for typed input — only paste can ever produce multi-line text here, which is exactly the surface `onPaste` interception covers. ## Scope decision: only intercept when the field is currently empty The story doesn't specify what happens when a user pastes multi-line text into a field that already has some typed content (splicing at cursor position). Given the story's own framing ("quickly add a batch of items e.g. a pasted shopping list") describes pasting into a fresh input, and implementing correct cursor-position splicing adds real complexity for a case the acceptance criteria never describes, this only triggers when `value === ''` at paste time. If the field already has content, the paste falls through to native browser behavior (newlines stripped, same as today) — unchanged from current behavior, not a regression. ## Flow - `onPaste`: if `value === ''`, read `clipboardData.getData('text')`, split on `\n`, filter blank (`trim() !== ''`) lines. If the resulting count is `>= 2`: `event.preventDefault()` (block the default paste so nothing lands in the input), store the raw pasted text in new state (`pendingPaste: string | null`), open a confirmation dialog. Otherwise (0 or 1 non-blank lines): let the paste proceed natively — same as today. - New `MultilinePasteConfirmDialog` component (matching the existing pattern of `TransferOwnershipDialog.tsx`/`AccountDeletionDialog.tsx` — small dialog bodies extracted to their own file, rendered inside `<Dialog><DialogContent>` by the parent), title `Create N todos?` (N = non-blank line count), body `One todo will be created per line.` - **Confirm:** for each non-blank line (trimmed), `await callApi('CreateTodoCommand', {todoListId, title: line, description: '', dueDate: null})` **sequentially** (not `Promise.all`) — the story requires "in order," and while the backend's per-list insert trigger serializes concurrent inserts via a row lock regardless, sequential awaits are what actually *guarantee* completion order matches line order (a `Promise.all` batch has no ordering guarantee on which request's lock-acquisition/insert completes first). - **Cancel:** `await callApi('CreateTodoCommand', {todoListId, title: pendingPaste, description: '', dueDate: null})` — the *raw, unmodified* pasted text, matching "today's behavior" (a single todo with the full original text as its title, not trimmed/altered). - Either path clears `pendingPaste` and closes the dialog. ## Out of scope (per story) Editing individual lines before creating, a configurable delimiter, and applying this to editing an existing todo's title — none of the above touches `TodoItem.tsx`'s edit paths. ## Backend No backend changes. Each split line becomes one `CreateTodoCommand` call — an existing command, already authorized/validated (list access + not-archived) per-call, same as any other todo creation.
lena commented 2026-08-18 13:13:18 +02:00 (Migrated from git.butzei.de)

qa (52_multiline_todo_split_confirmation_qa.md)

QA Notes — #52 Multi-line Paste → Split Confirmation

Author: QA Agent
Date: 2026-07-19

What was verified

  • tsc -b clean, npm run build clean, full Vitest suite: 39 files / 279 tests passing.
  • New coverage: multilinePaste.test.ts (5 tests, the pure splitNonBlankLines helper —
    newline splitting, trimming, blank-line dropping, single-line, all-blank);
    MultilinePasteConfirmDialog.test.tsx (5 tests — title/body text, confirm calls onConfirm
    with the right lines and shows a loading state, a second click while pending doesn't
    double-call, a rejected onConfirm re-enables the buttons instead of getting stuck, Cancel
    is a plain dismiss with no onConfirm call); TodoInput.test.tsx gained 7 new tests covering
    the full paste-to-split flow end to end (dialog opens for 2+ non-blank lines, blank lines
    don't count, single-line paste doesn't open it, paste into a non-empty field falls through
    to native behavior, confirming creates one todo per line in order, cancelling creates one
    todo with the original text unchanged, and dismissing via Escape behaves the same as
    cancelling).
  • No manual/E2E verification was possible — this sandbox has no Docker, so the full
    Postgres/Redis-backed dev stack (and Playwright) can't run locally (see
    ai/roles/memory/06_qa_agent_memory.md/07_team_coach_memory.md for the established note on
    this). Real device/mobile clipboard behavior specifically (the story's "works on mobile/touch"
    AC) could not be exercised beyond the jsdom-simulated paste event and the defensive
    clipboardData?. guard added during code review — this is a genuine verification gap, not a
    claim of full mobile coverage, and should be spot-checked on a real device if one becomes
    available before this is considered fully proven end-to-end.

Acceptance criteria status

  • 2+ non-blank lines pasted into the new-todo input intercepts creation and shows a
    confirmation dialog before anything is created
  • Dialog title: "Create N todos?"
  • Dialog body: "One todo will be created per line."
  • Confirming creates one todo per non-blank line, in order (sequential awaits, not
    Promise.all — see design note)
  • Cancelling/declining creates a single todo with the full original text unchanged — and,
    per a code-review fix, this now holds for every dismissal path (Cancel button, Escape,
    backdrop click), not just the explicit button
  • Blank lines ignored both for the count and the split
  • Single-line input never triggers the popup
  • Reuses the existing Dialog/DialogContent pattern — code review corrected an initial
    version that had the new component owning its own Dialog wrapper instead of following
    the established parent-owns-Dialog convention (TransferOwnershipDialog.tsx,
    AccountDeletionDialog.tsx)
  • Works on both desktop and mobile/touch viewports — implemented and defensively guarded
    (clipboardData?.), but not verified on a real mobile device/browser (see note above)
**qa** (`52_multiline_todo_split_confirmation_qa.md`) # QA Notes — `#52` Multi-line Paste → Split Confirmation **Author:** QA Agent **Date:** 2026-07-19 ## What was verified - `tsc -b` clean, `npm run build` clean, full Vitest suite: 39 files / 279 tests passing. - New coverage: `multilinePaste.test.ts` (5 tests, the pure `splitNonBlankLines` helper — newline splitting, trimming, blank-line dropping, single-line, all-blank); `MultilinePasteConfirmDialog.test.tsx` (5 tests — title/body text, confirm calls `onConfirm` with the right lines and shows a loading state, a second click while pending doesn't double-call, a rejected `onConfirm` re-enables the buttons instead of getting stuck, Cancel is a plain dismiss with no `onConfirm` call); `TodoInput.test.tsx` gained 7 new tests covering the full paste-to-split flow end to end (dialog opens for 2+ non-blank lines, blank lines don't count, single-line paste doesn't open it, paste into a non-empty field falls through to native behavior, confirming creates one todo per line in order, cancelling creates one todo with the original text unchanged, and dismissing via Escape behaves the same as cancelling). - No manual/E2E verification was possible — this sandbox has no Docker, so the full Postgres/Redis-backed dev stack (and Playwright) can't run locally (see `ai/roles/memory/06_qa_agent_memory.md`/`07_team_coach_memory.md` for the established note on this). Real device/mobile clipboard behavior specifically (the story's "works on mobile/touch" AC) could not be exercised beyond the jsdom-simulated `paste` event and the defensive `clipboardData?.` guard added during code review — this is a genuine verification gap, not a claim of full mobile coverage, and should be spot-checked on a real device if one becomes available before this is considered fully proven end-to-end. ## Acceptance criteria status - [x] 2+ non-blank lines pasted into the new-todo input intercepts creation and shows a confirmation dialog before anything is created - [x] Dialog title: "Create N todos?" - [x] Dialog body: "One todo will be created per line." - [x] Confirming creates one todo per non-blank line, in order (sequential awaits, not `Promise.all` — see design note) - [x] Cancelling/declining creates a single todo with the full original text unchanged — and, per a code-review fix, this now holds for *every* dismissal path (Cancel button, Escape, backdrop click), not just the explicit button - [x] Blank lines ignored both for the count and the split - [x] Single-line input never triggers the popup - [x] Reuses the existing `Dialog`/`DialogContent` pattern — code review corrected an initial version that had the new component owning its own `Dialog` wrapper instead of following the established parent-owns-`Dialog` convention (`TransferOwnershipDialog.tsx`, `AccountDeletionDialog.tsx`) - [ ] Works on both desktop and mobile/touch viewports — implemented and defensively guarded (`clipboardData?.`), but not verified on a real mobile device/browser (see note above)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
robert/todo#52
No description provided.