feat(core): remove mode and pages field from Collection (#4817)
This commit is contained in:
3
packages/common/env/src/filter.ts
vendored
3
packages/common/env/src/filter.ts
vendored
@@ -51,11 +51,8 @@ export type Filter = z.input<typeof filterSchema>;
|
|||||||
export const collectionSchema = z.object({
|
export const collectionSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
mode: z.union([z.literal('page'), z.literal('rule')]),
|
|
||||||
filterList: z.array(filterSchema),
|
filterList: z.array(filterSchema),
|
||||||
allowList: z.array(z.string()),
|
allowList: z.array(z.string()),
|
||||||
// page id list
|
|
||||||
pages: z.array(z.string()),
|
|
||||||
});
|
});
|
||||||
export const deletedCollectionSchema = z.object({
|
export const deletedCollectionSchema = z.object({
|
||||||
userId: z.string().optional(),
|
userId: z.string().optional(),
|
||||||
|
|||||||
@@ -19,16 +19,13 @@ export const createEmptyCollection = (
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: '',
|
name: '',
|
||||||
mode: 'page',
|
|
||||||
filterList: [],
|
filterList: [],
|
||||||
pages: [],
|
|
||||||
allowList: [],
|
allowList: [],
|
||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const defaultCollection: Collection = createEmptyCollection(NIL, {
|
const defaultCollection: Collection = createEmptyCollection(NIL, {
|
||||||
name: 'All',
|
name: 'All',
|
||||||
mode: 'rule',
|
|
||||||
});
|
});
|
||||||
const defaultCollectionAtom = atomWithReset<Collection>(defaultCollection);
|
const defaultCollectionAtom = atomWithReset<Collection>(defaultCollection);
|
||||||
export const currentCollectionAtom = atomWithReset<string>(NIL);
|
export const currentCollectionAtom = atomWithReset<string>(NIL);
|
||||||
@@ -52,12 +49,6 @@ export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => {
|
|||||||
const addPage = useCallback(
|
const addPage = useCallback(
|
||||||
async (collectionId: string, pageId: string) => {
|
async (collectionId: string, pageId: string) => {
|
||||||
await updateCollection(collectionId, old => {
|
await updateCollection(collectionId, old => {
|
||||||
if (old.mode === 'page') {
|
|
||||||
return {
|
|
||||||
...old,
|
|
||||||
pages: [pageId, ...(old.pages ?? [])],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
allowList: [pageId, ...(old.allowList ?? [])],
|
allowList: [pageId, ...(old.allowList ?? [])],
|
||||||
@@ -128,8 +119,8 @@ export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
|
|||||||
evalFilterList(filterList, varMap);
|
evalFilterList(filterList, varMap);
|
||||||
|
|
||||||
export const filterPage = (collection: Collection, page: PageMeta) => {
|
export const filterPage = (collection: Collection, page: PageMeta) => {
|
||||||
if (collection.mode === 'page') {
|
if (collection.filterList.length === 0) {
|
||||||
return collection.pages.includes(page.id);
|
return collection.allowList.includes(page.id);
|
||||||
}
|
}
|
||||||
return filterPageByRules(collection.filterList, collection.allowList, page);
|
return filterPageByRules(collection.filterList, collection.allowList, page);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
useCollectionManager,
|
useCollectionManager,
|
||||||
} from '../use-collection-manager';
|
} from '../use-collection-manager';
|
||||||
import * as styles from './collection-bar.css';
|
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';
|
import { useActions } from './use-action';
|
||||||
|
|
||||||
interface CollectionBarProps {
|
interface CollectionBarProps {
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import { CreateFilterMenu } from '../filter/vars';
|
|||||||
import type { useCollectionManager } from '../use-collection-manager';
|
import type { useCollectionManager } from '../use-collection-manager';
|
||||||
import * as styles from './collection-list.css';
|
import * as styles from './collection-list.css';
|
||||||
import { CollectionOperations } from './collection-operations';
|
import { CollectionOperations } from './collection-operations';
|
||||||
import { type AllPageListConfig, EditCollectionModal } from './edit-collection';
|
import {
|
||||||
|
type AllPageListConfig,
|
||||||
|
EditCollectionModal,
|
||||||
|
} from './edit-collection/edit-collection';
|
||||||
|
|
||||||
export const CollectionList = ({
|
export const CollectionList = ({
|
||||||
setting,
|
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({
|
export const includeListTitle = style({
|
||||||
marginTop: 8,
|
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
color: 'var(--affine-text-secondary-color)',
|
color: 'var(--affine-text-secondary-color)',
|
||||||
paddingLeft: 18,
|
padding: '4px 16px',
|
||||||
|
borderTop: '1px solid var(--affine-border-color)',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const rulesContainerRight = style({
|
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-list';
|
||||||
export * from './collection-operations';
|
export * from './collection-operations';
|
||||||
export * from './create-collection';
|
export * from './create-collection';
|
||||||
export * from './edit-collection';
|
export * from './edit-collection/edit-collection';
|
||||||
export * from './use-edit-collection';
|
export * from './use-edit-collection';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
type AllPageListConfig,
|
type AllPageListConfig,
|
||||||
CreateCollectionModal,
|
CreateCollectionModal,
|
||||||
EditCollectionModal,
|
EditCollectionModal,
|
||||||
|
type EditCollectionMode,
|
||||||
} from '@affine/component/page-list';
|
} from '@affine/component/page-list';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
@@ -9,6 +10,7 @@ import { useCallback, useState } from 'react';
|
|||||||
export const useEditCollection = (config: AllPageListConfig) => {
|
export const useEditCollection = (config: AllPageListConfig) => {
|
||||||
const [data, setData] = useState<{
|
const [data, setData] = useState<{
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
|
mode?: 'page' | 'rule';
|
||||||
onConfirm: (collection: Collection) => Promise<void>;
|
onConfirm: (collection: Collection) => Promise<void>;
|
||||||
}>();
|
}>();
|
||||||
const close = useCallback(() => setData(undefined), []);
|
const close = useCallback(() => setData(undefined), []);
|
||||||
@@ -19,14 +21,19 @@ export const useEditCollection = (config: AllPageListConfig) => {
|
|||||||
allPageListConfig={config}
|
allPageListConfig={config}
|
||||||
init={data.collection}
|
init={data.collection}
|
||||||
open={!!data}
|
open={!!data}
|
||||||
|
mode={data.mode}
|
||||||
onOpenChange={close}
|
onOpenChange={close}
|
||||||
onConfirm={data.onConfirm}
|
onConfirm={data.onConfirm}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
open: (collection: Collection): Promise<Collection> =>
|
open: (
|
||||||
|
collection: Collection,
|
||||||
|
mode?: EditCollectionMode
|
||||||
|
): Promise<Collection> =>
|
||||||
new Promise<Collection>(res => {
|
new Promise<Collection>(res => {
|
||||||
setData({
|
setData({
|
||||||
collection,
|
collection,
|
||||||
|
mode,
|
||||||
onConfirm: async collection => {
|
onConfirm: async collection => {
|
||||||
res(collection);
|
res(collection);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -116,10 +116,8 @@ export const pageCollectionBaseAtom =
|
|||||||
return {
|
return {
|
||||||
id: v.id,
|
id: v.id,
|
||||||
name: v.name,
|
name: v.name,
|
||||||
mode: 'rule',
|
|
||||||
filterList: v.filterList,
|
filterList: v.filterList,
|
||||||
allowList: v.allowList ?? [],
|
allowList: v.allowList ?? [],
|
||||||
pages: [],
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -140,10 +138,8 @@ export const pageCollectionBaseAtom =
|
|||||||
return {
|
return {
|
||||||
id: v.id,
|
id: v.id,
|
||||||
name: v.name,
|
name: v.name,
|
||||||
mode: 'rule',
|
|
||||||
filterList: v.filterList,
|
filterList: v.filterList,
|
||||||
allowList: v.allowList ?? [],
|
allowList: v.allowList ?? [],
|
||||||
pages: [],
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AnimatedCollectionsIcon } from '@affine/component';
|
import { AnimatedCollectionsIcon, toast } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
MenuItem as SidebarMenuItem,
|
MenuItem as SidebarMenuItem,
|
||||||
MenuLinkItem as SidebarMenuLinkItem,
|
MenuLinkItem as SidebarMenuLinkItem,
|
||||||
@@ -54,10 +54,17 @@ const CollectionRenderer = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [collapsed, setCollapsed] = useState(true);
|
const [collapsed, setCollapsed] = useState(true);
|
||||||
const setting = useCollectionManager(collectionsCRUDAtom);
|
const setting = useCollectionManager(collectionsCRUDAtom);
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
const { setNodeRef, isOver } = useDroppable({
|
const { setNodeRef, isOver } = useDroppable({
|
||||||
id: `${Collections_DROP_AREA_PREFIX}${collection.id}`,
|
id: `${Collections_DROP_AREA_PREFIX}${collection.id}`,
|
||||||
data: {
|
data: {
|
||||||
addToCollection: (id: string) => {
|
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 => {
|
setting.addPage(collection.id, id).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,10 +36,8 @@ const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
|
|||||||
const setting = useCollectionManager(collectionsCRUDAtom);
|
const setting = useCollectionManager(collectionsCRUDAtom);
|
||||||
const saveToCollection = useCallback(
|
const saveToCollection = useCallback(
|
||||||
async (collection: Collection) => {
|
async (collection: Collection) => {
|
||||||
console.log(setting.currentCollection.filterList);
|
|
||||||
await setting.createCollection({
|
await setting.createCollection({
|
||||||
...collection,
|
...collection,
|
||||||
mode: 'rule',
|
|
||||||
filterList: setting.currentCollection.filterList,
|
filterList: setting.currentCollection.filterList,
|
||||||
});
|
});
|
||||||
navigateHelper.jumpToCollection(workspaceId, collection.id);
|
navigateHelper.jumpToCollection(workspaceId, collection.id);
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
|
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
|
||||||
const { node, open } = useEditCollection(useAllPageListConfig());
|
const { node, open } = useEditCollection(useAllPageListConfig());
|
||||||
const openPageEdit = useCallback(() => {
|
const openPageEdit = useCallback(() => {
|
||||||
open({ ...collection, mode: 'page' }).then(updateCollection);
|
open({ ...collection }, 'page').then(updateCollection);
|
||||||
}, [open, collection, updateCollection]);
|
}, [open, collection, updateCollection]);
|
||||||
const openRuleEdit = useCallback(() => {
|
const openRuleEdit = useCallback(() => {
|
||||||
open({ ...collection, mode: 'rule' }).then(updateCollection);
|
open({ ...collection }, 'rule').then(updateCollection);
|
||||||
}, [collection, open, updateCollection]);
|
}, [collection, open, updateCollection]);
|
||||||
const [showTips, setShowTips] = useState(false);
|
const [showTips, setShowTips] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -277,9 +277,6 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
|||||||
|
|
||||||
const isEmpty = (collection: Collection) => {
|
const isEmpty = (collection: Collection) => {
|
||||||
return (
|
return (
|
||||||
(collection.mode === 'page' && collection.pages.length === 0) ||
|
collection.allowList.length === 0 && collection.filterList.length === 0
|
||||||
(collection.mode === 'rule' &&
|
|
||||||
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 type { PageMeta } from '@blocksuite/store';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -15,7 +19,8 @@ export const useFilteredPageMetas = (
|
|||||||
) => {
|
) => {
|
||||||
const { isPreferredEdgeless } = usePageHelper(workspace);
|
const { isPreferredEdgeless } = usePageHelper(workspace);
|
||||||
const pageMode = useAtomValue(allPageModeSelectAtom);
|
const pageMode = useAtomValue(allPageModeSelectAtom);
|
||||||
const { currentCollection } = useCollectionManager(collectionsCRUDAtom);
|
const { currentCollection, isDefault } =
|
||||||
|
useCollectionManager(collectionsCRUDAtom);
|
||||||
|
|
||||||
const filteredPageMetas = useMemo(
|
const filteredPageMetas = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -43,9 +48,22 @@ export const useFilteredPageMetas = (
|
|||||||
if (!currentCollection) {
|
if (!currentCollection) {
|
||||||
return true;
|
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;
|
return filteredPageMetas;
|
||||||
|
|||||||
@@ -95,16 +95,11 @@ export class WorkspaceSetting {
|
|||||||
|
|
||||||
deletePagesFromCollection(collection: Collection, idSet: Set<string>) {
|
deletePagesFromCollection(collection: Collection, idSet: Set<string>) {
|
||||||
const newAllowList = collection.allowList.filter(id => !idSet.has(id));
|
const newAllowList = collection.allowList.filter(id => !idSet.has(id));
|
||||||
const newPages = collection.pages.filter(id => !idSet.has(id));
|
if (newAllowList.length !== collection.allowList.length) {
|
||||||
if (
|
|
||||||
newAllowList.length !== collection.allowList.length ||
|
|
||||||
newPages.length !== collection.pages.length
|
|
||||||
) {
|
|
||||||
this.updateCollection(collection.id, old => {
|
this.updateCollection(collection.id, old => {
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
allowList: newAllowList,
|
allowList: newAllowList,
|
||||||
pages: newPages,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,20 +407,25 @@
|
|||||||
"com.affine.editCollectionName.name": "Name",
|
"com.affine.editCollectionName.name": "Name",
|
||||||
"com.affine.editCollectionName.name.placeholder": "Collection 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.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.page": "Page",
|
||||||
"com.affine.editCollection.rules.include.is": "is",
|
"com.affine.editCollection.rules.include.is": "is",
|
||||||
"com.affine.editCollection.rules.include.add": "Add include page",
|
"com.affine.editCollection.rules.include.add": "Add selected page",
|
||||||
"com.affine.editCollection.rules.include.tipsTitle": "What is \"Include\"?",
|
"com.affine.editCollection.rules.include.tipsTitle": "What is \"Selected pages\"?",
|
||||||
"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.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.preview": "Preview",
|
||||||
"com.affine.editCollection.rules.reset": "Reset",
|
"com.affine.editCollection.rules.reset": "Reset",
|
||||||
"com.affine.editCollection.rules.countTips.zero": "Showing <1>{{count}}</1> pages.",
|
"com.affine.editCollection.rules.countTips": "Selected <1>{{selectedCount}}</1>, filtered <3>{{filteredCount}}</3>",
|
||||||
"com.affine.editCollection.rules.countTips.one": "Showing <1>{{count}}</1> page.",
|
"com.affine.editCollection.rules.empty.noRules": "No Rules",
|
||||||
"com.affine.editCollection.rules.countTips.more": "Showing <1>{{count}}</1> pages.",
|
"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.title": "Add include page",
|
||||||
"com.affine.selectPage.selected": "Selected",
|
"com.affine.selectPage.selected": "Selected",
|
||||||
"com.affine.selectPage.empty": "Empty",
|
"com.affine.selectPage.empty": "Empty",
|
||||||
"com.affine.selectPage.empty.tips": "No page titles contain <1>{{search}}</1>",
|
"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",
|
"Confirm": "Confirm",
|
||||||
"Connector": "Connector",
|
"Connector": "Connector",
|
||||||
"Continue with Google": "Continue with Google",
|
"Continue with Google": "Continue with Google",
|
||||||
|
|||||||
Reference in New Issue
Block a user