diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index 90e77f80..3656f31a 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -186,9 +186,9 @@ const MECHANISM = { if (this.getFieldValue(FIELD_TYPE) !== foundMechanism.className) { this.setFieldValue(foundMechanism.className, FIELD_TYPE); } - // If the mechanism module name has changed, update this block. - if (this.mrcImportModule !== foundMechanism.moduleName) { - this.mrcImportModule = foundMechanism.moduleName; + const importModule = commonStorage.pascalCaseToSnakeCase(foundMechanism.className); + if (this.mrcImportModule !== importModule) { + this.mrcImportModule = importModule; } this.mrcParameters = []; components.forEach(component => { @@ -245,12 +245,10 @@ export const pythonFromBlock = function ( export function createMechanismBlock( mechanism: commonStorage.Mechanism, components: commonStorage.Component[]): toolboxItems.Block { - const lastDot = mechanism.className.lastIndexOf('.'); - const mechanismName = ( - 'my_' + - commonStorage.pascalCaseToSnakeCase(mechanism.className.substring(lastDot + 1))); + const snakeCaseName = commonStorage.pascalCaseToSnakeCase(mechanism.className); + const mechanismName = 'my_' + snakeCaseName; const extraState: MechanismExtraState = { - importModule: mechanism.moduleName, + importModule: snakeCaseName, parameters: [], }; const inputs: {[key: string]: any} = {}; diff --git a/src/blocks/utils/python.ts b/src/blocks/utils/python.ts index d18e1b30..1f26fd25 100644 --- a/src/blocks/utils/python.ts +++ b/src/blocks/utils/python.ts @@ -268,6 +268,8 @@ export function getOutputCheck(type: string): string { // This is a legal name for python methods and variables. export function getLegalName(proposedName: string, existingNames: string[]){ let newName = proposedName.trim().replace(' ', '_'); + + // TODO: Allow the user to put numbers in the name. if (!/^[A-Za-z_]/.test(newName)){ newName = "_" + newName; diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 6db3fae2..c6babd67 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -382,7 +382,8 @@ export class Editor { public getMechanism(mechanismInRobot: commonStorage.MechanismInRobot): commonStorage.Mechanism | null { if (this.currentProject) { for (const mechanism of this.currentProject.mechanisms) { - if (mechanism.moduleName + '.' + mechanism.className === mechanismInRobot.className) { + const fullClassName = commonStorage.pascalCaseToSnakeCase(mechanism.className) + '.' + mechanism.className; + if (fullClassName === mechanismInRobot.className) { return mechanism; } } diff --git a/src/reactComponents/AddTabDialog.tsx b/src/reactComponents/AddTabDialog.tsx index 2a76a064..b3d8e9f9 100644 --- a/src/reactComponents/AddTabDialog.tsx +++ b/src/reactComponents/AddTabDialog.tsx @@ -24,7 +24,7 @@ import * as Antd from 'antd'; import * as I18Next from 'react-i18next'; import * as React from 'react'; import * as commonStorage from '../storage/common_storage'; -import ModuleNameComponent from './ModuleNameComponent'; +import ClassNameComponent from './ClassNameComponent'; /** Represents a module item in the dialog. */ interface Module { @@ -306,7 +306,7 @@ export default function AddTabDialog(props: AddTabDialogProps) { - void; @@ -46,10 +46,10 @@ const INPUT_WIDTH_FULL = '100%'; const ERROR_ALERT_MARGIN_TOP = 8; /** - * Component for entering and validating module names. + * Component for entering and validating class names. * Provides input validation, error display, and automatic capitalization. */ -export default function ModuleNameComponent(props: ModuleNameComponentProps): React.JSX.Element { +export default function ClassNameComponent(props: ClassNameComponentProps): React.JSX.Element { const {t} = I18Next.useTranslation(); const [alertErrorMessage, setAlertErrorMessage] = React.useState(''); const [alertErrorVisible, setAlertErrorVisible] = React.useState(false); diff --git a/src/reactComponents/FileManageModal.tsx b/src/reactComponents/FileManageModal.tsx index cda4041d..209fabcf 100644 --- a/src/reactComponents/FileManageModal.tsx +++ b/src/reactComponents/FileManageModal.tsx @@ -24,7 +24,7 @@ import * as I18Next from 'react-i18next'; import * as React from 'react'; import * as commonStorage from '../storage/common_storage'; import {EditOutlined, DeleteOutlined, CopyOutlined} from '@ant-design/icons'; -import ModuleNameComponent from './ModuleNameComponent'; +import ClassNameComponent from './ClassNameComponent'; /** Represents a module in the file management system. */ interface Module { @@ -347,7 +347,7 @@ export default function FileManageModal(props: FileManageModalProps) { cancelText={t('Cancel')} > {currentRecord && ( - {currentRecord && ( - - { - return props.project?.userVisibleName || 'No Project Selected'; + return props.project?.projectName || 'No Project Selected'; }; return ( diff --git a/src/reactComponents/Menu.tsx b/src/reactComponents/Menu.tsx index c51922bc..ce5097dd 100644 --- a/src/reactComponents/Menu.tsx +++ b/src/reactComponents/Menu.tsx @@ -20,6 +20,7 @@ */ import * as Antd from 'antd'; import * as React from 'react'; +import { RcFile, UploadRequestOption } from 'rc-upload/lib/interface'; import * as commonStorage from '../storage/common_storage'; import * as createPythonFiles from '../storage/create_python_files'; import * as I18Next from 'react-i18next'; @@ -38,6 +39,8 @@ import { BgColorsOutlined, GlobalOutlined, CheckOutlined, + DownloadOutlined, + UploadOutlined, } from '@ant-design/icons'; import FileManageModal from './FileManageModal'; import ProjectManageModal from './ProjectManageModal'; @@ -180,6 +183,7 @@ export function Component(props: MenuProps): React.JSX.Element { const [noProjects, setNoProjects] = React.useState(false); const [aboutDialogVisible, setAboutDialogVisible] = React.useState(false); const [themeModalOpen, setThemeModalOpen] = React.useState(false); + const [showUploadAndDownload, _setShowUploadAndDownload] = React.useState(false); const handleThemeChange = (newTheme: string) => { props.setTheme(newTheme); @@ -322,6 +326,81 @@ export function Component(props: MenuProps): React.JSX.Element { } }; + // TODO: Add UI for the download action. + /** Handles the download action to generate and download json files. */ + const handleDownload = async (): Promise => { + if (!props.project || !props.storage) { + return; + } + + try { + const blobUrl = await props.storage.downloadProject(props.project.projectName); + const filename = props.project.projectName + commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION; + + // Create a temporary link to download the file + const link = document.createElement('a'); + link.href = blobUrl; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Clean up the blob URL + URL.revokeObjectURL(blobUrl); + } catch (error) { + console.error('Failed to download project:', error); + props.setAlertErrorMessage(t('DOWNLOAD_FAILED') || 'Failed to download project'); + } + } + + // TODO: Add UI for the upload action. + /** Handles the upload action to upload a previously downloaded project. */ + const handleUpload = (): Antd.UploadProps | null => { + if (!props.storage) { + return null; + } + + const uploadProps: Antd.UploadProps = { + accept: commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION, + beforeUpload: (file) => { + const isBlocks = file.name.endsWith(commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION) + if (!isBlocks) { + // TODO: i18n + props.setAlertErrorMessage(file.name + ' is not a blocks file'); + return false; + } + return isBlocks || Antd.Upload.LIST_IGNORE; + }, + onChange: (_info) => { + }, + customRequest: (options: UploadRequestOption) => { + const reader = new FileReader(); + reader.onload = (event) => { + if (!event.target) { + return; + } + const dataUrl = event.target.result as string; + const existingProjectNames: string[] = []; + projects.forEach(project => { + existingProjectNames.push(project.projectName); + }); + const file = options.file as RcFile; + const uploadProjectName = commonStorage.makeUploadProjectName(file.name, existingProjectNames); + if (props.storage) { + props.storage.uploadProject(uploadProjectName, dataUrl); + } + }; + reader.onerror = (_error) => { + console.log('Error reading file: ' + reader.error); + // TODO: i18n + props.setAlertErrorMessage(t('UPLOAD_FAILED') || 'Failed to upload project'); + }; + reader.readAsDataURL(options.file as Blob); + }, + }; + return uploadProps; + }; + /** Handles closing the file management modal. */ const handleFileModalClose = (): void => { console.log('Modal onCancel called'); @@ -382,6 +461,30 @@ export function Component(props: MenuProps): React.JSX.Element { items={menuItems} onClick={handleClick} /> + {showUploadAndDownload ? ( +
+ + } + size="small" + style={{ color: 'white' }} + /> + + } + size="small" + disabled={!props.project} + onClick={handleDownload} + style={{ color: 'white' }} + /> +
+ ) : ( +
+
+ )} setAboutDialogVisible(false)} diff --git a/src/reactComponents/ProjectManageModal.tsx b/src/reactComponents/ProjectManageModal.tsx index cb4a3b90..843f59d5 100644 --- a/src/reactComponents/ProjectManageModal.tsx +++ b/src/reactComponents/ProjectManageModal.tsx @@ -74,7 +74,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac const projects = await storage.listProjects(); // Sort projects alphabetically by name - projects.sort((a, b) => a.userVisibleName.localeCompare(b.userVisibleName)); + projects.sort((a, b) => a.projectName.localeCompare(b.projectName)); setAllProjects(projects); if (projects.length > 0 && props.noProjects) { @@ -186,25 +186,25 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac /** Opens the rename modal for a specific project. */ const openRenameModal = (record: commonStorage.Project): void => { setCurrentRecord(record); - setName(record.userVisibleName); + setName(record.projectName); setRenameModalOpen(true); }; /** Opens the copy modal for a specific project. */ const openCopyModal = (record: commonStorage.Project): void => { setCurrentRecord(record); - setName(record.userVisibleName + COPY_SUFFIX); + setName(record.projectName + COPY_SUFFIX); setCopyModalOpen(true); }; /** Gets the rename modal title. */ const getRenameModalTitle = (): string => { - return `Rename Project: ${currentRecord ? currentRecord.userVisibleName : ''}`; + return `Rename Project: ${currentRecord ? currentRecord.projectName : ''}`; }; /** Gets the copy modal title. */ const getCopyModalTitle = (): string => { - return `Copy Project: ${currentRecord ? currentRecord.userVisibleName : ''}`; + return `Copy Project: ${currentRecord ? currentRecord.projectName : ''}`; }; /** Creates the container style object. */ @@ -224,8 +224,8 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac const columns: Antd.TableProps['columns'] = [ { title: 'Name', - dataIndex: 'userVisibleName', - key: 'userVisibleName', + dataIndex: 'projectName', + key: 'projectName', ellipsis: { showTitle: false, }, @@ -268,7 +268,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac {allProjects.length > 1 && ( handleDeleteProject(record)} okText={t('Delete')} @@ -385,7 +385,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac columns={columns} dataSource={allProjects} - rowKey="userVisibleName" + rowKey="projectName" size="small" pagination={allProjects.length > DEFAULT_PAGE_SIZE ? { pageSize: DEFAULT_PAGE_SIZE, diff --git a/src/reactComponents/ProjectNameComponent.tsx b/src/reactComponents/ProjectNameComponent.tsx index 3856a01a..746c53b7 100644 --- a/src/reactComponents/ProjectNameComponent.tsx +++ b/src/reactComponents/ProjectNameComponent.tsx @@ -58,18 +58,18 @@ export default function ProjectNameComponent(props: ProjectNameComponentProps): /** Handles adding a new item with validation. */ const handleAddNewItem = (): void => { - const newUserVisibleName = props.newItemName.trim(); - if (!newUserVisibleName || !props.projects) { + const newProjectName = props.newItemName.trim(); + if (!newProjectName || !props.projects) { return; } - if (!commonStorage.isValidClassName(newUserVisibleName)) { - showError(newUserVisibleName + INVALID_NAME_MESSAGE_SUFFIX); + if (!commonStorage.isValidClassName(newProjectName)) { + showError(newProjectName + INVALID_NAME_MESSAGE_SUFFIX); return; } - if (props.projects.some((project) => project.userVisibleName === newUserVisibleName)) { - showError(DUPLICATE_NAME_MESSAGE_PREFIX + newUserVisibleName + DUPLICATE_NAME_MESSAGE_SUFFIX); + if (props.projects.some((project) => project.projectName === newProjectName)) { + showError(DUPLICATE_NAME_MESSAGE_PREFIX + newProjectName + DUPLICATE_NAME_MESSAGE_SUFFIX); return; } diff --git a/src/reactComponents/Tabs.tsx b/src/reactComponents/Tabs.tsx index f64e0378..b2fb69f5 100644 --- a/src/reactComponents/Tabs.tsx +++ b/src/reactComponents/Tabs.tsx @@ -30,7 +30,7 @@ import { CloseCircleOutlined, } from '@ant-design/icons'; import AddTabDialog from './AddTabDialog'; -import ModuleNameComponent from './ModuleNameComponent'; +import ClassNameComponent from './ClassNameComponent'; import { TabType, TabTypeUtils } from '../types/TabType'; /** Represents a tab item in the tab bar. */ @@ -375,7 +375,7 @@ export function Component(props: TabsProps): React.JSX.Element { cancelText={t('Cancel')} > {currentTab && ( - {currentTab && ( - { return new Promise((resolve, reject) => { const openRequest = window.indexedDB.open(DATABASE_NAME, 1); @@ -38,14 +44,14 @@ export async function openClientSideStorage(): Promise { const stores = db.objectStoreNames; - if (!stores.contains('entries')) { + if (!stores.contains(ENTRIES_STORE_NAME)) { // Create an object store for key/value entries. - db.createObjectStore('entries', { keyPath: 'key' }); + db.createObjectStore(ENTRIES_STORE_NAME, { keyPath: ENTRIES_KEY }); } - if (!stores.contains('modules')) { + if (!stores.contains(MODULES_STORE_NAME)) { // Create the object store for modules. - db.createObjectStore('modules', { keyPath: 'path' }); + db.createObjectStore(MODULES_STORE_NAME, { keyPath: MODULES_KEY }); } }; openRequest.onsuccess = () => { @@ -68,7 +74,7 @@ class ClientSideStorage implements commonStorage.Storage { async saveEntry(entryKey: string, entryValue: string): Promise { return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['entries'], 'readwrite'); + const transaction = this.db.transaction([ENTRIES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { resolve(); }; @@ -76,7 +82,7 @@ class ClientSideStorage implements commonStorage.Storage { console.log('IndexedDB transaction aborted.'); reject(new Error('IndexedDB transaction aborted.')); }; - const entriesObjectStore = transaction.objectStore('entries'); + const entriesObjectStore = transaction.objectStore(ENTRIES_STORE_NAME); const getRequest = entriesObjectStore.get(entryKey); getRequest.onerror = () => { console.log('IndexedDB get request failed. getRequest.error is...'); @@ -104,8 +110,8 @@ class ClientSideStorage implements commonStorage.Storage { async fetchEntry(entryKey: string, defaultValue: string): Promise { return new Promise((resolve, reject) => { - const getRequest = this.db.transaction(['entries'], 'readonly') - .objectStore('entries').get(entryKey); + const getRequest = this.db.transaction([ENTRIES_STORE_NAME], 'readonly') + .objectStore(ENTRIES_STORE_NAME).get(entryKey); getRequest.onerror = () => { console.log('IndexedDB get request failed. getRequest.error is...'); console.log(getRequest.error); @@ -125,8 +131,8 @@ class ClientSideStorage implements commonStorage.Storage { // are read before the Project to which they belong is read. const mechanisms: {[key: string]: commonStorage.Mechanism[]} = {}; // key is project name, value is list of Mechanisms const opModes: {[key: string]: commonStorage.OpMode[]} = {}; // key is project name, value is list of OpModes - const openCursorRequest = this.db.transaction(['modules'], 'readonly') - .objectStore('modules') + const openCursorRequest = this.db.transaction([MODULES_STORE_NAME], 'readonly') + .objectStore(MODULES_STORE_NAME) .openCursor(); openCursorRequest.onerror = () => { console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); @@ -139,14 +145,12 @@ class ClientSideStorage implements commonStorage.Storage { const value = cursor.value; const path = value.path; const moduleType = value.type; - const moduleName = commonStorage.getModuleName(path); const module: commonStorage.Module = { modulePath: path, moduleType: moduleType, projectName: commonStorage.getProjectName(path), - moduleName: moduleName, + className: commonStorage.getClassName(path), dateModifiedMillis: value.dateModifiedMillis, - className: commonStorage.getClassNameForModule(moduleType, moduleName), } if (moduleType === commonStorage.MODULE_TYPE_ROBOT) { const robot: commonStorage.Robot = { @@ -154,7 +158,6 @@ class ClientSideStorage implements commonStorage.Storage { }; const project: commonStorage.Project = { projectName: module.projectName, - userVisibleName: commonStorage.snakeCaseToPascalCase(module.projectName), robot: robot, mechanisms: [], opModes: [], @@ -221,8 +224,8 @@ class ClientSideStorage implements commonStorage.Storage { async fetchModuleContentText(modulePath: string): Promise { return new Promise((resolve, reject) => { - const getRequest = this.db.transaction(['modules'], 'readonly') - .objectStore('modules').get(modulePath); + const getRequest = this.db.transaction([MODULES_STORE_NAME], 'readonly') + .objectStore(MODULES_STORE_NAME).get(modulePath); getRequest.onerror = () => { console.log('IndexedDB get request failed. getRequest.error is...'); console.log(getRequest.error); @@ -241,7 +244,7 @@ class ClientSideStorage implements commonStorage.Storage { async createProject(projectName: string, robotContent: string, opmodeContent : string): Promise { const modulePath = commonStorage.makeRobotPath(projectName); - const opmodePath = commonStorage.makeModulePath(projectName, 'teleop'); + const opmodePath = commonStorage.makeModulePath(projectName, commonStorage.CLASS_NAME_TELEOP); await this._saveModule(commonStorage.MODULE_TYPE_ROBOT, modulePath, robotContent); await this._saveModule(commonStorage.MODULE_TYPE_OPMODE, opmodePath, opmodeContent); @@ -260,7 +263,7 @@ class ClientSideStorage implements commonStorage.Storage { // When creating a new module, moduleType must be truthy. // When saving an existing module, the moduleType must be falsy. return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['modules'], 'readwrite'); + const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { resolve(); }; @@ -268,7 +271,7 @@ class ClientSideStorage implements commonStorage.Storage { console.log('IndexedDB transaction aborted.'); reject(new Error('IndexedDB transaction aborted.')); }; - const modulesObjectStore = transaction.objectStore('modules'); + const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); const getRequest = modulesObjectStore.get(modulePath); getRequest.onerror = () => { console.log('IndexedDB get request failed. getRequest.error is...'); @@ -314,7 +317,7 @@ class ClientSideStorage implements commonStorage.Storage { private async _renameOrCopyProject(oldProjectName: string, newProjectName: string, copy: boolean): Promise { return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['modules'], 'readwrite'); + const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { resolve(); }; @@ -322,7 +325,7 @@ class ClientSideStorage implements commonStorage.Storage { console.log('IndexedDB transaction aborted.'); reject(new Error('IndexedDB transaction aborted.')); }; - const modulesObjectStore = transaction.objectStore('modules'); + const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); // First get the list of modules in the project. const oldToNewModulePaths: {[key: string]: string} = {}; const openCursorRequest = modulesObjectStore.openCursor(); @@ -336,15 +339,14 @@ class ClientSideStorage implements commonStorage.Storage { if (cursor) { const value = cursor.value; const path = value.path; - // Before PR #143, robot modules were stored with the type 'project'. - const moduleType = (value.type == 'project') ? commonStorage.MODULE_TYPE_ROBOT : value.type; + const moduleType = value.type; if (commonStorage.getProjectName(path) === oldProjectName) { let newPath; if (moduleType === commonStorage.MODULE_TYPE_ROBOT) { newPath = commonStorage.makeRobotPath(newProjectName); } else { - const moduleName = commonStorage.getModuleName(path); - newPath = commonStorage.makeModulePath(newProjectName, moduleName); + const className = commonStorage.getClassName(path); + newPath = commonStorage.makeModulePath(newProjectName, className); } oldToNewModulePaths[path] = newPath; } @@ -399,30 +401,30 @@ class ClientSideStorage implements commonStorage.Storage { async renameModule( moduleType: string, projectName: string, - oldModuleName: string, newModuleName: string): Promise { + oldClassName: string, newClassName: string): Promise { if (moduleType == commonStorage.MODULE_TYPE_ROBOT) { throw new Error('Renaming the robot module is not allowed. Call renameProject to rename the project.'); } return this._renameOrCopyModule( - projectName, oldModuleName, newModuleName, false); + projectName, oldClassName, newClassName, false); } async copyModule( moduleType: string, projectName: string, - oldModuleName: string, newModuleName: string): Promise { + oldClassName: string, newClassName: string): Promise { if (moduleType == commonStorage.MODULE_TYPE_ROBOT) { throw new Error('Copying the robot module is not allowed. Call copyProject to rename the project.'); } return this._renameOrCopyModule( - projectName, oldModuleName, newModuleName, true); + projectName, oldClassName, newClassName, true); } private async _renameOrCopyModule( projectName: string, - oldModuleName: string, newModuleName: string, copy: boolean): Promise { + oldClassName: string, newClassName: string, copy: boolean): Promise { return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['modules'], 'readwrite'); + const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { resolve(); }; @@ -430,9 +432,9 @@ class ClientSideStorage implements commonStorage.Storage { console.log('IndexedDB transaction aborted.'); reject(new Error('IndexedDB transaction aborted.')); }; - const modulesObjectStore = transaction.objectStore('modules'); - const oldModulePath = commonStorage.makeModulePath(projectName, oldModuleName); - const newModulePath = commonStorage.makeModulePath(projectName, newModuleName); + const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); + const oldModulePath = commonStorage.makeModulePath(projectName, oldClassName); + const newModulePath = commonStorage.makeModulePath(projectName, newClassName); const getRequest = modulesObjectStore.get(oldModulePath); getRequest.onerror = () => { console.log('IndexedDB get request failed. getRequest.error is...'); @@ -472,7 +474,7 @@ class ClientSideStorage implements commonStorage.Storage { async deleteProject(projectName: string): Promise { return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['modules'], 'readwrite'); + const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { resolve(); }; @@ -480,7 +482,7 @@ class ClientSideStorage implements commonStorage.Storage { console.log('IndexedDB transaction aborted.'); reject(new Error('IndexedDB transaction aborted.')); }; - const modulesObjectStore = transaction.objectStore('modules'); + const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); // First get the list of modulePaths in the project. const modulePaths: string[] = []; const openCursorRequest = modulesObjectStore.openCursor(); @@ -521,7 +523,7 @@ class ClientSideStorage implements commonStorage.Storage { } return new Promise((resolve, reject) => { - const transaction = this.db.transaction(['modules'], 'readwrite'); + const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { resolve(); }; @@ -529,7 +531,7 @@ class ClientSideStorage implements commonStorage.Storage { console.log('IndexedDB transaction aborted.'); reject(new Error('IndexedDB transaction aborted.')); }; - const modulesObjectStore = transaction.objectStore('modules'); + const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); const deleteRequest = modulesObjectStore.delete(modulePath); deleteRequest.onerror = () => { console.log('IndexedDB delete request failed. deleteRequest.error is...'); @@ -544,9 +546,9 @@ class ClientSideStorage implements commonStorage.Storage { async downloadProject(projectName: string): Promise { return new Promise((resolve, reject) => { // Collect all the modules in the project. - const moduleNameToContentText: {[key: string]: string} = {}; // key is module name, value is module content - const openCursorRequest = this.db.transaction(['modules'], 'readonly') - .objectStore('modules') + const classNameToModuleContentText: {[className: string]: string} = {}; // key is class name, value is module content + const openCursorRequest = this.db.transaction([MODULES_STORE_NAME], 'readonly') + .objectStore(MODULES_STORE_NAME) .openCursor(); openCursorRequest.onerror = () => { console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); @@ -558,14 +560,14 @@ class ClientSideStorage implements commonStorage.Storage { if (cursor) { const value = cursor.value; if (commonStorage.getProjectName(value.path) === projectName) { - const moduleName = commonStorage.getModuleName(value.path); - moduleNameToContentText[moduleName] = value.content; + const className = commonStorage.getClassName(value.path); + classNameToModuleContentText[className] = value.content; } cursor.continue(); } else { // The cursor is done. We have finished collecting all the modules in the project. // Now create the blob for download. - const blobUrl = await commonStorage.produceDownloadProjectBlob(moduleNameToContentText); + const blobUrl = await commonStorage.produceDownloadProjectBlob(classNameToModuleContentText); resolve(blobUrl); } }; @@ -575,10 +577,10 @@ class ClientSideStorage implements commonStorage.Storage { async uploadProject(projectName: string, blobUrl: string): Promise { return new Promise(async (resolve, reject) => { // Process the uploaded blob to get the module types and contents. - let moduleNameToType: {[key: string]: string}; // key is module name, value is module content - let moduleNameToContentText: {[key: string]: string}; // key is module name, value is module content + let classNameToModuleType: {[className: string]: string}; // key is class name, value is module type + let classNameToModuleContentText: {[className: string]: string}; // key is class name, value is module content try { - [moduleNameToType, moduleNameToContentText] = await commonStorage.processUploadedBlob( + [classNameToModuleType, classNameToModuleContentText] = await commonStorage.processUploadedBlob( blobUrl); } catch (e) { console.log('commonStorage.processUploadedBlob failed.'); @@ -587,7 +589,7 @@ class ClientSideStorage implements commonStorage.Storage { } // Save each module. - const transaction = this.db.transaction(['modules'], 'readwrite'); + const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { resolve(); }; @@ -595,12 +597,12 @@ class ClientSideStorage implements commonStorage.Storage { console.log('IndexedDB transaction aborted.'); reject(new Error('IndexedDB transaction aborted.')); }; - const modulesObjectStore = transaction.objectStore('modules'); + const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); - for (const moduleName in moduleNameToType) { - const moduleType = moduleNameToType[moduleName]; - const moduleContentText = moduleNameToContentText[moduleName]; - const modulePath = commonStorage.makeModulePath(projectName, moduleName); + for (const className in classNameToModuleType) { + const moduleType = classNameToModuleType[className]; + const moduleContentText = classNameToModuleContentText[className]; + const modulePath = commonStorage.makeModulePath(projectName, className); const getRequest = modulesObjectStore.get(modulePath); getRequest.onerror = () => { console.log('IndexedDB get request failed. getRequest.error is...'); diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index bc993876..f9cdb31e 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -31,10 +31,9 @@ export type Module = { // TODO(lizlooney): Add a uuid so we can keep track of mechanisms in the robot even if the user renames the mechamism modulePath: string, moduleType: string, - projectName: string, - moduleName: string, + projectName: string, // For example, WackyWheelerRobot + className: string, // For example, GamePieceShooter. dateModifiedMillis: number, - className: string, // Does not include the module name. }; export type Robot = Module; @@ -42,8 +41,7 @@ export type Mechanism = Module; export type OpMode = Module; export type Project = { - projectName: string, // snake_case - userVisibleName: string, // PascalCase + projectName: string, // For example, WackyWheelerRobot robot: Robot, mechanisms: Mechanism[] opModes: OpMode[], @@ -87,9 +85,16 @@ export const MODULE_TYPE_MECHANISM = 'mechanism'; export const MODULE_TYPE_OPMODE = 'opmode'; const CLASS_NAME_ROBOT = 'Robot'; +export const CLASS_NAME_TELEOP = 'Teleop'; +export const JSON_FILE_EXTENSION = '.json'; export const UPLOAD_DOWNLOAD_FILE_EXTENSION = '.blocks'; +/** + * Regular expression to extract the project name and the class name from a module path. + */ +const REGEX_MODULE_PATH = '^([A-Za-z_][A-Za-z0-9_]*)/([A-Za-z_][A-Za-z0-9_]*).json$'; + export interface Storage { saveEntry(entryKey: string, entryValue: string): Promise; fetchEntry(entryKey: string, defaultValue: string): Promise; @@ -100,8 +105,8 @@ export interface Storage { saveModule(modulePath: string, moduleContentText: string): Promise; renameProject(oldProjectName: string, newProjectName: string): Promise; copyProject(oldProjectName: string, newProjectName: string): Promise; - renameModule(moduleType: string, projectName: string, oldModuleName: string, newModuleName: string): Promise; - copyModule(moduleType: string, projectName: string, oldModuleName: string, newModuleName: string): Promise; + renameModule(moduleType: string, projectName: string, oldClassName: string, newClassName: string): Promise; + copyModule(moduleType: string, projectName: string, oldClassName: string, newClassName: string): Promise; deleteProject(projectName: string): Promise; deleteModule(moduleType: string, modulePath: string): Promise; downloadProject(projectName: string): Promise; @@ -111,14 +116,13 @@ export interface Storage { /** * Creates a new project. * @param storage The storage interface to use for creating the project. - * @param proposedUserVisibleName The name for the new project. + * @param newProjectName The name for the new project. For example, WackyWheelerRobot * @returns A promise that resolves when the project has been created. */ export async function createProject( - storage: Storage, proposedUserVisibleName: string): Promise { - const newProjectName = pascalCaseToSnakeCase(proposedUserVisibleName); + storage: Storage, newProjectName: string): Promise { const robotContent = newRobotContent(newProjectName); - const opmodeContent = newOpModeContent(newProjectName, 'Teleop'); + const opmodeContent = newOpModeContent(newProjectName, CLASS_NAME_TELEOP); await storage.createProject(newProjectName, robotContent, opmodeContent); } @@ -126,12 +130,11 @@ export async function createProject( * Renames a project. * @param storage The storage interface to use for renaming the project. * @param project The project to rename - * @param proposedUserVisibleName The new name for the project. + * @param newProjectName The new name for the project. For example, WackyWheelerRobot * @returns A promise that resolves when the project has been renamed. */ export async function renameProject( - storage: Storage, project: Project, proposedUserVisibleName: string): Promise { - const newProjectName = pascalCaseToSnakeCase(proposedUserVisibleName); + storage: Storage, project: Project, newProjectName: string): Promise { await storage.renameProject(project.projectName, newProjectName); } @@ -139,12 +142,11 @@ export async function renameProject( * Copies a project. * @param storage The storage interface to use for copying the project. * @param project The project to copy - * @param proposedUserVisibleName The name for the new project. + * @param newProjectName The name for the new project. For example, WackyWheelerRobot * @returns A promise that resolves when the project has been copied. */ export async function copyProject( - storage: Storage, project: Project, proposedUserVisibleName: string): Promise { - const newProjectName = pascalCaseToSnakeCase(proposedUserVisibleName); + storage: Storage, project: Project, newProjectName: string): Promise { await storage.copyProject(project.projectName, newProjectName); } @@ -164,31 +166,28 @@ export async function deleteProject( * @param storage The storage interface to use for creating the module. * @param project The project to add the module to. * @param moduleType The type of the module (e.g., 'mechanism', 'opmode'). - * @param newClassName The name of the class. + * @param newClassName The name of the class. For example, GamePieceShooter. */ export async function addModuleToProject( storage: Storage, project: Project, moduleType: string, newClassName: string): Promise { - const newModuleName = pascalCaseToSnakeCase(newClassName); - const newModulePath = makeModulePath(project.projectName, newModuleName); + const newModulePath = makeModulePath(project.projectName, newClassName); if (moduleType === MODULE_TYPE_MECHANISM) { - const mechanismContent = newMechanismContent(project.projectName, newModuleName); + const mechanismContent = newMechanismContent(project.projectName, newClassName); await storage.createModule(MODULE_TYPE_MECHANISM, newModulePath, mechanismContent); project.mechanisms.push({ modulePath: newModulePath, moduleType: MODULE_TYPE_MECHANISM, projectName: project.projectName, - moduleName: newModuleName, className: newClassName } as Mechanism); } else if (moduleType === MODULE_TYPE_OPMODE) { - const opModeContent = newOpModeContent(project.projectName, newModuleName); + const opModeContent = newOpModeContent(project.projectName, newClassName); await storage.createModule(MODULE_TYPE_OPMODE, newModulePath, opModeContent); project.opModes.push({ modulePath: newModulePath, moduleType: MODULE_TYPE_OPMODE, projectName: project.projectName, - moduleName: newModuleName, className: newClassName } as OpMode); } @@ -219,38 +218,37 @@ export async function removeModuleFromProject( * Renames a module in the project. * @param storage The storage interface to use for renaming the module. * @param project The project containing the module to rename. - * @param proposedClassName The new class name for the module. + * @param newClassName The new name for the module. For example, GamePieceShooter. * @param oldModulePath The current path of the module. * @returns A promise that resolves when the module has been renamed. */ export async function renameModuleInProject( - storage: Storage, project: Project, proposedClassName: string, oldModulePath: string): Promise { + storage: Storage, project: Project, newClassName: string, oldModulePath: string): Promise { const module = findModuleByModulePath(project, oldModulePath); if (module) { if (module.moduleType == MODULE_TYPE_ROBOT) { throw new Error('Renaming the robot module is not allowed.'); } - const newModuleName = pascalCaseToSnakeCase(proposedClassName); - const newModulePath = makeModulePath(project.projectName, newModuleName); - await storage.renameModule(module.moduleType, project.projectName, module.moduleName, newModuleName); + const newModulePath = makeModulePath(project.projectName, newClassName); + await storage.renameModule(module.moduleType, project.projectName, module.className, newClassName); module.modulePath = newModulePath; - module.moduleName = newModuleName; - module.className = proposedClassName; + module.className = newClassName; + module.className = newClassName; if (module.moduleType === MODULE_TYPE_MECHANISM) { const mechanism = project.mechanisms.find(m => m.modulePath === module.modulePath); if (mechanism) { mechanism.modulePath = newModulePath; - mechanism.moduleName = newModuleName; - mechanism.className = proposedClassName; + mechanism.className = newClassName; + mechanism.className = newClassName; } return newModulePath; } else if (module.moduleType === MODULE_TYPE_OPMODE) { const opMode = project.opModes.find(o => o.modulePath === module.modulePath); if (opMode) { opMode.modulePath = newModulePath; - opMode.moduleName = newModuleName; - opMode.className = proposedClassName; + opMode.className = newClassName; + opMode.className = newClassName; } return newModulePath } @@ -261,36 +259,33 @@ export async function renameModuleInProject( * Copies a module in the project. * @param storage The storage interface to use for copying the module. * @param project The project containing the module to copy. - * @param proposedClassName The new name for the module. - * @param oldModuleName The current name of the module. + * @param newClassName The new name for the module. For example, GamePieceShooter. + * @param oldModulePath The current path of the module. * @returns A promise that resolves when the module has been copied. */ export async function copyModuleInProject( - storage: Storage, project: Project, proposedClassName: string, oldModulePath: string): Promise { + storage: Storage, project: Project, newClassName: string, oldModulePath: string): Promise { const module = findModuleByModulePath(project, oldModulePath); if (module) { if (module.moduleType == MODULE_TYPE_ROBOT) { throw new Error('Copying the robot module is not allowed.'); } - const newModuleName = pascalCaseToSnakeCase(proposedClassName); - const newModulePath = makeModulePath(project.projectName, newModuleName); - await storage.copyModule(module.moduleType, project.projectName, module.moduleName, newModuleName); + const newModulePath = makeModulePath(project.projectName, newClassName); + await storage.copyModule(module.moduleType, project.projectName, module.className, newClassName); if (module.moduleType === MODULE_TYPE_MECHANISM) { project.mechanisms.push({ modulePath: newModulePath, moduleType: MODULE_TYPE_MECHANISM, projectName: project.projectName, - moduleName: newModuleName, - className: proposedClassName + className: newClassName } as Mechanism); } else if (module.moduleType === MODULE_TYPE_OPMODE) { project.opModes.push({ modulePath: newModulePath, moduleType: MODULE_TYPE_OPMODE, projectName: project.projectName, - moduleName: newModuleName, - className: proposedClassName + className: newClassName } as OpMode); } return newModulePath; @@ -407,84 +402,74 @@ export function isValidClassName(name: string): boolean { } /** - * Returns the module name (snake_case) for the given class name (PascalCase). + * Returns the snake_case name for the given PascalCase name. */ -export function pascalCaseToSnakeCase(className: string): string { - let moduleName = ''; - for (let i = 0; i < className.length; i++) { - const char = className.charAt(i); +export function pascalCaseToSnakeCase(pascalCaseName: string): string { + let snakeCaseName = ''; + for (let i = 0; i < pascalCaseName.length; i++) { + const char = pascalCaseName.charAt(i); if (char >= 'A' && char <= 'Z') { if (i > 0) { - moduleName += '_'; + snakeCaseName += '_'; } - moduleName += char.toLowerCase(); + snakeCaseName += char.toLowerCase(); } else { - moduleName += char; + snakeCaseName += char; } } - return moduleName; + return snakeCaseName; } /** - * Returns the class name (PascalCase) for the given module name (snake_case). + * Returns the PascalCase name for the given snake_case name. */ -export function snakeCaseToPascalCase(moduleName: string): string { - let className = ''; +export function snakeCaseToPascalCase(snakeCaseName: string): string { + let pascalCaseName = ''; let nextCharUpper = true; - for (let i = 0; i < moduleName.length; i++) { - const char = moduleName.charAt(i); + for (let i = 0; i < snakeCaseName.length; i++) { + const char = snakeCaseName.charAt(i); if (char !== '_') { - className += nextCharUpper ? char.toUpperCase() : char; + pascalCaseName += nextCharUpper ? char.toUpperCase() : char; } nextCharUpper = (char === '_'); } - return className; + return pascalCaseName; } /** - * Returns true if the given name is a valid python module name. + * Returns the module path for the given project name and class name. */ -export function isValidPythonModuleName(name: string): boolean { - if (name) { - return /^[a-z_][a-z0-9_]*$/.test(name); - } - return false; -} - -/** - * Returns the module path for the given project and module names. - */ -export function makeModulePath(projectName: string, moduleName: string): string { - return projectName + '/' + moduleName + '.blocks'; +export function makeModulePath(projectName: string, className: string): string { + return projectName + '/' + className + JSON_FILE_EXTENSION;; } /** * Returns the robot module path for the given project names. */ export function makeRobotPath(projectName: string): string { - return makeModulePath(projectName, 'robot'); + return makeModulePath(projectName, CLASS_NAME_ROBOT); } /** * Returns the project path for given module path. */ export function getProjectName(modulePath: string): string { - const regex = new RegExp('^([a-z_A-Z][a-z0-9_]*)/([a-z_A-Z][a-z0-9_]*).blocks$'); + const regex = new RegExp(REGEX_MODULE_PATH); const result = regex.exec(modulePath) if (!result) { - throw new Error('Unable to extract the project name.'); + throw new Error('Unable to extract the project name from "' + modulePath + '"'); } return result[1]; } /** - * Returns the module name for given module path. + * Returns the class name for given module path. */ -export function getModuleName(modulePath: string): string { - const regex = new RegExp('^([a-z_A-Z][a-z0-9_]*)/([a-z_A-Z][a-z0-9_]*).blocks$'); +export function getClassName(modulePath: string): string { + const regex = new RegExp(REGEX_MODULE_PATH); const result = regex.exec(modulePath) if (!result) { - throw new Error('Unable to extract the module name.'); + throw new Error('Unable to extract the class name from "' + modulePath + '"'); } return result[2]; } @@ -512,9 +497,8 @@ export function newRobotContent(projectName: string): string { modulePath: makeRobotPath(projectName), moduleType: MODULE_TYPE_ROBOT, projectName: projectName, - moduleName: 'robot', - dateModifiedMillis: 0, className: CLASS_NAME_ROBOT, + dateModifiedMillis: 0, }; return startingBlocksToModuleContentText(module, startingRobotBlocks); @@ -523,14 +507,13 @@ export function newRobotContent(projectName: string): string { /** * Returns the module content for a new Mechanism. */ -export function newMechanismContent(projectName: string, mechanismName: string): string { - const module: Module = { - modulePath: makeModulePath(projectName, mechanismName), +export function newMechanismContent(projectName: string, mechanismClassName: string): string { + const module: Mechanism = { + modulePath: makeModulePath(projectName, mechanismClassName), moduleType: MODULE_TYPE_MECHANISM, projectName: projectName, - moduleName: mechanismName, + className: mechanismClassName, dateModifiedMillis: 0, - className: snakeCaseToPascalCase(mechanismName), }; return startingBlocksToModuleContentText(module, startingMechanismBlocks); @@ -539,14 +522,13 @@ export function newMechanismContent(projectName: string, mechanismName: string): /** * Returns the module content for a new OpMode. */ -export function newOpModeContent(projectName: string, opModeName: string): string { - const module: Module = { - modulePath: makeModulePath(projectName, opModeName), +export function newOpModeContent(projectName: string, opModeClassName: string): string { + const module: OpMode = { + modulePath: makeModulePath(projectName, opModeClassName), moduleType: MODULE_TYPE_OPMODE, projectName: projectName, - moduleName: opModeName, + className: opModeClassName, dateModifiedMillis: 0, - className: snakeCaseToPascalCase(opModeName), }; return startingBlocksToModuleContentText(module, startingOpModeBlocks); @@ -594,7 +576,7 @@ export class ModuleContent { } getModuleContentText(): string { - return JSON.stringify(this); + return JSON.stringify(this, null, 2); } getModuleType(): string { @@ -626,23 +608,18 @@ export class ModuleContent { * Produce the blob for downloading a project. */ export async function produceDownloadProjectBlob( - moduleNameToContentText: { [key: string]: string }): Promise { + classNameToModuleContentText: { [key: string]: string }): Promise { const zip = new JSZip(); - for (const moduleName in moduleNameToContentText) { - const moduleContentText = moduleNameToContentText[moduleName]; - zip.file(moduleName, moduleContentText); + for (const className in classNameToModuleContentText) { + const moduleContentText = classNameToModuleContentText[className]; + const filename = className + JSON_FILE_EXTENSION; + zip.file(filename, moduleContentText); } const content = await zip.generateAsync({ type: "blob" }); const blobUrl = URL.createObjectURL(content); return blobUrl; } -export function getClassNameForModule(moduleType: string, moduleName: string) { - return (moduleType == MODULE_TYPE_ROBOT) - ? CLASS_NAME_ROBOT - : snakeCaseToPascalCase(moduleName); -} - /** * Make a unique project name for an uploaded project. */ @@ -655,10 +632,11 @@ export function makeUploadProjectName( /** * Process the uploaded blob to get the module types and contents. + * Returns a promise of classNameToModuleType and classNameToModuleContentText. */ export async function processUploadedBlob( blobUrl: string) - : Promise<[{ [key: string]: string }, { [key: string]: string }]> { + : Promise<[{ [className: string]: string }, { [className: string]: string }]> { const prefix = 'data:application/octet-stream;base64,'; if (!blobUrl.startsWith(prefix)) { @@ -668,8 +646,11 @@ export async function processUploadedBlob( const zip = await JSZip.loadAsync(data, { base64: true }); const promises: { [key: string]: Promise } = {}; - zip.forEach((moduleName, zipEntry) => { - promises[moduleName] = zipEntry.async('text'); + zip.forEach((filename, zipEntry) => { + const className = filename.endsWith(JSON_FILE_EXTENSION) + ? filename.substring(0, filename.length - JSON_FILE_EXTENSION.length) + : filename; + promises[className] = zipEntry.async('text'); }); // Wait for all promises to resolve. @@ -681,31 +662,30 @@ export async function processUploadedBlob( ); // Process each module's content. - const moduleNameToType: { [key: string]: string } = {}; // key is module name, value is module type - const moduleNameToContentText: { [key: string]: string } = {}; // key is module name, value is module content text + const classNameToModuleType: { [className: string]: string } = {}; // key is class name, value is module type + const classNameToModuleContentText: { [className: string]: string } = {}; // key is class name, value is module content text for (const filename in files) { const uploadedContent = files[filename]; - const [moduleName, moduleType, moduleContent] = _processUploadedModule( + const [className, moduleType, moduleContent] = _processUploadedModule( filename, uploadedContent); - moduleNameToType[moduleName] = moduleType; - moduleNameToContentText[moduleName] = moduleContent; + classNameToModuleType[className] = moduleType; + classNameToModuleContentText[className] = moduleContent; } - return [moduleNameToType, moduleNameToContentText]; + return [classNameToModuleType, classNameToModuleContentText]; } /** - * Processes an uploaded module to get the module name, type, and content text. + * Processes an uploaded module to get the class name, type, and content text. */ export function _processUploadedModule( - filename: string, uploadedContent: string) - : [string, string, string] { + filename: string, uploadedContent: string): [string, string, string] { const moduleContent = parseModuleContentText(uploadedContent); const moduleType = moduleContent.getModuleType(); - const moduleName = (moduleType === MODULE_TYPE_ROBOT) ? 'robot' : filename; + const className = filename; const moduleContentText = moduleContent.getModuleContentText(); - return [moduleName, moduleType, moduleContentText]; + return [className, moduleType, moduleContentText]; } /** diff --git a/src/storage/create_python_files.ts b/src/storage/create_python_files.ts index d815c2bc..2e298621 100644 --- a/src/storage/create_python_files.ts +++ b/src/storage/create_python_files.ts @@ -23,12 +23,17 @@ import * as Blockly from 'blockly/core'; import { extendedPythonGenerator } from '../editor/extended_python_generator'; -import { Project, Module, Storage, parseModuleContentText } from './common_storage'; +import { + Project, + Module, + Storage, + parseModuleContentText, + pascalCaseToSnakeCase } from './common_storage'; import JSZip from 'jszip'; import { GeneratorContext } from '../editor/generator_context'; /** Result of Python code generation for a single module */ -export interface ModulePythonResult { +interface ModulePythonResult { moduleName: string; pythonCode: string; success: boolean; @@ -36,7 +41,7 @@ export interface ModulePythonResult { } /** Result of Python code generation for an entire project */ -export interface ProjectPythonResult { +interface ProjectPythonResult { projectName: string; modules: ModulePythonResult[]; success: boolean; @@ -49,45 +54,46 @@ export interface ProjectPythonResult { * @param storage The storage interface to fetch module content * @returns Result containing generated Python code or error */ -export async function generatePythonForModule(module: Module, storage: Storage): Promise { +async function generatePythonForModule(module: Module, storage: Storage): Promise { + const moduleName = pascalCaseToSnakeCase(module.className); + try { // Fetch the module content from storage const moduleContentText = await storage.fetchModuleContentText(module.modulePath); const moduleContent = parseModuleContentText(moduleContentText); - + // Create a headless workspace const workspace = new Blockly.Workspace(); - + // Parse and load the JSON into the workspace const blocks = moduleContent.getBlocks(); - + Blockly.serialization.workspaces.load(blocks, workspace); - - + // Create and set up generator context like the editor does const generatorContext = new GeneratorContext(); generatorContext.setModule(module); - + // Initialize the generator if not already done if (!extendedPythonGenerator.isInitialized) { extendedPythonGenerator.init(workspace); } - + // Generate Python code using the same method as the editor const pythonCode = extendedPythonGenerator.mrcWorkspaceToCode(workspace, generatorContext); - + // Clean up the workspace workspace.dispose(); - + return { - moduleName: module.moduleName, + moduleName: moduleName, pythonCode, success: true, }; } catch (error) { - console.error('Error generating Python for module', module.moduleName, ':', error); + console.error('Error generating Python for module ', moduleName, ':', error); return { - moduleName: module.moduleName, + moduleName: moduleName, pythonCode: '', success: false, error: error instanceof Error ? error.message : String(error), @@ -101,17 +107,17 @@ export async function generatePythonForModule(module: Module, storage: Storage): * @param storage The storage interface to fetch module content * @returns Result containing Python code for all modules */ -export async function generatePythonForProject(project: Project, storage: Storage): Promise { +async function generatePythonForProject(project: Project, storage: Storage): Promise { const moduleResults: ModulePythonResult[] = []; let errorCount = 0; - + // Process the robot module const robotResult = await generatePythonForModule(project.robot, storage); moduleResults.push(robotResult); if (!robotResult.success) { errorCount++; } - + // Process all mechanism modules for (const mechanism of project.mechanisms) { const result = await generatePythonForModule(mechanism, storage); @@ -120,7 +126,7 @@ export async function generatePythonForProject(project: Project, storage: Storag errorCount++; } } - + // Process all opmode modules for (const opMode of project.opModes) { const result = await generatePythonForModule(opMode, storage); @@ -129,7 +135,7 @@ export async function generatePythonForProject(project: Project, storage: Storag errorCount++; } } - + return { projectName: project.projectName, modules: moduleResults, @@ -144,17 +150,17 @@ export async function generatePythonForProject(project: Project, storage: Storag * @param storage The storage interface to fetch module content * @returns Map of filename to Python code content */ -export async function generatePythonFilesMap(project: Project, storage: Storage): Promise> { +async function generatePythonFilesMap(project: Project, storage: Storage): Promise> { const filesMap = new Map(); const result = await generatePythonForProject(project, storage); - + for (const moduleResult of result.modules) { if (moduleResult.success) { const filename = `${moduleResult.moduleName}.py`; filesMap.set(filename, moduleResult.pythonCode); } } - + return filesMap; } @@ -167,19 +173,19 @@ export async function generatePythonFilesMap(project: Project, storage: Storage) export async function producePythonProjectBlob(project: Project, storage: Storage): Promise { // Initialize the generator first initializeHeadlessBlockly(); - + const pythonFilesMap = await generatePythonFilesMap(project, storage); - - + + if (pythonFilesMap.size === 0) { throw new Error('No Python files were generated successfully'); } - + const zip = new JSZip(); for (const [filename, pythonCode] of pythonFilesMap) { zip.file(filename, pythonCode); } - + const content = await zip.generateAsync({ type: "blob" }); const blobUrl = URL.createObjectURL(content); return blobUrl; @@ -189,7 +195,7 @@ export async function producePythonProjectBlob(project: Project, storage: Storag * Initialize Blockly for headless operation * This should be called once before using the generation functions */ -export function initializeHeadlessBlockly(): void { +function initializeHeadlessBlockly(): void { // Initialize Blockly for headless operation // This ensures all necessary generators and blocks are loaded extendedPythonGenerator.init(new Blockly.Workspace());