diff --git a/src/App.tsx b/src/App.tsx index 16f197f2..d56e4554 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,6 @@ import { Flex, Input, message, - Modal, Popconfirm, Space, Splitter, @@ -40,10 +39,13 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { dracula } from 'react-syntax-highlighter/dist/esm/styles/prism'; import * as Blockly from 'blockly/core'; -import {pythonGenerator} from 'blockly/python' +import {pythonGenerator} from 'blockly/python'; import BlocklyComponent from './Blockly'; +import * as NewProjectNameModal from './modal/NewProjectNameModal'; +import * as NewModuleNameModal from './modal/NewModuleNameModal'; + import * as CustomBlocks from './blocks/setup_custom_blocks'; import { initialize as initializeGeneratedBlocks } from './blocks/utils/generated/initialize'; @@ -63,173 +65,6 @@ import * as ChangeFramework from './blocks/utils/change_framework' import {mutatorOpenListener} from './blocks/mrc_class_method_def' -type NewProjectNameModalProps = { - title: string; - isOpen: boolean; - initialValue: string; - getProjectNames: () => string[]; - onOk: (newName: string) => void; - onCancel: () => void; -} - -const NewProjectNameModal: React.FC = ({ title, isOpen, initialValue, getProjectNames, onOk, onCancel }) => { - const inputRef = useRef(null); - const [message, setMessage] = useState(''); - const [value, setValue] = useState(''); - const [projectNames, setProjectNames] = useState([]); - const [alertErrorMessage, setAlertErrorMessage] = useState(''); - const [alertErrorVisible, setAlertErrorVisible] = useState(false); - - const afterOpenChange = (open: boolean) => { - if (open) { - setValue(initialValue); - const w = getProjectNames(); - if (w.length === 0) { - setMessage('Let\'s create your first project. You just need to give it a name.'); - } - setProjectNames(w); - if (inputRef.current) { - inputRef.current.focus(); - } - } else { - setValue(''); - setMessage(''); - } - }; - - const handleOk = () => { - if (!commonStorage.isValidPythonModuleName(value)) { - setAlertErrorMessage(value + ' is not a valid blocks module name'); - setAlertErrorVisible(true); - return; - } - if (projectNames.includes(value)) { - setAlertErrorMessage('There is already a project named ' + value); - setAlertErrorVisible(true); - return; - } - onOk(value); - }; - - return ( - Cancel , - - ]} - > - - {message} - Project Name - setValue(e.target.value.toLowerCase())} - /> - {alertErrorVisible && ( - setAlertErrorVisible(false)} - /> - )} - - - ); -}; - - -type NewModuleNameModalProps = { - title: string; - isOpen: boolean; - initialValue: string; - getCurrentProjectName: () => string; - getModuleNames: (projectName: string) => string[]; - onOk: (newName: string) => void; - onCancel: () => void; -} - -const NewModuleNameModal: React.FC = ({ title, isOpen, initialValue, getCurrentProjectName, getModuleNames, onOk, onCancel }) => { - const inputRef = useRef(null); - const [value, setValue] = useState(''); - const [projectName, setProjectName] = useState(''); - const [moduleNames, setModuleNames] = useState([]); - const [alertErrorMessage, setAlertErrorMessage] = useState(''); - const [alertErrorVisible, setAlertErrorVisible] = useState(false); - - const afterOpenChange = (open: boolean) => { - if (open) { - setValue(initialValue); - const currentProjectName = getCurrentProjectName(); - setProjectName(currentProjectName); - setModuleNames(getModuleNames(currentProjectName)); - if (inputRef.current) { - inputRef.current.focus(); - } - } else { - setValue(''); - } - }; - - const handleOk = () => { - if (!commonStorage.isValidPythonModuleName(value)) { - setAlertErrorMessage(value + ' is not a valid blocks module name'); - setAlertErrorVisible(true); - return; - } - if (projectName === value) { - setAlertErrorMessage('The project is already named ' + value); - setAlertErrorVisible(true); - return; - } - if (moduleNames.includes(value)) { - setAlertErrorMessage('Another module is already named ' + value); - setAlertErrorVisible(true); - return; - } - onOk(value); - }; - - return ( - Cancel , - - ]} - > - - Name - setValue(e.target.value.toLowerCase())} - /> - {alertErrorVisible && ( - setAlertErrorVisible(false)} - /> - )} - - - ); -}; - type BlocklyComponentType = { getBlocklyWorkspace: () => Blockly.WorkspaceSvg, }; @@ -259,10 +94,13 @@ const App: React.FC = () => { const [newProjectNameModalPurpose, setNewProjectNameModalPurpose] = useState(''); const [newProjectNameModalInitialValue, setNewProjectNameModalInitialValue] = useState(''); const [newProjectNameModalTitle, setNewProjectNameModalTitle] = useState(''); + const [newProjectNameModalMessage, setNewProjectNameModalMessage] = useState(''); const [newProjectNameModalIsOpen, setNewProjectNameModalIsOpen] = useState(false); const [newModuleNameModalPurpose, setNewModuleNameModalPurpose] = useState(''); const [newModuleNameModalInitialValue, setNewModuleNameModalInitialValue] = useState(''); const [newModuleNameModalTitle, setNewModuleNameModalTitle] = useState(''); + const [newModuleNameModalExample, setNewModuleNameModalExample] = useState(''); + const [newModuleNameModalLabel, setNewModuleNameModalLabel] = useState(''); const [newModuleNameModalIsOpen, setNewModuleNameModalIsOpen] = useState(false); const [toolboxSettingsModalIsOpen, setToolboxSettingsModalIsOpen] = useState(false); const [popconfirmTitle, setPopconfirmTitle] = useState(''); @@ -356,7 +194,8 @@ const App: React.FC = () => { if (array.length === 0) { setNewProjectNameModalPurpose(PURPOSE_NEW_PROJECT); setNewProjectNameModalInitialValue(''); - setNewProjectNameModalTitle('Welcome to WPILib Blocks!'); + setNewProjectNameModalTitle(NewProjectNameModal.TITLE_WELCOME); + setNewProjectNameModalMessage(NewProjectNameModal.MESSAGE_WELCOME); setNewProjectNameModalIsOpen(true); } }; @@ -406,7 +245,7 @@ const App: React.FC = () => { } const child: TreeDataNode = { key: mechanism.modulePath, - title: mechanism.moduleName, + title: mechanism.className, icon: , }; children.push(child); @@ -420,14 +259,14 @@ const App: React.FC = () => { } const child: TreeDataNode = { key: opMode.modulePath, - title: opMode.moduleName, + title: opMode.className, icon: , }; children.push(child); }); const parent: TreeDataNode = { key: project.modulePath, - title: project.projectName, + title: project.className, children: children, icon: , }; @@ -596,11 +435,20 @@ const App: React.FC = () => { checkIfBlocksWereModified(() => { setNewProjectNameModalPurpose(PURPOSE_NEW_PROJECT); setNewProjectNameModalInitialValue(''); - setNewProjectNameModalTitle('New Project'); + setNewProjectNameModalTitle(NewProjectNameModal.TITLE_NEW_PROJECT); + setNewProjectNameModalMessage(''); setNewProjectNameModalIsOpen(true); }); }; + const getProjectClassNames = (): string[] => { + const projectClassNames: string[] = []; + modules.forEach((project) => { + projectClassNames.push(project.className); + }); + return projectClassNames; + }; + const getProjectNames = (): string[] => { const projectNames: string[] = []; modules.forEach((project) => { @@ -609,7 +457,8 @@ const App: React.FC = () => { return projectNames; }; - const handleNewProjectNameOk = async (newProjectName: string) => { + const handleNewProjectNameOk = async (newProjectClassName: string) => { + const newProjectName = commonStorage.classNameToModuleName(newProjectClassName); const newProjectPath = commonStorage.makeProjectPath(newProjectName); if (newProjectNameModalPurpose === PURPOSE_NEW_PROJECT) { const projectContent = commonStorage.newProjectContent(newProjectName); @@ -657,7 +506,9 @@ const App: React.FC = () => { checkIfBlocksWereModified(() => { setNewModuleNameModalPurpose(PURPOSE_NEW_MECHANISM); setNewModuleNameModalInitialValue(''); - setNewModuleNameModalTitle('New Mechanism'); + setNewModuleNameModalTitle(NewModuleNameModal.TITLE_NEW_MECHANISM); + setNewModuleNameModalExample(NewModuleNameModal.EXAMPLE_MECHANISM); + setNewModuleNameModalLabel(NewModuleNameModal.LABEL_MECHANISM); setNewModuleNameModalIsOpen(true); }); }; @@ -666,7 +517,9 @@ const App: React.FC = () => { checkIfBlocksWereModified(() => { setNewModuleNameModalPurpose(PURPOSE_NEW_OPMODE); setNewModuleNameModalInitialValue(''); - setNewModuleNameModalTitle('New OpMode'); + setNewModuleNameModalTitle(NewModuleNameModal.TITLE_NEW_OPMODE); + setNewModuleNameModalExample(NewModuleNameModal.EXAMPLE_OPMODE); + setNewModuleNameModalLabel(NewModuleNameModal.LABEL_OPMODE); setNewModuleNameModalIsOpen(true); }); }; @@ -678,24 +531,25 @@ const App: React.FC = () => { }; // Provide a callback so the NewModuleNameModal will know what the existing - // module names are in the current project. - const getModuleNames = (projectName: string): string[] => { - const moduleNames: string[] = []; + // module class names are in the current project. + const getModuleClassNames = (projectName: string): string[] => { + const moduleClassNames: string[] = []; for (const project of modules) { if (project.projectName === projectName) { project.mechanisms.forEach((mechanism) => { - moduleNames.push(mechanism.moduleName); + moduleClassNames.push(mechanism.className); }); project.opModes.forEach((opMode) => { - moduleNames.push(opMode.moduleName); + moduleClassNames.push(opMode.className); }); break; } } - return moduleNames; + return moduleClassNames; }; - const handleNewModuleNameOk = async (newModuleName: string) => { + const handleNewModuleNameOk = async (newModuleClassName: string) => { + const newModuleName = commonStorage.classNameToModuleName(newModuleClassName); const newModulePath = commonStorage.makeModulePath(currentModule.projectName, newModuleName); if (newModuleNameModalPurpose === PURPOSE_NEW_MECHANISM) { const mechanismContent = commonStorage.newMechanismContent( @@ -784,20 +638,25 @@ const App: React.FC = () => { if (currentModule.moduleType == commonStorage.MODULE_TYPE_PROJECT) { // This is a Project. setNewProjectNameModalPurpose(PURPOSE_RENAME_PROJECT); - setNewProjectNameModalInitialValue(currentModule.projectName); - setNewProjectNameModalTitle('Rename Project'); + setNewProjectNameModalInitialValue(commonStorage.moduleNameToClassName(currentModule.projectName)); + setNewProjectNameModalTitle(NewProjectNameModal.TITLE_RENAME_PROJECT); + setNewProjectNameModalMessage(''); setNewProjectNameModalIsOpen(true); } else if (currentModule.moduleType == commonStorage.MODULE_TYPE_MECHANISM) { // This is a Mechanism. setNewModuleNameModalPurpose(PURPOSE_RENAME_MODULE); - setNewModuleNameModalInitialValue(currentModule.moduleName); - setNewModuleNameModalTitle('Rename Mechanism'); + setNewModuleNameModalInitialValue(currentModule.className); + setNewModuleNameModalTitle(NewModuleNameModal.TITLE_RENAME_MECHANISM); + setNewModuleNameModalExample(NewModuleNameModal.EXAMPLE_MECHANISM); + setNewModuleNameModalLabel(NewModuleNameModal.LABEL_MECHANISM); setNewModuleNameModalIsOpen(true); } else if (currentModule.moduleType == commonStorage.MODULE_TYPE_OPMODE) { // This is an OpMode. setNewModuleNameModalPurpose(PURPOSE_RENAME_MODULE); - setNewModuleNameModalInitialValue(currentModule.moduleName); - setNewModuleNameModalTitle('Rename OpMode'); + setNewModuleNameModalInitialValue(currentModule.className); + setNewModuleNameModalTitle(NewModuleNameModal.TITLE_RENAME_OPMODE); + setNewModuleNameModalExample(NewModuleNameModal.EXAMPLE_OPMODE); + setNewModuleNameModalLabel(NewModuleNameModal.LABEL_OPMODE); setNewModuleNameModalIsOpen(true); } }); @@ -811,20 +670,25 @@ const App: React.FC = () => { if (currentModule.moduleType == commonStorage.MODULE_TYPE_PROJECT) { // This is a Project. setNewProjectNameModalPurpose(PURPOSE_COPY_PROJECT); - setNewProjectNameModalInitialValue(currentModule.projectName + '_copy'); - setNewProjectNameModalTitle('Copy Project'); + setNewProjectNameModalInitialValue(commonStorage.moduleNameToClassName(currentModule.projectName) + 'Copy'); + setNewProjectNameModalTitle(NewProjectNameModal.TITLE_COPY_PROJECT); + setNewProjectNameModalMessage(''); setNewProjectNameModalIsOpen(true); } else if (currentModule.moduleType == commonStorage.MODULE_TYPE_MECHANISM) { // This is a Mechanism. setNewModuleNameModalPurpose(PURPOSE_COPY_MODULE); - setNewModuleNameModalInitialValue(currentModule.moduleName + '_copy'); - setNewModuleNameModalTitle('Copy Mechanism'); + setNewModuleNameModalInitialValue(currentModule.className + 'Copy'); + setNewModuleNameModalTitle(NewModuleNameModal.TITLE_COPY_MECHANISM); + setNewModuleNameModalExample(NewModuleNameModal.EXAMPLE_MECHANISM); + setNewModuleNameModalLabel(NewModuleNameModal.LABEL_MECHANISM); setNewModuleNameModalIsOpen(true); } else if (currentModule.moduleType == commonStorage.MODULE_TYPE_OPMODE) { // This is an OpMode. setNewModuleNameModalPurpose(PURPOSE_COPY_MODULE); - setNewModuleNameModalInitialValue(currentModule.moduleName + '_copy'); - setNewModuleNameModalTitle('Copy OpMode'); + setNewModuleNameModalInitialValue(currentModule.className + 'Copy'); + setNewModuleNameModalTitle(NewModuleNameModal.TITLE_COPY_OPMODE); + setNewModuleNameModalExample(NewModuleNameModal.EXAMPLE_OPMODE); + setNewModuleNameModalLabel(NewModuleNameModal.LABEL_OPMODE); setNewModuleNameModalIsOpen(true); } }); @@ -1238,23 +1102,26 @@ const App: React.FC = () => { - { setNewProjectNameModalIsOpen(false); handleNewProjectNameOk(newName); }} onCancel={() => setNewProjectNameModalIsOpen(false)} /> - { setNewModuleNameModalIsOpen(false); handleNewModuleNameOk(newName); diff --git a/src/modal/NewModuleNameModal.tsx b/src/modal/NewModuleNameModal.tsx new file mode 100644 index 00000000..a4875faa --- /dev/null +++ b/src/modal/NewModuleNameModal.tsx @@ -0,0 +1,143 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author lizlooney@google.com (Liz Looney) + */ + +import React, { useState, useRef } from 'react'; + +import { + Alert, + Button, + Flex, + Input, + Modal, + Typography, +} from 'antd'; +import type { InputRef } from 'antd'; + +import * as commonStorage from '../storage/common_storage'; + + +export const TITLE_NEW_MECHANISM = 'New Mechanism'; +export const TITLE_RENAME_MECHANISM = 'Rename Mechanism'; +export const TITLE_COPY_MECHANISM = 'Copy Mechanism'; +export const TITLE_NEW_OPMODE = 'New OpMode'; +export const TITLE_RENAME_OPMODE = 'Rename OpMode'; +export const TITLE_COPY_OPMODE = 'Copy OpMode'; + +const DESCRIPTION = 'No spaces are allowed in the name. Each word in the name should start with a capital letter.'; + +export const EXAMPLE_MECHANISM = 'For example: GamePieceShooter'; +export const EXAMPLE_OPMODE = 'For example: AutoParkAndShoot'; +export const LABEL_MECHANISM = 'Mechanism Name:'; +export const LABEL_OPMODE = 'OpMode Name:'; + +type NewModuleNameModalProps = { + title: string; + example: string; + label: string; + isOpen: boolean; + initialValue: string; + getCurrentProjectName: () => string; + getModuleClassNames: (projectName: string) => string[]; + onOk: (newName: string) => void; + onCancel: () => void; +} + +export const NewModuleNameModal: React.FC = ({ title, example, label, isOpen, initialValue, getCurrentProjectName, getModuleClassNames, onOk, onCancel }) => { + const inputRef = useRef(null); + const [value, setValue] = useState(''); + const [projectClassName, setProjectClassName] = useState(''); + const [moduleClassNames, setModuleClassNames] = useState([]); + const [alertErrorMessage, setAlertErrorMessage] = useState(''); + const [alertErrorVisible, setAlertErrorVisible] = useState(false); + + const afterOpenChange = (open: boolean) => { + if (open) { + setValue(initialValue); + const currentProjectName = getCurrentProjectName(); + setProjectClassName(commonStorage.moduleNameToClassName(currentProjectName)); + setModuleClassNames(getModuleClassNames(currentProjectName)); + if (inputRef.current) { + inputRef.current.focus(); + } + } else { + setValue(''); + setAlertErrorVisible(false); + } + }; + + const onChange = (e) => { + setValue(commonStorage.onChangeClassName(e.target.value)); + }; + + const handleOk = () => { + if (!commonStorage.isValidClassName(value)) { + setAlertErrorMessage(value + ' is not a valid name. Please enter a different name.'); + setAlertErrorVisible(true); + return; + } + if (projectClassName === value) { + setAlertErrorMessage('The project is already named ' + value + '. Please enter a different name.'); + setAlertErrorVisible(true); + return; + } + if (moduleClassNames.includes(value)) { + setAlertErrorMessage('Another Mechanism or OpMode is already named ' + value + '. Please enter a different name.'); + setAlertErrorVisible(true); + return; + } + onOk(value); + }; + + return ( + Cancel , + + ]} + > + + {DESCRIPTION} + {example} + + {label} + + + {alertErrorVisible && ( + setAlertErrorVisible(false)} + /> + )} + + + ); +}; diff --git a/src/modal/NewProjectNameModal.tsx b/src/modal/NewProjectNameModal.tsx new file mode 100644 index 00000000..63335d07 --- /dev/null +++ b/src/modal/NewProjectNameModal.tsx @@ -0,0 +1,134 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author lizlooney@google.com (Liz Looney) + */ + +import React, { useState, useRef } from 'react'; + +import { + Alert, + Button, + Flex, + Input, + Modal, + Typography, +} from 'antd'; +import type { InputRef } from 'antd'; + +import * as commonStorage from '../storage/common_storage'; + + +export const TITLE_WELCOME = 'Welcome to WPILib Blocks!'; +export const TITLE_NEW_PROJECT = 'New Project'; +export const TITLE_RENAME_PROJECT = 'Rename Project'; +export const TITLE_COPY_PROJECT = 'Copy Project'; + +export const MESSAGE_WELCOME = 'Let\'s create your first project. You just need to give it a name.'; + +const DESCRIPTION = 'No spaces are allowed in the name. Each word in the name should start with a capital letter.'; +const EXAMPLE = 'For example: WackyWheelerRobot'; +const LABEL = 'Project Name:'; + +type NewProjectNameModalProps = { + title: string; + message: string; + isOpen: boolean; + initialValue: string; + getProjectClassNames: () => string[]; + onOk: (newName: string) => void; + onCancel: () => void; +} + +export const NewProjectNameModal: React.FC = ({ title, message, isOpen, initialValue, getProjectClassNames, onOk, onCancel }) => { + const inputRef = useRef(null); + const [value, setValue] = useState(''); + const [projectNames, setProjectNames] = useState([]); + const [alertErrorMessage, setAlertErrorMessage] = useState(''); + const [alertErrorVisible, setAlertErrorVisible] = useState(false); + + const afterOpenChange = (open: boolean) => { + if (open) { + setValue(initialValue); + const w = getProjectClassNames(); + setProjectNames(w); + if (inputRef.current) { + inputRef.current.focus(); + } + } else { + setValue(''); + setAlertErrorVisible(false); + } + }; + + const onChange = (e) => { + setValue(commonStorage.onChangeClassName(e.target.value)); + }; + + const handleOk = () => { + if (!commonStorage.isValidClassName(value)) { + setAlertErrorMessage(value + ' is not a valid project name. Please enter a different name.'); + setAlertErrorVisible(true); + return; + } + if (projectNames.includes(value)) { + setAlertErrorMessage('There is already a project named ' + value + '. Please enter a different name.'); + setAlertErrorVisible(true); + return; + } + onOk(value); + }; + + return ( + Cancel , + + ]} + > + + {message} + {DESCRIPTION} + {EXAMPLE} + + {LABEL} + + + {alertErrorVisible && ( + setAlertErrorVisible(false)} + /> + )} + + + ); +}; diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index c768bcb7..5a976462 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -135,12 +135,14 @@ 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: commonStorage.getModuleName(path), + moduleName: moduleName, dateModifiedMillis: value.dateModifiedMillis, + className: commonStorage.moduleNameToClassName(moduleName), } if (moduleType === commonStorage.MODULE_TYPE_PROJECT) { const project: commonStorage.Project = { diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 04355845..fa24bba8 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -23,7 +23,7 @@ import JSZip from 'jszip'; import * as Blockly from 'blockly/core'; -import {Block} from "../toolbox/items"; +import {Block} from '../toolbox/items'; import startingOpModeBlocks from '../modules/opmode_start.json'; import startingMechanismBlocks from '../modules/mechanism_start.json'; import startingRobotBlocks from '../modules/robot_start.json'; @@ -39,6 +39,7 @@ export type Module = { projectName: string, moduleName: string, dateModifiedMillis: number, + className: string, }; export type Mechanism = Module; @@ -104,6 +105,84 @@ export function findModule(modules: Project[], modulePath: string): Module | nul return null; } +/** + * Makes the given name a valid class name. + */ +export function onChangeClassName(name: string): string { + let newName = ''; + + // Force the first character to be an upper case letter + let i = 0; + for (; i < name.length; i++) { + const firstChar = name.charAt(0); + if (firstChar >= 'A' && firstChar <= 'Z') { + newName += firstChar; + i++; + break; + } else if (firstChar >= 'a' && firstChar <= 'z') { + newName += firstChar.toUpperCase(); + i++; + break; + } + } + + for (; i < name.length; i++) { + const char = name.charAt(i); + if ((char >= 'A' && char <= 'Z') || + (char >= 'a' && char <= 'z') || + (char >= '0' && char <= '9')) { + newName += char; + } + } + + return newName; +} + +/** + * Returns true if the given name is a valid class name. + */ +export function isValidClassName(name: string): boolean { + if (name) { + return /^[A-Z][A-Za-z0-9]*$/.test(name); + } + return false; +} + +/** + * Returns the module name for the given class name. + */ +export function classNameToModuleName(className: string): boolean { + let moduleName = ''; + for (let i = 0; i < className.length; i++) { + const char = className.charAt(i); + if (char >= 'A' && char <= 'Z') { + if (i > 0) { + moduleName += '_'; + } + moduleName += char.toLowerCase(); + } else { + moduleName += char; + } + } + return moduleName; +} + +/** + * Returns the class name for the given module name. + */ +export function moduleNameToClassName(moduleName: string): boolean { + let className = ''; + let nextCharUpper = true; + for (let i = 0; i < moduleName.length; i++) { + const char = moduleName.charAt(i); + if (char !== '_') { + className += nextCharUpper ? char.toUpperCase() : char; + } + nextCharUpper = (char === '_'); + } + return className; +} + /** * Returns true if the given name is a valid python module name. */ @@ -180,6 +259,7 @@ export function newProjectContent(projectName: string): string { projectName: projectName, moduleName: projectName, dateModifiedMillis: 0, + className: moduleNameToClassName(projectName), }; return startingBlocksToModuleContent(module, startingRobotBlocks); @@ -195,6 +275,7 @@ export function newMechanismContent(projectName: string, mechanismName: string): projectName: projectName, moduleName: mechanismName, dateModifiedMillis: 0, + className: moduleNameToClassName(mechanismName), }; return startingBlocksToModuleContent(module, startingMechanismBlocks); @@ -210,6 +291,7 @@ export function newOpModeContent(projectName: string, opModeName: string): strin projectName: projectName, moduleName: opModeName, dateModifiedMillis: 0, + className: moduleNameToClassName(opModeName), }; return startingBlocksToModuleContent(module, startingOpModeBlocks); @@ -356,6 +438,7 @@ function _processModuleContentForDownload( projectName: projectName, moduleName: moduleName, dateModifiedMillis: 0, + className: moduleNameToClassName(moduleName), }; // Clear out the python content and exported blocks. @@ -455,6 +538,7 @@ export function _processUploadedModule( projectName: projectName, moduleName: moduleName, dateModifiedMillis: 0, + className: moduleNameToClassName(moduleName), }; // Generate the python content and exported blocks.