From ec140da0d92269beab33766dfdf7e6d982ec1ada Mon Sep 17 00:00:00 2001 From: pengx17 Date: Tue, 10 Dec 2024 02:28:20 +0000 Subject: [PATCH] feat(core): remember backlink open/close state (#9073) fix AF-1850, AF-1883 --- .../common/infra/src/modules/storage/index.ts | 26 ++++++++-- .../src/modules/storage/providers/global.ts | 10 +++- .../src/modules/storage/services/global.ts | 12 ++++- .../bi-directional-link-panel.tsx | 50 +++++++++++++++++-- packages/frontend/core/src/modules/index.ts | 2 + .../impls/{local-storage.ts => storage.ts} | 43 +++++++++++----- .../core/src/modules/storage/index.ts | 14 +++++- 7 files changed, 132 insertions(+), 25 deletions(-) rename packages/frontend/core/src/modules/storage/impls/{local-storage.ts => storage.ts} (57%) diff --git a/packages/common/infra/src/modules/storage/index.ts b/packages/common/infra/src/modules/storage/index.ts index f718f1f6c6..2032f44a66 100644 --- a/packages/common/infra/src/modules/storage/index.ts +++ b/packages/common/infra/src/modules/storage/index.ts @@ -1,17 +1,35 @@ -export { GlobalCache, GlobalState } from './providers/global'; -export { GlobalCacheService, GlobalStateService } from './services/global'; +export { + GlobalCache, + GlobalSessionState, + GlobalState, +} from './providers/global'; +export { + GlobalCacheService, + GlobalSessionStateService, + GlobalStateService, +} from './services/global'; import type { Framework } from '../../framework'; import { MemoryMemento } from '../../storage'; -import { GlobalCache, GlobalState } from './providers/global'; -import { GlobalCacheService, GlobalStateService } from './services/global'; +import { + GlobalCache, + GlobalSessionState, + GlobalState, +} from './providers/global'; +import { + GlobalCacheService, + GlobalSessionStateService, + GlobalStateService, +} from './services/global'; export const configureGlobalStorageModule = (framework: Framework) => { framework.service(GlobalStateService, [GlobalState]); framework.service(GlobalCacheService, [GlobalCache]); + framework.service(GlobalSessionStateService, [GlobalSessionState]); }; export const configureTestingGlobalStorage = (framework: Framework) => { framework.impl(GlobalCache, MemoryMemento); framework.impl(GlobalState, MemoryMemento); + framework.impl(GlobalSessionState, MemoryMemento); }; diff --git a/packages/common/infra/src/modules/storage/providers/global.ts b/packages/common/infra/src/modules/storage/providers/global.ts index e320cab98c..5f127e9521 100644 --- a/packages/common/infra/src/modules/storage/providers/global.ts +++ b/packages/common/infra/src/modules/storage/providers/global.ts @@ -16,5 +16,13 @@ export const GlobalState = createIdentifier('GlobalState'); * Cache may be deleted from time to time, business logic should not rely on cache. */ export interface GlobalCache extends Memento {} - export const GlobalCache = createIdentifier('GlobalCache'); + +/** + * A memento object that stores session state. + * + * Session state is not persisted, it will be cleared when the application is closed. (thinking about sessionStorage) + */ +export interface GlobalSessionState extends Memento {} +export const GlobalSessionState = + createIdentifier('GlobalSessionState'); diff --git a/packages/common/infra/src/modules/storage/services/global.ts b/packages/common/infra/src/modules/storage/services/global.ts index 2c5ffda4bd..d121b46a5f 100644 --- a/packages/common/infra/src/modules/storage/services/global.ts +++ b/packages/common/infra/src/modules/storage/services/global.ts @@ -1,5 +1,9 @@ import { Service } from '../../../framework'; -import type { GlobalCache, GlobalState } from '../providers/global'; +import type { + GlobalCache, + GlobalSessionState, + GlobalState, +} from '../providers/global'; export class GlobalStateService extends Service { constructor(public readonly globalState: GlobalState) { @@ -12,3 +16,9 @@ export class GlobalCacheService extends Service { super(); } } + +export class GlobalSessionStateService extends Service { + constructor(public readonly globalSessionState: GlobalSessionState) { + super(); + } +} diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx index 689be5b91f..f38a17eef4 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/bi-directional-link-panel.tsx @@ -16,7 +16,9 @@ import type { JobMiddleware } from '@blocksuite/affine/store'; import { ToggleExpandIcon } from '@blocksuite/icons/rc'; import * as Collapsible from '@radix-ui/react-collapsible'; import { + DocService, getAFFiNEWorkspaceSchema, + GlobalSessionStateService, LiveData, useFramework, useLiveData, @@ -47,16 +49,50 @@ const BlocksuiteTextRenderer = createReactComponentFromLit({ elementClass: TextRenderer, }); +const PREFIX = 'bi-directional-link-panel-collapse:'; + +const useBiDirectionalLinkPanelCollapseState = ( + docId: string, + linkDocId?: string +) => { + const { globalSessionStateService } = useServices({ + GlobalSessionStateService, + }); + + const path = linkDocId ? docId + ':' + linkDocId : docId; + + const [open, setOpen] = useState( + globalSessionStateService.globalSessionState.get(PREFIX + path) ?? false + ); + + const wrappedSetOpen = useCallback( + (open: boolean) => { + setOpen(open); + globalSessionStateService.globalSessionState.set(PREFIX + path, open); + }, + [path, globalSessionStateService] + ); + + return [open, wrappedSetOpen] as const; +}; + const CollapsibleSection = ({ title, children, length, + docId, + linkDocId, }: { title: ReactNode; children: ReactNode; length?: number; + docId: string; + linkDocId?: string; }) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useBiDirectionalLinkPanelCollapseState( + docId, + linkDocId + ); return ( @@ -114,15 +150,19 @@ const usePreviewExtensions = () => { }; export const BiDirectionalLinkPanel = () => { - const [show, setShow] = useState(false); - const { docLinksService, workspaceService } = useServices({ + const { docLinksService, workspaceService, docService } = useServices({ DocLinksService, WorkspaceService, + DocService, }); const [extensions, portals] = usePreviewExtensions(); const t = useI18n(); + const [show, setShow] = useBiDirectionalLinkPanelCollapseState( + docService.doc.id + ); + const links = useLiveData( show ? docLinksService.links.links$ : new LiveData([] as Link[]) ); @@ -157,7 +197,7 @@ export const BiDirectionalLinkPanel = () => { const handleClickShow = useCallback(() => { setShow(!show); - }, [show]); + }, [show, setShow]); const textRendererOptions = useMemo(() => { const docLinkBaseURLMiddleware: JobMiddleware = ({ adapterConfigs }) => { @@ -205,6 +245,8 @@ export const BiDirectionalLinkPanel = () => { key={linkGroup.docId} title={} length={linkGroup.links.length} + docId={docService.doc.id} + linkDocId={linkGroup.docId} >
{linkGroup.links.map(link => { diff --git a/packages/frontend/core/src/modules/index.ts b/packages/frontend/core/src/modules/index.ts index db85cf23b8..9e274b0d17 100644 --- a/packages/frontend/core/src/modules/index.ts +++ b/packages/frontend/core/src/modules/index.ts @@ -27,6 +27,7 @@ import { configurePermissionsModule } from './permissions'; import { configureQuickSearchModule } from './quicksearch'; import { configureShareDocsModule } from './share-doc'; import { configureShareSettingModule } from './share-setting'; +import { configureCommonGlobalStorageImpls } from './storage'; import { configureSystemFontFamilyModule } from './system-font-family'; import { configureTagModule } from './tag'; import { configureTelemetryModule } from './telemetry'; @@ -71,4 +72,5 @@ export function configureCommonModules(framework: Framework) { configureOpenInApp(framework); configAtMenuConfigModule(framework); configureDndModule(framework); + configureCommonGlobalStorageImpls(framework); } diff --git a/packages/frontend/core/src/modules/storage/impls/local-storage.ts b/packages/frontend/core/src/modules/storage/impls/storage.ts similarity index 57% rename from packages/frontend/core/src/modules/storage/impls/local-storage.ts rename to packages/frontend/core/src/modules/storage/impls/storage.ts index 141f6a0b5b..99aa43caca 100644 --- a/packages/frontend/core/src/modules/storage/impls/local-storage.ts +++ b/packages/frontend/core/src/modules/storage/impls/storage.ts @@ -1,13 +1,21 @@ -import type { GlobalCache, GlobalState, Memento } from '@toeverything/infra'; +import type { + GlobalCache, + GlobalSessionState, + GlobalState, + Memento, +} from '@toeverything/infra'; import { Observable } from 'rxjs'; -export class LocalStorageMemento implements Memento { - constructor(private readonly prefix: string) {} +export class StorageMemento implements Memento { + constructor( + private readonly storage: Storage, + private readonly prefix: string + ) {} keys(): string[] { const keys: string[] = []; - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); + for (let i = 0; i < this.storage.length; i++) { + const key = this.storage.key(i); if (key && key.startsWith(this.prefix)) { keys.push(key.slice(this.prefix.length)); } @@ -16,12 +24,12 @@ export class LocalStorageMemento implements Memento { } get(key: string): T | undefined { - const json = localStorage.getItem(this.prefix + key); + const json = this.storage.getItem(this.prefix + key); return json ? JSON.parse(json) : undefined; } watch(key: string): Observable { return new Observable(subscriber => { - const json = localStorage.getItem(this.prefix + key); + const json = this.storage.getItem(this.prefix + key); const first = json ? JSON.parse(json) : undefined; subscriber.next(first); @@ -35,14 +43,14 @@ export class LocalStorageMemento implements Memento { }); } set(key: string, value: T): void { - localStorage.setItem(this.prefix + key, JSON.stringify(value)); + this.storage.setItem(this.prefix + key, JSON.stringify(value)); const channel = new BroadcastChannel(this.prefix + key); channel.postMessage(value); channel.close(); } del(key: string): void { - localStorage.removeItem(this.prefix + key); + this.storage.removeItem(this.prefix + key); } clear(): void { @@ -53,19 +61,28 @@ export class LocalStorageMemento implements Memento { } export class LocalStorageGlobalCache - extends LocalStorageMemento + extends StorageMemento implements GlobalCache { constructor() { - super('global-cache:'); + super(localStorage, 'global-cache:'); } } export class LocalStorageGlobalState - extends LocalStorageMemento + extends StorageMemento implements GlobalState { constructor() { - super('global-state:'); + super(localStorage, 'global-state:'); + } +} + +export class SessionStorageGlobalSessionState + extends StorageMemento + implements GlobalSessionState +{ + constructor() { + super(sessionStorage, 'global-session-state:'); } } diff --git a/packages/frontend/core/src/modules/storage/index.ts b/packages/frontend/core/src/modules/storage/index.ts index 628099a0fc..3f2cf1c5e9 100644 --- a/packages/frontend/core/src/modules/storage/index.ts +++ b/packages/frontend/core/src/modules/storage/index.ts @@ -1,11 +1,17 @@ -import { type Framework, GlobalCache, GlobalState } from '@toeverything/infra'; +import { + type Framework, + GlobalCache, + GlobalSessionState, + GlobalState, +} from '@toeverything/infra'; import { DesktopApiService } from '../desktop-api'; import { ElectronGlobalCache, ElectronGlobalState } from './impls/electron'; import { LocalStorageGlobalCache, LocalStorageGlobalState, -} from './impls/local-storage'; + SessionStorageGlobalSessionState, +} from './impls/storage'; export function configureLocalStorageStateStorageImpls(framework: Framework) { framework.impl(GlobalCache, LocalStorageGlobalCache); @@ -16,3 +22,7 @@ export function configureElectronStateStorageImpls(framework: Framework) { framework.impl(GlobalCache, ElectronGlobalCache, [DesktopApiService]); framework.impl(GlobalState, ElectronGlobalState, [DesktopApiService]); } + +export function configureCommonGlobalStorageImpls(framework: Framework) { + framework.impl(GlobalSessionState, SessionStorageGlobalSessionState); +}