refactor: 更新 RouterProvider 以过滤非法页面并合并配置

This commit is contained in:
2026-07-05 22:20:04 +08:00
parent 9aa7a51f0b
commit 43038c5edb
2 changed files with 88 additions and 32 deletions
+35 -30
View File
@@ -28,10 +28,11 @@ const isValidPage = (page: unknown): page is PageType => {
}; };
/** /**
* 校验页面列表是否合法 * 页面列表中过滤非法项(如已移除的 dashboard),保留合法 PageType
*/ */
const isValidPageList = (pages: unknown): pages is PageType[] => { const sanitizePageList = (pages: unknown): PageType[] | null => {
return Array.isArray(pages) && pages.every(isValidPage); if (!Array.isArray(pages)) return null;
return pages.filter(isValidPage);
}; };
/** /**
@@ -47,6 +48,15 @@ const mergeWithDefaults = (saved: PageType[], defaults: PageType[]): PageType[]
return [...preserved, ...newItems]; return [...preserved, ...newItems];
}; };
/**
* 解析并合并页面列表:过滤非法项后与默认列表合并
*/
const resolvePageList = (pages: unknown, defaults: PageType[]): PageType[] => {
const sanitized = sanitizePageList(pages);
if (sanitized === null) return defaults;
return mergeWithDefaults(sanitized, defaults);
};
interface RouterContextType { interface RouterContextType {
currentPage: PageType; currentPage: PageType;
visiblePages: PageType[]; visiblePages: PageType[];
@@ -81,26 +91,19 @@ export function RouterProvider({
getSyncSnapshot(syncKey as string, defaultRoute, isValidPage), getSyncSnapshot(syncKey as string, defaultRoute, isValidPage),
); );
const [visiblePages, setVisiblePages] = useState<PageType[]>(() => { const [visiblePages, setVisiblePages] = useState<PageType[]>(() =>
const snapshot = getSyncSnapshot( resolvePageList(
visiblePagesKey as string, getSyncSnapshot(visiblePagesKey as string, null),
getDefaultVisibleFeatureKeys(), getDefaultVisibleFeatureKeys(),
isValidPageList, ),
); );
return mergeWithDefaults(snapshot, getDefaultVisibleFeatureKeys());
});
const [pageOrder, setPageOrder] = useState<PageType[]>(() => { const [pageOrder, setPageOrder] = useState<PageType[]>(() =>
const snapshot = getSyncSnapshot( resolvePageList(getSyncSnapshot(pageOrderKey as string, null), getDefaultPageOrder()),
pageOrderKey as string,
getDefaultPageOrder(),
isValidPageList,
); );
return mergeWithDefaults(snapshot, getDefaultPageOrder());
});
const [recentlyUsedTools, setRecentlyUsedTools] = useState<PageType[]>(() => const [recentlyUsedTools, setRecentlyUsedTools] = useState<PageType[]>(
getSyncSnapshot('app/recentlyUsedTools', [], isValidPageList), () => sanitizePageList(getSyncSnapshot('app/recentlyUsedTools', null)) ?? [],
); );
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
@@ -126,14 +129,15 @@ export function RouterProvider({
if (isValidPage(savedRoute) && syncRoute && !hasUserNavigatedRef.current) { if (isValidPage(savedRoute) && syncRoute && !hasUserNavigatedRef.current) {
setCurrentPage(savedRoute); setCurrentPage(savedRoute);
} }
if (isValidPageList(savedVisiblePages)) { if (sanitizePageList(savedVisiblePages) !== null) {
setVisiblePages(mergeWithDefaults(savedVisiblePages, getDefaultVisibleFeatureKeys())); setVisiblePages(resolvePageList(savedVisiblePages, getDefaultVisibleFeatureKeys()));
} }
if (isValidPageList(savedPageOrder) && savedPageOrder.length > 0) { if (sanitizePageList(savedPageOrder) !== null) {
setPageOrder(mergeWithDefaults(savedPageOrder, getDefaultPageOrder())); setPageOrder(resolvePageList(savedPageOrder, getDefaultPageOrder()));
} }
if (isValidPageList(savedRecentTools)) { const sanitizedRecentTools = sanitizePageList(savedRecentTools);
setRecentlyUsedTools(savedRecentTools); if (sanitizedRecentTools !== null) {
setRecentlyUsedTools(sanitizedRecentTools);
} }
canPersistRef.current = true; canPersistRef.current = true;
@@ -249,20 +253,21 @@ export function RouterProvider({
} }
if (changes[visiblePagesKey as string]) { if (changes[visiblePagesKey as string]) {
const newPages = changes[visiblePagesKey as string].newValue; const newPages = changes[visiblePagesKey as string].newValue;
if (isValidPageList(newPages)) { if (sanitizePageList(newPages) !== null) {
setVisiblePages(mergeWithDefaults(newPages, getDefaultVisibleFeatureKeys())); setVisiblePages(resolvePageList(newPages, getDefaultVisibleFeatureKeys()));
} }
} }
if (changes[pageOrderKey as string]) { if (changes[pageOrderKey as string]) {
const newOrder = changes[pageOrderKey as string].newValue; const newOrder = changes[pageOrderKey as string].newValue;
if (isValidPageList(newOrder)) { if (sanitizePageList(newOrder) !== null) {
setPageOrder(mergeWithDefaults(newOrder, getDefaultPageOrder())); setPageOrder(resolvePageList(newOrder, getDefaultPageOrder()));
} }
} }
if (changes['app/recentlyUsedTools']) { if (changes['app/recentlyUsedTools']) {
const newRecent = changes['app/recentlyUsedTools'].newValue; const newRecent = changes['app/recentlyUsedTools'].newValue;
if (isValidPageList(newRecent)) { const sanitizedRecent = sanitizePageList(newRecent);
setRecentlyUsedTools(newRecent); if (sanitizedRecent !== null) {
setRecentlyUsedTools(sanitizedRecent);
} }
} }
if (changes['contextMenu/pendingData']) { if (changes['contextMenu/pendingData']) {
@@ -193,6 +193,57 @@ describe('RouterProvider', () => {
}); });
}); });
it('storage 列表含 dashboard 时应过滤并保留其余自定义配置', async () => {
mockStorageBatch({
'app/popupVisiblePages': ['dashboard', 'jwt', 'timestamp'],
'app/popupPageOrder': ['jwt', 'dashboard', 'timestamp'],
'app/recentlyUsedTools': ['dashboard', 'jwt'],
});
render(
<RouterProvider visiblePagesKey="app/popupVisiblePages" pageOrderKey="app/popupPageOrder">
<TestComponent />
</RouterProvider>,
);
await waitFor(() => {
const visiblePages = screen.getByTestId('visible-pages').textContent!;
const pageOrder = screen.getByTestId('page-order').textContent!;
expect(visiblePages.startsWith('jwt,timestamp')).toBe(true);
expect(pageOrder.startsWith('jwt,timestamp')).toBe(true);
expect(visiblePages).not.toContain('dashboard');
expect(pageOrder).not.toContain('dashboard');
});
});
it('localStorage 快照含 dashboard 时应过滤并保留其余配置', async () => {
localStorage.setItem(
'snapshot/app/popupVisiblePages',
JSON.stringify(['dashboard', 'jwt', 'timestamp']),
);
localStorage.setItem(
'snapshot/app/popupPageOrder',
JSON.stringify(['jwt', 'dashboard', 'timestamp']),
);
mockStorageBatch();
render(
<RouterProvider visiblePagesKey="app/popupVisiblePages" pageOrderKey="app/popupPageOrder">
<TestComponent />
</RouterProvider>,
);
const visiblePages = screen.getByTestId('visible-pages').textContent!;
const pageOrder = screen.getByTestId('page-order').textContent!;
expect(visiblePages.startsWith('jwt,timestamp')).toBe(true);
expect(pageOrder.startsWith('jwt,timestamp')).toBe(true);
expect(visiblePages).not.toContain('dashboard');
expect(pageOrder).not.toContain('dashboard');
});
it('应该从 localStorage 快照合并缺失的新功能', async () => { it('应该从 localStorage 快照合并缺失的新功能', async () => {
const oldVisiblePages = [ const oldVisiblePages = [
'timestamp', 'timestamp',