Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions src/blocks/mrc_mechanism.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ const MECHANISM = {
if (this.getFieldValue(FIELD_TYPE) !== foundMechanism.className) {
this.setFieldValue(foundMechanism.className, FIELD_TYPE);
}
// If the mechanism module name has changed, update this block.
if (this.mrcImportModule !== foundMechanism.moduleName) {
this.mrcImportModule = foundMechanism.moduleName;
const importModule = commonStorage.pascalCaseToSnakeCase(foundMechanism.className);
if (this.mrcImportModule !== importModule) {
this.mrcImportModule = importModule;
}
this.mrcParameters = [];
components.forEach(component => {
Expand Down Expand Up @@ -245,12 +245,10 @@ export const pythonFromBlock = function (

export function createMechanismBlock(
mechanism: commonStorage.Mechanism, components: commonStorage.Component[]): toolboxItems.Block {
const lastDot = mechanism.className.lastIndexOf('.');
const mechanismName = (
'my_' +
commonStorage.pascalCaseToSnakeCase(mechanism.className.substring(lastDot + 1)));
const snakeCaseName = commonStorage.pascalCaseToSnakeCase(mechanism.className);
const mechanismName = 'my_' + snakeCaseName;
const extraState: MechanismExtraState = {
importModule: mechanism.moduleName,
importModule: snakeCaseName,
parameters: [],
};
const inputs: {[key: string]: any} = {};
Expand Down
2 changes: 2 additions & 0 deletions src/blocks/utils/python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ export function getOutputCheck(type: string): string {
// This is a legal name for python methods and variables.
export function getLegalName(proposedName: string, existingNames: string[]){
let newName = proposedName.trim().replace(' ', '_');

// TODO: Allow the user to put numbers in the name.

if (!/^[A-Za-z_]/.test(newName)){
newName = "_" + newName;
Expand Down
3 changes: 2 additions & 1 deletion src/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@ export class Editor {
public getMechanism(mechanismInRobot: commonStorage.MechanismInRobot): commonStorage.Mechanism | null {
if (this.currentProject) {
for (const mechanism of this.currentProject.mechanisms) {
if (mechanism.moduleName + '.' + mechanism.className === mechanismInRobot.className) {
const fullClassName = commonStorage.pascalCaseToSnakeCase(mechanism.className) + '.' + mechanism.className;
if (fullClassName === mechanismInRobot.className) {
return mechanism;
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/reactComponents/AddTabDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import * as Antd from 'antd';
import * as I18Next from 'react-i18next';
import * as React from 'react';
import * as commonStorage from '../storage/common_storage';
import ModuleNameComponent from './ModuleNameComponent';
import ClassNameComponent from './ClassNameComponent';

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

<ModuleNameComponent
<ClassNameComponent
tabType={tabType}
newItemName={newItemName}
setNewItemName={setNewItemName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import * as I18Next from 'react-i18next';
import * as React from 'react';
import * as commonStorage from '../storage/common_storage';

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

/**
* Component for entering and validating module names.
* Component for entering and validating class names.
* Provides input validation, error display, and automatic capitalization.
*/
export default function ModuleNameComponent(props: ModuleNameComponentProps): React.JSX.Element {
export default function ClassNameComponent(props: ClassNameComponentProps): React.JSX.Element {
const {t} = I18Next.useTranslation();
const [alertErrorMessage, setAlertErrorMessage] = React.useState('');
const [alertErrorVisible, setAlertErrorVisible] = React.useState(false);
Expand Down
8 changes: 4 additions & 4 deletions src/reactComponents/FileManageModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import * as I18Next from 'react-i18next';
import * as React from 'react';
import * as commonStorage from '../storage/common_storage';
import {EditOutlined, DeleteOutlined, CopyOutlined} from '@ant-design/icons';
import ModuleNameComponent from './ModuleNameComponent';
import ClassNameComponent from './ClassNameComponent';

/** Represents a module in the file management system. */
interface Module {
Expand Down Expand Up @@ -347,7 +347,7 @@ export default function FileManageModal(props: FileManageModalProps) {
cancelText={t('Cancel')}
>
{currentRecord && (
<ModuleNameComponent
<ClassNameComponent
tabType={currentRecord.type}
newItemName={name}
setNewItemName={setName}
Expand Down Expand Up @@ -376,7 +376,7 @@ export default function FileManageModal(props: FileManageModalProps) {
cancelText={t('Cancel')}
>
{currentRecord && (
<ModuleNameComponent
<ClassNameComponent
tabType={currentRecord.type}
newItemName={name}
setNewItemName={setName}
Expand Down Expand Up @@ -409,7 +409,7 @@ export default function FileManageModal(props: FileManageModalProps) {
borderRadius: '6px',
padding: '12px',
}}>
<ModuleNameComponent
<ClassNameComponent
tabType={props.moduleType}
newItemName={newItemName}
setNewItemName={setNewItemName}
Expand Down
2 changes: 1 addition & 1 deletion src/reactComponents/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function Header(props: HeaderProps): React.JSX.Element {

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

return (
Expand Down
103 changes: 103 additions & 0 deletions src/reactComponents/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*/
import * as Antd from 'antd';
import * as React from 'react';
import { RcFile, UploadRequestOption } from 'rc-upload/lib/interface';
import * as commonStorage from '../storage/common_storage';
import * as createPythonFiles from '../storage/create_python_files';
import * as I18Next from 'react-i18next';
Expand All @@ -38,6 +39,8 @@ import {
BgColorsOutlined,
GlobalOutlined,
CheckOutlined,
DownloadOutlined,
UploadOutlined,
} from '@ant-design/icons';
import FileManageModal from './FileManageModal';
import ProjectManageModal from './ProjectManageModal';
Expand Down Expand Up @@ -180,6 +183,7 @@ export function Component(props: MenuProps): React.JSX.Element {
const [noProjects, setNoProjects] = React.useState<boolean>(false);
const [aboutDialogVisible, setAboutDialogVisible] = React.useState<boolean>(false);
const [themeModalOpen, setThemeModalOpen] = React.useState<boolean>(false);
const [showUploadAndDownload, _setShowUploadAndDownload] = React.useState(false);

const handleThemeChange = (newTheme: string) => {
props.setTheme(newTheme);
Expand Down Expand Up @@ -322,6 +326,81 @@ export function Component(props: MenuProps): React.JSX.Element {
}
};

// TODO: Add UI for the download action.
/** Handles the download action to generate and download json files. */
const handleDownload = async (): Promise<void> => {
if (!props.project || !props.storage) {
return;
}

try {
const blobUrl = await props.storage.downloadProject(props.project.projectName);
const filename = props.project.projectName + commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION;

// Create a temporary link to download the file
const link = document.createElement('a');
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);

// Clean up the blob URL
URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error('Failed to download project:', error);
props.setAlertErrorMessage(t('DOWNLOAD_FAILED') || 'Failed to download project');
}
}

// TODO: Add UI for the upload action.
/** Handles the upload action to upload a previously downloaded project. */
const handleUpload = (): Antd.UploadProps | null => {
if (!props.storage) {
return null;
}

const uploadProps: Antd.UploadProps = {
accept: commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION,
beforeUpload: (file) => {
const isBlocks = file.name.endsWith(commonStorage.UPLOAD_DOWNLOAD_FILE_EXTENSION)
if (!isBlocks) {
// TODO: i18n
props.setAlertErrorMessage(file.name + ' is not a blocks file');
return false;
}
return isBlocks || Antd.Upload.LIST_IGNORE;
},
onChange: (_info) => {
},
customRequest: (options: UploadRequestOption) => {
const reader = new FileReader();
reader.onload = (event) => {
if (!event.target) {
return;
}
const dataUrl = event.target.result as string;
const existingProjectNames: string[] = [];
projects.forEach(project => {
existingProjectNames.push(project.projectName);
});
const file = options.file as RcFile;
const uploadProjectName = commonStorage.makeUploadProjectName(file.name, existingProjectNames);
if (props.storage) {
props.storage.uploadProject(uploadProjectName, dataUrl);
}
};
reader.onerror = (_error) => {
console.log('Error reading file: ' + reader.error);
// TODO: i18n
props.setAlertErrorMessage(t('UPLOAD_FAILED') || 'Failed to upload project');
};
reader.readAsDataURL(options.file as Blob);
},
};
return uploadProps;
};

/** Handles closing the file management modal. */
const handleFileModalClose = (): void => {
console.log('Modal onCancel called');
Expand Down Expand Up @@ -382,6 +461,30 @@ export function Component(props: MenuProps): React.JSX.Element {
items={menuItems}
onClick={handleClick}
/>
{showUploadAndDownload ? (
<div>
<Antd.Upload
{...handleUpload()}
showUploadList={false}
>
<Antd.Button
icon={<UploadOutlined />}
size="small"
style={{ color: 'white' }}
/>
</Antd.Upload>
<Antd.Button
icon={<DownloadOutlined />}
size="small"
disabled={!props.project}
onClick={handleDownload}
style={{ color: 'white' }}
/>
</div>
) : (
<div>
</div>
)}
<AboutDialog
visible={aboutDialogVisible}
onClose={() => setAboutDialogVisible(false)}
Expand Down
18 changes: 9 additions & 9 deletions src/reactComponents/ProjectManageModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
const projects = await storage.listProjects();

// Sort projects alphabetically by name
projects.sort((a, b) => a.userVisibleName.localeCompare(b.userVisibleName));
projects.sort((a, b) => a.projectName.localeCompare(b.projectName));
setAllProjects(projects);

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

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

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

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

/** Creates the container style object. */
Expand All @@ -224,8 +224,8 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
const columns: Antd.TableProps<commonStorage.Project>['columns'] = [
{
title: 'Name',
dataIndex: 'userVisibleName',
key: 'userVisibleName',
dataIndex: 'projectName',
key: 'projectName',
ellipsis: {
showTitle: false,
},
Expand Down Expand Up @@ -268,7 +268,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
{allProjects.length > 1 && (
<Antd.Tooltip title={t('Delete')}>
<Antd.Popconfirm
title={`Delete ${record.userVisibleName}?`}
title={`Delete ${record.projectName}?`}
description="This action cannot be undone."
onConfirm={() => handleDeleteProject(record)}
okText={t('Delete')}
Expand Down Expand Up @@ -385,7 +385,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac
<Antd.Table<commonStorage.Project>
columns={columns}
dataSource={allProjects}
rowKey="userVisibleName"
rowKey="projectName"
size="small"
pagination={allProjects.length > DEFAULT_PAGE_SIZE ? {
pageSize: DEFAULT_PAGE_SIZE,
Expand Down
12 changes: 6 additions & 6 deletions src/reactComponents/ProjectNameComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,18 @@ export default function ProjectNameComponent(props: ProjectNameComponentProps):

/** Handles adding a new item with validation. */
const handleAddNewItem = (): void => {
const newUserVisibleName = props.newItemName.trim();
if (!newUserVisibleName || !props.projects) {
const newProjectName = props.newItemName.trim();
if (!newProjectName || !props.projects) {
return;
}

if (!commonStorage.isValidClassName(newUserVisibleName)) {
showError(newUserVisibleName + INVALID_NAME_MESSAGE_SUFFIX);
if (!commonStorage.isValidClassName(newProjectName)) {
showError(newProjectName + INVALID_NAME_MESSAGE_SUFFIX);
return;
}

if (props.projects.some((project) => project.userVisibleName === newUserVisibleName)) {
showError(DUPLICATE_NAME_MESSAGE_PREFIX + newUserVisibleName + DUPLICATE_NAME_MESSAGE_SUFFIX);
if (props.projects.some((project) => project.projectName === newProjectName)) {
showError(DUPLICATE_NAME_MESSAGE_PREFIX + newProjectName + DUPLICATE_NAME_MESSAGE_SUFFIX);
return;
}

Expand Down
6 changes: 3 additions & 3 deletions src/reactComponents/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
CloseCircleOutlined,
} from '@ant-design/icons';
import AddTabDialog from './AddTabDialog';
import ModuleNameComponent from './ModuleNameComponent';
import ClassNameComponent from './ClassNameComponent';
import { TabType, TabTypeUtils } from '../types/TabType';

/** Represents a tab item in the tab bar. */
Expand Down Expand Up @@ -375,7 +375,7 @@ export function Component(props: TabsProps): React.JSX.Element {
cancelText={t('Cancel')}
>
{currentTab && (
<ModuleNameComponent
<ClassNameComponent
tabType={currentTab.type}
newItemName={name}
setNewItemName={setName}
Expand Down Expand Up @@ -405,7 +405,7 @@ export function Component(props: TabsProps): React.JSX.Element {
cancelText={t('Cancel')}
>
{currentTab && (
<ModuleNameComponent
<ClassNameComponent
tabType={currentTab.type}
newItemName={name}
setNewItemName={setName}
Expand Down
Loading