refactor(core): refactor collection to use new filter system (#12228)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new Collection entity and store with reactive management and real-time updates.
  - Added reactive favorite and shared filters with expanded filtering options.
- **Refactor**
  - Overhauled collection and filtering logic for better performance and maintainability.
  - Replaced legacy filtering UI and logic with a streamlined, service-driven rules system.
  - Updated collection components to use reactive data streams and simplified props.
  - Simplified collection creation by delegating ID generation and instantiation to the service layer.
  - Removed deprecated hooks and replaced state-based filtering with observable-driven filtering.
- **Bug Fixes**
  - Improved accuracy and consistency of tag and favorite filtering in collections.
- **Chores**
  - Removed deprecated and unused filter-related files, types, components, and styles to reduce complexity.
  - Cleaned up imports and removed unused code across multiple components.
- **Documentation**
  - Corrected inline documentation for improved clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN
2025-05-13 08:28:02 +00:00
parent 5dbe6ff68b
commit 13d882d6d8
96 changed files with 1274 additions and 3161 deletions
@@ -5,26 +5,17 @@ import {
MenuItem,
toast,
} from '@affine/component';
import { filterPage } from '@affine/core/components/page-list';
import { CollectionService } from '@affine/core/modules/collection';
import {
type Collection,
CollectionService,
} from '@affine/core/modules/collection';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { DocsService } from '@affine/core/modules/doc';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { ShareDocsListService } from '@affine/core/modules/share-doc';
import type { AffineDNDData } from '@affine/core/types/dnd';
import type { Collection } from '@affine/env/filter';
import { PublicDocMode } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import type { DocMeta } from '@blocksuite/affine/store';
import { FilterMinusIcon } from '@blocksuite/icons/rc';
import {
LiveData,
useLiveData,
useService,
useServices,
} from '@toeverything/infra';
import { useLiveData, useService, useServices } from '@toeverything/infra';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
@@ -71,6 +62,7 @@ export const NavigationPanelCollectionNode = ({
const collectionService = useService(CollectionService);
const collection = useLiveData(collectionService.collection$(collectionId));
const name = useLiveData(collection?.name$);
const dndData = useMemo(() => {
return {
@@ -89,11 +81,10 @@ export const NavigationPanelCollectionNode = ({
const handleRename = useCallback(
(name: string) => {
if (collection && collection.name !== name) {
collectionService.updateCollection(collectionId, () => ({
...collection,
if (collection && collection.name$.value !== name) {
collectionService.updateCollection(collectionId, {
name,
}));
});
track.$.navigationPanel.organize.renameOrganizeItem({
type: 'collection',
@@ -109,10 +100,10 @@ export const NavigationPanelCollectionNode = ({
if (!collection) {
return;
}
if (collection.allowList.includes(docId)) {
if (collection.allowList$.value.includes(docId)) {
toast(t['com.affine.collection.addPage.alreadyExists']());
} else {
collectionService.addPageToCollection(collection.id, docId);
collectionService.addDocToCollection(collection.id, docId);
}
},
[collection, collectionService, t]
@@ -210,7 +201,7 @@ export const NavigationPanelCollectionNode = ({
return (
<NavigationPanelTreeNode
icon={CollectionIcon}
name={collection.name || t['Untitled']()}
name={name || t['Untitled']()}
dndData={dndData}
onDrop={handleDropOnCollection}
renameable
@@ -237,84 +228,51 @@ const NavigationPanelCollectionNodeChildren = ({
collection: Collection;
}) => {
const t = useI18n();
const {
docsService,
compatibleFavoriteItemsAdapter,
shareDocsListService,
collectionService,
} = useServices({
DocsService,
CompatibleFavoriteItemsAdapter,
ShareDocsListService,
const { collectionService } = useServices({
CollectionService,
});
useEffect(() => {
// TODO(@eyhn): loading & error UI
shareDocsListService.shareDocs?.revalidate();
}, [shareDocsListService]);
const docMetas = useLiveData(
useMemo(
() =>
LiveData.computed(get => {
return get(docsService.list.docs$).map(
doc => get(doc.meta$) as DocMeta
);
}),
[docsService]
)
const allowList = useLiveData(
collection.allowList$.map(list => new Set(list))
);
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
const allowList = useMemo(
() => new Set(collection.allowList),
[collection.allowList]
);
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
const handleRemoveFromAllowList = useCallback(
(id: string) => {
track.$.navigationPanel.collections.removeOrganizeItem({ type: 'doc' });
collectionService.deletePageFromCollection(collection.id, id);
collectionService.removeDocFromCollection(collection.id, id);
toast(t['com.affine.collection.removePage.success']());
},
[collection.id, collectionService, t]
);
const filtered = docMetas.filter(meta => {
if (meta.trash) return false;
const publicMode = shareDocs?.find(d => d.id === meta.id)?.mode;
const pageData = {
meta: meta as DocMeta,
publicMode:
publicMode === PublicDocMode.Edgeless
? ('edgeless' as const)
: publicMode === PublicDocMode.Page
? ('page' as const)
: undefined,
favorite: favourites.some(fav => fav.id === meta.id),
};
return filterPage(collection, pageData);
});
const [filteredDocIds, setFilteredDocIds] = useState<string[]>([]);
return filtered.map(doc => (
useEffect(() => {
const subscription = collection.watch().subscribe(docIds => {
setFilteredDocIds(docIds);
});
return () => subscription.unsubscribe();
}, [collection]);
return filteredDocIds.map(docId => (
<NavigationPanelDocNode
key={doc.id}
docId={doc.id}
key={docId}
docId={docId}
reorderable={false}
location={{
at: 'navigation-panel:collection:filtered-docs',
collectionId: collection.id,
}}
operations={
allowList
allowList.has(docId)
? [
{
index: 99,
view: (
<MenuItem
prefixIcon={<FilterMinusIcon />}
onClick={() => handleRemoveFromAllowList(doc.id)}
onClick={() => handleRemoveFromAllowList(docId)}
>
{t['Remove special filter']()}
</MenuItem>
@@ -5,7 +5,6 @@ import {
useConfirmModal,
} from '@affine/component';
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { CollectionService } from '@affine/core/modules/collection';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
@@ -42,7 +41,6 @@ export const useNavigationPanelCollectionNodeOperations = (
CollectionService,
CompatibleFavoriteItemsAdapter,
});
const deleteInfo = useDeleteCollectionInfo();
const { createPage } = usePageHelper(
workspaceService.workspace.docCollection
@@ -59,7 +57,7 @@ export const useNavigationPanelCollectionNodeOperations = (
const createAndAddDocument = useCallback(() => {
const newDoc = createPage();
collectionService.addPageToCollection(collectionId, newDoc.id);
collectionService.addDocToCollection(collectionId, newDoc.id);
track.$.navigationPanel.collections.createDoc();
track.$.navigationPanel.collections.addDocToCollection({
control: 'button',
@@ -100,11 +98,11 @@ export const useNavigationPanelCollectionNodeOperations = (
}, [collectionId, workbenchService.workbench]);
const handleDeleteCollection = useCallback(() => {
collectionService.deleteCollection(deleteInfo, collectionId);
collectionService.deleteCollection(collectionId);
track.$.navigationPanel.organize.deleteOrganizeItem({
type: 'collection',
});
}, [collectionId, collectionService, deleteInfo]);
}, [collectionId, collectionService]);
const handleShowEdit = useCallback(() => {
onOpenEdit();
@@ -1,5 +1,4 @@
import { IconButton, usePromptModal } from '@affine/component';
import { createEmptyCollection } from '@affine/core/components/page-list/use-collection-manager';
import { CollectionService } from '@affine/core/modules/collection';
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
import { WorkbenchService } from '@affine/core/modules/workbench';
@@ -7,7 +6,6 @@ import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import { AddCollectionIcon } from '@blocksuite/icons/rc';
import { useLiveData, useServices } from '@toeverything/infra';
import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { CollapsibleSection } from '../../layouts/collapsible-section';
@@ -46,8 +44,7 @@ export const NavigationPanelCollections = () => {
variant: 'primary',
},
onConfirm(name) {
const id = nanoid();
collectionService.addCollection(createEmptyCollection(id, { name }));
const id = collectionService.createCollection({ name });
track.$.navigationPanel.organize.createOrganizeItem({
type: 'collection',
});
@@ -84,7 +81,7 @@ export const NavigationPanelCollections = () => {
<NavigationPanelTreeRoot
placeholder={<RootEmpty onClickCreate={handleCreateCollection} />}
>
{collections.map(collection => (
{Array.from(collections.values()).map(collection => (
<NavigationPanelCollectionNode
key={collection.id}
collectionId={collection.id}
@@ -1,7 +1,7 @@
import { Button, RadioGroup } from '@affine/component';
import { useAllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
import type { Collection } from '@affine/env/filter';
import type { CollectionInfo } from '@affine/core/modules/collection';
import { useI18n } from '@affine/i18n';
import { useCallback, useMemo, useState } from 'react';
@@ -12,10 +12,10 @@ export type EditCollectionMode = 'page' | 'rule';
export interface EditCollectionProps {
onConfirmText?: string;
init: Collection;
init: CollectionInfo;
mode?: EditCollectionMode;
onCancel: () => void;
onConfirm: (collection: Collection) => void;
onConfirm: (collection: CollectionInfo) => void;
}
export const EditCollection = ({
@@ -27,9 +27,9 @@ export const EditCollection = ({
}: EditCollectionProps) => {
const t = useI18n();
const config = useAllPageListConfig();
const [value, onChange] = useState<Collection>(init);
const [value, onChange] = useState<CollectionInfo>(init);
const [mode, setMode] = useState<'page' | 'rule'>(
initMode ?? (init.filterList.length === 0 ? 'page' : 'rule')
initMode ?? (init.rules.filters.length === 0 ? 'page' : 'rule')
);
const isNameEmpty = useMemo(() => value.name.trim().length === 0, [value]);
const onSaveCollection = useCallback(() => {
@@ -40,10 +40,10 @@ export const EditCollection = ({
const reset = useCallback(() => {
onChange({
...value,
filterList: init.filterList,
rules: init.rules,
allowList: init.allowList,
});
}, [init.allowList, init.filterList, value]);
}, [init, value]);
const onIdsChange = useCallback(
(ids: string[]) => {
onChange({ ...value, allowList: ids });
@@ -1,8 +1,10 @@
import { Modal } from '@affine/component';
import { CollectionService } from '@affine/core/modules/collection';
import {
type CollectionInfo,
CollectionService,
} from '@affine/core/modules/collection';
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
import type { Collection } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react';
@@ -18,17 +20,18 @@ export const CollectionEditorDialog = ({
const collectionService = useService(CollectionService);
const collection = useLiveData(collectionService.collection$(collectionId));
const onConfirmOnCollection = useCallback(
(collection: Collection) => {
collectionService.updateCollection(collection.id, () => collection);
(collection: CollectionInfo) => {
collectionService.updateCollection(collection.id, collection);
close();
},
[close, collectionService]
);
const info = useLiveData(collection?.info$);
const onCancel = useCallback(() => {
close();
}, [close]);
if (!collection) {
if (!collection || !info) {
return null;
}
@@ -50,7 +53,7 @@ export const CollectionEditorDialog = ({
>
<EditCollection
onConfirmText={t['com.affine.editCollection.save']()}
init={collection}
init={info}
mode={mode}
onCancel={onCancel}
onConfirm={onConfirmOnCollection}
@@ -1,16 +1,15 @@
import { Button, IconButton, Tooltip } from '@affine/component';
import { Filters } from '@affine/core/components/filter';
import type { AllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
import {
AffineShapeIcon,
FilterList,
filterPageByRules,
List,
type ListItem,
ListScrollContainer,
} from '@affine/core/components/page-list';
import type { CollectionInfo } from '@affine/core/modules/collection';
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
import { DocsService } from '@affine/core/modules/doc';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import type { Collection } from '@affine/env/filter';
import { Trans, useI18n } from '@affine/i18n';
import type { DocMeta } from '@blocksuite/affine/store';
import {
@@ -19,11 +18,11 @@ import {
PageIcon,
ToggleRightIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import clsx from 'clsx';
import type { ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import * as styles from './edit-collection.css';
@@ -35,8 +34,8 @@ export const RulesMode = ({
switchMode,
allPageListConfig,
}: {
collection: Collection;
updateCollection: (collection: Collection) => void;
collection: CollectionInfo;
updateCollection: (collection: CollectionInfo) => void;
reset: () => void;
buttons: ReactNode;
switchMode: ReactNode;
@@ -44,30 +43,50 @@ export const RulesMode = ({
}) => {
const t = useI18n();
const [showPreview, setShowPreview] = useState(true);
const allowListPages: DocMeta[] = [];
const rulesPages: DocMeta[] = [];
const docsService = useService(DocsService);
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
const favorites = useLiveData(favAdapter.favorites$);
allPageListConfig.allPages.forEach(meta => {
if (meta.trash) {
return;
}
const pageData = {
meta,
publicMode: allPageListConfig.getPublicMode(meta.id),
favorite: favorites.some(f => f.id === meta.id),
const collectionRulesService = useService(CollectionRulesService);
const [rulesPageIds, setRulesPageIds] = useState<string[]>([]);
useEffect(() => {
const subscription = collectionRulesService
.watch(
collection.rules.filters.length > 0
? [
...collection.rules.filters,
{
type: 'system',
key: 'trash',
method: 'is',
value: 'false',
},
]
: [],
undefined,
undefined
)
.subscribe(rules => {
setRulesPageIds(rules.groups.flatMap(group => group.items));
});
return () => {
subscription.unsubscribe();
};
if (
collection.filterList.length &&
filterPageByRules(collection.filterList, [], pageData)
) {
rulesPages.push(meta);
}
if (collection.allowList.includes(meta.id)) {
allowListPages.push(meta);
}
});
}, [collection, collectionRulesService]);
const rulesPages = useMemo(() => {
return allPageListConfig.allPages.filter(meta => {
return rulesPageIds.includes(meta.id);
});
}, [allPageListConfig.allPages, rulesPageIds]);
const allowListPages = useMemo(() => {
return allPageListConfig.allPages.filter(meta => {
return (
collection.allowList.includes(meta.id) &&
!rulesPageIds.includes(meta.id)
);
});
}, [allPageListConfig.allPages, collection.allowList, rulesPageIds]);
const [expandInclude, setExpandInclude] = useState(
collection.allowList.length > 0
);
@@ -113,13 +132,17 @@ export const RulesMode = ({
overflowY: 'auto',
}}
>
<FilterList
propertiesMeta={allPageListConfig.docCollection.meta.properties}
value={collection.filterList}
onChange={useCallback(
filterList => updateCollection({ ...collection, filterList }),
[collection, updateCollection]
)}
<Filters
filters={collection.rules.filters}
onChange={filters => {
updateCollection({
...collection,
rules: {
...collection.rules,
filters,
},
});
}}
/>
<div className={styles.rulesContainerLeftContentInclude}>
{collection.allowList.length > 0 ? (
@@ -215,7 +238,7 @@ export const RulesMode = ({
></List>
) : (
<RulesEmpty
noRules={collection.filterList.length === 0}
noRules={collection.rules.filters.length === 0}
fullHeight={allowListPages.length === 0}
/>
)}
@@ -2,14 +2,16 @@ import { Modal, toast } from '@affine/component';
import {
collectionHeaderColsDef,
CollectionListItemRenderer,
type CollectionMeta,
FavoriteTag,
type ListItem,
ListTableHeader,
VirtualizedList,
} from '@affine/core/components/page-list';
import { SelectorLayout } from '@affine/core/components/page-list/selector/selector-layout';
import { CollectionService } from '@affine/core/modules/collection';
import {
type CollectionMeta,
CollectionService,
} from '@affine/core/modules/collection';
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
@@ -52,22 +54,15 @@ export const CollectionSelectorDialog = ({
const collectionService = useService(CollectionService);
const workspace = useService(WorkspaceService).workspace;
const collections = useLiveData(collectionService.collections$);
const collections = useLiveData(collectionService.collectionMetas$);
const [selection, setSelection] = useState(selectedCollectionIds);
const [keyword, setKeyword] = useState('');
const collectionMetas = useMemo(() => {
const collectionsList: CollectionMeta[] = collections
.map(collection => {
return {
...collection,
title: collection.name,
};
})
.filter(meta => {
const reg = new RegExp(keyword, 'i');
return reg.test(meta.title);
});
const collectionsList: CollectionMeta[] = collections.filter(meta => {
const reg = new RegExp(keyword, 'i');
return reg.test(meta.title);
});
return collectionsList;
}, [collections, keyword]);
@@ -1,9 +1,7 @@
import { usePromptModal } from '@affine/component';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import type { CollectionMeta } from '@affine/core/components/page-list';
import {
CollectionListHeader,
createEmptyCollection,
VirtualizedCollectionList,
} from '@affine/core/components/page-list';
import {
@@ -13,8 +11,7 @@ import {
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { nanoid } from 'nanoid';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import { CollectionService } from '../../../../modules/collection';
import { ViewBody, ViewHeader } from '../../../../modules/workbench';
@@ -31,16 +28,6 @@ export const AllCollection = () => {
const collectionService = useService(CollectionService);
const collections = useLiveData(collectionService.collections$);
const collectionMetas = useMemo(() => {
const collectionsList: CollectionMeta[] = collections.map(collection => {
return {
...collection,
title: collection.name,
};
});
return collectionsList;
}, [collections]);
const navigateHelper = useNavigateHelper();
const { openPromptModal } = usePromptModal();
@@ -62,8 +49,7 @@ export const AllCollection = () => {
variant: 'primary',
},
onConfirm(name) {
const id = nanoid();
collectionService.addCollection(createEmptyCollection(id, { name }));
const id = collectionService.createCollection({ name });
navigateHelper.jumpToCollection(currentWorkspace.id, id);
},
});
@@ -87,10 +73,8 @@ export const AllCollection = () => {
</ViewHeader>
<ViewBody>
<div className={styles.body}>
{collectionMetas.length > 0 ? (
{collections.size > 0 ? (
<VirtualizedCollectionList
collections={collections}
collectionMetas={collectionMetas}
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
handleCreateCollection={handleCreateCollection}
/>
@@ -1,55 +0,0 @@
import { CollectionService } from '@affine/core/modules/collection';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { Collection, Filter } from '@affine/env/filter';
import { useService } from '@toeverything/infra';
import { useCallback } from 'react';
import { filterContainerStyle } from '../../../../components/filter-container.css';
import { useNavigateHelper } from '../../../../components/hooks/use-navigate-helper';
import {
FilterList,
SaveAsCollectionButton,
} from '../../../../components/page-list';
export const FilterContainer = ({
filters,
onChangeFilters,
}: {
filters: Filter[];
onChangeFilters: (filters: Filter[]) => void;
}) => {
const currentWorkspace = useService(WorkspaceService).workspace;
const navigateHelper = useNavigateHelper();
const collectionService = useService(CollectionService);
const saveToCollection = useCallback(
(collection: Collection) => {
collectionService.addCollection({
...collection,
filterList: filters,
});
navigateHelper.jumpToCollection(currentWorkspace.id, collection.id);
},
[collectionService, filters, navigateHelper, currentWorkspace.id]
);
if (!filters.length) {
return null;
}
return (
<div className={filterContainerStyle}>
<div style={{ flex: 1 }}>
<FilterList
propertiesMeta={currentWorkspace.docCollection.meta.properties}
value={filters}
onChange={onChangeFilters}
/>
</div>
<div>
{filters.length > 0 ? (
<SaveAsCollectionButton onConfirm={saveToCollection} />
) : null}
</div>
</div>
);
};
@@ -1,7 +1,6 @@
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
import {
AllPageListOperationsMenu,
PageDisplayMenu,
PageListNewPageButton,
} from '@affine/core/components/page-list';
@@ -10,7 +9,6 @@ import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { inferOpenMode } from '@affine/core/utils';
import type { Filter } from '@affine/env/filter';
import { track } from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc';
import { useServices } from '@toeverything/infra';
@@ -21,12 +19,8 @@ import * as styles from './all-page.css';
export const AllPageHeader = ({
showCreateNew,
filters,
onChangeFilters,
}: {
showCreateNew: boolean;
filters: Filter[];
onChangeFilters: (filters: Filter[]) => void;
}) => {
const { workspaceService, workspaceDialogService, workbenchService } =
useServices({
@@ -90,11 +84,6 @@ export const AllPageHeader = ({
>
<PlusIcon />
</PageListNewPageButton>
<AllPageListOperationsMenu
filterList={filters}
onChangeFilterList={onChangeFilters}
propertiesMeta={workspace.docCollection.meta.properties}
/>
<PageDisplayMenu />
</>
}
@@ -1,17 +1,15 @@
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
import {
PageListHeader,
useFilteredPageMetas,
VirtualizedPageList,
} from '@affine/core/components/page-list';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { IntegrationService } from '@affine/core/modules/integration';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { Filter } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
useIsActiveView,
@@ -23,7 +21,6 @@ import {
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
import { EmptyPageList } from '../page-list-empty';
import * as styles from './all-page.css';
import { FilterContainer } from './all-page-filter';
import { AllPageHeader } from './all-page-header';
export const AllPage = () => {
@@ -37,10 +34,10 @@ export const AllPage = () => {
const isOwner = useLiveData(permissionService.permission.isOwner$);
const importing = useLiveData(integrationService.importing$);
const [filters, setFilters] = useState<Filter[]>([]);
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
filters: filters,
});
const filteredPageMetas = useMemo(
() => pageMetas.filter(page => !page.trash),
[pageMetas]
);
const isActiveView = useIsActiveView();
@@ -66,20 +63,14 @@ export const AllPage = () => {
<ViewTitle title={t['All pages']()} />
<ViewIcon icon="allDocs" />
<ViewHeader>
<AllPageHeader
showCreateNew={!hideHeaderCreateNew}
filters={filters}
onChangeFilters={setFilters}
/>
<AllPageHeader showCreateNew={!hideHeaderCreateNew} />
</ViewHeader>
<ViewBody>
<div className={styles.body}>
<FilterContainer filters={filters} onChangeFilters={setFilters} />
{filteredPageMetas.length > 0 ? (
<VirtualizedPageList
disableMultiDelete={!isAdmin && !isOwner}
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
filters={filters}
/>
) : (
<EmptyPageList type="all" heading={<PageListHeader />} />
@@ -1,12 +1,13 @@
import { notify } from '@affine/component';
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty/collection-detail';
import { VirtualizedPageList } from '@affine/core/components/page-list';
import { CollectionService } from '@affine/core/modules/collection';
import {
type Collection,
CollectionService,
} from '@affine/core/modules/collection';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { Collection } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
import { ViewLayersIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService, useServices } from '@toeverything/infra';
@@ -21,6 +22,7 @@ import {
ViewIcon,
ViewTitle,
} from '../../../../modules/workbench';
import { PageNotFound } from '../../404';
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
import { CollectionDetailHeader } from './header';
@@ -68,30 +70,16 @@ export const Component = function CollectionPage() {
GlobalContextService,
});
const globalContext = globalContextService.globalContext;
const collections = useLiveData(collectionService.collections$);
const navigate = useNavigateHelper();
const t = useI18n();
const params = useParams();
const workspace = useService(WorkspaceService).workspace;
const collection = collections.find(v => v.id === params.collectionId);
const collection = useLiveData(
params.collectionId
? collectionService.collection$(params.collectionId)
: null
);
const name = useLiveData(collection?.name$);
const isActiveView = useIsActiveView();
const notifyCollectionDeleted = useCallback(() => {
navigate.jumpToPage(workspace.id, 'all');
const collection = collectionService.collectionsTrash$.value.find(
v => v.collection.id === params.collectionId
);
let text = 'Collection does not exist';
if (collection) {
if (collection.userId) {
text = `${collection.collection.name} has been deleted by ${collection.userName}`;
} else {
text = `${collection.collection.name} has been deleted`;
}
}
return notify.error({ title: text });
}, [collectionService, navigate, params.collectionId, workspace.id]);
useEffect(() => {
if (isActiveView && collection) {
globalContext.collectionId.set(collection.id);
@@ -105,25 +93,22 @@ export const Component = function CollectionPage() {
return;
}, [collection, globalContext, isActiveView]);
useEffect(() => {
if (!collection) {
notifyCollectionDeleted();
}
}, [collection, notifyCollectionDeleted]);
const info = useLiveData(collection?.info$);
if (!collection) {
return null;
return <PageNotFound />;
}
const inner = isEmptyCollection(collection) ? (
<Placeholder collection={collection} />
) : (
<CollectionDetail collection={collection} />
);
const inner =
info?.allowList.length === 0 && info?.rules.filters.length === 0 ? (
<Placeholder collection={collection} />
) : (
<CollectionDetail collection={collection} />
);
return (
<>
<ViewIcon icon="collection" />
<ViewTitle title={collection.name} />
<ViewTitle title={name ?? t['Untitled']()} />
<AllDocSidebarTabs />
{inner}
</>
@@ -134,6 +119,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
const workspace = useService(WorkspaceService).workspace;
const { jumpToCollections } = useNavigateHelper();
const t = useI18n();
const name = useLiveData(collection?.name$);
const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspace.id);
@@ -176,7 +162,7 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
['WebkitAppRegion' as string]: 'no-drag',
}}
>
{collection.name}
{name ?? t['Untitled']()}
</div>
<div style={{ flex: 1 }} />
</div>
@@ -190,9 +176,3 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
</>
);
};
export const isEmptyCollection = (collection: Collection) => {
return (
collection.allowList.length === 0 && collection.filterList.length === 0
);
};
@@ -1,16 +1,14 @@
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
import {
useFilteredPageMetas,
VirtualizedTrashList,
} from '@affine/core/components/page-list';
import { VirtualizedTrashList } from '@affine/core/components/page-list';
import { Header } from '@affine/core/components/pure/header';
import { DocsService } from '@affine/core/modules/doc';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { DeleteIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect } from 'react';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import { useEffect, useMemo } from 'react';
import {
useIsActiveView,
@@ -43,11 +41,15 @@ export const TrashPage = () => {
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
const isOwner = useLiveData(permissionService.permission.isOwner$);
const docCollection = currentWorkspace.docCollection;
const docsService = useService(DocsService);
const allTrashPageIds = useLiveData(
LiveData.from(docsService.allTrashDocIds$(), [])
);
const pageMetas = useBlockSuiteDocMeta(docCollection);
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
trash: true,
});
const filteredPageMetas = useMemo(() => {
return pageMetas.filter(page => allTrashPageIds.includes(page.id));
}, [pageMetas, allTrashPageIds]);
const isActiveView = useIsActiveView();