Workspaces are not routeable — add workspace to the URL scheme (home filter + board routes) #213

Closed
opened 2026-05-21 12:56:06 +00:00 by AhmedHanafy725 · 2 comments

Workspaces are not routeable — add workspace to the URL scheme (home filter + board routes)

Summary

The app can deep-link to a board (/board/{id}) but not to a workspace. Workspace selection exists only as client-side state on the home page, and board URLs carry no workspace context. This makes a selected workspace impossible to bookmark or share, and gives no navigation between a board and its parent workspace.

Confirmed findings (current state)

  • The backend already models the relationship: Board.workspace_id (crates/hero_whiteboard_server/src/db/models.rs), board.create accepts workspace_id (handlers/board.rs), board.list filters by workspace_id (handlers/board.rs), and workspace.list / workspace.get exist.
  • The end-user home page (/, templates/web/home.html) already has a workspace dropdown (workspace-select), groups boards into per-workspace sections, and calls workspace.list + board.list({workspace_id}). But the chosen workspace is held only in JS state — it is not reflected in the URL, so reloading or sharing the page loses it.
  • UI routes (crates/hero_whiteboard_admin/src/routes.rs) are limited to /, /board/{id}, /board/{id}/view, /s/{token}, /board/{id}/mindmap/{object_id}. There is no workspace route, and the board route has no workspace segment/param. The board page (web_board / BoardTemplate) does not surface which workspace the board belongs to.

Goal

Make workspaces routeable and add workspace context to board navigation, reusing the existing backend (no schema or RPC changes expected):

  • A way to deep-link the home page to a specific workspace (so the filter survives reload/share).
  • Workspace context on the board route so a board can show and link back to its parent workspace.

Open design decision (to be settled in the implementation spec)

Exact URL scheme — options to weigh against the repo's hero_ui_routes deep-linking spec:

  • Home deep-link via query (/?workspace={id}) vs a dedicated workspace landing route (/workspace/{id}).
  • Board workspace context via path (/workspace/{ws}/board/{id}) vs query (/board/{id}?workspace={ws}) vs resolving the workspace from board.get and adding a back-link without changing the board path.

