Skip to content

Commit 23de0a8

Browse files
authored
Merge pull request #201 from Open-STEM/kq-bugs
Kq bugs
2 parents a5c3445 + abe8007 commit 23de0a8

6 files changed

Lines changed: 314 additions & 30 deletions

File tree

src/components/blockly.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ function BlocklyEditor({ tabId, tabName }: BlocklyEditorProps) {
153153
const [isListenerSet, setIsListenerSet] = useState(false);
154154
const [name, setName] = useState<string>(tabName);
155155
const nameRef = useRef(name);
156+
const isLoadingRef = useRef(isLoading);
157+
158+
useEffect(() => {
159+
isLoadingRef.current = isLoading;
160+
}, [isLoading]);
156161

157162
/**
158163
* handleOnInject
@@ -330,10 +335,10 @@ function BlocklyEditor({ tabId, tabName }: BlocklyEditorProps) {
330335
setIsLoading(false);
331336
return;
332337
}
333-
if (isLoading &&
334-
event.type === Blockly.Events.BLOCK_CREATE ||
338+
if (isLoadingRef.current &&
339+
(event.type === Blockly.Events.BLOCK_CREATE ||
335340
event.type === Blockly.Events.BLOCK_DELETE ||
336-
event.type === Blockly.Events.BLOCK_CHANGE
341+
event.type === Blockly.Events.BLOCK_CHANGE)
337342
) { return; }
338343
if (event.type === Blockly.Events.VIEWPORT_CHANGE || event.isUiEvent) { return; }
339344
try {
@@ -368,7 +373,7 @@ function BlocklyEditor({ tabId, tabName }: BlocklyEditorProps) {
368373
ws.removeChangeListener(listener);
369374
}
370375
}
371-
}, [isListenerSet, isLoading, saveEditor, tabId]);
376+
}, [isListenerSet, saveEditor, tabId]);
372377

373378
return (
374379
<BlocklyWorkspace
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 }) => {

0 commit comments

Comments
 (0)