feat(core): cache navigation collapsed state (#13315)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Collapsible section state in navigation panels is now managed using a
unified path-based approach, enabling more consistent and centralized
control across desktop and mobile interfaces.
* The collapsed/expanded state of navigation sections and nodes is now
persistently tracked using hierarchical paths, improving reliability
across sessions and devices.
* Internal state management is streamlined, with local state replaced by
a shared service, resulting in more predictable navigation behavior.

* **Chores**
* Removed obsolete types and legacy section management logic for
improved maintainability.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN
2025-07-25 18:19:21 +08:00
committed by GitHub
parent 7409940cc6
commit 1dd4bbbaba
26 changed files with 357 additions and 198 deletions
@@ -1,39 +0,0 @@
import { Entity, LiveData } from '@toeverything/infra';
import { map } from 'rxjs';
import type { GlobalCache } from '../../storage';
import type { CollapsibleSectionName } from '../types';
const DEFAULT_COLLAPSABLE_STATE: Record<CollapsibleSectionName, boolean> = {
recent: true,
favorites: false,
organize: false,
collections: true,
tags: true,
favoritesOld: true,
migrationFavorites: true,
others: false,
};
export class NavigationPanelSection extends Entity<{
name: CollapsibleSectionName;
}> {
name: CollapsibleSectionName = this.props.name;
key = `explorer.section.${this.name}`;
defaultValue = DEFAULT_COLLAPSABLE_STATE[this.name];
constructor(private readonly globalCache: GlobalCache) {
super();
}
collapsed$ = LiveData.from(
this.globalCache
.watch<boolean>(this.key)
.pipe(map(v => v ?? this.defaultValue)),
this.defaultValue
);
setCollapsed(collapsed: boolean) {
this.globalCache.set(this.key, collapsed);
}
}
@@ -1,15 +1,12 @@
import { type Framework } from '@toeverything/infra';
import { GlobalCache } from '../storage';
import { WorkspaceScope } from '../workspace';
import { NavigationPanelSection } from './entities/navigation-panel-section';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { NavigationPanelService } from './services/navigation-panel';
export { NavigationPanelService } from './services/navigation-panel';
export type { CollapsibleSectionName } from './types';
export function configureNavigationPanelModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(NavigationPanelService)
.entity(NavigationPanelSection, [GlobalCache]);
.service(NavigationPanelService, [GlobalCache, WorkspaceService]);
}
@@ -1,25 +1,47 @@
import { Service } from '@toeverything/infra';
import { LiveData, Service } from '@toeverything/infra';
import { NavigationPanelSection } from '../entities/navigation-panel-section';
import type { CollapsibleSectionName } from '../types';
import type { GlobalCache } from '../../storage/providers/global';
import type { WorkspaceService } from '../../workspace';
const allSectionName: Array<CollapsibleSectionName> = [
'recent', // mobile only
'favorites',
'organize',
'collections',
'tags',
'favoritesOld',
'migrationFavorites',
'others',
];
const DEFAULT_COLLAPSABLE_STATE: Record<string, boolean> = {
recent: true,
favorites: false,
organize: false,
collections: true,
tags: true,
favoritesOld: true,
migrationFavorites: true,
others: false,
};
export class NavigationPanelService extends Service {
readonly sections = allSectionName.reduce(
(prev, name) =>
Object.assign(prev, {
[name]: this.framework.createEntity(NavigationPanelSection, { name }),
}),
{} as Record<CollapsibleSectionName, NavigationPanelSection>
);
constructor(
private readonly globalCache: GlobalCache,
private readonly workspaceService: WorkspaceService
) {
super();
}
private readonly collapsedCache = new Map<string, LiveData<boolean>>();
collapsed$(path: string[]) {
const pathKey = path.join(':');
const key = `navigation:${this.workspaceService.workspace.id}:${pathKey}`;
const cached$ = this.collapsedCache.get(key);
if (!cached$) {
const liveData$ = LiveData.from(
this.globalCache.watch<boolean>(key),
undefined
).map(v => v ?? DEFAULT_COLLAPSABLE_STATE[pathKey] ?? true);
this.collapsedCache.set(key, liveData$);
return liveData$;
}
return cached$;
}
setCollapsed(path: string[], collapsed: boolean) {
const pathKey = path.join(':');
const key = `navigation:${this.workspaceService.workspace.id}:${pathKey}`;
this.globalCache.set(key, collapsed);
}
}
@@ -1,9 +0,0 @@
export type CollapsibleSectionName =
| 'recent'
| 'collections'
| 'favorites'
| 'tags'
| 'organize'
| 'favoritesOld'
| 'migrationFavorites'
| 'others';