Do not show the Presentation share link when the board has no frames #198

Open
opened 2026-05-18 10:21:00 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

The share dialog always offers a "Presentation link" row even when the board has no frames. A presentation link for a frameless board presents nothing, so it should not be shown.

Confirmed current behavior

crates/hero_whiteboard_admin/templates/web/home.html renderRows(viewUrl, editUrl) (~lines 762-788) unconditionally builds three rows — View link, Edit link, and Presentation link (viewUrl + '?present=1'). There is no check for whether the board contains any frames before rendering the Presentation row.

Expected

  • The "Presentation link" row (and its Copy button) is only rendered when the board has at least one frame.
  • When the board has no frames, the View and Edit rows still render normally; only the Presentation row is omitted (optionally with a small muted hint like "Add a frame to enable presentation").
  • Any other place that surfaces a presentation/?present=1 link must apply the same rule — audit and cover them all (e.g. a presenter control-bar "copy presentation link" action if present).

Requirements

  • Determine the board's frame count at share-dialog render time without a heavy load (e.g. from already-cached board/object data, or a lightweight count) and gate the Presentation row on frameCount > 0.
  • Keep the share dialog responsive — if frame count must be fetched, render the View/Edit rows immediately and resolve the Presentation row when the count is known (do not block the whole dialog).
  • No server or schema change expected; if a count is needed, reuse existing object/board data the client already has or already fetches. Confirm during planning.
  • Backwards compatible: boards with frames behave exactly as today.

Notes

  • Inspect how the share modal already loads/caches data (shareUrlsCache, the RPC it issues) to find the cheapest way to know the frame count.
  • Frames are objects of type frame; the client may already have them via the loaded board (when opened from the board) or may need a count when sharing from the boards list (home page) where the board is not open — handle both entry points.
  • Deploy reminder: assets embed at compile time via rust-embed; after editing, touch crates/hero_whiteboard_admin/src/assets.rs before cargo build --release -p hero_whiteboard_admin, and verify the served asset changed before testing.
## Summary The share dialog always offers a "Presentation link" row even when the board has no frames. A presentation link for a frameless board presents nothing, so it should not be shown. ## Confirmed current behavior `crates/hero_whiteboard_admin/templates/web/home.html` `renderRows(viewUrl, editUrl)` (~lines 762-788) unconditionally builds three rows — View link, Edit link, and Presentation link (`viewUrl + '?present=1'`). There is no check for whether the board contains any frames before rendering the Presentation row. ## Expected - The "Presentation link" row (and its Copy button) is only rendered when the board has at least one frame. - When the board has no frames, the View and Edit rows still render normally; only the Presentation row is omitted (optionally with a small muted hint like "Add a frame to enable presentation"). - Any other place that surfaces a presentation/`?present=1` link must apply the same rule — audit and cover them all (e.g. a presenter control-bar "copy presentation link" action if present). ## Requirements - Determine the board's frame count at share-dialog render time without a heavy load (e.g. from already-cached board/object data, or a lightweight count) and gate the Presentation row on `frameCount > 0`. - Keep the share dialog responsive — if frame count must be fetched, render the View/Edit rows immediately and resolve the Presentation row when the count is known (do not block the whole dialog). - No server or schema change expected; if a count is needed, reuse existing object/board data the client already has or already fetches. Confirm during planning. - Backwards compatible: boards with frames behave exactly as today. ## Notes - Inspect how the share modal already loads/caches data (`shareUrlsCache`, the RPC it issues) to find the cheapest way to know the frame count. - Frames are objects of type `frame`; the client may already have them via the loaded board (when opened from the board) or may need a count when sharing from the boards list (home page) where the board is not open — handle both entry points. - Deploy reminder: assets embed at compile time via rust-embed; after editing, `touch crates/hero_whiteboard_admin/src/assets.rs` before `cargo build --release -p hero_whiteboard_admin`, and verify the served asset changed before testing.
Author
Member

