diff --git a/README.md b/README.md index 0db71dcf..9ccb42bf 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,3 @@ WARNING! This is not ready for use and is under heavy development of basic featu 1. Mechanisms aren't limited to init 2. Mechanisms aren't limited to only Robot or Mechanism class 3. No way to specify whether an opmode is auto or teleop -4. Since we changed the "Workspace" terminology to "Project", existing Workspaces are no longer supported. They can be deleted via the browser's Developer Tools - Application tab. diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index b47a6209..fb911e00 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -1,14 +1,14 @@ { - "mechanism_delete": "Delete Project", - "mechanism_rename": "Rename Project", - "mechanism_copy": "Copy Project", - "opmode_delete": "Delete Project", - "opmode_rename": "Rename Project", - "opmode_copy": "Copy Project", + "mechanism_delete": "Delete Mechanism", + "mechanism_rename": "Rename Mechanism", + "mechanism_copy": "Copy Mechanism", + "opmode_delete": "Delete OpMode", + "opmode_rename": "Rename OpMode", + "opmode_copy": "Copy OpMode", "project_delete": "Delete Project", "project_rename": "Rename Project", "project_copy": "Copy Project", - "fail_list_modules": "Failed to load the list of modules.", + "fail_list_projects": "Failed to load the list of projects.", "mechanism": "Mechanism", "opmode": "OpMode", "class_rule_description": "No spaces are allowed in the name. Each word in the name should start with a capital letter.", @@ -21,4 +21,4 @@ "search": "Search..." } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 8ab31e25..9ce3bf2c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -134,8 +134,8 @@ const App: React.FC = (): React.JSX.Element => { try { const value = await storage.fetchEntry(SHOWN_TOOLBOX_CATEGORIES_KEY, DEFAULT_TOOLBOX_CATEGORIES_JSON); - const shownCategories: string[] = JSON.parse(value); - setShownPythonToolboxCategories(new Set(shownCategories)); + const shownCategories: Set = new Set(JSON.parse(value)); + setShownPythonToolboxCategories(shownCategories); } catch (e) { console.error(TOOLBOX_FETCH_ERROR_MESSAGE); console.error(e); @@ -231,7 +231,7 @@ const App: React.FC = (): React.JSX.Element => { const createTabItemsFromProject = (projectData: commonStorage.Project): Tabs.TabItem[] => { const tabs: Tabs.TabItem[] = [ { - key: projectData.modulePath, + key: projectData.robot.modulePath, title: 'Robot', type: TabType.ROBOT, }, @@ -278,6 +278,10 @@ const App: React.FC = (): React.JSX.Element => { initializeBlocks(); }, []); + React.useEffect(() => { + initializeShownPythonToolboxCategories(); + }, [storage]); + // Update generator context and load module blocks when current module changes React.useEffect(() => { if (generatorContext.current) { @@ -334,7 +338,7 @@ const App: React.FC = (): React.JSX.Element => { if (project) { const tabs = createTabItemsFromProject(project); setTabItems(tabs); - setActiveTab(project.modulePath); + setActiveTab(project.robot.modulePath); } }, [project]); diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 66dfa2fb..c82f6cc0 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -28,7 +28,6 @@ import { getClassData, getAllowedTypesForSetCheck, getOutputCheck } from './util import { FunctionData, findSuperFunctionData } from './utils/python_json_types'; import * as value from './utils/value'; import * as variable from './utils/variable'; -import { Editor } from '../editor/editor'; import { ExtendedPythonGenerator } from '../editor/extended_python_generator'; import { createFieldDropdown } from '../fields/FieldDropdown'; import { createFieldNonEditableText } from '../fields/FieldNonEditableText'; @@ -224,9 +223,15 @@ export function addInstanceComponentBlocks( contents: toolboxItems.ContentsType[]) { const classData = getClassData(componentType); + if (!classData) { + throw new Error('Could not find classData for ' + componentType); + } const functions = classData.instanceMethods; const componentClassData = getClassData('component.Component'); + if (!componentClassData) { + throw new Error('Could not find classData for component.Component'); + } const componentFunctions = componentClassData.instanceMethods; for (const functionData of functions) { @@ -331,8 +336,7 @@ type CallPythonFunctionExtraState = { */ actualFunctionName?: string, /** - * True if this blocks refers to an exported function (for example, from a - * user's Project). + * True if this blocks refers to an exported function (for example, from the Robot). */ exportedFunction?: boolean, /** @@ -540,7 +544,7 @@ const CALL_PYTHON_FUNCTION = { case FunctionKind.INSTANCE_COMPONENT: { // TODO: We need the list of component names for this.mrcComponentClassName so we can // create a dropdown that has the appropriate component names. - const componentNames = []; + const componentNames: string[] = []; const componentName = this.getComponentName(); if (!componentNames.includes(componentName)) { componentNames.push(componentName); @@ -679,7 +683,7 @@ export const pythonFromBlock = function( : block.getFieldValue(FIELD_FUNCTION_NAME); // Generate the correct code depending on the module type. switch (generator.getModuleType()) { - case commonStorage.MODULE_TYPE_PROJECT: + case commonStorage.MODULE_TYPE_ROBOT: case commonStorage.MODULE_TYPE_MECHANISM: code = 'self.'; break; diff --git a/src/blocks/mrc_get_python_variable.ts b/src/blocks/mrc_get_python_variable.ts index 5539b7c7..a7196c32 100644 --- a/src/blocks/mrc_get_python_variable.ts +++ b/src/blocks/mrc_get_python_variable.ts @@ -228,8 +228,7 @@ type GetPythonVariableExtraState = { */ actualVariableName?: string, /** - * True if this blocks refers to an exported variable (for example, from a - * user's Project). + * True if this blocks refers to an exported variable (for example, from the Robot). */ exportedVariable?: boolean, }; diff --git a/src/blocks/mrc_set_python_variable.ts b/src/blocks/mrc_set_python_variable.ts index d43d9c7f..218decf2 100644 --- a/src/blocks/mrc_set_python_variable.ts +++ b/src/blocks/mrc_set_python_variable.ts @@ -178,8 +178,7 @@ type SetPythonVariableExtraState = { */ actualVariableName?: string, /** - * True if this blocks refers to an exported variable (for example, from a - * user's Project). + * True if this blocks refers to an exported variable (for example, from the Robot). */ exportedVariable?: boolean, }; diff --git a/src/blocks/utils/external_samples_data.ts b/src/blocks/utils/external_samples_data.ts index 3f796aa9..66f7cfd7 100644 --- a/src/blocks/utils/external_samples_data.ts +++ b/src/blocks/utils/external_samples_data.ts @@ -19,7 +19,7 @@ * @author lizlooney@google.com (Liz Looney) */ -import { PythonData, ClassData } from './python_json_types'; +import { PythonData } from './python_json_types'; import generatedExternalSamplesData from './generated/external_samples_data.json'; export const externalSamplesData = generatedExternalSamplesData as PythonData; diff --git a/src/blocks/utils/python.ts b/src/blocks/utils/python.ts index a8afce86..f0f895fb 100644 --- a/src/blocks/utils/python.ts +++ b/src/blocks/utils/python.ts @@ -19,7 +19,7 @@ * @author lizlooney@google.com (Liz Looney) */ -import { PythonData, organizeVarDataByType, VariableGettersAndSetters } from './python_json_types'; +import { ClassData, PythonData, organizeVarDataByType, VariableGettersAndSetters } from './python_json_types'; import { robotPyData } from './robotpy_data'; import { externalSamplesData } from './external_samples_data'; diff --git a/src/blocks/utils/python_json_types.ts b/src/blocks/utils/python_json_types.ts index 514c767b..407893d6 100644 --- a/src/blocks/utils/python_json_types.ts +++ b/src/blocks/utils/python_json_types.ts @@ -123,7 +123,7 @@ function isSuperFunction(f1: FunctionData, f2: FunctionData): boolean { return true; } -export function findSuperFunctionData(functionData: FunctionData, superClassFunctions: FunctionData): FunctionData | null { +export function findSuperFunctionData(functionData: FunctionData, superClassFunctions: FunctionData[]): FunctionData | null { for (const superClassFunctionData of superClassFunctions) { if (isSuperFunction(superClassFunctionData, functionData)) { return superClassFunctionData; diff --git a/src/editor/editor.ts b/src/editor/editor.ts index 86d6f5a5..204bd517 100644 --- a/src/editor/editor.ts +++ b/src/editor/editor.ts @@ -45,9 +45,9 @@ export class Editor { private eventsCategory: EventsCategory; private currentModule: commonStorage.Module | null = null; private modulePath: string = ''; - private projectPath: string = ''; + private robotPath: string = ''; private moduleContent: string = ''; - private projectContent: string = ''; + private robotContent: string = ''; private bindedOnChange: any = null; private toolbox: Blockly.utils.toolbox.ToolboxDefinition = EMPTY_TOOLBOX; @@ -73,11 +73,11 @@ export class Editor { // TODO(lizlooney): As blocks are loaded, determine whether any blocks // are accessing variable or calling functions thar are defined in another - // blocks file (like a Project) and check whether the variable or function + // blocks file (like the Robot) and check whether the variable or function // definition has changed. This might happen if the user defines a variable - // or function in the Project, uses the variable or function in the + // or function in the Robot, uses the variable or function in the // OpMode, and then removes or changes the variable or function in the - // Project. + // Robot. // TODO(lizlooney): We will need a way to identify which variable or // function, other than by the variable name or function name, because the @@ -88,7 +88,7 @@ export class Editor { // TODO(lizlooney): Look at blocks with type 'mrc_get_python_variable' or // 'mrc_set_python_variable', and where block.mrcExportedVariable === true. // Look at block.mrcImportModule and get the exported blocks for that module. - // (It should be the project and we already have the project content.) + // It could be from the Robot (or a Mechanism?) and we already have the Robot content. // Check whether block.mrcActualVariableName matches any exportedBlock's // extraState.actualVariableName. If there is no match, put a warning on the // block. @@ -96,7 +96,7 @@ export class Editor { // TODO(lizlooney): Look at blocks with type 'mrc_call_python_function' and // where block.mrcExportedFunction === true. // Look at block.mrcImportModule and get the exported blocks for that module. - // (It should be the project and we already have the project content.) + // It could be from the Robot (or a Mechanism?) and we already have the Robot content. // Check whether block.mrcActualFunctionName matches any exportedBlock's // extraState.actualFunctionName. If there is no match, put a warning on the block. // If there is a match, check whether @@ -124,21 +124,23 @@ export class Editor { if (currentModule) { this.modulePath = currentModule.modulePath; - this.projectPath = commonStorage.makeProjectPath(currentModule.projectName); + this.robotPath = commonStorage.makeRobotPath(currentModule.projectName); } else { this.modulePath = ''; - this.projectPath = ''; + this.robotPath = ''; } this.moduleContent = ''; - this.projectContent = ''; + this.robotContent = ''; this.clearBlocklyWorkspace(); if (currentModule) { + // Fetch the content for the current module and the robot. + // TODO: Also fetch the content for the mechanisms? const promises: {[key: string]: Promise} = {}; // key is module path, value is promise of module content. promises[this.modulePath] = this.storage.fetchModuleContent(this.modulePath); - if (this.projectPath !== this.modulePath) { - // Also fetch the project module content. It contains exported blocks that can be used. - promises[this.projectPath] = this.storage.fetchModuleContent(this.projectPath) + if (this.robotPath !== this.modulePath) { + // Also fetch the robot module content. It contains components, etc, that can be used. + promises[this.robotPath] = this.storage.fetchModuleContent(this.robotPath) } const moduleContents: {[key: string]: string} = {}; // key is module path, value is module content @@ -148,10 +150,10 @@ export class Editor { }) ); this.moduleContent = moduleContents[this.modulePath]; - if (this.projectPath === this.modulePath) { - this.projectContent = this.moduleContent + if (this.robotPath === this.modulePath) { + this.robotContent = this.moduleContent } else { - this.projectContent = moduleContents[this.projectPath]; + this.robotContent = moduleContents[this.robotPath]; } this.loadBlocksIntoBlocklyWorkspace(); } @@ -187,8 +189,8 @@ export class Editor { public updateToolbox(shownPythonToolboxCategories: Set): void { if (this.currentModule) { - if (!this.projectContent) { - // The Project content hasn't been fetched yet. Try again in a bit. + if (!this.robotContent) { + // The Robot content hasn't been fetched yet. Try again in a bit. setTimeout(() => { this.updateToolbox(shownPythonToolboxCategories) }, 50); @@ -229,7 +231,8 @@ export class Editor { private getComponents(): commonStorage.Component[] { const components: commonStorage.Component[] = []; - if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_PROJECT) { + if (this.currentModule?.moduleType === commonStorage.MODULE_TYPE_ROBOT || + this.currentModule?.moduleType === commonStorage.MODULE_TYPE_MECHANISM) { // TODO(lizlooney): Fill the components array. } return components; diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index 1812a575..1a65a2c3 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -122,8 +122,8 @@ export class ExtendedPythonGenerator extends PythonGenerator { } getModuleType(): string | null { - if (this.context && this.context.module) { - return this.context.module.moduleType; + if (this.context) { + return this.context.getModuleType(); } return null; } diff --git a/src/editor/generator_context.ts b/src/editor/generator_context.ts index e6007ade..4dc1d5a5 100644 --- a/src/editor/generator_context.ts +++ b/src/editor/generator_context.ts @@ -41,6 +41,13 @@ export class GeneratorContext { this.clear(); } + getModuleType(): string | null { + if (this.module) { + return this.module.moduleType; + } + return null; + } + clear(): void { this.clearExportedBlocks(); this.hasHardware= false; @@ -58,10 +65,6 @@ export class GeneratorContext { if (!this.module) { throw new Error('getClassName: this.module is null.'); } - if (this.module.moduleType === commonStorage.MODULE_TYPE_PROJECT) { - return 'Robot'; - } - return this.module.className; } @@ -69,7 +72,7 @@ export class GeneratorContext { if (!this.module) { throw new Error('getClassParent: this.module is null.'); } - if (this.module.moduleType === commonStorage.MODULE_TYPE_PROJECT) { + if (this.module.moduleType === commonStorage.MODULE_TYPE_ROBOT) { return 'RobotBase'; } if (this.module.moduleType === commonStorage.MODULE_TYPE_OPMODE) { diff --git a/src/reactComponents/AddTabDialog.tsx b/src/reactComponents/AddTabDialog.tsx index 519b8f9e..2a76a064 100644 --- a/src/reactComponents/AddTabDialog.tsx +++ b/src/reactComponents/AddTabDialog.tsx @@ -106,14 +106,14 @@ export default function AddTabDialog(props: AddTabDialogProps) { /** Handles adding a new item or selecting an existing one. */ const handleAddNewItem = async (): Promise => { - const trimmedName = newItemName.trim(); - if (!trimmedName) { + const newClassName = newItemName.trim(); + if (!newClassName) { return; } // Check if there's an exact match in available items const matchingItem = availableItems.find((item) => - item.title.toLowerCase() === trimmedName.toLowerCase() + item.title.toLowerCase() === newClassName.toLowerCase() ); if (matchingItem) { @@ -132,9 +132,9 @@ export default function AddTabDialog(props: AddTabDialogProps) { commonStorage.MODULE_TYPE_OPMODE; await commonStorage.addModuleToProject( - props.storage, props.project, storageType, trimmedName); + props.storage, props.project, storageType, newClassName); - const newModule = commonStorage.getClassInProject(props.project, trimmedName); + const newModule = commonStorage.findModuleByClassName(props.project, newClassName); if (newModule) { const module: Module = { path: newModule.modulePath, diff --git a/src/reactComponents/FileManageModal.tsx b/src/reactComponents/FileManageModal.tsx index 14a2f714..cda4041d 100644 --- a/src/reactComponents/FileManageModal.tsx +++ b/src/reactComponents/FileManageModal.tsx @@ -100,22 +100,22 @@ export default function FileManageModal(props: FileManageModalProps) { }, [props.project, props.moduleType]); /** Handles renaming a module. */ - const handleRename = async (origModule: Module, newName: string): Promise => { + const handleRename = async (origModule: Module, newClassName: string): Promise => { if (!props.storage || !props.project) { return; } try { - const newPath = await commonStorage.renameModuleInProject( + const newModulePath = await commonStorage.renameModuleInProject( props.storage, props.project, - newName, + newClassName, origModule.path ); const newModules = modules.map((module) => { if (module.path === origModule.path) { - return {...module, title: newName, path: newPath}; + return {...module, title: newClassName, path: newModulePath}; } return module; }); @@ -131,16 +131,16 @@ export default function FileManageModal(props: FileManageModalProps) { }; /** Handles copying a module. */ - const handleCopy = async (origModule: Module, newName: string): Promise => { + const handleCopy = async (origModule: Module, newClassName: string): Promise => { if (!props.storage || !props.project) { return; } try { - const newPath = await commonStorage.copyModuleInProject( + const newModulePath = await commonStorage.copyModuleInProject( props.storage, props.project, - newName, + newClassName, origModule.path ); @@ -153,8 +153,8 @@ export default function FileManageModal(props: FileManageModalProps) { const newModules = [...modules]; newModules.push({ - path: newPath, - title: newName, + path: newModulePath, + title: newClassName, type: originalModule.type, }); @@ -170,8 +170,8 @@ export default function FileManageModal(props: FileManageModalProps) { /** Handles adding a new module. */ const handleAddNewItem = async (): Promise => { - const trimmedName = newItemName.trim(); - if (!trimmedName || !props.storage || !props.project) { + const newClassName = newItemName.trim(); + if (!newClassName || !props.storage || !props.project) { return; } @@ -183,10 +183,10 @@ export default function FileManageModal(props: FileManageModalProps) { props.storage, props.project, storageType, - trimmedName + newClassName ); - const newModule = commonStorage.getClassInProject(props.project, trimmedName); + const newModule = commonStorage.findModuleByClassName(props.project, newClassName); if (newModule) { const module: Module = { path: newModule.modulePath, diff --git a/src/reactComponents/Header.tsx b/src/reactComponents/Header.tsx index 01c706fb..3a34f17d 100644 --- a/src/reactComponents/Header.tsx +++ b/src/reactComponents/Header.tsx @@ -76,7 +76,7 @@ export default function Header(props: HeaderProps): React.JSX.Element { /** Gets the project name or fallback text. */ const getProjectName = (): string => { - return props.project?.className || 'No Project Selected'; + return props.project?.userVisibleName || 'No Project Selected'; }; return ( diff --git a/src/reactComponents/Menu.tsx b/src/reactComponents/Menu.tsx index e39e49c1..e14f6a21 100644 --- a/src/reactComponents/Menu.tsx +++ b/src/reactComponents/Menu.tsx @@ -130,7 +130,7 @@ function getMenuItems(t: (key: string) => string, project: commonStorage.Project getItem(t('Manage') + '...', 'manageProjects'), ]), getItem(t('Explorer'), 'explorer', , [ - getItem(t('Robot'), project.modulePath, ), + getItem(t('Robot'), project.robot.modulePath, ), getItem(t('Mechanisms'), 'mechanisms', , mechanisms), getItem(t('OpModes'), 'opmodes', , opmodes), ]), @@ -152,7 +152,7 @@ function getMenuItems(t: (key: string) => string, project: commonStorage.Project export function Component(props: MenuProps): React.JSX.Element { const {t} = I18Next.useTranslation(); - const [modules, setModules] = React.useState([]); + const [projects, setProjects] = React.useState([]); const [menuItems, setMenuItems] = React.useState([]); const [fileModalOpen, setFileModalOpen] = React.useState(false); const [projectModalOpen, setProjectModalOpen] = React.useState(false); @@ -168,28 +168,28 @@ export function Component(props: MenuProps): React.JSX.Element { props.setTheme(newTheme); }; - /** Fetches the list of modules from storage. */ - const fetchListOfModules = async (): Promise => { + /** Fetches the list of projects from storage. */ + const fetchListOfProjects = async (): Promise => { return new Promise(async (resolve, reject) => { if (!props.storage) { reject(new Error('Storage not available')); return; } try { - const array = await props.storage.listModules(); - setModules(array); + const array = await props.storage.listProjects(); + setProjects(array); resolve(array); } catch (e) { - console.error('Failed to load the list of modules:', e); - props.setAlertErrorMessage(t('fail_list_modules')); - reject(new Error(t('fail_list_modules'))); + console.error('Failed to load the list of projects:', e); + props.setAlertErrorMessage(t('fail_list_projects')); + reject(new Error(t('fail_list_projects'))); } }); }; - /** Initializes the modules and handles empty project state. */ - const initializeModules = async (): Promise => { - const array = await fetchListOfModules(); + /** Initializes the projects and handles empty project state. */ + const initializeProjects = async (): Promise => { + const array = await fetchListOfProjects(); if (array.length === 0) { setNoProjects(true); setProjectModalOpen(true); @@ -205,14 +205,14 @@ export function Component(props: MenuProps): React.JSX.Element { MOST_RECENT_PROJECT_KEY, '' ); - modules.forEach((module) => { - if (module.projectName === mostRecentProject) { - props.setProject(module); + projects.forEach((project) => { + if (project.projectName === mostRecentProject) { + props.setProject(project); found = true; } }); - if (!found && modules.length > 0) { - props.setProject(modules[0]); + if (!found && projects.length > 0) { + props.setProject(projects[0]); } } }; @@ -230,7 +230,7 @@ export function Component(props: MenuProps): React.JSX.Element { /** Handles menu item clicks. */ const handleClick: Antd.MenuProps['onClick'] = ({key}): void => { const newModule = props.project ? - commonStorage.findModuleInProject(props.project, key) : + commonStorage.findModuleByModulePath(props.project, key) : null; if (newModule) { @@ -281,18 +281,18 @@ export function Component(props: MenuProps): React.JSX.Element { setProjectModalOpen(false); }; - // Initialize modules when storage is available + // Initialize projects when storage is available React.useEffect(() => { if (!props.storage) { return; } - initializeModules(); + initializeProjects(); }, [props.storage]); - // Fetch most recent project when modules change + // Fetch most recent project when projects change React.useEffect(() => { fetchMostRecentProject(); - }, [modules]); + }, [projects]); // Update menu items and save project when project changes React.useEffect(() => { @@ -320,7 +320,6 @@ export function Component(props: MenuProps): React.JSX.Element { isOpen={projectModalOpen} onCancel={handleProjectModalClose} storage={props.storage} - moduleType={moduleType} setProject={props.setProject} setAlertErrorMessage={props.setAlertErrorMessage} /> diff --git a/src/reactComponents/ModuleNameComponent.tsx b/src/reactComponents/ModuleNameComponent.tsx index 74ba0307..6beb5c9f 100644 --- a/src/reactComponents/ModuleNameComponent.tsx +++ b/src/reactComponents/ModuleNameComponent.tsx @@ -56,8 +56,8 @@ export default function ModuleNameComponent(props: ModuleNameComponentProps): Re /** Handles adding a new item with validation. */ const handleAddNewItem = (): void => { - const trimmedName = props.newItemName.trim(); - if (!trimmedName) { + const newClassName = props.newItemName.trim(); + if (!newClassName) { return; } @@ -65,7 +65,7 @@ export default function ModuleNameComponent(props: ModuleNameComponentProps): Re return; } - const {ok, error} = commonStorage.isClassNameOk(props.project, trimmedName); + const {ok, error} = commonStorage.isClassNameOk(props.project, newClassName); if (ok) { clearError(); props.onAddNewItem(); diff --git a/src/reactComponents/ProjectManageModal.tsx b/src/reactComponents/ProjectManageModal.tsx index 047258af..cb4a3b90 100644 --- a/src/reactComponents/ProjectManageModal.tsx +++ b/src/reactComponents/ProjectManageModal.tsx @@ -18,7 +18,6 @@ /** * @author alan@porpoiseful.com (Alan Smith) */ -import {TabType} from '../types/TabType'; import * as Antd from 'antd'; import * as I18Next from 'react-i18next'; import * as React from 'react'; @@ -34,7 +33,6 @@ interface ProjectManageModalProps { setProject: (project: commonStorage.Project | null) => void; setAlertErrorMessage: (message: string) => void; storage: commonStorage.Storage | null; - moduleType: TabType; } /** Default page size for table pagination. */ @@ -64,20 +62,20 @@ const CONTAINER_PADDING = '12px'; */ export default function ProjectManageModal(props: ProjectManageModalProps): React.JSX.Element { const {t} = I18Next.useTranslation(); - const [modules, setModules] = React.useState([]); + const [allProjects, setAllProjects] = React.useState([]); const [newItemName, setNewItemName] = React.useState(''); const [currentRecord, setCurrentRecord] = React.useState(null); const [renameModalOpen, setRenameModalOpen] = React.useState(false); const [name, setName] = React.useState(''); const [copyModalOpen, setCopyModalOpen] = React.useState(false); - /** Loads modules from storage and sorts them alphabetically. */ - const loadModules = async (storage: commonStorage.Storage): Promise => { - const projects = await storage.listModules(); + /** Loads projects from storage and sorts them alphabetically. */ + const loadProjects = async (storage: commonStorage.Storage): Promise => { + const projects = await storage.listProjects(); - // Sort modules alphabetically by class name - projects.sort((a, b) => a.className.localeCompare(b.className)); - setModules(projects); + // Sort projects alphabetically by name + projects.sort((a, b) => a.userVisibleName.localeCompare(b.userVisibleName)); + setAllProjects(projects); if (projects.length > 0 && props.noProjects) { props.setProject(projects[0]); // Set the first project as the current project @@ -86,44 +84,42 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac }; /** Handles renaming a project. */ - const handleRename = async (origModule: commonStorage.Project, newName: string): Promise => { + const handleRename = async (origProject: commonStorage.Project, newUserVisibleName: string): Promise => { if (!props.storage) { return; } try { - await props.storage.renameModule( - commonStorage.MODULE_TYPE_PROJECT, - origModule.className, - origModule.className, - newName + await commonStorage.renameProject( + props.storage, + origProject, + newUserVisibleName ); - await loadModules(props.storage); + await loadProjects(props.storage); } catch (error) { - console.error('Error renaming module:', error); - props.setAlertErrorMessage('Failed to rename module'); + console.error('Error renaming project:', error); + props.setAlertErrorMessage('Failed to rename project'); } setRenameModalOpen(false); }; /** Handles copying a project. */ - const handleCopy = async (origModule: commonStorage.Project, newName: string): Promise => { + const handleCopy = async (origProject: commonStorage.Project, newUserVisibleName: string): Promise => { if (!props.storage) { return; } try { - await props.storage.copyModule( - commonStorage.MODULE_TYPE_PROJECT, - origModule.className, - origModule.className, - newName + await commonStorage.copyProject( + props.storage, + origProject, + newUserVisibleName ); - await loadModules(props.storage); + await loadProjects(props.storage); } catch (error) { - console.error('Error copying module:', error); - props.setAlertErrorMessage('Failed to copy module'); + console.error('Error copying project:', error); + props.setAlertErrorMessage('Failed to copy project'); } setCopyModalOpen(false); @@ -131,20 +127,15 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac /** Handles adding a new project. */ const handleAddNewItem = async (): Promise => { - const trimmedName = newItemName.trim(); - if (!trimmedName || !props.storage) { + const newUserVisibleName = newItemName.trim(); + if (!newUserVisibleName || !props.storage) { return; } - const newProjectName = commonStorage.classNameToModuleName(trimmedName); - const newProjectPath = commonStorage.makeProjectPath(newProjectName); - const projectContent = commonStorage.newProjectContent(newProjectName); - try { - await props.storage.createModule( - commonStorage.MODULE_TYPE_PROJECT, - newProjectPath, - projectContent + await commonStorage.createProject( + props.storage, + newUserVisibleName ); } catch (e) { console.error('Failed to create a new project:', e); @@ -152,7 +143,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac } setNewItemName(''); - await loadModules(props.storage); + await loadProjects(props.storage); }; /** Handles project deletion with proper cleanup. */ @@ -161,13 +152,13 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac return; } - const newModules = modules.filter((m) => m.modulePath !== record.modulePath); - setModules(newModules); + const newProjects = allProjects.filter((m) => m.projectName !== record.projectName); + setAllProjects(newProjects); // Find another project to set as current let foundAnotherProject = false; - for (const project of modules) { - if (project.modulePath !== record.modulePath) { + for (const project of allProjects) { + if (project.projectName !== record.projectName) { props.setProject(project); foundAnotherProject = true; break; @@ -179,7 +170,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac } try { - await props.storage.deleteModule(commonStorage.MODULE_TYPE_PROJECT, record.modulePath); + await commonStorage.deleteProject(props.storage, record); } catch (e) { console.error('Failed to delete the project:', e); props.setAlertErrorMessage('Failed to delete the project.'); @@ -195,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.className); + setName(record.userVisibleName); setRenameModalOpen(true); }; /** Opens the copy modal for a specific project. */ const openCopyModal = (record: commonStorage.Project): void => { setCurrentRecord(record); - setName(record.className + COPY_SUFFIX); + setName(record.userVisibleName + COPY_SUFFIX); setCopyModalOpen(true); }; /** Gets the rename modal title. */ const getRenameModalTitle = (): string => { - return `Rename Project: ${currentRecord ? currentRecord.className : ''}`; + return `Rename Project: ${currentRecord ? currentRecord.userVisibleName : ''}`; }; /** Gets the copy modal title. */ const getCopyModalTitle = (): string => { - return `Copy Project: ${currentRecord ? currentRecord.className : ''}`; + return `Copy Project: ${currentRecord ? currentRecord.userVisibleName : ''}`; }; /** Creates the container style object. */ @@ -233,8 +224,8 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac const columns: Antd.TableProps['columns'] = [ { title: 'Name', - dataIndex: 'className', - key: 'className', + dataIndex: 'userVisibleName', + key: 'userVisibleName', ellipsis: { showTitle: false, }, @@ -274,10 +265,10 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac onClick={() => openCopyModal(record)} /> - {modules.length > 1 && ( + {allProjects.length > 1 && ( handleDeleteProject(record)} okText={t('Delete')} @@ -298,10 +289,10 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac }, ]; - // Load modules when storage becomes available or modal opens + // Load projects when storage becomes available or modal opens React.useEffect(() => { if (props.storage) { - loadModules(props.storage); + loadProjects(props.storage); } }, [props.storage, props.isOpen]); @@ -328,8 +319,8 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac handleRename(currentRecord, name); } }} - projects={modules} - setProjects={setModules} + projects={allProjects} + setProjects={setAllProjects} /> )} @@ -355,8 +346,8 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac handleCopy(currentRecord, name); } }} - projects={modules} - setProjects={setModules} + projects={allProjects} + setProjects={setAllProjects} /> )} @@ -386,17 +377,17 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac newItemName={newItemName} setNewItemName={setNewItemName} onAddNewItem={handleAddNewItem} - projects={modules} - setProjects={setModules} + projects={allProjects} + setProjects={setAllProjects} /> {!props.noProjects && ( columns={columns} - dataSource={modules} - rowKey="modulePath" + dataSource={allProjects} + rowKey="userVisibleName" size="small" - pagination={modules.length > DEFAULT_PAGE_SIZE ? { + pagination={allProjects.length > DEFAULT_PAGE_SIZE ? { pageSize: DEFAULT_PAGE_SIZE, showSizeChanger: false, showQuickJumper: false, diff --git a/src/reactComponents/ProjectNameComponent.tsx b/src/reactComponents/ProjectNameComponent.tsx index 3e8c0b82..3856a01a 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 trimmedName = props.newItemName.trim(); - if (!trimmedName || !props.projects) { + const newUserVisibleName = props.newItemName.trim(); + if (!newUserVisibleName || !props.projects) { return; } - if (!commonStorage.isValidClassName(trimmedName)) { - showError(trimmedName + INVALID_NAME_MESSAGE_SUFFIX); + if (!commonStorage.isValidClassName(newUserVisibleName)) { + showError(newUserVisibleName + INVALID_NAME_MESSAGE_SUFFIX); return; } - if (props.projects.some((project) => project.className === trimmedName)) { - showError(DUPLICATE_NAME_MESSAGE_PREFIX + trimmedName + DUPLICATE_NAME_MESSAGE_SUFFIX); + if (props.projects.some((project) => project.userVisibleName === newUserVisibleName)) { + showError(DUPLICATE_NAME_MESSAGE_PREFIX + newUserVisibleName + DUPLICATE_NAME_MESSAGE_SUFFIX); return; } diff --git a/src/reactComponents/Tabs.tsx b/src/reactComponents/Tabs.tsx index 253e1147..368c6082 100644 --- a/src/reactComponents/Tabs.tsx +++ b/src/reactComponents/Tabs.tsx @@ -84,7 +84,8 @@ export function Component(props: TabsProps): React.JSX.Element { /** Handles tab change and updates current module. */ const handleTabChange = (key: string): void => { if (props.project) { - props.setCurrentModule(commonStorage.findModuleInProject(props.project, key)); + const modulePath = key; + props.setCurrentModule(commonStorage.findModuleByModulePath(props.project, modulePath)); setActiveKey(key); } }; @@ -101,7 +102,8 @@ export function Component(props: TabsProps): React.JSX.Element { return; } - const module = commonStorage.findModuleInProject(props.project, key); + const modulePath = key; + const module = commonStorage.findModuleByModulePath(props.project, modulePath); if (!module) { return; } @@ -156,28 +158,30 @@ export function Component(props: TabsProps): React.JSX.Element { }; /** Handles renaming a module tab. */ - const handleRename = async (key: string, newName: string): Promise => { + const handleRename = async (key: string, newClassName: string): Promise => { if (!props.storage || !props.project) { return; } + const oldModulePath = key; + try { - const newPath = await commonStorage.renameModuleInProject( + const newModulePath = await commonStorage.renameModuleInProject( props.storage, props.project, - newName, - key + newClassName, + oldModulePath, ); const newTabs = props.tabList.map((tab) => { if (tab.key === key) { - return { ...tab, title: newName, key: newPath }; + return { ...tab, title: newClassName, key: newModulePath }; } return tab; }); props.setTabList(newTabs); - setActiveKey(newPath); + setActiveKey(newModulePath); triggerProjectUpdate(); } catch (error) { console.error('Error renaming module:', error); @@ -188,17 +192,19 @@ export function Component(props: TabsProps): React.JSX.Element { }; /** Handles copying a module tab. */ - const handleCopy = async (key: string, newName: string): Promise => { + const handleCopy = async (key: string, newClassName: string): Promise => { if (!props.storage || !props.project) { return; } + const oldModulePath = key; + try { - const newPath = await commonStorage.copyModuleInProject( + const newModulePath = await commonStorage.copyModuleInProject( props.storage, props.project, - newName, - key + newClassName, + oldModulePath, ); const newTabs = [...props.tabList]; @@ -210,9 +216,9 @@ export function Component(props: TabsProps): React.JSX.Element { return; } - newTabs.push({ key: newPath, title: newName, type: originalTab.type }); + newTabs.push({ key: newModulePath, title: newClassName, type: originalTab.type }); props.setTabList(newTabs); - setActiveKey(newPath); + setActiveKey(newModulePath); triggerProjectUpdate(); } catch (error) { console.error('Error copying module:', error); diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index 9540fb6f..ec18a595 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -118,7 +118,7 @@ class ClientSideStorage implements commonStorage.Storage { }); } - async listModules(): Promise { + async listProjects(): Promise { return new Promise((resolve, reject) => { const projects: {[key: string]: commonStorage.Project} = {}; // key is project name, value is Project // The mechanisms and opModes variables hold any Mechanisms and OpModes that @@ -138,7 +138,8 @@ class ClientSideStorage implements commonStorage.Storage { if (cursor) { const value = cursor.value; const path = value.path; - const moduleType = value.type; + // Before PR #143, robot modules were stored with the type 'project'. + const moduleType = (value.type == 'project') ? commonStorage.MODULE_TYPE_ROBOT : value.type; const moduleName = commonStorage.getModuleName(path); const module: commonStorage.Module = { modulePath: path, @@ -146,11 +147,16 @@ class ClientSideStorage implements commonStorage.Storage { projectName: commonStorage.getProjectName(path), moduleName: moduleName, dateModifiedMillis: value.dateModifiedMillis, - className: commonStorage.moduleNameToClassName(moduleName), + className: commonStorage.getClassNameForModule(moduleType, moduleName), } - if (moduleType === commonStorage.MODULE_TYPE_PROJECT) { - const project: commonStorage.Project = { + if (moduleType === commonStorage.MODULE_TYPE_ROBOT) { + const robot: commonStorage.Robot = { ...module, + }; + const project: commonStorage.Project = { + projectName: moduleName, + userVisibleName: commonStorage.snakeCaseToPascalCase(moduleName), + robot: robot, mechanisms: [], opModes: [], }; @@ -203,12 +209,12 @@ class ClientSideStorage implements commonStorage.Storage { cursor.continue(); } else { // The cursor is done. We have finished reading all the modules. - const modules: commonStorage.Project[] = []; + const projectsToReturn: commonStorage.Project[] = []; const sortedProjectNames = Object.keys(projects).sort(); sortedProjectNames.forEach((projectName) => { - modules.push(projects[projectName]); + projectsToReturn.push(projects[projectName]); }); - resolve(modules); + resolve(projectsToReturn); } }; }); @@ -234,6 +240,11 @@ class ClientSideStorage implements commonStorage.Storage { }); } + async createProject(projectName: string, robotContent: string): Promise { + const modulePath = commonStorage.makeRobotPath(projectName); + return this._saveModule(commonStorage.MODULE_TYPE_ROBOT, modulePath, robotContent); + } + async createModule(moduleType: string, modulePath: string, moduleContent: string): Promise { return this._saveModule(moduleType, modulePath, moduleContent); } @@ -323,11 +334,12 @@ class ClientSideStorage implements commonStorage.Storage { if (cursor) { const value = cursor.value; const path = value.path; - const moduleType = value.type; + // Before PR #143, robot modules were stored with the type 'project'. + const moduleType = (value.type == 'project') ? commonStorage.MODULE_TYPE_ROBOT : value.type; if (commonStorage.getProjectName(path) === oldProjectName) { let newPath; - if (moduleType === commonStorage.MODULE_TYPE_PROJECT) { - newPath = commonStorage.makeProjectPath(newProjectName); + if (moduleType === commonStorage.MODULE_TYPE_ROBOT) { + newPath = commonStorage.makeRobotPath(newProjectName); } else { const moduleName = commonStorage.getModuleName(path); newPath = commonStorage.makeModulePath(newProjectName, moduleName); @@ -375,28 +387,38 @@ class ClientSideStorage implements commonStorage.Storage { }); } + async renameProject(oldProjectName: string, newProjectName: string): Promise { + return this._renameOrCopyProject(oldProjectName, newProjectName, false); + } + + async copyProject(oldProjectName: string, newProjectName: string): Promise { + return this._renameOrCopyProject(oldProjectName, newProjectName, true); + } + async renameModule( moduleType: string, projectName: string, oldModuleName: string, newModuleName: 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( - moduleType, projectName, oldModuleName, newModuleName, false); + projectName, oldModuleName, newModuleName, false); } async copyModule( moduleType: string, projectName: string, oldModuleName: string, newModuleName: 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( - moduleType, projectName, oldModuleName, newModuleName, true); + projectName, oldModuleName, newModuleName, true); } private async _renameOrCopyModule( - moduleType: string, projectName: string, + projectName: string, oldModuleName: string, newModuleName: string, copy: boolean): Promise { - if (moduleType == commonStorage.MODULE_TYPE_PROJECT) { - return this._renameOrCopyProject(oldModuleName, newModuleName, copy); - } - return new Promise((resolve, reject) => { const transaction = this.db.transaction(['modules'], 'readwrite'); transaction.oncomplete = () => { @@ -446,7 +468,7 @@ class ClientSideStorage implements commonStorage.Storage { }); } - private async _deleteProject(projectName: string): Promise { + async deleteProject(projectName: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction(['modules'], 'readwrite'); transaction.oncomplete = () => { @@ -492,9 +514,8 @@ class ClientSideStorage implements commonStorage.Storage { } async deleteModule(moduleType: string, modulePath: string): Promise { - if (moduleType == commonStorage.MODULE_TYPE_PROJECT) { - const projectName = commonStorage.getProjectName(modulePath); - return this._deleteProject(projectName); + if (moduleType == commonStorage.MODULE_TYPE_ROBOT) { + throw new Error('Deleting the robot module is not allowed. Call deleteProject to delete the project.'); } return new Promise((resolve, reject) => { diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 76912caf..7efcb617 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -42,10 +42,14 @@ export type Module = { className: string, }; +export type Robot = Module; export type Mechanism = Module; export type OpMode = Module; -export type Project = Module & { +export type Project = { + projectName: string, // snake_case + userVisibleName: string, // PascalCase + robot: Robot, mechanisms: Mechanism[] opModes: OpMode[], }; @@ -56,10 +60,12 @@ export type Component = { } export const MODULE_TYPE_UNKNOWN = 'unknown'; -export const MODULE_TYPE_PROJECT = 'project'; +export const MODULE_TYPE_ROBOT = 'robot'; export const MODULE_TYPE_MECHANISM = 'mechanism'; export const MODULE_TYPE_OPMODE = 'opmode'; +export const ROBOT_CLASS_NAME = 'Robot'; + export const MODULE_NAME_PLACEHOLDER = '%module_name%'; const DELIMITER_PREFIX = 'BlocksContent'; @@ -78,27 +84,81 @@ export const UPLOAD_DOWNLOAD_FILE_EXTENSION = '.blocks'; export interface Storage { saveEntry(entryKey: string, entryValue: string): Promise; fetchEntry(entryKey: string, defaultValue: string): Promise; - listModules(): Promise; + listProjects(): Promise; fetchModuleContent(modulePath: string): Promise; + createProject(projectName: string, robotContent: string): Promise; createModule(moduleType: string, modulePath: string, moduleContent: string): Promise; saveModule(modulePath: string, moduleContent: 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; + deleteProject(projectName: string): Promise; deleteModule(moduleType: string, modulePath: string): Promise; downloadProject(projectName: string): Promise; uploadProject(projectName: string, blobUrl: string): Promise; } +/** + * Creates a new project. + * @param storage The storage interface to use for creating the project. + * @param proposedUserVisibleName The name for the new project. + * @returns A promise that resolves when the project has been created. + */ +export async function createProject( + storage: Storage, proposedUserVisibleName: string): Promise { + const newProjectName = pascalCaseToSnakeCase(proposedUserVisibleName); + const robotContent = newRobotContent(newProjectName); + await storage.createProject(newProjectName, robotContent); +} + +/** + * 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. + * @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); + await storage.renameProject(project.projectName, newProjectName); +} + +/** + * 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. + * @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); + await storage.copyProject(project.projectName, newProjectName); +} + +/** + * Deletes a project. + * @param storage The storage interface to use for deleting the project. + * @param project The project to delete. + * @returns A promise that resolves when the project has been deleted. + */ +export async function deleteProject( + storage: Storage, project: Project): Promise { + await storage.deleteProject(project.projectName); +} + /** * Adds a new module to the project. * @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 className The name of the class. + * @param newClassName The name of the class. */ export async function addModuleToProject( - storage: Storage, project: Project, moduleType: string, className: string): Promise { - let newModuleName = classNameToModuleName(className); + storage: Storage, project: Project, moduleType: string, newClassName: string): Promise { + let newModuleName = pascalCaseToSnakeCase(newClassName); let newModulePath = makeModulePath(project.projectName, newModuleName); if (moduleType === MODULE_TYPE_MECHANISM) { @@ -109,7 +169,7 @@ export async function addModuleToProject( moduleType: MODULE_TYPE_MECHANISM, projectName: project.projectName, moduleName: newModuleName, - className: className + className: newClassName } as Mechanism); } else if (moduleType === MODULE_TYPE_OPMODE) { const opModeContent = newOpModeContent(project.projectName, newModuleName); @@ -119,7 +179,7 @@ export async function addModuleToProject( moduleType: MODULE_TYPE_OPMODE, projectName: project.projectName, moduleName: newModuleName, - className: className + className: newClassName } as OpMode); } } @@ -131,8 +191,11 @@ export async function addModuleToProject( */ export async function removeModuleFromProject( storage: Storage, project: Project, modulePath: string): Promise { - const module = findModuleInProject(project, modulePath); + const module = findModuleByModulePath(project, modulePath); if (module) { + if (module.moduleType == MODULE_TYPE_ROBOT) { + throw new Error('Removing the robot module from the project is not allowed.'); + } await storage.deleteModule(module.moduleType, modulePath); if (module.moduleType === MODULE_TYPE_MECHANISM) { project.mechanisms = project.mechanisms.filter(m => m.modulePath !== modulePath); @@ -146,34 +209,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 proposedName The new name for the module. - * @param oldModuleName The current name of the module. + * @param proposedClassName The new class name for the module. + * @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, proposedName: string, oldModulePath: string): Promise { - const module = findModuleInProject(project, oldModulePath); + storage: Storage, project: Project, proposedClassName: string, oldModulePath: string): Promise { + const module = findModuleByModulePath(project, oldModulePath); if (module) { - const newModuleName = classNameToModuleName(proposedName); + 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); module.modulePath = newModulePath; module.moduleName = newModuleName; - module.className = proposedName; + module.className = proposedClassName; 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 = proposedName; + mechanism.className = proposedClassName; } } 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 = proposedName; + opMode.className = proposedClassName; } return newModulePath } @@ -184,15 +250,18 @@ 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 proposedName The new name for the module. + * @param proposedClassName The new name for the module. * @param oldModuleName The current name of the module. * @returns A promise that resolves when the module has been copied. */ export async function copyModuleInProject( - storage: Storage, project: Project, proposedName: string, oldModulePath: string): Promise { - const module = findModuleInProject(project, oldModulePath); + storage: Storage, project: Project, proposedClassName: string, oldModulePath: string): Promise { + const module = findModuleByModulePath(project, oldModulePath); if (module) { - const newModuleName = classNameToModuleName(proposedName); + 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); @@ -202,7 +271,7 @@ export async function copyModuleInProject( moduleType: MODULE_TYPE_MECHANISM, projectName: project.projectName, moduleName: newModuleName, - className: proposedName + className: proposedClassName } as Mechanism); } else if (module.moduleType === MODULE_TYPE_OPMODE) { project.opModes.push({ @@ -210,7 +279,7 @@ export async function copyModuleInProject( moduleType: MODULE_TYPE_OPMODE, projectName: project.projectName, moduleName: newModuleName, - className: proposedName + className: proposedClassName } as OpMode); } return newModulePath; @@ -221,24 +290,19 @@ export async function copyModuleInProject( /** * Checks if the proposed class name is valid and does not conflict with existing names in the project. * @param project The project to check against. - * @param proposedName The proposed class name to validate. + * @param proposedClassName The proposed class name to validate. * @returns An object containing a boolean `ok` indicating if the name is valid, and an `error` message if it is not. */ -export function isClassNameOk(project: Project, proposedName: string) { +export function isClassNameOk(project: Project, proposedClassName: string) { let ok = true; let error = ''; - if (!isValidClassName(proposedName)) { + if (!isValidClassName(proposedClassName)) { ok = false; - error = proposedName + ' is not a valid name. Please enter a different name.'; - } - else if (proposedName == project.className) { + error = proposedClassName + ' is not a valid name. Please enter a different name.'; + } else if (findModuleByClassName(project, proposedClassName) != null) { ok = false; - error = 'The project is already named ' + proposedName + '. Please enter a different name.'; - } - else if (getClassInProject(project, proposedName) != null) { - ok = false; - error = 'Another Mechanism or OpMode is already named ' + proposedName + '. Please enter a different name.' + error = 'Another Mechanism or OpMode is already named ' + proposedClassName + '. Please enter a different name.' } return { @@ -248,42 +312,31 @@ export function isClassNameOk(project: Project, proposedName: string) { } /** - * Returns true if the given classname is in the project + * Returns the module in the given project with the given class name. */ -export function getClassInProject(project: Project, name: string): Module | null { +export function findModuleByClassName(project: Project, className: string): Module | null { + if (project.robot.className === className) { + return project.robot; + } for (const mechanism of project.mechanisms) { - if (mechanism.className === name) { + if (mechanism.className === className) { return mechanism; } } for (const opMode of project.opModes) { - if (opMode.className === name) { + if (opMode.className === className) { return opMode; } } return null; } -/** - * Returns the module with the given module path, or null if it is not found. - */ -export function findModule(modules: Project[], modulePath: string): Module | null { - for (const project of modules) { - const result = findModuleInProject(project, modulePath); - if (result) { - return result; - } - } - - return null; -} - /** * Returns the module with the given module path inside the given project, or null if it is not found. */ -export function findModuleInProject(project: Project, modulePath: string): Module | null { - if (project.modulePath === modulePath) { - return project; +export function findModuleByModulePath(project: Project, modulePath: string): Module | null { + if (project.robot.modulePath === modulePath) { + return project.robot; } for (const mechanism of project.mechanisms) { if (mechanism.modulePath === modulePath) { @@ -343,9 +396,9 @@ export function isValidClassName(name: string): boolean { } /** - * Returns the module name for the given class name. + * Returns the module name (snake_case) for the given class name (PascalCase). */ -export function classNameToModuleName(className: string): string { +export function pascalCaseToSnakeCase(className: string): string { let moduleName = ''; for (let i = 0; i < className.length; i++) { const char = className.charAt(i); @@ -362,9 +415,9 @@ export function classNameToModuleName(className: string): string { } /** - * Returns the class name for the given module name. + * Returns the class name (PascalCase) for the given module name (snake_case). */ -export function moduleNameToClassName(moduleName: string): string { +export function snakeCaseToPascalCase(moduleName: string): string { let className = ''; let nextCharUpper = true; for (let i = 0; i < moduleName.length; i++) { @@ -395,14 +448,14 @@ export function makeModulePath(projectName: string, moduleName: string): string } /** - * Returns the project path for the given project names. + * Returns the robot module path for the given project names. */ -export function makeProjectPath(projectName: string): string { +export function makeRobotPath(projectName: string): string { return makeModulePath(projectName, projectName); } /** - * Returns the project name for given module path. + * 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_]*).py$'); @@ -446,16 +499,16 @@ function startingBlocksToModuleContent( } /** - * Returns the module content for a new Project. + * Returns the robot module content for a new Project. */ -export function newProjectContent(projectName: string): string { - const module: Module = { - modulePath: makeProjectPath(projectName), - moduleType: MODULE_TYPE_PROJECT, +export function newRobotContent(projectName: string): string { + const module: Robot = { + modulePath: makeRobotPath(projectName), + moduleType: MODULE_TYPE_ROBOT, projectName: projectName, moduleName: projectName, dateModifiedMillis: 0, - className: moduleNameToClassName(projectName), + className: ROBOT_CLASS_NAME, }; return startingBlocksToModuleContent(module, startingRobotBlocks); @@ -471,7 +524,7 @@ export function newMechanismContent(projectName: string, mechanismName: string): projectName: projectName, moduleName: mechanismName, dateModifiedMillis: 0, - className: moduleNameToClassName(mechanismName), + className: snakeCaseToPascalCase(mechanismName), }; return startingBlocksToModuleContent(module, startingMechanismBlocks); @@ -487,7 +540,7 @@ export function newOpModeContent(projectName: string, opModeName: string): strin projectName: projectName, moduleName: opModeName, dateModifiedMillis: 0, - className: moduleNameToClassName(opModeName), + className: snakeCaseToPascalCase(opModeName), }; return startingBlocksToModuleContent(module, startingOpModeBlocks); @@ -640,6 +693,12 @@ export async function produceDownloadProjectBlob( return blobUrl; } +export function getClassNameForModule(moduleType: string, moduleName: string) { + return (moduleType == MODULE_TYPE_ROBOT) + ? ROBOT_CLASS_NAME + : snakeCaseToPascalCase(moduleName); +} + /** * Process the module content so it can be downloaded. */ @@ -661,7 +720,7 @@ function _processModuleContentForDownload( projectName: projectName, moduleName: moduleName, dateModifiedMillis: 0, - className: moduleNameToClassName(moduleName), + className: getClassNameForModule(moduleType, moduleName), }; // Clear out the python content and exported blocks. @@ -754,7 +813,7 @@ export function _processUploadedModule( moduleType = moduleType.substring(MARKER_MODULE_TYPE.length); } - const moduleName = (moduleType === MODULE_TYPE_PROJECT) + const moduleName = (moduleType === MODULE_TYPE_ROBOT) ? projectName : filename; const module: Module = { @@ -763,7 +822,7 @@ export function _processUploadedModule( projectName: projectName, moduleName: moduleName, dateModifiedMillis: 0, - className: moduleNameToClassName(moduleName), + className: snakeCaseToPascalCase(moduleName), }; // Generate the python content and exported blocks. diff --git a/src/toolbox/hardware_category.ts b/src/toolbox/hardware_category.ts index 6f511ec7..b9c0cb35 100644 --- a/src/toolbox/hardware_category.ts +++ b/src/toolbox/hardware_category.ts @@ -43,7 +43,7 @@ export function getHardwareCategory(currentModule: commonStorage.Module) { ] }; } - if (currentModule.moduleType === commonStorage.MODULE_TYPE_PROJECT) { + if (currentModule.moduleType === commonStorage.MODULE_TYPE_ROBOT) { return { kind: 'category', name: 'Hardware', diff --git a/src/toolbox/methods_category.ts b/src/toolbox/methods_category.ts index d41ced35..4e72acba 100644 --- a/src/toolbox/methods_category.ts +++ b/src/toolbox/methods_category.ts @@ -67,8 +67,8 @@ export class MethodsCategory { } }); - if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_PROJECT) { - // Add the methods for a Project (Robot). + if (this.currentModule.moduleType == commonStorage.MODULE_TYPE_ROBOT) { + // Add the methods for a Robot. this.addClassBlocksForCurrentModule( 'More Robot Methods', robot_class_blocks, methodNamesAlreadyUsed, contents); diff --git a/src/toolbox/toolbox.ts b/src/toolbox/toolbox.ts index 1509966a..2aec4211 100644 --- a/src/toolbox/toolbox.ts +++ b/src/toolbox/toolbox.ts @@ -8,7 +8,7 @@ export function getToolboxJSON( shownPythonToolboxCategories: Set | null, currentModule: commonStorage.Module): Blockly.utils.toolbox.ToolboxDefinition { switch (currentModule.moduleType) { - case commonStorage.MODULE_TYPE_PROJECT: + case commonStorage.MODULE_TYPE_ROBOT: case commonStorage.MODULE_TYPE_MECHANISM: return { kind: 'categoryToolbox',