1+ import { useRef , useState } from 'react' ;
2+ import { useTranslation } from 'react-i18next' ;
3+ import DialogFooter from './dialog-footer' ;
4+ import { Constants } from '@/utils/constants' ;
5+ import { ListItem } from '@/utils/types' ;
6+ import pythonicon from '@assets/images/python-svgrepo-com.svg' ;
7+ import blockIcon from '@assets/images/blockly.svg' ;
8+ import fileIcon from '@assets/images/file.svg' ;
9+ import { TiArrowSortedDown } from 'react-icons/ti' ;
10+ import { FiCheckSquare } from 'react-icons/fi' ;
11+ import AppMgr from '@/managers/appmgr' ;
12+
13+ type CreateNodeDlgProps = {
14+ type : 'internal' | 'leaf' ;
15+ parentPath : string ;
16+ onConfirm : ( name : string ) => void ;
17+ onCancel : ( ) => void ;
18+ } ;
19+
20+ function CreateNodeDlg ( { type, parentPath, onConfirm, onCancel } : CreateNodeDlgProps ) {
21+ const { t } = useTranslation ( ) ;
22+ const [ error , setError ] = useState < string | null > ( null ) ;
23+ const [ name , setName ] = useState ( '' ) ;
24+ const [ isFileExists , setIsFileExists ] = useState ( false ) ;
25+ const [ isOkayToSubmit , setIsOkayToSubmit ] = useState ( false ) ;
26+ const [ filename , setFilename ] = useState ( '' ) ;
27+ const [ filetype , setFileType ] = useState < number | null > ( null ) ;
28+ const [ isPopoverOpen , setIsPopoverOpen ] = useState ( false ) ;
29+ const [ selectedOption , setSelectedOption ] = useState < ListItem | null > ( null ) ;
30+ const popoverRef = useRef < HTMLDivElement > ( null ) ;
31+
32+ /**
33+ * filetype options
34+ */
35+ const fileOptions : ListItem [ ] = [
36+ {
37+ label : t ( 'blocklyfile' ) ,
38+ image : blockIcon
39+ } ,
40+ {
41+ label : t ( 'pythonfile' ) ,
42+ image : pythonicon
43+ } ,
44+ {
45+ label : t ( 'other' ) ,
46+ image : fileIcon
47+ } ,
48+ ] ;
49+
50+ /**
51+ * Toggle popover open/close
52+ */
53+ const togglePopover = ( ) => {
54+ if ( parentPath !== '' ) {
55+ setIsPopoverOpen ( ! isPopoverOpen ) ;
56+ }
57+ } ;
58+
59+ /**
60+ * Handle file type option selection
61+ */
62+ const handleOptionSelect = ( option : ListItem , index : number ) => {
63+ setSelectedOption ( option ) ;
64+ // Map index to filetype: 0 = Blockly (filetype 1), 1 = Python (filetype 0), 2 = Other (filetype 0)
65+ const filetypeValue = index === 0 ? 1 : 0 ;
66+ setFileType ( filetypeValue ) ;
67+ setIsPopoverOpen ( false ) ;
68+ // Re-validate filename if it exists
69+ if ( filename ) {
70+ const filenameWithExt = filename + ( filetypeValue === 1 ? '.blocks' : '.py' ) ;
71+ const isValid = Constants . REGEX_FILENAME . test ( filenameWithExt ) ;
72+ const parts = parentPath . split ( '/' ) . filter ( ( part ) => part !== '' ) ;
73+ const foldername = parts [ parts . length - 1 ] ;
74+ if ( ! isValid || AppMgr . getInstance ( ) . IsFileExists ( foldername , filenameWithExt ) ) {
75+ setIsFileExists ( true ) ;
76+ setIsOkayToSubmit ( false ) ;
77+ } else {
78+ setIsFileExists ( false ) ;
79+ setIsOkayToSubmit ( true ) ;
80+ }
81+ }
82+ } ;
83+
84+ /**
85+ * handleNameInput - handles the filename input from user
86+ * @param e
87+ * @returns
88+ */
89+ const handleNameInput = ( e : React . ChangeEvent < HTMLInputElement > ) => {
90+ setFilename ( e . target . value ) ;
91+ const filename = e . target . value + ( type === 'leaf' ? ( filetype === 1 ? '.blocks' : '.py' ) : '' ) ;
92+ setName ( filename ) ;
93+ const isValid = ( type === 'leaf' ) ? Constants . REGEX_FILENAME . test ( filename ) : Constants . REGEX_DIRNAME . test ( filename ) ;
94+ const parts = parentPath . split ( '/' ) . filter ( ( part ) => part !== '' ) ;
95+ const foldername = parts . length > 0 ? parts [ parts . length - 1 ] : parentPath ;
96+ if ( ! isValid || AppMgr . getInstance ( ) . IsFileExists ( foldername , filename ) ) {
97+ setIsFileExists ( true ) ;
98+ setIsOkayToSubmit ( false ) ;
99+ return ;
100+ } else {
101+ setIsFileExists ( false ) ;
102+ setIsOkayToSubmit ( true ) ;
103+ }
104+ } ;
105+
106+ /**
107+ * handleConfirm - handle confirm action
108+ * @returns
109+ */
110+ const handleConfirm = ( ) => {
111+ if ( ! name . trim ( ) ) {
112+ setError ( t ( 'filename-required' ) || 'Filename is required' ) ;
113+ return ;
114+ }
115+ onConfirm ( name ) ;
116+ } ;
117+
118+ return (
119+ < div className = "flex flex-col items-center gap-4 rounded-md border border-mountain-mist-700 p-8 shadow-md transition-all dark:border-shark-500 dark:bg-shark-950" >
120+ < div className = "flex w-[90%] flex-col items-center" >
121+ < h1 className = "text-lg font-bold text-mountain-mist-700 dark:text-mountain-mist-300" > { type === 'internal' ? t ( 'newFolder' ) : t ( 'newFile' ) } </ h1 >
122+ < p className = "text-sm text-mountain-mist-700 dark:text-mountain-mist-300" > { type === 'internal' ? t ( 'chooseNewFolder' ) : t ( 'chooseNewFile2' ) } </ p >
123+ </ div >
124+ < hr className = "w-full border-mountain-mist-600" />
125+ < form id = "fileOptionId" className = "flex w-full flex-col gap-2" >
126+ { type === 'leaf' && (
127+ < div className = "flex flex-col gap-1 w-full" >
128+ < label className = "text-sm text-mountain-mist-700 dark:text-mountain-mist-300" htmlFor = "filesId" >
129+ { t ( 'fileType' ) }
130+ </ label >
131+ < div className = 'relative w-full' ref = { popoverRef } >
132+ < button
133+ type = 'button'
134+ name = 'filetypeSelecion'
135+ onClick = { togglePopover }
136+ disabled = { parentPath === '' }
137+ className = { `relative h-10 w-full p-2 text-left border rounded-md shadow-md cursor-default focus:outline-none focus:ring-2 focus:ring-curious-blue-400 dark:border-shark-600 dark:bg-shark-500 ${
138+ parentPath === ''
139+ ? 'border-shark-300 bg-shark-100 opacity-50 cursor-not-allowed'
140+ : 'border-shark-300 dark:border-shark-600 bg-white dark:bg-shark-500'
141+ } `}
142+ aria-haspopup = "listbox"
143+ aria-expanded = { isPopoverOpen }
144+ aria-labelledby = 'listbox-label'
145+ >
146+ < span className = 'flex items-center' >
147+ { selectedOption ? (
148+ < span className = 'flex items-center gap-2' >
149+ < img className = 'h-5 w-5' src = { selectedOption . image } alt = { selectedOption . label } />
150+ < span className = 'text-sm text-mountain-mist-700 dark:text-mountain-mist-200' > { selectedOption . label } </ span >
151+ </ span >
152+ ) : (
153+ < span className = 'text-sm text-mountain-mist-700 dark:text-mountain-mist-300' > { t ( 'files' ) } </ span >
154+ ) }
155+ </ span >
156+ < span className = 'absolute inset-y-0 right-0 flex pr-3 items-center pointer-events-none' >
157+ < TiArrowSortedDown className = { `transition-transform ${ isPopoverOpen ? 'rotate-180' : '' } text-mountain-mist-700 dark:text-mountain-mist-300` } />
158+ </ span >
159+ </ button >
160+ { isPopoverOpen && (
161+ < ul
162+ className = 'absolute z-[200] bg-mountain-mist-50 dark:bg-shark-700 w-full mt-1 py-1 overflow-auto text-base border border-gray-300 dark:border-shark-600 rounded-md shadow-xl max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm'
163+ role = 'listbox'
164+ aria-labelledby = 'listbox-label'
165+ >
166+ { fileOptions . map ( ( option , index ) => (
167+ < li
168+ key = { option . label }
169+ className = { `flex flex-row items-center gap-2 px-3 py-2 cursor-pointer hover:bg-curious-blue-100 dark:hover:bg-shark-600 ${
170+ selectedOption ?. label === option . label ? 'bg-curious-blue-50 dark:bg-shark-700' : ''
171+ } `}
172+ tabIndex = { 0 }
173+ role = 'option'
174+ aria-selected = { selectedOption ?. label === option . label }
175+ onClick = { ( ) => handleOptionSelect ( option , index ) }
176+ onKeyDown = { ( e ) => {
177+ if ( e . key === 'Enter' || e . key === ' ' ) {
178+ e . preventDefault ( ) ;
179+ handleOptionSelect ( option , index ) ;
180+ }
181+ } }
182+ >
183+ < img className = 'h-5 w-5' src = { option . image } alt = { option . label } />
184+ < span className = 'text-sm text-mountain-mist-700 dark:text-mountain-mist-200 flex-1' > { option . label } </ span >
185+ { selectedOption ?. label === option . label && (
186+ < span className = "flex items-center pr-2 text-curious-blue-600 dark:text-curious-blue-400" >
187+ < FiCheckSquare />
188+ </ span >
189+ ) }
190+ </ li >
191+ ) ) }
192+ </ ul >
193+ ) }
194+ </ div >
195+ </ div >
196+ ) }
197+ < span className = "text-sm text-mountain-mist-700 dark:text-mountain-mist-300" > { ( type === 'leaf' ) ? t ( 'filename' ) : t ( 'foldername' ) } </ span >
198+ < div className = "flex flex-col items-center gap-1" >
199+ < input
200+ className = { `w-full rounded border ${ isFileExists ? 'border-cinnabar-800' : 'border-shark-300 dark:border-shark-600' } p-2 text-md text-mountain-mist-700 dark:bg-shark-500 dark:text-mountain-mist-200 dark:placeholder-mountain-mist-200` }
201+ id = "filenameId"
202+ type = "text"
203+ placeholder = { ( type === 'leaf' ) ? t ( 'enterFilename' ) : t ( 'enterFoldername' ) }
204+ required
205+ minLength = { 2 }
206+ value = { filename }
207+ onChange = { handleNameInput }
208+ disabled = { ( type === 'leaf' ) ? filetype === null || parentPath === '' : false }
209+ />
210+ { isFileExists && (
211+ < span className = "text-sm text-cinnabar-800" > { t ( 'fileExists' ) } </ span >
212+ ) }
213+ </ div >
214+ < span className = "text-mountain-mist-700 text-sm dark:text-mountain-mist-300" >
215+ { t ( 'final-path' ) }
216+ { parentPath } { filename } { ( type === 'leaf' ) ? ( filetype === 1 ? '.blocks' : '.py' ) : '' }
217+ </ span >
218+ { error && < span className = "text-cinnabar-500 text-sm" > { error } </ span > }
219+ </ form >
220+ < hr className = "w-full border-mountain-mist-600" />
221+ < DialogFooter
222+ disabledAccept = { ! isOkayToSubmit }
223+ btnAcceptLabel = { t ( 'create' ) || 'Create' }
224+ btnAcceptCallback = { handleConfirm }
225+ btnCancelCallback = { onCancel }
226+ />
227+ </ div >
228+ ) ;
229+ }
230+
231+ export default CreateNodeDlg ;
0 commit comments