From 737ec5c6eb943b173d22f940da908531a1fe9eb7 Mon Sep 17 00:00:00 2001 From: David Montero Date: Fri, 22 May 2026 20:55:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(import):=20unify=20project=20import=20?= =?UTF-8?q?=E2=80=94=20accept=20.vlx=20and=20.zip=20in=20both=20entry=20po?= =?UTF-8?q?ints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Velxio had two parallel import paths that confused users (reported on Discord by AgUn / dmontero): * Toolbar "Import a project from a .zip file" → Wokwi .zip only * File-explorer "Open .vlx file" → Velxio .vlx only If you exported a Velxio project as .vlx and tried to bring it back via the toolbar Import button, you bounced off "wrong format" with no hint that the .vlx loader was hiding behind the file-explorer save-bar. Fix: introduce `utils/importProject.ts` as the single dispatcher. It sniffs the extension and routes: *.vlx → importVlxFile (writes directly to stores) *.zip → importFromWokwiZip (returns a payload the caller applies, so the toolbar can still trigger the install-libraries modal afterwards) Both UI entry points now go through the dispatcher with the same `accept=".vlx,.zip,application/json,application/zip"` filter: * Toolbar "Import project (.vlx Velxio or .zip Wokwi)" * File-explorer "Open project (.vlx Velxio or .zip Wokwi)" The toolbar tooltip is i18n-driven — updated EN + 8 other locales (es, fr, de, it, pt-br, ja, ru, zh-cn) so every user sees the same clarification. Wokwi compatibility kept intact — the .zip path still resolves to `importFromWokwiZip` and the same library-install modal pops if the imported project lists libraries we don't have locally. --- .../src/components/editor/EditorToolbar.tsx | 18 ++++- .../src/components/editor/FileExplorer.tsx | 51 +++++++++--- frontend/src/i18n/locales/de/common2.json | 2 +- frontend/src/i18n/locales/en/common2.json | 2 +- frontend/src/i18n/locales/es/common2.json | 2 +- frontend/src/i18n/locales/fr/common2.json | 2 +- frontend/src/i18n/locales/it/common2.json | 2 +- frontend/src/i18n/locales/ja/common2.json | 2 +- frontend/src/i18n/locales/pt-br/common2.json | 2 +- frontend/src/i18n/locales/ru/common2.json | 2 +- frontend/src/i18n/locales/zh-cn/common2.json | 2 +- frontend/src/utils/importProject.ts | 81 +++++++++++++++++++ 12 files changed, 142 insertions(+), 26 deletions(-) create mode 100644 frontend/src/utils/importProject.ts diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 5ae83ef5..7da4f6e9 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -23,7 +23,8 @@ import { LibraryManagerModal } from '../simulator/LibraryManagerModal'; import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal'; import { parseCompileResult } from '../../utils/compilationLogger'; import type { CompilationLog } from '../../utils/compilationLogger'; -import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip'; +import { exportToWokwiZip } from '../../utils/wokwiZip'; +import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject'; import { readFirmwareFile } from '../../utils/firmwareLoader'; import { trackCompileCode, @@ -903,7 +904,14 @@ export const EditorToolbar = ({ importInputRef.current.value = ''; if (!file) return; try { - const result = await importFromWokwiZip(file); + const result = await importProjectFile(file); + if (result.kind === 'vlx') { + // importVlxFile already wrote into the stores. + setMessage({ type: 'success', text: `Imported ${file.name}` }); + return; + } + // .zip path: apply the parsed payload to the stores ourselves, then + // surface any missing libraries via the existing install modal. const { loadFiles } = useEditorStore.getState(); const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } = useSimulatorStore.getState(); @@ -1110,11 +1118,13 @@ export const EditorToolbar = ({ {centerSlot &&
{centerSlot}
}
- {/* Hidden file input for import (always present) */} + {/* Hidden file input for project import. Accepts both .vlx + (Velxio native) and .zip (Wokwi bundle); the dispatcher in + utils/importProject.ts picks the right loader by extension. */} diff --git a/frontend/src/components/editor/FileExplorer.tsx b/frontend/src/components/editor/FileExplorer.tsx index 57e33901..641ff425 100644 --- a/frontend/src/components/editor/FileExplorer.tsx +++ b/frontend/src/components/editor/FileExplorer.tsx @@ -4,7 +4,7 @@ import { useEditorStore } from '../../store/useEditorStore'; import { useSimulatorStore } from '../../store/useSimulatorStore'; import type { BoardKind } from '../../types/board'; import { BOARD_KIND_LABELS } from '../../types/board'; -import { importVlxFile, VlxParseError } from '../../utils/vlxFile'; +import { importProjectFile, PROJECT_FILE_ACCEPT } from '../../utils/importProject'; import './FileExplorer.css'; // SVG icons — same style as EditorToolbar (stroke-based, 16x16) @@ -173,29 +173,54 @@ interface FileExplorerProps { export const FileExplorer: React.FC = ({ onSaveClick, onNewClick }) => { // Hidden we trigger via ref when the user clicks - // the Open .vlx button. Kept outside React state so the change event - // can fire repeatedly even if the user picks the same file twice. + // the Open project button. Accepts both .vlx (Velxio native) and .zip + // (Wokwi bundle); the dispatcher in utils/importProject.ts decides which + // loader to run based on the file extension. Kept outside React state so + // the change event still fires when the user picks the same file twice. const fileInputRef = useRef(null); - const handleOpenVlxClick = useCallback(() => { + const handleOpenProjectClick = useCallback(() => { fileInputRef.current?.click(); }, []); - const handleVlxFilePicked = useCallback(async (e: React.ChangeEvent) => { + const handleProjectFilePicked = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; // Reset so picking the SAME file again later still fires onchange. e.target.value = ''; if (!file) return; + const friendlyName = file.name.toLowerCase().endsWith('.zip') ? 'Wokwi .zip' : '.vlx'; if ( !window.confirm( - 'Load this .vlx file? Your current workspace will be replaced. This cannot be undone.', + `Load this ${friendlyName} project? Your current workspace will be replaced. ` + + `This cannot be undone.`, ) ) { return; } try { - await importVlxFile(file); + const result = await importProjectFile(file); + // .zip needs the caller to apply the payload to the stores (we keep + // that asymmetry so the toolbar's import flow can also pop the + // install-libraries modal afterwards). Here in the file explorer we + // don't have that modal, so we apply the payload silently and just + // warn in the console if the project references uninstalled libs. + if (result.kind === 'zip') { + const { loadFiles } = useEditorStore.getState(); + const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } = + useSimulatorStore.getState(); + stopSimulation(); + if (result.boardType) setBoardType(result.boardType); + setBoardPosition(result.boardPosition); + setComponents(result.components); + setWires(result.wires); + if (result.files.length > 0) loadFiles(result.files); + if (result.libraries.length > 0) { + console.warn( + '[FileExplorer] Imported Wokwi zip references libraries you may need to install:', + result.libraries, + ); + } + } } catch (err) { - const msg = err instanceof VlxParseError ? err.message : (err as Error).message; - window.alert(`Could not load .vlx file:\n\n${msg}`); + window.alert((err as Error).message); } }, []); @@ -330,16 +355,16 @@ export const FileExplorer: React.FC = ({ onSaveClick, onNewCl