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:
EYHN
2025-05-19 09:15:37 +00:00
parent b7679497ca
commit fbf590ddd4
16 changed files with 344 additions and 105 deletions

View File

@@ -12,9 +12,7 @@ export function useFramework(): FrameworkProvider {
return useContext(FrameworkProviderContext); // never null, because the default value return useContext(FrameworkProviderContext); // never null, because the default value
} }
export function useService<T extends Service>( export function useService<T>(identifier: GeneralIdentifier<T>): T {
identifier: GeneralIdentifier<T>
): T {
return useContext(FrameworkProviderContext).get(identifier); return useContext(FrameworkProviderContext).get(identifier);
} }

View File

@@ -1,19 +1,18 @@
import { LiveData } from '@toeverything/infra'; import { LiveData } from '@toeverything/infra';
import { createContext } from 'react'; import { createContext } from 'react';
import type { DocListItemView } from './docs-view/doc-list-item'; import type { ExplorerDisplayPreference } from './types';
import type { ExplorerPreference } from './types';
export type DocExplorerContextType = { export type DocExplorerContextType = {
view$: LiveData<DocListItemView>;
groups$: LiveData<Array<{ key: string; items: string[] }>>; groups$: LiveData<Array<{ key: string; items: string[] }>>;
collapsedGroups$: LiveData<string[]>; collapsedGroups$: LiveData<string[]>;
selectMode$?: LiveData<boolean>; selectMode$?: LiveData<boolean>;
selectedDocIds$: LiveData<string[]>; selectedDocIds$: LiveData<string[]>;
prevCheckAnchorId$?: LiveData<string | null>; prevCheckAnchorId$?: LiveData<string | null>;
displayPreference$: LiveData<ExplorerDisplayPreference>;
} & { } & {
[K in keyof Omit<ExplorerPreference, 'filters'> as `${K}$`]: LiveData< [K in keyof ExplorerDisplayPreference as `${K}$`]: LiveData<
ExplorerPreference[K] ExplorerDisplayPreference[K]
>; >;
}; };
@@ -21,24 +20,51 @@ export const DocExplorerContext = createContext<DocExplorerContextType>(
{} as any {} as any
); );
export const createDocExplorerContext = () => export const createDocExplorerContext = (
({ initialState?: ExplorerDisplayPreference
view$: new LiveData<DocListItemView>('list'), ) => {
const displayPreference$ = new LiveData<ExplorerDisplayPreference>(
initialState ?? {}
);
return {
groups$: new LiveData<Array<{ key: string; items: string[] }>>([]), groups$: new LiveData<Array<{ key: string; items: string[] }>>([]),
collapsedGroups$: new LiveData<string[]>([]), collapsedGroups$: new LiveData<string[]>([]),
selectMode$: new LiveData<boolean>(false), selectMode$: new LiveData<boolean>(false),
selectedDocIds$: new LiveData<string[]>([]), selectedDocIds$: new LiveData<string[]>([]),
prevCheckAnchorId$: new LiveData<string | null>(null), prevCheckAnchorId$: new LiveData<string | null>(null),
groupBy$: new LiveData<ExplorerPreference['groupBy']>(undefined), displayPreference$: displayPreference$,
orderBy$: new LiveData<ExplorerPreference['orderBy']>(undefined), view$: displayPreference$.selector(
displayProperties$: new LiveData<ExplorerPreference['displayProperties']>( displayPreference => displayPreference.view
[]
), ),
showDocIcon$: new LiveData<ExplorerPreference['showDocIcon']>(true), groupBy$: displayPreference$.selector(
showDocPreview$: new LiveData<ExplorerPreference['showDocPreview']>(true), displayPreference => displayPreference.groupBy
quickFavorite$: new LiveData<ExplorerPreference['quickFavorite']>(false), ),
quickSelect$: new LiveData<ExplorerPreference['quickSelect']>(false), orderBy$: displayPreference$.selector(
quickSplit$: new LiveData<ExplorerPreference['quickSplit']>(false), displayPreference => displayPreference.orderBy
quickTrash$: new LiveData<ExplorerPreference['quickTrash']>(false), ),
quickTab$: new LiveData<ExplorerPreference['quickTab']>(false), displayProperties$: displayPreference$.selector(
}) satisfies DocExplorerContextType; 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;
};

View File

@@ -11,63 +11,87 @@ import type {
} from '@affine/core/modules/collection-rules/types'; } from '@affine/core/modules/collection-rules/types';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { ArrowDownSmallIcon } from '@blocksuite/icons/rc'; import { ArrowDownSmallIcon } from '@blocksuite/icons/rc';
import { useLiveData } from '@toeverything/infra';
import type React from 'react'; 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 { GroupByList, GroupByName } from './group';
import { OrderByList, OrderByName } from './order'; import { OrderByList, OrderByName } from './order';
import { DisplayProperties } from './properties'; import { DisplayProperties } from './properties';
import { QuickActionsConfig } from './quick-actions'; import { QuickActionsConfig } from './quick-actions';
import * as styles from './styles.css'; import * as styles from './styles.css';
const ExplorerDisplayMenu = () => { const ExplorerDisplayMenu = ({
displayPreference,
onDisplayPreferenceChange,
}: {
displayPreference: ExplorerDisplayPreference;
onDisplayPreferenceChange: (
displayPreference: ExplorerDisplayPreference
) => void;
}) => {
const t = useI18n(); const t = useI18n();
const explorerContextValue = useContext(DocExplorerContext);
const groupBy = useLiveData(explorerContextValue.groupBy$);
const orderBy = useLiveData(explorerContextValue.orderBy$);
const handleGroupByChange = useCallback( const handleGroupByChange = useCallback(
(groupBy: GroupByParams) => { (groupBy: GroupByParams) => {
explorerContextValue.groupBy$?.next(groupBy); onDisplayPreferenceChange({ ...displayPreference, groupBy });
}, },
[explorerContextValue.groupBy$] [displayPreference, onDisplayPreferenceChange]
); );
const handleOrderByChange = useCallback( const handleOrderByChange = useCallback(
(orderBy: OrderByParams) => { (orderBy: OrderByParams) => {
explorerContextValue.orderBy$?.next(orderBy); onDisplayPreferenceChange({ ...displayPreference, orderBy });
}, },
[explorerContextValue.orderBy$] [displayPreference, onDisplayPreferenceChange]
); );
return ( return (
<div className={styles.displayMenuContainer}> <div className={styles.displayMenuContainer}>
<MenuSub <MenuSub
items={<GroupByList groupBy={groupBy} onChange={handleGroupByChange} />} items={
<GroupByList
groupBy={displayPreference.groupBy}
onChange={handleGroupByChange}
/>
}
> >
<div className={styles.subMenuSelectorContainer}> <div className={styles.subMenuSelectorContainer}>
<span>{t['com.affine.explorer.display-menu.grouping']()}</span> <span>{t['com.affine.explorer.display-menu.grouping']()}</span>
<span className={styles.subMenuSelectorSelected}> <span className={styles.subMenuSelectorSelected}>
{groupBy ? <GroupByName groupBy={groupBy} /> : null} {displayPreference.groupBy ? (
<GroupByName groupBy={displayPreference.groupBy} />
) : null}
</span> </span>
</div> </div>
</MenuSub> </MenuSub>
<MenuSub <MenuSub
items={<OrderByList orderBy={orderBy} onChange={handleOrderByChange} />} items={
<OrderByList
orderBy={displayPreference.orderBy}
onChange={handleOrderByChange}
/>
}
> >
<div className={styles.subMenuSelectorContainer}> <div className={styles.subMenuSelectorContainer}>
<span>{t['com.affine.explorer.display-menu.ordering']()}</span> <span>{t['com.affine.explorer.display-menu.ordering']()}</span>
<span className={styles.subMenuSelectorSelected}> <span className={styles.subMenuSelectorSelected}>
{orderBy ? <OrderByName orderBy={orderBy} /> : null} {displayPreference.orderBy ? (
<OrderByName orderBy={displayPreference.orderBy} />
) : null}
</span> </span>
</div> </div>
</MenuSub> </MenuSub>
<Divider size="thinner" /> <Divider size="thinner" />
<DisplayProperties /> <DisplayProperties
displayPreference={displayPreference}
onDisplayPreferenceChange={onDisplayPreferenceChange}
/>
<Divider size="thinner" /> <Divider size="thinner" />
<QuickActionsConfig /> <QuickActionsConfig
displayPreference={displayPreference}
onDisplayPreferenceChange={onDisplayPreferenceChange}
/>
</div> </div>
); );
}; };
@@ -76,14 +100,28 @@ export const ExplorerDisplayMenuButton = ({
style, style,
className, className,
menuProps, menuProps,
displayPreference,
onDisplayPreferenceChange,
}: { }: {
style?: React.CSSProperties; style?: React.CSSProperties;
className?: string; className?: string;
menuProps?: Omit<MenuProps, 'items' | 'children'>; menuProps?: Omit<MenuProps, 'items' | 'children'>;
displayPreference: ExplorerDisplayPreference;
onDisplayPreferenceChange: (
displayPreference: ExplorerDisplayPreference
) => void;
}) => { }) => {
const t = useI18n(); const t = useI18n();
return ( return (
<Menu items={<ExplorerDisplayMenu />} {...menuProps}> <Menu
items={
<ExplorerDisplayMenu
displayPreference={displayPreference}
onDisplayPreferenceChange={onDisplayPreferenceChange}
/>
}
{...menuProps}
>
<Button <Button
className={className} className={className}
style={style} style={style}

View File

@@ -5,11 +5,11 @@ import {
} from '@affine/core/modules/workspace-property'; } from '@affine/core/modules/workspace-property';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useContext, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { WorkspacePropertyName } from '../../properties'; import { WorkspacePropertyName } from '../../properties';
import { WorkspacePropertyTypes } from '../../workspace-property-types'; import { WorkspacePropertyTypes } from '../../workspace-property-types';
import { DocExplorerContext } from '../context'; import type { ExplorerDisplayPreference } from '../types';
import * as styles from './properties.css'; import * as styles from './properties.css';
export const filterDisplayProperties = < 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 t = useI18n();
const explorerContextValue = useContext(DocExplorerContext);
const workspacePropertyService = useService(WorkspacePropertyService); const workspacePropertyService = useService(WorkspacePropertyService);
const propertyList = useLiveData(workspacePropertyService.properties$); const propertyList = useLiveData(workspacePropertyService.properties$);
const displayProperties = useLiveData( const displayProperties = displayPreference.displayProperties;
explorerContextValue.displayProperties$ const showIcon = displayPreference.showDocIcon ?? false;
); const showBody = displayPreference.showDocPreview ?? false;
const showIcon = useLiveData(explorerContextValue.showDocIcon$);
const showBody = useLiveData(explorerContextValue.showDocPreview$);
const propertiesGroups = useMemo( const propertiesGroups = useMemo(
() => [ () => [
@@ -57,9 +62,9 @@ export const DisplayProperties = () => {
const handleDisplayPropertiesChange = useCallback( const handleDisplayPropertiesChange = useCallback(
(displayProperties: string[]) => { (displayProperties: string[]) => {
explorerContextValue.displayProperties$?.next(displayProperties); onDisplayPreferenceChange({ ...displayPreference, displayProperties });
}, },
[explorerContextValue.displayProperties$] [displayPreference, onDisplayPreferenceChange]
); );
const handlePropertyClick = useCallback( const handlePropertyClick = useCallback(
@@ -74,12 +79,18 @@ export const DisplayProperties = () => {
); );
const toggleIcon = useCallback(() => { const toggleIcon = useCallback(() => {
explorerContextValue.showDocIcon$?.next(!showIcon); onDisplayPreferenceChange({
}, [explorerContextValue.showDocIcon$, showIcon]); ...displayPreference,
showDocIcon: !showIcon,
});
}, [displayPreference, onDisplayPreferenceChange, showIcon]);
const toggleBody = useCallback(() => { const toggleBody = useCallback(() => {
explorerContextValue.showDocPreview$?.next(!showBody); onDisplayPreferenceChange({
}, [explorerContextValue.showDocPreview$, showBody]); ...displayPreference,
showDocPreview: !showBody,
});
}, [displayPreference, onDisplayPreferenceChange, showBody]);
return ( return (
<div className={styles.root}> <div className={styles.root}>
@@ -94,7 +105,9 @@ export const DisplayProperties = () => {
<Button <Button
key={property.id} key={property.id}
data-show={ data-show={
displayProperties && displayProperties.includes(property.id) displayProperties
? displayProperties.includes(property.id)
: false
} }
onClick={() => handlePropertyClick(property.id)} onClick={() => handlePropertyClick(property.id)}
className={styles.property} className={styles.property}

View File

@@ -1,12 +1,19 @@
import { Checkbox, MenuItem, MenuSub } from '@affine/component'; import { Checkbox, MenuItem, MenuSub } from '@affine/component';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData } from '@toeverything/infra'; import { useCallback } from 'react';
import { useCallback, useContext } from 'react';
import { DocExplorerContext } from '../context';
import { type QuickAction, quickActions } from '../quick-actions.constants'; 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(); const t = useI18n();
return ( return (
@@ -14,7 +21,19 @@ export const QuickActionsConfig = () => {
items={quickActions.map(action => { items={quickActions.map(action => {
if (action.disabled) return null; 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']()} {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 t = useI18n();
const explorerContextValue = useContext(DocExplorerContext);
const value = useLiveData(explorerContextValue[`${action.key}$`]);
const handleClick = useCallback( const handleClick = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
const value = explorerContextValue[`${action.key}$`]?.value; onClick();
explorerContextValue[`${action.key}$`]?.next(!value);
}, },
[action.key, explorerContextValue] [onClick]
); );
return ( return (
<MenuItem prefixIcon={<Checkbox checked={!!value} />} onClick={handleClick}> <MenuItem prefixIcon={<Checkbox checked={active} />} onClick={handleClick}>
{t.t(action.name)} {t.t(action.name)}
</MenuItem> </MenuItem>
); );

View File

@@ -1,8 +1,6 @@
import { RadioGroup, type RadioItem } from '@affine/component'; import { RadioGroup, type RadioItem } from '@affine/component';
import { useLiveData } from '@toeverything/infra'; import { useCallback } from 'react';
import { useCallback, useContext } from 'react';
import { DocExplorerContext } from '../context';
import { import {
type DocListItemView, type DocListItemView,
DocListViewIcon, DocListViewIcon,
@@ -27,16 +25,18 @@ const views = [
}, },
] satisfies RadioItem[]; ] satisfies RadioItem[];
export const ViewToggle = () => { export const ViewToggle = ({
const explorerContextValue = useContext(DocExplorerContext); view,
onViewChange,
const view = useLiveData(explorerContextValue.view$); }: {
view: DocListItemView;
onViewChange: (view: DocListItemView) => void;
}) => {
const handleViewChange = useCallback( const handleViewChange = useCallback(
(view: DocListItemView) => { (view: DocListItemView) => {
explorerContextValue.view$?.next(view); onViewChange(view);
}, },
[explorerContextValue.view$] [onViewChange]
); );
return ( return (

View File

@@ -67,7 +67,7 @@ class MixId {
} }
export const DocListItem = ({ ...props }: DocListItemProps) => { export const DocListItem = ({ ...props }: DocListItemProps) => {
const contextValue = useContext(DocExplorerContext); const contextValue = useContext(DocExplorerContext);
const view = useLiveData(contextValue.view$); const view = useLiveData(contextValue.view$) ?? 'list';
const groups = useLiveData(contextValue.groups$); const groups = useLiveData(contextValue.groups$);
const selectMode = useLiveData(contextValue.selectMode$); const selectMode = useLiveData(contextValue.selectMode$);
const selectedDocIds = useLiveData(contextValue.selectedDocIds$); const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
@@ -255,7 +255,7 @@ const Select = memo(function Select({
</div> </div>
); );
}); });
// Different with RawDocIcon, refer to `ExplorerPreference.showDocIcon` // Different with RawDocIcon, refer to `ExplorerDisplayPreference.showDocIcon`
const DocIcon = memo(function DocIcon({ const DocIcon = memo(function DocIcon({
id, id,
...props ...props

View File

@@ -8,7 +8,7 @@ import {
QuickSplit, QuickSplit,
QuickTab, QuickTab,
} from './docs-view/quick-actions'; } from './docs-view/quick-actions';
import type { ExplorerPreference } from './types'; import type { ExplorerDisplayPreference } from './types';
interface QuickActionItem { interface QuickActionItem {
name: I18nString; name: I18nString;
@@ -20,7 +20,10 @@ type ExtractPrefixKeys<Obj extends object, Prefix extends string> = {
[Key in keyof Obj]-?: Key extends `${Prefix}${string}` ? Key : never; [Key in keyof Obj]-?: Key extends `${Prefix}${string}` ? Key : never;
}[keyof Obj]; }[keyof Obj];
export type QuickActionKey = ExtractPrefixKeys<ExplorerPreference, 'quick'>; export type QuickActionKey = ExtractPrefixKeys<
ExplorerDisplayPreference,
'quick'
>;
const QUICK_ACTION_MAP: Record<QuickActionKey, QuickActionItem> = { const QUICK_ACTION_MAP: Record<QuickActionKey, QuickActionItem> = {
quickFavorite: { quickFavorite: {

View File

@@ -1,4 +1,3 @@
import type { FilterParams } from '@affine/core/modules/collection-rules';
import type { import type {
GroupByParams, GroupByParams,
OrderByParams, OrderByParams,
@@ -6,8 +5,10 @@ import type {
import type { DocCustomPropertyInfo } from '@affine/core/modules/db'; import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
import type { DocRecord } from '@affine/core/modules/doc'; import type { DocRecord } from '@affine/core/modules/doc';
export interface ExplorerPreference { import type { DocListItemView } from './docs-view/doc-list-item';
filters?: FilterParams[];
export interface ExplorerDisplayPreference {
view?: DocListItemView;
groupBy?: GroupByParams; groupBy?: GroupByParams;
orderBy?: OrderByParams; orderBy?: OrderByParams;
displayProperties?: string[]; displayProperties?: string[];

View File

@@ -1,7 +1,10 @@
import { type MenuProps } from '@affine/component'; import { type MenuProps } from '@affine/component';
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu'; import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle'; 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 { 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'; import * as styles from './all-page-header.css';
@@ -13,14 +16,36 @@ const menuProps: Partial<MenuProps> = {
sideOffset: 8, 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 ( return (
<div className={styles.header}> <div className={styles.header}>
<ExplorerNavigation active="docs" /> <ExplorerNavigation active="docs" />
<div className={styles.actions}> <div className={styles.actions}>
<ViewToggle /> <ViewToggle
<ExplorerDisplayMenuButton menuProps={menuProps} /> view={displayPreference.view ?? 'list'}
onViewChange={handleViewChange}
/>
<ExplorerDisplayMenuButton
menuProps={menuProps}
displayPreference={displayPreference}
onDisplayPreferenceChange={onDisplayPreferenceChange}
/>
</div> </div>
</div> </div>
); );

View File

@@ -4,6 +4,7 @@ import {
DocExplorerContext, DocExplorerContext,
} from '@affine/core/components/explorer/context'; } from '@affine/core/components/explorer/context';
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list'; 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 { Filters } from '@affine/core/components/filter';
import { import {
CollectionService, CollectionService,
@@ -12,9 +13,10 @@ import {
import { CollectionRulesService } from '@affine/core/modules/collection-rules'; import { CollectionRulesService } from '@affine/core/modules/collection-rules';
import type { FilterParams } from '@affine/core/modules/collection-rules/types'; import type { FilterParams } from '@affine/core/modules/collection-rules/types';
import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WorkspaceLocalState } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
ViewBody, ViewBody,
@@ -29,11 +31,22 @@ import { AllDocsHeader } from './all-page-header';
import { MigrationAllDocsDataNotification } from './migration-data'; import { MigrationAllDocsDataNotification } from './migration-data';
import { PinnedCollections } from './pinned-collections'; import { PinnedCollections } from './pinned-collections';
interface AllDocsStateSave extends ExplorerDisplayPreference {
selectedCollectionId: string | null;
}
export const AllPage = () => { export const AllPage = () => {
const t = useI18n(); const t = useI18n();
const collectionService = useService(CollectionService); const collectionService = useService(CollectionService);
const pinnedCollectionService = useService(PinnedCollectionService); const pinnedCollectionService = useService(PinnedCollectionService);
const workspaceLocalState = useService(WorkspaceLocalState);
const [initialState] = useState(() => {
return workspaceLocalState.get<AllDocsStateSave>(
'allDocsDisplayPreference'
);
});
const isCollectionDataReady = useLiveData( const isCollectionDataReady = useLiveData(
collectionService.collectionDataReady$ collectionService.collectionDataReady$
@@ -49,7 +62,7 @@ export const AllPage = () => {
const [selectedCollectionId, setSelectedCollectionId] = useState< const [selectedCollectionId, setSelectedCollectionId] = useState<
string | null string | null
>(null); >(initialState?.selectedCollectionId ?? null);
const selectedCollection = useLiveData( const selectedCollection = useLiveData(
selectedCollectionId selectedCollectionId
? collectionService.collection$(selectedCollectionId) ? collectionService.collection$(selectedCollectionId)
@@ -80,10 +93,26 @@ export const AllPage = () => {
const [tempFilters, setTempFilters] = useState<FilterParams[] | null>(null); const [tempFilters, setTempFilters] = useState<FilterParams[] | null>(null);
const [explorerContextValue] = useState(createDocExplorerContext); const [explorerContextValue] = useState(() =>
createDocExplorerContext(initialState)
);
const groupBy = useLiveData(explorerContextValue.groupBy$); const groupBy = useLiveData(explorerContextValue.groupBy$);
const orderBy = useLiveData(explorerContextValue.orderBy$); 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(); const { openPromptModal } = usePromptModal();
@@ -187,6 +216,11 @@ export const AllPage = () => {
setTempFilters(null); setTempFilters(null);
}, []); }, []);
const handleSelectAll = useCallback(() => {
setSelectedCollectionId(null);
setTempFilters(null);
}, []);
const handleEditCollection = useCallback( const handleEditCollection = useCallback(
(collectionId: string) => { (collectionId: string) => {
const collection = collectionService.collection$(collectionId).value; const collection = collectionService.collection$(collectionId).value;
@@ -250,12 +284,22 @@ export const AllPage = () => {
setTempFilters([params]); setTempFilters([params]);
}, []); }, []);
const handleDisplayPreferenceChange = useCallback(
(displayPreference: ExplorerDisplayPreference) => {
explorerContextValue.displayPreference$.next(displayPreference);
},
[explorerContextValue]
);
return ( return (
<DocExplorerContext.Provider value={explorerContextValue}> <DocExplorerContext.Provider value={explorerContextValue}>
<ViewTitle title={t['All pages']()} /> <ViewTitle title={t['All pages']()} />
<ViewIcon icon="allDocs" /> <ViewIcon icon="allDocs" />
<ViewHeader> <ViewHeader>
<AllDocsHeader /> <AllDocsHeader
displayPreference={displayPreference}
onDisplayPreferenceChange={handleDisplayPreferenceChange}
/>
</ViewHeader> </ViewHeader>
<ViewBody> <ViewBody>
<div className={styles.body}> <div className={styles.body}>
@@ -263,7 +307,7 @@ export const AllPage = () => {
<div className={styles.pinnedCollection}> <div className={styles.pinnedCollection}>
<PinnedCollections <PinnedCollections
activeCollectionId={selectedCollectionId} activeCollectionId={selectedCollectionId}
onActiveAll={() => setSelectedCollectionId(null)} onActiveAll={handleSelectAll}
onActiveCollection={handleSelectCollection} onActiveCollection={handleSelectCollection}
onAddFilter={handleNewTempFilter} onAddFilter={handleNewTempFilter}
onEditCollection={handleEditCollection} onEditCollection={handleEditCollection}

View File

@@ -108,7 +108,10 @@ export const PinnedCollections = ({
<div <div
className={styles.item} className={styles.item}
data-active={activeCollectionId === null ? 'true' : undefined} 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" role="button"
> >
{t['com.affine.all-docs.pinned-collection.all']()} {t['com.affine.all-docs.pinned-collection.all']()}
@@ -118,7 +121,12 @@ export const PinnedCollections = ({
key={record.collectionId} key={record.collectionId}
record={record} record={record}
isActive={activeCollectionId === record.collectionId} 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)} onClickEdit={() => onEditCollection(record.collectionId)}
onClickRemove={() => { onClickRemove={() => {
const nextCollectionId = pinnedCollections[index - 1]?.collectionId; const nextCollectionId = pinnedCollections[index - 1]?.collectionId;

View File

@@ -2,15 +2,32 @@ import { FlexWrapper } from '@affine/component';
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu'; import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle'; import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle';
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation'; 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'; import { Header } from '@affine/core/components/pure/header';
export const CollectionDetailHeader = () => { export const CollectionDetailHeader = ({
displayPreference,
onDisplayPreferenceChange,
}: {
displayPreference: ExplorerDisplayPreference;
onDisplayPreferenceChange: (
displayPreference: ExplorerDisplayPreference
) => void;
}) => {
return ( return (
<Header <Header
right={ right={
<FlexWrapper gap={16}> <FlexWrapper gap={16}>
<ViewToggle /> <ViewToggle
<ExplorerDisplayMenuButton /> view={displayPreference.view ?? 'list'}
onViewChange={view => {
onDisplayPreferenceChange({ ...displayPreference, view });
}}
/>
<ExplorerDisplayMenuButton
displayPreference={displayPreference}
onDisplayPreferenceChange={onDisplayPreferenceChange}
/>
</FlexWrapper> </FlexWrapper>
} }
left={<ExplorerNavigation active="collections" />} left={<ExplorerNavigation active="collections" />}

View File

@@ -5,6 +5,7 @@ import {
DocExplorerContext, DocExplorerContext,
} from '@affine/core/components/explorer/context'; } from '@affine/core/components/explorer/context';
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list'; import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
import type { ExplorerDisplayPreference } from '@affine/core/components/explorer/types';
import { import {
type Collection, type Collection,
CollectionService, CollectionService,
@@ -45,11 +46,21 @@ export const CollectionDetail = ({
const isAdmin = useLiveData(permissionService.permission.isAdmin$); const isAdmin = useLiveData(permissionService.permission.isAdmin$);
const isOwner = useLiveData(permissionService.permission.isOwner$); const isOwner = useLiveData(permissionService.permission.isOwner$);
const displayPreference = useLiveData(
explorerContextValue.displayPreference$
);
const groupBy = useLiveData(explorerContextValue.groupBy$); const groupBy = useLiveData(explorerContextValue.groupBy$);
const orderBy = useLiveData(explorerContextValue.orderBy$); const orderBy = useLiveData(explorerContextValue.orderBy$);
const rules = useLiveData(collection.rules$); const rules = useLiveData(collection.rules$);
const allowList = useLiveData(collection.allowList$); const allowList = useLiveData(collection.allowList$);
const handleDisplayPreferenceChange = useCallback(
(displayPreference: ExplorerDisplayPreference) => {
explorerContextValue.displayPreference$.next(displayPreference);
},
[explorerContextValue]
);
useEffect(() => { useEffect(() => {
const subscription = collectionRulesService const subscription = collectionRulesService
.watch({ .watch({
@@ -95,7 +106,10 @@ export const CollectionDetail = ({
return ( return (
<DocExplorerContext.Provider value={explorerContextValue}> <DocExplorerContext.Provider value={explorerContextValue}>
<ViewHeader> <ViewHeader>
<CollectionDetailHeader /> <CollectionDetailHeader
displayPreference={displayPreference}
onDisplayPreferenceChange={handleDisplayPreferenceChange}
/>
</ViewHeader> </ViewHeader>
<ViewBody> <ViewBody>
<FlexWrapper flexDirection="column" alignItems="stretch" width="100%"> <FlexWrapper flexDirection="column" alignItems="stretch" width="100%">

View File

@@ -1,12 +1,26 @@
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu'; import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation'; 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'; import { Header } from '@affine/core/components/pure/header';
export const TagDetailHeader = () => { export const TagDetailHeader = ({
displayPreference,
onDisplayPreferenceChange,
}: {
displayPreference: ExplorerDisplayPreference;
onDisplayPreferenceChange: (
displayPreference: ExplorerDisplayPreference
) => void;
}) => {
return ( return (
<Header <Header
left={<ExplorerNavigation active={'tags'} />} left={<ExplorerNavigation active={'tags'} />}
right={<ExplorerDisplayMenuButton />} right={
<ExplorerDisplayMenuButton
displayPreference={displayPreference}
onDisplayPreferenceChange={onDisplayPreferenceChange}
/>
}
/> />
); );
}; };

View File

@@ -3,6 +3,7 @@ import {
DocExplorerContext, DocExplorerContext,
} from '@affine/core/components/explorer/context'; } from '@affine/core/components/explorer/context';
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list'; 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 { CollectionRulesService } from '@affine/core/modules/collection-rules';
import { GlobalContextService } from '@affine/core/modules/global-context'; import { GlobalContextService } from '@affine/core/modules/global-context';
import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspacePermissionService } from '@affine/core/modules/permissions';
@@ -15,7 +16,7 @@ import {
ViewTitle, ViewTitle,
} from '@affine/core/modules/workbench'; } from '@affine/core/modules/workbench';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
@@ -36,6 +37,9 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
const tagList = useService(TagService).tagList; const tagList = useService(TagService).tagList;
const currentTag = useLiveData(tagList.tagByTagId$(tagId)); const currentTag = useLiveData(tagList.tagByTagId$(tagId));
const displayPreference = useLiveData(
explorerContextValue.displayPreference$
);
const groupBy = useLiveData(explorerContextValue.groupBy$); const groupBy = useLiveData(explorerContextValue.groupBy$);
const orderBy = useLiveData(explorerContextValue.orderBy$); const orderBy = useLiveData(explorerContextValue.orderBy$);
const groups = useLiveData(explorerContextValue.groups$); const groups = useLiveData(explorerContextValue.groups$);
@@ -109,12 +113,23 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
return <PageNotFound />; return <PageNotFound />;
} }
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleDisplayPreferenceChange = useCallback(
(displayPreference: ExplorerDisplayPreference) => {
explorerContextValue.displayPreference$.next(displayPreference);
},
[explorerContextValue]
);
return ( return (
<DocExplorerContext.Provider value={explorerContextValue}> <DocExplorerContext.Provider value={explorerContextValue}>
<ViewTitle title={tagName ?? 'Untitled'} /> <ViewTitle title={tagName ?? 'Untitled'} />
<ViewIcon icon="tag" /> <ViewIcon icon="tag" />
<ViewHeader> <ViewHeader>
<TagDetailHeader /> <TagDetailHeader
displayPreference={displayPreference}
onDisplayPreferenceChange={handleDisplayPreferenceChange}
/>
</ViewHeader> </ViewHeader>
<ViewBody> <ViewBody>
<div className={styles.body}> <div className={styles.body}>