Skip to content

fix(meetings): select a person's older tasks at once and add them in one step - #322

Merged
novoj merged 2 commits into
devfrom
fix/agenda-older-tasks-batch
Nov 2, 2026
Merged

novoj merged 2 commits into
devfrom
fix/agenda-older-tasks-batch

Conversation

@novoj

@novoj novoj commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The earlier-task chooser in meeting preparation ("Add older tasks…") added the selected tasks one request at a time. Every request refreshed the agenda behind the dialog, so adding ten tasks took long and the page jumped once per task. Selecting all tasks of one person also took one click per task.

Changes

  • Each person group (and "No assignee") has a three-state checkbox. It selects or clears that group's tasks currently visible under the search.
  • POST /api/meetings/[id]/tasks accepts taskIds (up to 500, deduplicated) next to the single taskId, so MCP and other callers keep working. All ids are checked and added in one transaction under the agenda lock:
    • one refused id refuses the whole batch, and the error names the refused ids;
    • tasks already active in the agenda are skipped;
    • a removed task is restored;
    • added tasks start at position 100000, above everything the last-meeting snapshot assigns, and follow the selection order from there (previously every added task got exactly 100000); ties order by task id.
  • The dialog sends one request with one optimistic update and closes. On failure it keeps the selection and shows one error.
  • The listing and the add share one eligibility condition, which matches getVisibleTask's item rule. Personal tasks no longer appear in the chooser; they could never be added.
  • The group header's checkbox sits outside the <h3>, so the heading reads only the person's name.

Testing

  • older-tasks-dialog.test.tsx: group select-all only on visible tasks, mixed state, one request and one optimistic update, failure keeps the selection.
  • older-tasks-batch.integration.test.ts (Postgres): ordering, all-or-nothing on an invisible task, restore, duplicates, already-active skips, concurrent overlapping batches.
  • Biome, tsc, meeting tests including integration: 428 passed. Whole unit suite: 7,378 passed, 0 failed.
  • Browser check on the demo stack: selecting both groups and adding them sent one POST with both ids. The dialog closed and both tasks appeared in the agenda.

…one step

The earlier-task chooser in meeting preparation added the selected tasks one
request at a time. Every request refreshed the agenda behind the dialog, so
adding ten tasks took long and the page jumped once per task.

- Each person group (and "No assignee") has a three-state checkbox that selects
  or clears the group's tasks visible under the current search.
- POST /api/meetings/[id]/tasks accepts `taskIds` (up to 500, deduplicated)
  next to the single `taskId`. All ids are checked and added in one
  transaction under the agenda lock: one refused id refuses the batch and the
  error names the refused ids. Tasks already active in the agenda are skipped,
  a removed task is restored, and positions follow the selection order after
  the current last task.
- The dialog sends one request with one optimistic update and closes; on
  failure it keeps the selection and shows one error.
- The listing and the add share one eligibility condition, matching the item
  rule of getVisibleTask; personal tasks no longer appear in the chooser
  (they could never be added).
Comment thread src/lib/meetings/service.ts Resolved
@claude

claude Bot commented Nov 2, 2026

Copy link
Copy Markdown

Review: fix(meetings): select a person's older tasks at once and add them in one step

Batches the older-task picker into one POST /api/meetings/[id]/tasks call with taskIds, moves eligibility checking and insertion into one atomic, agenda-locked transaction, and adds a three-state per-person group checkbox to the dialog. The service-layer rewrite is solid: single eligibility condition shared between listing and adding, all-or-nothing refusal with named ids, restore-on-removal, dedup, and selection-order positions, all backed by thorough integration and component tests. One ordering edge case is worth a look before merge.

Verdict: COMMENT

Findings: Blocker 0 | Critical 0 | Major 1 | Minor 0 | Nit 0

Findings

MAJOR — src/lib/meetings/service.ts:1472 (see inline comment). addAgendaTasks now assigns new "older" tasks positions right after the current max (lastPosition + index + 1), replacing the old fixed 100000 offset that the PR correctly identifies as a bug. But snapshotTasks (service.ts:239-251) still unconditionally resets last_meeting-origin positions to 0..N-1 whenever the task source changes or the agenda is filed into another folder, with no knowledge of positions already claimed by older-origin rows. The old 100000 gap made a collision impossible; the new small sequential positions make it possible: add an older task, then change the agenda's task source, and a last_meeting task can land on the same position as an existing older task. agenda_tasks.position has no uniqueness constraint and the list is ordered by asc(position) alone (no tiebreaker), so ties sort unstably. No data loss, but it undercuts this PR's own "positions follow selection order" fix. Flagged as a question since I couldn't exercise this against Postgres — worth a regression test covering addAgendaTasks followed by a task-source or filing change either way.

