Skip to content

Commit 33745ac

Browse files
authored
Pr upload download (#373)
* Add download/upload functionality * Fix naming conflict * Changed to respect theme * Make the UI update when a new project is uploaded * Updated readme to no longer say we don't have upload/download * Make sure we update list of projects when project is deleted * Address PR comments * Remove no longer needed method makeUploadProjectName
1 parent 277c583 commit 33745ac

File tree

6 files changed

+137
-78
lines changed

6 files changed

+137
-78
lines changed

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,4 @@ WARNING! This is not ready for use and is under heavy development of basic featu
1212
7. Make sure any python backends are running on your PC.
1313

1414
## Known Issues
15-
In addition to those listed in the issues on github, there are some major pieces not ready yet.
16-
1. Deploy doesn't connect to backend
17-
1. No ability to download or upload blocks code
15+
Please check the issues on github for known issues.

src/i18n/locales/en/translation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
"DOWNLOAD_FAILED": "Failed to download project.",
1515
"UPLOAD_FILE_NOT_BLOCKS": "{{filename}} is not a blocks file.",
1616
"UPLOAD_FAILED": "Failed to upload project.",
17+
"PROJECT_NAME_CONFLICT": "Project Name Conflict",
18+
"PROJECT_NAME_EXISTS": "A project named '{{projectName}}' already exists. Please choose a different name.",
19+
"UPLOAD_SUCCESS": "Project uploaded successfully as '{{projectName}}'.",
1720
"MECHANISM": "Mechanism",
1821
"OPMODE": "OpMode",
1922
"class_rule_description": "No spaces are allowed in the name. Each word in the name should start with a capital letter.",
@@ -28,6 +31,8 @@
2831
"PROJECTS": "Projects",
2932
"SAVE": "Save",
3033
"DEPLOY": "Deploy",
34+
"DOWNLOAD": "Download",
35+
"UPLOAD": "Upload",
3136
"MANAGE": "Manage",
3237
"EXPLORER": "Explorer",
3338
"ROBOT": "Robot",

src/i18n/locales/es/translation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"DOWNLOAD_FAILED": "Error al descargar el proyecto.",
1616
"UPLOAD_FILE_NOT_BLOCKS": "{{filename}} no es un archivo de bloques.",
1717
"UPLOAD_FAILED": "Error al cargar el proyecto.",
18+
"PROJECT_NAME_CONFLICT": "Conflicto de Nombre de Proyecto",
19+
"PROJECT_NAME_EXISTS": "Ya existe un proyecto llamado '{{projectName}}'. Por favor, elija un nombre diferente.",
20+
"UPLOAD_SUCCESS": "Proyecto cargado exitosamente como '{{projectName}}'.",
1821
"MECHANISM": "Mecanismo",
1922
"OPMODE": "OpMode",
2023
"class_rule_description": "No se permiten espacios en el nombre. Cada palabra en el nombre debe comenzar con una letra mayúscula.",
@@ -25,6 +28,8 @@
2528
"PROJECTS": "Proyectos",
2629
"SAVE": "Guardar",
2730
"DEPLOY": "Desplegar",
31+
"DOWNLOAD": "Descargar",
32+
"UPLOAD": "Cargar",
2833
"MANAGE": "Gestionar",
2934
"EXPLORER": "Explorador",
3035
"ROBOT": "Robot",

src/i18n/locales/he/translation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
"DOWNLOAD_FAILED": "הורדת הפרויקט נכשלה.",
1515
"UPLOAD_FILE_NOT_BLOCKS": "{{filename}} אינו קובץ בלוקים.",
1616
"UPLOAD_FAILED": "העלאת הפרויקט נכשלה.",
17+
"PROJECT_NAME_CONFLICT": "התנגשות בשם הפרויקט",
18+
"PROJECT_NAME_EXISTS": "פרויקט בשם '{{projectName}}' כבר קיים. אנא בחר שם אחר.",
19+
"UPLOAD_SUCCESS": "הפרויקט הועלה בהצלחה בשם '{{projectName}}'.",
1720
"MECHANISM": "מנגנון",
1821
"OPMODE": "אופמוד",
1922
"class_rule_description": "אסור שיהיו רווחים בשם. כל מילה בשם צריכה להתחיל באות גדולה.",
@@ -28,6 +31,8 @@
2831
"PROJECTS": "פרויקטים",
2932
"SAVE": "שמור",
3033
"DEPLOY": "העלה לרובוט",
34+
"DOWNLOAD": "הורד",
35+
"UPLOAD": "העלה",
3136
"MANAGE": "ניהול",
3237
"EXPLORER": "סייר",
3338
"ROBOT": "רובוט",

src/reactComponents/Menu.tsx

Lines changed: 121 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ function getMenuItems(t: (key: string) => string, project: storageProject.Projec
123123
getItem(t('PROJECT'), 'project', <FolderOutlined />, [
124124
getItem(t('SAVE'), 'save', <SaveOutlined />),
125125
getItem(t('DEPLOY'), 'deploy'),
126+
getItem(t('DOWNLOAD'), 'download', <DownloadOutlined />),
127+
getItem(t('UPLOAD') + '...', 'upload', <UploadOutlined />),
126128
]),
127129
getItem(t('MANAGE'), 'manage', <ControlOutlined />, [
128130
getItem(t('PROJECTS') + '...', 'manageProjects', <FolderOutlined />),
@@ -167,6 +169,7 @@ function getMenuItems(t: (key: string) => string, project: storageProject.Projec
167169
*/
168170
export function Component(props: MenuProps): React.JSX.Element {
169171
const {t, i18n} = I18Next.useTranslation();
172+
const [modal, contextHolder] = Antd.Modal.useModal();
170173

171174
const [projectNames, setProjectNames] = React.useState<string[]>([]);
172175
const [menuItems, setMenuItems] = React.useState<MenuItem[]>([]);
@@ -176,7 +179,6 @@ export function Component(props: MenuProps): React.JSX.Element {
176179
const [noProjects, setNoProjects] = React.useState<boolean>(false);
177180
const [aboutDialogVisible, setAboutDialogVisible] = React.useState<boolean>(false);
178181
const [themeModalOpen, setThemeModalOpen] = React.useState<boolean>(false);
179-
const [showUploadAndDownload, _setShowUploadAndDownload] = React.useState(false);
180182

181183
const handleThemeChange = (newTheme: string) => {
182184
props.setTheme(newTheme);
@@ -276,6 +278,10 @@ export function Component(props: MenuProps): React.JSX.Element {
276278
handleDeploy();
277279
} else if (key == 'save') {
278280
handleSave();
281+
} else if (key == 'download') {
282+
handleDownload();
283+
} else if (key == 'upload') {
284+
handleUpload();
279285
} else if (key.startsWith('setlang:')) {
280286
const lang = key.split(':')[1];
281287
i18n.changeLanguage(lang);
@@ -356,7 +362,6 @@ export function Component(props: MenuProps): React.JSX.Element {
356362
}
357363
};
358364

359-
// TODO: Add UI for the download action.
360365
/** Handles the download action to generate and download json files. */
361366
const handleDownload = async (): Promise<void> => {
362367
if (!props.currentProject) {
@@ -387,56 +392,126 @@ export function Component(props: MenuProps): React.JSX.Element {
387392
}
388393
}
389394

390-
// TODO: Add UI for the upload action.
391395
/** Handles the upload action to upload a previously downloaded project. */
392-
const handleUpload = (): Antd.UploadProps | null => {
393-
if (!props.storage) {
394-
return null;
395-
}
396-
397-
const uploadProps: Antd.UploadProps = {
398-
accept: storageNames.UPLOAD_DOWNLOAD_FILE_EXTENSION,
399-
beforeUpload: (file) => {
400-
const isBlocks = file.name.endsWith(storageNames.UPLOAD_DOWNLOAD_FILE_EXTENSION)
401-
if (!isBlocks) {
402-
// TODO: i18n
403-
props.setAlertErrorMessage(t('UPLOAD_FILE_NOT_BLOCKS', { filename: file.name }));
404-
return false;
405-
}
406-
return isBlocks || Antd.Upload.LIST_IGNORE;
407-
},
408-
onChange: (_info) => {
409-
},
410-
customRequest: (options) => {
396+
const handleUpload = (): void => {
397+
const input = document.createElement('input');
398+
input.type = 'file';
399+
input.accept = storageNames.UPLOAD_DOWNLOAD_FILE_EXTENSION;
400+
401+
input.onchange = async (e: Event) => {
402+
const target = e.target as HTMLInputElement;
403+
const file = target.files?.[0];
404+
405+
if (!file) {
406+
return;
407+
}
408+
409+
const isBlocks = file.name.endsWith(storageNames.UPLOAD_DOWNLOAD_FILE_EXTENSION);
410+
if (!isBlocks) {
411+
props.setAlertErrorMessage(t('UPLOAD_FILE_NOT_BLOCKS', { filename: file.name }));
412+
return;
413+
}
414+
415+
try {
411416
const reader = new FileReader();
412-
reader.onload = (event) => {
413-
if (!event.target) {
417+
reader.onload = async (event) => {
418+
if (!event.target || !props.storage) {
414419
return;
415420
}
421+
416422
const dataUrl = event.target.result as string;
417-
const existingProjectNames: string[] = [];
418-
projectNames.forEach(projectName => {
419-
existingProjectNames.push(projectName);
420-
});
421-
const file = options.file as File;
422-
const uploadProjectName = storageProject.makeUploadProjectName(file.name, existingProjectNames);
423-
if (props.storage) {
424-
storageProject.uploadProject(props.storage, uploadProjectName, dataUrl);
423+
const existingProjectNames: string[] = projectNames;
424+
425+
// Generate the initial project name
426+
const preferredName = file.name.substring(
427+
0, file.name.length - storageNames.UPLOAD_DOWNLOAD_FILE_EXTENSION.length);
428+
429+
// Smart name conflict resolution: extract base name and trailing number
430+
// e.g., "PrSimplify2" -> base="PrSimplify", num=2
431+
const match = preferredName.match(/^(.+?)(\d+)$/);
432+
let uploadProjectName: string;
433+
434+
if (match && existingProjectNames.includes(preferredName)) {
435+
// Name has a trailing number and conflicts - find next available
436+
const baseName = match[1];
437+
const startNum = parseInt(match[2], 10);
438+
let num = startNum + 1;
439+
while (existingProjectNames.includes(baseName + num)) {
440+
num++;
441+
}
442+
uploadProjectName = baseName + num;
443+
} else {
444+
// No trailing number or no conflict - use standard logic
445+
uploadProjectName = storageNames.makeUniqueName(preferredName, existingProjectNames);
425446
}
426-
if (options.onSuccess) {
427-
options.onSuccess(dataUrl);
447+
448+
// Check if there's a conflict (meaning we had to change the name)
449+
if (existingProjectNames.includes(preferredName)) {
450+
// Show a modal to let the user rename the project
451+
let inputValue = uploadProjectName;
452+
453+
modal.confirm({
454+
title: t('PROJECT_NAME_CONFLICT'),
455+
content: (
456+
<div>
457+
<p>{t('PROJECT_NAME_EXISTS', { projectName: preferredName })}</p>
458+
<Antd.Input
459+
defaultValue={uploadProjectName}
460+
onChange={(e) => {
461+
inputValue = e.target.value;
462+
}}
463+
/>
464+
</div>
465+
),
466+
okText: t('UPLOAD'),
467+
cancelText: t('CANCEL'),
468+
onOk: async () => {
469+
try {
470+
if (props.storage) {
471+
await storageProject.uploadProject(props.storage, inputValue, dataUrl);
472+
await fetchListOfProjectNames();
473+
const project = await storageProject.fetchProject(props.storage, inputValue);
474+
props.setCurrentProject(project);
475+
await props.onProjectChanged();
476+
Antd.message.success(t('UPLOAD_SUCCESS', { projectName: inputValue }));
477+
}
478+
} catch (error) {
479+
console.error('Error uploading file:', error);
480+
props.setAlertErrorMessage(t('UPLOAD_FAILED'));
481+
}
482+
},
483+
});
484+
} else {
485+
// No conflict, upload directly
486+
try {
487+
if (props.storage) {
488+
await storageProject.uploadProject(props.storage, uploadProjectName, dataUrl);
489+
await fetchListOfProjectNames();
490+
const project = await storageProject.fetchProject(props.storage, uploadProjectName);
491+
props.setCurrentProject(project);
492+
await props.onProjectChanged();
493+
Antd.message.success(t('UPLOAD_SUCCESS', { projectName: uploadProjectName }));
494+
}
495+
} catch (error) {
496+
console.error('Error uploading file:', error);
497+
props.setAlertErrorMessage(t('UPLOAD_FAILED'));
498+
}
428499
}
429500
};
501+
430502
reader.onerror = (_error) => {
431503
console.log('Error reading file: ' + reader.error);
432-
if (options.onError) {
433-
options.onError(new Error(t('UPLOAD_FAILED')));
434-
}
504+
props.setAlertErrorMessage(t('UPLOAD_FAILED'));
435505
};
436-
reader.readAsDataURL(options.file as Blob);
437-
},
506+
507+
reader.readAsDataURL(file);
508+
} catch (error) {
509+
console.error('Error handling upload:', error);
510+
props.setAlertErrorMessage(t('UPLOAD_FAILED'));
511+
}
438512
};
439-
return uploadProps;
513+
514+
input.click();
440515
};
441516

442517
/** Handles closing the file management modal. */
@@ -445,8 +520,12 @@ export function Component(props: MenuProps): React.JSX.Element {
445520
};
446521

447522
/** Handles closing the project management modal. */
448-
const handleProjectModalClose = (): void => {
523+
const handleProjectModalClose = async (): Promise<void> => {
449524
setProjectModalOpen(false);
525+
// Refresh project names to reflect any changes (deletions, renames, etc.)
526+
if (props.storage) {
527+
await fetchListOfProjectNames();
528+
}
450529
};
451530

452531
// Initialize project names when storage is available
@@ -473,6 +552,7 @@ export function Component(props: MenuProps): React.JSX.Element {
473552

474553
return (
475554
<>
555+
{contextHolder}
476556
<FileManageModal
477557
isOpen={fileModalOpen}
478558
onClose={handleFileModalClose}
@@ -500,30 +580,6 @@ export function Component(props: MenuProps): React.JSX.Element {
500580
items={menuItems}
501581
onClick={handleClick}
502582
/>
503-
{showUploadAndDownload ? (
504-
<div>
505-
<Antd.Upload
506-
{...handleUpload()}
507-
showUploadList={false}
508-
>
509-
<Antd.Button
510-
icon={<UploadOutlined />}
511-
size="small"
512-
style={{ color: 'white' }}
513-
/>
514-
</Antd.Upload>
515-
<Antd.Button
516-
icon={<DownloadOutlined />}
517-
size="small"
518-
disabled={!props.currentProject}
519-
onClick={handleDownload}
520-
style={{ color: 'white' }}
521-
/>
522-
</div>
523-
) : (
524-
<div>
525-
</div>
526-
)}
527583
<AboutDialog
528584
open={aboutDialogVisible}
529585
onClose={() => setAboutDialogVisible(false)}

src/storage/project.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -382,16 +382,6 @@ export async function downloadProject(
382382
return URL.createObjectURL(content);
383383
}
384384

385-
/**
386-
* Make a unique project name for an uploaded project.
387-
*/
388-
export function makeUploadProjectName(
389-
uploadFileName: string, existingProjectNames: string[]): string {
390-
const preferredName = uploadFileName.substring(
391-
0, uploadFileName.length - storageNames.UPLOAD_DOWNLOAD_FILE_EXTENSION.length);
392-
return storageNames.makeUniqueName(preferredName, existingProjectNames);
393-
}
394-
395385
export async function uploadProject(
396386
storage: commonStorage.Storage, projectName: string, blobUrl: string): Promise<void> {
397387
// Process the uploaded blob to get the file names and contents.

0 commit comments

Comments
 (0)