Implementation Spec for Issue #198

Objective

Stop advertising a Presentation share link (viewUrl + '?present=1') for boards with zero frames (a presentation with no frames is non-functional). View and Edit rows always render and render immediately; the Presentation row is gated on the board having >= 1 frame (object with type === 'frame'). Boards with frames keep today's exact behavior.

Requirements

  • Home-page share modal renders View and Edit rows synchronously, never blocked on a frame count.
  • Presentation row rendered only when the board has >= 1 frame; otherwise omitted with a small muted hint ("Add a frame to enable presentation").
  • Frame count from object.list client-side, counting type === 'frame' (no server/schema change).
  • Resolved count cached alongside shareUrlsCache so reopening for the same board is instant (no re-fetch, no flicker).
  • All other ?present=1 emitters audited and gated with the same rule.
  • Backwards compatible: a board with frames is identical to today.

Files to Modify/Create

  • crates/hero_whiteboard_admin/templates/web/home.html - gate the Presentation row in the home-page share modal; add frame-count fetch + cache.
  • crates/hero_whiteboard_admin/templates/web/board.html - gate the Presentation row in the in-board "Share Board" dialog (shareReadonly, ~460-464) and audit the presenter control-bar "Copy presentation link" button (pres-share, ~573-596).
  • crates/hero_whiteboard_admin/src/assets.rs - not edited; only touched so rust-embed re-embeds the changed templates.

Implementation Plan

Step 1: Frame-count helper + cache field (home.html)

Files: crates/hero_whiteboard_admin/templates/web/home.html

  • shareUrlsCache[boardId] entries may carry a frameCount number (no structural change).
  • Add async function getFrameCount(boardId) near openShareModal (~before :751): return cached frameCount if present; else var objs = await rpcCall('object.list', { board_id: boardId }); then objs.filter(o => o && o.type === 'frame').length, store it back on the cache entry, return it. On RPC failure return null (treated as omit the row).
    Dependencies: none

Step 2: Split renderRows so the Presentation row is separately injectable (home.html ~762-788)

Files: crates/hero_whiteboard_admin/templates/web/home.html

  • renderRows(viewUrl, editUrl) renders only View (~764-771) + Edit (~772-779) then appends a stable placeholder <div id="share-present-slot"></div> where the Presentation row OR hint goes; remove the unconditional Presentation block (~780-787).
  • Add function renderPresentRow(viewUrl, frameCount) targeting #share-present-slot: if frameCount >= 1 set slot innerHTML to the original Presentation block markup byte-for-byte (label + readonly viewUrl + '?present=1' input + Copy); else a single muted line "Add a frame to enable presentation." Reserving the slot up front prevents layout shift.
    Dependencies: Step 1

Step 3: Wire async resolution into openShareModal (home.html ~751-816)

Files: crates/hero_whiteboard_admin/templates/web/home.html

  • Cache-hit path (~791-792): after renderRows(cached.viewUrl, cached.editUrl) do not early-return; if typeof cached.frameCount === 'number' call renderPresentRow(cached.viewUrl, cached.frameCount) synchronously, else getFrameCount(boardId).then(n => renderPresentRow(cached.viewUrl, n)).
  • Fetch path (~795-810): after renderRows(viewUrl, editUrl) (View/Edit visible immediately) run getFrameCount(boardId).then(n => renderPresentRow(viewUrl, n)).catch(() => renderPresentRow(viewUrl, null)); — not awaited before rows show.
  • Stale-result guard: renderPresentRow re-reads document.getElementById('share-present-slot') and no-ops if missing (modal closed / different board).
    Dependencies: Steps 1, 2

Step 4: Gate the in-board "Share Board" dialog (board.html shareReadonly ~437-473)

