diff --git a/src/App.tsx b/src/App.tsx index 5ce0e9c3..08a3a4c7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -141,13 +141,14 @@ const AppContent: React.FC = ({ project, setProject }): React.J const [toolboxSettingsModalIsOpen, setToolboxSettingsModalIsOpen] = React.useState(false); 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 [leftCollapsed, setLeftCollapsed] = React.useState(false); const [theme, setTheme] = React.useState('dark'); const [languageInitialized, setLanguageInitialized] = React.useState(false); const [themeInitialized, setThemeInitialized] = React.useState(false); + + const tabsRef = React.useRef(null); /** Initialize language from UserSettings when app first starts. */ React.useEffect(() => { @@ -402,12 +403,6 @@ const AppContent: React.FC = ({ project, setProject }): React.J // 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 { @@ -469,6 +464,14 @@ const AppContent: React.FC = ({ project, setProject }): React.J await fetchModules(); }; + const gotoTab = (tabKey: string): void => { + tabsRef.current?.gotoTab(tabKey); + }; + + const closeTab = (tabKey: string): void => { + tabsRef.current?.closeTab(tabKey); + }; + const { Sider } = Antd.Layout; return ( @@ -498,7 +501,8 @@ const AppContent: React.FC = ({ project, setProject }): React.J = ({ project, setProject }): React.J Promise; gotoTab: (path: string) => void; + closeTab: (path: string) => void; setAlertErrorMessage: (message: string) => void; storage: commonStorage.Storage | null; tabType: TabType; @@ -65,7 +66,7 @@ export default function FileManageModal(props: FileManageModalProps) { const [copyModalOpen, setCopyModalOpen] = React.useState(false); React.useEffect(() => { - if (!props.project || props.tabType === null) { + if (!props.project || props.tabType === null || !props.isOpen) { setModules([]); return; } @@ -89,7 +90,7 @@ export default function FileManageModal(props: FileManageModalProps) { // Sort modules alphabetically by name moduleList.sort((a, b) => a.name.localeCompare(b.name)); setModules(moduleList); - }, [props.project, props.tabType]); + }, [props.project, props.tabType, props.isOpen]); /** Handles renaming a module. */ const handleRename = async (origModule: Module, newClassName: string): Promise => { @@ -106,19 +107,10 @@ export default function FileManageModal(props: FileManageModalProps) { ); await props.onProjectChanged(); - const newModules = modules.map((module) => { - if (module.path === origModule.path) { - return {...module, title: newClassName, path: newModulePath}; - } - return module; - }); - - setModules(newModules); - // Close the rename modal first setRenameModalOpen(false); - // Automatically select and open the newly created module + // Automatically select and open the renamed module props.gotoTab(newModulePath); props.onClose(); @@ -218,6 +210,9 @@ export default function FileManageModal(props: FileManageModalProps) { setModules(newModules); if (props.storage && props.project) { + // Close the tab before removing the module + props.closeTab(record.path); + await storageProject.removeModuleFromProject( props.storage, props.project, diff --git a/src/reactComponents/Menu.tsx b/src/reactComponents/Menu.tsx index 5651b5ca..9d65a71a 100644 --- a/src/reactComponents/Menu.tsx +++ b/src/reactComponents/Menu.tsx @@ -59,6 +59,7 @@ export interface MenuProps { storage: commonStorage.Storage | null; setAlertErrorMessage: (message: string) => void; gotoTab: (tabKey: string) => void; + closeTab: (tabKey: string) => void; currentProject: storageProject.Project | null; setCurrentProject: (project: storageProject.Project | null) => void; onProjectChanged: () => Promise; @@ -462,6 +463,7 @@ export function Component(props: MenuProps): React.JSX.Element { onProjectChanged={props.onProjectChanged} setAlertErrorMessage={props.setAlertErrorMessage} gotoTab={props.gotoTab} + closeTab={props.closeTab} /> void; + closeTab: (tabKey: string) => void; +} + /** Props for the Tabs component. */ export interface TabsProps { tabList: TabItem[]; setTabList: (items: TabItem[]) => void; - activeTab: string; project: storageProject.Project | null; onProjectChanged: () => Promise; setAlertErrorMessage: (message: string) => void; @@ -69,11 +74,11 @@ const MIN_TABS_FOR_CLOSE_OTHERS = 2; * Tab component that manages project module tabs with add, edit, delete, and rename functionality. * Provides context menus for tab operations and modal dialogs for user input. */ -export function Component(props: TabsProps): React.JSX.Element { +export const Component = React.forwardRef((props, ref): React.JSX.Element => { const { t } = I18Next.useTranslation(); const [modal, contextHolder] = Antd.Modal.useModal(); - const [activeKey, setActiveKey] = React.useState(props.activeTab); + const [activeKey, setActiveKey] = React.useState(props.tabList.length > 0 ? props.tabList[0].key : ''); const [addTabDialogOpen, setAddTabDialogOpen] = React.useState(false); const [name, setName] = React.useState(''); const [renameModalOpen, setRenameModalOpen] = React.useState(false); @@ -100,41 +105,75 @@ export function Component(props: TabsProps): React.JSX.Element { setActiveKey(key); }; - /** Checks if a key exists in the current tab list. */ - const isTabOpen = (key: string): boolean => { - return props.tabList.some((tab) => tab.key === key); - }; - - /** Adds a new tab for the given module key. */ - const addTab = (key: string): void => { - const newTabs = [...props.tabList]; - if (!props.project) { + /** Goes to a specific tab, adding it if needed or updating if renamed. */ + const gotoTab = (tabKey: string): void => { + // Check if tab already exists + const existingTab = props.tabList.find(tab => tab.key === tabKey); + if (existingTab) { + // Tab exists, just activate it + setActiveKey(tabKey); return; } - const modulePath = key; - const module = storageProject.findModuleByModulePath(props.project, modulePath); - if (!module) { - return; - } + if (!props.project) return; + + // Check if this is a renamed module - look for a tab whose module no longer exists + const targetModule = storageProject.findModuleByModulePath(props.project, tabKey); + if (targetModule) { + // Look for a tab with the same type but whose path no longer exists (indicating a rename) + const staleTab = props.tabList.find(tab => { + const tabModule = storageProject.findModuleByModulePath(props.project!, tab.key); + // If tab's module doesn't exist and it matches the target type + return !tabModule && + ((tab.type === TabType.MECHANISM && targetModule.moduleType === storageModule.ModuleType.MECHANISM) || + (tab.type === TabType.OPMODE && targetModule.moduleType === storageModule.ModuleType.OPMODE)); + }); - switch (module.moduleType) { - case storageModule.ModuleType.MECHANISM: - newTabs.push({ key, title: module.className, type: TabType.MECHANISM }); - break; - case storageModule.ModuleType.OPMODE: - newTabs.push({ key, title: module.className, type: TabType.OPMODE }); - break; - case storageModule.ModuleType.ROBOT: - break; // Robot tab is always first and cannot be added again. - default: - console.warn('Unknown module type:', module.moduleType); - break; + if (staleTab) { + // This is a rename - update the existing tab + const updatedTabs = props.tabList.map(tab => + tab.key === staleTab.key + ? { ...tab, key: tabKey, title: targetModule.className } + : tab + ); + props.setTabList(updatedTabs); + setActiveKey(tabKey); + return; + } + + // Not a rename - add new tab + let newTab: TabItem; + switch (targetModule.moduleType) { + case storageModule.ModuleType.MECHANISM: + newTab = { key: tabKey, title: targetModule.className, type: TabType.MECHANISM }; + break; + case storageModule.ModuleType.OPMODE: + newTab = { key: tabKey, title: targetModule.className, type: TabType.OPMODE }; + break; + case storageModule.ModuleType.ROBOT: + newTab = { key: tabKey, title: targetModule.className, type: TabType.ROBOT }; + break; + default: + return; + } + props.setTabList([...props.tabList, newTab]); + setActiveKey(tabKey); } + }; + /** Closes a specific tab. */ + const closeTabMethod = (tabKey: string): void => { + const newTabs = props.tabList.filter((tab) => tab.key !== tabKey); props.setTabList(newTabs); + // The useEffect will handle switching to another tab if needed }; + // Expose imperative methods via ref + React.useImperativeHandle(ref, () => ({ + gotoTab, + closeTab: closeTabMethod, + })); + /** Handles tab edit actions (add/remove). */ const handleTabEdit = ( targetKey: React.MouseEvent | React.KeyboardEvent | string, @@ -378,15 +417,19 @@ export function Component(props: TabsProps): React.JSX.Element { }); }; - // Effect to handle active tab changes + // Effect to ensure activeKey is valid when tab list changes React.useEffect(() => { - if (activeKey !== props.activeTab) { - if (!isTabOpen(props.activeTab)) { - addTab(props.activeTab); - } - handleTabChange(props.activeTab); + // Check if current activeKey is still in the tab list + const isActiveKeyValid = props.tabList.some(tab => tab.key === activeKey); + + if (!isActiveKeyValid && props.tabList.length > 0) { + // Active tab was removed, switch to first available tab + const newActiveKey = props.tabList[0].key; + setActiveKey(newActiveKey); + } else if (props.tabList.length === 0) { + setActiveKey(''); } - }, [props.activeTab]); + }, [props.tabList.length]); return ( <> @@ -480,4 +523,4 @@ export function Component(props: TabsProps): React.JSX.Element { /> ); -} \ No newline at end of file +}); \ No newline at end of file