Support importing a board from an exported JSON file #204
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#204
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
The app can export a board to JSON (
{ board, objects, connectors, exported_at, version }) but there is no way to import it back. Add JSON import so an exported board round-trips.Scope
Allow a user to pick a previously exported
.jsonfile and recreate its contents.parent_frame_id) must be remapped to the new frame id using the id map, after frames are created.from_id/to_idthrough the id map. Skip connectors whose endpoints are missing and report how many were skipped.version; reject unknown/incompatible versions with a clear message. Guard against very large files with a sane size limit and user feedback.Requirements
Acceptance Criteria
Notes
The whiteboard frontend is vanilla JS modules under the admin crate's static web assets, embedded via rust-embed (rebuild required for asset changes). Object/connector/board creation already exists as JSON-RPC methods used by the live editor and the SDK. Reuse the same data shapes the export produced (it intentionally mirrors the server payloads) and the same creation paths the editor uses rather than introducing a parallel model. Determine the cleanest place for the Import entry by inspecting how boards are currently created/listed.
Implementation Spec for Issue #204
Objective
Add a client-side "Import board" feature to the boards list (
home.html) that reads a JSON file previously produced by the exporter, creates a brand-new board from it (never touching any existing board), recreates all objects and connectors via the existing JSON-RPC methods so the server mints fresh ids, remapsparent_frame_id/from_id/to_idthrough an old-id→new-id map, reports progress and a final summary, and navigates to the new board.Requirements
file.size > 10MB); top-level object withversion === 1;boardobject;objects/connectorsarrays; each object has anidand stringtype.data.board.name(fallback "Imported Board"), sanitized (trim, collapse whitespace, strip control chars, cap ~120). On server duplicate-name error, retry once with a suffix.workspace.create);board.createrequiresworkspace_id+name.board_id = newBoard.id, dropid, omitparent_frame_idon pass 1, callobject.create, recordidMap[oldId] = result.id.parent_frame_id, if mapped,object.updatethe newly created object with the remapped parent; else leave unparented and count it.from_id/to_idviaidMap; missing endpoint → skip + count; elseconnector.createwith{ board_id, from_id, to_id, line_type: <line_style>, style: JSON.stringify({stroke, strokeWidth}) }.WB_BASE + '/board/' + newBoard.id.rpcCall.Files to Modify/Create
crates/hero_whiteboard_admin/templates/web/home.html— Import button + hidden file input next to "New Board"; an import status/progress region reusing existing modal styling; import orchestration in the existing inline<script>(nearsubmitNewBoard/duplicateBoard).Implementation Plan
(Single tightly-coupled file — sequential, not parallelizable.)
Step 1: Add the Import UI controls
Files:
templates/web/home.html#import-board-btn("Import") and a hidden<input type="file" id="import-file-input" accept="application/json,.json">.#new-board-modal(import-prefixed ids) or reuse it parameterized; add an#import-statusregion (reuse modal styling) for progress + final summary.Dependencies: none
Step 2: Validation + parse helper
Files:
templates/web/home.html(inline script)validateImportPayload(text)→{ok,data,error}: size already checked on the File;JSON.parsein try/catch; assert shape,version===1,boardobject, arrays, per-objectid+type. AddsanitizeBoardName(raw).Dependencies: Step 1
Step 3: Import orchestration
Files:
templates/web/home.html(inline script)runImport(data, wsId)modeled onduplicateBoard+submitNewBoard:board.create(+duplicate-name retry); pass-1 frames-first object loop buildingidMap(stripid, setboard_id, omitparent_frame_id); pass-2object.updateparent remap for new-board objects only; connector loop with remap + skip counting; update#import-statuseach iteration; final summary; persist workspace likesubmitNewBoard; navigate to/board/{id}.#import-board-btn→ file input; onchangeread file (size guard), validate, then workspace-pick →runImport.Dependencies: Steps 1, 2
Step 4: Error-handling polish
Files:
templates/web/home.html(inline script)change.Dependencies: Step 3
Acceptance Criteria
parent_frame_idremapped (frames before children + second pass); orphaned-parent objects created unparented and counted.connector.createwith remapped endpoints; missing-endpoint connectors skipped + counted;line_style→line_type,stroke/stroke_width→stylemapping applied.version!==1→ clear error, nothing created (noboard.create)./board/{newId}.home.htmlchanged.Notes
rpcCall(method, params)(rpc.js, already loaded by home.html).board.create→{workspace_id, name, ...}, returns board atresult.id, unique-name-per-workspace enforced (handle duplicate with suffix retry).object.createaccepts the full object payload, ignoresid, returnsresult.id;serializeForServerkeys map 1:1 — only transforms: replaceboard_id, dropid, deferparent_frame_id.object.update {id, ...}used only on newly created objects.connector.create {board_id, from_id, to_id, line_type, style, ...}returnsresult.id; map exportedline_style/stroke/stroke_widthas above (mirrors connectors.js createConnector). Board-open URLWB_BASE + '/board/' + id.submitNewBoard()(workspace pick + create + navigate) andduplicateBoard()(board.create then looped object.create withdelete obj.id; obj.board_id = newBoard.id). Client-side orchestration is sufficient and consistent — no server-side import method needed.from_id/to_idneed remapping.home.htmlis rust-embed-embedded; rebuildhero_whiteboard_adminto serve changes; no static file added soassets.rsuntouched. Verify via curl over the admin unix socket for/.Test Results
Note: this feature is a single HTML/inline-JS template change (no Rust source changed); workspace lib tests run as a regression guard.
Implementation Summary
JSON board import, fully client-side, on the boards list.
Changes
crates/hero_whiteboard_admin/templates/web/home.html(only file changed):#import-modal(workspace pick, mirroring the New Board modal) and an#import-statusprogress/summary region.validateImportPayload(text): JSON parse + shape/version === 1/array checks;sanitizeImportName; a 10 MB file-size guard before reading. Any failure shows a clear message and creates nothing.runImport(data, wsId, rawName):board.create(with duplicate-name retry), frames-first object recreation viaobject.createbuilding an old-id to new-id map, a second pass that remapsparent_frame_idviaobject.updateon the newly created objects only, then connector recreation viaconnector.createwithfrom_id/to_idremapped (connectors with an unmapped endpoint are skipped and counted). Final summary (objects created, connectors created, connectors skipped, objects unparented), workspace persisted like the New Board flow, then navigation to the new board.Behavior
Test results
cargo test --workspace --lib(matches CI): compiled cleanly, no failures (single HTML/inline-JS template change; run as a regression guard).node --checkon the inline script: OK.home.htmlverified UTF-8 with zero control bytes.Notes
styleis sent as{ stroke, strokeWidth }; the editor's connector loader accepts both object and string style, so it round-trips.Scope update
Import was added to a second surface. The initial implementation placed Import only on the end-user boards list, but board management is done from the admin dashboard, so the control was not reachable there.
Final state:
crates/hero_whiteboard_admin/templates/web/home.html— Import button + workspace modal + client-side import on the end-user boards list.crates/hero_whiteboard_admin/templates/index.htmlandcrates/hero_whiteboard_admin/static/js/dashboard.js— equivalent Import control on the admin dashboard (next to Create Board), reusing the dashboard's own rpcCall, Bootstrap modal, toast, and list-refresh patterns.Both paths use the same algorithm: validate (shape/version/size), create a new board, recreate objects via the create RPCs with an old-id to new-id map (frames first, parent_frame_id remapped), recreate connectors with endpoints remapped (missing-endpoint connectors skipped and counted), no existing board modified, summary reported. Verified working on the admin dashboard.