refactor(core): remove collection atom (#5832)

This commit is contained in:
EYHN 2024-02-27 03:50:56 +00:00
parent ad9b0303c4
commit 5cd488fe1d
No known key found for this signature in database
GPG Key ID: 46C9E26A75AB276C
41 changed files with 644 additions and 962 deletions

View File

@ -48,9 +48,6 @@ export const recentPageIdsBaseAtom = atomWithStorage<string[]>(
[] []
); );
export type PageModeOption = 'all' | 'page' | 'edgeless';
export const allPageModeSelectAtom = atom<PageModeOption>('all');
export type AllPageFilterOption = 'docs' | 'collections' | 'tags'; export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
export const allPageFilterSelectAtom = atom<AllPageFilterOption>('docs'); export const allPageFilterSelectAtom = atom<AllPageFilterOption>('docs');

View File

@ -5,11 +5,7 @@ import type { Workspace } from '@blocksuite/store';
import { registerAffineCommand } from '@toeverything/infra/command'; import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai'; import type { createStore } from 'jotai';
import { import { openSettingModalAtom, openWorkspaceListModalAtom } from '../atoms';
openSettingModalAtom,
openWorkspaceListModalAtom,
type PageModeOption,
} from '../atoms';
import type { useNavigateHelper } from '../hooks/use-navigate-helper'; import type { useNavigateHelper } from '../hooks/use-navigate-helper';
export function registerAffineNavigationCommands({ export function registerAffineNavigationCommands({
@ -17,12 +13,10 @@ export function registerAffineNavigationCommands({
store, store,
workspace, workspace,
navigationHelper, navigationHelper,
setPageListMode,
}: { }: {
t: ReturnType<typeof useAFFiNEI18N>; t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>; store: ReturnType<typeof createStore>;
navigationHelper: ReturnType<typeof useNavigateHelper>; navigationHelper: ReturnType<typeof useNavigateHelper>;
setPageListMode: React.Dispatch<React.SetStateAction<PageModeOption>>;
workspace: Workspace; workspace: Workspace;
}) { }) {
const unsubs: Array<() => void> = []; const unsubs: Array<() => void> = [];
@ -34,7 +28,6 @@ export function registerAffineNavigationCommands({
label: t['com.affine.cmdk.affine.navigation.goto-all-pages'](), label: t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
run() { run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
setPageListMode('all');
}, },
}) })
); );
@ -47,7 +40,6 @@ export function registerAffineNavigationCommands({
label: 'Go to Collection List', label: 'Go to Collection List',
run() { run() {
navigationHelper.jumpToCollections(workspace.id); navigationHelper.jumpToCollections(workspace.id);
setPageListMode('all');
}, },
}) })
); );
@ -60,7 +52,6 @@ export function registerAffineNavigationCommands({
label: 'Go to Tag List', label: 'Go to Tag List',
run() { run() {
navigationHelper.jumpToTags(workspace.id); navigationHelper.jumpToTags(workspace.id);
setPageListMode('all');
}, },
}) })
); );
@ -101,7 +92,6 @@ export function registerAffineNavigationCommands({
label: t['com.affine.cmdk.affine.navigation.goto-trash'](), label: t['com.affine.cmdk.affine.navigation.goto-trash'](),
run() { run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH); navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
setPageListMode('all');
}, },
}) })
); );

View File

@ -1,65 +0,0 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import type { CollectionService } from '@affine/core/modules/collection';
import type { Collection } from '@affine/env/filter';
import { renderHook } from '@testing-library/react';
import { LiveData } from '@toeverything/infra';
import { BehaviorSubject } from 'rxjs';
import { expect, test } from 'vitest';
import { createDefaultFilter, vars } from '../filter/vars';
import { useCollectionManager } from '../use-collection-manager';
const defaultMeta = { tags: { options: [] } };
const collectionsSubject = new BehaviorSubject<Collection[]>([]);
const mockWorkspaceCollectionService = {
collections: LiveData.from(collectionsSubject, []),
addCollection: (...collections) => {
const prev = collectionsSubject.value;
collectionsSubject.next([...collections, ...prev]);
},
deleteCollection: (...ids) => {
const prev = collectionsSubject.value;
collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
},
updateCollection: (id, updater) => {
const prev = collectionsSubject.value;
collectionsSubject.next(
prev.map(v => {
if (v.id === id) {
return updater(v);
}
return v;
})
);
},
} as CollectionService;
test('useAllPageSetting', async () => {
const settingHook = renderHook(() =>
useCollectionManager(mockWorkspaceCollectionService)
);
const prevCollection = settingHook.result.current.currentCollection;
expect(settingHook.result.current.savedCollections).toEqual([]);
settingHook.result.current.updateCollection({
...settingHook.result.current.currentCollection,
filterList: [createDefaultFilter(vars[0], defaultMeta)],
});
settingHook.rerender();
const nextCollection = settingHook.result.current.currentCollection;
expect(nextCollection).not.toBe(prevCollection);
expect(nextCollection.filterList).toEqual([
createDefaultFilter(vars[0], defaultMeta),
]);
settingHook.result.current.createCollection({
...settingHook.result.current.currentCollection,
id: '1',
});
settingHook.rerender();
expect(settingHook.result.current.savedCollections.length).toBe(1);
expect(settingHook.result.current.savedCollections[0].id).toBe('1');
});

View File

