mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 19:46:32 +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,19 +1,18 @@
|
||||
import { LiveData } from '@toeverything/infra';
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { DocListItemView } from './docs-view/doc-list-item';
|
||||
import type { ExplorerPreference } from './types';
|
||||
import type { ExplorerDisplayPreference } from './types';
|
||||
|
||||
export type DocExplorerContextType = {
|
||||
view$: LiveData<DocListItemView>;
|
||||
groups$: LiveData<Array<{ key: string; items: string[] }>>;
|
||||
collapsedGroups$: LiveData<string[]>;
|
||||
selectMode$?: LiveData<boolean>;
|
||||
selectedDocIds$: LiveData<string[]>;
|
||||
prevCheckAnchorId$?: LiveData<string | null>;
|
||||
displayPreference$: LiveData<ExplorerDisplayPreference>;
|
||||
} & {
|
||||
[K in keyof Omit<ExplorerPreference, 'filters'> as `${K}$`]: LiveData<
|
||||
ExplorerPreference[K]
|
||||
[K in keyof ExplorerDisplayPreference as `${K}$`]: LiveData<
|
||||
ExplorerDisplayPreference[K]
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -21,24 +20,51 @@ export const DocExplorerContext = createContext<DocExplorerContextType>(
|
||||
{} as any
|
||||
);
|
||||
|
||||
export const createDocExplorerContext = () =>
|
||||
({
|
||||
view$: new LiveData<DocListItemView>('list'),
|
||||
export const createDocExplorerContext = (
|
||||
initialState?: ExplorerDisplayPreference
|
||||
) => {
|
||||
const displayPreference$ = new LiveData<ExplorerDisplayPreference>(
|
||||
initialState ?? {}
|
||||
);
|
||||
return {
|
||||
groups$: new LiveData<Array<{ key: string; items: string[] }>>([]),
|
||||
collapsedGroups$: new LiveData<string[]>([]),
|
||||
selectMode$: new LiveData<boolean>(false),
|
||||
selectedDocIds$: new LiveData<string[]>([]),
|
||||
prevCheckAnchorId$: new LiveData<string | null>(null),
|
||||
groupBy$: new LiveData<ExplorerPreference['groupBy']>(undefined),
|
||||
orderBy$: new LiveData<ExplorerPreference['orderBy']>(undefined),
|
||||
displayProperties$: new LiveData<ExplorerPreference['displayProperties']>(
|
||||
[]
|
||||
displayPreference$: displayPreference$,
|
||||
view$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.view
|
||||
),
|
||||
showDocIcon$: new LiveData<ExplorerPreference['showDocIcon']>(true),
|
||||
showDocPreview$: new LiveData<ExplorerPreference['showDocPreview']>(true),
|
||||
quickFavorite$: new LiveData<ExplorerPreference['quickFavorite']>(false),
|
||||
quickSelect$: new LiveData<ExplorerPreference['quickSelect']>(false),
|
||||
quickSplit$: new LiveData<ExplorerPreference['quickSplit']>(false),
|
||||
quickTrash$: new LiveData<ExplorerPreference['quickTrash']>(false),
|
||||
quickTab$: new LiveData<ExplorerPreference['quickTab']>(false),
|
||||
}) satisfies DocExplorerContextType;
|
||||
groupBy$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.groupBy
|
||||
),
|
||||
orderBy$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.orderBy
|
||||
),
|
||||
displayProperties$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.displayProperties
|
||||
),
|
||||
showDocIcon$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.showDocIcon
|
||||
),
|
||||
showDocPreview$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.showDocPreview
|
||||
),
|
||||
quickFavorite$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.quickFavorite
|
||||
),
|
||||
quickSelect$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.quickSelect
|
||||
),
|
||||
quickSplit$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.quickSplit
|
||||
),
|
||||
quickTrash$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.quickTrash
|
||||
),
|
||||
quickTab$: displayPreference$.selector(
|
||||
displayPreference => displayPreference.quickTab
|
||||
),
|
||||
} satisfies DocExplorerContextType;
|
||||
};
|
||||
|
||||
@@ -11,63 +11,87 @@ import type {
|
||||
} from '@affine/core/modules/collection-rules/types';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import type React from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DocExplorerContext } from '../context';
|
||||
import type { ExplorerDisplayPreference } from '../types';
|
||||
import { GroupByList, GroupByName } from './group';
|
||||
import { OrderByList, OrderByName } from './order';
|
||||
import { DisplayProperties } from './properties';
|
||||
import { QuickActionsConfig } from './quick-actions';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const ExplorerDisplayMenu = () => {
|
||||
const ExplorerDisplayMenu = ({
|
||||
displayPreference,
|
||||
onDisplayPreferenceChange,
|
||||
}: {
|
||||
displayPreference: ExplorerDisplayPreference;
|
||||
onDisplayPreferenceChange: (
|
||||
displayPreference: ExplorerDisplayPreference
|
||||
) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const explorerContextValue = useContext(DocExplorerContext);
|
||||
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
||||
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(groupBy: GroupByParams) => {
|
||||
explorerContextValue.groupBy$?.next(groupBy);
|
||||
onDisplayPreferenceChange({ ...displayPreference, groupBy });
|
||||
},
|
||||
[explorerContextValue.groupBy$]
|
||||
[displayPreference, onDisplayPreferenceChange]
|
||||
);
|
||||
|
||||
const handleOrderByChange = useCallback(
|
||||
(orderBy: OrderByParams) => {
|
||||
explorerContextValue.orderBy$?.next(orderBy);
|
||||
onDisplayPreferenceChange({ ...displayPreference, orderBy });
|
||||
},
|
||||
[explorerContextValue.orderBy$]
|
||||
[displayPreference, onDisplayPreferenceChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.displayMenuContainer}>
|
||||
<MenuSub
|
||||
items={<GroupByList groupBy={groupBy} onChange={handleGroupByChange} />}
|
||||
items={
|
||||
<GroupByList
|
||||
groupBy={displayPreference.groupBy}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.subMenuSelectorContainer}>
|
||||
<span>{t['com.affine.explorer.display-menu.grouping']()}</span>
|
||||
<span className={styles.subMenuSelectorSelected}>
|
||||
{groupBy ? <GroupByName groupBy={groupBy} /> : null}
|
||||
{displayPreference.groupBy ? (
|
||||
<GroupByName groupBy={displayPreference.groupBy} />
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
</MenuSub>
|
||||
<MenuSub
|
||||
items={<OrderByList orderBy={orderBy} onChange={handleOrderByChange} />}
|
||||
items={
|
||||
<OrderByList
|
||||
orderBy={displayPreference.orderBy}
|
||||
onChange={handleOrderByChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.subMenuSelectorContainer}>
|
||||
<span>{t['com.affine.explorer.display-menu.ordering']()}</span>
|
||||
<span className={styles.subMenuSelectorSelected}>
|
||||
{orderBy ? <OrderByName orderBy={orderBy} /> : null}
|
||||
{displayPreference.orderBy ? (
|
||||
<OrderByName orderBy={displayPreference.orderBy} />
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
</MenuSub>
|
||||
<Divider size="thinner" />
|
||||
<DisplayProperties />
|
||||
<DisplayProperties
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={onDisplayPreferenceChange}
|
||||
/>
|
||||
<Divider size="thinner" />
|
||||
<QuickActionsConfig />
|
||||
<QuickActionsConfig
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={onDisplayPreferenceChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -76,14 +100,28 @@ export const ExplorerDisplayMenuButton = ({
|
||||
style,
|
||||
className,
|
||||
menuProps,
|
||||
displayPreference,
|
||||
onDisplayPreferenceChange,
|
||||
}: {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
menuProps?: Omit<MenuProps, 'items' | 'children'>;
|
||||
displayPreference: ExplorerDisplayPreference;
|
||||
onDisplayPreferenceChange: (
|
||||
displayPreference: ExplorerDisplayPreference
|
||||
) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<Menu items={<ExplorerDisplayMenu />} {...menuProps}>
|
||||
<Menu
|
||||
items={
|
||||
<ExplorerDisplayMenu
|
||||
displayPreference={displayPreference}
|
||||
onDisplayPreferenceChange={onDisplayPreferenceChange}
|
||||
/>
|
||||
}
|
||||
{...menuProps}
|
||||
>
|
||||
<Button
|
||||
className={className}
|
||||
style={style}
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
} from '@affine/core/modules/workspace-property';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { WorkspacePropertyName } from '../../properties';
|
||||
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
||||
import { DocExplorerContext } from '../context';
|
||||
import type { ExplorerDisplayPreference } from '../types';
|
||||
import * as styles from './properties.css';
|
||||
|
||||
export const filterDisplayProperties = <
|
||||
@@ -29,17 +29,22 @@ export const filterDisplayProperties = <
|
||||
}));
|
||||
};
|
||||
|
||||
export const DisplayProperties = () => {
|
||||
export const DisplayProperties = ({
|
||||
displayPreference,
|
||||
onDisplayPreferenceChange,
|
||||
}: {
|
||||
displayPreference: ExplorerDisplayPreference;
|
||||
onDisplayPreferenceChange: (
|
||||
displayPreference: ExplorerDisplayPreference
|
||||
) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const explorerContextValue = useContext(DocExplorerContext);
|
||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||
const propertyList = useLiveData(workspacePropertyService.properties$);
|
||||
|
||||
const displayProperties = useLiveData(
|
||||
explorerContextValue.displayProperties$
|
||||
);
|
||||
const showIcon = useLiveData(explorerContextValue.showDocIcon$);
|
||||
const showBody = useLiveData(explorerContextValue.showDocPreview$);
|
||||
const displayProperties = displayPreference.displayProperties;
|
||||
const showIcon = displayPreference.showDocIcon ?? false;
|
||||
const showBody = displayPreference.showDocPreview ?? false;
|
||||
|
||||
const propertiesGroups = useMemo(
|
||||
() => [
|
||||
@@ -57,9 +62,9 @@ export const DisplayProperties = () => {
|
||||
|
||||
const handleDisplayPropertiesChange = useCallback(
|
||||
(displayProperties: string[]) => {
|
||||
explorerContextValue.displayProperties$?.next(displayProperties);
|
||||
onDisplayPreferenceChange({ ...displayPreference, displayProperties });
|
||||
},
|
||||
[explorerContextValue.displayProperties$]
|
||||
[displayPreference, onDisplayPreferenceChange]
|
||||
);
|
||||
|
||||
const handlePropertyClick = useCallback(
|
||||
@@ -74,12 +79,18 @@ export const DisplayProperties = () => {
|
||||
);
|
||||
|
||||
const toggleIcon = useCallback(() => {
|
||||
explorerContextValue.showDocIcon$?.next(!showIcon);
|
||||
}, [explorerContextValue.showDocIcon$, showIcon]);
|
||||
onDisplayPreferenceChange({
|
||||
...displayPreference,
|
||||
showDocIcon: !showIcon,
|
||||
});
|
||||
}, [displayPreference, onDisplayPreferenceChange, showIcon]);
|
||||
|
||||
const toggleBody = useCallback(() => {
|
||||
explorerContextValue.showDocPreview$?.next(!showBody);
|
||||
}, [explorerContextValue.showDocPreview$, showBody]);
|
||||
onDisplayPreferenceChange({
|
||||
...displayPreference,
|
||||
showDocPreview: !showBody,
|
||||
});
|
||||
}, [displayPreference, onDisplayPreferenceChange, showBody]);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@@ -94,7 +105,9 @@ export const DisplayProperties = () => {
|
||||
<Button
|
||||
key={property.id}
|
||||
data-show={
|
||||
displayProperties && displayProperties.includes(property.id)
|
||||
displayProperties
|
||||
? displayProperties.includes(property.id)
|
||||
: false
|
||||
}
|
||||
onClick={() => handlePropertyClick(property.id)}
|
||||
className={styles.property}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Checkbox, MenuItem, MenuSub } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DocExplorerContext } from '../context';
|
||||
import { type QuickAction, quickActions } from '../quick-actions.constants';
|
||||
import type { ExplorerDisplayPreference } from '../types';
|
||||
|
||||
export const QuickActionsConfig = () => {
|
||||
export const QuickActionsConfig = ({
|
||||
displayPreference,
|
||||
onDisplayPreferenceChange,
|
||||
}: {
|
||||
displayPreference: ExplorerDisplayPreference;
|
||||
onDisplayPreferenceChange: (
|
||||
displayPreference: ExplorerDisplayPreference
|
||||
) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
@@ -14,7 +21,19 @@ export const QuickActionsConfig = () => {
|
||||
items={quickActions.map(action => {
|
||||
if (action.disabled) return null;
|
||||
|
||||
return <QuickActionItem key={action.key} action={action} />;
|
||||
return (
|
||||
<QuickActionItem
|
||||
key={action.key}
|
||||
action={action}
|
||||
active={displayPreference[`${action.key}`] ?? false}
|
||||
onClick={() => {
|
||||
onDisplayPreferenceChange({
|
||||
...displayPreference,
|
||||
[action.key]: !displayPreference[action.key],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
>
|
||||
{t['com.affine.all-docs.quick-actions']()}
|
||||
@@ -22,24 +41,28 @@ export const QuickActionsConfig = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const QuickActionItem = ({ action }: { action: QuickAction }) => {
|
||||
const QuickActionItem = ({
|
||||
action,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
action: QuickAction;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const explorerContextValue = useContext(DocExplorerContext);
|
||||
|
||||
const value = useLiveData(explorerContextValue[`${action.key}$`]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const value = explorerContextValue[`${action.key}$`]?.value;
|
||||
explorerContextValue[`${action.key}$`]?.next(!value);
|
||||
onClick();
|
||||
},
|
||||
[action.key, explorerContextValue]
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem prefixIcon={<Checkbox checked={!!value} />} onClick={handleClick}>
|
||||
<MenuItem prefixIcon={<Checkbox checked={active} />} onClick={handleClick}>
|
||||
{t.t(action.name)}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { RadioGroup, type RadioItem } from '@affine/component';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DocExplorerContext } from '../context';
|
||||
import {
|
||||
type DocListItemView,
|
||||
DocListViewIcon,
|
||||
@@ -27,16 +25,18 @@ const views = [
|
||||
},
|
||||
] satisfies RadioItem[];
|
||||
|
||||
export const ViewToggle = () => {
|
||||
const explorerContextValue = useContext(DocExplorerContext);
|
||||
|
||||
const view = useLiveData(explorerContextValue.view$);
|
||||
|
||||
export const ViewToggle = ({
|
||||
view,
|
||||
onViewChange,
|
||||
}: {
|
||||
view: DocListItemView;
|
||||
onViewChange: (view: DocListItemView) => void;
|
||||
}) => {
|
||||
const handleViewChange = useCallback(
|
||||
(view: DocListItemView) => {
|
||||
explorerContextValue.view$?.next(view);
|
||||
onViewChange(view);
|
||||
},
|
||||
[explorerContextValue.view$]
|
||||
[onViewChange]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -67,7 +67,7 @@ class MixId {
|
||||
}
|
||||
export const DocListItem = ({ ...props }: DocListItemProps) => {
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const view = useLiveData(contextValue.view$);
|
||||
const view = useLiveData(contextValue.view$) ?? 'list';
|
||||
const groups = useLiveData(contextValue.groups$);
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||
@@ -255,7 +255,7 @@ const Select = memo(function Select({
|
||||
</div>
|
||||
);
|
||||
});
|
||||
// Different with RawDocIcon, refer to `ExplorerPreference.showDocIcon`
|
||||
// Different with RawDocIcon, refer to `ExplorerDisplayPreference.showDocIcon`
|
||||
const DocIcon = memo(function DocIcon({
|
||||
id,
|
||||
...props
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
QuickSplit,
|
||||
QuickTab,
|
||||
} from './docs-view/quick-actions';
|
||||
import type { ExplorerPreference } from './types';
|
||||
import type { ExplorerDisplayPreference } from './types';
|
||||
|
||||
interface QuickActionItem {
|
||||
name: I18nString;
|
||||
@@ -20,7 +20,10 @@ type ExtractPrefixKeys<Obj extends object, Prefix extends string> = {
|
||||
[Key in keyof Obj]-?: Key extends `${Prefix}${string}` ? Key : never;
|
||||
}[keyof Obj];
|
||||
|
||||
export type QuickActionKey = ExtractPrefixKeys<ExplorerPreference, 'quick'>;
|
||||
export type QuickActionKey = ExtractPrefixKeys<
|
||||
ExplorerDisplayPreference,
|
||||
'quick'
|
||||
>;
|
||||
|
||||
const QUICK_ACTION_MAP: Record<QuickActionKey, QuickActionItem> = {
|
||||
quickFavorite: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
import type {
|
||||
GroupByParams,
|
||||
OrderByParams,
|
||||
@@ -6,8 +5,10 @@ import type {
|
||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||
import type { DocRecord } from '@affine/core/modules/doc';
|
||||
|
||||
export interface ExplorerPreference {
|
||||
filters?: FilterParams[];
|
||||
import type { DocListItemView } from './docs-view/doc-list-item';
|
||||
|
||||
export interface ExplorerDisplayPreference {
|
||||
view?: DocListItemView;
|
||||
groupBy?: GroupByParams;
|
||||
orderBy?: OrderByParams;
|
||||
displayProperties?: string[];
|
||||
|
||||
Reference in New Issue
Block a user