Floating toolbar: edit multiple same-type items at once, with group actions shown alongside #209
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#209
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?
Summary
When the user selects multiple items of the same type (e.g. several sticky notes, several text blocks, several shapes), the floating selection toolbar should render the per-type editing controls (color, font, stroke width, etc.) and apply each change to every selected item at once. The existing multi-select group actions (Lock / Group / Ungroup) should remain visible alongside the per-type controls so the user gets both.
Scope
type(e.g. all'sticky'): show the same per-type controls that appear today for a single item, but every change applies to all selected items in one history step.Editing semantics
Acceptance Criteria
Notes
The floating toolbar lives in
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js. Today it dispatches viaupdate(nodes)and renders single-item controls in_renderForNode(node)(around line 230+), with_renderMultiSelection()handling the multi-select group/lock cluster. The fix should reuse the per-type renderers rather than duplicate them: extend each per-type renderer (or wrap it in a small helper) so its property handlers apply to every selected item of that type when invoked from the multi-select branch. Indeterminate state should be a tiny convention (e.g. adata-mixedclass or empty value) rather than separate "mixed" widgets per type. Frontend assets are embedded via rust-embed; rebuild the admin crate to pick up JS changes.Implementation Spec for Issue #209
Objective
When 2+ items of the same non-comment type are selected, the floating selection toolbar renders the per-type editor controls (color, font size, stroke width, alignment, etc.) alongside the existing multi-select group/lock cluster. Every change applies to all selected items as a single undo step. Mixed-type and all-comment selections behave as today.
Requirements
update(nodes): a new homogeneous-multi path triggers whencachedNodes.length >= 2 && !allComments && _distinctTypes(cachedNodes) === 1AND the shared type is inV1_MULTI_TYPES = { sticky, text, shape, drawing }._renderMultiSelection()(Lock + Group/Ungroup) renders first (before the per-type controls) so users see both clusters.WhiteboardHistory.batch(...)for one-step undo.cachedNodes[0]). Indeterminate/mixed visualization is out of scope for v1 (deferred to a follow-up).V1_MULTI_TYPESbecause no per-type controls survive multi mode for it.WhiteboardSync.onUpdatefires for every mutated node; the existing debounce/batch coalesces.refresh()(re-render after handler-triggered mutations like align) must preserve the multi-target context. Use a_lastTargetsstash + restore around_renderForNode.Files to Modify/Create
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js— single file change.objects.js,history.js, templates.Implementation Plan
Step 1: Add multi-target state +
_applyToTargetshelperFiles:
selection_toolbar.jsvar _targetNodes = null; var _lastTargets = null;andvar V1_MULTI_TYPES = { sticky: true, text: true, shape: true, drawing: true };near other module-level vars._applyToTargets(mutateFn)afterpersistToolbarMutation:_filterUnlocked(nodes)near_allLockedreturning the subset whose ids aren't locked.Dependencies: none
Step 2: Refactor in-scope per-type renderers to route mutations through
_applyToTargetsFiles:
selection_toolbar.js_renderSticky(1228-1272),_renderText(1274-1313),_renderShape(1315-1380),_renderDrawing(1382-1406), replace the existingsnapshotBefore(node.id()); /*mutate*/ commitUpdate(node.id());(Pattern A) orpersistToolbarMutation(node)(Pattern B) pattern with_applyToTargets(function(n) { /*mutate against n; for Pattern B end with WhiteboardSync.onUpdate(n)*/ });.snapshotBefore/commitUpdate/batch; mutator bodies still own canvas redraws and Pattern B'sWhiteboardSync.onUpdate(n)(Pattern A'sWhiteboardObjects.rerenderXxx(n)already covers sync)._targetNodes === null→ applicator falls back to[cachedNode].Dependencies: Step 1
Step 3: Gate per-instance UI by multi mode
Files:
selection_toolbar.jsvar isMulti = !!(_targetNodes && _targetNodes.length > 1);.if (!isMulti) { ... }.Dependencies: Step 2 (same hunks)
Step 4: Dispatcher branch for homogeneous multi-select
Files:
selection_toolbar.jsupdate(nodes)(~line 250)refresh()(~line 397), if_lastTargets && _lastTargets.length >= 2, restore_targetNodes = _lastTargets;before re-rendering and clear after.update(nodes)(aftercachedNodesrebuild) and inhide(), reset_targetNodes = null; _lastTargets = null;.Dependencies: Steps 1-3
Step 5: Build + manual verify
touch crates/hero_whiteboard_admin/src/assets.rs && cargo build --release -p hero_whiteboard_admin.Acceptance Criteria
refresh()after a multi-edit handler keeps the multi-target context (no degradation to single).WhiteboardSync.onUpdatefires for every mutated node so peers see the same change set.Notes
cachedNodes[0]is acceptable for v1. Follow-up: add_isUniform(cachedNodes, getter)+wb-input-mixedCSS class on triggers when values disagree._applyToTargetsover a shared mutator swap: existing renderers don't go through a shared per-node mutator — they're inline closures. Routing each handler through the applicator is the smallest correct refactor and is the canonical extension point if more types are added later._lastTargetsstash is the minimal correct change; clearing it on everyupdate()/hide()prevents leaking across selection changes.Implementation Summary
The floating selection toolbar now supports homogeneous multi-select editing for sticky / text / shape / drawing, with the existing Lock and Group/Ungroup cluster rendered alongside.
Changes
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js:_targetNodes(live targets while rendering) and_lastTargets(stashed forrefresh()), and aV1_MULTI_TYPES = { sticky, text, shape, drawing }table._filterUnlocked(nodes)helper and_applyToTargets(mutateFn)helper. The applicator wraps aWhiteboardHistory.batch(...)and, for each target, runssnapshotBefore->mutateFn(n)->commitUpdate; locked nodes are skipped._renderSticky/_renderText/_renderShape/_renderDrawingto route through_applyToTargets(function(n) { ... }). Pattern A handlers end withWhiteboardObjects.rerenderXxx(n)(sync via re-render); Pattern B handlers end withbatchDraw()+WhiteboardSync.onUpdate(n)(the applicator now owns history bracketing, replacing the previouspersistToolbarMutation).isMultiflag in those renderers to suppress the per-instance "Edit text" pencil button when 2+ items are selected.update(nodes)clears_targetNodes/_lastTargetsat entry, then in the multi-select branch (when_distinctTypes(cachedNodes) === 1and the shared type is inV1_MULTI_TYPES) populates them with the unlocked subset and calls_renderForNode(rep)— so the existing_renderMultiSelection()group cluster renders first, then the per-type controls.refresh()stashes/restores_targetNodesfrom_lastTargetsso handler-triggered re-renders keep multi context.hide()clears both.Behavior
WhiteboardSync.onUpdatefires for every mutated node so peers see the same change set.Test results
cargo test --workspace --lib: compiled cleanly, no failures (change is JS-only; regression guard).node --check selection_toolbar.js: OK. File encoding clean (UTF-8, zero control bytes).selection_toolbar.jscontains the new helpers and refactored handlers (20 references to_applyToTargets/V1_MULTI_TYPESetc.).Notes
cachedNodes[0]). A follow-up should add an_isUniform(cachedNodes, getter)check and awb-input-mixedCSS class on triggers when values disagree.V1_MULTI_TYPES: its only per-type controls (title editor, slide order buttons) are per-instance, so including it would add code paths without user-visible benefit.