Support importing a board from an exported JSON file #204

Open
opened 2026-05-19 10:48:11 +00:00 by AhmedHanafy725 · 4 comments
Member

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 .json file and recreate its contents.

  • Target: import creates a new board from the file (do not overwrite or merge into an existing board for this first version — a new board avoids id-collision and destructive edits). After import, navigate the user to the new board.
  • Objects: recreate every object via the existing object-create RPC so the server assigns fresh ids. Preserve type, position, size, rotation, z-index, style, text, and data. Build an old-id to new-id map.
  • Frames / parent references: objects that reference a parent frame (parent_frame_id) must be remapped to the new frame id using the id map, after frames are created.
  • Connectors: recreate connectors via the existing connector-create RPC, remapping from_id/to_id through the id map. Skip connectors whose endpoints are missing and report how many were skipped.
  • Validation: verify the file is well-formed JSON with the expected shape and a supported version; reject unknown/incompatible versions with a clear message. Guard against very large files with a sane size limit and user feedback.
  • UI: an Import entry reachable from the same area a user would naturally start a board from (the boards list / dashboard is preferred; if an in-board entry is added it must also create a new board, not mutate the current one). Use a normal file input; show progress and a final summary (objects created, connectors created, anything skipped).

Requirements

  • Round-trips with the existing JSON export: exporting a board then importing the file reproduces an equivalent board (same objects/connectors, visually equivalent layout).
  • All creation goes through existing JSON-RPC methods/SDK; no new network ports or listeners; any new server method must follow the existing Unix-socket JSON-RPC pattern. Prefer doing the orchestration client-side using already-exposed RPCs (board.create, object.create, connector.create) if sufficient.
  • The id remap must correctly handle frame parent references and connector endpoints.
  • Import must not corrupt or modify any existing board.
  • Clear, non-blocking feedback during and after import; failures leave no half-created board, or if partial creation is unavoidable, the summary states exactly what was created.
  • Sanitized, sensible name for the new board (e.g. derived from the file's board name, with a suffix if needed).

Acceptance Criteria

  • Exporting a board with objects, frames (with child objects), and connectors to JSON, then importing that file, produces a new board containing all objects and connectors with correct relative positions and intact frame/connector relationships.
  • The original/source board is unchanged.
  • Importing a malformed file or unsupported version shows a clear error and creates nothing.
  • Connectors with missing endpoints are skipped and counted in the summary rather than aborting the whole import.
  • After a successful import the user lands on the new board.
  • No new network listeners/ports are introduced.

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.

## 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 `.json` file and recreate its contents. - **Target:** import creates a **new board** from the file (do not overwrite or merge into an existing board for this first version — a new board avoids id-collision and destructive edits). After import, navigate the user to the new board. - **Objects:** recreate every object via the existing object-create RPC so the server assigns fresh ids. Preserve type, position, size, rotation, z-index, style, text, and data. Build an old-id to new-id map. - **Frames / parent references:** objects that reference a parent frame (`parent_frame_id`) must be remapped to the new frame id using the id map, after frames are created. - **Connectors:** recreate connectors via the existing connector-create RPC, remapping `from_id`/`to_id` through the id map. Skip connectors whose endpoints are missing and report how many were skipped. - **Validation:** verify the file is well-formed JSON with the expected shape and a supported `version`; reject unknown/incompatible versions with a clear message. Guard against very large files with a sane size limit and user feedback. - **UI:** an Import entry reachable from the same area a user would naturally start a board from (the boards list / dashboard is preferred; if an in-board entry is added it must also create a new board, not mutate the current one). Use a normal file input; show progress and a final summary (objects created, connectors created, anything skipped). ## Requirements - Round-trips with the existing JSON export: exporting a board then importing the file reproduces an equivalent board (same objects/connectors, visually equivalent layout). - All creation goes through existing JSON-RPC methods/SDK; no new network ports or listeners; any new server method must follow the existing Unix-socket JSON-RPC pattern. Prefer doing the orchestration client-side using already-exposed RPCs (board.create, object.create, connector.create) if sufficient. - The id remap must correctly handle frame parent references and connector endpoints. - Import must not corrupt or modify any existing board. - Clear, non-blocking feedback during and after import; failures leave no half-created board, or if partial creation is unavoidable, the summary states exactly what was created. - Sanitized, sensible name for the new board (e.g. derived from the file's board name, with a suffix if needed). ## Acceptance Criteria - Exporting a board with objects, frames (with child objects), and connectors to JSON, then importing that file, produces a new board containing all objects and connectors with correct relative positions and intact frame/connector relationships. - The original/source board is unchanged. - Importing a malformed file or unsupported version shows a clear error and creates nothing. - Connectors with missing endpoints are skipped and counted in the summary rather than aborting the whole import. - After a successful import the user lands on the new board. - No new network listeners/ports are introduced. ## 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.
Author
Member

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, remaps parent_frame_id/from_id/to_id through an old-id→new-id map, reports progress and a final summary, and navigates to the new board.

Requirements

  • Import creates a NEW board only. No update/delete against any existing board.
  • File picked from disk, parsed and validated client-side. On any validation failure: clear message, nothing created.
  • Validation (reject and create nothing if any fail): parses as JSON; size guard (reject if file.size > 10MB); top-level object with version === 1; board object; objects/connectors arrays; each object has an id and string type.
  • New board name derived from 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 selection reuses the existing New Board modal pattern (pick existing workspace or create one via workspace.create); board.create requires workspace_id + name.
  • Two-pass id remapping:
    1. Sort objects so frames are created before non-frames.
    2. Each object: shallow-copy, set board_id = newBoard.id, drop id, omit parent_frame_id on pass 1, call object.create, record idMap[oldId] = result.id.
    3. Pass 2: for objects that had parent_frame_id, if mapped, object.update the newly created object with the remapped parent; else leave unparented and count it.
    4. Connectors: remap from_id/to_id via idMap; missing endpoint → skip + count; else connector.create with { board_id, from_id, to_id, line_type: <line_style>, style: JSON.stringify({stroke, strokeWidth}) }.
  • Progress text during import; final summary: objects created, connectors created, connectors skipped, objects unparented.
  • On success navigate to WB_BASE + '/board/' + newBoard.id.
  • No new ports/listeners/routes/server code; pure client-side orchestration using the existing rpcCall.

Files to Modify/Create

  • Modify: 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> (near submitNewBoard/duplicateBoard).
  • No changes to rpc.js, export.js, server crate, assets.rs, routes.rs (all required RPCs already exist; HTML is template-embedded so no new static file).

Implementation Plan

(Single tightly-coupled file — sequential, not parallelizable.)

Step 1: Add the Import UI controls

Files: templates/web/home.html

  • After the New Board button, add #import-board-btn ("Import") and a hidden <input type="file" id="import-file-input" accept="application/json,.json">.
  • Add a workspace-pick step mirroring the existing #new-board-modal (import-prefixed ids) or reuse it parameterized; add an #import-status region (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.parse in try/catch; assert shape, version===1, board object, arrays, per-object id+type. Add sanitizeBoardName(raw).
    Dependencies: Step 1

Step 3: Import orchestration

Files: templates/web/home.html (inline script)

  • runImport(data, wsId) modeled on duplicateBoard + submitNewBoard: board.create (+duplicate-name retry); pass-1 frames-first object loop building idMap (strip id, set board_id, omit parent_frame_id); pass-2 object.update parent remap for new-board objects only; connector loop with remap + skip counting; update #import-status each iteration; final summary; persist workspace like submitNewBoard; navigate to /board/{id}.
  • Wire #import-board-btn → file input; on change read file (size guard), validate, then workspace-pick → runImport.
    Dependencies: Steps 1, 2

Step 4: Error-handling polish

Files: templates/web/home.html (inline script)

  • Every failure path shows a clear message; mid-import RPC failure surfaces an error with partial-progress context (no rollback in v1; never delete existing data). Reset file input value so re-selecting the same file re-fires change.
    Dependencies: Step 3

Acceptance Criteria

  • An "Import" control appears next to "New Board" on the boards list; a valid exported JSON prompts for workspace and creates a new board.
  • Round-trip: objects recreated preserving type/x/y/width/height/rotation/z_index/style/text/data; server assigns fresh ids.
  • parent_frame_id remapped (frames before children + second pass); orphaned-parent objects created unparented and counted.
  • Connectors recreated via connector.create with remapped endpoints; missing-endpoint connectors skipped + counted; line_styleline_type, stroke/stroke_widthstyle mapping applied.
  • Malformed JSON / wrong shape / version!==1 → clear error, nothing created (no board.create).
  • File > 10MB rejected before parsing.
  • No existing board modified or deleted.
  • Progress shown; final summary (created/skipped/unparented) displayed.
  • After success, navigates to /board/{newId}.
  • No new RPC methods/routes/ports/static files; only home.html changed.

Notes

  • Confirmed surface: RPC helper rpcCall(method, params) (rpc.js, already loaded by home.html). board.create{workspace_id, name, ...}, returns board at result.id, unique-name-per-workspace enforced (handle duplicate with suffix retry). object.create accepts the full object payload, ignores id, returns result.id; serializeForServer keys map 1:1 — only transforms: replace board_id, drop id, defer parent_frame_id. object.update {id, ...} used only on newly created objects. connector.create {board_id, from_id, to_id, line_type, style, ...} returns result.id; map exported line_style/stroke/stroke_width as above (mirrors connectors.js createConnector). Board-open URL WB_BASE + '/board/' + id.
  • Reference implementations in the same file: submitNewBoard() (workspace pick + create + navigate) and duplicateBoard() (board.create then looped object.create with delete obj.id; obj.board_id = newBoard.id). Client-side orchestration is sufficient and consistent — no server-side import method needed.
  • Connector ids in the export are local string keys, not server ids — only from_id/to_id need remapping.
  • Deploy (test phase, not implementers): home.html is rust-embed-embedded; rebuild hero_whiteboard_admin to serve changes; no static file added so assets.rs untouched. Verify via curl over the admin unix socket for /.
## 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, remaps `parent_frame_id`/`from_id`/`to_id` through an old-id→new-id map, reports progress and a final summary, and navigates to the new board. ### Requirements - Import creates a NEW board only. No update/delete against any existing board. - File picked from disk, parsed and validated client-side. On any validation failure: clear message, nothing created. - Validation (reject and create nothing if any fail): parses as JSON; size guard (reject if `file.size > 10MB`); top-level object with `version === 1`; `board` object; `objects`/`connectors` arrays; each object has an `id` and string `type`. - New board name derived from `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 selection reuses the existing New Board modal pattern (pick existing workspace or create one via `workspace.create`); `board.create` requires `workspace_id` + `name`. - Two-pass id remapping: 1. Sort objects so frames are created before non-frames. 2. Each object: shallow-copy, set `board_id = newBoard.id`, drop `id`, omit `parent_frame_id` on pass 1, call `object.create`, record `idMap[oldId] = result.id`. 3. Pass 2: for objects that had `parent_frame_id`, if mapped, `object.update` the newly created object with the remapped parent; else leave unparented and count it. 4. Connectors: remap `from_id`/`to_id` via `idMap`; missing endpoint → skip + count; else `connector.create` with `{ board_id, from_id, to_id, line_type: <line_style>, style: JSON.stringify({stroke, strokeWidth}) }`. - Progress text during import; final summary: objects created, connectors created, connectors skipped, objects unparented. - On success navigate to `WB_BASE + '/board/' + newBoard.id`. - No new ports/listeners/routes/server code; pure client-side orchestration using the existing `rpcCall`. ### Files to Modify/Create - Modify: `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>` (near `submitNewBoard`/`duplicateBoard`). - No changes to rpc.js, export.js, server crate, assets.rs, routes.rs (all required RPCs already exist; HTML is template-embedded so no new static file). ### Implementation Plan (Single tightly-coupled file — sequential, not parallelizable.) #### Step 1: Add the Import UI controls Files: `templates/web/home.html` - After the New Board button, add `#import-board-btn` ("Import") and a hidden `<input type="file" id="import-file-input" accept="application/json,.json">`. - Add a workspace-pick step mirroring the existing `#new-board-modal` (import-prefixed ids) or reuse it parameterized; add an `#import-status` region (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.parse` in try/catch; assert shape, `version===1`, `board` object, arrays, per-object `id`+`type`. Add `sanitizeBoardName(raw)`. Dependencies: Step 1 #### Step 3: Import orchestration Files: `templates/web/home.html` (inline script) - `runImport(data, wsId)` modeled on `duplicateBoard` + `submitNewBoard`: `board.create` (+duplicate-name retry); pass-1 frames-first object loop building `idMap` (strip `id`, set `board_id`, omit `parent_frame_id`); pass-2 `object.update` parent remap for new-board objects only; connector loop with remap + skip counting; update `#import-status` each iteration; final summary; persist workspace like `submitNewBoard`; navigate to `/board/{id}`. - Wire `#import-board-btn` → file input; on `change` read file (size guard), validate, then workspace-pick → `runImport`. Dependencies: Steps 1, 2 #### Step 4: Error-handling polish Files: `templates/web/home.html` (inline script) - Every failure path shows a clear message; mid-import RPC failure surfaces an error with partial-progress context (no rollback in v1; never delete existing data). Reset file input value so re-selecting the same file re-fires `change`. Dependencies: Step 3 ### Acceptance Criteria - [ ] An "Import" control appears next to "New Board" on the boards list; a valid exported JSON prompts for workspace and creates a new board. - [ ] Round-trip: objects recreated preserving type/x/y/width/height/rotation/z_index/style/text/data; server assigns fresh ids. - [ ] `parent_frame_id` remapped (frames before children + second pass); orphaned-parent objects created unparented and counted. - [ ] Connectors recreated via `connector.create` with remapped endpoints; missing-endpoint connectors skipped + counted; `line_style`→`line_type`, `stroke`/`stroke_width`→`style` mapping applied. - [ ] Malformed JSON / wrong shape / `version!==1` → clear error, nothing created (no `board.create`). - [ ] File > 10MB rejected before parsing. - [ ] No existing board modified or deleted. - [ ] Progress shown; final summary (created/skipped/unparented) displayed. - [ ] After success, navigates to `/board/{newId}`. - [ ] No new RPC methods/routes/ports/static files; only `home.html` changed. ### Notes - Confirmed surface: RPC helper `rpcCall(method, params)` (rpc.js, already loaded by home.html). `board.create` → `{workspace_id, name, ...}`, returns board at `result.id`, unique-name-per-workspace enforced (handle duplicate with suffix retry). `object.create` accepts the full object payload, ignores `id`, returns `result.id`; `serializeForServer` keys map 1:1 — only transforms: replace `board_id`, drop `id`, defer `parent_frame_id`. `object.update {id, ...}` used only on newly created objects. `connector.create {board_id, from_id, to_id, line_type, style, ...}` returns `result.id`; map exported `line_style`/`stroke`/`stroke_width` as above (mirrors connectors.js createConnector). Board-open URL `WB_BASE + '/board/' + id`. - Reference implementations in the same file: `submitNewBoard()` (workspace pick + create + navigate) and `duplicateBoard()` (board.create then looped object.create with `delete obj.id; obj.board_id = newBoard.id`). Client-side orchestration is sufficient and consistent — no server-side import method needed. - Connector ids in the export are local string keys, not server ids — only `from_id`/`to_id` need remapping. - Deploy (test phase, not implementers): `home.html` is rust-embed-embedded; rebuild `hero_whiteboard_admin` to serve changes; no static file added so `assets.rs` untouched. Verify via curl over the admin unix socket for `/`.
Author
Member

Test Results

  • Workspace lib tests (cargo test --workspace --lib): compiled cleanly, no failures (all crates: 0 passed; 0 failed; 0 ignored)
  • Inline JS syntax (node --check on home.html script): OK
  • home.html encoding: UTF-8 text, control-byte scan = 0

Note: this feature is a single HTML/inline-JS template change (no Rust source changed); workspace lib tests run as a regression guard.

## Test Results - Workspace lib tests (cargo test --workspace --lib): compiled cleanly, no failures (all crates: 0 passed; 0 failed; 0 ignored) - Inline JS syntax (node --check on home.html script): OK - home.html encoding: UTF-8 text, control-byte scan = 0 Note: this feature is a single HTML/inline-JS template change (no Rust source changed); workspace lib tests run as a regression guard.
Author
Member

Implementation Summary

JSON board import, fully client-side, on the boards list.

Changes

  • crates/hero_whiteboard_admin/templates/web/home.html (only file changed):
    • Added an "Import" button + hidden file input next to "New Board", an #import-modal (workspace pick, mirroring the New Board modal) and an #import-status progress/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 via object.create building an old-id to new-id map, a second pass that remaps parent_frame_id via object.update on the newly created objects only, then connector recreation via connector.create with from_id/to_id remapped (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

  • Import always creates a NEW board; no existing board is updated or deleted (only board.create / object.create / object.update / connector.create / workspace.create on the freshly created board).
  • Round-trips with the JSON exporter; reuses the existing RPC methods and the same patterns as the existing duplicate-board flow.
  • Malformed file / wrong shape / unsupported version → clear error, nothing created. Oversized file rejected before parsing. Re-selecting the same file re-triggers import.

Test results

  • cargo test --workspace --lib (matches CI): compiled cleanly, no failures (single HTML/inline-JS template change; run as a regression guard).
  • node --check on the inline script: OK. home.html verified UTF-8 with zero control bytes.
  • Rebuilt and redeployed the admin binary; the boards list served with the Import control and import logic verified present.

Notes

  • Client-side orchestration was sufficient (the existing duplicate-board flow already does board.create + looped object.create); no server-side import method or new RPC/route/port was added.
  • Connector style is sent as { stroke, strokeWidth }; the editor's connector loader accepts both object and string style, so it round-trips.
  • v1 does not roll back a partially created board on a mid-import RPC failure; the summary/error states what was created.
## Implementation Summary JSON board import, fully client-side, on the boards list. ### Changes - `crates/hero_whiteboard_admin/templates/web/home.html` (only file changed): - Added an "Import" button + hidden file input next to "New Board", an `#import-modal` (workspace pick, mirroring the New Board modal) and an `#import-status` progress/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 via `object.create` building an old-id to new-id map, a second pass that remaps `parent_frame_id` via `object.update` on the newly created objects only, then connector recreation via `connector.create` with `from_id`/`to_id` remapped (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 - Import always creates a NEW board; no existing board is updated or deleted (only board.create / object.create / object.update / connector.create / workspace.create on the freshly created board). - Round-trips with the JSON exporter; reuses the existing RPC methods and the same patterns as the existing duplicate-board flow. - Malformed file / wrong shape / unsupported version → clear error, nothing created. Oversized file rejected before parsing. Re-selecting the same file re-triggers import. ### Test results - `cargo test --workspace --lib` (matches CI): compiled cleanly, no failures (single HTML/inline-JS template change; run as a regression guard). - `node --check` on the inline script: OK. `home.html` verified UTF-8 with zero control bytes. - Rebuilt and redeployed the admin binary; the boards list served with the Import control and import logic verified present. ### Notes - Client-side orchestration was sufficient (the existing duplicate-board flow already does board.create + looped object.create); no server-side import method or new RPC/route/port was added. - Connector `style` is sent as `{ stroke, strokeWidth }`; the editor's connector loader accepts both object and string style, so it round-trips. - v1 does not roll back a partially created board on a mid-import RPC failure; the summary/error states what was created.
Author
Member

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.html and crates/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.

## 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.html` and `crates/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.
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#204
No description provided.