Files: crates/hero_whiteboard_admin/templates/web/board.html

  • Opened from inside an open board → objects already in memory. Count frames from WhiteboardObjects.getAllObjects() entries with type === 'frame' (in-memory frame entries are tagged type:'frame', objects.js:1245; pattern used at sync.js:169); fall back to an object.list count only if the objects API is unavailable.
  • Build the Presentation block (~460-464) only when count >= 1; else the same muted hint. View/Edit (~450-459) always render. Otherwise markup unchanged.
  • Parallelizable with Steps 1-3 (different file).
    Dependencies: none

Files: crates/hero_whiteboard_admin/templates/web/board.html

  • The pres-share button only exists while presentation mode runs (body.wb-presenting), which requires >= 1 frame. Confirm WhiteboardFrames.startPresentation cannot start with zero frames (it returns early on no frames). If confirmed, add a one-line comment noting the state.total >= 1 invariant — no behavioral change. If it CAN start with zero frames, guard the click handler to toast "Add a frame to enable presentation." and return.
    Dependencies: none (parallel)

Step 6: Re-embed and verify

Files: crates/hero_whiteboard_admin/src/assets.rs

  • After template edits: touch crates/hero_whiteboard_admin/src/assets.rs; cargo build --release -p hero_whiteboard_admin; verify the served HTML contains the new slot/gating before testing (rust-embed embeds at compile time).
    Dependencies: Steps 1-5

Acceptance Criteria

  • Presentation row hidden (replaced by muted "Add a frame to enable presentation" hint) when board has 0 frames; View/Edit still shown.
  • Presentation row shown with the exact same markup/URL as today when board has >= 1 frame.
  • Share dialog renders View/Edit immediately; the frame-count lookup never blocks them.
  • Count cached on shareUrlsCache[boardId] so reopening for the same board is instant (no extra RPC, no flicker/layout shift).
  • All ?present=1 emitters covered: home-page modal, in-board Share dialog, presenter control-bar copy button.
  • No server/openrpc/schema change; JS/template-only.

Notes

  • No count-only RPC exists in openrpc.json; only object.list({board_id}) returning all objects. Frames are objects with type === 'frame' (sync.js:169, objects.js:1245). Filter client-side. Payload concern on dense boards is acceptable: one-shot on share-modal open, cached after first open, comparable to existing full object.list flows (home.html:621 duplicateBoard); no lighter alternative without a new server method (forbidden). In-board surfaces use the already-loaded in-memory model (no RPC).
  • No layout jank: the slot is reserved at initial render; the async result only fills the pre-existing container; renderPresentRow no-ops if the slot is gone (stale late response).
  • ?present=1 emitters: (1) home.html renderRows (~783-785), (2) board.html shareReadonly (~460-464), (3) board.html pres-share control bar (~573-596, builds /s/<token>?present=1). board_view.html references are read-side audience detection, not link emission.
  • Backwards-compat: add a sibling frameCount to the existing shareUrlsCache[boardId] object; existing if (cached) readers unaffected; frames>=1 emits byte-identical markup. Unknown (RPC fail) → omit + hint, per the issue.
  • Deploy: rust-embed embeds at compile time; after editing the .html, touch crates/hero_whiteboard_admin/src/assets.rs then cargo build --release -p hero_whiteboard_admin, verify served HTML changed before testing.
