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
-3
View File
@@ -51,11 +51,8 @@ export type Filter = z.input<typeof filterSchema>;
export const collectionSchema = z.object({
id: z.string(),
name: z.string(),
mode: z.union([z.literal('page'), z.literal('rule')]),
filterList: z.array(filterSchema),
allowList: z.array(z.string()),
// page id list
pages: z.array(z.string()),
});
export const deletedCollectionSchema = z.object({
userId: z.string().optional(),
@@ -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);
};
@@ -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 {
@@ -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,
@@ -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>
);
};
@@ -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({
@@ -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;
};
@@ -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,
};
};
@@ -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>
</>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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';
@@ -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);
},
@@ -116,10 +116,8 @@ export const pageCollectionBaseAtom =
return {
id: v.id,
name: v.name,
mode: 'rule',
filterList: v.filterList,
allowList: v.allowList ?? [],
pages: [],
};
});
};
@@ -140,10 +138,8 @@ export const pageCollectionBaseAtom =
return {
id: v.id,
name: v.name,
mode: 'rule',
filterList: v.filterList,
allowList: v.allowList ?? [],
pages: [],
};
});
}
@@ -1,4 +1,4 @@
import { AnimatedCollectionsIcon } from '@affine/component';
import { AnimatedCollectionsIcon, toast } from '@affine/component';
import {
MenuItem as SidebarMenuItem,
MenuLinkItem as SidebarMenuLinkItem,
@@ -54,10 +54,17 @@ const CollectionRenderer = ({
}) => {
const [collapsed, setCollapsed] = useState(true);
const setting = useCollectionManager(collectionsCRUDAtom);
const t = useAFFiNEI18N();
const { setNodeRef, isOver } = useDroppable({
id: `${Collections_DROP_AREA_PREFIX}${collection.id}`,
data: {
addToCollection: (id: string) => {
if (collection.allowList.includes(id)) {
toast(t['com.affine.collection.addPage.alreadyExists']());
return;
} else {
toast(t['com.affine.collection.addPage.success']());
}
setting.addPage(collection.id, id).catch(err => {
console.error(err);
});
@@ -36,10 +36,8 @@ const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
const setting = useCollectionManager(collectionsCRUDAtom);
const saveToCollection = useCallback(
async (collection: Collection) => {
console.log(setting.currentCollection.filterList);
await setting.createCollection({
...collection,
mode: 'rule',
filterList: setting.currentCollection.filterList,
});
navigateHelper.jumpToCollection(workspaceId, collection.id);
@@ -93,10 +93,10 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
const { node, open } = useEditCollection(useAllPageListConfig());
const openPageEdit = useCallback(() => {
open({ ...collection, mode: 'page' }).then(updateCollection);
open({ ...collection }, 'page').then(updateCollection);
}, [open, collection, updateCollection]);
const openRuleEdit = useCallback(() => {
open({ ...collection, mode: 'rule' }).then(updateCollection);
open({ ...collection }, 'rule').then(updateCollection);
}, [collection, open, updateCollection]);
const [showTips, setShowTips] = useState(false);
useEffect(() => {
@@ -277,9 +277,6 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
const isEmpty = (collection: Collection) => {
return (
(collection.mode === 'page' && collection.pages.length === 0) ||
(collection.mode === 'rule' &&
collection.allowList.length === 0 &&
collection.filterList.length === 0)
collection.allowList.length === 0 && collection.filterList.length === 0
);
};
@@ -1,4 +1,8 @@
import { filterPage, useCollectionManager } from '@affine/component/page-list';
import {
filterPage,
filterPageByRules,
useCollectionManager,
} from '@affine/component/page-list';
import type { PageMeta } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react';
@@ -15,7 +19,8 @@ export const useFilteredPageMetas = (
) => {
const { isPreferredEdgeless } = usePageHelper(workspace);
const pageMode = useAtomValue(allPageModeSelectAtom);
const { currentCollection } = useCollectionManager(collectionsCRUDAtom);
const { currentCollection, isDefault } =
useCollectionManager(collectionsCRUDAtom);
const filteredPageMetas = useMemo(
() =>
@@ -43,9 +48,22 @@ export const useFilteredPageMetas = (
if (!currentCollection) {
return true;
}
return filterPage(currentCollection, pageMeta);
return isDefault
? filterPageByRules(
currentCollection.filterList,
currentCollection.allowList,
pageMeta
)
: filterPage(currentCollection, pageMeta);
}),
[pageMetas, pageMode, isPreferredEdgeless, route, currentCollection]
[
currentCollection,
isDefault,
isPreferredEdgeless,
pageMetas,
pageMode,
route,
]
);
return filteredPageMetas;
@@ -95,16 +95,11 @@ export class WorkspaceSetting {
deletePagesFromCollection(collection: Collection, idSet: Set<string>) {
const newAllowList = collection.allowList.filter(id => !idSet.has(id));
const newPages = collection.pages.filter(id => !idSet.has(id));
if (
newAllowList.length !== collection.allowList.length ||
newPages.length !== collection.pages.length
) {
if (newAllowList.length !== collection.allowList.length) {
this.updateCollection(collection.id, old => {
return {
...old,
allowList: newAllowList,
pages: newPages,
};
});
}
+11 -6
View File
@@ -407,20 +407,25 @@
"com.affine.editCollectionName.name": "Name",
"com.affine.editCollectionName.name.placeholder": "Collection Name",
"com.affine.editCollectionName.createTips": "Collection is a smart folder where you can manually add pages or automatically add pages through rules.",
"com.affine.editCollection.rules.include.title": "Selected pages",
"com.affine.editCollection.rules.include.page": "Page",
"com.affine.editCollection.rules.include.is": "is",
"com.affine.editCollection.rules.include.add": "Add include page",
"com.affine.editCollection.rules.include.tipsTitle": "What is \"Include\"",
"com.affine.editCollection.rules.include.tips": "\"Include\" refers to manually adding pages rather than automatically adding them through rule matching. You can manually add pages through the \"Add pages\" option or by dragging and dropping (coming soon).",
"com.affine.editCollection.rules.include.add": "Add selected page",
"com.affine.editCollection.rules.include.tipsTitle": "What is \"Selected pages\"",
"com.affine.editCollection.rules.include.tips": "“Selected pages” refers to manually adding pages rather than automatically adding them through rule matching. You can manually add pages through the Add selected pages option or by dragging and dropping.",
"com.affine.editCollection.rules.preview": "Preview",
"com.affine.editCollection.rules.reset": "Reset",
"com.affine.editCollection.rules.countTips.zero": "Showing <1>{{count}}</1> pages.",
"com.affine.editCollection.rules.countTips.one": "Showing <1>{{count}}</1> page.",
"com.affine.editCollection.rules.countTips.more": "Showing <1>{{count}}</1> pages.",
"com.affine.editCollection.rules.countTips": "Selected <1>{{selectedCount}}</1>, filtered <3>{{filteredCount}}</3>",
"com.affine.editCollection.rules.empty.noRules": "No Rules",
"com.affine.editCollection.rules.empty.noRules.tips": "Please <1>add rules</1> to save this collection or switch to <3>Pages</3>, use manual selection mode",
"com.affine.editCollection.rules.empty.noResults": "No Results",
"com.affine.editCollection.rules.empty.noResults.tips": "No pages meet the filtering rules",
"com.affine.selectPage.title": "Add include page",
"com.affine.selectPage.selected": "Selected",
"com.affine.selectPage.empty": "Empty",
"com.affine.selectPage.empty.tips": "No page titles contain <1>{{search}}</1>",
"com.affine.collection.addPage.alreadyExists": "Page already exists",
"com.affine.collection.addPage.success": "Added successfully",
"Confirm": "Confirm",
"Connector": "Connector",
"Continue with Google": "Continue with Google",