feat(core): add doc/collection/tag select hook (#7593)

This commit is contained in:
Cats Juice
2024-07-26 15:44:56 +08:00
committed by GitHub
parent 2a2a19fec7
commit c63d007571
14 changed files with 596 additions and 259 deletions

View File

@@ -1,3 +1,4 @@
export * from './collection-list-header';
export * from './collection-list-item';
export * from './select-collection';
export * from './virtualized-collection-list';

View File

@@ -0,0 +1,107 @@
import { toast } from '@affine/component';
import { CollectionService } from '@affine/core/modules/collection';
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { useCallback, useMemo, useState } from 'react';
import { FavoriteTag } from '../components/favorite-tag';
import { collectionHeaderColsDef } from '../header-col-def';
import { CollectionListItemRenderer } from '../page-group';
import { ListTableHeader } from '../page-header';
import type { BaseSelectorDialogProps } from '../selector';
import { SelectorLayout } from '../selector/selector-layout';
import type { CollectionMeta, ListItem } from '../types';
import { VirtualizedList } from '../virtualized-list';
const FavoriteOperation = ({ collection }: { collection: ListItem }) => {
const t = useI18n();
const favAdapter = useService(FavoriteItemsAdapter);
const isFavorite = useLiveData(
favAdapter.isFavorite$(collection.id, 'collection')
);
const onToggleFavoriteCollection = useCallback(() => {
favAdapter.toggle(collection.id, 'collection');
toast(
isFavorite
? t['com.affine.toastMessage.removedFavorites']()
: t['com.affine.toastMessage.addedFavorites']()
);
}, [collection.id, favAdapter, isFavorite, t]);
return (
<FavoriteTag
style={{ marginRight: 8 }}
onClick={onToggleFavoriteCollection}
active={isFavorite}
/>
);
};
export const SelectCollection = ({
init = [],
onCancel,
onConfirm,
}: BaseSelectorDialogProps<string[]>) => {
const t = useI18n();
const collectionService = useService(CollectionService);
const workspace = useService(WorkspaceService).workspace;
const collections = useLiveData(collectionService.collections$);
const [selection, setSelection] = useState(init);
const [keyword, setKeyword] = useState('');
const collectionMetas = useMemo(() => {
const collectionsList: CollectionMeta[] = collections
.map(collection => {
return {
...collection,
title: collection.name,
};
})
.filter(meta => {
const reg = new RegExp(keyword, 'i');
return reg.test(meta.title);
});
return collectionsList;
}, [collections, keyword]);
const collectionItemRenderer = useCallback((item: ListItem) => {
return <CollectionListItemRenderer {...item} />;
}, []);
const collectionHeaderRenderer = useCallback(() => {
return <ListTableHeader headerCols={collectionHeaderColsDef} />;
}, []);
const collectionOperationRenderer = useCallback((item: ListItem) => {
return <FavoriteOperation collection={item} />;
}, []);
return (
<SelectorLayout
searchPlaceholder={t[
'com.affine.selector-collection.search.placeholder'
]()}
selectedCount={selection.length}
onSearch={setKeyword}
onClear={() => setSelection([])}
onCancel={() => onCancel?.()}
onConfirm={() => onConfirm?.(selection)}
>
<VirtualizedList
selectable={true}
draggable={false}
selectedIds={selection}
onSelectedIdsChange={setSelection}
items={collectionMetas}
itemRenderer={collectionItemRenderer}
rowAsLink
docCollection={workspace.docCollection}
operationsRenderer={collectionOperationRenderer}
headerRenderer={collectionHeaderRenderer}
/>
</SelectorLayout>
);
};

View File

@@ -0,0 +1,31 @@
import { SelectCollection } from '../collections';
import { SelectTag } from '../tags';
import { SelectPage } from '../view/edit-collection/select-page';
import { useSelectDialog } from './use-select-dialog';
export interface BaseSelectorDialogProps<T> {
init?: T;
onConfirm?: (data: T) => void;
onCancel?: () => void;
}
/**
* Return a `open` function to open the select collection dialog.
*/
export const useSelectCollection = () => {
return useSelectDialog(SelectCollection, 'select-collection');
};
/**
* Return a `open` function to open the select page dialog.
*/
export const useSelectDoc = () => {
return useSelectDialog(SelectPage, 'select-doc-dialog');
};
/**
* Return a `open` function to open the select tag dialog.
*/
export const useSelectTag = () => {
return useSelectDialog(SelectTag, 'select-tag-dialog');
};

View File

@@ -0,0 +1,72 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'flex',
flexDirection: 'column',
height: '100%',
});
export const header = style({
borderBottom: `1px solid ${cssVarV2('layer/border')}`,
minHeight: 64,
});
export const search = style({
width: '100%',
height: '100%',
outline: 'none',
padding: '20px 20px 20px 24px',
fontSize: 20,
lineHeight: '24px',
fontWeight: 400,
letterSpacing: -0.2,
});
export const content = style({
height: 0,
flex: 1,
});
export const footer = style({
borderTop: `1px solid ${cssVarV2('layer/border')}`,
minHeight: 64,
padding: '20px 24px',
borderBottomLeftRadius: 'inherit',
borderBottomRightRadius: 'inherit',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
export const footerInfo = style({
display: 'flex',
alignItems: 'center',
gap: 18,
fontSize: cssVar('fontXs'),
lineHeight: '20px',
fontWeight: 500,
});
export const selectedCount = style({
display: 'flex',
alignItems: 'center',
gap: 7,
});
export const selectedNum = style({
color: cssVar('primaryColor'),
});
export const clearButton = style({
padding: '4px 18px',
});
export const footerAction = style({
display: 'flex',
gap: 20,
});
export const actionButton = style({
padding: '4px 18px',
});

View File

@@ -0,0 +1,88 @@
import { Button } from '@affine/component';
import { useI18n } from '@affine/i18n';
import { type PropsWithChildren, type ReactNode, useCallback } from 'react';
import * as styles from './selector-layout.css';
export interface SelectorContentProps extends PropsWithChildren {
searchPlaceholder?: string;
selectedCount?: number;
onSearch?: (value: string) => void;
onClear?: () => void;
onCancel?: () => void;
onConfirm?: () => void;
actions?: ReactNode;
}
/**
* Provides a unified layout for doc/collection/tag selector
* - Header (Search input)
* - Content
* - Footer (Selected count + Actions)
*/
export const SelectorLayout = ({
children,
searchPlaceholder,
selectedCount,
onSearch,
onClear,
onCancel,
onConfirm,
actions,
}: SelectorContentProps) => {
const t = useI18n();
const onSearchChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onSearch?.(e.target.value);
},
[onSearch]
);
return (
<div className={styles.root}>
<header className={styles.header}>
<input
className={styles.search}
placeholder={searchPlaceholder}
onChange={onSearchChange}
/>
</header>
<main className={styles.content}>{children}</main>
<footer className={styles.footer}>
<div className={styles.footerInfo}>
<div className={styles.selectedCount}>
<span>{t['com.affine.selectPage.selected']()}</span>
<span className={styles.selectedNum}>{selectedCount ?? 0}</span>
</div>
<Button type="plain" className={styles.clearButton} onClick={onClear}>
{t['com.affine.editCollection.pages.clear']()}
</Button>
</div>
<div className={styles.footerAction}>
{actions ?? (
<>
<Button onClick={onCancel} className={styles.actionButton}>
{t['Cancel']()}
</Button>
<Button
onClick={onConfirm}
className={styles.actionButton}
type="primary"
>
{t['Confirm']()}
</Button>
</>
)}
</div>
</footer>
</div>
);
};