## Implementation Spec for Issue #198 ### Objective Stop advertising a Presentation share link (`viewUrl + '?present=1'`) for boards with zero frames (a presentation with no frames is non-functional). View and Edit rows always render and render immediately; the Presentation row is gated on the board having >= 1 frame (object with `type === 'frame'`). Boards with frames keep today's exact behavior. ### Requirements - Home-page share modal renders View and Edit rows synchronously, never blocked on a frame count. - Presentation row rendered only when the board has >= 1 frame; otherwise omitted with a small muted hint ("Add a frame to enable presentation"). - Frame count from `object.list` client-side, counting `type === 'frame'` (no server/schema change). - Resolved count cached alongside `shareUrlsCache` so reopening for the same board is instant (no re-fetch, no flicker). - All other `?present=1` emitters audited and gated with the same rule. - Backwards compatible: a board with frames is identical to today. ### Files to Modify/Create - `crates/hero_whiteboard_admin/templates/web/home.html` - gate the Presentation row in the home-page share modal; add frame-count fetch + cache. - `crates/hero_whiteboard_admin/templates/web/board.html` - gate the Presentation row in the in-board "Share Board" dialog (`shareReadonly`, ~460-464) and audit the presenter control-bar "Copy presentation link" button (`pres-share`, ~573-596). - `crates/hero_whiteboard_admin/src/assets.rs` - not edited; only `touch`ed so rust-embed re-embeds the changed templates. ### Implementation Plan #### Step 1: Frame-count helper + cache field (home.html) Files: `crates/hero_whiteboard_admin/templates/web/home.html` - `shareUrlsCache[boardId]` entries may carry a `frameCount` number (no structural change). - Add `async function getFrameCount(boardId)` near `openShareModal` (~before :751): return cached `frameCount` if present; else `var objs = await rpcCall('object.list', { board_id: boardId });` then `objs.filter(o => o && o.type === 'frame').length`, store it back on the cache entry, return it. On RPC failure return `null` (treated as omit the row). Dependencies: none #### Step 2: Split renderRows so the Presentation row is separately injectable (home.html ~762-788) Files: `crates/hero_whiteboard_admin/templates/web/home.html` - `renderRows(viewUrl, editUrl)` renders only View (~764-771) + Edit (~772-779) then appends a stable placeholder `<div id="share-present-slot"></div>` where the Presentation row OR hint goes; remove the unconditional Presentation block (~780-787). - Add `function renderPresentRow(viewUrl, frameCount)` targeting `#share-present-slot`: if `frameCount >= 1` set slot innerHTML to the original Presentation block markup byte-for-byte (label + readonly `viewUrl + '?present=1'` input + Copy); else a single muted line "Add a frame to enable presentation." Reserving the slot up front prevents layout shift. Dependencies: Step 1 #### Step 3: Wire async resolution into openShareModal (home.html ~751-816) Files: `crates/hero_whiteboard_admin/templates/web/home.html` - Cache-hit path (~791-792): after `renderRows(cached.viewUrl, cached.editUrl)` do not early-return; if `typeof cached.frameCount === 'number'` call `renderPresentRow(cached.viewUrl, cached.frameCount)` synchronously, else `getFrameCount(boardId).then(n => renderPresentRow(cached.viewUrl, n))`. - Fetch path (~795-810): after `renderRows(viewUrl, editUrl)` (View/Edit visible immediately) run `getFrameCount(boardId).then(n => renderPresentRow(viewUrl, n)).catch(() => renderPresentRow(viewUrl, null));` — not awaited before rows show. - Stale-result guard: `renderPresentRow` re-reads `document.getElementById('share-present-slot')` and no-ops if missing (modal closed / different board). Dependencies: Steps 1, 2 #### Step 4: Gate the in-board "Share Board" dialog (board.html shareReadonly ~437-473) Files: `crates/hero_whiteboard_admin/templates/web/board.html` - Opened from inside an open board → objects already in memory. Count frames from `WhiteboardObjects.getAllObjects()` entries with `type === 'frame'` (in-memory frame entries are tagged `type:'frame'`, objects.js:1245; pattern used at sync.js:169); fall back to an `object.list` count only if the objects API is unavailable. - Build the Presentation block (~460-464) only when count >= 1; else the same muted hint. View/Edit (~450-459) always render. Otherwise markup unchanged. - Parallelizable with Steps 1-3 (different file). Dependencies: none #### Step 5: Audit the presenter control-bar "Copy presentation link" button (board.html pres-share ~573-596) Files: `crates/hero_whiteboard_admin/templates/web/board.html` - The `pres-share` button only exists while presentation mode runs (`body.wb-presenting`), which requires >= 1 frame. Confirm `WhiteboardFrames.startPresentation` cannot start with zero frames (it returns early on no frames). If confirmed, add a one-line comment noting the `state.total >= 1` invariant — no behavioral change. If it CAN start with zero frames, guard the click handler to toast "Add a frame to enable presentation." and return. Dependencies: none (parallel) #### Step 6: Re-embed and verify Files: `crates/hero_whiteboard_admin/src/assets.rs` - After template edits: `touch crates/hero_whiteboard_admin/src/assets.rs`; `cargo build --release -p hero_whiteboard_admin`; verify the served HTML contains the new slot/gating before testing (rust-embed embeds at compile time). Dependencies: Steps 1-5 ### Acceptance Criteria - [ ] Presentation row hidden (replaced by muted "Add a frame to enable presentation" hint) when board has 0 frames; View/Edit still shown. - [ ] Presentation row shown with the exact same markup/URL as today when board has >= 1 frame. - [ ] Share dialog renders View/Edit immediately; the frame-count lookup never blocks them. - [ ] Count cached on `shareUrlsCache[boardId]` so reopening for the same board is instant (no extra RPC, no flicker/layout shift). - [ ] All `?present=1` emitters covered: home-page modal, in-board Share dialog, presenter control-bar copy button. - [ ] No server/openrpc/schema change; JS/template-only. ### Notes - No count-only RPC exists in openrpc.json; only `object.list({board_id})` returning all objects. Frames are objects with `type === 'frame'` (sync.js:169, objects.js:1245). Filter client-side. Payload concern on dense boards is acceptable: one-shot on share-modal open, cached after first open, comparable to existing full `object.list` flows (home.html:621 duplicateBoard); no lighter alternative without a new server method (forbidden). In-board surfaces use the already-loaded in-memory model (no RPC). - No layout jank: the slot is reserved at initial render; the async result only fills the pre-existing container; `renderPresentRow` no-ops if the slot is gone (stale late response). - `?present=1` emitters: (1) home.html renderRows (~783-785), (2) board.html shareReadonly (~460-464), (3) board.html pres-share control bar (~573-596, builds `/s/<token>?present=1`). `board_view.html` references are read-side audience detection, not link emission. - Backwards-compat: add a sibling `frameCount` to the existing `shareUrlsCache[boardId]` object; existing `if (cached)` readers unaffected; frames>=1 emits byte-identical markup. Unknown (RPC fail) → omit + hint, per the issue. - Deploy: rust-embed embeds at compile time; after editing the .html, `touch crates/hero_whiteboard_admin/src/assets.rs` then `cargo build --release -p hero_whiteboard_admin`, verify served HTML changed before testing.
Author
Member

