mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 14:28:51 +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,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 { DocListItemView } from './docs-view/doc-list-item';
|
||||||
import type { ExplorerPreference } from './types';
|
import type { ExplorerPreference } from './types';
|
||||||
|
|
||||||
export type DocExplorerContextType = ExplorerPreference & {
|
export type DocExplorerContextType = {
|
||||||
view: DocListItemView;
|
view$: LiveData<DocListItemView>;
|
||||||
setView: Dispatch<SetStateAction<DocListItemView>>;
|
groups$: LiveData<Array<{ key: string; items: string[] }>>;
|
||||||
groups: Array<{ key: string; items: string[] }>;
|
collapsedGroups$: LiveData<string[]>;
|
||||||
collapsed: string[];
|
selectMode$?: LiveData<boolean>;
|
||||||
selectMode?: boolean;
|
selectedDocIds$: LiveData<string[]>;
|
||||||
selectedDocIds: string[];
|
prevCheckAnchorId$?: LiveData<string | null>;
|
||||||
prevCheckAnchorId?: string | null;
|
} & {
|
||||||
onToggleCollapse: (groupId: string) => void;
|
[K in keyof ExplorerPreference as `${K}$`]: LiveData<ExplorerPreference[K]>;
|
||||||
onToggleSelect: (docId: string) => void;
|
|
||||||
onSelect: Dispatch<SetStateAction<string[]>>;
|
|
||||||
setPrevCheckAnchorId: Dispatch<SetStateAction<string | null>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocExplorerContext = createContext<DocExplorerContextType>({
|
export const DocExplorerContext = createContext<DocExplorerContextType>(
|
||||||
view: 'list',
|
{} as any
|
||||||
setView: () => {},
|
);
|
||||||
groups: [],
|
|
||||||
collapsed: [],
|
export const createDocExplorerContext = () =>
|
||||||
selectedDocIds: [],
|
({
|
||||||
prevCheckAnchorId: null,
|
view$: new LiveData<DocListItemView>('list'),
|
||||||
onToggleSelect: () => {},
|
groups$: new LiveData<Array<{ key: string; items: string[] }>>([]),
|
||||||
onToggleCollapse: () => {},
|
collapsedGroups$: new LiveData<string[]>([]),
|
||||||
onSelect: () => {},
|
selectMode$: new LiveData<boolean>(false),
|
||||||
setPrevCheckAnchorId: () => {},
|
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';
|
} 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 } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
import type { ExplorerPreference } from '../types';
|
import { DocExplorerContext } from '../context';
|
||||||
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 = () => {
|
||||||
preference,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
preference: ExplorerPreference;
|
|
||||||
onChange?: (preference: ExplorerPreference) => 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) => {
|
||||||
onChange?.({
|
explorerContextValue.groupBy$?.next(groupBy);
|
||||||
...preference,
|
|
||||||
groupBy,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[onChange, preference]
|
[explorerContextValue.groupBy$]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOrderByChange = useCallback(
|
const handleOrderByChange = useCallback(
|
||||||
(orderBy: OrderByParams) => {
|
(orderBy: OrderByParams) => {
|
||||||
onChange?.({
|
explorerContextValue.orderBy$?.next(orderBy);
|
||||||
...preference,
|
|
||||||
orderBy,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[onChange, preference]
|
[explorerContextValue.orderBy$]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.displayMenuContainer}>
|
<div className={styles.displayMenuContainer}>
|
||||||
<MenuSub
|
<MenuSub
|
||||||
items={
|
items={<GroupByList groupBy={groupBy} onChange={handleGroupByChange} />}
|
||||||
<GroupByList
|
|
||||||
groupBy={preference.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}>
|
||||||
{preference.groupBy ? (
|
{groupBy ? <GroupByName groupBy={groupBy} /> : null}
|
||||||
<GroupByName groupBy={preference.groupBy} />
|
|
||||||
) : null}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</MenuSub>
|
</MenuSub>
|
||||||
<MenuSub
|
<MenuSub
|
||||||
items={
|
items={<OrderByList orderBy={orderBy} onChange={handleOrderByChange} />}
|
||||||
<OrderByList
|
|
||||||
orderBy={preference.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}>
|
||||||
{preference.orderBy ? (
|
{orderBy ? <OrderByName orderBy={orderBy} /> : null}
|
||||||
<OrderByName orderBy={preference.orderBy} />
|
|
||||||
) : null}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</MenuSub>
|
</MenuSub>
|
||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
<DisplayProperties preference={preference} onChange={onChange} />
|
<DisplayProperties />
|
||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
<QuickActionsConfig preference={preference} onChange={onChange} />
|
<QuickActionsConfig />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -97,24 +75,15 @@ const ExplorerDisplayMenu = ({
|
|||||||
export const ExplorerDisplayMenuButton = ({
|
export const ExplorerDisplayMenuButton = ({
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
preference,
|
|
||||||
menuProps,
|
menuProps,
|
||||||
onChange,
|
|
||||||
}: {
|
}: {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
className?: string;
|
className?: string;
|
||||||
preference: ExplorerPreference;
|
|
||||||
onChange?: (preference: ExplorerPreference) => void;
|
|
||||||
menuProps?: Omit<MenuProps, 'items' | 'children'>;
|
menuProps?: Omit<MenuProps, 'items' | 'children'>;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu items={<ExplorerDisplayMenu />} {...menuProps}>
|
||||||
items={
|
|
||||||
<ExplorerDisplayMenu preference={preference} onChange={onChange} />
|
|
||||||
}
|
|
||||||
{...menuProps}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
className={className}
|
className={className}
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
@@ -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, useMemo } from 'react';
|
import { useCallback, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
import { WorkspacePropertyName } from '../../properties';
|
import { WorkspacePropertyName } from '../../properties';
|
||||||
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
||||||
import type { ExplorerPreference } from '../types';
|
import { DocExplorerContext } from '../context';
|
||||||
import * as styles from './properties.css';
|
import * as styles from './properties.css';
|
||||||
|
|
||||||
export const filterDisplayProperties = <
|
export const filterDisplayProperties = <
|
||||||
@@ -29,20 +29,17 @@ export const filterDisplayProperties = <
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DisplayProperties = ({
|
export const DisplayProperties = () => {
|
||||||
preference,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
preference: ExplorerPreference;
|
|
||||||
onChange?: (preference: ExplorerPreference) => 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 = preference.displayProperties;
|
const displayProperties = useLiveData(
|
||||||
const showIcon = preference.showDocIcon;
|
explorerContextValue.displayProperties$
|
||||||
const showBody = preference.showDocPreview;
|
);
|
||||||
|
const showIcon = useLiveData(explorerContextValue.showDocIcon$);
|
||||||
|
const showBody = useLiveData(explorerContextValue.showDocPreview$);
|
||||||
|
|
||||||
const propertiesGroups = useMemo(
|
const propertiesGroups = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -60,12 +57,9 @@ export const DisplayProperties = ({
|
|||||||
|
|
||||||
const handleDisplayPropertiesChange = useCallback(
|
const handleDisplayPropertiesChange = useCallback(
|
||||||
(displayProperties: string[]) => {
|
(displayProperties: string[]) => {
|
||||||
onChange?.({
|
explorerContextValue.displayProperties$?.next(displayProperties);
|
||||||
...preference,
|
|
||||||
displayProperties,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[onChange, preference]
|
[explorerContextValue.displayProperties$]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePropertyClick = useCallback(
|
const handlePropertyClick = useCallback(
|
||||||
@@ -80,18 +74,12 @@ export const DisplayProperties = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const toggleIcon = useCallback(() => {
|
const toggleIcon = useCallback(() => {
|
||||||
onChange?.({
|
explorerContextValue.showDocIcon$?.next(!showIcon);
|
||||||
...preference,
|
}, [explorerContextValue.showDocIcon$, showIcon]);
|
||||||
showDocIcon: !showIcon,
|
|
||||||
});
|
|
||||||
}, [onChange, preference, showIcon]);
|
|
||||||
|
|
||||||
const toggleBody = useCallback(() => {
|
const toggleBody = useCallback(() => {
|
||||||
onChange?.({
|
explorerContextValue.showDocPreview$?.next(!showBody);
|
||||||
...preference,
|
}, [explorerContextValue.showDocPreview$, showBody]);
|
||||||
showDocPreview: !showBody,
|
|
||||||
});
|
|
||||||
}, [onChange, preference, showBody]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
|
|||||||
@@ -1,50 +1,46 @@
|
|||||||
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 { useCallback } from 'react';
|
import { useLiveData } from '@toeverything/infra';
|
||||||
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
import { type QuickActionKey, quickActions } from '../quick-actions.constants';
|
import { DocExplorerContext } from '../context';
|
||||||
import type { ExplorerPreference } from '../types';
|
import { type QuickAction, quickActions } from '../quick-actions.constants';
|
||||||
|
|
||||||
export const QuickActionsConfig = ({
|
export const QuickActionsConfig = () => {
|
||||||
preference,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
preference: ExplorerPreference;
|
|
||||||
onChange?: (preference: ExplorerPreference) => void;
|
|
||||||
}) => {
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const toggleAction = useCallback(
|
|
||||||
(key: QuickActionKey) => {
|
|
||||||
onChange?.({
|
|
||||||
...preference,
|
|
||||||
[key]: !preference[key],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[preference, onChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuSub
|
<MenuSub
|
||||||
items={quickActions.map(action => {
|
items={quickActions.map(action => {
|
||||||
if (action.disabled) return null;
|
if (action.disabled) return null;
|
||||||
|
|
||||||
return (
|
return <QuickActionItem key={action.key} action={action} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{t['com.affine.all-docs.quick-actions']()}
|
{t['com.affine.all-docs.quick-actions']()}
|
||||||
</MenuSub>
|
</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) => {
|
export const DocListItem = ({ ...props }: DocListItemProps) => {
|
||||||
const {
|
const contextValue = useContext(DocExplorerContext);
|
||||||
view,
|
const view = useLiveData(contextValue.view$);
|
||||||
groups,
|
const groups = useLiveData(contextValue.groups$);
|
||||||
selectMode,
|
const selectMode = useLiveData(contextValue.selectMode$);
|
||||||
selectedDocIds,
|
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||||
prevCheckAnchorId,
|
const prevCheckAnchorId = useLiveData(contextValue.prevCheckAnchorId$);
|
||||||
onSelect,
|
|
||||||
onToggleSelect,
|
|
||||||
setPrevCheckAnchorId,
|
|
||||||
} = useContext(DocExplorerContext);
|
|
||||||
|
|
||||||
const handleMultiSelect = useCallback(
|
const handleMultiSelect = useCallback(
|
||||||
(prevCursor: string, currCursor: string) => {
|
(prevCursor: string, currCursor: string) => {
|
||||||
const flattenList = groups.flatMap(group =>
|
const flattenList = groups.flatMap(group =>
|
||||||
group.items.map(docId => MixId.create(group.key, docId))
|
group.items.map(docId => MixId.create(group.key, docId))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const prev = contextValue.selectedDocIds$?.value ?? [];
|
||||||
const prevIndex = flattenList.indexOf(prevCursor);
|
const prevIndex = flattenList.indexOf(prevCursor);
|
||||||
const currIndex = flattenList.indexOf(currCursor);
|
const currIndex = flattenList.indexOf(currCursor);
|
||||||
|
|
||||||
onSelect(prev => {
|
const lowerIndex = Math.min(prevIndex, currIndex);
|
||||||
const lowerIndex = Math.min(prevIndex, currIndex);
|
const upperIndex = Math.max(prevIndex, currIndex);
|
||||||
const upperIndex = Math.max(prevIndex, currIndex);
|
|
||||||
|
|
||||||
const resSet = new Set(prev);
|
const resSet = new Set(prev);
|
||||||
const handledSet = new Set<string>();
|
const handledSet = new Set<string>();
|
||||||
for (let i = lowerIndex; i <= upperIndex; i++) {
|
for (let i = lowerIndex; i <= upperIndex; i++) {
|
||||||
const mixId = flattenList[i];
|
const mixId = flattenList[i];
|
||||||
const { groupId, docId } = MixId.parse(mixId);
|
const { groupId, docId } = MixId.parse(mixId);
|
||||||
if (groupId === null || docId === null) {
|
if (groupId === null || docId === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
if (handledSet.has(docId) || mixId === prevCursor) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (resSet.has(docId)) {
|
|
||||||
resSet.delete(docId);
|
|
||||||
} else {
|
|
||||||
resSet.add(docId);
|
|
||||||
}
|
|
||||||
handledSet.add(docId);
|
|
||||||
}
|
}
|
||||||
return [...resSet];
|
if (handledSet.has(docId) || mixId === prevCursor) {
|
||||||
});
|
continue;
|
||||||
setPrevCheckAnchorId(currCursor);
|
}
|
||||||
|
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(
|
const handleClick = useCallback(
|
||||||
@@ -127,13 +124,13 @@ export const DocListItem = ({ ...props }: DocListItemProps) => {
|
|||||||
// do multi select
|
// do multi select
|
||||||
handleMultiSelect(prevCheckAnchorId, currCursor);
|
handleMultiSelect(prevCheckAnchorId, currCursor);
|
||||||
} else {
|
} else {
|
||||||
onToggleSelect(docId);
|
contextValue.selectedDocIds$?.next([docId]);
|
||||||
setPrevCheckAnchorId(currCursor);
|
contextValue.prevCheckAnchorId$?.next(currCursor);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
onToggleSelect(docId);
|
contextValue.selectedDocIds$?.next([docId]);
|
||||||
setPrevCheckAnchorId(currCursor);
|
contextValue.prevCheckAnchorId$?.next(currCursor);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// as link
|
// as link
|
||||||
@@ -141,14 +138,7 @@ export const DocListItem = ({ ...props }: DocListItemProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[contextValue, handleMultiSelect, prevCheckAnchorId, props, selectMode]
|
||||||
handleMultiSelect,
|
|
||||||
onToggleSelect,
|
|
||||||
prevCheckAnchorId,
|
|
||||||
props,
|
|
||||||
selectMode,
|
|
||||||
setPrevCheckAnchorId,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -196,7 +186,8 @@ const DragHandle = memo(function DragHandle({
|
|||||||
preview,
|
preview,
|
||||||
...props
|
...props
|
||||||
}: HTMLProps<HTMLDivElement> & { preview?: ReactNode }) {
|
}: HTMLProps<HTMLDivElement> & { preview?: ReactNode }) {
|
||||||
const { selectMode } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const selectMode = useLiveData(contextValue.selectMode$);
|
||||||
|
|
||||||
const { dragRef, CustomDragPreview } = useDraggable<AffineDNDData>(
|
const { dragRef, CustomDragPreview } = useDraggable<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -238,12 +229,13 @@ const Select = memo(function Select({
|
|||||||
id,
|
id,
|
||||||
...props
|
...props
|
||||||
}: HTMLProps<HTMLDivElement>) {
|
}: HTMLProps<HTMLDivElement>) {
|
||||||
const { selectMode, selectedDocIds, onToggleSelect } =
|
const contextValue = useContext(DocExplorerContext);
|
||||||
useContext(DocExplorerContext);
|
const selectMode = useLiveData(contextValue.selectMode$);
|
||||||
|
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||||
|
|
||||||
const handleSelectChange = useCallback(() => {
|
const handleSelectChange = useCallback(() => {
|
||||||
id && onToggleSelect(id);
|
id && contextValue.selectedDocIds$?.next([id]);
|
||||||
}, [id, onToggleSelect]);
|
}, [id, contextValue]);
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return null;
|
return null;
|
||||||
@@ -263,7 +255,8 @@ const DocIcon = memo(function DocIcon({
|
|||||||
id,
|
id,
|
||||||
...props
|
...props
|
||||||
}: HTMLProps<HTMLDivElement>) {
|
}: HTMLProps<HTMLDivElement>) {
|
||||||
const { showDocIcon } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const showDocIcon = useLiveData(contextValue.showDocIcon$);
|
||||||
if (!showDocIcon) {
|
if (!showDocIcon) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -289,7 +282,8 @@ const DocPreview = memo(function DocPreview({
|
|||||||
loading,
|
loading,
|
||||||
...props
|
...props
|
||||||
}: HTMLProps<HTMLDivElement> & { loading?: ReactNode }) {
|
}: HTMLProps<HTMLDivElement> & { loading?: ReactNode }) {
|
||||||
const { showDocPreview } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const showDocPreview = useLiveData(contextValue.showDocPreview$);
|
||||||
|
|
||||||
if (!id || !showDocPreview) return null;
|
if (!id || !showDocPreview) return null;
|
||||||
|
|
||||||
@@ -356,7 +350,8 @@ const randomPreviewSkeleton = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
export const CardViewDoc = ({ docId }: DocListItemProps) => {
|
||||||
const { selectMode } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const selectMode = useLiveData(contextValue.selectMode$);
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
const doc = useLiveData(docsService.list.doc$(docId));
|
const doc = useLiveData(docsService.list.doc$(docId));
|
||||||
const [previewSkeleton] = useState(randomPreviewSkeleton);
|
const [previewSkeleton] = useState(randomPreviewSkeleton);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button, IconButton } from '@affine/component';
|
import { Button, IconButton } from '@affine/component';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { ToggleRightIcon } from '@blocksuite/icons/rc';
|
import { ToggleRightIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
type HTMLAttributes,
|
type HTMLAttributes,
|
||||||
@@ -20,14 +21,12 @@ export const DocGroupHeader = ({
|
|||||||
groupId: string;
|
groupId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const {
|
const contextValue = useContext(DocExplorerContext);
|
||||||
selectMode,
|
|
||||||
collapsed,
|
const groups = useLiveData(contextValue.groups$);
|
||||||
groups,
|
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||||
selectedDocIds,
|
const collapsedGroups = useLiveData(contextValue.collapsedGroups$);
|
||||||
onToggleCollapse,
|
const selectMode = useLiveData(contextValue.selectMode$);
|
||||||
onSelect,
|
|
||||||
} = useContext(DocExplorerContext);
|
|
||||||
|
|
||||||
const group = groups.find(g => g.key === groupId);
|
const group = groups.find(g => g.key === groupId);
|
||||||
const isGroupAllSelected = group?.items.every(id =>
|
const isGroupAllSelected = group?.items.every(id =>
|
||||||
@@ -35,24 +34,30 @@ export const DocGroupHeader = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleCollapse = useCallback(() => {
|
const handleToggleCollapse = useCallback(() => {
|
||||||
onToggleCollapse(groupId);
|
const prev = contextValue.collapsedGroups$.value;
|
||||||
}, [groupId, onToggleCollapse]);
|
contextValue.collapsedGroups$.next(
|
||||||
|
prev.includes(groupId)
|
||||||
|
? prev.filter(id => id !== groupId)
|
||||||
|
: [...prev, groupId]
|
||||||
|
);
|
||||||
|
}, [groupId, contextValue]);
|
||||||
|
|
||||||
const handleSelectAll = useCallback(() => {
|
const handleSelectAll = useCallback(() => {
|
||||||
|
const prev = contextValue.selectedDocIds$.value;
|
||||||
if (isGroupAllSelected) {
|
if (isGroupAllSelected) {
|
||||||
onSelect(prev => prev.filter(id => !group?.items.includes(id)));
|
contextValue.selectedDocIds$.next(
|
||||||
|
prev.filter(id => !group?.items.includes(id))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
onSelect(prev => {
|
const newSelected = [...prev];
|
||||||
const newSelected = [...prev];
|
group?.items.forEach(id => {
|
||||||
group?.items.forEach(id => {
|
if (!newSelected.includes(id)) {
|
||||||
if (!newSelected.includes(id)) {
|
newSelected.push(id);
|
||||||
newSelected.push(id);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
return newSelected;
|
|
||||||
});
|
});
|
||||||
|
contextValue.selectedDocIds$.next(newSelected);
|
||||||
}
|
}
|
||||||
}, [group?.items, isGroupAllSelected, onSelect]);
|
}, [contextValue, group?.items, isGroupAllSelected]);
|
||||||
|
|
||||||
const selectedCount = group?.items.filter(id =>
|
const selectedCount = group?.items.filter(id =>
|
||||||
selectedDocIds.includes(id)
|
selectedDocIds.includes(id)
|
||||||
@@ -61,7 +66,7 @@ export const DocGroupHeader = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles.groupHeader}
|
className={styles.groupHeader}
|
||||||
data-collapsed={collapsed.includes(groupId)}
|
data-collapsed={collapsedGroups.includes(groupId)}
|
||||||
>
|
>
|
||||||
<div className={clsx(styles.content, className)} {...props} />
|
<div className={clsx(styles.content, className)} {...props} />
|
||||||
{selectMode ? (
|
{selectMode ? (
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ const cardInlinePropertyOrder: WorkspacePropertyType[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const useProperties = (docId: string, view: 'list' | 'card') => {
|
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 docsService = useService(DocsService);
|
||||||
const workspacePropertyService = useService(WorkspacePropertyService);
|
const workspacePropertyService = useService(WorkspacePropertyService);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export const QuickFavorite = memo(function QuickFavorite({
|
|||||||
doc,
|
doc,
|
||||||
...iconButtonProps
|
...iconButtonProps
|
||||||
}: QuickActionProps) {
|
}: QuickActionProps) {
|
||||||
const { quickFavorite } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const quickFavorite = useLiveData(contextValue.quickFavorite$);
|
||||||
|
|
||||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const favourite = useLiveData(favAdapter.isFavorite$(doc.id, 'doc'));
|
const favourite = useLiveData(favAdapter.isFavorite$(doc.id, 'doc'));
|
||||||
@@ -55,7 +56,8 @@ export const QuickTab = memo(function QuickTab({
|
|||||||
doc,
|
doc,
|
||||||
...iconButtonProps
|
...iconButtonProps
|
||||||
}: QuickActionProps) {
|
}: QuickActionProps) {
|
||||||
const { quickTab } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const quickTab = useLiveData(contextValue.quickTab$);
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const onOpenInNewTab = useCallback(
|
const onOpenInNewTab = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -83,7 +85,8 @@ export const QuickSplit = memo(function QuickSplit({
|
|||||||
doc,
|
doc,
|
||||||
...iconButtonProps
|
...iconButtonProps
|
||||||
}: QuickActionProps) {
|
}: QuickActionProps) {
|
||||||
const { quickSplit } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const quickSplit = useLiveData(contextValue.quickSplit$);
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|
||||||
const onOpenInSplitView = useCallback(
|
const onOpenInSplitView = useCallback(
|
||||||
@@ -115,7 +118,8 @@ export const QuickDelete = memo(function QuickDelete({
|
|||||||
}: QuickActionProps) {
|
}: QuickActionProps) {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const { quickTrash } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const quickTrash = useLiveData(contextValue.quickTrash$);
|
||||||
|
|
||||||
const onMoveToTrash = useCallback(
|
const onMoveToTrash = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -162,8 +166,9 @@ export const QuickSelect = memo(function QuickSelect({
|
|||||||
doc,
|
doc,
|
||||||
...iconButtonProps
|
...iconButtonProps
|
||||||
}: QuickActionProps) {
|
}: QuickActionProps) {
|
||||||
const { quickSelect, selectedDocIds, onToggleSelect } =
|
const contextValue = useContext(DocExplorerContext);
|
||||||
useContext(DocExplorerContext);
|
const quickSelect = useLiveData(contextValue.quickSelect$);
|
||||||
|
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||||
|
|
||||||
const selected = selectedDocIds.includes(doc.id);
|
const selected = selectedDocIds.includes(doc.id);
|
||||||
|
|
||||||
@@ -171,9 +176,13 @@ export const QuickSelect = memo(function QuickSelect({
|
|||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
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) {
|
if (!quickSelect) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from './docs-view/quick-actions';
|
} from './docs-view/quick-actions';
|
||||||
import type { ExplorerPreference } from './types';
|
import type { ExplorerPreference } from './types';
|
||||||
|
|
||||||
export interface QuickAction {
|
interface QuickActionItem {
|
||||||
name: I18nString;
|
name: I18nString;
|
||||||
Component: React.FC<QuickActionProps>;
|
Component: React.FC<QuickActionProps>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -22,7 +22,7 @@ type ExtractPrefixKeys<Obj extends object, Prefix extends string> = {
|
|||||||
|
|
||||||
export type QuickActionKey = ExtractPrefixKeys<ExplorerPreference, 'quick'>;
|
export type QuickActionKey = ExtractPrefixKeys<ExplorerPreference, 'quick'>;
|
||||||
|
|
||||||
const QUICK_ACTION_MAP: Record<QuickActionKey, QuickAction> = {
|
const QUICK_ACTION_MAP: Record<QuickActionKey, QuickActionItem> = {
|
||||||
quickFavorite: {
|
quickFavorite: {
|
||||||
name: 'com.affine.all-docs.quick-action.favorite',
|
name: 'com.affine.all-docs.quick-action.favorite',
|
||||||
Component: QuickFavorite,
|
Component: QuickFavorite,
|
||||||
@@ -50,3 +50,5 @@ export const quickActions = Object.entries(QUICK_ACTION_MAP).map(
|
|||||||
return { key: key as QuickActionKey, ...config };
|
return { key: key as QuickActionKey, ...config };
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type QuickAction = (typeof quickActions)[number];
|
||||||
|
|||||||
+2
-1
@@ -144,6 +144,7 @@ export const CreatedByDocListInlineProperty = ({
|
|||||||
type="CreatedBy"
|
type="CreatedBy"
|
||||||
size={20}
|
size={20}
|
||||||
emptyFallback={null}
|
emptyFallback={null}
|
||||||
|
showName={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -171,7 +172,7 @@ export const ModifiedByGroupHeader = ({
|
|||||||
return (
|
return (
|
||||||
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||||
<div className={styles.userLabelContainer}>
|
<div className={styles.userLabelContainer}>
|
||||||
<PublicUserLabel id={userId} size={20} />
|
<PublicUserLabel id={userId} size={20} showName={false} />
|
||||||
</div>
|
</div>
|
||||||
</PlainTextDocGroupHeader>
|
</PlainTextDocGroupHeader>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -223,6 +223,7 @@ export const WorkspacePropertyTypes = {
|
|||||||
defaultFilter: { method: 'this-week' },
|
defaultFilter: { method: 'this-week' },
|
||||||
showInDocList: 'inline',
|
showInDocList: 'inline',
|
||||||
docListProperty: UpdatedDateDocListProperty,
|
docListProperty: UpdatedDateDocListProperty,
|
||||||
|
groupHeader: CreatedGroupHeader,
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
icon: HistoryIcon,
|
icon: HistoryIcon,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { type MenuProps, RadioGroup, type RadioItem } from '@affine/component';
|
import { type MenuProps, RadioGroup, type RadioItem } from '@affine/component';
|
||||||
import { DocExplorerContext } from '@affine/core/components/explorer/context';
|
import { DocExplorerContext } from '@affine/core/components/explorer/context';
|
||||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
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 { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||||
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
import { useLiveData } from '@toeverything/infra';
|
||||||
import { type Dispatch, type SetStateAction, useContext } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
import * as styles from './all-page-header.css';
|
import * as styles from './all-page-header.css';
|
||||||
|
|
||||||
@@ -27,7 +30,17 @@ const views = [
|
|||||||
] satisfies RadioItem[];
|
] satisfies RadioItem[];
|
||||||
|
|
||||||
const ViewToggle = () => {
|
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 (
|
return (
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
itemHeight={24}
|
itemHeight={24}
|
||||||
@@ -35,7 +48,7 @@ const ViewToggle = () => {
|
|||||||
padding={0}
|
padding={0}
|
||||||
items={views}
|
items={views}
|
||||||
value={view}
|
value={view}
|
||||||
onChange={setView}
|
onChange={handleViewChange}
|
||||||
className={styles.viewToggle}
|
className={styles.viewToggle}
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
indicatorClassName={styles.viewToggleIndicator}
|
indicatorClassName={styles.viewToggleIndicator}
|
||||||
@@ -51,24 +64,14 @@ const menuProps: Partial<MenuProps> = {
|
|||||||
sideOffset: 8,
|
sideOffset: 8,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const AllDocsHeader = ({
|
export const AllDocsHeader = () => {
|
||||||
explorerPreference,
|
|
||||||
setExplorerPreference,
|
|
||||||
}: {
|
|
||||||
explorerPreference: ExplorerPreference;
|
|
||||||
setExplorerPreference: Dispatch<SetStateAction<ExplorerPreference>>;
|
|
||||||
}) => {
|
|
||||||
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
|
<ExplorerDisplayMenuButton menuProps={menuProps} />
|
||||||
preference={explorerPreference}
|
|
||||||
onChange={setExplorerPreference}
|
|
||||||
menuProps={menuProps}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { Masonry, type MasonryGroup, useConfirmModal } from '@affine/component';
|
import { Masonry, type MasonryGroup, useConfirmModal } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
|
createDocExplorerContext,
|
||||||
DocExplorerContext,
|
DocExplorerContext,
|
||||||
type DocExplorerContextType,
|
|
||||||
} from '@affine/core/components/explorer/context';
|
} from '@affine/core/components/explorer/context';
|
||||||
import {
|
import { DocListItem } from '@affine/core/components/explorer/docs-view/doc-list-item';
|
||||||
DocListItem,
|
|
||||||
type DocListItemView,
|
|
||||||
} from '@affine/core/components/explorer/docs-view/doc-list-item';
|
|
||||||
import type { ExplorerPreference } from '@affine/core/components/explorer/types';
|
|
||||||
import { Filters } from '@affine/core/components/filter';
|
import { Filters } from '@affine/core/components/filter';
|
||||||
import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar';
|
import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar';
|
||||||
import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types';
|
import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types';
|
||||||
@@ -49,9 +45,10 @@ const GroupHeader = memo(function GroupHeader({
|
|||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
}) {
|
}) {
|
||||||
const { groupBy } = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
const propertyService = useService(WorkspacePropertyService);
|
const propertyService = useService(WorkspacePropertyService);
|
||||||
const allProperties = useLiveData(propertyService.sortedProperties$);
|
const allProperties = useLiveData(propertyService.sortedProperties$);
|
||||||
|
const groupBy = useLiveData(contextValue.groupBy$);
|
||||||
|
|
||||||
const groupType = groupBy?.type;
|
const groupType = groupBy?.type;
|
||||||
const groupKey = groupBy?.key;
|
const groupKey = groupBy?.key;
|
||||||
@@ -103,30 +100,17 @@ const DocListItemComponent = memo(function DocListItemComponent({
|
|||||||
export const AllPage = () => {
|
export const AllPage = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const docsService = useService(DocsService);
|
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] =
|
const [explorerContextValue] = useState(createDocExplorerContext);
|
||||||
useState<ExplorerPreference>({
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
type: 'system',
|
|
||||||
key: 'trash',
|
|
||||||
value: 'false',
|
|
||||||
method: 'is',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
displayProperties: [],
|
|
||||||
showDocIcon: true,
|
|
||||||
showDocPreview: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
@@ -159,14 +143,10 @@ export const AllPage = () => {
|
|||||||
const collectionRulesService = useService(CollectionRulesService);
|
const collectionRulesService = useService(CollectionRulesService);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = collectionRulesService
|
const subscription = collectionRulesService
|
||||||
.watch(
|
.watch(filters ?? [], groupBy, orderBy)
|
||||||
explorerPreference.filters ?? [],
|
|
||||||
explorerPreference.groupBy,
|
|
||||||
explorerPreference.orderBy
|
|
||||||
)
|
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: result => {
|
next: result => {
|
||||||
setGroups(result.groups);
|
explorerContextValue.groups$.next(result.groups);
|
||||||
},
|
},
|
||||||
error: error => {
|
error: error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -177,58 +157,38 @@ export const AllPage = () => {
|
|||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
collectionRulesService,
|
collectionRulesService,
|
||||||
explorerPreference.filters,
|
explorerContextValue.groups$,
|
||||||
explorerPreference.groupBy,
|
filters,
|
||||||
explorerPreference.orderBy,
|
groupBy,
|
||||||
|
orderBy,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
setSelectMode(false);
|
explorerContextValue.selectMode$.next(false);
|
||||||
setSelectedDocIds([]);
|
explorerContextValue.selectedDocIds$.next([]);
|
||||||
setPrevCheckAnchorId(null);
|
explorerContextValue.prevCheckAnchorId$.next(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [explorerContextValue]);
|
||||||
|
|
||||||
const handleFilterChange = useCallback((filters: FilterParams[]) => {
|
const handleFilterChange = useCallback(
|
||||||
setExplorerPreference(prev => ({
|
(filters: FilterParams[]) => {
|
||||||
...prev,
|
explorerContextValue.filters$.next(filters);
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
[]
|
[explorerContextValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCloseFloatingToolbar = useCallback(() => {
|
const handleCloseFloatingToolbar = useCallback(() => {
|
||||||
setSelectMode(false);
|
explorerContextValue.selectMode$.next(false);
|
||||||
setSelectedDocIds([]);
|
explorerContextValue.selectedDocIds$.next([]);
|
||||||
}, []);
|
}, [explorerContextValue]);
|
||||||
|
|
||||||
const handleMultiDelete = useCallback(() => {
|
const handleMultiDelete = useCallback(() => {
|
||||||
if (selectedDocIds.length === 0) {
|
if (selectedDocIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -257,54 +217,18 @@ export const AllPage = () => {
|
|||||||
});
|
});
|
||||||
}, [docsService.list, openConfirmModal, selectedDocIds, t]);
|
}, [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 (
|
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 />
|
||||||
explorerPreference={explorerPreference}
|
|
||||||
setExplorerPreference={setExplorerPreference}
|
|
||||||
/>
|
|
||||||
</ViewHeader>
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
<div className={styles.filterArea}>
|
<div className={styles.filterArea}>
|
||||||
<MigrationAllDocsDataNotification />
|
<MigrationAllDocsDataNotification />
|
||||||
<Filters
|
<Filters filters={filters ?? []} onChange={handleFilterChange} />
|
||||||
filters={explorerPreference.filters ?? []}
|
|
||||||
onChange={handleFilterChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.scrollArea}>
|
<div className={styles.scrollArea}>
|
||||||
<Masonry
|
<Masonry
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export const publicUserLabelRemoved = style([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const publicUserLabelAvatar = style({
|
export const publicUserLabelAvatar = style({
|
||||||
marginRight: '0.5em',
|
selectors: {
|
||||||
transform: 'translateY(4px)',
|
'&[data-show-name="true"]': {
|
||||||
|
marginRight: '0.5em',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export const PublicUserLabel = ({
|
|||||||
name={user?.name ?? ''}
|
name={user?.name ?? ''}
|
||||||
size={size}
|
size={size}
|
||||||
className={styles.publicUserLabelAvatar}
|
className={styles.publicUserLabelAvatar}
|
||||||
|
data-show-name={showName}
|
||||||
/>
|
/>
|
||||||
{showName && user?.name}
|
{showName && user?.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -108,7 +108,13 @@ export class DocsStore extends Store {
|
|||||||
if (pages instanceof YArray) {
|
if (pages instanceof YArray) {
|
||||||
return pages.map(v => ({
|
return pages.map(v => ({
|
||||||
id: v.get('id') as string,
|
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 {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
Reference in New Issue
Block a user