Home page: no UI to delete a workspace; cascade should remove its boards and notify open editors #85
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#85
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 boards home page does not expose any way to delete a workspace. Users can create workspaces inline (
+ New workspace...in the filter dropdown and the New Board modal) but cannot remove one once it's no longer wanted, except by hopping over to the admin dashboard.When a workspace is deleted, every board inside it should be deleted too. The DB schema already declares
boards.workspace_id ... REFERENCES workspaces(id) ON DELETE CASCADE, so a hardDELETE FROM workspacesdoes cascade-remove the boards -- but without aboard.deletedWebSocket broadcast for each cascaded board, any user already editing one of those boards keeps trying to sync. They eventually catch the failure via the rpc.js fallback we landed for issue #83, but they should be told immediately, the same way a single-board deletion already informs them.Reproduction
Delete workspaceoption, no trash icon next to the selector, no menu, nothing./index.html, find the workspace, and delete it from there. After that, related boards vanish from the home grid (cascade) but any open editor session on one of those boards stays interactive until its next sync errors.Affected files (expected)
crates/hero_whiteboard_server/src/handlers/workspace.rs--deleteshould return the cascaded board ids alongside the existing{deleted: N}shape so the UI proxy can broadcast.crates/hero_whiteboard_server/src/db/queries.rs-- a small helper to list the live board ids belonging to the workspace, before the cascade fires.crates/hero_whiteboard_ui/src/routes.rs-- extend the existingrpc_proxysniffer (today:method == "board.delete") to also handlemethod == "workspace.delete". After a successful response, broadcast{type: "board.deleted", board_id}for each id inresult.board_ids.crates/hero_whiteboard_ui/templates/web/home.html-- add a Delete affordance next to the workspace dropdown when a specific workspace is selected (notAll Workspacesand not+ New workspace...). Open a themed confirmation modal that names the workspace and warns the boards inside will also be deleted; on confirm callworkspace.delete. After success, resetcurrentWorkspaceId = '', refresh the dropdown, refresh the board list. Match the styling of the existingdelete-board-modal(red destructive button, inline error region, Enter/Escape handlers).No SDK / openrpc edits required; the JSON-RPC error path is already in shape.
Expected behavior
Delete workspace "Sprint planning"? This will also delete all N boards inside it. This cannot be undone.{deleted: 1, board_ids: [...]}listing the cascaded board ids.board.deletedfor each id so any open editor closes via the existing overlay flow.All Workspaces, clears the localStorage entry, refreshes the dropdown and the board list.alert); the modal stays open for retry.Acceptance criteria
All Workspacesand never shown for the+ New workspace...sentinel.board.list).workspace.delete; on success closes the modal, broadcastsboard.deletedfor every cascaded board id, resets the filter toAll Workspaces, refreshes the dropdown and the board list.workspace.deletereturns{deleted: 1, board_ids: [...]}listing the boards that were cascade-deleted.rpc_proxysniffer broadcastsboard.deletedfor each cascaded id; existing single-board delete behavior is unchanged.Board deletedoverlay (already wired in board.html / sync.js).workspace.deletereturns the cascaded board ids and that the boards no longer satisfyis_board_live.cargo test --workspacepasses;cargo clippy --workspace -- -D warningsclean;cargo fmt --all -- --checkclean.Notes
DELETE FROM workspaces-- once the cascade fires, the rows are gone and we can't enumerate them anymore.delete-board-modal(red Delete button,var(--wb-error)).board.createwill recreate it. If that turns out to be confusing UX, a separate issue can add a guard.Implementation Spec for Issue #85
Objective
Give the home page a workspace-delete affordance that hard-removes the workspace, lets the FK cascade tear down its boards, and broadcasts
board.deletedto every open editor on those boards so they close out cleanly via the existing notice flow.Requirements
All Workspacesand not+ New workspace...).workspace.delete. The server returns{deleted: 1, board_ids: [...]}listing the cascaded board ids.rpc_proxysniffer (which already handlesboard.delete) is extended to also handleworkspace.deleteand broadcast{type: "board.deleted", board_id}for each id inresult.board_ids.currentWorkspaceId = '', clear the localStorage entry, refresh the dropdown and the board list.workspace.deletereturns the cascaded board ids; subsequentis_board_livefor any of them returnsfalse.cargo fmt --all -- --checkclean (CI catches this).Files to Modify
crates/hero_whiteboard_server/src/db/queries.rs—live_board_ids_in_workspace(conn, workspace_id) -> Vec<u64>helper.crates/hero_whiteboard_server/src/handlers/workspace.rs::delete— capture live board ids first, then delete the workspace; return bothdeletedandboard_ids.crates/hero_whiteboard_server/src/handlers/object.rs(test module) — extend withworkspace_delete_returns_cascaded_board_ids.crates/hero_whiteboard_ui/src/routes.rs::rpc_proxy— generalize the existing sniff/broadcast block so it also recognizesworkspace.deleteand broadcasts on each id inresult.board_ids.crates/hero_whiteboard_ui/templates/web/home.html— Delete-workspace button + confirmation modal + handlers; reset state after delete.No SDK / openrpc edits.
Implementation Plan
Step 1: Server-side
workspace.deletereturns cascaded board ids + regression testFiles:
crates/hero_whiteboard_server/src/db/queries.rs,crates/hero_whiteboard_server/src/handlers/workspace.rs,crates/hero_whiteboard_server/src/handlers/object.rs(tests).db/queries.rs, add:handlers/workspace.rs::delete, capture the ids before issuing the delete: The cascade fires onDELETE FROM workspaces; we just snapshot the ids before letting it run so the proxy can broadcast.handlers/object.rs::tests, add:Dependencies: none.
Step 2: rpc_proxy broadcasts
board.deletedfor each cascaded idFiles:
crates/hero_whiteboard_ui/src/routes.rs.method == "board.delete"and broadcasts on success. Generalize:"board.delete": behavior unchanged (single id fromparams.id, broadcast one event)."workspace.delete": after a 200 response, parseresult.board_ids(an array of u64) and broadcast oneboard.deletedper id.BroadcastMsg { sender_id: 0, ... }sentinel is reused -- no changes tows.rsare needed.Recommended structure:
Dependencies: Step 1 (response shape).
Step 3: UI button + confirmation modal
Files:
crates/hero_whiteboard_ui/templates/web/home.html.<select>. Render only whencurrentWorkspaceIdis a real id (not''and not'__new__'). Wireonclick="openDeleteWorkspaceModal()".delete-workspace-modalblock matching the styling ofdelete-board-modal:Delete Workspace.Delete workspace "<name>"?plus, when board count > 0,This will also delete <N> board(s) inside it..This cannot be undone.line.#delete-workspace-errorregion.var(--wb-error)) buttons.openDeleteWorkspaceModal(): looks up the current workspace name fromworkspacesCache, callsrpcCall('board.list', { workspace_id: id })to count, populates the prompt, opens the modal, clears the error region, focuses Delete.closeDeleteWorkspaceModal(): hides the modal.submitDeleteWorkspace(): callsrpcCall('workspace.delete', { id }). On success closes the modal, resetscurrentWorkspaceId = '', removes localStorage entry, callsloadWorkspaces()andloadBoards(). On error shows inline.currentWorkspaceIdchanges -- update it inloadWorkspaces,onWorkspaceChange, and aftersubmitDeleteWorkspacesucceeds.Dependencies: Step 2 (so confirmed deletes propagate to open editors).
Acceptance Criteria
{deleted, board_ids}; the UI proxy broadcastsboard.deletedfor each id; the home page resets toAll Workspacesand refreshes.Board deletedoverlay.workspace_delete_returns_cascaded_board_ids).cargo test --workspace,cargo clippy --workspace -- -D warnings,cargo fmt --all -- --checkall pass.Notes
DELETE FROM workspacesruns; the FK cascade hard-deletes the rows, so we can't enumerate them after the fact.delete-board-modal(red Delete button onvar(--wb-error),#ffftext).board.create. Allow it to be deleted; do not special-case it.Test Results
cargo test --workspace: all green, including the newhandlers::object::tests::workspace_delete_returns_cascaded_board_idsregression test (3 tests in the module total).cargo clippy --workspace -- -D warnings: clean.cargo fmt --all -- --check: clean.cargo check --workspace: clean.The new test seeds two boards in a workspace, calls
workspace.delete, asserts the response contains both board ids inboard_ids, and thatis_board_livereturns false for each (they were cascaded away).Manual verification recommended:
Implementation Summary
5 files changed, +232 / -31.
Server (
crates/hero_whiteboard_server/)db/queries.rs: addedlive_board_ids_in_workspace(conn, workspace_id) -> Vec<u64>so the handler can snapshot the live boards in a workspace before the FK cascade removes them.handlers/workspace.rs::delete: now snapshots the cascaded board ids first, then issuesDELETE FROM workspaces(which cascades). Response shape is{deleted: 1, board_ids: [...]}so the UI proxy can broadcast.handlers/object.rs::tests: addedworkspace_delete_returns_cascaded_board_ids(asserts the response contains the expected ids and that the boards no longer satisfyis_board_live).UI proxy (
crates/hero_whiteboard_ui/src/routes.rs)rpc_proxygeneralized from a single-method (board.delete) sniffer to a small enum that also recognizesworkspace.delete. After a successful response, it broadcasts{type: "board.deleted", board_id: <id>}to every relevant channel — one event forboard.delete(fromparams.id), N events forworkspace.delete(fromresult.board_ids).UI (
crates/hero_whiteboard_ui/templates/web/home.html)updateDeleteWorkspaceBtn(); only shown when a real workspace is currently selected (notAll Workspaces, never the+ New workspace...sentinel).delete-workspace-modalmatching thedelete-board-modalstyling: workspace name in the prompt, optional cascade count line (best-effortboard.listlookup), inline error region, red destructive Delete button (var(--wb-error)), Cancel button.openDeleteWorkspaceModal()/closeDeleteWorkspaceModal()/submitDeleteWorkspace(). On success: closes the modal, resetscurrentWorkspaceIdto'', removes the localStorage entry, refreshes the dropdown and the board list. On failure: error inline, modal stays open.loadWorkspacesnow falls back toAll Workspaceswhen the cachedcurrentWorkspaceIdis no longer in the list (handles the just-deleted-workspace case on reload).Verification
cargo test --workspace: all green.cargo clippy --workspace -- -D warnings: clean.cargo fmt --all -- --check: clean.cargo check --workspace: clean.Notes / caveats
DELETE FROM workspaceshard-removes the board rows, so we snapshot the ids first.board.createif missing, so deleting it is allowed (no special-casing).Board deletedoverlay (already wired in #83's work).