From 7068d5f38a5d4b3b2343a08dd93188701966e5f6 Mon Sep 17 00:00:00 2001 From: 3720 Date: Thu, 2 Nov 2023 18:47:43 +0800 Subject: [PATCH] feat(core): remove `mode` and `pages` field from Collection (#4817) --- packages/common/env/src/filter.ts | 3 - .../page-list/use-collection-manager.ts | 13 +- .../page-list/view/collection-bar.tsx | 5 +- .../page-list/view/collection-list.tsx | 5 +- .../page-list/view/edit-collection.tsx | 934 ------------------ .../edit-collection.css.ts | 4 +- .../view/edit-collection/edit-collection.tsx | 199 ++++ .../page-list/view/edit-collection/hooks.tsx | 106 ++ .../view/edit-collection/pages-mode.tsx | 154 +++ .../view/edit-collection/rules-mode.tsx | 370 +++++++ .../view/edit-collection/select-page.tsx | 190 ++++ .../src/components/page-list/view/index.ts | 2 +- .../page-list/view/use-edit-collection.tsx | 9 +- .../frontend/core/src/atoms/collections.ts | 4 - .../collections/collections-list.tsx | 9 +- .../core/src/components/workspace-header.tsx | 2 - .../core/src/pages/workspace/collection.tsx | 9 +- .../core/src/pages/workspace/pages.tsx | 26 +- .../core/src/utils/workspace-setting.ts | 7 +- packages/frontend/i18n/src/resources/en.json | 17 +- 20 files changed, 1085 insertions(+), 983 deletions(-) delete mode 100644 packages/frontend/component/src/components/page-list/view/edit-collection.tsx rename packages/frontend/component/src/components/page-list/view/{ => edit-collection}/edit-collection.css.ts (98%) create mode 100644 packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.tsx create mode 100644 packages/frontend/component/src/components/page-list/view/edit-collection/hooks.tsx create mode 100644 packages/frontend/component/src/components/page-list/view/edit-collection/pages-mode.tsx create mode 100644 packages/frontend/component/src/components/page-list/view/edit-collection/rules-mode.tsx create mode 100644 packages/frontend/component/src/components/page-list/view/edit-collection/select-page.tsx diff --git a/packages/common/env/src/filter.ts b/packages/common/env/src/filter.ts index 2368938a4e..948d9beda7 100644 --- a/packages/common/env/src/filter.ts +++ b/packages/common/env/src/filter.ts @@ -51,11 +51,8 @@ export type Filter = z.input; 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(), diff --git a/packages/frontend/component/src/components/page-list/use-collection-manager.ts b/packages/frontend/component/src/components/page-list/use-collection-manager.ts index 6586bdb58e..4e3cb13273 100644 --- a/packages/frontend/component/src/components/page-list/use-collection-manager.ts +++ b/packages/frontend/component/src/components/page-list/use-collection-manager.ts @@ -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(defaultCollection); export const currentCollectionAtom = atomWithReset(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); }; diff --git a/packages/frontend/component/src/components/page-list/view/collection-bar.tsx b/packages/frontend/component/src/components/page-list/view/collection-bar.tsx index d7f0c6434f..453dafa6ae 100644 --- a/packages/frontend/component/src/components/page-list/view/collection-bar.tsx +++ b/packages/frontend/component/src/components/page-list/view/collection-bar.tsx @@ -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 { diff --git a/packages/frontend/component/src/components/page-list/view/collection-list.tsx b/packages/frontend/component/src/components/page-list/view/collection-list.tsx index eefae9aba5..0c6f949aba 100644 --- a/packages/frontend/component/src/components/page-list/view/collection-list.tsx +++ b/packages/frontend/component/src/components/page-list/view/collection-list.tsx @@ -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, diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection.tsx b/packages/frontend/component/src/components/page-list/view/edit-collection.tsx deleted file mode 100644 index 0d3ab75311..0000000000 --- a/packages/frontend/component/src/components/page-list/view/edit-collection.tsx +++ /dev/null @@ -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; - 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 ( - - {init ? ( - - ) : null} - - ); -}; - -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(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( - () => ( - <> - - - - ), - [onCancel, t, isNameEmpty, onSaveCollection, onConfirmText] - ); - - return ( -
- {value.mode === 'page' ? ( - - ) : ( - - )} -
- ); -}; - -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*/} - requestAnimationFrame(() => e.target.blur())} - /> -
- - Pages that meet the rules will be added to the current collection{' '} - highlight. - -
-
-
-
- { - updateCollection({ - ...collection, - mode, - }); - }, - [collection, updateCollection] - )} - > - - {t['com.affine.editCollection.pages']()} - - - {t['com.affine.editCollection.rules']()} - - -
-
-
- updateCollection({ ...collection, filterList }), - [collection, updateCollection] - )} - /> -
-
- setExpandInclude(!expandInclude)} - className={styles.button} - width={24} - height={24} - style={{ - transform: expandInclude ? 'rotate(90deg)' : undefined, - }} - > -
- include -
-
-
- {collection.allowList.map(id => { - const page = allPageListConfig.allPages.find( - v => v.id === id - ); - return ( -
-
-
- {allPageListConfig.isEdgeless(id) ? ( - - ) : ( - - )} - {t[ - 'com.affine.editCollection.rules.include.page' - ]()} -
-
- {t['com.affine.editCollection.rules.include.is']()} -
-
- {page?.title || t['Untitled']()} -
-
- { - updateCollection({ - ...collection, - allowList: collection.allowList.filter( - v => v !== id - ), - }); - }} - > -
- ); - })} -
- -
- {t['com.affine.editCollection.rules.include.add']()} -
-
-
-
-
- {showTips ? ( -
-
-
{t['com.affine.collection.helpInfo']()}
- -
-
- {t['com.affine.editCollection.rules.include.tipsTitle']()} -
-
{t['com.affine.editCollection.rules.include.tips']()}
-
- ) : null} -
-
- - {rulesPages.length > 0 ? ( - - ) : null} - {allowListPages.length > 0 ? ( -
-
include
- -
- ) : null} -
-
-
-
-
{ - setShowPreview(!showPreview); - }} - > - {t['com.affine.editCollection.rules.preview']()} -
-
- {t['com.affine.editCollection.rules.reset']()} -
-
- - After searching, there are currently - count - pages. - -
-
-
{buttons}
-
- {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 ( - <> - updateSearchText(e.target.value)} - className={styles.rulesTitle} - style={{ - color: 'var(--affine-text-primary-color)', - }} - placeholder={t['com.affine.editCollection.search.placeholder']()} - > -
-
-
- { - updateCollection({ - ...collection, - mode, - }); - }, - [collection, updateCollection] - )} - > - - {t['com.affine.editCollection.pages']()} - - - {t['com.affine.editCollection.rules']()} - - - {!showFilter && filters.length === 0 ? ( - - } - > -
- -
-
- ) : ( - - )} -
- {showFilter ? ( -
- -
- ) : null} - {searchedList.length ? ( - - { - updateCollection({ - ...collection, - pages: ids, - }); - }} - pageOperationsRenderer={pageOperationsRenderer} - selectedPageIds={collection.pages} - isPreferredEdgeless={allPageListConfig.isEdgeless} - > - - ) : ( - - )} -
-
-
-
-
- {t['com.affine.selectPage.selected']()} - - {collection.pages.length} - -
-
- {t['com.affine.editCollection.pages.clear']()} -
-
-
{buttons}
-
- - ); -}; -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 ( -
- updateSearchText(e.target.value)} - placeholder={t['com.affine.editCollection.search.placeholder']()} - > -
-
-
- {t['com.affine.selectPage.title']()} -
- {!showFilter && filters.length === 0 ? ( - - } - > -
- -
-
- ) : ( - - )} -
- {showFilter ? ( -
- -
- ) : null} - {searchedList.length ? ( - - - - ) : ( - - )} -
-
-
-
- {t['com.affine.selectPage.selected']()} - - {value.length} - -
-
- {t['com.affine.editCollection.pages.clear']()} -
-
-
- - -
-
-
- ); -}; -const useSelectPage = ({ - allPageListConfig, -}: { - allPageListConfig: AllPageListConfig; -}) => { - const [value, onChange] = useState<{ - init: string[]; - onConfirm: (ids: string[]) => void; - }>(); - const close = useCallback(() => { - onChange(undefined); - }, []); - return { - node: ( - - {value ? ( - - ) : null} - - ), - open: (init: string[]): Promise => - new Promise(res => { - onChange({ - init, - onConfirm: list => { - close(); - res(list); - }, - }); - }), - }; -}; -const useFilter = (list: PageMeta[]) => { - const [filters, changeFilters] = useState([]); - 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 ( -
- -
- {t['com.affine.selectPage.empty']()} -
- {search ? ( -
- - No page titles contain - - search - - -
- ) : null} -
- ); -}; diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection.css.ts b/packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.css.ts similarity index 98% rename from packages/frontend/component/src/components/page-list/view/edit-collection.css.ts rename to packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.css.ts index 01e8e3a8c7..6ad2253613 100644 --- a/packages/frontend/component/src/components/page-list/view/edit-collection.css.ts +++ b/packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.css.ts @@ -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({ diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.tsx b/packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.tsx new file mode 100644 index 0000000000..2b3e5a95be --- /dev/null +++ b/packages/frontend/component/src/components/page-list/view/edit-collection/edit-collection.tsx @@ -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; + 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 ( + + {init ? ( + + ) : null} + + ); +}; + +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(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( + () => ( + <> + + + + ), + [onCancel, t, isNameEmpty, onSaveCollection, onConfirmText] + ); + const switchMode = useMemo( + () => ( + { + setMode(mode); + }} + > + + {t['com.affine.editCollection.pages']()} + + + {t['com.affine.editCollection.rules']()} + + + ), + [mode, t] + ); + return ( +
+ {mode === 'page' ? ( + + ) : ( + + )} +
+ ); +}; + +export type AllPageListConfig = { + allPages: PageMeta[]; + workspace: Workspace; + isEdgeless: (id: string) => boolean; + getPage: (id: string) => PageMeta | undefined; + favoriteRender: (page: PageMeta) => ReactNode; +}; diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/hooks.tsx b/packages/frontend/component/src/components/page-list/view/edit-collection/hooks.tsx new file mode 100644 index 0000000000..55ba854682 --- /dev/null +++ b/packages/frontend/component/src/components/page-list/view/edit-collection/hooks.tsx @@ -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: ( + + {value ? ( + + ) : null} + + ), + open: (init: string[]): Promise => + new Promise(res => { + onChange({ + init, + onConfirm: list => { + close(); + res(list); + }, + }); + }), + }; +}; +export const useFilter = (list: PageMeta[]) => { + const [filters, changeFilters] = useState([]); + 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, + }; +}; diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/pages-mode.tsx b/packages/frontend/component/src/components/page-list/view/edit-collection/pages-mode.tsx new file mode 100644 index 0000000000..5ae9a96687 --- /dev/null +++ b/packages/frontend/component/src/components/page-list/view/edit-collection/pages-mode.tsx @@ -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 ( + <> + updateSearchText(e.target.value)} + className={styles.rulesTitle} + style={{ + color: 'var(--affine-text-primary-color)', + }} + placeholder={t['com.affine.editCollection.search.placeholder']()} + > +
+
+
+ {switchMode} + {!showFilter && filters.length === 0 ? ( + + } + > +
+ +
+
+ ) : ( + + )} +
+ {showFilter ? ( +
+ +
+ ) : null} + {searchedList.length ? ( + + { + updateCollection({ + ...collection, + allowList: ids, + }); + }} + pageOperationsRenderer={pageOperationsRenderer} + selectedPageIds={collection.allowList} + isPreferredEdgeless={allPageListConfig.isEdgeless} + > + + ) : ( + + )} +
+
+
+
+
+ {t['com.affine.selectPage.selected']()} + + {collection.allowList.length} + +
+
+ {t['com.affine.editCollection.pages.clear']()} +
+
+
{buttons}
+
+ + ); +}; diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/rules-mode.tsx b/packages/frontend/component/src/components/page-list/view/edit-collection/rules-mode.tsx new file mode 100644 index 0000000000..cdda6769b4 --- /dev/null +++ b/packages/frontend/component/src/components/page-list/view/edit-collection/rules-mode.tsx @@ -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*/} + requestAnimationFrame(() => e.target.blur())} + /> +
+ + Pages that meet the rules will be added to the current collection{' '} + highlight. + +
+
+
+
{switchMode}
+
+
+ updateCollection({ ...collection, filterList }), + [collection, updateCollection] + )} + /> +
+
+ setExpandInclude(!expandInclude)} + className={styles.button} + width={24} + height={24} + style={{ + transform: expandInclude ? 'rotate(90deg)' : undefined, + }} + > +
+ {t['com.affine.editCollection.rules.include.title']()} +
+
+
+ {collection.allowList.map(id => { + const page = allPageListConfig.allPages.find( + v => v.id === id + ); + return ( +
+
+
+ {allPageListConfig.isEdgeless(id) ? ( + + ) : ( + + )} + {t[ + 'com.affine.editCollection.rules.include.page' + ]()} +
+
+ {t['com.affine.editCollection.rules.include.is']()} +
+
+ {page?.title || t['Untitled']()} +
+
+ { + updateCollection({ + ...collection, + allowList: collection.allowList.filter( + v => v !== id + ), + }); + }} + > +
+ ); + })} +
+ +
+ {t['com.affine.editCollection.rules.include.add']()} +
+
+
+
+
+ {showTips ? ( +
+
+
{t['com.affine.collection.helpInfo']()}
+ +
+
+ {t['com.affine.editCollection.rules.include.tipsTitle']()} +
+
{t['com.affine.editCollection.rules.include.tips']()}
+
+ ) : null} +
+
+ + {rulesPages.length > 0 ? ( + + ) : ( + + )} + {allowListPages.length > 0 ? ( +
+
+ {t['com.affine.editCollection.rules.include.title']()} +
+ +
+ ) : null} +
+
+
+
+
{ + setShowPreview(!showPreview); + }} + > + {t['com.affine.editCollection.rules.preview']()} +
+
+ {t['com.affine.editCollection.rules.reset']()} +
+
+ + Selected + count, + filtered + count + +
+
+
{buttons}
+
+ {selectPageNode} + + ); +}; +const RulesEmpty = ({ + noRules, + fullHeight, +}: { + noRules: boolean; + fullHeight: boolean; +}) => { + const t = useAFFiNEI18N(); + return ( +
+ + + {noRules + ? t['com.affine.editCollection.rules.empty.noRules']() + : t['com.affine.editCollection.rules.empty.noResults']()} + +
+ {noRules ? ( + + Please add rules to save this collection or switch + to Pages, use manual selection mode + + ) : ( + t['com.affine.editCollection.rules.empty.noResults.tips']() + )} +
+
+ ); +}; diff --git a/packages/frontend/component/src/components/page-list/view/edit-collection/select-page.tsx b/packages/frontend/component/src/components/page-list/view/edit-collection/select-page.tsx new file mode 100644 index 0000000000..e69e5cf3c6 --- /dev/null +++ b/packages/frontend/component/src/components/page-list/view/edit-collection/select-page.tsx @@ -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 ( +
+ updateSearchText(e.target.value)} + placeholder={t['com.affine.editCollection.search.placeholder']()} + > +
+
+
+ {t['com.affine.selectPage.title']()} +
+ {!showFilter && filters.length === 0 ? ( + + } + > +
+ +
+
+ ) : ( + + )} +
+ {showFilter ? ( +
+ +
+ ) : null} + {searchedList.length ? ( + + + + ) : ( + + )} +
+
+
+
+ {t['com.affine.selectPage.selected']()} + + {value.length} + +
+
+ {t['com.affine.editCollection.pages.clear']()} +
+
+
+ + +
+
+
+ ); +}; +export const EmptyList = ({ search }: { search?: string }) => { + const t = useAFFiNEI18N(); + return ( +
+ +
+ {t['com.affine.selectPage.empty']()} +
+ {search ? ( +
+ + No page titles contain + + search + + +
+ ) : null} +
+ ); +}; diff --git a/packages/frontend/component/src/components/page-list/view/index.ts b/packages/frontend/component/src/components/page-list/view/index.ts index 98cd08ad3a..8c130dca83 100644 --- a/packages/frontend/component/src/components/page-list/view/index.ts +++ b/packages/frontend/component/src/components/page-list/view/index.ts @@ -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'; diff --git a/packages/frontend/component/src/components/page-list/view/use-edit-collection.tsx b/packages/frontend/component/src/components/page-list/view/use-edit-collection.tsx index da331adfc0..901ec58beb 100644 --- a/packages/frontend/component/src/components/page-list/view/use-edit-collection.tsx +++ b/packages/frontend/component/src/components/page-list/view/use-edit-collection.tsx @@ -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; }>(); 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 => + open: ( + collection: Collection, + mode?: EditCollectionMode + ): Promise => new Promise(res => { setData({ collection, + mode, onConfirm: async collection => { res(collection); }, diff --git a/packages/frontend/core/src/atoms/collections.ts b/packages/frontend/core/src/atoms/collections.ts index d6564073c0..af63fb76c9 100644 --- a/packages/frontend/core/src/atoms/collections.ts +++ b/packages/frontend/core/src/atoms/collections.ts @@ -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: [], }; }); } diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx index abf05cb0a3..50941ba37b 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/collections/collections-list.tsx @@ -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); }); diff --git a/packages/frontend/core/src/components/workspace-header.tsx b/packages/frontend/core/src/components/workspace-header.tsx index 8b0254be1f..de7e5c3eb1 100644 --- a/packages/frontend/core/src/components/workspace-header.tsx +++ b/packages/frontend/core/src/components/workspace-header.tsx @@ -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); diff --git a/packages/frontend/core/src/pages/workspace/collection.tsx b/packages/frontend/core/src/pages/workspace/collection.tsx index e148cf8a1e..5f37de1ed8 100644 --- a/packages/frontend/core/src/pages/workspace/collection.tsx +++ b/packages/frontend/core/src/pages/workspace/collection.tsx @@ -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 ); }; diff --git a/packages/frontend/core/src/pages/workspace/pages.tsx b/packages/frontend/core/src/pages/workspace/pages.tsx index 31edb9454e..13938a21d1 100644 --- a/packages/frontend/core/src/pages/workspace/pages.tsx +++ b/packages/frontend/core/src/pages/workspace/pages.tsx @@ -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; diff --git a/packages/frontend/core/src/utils/workspace-setting.ts b/packages/frontend/core/src/utils/workspace-setting.ts index 22aba87d3e..18d0344588 100644 --- a/packages/frontend/core/src/utils/workspace-setting.ts +++ b/packages/frontend/core/src/utils/workspace-setting.ts @@ -95,16 +95,11 @@ export class WorkspaceSetting { deletePagesFromCollection(collection: Collection, idSet: Set) { 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, }; }); } diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 0d8b1592d2..a8a5328ee1 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -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}} pages.", - "com.affine.editCollection.rules.countTips.one": "Showing <1>{{count}} page.", - "com.affine.editCollection.rules.countTips.more": "Showing <1>{{count}} pages.", + "com.affine.editCollection.rules.countTips": "Selected <1>{{selectedCount}}, filtered <3>{{filteredCount}}", + "com.affine.editCollection.rules.empty.noRules": "No Rules", + "com.affine.editCollection.rules.empty.noRules.tips": "Please <1>add rules to save this collection or switch to <3>Pages, 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}}", + "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",