View File

@@ -0,0 +1,76 @@
import { Modal } from '@affine/component';
import { useMount } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useCallback, useEffect, useState } from 'react';
import type { BaseSelectorDialogProps } from '.';
export const useSelectDialog = function useSelectDialog<T>(
Component: React.FC<BaseSelectorDialogProps<T>>,
debugKey?: string
) {
// to control whether the dialog is open, it's not equal to !!value
// when closing the dialog, show will be `false` first, then after the animation, value turns to `undefined`
const [show, setShow] = useState(false);
const [value, setValue] = useState<{
init?: T;
onConfirm: (v: T) => void;
}>();
const onOpenChanged = useCallback((open: boolean) => {
if (!open) setValue(undefined);
setShow(open);
}, []);
const close = useCallback(() => setShow(false), []);
/**
* Open a dialog to select items
*/
const open = useCallback(
(ids?: T) => {
return new Promise<T>(resolve => {
setShow(true);
setValue({
init: ids,
onConfirm: list => {
close();
resolve(list);
},
});
});
},
[close]
);
const { mount } = useMount(debugKey);
useEffect(() => {
return mount(
<Modal
open={show}
onOpenChange={onOpenChanged}
withoutCloseButton
width="calc(100% - 32px)"
height="80%"
contentOptions={{
style: {
padding: 0,
maxWidth: 976,
background: cssVar('backgroundPrimaryColor'),
},
}}
>
{value ? (
<Component
init={value.init}
onCancel={close}
onConfirm={value.onConfirm}
/>
) : null}
</Modal>
);
}, [Component, close, mount, onOpenChanged, show, value]);
return open;
};

