@@ -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 */
168170export 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 ) }
0 commit comments