Test Results

  • Total: 0
  • Passed: 0
  • Failed: 0

cargo test --workspace --lib: ok - all 4 lib targets compiled and passed (0 tests defined; suite is the regression gate)
inline script syntax (home.html, board.html): ok

Note: #198 is a template/JS-only change (gate the Presentation share link on board frame count). No JS unit harness exists in this repo; the Rust suite is the regression gate and the share-dialog gating is verified manually in-browser.

## Test Results - Total: 0 - Passed: 0 - Failed: 0 cargo test --workspace --lib: ok - all 4 lib targets compiled and passed (0 tests defined; suite is the regression gate) inline script syntax (home.html, board.html): ok Note: #198 is a template/JS-only change (gate the Presentation share link on board frame count). No JS unit harness exists in this repo; the Rust suite is the regression gate and the share-dialog gating is verified manually in-browser.
Author
Member

Implementation Summary

Template/JS-only. The Presentation share link is now gated on the board having at least one frame; View and Edit links always render and are never blocked on the frame count. No server or schema change.

Changes

home.html (home-page share modal)

  • Added getFrameCount(boardId): returns a cached count if present, else object.list({board_id}) filtered to type === 'frame', cached back onto shareUrlsCache[boardId].frameCount (RPC failure -> null = omit the row).
  • renderRows now renders only View and Edit rows plus a stable empty #share-present-slot; the unconditional Presentation block was removed.
  • renderPresentRow(viewUrl, frameCount): fills the slot with the byte-identical original Presentation row when frameCount >= 1, otherwise a muted "Add a frame to enable presentation." hint; no-ops if the slot is gone (stale-result guard).
  • openShareModal resolves the present row asynchronously on both the cache-hit and fetch paths without blocking View/Edit; the slot is reserved up front so there is no layout shift.

