mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-08 18:43:46 +00: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,31 +1,50 @@
|
||||
import { createContext, type Dispatch, type SetStateAction } from 'react';
|
||||
import { LiveData } from '@toeverything/infra';
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { DocListItemView } from './docs-view/doc-list-item';
|
||||
import type { ExplorerPreference } from './types';
|
||||
|
||||
export type DocExplorerContextType = ExplorerPreference & {
|
||||
view: DocListItemView;
|
||||
setView: Dispatch<SetStateAction<DocListItemView>>;
|
||||
groups: Array<{ key: string; items: string[] }>;
|
||||
collapsed: string[];
|
||||
selectMode?: boolean;
|
||||
selectedDocIds: string[];
|
||||
prevCheckAnchorId?: string | null;
|
||||
onToggleCollapse: (groupId: string) => void;
|
||||
onToggleSelect: (docId: string) => void;
|
||||
onSelect: Dispatch<SetStateAction<string[]>>;
|
||||
setPrevCheckAnchorId: Dispatch<SetStateAction<string | null>>;
|
||||
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>;
|
||||
} & {
|
||||
[K in keyof ExplorerPreference as `${K}$`]: LiveData<ExplorerPreference[K]>;
|
||||
};
|
||||
|
||||
export const DocExplorerContext = createContext<DocExplorerContextType>({
|
||||
view: 'list',
|
||||
setView: () => {},
|
||||
groups: [],
|
||||
collapsed: [],
|
||||
selectedDocIds: [],
|
||||
prevCheckAnchorId: null,
|
||||
onToggleSelect: () => {},
|
||||
onToggleCollapse: () => {},
|
||||
onSelect: () => {},
|
||||
setPrevCheckAnchorId: () => {},
|
||||
});
|
||||
export const DocExplorerContext = createContext<DocExplorerContextType>(
|
||||
{} as any
|
||||
);
|
||||
|
||||
export const createDocExplorerContext = () =>
|
||||
({
|
||||
view$: new LiveData<DocListItemView>('list'),
|
||||
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),
|
||||
filters$: new LiveData<ExplorerPreference['filters']>([
|
||||
{
|
||||
type: 'system',
|
||||
key: 'trash',
|
||||
value: 'false',
|
||||
method: 'is',
|
||||
},
|
||||
]),
|
||||
groupBy$: new LiveData<ExplorerPreference['groupBy']>(undefined),
|
||||
orderBy$: new LiveData<ExplorerPreference['orderBy']>(undefined),
|
||||
displayProperties$: new LiveData<ExplorerPreference['displayProperties']>(
|
||||
[]
|
||||
),
|
||||
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;
|
||||
|
||||
@@ -11,85 +11,63 @@ 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 } from 'react';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
import type { ExplorerPreference } from '../types';
|
||||
import { DocExplorerContext } from '../context';
|
||||
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 = ({
|
||||
preference,
|
||||
onChange,
|
||||
}: {
|
||||
preference: ExplorerPreference;
|
||||
onChange?: (preference: ExplorerPreference) => void;
|
||||
}) => {
|
||||
const ExplorerDisplayMenu = () => {
|
||||
const t = useI18n();
|
||||
const explorerContextValue = useContext(DocExplorerContext);
|
||||
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
||||
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(groupBy: GroupByParams) => {
|
||||
onChange?.({
|
||||
...preference,
|
||||
groupBy,
|
||||
});
|
||||
explorerContextValue.groupBy$?.next(groupBy);
|
||||
},
|
||||
[onChange, preference]
|
||||
[explorerContextValue.groupBy$]
|
||||
);
|
||||
|
||||
const handleOrderByChange = useCallback(
|
||||
(orderBy: OrderByParams) => {
|
||||
onChange?.({
|
||||
...preference,
|
||||
orderBy,
|
||||
});
|
||||
explorerContextValue.orderBy$?.next(orderBy);
|
||||
},
|
||||
[onChange, preference]
|
||||
[explorerContextValue.orderBy$]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.displayMenuContainer}>
|
||||
<MenuSub
|
||||
items={
|
||||
<GroupByList
|
||||
groupBy={preference.groupBy}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
}
|
||||
items={<GroupByList groupBy={groupBy} onChange={handleGroupByChange} />}
|
||||
>
|
||||
<div className={styles.subMenuSelectorContainer}>
|
||||
<span>{t['com.affine.explorer.display-menu.grouping']()}</span>
|
||||
<span className={styles.subMenuSelectorSelected}>
|
||||
{preference.groupBy ? (
|
||||
<GroupByName groupBy={preference.groupBy} />
|
||||
) : null}
|
||||
{groupBy ? <GroupByName groupBy={groupBy} /> : null}
|
||||
</span>
|
||||
</div>
|
||||
</MenuSub>
|
||||
<MenuSub
|
||||
items={
|
||||
<OrderByList
|
||||
orderBy={preference.orderBy}
|
||||
onChange={handleOrderByChange}
|
||||
/>
|
||||
}
|
||||
items={<OrderByList orderBy={orderBy} onChange={handleOrderByChange} />}
|
||||
>
|
||||
<div className={styles.subMenuSelectorContainer}>
|
||||
<span>{t['com.affine.explorer.display-menu.ordering']()}</span>
|
||||
<span className={styles.subMenuSelectorSelected}>
|
||||
{preference.orderBy ? (
|
||||
<OrderByName orderBy={preference.orderBy} />
|
||||
) : null}
|
||||
{orderBy ? <OrderByName orderBy={orderBy} /> : null}
|
||||
</span>
|
||||
</div>
|
||||
</MenuSub>
|
||||
<Divider size="thinner" />
|
||||
<DisplayProperties preference={preference} onChange={onChange} />
|
||||
<DisplayProperties />
|
||||
<Divider size="thinner" />
|
||||
<QuickActionsConfig preference={preference} onChange={onChange} />
|
||||
<QuickActionsConfig />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -97,24 +75,15 @@ const ExplorerDisplayMenu = ({
|
||||
export const ExplorerDisplayMenuButton = ({
|
||||
style,
|
||||
className,
|
||||
preference,
|
||||
menuProps,
|
||||
onChange,
|
||||
}: {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
preference: ExplorerPreference;
|
||||
onChange?: (preference: ExplorerPreference) => void;
|
||||
menuProps?: Omit<MenuProps, 'items' | 'children'>;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<Menu
|
||||
items={
|
||||
<ExplorerDisplayMenu preference={preference} onChange={onChange} />
|
||||
}
|
||||
{...menuProps}
|
||||
>
|
||||
<Menu items={<ExplorerDisplayMenu />} {...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, useMemo } from 'react';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { WorkspacePropertyName } from '../../properties';
|
||||
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
||||
import type { ExplorerPreference } from '../types';
|
||||
import { DocExplorerContext } from '../context';
|
||||
import * as styles from './properties.css';
|
||||
|
||||
export const filterDisplayProperties = <
|
||||
@@ -29,20 +29,17 @@ export const filterDisplayProperties = <
|
||||
}));
|
||||
};
|
||||
|
||||
export const DisplayProperties = ({
|
||||
preference,
|
||||
onChange,
|
||||
}: {
|
||||
preference: ExplorerPreference;
|
||||
onChange?: (preference: ExplorerPreference) => void;
|
||||
}) => {
|
||||
export const DisplayProperties = () => {
|
||||
const t = useI18n();
|
||||
const explorerContextValue = useContext(DocExplorerContext);
|
||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||
const propertyList = useLiveData(workspacePropertyService.properties$);
|
||||
|
||||
const displayProperties = preference.displayProperties;
|
||||
const showIcon = preference.showDocIcon;
|
||||
const showBody = preference.showDocPreview;
|
||||
const displayProperties = useLiveData(
|
||||
explorerContextValue.displayProperties$
|
||||
);
|
||||
const showIcon = useLiveData(explorerContextValue.showDocIcon$);
|
||||
const showBody = useLiveData(explorerContextValue.showDocPreview$);
|
||||
|
||||
const propertiesGroups = useMemo(
|
||||
() => [
|
||||
@@ -60,12 +57,9 @@ export const DisplayProperties = ({
|
||||
|
||||
const handleDisplayPropertiesChange = useCallback(
|
||||
(displayProperties: string[]) => {
|
||||
onChange?.({
|
||||
...preference,
|
||||
displayProperties,
|
||||
});
|
||||
explorerContextValue.displayProperties$?.next(displayProperties);
|
||||
},
|
||||
[onChange, preference]
|
||||
[explorerContextValue.displayProperties$]
|
||||
);
|
||||
|
||||
const handlePropertyClick = useCallback(
|
||||
@@ -80,18 +74,12 @@ export const DisplayProperties = ({
|
||||
);
|
||||
|
||||
const toggleIcon = useCallback(() => {
|
||||
onChange?.({
|
||||
...preference,
|
||||
showDocIcon: !showIcon,
|
||||
});
|
||||
}, [onChange, preference, showIcon]);
|
||||
explorerContextValue.showDocIcon$?.next(!showIcon);
|
||||
}, [explorerContextValue.showDocIcon$, showIcon]);
|
||||
|
||||
const toggleBody = useCallback(() => {
|
||||
onChange?.({
|
||||
...preference,
|
||||
showDocPreview: !showBody,
|
||||
});
|
||||
}, [onChange, preference, showBody]);
|
||||
explorerContextValue.showDocPreview$?.next(!showBody);
|
||||
}, [explorerContextValue.showDocPreview$, showBody]);
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
|
||||
@@ -1,50 +1,46 @@
|
||||
import { Checkbox, MenuItem, MenuSub } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useCallback } from 'react';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
import { type QuickActionKey, quickActions } from '../quick-actions.constants';
|
||||
import type { ExplorerPreference } from '../types';
|
||||
import { DocExplorerContext } from '../context';
|
||||
import { type QuickAction, quickActions } from '../quick-actions.constants';
|
||||
|
||||
export const QuickActionsConfig = ({
|
||||
preference,
|
||||
onChange,
|
||||
}: {
|
||||
preference: ExplorerPreference;
|
||||
onChange?: (preference: ExplorerPreference) => void;
|
||||
}) => {
|
||||
export const QuickActionsConfig = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const toggleAction = useCallback(
|
||||
(key: QuickActionKey) => {
|
||||
onChange?.({
|
||||
...preference,
|
||||
[key]: !preference[key],
|
||||
});
|
||||
},
|
||||
[preference, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuSub
|
||||
items={quickActions.map(action => {
|
||||
if (action.disabled) return null;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={action.key}
|
||||
onClick={e => {
|
||||
// do not close sub menu
|
||||
e.preventDefault();
|
||||
toggleAction(action.key);
|
||||
}}
|
||||
prefixIcon={<Checkbox checked={!!preference[action.key]} />}
|
||||
>
|
||||
{t.t(action.name)}
|
||||
</MenuItem>
|
||||
);
|
||||
return <QuickActionItem key={action.key} action={action} />;
|
||||
})}
|
||||
>
|
||||
{t['com.affine.all-docs.quick-actions']()}
|
||||
</MenuSub>
|
||||
);
|
||||
};
|
||||
|
||||
const QuickActionItem = ({ action }: { action: QuickAction }) => {
|
||||
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);
|
||||
},
|
||||
[action.key, explorerContextValue]
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem prefixIcon={<Checkbox checked={!!value} />} onClick={handleClick}>
|
||||
{t.t(action.name)}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,52 +66,49 @@ class MixId {
|
||||
}
|
||||
}
|
||||
export const DocListItem = ({ ...props }: DocListItemProps) => {
|
||||
const {
|
||||
view,
|
||||
groups,
|
||||
selectMode,
|
||||
selectedDocIds,
|
||||
prevCheckAnchorId,
|
||||
onSelect,
|
||||
onToggleSelect,
|
||||
setPrevCheckAnchorId,
|
||||
} = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const view = useLiveData(contextValue.view$);
|
||||
const groups = useLiveData(contextValue.groups$);
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||
const prevCheckAnchorId = useLiveData(contextValue.prevCheckAnchorId$);
|
||||
|
||||
const handleMultiSelect = useCallback(
|
||||
(prevCursor: string, currCursor: string) => {
|
||||
const flattenList = groups.flatMap(group =>
|
||||
group.items.map(docId => MixId.create(group.key, docId))
|
||||
);
|
||||
|
||||
const prev = contextValue.selectedDocIds$?.value ?? [];
|
||||
const prevIndex = flattenList.indexOf(prevCursor);
|
||||
const currIndex = flattenList.indexOf(currCursor);
|
||||
|
||||
onSelect(prev => {
|
||||
const lowerIndex = Math.min(prevIndex, currIndex);
|
||||
const upperIndex = Math.max(prevIndex, currIndex);
|
||||
const lowerIndex = Math.min(prevIndex, currIndex);
|
||||
const upperIndex = Math.max(prevIndex, currIndex);
|
||||
|
||||
const resSet = new Set(prev);
|
||||
const handledSet = new Set<string>();
|
||||
for (let i = lowerIndex; i <= upperIndex; i++) {
|
||||
const mixId = flattenList[i];
|
||||
const { groupId, docId } = MixId.parse(mixId);
|
||||
if (groupId === null || docId === null) {
|
||||
continue;
|
||||
}
|
||||
if (handledSet.has(docId) || mixId === prevCursor) {
|
||||
continue;
|
||||
}
|
||||
if (resSet.has(docId)) {
|
||||
resSet.delete(docId);
|
||||
} else {
|
||||
resSet.add(docId);
|
||||
}
|
||||
handledSet.add(docId);
|
||||
const resSet = new Set(prev);
|
||||
const handledSet = new Set<string>();
|
||||
for (let i = lowerIndex; i <= upperIndex; i++) {
|
||||
const mixId = flattenList[i];
|
||||
const { groupId, docId } = MixId.parse(mixId);
|
||||
if (groupId === null || docId === null) {
|
||||
continue;
|
||||
}
|
||||
return [...resSet];
|
||||
});
|
||||
setPrevCheckAnchorId(currCursor);
|
||||
if (handledSet.has(docId) || mixId === prevCursor) {
|
||||
continue;
|
||||
}
|
||||
if (resSet.has(docId)) {
|
||||
resSet.delete(docId);
|
||||
} else {
|
||||
resSet.add(docId);
|
||||
}
|
||||
handledSet.add(docId);
|
||||
}
|
||||
|
||||
contextValue.selectedDocIds$?.next(Array.from(resSet));
|
||||
contextValue.prevCheckAnchorId$?.next(currCursor);
|
||||
},
|
||||
[groups, onSelect, setPrevCheckAnchorId]
|
||||
[contextValue, groups]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
@@ -127,13 +124,13 @@ export const DocListItem = ({ ...props }: DocListItemProps) => {
|
||||
// do multi select
|
||||
handleMultiSelect(prevCheckAnchorId, currCursor);
|
||||
} else {
|
||||
onToggleSelect(docId);
|
||||
setPrevCheckAnchorId(currCursor);
|
||||
contextValue.selectedDocIds$?.next([docId]);
|
||||
contextValue.prevCheckAnchorId$?.next(currCursor);
|
||||
}
|
||||
} else {
|
||||
if (e.shiftKey) {
|
||||
onToggleSelect(docId);
|
||||
setPrevCheckAnchorId(currCursor);
|
||||
contextValue.selectedDocIds$?.next([docId]);
|
||||
contextValue.prevCheckAnchorId$?.next(currCursor);
|
||||
return;
|
||||
} else {
|
||||
// as link
|
||||
@@ -141,14 +138,7 @@ export const DocListItem = ({ ...props }: DocListItemProps) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
handleMultiSelect,
|
||||
onToggleSelect,
|
||||
prevCheckAnchorId,
|
||||
props,
|
||||
selectMode,
|
||||
setPrevCheckAnchorId,
|
||||
]
|
||||
[contextValue, handleMultiSelect, prevCheckAnchorId, props, selectMode]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -196,7 +186,8 @@ const DragHandle = memo(function DragHandle({
|
||||
preview,
|
||||
...props
|
||||
}: HTMLProps<HTMLDivElement> & { preview?: ReactNode }) {
|
||||
const { selectMode } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
|
||||
const { dragRef, CustomDragPreview } = useDraggable<AffineDNDData>(
|
||||
() => ({
|
||||
@@ -238,12 +229,13 @@ const Select = memo(function Select({
|
||||
id,
|
||||
...props
|
||||
}: HTMLProps<HTMLDivElement>) {
|
||||
const { selectMode, selectedDocIds, onToggleSelect } =
|
||||
useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||
|
||||
const handleSelectChange = useCallback(() => {
|
||||
id && onToggleSelect(id);
|
||||
}, [id, onToggleSelect]);
|
||||
id && contextValue.selectedDocIds$?.next([id]);
|
||||
}, [id, contextValue]);
|
||||
|
||||
if (!id) {
|
||||
return null;
|
||||
@@ -263,7 +255,8 @@ const DocIcon = memo(function DocIcon({
|
||||
id,
|
||||
...props
|
||||
}: HTMLProps<HTMLDivElement>) {
|
||||
const { showDocIcon } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const showDocIcon = useLiveData(contextValue.showDocIcon$);
|
||||
if (!showDocIcon) {
|
||||
return null;
|
||||
}
|
||||
@@ -289,7 +282,8 @@ const DocPreview = memo(function DocPreview({
|
||||
loading,
|
||||
...props
|
||||
}: HTMLProps<HTMLDivElement> & { loading?: ReactNode }) {
|
||||
const { showDocPreview } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const showDocPreview = useLiveData(contextValue.showDocPreview$);
|
||||
|
||||
if (!id || !showDocPreview) return null;
|
||||
|
||||
@@ -356,7 +350,8 @@ const randomPreviewSkeleton = () => {
|
||||
}));
|
||||
};
|
||||
export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
||||
const { selectMode } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
const docsService = useService(DocsService);
|
||||
const doc = useLiveData(docsService.list.doc$(docId));
|
||||
const [previewSkeleton] = useState(randomPreviewSkeleton);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, IconButton } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ToggleRightIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
@@ -20,14 +21,12 @@ export const DocGroupHeader = ({
|
||||
groupId: string;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const {
|
||||
selectMode,
|
||||
collapsed,
|
||||
groups,
|
||||
selectedDocIds,
|
||||
onToggleCollapse,
|
||||
onSelect,
|
||||
} = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
|
||||
const groups = useLiveData(contextValue.groups$);
|
||||
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||
const collapsedGroups = useLiveData(contextValue.collapsedGroups$);
|
||||
const selectMode = useLiveData(contextValue.selectMode$);
|
||||
|
||||
const group = groups.find(g => g.key === groupId);
|
||||
const isGroupAllSelected = group?.items.every(id =>
|
||||
@@ -35,24 +34,30 @@ export const DocGroupHeader = ({
|
||||
);
|
||||
|
||||
const handleToggleCollapse = useCallback(() => {
|
||||
onToggleCollapse(groupId);
|
||||
}, [groupId, onToggleCollapse]);
|
||||
const prev = contextValue.collapsedGroups$.value;
|
||||
contextValue.collapsedGroups$.next(
|
||||
prev.includes(groupId)
|
||||
? prev.filter(id => id !== groupId)
|
||||
: [...prev, groupId]
|
||||
);
|
||||
}, [groupId, contextValue]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
const prev = contextValue.selectedDocIds$.value;
|
||||
if (isGroupAllSelected) {
|
||||
onSelect(prev => prev.filter(id => !group?.items.includes(id)));
|
||||
contextValue.selectedDocIds$.next(
|
||||
prev.filter(id => !group?.items.includes(id))
|
||||
);
|
||||
} else {
|
||||
onSelect(prev => {
|
||||
const newSelected = [...prev];
|
||||
group?.items.forEach(id => {
|
||||
if (!newSelected.includes(id)) {
|
||||
newSelected.push(id);
|
||||
}
|
||||
});
|
||||
return newSelected;
|
||||
const newSelected = [...prev];
|
||||
group?.items.forEach(id => {
|
||||
if (!newSelected.includes(id)) {
|
||||
newSelected.push(id);
|
||||
}
|
||||
});
|
||||
contextValue.selectedDocIds$.next(newSelected);
|
||||
}
|
||||
}, [group?.items, isGroupAllSelected, onSelect]);
|
||||
}, [contextValue, group?.items, isGroupAllSelected]);
|
||||
|
||||
const selectedCount = group?.items.filter(id =>
|
||||
selectedDocIds.includes(id)
|
||||
@@ -61,7 +66,7 @@ export const DocGroupHeader = ({
|
||||
return (
|
||||
<div
|
||||
className={styles.groupHeader}
|
||||
data-collapsed={collapsed.includes(groupId)}
|
||||
data-collapsed={collapsedGroups.includes(groupId)}
|
||||
>
|
||||
<div className={clsx(styles.content, className)} {...props} />
|
||||
{selectMode ? (
|
||||
|
||||
@@ -28,7 +28,8 @@ const cardInlinePropertyOrder: WorkspacePropertyType[] = [
|
||||
];
|
||||
|
||||
const useProperties = (docId: string, view: 'list' | 'card') => {
|
||||
const { displayProperties } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const displayProperties = useLiveData(contextValue.displayProperties$);
|
||||
const docsService = useService(DocsService);
|
||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ export const QuickFavorite = memo(function QuickFavorite({
|
||||
doc,
|
||||
...iconButtonProps
|
||||
}: QuickActionProps) {
|
||||
const { quickFavorite } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const quickFavorite = useLiveData(contextValue.quickFavorite$);
|
||||
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const favourite = useLiveData(favAdapter.isFavorite$(doc.id, 'doc'));
|
||||
@@ -55,7 +56,8 @@ export const QuickTab = memo(function QuickTab({
|
||||
doc,
|
||||
...iconButtonProps
|
||||
}: QuickActionProps) {
|
||||
const { quickTab } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const quickTab = useLiveData(contextValue.quickTab$);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const onOpenInNewTab = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
@@ -83,7 +85,8 @@ export const QuickSplit = memo(function QuickSplit({
|
||||
doc,
|
||||
...iconButtonProps
|
||||
}: QuickActionProps) {
|
||||
const { quickSplit } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const quickSplit = useLiveData(contextValue.quickSplit$);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
const onOpenInSplitView = useCallback(
|
||||
@@ -115,7 +118,8 @@ export const QuickDelete = memo(function QuickDelete({
|
||||
}: QuickActionProps) {
|
||||
const t = useI18n();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { quickTrash } = useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const quickTrash = useLiveData(contextValue.quickTrash$);
|
||||
|
||||
const onMoveToTrash = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
@@ -162,8 +166,9 @@ export const QuickSelect = memo(function QuickSelect({
|
||||
doc,
|
||||
...iconButtonProps
|
||||
}: QuickActionProps) {
|
||||
const { quickSelect, selectedDocIds, onToggleSelect } =
|
||||
useContext(DocExplorerContext);
|
||||
const contextValue = useContext(DocExplorerContext);
|
||||
const quickSelect = useLiveData(contextValue.quickSelect$);
|
||||
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||
|
||||
const selected = selectedDocIds.includes(doc.id);
|
||||
|
||||
@@ -171,9 +176,13 @@ export const QuickSelect = memo(function QuickSelect({
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onToggleSelect(doc.id);
|
||||
contextValue.selectedDocIds$?.next(
|
||||
selected
|
||||
? selectedDocIds.filter(id => id !== doc.id)
|
||||
: [...selectedDocIds, doc.id]
|
||||
);
|
||||
},
|
||||
[doc.id, onToggleSelect]
|
||||
[contextValue, doc.id, selected, selectedDocIds]
|
||||
);
|
||||
|
||||
if (!quickSelect) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from './docs-view/quick-actions';
|
||||
import type { ExplorerPreference } from './types';
|
||||
|
||||
export interface QuickAction {
|
||||
interface QuickActionItem {
|
||||
name: I18nString;
|
||||
Component: React.FC<QuickActionProps>;
|
||||
disabled?: boolean;
|
||||
@@ -22,7 +22,7 @@ type ExtractPrefixKeys<Obj extends object, Prefix extends string> = {
|
||||
|
||||
export type QuickActionKey = ExtractPrefixKeys<ExplorerPreference, 'quick'>;
|
||||
|
||||
const QUICK_ACTION_MAP: Record<QuickActionKey, QuickAction> = {
|
||||
const QUICK_ACTION_MAP: Record<QuickActionKey, QuickActionItem> = {
|
||||
quickFavorite: {
|
||||
name: 'com.affine.all-docs.quick-action.favorite',
|
||||
Component: QuickFavorite,
|
||||
@@ -50,3 +50,5 @@ export const quickActions = Object.entries(QUICK_ACTION_MAP).map(
|
||||
return { key: key as QuickActionKey, ...config };
|
||||
}
|
||||
);
|
||||
|
||||
export type QuickAction = (typeof quickActions)[number];
|
||||
|
||||
@@ -144,6 +144,7 @@ export const CreatedByDocListInlineProperty = ({
|
||||
type="CreatedBy"
|
||||
size={20}
|
||||
emptyFallback={null}
|
||||
showName={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -171,7 +172,7 @@ export const ModifiedByGroupHeader = ({
|
||||
return (
|
||||
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||
<div className={styles.userLabelContainer}>
|
||||
<PublicUserLabel id={userId} size={20} />
|
||||
<PublicUserLabel id={userId} size={20} showName={false} />
|
||||
</div>
|
||||
</PlainTextDocGroupHeader>
|
||||
);
|
||||
|
||||
@@ -223,6 +223,7 @@ export const WorkspacePropertyTypes = {
|
||||
defaultFilter: { method: 'this-week' },
|
||||
showInDocList: 'inline',
|
||||
docListProperty: UpdatedDateDocListProperty,
|
||||
groupHeader: CreatedGroupHeader,
|
||||
},
|
||||
createdAt: {
|
||||
icon: HistoryIcon,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,6 +22,9 @@ export const publicUserLabelRemoved = style([
|
||||
]);
|
||||
|
||||
export const publicUserLabelAvatar = style({
|
||||
marginRight: '0.5em',
|
||||
transform: 'translateY(4px)',
|
||||
selectors: {
|
||||
'&[data-show-name="true"]': {
|
||||
marginRight: '0.5em',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ export const PublicUserLabel = ({
|
||||
name={user?.name ?? ''}
|
||||
size={size}
|
||||
className={styles.publicUserLabelAvatar}
|
||||
data-show-name={showName}
|
||||
/>
|
||||
{showName && user?.name}
|
||||
</span>
|
||||
|
||||
@@ -108,7 +108,13 @@ export class DocsStore extends Store {
|
||||
if (pages instanceof YArray) {
|
||||
return pages.map(v => ({
|
||||
id: v.get('id') as string,
|
||||
tags: (v.get('tags')?.toJSON() ?? []) as string[],
|
||||
tags: (() => {
|
||||
const tags = v.get('tags');
|
||||
if (tags instanceof YArray) {
|
||||
return tags.toJSON() as string[];
|
||||
}
|
||||
return (tags ?? []) as string[];
|
||||
})(),
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user