Skip to content

Commit 0df8aa8

Browse files
committed
Addressed issue 195 & 196
changed path adjustment for XRP and added FS update at the end of onCreated
1 parent a5c3445 commit 0df8aa8

5 files changed

Lines changed: 305 additions & 26 deletions

File tree

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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;

src/components/folder-tree.tsx

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { fireGoogleUserTree, getUsernameFromEmail } from '@/utils/google-utils';
2424
import EditorMgr, { EdSearchParams } from '@/managers/editormgr';
2525
import Login from '@/widgets/login';
2626
import { UserProfile } from '@/services/google-auth';
27+
import CreateNodeDlg from './dialogs/create-node-dlg';
2728

2829
type TreeProps = {
2930
treeData: string | null;
@@ -408,39 +409,43 @@ function FolderTree(treeProps: TreeProps) {
408409
return null;
409410
}
410411

412+
const parentPath = (parentNode?.data.name === '/' || parentNode?.data.name === Constants.XRPCODE)
413+
? parentNode.data.path : (isLogin) ? `${parentNode?.data.path}${parentNode?.data.name}/` :
414+
`${parentNode?.data.path}/${parentNode?.data.name}/`;
415+
416+
// Ask for filename using a promise-based modal
417+
const name = await new Promise<string | null>((resolve) => {
418+
setDialogContent(
419+
<CreateNodeDlg
420+
type={type}
421+
parentPath={parentPath}
422+
onConfirm={(newName) => {
423+
toggleDialog();
424+
resolve(newName);
425+
}}
426+
onCancel={() => {
427+
toggleDialog();
428+
resolve(null);
429+
}}
430+
/>
431+
);
432+
toggleDialog();
433+
});
434+
435+
if (!name) return null;
436+
411437
// Generate a unique ID for the new node
412438
const newId = uniqueId(parentNode?.data.name || `node`);
413439

414440
// Create the new node object
415441
const newNode: FolderItem = {
416442
id: newId,
417-
name: type === 'internal' ? t('newFolder') : t('newFile'),
443+
name: name,
418444
isReadOnly: false,
419445
children: type === 'internal' ? [] : null,
420-
path: `${parentNode?.data.path}/${parentNode?.data.name}/`,
446+
path: parentPath,
421447
};
422448

423-
// Update the tree data
424-
setTreeData((prevTreeData) => {
425-
if (!prevTreeData) return prevTreeData;
426-
427-
const rootNode = prevTreeData.at(0);
428-
if (!rootNode) return prevTreeData;
429-
430-
if (parentNode) {
431-
const found = findItem(rootNode, parentNode.data.id);
432-
if (found) {
433-
found.children = found.children || [];
434-
found.children.splice(index, 0, newNode);
435-
}
436-
} else {
437-
rootNode.children = rootNode.children || [];
438-
rootNode.children.splice(index, 0, newNode);
439-
}
440-
441-
return [...prevTreeData];
442-
});
443-
444449
if (newNode) {
445450
let parentFileId = null;
446451
if (parentNode === null) {
@@ -453,7 +458,7 @@ function FolderTree(treeProps: TreeProps) {
453458
if (isLogin) {
454459
await AppMgr.getInstance().driveService?.createFolder(newNode.name, parentFileId ?? undefined).then((data) => {
455460
newNode.fileId = data?.id;
456-
});
461+
});
457462
} else if (isConnected) {
458463
// create the actual file in XRP
459464
await CommandToXRPMgr.getInstance().buildPath(
@@ -482,7 +487,39 @@ function FolderTree(treeProps: TreeProps) {
482487
}
483488
}
484489

485-
return newNode;
490+
// Update the tree data
491+
setTreeData((prevTreeData) => {
492+
if (!prevTreeData) return prevTreeData;
493+
494+
const rootNode = prevTreeData.at(0);
495+
if (!rootNode) return prevTreeData;
496+
497+
if (parentNode) {
498+
const found = findItem(rootNode, parentNode.data.id);
499+
if (found) {
500+
found.children = found.children || [];
501+
found.children.splice(index, 0, newNode);
502+
}
503+
} else {
504+
rootNode.children = rootNode.children || [];
505+
rootNode.children.splice(index, 0, newNode);
506+
}
507+
508+
return [...prevTreeData];
509+
});
510+
511+
if (isLogin) {
512+
const username = getUsernameFromEmail(AppMgr.getInstance().authService.userProfile.email);
513+
if (username) {
514+
// refresh the Google Drive tree
515+
fireGoogleUserTree(username);
516+
}
517+
} else if (isConnected) {
518+
// refresh the XRP onboard tree
519+
CommandToXRPMgr.getInstance().getOnBoardFSTree();
520+
}
521+
522+
return null;
486523
};
487524

488525
// const onMove = ({ dragIds, dragNodes, parentId, parentNode, index }: { dragIds: string[], dragNodes: NodeApi<FolderItem>[], parentId: string | null, parentNode: NodeApi<FolderItem> | null, index: number }) => {

src/utils/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class Constants {
1717
static readonly CONNECTED = "Connected";
1818
static readonly DISCONNECTED = "Disconnected";
1919
static readonly REGEX_FILENAME = /^[a-zA-Z0-9](?:[a-zA-Z0-9 ._-]*[a-zA-Z0-9])?\.[a-zA-Z0-9_-]+$/;
20+
static readonly REGEX_DIRNAME = /^[a-zA-Z0-9](?:[a-zA-Z0-9 _-]*[a-zA-Z0-9])?$/;
2021
static readonly XRP_PROCESSOR_BETA = "RPI-RP2"
2122
static readonly LASTSAVETIME_CONFG = '1';
2223
}

src/utils/i18n/locales/en/en.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
"user": "User",
4242
"users": "Users",
4343
"filename": "Filename",
44+
"foldername": "Folder Name",
4445
"enterFilename": "Please enter filename",
46+
"enterFoldername": "Please enter folder name",
4547
"files": "Choose a file type",
4648
"submitBtn": "Submit",
4749
"fileType": "File Type",
@@ -62,13 +64,16 @@
6264
"goouserfoldername": "Google User Folder Name",
6365
"chooseFolder": "Choose a Folder",
6466
"chooseNewFile": "Please choose a destination folder, new filename and file type to begin...",
67+
"chooseNewFile2": "Please choose a name to create a new file...",
68+
"chooseNewFolder": "Please choose a name to create a new folder...",
6569
"other": "Other File",
6670
"save": "Save",
6771
"openFolder": "Open the selected folder",
6872
"closeFolder": "Close the selected folder",
6973
"settingDescription": "Application settings can be changed here to your needs!",
7074
"filesToUpload": "Files to Upload",
7175
"fileExists": "Filename is used or invalid, please choose a different name!",
76+
"directoryExists": "Folder name is used or invalid, please choose a different name!",
7277
"power-switch-title": "XRP Power Switch",
7378
"power-switch-alerts": "The power switch on the XRP is not on. Motors and Servos will not work properly. Please turn on the switch before continuing!",
7479
"alert": "Notification",

0 commit comments

Comments
 (0)