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} />