feat(core): remove mode and pages field from Collection (#4817)

This commit is contained in:
3720
2023-11-02 18:47:43 +08:00
committed by GitHub
parent bf17b4789b
commit 7068d5f38a
20 changed files with 1085 additions and 983 deletions

View File

@@ -19,16 +19,13 @@ export const createEmptyCollection = (
return {
id,
name: '',
mode: 'page',
filterList: [],
pages: [],
allowList: [],
...data,
};
};
const defaultCollection: Collection = createEmptyCollection(NIL, {
name: 'All',
mode: 'rule',
});
const defaultCollectionAtom = atomWithReset<Collection>(defaultCollection);
export const currentCollectionAtom = atomWithReset<string>(NIL);
@@ -52,12 +49,6 @@ export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => {
const addPage = useCallback(
async (collectionId: string, pageId: string) => {
await updateCollection(collectionId, old => {
if (old.mode === 'page') {
return {
...old,
pages: [pageId, ...(old.pages ?? [])],
};
}
return {
...old,
allowList: [pageId, ...(old.allowList ?? [])],
@@ -128,8 +119,8 @@ export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
evalFilterList(filterList, varMap);
export const filterPage = (collection: Collection, page: PageMeta) => {
if (collection.mode === 'page') {
return collection.pages.includes(page.id);
if (collection.filterList.length === 0) {
return collection.allowList.includes(page.id);
}
return filterPageByRules(collection.filterList, collection.allowList, page);
};

View File

@@ -12,7 +12,10 @@ import {
useCollectionManager,
} from '../use-collection-manager';
import * as styles from './collection-bar.css';
import { type AllPageListConfig, EditCollectionModal } from './edit-collection';
import {
type AllPageListConfig,
EditCollectionModal,
} from './edit-collection/edit-collection';
import { useActions } from './use-action';
interface CollectionBarProps {

View File

@@ -15,7 +15,10 @@ import { CreateFilterMenu } from '../filter/vars';
import type { useCollectionManager } from '../use-collection-manager';
import * as styles from './collection-list.css';
import { CollectionOperations } from './collection-operations';
import { type AllPageListConfig, EditCollectionModal } from './edit-collection';
import {
type AllPageListConfig,
EditCollectionModal,
} from './edit-collection/edit-collection';
export const CollectionList = ({
setting,

View File

@@ -1,934 +0,0 @@
import {
AffineShapeIcon,
PageList,
PageListScrollContainer,
} from '@affine/component/page-list';
import type { Collection, Filter } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
CloseIcon,
EdgelessIcon,
FilterIcon,
PageIcon,
PlusIcon,
ToggleCollapseIcon,
} from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store';
import { Button } from '@toeverything/components/button';
import { Menu } from '@toeverything/components/menu';
import { Modal } from '@toeverything/components/modal';
import clsx from 'clsx';
import { type MouseEvent, useEffect } from 'react';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { RadioButton, RadioButtonGroup } from '../../..';
import { FilterList } from '../filter';
import { VariableSelect } from '../filter/vars';
import { filterPageByRules } from '../use-collection-manager';
import * as styles from './edit-collection.css';
export interface EditCollectionModalProps {
init?: Collection;
title?: string;
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (view: Collection) => Promise<void>;
allPageListConfig: AllPageListConfig;
}
export const EditCollectionModal = ({
init,
onConfirm,
open,
onOpenChange,
title,
allPageListConfig,
}: EditCollectionModalProps) => {
const t = useAFFiNEI18N();
const onConfirmOnCollection = useCallback(
(view: Collection) => {
onConfirm(view)
.then(() => {
onOpenChange(false);
})
.catch(err => {
console.error(err);
});
},
[onConfirm, onOpenChange]
);
const onCancel = useCallback(() => {
onOpenChange(false);
}, [onOpenChange]);
return (
<Modal
open={open}
onOpenChange={onOpenChange}
withoutCloseButton
width="calc(100% - 64px)"
height="80%"
contentOptions={{
style: {
padding: 0,
maxWidth: 944,
backgroundColor: 'var(--affine-white)',
},
}}
>
{init ? (
<EditCollection
title={title}
onConfirmText={t['com.affine.editCollection.save']()}
init={init}
onCancel={onCancel}
onConfirm={onConfirmOnCollection}
allPageListConfig={allPageListConfig}
/>
) : null}
</Modal>
);
};
export interface EditCollectionProps {
title?: string;
onConfirmText?: string;
init: Collection;
onCancel: () => void;
onConfirm: (collection: Collection) => void;
allPageListConfig: AllPageListConfig;
}
export const EditCollection = ({
init,
onConfirm,
onCancel,
onConfirmText,
allPageListConfig,
}: EditCollectionProps) => {
const t = useAFFiNEI18N();
const [value, onChange] = useState<Collection>(init);
const isNameEmpty = useMemo(() => value.name.trim().length === 0, [value]);
const onSaveCollection = useCallback(() => {
if (!isNameEmpty) {
onConfirm(value);
}
}, [value, isNameEmpty, onConfirm]);
const reset = useCallback(() => {
onChange({
...value,
filterList: init.filterList,
allowList: init.allowList,
});
}, [init.allowList, init.filterList, value]);
const buttons = useMemo(
() => (
<>
<Button size="large" onClick={onCancel}>
{t['com.affine.editCollection.button.cancel']()}
</Button>
<Button
className={styles.confirmButton}
size="large"
data-testid="save-collection"
type="primary"
disabled={isNameEmpty}
onClick={onSaveCollection}
>
{onConfirmText ?? t['com.affine.editCollection.button.create']()}
</Button>
</>
),
[onCancel, t, isNameEmpty, onSaveCollection, onConfirmText]
);
return (
<div className={styles.collectionEditContainer}>
{value.mode === 'page' ? (
<PagesMode
collection={value}
updateCollection={onChange}
buttons={buttons}
allPageListConfig={allPageListConfig}
></PagesMode>
) : (
<RulesMode
allPageListConfig={allPageListConfig}
collection={value}
reset={reset}
updateCollection={onChange}
buttons={buttons}
></RulesMode>
)}
</div>
);
};
export type AllPageListConfig = {
allPages: PageMeta[];
workspace: Workspace;
isEdgeless: (id: string) => boolean;
getPage: (id: string) => PageMeta | undefined;
favoriteRender: (page: PageMeta) => ReactNode;
};
const RulesMode = ({
collection,
updateCollection,
reset,
buttons,
allPageListConfig,
}: {
collection: Collection;
updateCollection: (collection: Collection) => void;
reset: () => void;
buttons: ReactNode;
allPageListConfig: AllPageListConfig;
}) => {
const t = useAFFiNEI18N();
const [showPreview, setShowPreview] = useState(true);
const allowListPages: PageMeta[] = [];
const rulesPages: PageMeta[] = [];
const [showTips, setShowTips] = useState(false);
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(v => {
if (v.trash) {
return;
}
const result = filterPageByRules(
collection.filterList,
collection.allowList,
v
);
if (result) {
if (collection.allowList.includes(v.id)) {
allowListPages.push(v);
} else {
rulesPages.push(v);
}
}
});
const { node: selectPageNode, open } = useSelectPage({ allPageListConfig });
const openSelectPage = useCallback(() => {
open(collection.allowList).then(
ids => {
updateCollection({
...collection,
allowList: ids,
});
},
() => {
//do nothing
}
);
}, [open, updateCollection, collection]);
const [expandInclude, setExpandInclude] = useState(false);
const count = allowListPages.length + rulesPages.length;
return (
<>
{/*prevents modal autofocus to the first input*/}
<input
type="text"
style={{ width: 0, height: 0 }}
onFocus={e => requestAnimationFrame(() => e.target.blur())}
/>
<div className={clsx(styles.rulesTitle, styles.ellipsis)}>
<Trans
i18nKey="com.affine.editCollection.rules.tips"
values={{
highlight: t['com.affine.editCollection.rules.tips.highlight'](),
}}
>
Pages that meet the rules will be added to the current collection{' '}
<span className={styles.rulesTitleHighlight}>highlight</span>.
</Trans>
</div>
<div className={styles.rulesContainer}>
<div className={styles.rulesContainerLeft}>
<div className={styles.rulesContainerLeftTab}>
<RadioButtonGroup
width={158}
style={{ height: 32 }}
value={collection.mode}
onValueChange={useCallback(
(mode: 'page' | 'rule') => {
updateCollection({
...collection,
mode,
});
},
[collection, updateCollection]
)}
>
<RadioButton
spanStyle={styles.tabButton}
value="page"
data-testid="edit-collection-pages-button"
>
{t['com.affine.editCollection.pages']()}
</RadioButton>
<RadioButton
spanStyle={styles.tabButton}
value="rule"
data-testid="edit-collection-rules-button"
>
{t['com.affine.editCollection.rules']()}
</RadioButton>
</RadioButtonGroup>
</div>
<div className={styles.rulesContainerLeftContent}>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 8,
overflowY: 'auto',
}}
>
<FilterList
propertiesMeta={allPageListConfig.workspace.meta.properties}
value={collection.filterList}
onChange={useCallback(
filterList => updateCollection({ ...collection, filterList }),
[collection, updateCollection]
)}
/>
<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)' }}>
include
</div>
</div>
<div
style={{
display: expandInclude ? 'flex' : 'none',
flexWrap: 'wrap',
gap: '8px 16px',
}}
>
{collection.allowList.map(id => {
const page = allPageListConfig.allPages.find(
v => v.id === id
);
return (
<div className={styles.includeItem} key={id}>
<div className={styles.includeItemContent}>
<div
style={{
display: 'flex',
gap: 6,
alignItems: 'center',
}}
>
{allPageListConfig.isEdgeless(id) ? (
<EdgelessIcon style={{ width: 16, height: 16 }} />
) : (
<PageIcon style={{ width: 16, height: 16 }} />
)}
{t[
'com.affine.editCollection.rules.include.page'
]()}
</div>
<div className={styles.includeItemContentIs}>
{t['com.affine.editCollection.rules.include.is']()}
</div>
<div
className={clsx(
styles.includeItemTitle,
styles.ellipsis
)}
>
{page?.title || t['Untitled']()}
</div>
</div>
<CloseIcon
className={styles.button}
onClick={() => {
updateCollection({
...collection,
allowList: collection.allowList.filter(
v => v !== id
),
});
}}
></CloseIcon>
</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>
<PageListScrollContainer
className={styles.rulesContainerRight}
style={{
display: showPreview ? 'flex' : 'none',
}}
>
{rulesPages.length > 0 ? (
<PageList
hideHeader
clickMode="select"
className={styles.resultPages}
pages={rulesPages}
groupBy={false}
blockSuiteWorkspace={allPageListConfig.workspace}
isPreferredEdgeless={allPageListConfig.isEdgeless}
pageOperationsRenderer={allPageListConfig.favoriteRender}
></PageList>
) : null}
{allowListPages.length > 0 ? (
<div>
<div className={styles.includeListTitle}>include</div>
<PageList
hideHeader
clickMode="select"
className={styles.resultPages}
pages={allowListPages}
groupBy={false}
blockSuiteWorkspace={allPageListConfig.workspace}
isPreferredEdgeless={allPageListConfig.isEdgeless}
></PageList>
</div>
) : null}
</PageListScrollContainer>
</div>
<div className={styles.rulesBottom}>
<div className={styles.bottomLeft}>
<div
className={clsx(
styles.button,
styles.bottomButton,
showPreview && styles.previewActive
)}
onClick={() => {
setShowPreview(!showPreview);
}}
>
{t['com.affine.editCollection.rules.preview']()}
</div>
<div
className={clsx(styles.button, styles.bottomButton)}
onClick={reset}
>
{t['com.affine.editCollection.rules.reset']()}
</div>
<div className={styles.previewCountTips}>
<Trans
i18nKey={
count === 0
? 'com.affine.editCollection.rules.countTips.zero'
: count === 1
? 'com.affine.editCollection.rules.countTips.one'
: 'com.affine.editCollection.rules.countTips.more'
}
values={{ count: count }}
>
After searching, there are currently
<span className={styles.previewCountTipsHighlight}>count</span>
pages.
</Trans>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>{buttons}</div>
</div>
{selectPageNode}
</>
);
};
const PagesMode = ({
collection,
updateCollection,
buttons,
allPageListConfig,
}: {
collection: Collection;
updateCollection: (collection: Collection) => void;
buttons: ReactNode;
allPageListConfig: AllPageListConfig;
}) => {
const t = useAFFiNEI18N();
const {
showFilter,
filters,
updateFilters,
clickFilter,
createFilter,
filteredList,
} = useFilter(allPageListConfig.allPages);
const { searchText, updateSearchText, searchedList } =
useSearch(filteredList);
const clearSelected = useCallback(() => {
updateCollection({
...collection,
pages: [],
});
}, [collection, updateCollection]);
const pageOperationsRenderer = useCallback(
(page: PageMeta) => allPageListConfig.favoriteRender(page),
[allPageListConfig]
);
return (
<>
<input
value={searchText}
onChange={e => updateSearchText(e.target.value)}
className={styles.rulesTitle}
style={{
color: 'var(--affine-text-primary-color)',
}}
placeholder={t['com.affine.editCollection.search.placeholder']()}
></input>
<div className={styles.pagesList}>
<div className={styles.pagesTab}>
<div className={styles.pagesTabContent}>
<RadioButtonGroup
width={158}
style={{ height: 32 }}
value={collection.mode}
onValueChange={useCallback(
(mode: 'page' | 'rule') => {
updateCollection({
...collection,
mode,
});
},
[collection, updateCollection]
)}
>
<RadioButton
spanStyle={styles.tabButton}
value="page"
data-testid="edit-collection-pages-button"
>
{t['com.affine.editCollection.pages']()}
</RadioButton>
<RadioButton
spanStyle={styles.tabButton}
value="rule"
data-testid="edit-collection-rules-button"
>
{t['com.affine.editCollection.rules']()}
</RadioButton>
</RadioButtonGroup>
{!showFilter && filters.length === 0 ? (
<Menu
items={
<VariableSelect
propertiesMeta={allPageListConfig.workspace.meta.properties}
selected={filters}
onSelect={createFilter}
/>
}
>
<div>
<FilterIcon
className={clsx(styles.icon, styles.button)}
onClick={clickFilter}
width={24}
height={24}
></FilterIcon>
</div>
</Menu>
) : (
<FilterIcon
className={clsx(styles.icon, styles.button)}
onClick={clickFilter}
width={24}
height={24}
></FilterIcon>
)}
</div>
{showFilter ? (
<div style={{ padding: '12px 16px 16px' }}>
<FilterList
propertiesMeta={allPageListConfig.workspace.meta.properties}
value={filters}
onChange={updateFilters}
/>
</div>
) : null}
{searchedList.length ? (
<PageListScrollContainer>
<PageList
clickMode="select"
className={styles.pageList}
pages={searchedList}
groupBy={false}
blockSuiteWorkspace={allPageListConfig.workspace}
selectable
onSelectedPageIdsChange={ids => {
updateCollection({
...collection,
pages: ids,
});
}}
pageOperationsRenderer={pageOperationsRenderer}
selectedPageIds={collection.pages}
isPreferredEdgeless={allPageListConfig.isEdgeless}
></PageList>
</PageListScrollContainer>
) : (
<EmptyList search={searchText} />
)}
</div>
</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}
>
{collection.pages.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}</div>
</div>
</>
);
};
const SelectPage = ({
allPageListConfig,
init,
onConfirm,
onCancel,
}: {
allPageListConfig: AllPageListConfig;
init: string[];
onConfirm: (pageIds: string[]) => void;
onCancel: () => void;
}) => {
const t = useAFFiNEI18N();
const [value, onChange] = useState(init);
const confirm = useCallback(() => {
onConfirm(value);
}, [value, onConfirm]);
const clearSelected = useCallback(() => {
onChange([]);
}, []);
const {
clickFilter,
createFilter,
filters,
showFilter,
updateFilters,
filteredList,
} = useFilter(allPageListConfig.allPages);
const { searchText, updateSearchText, searchedList } =
useSearch(filteredList);
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>
<div className={styles.pagesTab}>
<div className={styles.pagesTabContent}>
<div style={{ fontSize: 12, lineHeight: '20px', fontWeight: 600 }}>
{t['com.affine.selectPage.title']()}
</div>
{!showFilter && filters.length === 0 ? (
<Menu
items={
<VariableSelect
propertiesMeta={allPageListConfig.workspace.meta.properties}
selected={filters}
onSelect={createFilter}
/>
}
>
<div>
<FilterIcon
className={clsx(styles.icon, styles.button)}
onClick={clickFilter}
width={24}
height={24}
></FilterIcon>
</div>
</Menu>
) : (
<FilterIcon
className={clsx(styles.icon, styles.button)}
onClick={clickFilter}
width={24}
height={24}
></FilterIcon>
)}
</div>
{showFilter ? (
<div style={{ padding: '12px 16px 16px' }}>
<FilterList
propertiesMeta={allPageListConfig.workspace.meta.properties}
value={filters}
onChange={updateFilters}
/>
</div>
) : null}
{searchedList.length ? (
<PageListScrollContainer>
<PageList
clickMode="select"
className={styles.pageList}
pages={searchedList}
blockSuiteWorkspace={allPageListConfig.workspace}
selectable
onSelectedPageIdsChange={onChange}
selectedPageIds={value}
isPreferredEdgeless={allPageListConfig.isEdgeless}
pageOperationsRenderer={allPageListConfig.favoriteRender}
></PageList>
</PageListScrollContainer>
) : (
<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>
<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}
>
{t['Confirm']()}
</Button>
</div>
</div>
</div>
);
};
const useSelectPage = ({
allPageListConfig,
}: {
allPageListConfig: AllPageListConfig;
}) => {
const [value, onChange] = useState<{
init: string[];
onConfirm: (ids: string[]) => void;
}>();
const close = useCallback(() => {
onChange(undefined);
}, []);
return {
node: (
<Modal
open={!!value}
onOpenChange={close}
withoutCloseButton
width="calc(100% - 32px)"
height="80%"
overlayOptions={{ style: { backgroundColor: 'transparent' } }}
contentOptions={{
style: {
padding: 0,
transform: 'translate(-50%,calc(-50% + 16px))',
maxWidth: 976,
backgroundColor: 'var(--affine-white)',
},
}}
>
{value ? (
<SelectPage
allPageListConfig={allPageListConfig}
init={value.init}
onConfirm={value.onConfirm}
onCancel={close}
/>
) : null}
</Modal>
),
open: (init: string[]): Promise<string[]> =>
new Promise<string[]>(res => {
onChange({
init,
onConfirm: list => {
close();
res(list);
},
});
}),
};
};
const useFilter = (list: PageMeta[]) => {
const [filters, changeFilters] = useState<Filter[]>([]);
const [showFilter, setShowFilter] = useState(false);
const clickFilter = useCallback(
(e: MouseEvent) => {
if (showFilter || filters.length !== 0) {
e.stopPropagation();
e.preventDefault();
setShowFilter(!showFilter);
}
},
[filters.length, showFilter]
);
const onCreateFilter = useCallback(
(filter: Filter) => {
changeFilters([...filters, filter]);
setShowFilter(true);
},
[filters]
);
return {
showFilter,
filters,
updateFilters: changeFilters,
clickFilter,
createFilter: onCreateFilter,
filteredList: list.filter(v => {
if (v.trash) {
return false;
}
return filterPageByRules(filters, [], v);
}),
};
};
const useSearch = (list: PageMeta[]) => {
const [value, onChange] = useState('');
return {
searchText: value,
updateSearchText: onChange,
searchedList: value
? list.filter(v => v.title.toLowerCase().includes(value.toLowerCase()))
: list,
};
};
const EmptyList = ({ search }: { search?: string }) => {
const t = useAFFiNEI18N();
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
flex: 1,
}}
>
<AffineShapeIcon />
<div
style={{
margin: '18px 0',
fontSize: 20,
lineHeight: '28px',
fontWeight: 600,
}}
>
{t['com.affine.selectPage.empty']()}
</div>
{search ? (
<div
className={styles.ellipsis}
style={{ maxWidth: 300, fontSize: 15, lineHeight: '24px' }}
>
<Trans i18nKey="com.affine.selectPage.empty.tips" values={{ search }}>
No page titles contain
<span
style={{ fontWeight: 600, color: 'var(--affine-primary-color)' }}
>
search
</span>
</Trans>
</div>
) : null}
</div>
);
};

View File

@@ -59,12 +59,12 @@ export const rulesBottom = style({
});
export const includeListTitle = style({
marginTop: 8,
fontSize: 14,
fontWeight: 400,
lineHeight: '22px',
color: 'var(--affine-text-secondary-color)',
paddingLeft: 18,
padding: '4px 16px',
borderTop: '1px solid var(--affine-border-color)',
});
export const rulesContainerRight = style({

View File

@@ -0,0 +1,199 @@
import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageMeta, Workspace } from '@blocksuite/store';
import type { DialogContentProps } from '@radix-ui/react-dialog';
import { Button } from '@toeverything/components/button';
import { Modal } from '@toeverything/components/modal';
import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { RadioButton, RadioButtonGroup } from '../../../../index';
import * as styles from './edit-collection.css';
import { PagesMode } from './pages-mode';
import { RulesMode } from './rules-mode';
export type EditCollectionMode = 'page' | 'rule';
export interface EditCollectionModalProps {
init?: Collection;
title?: string;
open: boolean;
mode?: EditCollectionMode;
onOpenChange: (open: boolean) => void;
onConfirm: (view: Collection) => Promise<void>;
allPageListConfig: AllPageListConfig;
}
const contentOptions: DialogContentProps = {
onPointerDownOutside: e => {
e.preventDefault();
},
style: {
padding: 0,
maxWidth: 944,
backgroundColor: 'var(--affine-white)',
},
};
export const EditCollectionModal = ({
init,
onConfirm,
open,
onOpenChange,
title,
mode,
allPageListConfig,
}: EditCollectionModalProps) => {
const t = useAFFiNEI18N();
const onConfirmOnCollection = useCallback(
(view: Collection) => {
onConfirm(view)
.then(() => {
onOpenChange(false);
})
.catch(err => {
console.error(err);
});
},
[onConfirm, onOpenChange]
);
const onCancel = useCallback(() => {
onOpenChange(false);
}, [onOpenChange]);
return (
<Modal
open={open}
onOpenChange={onOpenChange}
withoutCloseButton
width="calc(100% - 64px)"
height="80%"
contentOptions={contentOptions}
>
{init ? (
<EditCollection
title={title}
onConfirmText={t['com.affine.editCollection.save']()}
init={init}
mode={mode}
onCancel={onCancel}
onConfirm={onConfirmOnCollection}
allPageListConfig={allPageListConfig}
/>
) : null}
</Modal>
);
};
export interface EditCollectionProps {
title?: string;
onConfirmText?: string;
init: Collection;
mode?: EditCollectionMode;
onCancel: () => void;
onConfirm: (collection: Collection) => void;
allPageListConfig: AllPageListConfig;
}
export const EditCollection = ({
init,
onConfirm,
onCancel,
onConfirmText,
mode: initMode,
allPageListConfig,
}: EditCollectionProps) => {
const t = useAFFiNEI18N();
const [value, onChange] = useState<Collection>(init);
const [mode, setMode] = useState<'page' | 'rule'>(
initMode ?? (init.filterList.length === 0 ? 'page' : 'rule')
);
const isNameEmpty = useMemo(() => value.name.trim().length === 0, [value]);
const onSaveCollection = useCallback(() => {
if (!isNameEmpty) {
onConfirm(value);
}
}, [value, isNameEmpty, onConfirm]);
const reset = useCallback(() => {
onChange({
...value,
filterList: init.filterList,
allowList: init.allowList,
});
}, [init.allowList, init.filterList, value]);
const buttons = useMemo(
() => (
<>
<Button size="large" onClick={onCancel}>
{t['com.affine.editCollection.button.cancel']()}
</Button>
<Button
className={styles.confirmButton}
size="large"
data-testid="save-collection"
type="primary"
disabled={isNameEmpty}
onClick={onSaveCollection}
>
{onConfirmText ?? t['com.affine.editCollection.button.create']()}
</Button>
</>
),
[onCancel, t, isNameEmpty, onSaveCollection, onConfirmText]
);
const switchMode = useMemo(
() => (
<RadioButtonGroup
width={158}
style={{ height: 32 }}
value={mode}
onValueChange={(mode: 'page' | 'rule') => {
setMode(mode);
}}
>
<RadioButton
spanStyle={styles.tabButton}
value="page"
data-testid="edit-collection-pages-button"
>
{t['com.affine.editCollection.pages']()}
</RadioButton>
<RadioButton
spanStyle={styles.tabButton}
value="rule"
data-testid="edit-collection-rules-button"
>
{t['com.affine.editCollection.rules']()}
</RadioButton>
</RadioButtonGroup>
),
[mode, t]
);
return (
<div className={styles.collectionEditContainer}>
{mode === 'page' ? (
<PagesMode
collection={value}
updateCollection={onChange}
switchMode={switchMode}
buttons={buttons}
allPageListConfig={allPageListConfig}
></PagesMode>
) : (
<RulesMode
allPageListConfig={allPageListConfig}
collection={value}
switchMode={switchMode}
reset={reset}
updateCollection={onChange}
buttons={buttons}
></RulesMode>
)}
</div>
);
};
export type AllPageListConfig = {
allPages: PageMeta[];
workspace: Workspace;
isEdgeless: (id: string) => boolean;
getPage: (id: string) => PageMeta | undefined;
favoriteRender: (page: PageMeta) => ReactNode;
};

View File

@@ -0,0 +1,106 @@
import {
type AllPageListConfig,
filterPageByRules,
} from '@affine/component/page-list';
import type { Filter } from '@affine/env/filter';
import type { PageMeta } from '@blocksuite/store';
import { Modal } from '@toeverything/components/modal';
import { type MouseEvent, useCallback, useState } from 'react';
import { SelectPage } from './select-page';
export const useSelectPage = ({
allPageListConfig,
}: {
allPageListConfig: AllPageListConfig;
}) => {
const [value, onChange] = useState<{
init: string[];
onConfirm: (ids: string[]) => void;
}>();
const close = useCallback(() => {
onChange(undefined);
}, []);
return {
node: (
<Modal
open={!!value}
onOpenChange={close}
withoutCloseButton
width="calc(100% - 32px)"
height="80%"
overlayOptions={{ style: { backgroundColor: 'transparent' } }}
contentOptions={{
style: {
padding: 0,
transform: 'translate(-50%,calc(-50% + 16px))',
maxWidth: 976,
backgroundColor: 'var(--affine-white)',
},
}}
>
{value ? (
<SelectPage
allPageListConfig={allPageListConfig}
init={value.init}
onConfirm={value.onConfirm}
onCancel={close}
/>
) : null}
</Modal>
),
open: (init: string[]): Promise<string[]> =>
new Promise<string[]>(res => {
onChange({
init,
onConfirm: list => {
close();
res(list);
},
});
}),
};
};
export const useFilter = (list: PageMeta[]) => {
const [filters, changeFilters] = useState<Filter[]>([]);
const [showFilter, setShowFilter] = useState(false);
const clickFilter = useCallback(
(e: MouseEvent) => {
if (showFilter || filters.length !== 0) {
e.stopPropagation();
e.preventDefault();
setShowFilter(!showFilter);
}
},
[filters.length, showFilter]
);
const onCreateFilter = useCallback(
(filter: Filter) => {
changeFilters([...filters, filter]);
setShowFilter(true);
},
[filters]
);
return {
showFilter,
filters,
updateFilters: changeFilters,
clickFilter,
createFilter: onCreateFilter,
filteredList: list.filter(v => {
if (v.trash) {
return false;
}
return filterPageByRules(filters, [], v);
}),
};
};
export const useSearch = (list: PageMeta[]) => {
const [value, onChange] = useState('');
return {
searchText: value,
updateSearchText: onChange,
searchedList: value
? list.filter(v => v.title.toLowerCase().includes(value.toLowerCase()))
: list,
};
};

View File

@@ -0,0 +1,154 @@
import {
type AllPageListConfig,
FilterList,
PageList,
PageListScrollContainer,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FilterIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { Menu } from '@toeverything/components/menu';
import clsx from 'clsx';
import { type ReactNode, useCallback } from 'react';
import { VariableSelect } from '../../filter/vars';
import * as styles from './edit-collection.css';
import { useFilter, useSearch } from './hooks';
import { EmptyList } from './select-page';
export const PagesMode = ({
switchMode,
collection,
updateCollection,
buttons,
allPageListConfig,
}: {
collection: Collection;
updateCollection: (collection: Collection) => void;
buttons: ReactNode;
switchMode: ReactNode;
allPageListConfig: AllPageListConfig;
}) => {
const t = useAFFiNEI18N();
const {
showFilter,
filters,
updateFilters,
clickFilter,
createFilter,
filteredList,
} = useFilter(allPageListConfig.allPages);
const { searchText, updateSearchText, searchedList } =
useSearch(filteredList);
const clearSelected = useCallback(() => {
updateCollection({
...collection,
allowList: [],
});
}, [collection, updateCollection]);
const pageOperationsRenderer = useCallback(
(page: PageMeta) => allPageListConfig.favoriteRender(page),
[allPageListConfig]
);
return (
<>
<input
value={searchText}
onChange={e => updateSearchText(e.target.value)}
className={styles.rulesTitle}
style={{
color: 'var(--affine-text-primary-color)',
}}
placeholder={t['com.affine.editCollection.search.placeholder']()}
></input>
<div className={styles.pagesList}>
<div className={styles.pagesTab}>
<div className={styles.pagesTabContent}>
{switchMode}
{!showFilter && filters.length === 0 ? (
<Menu
items={
<VariableSelect
propertiesMeta={allPageListConfig.workspace.meta.properties}
selected={filters}
onSelect={createFilter}
/>
}
>
<div>
<FilterIcon
className={clsx(styles.icon, styles.button)}
onClick={clickFilter}
width={24}
height={24}
></FilterIcon>
</div>
</Menu>
) : (
<FilterIcon
className={clsx(styles.icon, styles.button)}
onClick={clickFilter}
width={24}
height={24}
></FilterIcon>
)}
</div>
{showFilter ? (
<div style={{ padding: '12px 16px 16px' }}>
<FilterList
propertiesMeta={allPageListConfig.workspace.meta.properties}
value={filters}
onChange={updateFilters}
/>
</div>
) : null}
{searchedList.length ? (
<PageListScrollContainer>
<PageList
clickMode="select"
className={styles.pageList}
pages={searchedList}
groupBy={false}
blockSuiteWorkspace={allPageListConfig.workspace}
selectable
onSelectedPageIdsChange={ids => {
updateCollection({
...collection,
allowList: ids,
});
}}
pageOperationsRenderer={pageOperationsRenderer}
selectedPageIds={collection.allowList}
isPreferredEdgeless={allPageListConfig.isEdgeless}
></PageList>
</PageListScrollContainer>
) : (
<EmptyList search={searchText} />
)}
</div>
</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}
>
{collection.allowList.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}</div>
</div>
</>
);
};

View File

@@ -0,0 +1,370 @@
import type { Collection } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
CloseIcon,
EdgelessIcon,
PageIcon,
PlusIcon,
ToggleCollapseIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import clsx from 'clsx';
import { type ReactNode, useCallback, useEffect, useState } from 'react';
import { FilterList } from '../../filter';
import { PageList, PageListScrollContainer } from '../../page-list';
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,
updateCollection,
reset,
buttons,
switchMode,
allPageListConfig,
}: {
collection: Collection;
updateCollection: (collection: Collection) => void;
reset: () => void;
buttons: ReactNode;
switchMode: ReactNode;
allPageListConfig: AllPageListConfig;
}) => {
const t = useAFFiNEI18N();
const [showPreview, setShowPreview] = useState(true);
const allowListPages: PageMeta[] = [];
const rulesPages: PageMeta[] = [];
const [showTips, setShowTips] = useState(false);
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(v => {
if (v.trash) {
return;
}
if (
collection.filterList.length &&
filterPageByRules(collection.filterList, [], v)
) {
rulesPages.push(v);
}
if (collection.allowList.includes(v.id)) {
allowListPages.push(v);
}
});
const { node: selectPageNode, open } = useSelectPage({ allPageListConfig });
const openSelectPage = useCallback(() => {
open(collection.allowList).then(
ids => {
updateCollection({
...collection,
allowList: ids,
});
},
() => {
//do nothing
}
);
}, [open, updateCollection, collection]);
const [expandInclude, setExpandInclude] = useState(true);
return (
<>
{/*prevents modal autofocus to the first input*/}
<input
type="text"
style={{ width: 0, height: 0 }}
onFocus={e => requestAnimationFrame(() => e.target.blur())}
/>
<div className={clsx(styles.rulesTitle, styles.ellipsis)}>
<Trans
i18nKey="com.affine.editCollection.rules.tips"
values={{
highlight: t['com.affine.editCollection.rules.tips.highlight'](),
}}
>
Pages that meet the rules will be added to the current collection{' '}
<span className={styles.rulesTitleHighlight}>highlight</span>.
</Trans>
</div>
<div className={styles.rulesContainer}>
<div className={styles.rulesContainerLeft}>
<div className={styles.rulesContainerLeftTab}>{switchMode}</div>
<div className={styles.rulesContainerLeftContent}>
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
gap: 8,
overflowY: 'auto',
}}
>
<FilterList
propertiesMeta={allPageListConfig.workspace.meta.properties}
value={collection.filterList}
onChange={useCallback(
filterList => updateCollection({ ...collection, filterList }),
[collection, updateCollection]
)}
/>
<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']()}
</div>
</div>
<div
style={{
display: expandInclude ? 'flex' : 'none',
flexWrap: 'wrap',
gap: '8px 16px',
}}
>
{collection.allowList.map(id => {
const page = allPageListConfig.allPages.find(
v => v.id === id
);
return (
<div className={styles.includeItem} key={id}>
<div className={styles.includeItemContent}>
<div
style={{
display: 'flex',
gap: 6,
alignItems: 'center',
}}
>
{allPageListConfig.isEdgeless(id) ? (
<EdgelessIcon style={{ width: 16, height: 16 }} />
) : (
<PageIcon style={{ width: 16, height: 16 }} />
)}
{t[
'com.affine.editCollection.rules.include.page'
]()}
</div>
<div className={styles.includeItemContentIs}>
{t['com.affine.editCollection.rules.include.is']()}
</div>
<div
className={clsx(
styles.includeItemTitle,
styles.ellipsis
)}
>
{page?.title || t['Untitled']()}
</div>
</div>
<CloseIcon
className={styles.button}
onClick={() => {
updateCollection({
...collection,
allowList: collection.allowList.filter(
v => v !== id
),
});
}}
></CloseIcon>
</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>
<PageListScrollContainer
className={styles.rulesContainerRight}
style={{
display: showPreview ? 'flex' : 'none',
}}
>
{rulesPages.length > 0 ? (
<PageList
hideHeader
clickMode="select"
className={styles.resultPages}
pages={rulesPages}
groupBy={false}
blockSuiteWorkspace={allPageListConfig.workspace}
isPreferredEdgeless={allPageListConfig.isEdgeless}
pageOperationsRenderer={allPageListConfig.favoriteRender}
></PageList>
) : (
<RulesEmpty
noRules={collection.filterList.length === 0}
fullHeight={allowListPages.length === 0}
/>
)}
{allowListPages.length > 0 ? (
<div>
<div className={styles.includeListTitle}>
{t['com.affine.editCollection.rules.include.title']()}
</div>
<PageList
hideHeader
clickMode="select"
className={styles.resultPages}
pages={allowListPages}
groupBy={false}
blockSuiteWorkspace={allPageListConfig.workspace}
isPreferredEdgeless={allPageListConfig.isEdgeless}
pageOperationsRenderer={allPageListConfig.favoriteRender}
></PageList>
</div>
) : null}
</PageListScrollContainer>
</div>
<div className={styles.rulesBottom}>
<div className={styles.bottomLeft}>
<div
className={clsx(
styles.button,
styles.bottomButton,
showPreview && styles.previewActive
)}
onClick={() => {
setShowPreview(!showPreview);
}}
>
{t['com.affine.editCollection.rules.preview']()}
</div>
<div
className={clsx(styles.button, styles.bottomButton)}
onClick={reset}
>
{t['com.affine.editCollection.rules.reset']()}
</div>
<div className={styles.previewCountTips}>
<Trans
i18nKey="com.affine.editCollection.rules.countTips"
values={{
selectedCount: allowListPages.length,
filteredCount: rulesPages.length,
}}
>
Selected
<span className={styles.previewCountTipsHighlight}>count</span>,
filtered
<span className={styles.previewCountTipsHighlight}>count</span>
</Trans>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>{buttons}</div>
</div>
{selectPageNode}
</>
);
};
const RulesEmpty = ({
noRules,
fullHeight,
}: {
noRules: boolean;
fullHeight: boolean;
}) => {
const t = useAFFiNEI18N();
return (
<div
style={{
height: fullHeight ? '100%' : '70%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 18,
padding: '48px 0',
}}
>
<AffineShapeIcon />
<strong style={{ fontSize: 20, lineHeight: '28px' }}>
{noRules
? t['com.affine.editCollection.rules.empty.noRules']()
: t['com.affine.editCollection.rules.empty.noResults']()}
</strong>
<div
style={{
width: '389px',
textAlign: 'center',
fontSize: 15,
lineHeight: '24px',
}}
>
{noRules ? (
<Trans i18nKey="com.affine.editCollection.rules.empty.noRules.tips">
Please <strong>add rules</strong> to save this collection or switch
to <strong>Pages</strong>, use manual selection mode
</Trans>
) : (
t['com.affine.editCollection.rules.empty.noResults.tips']()
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,190 @@
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FilterIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { Menu } from '@toeverything/components/menu';
import clsx from 'clsx';
import { useCallback, useState } from 'react';
import { FilterList } from '../../filter';
import { VariableSelect } from '../../filter/vars';
import { PageList, PageListScrollContainer } from '../../page-list';
import { AffineShapeIcon } from '../affine-shape';
import type { AllPageListConfig } from './edit-collection';
import * as styles from './edit-collection.css';
import { useFilter, useSearch } from './hooks';
export const SelectPage = ({
allPageListConfig,
init,
onConfirm,
onCancel,
}: {
allPageListConfig: AllPageListConfig;
init: string[];
onConfirm: (pageIds: string[]) => void;
onCancel: () => void;
}) => {
const t = useAFFiNEI18N();
const [value, onChange] = useState(init);
const confirm = useCallback(() => {
onConfirm(value);
}, [value, onConfirm]);
const clearSelected = useCallback(() => {
onChange([]);
}, []);
const {
clickFilter,
createFilter,
filters,
showFilter,
updateFilters,
filteredList,
} = useFilter(allPageListConfig.allPages);
const { searchText, updateSearchText, searchedList } =
useSearch(filteredList);
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>
<div className={styles.pagesTab}>
<div className={styles.pagesTabContent}>
<div style={{ fontSize: 12, lineHeight: '20px', fontWeight: 600 }}>
{t['com.affine.selectPage.title']()}
</div>
{!showFilter && filters.length === 0 ? (
<Menu
items={
<VariableSelect
propertiesMeta={allPageListConfig.workspace.meta.properties}
selected={filters}
onSelect={createFilter}
/>
}
>
<div>
<FilterIcon
className={clsx(styles.icon, styles.button)}
onClick={clickFilter}
width={24}
height={24}
></FilterIcon>
</div>
</Menu>
) : (
<FilterIcon
className={clsx(styles.icon, styles.button)}
onClick={clickFilter}
width={24}
height={24}
></FilterIcon>
)}
</div>
{showFilter ? (
<div style={{ padding: '12px 16px 16px' }}>
<FilterList
propertiesMeta={allPageListConfig.workspace.meta.properties}
value={filters}
onChange={updateFilters}
/>
</div>
) : null}
{searchedList.length ? (
<PageListScrollContainer>
<PageList
clickMode="select"
className={styles.pageList}
pages={searchedList}
blockSuiteWorkspace={allPageListConfig.workspace}
selectable
onSelectedPageIdsChange={onChange}
selectedPageIds={value}
isPreferredEdgeless={allPageListConfig.isEdgeless}
pageOperationsRenderer={allPageListConfig.favoriteRender}
></PageList>
</PageListScrollContainer>
) : (
<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>
<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}
>
{t['Confirm']()}
</Button>
</div>
</div>
</div>
);
};
export const EmptyList = ({ search }: { search?: string }) => {
const t = useAFFiNEI18N();
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
flex: 1,
}}
>
<AffineShapeIcon />
<div
style={{
margin: '18px 0',
fontSize: 20,
lineHeight: '28px',
fontWeight: 600,
}}
>
{t['com.affine.selectPage.empty']()}
</div>
{search ? (
<div
className={styles.ellipsis}
style={{ maxWidth: 300, fontSize: 15, lineHeight: '24px' }}
>
<Trans i18nKey="com.affine.selectPage.empty.tips" values={{ search }}>
No page titles contain
<span
style={{ fontWeight: 600, color: 'var(--affine-primary-color)' }}
>
search
</span>
</Trans>
</div>
) : null}
</div>
);
};

View File

@@ -3,5 +3,5 @@ export * from './collection-bar';
export * from './collection-list';
export * from './collection-operations';
export * from './create-collection';
export * from './edit-collection';
export * from './edit-collection/edit-collection';
export * from './use-edit-collection';

View File

@@ -2,6 +2,7 @@ import {
type AllPageListConfig,
CreateCollectionModal,
EditCollectionModal,
type EditCollectionMode,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { useCallback, useState } from 'react';
@@ -9,6 +10,7 @@ import { useCallback, useState } from 'react';
export const useEditCollection = (config: AllPageListConfig) => {
const [data, setData] = useState<{
collection: Collection;
mode?: 'page' | 'rule';
onConfirm: (collection: Collection) => Promise<void>;
}>();
const close = useCallback(() => setData(undefined), []);
@@ -19,14 +21,19 @@ export const useEditCollection = (config: AllPageListConfig) => {
allPageListConfig={config}
init={data.collection}
open={!!data}
mode={data.mode}
onOpenChange={close}
onConfirm={data.onConfirm}
/>
) : null,
open: (collection: Collection): Promise<Collection> =>
open: (
collection: Collection,
mode?: EditCollectionMode
): Promise<Collection> =>
new Promise<Collection>(res => {
setData({
collection,
mode,
onConfirm: async collection => {
res(collection);
},