Home page: group boards by workspace when "All Workspaces" is selected #86
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#86
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?
Improvement
When the workspace filter dropdown is on
All Workspaces, the home page renders every board in a single flat grid sorted by recency. There is no visual cue showing which workspace each board belongs to. As soon as a user has more than a couple of workspaces, scanning the grid becomes confusing — two boards with similar names from different workspaces look identical, and there's no way to mentally group what you're seeing.When a specific workspace is selected the grid is fine; the issue only appears for the "all" view.
Best practice
Products in the same space (Miro, Trello, Linear, Notion's database views, GitHub project boards) all solve this the same way: when listing entities across multiple parents, group by parent with section headers. Specifically:
updated_at DESCorder.The alternative — a small workspace badge on each card — is what mail clients like Gmail do for labels, but it scales poorly here: workspace names are often long, the grid is dense, and the user usually wants to think about "the boards in workspace X" as a unit.
Affected files
crates/hero_whiteboard_ui/templates/web/home.html--loadBoards()(around lines 240+) is where the rendering happens. The change is contained to this function plus a small CSS tweak in the<style>block at the top.No server / SDK / openrpc / DB changes. The
workspacesCache(already populated byloadWorkspaces) gives us the workspace name from theworkspace_idalready on each board.Expected behavior
currentWorkspaceId === ''(All Workspaces): boards are grouped by workspace. Each group has a header showing<workspace name> · <count>(e.g.Sprint planning · 4). Sections appear in the same order as the dropdown (most-recently-updated workspace first). Boards inside a section preserve the current updated-at-desc order.currentWorkspaceIdset to a real id: the flat grid is rendered, exactly as it does today (no headers).workspace_iddoesn't appear inworkspacesCache(e.g. a workspace that was just deleted but the cache is stale) is grouped under a fallback headerWorkspace #<id>-- never lost.Acceptance criteria
All Workspacesis selected, board cards are grouped under per-workspace section headers showing the workspace name and the board count.updated_at DESCorder.workspace_idis not inworkspacesCacheare grouped under aWorkspace #<id>fallback header at the end of the list.var(--wb-...)tokens only).crates/hero_whiteboard_ui/templates/web/home.html.cargo check --workspace,cargo clippy --workspace -- -D warnings,cargo fmt --all -- --check,cargo test --workspaceall pass.Notes
.board-gridclass for the per-section grid so the card layout (column count, gap, hover affordance) is unchanged. Add a small wrapper class (e.g..workspace-section) and a header class (e.g..workspace-section-header).var(--wb-text-muted)with the count, top margin of ~16px between sections. Match the styling of the existingLoading boards...placeholder so it doesn't compete with the navbar.loadWorkspacesandloadBoardsalready run concurrently fromDOMContentLoaded. IfworkspacesCachehappens to be empty whenloadBoardsruns first (race), the fallbackWorkspace #<id>headers cover the gap and the next render afterloadWorkspacesfinishes will resolve the names. To avoid the flicker,loadBoards()canawait loadWorkspaces()first when the cache is empty -- the spec should pick one approach and document it.Implementation Spec for Issue #86
Objective
When the workspace filter on the home page is
All Workspaces, render boards grouped by workspace under section headers; when a specific workspace is selected, keep the current flat grid.Requirements
<workspace name> · <count>.workspace.listreturnsupdated_at DESC).updated_at DESCorder fromboard.list.workspace_idis not inworkspacesCacheare grouped underWorkspace #<id>at the end.var(--wb-...)tokens only — works in light + dark themes.crates/hero_whiteboard_ui/templates/web/home.html.cargo check / clippy / fmt --check / testclean.Files to Modify
crates/hero_whiteboard_ui/templates/web/home.html—loadBoards()and a small CSS addition in{% block head %}.No server / SDK / openrpc / DB changes;
workspacesCachealready gives the UI everything it needs.Implementation Plan
Step 1: Group boards by workspace in
loadBoardsand add section-header CSSFiles:
crates/hero_whiteboard_ui/templates/web/home.htmlExtract the per-card render into a small inner helper
renderBoardCard(board, gridEl)that builds the card and appends it to a passed-in grid element. Today the loop builds the card inline and appends it togrid; pulling the inner block into a helper keeps both code paths (flat and grouped) readable.Make the workspaces cache available before the first render. The cleanest fix is to
await loadWorkspaces()from insideloadBoards()whenworkspacesCacheis empty. The DOMContentLoaded init already calls both, but races aren't guaranteed to favor workspaces-first. Adding the await inloadBoardsis the smallest change and avoids theWorkspace #<id>flicker.After the existing
boardsfetch, branch oncurrentWorkspaceId:Build the rendered output. Iterate
workspacesCachein order so section order matches the dropdown. For each workspace that has boards, render a.workspace-sectioncontaining a.workspace-section-headerand a fresh.board-grid; the helper appends each card to that grid. Append orphan groups at the end (one per distinct id) with the fallback nameWorkspace #<id>.CSS — add a small block in the existing
{% block head %}<style>:Reuse the existing
.board-gridclass for the inner grids so the column / gap / hover behavior is identical.Empty state — keep the existing
boards.length === 0early-return that renders the centeredNo boards yetplaceholder. That still goes inside the outer#board-gridand shows whether we're filtered or not.Dependencies: none.
Acceptance Criteria
All Workspacesis selected, boards are grouped under per-workspace section headers (<name> · <count>).updated_at DESCorder.workspace_idis not inworkspacesCacheare grouped underWorkspace #<id>at the end.crates/hero_whiteboard_ui/templates/web/home.html.cargo check / clippy / fmt --check / testall pass.Notes
loadBoards()shouldawait loadWorkspaces()when the cache is empty, so the first render after a hard refresh already has names. After that the cache is populated and subsequent renders are instant.document.createElement/appendChildin this branch — building the outer DOM via string concatenation gets messy with nested grids. The per-card render can keep usingcard.innerHTML = '...'as it does today.Workspace #<id>) is rare in practice but cheap to handle and avoids dropping boards from the view.<details>wrapper around each section.Test Results
cargo test --workspace: all green (no test changes needed; this is a UI-only behavior).cargo clippy --workspace -- -D warnings: clean.cargo fmt --all -- --check: clean.cargo check --workspace: clean.Manual verification recommended:
All Workspacesselected — boards appear under per-workspace section headers (<workspace name> · <count>).updated_at DESCorder.All Workspaces— grouped layout returns immediately.No boards yet) renders correctly.workspace_idisn't in the cache, it shows up under aWorkspace #<id>section at the end.Implementation Summary
1 file changed, +105 / -27 — all in
crates/hero_whiteboard_ui/templates/web/home.html.Changes
renderBoardCard(board, gridEl)and added a thinrenderBoardSection(title, boards)helper.loadBoardsnow branches on the filter:groupedclass from#board-gridand renders a flat grid (behavior unchanged).groupedto#board-grid, builds a stack of.workspace-sectionelements (header + inner.board-grid) ordered byworkspacesCache. Boards whose workspace isn't in the cache fall through toWorkspace #<id>orphan sections at the end.loadBoardsawaitsloadWorkspaces()when the cache is empty, so the first render after a hard refresh already has workspace names (no flicker / no fallback labels for normal cases).#board-grid.groupedbecomes a vertical stack (no inner column grid), each.workspace-sectioncarries the horizontal padding that the flat grid normally would, and the inner per-section.board-gridkeeps its column layout but loses its outer padding so the header aligns with the cards.No boards yetempty state explicitly removes thegroupedclass so thegrid-column:1/-1layout still works.Verification
cargo test --workspace: all green.cargo clippy --workspace -- -D warnings: clean.cargo fmt --all -- --check: clean.cargo check --workspace: clean.Notes / caveats
workspacesCachealready populated byloadWorkspacesis the only source of truth for the section names.workspace.listreturnsupdated_at DESC)..workspace-sectionin a<details>and persist the open/closed state per workspace id.