mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 13:25:12 +00:00
feat(core): new doc list for editing collection docs and rules (#12320)
close AF-2626 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for debounced input changes in input fields, improving performance for rapid typing scenarios. - Enhanced document explorer with dynamic visibility controls for drag handles and "more" menu options. - Introduced a new filter for searching documents by title, enabling more precise filtering in collections. - Added a direct search method for document titles to improve search accuracy and speed. - **Bug Fixes** - Improved layout and centering of icons in document list items. - Updated border styles across collection editor components for a more consistent appearance. - **Refactor** - Simplified page selection and rule-matching logic in collection and selector components by consolidating state management and leveraging context-driven rendering. - Removed deprecated and redundant hooks for page list configuration. - **Chores** - Updated code to use new theme variables for border colors, ensuring visual consistency with the latest design standards. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
export const useDebounceCallback = <T extends (...args: any[]) => any>(
|
||||||
|
callback: T,
|
||||||
|
delay?: number,
|
||||||
|
options?: Parameters<typeof debounce>[2]
|
||||||
|
) => {
|
||||||
|
const callbackRef = useRef(callback);
|
||||||
|
|
||||||
|
const debouncedCallback = useMemo(
|
||||||
|
() => debounce(callbackRef.current, delay, options),
|
||||||
|
[delay, options]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
callbackRef.current = callback;
|
||||||
|
}, [callback]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedCallback.cancel();
|
||||||
|
};
|
||||||
|
}, [debouncedCallback]);
|
||||||
|
|
||||||
|
return debouncedCallback;
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
import { forwardRef, useCallback, useEffect, useState } from 'react';
|
import { forwardRef, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useAutoFocus, useAutoSelect } from '../../hooks';
|
import { useAutoFocus, useAutoSelect } from '../../hooks';
|
||||||
|
import { useDebounceCallback } from '../../hooks/use-debounce-callback';
|
||||||
|
|
||||||
export type RowInputProps = {
|
export type RowInputProps = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -21,6 +22,7 @@ export type RowInputProps = {
|
|||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
onEnter?: (value: string) => void;
|
onEnter?: (value: string) => void;
|
||||||
[key: `data-${string}`]: string;
|
[key: `data-${string}`]: string;
|
||||||
|
debounce?: number;
|
||||||
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size' | 'onBlur'>;
|
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'size' | 'onBlur'>;
|
||||||
|
|
||||||
// RowInput component that is used in the selector layout for search input
|
// RowInput component that is used in the selector layout for search input
|
||||||
@@ -37,6 +39,7 @@ export const RowInput = forwardRef<HTMLInputElement, RowInputProps>(
|
|||||||
onBlur,
|
onBlur,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
autoSelect,
|
autoSelect,
|
||||||
|
debounce,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: RowInputProps,
|
}: RowInputProps,
|
||||||
upstreamRef: ForwardedRef<HTMLInputElement>
|
upstreamRef: ForwardedRef<HTMLInputElement>
|
||||||
@@ -66,7 +69,7 @@ export const RowInput = forwardRef<HTMLInputElement, RowInputProps>(
|
|||||||
if (!onBlur) return;
|
if (!onBlur) return;
|
||||||
selectRef.current?.addEventListener('blur', onBlur as any);
|
selectRef.current?.addEventListener('blur', onBlur as any);
|
||||||
return () => {
|
return () => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
||||||
selectRef.current?.removeEventListener('blur', onBlur as any);
|
selectRef.current?.removeEventListener('blur', onBlur as any);
|
||||||
};
|
};
|
||||||
}, [onBlur, selectRef]);
|
}, [onBlur, selectRef]);
|
||||||
@@ -77,6 +80,7 @@ export const RowInput = forwardRef<HTMLInputElement, RowInputProps>(
|
|||||||
},
|
},
|
||||||
[propsOnChange]
|
[propsOnChange]
|
||||||
);
|
);
|
||||||
|
const debounceHandleChange = useDebounceCallback(handleChange, debounce);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
@@ -105,7 +109,7 @@ export const RowInput = forwardRef<HTMLInputElement, RowInputProps>(
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={style}
|
style={style}
|
||||||
onChange={handleChange}
|
onChange={debounce ? debounceHandleChange : handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onCompositionStart={handleCompositionStart}
|
onCompositionStart={handleCompositionStart}
|
||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ export const listIcon = style({
|
|||||||
height: 24,
|
height: 24,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
color: cssVarV2.icon.primary,
|
color: cssVarV2.icon.primary,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
});
|
});
|
||||||
export const listContent = style({
|
export const listContent = style({
|
||||||
width: 0,
|
width: 0,
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ const DragHandle = memo(function DragHandle({
|
|||||||
}: HTMLProps<HTMLDivElement> & { preview?: ReactNode }) {
|
}: HTMLProps<HTMLDivElement> & { preview?: ReactNode }) {
|
||||||
const contextValue = useContext(DocExplorerContext);
|
const contextValue = useContext(DocExplorerContext);
|
||||||
const selectMode = useLiveData(contextValue.selectMode$);
|
const selectMode = useLiveData(contextValue.selectMode$);
|
||||||
|
const showDragHandle = useLiveData(contextValue.showDragHandle$);
|
||||||
|
|
||||||
const { dragRef, CustomDragPreview } = useDraggable<AffineDNDData>(
|
const { dragRef, CustomDragPreview } = useDraggable<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -210,7 +211,7 @@ const DragHandle = memo(function DragHandle({
|
|||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectMode || !id) {
|
if (selectMode || !id || !showDragHandle) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const calcCardHeightById = (id: string) => {
|
|||||||
return 250 + value * 10;
|
return 250 + value * 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DocListItemComponent = memo(function DocListItemComponent({
|
export const DocListItemComponent = memo(function DocListItemComponent({
|
||||||
itemId,
|
itemId,
|
||||||
groupId,
|
groupId,
|
||||||
}: {
|
}: {
|
||||||
@@ -216,22 +216,24 @@ export const DocsExplorer = ({
|
|||||||
[]
|
[]
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ListFloatingToolbar
|
{!disableMultiDelete ? (
|
||||||
open={!!selectMode}
|
<ListFloatingToolbar
|
||||||
onDelete={handleMultiDelete}
|
open={!!selectMode}
|
||||||
onClose={handleCloseFloatingToolbar}
|
onDelete={handleMultiDelete}
|
||||||
content={
|
onClose={handleCloseFloatingToolbar}
|
||||||
<Trans
|
content={
|
||||||
i18nKey="com.affine.page.toolbar.selected"
|
<Trans
|
||||||
count={selectedDocIds.length}
|
i18nKey="com.affine.page.toolbar.selected"
|
||||||
>
|
count={selectedDocIds.length}
|
||||||
<div style={{ color: cssVarV2.text.secondary }}>
|
>
|
||||||
{{ count: selectedDocIds.length } as any}
|
<div style={{ color: cssVarV2.text.secondary }}>
|
||||||
</div>
|
{{ count: selectedDocIds.length } as any}
|
||||||
selected
|
</div>
|
||||||
</Trans>
|
selected
|
||||||
}
|
</Trans>
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ import {
|
|||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
import { useBlockSuiteMetaHelper } from '../../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../../hooks/affine/use-block-suite-meta-helper';
|
||||||
import { IsFavoriteIcon } from '../../pure/icons';
|
import { IsFavoriteIcon } from '../../pure/icons';
|
||||||
|
import { DocExplorerContext } from '../context';
|
||||||
|
|
||||||
interface DocOperationProps {
|
interface DocOperationProps {
|
||||||
docId: string;
|
docId: string;
|
||||||
@@ -203,6 +204,13 @@ export const MoreMenuButton = ({
|
|||||||
docId: string;
|
docId: string;
|
||||||
iconProps?: IconButtonProps;
|
iconProps?: IconButtonProps;
|
||||||
}) => {
|
}) => {
|
||||||
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const showMoreOperation = useLiveData(contextValue.showMoreOperation$);
|
||||||
|
|
||||||
|
if (!showMoreOperation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MoreMenu docId={docId} {...menuProps}>
|
<MoreMenu docId={docId} {...menuProps}>
|
||||||
<IconButton icon={<MoreVerticalIcon />} {...iconProps} />
|
<IconButton icon={<MoreVerticalIcon />} {...iconProps} />
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface ExplorerDisplayPreference {
|
|||||||
displayProperties?: string[];
|
displayProperties?: string[];
|
||||||
showDocIcon?: boolean;
|
showDocIcon?: boolean;
|
||||||
showDocPreview?: boolean;
|
showDocPreview?: boolean;
|
||||||
|
showMoreOperation?: boolean;
|
||||||
|
showDragHandle?: boolean;
|
||||||
quickFavorite?: boolean;
|
quickFavorite?: boolean;
|
||||||
quickTrash?: boolean;
|
quickTrash?: boolean;
|
||||||
quickSplit?: boolean;
|
quickSplit?: boolean;
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { toast } from '@affine/component';
|
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
|
||||||
import { FavoriteTag } from '@affine/core/components/page-list/components/favorite-tag';
|
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
|
||||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
|
||||||
import { PublicDocMode } from '@affine/graphql';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import type { DocMeta, Workspace } from '@blocksuite/affine/store';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { type ReactNode, useCallback, useEffect, useMemo } from 'react';
|
|
||||||
|
|
||||||
export type AllPageListConfig = {
|
|
||||||
allPages: DocMeta[];
|
|
||||||
docCollection: Workspace;
|
|
||||||
/**
|
|
||||||
* Return `undefined` if the page is not public
|
|
||||||
*/
|
|
||||||
getPublicMode: (id: string) => undefined | 'page' | 'edgeless';
|
|
||||||
getPage: (id: string) => DocMeta | undefined;
|
|
||||||
favoriteRender: (page: DocMeta) => ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated very poor performance
|
|
||||||
*/
|
|
||||||
export const useAllPageListConfig = () => {
|
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
|
||||||
const shareDocsListService = useService(ShareDocsListService);
|
|
||||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// TODO(@eyhn): loading & error UI
|
|
||||||
shareDocsListService.shareDocs?.revalidate();
|
|
||||||
}, [shareDocsListService]);
|
|
||||||
|
|
||||||
const workspace = currentWorkspace.docCollection;
|
|
||||||
const pageMetas = useBlockSuiteDocMeta(workspace);
|
|
||||||
const pageMap = useMemo(
|
|
||||||
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
|
|
||||||
[pageMetas]
|
|
||||||
);
|
|
||||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
|
||||||
const t = useI18n();
|
|
||||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
|
||||||
|
|
||||||
const isActive = useCallback(
|
|
||||||
(page: DocMeta) => {
|
|
||||||
return favoriteItems.some(fav => fav.id === page.id);
|
|
||||||
},
|
|
||||||
[favoriteItems]
|
|
||||||
);
|
|
||||||
const onToggleFavoritePage = useCallback(
|
|
||||||
(page: DocMeta) => {
|
|
||||||
const status = isActive(page);
|
|
||||||
favAdapter.toggle(page.id, 'doc');
|
|
||||||
toast(
|
|
||||||
status
|
|
||||||
? t['com.affine.toastMessage.removedFavorites']()
|
|
||||||
: t['com.affine.toastMessage.addedFavorites']()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[favAdapter, isActive, t]
|
|
||||||
);
|
|
||||||
|
|
||||||
return useMemo<AllPageListConfig>(() => {
|
|
||||||
return {
|
|
||||||
allPages: pageMetas,
|
|
||||||
getPublicMode(id) {
|
|
||||||
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
|
|
||||||
if (mode === PublicDocMode.Edgeless) {
|
|
||||||
return 'edgeless';
|
|
||||||
} else if (mode === PublicDocMode.Page) {
|
|
||||||
return 'page';
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
docCollection: currentWorkspace.docCollection,
|
|
||||||
getPage: id => pageMap[id],
|
|
||||||
favoriteRender: page => {
|
|
||||||
return (
|
|
||||||
<FavoriteTag
|
|
||||||
style={{ marginRight: 8 }}
|
|
||||||
onClick={() => onToggleFavoritePage(page)}
|
|
||||||
active={isActive(page)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
pageMetas,
|
|
||||||
currentWorkspace.docCollection,
|
|
||||||
shareDocs,
|
|
||||||
pageMap,
|
|
||||||
isActive,
|
|
||||||
onToggleFavoritePage,
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
@@ -1,37 +1,26 @@
|
|||||||
import { IconButton, Menu, toast } from '@affine/component';
|
import { IconButton, Menu } from '@affine/component';
|
||||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
|
||||||
import {
|
import {
|
||||||
CollectionRulesService,
|
CollectionRulesService,
|
||||||
type FilterParams,
|
type FilterParams,
|
||||||
} from '@affine/core/modules/collection-rules';
|
} from '@affine/core/modules/collection-rules';
|
||||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
|
||||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
|
||||||
import { FilterIcon } from '@blocksuite/icons/rc';
|
import { FilterIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useServices } from '@toeverything/infra';
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
import {
|
import { memo, type ReactNode, useCallback, useEffect, useState } from 'react';
|
||||||
type ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDocExplorerContext,
|
||||||
|
DocExplorerContext,
|
||||||
|
} from '../../explorer/context';
|
||||||
|
import { DocsExplorer } from '../../explorer/docs-view/docs-list';
|
||||||
import { Filters } from '../../filter';
|
import { Filters } from '../../filter';
|
||||||
import { AddFilterMenu } from '../../filter/add-filter';
|
import { AddFilterMenu } from '../../filter/add-filter';
|
||||||
import { AffineShapeIcon, FavoriteTag } from '..';
|
import { AffineShapeIcon } from '..';
|
||||||
import { usePageHeaderColsDef } from '../header-col-def';
|
|
||||||
import { PageListItemRenderer } from '../page-group';
|
|
||||||
import { ListTableHeader } from '../page-header';
|
|
||||||
import { SelectorLayout } from '../selector/selector-layout';
|
import { SelectorLayout } from '../selector/selector-layout';
|
||||||
import type { ListItem } from '../types';
|
|
||||||
import { VirtualizedList } from '../virtualized-list';
|
|
||||||
import * as styles from './select-page.css';
|
import * as styles from './select-page.css';
|
||||||
import { useSearch } from './use-search';
|
|
||||||
|
|
||||||
export const SelectPage = ({
|
export const SelectPage = memo(function SelectPage({
|
||||||
init = [],
|
init = [],
|
||||||
onConfirm,
|
onConfirm,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -46,87 +35,89 @@ export const SelectPage = ({
|
|||||||
init?: string[];
|
init?: string[];
|
||||||
onConfirm?: (data: string[]) => void;
|
onConfirm?: (data: string[]) => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}) => {
|
}) {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [value, setValue] = useState(init);
|
const [searchText, setSearchText] = useState('');
|
||||||
const onChange = useCallback(
|
|
||||||
(value: string[]) => {
|
const { shareDocsListService, collectionRulesService } = useServices({
|
||||||
propsOnChange?.(value);
|
|
||||||
setValue(value);
|
|
||||||
},
|
|
||||||
[propsOnChange]
|
|
||||||
);
|
|
||||||
const confirm = useCallback(() => {
|
|
||||||
onConfirm?.(value);
|
|
||||||
}, [value, onConfirm]);
|
|
||||||
const clearSelected = useCallback(() => {
|
|
||||||
onChange([]);
|
|
||||||
}, [onChange]);
|
|
||||||
const {
|
|
||||||
workspaceService,
|
|
||||||
compatibleFavoriteItemsAdapter,
|
|
||||||
shareDocsListService,
|
|
||||||
collectionRulesService,
|
|
||||||
} = useServices({
|
|
||||||
ShareDocsListService,
|
ShareDocsListService,
|
||||||
WorkspaceService,
|
|
||||||
CompatibleFavoriteItemsAdapter,
|
|
||||||
CollectionRulesService,
|
CollectionRulesService,
|
||||||
});
|
});
|
||||||
const workspace = workspaceService.workspace;
|
const [docExplorerContextValue] = useState(() => {
|
||||||
const docCollection = workspace.docCollection;
|
return createDocExplorerContext({
|
||||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
displayProperties: ['createdAt', 'updatedAt', 'tags'],
|
||||||
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
quickFavorite: true,
|
||||||
|
showMoreOperation: false,
|
||||||
|
showDragHandle: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// init context value
|
||||||
|
useEffect(() => {
|
||||||
|
docExplorerContextValue.selectMode$.next(true);
|
||||||
|
docExplorerContextValue.selectedDocIds$.next(init);
|
||||||
|
}, [
|
||||||
|
docExplorerContextValue.selectMode$,
|
||||||
|
docExplorerContextValue.selectedDocIds$,
|
||||||
|
init,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const groups = useLiveData(docExplorerContextValue.groups$);
|
||||||
|
const selectedDocIds = useLiveData(docExplorerContextValue.selectedDocIds$);
|
||||||
|
const isEmpty =
|
||||||
|
groups.length === 0 ||
|
||||||
|
(groups.length && groups.every(group => group.items.length === 0));
|
||||||
|
|
||||||
|
const confirm = useCallback(() => {
|
||||||
|
onConfirm?.(docExplorerContextValue.selectedDocIds$.value);
|
||||||
|
}, [onConfirm, docExplorerContextValue.selectedDocIds$]);
|
||||||
|
const clearSelected = useCallback(() => {
|
||||||
|
docExplorerContextValue.selectedDocIds$.next([]);
|
||||||
|
}, [docExplorerContextValue.selectedDocIds$]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const ob = docExplorerContextValue.selectedDocIds$.subscribe(value => {
|
||||||
|
propsOnChange?.(value);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
ob.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [propsOnChange, docExplorerContextValue.selectedDocIds$]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shareDocsListService.shareDocs?.revalidate();
|
shareDocsListService.shareDocs?.revalidate();
|
||||||
}, [shareDocsListService.shareDocs]);
|
}, [shareDocsListService.shareDocs]);
|
||||||
|
|
||||||
const isFavorite = useCallback(
|
|
||||||
(meta: DocMeta) => favourites.some(fav => fav.id === meta.id),
|
|
||||||
[favourites]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onToggleFavoritePage = useCallback(
|
|
||||||
(page: DocMeta) => {
|
|
||||||
const status = isFavorite(page);
|
|
||||||
compatibleFavoriteItemsAdapter.toggle(page.id, 'doc');
|
|
||||||
toast(
|
|
||||||
status
|
|
||||||
? t['com.affine.toastMessage.removedFavorites']()
|
|
||||||
: t['com.affine.toastMessage.addedFavorites']()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[compatibleFavoriteItemsAdapter, isFavorite, t]
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
|
||||||
const [filters, setFilters] = useState<FilterParams[]>([]);
|
const [filters, setFilters] = useState<FilterParams[]>([]);
|
||||||
|
|
||||||
const [filteredDocIds, setFilteredDocIds] = useState<string[]>([]);
|
|
||||||
const filteredPageMetas = useMemo(() => {
|
|
||||||
const idSet = new Set(filteredDocIds);
|
|
||||||
return pageMetas.filter(page => idSet.has(page.id));
|
|
||||||
}, [pageMetas, filteredDocIds]);
|
|
||||||
|
|
||||||
const { searchText, updateSearchText, searchedList } =
|
|
||||||
useSearch(filteredPageMetas);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const searchFilter = searchText
|
||||||
|
? {
|
||||||
|
type: 'system',
|
||||||
|
key: 'title',
|
||||||
|
method: 'match',
|
||||||
|
value: searchText,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const watchFilters: FilterParams[] =
|
||||||
|
filters.length > 0
|
||||||
|
? filters
|
||||||
|
: [
|
||||||
|
// if no filters are present, match all non-trash documents
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'trash',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (searchFilter) {
|
||||||
|
watchFilters.push(searchFilter);
|
||||||
|
}
|
||||||
const subscription = collectionRulesService
|
const subscription = collectionRulesService
|
||||||
.watch({
|
.watch({
|
||||||
filters:
|
filters: watchFilters,
|
||||||
filters.length > 0
|
|
||||||
? filters
|
|
||||||
: [
|
|
||||||
// if no filters are present, match all non-trash documents
|
|
||||||
{
|
|
||||||
type: 'system',
|
|
||||||
key: 'trash',
|
|
||||||
method: 'is',
|
|
||||||
value: 'false',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extraFilters: [
|
extraFilters: [
|
||||||
{
|
{
|
||||||
type: 'system',
|
type: 'system',
|
||||||
@@ -143,40 +134,23 @@ export const SelectPage = ({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
.subscribe(result => {
|
.subscribe(result => {
|
||||||
setFilteredDocIds(result.groups.flatMap(group => group.items));
|
docExplorerContextValue.groups$.next(result.groups);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
};
|
};
|
||||||
}, [collectionRulesService, filters]);
|
}, [
|
||||||
|
collectionRulesService,
|
||||||
const operationsRenderer = useCallback(
|
docExplorerContextValue.groups$,
|
||||||
(item: ListItem) => {
|
filters,
|
||||||
const page = item as DocMeta;
|
searchText,
|
||||||
return (
|
]);
|
||||||
<FavoriteTag
|
|
||||||
style={{ marginRight: 8 }}
|
|
||||||
onClick={() => onToggleFavoritePage(page)}
|
|
||||||
active={isFavorite(page)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[isFavorite, onToggleFavoritePage]
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageHeaderRenderer = useCallback(() => {
|
|
||||||
return <ListTableHeader headerCols={pageHeaderColsDef} />;
|
|
||||||
}, [pageHeaderColsDef]);
|
|
||||||
|
|
||||||
const pageItemRenderer = useCallback((item: ListItem) => {
|
|
||||||
return <PageListItemRenderer {...item} />;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectorLayout
|
<SelectorLayout
|
||||||
searchPlaceholder={t['com.affine.editCollection.search.placeholder']()}
|
searchPlaceholder={t['com.affine.editCollection.search.placeholder']()}
|
||||||
selectedCount={value.length}
|
selectedCount={selectedDocIds.length}
|
||||||
onSearch={updateSearchText}
|
onSearch={setSearchText}
|
||||||
onClear={clearSelected}
|
onClear={clearSelected}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
onConfirm={confirm}
|
onConfirm={confirm}
|
||||||
@@ -206,25 +180,17 @@ export const SelectPage = ({
|
|||||||
<Filters filters={filters} onChange={setFilters} />
|
<Filters filters={filters} onChange={setFilters} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{searchedList.length ? (
|
{!isEmpty ? (
|
||||||
<VirtualizedList
|
<DocExplorerContext.Provider value={docExplorerContextValue}>
|
||||||
className={styles.pageList}
|
<DocsExplorer disableMultiDelete />
|
||||||
items={searchedList}
|
</DocExplorerContext.Provider>
|
||||||
docCollection={docCollection}
|
|
||||||
selectable
|
|
||||||
onSelectedIdsChange={onChange}
|
|
||||||
selectedIds={value}
|
|
||||||
operationsRenderer={operationsRenderer}
|
|
||||||
itemRenderer={pageItemRenderer}
|
|
||||||
headerRenderer={pageHeaderRenderer}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<EmptyList search={searchText} />
|
<EmptyList search={searchText} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SelectorLayout>
|
</SelectorLayout>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
export const EmptyList = ({ search }: { search?: string }) => {
|
export const EmptyList = ({ search }: { search?: string }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const SelectorLayout = ({
|
|||||||
className={styles.search}
|
className={styles.search}
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
onChange={onSearchChange}
|
onChange={onSearchChange}
|
||||||
|
debounce={200}
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const ellipsis = style({
|
export const ellipsis = style({
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -14,22 +15,25 @@ export const rulesBottom = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
padding: '20px 24px',
|
padding: '20px 24px',
|
||||||
borderTop: `1px solid ${cssVar('borderColor')}`,
|
borderTop: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: '12px',
|
gap: '12px',
|
||||||
});
|
});
|
||||||
|
export const includeListGroup = style({
|
||||||
|
borderTop: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
|
});
|
||||||
export const includeListTitle = style({
|
export const includeListTitle = style({
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
color: cssVar('textSecondaryColor'),
|
color: cssVar('textSecondaryColor'),
|
||||||
padding: '4px 16px',
|
padding: '8px',
|
||||||
borderTop: `1px solid ${cssVar('borderColor')}`,
|
paddingBottom: 0,
|
||||||
});
|
});
|
||||||
export const rulesContainerRight = style({
|
export const rulesContainerRight = style({
|
||||||
flex: 2,
|
flex: 2,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
borderLeft: `1px solid ${cssVar('borderColor')}`,
|
borderLeft: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
});
|
});
|
||||||
@@ -60,7 +64,7 @@ export const includeItem = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
border: `1px solid ${cssVar('borderColor')}`,
|
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: '4px 8px 4px',
|
padding: '4px 8px 4px',
|
||||||
});
|
});
|
||||||
@@ -143,5 +147,5 @@ export const rulesTitle = style({
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
lineHeight: '24px',
|
lineHeight: '24px',
|
||||||
color: cssVar('textSecondaryColor'),
|
color: cssVar('textSecondaryColor'),
|
||||||
borderBottom: `1px solid ${cssVar('borderColor')}`,
|
borderBottom: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Button, RadioGroup } from '@affine/component';
|
import { Button, RadioGroup } from '@affine/component';
|
||||||
import { useAllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
|
||||||
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
|
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
|
||||||
import type { CollectionInfo } from '@affine/core/modules/collection';
|
import type { CollectionInfo } from '@affine/core/modules/collection';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
@@ -26,7 +25,6 @@ export const EditCollection = ({
|
|||||||
mode: initMode,
|
mode: initMode,
|
||||||
}: EditCollectionProps) => {
|
}: EditCollectionProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const config = useAllPageListConfig();
|
|
||||||
const [value, onChange] = useState<CollectionInfo>(init);
|
const [value, onChange] = useState<CollectionInfo>(init);
|
||||||
const [mode, setMode] = useState<'page' | 'rule'>(
|
const [mode, setMode] = useState<'page' | 'rule'>(
|
||||||
initMode ?? (init.rules.filters.length === 0 ? 'page' : 'rule')
|
initMode ?? (init.rules.filters.length === 0 ? 'page' : 'rule')
|
||||||
@@ -44,12 +42,9 @@ export const EditCollection = ({
|
|||||||
allowList: init.allowList,
|
allowList: init.allowList,
|
||||||
});
|
});
|
||||||
}, [init, value]);
|
}, [init, value]);
|
||||||
const onIdsChange = useCallback(
|
const onIdsChange = useCallback((ids: string[]) => {
|
||||||
(ids: string[]) => {
|
onChange(prev => ({ ...prev, allowList: ids }));
|
||||||
onChange({ ...value, allowList: ids });
|
}, []);
|
||||||
},
|
|
||||||
[value]
|
|
||||||
);
|
|
||||||
const buttons = useMemo(
|
const buttons = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<>
|
<>
|
||||||
@@ -104,14 +99,13 @@ export const EditCollection = ({
|
|||||||
>
|
>
|
||||||
{mode === 'page' ? (
|
{mode === 'page' ? (
|
||||||
<SelectPage
|
<SelectPage
|
||||||
init={value.allowList}
|
init={init.allowList}
|
||||||
onChange={onIdsChange}
|
onChange={onIdsChange}
|
||||||
header={switchMode}
|
header={switchMode}
|
||||||
buttons={buttons}
|
buttons={buttons}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RulesMode
|
<RulesMode
|
||||||
allPageListConfig={config}
|
|
||||||
collection={value}
|
collection={value}
|
||||||
switchMode={switchMode}
|
switchMode={switchMode}
|
||||||
reset={reset}
|
reset={reset}
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
import { Button, IconButton, Tooltip } from '@affine/component';
|
|
||||||
import { Filters } from '@affine/core/components/filter';
|
|
||||||
import type { AllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
|
||||||
import {
|
import {
|
||||||
AffineShapeIcon,
|
Button,
|
||||||
List,
|
IconButton,
|
||||||
type ListItem,
|
Masonry,
|
||||||
ListScrollContainer,
|
type MasonryGroup,
|
||||||
} from '@affine/core/components/page-list';
|
Tooltip,
|
||||||
|
} from '@affine/component';
|
||||||
|
import {
|
||||||
|
createDocExplorerContext,
|
||||||
|
DocExplorerContext,
|
||||||
|
} from '@affine/core/components/explorer/context';
|
||||||
|
import { DocListItemComponent } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||||
|
import { Filters } from '@affine/core/components/filter';
|
||||||
|
import { AffineShapeIcon } from '@affine/core/components/page-list';
|
||||||
import type { CollectionInfo } from '@affine/core/modules/collection';
|
import type { CollectionInfo } from '@affine/core/modules/collection';
|
||||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
|
||||||
import {
|
import {
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
EdgelessIcon,
|
EdgelessIcon,
|
||||||
PageIcon,
|
PageIcon,
|
||||||
ToggleRightIcon,
|
ToggleRightIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
import { useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { memo, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './edit-collection.css';
|
import * as styles from './edit-collection.css';
|
||||||
|
|
||||||
@@ -32,20 +37,26 @@ export const RulesMode = ({
|
|||||||
reset,
|
reset,
|
||||||
buttons,
|
buttons,
|
||||||
switchMode,
|
switchMode,
|
||||||
allPageListConfig,
|
|
||||||
}: {
|
}: {
|
||||||
collection: CollectionInfo;
|
collection: CollectionInfo;
|
||||||
updateCollection: (collection: CollectionInfo) => void;
|
updateCollection: (collection: CollectionInfo) => void;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
buttons: ReactNode;
|
buttons: ReactNode;
|
||||||
switchMode: ReactNode;
|
switchMode: ReactNode;
|
||||||
allPageListConfig: AllPageListConfig;
|
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [showPreview, setShowPreview] = useState(true);
|
const [showPreview, setShowPreview] = useState(true);
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
const collectionRulesService = useService(CollectionRulesService);
|
const collectionRulesService = useService(CollectionRulesService);
|
||||||
const [rulesPageIds, setRulesPageIds] = useState<string[]>([]);
|
const [rulesPageIds, setRulesPageIds] = useState<string[]>([]);
|
||||||
|
const [docExplorerContextValue] = useState(() =>
|
||||||
|
createDocExplorerContext({
|
||||||
|
displayProperties: ['createdAt', 'updatedAt', 'tags'],
|
||||||
|
showDragHandle: false,
|
||||||
|
showMoreOperation: false,
|
||||||
|
quickFavorite: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = collectionRulesService
|
const subscription = collectionRulesService
|
||||||
@@ -74,32 +85,58 @@ export const RulesMode = ({
|
|||||||
};
|
};
|
||||||
}, [collection, collectionRulesService]);
|
}, [collection, collectionRulesService]);
|
||||||
|
|
||||||
const rulesPages = useMemo(() => {
|
const masonryItems = useMemo(
|
||||||
return allPageListConfig.allPages.filter(meta => {
|
() =>
|
||||||
return rulesPageIds.includes(meta.id);
|
[
|
||||||
});
|
{
|
||||||
}, [allPageListConfig.allPages, rulesPageIds]);
|
id: 'rules-group',
|
||||||
|
height: 0,
|
||||||
const allowListPages = useMemo(() => {
|
children: null,
|
||||||
return allPageListConfig.allPages.filter(meta => {
|
items: rulesPageIds.length
|
||||||
return (
|
? rulesPageIds.map(docId => {
|
||||||
collection.allowList.includes(meta.id) &&
|
return {
|
||||||
!rulesPageIds.includes(meta.id) &&
|
id: docId,
|
||||||
!meta.trash
|
height: 42,
|
||||||
);
|
Component: DocListItemComponent,
|
||||||
});
|
};
|
||||||
}, [allPageListConfig.allPages, collection.allowList, rulesPageIds]);
|
})
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: 'rules-empty',
|
||||||
|
height: 300,
|
||||||
|
children: (
|
||||||
|
<RulesEmpty
|
||||||
|
noRules={collection.rules.filters.length === 0}
|
||||||
|
fullHeight
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'allow-list-group',
|
||||||
|
height: 30,
|
||||||
|
children: (
|
||||||
|
<div className={styles.includeListTitle}>
|
||||||
|
{t['com.affine.editCollection.rules.include.title']()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
className: styles.includeListGroup,
|
||||||
|
items: collection.allowList.map(docId => {
|
||||||
|
return {
|
||||||
|
id: docId,
|
||||||
|
height: 42,
|
||||||
|
Component: DocListItemComponent,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
] satisfies MasonryGroup[],
|
||||||
|
[collection.allowList, collection.rules.filters.length, rulesPageIds, t]
|
||||||
|
);
|
||||||
|
|
||||||
const [expandInclude, setExpandInclude] = useState(
|
const [expandInclude, setExpandInclude] = useState(
|
||||||
collection.allowList.length > 0
|
collection.allowList.length > 0
|
||||||
);
|
);
|
||||||
const operationsRenderer = useCallback(
|
|
||||||
(item: ListItem) => {
|
|
||||||
const page = item as DocMeta;
|
|
||||||
return allPageListConfig.favoriteRender(page);
|
|
||||||
},
|
|
||||||
[allPageListConfig]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tips = useMemo(
|
const tips = useMemo(
|
||||||
() => (
|
() => (
|
||||||
@@ -170,9 +207,6 @@ export const RulesMode = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{collection.allowList.map(id => {
|
{collection.allowList.map(id => {
|
||||||
const page = allPageListConfig.allPages.find(
|
|
||||||
v => v.id === id
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.includeItem} key={id}>
|
<div className={styles.includeItem} key={id}>
|
||||||
<div className={styles.includeItemContent}>
|
<div className={styles.includeItemContent}>
|
||||||
@@ -196,15 +230,7 @@ export const RulesMode = ({
|
|||||||
<div className={styles.includeItemContentIs}>
|
<div className={styles.includeItemContentIs}>
|
||||||
{t['com.affine.editCollection.rules.include.is']()}
|
{t['com.affine.editCollection.rules.include.is']()}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<DocTitle id={id} />
|
||||||
className={clsx(
|
|
||||||
styles.includeItemTitle,
|
|
||||||
page?.trash && styles.trashTitle,
|
|
||||||
styles.ellipsis
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{page?.title || t['Untitled']()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="14"
|
size="14"
|
||||||
@@ -226,41 +252,19 @@ export const RulesMode = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ListScrollContainer
|
<div className={styles.rulesContainerRight}>
|
||||||
className={styles.rulesContainerRight}
|
<DocExplorerContext.Provider value={docExplorerContextValue}>
|
||||||
style={{
|
<Masonry
|
||||||
display: showPreview ? 'flex' : 'none',
|
items={masonryItems}
|
||||||
}}
|
columns={1}
|
||||||
>
|
gapY={12}
|
||||||
{rulesPages.length > 0 ? (
|
virtualScroll
|
||||||
<List
|
paddingX={12}
|
||||||
hideHeader
|
groupHeaderGapWithItems={12}
|
||||||
className={styles.resultPages}
|
groupsGap={12}
|
||||||
items={rulesPages}
|
|
||||||
docCollection={allPageListConfig.docCollection}
|
|
||||||
operationsRenderer={operationsRenderer}
|
|
||||||
></List>
|
|
||||||
) : (
|
|
||||||
<RulesEmpty
|
|
||||||
noRules={collection.rules.filters.length === 0}
|
|
||||||
fullHeight={allowListPages.length === 0}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</DocExplorerContext.Provider>
|
||||||
{allowListPages.length > 0 ? (
|
</div>
|
||||||
<div>
|
|
||||||
<div className={styles.includeListTitle}>
|
|
||||||
{t['com.affine.editCollection.rules.include.title']()}
|
|
||||||
</div>
|
|
||||||
<List
|
|
||||||
hideHeader
|
|
||||||
className={styles.resultPages}
|
|
||||||
items={allowListPages}
|
|
||||||
docCollection={allPageListConfig.docCollection}
|
|
||||||
operationsRenderer={operationsRenderer}
|
|
||||||
></List>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</ListScrollContainer>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rulesBottom}>
|
<div className={styles.rulesBottom}>
|
||||||
<div className={styles.bottomLeft}>
|
<div className={styles.bottomLeft}>
|
||||||
@@ -278,8 +282,8 @@ export const RulesMode = ({
|
|||||||
<Trans
|
<Trans
|
||||||
i18nKey="com.affine.editCollection.rules.countTips"
|
i18nKey="com.affine.editCollection.rules.countTips"
|
||||||
values={{
|
values={{
|
||||||
selectedCount: allowListPages.length,
|
selectedCount: collection.allowList.length,
|
||||||
filteredCount: rulesPages.length,
|
filteredCount: rulesPageIds.length,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Selected
|
Selected
|
||||||
@@ -342,3 +346,23 @@ const RulesEmpty = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DocTitle = memo(function DocTitle({ id }: { id: string }) {
|
||||||
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
const doc = useLiveData(docsService.list.doc$(id));
|
||||||
|
const trash = useLiveData(doc?.trash$);
|
||||||
|
const title = useLiveData(docDisplayMetaService.title$(id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.includeItemTitle,
|
||||||
|
trash && styles.trashTitle,
|
||||||
|
styles.ellipsis
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
import { map, type Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import type { FilterProvider } from '../../provider';
|
||||||
|
import type { FilterParams } from '../../types';
|
||||||
|
|
||||||
|
export class TitleFilterProvider extends Service implements FilterProvider {
|
||||||
|
constructor(private readonly docsSearchService: DocsSearchService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
filter$(params: FilterParams): Observable<Set<string>> {
|
||||||
|
const method = params.method as 'match';
|
||||||
|
|
||||||
|
if (method === 'match') {
|
||||||
|
return this.docsSearchService
|
||||||
|
.searchTitle$(params.value ?? '')
|
||||||
|
.pipe(map(list => new Set(list)));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported method: ${params.method}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Framework } from '@toeverything/infra';
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
import { DocsService } from '../doc';
|
import { DocsService } from '../doc';
|
||||||
|
import { DocsSearchService } from '../docs-search';
|
||||||
import { FavoriteService } from '../favorite';
|
import { FavoriteService } from '../favorite';
|
||||||
import { ShareDocsListService } from '../share-doc';
|
import { ShareDocsListService } from '../share-doc';
|
||||||
import { TagService } from '../tag';
|
import { TagService } from '../tag';
|
||||||
@@ -19,6 +20,7 @@ import { SharedFilterProvider } from './impls/filters/shared';
|
|||||||
import { SystemFilterProvider } from './impls/filters/system';
|
import { SystemFilterProvider } from './impls/filters/system';
|
||||||
import { TagsFilterProvider } from './impls/filters/tags';
|
import { TagsFilterProvider } from './impls/filters/tags';
|
||||||
import { TextPropertyFilterProvider } from './impls/filters/text';
|
import { TextPropertyFilterProvider } from './impls/filters/text';
|
||||||
|
import { TitleFilterProvider } from './impls/filters/title';
|
||||||
import { TrashFilterProvider } from './impls/filters/trash';
|
import { TrashFilterProvider } from './impls/filters/trash';
|
||||||
import { UpdatedAtFilterProvider } from './impls/filters/updated-at';
|
import { UpdatedAtFilterProvider } from './impls/filters/updated-at';
|
||||||
import { UpdatedByFilterProvider } from './impls/filters/updated-by';
|
import { UpdatedByFilterProvider } from './impls/filters/updated-by';
|
||||||
@@ -130,6 +132,9 @@ export function configureCollectionRulesModule(framework: Framework) {
|
|||||||
ShareDocsListService,
|
ShareDocsListService,
|
||||||
DocsService,
|
DocsService,
|
||||||
])
|
])
|
||||||
|
.impl(FilterProvider('system:title'), TitleFilterProvider, [
|
||||||
|
DocsSearchService,
|
||||||
|
])
|
||||||
// --------------- Group By ---------------
|
// --------------- Group By ---------------
|
||||||
.impl(GroupByProvider('system'), SystemGroupByProvider)
|
.impl(GroupByProvider('system'), SystemGroupByProvider)
|
||||||
.impl(GroupByProvider('property'), PropertyGroupByProvider, [
|
.impl(GroupByProvider('property'), PropertyGroupByProvider, [
|
||||||
|
|||||||
@@ -26,6 +26,29 @@ export class DocsSearchService extends Service {
|
|||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
} as IndexerSyncState);
|
} as IndexerSyncState);
|
||||||
|
|
||||||
|
searchTitle$(query: string) {
|
||||||
|
return this.indexer
|
||||||
|
.search$(
|
||||||
|
'doc',
|
||||||
|
{
|
||||||
|
type: 'match',
|
||||||
|
field: 'title',
|
||||||
|
match: query,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pagination: {
|
||||||
|
skip: 0,
|
||||||
|
limit: Infinity,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
map(({ nodes }) => {
|
||||||
|
return nodes.map(node => node.id);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
search$(query: string): Observable<
|
search$(query: string): Observable<
|
||||||
{
|
{
|
||||||
docId: string;
|
docId: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user