Missing tests

The interaction above: older-task addition followed by a taskSourceItemId/calendarEventId change (or a new folder filing) that re-triggers snapshotTasks.

Everything else checked out: user scoping and the agenda lock are correct, the route's union schema (taskIds xor taskId) and its 500/dedup limits are enforced and tested, CI's Type Check and Lint/Format jobs are green on the head commit, the Czech string was added, and the MCP single-task path (addAgendaTask) is untouched and still covered.

The last-meeting snapshot re-inserts its tasks at positions 0..N-1 when the task source changes or the agenda is filed elsewhere. Added older tasks had moved to max+1, so they could tie or interleave with a larger re-snapshot. They start at OLDER_TASK_POSITION_BASE (100000) again, sequential in selection order, in the service and the optimistic update; agenda tasks order by position, then task id.
@claude

claude Bot commented Nov 2, 2026

Copy link
Copy Markdown

Review: fix(meetings): select a person's older tasks at once and add them in one step

Batches the older-task picker into one POST /api/meetings/[id]/tasks call with taskIds, moves eligibility and insertion into one atomic, agenda-locked transaction, and adds a three-state per-person group checkbox. This re-review covers the fix-up commit (873a35e3) added after the first round.

Verdict: APPROVE

Findings: Blocker 0 | Critical 0 | Major 0 | Minor 0 | Nit 0

Prior finding, now resolved

The only MAJOR finding from the first review round (src/lib/meetings/service.ts, position collision between newly-added "older" tasks and snapshotTasks's unconditional 0..N-1 reset of last_meeting positions) is fixed in 873a35e3: added tasks now start at max(lastPosition + 1, OLDER_TASK_POSITION_BASE) via the shared src/lib/meetings/positions.ts constant, applied identically in the service and the dialog's optimistic update, with getMeeting's agenda-task query now tiebreaking asc(position), asc(taskId). The new integration test "keeps a larger replacement snapshot before older tasks in selection order" exercises exactly this interaction against Postgres and would have failed before the fix. Confirmed correct.

Everything else checked

  • olderTaskCondition correctly unifies the listing and add-eligibility rule; ne(source, "personal") reproduces the old task.own rejection exactly (own = source === "personal" in src/lib/tasks/tasks.ts:402), and the added chatterItems.deletedAt/kind clauses mirror getVisibleTask's item rule as described.
  • addAgendaTasks: all-or-nothing refusal with named ids, skip-if-active, restore-on-removal (note/origin preserved via the conflict set not touching them), dedup via agendaTaskIdsInput, and the no-op short-circuit (return false) correctly skips bumpAgenda when nothing changes. meetingFolders reused from the existing shared helper, consistent with other call sites.
  • addAgendaTasksInput's taskId/taskIds XOR union is exercised directly ({ taskIds: ["one"], taskId: "two" } -> 400).
  • User scoping and the agenda edit-lock are intact (editMeeting(..., "edit", ...)), tested for a non-owner and for an absorbed/read-only agenda.
  • Raw SQL for the shared condition lives in service.ts using the existing join/condition pattern already established for olderTasks, consistent with the file's prior style; values are parameter-bound.
  • Dialog: single POST + single optimistic update + single revalidation per add, tri-state group checkbox correctly scoped to currently-visible (searched) tasks, failure path keeps the full selection and the dialog open.
  • i18n: new string added to both en.json and cs-CZ.json.
  • CI on the head commit (873a35e3): Type Check, Build, Lint & Format, Test, CodeQL all green.

Missing tests

None found; the integration suite covers ordering, all-or-nothing, restore, dedup, active-skip, folder/visibility edge cases, concurrency (serialized and overlapping batches), and malformed input. Component tests cover group tri-state behavior, batched add/optimistic update, and failure/retry.

@novoj
novoj merged commit de79be2 into dev Nov 2, 2026
8 checks passed
@novoj
novoj deleted the fix/agenda-older-tasks-batch branch November 2, 2026 10:14
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