View File

@@ -1,3 +1,4 @@
export * from './select-tag';
export * from './tag-list-header';
export * from './tag-list-item';
export * from './virtualized-tag-list';

View File

@@ -0,0 +1,97 @@
import { TagService } from '@affine/core/modules/tag';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { useCallback, useMemo, useState } from 'react';
import { tagHeaderColsDef } from '../header-col-def';
import { TagListItemRenderer } from '../page-group';
import { ListTableHeader } from '../page-header';
import type { BaseSelectorDialogProps } from '../selector';
import { SelectorLayout } from '../selector/selector-layout';
import type { ListItem, TagMeta } from '../types';
import { VirtualizedList } from '../virtualized-list';
// TODO(@EYHN): add tag to favourite support
const FavoriteOperation = ({ tag: _ }: { tag: ListItem }) => {
// const t = useI18n();
// const favAdapter = useService(FavoriteItemsAdapter);
// const isFavorite = useLiveData(
// favAdapter.isFavorite$(tag.id, 'tag')
// );
// const onToggleFavoriteCollection = useCallback(() => {
// favAdapter.toggle(tag.id, 'tag');
// toast(
// isFavorite
// ? t['com.affine.toastMessage.removedFavorites']()
// : t['com.affine.toastMessage.addedFavorites']()
// );
// }, [tag.id, favAdapter, isFavorite, t]);
// return (
// <FavoriteTag
// style={{ marginRight: 8 }}
// onClick={onToggleFavoriteCollection}
// active={isFavorite}
// />
// );
return null;
};
export const SelectTag = ({
init = [],
onConfirm,
onCancel,
}: BaseSelectorDialogProps<string[]>) => {
const t = useI18n();
const workspace = useService(WorkspaceService).workspace;
const tagList = useService(TagService).tagList;
const [selection, setSelection] = useState(init);
const [keyword, setKeyword] = useState('');
const tagMetas: TagMeta[] = useLiveData(tagList.tagMetas$);
const filteredTagMetas = useMemo(() => {
return tagMetas.filter(tag => {
const reg = new RegExp(keyword, 'i');
return reg.test(tag.title);
});
}, [keyword, tagMetas]);
const tagItemRenderer = useCallback((item: ListItem) => {
return <TagListItemRenderer {...item} />;
}, []);
const tagOperationRenderer = useCallback((item: ListItem) => {
return <FavoriteOperation tag={item} />;
}, []);
const tagHeaderRenderer = useCallback(() => {
return <ListTableHeader headerCols={tagHeaderColsDef} />;
}, []);
return (
<SelectorLayout
searchPlaceholder={t['com.affine.selector-tag.search.placeholder']()}
selectedCount={selection.length}
onSearch={setKeyword}
onConfirm={() => onConfirm?.(selection)}
onCancel={onCancel}
onClear={() => setSelection([])}
>
<VirtualizedList
selectable={true}
draggable={false}
selectedIds={selection}
onSelectedIdsChange={setSelection}
items={filteredTagMetas}
docCollection={workspace.docCollection}
itemRenderer={tagItemRenderer}
operationsRenderer={tagOperationRenderer}
headerRenderer={tagHeaderRenderer}
/>
</SelectorLayout>
);
};

