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:
@@ -1,4 +1,5 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const ellipsis = style({
|
||||
overflow: 'hidden',
|
||||
@@ -14,22 +15,25 @@ export const rulesBottom = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '20px 24px',
|
||||
borderTop: `1px solid ${cssVar('borderColor')}`,
|
||||
borderTop: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
flexWrap: 'wrap',
|
||||
gap: '12px',
|
||||
});
|
||||
export const includeListGroup = style({
|
||||
borderTop: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
});
|
||||
export const includeListTitle = style({
|
||||
fontSize: 14,
|
||||
fontWeight: 400,
|
||||
lineHeight: '22px',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
padding: '4px 16px',
|
||||
borderTop: `1px solid ${cssVar('borderColor')}`,
|
||||
padding: '8px',
|
||||
paddingBottom: 0,
|
||||
});
|
||||
export const rulesContainerRight = style({
|
||||
flex: 2,
|
||||
flexDirection: 'column',
|
||||
borderLeft: `1px solid ${cssVar('borderColor')}`,
|
||||
borderLeft: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
@@ -60,7 +64,7 @@ export const includeItem = style({
|
||||
overflow: 'hidden',
|
||||
gap: 16,
|
||||
whiteSpace: 'nowrap',
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
borderRadius: 8,
|
||||
padding: '4px 8px 4px',
|
||||
});
|
||||
@@ -143,5 +147,5 @@ export const rulesTitle = style({
|
||||
fontSize: 20,
|
||||
lineHeight: '24px',
|
||||
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 { useAllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
||||
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
|
||||
import type { CollectionInfo } from '@affine/core/modules/collection';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -26,7 +25,6 @@ export const EditCollection = ({
|
||||
mode: initMode,
|
||||
}: EditCollectionProps) => {
|
||||
const t = useI18n();
|
||||
const config = useAllPageListConfig();
|
||||
const [value, onChange] = useState<CollectionInfo>(init);
|
||||
const [mode, setMode] = useState<'page' | 'rule'>(
|
||||
initMode ?? (init.rules.filters.length === 0 ? 'page' : 'rule')
|
||||
@@ -44,12 +42,9 @@ export const EditCollection = ({
|
||||
allowList: init.allowList,
|
||||
});
|
||||
}, [init, value]);
|
||||
const onIdsChange = useCallback(
|
||||
(ids: string[]) => {
|
||||
onChange({ ...value, allowList: ids });
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const onIdsChange = useCallback((ids: string[]) => {
|
||||
onChange(prev => ({ ...prev, allowList: ids }));
|
||||
}, []);
|
||||
const buttons = useMemo(
|
||||
() => (
|
||||
<>
|
||||
@@ -104,14 +99,13 @@ export const EditCollection = ({
|
||||
>
|
||||
{mode === 'page' ? (
|
||||
<SelectPage
|
||||
init={value.allowList}
|
||||
init={init.allowList}
|
||||
onChange={onIdsChange}
|
||||
header={switchMode}
|
||||
buttons={buttons}
|
||||
/>
|
||||
) : (
|
||||
<RulesMode
|
||||
allPageListConfig={config}
|
||||
collection={value}
|
||||
switchMode={switchMode}
|
||||
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 {
|
||||
AffineShapeIcon,
|
||||
List,
|
||||
type ListItem,
|
||||
ListScrollContainer,
|
||||
} from '@affine/core/components/page-list';
|
||||
Button,
|
||||
IconButton,
|
||||
Masonry,
|
||||
type MasonryGroup,
|
||||
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 { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import {
|
||||
CloseIcon,
|
||||
EdgelessIcon,
|
||||
PageIcon,
|
||||
ToggleRightIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import clsx from 'clsx';
|
||||
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';
|
||||
|
||||
@@ -32,20 +37,26 @@ export const RulesMode = ({
|
||||
reset,
|
||||
buttons,
|
||||
switchMode,
|
||||
allPageListConfig,
|
||||
}: {
|
||||
collection: CollectionInfo;
|
||||
updateCollection: (collection: CollectionInfo) => void;
|
||||
reset: () => void;
|
||||
buttons: ReactNode;
|
||||
switchMode: ReactNode;
|
||||
allPageListConfig: AllPageListConfig;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [showPreview, setShowPreview] = useState(true);
|
||||
const docsService = useService(DocsService);
|
||||
const collectionRulesService = useService(CollectionRulesService);
|
||||
const [rulesPageIds, setRulesPageIds] = useState<string[]>([]);
|
||||
const [docExplorerContextValue] = useState(() =>
|
||||
createDocExplorerContext({
|
||||
displayProperties: ['createdAt', 'updatedAt', 'tags'],
|
||||
showDragHandle: false,
|
||||
showMoreOperation: false,
|
||||
quickFavorite: true,
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
@@ -74,32 +85,58 @@ export const RulesMode = ({
|
||||
};
|
||||
}, [collection, collectionRulesService]);
|
||||
|
||||
const rulesPages = useMemo(() => {
|
||||
return allPageListConfig.allPages.filter(meta => {
|
||||
return rulesPageIds.includes(meta.id);
|
||||
});
|
||||
}, [allPageListConfig.allPages, rulesPageIds]);
|
||||
|
||||
const allowListPages = useMemo(() => {
|
||||
return allPageListConfig.allPages.filter(meta => {
|
||||
return (
|
||||
collection.allowList.includes(meta.id) &&
|
||||
!rulesPageIds.includes(meta.id) &&
|
||||
!meta.trash
|
||||
);
|
||||
});
|
||||
}, [allPageListConfig.allPages, collection.allowList, rulesPageIds]);
|
||||
const masonryItems = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
id: 'rules-group',
|
||||
height: 0,
|
||||
children: null,
|
||||
items: rulesPageIds.length
|
||||
? rulesPageIds.map(docId => {
|
||||
return {
|
||||
id: docId,
|
||||
height: 42,
|
||||
Component: DocListItemComponent,
|
||||
};
|
||||
})
|
||||
: [
|
||||
{
|
||||
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(
|
||||
collection.allowList.length > 0
|
||||
);
|
||||
const operationsRenderer = useCallback(
|
||||
(item: ListItem) => {
|
||||
const page = item as DocMeta;
|
||||
return allPageListConfig.favoriteRender(page);
|
||||
},
|
||||
[allPageListConfig]
|
||||
);
|
||||
|
||||
const tips = useMemo(
|
||||
() => (
|
||||
@@ -170,9 +207,6 @@ export const RulesMode = ({
|
||||
}}
|
||||
>
|
||||
{collection.allowList.map(id => {
|
||||
const page = allPageListConfig.allPages.find(
|
||||
v => v.id === id
|
||||
);
|
||||
return (
|
||||
<div className={styles.includeItem} key={id}>
|
||||
<div className={styles.includeItemContent}>
|
||||
@@ -196,15 +230,7 @@ export const RulesMode = ({
|
||||
<div className={styles.includeItemContentIs}>
|
||||
{t['com.affine.editCollection.rules.include.is']()}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.includeItemTitle,
|
||||
page?.trash && styles.trashTitle,
|
||||
styles.ellipsis
|
||||
)}
|
||||
>
|
||||
{page?.title || t['Untitled']()}
|
||||
</div>
|
||||
<DocTitle id={id} />
|
||||
</div>
|
||||
<IconButton
|
||||
size="14"
|
||||
@@ -226,41 +252,19 @@ export const RulesMode = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ListScrollContainer
|
||||
className={styles.rulesContainerRight}
|
||||
style={{
|
||||
display: showPreview ? 'flex' : 'none',
|
||||
}}
|
||||
>
|
||||
{rulesPages.length > 0 ? (
|
||||
<List
|
||||
hideHeader
|
||||
className={styles.resultPages}
|
||||
items={rulesPages}
|
||||
docCollection={allPageListConfig.docCollection}
|
||||
operationsRenderer={operationsRenderer}
|
||||
></List>
|
||||
) : (
|
||||
<RulesEmpty
|
||||
noRules={collection.rules.filters.length === 0}
|
||||
fullHeight={allowListPages.length === 0}
|
||||
<div className={styles.rulesContainerRight}>
|
||||
<DocExplorerContext.Provider value={docExplorerContextValue}>
|
||||
<Masonry
|
||||
items={masonryItems}
|
||||
columns={1}
|
||||
gapY={12}
|
||||
virtualScroll
|
||||
paddingX={12}
|
||||
groupHeaderGapWithItems={12}
|
||||
groupsGap={12}
|
||||
/>
|
||||
)}
|
||||
{allowListPages.length > 0 ? (
|
||||
<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>
|
||||
</DocExplorerContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rulesBottom}>
|
||||
<div className={styles.bottomLeft}>
|
||||
@@ -278,8 +282,8 @@ export const RulesMode = ({
|
||||
<Trans
|
||||
i18nKey="com.affine.editCollection.rules.countTips"
|
||||
values={{
|
||||
selectedCount: allowListPages.length,
|
||||
filteredCount: rulesPages.length,
|
||||
selectedCount: collection.allowList.length,
|
||||
filteredCount: rulesPageIds.length,
|
||||
}}
|
||||
>
|
||||
Selected
|
||||
@@ -342,3 +346,23 @@ const RulesEmpty = ({
|
||||
</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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user