Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,14 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
const [toolboxSettingsModalIsOpen, setToolboxSettingsModalIsOpen] = React.useState(false);
const [modulePathToContentText, setModulePathToContentText] = React.useState<{[modulePath: string]: string}>({});
const [tabItems, setTabItems] = React.useState<Tabs.TabItem[]>([]);
const [activeTab, setActiveTab] = React.useState('');
const [isLoadingTabs, setIsLoadingTabs] = React.useState(false);
const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState<Set<string>>(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<Tabs.TabsRef>(null);

/** Initialize language from UserSettings when app first starts. */
React.useEffect(() => {
Expand Down Expand Up @@ -402,12 +403,6 @@ const AppContent: React.FC<AppContentProps> = ({ 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 {
Expand Down Expand Up @@ -469,6 +464,14 @@ const AppContent: React.FC<AppContentProps> = ({ 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 (
Expand Down Expand Up @@ -498,7 +501,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
<Menu.Component
storage={storage}
setAlertErrorMessage={setAlertErrorMessage}
gotoTab={setActiveTab}
gotoTab={gotoTab}
closeTab={closeTab}
currentProject={project}
setCurrentProject={setProject}
onProjectChanged={onProjectChanged}
Expand All @@ -513,8 +517,8 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
</Sider>
<Antd.Layout>
<Tabs.Component
ref={tabsRef}
tabList={tabItems}
activeTab={activeTab}
setTabList={setTabItems}
setAlertErrorMessage={setAlertErrorMessage}
project={project}
Expand Down
19 changes: 7 additions & 12 deletions src/reactComponents/FileManageModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ interface FileManageModalProps {
project: storageProject.Project | null;
onProjectChanged: () => Promise<void>;
gotoTab: (path: string) => void;
closeTab: (path: string) => void;
setAlertErrorMessage: (message: string) => void;
storage: commonStorage.Storage | null;
tabType: TabType;
Expand All @@ -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;
}
Expand All @@ -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<void> => {
Expand All @@ -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();

Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/reactComponents/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
Expand Down Expand Up @@ -462,6 +463,7 @@ export function Component(props: MenuProps): React.JSX.Element {
onProjectChanged={props.onProjectChanged}
setAlertErrorMessage={props.setAlertErrorMessage}
gotoTab={props.gotoTab}
closeTab={props.closeTab}
/>
<ProjectManageModal
noProjects={noProjects}
Expand Down
117 changes: 80 additions & 37 deletions src/reactComponents/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,16 @@ export interface TabItem {
type: TabType;
}

/** Imperative methods exposed by Tabs component. */
export interface TabsRef {
gotoTab: (tabKey: string) => 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<void>;
setAlertErrorMessage: (message: string) => void;
Expand All @@ -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<TabsRef, TabsProps>((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);
Expand All @@ -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,
Expand Down Expand Up @@ -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 (
<>
Expand Down Expand Up @@ -480,4 +523,4 @@ export function Component(props: TabsProps): React.JSX.Element {
/>
</>
);
}
});