Skip to content

Commit acbfc1f

Browse files
authored
Save modules with names like ProjectName.ClassName.json. (#190)
* Save modules with names like ProjectName.ClassName.json. Save modules with multi-line formatted json. Removed moduleName field from Module (and Robot, Mechanism, and OpMode). Removed userVisibleName from Project. Renamed ModuleNameComponent.tsx to ClassNameComponent.tsx. In Menu.tsx, added handleDownload and handleUpload. I didn't add the actual UI element (menu or button, etc) to trigger the handlers. * Fixed typescript errors. * Fixed editor code that was using moduleName.
1 parent db01671 commit acbfc1f

File tree

14 files changed

+331
-239
lines changed

14 files changed

+331
-239
lines changed

src/blocks/mrc_mechanism.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,9 @@ const MECHANISM = {
186186
if (this.getFieldValue(FIELD_TYPE) !== foundMechanism.className) {
187187
this.setFieldValue(foundMechanism.className, FIELD_TYPE);
188188
}
189-
// If the mechanism module name has changed, update this block.
190-
if (this.mrcImportModule !== foundMechanism.moduleName) {
191-
this.mrcImportModule = foundMechanism.moduleName;
189+
const importModule = commonStorage.pascalCaseToSnakeCase(foundMechanism.className);
190+
if (this.mrcImportModule !== importModule) {
191+
this.mrcImportModule = importModule;
192192
}
193193
this.mrcParameters = [];
194194
components.forEach(component => {
@@ -245,12 +245,10 @@ export const pythonFromBlock = function (
245245

246246
export function createMechanismBlock(
247247
mechanism: commonStorage.Mechanism, components: commonStorage.Component[]): toolboxItems.Block {
248-
const lastDot = mechanism.className.lastIndexOf('.');
249-
const mechanismName = (
250-
'my_' +
251-
commonStorage.pascalCaseToSnakeCase(mechanism.className.substring(lastDot + 1)));
248+
const snakeCaseName = commonStorage.pascalCaseToSnakeCase(mechanism.className);
249+
const mechanismName = 'my_' + snakeCaseName;
252250
const extraState: MechanismExtraState = {
253-
importModule: mechanism.moduleName,
251+
importModule: snakeCaseName,
254252
parameters: [],
255253
};
256254
const inputs: {[key: string]: any} = {};

src/blocks/utils/python.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ export function getOutputCheck(type: string): string {
268268
// This is a legal name for python methods and variables.
269269
export function getLegalName(proposedName: string, existingNames: string[]){
270270
let newName = proposedName.trim().replace(' ', '_');
271+
272+
// TODO: Allow the user to put numbers in the name.
271273

272274
if (!/^[A-Za-z_]/.test(newName)){
273275
newName = "_" + newName;

src/editor/editor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,8 @@ export class Editor {
382382
public getMechanism(mechanismInRobot: commonStorage.MechanismInRobot): commonStorage.Mechanism | null {
383383
if (this.currentProject) {
384384
for (const mechanism of this.currentProject.mechanisms) {
385-
if (mechanism.moduleName + '.' + mechanism.className === mechanismInRobot.className) {
385+
const fullClassName = commonStorage.pascalCaseToSnakeCase(mechanism.className) + '.' + mechanism.className;
386+
if (fullClassName === mechanismInRobot.className) {
386387
return mechanism;
387388
}
388389
}

src/reactComponents/AddTabDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import * as Antd from 'antd';
2424
import * as I18Next from 'react-i18next';
2525
import * as React from 'react';
2626
import * as commonStorage from '../storage/common_storage';
27-
import ModuleNameComponent from './ModuleNameComponent';
27+
import ClassNameComponent from './ClassNameComponent';
2828

2929
/** Represents a module item in the dialog. */
3030
interface Module {
@@ -306,7 +306,7 @@ export default function AddTabDialog(props: AddTabDialogProps) {
306306
</Antd.Radio.Button>
307307
</Antd.Radio.Group>
308308

309-
<ModuleNameComponent
309+
<ClassNameComponent
310310
tabType={tabType}
311311
newItemName={newItemName}
312312
setNewItemName={setNewItemName}

src/reactComponents/ModuleNameComponent.tsx renamed to src/reactComponents/ClassNameComponent.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import * as I18Next from 'react-i18next';
2525
import * as React from 'react';
2626
import * as commonStorage from '../storage/common_storage';
2727

28-
/** Props for the ModuleNameComponent. */
29-
interface ModuleNameComponentProps {
28+
/** Props for the ClassNameComponent. */
29+
interface ClassNameComponentProps {
3030
tabType: TabType;
3131
newItemName: string;
3232
setNewItemName: (name: string) => void;
@@ -46,10 +46,10 @@ const INPUT_WIDTH_FULL = '100%';
4646
const ERROR_ALERT_MARGIN_TOP = 8;
4747

4848
/**
49-
* Component for entering and validating module names.
49+
* Component for entering and validating class names.
5050
* Provides input validation, error display, and automatic capitalization.
5151
*/
52-
export default function ModuleNameComponent(props: ModuleNameComponentProps): React.JSX.Element {
52+
export default function ClassNameComponent(props: ClassNameComponentProps): React.JSX.Element {
5353
const {t} = I18Next.useTranslation();
5454
const [alertErrorMessage, setAlertErrorMessage] = React.useState('');
5555
const [alertErrorVisible, setAlertErrorVisible] = React.useState(false);

src/reactComponents/FileManageModal.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import * as I18Next from 'react-i18next';
2424
import * as React from 'react';
2525
import * as commonStorage from '../storage/common_storage';
2626
import {EditOutlined, DeleteOutlined, CopyOutlined} from '@ant-design/icons';
27-
import ModuleNameComponent from './ModuleNameComponent';
27+
import ClassNameComponent from './ClassNameComponent';
2828

2929
/** Represents a module in the file management system. */
3030
interface Module {
@@ -347,7 +347,7 @@ export default function FileManageModal(props: FileManageModalProps) {
347347
cancelText={t('Cancel')}
348348
>
349349
{currentRecord && (
350-
<ModuleNameComponent
350+
<ClassNameComponent
351351
tabType={currentRecord.type}
352352
newItemName={name}
353353
setNewItemName={setName}
@@ -376,7 +376,7 @@ export default function FileManageModal(props: FileManageModalProps) {
376376
cancelText={t('Cancel')}
377377
>
378378
{currentRecord && (
379-
<ModuleNameComponent
379+
<ClassNameComponent
380380
tabType={currentRecord.type}
381381
newItemName={name}
382382
setNewItemName={setName}
@@ -409,7 +409,7 @@ export default function FileManageModal(props: FileManageModalProps) {
409409
borderRadius: '6px',
410410
padding: '12px',
411411
}}>
412-
<ModuleNameComponent
412+
<ClassNameComponent
413413
tabType={props.moduleType}
414414
newItemName={newItemName}
415415
setNewItemName={setNewItemName}

src/reactComponents/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default function Header(props: HeaderProps): React.JSX.Element {
7878

7979
/** Gets the project name or fallback text. */
8080
const getProjectName = (): string => {
81-
return props.project?.userVisibleName || 'No Project Selected';
81+
return props.project?.projectName || 'No Project Selected';
8282
};
8383

8484
return (

src/reactComponents/Menu.tsx

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121
import * as Antd from 'antd';
2222
import * as React from 'react';
23+
import { RcFile, UploadRequestOption } from 'rc-upload/lib/interface';
2324
import * as commonStorage from '../storage/common_storage';
2425
import * as createPythonFiles from '../storage/create_python_files';
2526
import * as I18Next from 'react-i18next';
@@ -38,6 +39,8 @@ import {
3839
BgColorsOutlined,
3940
GlobalOutlined,
4041
CheckOutlined,
42+
DownloadOutlined,
43+
UploadOutlined,
4144
} from '@ant-design/icons';
4245
import FileManageModal from './FileManageModal';
4346
import ProjectManageModal from './ProjectManageModal';
@@ -180,6 +183,7 @@ export function Component(props: MenuProps): React.JSX.Element {
180183
const [noProjects, setNoProjects] = React.useState<boolean>(false);
181184
const [aboutDialogVisible, setAboutDialogVisible] = React.useState<boolean>(false);
182185
const [themeModalOpen, setThemeModalOpen] = React.useState<boolean>(false);
186+
const [showUploadAndDownload, _setShowUploadAndDownload] = React.useState(false);
183187

184188
const handleThemeChange = (newTheme: string) => {
185189
props.setTheme(newTheme);
@@ -322,6 +326,81 @@ export function Component(props: MenuProps): React.JSX.Element {
322326
}
323327
};
324328

329+
// TODO: Add UI for the download action.
330+
/** Handles the download action to generate and download json files. */
331+
const handleDownload = async (): Promise<void> => {
332+
if (!props.project || !props.storage) {
333+
return;
334+
}
335+
336+
try {
337+
const blobUrl = await props.storage.downloadProject(props.project.projectName);
338+
const filename = props.project.projectName + commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION;
339+
340+
// Create a temporary link to download the file
341+
const link = document.createElement('a');
342+
link.href = blobUrl;
343+
link.download = filename;
344+
document.body.appendChild(link);
345+
link.click();
346+
document.body.removeChild(link);
347+
348+
// Clean up the blob URL
349+
URL.revokeObjectURL(blobUrl);
350+
} catch (error) {
351+
console.error('Failed to download project:', error);
352+
props.setAlertErrorMessage(t('DOWNLOAD_FAILED') || 'Failed to download project');
353+
}
354+
}
355+
356+
// TODO: Add UI for the upload action.
357+
/** Handles the upload action to upload a previously downloaded project. */
358+
const handleUpload = (): Antd.UploadProps | null => {
359+
if (!props.storage) {
360+
return null;
361+
}
362+
363+
const uploadProps: Antd.UploadProps = {
364+
accept: commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION,
365+
beforeUpload: (file) => {
366+
const isBlocks = file.name.endsWith(commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION)
367+
if (!isBlocks) {
368+
// TODO: i18n
369+
props.setAlertErrorMessage(file.name + ' is not a blocks file');
370+
return false;
371+
}
372+
return isBlocks || Antd.Upload.LIST_IGNORE;
373+
},
374+
onChange: (_info) => {
375+
},
376+
customRequest: (options: UploadRequestOption) => {
377+
const reader = new FileReader();
378+
reader.onload = (event) => {
379+
if (!event.target) {
380+
return;
381+
}
382+
const dataUrl = event.target.result as string;
383+
const existingProjectNames: string[] = [];
384+
projects.forEach(project => {
385+
existingProjectNames.push(project.projectName);
386+
});
387+
const file = options.file as RcFile;
388+
const uploadProjectName = commonStorage.makeUploadProjectName(file.name, existingProjectNames);
389+
if (props.storage) {
390+
props.storage.uploadProject(uploadProjectName, dataUrl);
391+
}
392+
};
393+
reader.onerror = (_error) => {
394+
console.log('Error reading file: ' + reader.error);
395+
// TODO: i18n
396+
props.setAlertErrorMessage(t('UPLOAD_FAILED') || 'Failed to upload project');
397+
};
398+
reader.readAsDataURL(options.file as Blob);
399+
},
400+
};
401+
return uploadProps;
402+
};
403+
325404
/** Handles closing the file management modal. */
326405
const handleFileModalClose = (): void => {
327406
console.log('Modal onCancel called');
@@ -382,6 +461,30 @@ export function Component(props: MenuProps): React.JSX.Element {
382461
items={menuItems}
383462
onClick={handleClick}
384463
/>
464+
{showUploadAndDownload ? (
465+
<div>
466+
<Antd.Upload
467+
{...handleUpload()}
468+
showUploadList={false}
469+
>
470+
<Antd.Button
471+
icon={<UploadOutlined />}
472+
size="small"
473+
style={{ color: 'white' }}
474+
/>
475+
</Antd.Upload>
476+
<Antd.Button
477+
icon={<DownloadOutlined />}
478+
size="small"
479+
disabled={!props.project}
480+
onClick={handleDownload}
481+
style={{ color: 'white' }}
482+
/>
483+
</div>
484+
) : (
485+
<div>
486+
</div>
487+
)}
385488
<AboutDialog
386489
visible={aboutDialogVisible}
387490
onClose={() => setAboutDialogVisible(false)}

src/reactComponents/ProjectManageModal.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
7474
const projects = await storage.listProjects();
7575

7676
// Sort projects alphabetically by name
77-
projects.sort((a, b) => a.userVisibleName.localeCompare(b.userVisibleName));
77+
projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
7878
setAllProjects(projects);
7979

8080
if (projects.length > 0 && props.noProjects) {
@@ -186,25 +186,25 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
186186
/** Opens the rename modal for a specific project. */
187187
const openRenameModal = (record: commonStorage.Project): void => {
188188
setCurrentRecord(record);
189-
setName(record.userVisibleName);
189+
setName(record.projectName);
190190
setRenameModalOpen(true);
191191
};
192192

193193
/** Opens the copy modal for a specific project. */
194194
const openCopyModal = (record: commonStorage.Project): void => {
195195
setCurrentRecord(record);
196-
setName(record.userVisibleName + COPY_SUFFIX);
196+
setName(record.projectName + COPY_SUFFIX);
197197
setCopyModalOpen(true);
198198
};
199199

200200
/** Gets the rename modal title. */
201201
const getRenameModalTitle = (): string => {
202-
return `Rename Project: ${currentRecord ? currentRecord.userVisibleName : ''}`;
202+
return `Rename Project: ${currentRecord ? currentRecord.projectName : ''}`;
203203
};
204204

205205
/** Gets the copy modal title. */
206206
const getCopyModalTitle = (): string => {
207-
return `Copy Project: ${currentRecord ? currentRecord.userVisibleName : ''}`;
207+
return `Copy Project: ${currentRecord ? currentRecord.projectName : ''}`;
208208
};
209209

210210
/** Creates the container style object. */
@@ -224,8 +224,8 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
224224
const columns: Antd.TableProps<commonStorage.Project>['columns'] = [
225225
{
226226
title: 'Name',
227-
dataIndex: 'userVisibleName',
228-
key: 'userVisibleName',
227+
dataIndex: 'projectName',
228+
key: 'projectName',
229229
ellipsis: {
230230
showTitle: false,
231231
},
@@ -268,7 +268,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
268268
{allProjects.length > 1 && (
269269
<Antd.Tooltip title={t('Delete')}>
270270
<Antd.Popconfirm
271-
title={`Delete ${record.userVisibleName}?`}
271+
title={`Delete ${record.projectName}?`}
272272
description="This action cannot be undone."
273273
onConfirm={() => handleDeleteProject(record)}
274274
okText={t('Delete')}
@@ -385,7 +385,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
385385
<Antd.Table<commonStorage.Project>
386386
columns={columns}
387387
dataSource={allProjects}
388-
rowKey="userVisibleName"
388+
rowKey="projectName"
389389
size="small"
390390
pagination={allProjects.length > DEFAULT_PAGE_SIZE ? {
391391
pageSize: DEFAULT_PAGE_SIZE,

src/reactComponents/ProjectNameComponent.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,18 @@ export default function ProjectNameComponent(props: ProjectNameComponentProps):
5858

5959
/** Handles adding a new item with validation. */
6060
const handleAddNewItem = (): void => {
61-
const newUserVisibleName = props.newItemName.trim();
62-
if (!newUserVisibleName || !props.projects) {
61+
const newProjectName = props.newItemName.trim();
62+
if (!newProjectName || !props.projects) {
6363
return;
6464
}
6565

66-
if (!commonStorage.isValidClassName(newUserVisibleName)) {
67-
showError(newUserVisibleName + INVALID_NAME_MESSAGE_SUFFIX);
66+
if (!commonStorage.isValidClassName(newProjectName)) {
67+
showError(newProjectName + INVALID_NAME_MESSAGE_SUFFIX);
6868
return;
6969
}
7070

71-
if (props.projects.some((project) => project.userVisibleName === newUserVisibleName)) {
72-
showError(DUPLICATE_NAME_MESSAGE_PREFIX + newUserVisibleName + DUPLICATE_NAME_MESSAGE_SUFFIX);
71+
if (props.projects.some((project) => project.projectName === newProjectName)) {
72+
showError(DUPLICATE_NAME_MESSAGE_PREFIX + newProjectName + DUPLICATE_NAME_MESSAGE_SUFFIX);
7373
return;
7474
}
7575

0 commit comments

Comments
 (0)