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
191 changes: 153 additions & 38 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ interface AppContentProps {

const AppContent: React.FC<AppContentProps> = ({ 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<storageModule.Module | null>(null);
Expand All @@ -163,6 +163,7 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
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 [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0);
const [leftCollapsed, setLeftCollapsed] = React.useState(false);
Expand Down Expand Up @@ -384,35 +385,6 @@ const AppContent: React.FC<AppContentProps> = ({ 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;
Expand Down Expand Up @@ -581,20 +553,163 @@ const AppContent: React.FC<AppContentProps> = ({ 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 (
Expand Down
56 changes: 56 additions & 0 deletions src/reactComponents/UserSettingsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this called getUserOptionsKey, but it is for storing open tabs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I am convinced that there will be more project options per user in the future. I just don't know what they are yet....


/** User settings interface. */
export interface UserSettings {
language: string;
Expand All @@ -42,6 +45,8 @@ export interface UserSettingsContextType {
settings: UserSettings;
updateLanguage: (language: string) => Promise<void>;
updateTheme: (theme: string) => Promise<void>;
updateOpenTabs: (projectName: string, tabPaths: string[]) => Promise<void>;
getOpenTabs: (projectName: string) => Promise<string[]>;
isLoading: boolean;
error: string | null;
storage: Storage | null;
Expand Down Expand Up @@ -134,10 +139,52 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({
}
};

/** Update open tabs for a specific project. */
const updateOpenTabs = async (projectName: string, tabPaths: string[]): Promise<void> => {
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<string[]> => {
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,
Expand All @@ -149,3 +196,12 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({
</UserSettingsContext.Provider>
);
};

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