@ -18,19 +18,18 @@ import { CollectionOperationCell } from '../operation-cell';
import { CollectionListItemRenderer } from '../page-group'; import { CollectionListItemRenderer } from '../page-group';
import { ListTableHeader } from '../page-header'; import { ListTableHeader } from '../page-header';
import type { CollectionMeta, ItemListHandle, ListItem } from '../types'; import type { CollectionMeta, ItemListHandle, ListItem } from '../types';
import { useCollectionManager } from '../use-collection-manager';
import type { AllPageListConfig } from '../view'; import type { AllPageListConfig } from '../view';
import { VirtualizedList } from '../virtualized-list'; import { VirtualizedList } from '../virtualized-list';
import { CollectionListHeader } from './collection-list-header'; import { CollectionListHeader } from './collection-list-header';
const useCollectionOperationsRenderer = ({ const useCollectionOperationsRenderer = ({
info, info,
setting, service,
config, config,
}: { }: {
info: DeleteCollectionInfo; info: DeleteCollectionInfo;
config: AllPageListConfig; config: AllPageListConfig;
setting: ReturnType<typeof useCollectionManager>; service: CollectionService;
}) => { }) => {
const pageOperationsRenderer = useCallback( const pageOperationsRenderer = useCallback(
(collection: Collection) => { (collection: Collection) => {
@ -38,12 +37,12 @@ const useCollectionOperationsRenderer = ({
<CollectionOperationCell <CollectionOperationCell
info={info} info={info}
collection={collection} collection={collection}
setting={setting} service={service}
config={config} config={config}
/> />
); );
}, },
[config, info, setting] [config, info, service]
); );
return pageOperationsRenderer; return pageOperationsRenderer;
@ -69,13 +68,13 @@ export const VirtualizedCollectionList = ({
const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>( const [selectedCollectionIds, setSelectedCollectionIds] = useState<string[]>(
[] []
); );
const setting = useCollectionManager(useService(CollectionService)); const collectionService = useService(CollectionService);
const currentWorkspace = useService(Workspace); const currentWorkspace = useService(Workspace);
const info = useDeleteCollectionInfo(); const info = useDeleteCollectionInfo();
const collectionOperations = useCollectionOperationsRenderer({ const collectionOperations = useCollectionOperationsRenderer({
info, info,
setting, service: collectionService,
config, config,
}); });
@ -105,8 +104,8 @@ export const VirtualizedCollectionList = ({
}, []); }, []);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
return setting.deleteCollection(info, ...selectedCollectionIds); return collectionService.deleteCollection(info, ...selectedCollectionIds);
}, [setting, info, selectedCollectionIds]); }, [collectionService, info, selectedCollectionIds]);
return ( return (
<> <>

View File

@ -10,10 +10,7 @@ import { useCallback, useMemo } from 'react';
import { CollectionService } from '../../../modules/collection'; import { CollectionService } from '../../../modules/collection';
import { createTagFilter } from '../filter/utils'; import { createTagFilter } from '../filter/utils';
import { import { createEmptyCollection } from '../use-collection-manager';
createEmptyCollection,
useCollectionManager,
} from '../use-collection-manager';
import { tagColorMap } from '../utils'; import { tagColorMap } from '../utils';
import type { AllPageListConfig } from '../view/edit-collection/edit-collection'; import type { AllPageListConfig } from '../view/edit-collection/edit-collection';
import { import {
@ -23,38 +20,12 @@ import {
import * as styles from './page-list-header.css'; import * as styles from './page-list-header.css';
import { PageListNewPageButton } from './page-list-new-page-button'; import { PageListNewPageButton } from './page-list-new-page-button';
export const PageListHeader = ({ workspaceId }: { workspaceId: string }) => { export const PageListHeader = () => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const setting = useCollectionManager(useService(CollectionService));
const { jumpToCollections } = useNavigateHelper();
const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspaceId);
}, [jumpToCollections, workspaceId]);
const title = useMemo(() => { const title = useMemo(() => {
if (setting.isDefault) {
return t['com.affine.all-pages.header'](); return t['com.affine.all-pages.header']();
} }, [t]);
return (
<>
<div style={{ cursor: 'pointer' }} onClick={handleJumpToCollections}>
{t['com.affine.collections.header']()} /
</div>
<div className={styles.titleIcon}>
<ViewLayersIcon />
</div>
<div className={styles.titleCollectionName}>
{setting.currentCollection.name}
</div>
</>
);
}, [
handleJumpToCollections,
setting.currentCollection.name,
setting.isDefault,
t,
]);
return ( return (
<div className={styles.docListHeader}> <div className={styles.docListHeader}>
@ -75,22 +46,19 @@ export const CollectionPageListHeader = ({
workspaceId: string; workspaceId: string;
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const setting = useCollectionManager(useService(CollectionService));
const { jumpToCollections } = useNavigateHelper(); const { jumpToCollections } = useNavigateHelper();
const handleJumpToCollections = useCallback(() => { const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspaceId); jumpToCollections(workspaceId);
}, [jumpToCollections, workspaceId]); }, [jumpToCollections, workspaceId]);
const { updateCollection } = useCollectionManager( const collectionService = useService(CollectionService);
useService(CollectionService)
);
const { node, open } = useEditCollection(config); const { node, open } = useEditCollection(config);
const handleAddPage = useAsyncCallback(async () => { const handleAddPage = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'page'); const ret = await open({ ...collection }, 'page');
updateCollection(ret); collectionService.updateCollection(collection.id, () => ret);
}, [collection, open, updateCollection]); }, [collection, collectionService, open]);
return ( return (
<> <>
@ -103,9 +71,7 @@ export const CollectionPageListHeader = ({
<div className={styles.titleIcon}> <div className={styles.titleIcon}>
<ViewLayersIcon /> <ViewLayersIcon />
</div> </div>
<div className={styles.titleCollectionName}> <div className={styles.titleCollectionName}>{collection.name}</div>
{setting.currentCollection.name}
</div>
</div> </div>
<Button className={styles.addPageButton} onClick={handleAddPage}> <Button className={styles.addPageButton} onClick={handleAddPage}>
{t['com.affine.collection.addPages']()} {t['com.affine.collection.addPages']()}
@ -124,7 +90,7 @@ export const TagPageListHeader = ({
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const { jumpToTags, jumpToCollection } = useNavigateHelper(); const { jumpToTags, jumpToCollection } = useNavigateHelper();
const setting = useCollectionManager(useService(CollectionService)); const collectionService = useService(CollectionService);
const { open, node } = useEditCollectionName({ const { open, node } = useEditCollectionName({
title: t['com.affine.editCollection.saveCollection'](), title: t['com.affine.editCollection.saveCollection'](),
showTips: true, showTips: true,
@ -136,13 +102,13 @@ export const TagPageListHeader = ({
const saveToCollection = useCallback( const saveToCollection = useCallback(
(collection: Collection) => { (collection: Collection) => {
setting.createCollection({ collectionService.addCollection({
...collection, ...collection,
filterList: [createTagFilter(tag.id)], filterList: [createTagFilter(tag.id)],
}); });
jumpToCollection(workspaceId, collection.id); jumpToCollection(workspaceId, collection.id);
}, },
[setting, tag.id, jumpToCollection, workspaceId] [collectionService, tag.id, jumpToCollection, workspaceId]
); );
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
open('') open('')

View File

@ -2,7 +2,7 @@ import { toast } from '@affine/component';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper'; import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper'; import { useTrashModalHelper } from '@affine/core/hooks/affine/use-trash-modal-helper';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import type { Collection } from '@affine/env/filter'; import type { Collection, Filter } from '@affine/env/filter';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageMeta, Tag } from '@blocksuite/store'; import type { PageMeta, Tag } from '@blocksuite/store';
@ -81,15 +81,17 @@ const usePageOperationsRenderer = () => {
export const VirtualizedPageList = ({ export const VirtualizedPageList = ({
tag, tag,
collection, collection,
filters,
config, config,
listItem, listItem,
setHideHeaderCreateNewPage, setHideHeaderCreateNewPage,
}: { }: {
tag?: Tag; tag?: Tag;
collection?: Collection; collection?: Collection;
filters?: Filter[];
config?: AllPageListConfig; config?: AllPageListConfig;
listItem?: PageMeta[]; listItem?: PageMeta[];
setHideHeaderCreateNewPage: (hide: boolean) => void; setHideHeaderCreateNewPage?: (hide: boolean) => void;
}) => { }) => {
const listRef = useRef<ItemListHandle>(null); const listRef = useRef<ItemListHandle>(null);
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false); const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
@ -101,11 +103,10 @@ export const VirtualizedPageList = ({
currentWorkspace.blockSuiteWorkspace currentWorkspace.blockSuiteWorkspace
); );
const filteredPageMetas = useFilteredPageMetas( const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
'all', filters,
pageMetas, collection,
currentWorkspace });
);
const pageMetasToRender = useMemo(() => { const pageMetasToRender = useMemo(() => {
if (listItem) { if (listItem) {
return listItem; return listItem;
@ -151,7 +152,7 @@ export const VirtualizedPageList = ({
/> />
); );
} }
return <PageListHeader workspaceId={currentWorkspace.id} />; return <PageListHeader />;
}, [collection, config, currentWorkspace.id, tag]); }, [collection, config, currentWorkspace.id, tag]);
const { setTrashModal } = useTrashModalHelper( const { setTrashModal } = useTrashModalHelper(

View File

@ -23,10 +23,10 @@ import {
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import type { CollectionService } from '../../modules/collection';
import { FavoriteTag } from './components/favorite-tag'; import { FavoriteTag } from './components/favorite-tag';
import * as styles from './list.css'; import * as styles from './list.css';
import { DisablePublicSharing, MoveToTrash } from './operation-menu-items'; import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
import type { useCollectionManager } from './use-collection-manager';
import { ColWrapper, stopPropagationWithoutPrevent } from './utils'; import { ColWrapper, stopPropagationWithoutPrevent } from './utils';
import { import {
type AllPageListConfig, type AllPageListConfig,
@ -208,13 +208,13 @@ export interface CollectionOperationCellProps {
collection: Collection; collection: Collection;
info: DeleteCollectionInfo; info: DeleteCollectionInfo;
config: AllPageListConfig; config: AllPageListConfig;
setting: ReturnType<typeof useCollectionManager>; service: CollectionService;
} }
export const CollectionOperationCell = ({ export const CollectionOperationCell = ({
collection, collection,
config, config,
setting, service,
info, info,
}: CollectionOperationCellProps) => { }: CollectionOperationCellProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@ -231,26 +231,29 @@ export const CollectionOperationCell = ({
// use openRenameModal if it is in the sidebar collection list // use openRenameModal if it is in the sidebar collection list
openEditCollectionNameModal(collection.name) openEditCollectionNameModal(collection.name)
.then(name => { .then(name => {
return setting.updateCollection({ ...collection, name }); return service.updateCollection(collection.id, collection => ({
...collection,
name,
}));
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
}, [collection, openEditCollectionNameModal, setting]); }, [collection.id, collection.name, openEditCollectionNameModal, service]);
const handleEdit = useCallback(() => { const handleEdit = useCallback(() => {
openEditCollectionModal(collection) openEditCollectionModal(collection)
.then(collection => { .then(collection => {
return setting.updateCollection(collection); return service.updateCollection(collection.id, () => collection);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
}, [setting, collection, openEditCollectionModal]); }, [openEditCollectionModal, collection, service]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
return setting.deleteCollection(info, collection.id); return service.deleteCollection(info, collection.id);
}, [setting, info, collection]); }, [service, info, collection]);
return ( return (
<> <>

View File

@ -15,12 +15,10 @@ import { TagListHeader } from './tag-list-header';
export const VirtualizedTagList = ({ export const VirtualizedTagList = ({
tags, tags,
tagMetas, tagMetas,
setHideHeaderCreateNewTag,
onTagDelete, onTagDelete,
}: { }: {
tags: Tag[]; tags: Tag[];
tagMetas: TagMeta[]; tagMetas: TagMeta[];
setHideHeaderCreateNewTag: (hide: boolean) => void;
onTagDelete: (tagIds: string[]) => void; onTagDelete: (tagIds: string[]) => void;
}) => { }) => {
const listRef = useRef<ItemListHandle>(null); const listRef = useRef<ItemListHandle>(null);
@ -63,7 +61,6 @@ export const VirtualizedTagList = ({
draggable={false} draggable={false}
groupBy={false} groupBy={false}
atTopThreshold={80} atTopThreshold={80}
atTopStateChange={setHideHeaderCreateNewTag}
onSelectionActiveChange={setShowFloatingToolbar} onSelectionActiveChange={setShowFloatingToolbar}
heading={<TagListHeader />} heading={<TagListHeader />}
selectedIds={filteredSelectedTagIds} selectedIds={filteredSelectedTagIds}

View File

@ -1,11 +1,5 @@
import type { CollectionService } from '@affine/core/modules/collection';
import type { Collection, Filter, VariableMap } from '@affine/env/filter'; import type { Collection, Filter, VariableMap } from '@affine/env/filter';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { useLiveData } from '@toeverything/infra/livedata';
import { useAtom, useAtomValue } from 'jotai';
import { atomWithReset } from 'jotai/utils';
import { useCallback } from 'react';
import { NIL } from 'uuid';
import { evalFilterList } from './filter'; import { evalFilterList } from './filter';
@ -21,79 +15,7 @@ export const createEmptyCollection = (
...data, ...data,
}; };
}; };
const defaultCollection: Collection = createEmptyCollection(NIL, {
name: 'All',
});
const defaultCollectionAtom = atomWithReset<Collection>(defaultCollection);
export const currentCollectionAtom = atomWithReset<string>(NIL);
export type Updater<T> = (value: T) => T;
export type CollectionUpdater = Updater<Collection>;
export const useSavedCollections = (collectionService: CollectionService) => {
const addPage = useCallback(
(collectionId: string, pageId: string) => {
collectionService.updateCollection(collectionId, old => {
return {
...old,
allowList: [pageId, ...(old.allowList ?? [])],
};
});
},
[collectionService]
);
return {
collectionService,
addPage,
};
};
export const useCollectionManager = (collectionService: CollectionService) => {
const collections = useLiveData(collectionService.collections);
const { addPage } = useSavedCollections(collectionService);
const currentCollectionId = useAtomValue(currentCollectionAtom);
const [defaultCollection, updateDefaultCollection] = useAtom(
defaultCollectionAtom
);
const update = useCallback(
(collection: Collection) => {
if (collection.id === NIL) {
updateDefaultCollection(collection);
} else {
collectionService.updateCollection(collection.id, () => collection);
}
},
[updateDefaultCollection, collectionService]
);
const setTemporaryFilter = useCallback(
(filterList: Filter[]) => {
updateDefaultCollection({
...defaultCollection,
filterList: filterList,
});
},
[updateDefaultCollection, defaultCollection]
);
const currentCollection =
currentCollectionId === NIL
? defaultCollection
: collections.find(v => v.id === currentCollectionId) ??
defaultCollection;
return {
currentCollection: currentCollection,
savedCollections: collections,
isDefault: currentCollectionId === NIL,
// actions
createCollection: collectionService.addCollection.bind(collectionService),
updateCollection: update,
deleteCollection:
collectionService.deleteCollection.bind(collectionService),
addPage,
setTemporaryFilter,
};
};
export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) => export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
evalFilterList(filterList, varMap); evalFilterList(filterList, varMap);

View File

@ -1,77 +1,55 @@
import { allPageModeSelectAtom } from '@affine/core/atoms'; import type { Collection, Filter } from '@affine/env/filter';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { usePublicPages } from '@affine/core/hooks/affine/use-is-shared-page';
import { CollectionService } from '@affine/core/modules/collection';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import type { Workspace } from '@toeverything/infra'; import type { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di';
import { useAtomValue } from 'jotai';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { import { usePublicPages } from '../../hooks/affine/use-is-shared-page';
filterPage, import { filterPage, filterPageByRules } from './use-collection-manager';
filterPageByRules,
useCollectionManager,
} from './use-collection-manager';
export const useFilteredPageMetas = ( export const useFilteredPageMetas = (
route: 'all' | 'trash', workspace: Workspace,
pageMetas: PageMeta[], pageMetas: PageMeta[],
workspace: Workspace options: {
trash?: boolean;
filters?: Filter[];
collection?: Collection;
} = {}
) => { ) => {
const { isPreferredEdgeless } = usePageHelper(workspace.blockSuiteWorkspace);
const pageMode = useAtomValue(allPageModeSelectAtom);
const { currentCollection, isDefault } = useCollectionManager(
useService(CollectionService)
);
const { getPublicMode } = usePublicPages(workspace); const { getPublicMode } = usePublicPages(workspace);
const filteredPageMetas = useMemo( const filteredPageMetas = useMemo(
() => () =>
pageMetas pageMetas.filter(pageMeta => {
.filter(pageMeta => { if (options.trash) {
if (pageMode === 'all') { if (!pageMeta.trash) {
return true;
}
if (pageMode === 'edgeless') {
return isPreferredEdgeless(pageMeta.id);
}
if (pageMode === 'page') {
return !isPreferredEdgeless(pageMeta.id);
}
console.error('unknown filter mode', pageMeta, pageMode);
return true;
})
.filter(pageMeta => {
if (
(route === 'trash' && !pageMeta.trash) ||
(route === 'all' && pageMeta.trash)
) {
return false; return false;
} }
if (!currentCollection) { } else if (pageMeta.trash) {
return true; return false;
} }
const pageData = { const pageData = {
meta: pageMeta, meta: pageMeta,
publicMode: getPublicMode(pageMeta.id), publicMode: getPublicMode(pageMeta.id),
}; };
return isDefault if (
? filterPageByRules( options.filters &&
currentCollection.filterList, !filterPageByRules(options.filters, [], pageData)
currentCollection.allowList, ) {
pageData return false;
) }
: filterPage(currentCollection, pageData);
if (options.collection && !filterPage(options.collection, pageData)) {
return false;
}
return true;
}), }),
[ [
currentCollection,
isDefault,
isPreferredEdgeless,
getPublicMode,
pageMetas, pageMetas,
pageMode, options.trash,
route, options.filters,
options.collection,
getPublicMode,
] ]
); );

View File

@ -1,27 +0,0 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const view = style({
display: 'flex',
alignItems: 'center',
gap: 10,
fontSize: 14,
fontWeight: 600,
height: '100%',
});
export const option = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 4,
cursor: 'pointer',
borderRadius: 4,
':hover': {
backgroundColor: cssVar('hoverColor'),
},
opacity: 0,
selectors: {
[`${view}:hover &`]: {
opacity: 1,
},
},
});

View File

@ -1,112 +0,0 @@
import { Button, Tooltip } from '@affine/component';
import type { CollectionService } from '@affine/core/modules/collection';
import type { DeleteCollectionInfo, PropertiesMeta } from '@affine/env/filter';
import type { GetPageInfoById } from '@affine/env/page-info';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ViewLayersIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import { useState } from 'react';
import { useCollectionManager } from '../use-collection-manager';
import * as styles from './collection-bar.css';
import {
type AllPageListConfig,
EditCollectionModal,
} from './edit-collection/edit-collection';
import { useActions } from './use-action';
interface CollectionBarProps {
getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta;
collectionService: CollectionService;
backToAll: () => void;
allPageListConfig: AllPageListConfig;
info: DeleteCollectionInfo;
}
export const CollectionBar = (props: CollectionBarProps) => {
const { collectionService } = props;
const t = useAFFiNEI18N();
const setting = useCollectionManager(collectionService);
const collection = setting.currentCollection;
const [open, setOpen] = useState(false);
const actions = useActions({
collection,
setting,
info: props.info,
openEdit: () => setOpen(true),
});
return !setting.isDefault ? (
<div
style={{
userSelect: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 20px',
}}
>
<div>
<div className={styles.view}>
<EditCollectionModal
allPageListConfig={props.allPageListConfig}
init={collection}
open={open}
onOpenChange={setOpen}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onConfirm={setting.updateCollection}
/>
<ViewLayersIcon
style={{
height: 20,
width: 20,
}}
/>
<Tooltip
content={setting.currentCollection.name}
rootOptions={{
delayDuration: 1500,
}}
>
<div
style={{
marginRight: 10,
maxWidth: 200,
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{setting.currentCollection.name}
</div>
</Tooltip>
{actions.map(action => {
return (
<Tooltip key={action.name} content={action.tooltip}>
<div
data-testid={`collection-bar-option-${action.name}`}
onClick={action.click}
className={clsx(styles.option, action.className)}
>
{action.icon}
</div>
</Tooltip>
);
})}
</div>
</div>
<div
style={{
display: 'flex',
justifyContent: 'end',
}}
>
<Button
style={{ border: 'none', position: 'static' }}
onClick={props.backToAll}
>
{t['com.affine.collectionBar.backToAll']()}
</Button>
</div>
</div>
) : null;
};

View File

@ -9,91 +9,28 @@ import type {
import type { PropertiesMeta } from '@affine/env/filter'; import type { PropertiesMeta } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FilterIcon } from '@blocksuite/icons'; import { FilterIcon } from '@blocksuite/icons';
import { useCallback, useState } from 'react';
import { CreateFilterMenu } from '../filter/vars'; import { CreateFilterMenu } from '../filter/vars';
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 { import { type AllPageListConfig } from './edit-collection/edit-collection';
type AllPageListConfig,
EditCollectionModal,
} from './edit-collection/edit-collection';
export const CollectionList = ({ export const CollectionPageListOperationsMenu = ({
setting, collection,
propertiesMeta,
allPageListConfig, allPageListConfig,
userInfo, userInfo,
disable,
}: { }: {
setting: ReturnType<typeof useCollectionManager>; collection: Collection;
propertiesMeta: PropertiesMeta;
allPageListConfig: AllPageListConfig; allPageListConfig: AllPageListConfig;
userInfo: DeleteCollectionInfo; userInfo: DeleteCollectionInfo;
disable?: boolean;
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [collection, setCollection] = useState<Collection>();
const onChange = useCallback(
(filterList: Filter[]) => {
setting.updateCollection({
...setting.currentCollection,
filterList,
});
},
[setting]
);
const closeUpdateCollectionModal = useCallback((open: boolean) => {
if (!open) {
setCollection(undefined);
}
}, []);
const onConfirm = useCallback(
(view: Collection) => {
setting.updateCollection(view);
closeUpdateCollectionModal(false);
},
[closeUpdateCollectionModal, setting]
);
return ( return (
<FlexWrapper alignItems="center"> <FlexWrapper alignItems="center">
{setting.isDefault ? (
<>
<Menu
items={
<CreateFilterMenu
propertiesMeta={propertiesMeta}
value={setting.currentCollection.filterList}
onChange={onChange}
/>
}
>
<Button
className={styles.filterMenuTrigger}
type="default"
icon={<FilterIcon />}
data-is-hidden={disable}
data-testid="create-first-filter"
>
{t['com.affine.filter']()}
</Button>
</Menu>
<EditCollectionModal
allPageListConfig={allPageListConfig}
init={collection}
open={!!collection}
onOpenChange={closeUpdateCollectionModal}
onConfirm={onConfirm}
/>
</>
) : (
<CollectionOperations <CollectionOperations
info={userInfo} info={userInfo}
collection={setting.currentCollection} collection={collection}
config={allPageListConfig} config={allPageListConfig}
setting={setting}
> >
<Button <Button
className={styles.filterMenuTrigger} className={styles.filterMenuTrigger}
@ -104,7 +41,41 @@ export const CollectionList = ({
{t['com.affine.filter']()} {t['com.affine.filter']()}
</Button> </Button>
</CollectionOperations> </CollectionOperations>
)} </FlexWrapper>
);
};
export const AllPageListOperationsMenu = ({
propertiesMeta,
filterList,
onChangeFilterList,
}: {
propertiesMeta: PropertiesMeta;
filterList: Filter[];
onChangeFilterList: (filterList: Filter[]) => void;
}) => {
const t = useAFFiNEI18N();
return (
<FlexWrapper alignItems="center">
<Menu
items={
<CreateFilterMenu
propertiesMeta={propertiesMeta}
value={filterList}
onChange={onChangeFilterList}
/>
}
>
<Button
className={styles.filterMenuTrigger}
type="default"
icon={<FilterIcon />}
data-testid="create-first-filter"
>
{t['com.affine.filter']()}
</Button>
</Menu>
</FlexWrapper> </FlexWrapper>
); );
}; };

View File

@ -7,6 +7,7 @@ import {
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DeleteIcon, EditIcon, FilterIcon } from '@blocksuite/icons'; import { DeleteIcon, EditIcon, FilterIcon } from '@blocksuite/icons';
import { useService } from '@toeverything/infra/di';
import { import {
type PropsWithChildren, type PropsWithChildren,
type ReactElement, type ReactElement,
@ -14,7 +15,7 @@ import {
useMemo, useMemo,
} from 'react'; } from 'react';
import type { useCollectionManager } from '../use-collection-manager'; import { CollectionService } from '../../../modules/collection';
import * as styles from './collection-operations.css'; import * as styles from './collection-operations.css';
import type { AllPageListConfig } from './index'; import type { AllPageListConfig } from './index';
import { import {
@ -25,7 +26,6 @@ import {
export const CollectionOperations = ({ export const CollectionOperations = ({
collection, collection,
config, config,
setting,
info, info,
openRenameModal, openRenameModal,
children, children,
@ -33,9 +33,9 @@ export const CollectionOperations = ({
info: DeleteCollectionInfo; info: DeleteCollectionInfo;
collection: Collection; collection: Collection;
config: AllPageListConfig; config: AllPageListConfig;
setting: ReturnType<typeof useCollectionManager>;
openRenameModal?: () => void; openRenameModal?: () => void;
}>) => { }>) => {
const service = useService(CollectionService);
const { open: openEditCollectionModal, node: editModal } = const { open: openEditCollectionModal, node: editModal } =
useEditCollection(config); useEditCollection(config);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@ -51,22 +51,25 @@ export const CollectionOperations = ({
} }
openEditCollectionNameModal(collection.name) openEditCollectionNameModal(collection.name)
.then(name => { .then(name => {
return setting.updateCollection({ ...collection, name }); return service.updateCollection(collection.id, () => ({
...collection,
name,
}));
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
}, [openRenameModal, openEditCollectionNameModal, collection, setting]); }, [openRenameModal, openEditCollectionNameModal, collection, service]);
const showEdit = useCallback(() => { const showEdit = useCallback(() => {
openEditCollectionModal(collection) openEditCollectionModal(collection)
.then(collection => { .then(collection => {
return setting.updateCollection(collection); return service.updateCollection(collection.id, () => collection);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
}, [setting, collection, openEditCollectionModal]); }, [openEditCollectionModal, collection, service]);
const actions = useMemo< const actions = useMemo<
Array< Array<
@ -112,12 +115,12 @@ export const CollectionOperations = ({
), ),
name: t['Delete'](), name: t['Delete'](),
click: () => { click: () => {
setting.deleteCollection(info, collection.id); service.deleteCollection(info, collection.id);
}, },
type: 'danger', type: 'danger',
}, },
], ],
[t, showEditName, showEdit, setting, info, collection.id] [t, showEditName, showEdit, service, info, collection.id]
); );
return ( return (
<> <>

View File

@ -1,5 +1,4 @@
export * from './affine-shape'; export * from './affine-shape';
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';

View File

@ -1,10 +1,8 @@
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { DeleteIcon, FilterIcon } from '@blocksuite/icons'; import { DeleteIcon, FilterIcon } from '@blocksuite/icons';
import { type ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from 'react';
import type { useCollectionManager } from '../use-collection-manager';
interface CollectionBarAction { interface CollectionBarAction {
icon: ReactNode; icon: ReactNode;
click: () => void; click: () => void;
@ -15,14 +13,12 @@ interface CollectionBarAction {
export const useActions = ({ export const useActions = ({
collection, collection,
setting,
openEdit, openEdit,
info, onDelete,
}: { }: {
info: DeleteCollectionInfo;
collection: Collection; collection: Collection;
setting: ReturnType<typeof useCollectionManager>;
openEdit: (open: Collection) => void; openEdit: (open: Collection) => void;
onDelete: () => void;
}) => { }) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return useMemo<CollectionBarAction[]>(() => { return useMemo<CollectionBarAction[]>(() => {
@ -39,10 +35,8 @@ export const useActions = ({
icon: <DeleteIcon style={{ color: 'var(--affine-error-color)' }} />, icon: <DeleteIcon style={{ color: 'var(--affine-error-color)' }} />,
name: 'delete', name: 'delete',
tooltip: t['com.affine.collection-bar.action.tooltip.delete'](), tooltip: t['com.affine.collection-bar.action.tooltip.delete'](),
click: () => { click: onDelete,
setting.deleteCollection(info, collection.id);
},
}, },
]; ];
}, [info, collection, t, setting, openEdit]); }, [t, onDelete, openEdit, collection]);
}; };

View File

@ -1,4 +1,3 @@
import { useCollectionManager } from '@affine/core/components/page-list';
import { import {
useBlockSuitePageMeta, useBlockSuitePageMeta,
usePageMetaHelper, usePageMetaHelper,
@ -322,9 +321,8 @@ export const collectionToCommand = (
export const useCollectionsCommands = () => { export const useCollectionsCommands = () => {
// todo: considering collections for searching pages // todo: considering collections for searching pages
const { savedCollections } = useCollectionManager( const collectionService = useService(CollectionService);
useService(CollectionService) const collections = useLiveData(collectionService.collections);
);
const query = useAtomValue(cmdkQueryAtom); const query = useAtomValue(cmdkQueryAtom);
const navigationHelper = useNavigateHelper(); const navigationHelper = useNavigateHelper();
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@ -340,7 +338,7 @@ export const useCollectionsCommands = () => {
if (query.trim() === '') { if (query.trim() === '') {
return results; return results;
} else { } else {
results = savedCollections.map(collection => { results = collections.map(collection => {
const command = collectionToCommand( const command = collectionToCommand(
collection, collection,
navigationHelper, navigationHelper,
@ -352,14 +350,7 @@ export const useCollectionsCommands = () => {
}); });
return results; return results;
} }
}, [ }, [query, collections, navigationHelper, selectCollection, t, workspace]);
query,
savedCollections,
navigationHelper,
selectCollection,
t,
workspace,
]);
}; };
export const useCMDKCommandGroups = () => { export const useCMDKCommandGroups = () => {

View File

@ -4,18 +4,19 @@ import { allPageFilterSelectAtom } from '@affine/core/atoms';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { WorkspaceSubPath } from '@affine/core/shared'; import { WorkspaceSubPath } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import * as styles from './index.css'; import * as styles from './index.css';
export const WorkspaceModeFilterTab = ({ export const WorkspaceModeFilterTab = ({
workspaceId,
activeFilter, activeFilter,
}: { }: {
workspaceId: string;
activeFilter: AllPageFilterOption; activeFilter: AllPageFilterOption;
}) => { }) => {
const workspace = useService(Workspace);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [value, setValue] = useState(activeFilter); const [value, setValue] = useState(activeFilter);
const [filterMode, setFilterMode] = useAtom(allPageFilterSelectAtom); const [filterMode, setFilterMode] = useAtom(allPageFilterSelectAtom);
@ -24,17 +25,17 @@ export const WorkspaceModeFilterTab = ({
(value: AllPageFilterOption) => { (value: AllPageFilterOption) => {
switch (value) { switch (value) {
case 'collections': case 'collections':
jumpToCollections(workspaceId); jumpToCollections(workspace.id);
break; break;
case 'tags': case 'tags':
jumpToTags(workspaceId); jumpToTags(workspace.id);
break; break;
case 'docs': case 'docs':
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL); jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
break; break;
} }
}, },
[jumpToCollections, jumpToSubPath, jumpToTags, workspaceId] [jumpToCollections, jumpToSubPath, jumpToTags, workspace]
); );
useEffect(() => { useEffect(() => {

View File

@ -6,7 +6,6 @@ import {
CollectionOperations, CollectionOperations,
filterPage, filterPage,
stopPropagation, stopPropagation,
useCollectionManager,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { CollectionService } from '@affine/core/modules/collection'; import { CollectionService } from '@affine/core/modules/collection';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
@ -40,20 +39,20 @@ const CollectionRenderer = ({
}) => { }) => {
const [collapsed, setCollapsed] = useState(true); const [collapsed, setCollapsed] = useState(true);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const setting = useCollectionManager(useService(CollectionService)); const collectionService = useService(CollectionService);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const dragItemId = getDropItemId('collections', collection.id); const dragItemId = getDropItemId('collections', collection.id);
const removeFromAllowList = useCallback( const removeFromAllowList = useCallback(
(id: string) => { (id: string) => {
setting.updateCollection({ collectionService.updateCollection(collection.id, () => ({
...collection, ...collection,
allowList: collection.allowList?.filter(v => v !== id), allowList: collection.allowList?.filter(v => v !== id),
}); }));
toast(t['com.affine.collection.removePage.success']()); toast(t['com.affine.collection.removePage.success']());
}, },
[collection, setting, t] [collection, collectionService, t]
); );
const { setNodeRef, isOver } = useDroppable({ const { setNodeRef, isOver } = useDroppable({
@ -66,7 +65,7 @@ const CollectionRenderer = ({
} else { } else {
toast(t['com.affine.collection.addPage.success']()); toast(t['com.affine.collection.addPage.success']());
} }
setting.addPage(collection.id, id); collectionService.addPageToCollection(collection.id, id);
}, },
}, },
}); });
@ -95,13 +94,13 @@ const CollectionRenderer = ({
const onRename = useCallback( const onRename = useCallback(
(name: string) => { (name: string) => {
setting.updateCollection({ collectionService.updateCollection(collection.id, () => ({
...collection, ...collection,
name, name,
}); }));
toast(t['com.affine.toastMessage.rename']()); toast(t['com.affine.toastMessage.rename']());
}, },
[collection, setting, t] [collection, collectionService, t]
); );
const handleOpen = useCallback(() => { const handleOpen = useCallback(() => {
setOpen(true); setOpen(true);
@ -124,7 +123,6 @@ const CollectionRenderer = ({
<CollectionOperations <CollectionOperations
info={info} info={info}
collection={collection} collection={collection}
setting={setting}
config={config} config={config}
openRenameModal={handleOpen} openRenameModal={handleOpen}
> >

View File

@ -38,7 +38,6 @@ import { WorkspaceSubPath } from '../../shared';
import { import {
createEmptyCollection, createEmptyCollection,
MoveToTrash, MoveToTrash,
useCollectionManager,
useEditCollectionName, useEditCollectionName,
} from '../page-list'; } from '../page-list';
import { CollectionsList } from '../pure/workspace-slider-bar/collections'; import { CollectionsList } from '../pure/workspace-slider-bar/collections';
@ -177,7 +176,7 @@ export const RootAppSidebar = ({
useRegisterBrowserHistoryCommands(router.back, router.forward); useRegisterBrowserHistoryCommands(router.back, router.forward);
const userInfo = useDeleteCollectionInfo(); const userInfo = useDeleteCollectionInfo();
const setting = useCollectionManager(useService(CollectionService)); const collection = useService(CollectionService);
const { node, open } = useEditCollectionName({ const { node, open } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](), title: t['com.affine.editCollection.createCollection'](),
showTips: true, showTips: true,
@ -186,13 +185,13 @@ export const RootAppSidebar = ({
open('') open('')
.then(name => { .then(name => {
const id = nanoid(); const id = nanoid();
setting.createCollection(createEmptyCollection(id, { name })); collection.addCollection(createEmptyCollection(id, { name }));
navigateHelper.jumpToCollection(blockSuiteWorkspace.id, id); navigateHelper.jumpToCollection(blockSuiteWorkspace.id, id);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
}, [blockSuiteWorkspace.id, navigateHelper, open, setting]); }, [blockSuiteWorkspace.id, collection, navigateHelper, open]);
const allPageActive = useMemo(() => { const allPageActive = useMemo(() => {
if ( if (

View File

@ -44,7 +44,7 @@ export function useNavigateHelper() {
); );
const jumpToCollections = useCallback( const jumpToCollections = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => { (workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
return navigate(`/workspace/${workspaceId}/all?filterMode=collections`, { return navigate(`/workspace/${workspaceId}/collection`, {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },
@ -52,7 +52,7 @@ export function useNavigateHelper() {
); );
const jumpToTags = useCallback( const jumpToTags = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => { (workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
return navigate(`/workspace/${workspaceId}/all?filterMode=tags`, { return navigate(`/workspace/${workspaceId}/tag`, {
replace: logic === RouteLogic.REPLACE, replace: logic === RouteLogic.REPLACE,
}); });
}, },

View File

@ -1,11 +1,10 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { useSetAtom, useStore } from 'jotai'; import { useStore } from 'jotai';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { allPageModeSelectAtom } from '../atoms';
import { import {
registerAffineCreationCommands, registerAffineCreationCommands,
registerAffineHelpCommands, registerAffineHelpCommands,
@ -27,7 +26,6 @@ export function useRegisterWorkspaceCommands() {
const languageHelper = useLanguageHelper(); const languageHelper = useLanguageHelper();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace); const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const navigationHelper = useNavigateHelper(); const navigationHelper = useNavigateHelper();
const setPageListMode = useSetAtom(allPageModeSelectAtom);
const [editor] = useActiveBlocksuiteEditor(); const [editor] = useActiveBlocksuiteEditor();
// register AffineUpdatesCommands // register AffineUpdatesCommands
@ -49,19 +47,12 @@ export function useRegisterWorkspaceCommands() {
t, t,
workspace: currentWorkspace.blockSuiteWorkspace, workspace: currentWorkspace.blockSuiteWorkspace,
navigationHelper, navigationHelper,
setPageListMode,
}); });
return () => { return () => {
unsub(); unsub();
}; };
}, [ }, [store, t, currentWorkspace.blockSuiteWorkspace, navigationHelper]);
store,
t,
currentWorkspace.blockSuiteWorkspace,
navigationHelper,
setPageListMode,
]);
// register AffineSettingsCommands // register AffineSettingsCommands
useEffect(() => { useEffect(() => {

View File

@ -81,6 +81,15 @@ export class CollectionService {
} }
} }
addPageToCollection(collectionId: string, pageId: string) {
this.updateCollection(collectionId, old => {
return {
...old,
allowList: [pageId, ...(old.allowList ?? [])],
};
});
}
deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) { deleteCollection(info: DeleteCollectionInfo, ...ids: string[]) {
const collectionsYArray = this.collectionsYArray; const collectionsYArray = this.collectionsYArray;
if (!collectionsYArray) { if (!collectionsYArray) {

View File

@ -0,0 +1,3 @@
export class PageListView {
constructor() {}
}

View File

@ -0,0 +1,49 @@
import { IconButton } from '@affine/component';
import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import { PlusIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import { useMemo } from 'react';
import * as styles from '../all-page/all-page.css';
export const AllCollectionHeader = ({
showCreateNew,
onCreateCollection,
}: {
showCreateNew: boolean;
onCreateCollection?: () => void;
}) => {
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const renderRightItem = useMemo(() => {
return (
<IconButton
type="default"
icon={<PlusIcon fontSize={16} />}
onClick={onCreateCollection}
className={clsx(
styles.headerCreateNewButton,
styles.headerCreateNewCollectionIconButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
/>
);
}, [onCreateCollection, showCreateNew]);
return (
<Header
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{renderRightItem}
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
}
center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
/>
);
};

View File

@ -0,0 +1,93 @@
import { HubIsland } from '@affine/core/components/affine/hub-island';
import {
CollectionListHeader,
type CollectionMeta,
createEmptyCollection,
useEditCollectionName,
VirtualizedCollectionList,
} from '@affine/core/components/page-list';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { nanoid } from 'nanoid';
import { useCallback, useMemo, useState } from 'react';
import { CollectionService } from '../../../modules/collection';
import * as styles from '../all-page/all-page.css';
import { EmptyCollectionList } from '../page-list-empty';
import { AllCollectionHeader } from './header';
export const AllCollection = () => {
const t = useAFFiNEI18N();
const currentWorkspace = useService(Workspace);
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
const collectionService = useService(CollectionService);
const collections = useLiveData(collectionService.collections);
const config = useAllPageListConfig();
const collectionMetas = useMemo(() => {
const collectionsList: CollectionMeta[] = collections.map(collection => {
return {
...collection,
title: collection.name,
};
});
return collectionsList;
}, [collections]);
const navigateHelper = useNavigateHelper();
const { open, node } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](),
showTips: true,
});
const handleCreateCollection = useCallback(() => {
open('')
.then(name => {
const id = nanoid();
collectionService.addCollection(createEmptyCollection(id, { name }));
navigateHelper.jumpToCollection(currentWorkspace.id, id);
})
.catch(err => {
console.error(err);
});
}, [collectionService, currentWorkspace, navigateHelper, open]);
return (
<div className={styles.root}>
<AllCollectionHeader
showCreateNew={!hideHeaderCreateNew}
onCreateCollection={handleCreateCollection}
/>
{collectionMetas.length > 0 ? (
<VirtualizedCollectionList
collections={collections}
collectionMetas={collectionMetas}
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
node={node}
config={config}
handleCreateCollection={handleCreateCollection}
/>
) : (
<EmptyCollectionList
heading={
<CollectionListHeader
node={node}
onCreate={handleCreateCollection}
/>
}
/>
)}
<HubIsland />
</div>
);
};
export const Component = () => {
return <AllCollection />;
};

View File

@ -8,36 +8,31 @@ import { filterContainerStyle } from '../../../components/filter-container.css';
import { import {
FilterList, FilterList,
SaveAsCollectionButton, SaveAsCollectionButton,
useCollectionManager,
} from '../../../components/page-list'; } from '../../../components/page-list';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
export const FilterContainer = () => { export const FilterContainer = ({
filters,
onChangeFilters,
}: {
filters: Filter[];
onChangeFilters: (filters: Filter[]) => void;
}) => {
const currentWorkspace = useService(Workspace); const currentWorkspace = useService(Workspace);
const navigateHelper = useNavigateHelper(); const navigateHelper = useNavigateHelper();
const setting = useCollectionManager(useService(CollectionService)); const collectionService = useService(CollectionService);
const saveToCollection = useCallback( const saveToCollection = useCallback(
(collection: Collection) => { (collection: Collection) => {
setting.createCollection({ collectionService.addCollection({
...collection, ...collection,
filterList: setting.currentCollection.filterList, filterList: filters,
}); });
navigateHelper.jumpToCollection(currentWorkspace.id, collection.id); navigateHelper.jumpToCollection(currentWorkspace.id, collection.id);
}, },
[setting, navigateHelper, currentWorkspace.id] [collectionService, filters, navigateHelper, currentWorkspace.id]
); );
const onFilterChange = useCallback( if (!filters.length) {
(filterList: Filter[]) => {
setting.updateCollection({
...setting.currentCollection,
filterList,
});
},
[setting]
);
if (!setting.isDefault || !setting.currentCollection.filterList.length) {
return null; return null;
} }
@ -46,12 +41,12 @@ export const FilterContainer = () => {
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<FilterList <FilterList
propertiesMeta={currentWorkspace.blockSuiteWorkspace.meta.properties} propertiesMeta={currentWorkspace.blockSuiteWorkspace.meta.properties}
value={setting.currentCollection.filterList} value={filters}
onChange={onFilterChange} onChange={onChangeFilters}
/> />
</div> </div>
<div> <div>
{setting.currentCollection.filterList.length > 0 ? ( {filters.length > 0 ? (
<SaveAsCollectionButton onConfirm={saveToCollection} /> <SaveAsCollectionButton onConfirm={saveToCollection} />
) : null} ) : null}
</div> </div>

View File

@ -1,69 +1,33 @@
import { IconButton } from '@affine/component';
import type { AllPageFilterOption } from '@affine/core/atoms';
import { import {
CollectionList, AllPageListOperationsMenu,
PageListNewPageButton, PageListNewPageButton,
useCollectionManager,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { Header } from '@affine/core/components/pure/header'; import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab'; import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config'; import type { Filter } from '@affine/env/filter';
import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-collection-info';
import { PlusIcon } from '@blocksuite/icons'; import { PlusIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store'; import { useService } from '@toeverything/infra';
import { useService } from '@toeverything/infra/di'; import { Workspace } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { CollectionService } from '../../../modules/collection';
import * as styles from './all-page.css'; import * as styles from './all-page.css';
import { FilterContainer } from './all-page-filter'; import { FilterContainer } from './all-page-filter';
export const AllPageHeader = ({ export const AllPageHeader = ({
workspace,
showCreateNew, showCreateNew,
isDefaultFilter, filters,
activeFilter, onChangeFilters,
onCreateCollection,
}: { }: {
workspace: Workspace;
showCreateNew: boolean; showCreateNew: boolean;
isDefaultFilter: boolean; filters: Filter[];
activeFilter: AllPageFilterOption; onChangeFilters: (filters: Filter[]) => void;
onCreateCollection?: () => void;
}) => { }) => {
const setting = useCollectionManager(useService(CollectionService)); const workspace = useService(Workspace);
const config = useAllPageListConfig();
const userInfo = useDeleteCollectionInfo();
const isWindowsDesktop = environment.isDesktop && environment.isWindows; const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const disableFilterButton = useMemo(() => {
return activeFilter !== 'docs' && isDefaultFilter;
}, [activeFilter, isDefaultFilter]);
const renderRightItem = useMemo(() => { const renderRightItem = useMemo(() => {
if (activeFilter === 'tags') {
return null;
}
if (
activeFilter === 'collections' &&
isDefaultFilter &&
onCreateCollection
) {
return (
<IconButton
type="default"
icon={<PlusIcon fontSize={16} />}
onClick={onCreateCollection}
className={clsx(
styles.headerCreateNewButton,
styles.headerCreateNewCollectionIconButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
/>
);
}
return ( return (
<PageListNewPageButton <PageListNewPageButton
size="small" size="small"
@ -75,18 +39,16 @@ export const AllPageHeader = ({
<PlusIcon /> <PlusIcon />
</PageListNewPageButton> </PageListNewPageButton>
); );
}, [activeFilter, isDefaultFilter, onCreateCollection, showCreateNew]); }, [showCreateNew]);
return ( return (
<> <>
<Header <Header
left={ left={
<CollectionList <AllPageListOperationsMenu
userInfo={userInfo} filterList={filters}
allPageListConfig={config} onChangeFilterList={onChangeFilters}
setting={setting} propertiesMeta={workspace.blockSuiteWorkspace.meta.properties}
propertiesMeta={workspace.meta.properties}
disable={disableFilterButton}
/> />
} }
right={ right={
@ -98,14 +60,9 @@ export const AllPageHeader = ({
{isWindowsDesktop ? <WindowsAppControls /> : null} {isWindowsDesktop ? <WindowsAppControls /> : null}
</div> </div>
} }
center={ center={<WorkspaceModeFilterTab activeFilter={'docs'} />}
<WorkspaceModeFilterTab
workspaceId={workspace.id}
activeFilter={activeFilter}
/> />
} <FilterContainer filters={filters} onChangeFilters={onChangeFilters} />
/>
<FilterContainer />
</> </>
); );
}; };

View File

@ -1,211 +1,50 @@
import type { AllPageFilterOption } from '@affine/core/atoms';
import { HubIsland } from '@affine/core/components/affine/hub-island'; import { HubIsland } from '@affine/core/components/affine/hub-island';
import { import {
CollectionListHeader,
type CollectionMeta,
createEmptyCollection,
currentCollectionAtom,
PageListHeader, PageListHeader,
useCollectionManager,
useEditCollectionName,
useFilteredPageMetas, useFilteredPageMetas,
useTagMetas,
VirtualizedCollectionList,
VirtualizedPageList, VirtualizedPageList,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import {
TagListHeader,
VirtualizedTagList,
} from '@affine/core/components/page-list/tags';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { performanceRenderLogger } from '@affine/core/shared'; import { performanceRenderLogger } from '@affine/core/shared';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { Filter } from '@affine/env/filter';
import { useService } from '@toeverything/infra'; import { useService } from '@toeverything/infra';
import { useLiveData } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useEffect, useState } from 'react';
import { nanoid } from 'nanoid';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { NIL } from 'uuid';
import { CollectionService } from '../../../modules/collection'; import { EmptyPageList } from '../page-list-empty';
import {
EmptyCollectionList,
EmptyPageList,
EmptyTagList,
} from '../page-list-empty';
import * as styles from './all-page.css'; import * as styles from './all-page.css';
import { AllPageHeader } from './all-page-header'; import { AllPageHeader } from './all-page-header';
// even though it is called all page, it is also being used for collection route as well export const AllPage = () => {
export const AllPage = ({
activeFilter,
}: {
activeFilter: AllPageFilterOption;
}) => {
const t = useAFFiNEI18N();
const params = useParams();
const currentWorkspace = useService(Workspace); const currentWorkspace = useService(Workspace);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace); const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true); const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
const collectionService = useService(CollectionService); const [filters, setFilters] = useState<Filter[]>([]);
const collections = useLiveData(collectionService.collections); const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
const setting = useCollectionManager(collectionService); filters: filters,
const config = useAllPageListConfig();
const { tags, tagMetas, filterPageMetaByTag, deleteTags } = useTagMetas(
currentWorkspace.blockSuiteWorkspace,
pageMetas
);
const filteredPageMetas = useFilteredPageMetas(
'all',
pageMetas,
currentWorkspace
);
const tagPageMetas = useMemo(() => {
if (params.tagId) {
return filterPageMetaByTag(params.tagId);
}
return [];
}, [filterPageMetaByTag, params.tagId]);
const collectionMetas = useMemo(() => {
const collectionsList: CollectionMeta[] = collections.map(collection => {
return {
...collection,
title: collection.name,
};
}); });
return collectionsList;
}, [collections]);
const navigateHelper = useNavigateHelper();
const { open, node } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](),
showTips: true,
});
const handleCreateCollection = useCallback(() => {
open('')
.then(name => {
const id = nanoid();
setting.createCollection(createEmptyCollection(id, { name }));
navigateHelper.jumpToCollection(currentWorkspace.id, id);
})
.catch(err => {
console.error(err);
});
}, [currentWorkspace.id, navigateHelper, open, setting]);
const currentTag = useMemo(() => {
if (params.tagId) {
return tags.find(tag => tag.id === params.tagId);
}
return;
}, [params.tagId, tags]);
const content = useMemo(() => {
if (filteredPageMetas.length > 0 && activeFilter === 'docs') {
return (
<VirtualizedPageList
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/>
);
} else if (activeFilter === 'collections' && !setting.isDefault) {
return (
<VirtualizedPageList
collection={setting.currentCollection}
config={config}
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/>
);
} else if (activeFilter === 'collections' && setting.isDefault) {
return collectionMetas.length > 0 ? (
<VirtualizedCollectionList
collections={collections}
collectionMetas={collectionMetas}
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
node={node}
config={config}
handleCreateCollection={handleCreateCollection}
/>
) : (
<EmptyCollectionList
heading={
<CollectionListHeader
node={node}
onCreate={handleCreateCollection}
/>
}
/>
);
} else if (activeFilter === 'tags') {
if (params.tagId) {
return tagPageMetas.length > 0 ? (
<VirtualizedPageList
tag={currentTag}
listItem={tagPageMetas}
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/>
) : (
<EmptyPageList
type="all"
heading={<PageListHeader workspaceId={currentWorkspace.id} />}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
);
}
return tags.length > 0 ? (
<VirtualizedTagList
tags={tags}
tagMetas={tagMetas}
setHideHeaderCreateNewTag={setHideHeaderCreateNew}
onTagDelete={deleteTags}
/>
) : (
<EmptyTagList heading={<TagListHeader />} />
);
}
return (
<EmptyPageList
type="all"
heading={<PageListHeader workspaceId={currentWorkspace.id} />}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
);
}, [
activeFilter,
collectionMetas,
collections,
config,
currentTag,
currentWorkspace.blockSuiteWorkspace,
currentWorkspace.id,
deleteTags,
filteredPageMetas.length,
handleCreateCollection,
node,
params.tagId,
setting.currentCollection,
setting.isDefault,
tagMetas,
tagPageMetas,
tags,
]);
return ( return (
<div className={styles.root}> <div className={styles.root}>
<AllPageHeader <AllPageHeader
workspace={currentWorkspace.blockSuiteWorkspace}
showCreateNew={!hideHeaderCreateNew} showCreateNew={!hideHeaderCreateNew}
isDefaultFilter={setting.isDefault} filters={filters}
activeFilter={activeFilter} onChangeFilters={setFilters}
onCreateCollection={handleCreateCollection}
/> />
{content} {filteredPageMetas.length > 0 ? (
<VirtualizedPageList
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
filters={filters}
/>
) : (
<EmptyPageList
type="all"
heading={<PageListHeader />}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
)}
<HubIsland /> <HubIsland />
</div> </div>
); );
@ -215,21 +54,8 @@ export const Component = () => {
performanceRenderLogger.info('AllPage'); performanceRenderLogger.info('AllPage');
const currentWorkspace = useService(Workspace); const currentWorkspace = useService(Workspace);
const currentCollection = useSetAtom(currentCollectionAtom);
const navigateHelper = useNavigateHelper(); const navigateHelper = useNavigateHelper();
const location = useLocation();
const activeFilter = useMemo(() => {
const query = new URLSearchParams(location.search);
const filterMode = query.get('filterMode');
if (filterMode === 'collections') {
return 'collections';
} else if (filterMode === 'tags') {
return 'tags';
}
return 'docs';
}, [location.search]);
useEffect(() => { useEffect(() => {
function checkJumpOnce() { function checkJumpOnce() {
for (const [pageId] of currentWorkspace.blockSuiteWorkspace.pages) { for (const [pageId] of currentWorkspace.blockSuiteWorkspace.pages) {
@ -252,9 +78,5 @@ export const Component = () => {
navigateHelper, navigateHelper,
]); ]);
useEffect(() => { return <AllPage />;
currentCollection(NIL);
}, [currentCollection]);
return <AllPage activeFilter={activeFilter} />;
}; };

View File

@ -0,0 +1,23 @@
import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import * as styles from '../all-page/all-page.css';
export const AllTagHeader = () => {
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
return (
<Header
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
}
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
/>
);
};

View File

@ -0,0 +1,43 @@
import { HubIsland } from '@affine/core/components/affine/hub-island';
import { useTagMetas } from '@affine/core/components/page-list';
import {
TagListHeader,
VirtualizedTagList,
} from '@affine/core/components/page-list/tags';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import * as styles from '../all-page/all-page.css';
import { EmptyTagList } from '../page-list-empty';
import { AllTagHeader } from './header';
export const AllTag = () => {
const currentWorkspace = useService(Workspace);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const { tags, tagMetas, deleteTags } = useTagMetas(
currentWorkspace.blockSuiteWorkspace,
pageMetas
);
return (
<div className={styles.root}>
<AllTagHeader />
{tags.length > 0 ? (
<VirtualizedTagList
tags={tags}
tagMetas={tagMetas}
onTagDelete={deleteTags}
/>
) : (
<EmptyTagList heading={<TagListHeader />} />
)}
<HubIsland />
</div>
);
};
export const Component = () => {
return <AllTag />;
};

View File

@ -0,0 +1,49 @@
import { IconButton } from '@affine/component';
import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import { PlusIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import { useMemo } from 'react';
import * as styles from '../all-page/all-page.css';
export const CollectionDetailHeader = ({
showCreateNew,
onCreate,
}: {
showCreateNew: boolean;
onCreate: () => void;
}) => {
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const renderRightItem = useMemo(() => {
return (
<IconButton
type="default"
icon={<PlusIcon fontSize={16} />}
onClick={onCreate}
className={clsx(
styles.headerCreateNewButton,
styles.headerCreateNewCollectionIconButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
/>
);
}, [onCreate, showCreateNew]);
return (
<Header
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{renderRightItem}
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
}
center={<WorkspaceModeFilterTab activeFilter={'collections'} />}
/>
);
};

View File

@ -3,11 +3,11 @@ import {
SidebarSwitch, SidebarSwitch,
} from '@affine/component/app-sidebar'; } from '@affine/component/app-sidebar';
import { pushNotificationAtom } from '@affine/component/notification-center'; import { pushNotificationAtom } from '@affine/component/notification-center';
import { HubIsland } from '@affine/core/components/affine/hub-island';
import { import {
AffineShapeIcon, AffineShapeIcon,
currentCollectionAtom,
useCollectionManager,
useEditCollection, useEditCollection,
VirtualizedPageList,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls'; import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config'; import { useAllPageListConfig } from '@affine/core/hooks/affine/use-all-page-list-config';
@ -23,30 +23,54 @@ import {
ViewLayersIcon, ViewLayersIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { useLiveData } from '@toeverything/infra/livedata'; import { useLiveData } from '@toeverything/infra/livedata';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { type LoaderFunction, redirect, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useNavigateHelper } from '../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../shared'; import { WorkspaceSubPath } from '../../../shared';
import { AllPage } from './all-page/all-page'; import * as allPageStyles from '../all-page/all-page.css';
import * as styles from './collection.css'; import * as styles from './collection.css';
import { CollectionDetailHeader } from './header';
export const loader: LoaderFunction = async args => { export const CollectionDetail = ({
const rootStore = getCurrentStore(); collection,
if (!args.params.collectionId) { }: {
return redirect('/404'); collection: Collection;
} }) => {
rootStore.set(currentCollectionAtom, args.params.collectionId); const config = useAllPageListConfig();
return null; const { node, open } = useEditCollection(useAllPageListConfig());
const collectionService = useService(CollectionService);
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
const handleEditCollection = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'page');
collectionService.updateCollection(ret.id, () => ret);
}, [collection, collectionService, open]);
return (
<div className={allPageStyles.root}>
<CollectionDetailHeader
showCreateNew={!hideHeaderCreateNew}
onCreate={handleEditCollection}
/>
<VirtualizedPageList
collection={collection}
config={config}
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/>
<HubIsland />
{node}
</div>
);
}; };
export const Component = function CollectionPage() { export const Component = function CollectionPage() {
const collectionService = useService(CollectionService); const collectionService = useService(CollectionService);
const collections = useLiveData(collectionService.collections); const collections = useLiveData(collectionService.collections);
const navigate = useNavigateHelper(); const navigate = useNavigateHelper();
const params = useParams(); const params = useParams();
@ -87,7 +111,7 @@ export const Component = function CollectionPage() {
return isEmpty(collection) ? ( return isEmpty(collection) ? (
<Placeholder collection={collection} /> <Placeholder collection={collection} />
) : ( ) : (
<AllPage activeFilter="collections" /> <CollectionDetail collection={collection} />
); );
}; };
@ -95,16 +119,16 @@ const isWindowsDesktop = environment.isDesktop && environment.isWindows;
const Placeholder = ({ collection }: { collection: Collection }) => { const Placeholder = ({ collection }: { collection: Collection }) => {
const workspace = useService(Workspace); const workspace = useService(Workspace);
const collectionService = useCollectionManager(useService(CollectionService)); const collectionService = useService(CollectionService);
const { node, open } = useEditCollection(useAllPageListConfig()); const { node, open } = useEditCollection(useAllPageListConfig());
const { jumpToCollections } = useNavigateHelper(); const { jumpToCollections } = useNavigateHelper();
const openPageEdit = useAsyncCallback(async () => { const openPageEdit = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'page'); const ret = await open({ ...collection }, 'page');
collectionService.updateCollection(ret); collectionService.updateCollection(ret.id, () => ret);
}, [open, collection, collectionService]); }, [open, collection, collectionService]);
const openRuleEdit = useAsyncCallback(async () => { const openRuleEdit = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'rule'); const ret = await open({ ...collection }, 'rule');
collectionService.updateCollection(ret); collectionService.updateCollection(ret.id, () => ret);
}, [collection, open, collectionService]); }, [collection, open, collectionService]);
const [showTips, setShowTips] = useState(false); const [showTips, setShowTips] = useState(false);
useEffect(() => { useEffect(() => {

View File

@ -2,7 +2,6 @@ import { Scrollable } from '@affine/component';
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { ResizePanel } from '@affine/component/resize-panel'; import { ResizePanel } from '@affine/component/resize-panel';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { CollectionService } from '@affine/core/modules/collection';
import type { PageService } from '@blocksuite/blocks'; import type { PageService } from '@blocksuite/blocks';
import { import {
BookmarkService, BookmarkService,
@ -42,17 +41,13 @@ import { HubIsland } from '../../../components/affine/hub-island';
import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal'; import { GlobalPageHistoryModal } from '../../../components/affine/page-history-modal';
import { ImagePreviewModal } from '../../../components/image-preview'; import { ImagePreviewModal } from '../../../components/image-preview';
import { PageDetailEditor } from '../../../components/page-detail-editor'; import { PageDetailEditor } from '../../../components/page-detail-editor';
import {
createTagFilter,
useCollectionManager,
} from '../../../components/page-list';
import { TrashPageFooter } from '../../../components/pure/trash-page-footer'; import { TrashPageFooter } from '../../../components/pure/trash-page-footer';
import { TopTip } from '../../../components/top-tip'; import { TopTip } from '../../../components/top-tip';
import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands'; import { useRegisterBlocksuiteEditorCommands } from '../../../hooks/affine/use-register-blocksuite-editor-commands';
import { usePageDocumentTitle } from '../../../hooks/use-global-state'; import { usePageDocumentTitle } from '../../../hooks/use-global-state';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper'; import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { CurrentPageService } from '../../../modules/page'; import { CurrentPageService } from '../../../modules/page';
import { performanceRenderLogger, WorkspaceSubPath } from '../../../shared'; import { performanceRenderLogger } from '../../../shared';
import { PageNotFound } from '../../404'; import { PageNotFound } from '../../404';
import * as styles from './detail-page.css'; import * as styles from './detail-page.css';
import { DetailPageHeader, RightSidebarHeader } from './detail-page-header'; import { DetailPageHeader, RightSidebarHeader } from './detail-page-header';
@ -117,7 +112,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const page = useService(Page); const page = useService(Page);
const pageRecordList = useService(PageRecordList); const pageRecordList = useService(PageRecordList);
const currentPageId = page.id; const currentPageId = page.id;
const { openPage, jumpToSubPath } = useNavigateHelper(); const { openPage, jumpToTag } = useNavigateHelper();
const currentWorkspace = useService(Workspace); const currentWorkspace = useService(Workspace);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
@ -127,8 +122,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const isInTrash = pageMeta?.trash; const isInTrash = pageMeta?.trash;
const collectionService = useService(CollectionService);
const { setTemporaryFilter } = useCollectionManager(collectionService);
const mode = useLiveData(page.mode); const mode = useLiveData(page.mode);
useRegisterBlocksuiteEditorCommands(); useRegisterBlocksuiteEditorCommands();
const title = useLiveData(page.title); const title = useLiveData(page.title);
@ -191,9 +184,8 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => { const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
return openPage(blockSuiteWorkspace.id, pageId); return openPage(blockSuiteWorkspace.id, pageId);
}); });
const disposeTagClick = editor.slots.tagClicked.on(async ({ tagId }) => { const disposeTagClick = editor.slots.tagClicked.on(({ tagId }) => {
jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL); jumpToTag(currentWorkspace.id, tagId);
setTemporaryFilter([createTagFilter(tagId)]);
}); });
return () => { return () => {
dispose.dispose(); dispose.dispose();
@ -201,14 +193,13 @@ const DetailPageImpl = memo(function DetailPageImpl() {
}; };
}, },
[ [
page,
mode,
pageRecordList,
openPage,
blockSuiteWorkspace.id, blockSuiteWorkspace.id,
jumpToSubPath,
currentWorkspace.id, currentWorkspace.id,
setTemporaryFilter, jumpToTag,
mode,
openPage,
page,
pageRecordList,
] ]
); );

View File

@ -1,51 +0,0 @@
import { TagListHeader, useTagMetas } from '@affine/core/components/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useService, Workspace } from '@toeverything/infra';
import { useMemo } from 'react';
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
import { AllPage } from './all-page/all-page';
import { AllPageHeader } from './all-page/all-page-header';
import { EmptyPageList } from './page-list-empty';
export const loader: LoaderFunction = async args => {
if (!args.params.tagId) {
return redirect('/404');
}
return null;
};
export const Component = function TagPage() {
const params = useParams();
const currentWorkspace = useService(Workspace);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const { tagUsageCounts } = useTagMetas(
currentWorkspace.blockSuiteWorkspace,
pageMetas
);
const isEmpty = useMemo(() => {
if (params.tagId) {
return tagUsageCounts[params.tagId] === 0;
}
return true;
}, [params.tagId, tagUsageCounts]);
return isEmpty ? (
<>
<AllPageHeader
workspace={currentWorkspace.blockSuiteWorkspace}
showCreateNew={false}
isDefaultFilter={true}
activeFilter={'tags'}
/>
<EmptyPageList
type="all"
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
heading={<TagListHeader />}
/>
</>
) : (
<AllPage activeFilter="tags" />
);
};

View File

@ -0,0 +1,23 @@
import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
import * as styles from '../all-page/all-page.css';
export const TagDetailHeader = () => {
const isWindowsDesktop = environment.isDesktop && environment.isWindows;
return (
<Header
right={
<div
className={styles.headerRightWindows}
data-is-windows-desktop={isWindowsDesktop}
>
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
}
center={<WorkspaceModeFilterTab activeFilter={'tags'} />}
/>
);
};

View File

@ -0,0 +1,63 @@
import { HubIsland } from '@affine/core/components/affine/hub-island';
import {
PageListHeader,
useTagMetas,
VirtualizedPageList,
} from '@affine/core/components/page-list';
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import { useService } from '@toeverything/infra';
import { Workspace } from '@toeverything/infra';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { PageNotFound } from '../../404';
import * as styles from '../all-page/all-page.css';
import { EmptyPageList } from '../page-list-empty';
import { TagDetailHeader } from './header';
export const TagDetail = ({ tagId }: { tagId?: string }) => {
const currentWorkspace = useService(Workspace);
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
const { tags, filterPageMetaByTag } = useTagMetas(
currentWorkspace.blockSuiteWorkspace,
pageMetas
);
const tagPageMetas = useMemo(() => {
if (tagId) {
return filterPageMetaByTag(tagId);
}
return [];
}, [filterPageMetaByTag, tagId]);
const currentTag = useMemo(
() => tags.find(tag => tag.id === tagId),
[tagId, tags]
);
if (!currentTag) {
return <PageNotFound />;
}
return (
<div className={styles.root}>
<TagDetailHeader />
{tagPageMetas.length > 0 ? (
<VirtualizedPageList tag={currentTag} listItem={tagPageMetas} />
) : (
<EmptyPageList
type="all"
heading={<PageListHeader />}
blockSuiteWorkspace={currentWorkspace.blockSuiteWorkspace}
/>
)}
<HubIsland />
</div>
);
};
export const Component = () => {
const params = useParams();
return <TagDetail tagId={params.tagId} />;
};

View File

@ -1,7 +1,6 @@
import { toast } from '@affine/component'; import { toast } from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { import {
currentCollectionAtom,
type ListItem, type ListItem,
ListTableHeader, ListTableHeader,
PageListItemRenderer, PageListItemRenderer,
@ -19,11 +18,8 @@ import { assertExists } from '@blocksuite/global/utils';
import { DeleteIcon } from '@blocksuite/icons'; import { DeleteIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store'; import type { PageMeta } from '@blocksuite/store';
import { Workspace } from '@toeverything/infra'; import { Workspace } from '@toeverything/infra';
import { getCurrentStore } from '@toeverything/infra/atom';
import { useService } from '@toeverything/infra/di'; import { useService } from '@toeverything/infra/di';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { type LoaderFunction } from 'react-router-dom';
import { NIL } from 'uuid';
import { EmptyPageList } from './page-list-empty'; import { EmptyPageList } from './page-list-empty';
import * as styles from './trash-page.css'; import * as styles from './trash-page.css';
@ -50,27 +46,15 @@ const TrashHeader = () => {
); );
}; };
export const loader: LoaderFunction = async () => {
// to fix the bug that the trash page list is not updated when route from collection to trash
// but it's not a good solution, the page will jitter when collection and trash are switched between each other.
// TODO: fix this bug
const rootStore = getCurrentStore();
rootStore.set(currentCollectionAtom, NIL);
return null;
};
export const TrashPage = () => { export const TrashPage = () => {
const currentWorkspace = useService(Workspace); const currentWorkspace = useService(Workspace);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace; const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
assertExists(blockSuiteWorkspace); assertExists(blockSuiteWorkspace);
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace); const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const filteredPageMetas = useFilteredPageMetas( const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
'trash', trash: true,
pageMetas, });
currentWorkspace
);
const { restoreFromTrash, permanentlyDeletePage } = const { restoreFromTrash, permanentlyDeletePage } =
useBlockSuiteMetaHelper(blockSuiteWorkspace); useBlockSuiteMetaHelper(blockSuiteWorkspace);

View File

@ -15,9 +15,17 @@ export const routes = [
path: 'all', path: 'all',
lazy: () => import('./pages/workspace/all-page/all-page'), lazy: () => import('./pages/workspace/all-page/all-page'),
}, },
{
path: 'collection',
lazy: () => import('./pages/workspace/all-collection'),
},
{ {
path: 'collection/:collectionId', path: 'collection/:collectionId',
lazy: () => import('./pages/workspace/collection'), lazy: () => import('./pages/workspace/collection/index'),
},
{
path: 'tag',
lazy: () => import('./pages/workspace/all-tag'),
}, },
{ {
path: 'tag/:tagId', path: 'tag/:tagId',

View File

@ -123,6 +123,7 @@ test('allow creation of filters by tags', async ({ page }) => {
await createPageWithTag(page, { title: 'Page A', tags: ['Page A'] }); await createPageWithTag(page, { title: 'Page A', tags: ['Page A'] });
await createPageWithTag(page, { title: 'Page B', tags: ['Page B'] }); await createPageWithTag(page, { title: 'Page B', tags: ['Page B'] });
await clickSideBarAllPageButton(page); await clickSideBarAllPageButton(page);
await createFirstFilter(page, 'Tags');
await checkFilterName(page, 'is not empty'); await checkFilterName(page, 'is not empty');
expect(await getPagesCount(page)).toBe(pagesWithTagsCount + 2); expect(await getPagesCount(page)).toBe(pagesWithTagsCount + 2);
await changeFilter(page, 'contains all'); await changeFilter(page, 'contains all');