diff --git a/src/App.tsx b/src/App.tsx index a449a07d..b1f7785f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -153,7 +153,7 @@ interface AppContentProps { const AppContent: React.FC = ({ project, setProject }): React.JSX.Element => { const { t, i18n } = useTranslation(); - const { settings, updateLanguage, updateTheme, storage, isLoading } = useUserSettings(); + const { settings, updateLanguage, updateTheme, updateOpenTabs, getOpenTabs, storage, isLoading } = useUserSettings(); const [alertErrorMessage, setAlertErrorMessage] = React.useState(''); const [currentModule, setCurrentModule] = React.useState(null); @@ -163,6 +163,7 @@ const AppContent: React.FC = ({ project, setProject }): React.J const [modulePathToContentText, setModulePathToContentText] = React.useState<{[modulePath: string]: string}>({}); const [tabItems, setTabItems] = React.useState([]); const [activeTab, setActiveTab] = React.useState(''); + const [isLoadingTabs, setIsLoadingTabs] = React.useState(false); const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState>(new Set()); const [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0); const [leftCollapsed, setLeftCollapsed] = React.useState(false); @@ -384,35 +385,6 @@ const AppContent: React.FC = ({ project, setProject }): React.J handleToolboxSettingsOk(updatedShownCategories); }; - /** Creates tab items from project data. */ - const createTabItemsFromProject = (projectData: storageProject.Project): Tabs.TabItem[] => { - const tabs: Tabs.TabItem[] = [ - { - key: projectData.robot.modulePath, - title: t('ROBOT'), - type: TabType.ROBOT, - }, - ]; - - projectData.mechanisms.forEach((mechanism) => { - tabs.push({ - key: mechanism.modulePath, - title: mechanism.className, - type: TabType.MECHANISM, - }); - }); - - projectData.opModes.forEach((opmode) => { - tabs.push({ - key: opmode.modulePath, - title: opmode.className, - type: TabType.OPMODE, - }); - }); - - return tabs; - }; - /** Handles toolbox update requests from blocks */ const handleToolboxUpdateRequest = React.useCallback((e: Event) => { const workspaceId = (e as CustomEvent).detail.workspaceId; @@ -581,20 +553,163 @@ const AppContent: React.FC = ({ project, setProject }): React.J } }, [project]); - // Update tab items when ever the modules in the project change. + // Load saved tabs when project changes React.useEffect(() => { - if (project) { - const tabs = createTabItemsFromProject(project); - setTabItems(tabs); + 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)); + + let tabsToSet: Tabs.TabItem[] = []; + let usedSavedTabs = false; + + // Try to load saved tabs first + try { + const savedTabPaths = await getOpenTabs(project.projectName); + + if (savedTabPaths.length > 0) { + // Filter saved tabs to only include those that still exist in the project + const validSavedTabs = savedTabPaths.filter((tabPath: string) => { + const module = storageProject.findModuleByModulePath(project!, tabPath); + return module !== null; + }); + + if (validSavedTabs.length > 0) { + usedSavedTabs = true; + // Convert paths back to TabItem objects + tabsToSet = validSavedTabs.map((path: string) => { + const module = storageProject.findModuleByModulePath(project!, path); + if (!module) return null; + + let type: TabType; + let title: string; + + switch (module.moduleType) { + case storageModule.ModuleType.ROBOT: + type = TabType.ROBOT; + title = t('ROBOT'); + break; + case storageModule.ModuleType.MECHANISM: + type = TabType.MECHANISM; + title = module.className; + break; + case storageModule.ModuleType.OPMODE: + type = TabType.OPMODE; + title = module.className; + break; + default: + return null; + } + + return { + key: path, + title, + type, + }; + }).filter((item): item is Tabs.TabItem => item !== null); + } + } + } catch (error) { + console.error('Failed to load saved tabs:', error); + } + + // If no saved tabs or loading failed, create default tabs (all project files) + if (tabsToSet.length === 0) { + tabsToSet = [ + { + key: project.robot.modulePath, + title: t('ROBOT'), + type: TabType.ROBOT, + } + ]; + + // Add all mechanisms + project.mechanisms.forEach((mechanism) => { + tabsToSet.push({ + key: mechanism.modulePath, + title: mechanism.className, + type: TabType.MECHANISM, + }); + }); + + // Add all opmodes + project.opModes.forEach((opmode) => { + tabsToSet.push({ + key: opmode.modulePath, + title: opmode.className, + type: TabType.OPMODE, + }); + }); + } + + // Set the tabs + setTabItems(tabsToSet); + + // Only set active tab to robot if no active tab is set or if the current active tab no longer exists + const currentActiveTabExists = tabsToSet.some(tab => tab.key === activeTab); + if (!activeTab || !currentActiveTabExists) { + setActiveTab(project.robot.modulePath); + } + + // Only auto-save if we didn't use saved tabs (i.e., this is a new project or the first time) + if (!usedSavedTabs) { + try { + const tabPaths = tabsToSet.map(tab => tab.key); + await updateOpenTabs(project.projectName, tabPaths); + } catch (error) { + console.error('Failed to auto-save default tabs:', error); + } + } + + setIsLoadingTabs(false); + } + }; + + loadSavedTabs(); + }, [project?.projectName, isLoading, getOpenTabs]); + + // Update tab items when modules in project change (for title updates, etc) + React.useEffect(() => { + if (project && tabItems.length > 0) { + // Update existing tab titles in case they changed + const updatedTabs = tabItems.map(tab => { + const module = storageProject.findModuleByModulePath(project, tab.key); + if (module && module.moduleType !== storageModule.ModuleType.ROBOT) { + return { ...tab, title: module.className }; + } + return tab; + }); - // Only set active tab to robot if no active tab is set or if the current active tab no longer exists - const currentActiveTabExists = tabs.some(tab => tab.key === activeTab); - if (!activeTab || !currentActiveTabExists) { - setActiveTab(project.robot.modulePath); + // Only update if something actually changed + const titlesChanged = updatedTabs.some((tab, index) => tab.title !== tabItems[index]?.title); + if (titlesChanged) { + setTabItems(updatedTabs); } } }, [modulePathToContentText]); + // Save tabs when tab list changes (but not during initial loading) + React.useEffect(() => { + const saveTabs = async () => { + // Don't save tabs while we're in the process of loading them + if (project?.projectName && tabItems.length > 0 && !isLoadingTabs) { + try { + const tabPaths = tabItems.map(tab => tab.key); + await updateOpenTabs(project.projectName, tabPaths); + } catch (error) { + console.error('Failed to save open tabs:', error); + // Don't show alert for save failures as they're not critical to user workflow + } + } + }; + + // Use a small delay to debounce rapid tab changes + const timeoutId = setTimeout(saveTabs, 100); + return () => clearTimeout(timeoutId); + }, [tabItems, project?.projectName, isLoadingTabs]); + const { Sider, Content } = Antd.Layout; return ( diff --git a/src/reactComponents/UserSettingsProvider.tsx b/src/reactComponents/UserSettingsProvider.tsx index 1dd0ea75..2e562ee3 100644 --- a/src/reactComponents/UserSettingsProvider.tsx +++ b/src/reactComponents/UserSettingsProvider.tsx @@ -31,6 +31,9 @@ const USER_THEME_KEY = 'userTheme'; const DEFAULT_LANGUAGE = 'en'; const DEFAULT_THEME = 'dark'; +/** Helper function to generate project-specific storage key for open tabs. */ +const getUserOptionsKey = (projectName: string): string => `user_options_${projectName}`; + /** User settings interface. */ export interface UserSettings { language: string; @@ -42,6 +45,8 @@ export interface UserSettingsContextType { settings: UserSettings; updateLanguage: (language: string) => Promise; updateTheme: (theme: string) => Promise; + updateOpenTabs: (projectName: string, tabPaths: string[]) => Promise; + getOpenTabs: (projectName: string) => Promise; isLoading: boolean; error: string | null; storage: Storage | null; @@ -134,10 +139,52 @@ export const UserSettingsProvider: React.FC = ({ } }; + /** Update open tabs for a specific project. */ + const updateOpenTabs = async (projectName: string, tabPaths: string[]): Promise => { + try { + setError(null); + + if (storage) { + const storageKey = getUserOptionsKey(projectName); + await storage.saveEntry(storageKey, JSON.stringify(tabPaths)); + } else { + console.warn('No storage available, cannot save open tabs'); + } + } catch (err) { + setError(`Failed to save open tabs: ${err}`); + console.error('Error saving open tabs:', err); + throw err; + } + }; + + /** Get open tabs for a specific project. */ + const getOpenTabs = async (projectName: string): Promise => { + try { + if (!storage) { + return []; + } + + const storageKey = getUserOptionsKey(projectName); + const tabsJson = await storage.fetchEntry(storageKey, JSON.stringify([])); + + try { + return JSON.parse(tabsJson); + } catch (error) { + console.warn(`Failed to parse open tabs for project ${projectName}, using default:`, error); + return []; + } + } catch (err) { + console.error(`Error loading open tabs for project ${projectName}:`, err); + return []; + } + }; + const contextValue: UserSettingsContextType = { settings, updateLanguage, updateTheme, + updateOpenTabs, + getOpenTabs, isLoading, error, storage: storage || null, @@ -149,3 +196,12 @@ export const UserSettingsProvider: React.FC = ({ ); }; + +/** Custom hook to use user settings context. */ +export const useUserSettings = (): UserSettingsContextType => { + const context = React.useContext(UserSettingsContext); + if (!context) { + throw new Error('useUserSettings must be used within a UserSettingsProvider'); + } + return context; +};