Acceptance criteria

  • Selecting a workspace on the home page updates the URL so the selection is preserved on reload and when the link is shared.
  • Opening that URL directly restores the same workspace filter.
  • A board view exposes its parent workspace (name + a link back to that workspace's board list).
  • Existing links (/, /board/{id}, /s/{token}) keep working with no regression.
  • No backend schema or RPC changes required (workspace_id already available end to end).

Notes

Conform to the repository's hero_ui_routes UI routeability / deep-linking specification when choosing the scheme. Keep /s/{token} shared-board links workspace-agnostic.

## Workspaces are not routeable — add workspace to the URL scheme (home filter + board routes) ### Summary The app can deep-link to a board (`/board/{id}`) but not to a workspace. Workspace selection exists only as client-side state on the home page, and board URLs carry no workspace context. This makes a selected workspace impossible to bookmark or share, and gives no navigation between a board and its parent workspace. ### Confirmed findings (current state) - The backend already models the relationship: `Board.workspace_id` (`crates/hero_whiteboard_server/src/db/models.rs`), `board.create` accepts `workspace_id` (`handlers/board.rs`), `board.list` filters by `workspace_id` (`handlers/board.rs`), and `workspace.list` / `workspace.get` exist. - The end-user home page (`/`, `templates/web/home.html`) already has a workspace dropdown (`workspace-select`), groups boards into per-workspace sections, and calls `workspace.list` + `board.list({workspace_id})`. But the chosen workspace is held only in JS state — it is not reflected in the URL, so reloading or sharing the page loses it. - UI routes (`crates/hero_whiteboard_admin/src/routes.rs`) are limited to `/`, `/board/{id}`, `/board/{id}/view`, `/s/{token}`, `/board/{id}/mindmap/{object_id}`. There is no workspace route, and the board route has no workspace segment/param. The board page (`web_board` / `BoardTemplate`) does not surface which workspace the board belongs to. ### Goal Make workspaces routeable and add workspace context to board navigation, reusing the existing backend (no schema or RPC changes expected): - A way to deep-link the home page to a specific workspace (so the filter survives reload/share). - Workspace context on the board route so a board can show and link back to its parent workspace. ### Open design decision (to be settled in the implementation spec) Exact URL scheme — options to weigh against the repo's `hero_ui_routes` deep-linking spec: - Home deep-link via query (`/?workspace={id}`) vs a dedicated workspace landing route (`/workspace/{id}`). - Board workspace context via path (`/workspace/{ws}/board/{id}`) vs query (`/board/{id}?workspace={ws}`) vs resolving the workspace from `board.get` and adding a back-link without changing the board path. ### Acceptance criteria - [ ] Selecting a workspace on the home page updates the URL so the selection is preserved on reload and when the link is shared. - [ ] Opening that URL directly restores the same workspace filter. - [ ] A board view exposes its parent workspace (name + a link back to that workspace's board list). - [ ] Existing links (`/`, `/board/{id}`, `/s/{token}`) keep working with no regression. - [ ] No backend schema or RPC changes required (workspace_id already available end to end). ### Notes Conform to the repository's `hero_ui_routes` UI routeability / deep-linking specification when choosing the scheme. Keep `/s/{token}` shared-board links workspace-agnostic.
Author
Owner

Implementation Spec for Issue #213

Objective

Make workspaces routeable so the home page's workspace filter and a board's parent-workspace context are encoded in the URL. No backend schema/RPC changes, no new server routes. /s/{token} shared links stay workspace-agnostic; existing / and /board/{id} links keep working; all links remain base_path-prefixed.

Chosen URL scheme

  • Home workspace filter: query parameter on the existing home route — /?workspace={id} (/ with no param = "All Workspaces").
  • Board workspace context: resolved from board.get (returns workspace_id); the board page renders a workspace breadcrumb linking back to /?workspace={workspace_id}. No workspace segment added to the board path.

Rationale: the home page is a single collection filtered by workspace, so the canonical pattern (and what the hero_ui_routes deep-linking spec endorses for filtered collection views) is a query filter, not a separate route. This adds zero server routes and zero backend changes, avoids breaking existing /board/{id} links, and makes the board to workspace relationship a copyable link rather than hidden state.

Files to Modify (frontend only)

  • templates/web/home.html - read ?workspace= on load to seed the filter; write the param on dropdown change via history.replaceState; URL takes precedence over localStorage (kept as fallback for bare /).
  • templates/web/board.html - add a hidden-by-default workspace breadcrumb anchor in the navbar before #board-name.
  • static/web/js/whiteboard/app.js - in loadBoard() after board.get, resolve the workspace name via workspace.get, populate the breadcrumb, and point it + the back-arrow at WB_BASE + '/?workspace=' + workspace_id; suppress on /s/{token} pages.

Implementation Plan

  1. Home: read ?workspace= on load (seed currentWorkspaceId; localStorage only when param absent). Files: home.html. Deps: none.
  2. Home: write workspace to URL on filter change via replaceState (omit query for "All Workspaces"; skip the __new__ sentinel). Files: home.html. Deps: 1.
  3. Home: when a URL-supplied workspace no longer exists, fall back to "All Workspaces" and clear the stale param. Files: home.html. Deps: 1-2.
  4. Board: add hidden breadcrumb markup in .wb-navbar. Files: board.html. Deps: none.
  5. Board: resolve workspace from board.get + workspace.get, populate breadcrumb, set breadcrumb/back-arrow href to /?workspace={id}; guard against /s/{token}. Files: app.js. Deps: 4.
  6. Verify base_path on all new links; confirm /, /board/{id} unchanged and /s/{token} shows no workspace UI. Deps: 1-5.
  7. Release rebuild to re-embed assets/templates. Deps: 1-6.

Acceptance Criteria

  • /?workspace={id} selects that workspace and lists only its boards on first paint.
  • / (no param) shows the "All Workspaces" grouped view.
  • Changing the dropdown updates the address bar (or strips the param) without a full reload.
  • Copying the filtered home URL and reopening it reproduces the same view.
  • On /board/{id} the top bar shows WorkspaceName / BoardName; the workspace name links to /?workspace={workspace_id}.
  • The back-arrow goes to /?workspace={workspace_id} once resolved (and to / if resolution fails).
  • A ?workspace={id} for a deleted workspace falls back to "All Workspaces" and clears the stale param.
  • /s/{token} shows no workspace breadcrumb and adds no workspace param.
  • All links are base_path-prefixed; no .rs changes; no new routes; existing links still work.

Notes

board.get already returns workspace_id and workspace.get already exists, so the board page resolves both via existing /rpc calls. localStorage stays a fallback for bare /. Use replaceState (not pushState) for filter changes. Set workspace/board names via textContent (XSS-safe). Rebuild required (rust-embed + Askama compile at build time).

## Implementation Spec for Issue #213 ### Objective Make workspaces routeable so the home page's workspace filter and a board's parent-workspace context are encoded in the URL. No backend schema/RPC changes, no new server routes. `/s/{token}` shared links stay workspace-agnostic; existing `/` and `/board/{id}` links keep working; all links remain base_path-prefixed. ### Chosen URL scheme - Home workspace filter: query parameter on the existing home route — `/?workspace={id}` (`/` with no param = "All Workspaces"). - Board workspace context: resolved from `board.get` (returns `workspace_id`); the board page renders a workspace breadcrumb linking back to `/?workspace={workspace_id}`. No workspace segment added to the board path. Rationale: the home page is a single collection filtered by workspace, so the canonical pattern (and what the `hero_ui_routes` deep-linking spec endorses for filtered collection views) is a query filter, not a separate route. This adds zero server routes and zero backend changes, avoids breaking existing `/board/{id}` links, and makes the board to workspace relationship a copyable link rather than hidden state. ### Files to Modify (frontend only) - `templates/web/home.html` - read `?workspace=` on load to seed the filter; write the param on dropdown change via `history.replaceState`; URL takes precedence over `localStorage` (kept as fallback for bare `/`). - `templates/web/board.html` - add a hidden-by-default workspace breadcrumb anchor in the navbar before `#board-name`. - `static/web/js/whiteboard/app.js` - in `loadBoard()` after `board.get`, resolve the workspace name via `workspace.get`, populate the breadcrumb, and point it + the back-arrow at `WB_BASE + '/?workspace=' + workspace_id`; suppress on `/s/{token}` pages. ### Implementation Plan 1. Home: read `?workspace=` on load (seed `currentWorkspaceId`; localStorage only when param absent). Files: home.html. Deps: none. 2. Home: write workspace to URL on filter change via `replaceState` (omit query for "All Workspaces"; skip the `__new__` sentinel). Files: home.html. Deps: 1. 3. Home: when a URL-supplied workspace no longer exists, fall back to "All Workspaces" and clear the stale param. Files: home.html. Deps: 1-2. 4. Board: add hidden breadcrumb markup in `.wb-navbar`. Files: board.html. Deps: none. 5. Board: resolve workspace from `board.get` + `workspace.get`, populate breadcrumb, set breadcrumb/back-arrow href to `/?workspace={id}`; guard against `/s/{token}`. Files: app.js. Deps: 4. 6. Verify base_path on all new links; confirm `/`, `/board/{id}` unchanged and `/s/{token}` shows no workspace UI. Deps: 1-5. 7. Release rebuild to re-embed assets/templates. Deps: 1-6. ### Acceptance Criteria - [ ] `/?workspace={id}` selects that workspace and lists only its boards on first paint. - [ ] `/` (no param) shows the "All Workspaces" grouped view. - [ ] Changing the dropdown updates the address bar (or strips the param) without a full reload. - [ ] Copying the filtered home URL and reopening it reproduces the same view. - [ ] On `/board/{id}` the top bar shows `WorkspaceName / BoardName`; the workspace name links to `/?workspace={workspace_id}`. - [ ] The back-arrow goes to `/?workspace={workspace_id}` once resolved (and to `/` if resolution fails). - [ ] A `?workspace={id}` for a deleted workspace falls back to "All Workspaces" and clears the stale param. - [ ] `/s/{token}` shows no workspace breadcrumb and adds no workspace param. - [ ] All links are base_path-prefixed; no `.rs` changes; no new routes; existing links still work. ### Notes `board.get` already returns `workspace_id` and `workspace.get` already exists, so the board page resolves both via existing `/rpc` calls. `localStorage` stays a fallback for bare `/`. Use `replaceState` (not `pushState`) for filter changes. Set workspace/board names via `textContent` (XSS-safe). Rebuild required (rust-embed + Askama compile at build time).
Author
Owner

Implemented and verified

Frontend-only, as specified — no backend, no new server routes.

Changes

  • templates/web/home.html
    • On load, ?workspace={id} seeds the filter (URL is source of truth; localStorage is the fallback only when the param is absent).
    • New syncWorkspaceUrl() writes /?workspace={id} (or strips it for "All Workspaces") via history.replaceState on dropdown change and inline workspace creation.
    • When a URL-supplied workspace no longer exists, it falls back to "All Workspaces" and clears the stale param.
  • templates/web/board.html
    • Added a hidden-by-default workspace breadcrumb (#board-workspace-link + separator) before the board name, and gave the back-arrow an id (#board-back-link).
  • static/web/js/whiteboard/app.js
    • In loadBoard(), after board.get, resolves the workspace via workspace.get and renders WorkspaceName / BoardName, pointing both the breadcrumb and the back-arrow at /?workspace={workspace_id}. Suppressed on /s/{token} shared pages.

Verification

Rust workspace lib tests (no regression): cargo test --workspace --lib -> ok. 0 failed.

Live checks against the running service (admin.sock):

  • GET /?workspace=1 -> HTTP 200; served home contains syncWorkspaceUrl.
  • Served app.js contains _renderWorkspaceCrumb; served /board/{id} contains the breadcrumb element.
  • RPC chain used by the breadcrumb: board.list -> board 79, board.get -> workspace_id=1, workspace.get -> name "My Workspace". So /board/79 renders "My Workspace / gggggdddddd" linking to /?workspace=1.

Deployed locally (release build, installed, service restarted).

Manual check needed (no browser automation here)

Open home, pick a workspace -> the URL gains ?workspace={id}; reload/share reproduces the filter. Open a board -> top bar shows Workspace / Board; the workspace name and back-arrow return to the filtered home. Open an /s/{token} link -> no workspace breadcrumb.

## Implemented and verified Frontend-only, as specified — no backend, no new server routes. ### Changes - `templates/web/home.html` - On load, `?workspace={id}` seeds the filter (URL is source of truth; `localStorage` is the fallback only when the param is absent). - New `syncWorkspaceUrl()` writes `/?workspace={id}` (or strips it for "All Workspaces") via `history.replaceState` on dropdown change and inline workspace creation. - When a URL-supplied workspace no longer exists, it falls back to "All Workspaces" and clears the stale param. - `templates/web/board.html` - Added a hidden-by-default workspace breadcrumb (`#board-workspace-link` + separator) before the board name, and gave the back-arrow an id (`#board-back-link`). - `static/web/js/whiteboard/app.js` - In `loadBoard()`, after `board.get`, resolves the workspace via `workspace.get` and renders `WorkspaceName / BoardName`, pointing both the breadcrumb and the back-arrow at `/?workspace={workspace_id}`. Suppressed on `/s/{token}` shared pages. ### Verification Rust workspace lib tests (no regression): `cargo test --workspace --lib -> ok. 0 failed`. Live checks against the running service (admin.sock): - `GET /?workspace=1` -> HTTP 200; served home contains `syncWorkspaceUrl`. - Served `app.js` contains `_renderWorkspaceCrumb`; served `/board/{id}` contains the breadcrumb element. - RPC chain used by the breadcrumb: `board.list` -> board 79, `board.get` -> `workspace_id=1`, `workspace.get` -> name "My Workspace". So `/board/79` renders "My Workspace / gggggdddddd" linking to `/?workspace=1`. Deployed locally (release build, installed, service restarted). ### Manual check needed (no browser automation here) Open home, pick a workspace -> the URL gains `?workspace={id}`; reload/share reproduces the filter. Open a board -> top bar shows `Workspace / Board`; the workspace name and back-arrow return to the filtered home. Open an `/s/{token}` link -> no workspace breadcrumb.
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
lhumina_code/hero_whiteboard#213
No description provided.