mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
perf(core): optimize rendering of all docs (#12188)
close AF-2605 [CleanShot 2025-05-08 at 13.56.38.mp4 <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/LakojjjzZNf6ogjOVwKE/4e36e838-7c7f-4f0a-89a8-fd582c2ef573.mp4" />](https://app.graphite.dev/media/video/LakojjjzZNf6ogjOVwKE/4e36e838-7c7f-4f0a-89a8-fd582c2ef573.mp4) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Refactor** - Centralized state management in the document explorer using a reactive context, replacing prop drilling and local state with observable streams for preferences, selection, grouping, and view options. - Updated multiple components (display menus, quick actions, doc list items, group headers) to consume and update state directly via context observables. - Simplified component signatures by removing now-unnecessary props related to preferences and state handlers. - **Style** - Adjusted user avatar display: avatars now only have right margin when user names are shown, and vertical alignment was improved. - **New Features** - User avatar elements now include a data attribute to indicate if the user name is displayed. - **Bug Fixes** - Improved handling of document tags to prevent errors when tags are not in the expected format. - **Documentation** - Added missing group header property for updated date fields in workspace property types. - **Chores** - Clarified and renamed internal types for quick actions to improve code clarity. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { type MenuProps, RadioGroup, type RadioItem } from '@affine/component';
|
||||
import { DocExplorerContext } from '@affine/core/components/explorer/context';
|
||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||
import { DocListViewIcon } from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||
import {
|
||||
type DocListItemView,
|
||||
DocListViewIcon,
|
||||
} from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
||||
import { type Dispatch, type SetStateAction, useContext } from 'react';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
import * as styles from './all-page-header.css';
|
||||
|
||||
@@ -27,7 +30,17 @@ const views = [
|
||||
] satisfies RadioItem[];
|
||||
|
||||
const ViewToggle = () => {
|
||||
const { view, setView } = useContext(DocExplorerContext);
|
||||
const explorerContextValue = useContext(DocExplorerContext);
|
||||
|
||||
const view = useLiveData(explorerContextValue.view$);
|
||||
|
||||
const handleViewChange = useCallback(
|
||||
(view: DocListItemView) => {
|
||||
explorerContextValue.view$?.next(view);
|
||||
},
|
||||
[explorerContextValue.view$]
|
||||
);
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
itemHeight={24}
|
||||
@@ -35,7 +48,7 @@ const ViewToggle = () => {
|
||||
padding={0}
|
||||
items={views}
|
||||
value={view}
|
||||
onChange={setView}
|
||||
onChange={handleViewChange}
|
||||
className={styles.viewToggle}
|
||||
borderRadius={4}
|
||||
indicatorClassName={styles.viewToggleIndicator}
|
||||
@@ -51,24 +64,14 @@ const menuProps: Partial<MenuProps> = {
|
||||
sideOffset: 8,
|
||||
},
|
||||
};
|
||||
export const AllDocsHeader = ({
|
||||
explorerPreference,
|
||||
setExplorerPreference,
|
||||
}: {
|
||||
explorerPreference: ExplorerPreference;
|
||||
setExplorerPreference: Dispatch<SetStateAction<ExplorerPreference>>;
|
||||
}) => {
|
||||
export const AllDocsHeader = () => {
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<ExplorerNavigation active="docs" />
|
||||
|
||||
<div className={styles.actions}>
|
||||
<ViewToggle />
|
||||
<ExplorerDisplayMenuButton
|
||||
preference={explorerPreference}
|
||||
onChange={setExplorerPreference}
|
||||
menuProps={menuProps}
|
||||
/>
|
||||
<ExplorerDisplayMenuButton menuProps={menuProps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { Masonry, type MasonryGroup, useConfirmModal } from '@affine/component';
|
||||
import {
|
||||
createDocExplorerContext,
|
||||
DocExplorerContext,
|
||||
type DocExplorerContextType,
|
||||
} from '@affine/core/components/explorer/context';
|
||||
import {
|
||||
DocListItem,
|
||||
type DocListItemView,
|
||||
} from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
||||
import { DocListItem } from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||
import { Filters } from '@affine/core/components/filter';
|
||||
import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar';
|
||||
import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types';
|
||||
@@ -49,9 +45,10 @@ const GroupHeader = memo(function GroupHeader({
|
||||
collapsed?: boolean;
|
||||
itemCount: number;
|
||||
}) {
|
||||
const { groupBy } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const propertyService = useService(WorkspacePropertyService);
|
||||
const allProperties = useLiveData(propertyService.sortedProperties$);
|
||||
const groupBy = useLiveData(contextValue.groupBy$);
|
||||
|
||||
const groupType = groupBy?.type;
|
||||
const groupKey = groupBy?.key;
|
||||
@@ -103,30 +100,17 @@ const DocListItemComponent = memo(function DocListItemComponent({
|
||||
export const AllPage = () => {
|
||||
const t = useI18n();
|
||||
const docsService = useService(DocsService);
|
||||
const [view, setView] = useState<DocListItemView>('masonry');
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<string[]>([]);
|
||||
const [selectMode, setSelectMode] = useState(false);
|
||||
const [selectedDocIds, setSelectedDocIds] = useState<string[]>([]);
|
||||
const [prevCheckAnchorId, setPrevCheckAnchorId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const [explorerPreference, setExplorerPreference] =
|
||||
useState<ExplorerPreference>({
|
||||
filters: [
|
||||
{
|
||||
type: 'system',
|
||||
key: 'trash',
|
||||
value: 'false',
|
||||
method: 'is',
|
||||
},
|
||||
],
|
||||
displayProperties: [],
|
||||
showDocIcon: true,
|
||||
showDocPreview: true,
|
||||
});
|
||||
const [explorerContextValue] = useState(createDocExplorerContext);
|
||||
|
||||
const [groups, setGroups] = useState<any>([]);
|
||||
const view = useLiveData(explorerContextValue.view$);
|
||||
const filters = useLiveData(explorerContextValue.filters$);
|
||||
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
||||
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
||||
const groups = useLiveData(explorerContextValue.groups$);
|
||||
const selectedDocIds = useLiveData(explorerContextValue.selectedDocIds$);
|
||||
const collapsedGroups = useLiveData(explorerContextValue.collapsedGroups$);
|
||||
const selectMode = useLiveData(explorerContextValue.selectMode$);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
@@ -159,14 +143,10 @@ export const AllPage = () => {
|
||||
const collectionRulesService = useService(CollectionRulesService);
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch(
|
||||
explorerPreference.filters ?? [],
|
||||
explorerPreference.groupBy,
|
||||
explorerPreference.orderBy
|
||||
)
|
||||
.watch(filters ?? [], groupBy, orderBy)
|
||||
.subscribe({
|
||||
next: result => {
|
||||
setGroups(result.groups);
|
||||
explorerContextValue.groups$.next(result.groups);
|
||||
},
|
||||
error: error => {
|
||||
console.error(error);
|
||||
@@ -177,58 +157,38 @@ export const AllPage = () => {
|
||||
};
|
||||
}, [
|
||||
collectionRulesService,
|
||||
explorerPreference.filters,
|
||||
explorerPreference.groupBy,
|
||||
explorerPreference.orderBy,
|
||||
explorerContextValue.groups$,
|
||||
filters,
|
||||
groupBy,
|
||||
orderBy,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setSelectMode(false);
|
||||
setSelectedDocIds([]);
|
||||
setPrevCheckAnchorId(null);
|
||||
explorerContextValue.selectMode$.next(false);
|
||||
explorerContextValue.selectedDocIds$.next([]);
|
||||
explorerContextValue.prevCheckAnchorId$.next(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, []);
|
||||
}, [explorerContextValue]);
|
||||
|
||||
const handleFilterChange = useCallback((filters: FilterParams[]) => {
|
||||
setExplorerPreference(prev => ({
|
||||
...prev,
|
||||
filters,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const toggleGroupCollapse = useCallback((groupId: string) => {
|
||||
setCollapsedGroups(prev => {
|
||||
return prev.includes(groupId)
|
||||
? prev.filter(id => id !== groupId)
|
||||
: [...prev, groupId];
|
||||
});
|
||||
}, []);
|
||||
const toggleDocSelect = useCallback((docId: string) => {
|
||||
setSelectMode(true);
|
||||
setSelectedDocIds(prev => {
|
||||
return prev.includes(docId)
|
||||
? prev.filter(id => id !== docId)
|
||||
: [...prev, docId];
|
||||
});
|
||||
}, []);
|
||||
const onSelect = useCallback(
|
||||
(...args: Parameters<typeof setSelectedDocIds>) => {
|
||||
setSelectMode(true);
|
||||
setSelectedDocIds(...args);
|
||||
const handleFilterChange = useCallback(
|
||||
(filters: FilterParams[]) => {
|
||||
explorerContextValue.filters$.next(filters);
|
||||
},
|
||||
[]
|
||||
[explorerContextValue]
|
||||
);
|
||||
|
||||
const handleCloseFloatingToolbar = useCallback(() => {
|
||||
setSelectMode(false);
|
||||
setSelectedDocIds([]);
|
||||
}, []);
|
||||
explorerContextValue.selectMode$.next(false);
|
||||
explorerContextValue.selectedDocIds$.next([]);
|
||||
}, [explorerContextValue]);
|
||||
|
||||
const handleMultiDelete = useCallback(() => {
|
||||
if (selectedDocIds.length === 0) {
|
||||
return;
|
||||
@@ -257,54 +217,18 @@ export const AllPage = () => {
|
||||
});
|
||||
}, [docsService.list, openConfirmModal, selectedDocIds, t]);
|
||||
|
||||
const explorerContextValue = useMemo(
|
||||
() =>
|
||||
({
|
||||
...explorerPreference,
|
||||
view,
|
||||
setView,
|
||||
groups,
|
||||
collapsed: collapsedGroups,
|
||||
selectedDocIds,
|
||||
selectMode,
|
||||
prevCheckAnchorId,
|
||||
setPrevCheckAnchorId,
|
||||
onToggleCollapse: toggleGroupCollapse,
|
||||
onToggleSelect: toggleDocSelect,
|
||||
onSelect,
|
||||
}) satisfies DocExplorerContextType,
|
||||
[
|
||||
collapsedGroups,
|
||||
explorerPreference,
|
||||
groups,
|
||||
onSelect,
|
||||
prevCheckAnchorId,
|
||||
selectMode,
|
||||
selectedDocIds,
|
||||
toggleDocSelect,
|
||||
toggleGroupCollapse,
|
||||
view,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||
<ViewTitle title={t['All pages']()} />
|
||||
<ViewIcon icon="allDocs" />
|
||||
<ViewHeader>
|
||||
<AllDocsHeader
|
||||
explorerPreference={explorerPreference}
|
||||
setExplorerPreference={setExplorerPreference}
|
||||
/>
|
||||
<AllDocsHeader />
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.filterArea}>
|
||||
<MigrationAllDocsDataNotification />
|
||||
<Filters
|
||||
filters={explorerPreference.filters ?? []}
|
||||
onChange={handleFilterChange}
|
||||
/>
|
||||
<Filters filters={filters ?? []} onChange={handleFilterChange} />
|
||||
</div>
|
||||
<div className={styles.scrollArea}>
|
||||
<Masonry
|
||||
|
||||
Reference in New Issue
Block a user