From 557512b3dd92893ec668f05ced831b12ec5559ba Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Wed, 15 Oct 2025 19:54:48 -0700 Subject: [PATCH] Fixed issue 279 `Creating a new module causes errors`. App.tsx: Move fetchModules function from inside the useEffect callback for project to outside of it. Added onProjectChanged function that calls fetchModules and waits for it to complete. Pass onProjectChanged to Tabs and Menu components. Tabs.tsx: Added onProjectChanged to TabsProps. Removed setProject. Removed triggerProjectUpdate function. Call await props.onProjectChange() when project is changed (after renameModuleInProject, copyModuleInProject, and removeModuleFromProject). Pass onProjectChange to AddTabDialog component. AddTabDialog.tsx: Added onProjectChanged to AddTabDialogProps. Removed setProject. Call await props.onProjectChange() when project is changed (after addModuleToProject). Menu.tsx: Added onProjectChanged to MenuProps. Pass onProjectChange to FileManageModal component. FileManageModal.tsx: Added onProjectChanged to FileManageModalProps. Removed setProject. Removed triggerProjectUpdate function. Call await props.onProjectChange() when project is changed (after renameModuleInProject, copyModuleInProject, addModuleToProject, and removeModuleFromProject). --- src/App.tsx | 119 ++++++++++++++---------- src/reactComponents/AddTabDialog.tsx | 3 +- src/reactComponents/FileManageModal.tsx | 16 ++-- src/reactComponents/Menu.tsx | 3 +- src/reactComponents/Tabs.tsx | 16 ++-- 5 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 133367b6..8cfce081 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -528,61 +528,77 @@ const AppContent: React.FC = ({ project, setProject }): React.J } }, [shownPythonToolboxCategories]); - // Fetch modules when project changes. + // Fetch any unfetched modules when project changes. React.useEffect(() => { - if (project && storage) { - const fetchModules = async () => { - const promises: {[modulePath: string]: Promise} = {}; // value is promise of module content. - promises[project.robot.modulePath] = storage.fetchFileContentText(project.robot.modulePath); - project.mechanisms.forEach(mechanism => { - promises[mechanism.modulePath] = storage.fetchFileContentText(mechanism.modulePath); - }); - project.opModes.forEach(opmode => { - promises[opmode.modulePath] = storage.fetchFileContentText(opmode.modulePath); - }); - const updatedModulePathToContentText: {[modulePath: string]: string} = {}; // value is module content text - await Promise.all( - Object.entries(promises).map(async ([modulePath, promise]) => { - updatedModulePathToContentText[modulePath] = await promise; - }) - ); - const oldModulePathToContentText = modulePathToContentText; - setModulePathToContentText(updatedModulePathToContentText); - - // Remove any deleted modules from modulePaths, modulePathToBlocklyComponent, and - // modulePathToEditor. Update currentModule if the current module was deleted. - for (const modulePath in oldModulePathToContentText) { - if (modulePath in updatedModulePathToContentText) { - continue; - } - if (currentModule && currentModule.modulePath === modulePath) { - setCurrentModule(project.robot); - setActiveTab(project.robot.modulePath); - } - const indexToRemove: number = modulePaths.current.indexOf(modulePath); - if (indexToRemove !== -1) { - modulePaths.current.splice(indexToRemove, 1); - } - if (modulePath in modulePathToBlocklyComponent.current) { - delete modulePathToBlocklyComponent.current[modulePath]; - } - if (modulePath in modulePathToEditor.current) { - const editor = modulePathToEditor.current[modulePath]; - editor.abandon(); - delete modulePathToEditor.current[modulePath]; - } - } - }; - fetchModules(); - } + fetchModules(); }, [project]); + const fetchModules = async () => { + if (!project || !storage) { + return; + } + const oldModulePathToContentText = modulePathToContentText; + const promises: {[modulePath: string]: Promise} = {}; // value is promise of module content. + const updatedModulePathToContentText: {[modulePath: string]: string} = {}; // value is module content text + if (project.robot.modulePath in modulePathToContentText) { + updatedModulePathToContentText[project.robot.modulePath] = modulePathToContentText[project.robot.modulePath]; + } else { + promises[project.robot.modulePath] = storage.fetchFileContentText(project.robot.modulePath); + } + project.mechanisms.forEach(mechanism => { + if (mechanism.modulePath in modulePathToContentText) { + updatedModulePathToContentText[mechanism.modulePath] = modulePathToContentText[mechanism.modulePath]; + } else { + promises[mechanism.modulePath] = storage.fetchFileContentText(mechanism.modulePath); + } + }); + project.opModes.forEach(opmode => { + if (opmode.modulePath in modulePathToContentText) { + updatedModulePathToContentText[opmode.modulePath] = modulePathToContentText[opmode.modulePath]; + } else { + promises[opmode.modulePath] = storage.fetchFileContentText(opmode.modulePath); + } + }); + if (Object.keys(promises).length) { + await Promise.all( + Object.entries(promises).map(async ([modulePath, promise]) => { + updatedModulePathToContentText[modulePath] = await promise; + }) + ); + setModulePathToContentText(updatedModulePathToContentText); + } + + // Remove any deleted modules from modulePaths, modulePathToBlocklyComponent, and + // modulePathToEditor. Update currentModule if the current module was deleted. + for (const modulePath in oldModulePathToContentText) { + if (modulePath in updatedModulePathToContentText) { + continue; + } + if (currentModule && currentModule.modulePath === modulePath) { + setCurrentModule(project.robot); + setActiveTab(project.robot.modulePath); + } + const indexToRemove: number = modulePaths.current.indexOf(modulePath); + if (indexToRemove !== -1) { + modulePaths.current.splice(indexToRemove, 1); + } + if (modulePath in modulePathToBlocklyComponent.current) { + delete modulePathToBlocklyComponent.current[modulePath]; + } + if (modulePath in modulePathToEditor.current) { + const editor = modulePathToEditor.current[modulePath]; + editor.abandon(); + delete modulePathToEditor.current[modulePath]; + } + } + }; + // Load saved tabs when project changes React.useEffect(() => { const loadSavedTabs = async () => { if (project && !isLoading) { setIsLoadingTabs(true); - + // Add a small delay to ensure UserSettingsProvider context is updated await new Promise(resolve => setTimeout(resolve, 0)); @@ -705,7 +721,7 @@ const AppContent: React.FC = ({ project, setProject }): React.J } return tab; }); - + // Only update if something actually changed const titlesChanged = updatedTabs.some((tab, index) => tab.title !== tabItems[index]?.title); if (titlesChanged) { @@ -734,6 +750,10 @@ const AppContent: React.FC = ({ project, setProject }): React.J return () => clearTimeout(timeoutId); }, [tabItems, project?.projectName, isLoadingTabs]); + const onProjectChanged = async (): Promise => { + await fetchModules(); + }; + const { Sider, Content } = Antd.Layout; return ( @@ -766,6 +786,7 @@ const AppContent: React.FC = ({ project, setProject }): React.J gotoTab={setActiveTab} project={project} setProject={setProject} + onProjectChanged={onProjectChanged} openWPIToolboxSettings={() => setToolboxSettingsModalIsOpen(true)} theme={theme} setTheme={setTheme} @@ -784,7 +805,7 @@ const AppContent: React.FC = ({ project, setProject }): React.J currentModule={currentModule} setCurrentModule={changeModule} project={project} - setProject={setProject} + onProjectChanged={onProjectChanged} storage={storage} />
diff --git a/src/reactComponents/AddTabDialog.tsx b/src/reactComponents/AddTabDialog.tsx index 9da8ff3a..48f3a009 100644 --- a/src/reactComponents/AddTabDialog.tsx +++ b/src/reactComponents/AddTabDialog.tsx @@ -41,7 +41,7 @@ interface AddTabDialogProps { onOk: (newTab: TabItem) => void; onCancel: () => void; project: storageProject.Project | null; - setProject: (project: storageProject.Project | null) => void; + onProjectChanged: () => Promise; currentTabs: TabItem[]; storage: commonStorage.Storage | null; } @@ -111,6 +111,7 @@ export default function AddTabDialog(props: AddTabDialogProps) { await storageProject.addModuleToProject( props.storage, props.project, moduleType, newClassName); + await props.onProjectChanged(); const newModule = storageProject.findModuleByClassName(props.project, newClassName); if (newModule) { diff --git a/src/reactComponents/FileManageModal.tsx b/src/reactComponents/FileManageModal.tsx index cc44bbdc..4db37b94 100644 --- a/src/reactComponents/FileManageModal.tsx +++ b/src/reactComponents/FileManageModal.tsx @@ -40,7 +40,7 @@ interface FileManageModalProps { isOpen: boolean; onClose: () => void; project: storageProject.Project | null; - setProject: (project: storageProject.Project | null) => void; + onProjectChanged: () => Promise; gotoTab: (path: string) => void; setAlertErrorMessage: (message: string) => void; storage: commonStorage.Storage | null; @@ -64,11 +64,6 @@ export default function FileManageModal(props: FileManageModalProps) { const [name, setName] = React.useState(''); const [copyModalOpen, setCopyModalOpen] = React.useState(false); - const triggerProjectUpdate = (): void => { - if (props.project) { - props.setProject({...props.project}); - } - } React.useEffect(() => { if (!props.project || props.tabType === null) { setModules([]); @@ -109,6 +104,7 @@ export default function FileManageModal(props: FileManageModalProps) { newClassName, origModule.path ); + await props.onProjectChanged(); const newModules = modules.map((module) => { if (module.path === origModule.path) { @@ -118,7 +114,6 @@ export default function FileManageModal(props: FileManageModalProps) { }); setModules(newModules); - triggerProjectUpdate(); // Close the rename modal first setRenameModalOpen(false); @@ -147,6 +142,7 @@ export default function FileManageModal(props: FileManageModalProps) { newClassName, origModule.path ); + await props.onProjectChanged(); const originalModule = modules.find((module) => module.path === origModule.path); if (!originalModule) { @@ -165,7 +161,6 @@ export default function FileManageModal(props: FileManageModalProps) { newModules.push(newModule); setModules(newModules); - triggerProjectUpdate(); // Close the copy modal first setCopyModalOpen(false); @@ -197,6 +192,7 @@ export default function FileManageModal(props: FileManageModalProps) { moduleType, newClassName ); + await props.onProjectChanged(); const newModule = storageProject.findModuleByClassName(props.project, newClassName); if (newModule) { @@ -212,7 +208,7 @@ export default function FileManageModal(props: FileManageModalProps) { if(newModule){ props.gotoTab(newModule.modulePath); } - triggerProjectUpdate(); + props.onClose(); }; @@ -227,7 +223,7 @@ export default function FileManageModal(props: FileManageModalProps) { props.project, record.path ); - triggerProjectUpdate(); + await props.onProjectChanged(); } }; diff --git a/src/reactComponents/Menu.tsx b/src/reactComponents/Menu.tsx index 66577856..4c73bcaf 100644 --- a/src/reactComponents/Menu.tsx +++ b/src/reactComponents/Menu.tsx @@ -60,6 +60,7 @@ export interface MenuProps { gotoTab: (tabKey: string) => void; project: storageProject.Project | null; setProject: (project: storageProject.Project | null) => void; + onProjectChanged: () => Promise; openWPIToolboxSettings: () => void; theme: string; setTheme: (theme: string) => void; @@ -429,7 +430,7 @@ export function Component(props: MenuProps): React.JSX.Element { project={props.project} storage={props.storage} tabType={tabType} - setProject={props.setProject} + onProjectChanged={props.onProjectChanged} setAlertErrorMessage={props.setAlertErrorMessage} gotoTab={props.gotoTab} /> diff --git a/src/reactComponents/Tabs.tsx b/src/reactComponents/Tabs.tsx index f1fec91f..1243428f 100644 --- a/src/reactComponents/Tabs.tsx +++ b/src/reactComponents/Tabs.tsx @@ -48,7 +48,7 @@ export interface TabsProps { setTabList: (items: TabItem[]) => void; activeTab: string; project: storageProject.Project | null; - setProject: (project: storageProject.Project | null) => void; + onProjectChanged: () => Promise; setAlertErrorMessage: (message: string) => void; currentModule: storageModule.Module | null; setCurrentModule: (module: storageModule.Module | null) => void; @@ -76,10 +76,6 @@ export function Component(props: TabsProps): React.JSX.Element { const [copyModalOpen, setCopyModalOpen] = React.useState(false); const [currentTab, setCurrentTab] = React.useState(null); - const triggerProjectUpdate = (): void => { - props.setProject(structuredClone(props.project)); - } - /** Handles tab change and updates current module. */ const handleTabChange = (key: string): void => { if (props.project) { @@ -154,7 +150,7 @@ export function Component(props: TabsProps): React.JSX.Element { const handleAddTabOk = (newTab: TabItem): void => { props.setTabList([...props.tabList, newTab]); - setActiveKey(newTab.key); + handleTabChange(newTab.key); setAddTabDialogOpen(false); }; @@ -173,6 +169,7 @@ export function Component(props: TabsProps): React.JSX.Element { newClassName, oldModulePath, ); + await props.onProjectChanged(); const newTabs = props.tabList.map((tab) => { if (tab.key === key) { @@ -183,7 +180,6 @@ export function Component(props: TabsProps): React.JSX.Element { props.setTabList(newTabs); setActiveKey(newModulePath); - triggerProjectUpdate(); } catch (error) { console.error('Error renaming module:', error); props.setAlertErrorMessage(t('FAILED_TO_RENAME_MODULE')); @@ -207,6 +203,7 @@ export function Component(props: TabsProps): React.JSX.Element { newClassName, oldModulePath, ); + await props.onProjectChanged(); const newTabs = [...props.tabList]; const originalTab = props.tabList.find((tab) => tab.key === key); @@ -220,7 +217,6 @@ export function Component(props: TabsProps): React.JSX.Element { newTabs.push({ key: newModulePath, title: newClassName, type: originalTab.type }); props.setTabList(newTabs); setActiveKey(newModulePath); - triggerProjectUpdate(); } catch (error) { console.error('Error copying module:', error); props.setAlertErrorMessage(t('FAILED_TO_COPY_MODULE')); @@ -271,7 +267,7 @@ export function Component(props: TabsProps): React.JSX.Element { if (props.storage && props.project) { await storageProject.removeModuleFromProject(props.storage, props.project, tab.key); - triggerProjectUpdate(); + await props.onProjectChanged(); } if (newTabs.length > 0) { @@ -360,7 +356,7 @@ export function Component(props: TabsProps): React.JSX.Element { onCancel={() => setAddTabDialogOpen(false)} onOk={handleAddTabOk} project={props.project} - setProject={props.setProject} + onProjectChanged={props.onProjectChanged} currentTabs={props.tabList} storage={props.storage} />