Skip to content

Commit 912c007

Browse files
authored
Changes for saving open tabs as user settings (#259)
* Changes for saving open tabs as user settings * Make it so you can make methods that return a value * Revert "Make it so you can make methods that return a value" This reverts commit 71f27ca.
1 parent 65f914e commit 912c007

File tree

2 files changed

+209
-38
lines changed

2 files changed

+209
-38
lines changed

src/App.tsx

Lines changed: 153 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ interface AppContentProps {
153153

154154
const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.JSX.Element => {
155155
const { t, i18n } = useTranslation();
156-
const { settings, updateLanguage, updateTheme, storage, isLoading } = useUserSettings();
156+
const { settings, updateLanguage, updateTheme, updateOpenTabs, getOpenTabs, storage, isLoading } = useUserSettings();
157157

158158
const [alertErrorMessage, setAlertErrorMessage] = React.useState('');
159159
const [currentModule, setCurrentModule] = React.useState<storageModule.Module | null>(null);
@@ -163,6 +163,7 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
163163
const [modulePathToContentText, setModulePathToContentText] = React.useState<{[modulePath: string]: string}>({});
164164
const [tabItems, setTabItems] = React.useState<Tabs.TabItem[]>([]);
165165
const [activeTab, setActiveTab] = React.useState('');
166+
const [isLoadingTabs, setIsLoadingTabs] = React.useState(false);
166167
const [shownPythonToolboxCategories, setShownPythonToolboxCategories] = React.useState<Set<string>>(new Set());
167168
const [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0);
168169
const [leftCollapsed, setLeftCollapsed] = React.useState(false);
@@ -384,35 +385,6 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
384385
handleToolboxSettingsOk(updatedShownCategories);
385386
};
386387

387-
/** Creates tab items from project data. */
388-
const createTabItemsFromProject = (projectData: storageProject.Project): Tabs.TabItem[] => {
389-
const tabs: Tabs.TabItem[] = [
390-
{
391-
key: projectData.robot.modulePath,
392-
title: t('ROBOT'),
393-
type: TabType.ROBOT,
394-
},
395-
];
396-
397-
projectData.mechanisms.forEach((mechanism) => {
398-
tabs.push({
399-
key: mechanism.modulePath,
400-
title: mechanism.className,
401-
type: TabType.MECHANISM,
402-
});
403-
});
404-
405-
projectData.opModes.forEach((opmode) => {
406-
tabs.push({
407-
key: opmode.modulePath,
408-
title: opmode.className,
409-
type: TabType.OPMODE,
410-
});
411-
});
412-
413-
return tabs;
414-
};
415-
416388
/** Handles toolbox update requests from blocks */
417389
const handleToolboxUpdateRequest = React.useCallback((e: Event) => {
418390
const workspaceId = (e as CustomEvent).detail.workspaceId;
@@ -581,20 +553,163 @@ const AppContent: React.FC<AppContentProps> = ({ project, setProject }): React.J
581553
}
582554
}, [project]);
583555

584-
// Update tab items when ever the modules in the project change.
556+
// Load saved tabs when project changes
585557
React.useEffect(() => {
586-
if (project) {
587-
const tabs = createTabItemsFromProject(project);
588-
setTabItems(tabs);
558+
const loadSavedTabs = async () => {
559+
if (project && !isLoading) {
560+
setIsLoadingTabs(true);
561+
562+
// Add a small delay to ensure UserSettingsProvider context is updated
563+
await new Promise(resolve => setTimeout(resolve, 0));
564+
565+
let tabsToSet: Tabs.TabItem[] = [];
566+
let usedSavedTabs = false;
567+
568+
// Try to load saved tabs first
569+
try {
570+
const savedTabPaths = await getOpenTabs(project.projectName);
571+
572+
if (savedTabPaths.length > 0) {
573+
// Filter saved tabs to only include those that still exist in the project
574+
const validSavedTabs = savedTabPaths.filter((tabPath: string) => {
575+
const module = storageProject.findModuleByModulePath(project!, tabPath);
576+
return module !== null;
577+
});
578+
579+
if (validSavedTabs.length > 0) {
580+
usedSavedTabs = true;
581+
// Convert paths back to TabItem objects
582+
tabsToSet = validSavedTabs.map((path: string) => {
583+
const module = storageProject.findModuleByModulePath(project!, path);
584+
if (!module) return null;
585+
586+
let type: TabType;
587+
let title: string;
588+
589+
switch (module.moduleType) {
590+
case storageModule.ModuleType.ROBOT:
591+
type = TabType.ROBOT;
592+
title = t('ROBOT');
593+
break;
594+
case storageModule.ModuleType.MECHANISM:
595+
type = TabType.MECHANISM;
596+
title = module.className;
597+
break;
598+
case storageModule.ModuleType.OPMODE:
599+
type = TabType.OPMODE;
600+
title = module.className;
601+
break;
602+
default:
603+
return null;
604+
}
605+
606+
return {
607+
key: path,
608+
title,
609+
type,
610+
};
611+
}).filter((item): item is Tabs.TabItem => item !== null);
612+
}
613+
}
614+
} catch (error) {
615+
console.error('Failed to load saved tabs:', error);
616+
}
617+
618+
// If no saved tabs or loading failed, create default tabs (all project files)
619+
if (tabsToSet.length === 0) {
620+
tabsToSet = [
621+
{
622+
key: project.robot.modulePath,
623+
title: t('ROBOT'),
624+
type: TabType.ROBOT,
625+
}
626+
];
627+
628+
// Add all mechanisms
629+
project.mechanisms.forEach((mechanism) => {
630+
tabsToSet.push({
631+
key: mechanism.modulePath,
632+
title: mechanism.className,
633+
type: TabType.MECHANISM,
634+
});
635+
});
636+
637+
// Add all opmodes
638+
project.opModes.forEach((opmode) => {
639+
tabsToSet.push({
640+
key: opmode.modulePath,
641+
title: opmode.className,
642+
type: TabType.OPMODE,
643+
});
644+
});
645+
}
646+
647+
// Set the tabs
648+
setTabItems(tabsToSet);
649+
650+
// Only set active tab to robot if no active tab is set or if the current active tab no longer exists
651+
const currentActiveTabExists = tabsToSet.some(tab => tab.key === activeTab);
652+
if (!activeTab || !currentActiveTabExists) {
653+
setActiveTab(project.robot.modulePath);
654+
}
655+
656+
// Only auto-save if we didn't use saved tabs (i.e., this is a new project or the first time)
657+
if (!usedSavedTabs) {
658+
try {
659+
const tabPaths = tabsToSet.map(tab => tab.key);
660+
await updateOpenTabs(project.projectName, tabPaths);
661+
} catch (error) {
662+
console.error('Failed to auto-save default tabs:', error);
663+
}
664+
}
665+
666+
setIsLoadingTabs(false);
667+
}
668+
};
669+
670+
loadSavedTabs();
671+
}, [project?.projectName, isLoading, getOpenTabs]);
672+
673+
// Update tab items when modules in project change (for title updates, etc)
674+
React.useEffect(() => {
675+
if (project && tabItems.length > 0) {
676+
// Update existing tab titles in case they changed
677+
const updatedTabs = tabItems.map(tab => {
678+
const module = storageProject.findModuleByModulePath(project, tab.key);
679+
if (module && module.moduleType !== storageModule.ModuleType.ROBOT) {
680+
return { ...tab, title: module.className };
681+
}
682+
return tab;
683+
});
589684

590-
// Only set active tab to robot if no active tab is set or if the current active tab no longer exists
591-
const currentActiveTabExists = tabs.some(tab => tab.key === activeTab);
592-
if (!activeTab || !currentActiveTabExists) {
593-
setActiveTab(project.robot.modulePath);
685+
// Only update if something actually changed
686+
const titlesChanged = updatedTabs.some((tab, index) => tab.title !== tabItems[index]?.title);
687+
if (titlesChanged) {
688+
setTabItems(updatedTabs);
594689
}
595690
}
596691
}, [modulePathToContentText]);
597692

693+
// Save tabs when tab list changes (but not during initial loading)
694+
React.useEffect(() => {
695+
const saveTabs = async () => {
696+
// Don't save tabs while we're in the process of loading them
697+
if (project?.projectName && tabItems.length > 0 && !isLoadingTabs) {
698+
try {
699+
const tabPaths = tabItems.map(tab => tab.key);
700+
await updateOpenTabs(project.projectName, tabPaths);
701+
} catch (error) {
702+
console.error('Failed to save open tabs:', error);
703+
// Don't show alert for save failures as they're not critical to user workflow
704+
}
705+
}
706+
};
707+
708+
// Use a small delay to debounce rapid tab changes
709+
const timeoutId = setTimeout(saveTabs, 100);
710+
return () => clearTimeout(timeoutId);
711+
}, [tabItems, project?.projectName, isLoadingTabs]);
712+
598713
const { Sider, Content } = Antd.Layout;
599714

600715
return (

src/reactComponents/UserSettingsProvider.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const USER_THEME_KEY = 'userTheme';
3131
const DEFAULT_LANGUAGE = 'en';
3232
const DEFAULT_THEME = 'dark';
3333

34+
/** Helper function to generate project-specific storage key for open tabs. */
35+
const getUserOptionsKey = (projectName: string): string => `user_options_${projectName}`;
36+
3437
/** User settings interface. */
3538
export interface UserSettings {
3639
language: string;
@@ -42,6 +45,8 @@ export interface UserSettingsContextType {
4245
settings: UserSettings;
4346
updateLanguage: (language: string) => Promise<void>;
4447
updateTheme: (theme: string) => Promise<void>;
48+
updateOpenTabs: (projectName: string, tabPaths: string[]) => Promise<void>;
49+
getOpenTabs: (projectName: string) => Promise<string[]>;
4550
isLoading: boolean;
4651
error: string | null;
4752
storage: Storage | null;
@@ -134,10 +139,52 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({
134139
}
135140
};
136141

142+
/** Update open tabs for a specific project. */
143+
const updateOpenTabs = async (projectName: string, tabPaths: string[]): Promise<void> => {
144+
try {
145+
setError(null);
146+
147+
if (storage) {
148+
const storageKey = getUserOptionsKey(projectName);
149+
await storage.saveEntry(storageKey, JSON.stringify(tabPaths));
150+
} else {
151+
console.warn('No storage available, cannot save open tabs');
152+
}
153+
} catch (err) {
154+
setError(`Failed to save open tabs: ${err}`);
155+
console.error('Error saving open tabs:', err);
156+
throw err;
157+
}
158+
};
159+
160+
/** Get open tabs for a specific project. */
161+
const getOpenTabs = async (projectName: string): Promise<string[]> => {
162+
try {
163+
if (!storage) {
164+
return [];
165+
}
166+
167+
const storageKey = getUserOptionsKey(projectName);
168+
const tabsJson = await storage.fetchEntry(storageKey, JSON.stringify([]));
169+
170+
try {
171+
return JSON.parse(tabsJson);
172+
} catch (error) {
173+
console.warn(`Failed to parse open tabs for project ${projectName}, using default:`, error);
174+
return [];
175+
}
176+
} catch (err) {
177+
console.error(`Error loading open tabs for project ${projectName}:`, err);
178+
return [];
179+
}
180+
};
181+
137182
const contextValue: UserSettingsContextType = {
138183
settings,
139184
updateLanguage,
140185
updateTheme,
186+
updateOpenTabs,
187+
getOpenTabs,
141188
isLoading,
142189
error,
143190
storage: storage || null,
@@ -149,3 +196,12 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({
149196
</UserSettingsContext.Provider>
150197
);
151198
};
199+
200+
/** Custom hook to use user settings context. */
201+
export const useUserSettings = (): UserSettingsContextType => {
202+
const context = React.useContext(UserSettingsContext);
203+
if (!context) {
204+
throw new Error('useUserSettings must be used within a UserSettingsProvider');
205+
}
206+
return context;
207+
};

0 commit comments

Comments
 (0)