From a68e7f8e944c4f3a1ed4e83f61ac185569aa535e Mon Sep 17 00:00:00 2001 From: David Montero Date: Thu, 4 Jun 2026 06:22:25 +0200 Subject: [PATCH] feat(editor): rename boards & custom chips; show which target owns each file Phase 2 of the run-system/UX work. - BoardInstance gains an optional user ; boardDisplayName(board) resolver (name || kind label) routes every INSTANCE-label surface: file-explorer section header, compile console (EditorToolbar), canvas selector/tooltip/ context-menu, Serial Monitor tabs, Oscilloscope board picker, Board Options subtitle. Board/component pickers keep the KIND label (they pick new boards). - Inline rename on board AND chip section headers (double-click the name, or a hover pencil button). Board -> updateBoard(id,{name}); chip -> chipName in properties. Enter commits, Escape cancels (cancel-flag ref guards the unmount-fires-onBlur footgun), empty clears to the kind / 'Custom Chip'. - FileTabs shows an owner badge naming the board/chip whose files are shown (resolved as a selector so it doesn't re-render on every sim pin toggle). - CustomChipDialog no longer clobbers a user-given chipName: chip.json's name only seeds the blank defaults (My Chip / Custom Chip); loading an example relabels explicitly. - Persistence: board name round-trips via projectPayload (+ dirty hash), vlxFile, ProjectByIdPage load + loadProjectState; chipName rides components_json. - Drive-by: fixed a pre-existing rules-of-hooks violation in BoardOptionsModal (early return before a useCallback). Reviewed by a 3-agent adversarial pass (completeness / persistence / correctness); all major findings folded in. Co-Authored-By: Claude Opus 4.8 --- .../customChips/CustomChipDialog.tsx | 33 +++- .../src/components/editor/EditorToolbar.tsx | 8 +- .../src/components/editor/FileExplorer.css | 9 + .../src/components/editor/FileExplorer.tsx | 159 +++++++++++++++++- frontend/src/components/editor/FileTabs.css | 17 ++ frontend/src/components/editor/FileTabs.tsx | 30 +++- .../simulator/BoardOptionsModal.tsx | 16 +- .../src/components/simulator/Oscilloscope.tsx | 8 +- .../components/simulator/SerialMonitor.tsx | 6 +- .../components/simulator/SimulatorCanvas.tsx | 11 +- frontend/src/pages/ProjectByIdPage.tsx | 1 + frontend/src/store/useSimulatorStore.ts | 9 +- frontend/src/types/board.ts | 11 ++ frontend/src/utils/projectPayload.ts | 1 + frontend/src/utils/vlxFile.ts | 2 + 15 files changed, 280 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/customChips/CustomChipDialog.tsx b/frontend/src/components/customChips/CustomChipDialog.tsx index c8300f30..c9140e21 100644 --- a/frontend/src/components/customChips/CustomChipDialog.tsx +++ b/frontend/src/components/customChips/CustomChipDialog.tsx @@ -64,6 +64,11 @@ function parseAttributes(chipJson: string): AttributeDef[] { type Tab = 'examples' | 'editor'; +// Placeholder chip names that are treated as "not user-named" — chip.json's +// `name` may seed over any of these. 'My Chip' is the dialog default; 'Custom +// Chip' is the file-explorer empty-rename fallback. Keep both in sync here. +const BLANK_CHIP_NAMES = new Set(['', 'My Chip', 'Custom Chip']); + export const CustomChipDialog = ({ initial, onClose, onSave }: CustomChipDialogProps) => { const { t } = useTranslation(); // If the chip already has source code, skip the examples tab on open. @@ -105,20 +110,32 @@ export const CustomChipDialog = ({ initial, onClose, onSave }: CustomChipDialogP return Array.from(m.entries()); }, []); - // Pull chipName out of chip.json whenever it changes — keeps a single source - // of truth without forcing the user to edit two fields. + // Seed chipName from chip.json's `name` ONLY while the chip still has a + // blank default name — so editing chip.json names a fresh chip, but a name + // the user gave it (renamed in the file explorer) is NOT clobbered. The + // display name lives in properties.chipName and wins; chip.json `name` is + // just the initial seed. (Switching examples relabels explicitly in + // loadExample.) Both blank defaults are recognised so neither sticks. useEffect(() => { - try { - const obj = JSON.parse(chipJson); - if (obj && typeof obj.name === 'string' && obj.name.trim()) { - setChipName(obj.name); - } - } catch { /* user is mid-edit, ignore */ } + setChipName((prev) => { + if (prev && prev.trim() && !BLANK_CHIP_NAMES.has(prev.trim())) return prev; + try { + const obj = JSON.parse(chipJson); + if (obj && typeof obj.name === 'string' && obj.name.trim()) return obj.name; + } catch { /* user is mid-edit, ignore */ } + return prev; + }); }, [chipJson]); const loadExample = (e: ChipExample) => { setSourceC(e.sourceC); setChipJson(e.chipJson); + // Loading an example is an explicit "use this chip" — relabel from its + // chip.json name even over a previously-loaded example's name. + try { + const obj = JSON.parse(e.chipJson); + if (obj && typeof obj.name === 'string' && obj.name.trim()) setChipName(obj.name); + } catch { /* keep current name */ } setWasmBase64(''); // force re-compile so the binary matches the loaded source setResult(null); setTab('editor'); diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 4c2700a9..de105826 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -9,7 +9,7 @@ import { BOARD_PIN_GROUPS } from '../../simulation/spice/boardPinGroups'; import { CircuitVerificationModal } from '../simulator/CircuitVerificationModal'; import type { PinSourceState } from '../../simulation/spice/types'; import type { BoardKind, LanguageMode } from '../../types/board'; -import { BOARD_KIND_FQBN, BOARD_KIND_LABELS, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind } from '../../types/board'; +import { BOARD_KIND_FQBN, BOARD_SUPPORTS_MICROPYTHON, isPiBoardKind, boardDisplayName } from '../../types/board'; import { compileCode } from '../../services/compilation'; import { compileRom, @@ -421,7 +421,7 @@ export const EditorToolbar = ({ } const fqbn = kind ? BOARD_KIND_FQBN[kind] : null; - const boardLabel = kind ? BOARD_KIND_LABELS[kind] : 'Unknown'; + const boardLabel = activeBoard ? boardDisplayName(activeBoard) : 'Unknown'; if (!fqbn) { addLog({ timestamp: new Date(), type: 'error', message: `No FQBN for board kind: ${kind}` }); @@ -899,7 +899,7 @@ export const EditorToolbar = ({ let failed = 0; for (const board of boardsList) { - const label = BOARD_KIND_LABELS[board.boardKind] ?? board.boardKind; + const label = boardDisplayName(board); if (isPiBoardKind(board.boardKind)) { addLog({ @@ -1168,7 +1168,7 @@ export const EditorToolbar = ({ // Architecture mismatch warning for ELF files if (result.elfInfo?.suggestedBoard && result.elfInfo.suggestedBoard !== boardKind) { const detected = result.elfInfo.architectureName; - const current = activeBoard ? BOARD_KIND_LABELS[activeBoard.boardKind] : boardKind; + const current = activeBoard ? boardDisplayName(activeBoard) : boardKind; addLog({ timestamp: new Date(), type: 'info', diff --git a/frontend/src/components/editor/FileExplorer.css b/frontend/src/components/editor/FileExplorer.css index 1b2d690e..ef84de53 100644 --- a/frontend/src/components/editor/FileExplorer.css +++ b/frontend/src/components/editor/FileExplorer.css @@ -235,6 +235,15 @@ white-space: nowrap; } +/* Inline rename input for a board/chip section header — flex into the label + slot and match the header's compact type. */ +.fe-section-rename-input { + flex: 1; + min-width: 0; + font-size: 11px; + font-weight: 600; +} + .fe-status-dot { width: 7px; height: 7px; diff --git a/frontend/src/components/editor/FileExplorer.tsx b/frontend/src/components/editor/FileExplorer.tsx index 823b40ea..4797efb6 100644 --- a/frontend/src/components/editor/FileExplorer.tsx +++ b/frontend/src/components/editor/FileExplorer.tsx @@ -9,7 +9,7 @@ import { DEFAULT_CHIP_PROGRAM_C, } from '../../services/romCompileService'; import type { BoardKind } from '../../types/board'; -import { BOARD_KIND_LABELS } from '../../types/board'; +import { boardDisplayName } from '../../types/board'; import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject'; import './FileExplorer.css'; @@ -135,6 +135,23 @@ const IcoChevron = ({ open }: { open: boolean }) => ( ); +// Pencil icon — the rename affordance on a board/chip section header. +const IcoPencil = () => ( + + + + +); + // Integrated-circuit (chip) icon — a DIP package with pins. Marks a // programmable custom-chip's program section, distinct from board sections. const IcoChip = () => ( @@ -285,6 +302,8 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl const boards = useSimulatorStore((s) => s.boards); const activeBoardId = useSimulatorStore((s) => s.activeBoardId); const setActiveBoardId = useSimulatorStore((s) => s.setActiveBoardId); + const updateBoard = useSimulatorStore((s) => s.updateBoard); + const updateComponent = useSimulatorStore((s) => s.updateComponent); const components = useSimulatorStore((s) => s.components); // Programmable custom-chips (CPU emulators whose chip.json declares @@ -333,6 +352,13 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl const [contextMenu, setContextMenu] = useState(null); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); + // Inline rename of a SECTION header (a board or a chip). Kept separate from + // file rename (renamingId) so the two never collide. + const [renamingSection, setRenamingSection] = useState<{ + id: string; + kind: 'board' | 'chip'; + } | null>(null); + const [sectionRenameValue, setSectionRenameValue] = useState(''); // Track which board group is creating a file: boardGroupId or null const [creatingInGroup, setCreatingInGroup] = useState(null); const [newFileName, setNewFileName] = useState(''); @@ -340,6 +366,10 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl const [collapsed, setCollapsed] = useState>({}); const renameInputRef = useRef(null); + const sectionRenameInputRef = useRef(null); + // Set true by Escape so the input's onBlur (which fires when Escape unmounts + // the input) discards instead of committing the typed value. + const sectionRenameCancelledRef = useRef(false); const newFileInputRef = useRef(null); useEffect(() => { @@ -349,6 +379,54 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl } }, [renamingId]); + useEffect(() => { + if (renamingSection && sectionRenameInputRef.current) { + sectionRenameInputRef.current.focus(); + sectionRenameInputRef.current.select(); + } + }, [renamingSection]); + + const startBoardRename = useCallback((board: { id: string; name?: string; boardKind: BoardKind }) => { + sectionRenameCancelledRef.current = false; + setRenamingSection({ id: board.id, kind: 'board' }); + setSectionRenameValue(boardDisplayName(board)); + }, []); + + const startChipRename = useCallback((chipId: string, currentName: string) => { + sectionRenameCancelledRef.current = false; + setRenamingSection({ id: chipId, kind: 'chip' }); + setSectionRenameValue(currentName); + }, []); + + const cancelSectionRename = useCallback(() => { + sectionRenameCancelledRef.current = true; + setRenamingSection(null); + }, []); + + const commitSectionRename = useCallback(() => { + // Escape cancelled this edit (it unmounts the input, firing onBlur) — discard. + if (sectionRenameCancelledRef.current) { + sectionRenameCancelledRef.current = false; + return; + } + const target = renamingSection; + if (target) { + const value = sectionRenameValue.trim(); + if (target.kind === 'board') { + // Empty clears the custom name -> boardDisplayName falls back to kind. + updateBoard(target.id, { name: value }); + } else { + const comp = useSimulatorStore.getState().components.find((c) => c.id === target.id); + if (comp) { + updateComponent(target.id, { + properties: { ...comp.properties, chipName: value || 'Custom Chip' }, + }); + } + } + } + setRenamingSection(null); + }, [renamingSection, sectionRenameValue, updateBoard, updateComponent]); + useEffect(() => { if (creatingInGroup && newFileInputRef.current) { newFileInputRef.current.focus(); @@ -509,7 +587,7 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl switchToBoard(board.id, groupId); if (!isOpen) toggleCollapse(board.id); }} - title={`${BOARD_KIND_LABELS[board.boardKind]} — ${t('editor.fileExplorer.clickToEdit')}`} + title={`${boardDisplayName(board)} — ${t('editor.fileExplorer.clickToEdit')}`} > + )} {isOpen && ( diff --git a/frontend/src/components/editor/FileTabs.css b/frontend/src/components/editor/FileTabs.css index 3d64d50c..4f964231 100644 --- a/frontend/src/components/editor/FileTabs.css +++ b/frontend/src/components/editor/FileTabs.css @@ -10,6 +10,23 @@ scrollbar-color: #3c3c3c transparent; } +/* Owner badge — names the board/chip whose files are shown, so the user knows + whose code these tabs belong to. */ +.file-tabs-owner { + display: flex; + align-items: center; + padding: 0 10px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.4px; + text-transform: uppercase; + color: #9aa0a6; + background: #181818; + border-right: 1px solid #333; + white-space: nowrap; + flex-shrink: 0; +} + .file-tab { display: flex; align-items: center; diff --git a/frontend/src/components/editor/FileTabs.tsx b/frontend/src/components/editor/FileTabs.tsx index 3b524743..6040b250 100644 --- a/frontend/src/components/editor/FileTabs.tsx +++ b/frontend/src/components/editor/FileTabs.tsx @@ -1,17 +1,38 @@ import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useEditorStore } from '../../store/useEditorStore'; +import { useEditorStore, CHIP_GROUP_PREFIX } from '../../store/useEditorStore'; +import { useSimulatorStore } from '../../store/useSimulatorStore'; +import { boardDisplayName } from '../../types/board'; import './FileTabs.css'; export const FileTabs: React.FC = () => { const { t } = useTranslation(); - const { files, openFileIds, activeFileId, setActiveFile, closeFile } = useEditorStore(); + const { files, openFileIds, activeFileId, activeGroupId, setActiveFile, closeFile } = + useEditorStore(); const [confirmCloseId, setConfirmCloseId] = useState(null); const openFiles = openFileIds .map((id) => files.find((f) => f.id === id)) .filter(Boolean) as typeof files; + // Which target owns the files currently shown — a board or a custom chip — + // so the user can tell whose code they're editing at a glance. Resolved as a + // SELECTOR returning just the label string, so FileTabs only re-renders when + // that label changes — not on every pin toggle that mutates the components + // array during simulation. + const ownerLabel = useSimulatorStore((s): string | null => { + if (activeGroupId?.startsWith(CHIP_GROUP_PREFIX)) { + const chipId = activeGroupId.slice(CHIP_GROUP_PREFIX.length); + const chip = s.components.find((c) => c.id === chipId); + return ( + String((chip?.properties as Record)?.chipName ?? '').trim() || + 'Custom Chip' + ); + } + const board = s.boards.find((b) => b.activeFileGroupId === activeGroupId); + return board ? boardDisplayName(board) : null; + }); + const handleCloseClick = (e: React.MouseEvent, fileId: string, modified: boolean) => { e.stopPropagation(); if (modified) { @@ -29,6 +50,11 @@ export const FileTabs: React.FC = () => { return ( <>
+ {ownerLabel && ( + + {ownerLabel} + + )} {openFiles.map((file) => (
void; @@ -70,6 +72,7 @@ async function readFileAsBase64(file: File): Promise { export const BoardOptionsModal = ({ isOpen, boardId, + boardName, boardKind, currentOptions, spiffsFiles, @@ -85,6 +88,12 @@ export const BoardOptionsModal = ({ const [draft, setDraft] = useState(seed); const fileInputRef = useRef(null); + const update = useCallback( + (patch: Partial) => setDraft((d) => ({ ...d, ...patch })), + [], + ); + + // Hooks must run on every render — bail out only AFTER all of them. if (!isOpen) return null; const showPsram = boardSupportsPsram(boardKind); @@ -97,11 +106,6 @@ export const BoardOptionsModal = ({ const nonDio = draft.flashMode !== 'dio'; const sameCore = draft.eventsRunOnCore === draft.arduinoRunsOnCore; - const update = useCallback( - (patch: Partial) => setDraft((d) => ({ ...d, ...patch })), - [], - ); - const handleApply = () => { // Strip PSRAM for boards that don't support it (e.g. C3) so a stale // value can't smuggle through to the backend. @@ -146,7 +150,7 @@ export const BoardOptionsModal = ({
Board Options
- {BOARD_KIND_LABELS[boardKind]} - id {boardId} + {boardName?.trim() || BOARD_KIND_LABELS[boardKind]} - id {boardId}
))}
{/* Board label */} {selectedBoard && ( -
{BOARD_KIND_LABELS[selectedBoard.boardKind]}
+
{boardDisplayName(selectedBoard)}
)} {/* Pin grid */} diff --git a/frontend/src/components/simulator/SerialMonitor.tsx b/frontend/src/components/simulator/SerialMonitor.tsx index e77be17f..38587f70 100644 --- a/frontend/src/components/simulator/SerialMonitor.tsx +++ b/frontend/src/components/simulator/SerialMonitor.tsx @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import { getTabSessionId } from '../../simulation/Esp32Bridge'; import type { BoardKind } from '../../types/board'; -import { BOARD_KIND_LABELS } from '../../types/board'; +import { boardDisplayName } from '../../types/board'; // Short labels for tabs const BOARD_SHORT_LABEL: Partial> = { @@ -207,12 +207,12 @@ export const SerialMonitor: React.FC = () => { ...(isActive ? { ...styles.tabActive, borderBottomColor: color, color } : {}), }} onClick={() => handleTabClick(board.id)} - title={BOARD_KIND_LABELS[board.boardKind]} + title={boardDisplayName(board)} > {BOARD_ICON[board.boardKind] ?? '●'} - {BOARD_SHORT_LABEL[board.boardKind] ?? board.boardKind} + {board.name?.trim() || BOARD_SHORT_LABEL[board.boardKind] || board.boardKind} {hasUnread && !isActive && } ); diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index f0823686..253fdb56 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -42,7 +42,7 @@ import { import { useIsCoarsePointer } from '../../utils/useTouchDevice'; import type { ComponentMetadata } from '../../types/component-metadata'; import type { BoardKind } from '../../types/board'; -import { BOARD_KIND_FQBN, BOARD_KIND_LABELS } from '../../types/board'; +import { BOARD_KIND_FQBN, boardDisplayName } from '../../types/board'; import { boardGateDecision, proBoardFeatureName, triggerProUpgradePrompt } from '../../lib/proBoardGate'; import { FlashModal } from './FlashModal'; import { isTauri as isTauriRuntimeFn } from '../../desktop/tauriBridge'; @@ -2066,7 +2066,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { > {boards.map((b) => ( ))} @@ -2607,7 +2607,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { let title = pinPicker.kind === 'board' ? 'Board' : 'Component'; if (pinPicker.kind === 'board') { const b = boards.find((x) => x.id === id); - if (b) title = BOARD_KIND_LABELS[b.boardKind] ?? b.boardKind; + if (b) title = boardDisplayName(b); } else { const c = components.find((x) => x.id === id); const meta = c ? registry.getById(c.metadataId) : null; @@ -2809,7 +2809,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { {boardContextMenu && (() => { const board = boards.find((b) => b.id === boardContextMenu.boardId); - const label = board ? BOARD_KIND_LABELS[board.boardKind] : 'Board'; + const label = board ? boardDisplayName(board) : 'Board'; const connectedWires = wires.filter( (w) => w.start.componentId === boardContextMenu.boardId || @@ -3045,6 +3045,7 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => { { {boardToRemove && (() => { const board = boards.find((b) => b.id === boardToRemove); - const label = board ? BOARD_KIND_LABELS[board.boardKind] : t('editor.canvas.removeConfirm.boardFallback'); + const label = board ? boardDisplayName(board) : t('editor.canvas.removeConfirm.boardFallback'); const connectedWires = wires.filter( (w) => w.start.componentId === boardToRemove || w.end.componentId === boardToRemove, ).length; diff --git a/frontend/src/pages/ProjectByIdPage.tsx b/frontend/src/pages/ProjectByIdPage.tsx index 92419533..90a5f188 100644 --- a/frontend/src/pages/ProjectByIdPage.tsx +++ b/frontend/src/pages/ProjectByIdPage.tsx @@ -152,6 +152,7 @@ export function buildLoadPayload(project: RawProject) { if (Array.isArray(parsed) && parsed.length > 0) { boards = parsed.map((b: Partial & { id: string; boardKind: string }) => ({ id: b.id, + name: b.name, boardKind: b.boardKind as BoardKind, x: b.x ?? 50, y: b.y ?? 50, diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index c24d2483..d14cd175 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -1277,11 +1277,12 @@ export const useSimulatorStore = create((set, get) => { payload.boards.forEach((b) => { addBoard(b.boardKind, b.x, b.y, b.id); // Apply the rest of the saved fields that addBoard doesn't set. - if (b.languageMode && b.languageMode !== 'arduino') { + const patch: Partial = {}; + if (b.languageMode && b.languageMode !== 'arduino') patch.languageMode = b.languageMode; + if (b.name && b.name.trim()) patch.name = b.name; + if (Object.keys(patch).length > 0) { set((s) => ({ - boards: s.boards.map((bb) => - bb.id === b.id ? { ...bb, languageMode: b.languageMode } : bb, - ), + boards: s.boards.map((bb) => (bb.id === b.id ? { ...bb, ...patch } : bb)), })); } }); diff --git a/frontend/src/types/board.ts b/frontend/src/types/board.ts index 7d24db97..18b43b37 100644 --- a/frontend/src/types/board.ts +++ b/frontend/src/types/board.ts @@ -75,6 +75,10 @@ export interface BleStatus { export interface BoardInstance { id: string; // unique in canvas, e.g. 'arduino-uno', 'raspberry-pi-3' + /** Optional user-given display name. Falls back to the kind label when + * empty. Lets the user tell two same-kind boards apart in the file + * explorer, the compile console, and the canvas selector. */ + name?: string; boardKind: BoardKind; x: number; y: number; @@ -129,6 +133,13 @@ export const BOARD_KIND_LABELS: Record = { attiny85: 'ATtiny85', }; +/** Display name for a board instance: the user's custom name if set, else the + * kind label. Route every user-facing board label through this so renamed + * boards show their name everywhere (file explorer, compile console, canvas). */ +export function boardDisplayName(board: Pick): string { + return board.name?.trim() || BOARD_KIND_LABELS[board.boardKind]; +} + export const BOARD_KIND_FQBN: Record = { 'arduino-uno': 'arduino:avr:uno', 'arduino-nano': 'arduino:avr:nano:cpu=atmega328', diff --git a/frontend/src/utils/projectPayload.ts b/frontend/src/utils/projectPayload.ts index 8191b5f9..e253fc7a 100644 --- a/frontend/src/utils/projectPayload.ts +++ b/frontend/src/utils/projectPayload.ts @@ -31,6 +31,7 @@ function chipGroupIdsWithFiles(): string[] { function serialisableBoard(b: BoardInstance) { return { id: b.id, + name: b.name, boardKind: b.boardKind, x: b.x, y: b.y, diff --git a/frontend/src/utils/vlxFile.ts b/frontend/src/utils/vlxFile.ts index b7f9138a..f050749d 100644 --- a/frontend/src/utils/vlxFile.ts +++ b/frontend/src/utils/vlxFile.ts @@ -45,6 +45,7 @@ export interface VlxPayload { name?: string; boards: Array<{ id: string; + name?: string; boardKind: string; x: number; y: number; @@ -61,6 +62,7 @@ export interface VlxPayload { function serialisableBoard(b: BoardInstance) { return { id: b.id, + name: b.name, boardKind: b.boardKind, x: b.x, y: b.y,