View File

@@ -5,19 +5,6 @@ export const ellipsis = style({
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const pagesBottomLeft = style({
display: 'flex',
gap: 8,
alignItems: 'center',
});
export const pagesBottom = style({
display: 'flex',
justifyContent: 'space-between',
padding: '20px 24px',
borderTop: `1px solid ${cssVar('borderColor')}`,
flexWrap: 'wrap',
gap: '12px',
});
export const pagesTabContent = style({
display: 'flex',
justifyContent: 'space-between',
@@ -26,10 +13,10 @@ export const pagesTabContent = style({
padding: '16px 16px 8px 16px',
});
export const pagesTab = style({
flex: 1,
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
overflow: 'hidden',
});
export const pagesList = style({
@@ -65,15 +52,6 @@ export const rulesContainerRight = style({
overflowX: 'hidden',
overflowY: 'auto',
});
export const includeAddButton = style({
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '4px 8px',
fontSize: 14,
lineHeight: '22px',
width: 'max-content',
});
export const includeItemTitle = style({
overflow: 'hidden',
fontWeight: 600,
@@ -163,11 +141,6 @@ export const previewCountTips = style({
lineHeight: '20px',
color: cssVar('textSecondaryColor'),
});
export const selectedCountTips = style({
fontSize: 12,
lineHeight: '20px',
color: cssVar('textPrimaryColor'),
});
export const rulesTitleHighlight = style({
color: cssVar('primaryColor'),
fontStyle: 'italic',

View File

@@ -121,7 +121,6 @@ export const EditCollection = ({
{t['com.affine.editCollection.button.cancel']()}
</Button>
<Button
className={styles.confirmButton}
size="large"
data-testid="save-collection"
type="primary"
@@ -169,7 +168,6 @@ export const EditCollection = ({
>
{mode === 'page' ? (
<SelectPage
allPageListConfig={config}
init={value.allowList}
onChange={onIdsChange}
header={switchMode}

View File

@@ -1,72 +0,0 @@
import { Modal } from '@affine/component';
import { useMount } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react';
import type { AllPageListConfig } from './edit-collection';
import { SelectPage } from './select-page';
export const useSelectPage = ({
allPageListConfig,
}: {
allPageListConfig: AllPageListConfig;
}) => {
const [value, onChange] = useState<{
init: string[];
onConfirm: (ids: string[]) => void;
}>();
const close = useCallback((open: boolean) => {
if (!open) {
onChange(undefined);
}
}, []);
const handleCancel = useCallback(() => {
close(false);
}, [close]);
const { mount } = useMount('select-page-modal');
useEffect(() => {
return mount(
<Modal
open={!!value}
onOpenChange={close}
withoutCloseButton
width="calc(100% - 32px)"
height="80%"
overlayOptions={{ style: { backgroundColor: 'transparent' } }}
contentOptions={{
style: {
padding: 0,
transform: 'translateY(16px)',
maxWidth: 976,
backgroundColor: 'var(--affine-white)',
},
}}
>
{value ? (
<SelectPage
allPageListConfig={allPageListConfig}
init={value.init}
onConfirm={value.onConfirm}
onCancel={handleCancel}
/>
) : null}
</Modal>
);
}, [allPageListConfig, close, handleCancel, mount, value]);
return {
open: useCallback(
(init: string[]): Promise<string[]> =>
new Promise<string[]>(res => {
onChange({
init,
onConfirm: list => {
close(false);
res(list);
},
});
}),
[close]
),
};
};

View File

@@ -6,14 +6,14 @@ import {
CloseIcon,
EdgelessIcon,
PageIcon,
PlusIcon,
ToggleCollapseIcon,
} from '@blocksuite/icons/rc';
import type { DocMeta } from '@blocksuite/store';
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 { useCallback, useMemo, useState } from 'react';
import { FilterList } from '../../filter';
import { List, ListScrollContainer } from '../../list';
@@ -22,7 +22,6 @@ import { filterPageByRules } from '../../use-collection-manager';
import { AffineShapeIcon } from '../affine-shape';
import type { AllPageListConfig } from './edit-collection';
import * as styles from './edit-collection.css';
import { useSelectPage } from './hooks';
export const RulesMode = ({
collection,
@@ -43,16 +42,8 @@ export const RulesMode = ({
const [showPreview, setShowPreview] = useState(true);
const allowListPages: DocMeta[] = [];
const rulesPages: DocMeta[] = [];
const [showTips, setShowTips] = useState(false);
const favAdapter = useService(FavoriteItemsAdapter);
const favorites = useLiveData(favAdapter.favorites$);
useEffect(() => {
setShowTips(!localStorage.getItem('hide-rules-mode-include-page-tips'));
}, []);
const hideTips = useCallback(() => {
setShowTips(false);
localStorage.setItem('hide-rules-mode-include-page-tips', 'true');
}, []);
allPageListConfig.allPages.forEach(meta => {
if (meta.trash) {
return;
@@ -72,20 +63,6 @@ export const RulesMode = ({
allowListPages.push(meta);
}
});
const { open } = useSelectPage({ allPageListConfig });
const openSelectPage = useCallback(() => {
open(collection.allowList).then(
ids => {
updateCollection({
...collection,
allowList: ids,
});
},
() => {
//do nothing
}
);
}, [open, updateCollection, collection]);
const [expandInclude, setExpandInclude] = useState(
collection.allowList.length > 0
);
@@ -146,20 +123,22 @@ export const RulesMode = ({
)}
/>
<div className={styles.rulesContainerLeftContentInclude}>
<div className={styles.includeTitle}>
<ToggleCollapseIcon
onClick={() => setExpandInclude(!expandInclude)}
className={styles.button}
width={24}
height={24}
style={{
transform: expandInclude ? 'rotate(90deg)' : undefined,
}}
></ToggleCollapseIcon>
<div style={{ color: 'var(--affine-text-secondary-color)' }}>
{t['com.affine.editCollection.rules.include.title']()}
{collection.allowList.length > 0 ? (
<div className={styles.includeTitle}>
<ToggleCollapseIcon
onClick={() => setExpandInclude(!expandInclude)}
className={styles.button}
width={24}
height={24}
style={{
transform: expandInclude ? 'rotate(90deg)' : undefined,
}}
/>
<div style={{ color: cssVar('textSecondaryColor') }}>
{t['com.affine.editCollection.rules.include.title']()}
</div>
</div>
</div>
) : null}
<div
style={{
display: expandInclude ? 'flex' : 'none',
@@ -216,56 +195,9 @@ export const RulesMode = ({
</div>
);
})}
<div
onClick={openSelectPage}
className={clsx(styles.button, styles.includeAddButton)}
>
<PlusIcon></PlusIcon>
<div
style={{ color: 'var(--affine-text-secondary-color)' }}
>
{t['com.affine.editCollection.rules.include.add']()}
</div>
</div>
</div>
</div>
</div>
{showTips ? (
<div
style={{
marginTop: 16,
borderRadius: 8,
backgroundColor:
'var(--affine-background-overlay-panel-color)',
padding: 10,
fontSize: 12,
lineHeight: '20px',
}}
>
<div
style={{
marginBottom: 14,
fontWeight: 600,
color: 'var(--affine-text-secondary-color)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>{t['com.affine.collection.helpInfo']()}</div>
<CloseIcon
color="var(--affine-icon-color)"
onClick={hideTips}
className={styles.button}
style={{ width: 16, height: 16 }}
/>
</div>
<div style={{ marginBottom: 10, fontWeight: 600 }}>
{t['com.affine.editCollection.rules.include.tipsTitle']()}
</div>
<div>{t['com.affine.editCollection.rules.include.tips']()}</div>
</div>
) : null}
</div>
</div>
<ListScrollContainer

View File

@@ -1,44 +1,48 @@
import { Button, Menu } from '@affine/component';
import { Menu, toast } from '@affine/component';
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
import { ShareDocsService } from '@affine/core/modules/share-doc';
import { PublicPageMode } from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n';
import { FilterIcon } from '@blocksuite/icons/rc';
import type { DocMeta } from '@blocksuite/store';
import { useLiveData, useService } from '@toeverything/infra';
import {
DocsService,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import clsx from 'clsx';
import { type ReactNode, useCallback, useState } from 'react';
import { type ReactNode, useCallback, useEffect, useState } from 'react';
import { FavoriteTag } from '../../components/favorite-tag';
import { FilterList } from '../../filter';
import { VariableSelect } from '../../filter/vars';
import { usePageHeaderColsDef } from '../../header-col-def';
import { PageListItemRenderer } from '../../page-group';
import { ListTableHeader } from '../../page-header';
import type { BaseSelectorDialogProps } from '../../selector';
import { SelectorLayout } from '../../selector/selector-layout';
import type { ListItem } from '../../types';
import { VirtualizedList } from '../../virtualized-list';
import { AffineShapeIcon } from '../affine-shape';
import type { AllPageListConfig } from './edit-collection';
import * as styles from './edit-collection.css';
import { useFilter } from './use-filter';
import { useSearch } from './use-search';
export const SelectPage = ({
allPageListConfig,
init,
init = [],
onConfirm,
onCancel,
onChange: propsOnChange,
confirmText,
header,
buttons,
}: {
allPageListConfig: AllPageListConfig;
init: string[];
onConfirm?: (pageIds: string[]) => void;
onCancel?: () => void;
onChange?: (values: string[]) => void;
confirmText?: ReactNode;
header?: ReactNode;
buttons?: ReactNode;
}) => {
} & BaseSelectorDialogProps<string[]>) => {
const t = useI18n();
const [value, setValue] = useState(init);
const onChange = useCallback(
@@ -54,8 +58,66 @@ export const SelectPage = ({
const clearSelected = useCallback(() => {
onChange([]);
}, [onChange]);
const favAdapter = useService(FavoriteItemsAdapter);
const favourites = useLiveData(favAdapter.favorites$);
const {
workspaceService,
favoriteItemsAdapter,
shareDocsService,
docsService,
} = useServices({
DocsService,
ShareDocsService,
WorkspaceService,
FavoriteItemsAdapter,
});
const shareDocs = useLiveData(shareDocsService.shareDocs?.list$);
const workspace = workspaceService.workspace;
const docCollection = workspace.docCollection;
const pageMetas = useBlockSuiteDocMeta(docCollection);
const favourites = useLiveData(favoriteItemsAdapter.favorites$);
useEffect(() => {
shareDocsService.shareDocs?.revalidate();
}, [shareDocsService.shareDocs]);
const getPublicMode = useCallback(
(id: string) => {
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
if (mode === PublicPageMode.Edgeless) {
return 'edgeless';
} else if (mode === PublicPageMode.Page) {
return 'page';
} else {
return undefined;
}
},
[shareDocs]
);
const isFavorite = useCallback(
(meta: DocMeta) => favourites.some(fav => fav.id === meta.id),
[favourites]
);
const isEdgeless = useCallback(
(id: string) => {
return docsService.list.doc$(id).value?.mode$.value === 'edgeless';
},
[docsService.list]
);
const onToggleFavoritePage = useCallback(
(page: DocMeta) => {
const status = isFavorite(page);
favoriteItemsAdapter.toggle(page.id, 'doc');
toast(
status
? t['com.affine.toastMessage.removedFavorites']()
: t['com.affine.toastMessage.addedFavorites']()
);
},
[favoriteItemsAdapter, isFavorite, t]
);
const pageHeaderColsDef = usePageHeaderColsDef();
const {
clickFilter,
@@ -65,10 +127,10 @@ export const SelectPage = ({
updateFilters,
filteredList,
} = useFilter(
allPageListConfig.allPages.map(meta => ({
pageMetas.map(meta => ({
meta,
publicMode: allPageListConfig.getPublicMode(meta.id),
favorite: favourites.some(fav => fav.id === meta.id),
publicMode: getPublicMode(meta.id),
favorite: isFavorite(meta),
}))
);
const { searchText, updateSearchText, searchedList } =
@@ -77,9 +139,15 @@ export const SelectPage = ({
const operationsRenderer = useCallback(
(item: ListItem) => {
const page = item as DocMeta;
return allPageListConfig.favoriteRender(page);
return (
<FavoriteTag
style={{ marginRight: 8 }}
onClick={() => onToggleFavoritePage(page)}
active={isFavorite(page)}
/>
);
},
[allPageListConfig]
[isFavorite, onToggleFavoritePage]
);
const pageHeaderRenderer = useCallback(() => {
@@ -91,13 +159,15 @@ export const SelectPage = ({
}, []);
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<input
className={styles.rulesTitle}
value={searchText}
onChange={e => updateSearchText(e.target.value)}
placeholder={t['com.affine.editCollection.search.placeholder']()}
></input>
<SelectorLayout
searchPlaceholder={t['com.affine.editCollection.search.placeholder']()}
selectedCount={value.length}
onSearch={updateSearchText}
onClear={clearSelected}
onCancel={onCancel}
onConfirm={confirm}
actions={buttons}
>
<div className={styles.pagesTab}>
<div className={styles.pagesTabContent}>
{header ?? (
@@ -109,9 +179,7 @@ export const SelectPage = ({
<Menu
items={
<VariableSelect
propertiesMeta={
allPageListConfig.docCollection.meta.properties
}
propertiesMeta={docCollection.meta.properties}
selected={filters}
onSelect={createFilter}
/>
@@ -138,7 +206,7 @@ export const SelectPage = ({
{showFilter ? (
<div style={{ padding: '12px 16px 16px' }}>
<FilterList
propertiesMeta={allPageListConfig.docCollection.meta.properties}
propertiesMeta={docCollection.meta.properties}
value={filters}
onChange={updateFilters}
/>
@@ -148,11 +216,11 @@ export const SelectPage = ({
<VirtualizedList
className={styles.pageList}
items={searchedList}
docCollection={allPageListConfig.docCollection}
docCollection={docCollection}
selectable
onSelectedIdsChange={onChange}
selectedIds={value}
isPreferredEdgeless={allPageListConfig.isEdgeless}
isPreferredEdgeless={isEdgeless}
operationsRenderer={operationsRenderer}
itemRenderer={pageItemRenderer}
headerRenderer={pageHeaderRenderer}
@@ -161,45 +229,7 @@ export const SelectPage = ({
<EmptyList search={searchText} />
)}
</div>
<div className={styles.pagesBottom}>
<div className={styles.pagesBottomLeft}>
<div className={styles.selectedCountTips}>
{t['com.affine.selectPage.selected']()}
<span
style={{ marginLeft: 7 }}
className={styles.previewCountTipsHighlight}
>
{value.length}
</span>
</div>
<div
className={clsx(styles.button, styles.bottomButton)}
style={{ fontSize: 12, lineHeight: '20px' }}
onClick={clearSelected}
>
{t['com.affine.editCollection.pages.clear']()}
</div>
</div>
<div>
{buttons ?? (
<>
<Button size="large" onClick={onCancel}>
{t['com.affine.editCollection.button.cancel']()}
</Button>
<Button
className={styles.confirmButton}
size="large"
data-testid="save-collection"
type="primary"
onClick={confirm}
>
{confirmText ?? t['Confirm']()}
</Button>
</>
)}
</div>
</div>
</div>
</SelectorLayout>
);
};
export const EmptyList = ({ search }: { search?: string }) => {