board.html (in-board "Share Board" dialog)

  • shareReadonly counts frames from the already-loaded in-memory model (WhiteboardObjects.getAllObjects() entries with type === 'frame'); the Presentation block renders only when frameCount >= 1, otherwise the same muted hint. View/Edit unchanged. No extra RPC.
  • Presenter control-bar "Copy presentation link" button: confirmed it is implicitly safe — WhiteboardFrames.startPresentation() returns early when there are no frames, so the control bar (and that button) only exists with >= 1 frame. Added an explanatory comment; no behavioral change.

Frame-having boards render byte-identical markup to before.

Behavior after change

  • Sharing a board with no frames: View and Edit links shown; the Presentation row is replaced by a muted "Add a frame to enable presentation." hint.
  • Sharing a board with >= 1 frame: unchanged (Presentation link present as before).
  • Share dialog stays responsive: View/Edit paint immediately; the count is resolved in the background and cached so reopening is instant with no flicker or layout shift.

Tests

  • cargo test --workspace --lib: green, no Rust regression (template/JS-only; no JS unit harness in repo).
  • Inline script syntax for home.html and board.html: ok (node --check on the extracted blocks).
  • Share-dialog gating on frameless vs framed boards verified manually in-browser after a forced-embed rebuild and redeploy.
## Implementation Summary Template/JS-only. The Presentation share link is now gated on the board having at least one frame; View and Edit links always render and are never blocked on the frame count. No server or schema change. ### Changes home.html (home-page share modal) - Added getFrameCount(boardId): returns a cached count if present, else object.list({board_id}) filtered to type === 'frame', cached back onto shareUrlsCache[boardId].frameCount (RPC failure -> null = omit the row). - renderRows now renders only View and Edit rows plus a stable empty #share-present-slot; the unconditional Presentation block was removed. - renderPresentRow(viewUrl, frameCount): fills the slot with the byte-identical original Presentation row when frameCount >= 1, otherwise a muted "Add a frame to enable presentation." hint; no-ops if the slot is gone (stale-result guard). - openShareModal resolves the present row asynchronously on both the cache-hit and fetch paths without blocking View/Edit; the slot is reserved up front so there is no layout shift. board.html (in-board "Share Board" dialog) - shareReadonly counts frames from the already-loaded in-memory model (WhiteboardObjects.getAllObjects() entries with type === 'frame'); the Presentation block renders only when frameCount >= 1, otherwise the same muted hint. View/Edit unchanged. No extra RPC. - Presenter control-bar "Copy presentation link" button: confirmed it is implicitly safe — WhiteboardFrames.startPresentation() returns early when there are no frames, so the control bar (and that button) only exists with >= 1 frame. Added an explanatory comment; no behavioral change. Frame-having boards render byte-identical markup to before. ### Behavior after change - Sharing a board with no frames: View and Edit links shown; the Presentation row is replaced by a muted "Add a frame to enable presentation." hint. - Sharing a board with >= 1 frame: unchanged (Presentation link present as before). - Share dialog stays responsive: View/Edit paint immediately; the count is resolved in the background and cached so reopening is instant with no flicker or layout shift. ### Tests - cargo test --workspace --lib: green, no Rust regression (template/JS-only; no JS unit harness in repo). - Inline script syntax for home.html and board.html: ok (node --check on the extracted blocks). - Share-dialog gating on frameless vs framed boards verified manually in-browser after a forced-embed rebuild and redeploy.
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#198
No description provided.