mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-30 00:29:46 +08:00
feat(core): support save and restore display preference in all docs (#12315)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Display preferences and selected collections are now saved and restored across sessions, providing a persistent and personalized experience in the All Documents page. - **Refactor** - Display settings menus and related components have been updated to use a controlled component pattern, allowing preferences to be managed externally for improved consistency and flexibility. - Preference state management has been consolidated, simplifying how display options are handled throughout the interface. - Various headers and detail views now accept display preferences and update callbacks as props, enabling external control of display settings. - Components previously relying on internal context and reactive streams were refactored to receive explicit props and callbacks for state management. - **Bug Fixes** - Improved collection activation logic to prevent unnecessary updates when the selected collection is already active. - Added fallback default view to ensure consistent display in document list items. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { type MenuProps } from '@affine/component';
|
||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||
import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle';
|
||||
import type { DocListItemView } from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import type { ExplorerDisplayPreference } from '@affine/core/components/explorer/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './all-page-header.css';
|
||||
|
||||
@@ -13,14 +16,36 @@ const menuProps: Partial<MenuProps> = {
|
||||
sideOffset: 8,
|
||||
},
|
||||
};
|
||||
export const AllDocsHeader = () => {
|
||||
export const AllDocsHeader = ({
|
||||
displayPreference,
|
||||
onDisplayPreferenceChange,
|
||||
}: {
|
||||
displayPreference: ExplorerDisplayPreference;
|
||||
onDisplayPreferenceChange: (
|
||||
displayPreference: ExplorerDisplayPreference
|
||||
) => void;
|
||||
}) => {
|
||||
const handleViewChange = useCallback(
|
||||
(view: DocListItemView) => {
|
||||
onDisplayPreferenceChange({ ...displayPreference, view });
|
||||
},
|
||||
[displayPreference, onDisplayPreferenceChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<ExplorerNavigation active="docs" />
|
||||
|
||||
<div className={styles.actions}>
|
||||
<ViewToggle />
|
||||
<ExplorerDisplayMenuButton menuProps={menuProps} />
|
||||
<ViewToggle
|
||||
view={displayPreference.view ?? 'list'}
|
||||
onViewChange={handleViewChange}
|
||||
/>
|
||||
<ExplorerDisplayMenuButton
|
||||
menuProps={menuProps}
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={onDisplayPreferenceChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DocExplorerContext,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||
import type { ExplorerDisplayPreference } from '@affine/core/components/explorer/types';
|
||||
import { Filters } from '@affine/core/components/filter';
|
||||
import {
|
||||
CollectionService,
|
||||
@@ -12,9 +13,10 @@ import {
|
||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WorkspaceLocalState } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
ViewBody,
|
||||
@@ -29,11 +31,22 @@ import { AllDocsHeader } from './all-page-header';
|
||||
import { MigrationAllDocsDataNotification } from './migration-data';
|
||||
import { PinnedCollections } from './pinned-collections';
|
||||
|
||||
interface AllDocsStateSave extends ExplorerDisplayPreference {
|
||||
selectedCollectionId: string | null;
|
||||
}
|
||||
|
||||
export const AllPage = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const collectionService = useService(CollectionService);
|
||||
const pinnedCollectionService = useService(PinnedCollectionService);
|
||||
const workspaceLocalState = useService(WorkspaceLocalState);
|
||||
|
||||
const [initialState] = useState(() => {
|
||||
return workspaceLocalState.get<AllDocsStateSave>(
|
||||
'allDocsDisplayPreference'
|
||||
);
|
||||
});
|
||||
|
||||
const isCollectionDataReady = useLiveData(
|
||||
collectionService.collectionDataReady$
|
||||
@@ -49,7 +62,7 @@ export const AllPage = () => {
|
||||
|
||||
const [selectedCollectionId, setSelectedCollectionId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
>(initialState?.selectedCollectionId ?? null);
|
||||
const selectedCollection = useLiveData(
|
||||
selectedCollectionId
|
||||
? collectionService.collection$(selectedCollectionId)
|
||||
@@ -80,10 +93,26 @@ export const AllPage = () => {
|
||||
|
||||
const [tempFilters, setTempFilters] = useState<FilterParams[] | null>(null);
|
||||
|
||||
const [explorerContextValue] = useState(createDocExplorerContext);
|
||||
const [explorerContextValue] = useState(() =>
|
||||
createDocExplorerContext(initialState)
|
||||
);
|
||||
|
||||
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
||||
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
||||
const displayPreference = useLiveData(
|
||||
explorerContextValue.displayPreference$
|
||||
);
|
||||
|
||||
const allDocsStateSave = useMemo(() => {
|
||||
return {
|
||||
...displayPreference,
|
||||
selectedCollectionId,
|
||||
};
|
||||
}, [displayPreference, selectedCollectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
workspaceLocalState.set('allDocsDisplayPreference', allDocsStateSave);
|
||||
}, [allDocsStateSave, workspaceLocalState]);
|
||||
|
||||
const { openPromptModal } = usePromptModal();
|
||||
|
||||
@@ -187,6 +216,11 @@ export const AllPage = () => {
|
||||
setTempFilters(null);
|
||||
}, []);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setSelectedCollectionId(null);
|
||||
setTempFilters(null);
|
||||
}, []);
|
||||
|
||||
const handleEditCollection = useCallback(
|
||||
(collectionId: string) => {
|
||||
const collection = collectionService.collection$(collectionId).value;
|
||||
@@ -250,12 +284,22 @@ export const AllPage = () => {
|
||||
setTempFilters([params]);
|
||||
}, []);
|
||||
|
||||
const handleDisplayPreferenceChange = useCallback(
|
||||
(displayPreference: ExplorerDisplayPreference) => {
|
||||
explorerContextValue.displayPreference$.next(displayPreference);
|
||||
},
|
||||
[explorerContextValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<ViewTitle title={t['All pages']()} />
|
||||
<ViewIcon icon="allDocs" />
|
||||
<ViewHeader>
|
||||
<AllDocsHeader />
|
||||
<AllDocsHeader
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={handleDisplayPreferenceChange}
|
||||
/>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
@@ -263,7 +307,7 @@ export const AllPage = () => {
|
||||
<div className={styles.pinnedCollection}>
|
||||
<PinnedCollections
|
||||
activeCollectionId={selectedCollectionId}
|
||||
onActiveAll={() => setSelectedCollectionId(null)}
|
||||
onActiveAll={handleSelectAll}
|
||||
onActiveCollection={handleSelectCollection}
|
||||
onAddFilter={handleNewTempFilter}
|
||||
onEditCollection={handleEditCollection}
|
||||
|
||||
@@ -108,7 +108,10 @@ export const PinnedCollections = ({
|
||||
<div
|
||||
className={styles.item}
|
||||
data-active={activeCollectionId === null ? 'true' : undefined}
|
||||
onClick={onActiveAll}
|
||||
onClick={() =>
|
||||
// only fire onActiveAll if the collection is not already active
|
||||
activeCollectionId !== null ? onActiveAll() : undefined
|
||||
}
|
||||
role="button"
|
||||
>
|
||||
{t['com.affine.all-docs.pinned-collection.all']()}
|
||||
@@ -118,7 +121,12 @@ export const PinnedCollections = ({
|
||||
key={record.collectionId}
|
||||
record={record}
|
||||
isActive={activeCollectionId === record.collectionId}
|
||||
onClick={() => onActiveCollection(record.collectionId)}
|
||||
onClick={() =>
|
||||
// only fire onActiveCollection if the collection is not already active
|
||||
activeCollectionId !== record.collectionId
|
||||
? onActiveCollection(record.collectionId)
|
||||
: undefined
|
||||
}
|
||||
onClickEdit={() => onEditCollection(record.collectionId)}
|
||||
onClickRemove={() => {
|
||||
const nextCollectionId = pinnedCollections[index - 1]?.collectionId;
|
||||
|
||||
@@ -2,15 +2,32 @@ import { FlexWrapper } from '@affine/component';
|
||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||
import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle';
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import type { ExplorerDisplayPreference } from '@affine/core/components/explorer/types';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
|
||||
export const CollectionDetailHeader = () => {
|
||||
export const CollectionDetailHeader = ({
|
||||
displayPreference,
|
||||
onDisplayPreferenceChange,
|
||||
}: {
|
||||
displayPreference: ExplorerDisplayPreference;
|
||||
onDisplayPreferenceChange: (
|
||||
displayPreference: ExplorerDisplayPreference
|
||||
) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Header
|
||||
right={
|
||||
<FlexWrapper gap={16}>
|
||||
<ViewToggle />
|
||||
<ExplorerDisplayMenuButton />
|
||||
<ViewToggle
|
||||
view={displayPreference.view ?? 'list'}
|
||||
onViewChange={view => {
|
||||
onDisplayPreferenceChange({ ...displayPreference, view });
|
||||
}}
|
||||
/>
|
||||
<ExplorerDisplayMenuButton
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={onDisplayPreferenceChange}
|
||||
/>
|
||||
</FlexWrapper>
|
||||
}
|
||||
left={<ExplorerNavigation active="collections" />}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DocExplorerContext,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||
import type { ExplorerDisplayPreference } from '@affine/core/components/explorer/types';
|
||||
import {
|
||||
type Collection,
|
||||
CollectionService,
|
||||
@@ -45,11 +46,21 @@ export const CollectionDetail = ({
|
||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
|
||||
const displayPreference = useLiveData(
|
||||
explorerContextValue.displayPreference$
|
||||
);
|
||||
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
||||
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
||||
const rules = useLiveData(collection.rules$);
|
||||
const allowList = useLiveData(collection.allowList$);
|
||||
|
||||
const handleDisplayPreferenceChange = useCallback(
|
||||
(displayPreference: ExplorerDisplayPreference) => {
|
||||
explorerContextValue.displayPreference$.next(displayPreference);
|
||||
},
|
||||
[explorerContextValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch({
|
||||
@@ -95,7 +106,10 @@ export const CollectionDetail = ({
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<ViewHeader>
|
||||
<CollectionDetailHeader />
|
||||
<CollectionDetailHeader
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={handleDisplayPreferenceChange}
|
||||
/>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<FlexWrapper flexDirection="column" alignItems="stretch" width="100%">
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import type { ExplorerDisplayPreference } from '@affine/core/components/explorer/types';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
|
||||
export const TagDetailHeader = () => {
|
||||
export const TagDetailHeader = ({
|
||||
displayPreference,
|
||||
onDisplayPreferenceChange,
|
||||
}: {
|
||||
displayPreference: ExplorerDisplayPreference;
|
||||
onDisplayPreferenceChange: (
|
||||
displayPreference: ExplorerDisplayPreference
|
||||
) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Header
|
||||
left={<ExplorerNavigation active={'tags'} />}
|
||||
right={<ExplorerDisplayMenuButton />}
|
||||
right={
|
||||
<ExplorerDisplayMenuButton
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={onDisplayPreferenceChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
DocExplorerContext,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||
import type { ExplorerDisplayPreference } from '@affine/core/components/explorer/types';
|
||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
ViewTitle,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { PageNotFound } from '../../404';
|
||||
@@ -36,6 +37,9 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
const tagList = useService(TagService).tagList;
|
||||
const currentTag = useLiveData(tagList.tagByTagId$(tagId));
|
||||
|
||||
const displayPreference = useLiveData(
|
||||
explorerContextValue.displayPreference$
|
||||
);
|
||||
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
||||
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
||||
const groups = useLiveData(explorerContextValue.groups$);
|
||||
@@ -109,12 +113,23 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
return <PageNotFound />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const handleDisplayPreferenceChange = useCallback(
|
||||
(displayPreference: ExplorerDisplayPreference) => {
|
||||
explorerContextValue.displayPreference$.next(displayPreference);
|
||||
},
|
||||
[explorerContextValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<ViewTitle title={tagName ?? 'Untitled'} />
|
||||
<ViewIcon icon="tag" />
|
||||
<ViewHeader>
|
||||
<TagDetailHeader />
|
||||
<TagDetailHeader
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={handleDisplayPreferenceChange}
|
||||
/>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
|
||||
Reference in New Issue
Block a user