Two boards can have the same name in the same workspace #84
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#84
Loading…
Add table
Add a link
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?
Bug
The board-create flow places no constraint on board names within a workspace. A user can create two (or N) boards with identical names under the same
workspace_idand the home page will show both as separate cards with no way to tell them apart visually.Reproduction
All Workspacesand pick one in the modal).New Board, enterSprint planning, Create.New Boardagain in the same workspace, enterSprint planning, Create.Observed: both boards are created successfully and both appear in the list.
Expected: the second create fails with a clear error (
A board named "Sprint planning" already exists in this workspace), shown inline in the New Board modal so the user can adjust the name without losing their other input.The same issue applies to
board.update(renaming a board to collide with another live board in the same workspace) and toduplicateBoard(which currently appends(copy)once -- duplicating twice produces twoFoo (copy)boards).Root cause
crates/hero_whiteboard_server/src/db/queries.rs::create_board(around line 70) andupdate_board-- neither checks for an existing live board with the same(workspace_id, name).crates/hero_whiteboard_server/src/migrations/-- the boards table has no unique index on(workspace_id, name).crates/hero_whiteboard_server/src/handlers/board.rs--createandupdatedon't pre-validate uniqueness.crates/hero_whiteboard_ui/templates/web/home.html-- the New Board modal and the rename modal don't validate either; they call the RPC blindly.Affected files (expected)
crates/hero_whiteboard_server/src/migrations/006_unique_board_name.sql(new) -- partial unique index that ignores soft-deleted rows.crates/hero_whiteboard_server/src/db/mod.rs-- register the new migration.crates/hero_whiteboard_server/src/db/queries.rs-- a small helperboard_name_exists(conn, workspace_id, name, except_id) -> boolfor an explicit pre-flight check.crates/hero_whiteboard_server/src/handlers/board.rs--createandupdatereject duplicate names with"A board named \"<name>\" already exists in this workspace".crates/hero_whiteboard_server/tests(or inline) -- regression test that two creates with the same(workspace_id, name)errors on the second.crates/hero_whiteboard_ui/templates/web/home.html-- show the server's error message inline in the New Board modal (and the rename modal) instead of analert/silent failure.No SDK / openrpc edits required; the error path is already JSON-RPC-error-shape.
Expected behavior
name.alert.Acceptance criteria
boards (workspace_id, name) WHERE deleted_at IS NULL.board.createrejects an attempt to create a second board with the same(workspace_id, name)as a live board, with the messageA board named "<name>" already exists in this workspace.board.updaterejects renaming a board to a name already taken by another live board in the same workspace.board.delete, a new board with that name in the same workspace can be created.#new-board-error-style) and keeps the modal open for retry; same for the rename modal.board.list,board.get, or any other handler.cargo test --workspacepasses;cargo clippy --workspace -- -D warningsclean.(1),(2), ...) before creating the index. Document this behavior.Notes
WHERE deleted_at IS NULLmatches the soft-delete semantics already enforced elsewhere (get_board,is_board_live, etc.).sqlite_masteror a plain SELECT in the migration to detect collisions before creating the index, rather than letting the index creation itself fail on a populated DB.Implementation Spec for Issue #84
Objective
Enforce that two live boards in the same workspace cannot share the same
name. Reject duplicate(workspace_id, name)at the server (DB index + handler pre-check), and surface the rejection inline in the New Board / Rename modals on the home page.Requirements
boards (workspace_id, name) WHERE deleted_at IS NULL.board.createrejects duplicates withA board named "<name>" already exists in this workspace.board.updaterejects renames that collide with another live board in the same workspace; renaming a board to its current name is a no-op (no error).is_board_livesemantics).alert) and stay open for retry.Files to Modify / Create
crates/hero_whiteboard_server/src/migrations/006_unique_board_name.sql(new) — dedupe step (rename collisions to"<name> (N)"until unique) + partial unique index.crates/hero_whiteboard_server/src/db/mod.rs— register migration 006.crates/hero_whiteboard_server/src/db/queries.rs—board_name_taken(conn, workspace_id, name, except_id) -> rusqlite::Result<bool>helper.crates/hero_whiteboard_server/src/handlers/board.rs—createandupdatepre-flight check using the helper; consistent error message.crates/hero_whiteboard_server/src/handlers/object.rs— extend the existingtestsmodule with a duplicate-name regression test (or add a separatetestsmodule inboard.rs).crates/hero_whiteboard_ui/templates/web/home.html— show the server's error message inline in the New Board modal and the Rename modal.Implementation Plan
Step 1: Migration + queries helper
Files:
crates/hero_whiteboard_server/src/migrations/006_unique_board_name.sql(new),crates/hero_whiteboard_server/src/db/mod.rs,crates/hero_whiteboard_server/src/db/queries.rs.Create
006_unique_board_name.sql:ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)) to assign unique suffixes to all but the oldest row in each(workspace_id, name)group:"<name> (1)","<name> (2)", ... to the duplicates while leaving the oldest one alone. After this runs, every(workspace_id, name)pair among live rows is unique, so the index creation succeeds.In
db/mod.rs, appendM::up(include_str!("../migrations/006_unique_board_name.sql"))to the migration list.In
db/queries.rs, add the helper:Dependencies: none. Self-contained.
Step 2: Handler pre-flight check + regression test
Files:
crates/hero_whiteboard_server/src/handlers/board.rs,crates/hero_whiteboard_server/src/handlers/object.rs(test module).handlers/board.rs::create, after building theBoardstruct and beforequeries::create_board, do:handlers/board.rs::update, after fetching the existing board (and applying the optional name field), do:previous_nameis just the value ofboard.namebefore the optional update is applied — capture it before theif let Some(v) = params["name"].as_str() { board.name = v.to_string(); }block. Theboard.name != previous_nameshort-circuit is what makes "rename to current name" idempotent.handlers::object::testsmodule (or add an inlinetestsmodule inboard.rs) with aduplicate_board_name_rejectedtest that:Sprint planning.board.createwith the same name and asserts the call errors with the expected message.board.deleteon the original, then asserts a freshboard.createwith the same name now succeeds (soft-delete does not block reuse).Dependencies: Step 1.
Step 3: UI inline error
Files:
crates/hero_whiteboard_ui/templates/web/home.html.#new-board-errorregion (created earlier). MakesubmitNewBoard's catch path show the error inline instead of the existingalert: (If#new-board-errordoesn't already exist on this branch, add a small<div>matching the New Workspace modal's pattern.)#rename-error) styled the same way as the other modals' error regions. UpdatesaveRenameto show the error inline on failure and keep the modal open. Reset both regions on modal-open.Dependencies: Step 2 (so the error string is the one we surface).
Acceptance Criteria
board.createwith a duplicate(workspace_id, name)returns the messageA board named "<name>" already exists in this workspace.board.updaterejects renames that collide with another live board in the same workspace.board.delete, the same name can be reused for a new board in the same workspace.cargo test --workspacepasses.cargo clippy --workspace -- -D warningsclean.board.list,board.get, share/object/comment/connector handlers.Notes
EXISTS (...)is false on the second pass.WHERE deleted_at IS NULLso the same name can be re-used after a soft delete.UNIQUEconstraint error. The DB index is the safety net for race conditions.Test Results
cargo test --workspace: all green, including:handlers::object::tests::create_after_delete_returns_error(existing).handlers::object::tests::duplicate_board_name_rejected(new) — asserts: first create succeeds, duplicate fails with"already exists", name becomes reusable afterboard.delete.cargo clippy --workspace -- -D warnings: clean.cargo check --workspace: clean.Manual verification:
Sprint planningin workspace A. Try to create anotherSprint planningin workspace A — modal stays open with the inline errorA board named "Sprint planning" already exists in this workspace.Sprint planningin workspace B — succeeds (unique per workspace).Sprint planning, then create a new one with the same name — succeeds (soft-delete doesn't block reuse).Implementation Summary
6 files changed (5 modified + 1 new migration), +125 / -6.
Migration + queries (
crates/hero_whiteboard_server/)migrations/006_unique_board_name.sql(new): de-dupes any pre-existing collisions among live rows by appending" (1)"," (2)", ... to all but the oldest in each(workspace_id, name)group, then creates a partial unique indexidx_boards_workspace_name_live ON boards (workspace_id, name) WHERE deleted_at IS NULL. Idempotent.db/mod.rs: registered the new migration.db/queries.rs: addedboard_name_taken(conn, workspace_id, name, except_id)helper.Handlers (
crates/hero_whiteboard_server/src/handlers/board.rs)create: pre-flight check viaboard_name_taken(...); rejects duplicates withA board named "<name>" already exists in this workspace.update: snapshots the board's name before applying the optionalnamefield, then runs the same check withexcept_id = Some(board.id)so renaming to the current name is a no-op.Tests
handlers/object.rs::tests::duplicate_board_name_rejected(new): asserts the duplicate-create error and that the same name becomes reusable afterboard.delete.UI (
crates/hero_whiteboard_ui/templates/web/home.html)#new-board-errorinline region.submitNewBoardnow surfaces every error path (workspace name missing, workspace creation failure, no workspace selected, RPC failure including duplicate-name) inline instead of viawindow.alert.#rename-errorinline region;saveRenamenow shows server errors inline; modal stays open for retry. Empty-name validation is inline too.Verification
cargo test --workspace: all green; new test passes.cargo clippy --workspace -- -D warnings: clean.cargo check --workspace: clean.Notes / caveats
is_board_livesemantics elsewhere in the app).board.name != previous_name.