From 1ff6af85f5eefeca1096b3ee915cb47839d0cee4 Mon Sep 17 00:00:00 2001
From: JimmFly <447268514@qq.com>
Date: Mon, 25 Mar 2024 07:53:33 +0000
Subject: [PATCH] feat(core): add page group and display properties (#6228)
close TOV-23
https://github.com/toeverything/AFFiNE/assets/102217452/c05474de-b73c-40ab-9f18-cc43bb9fd828
---
.../virtualized-collection-list.tsx | 1 -
.../components/list-header-cell.css.ts | 1 -
.../page-list/components/list-header-cell.tsx | 2 +
.../components/page-display-menu.css.ts | 75 +++++++
.../components/page-display-menu.tsx | 163 ++++++++++++++
.../page-list/docs/page-list-item.tsx | 26 ++-
.../page-list/docs/virtualized-page-list.tsx | 9 +-
.../page-list/group-definitions.css.ts | 40 ++++
.../page-list/group-definitions.tsx | 205 ++++++++++++++++++
.../components/page-list/header-col-def.tsx | 86 ++++----
.../core/src/components/page-list/index.tsx | 3 +
.../page-list/items-to-item-group.tsx | 92 +++-----
.../core/src/components/page-list/list.css.ts | 4 +
.../core/src/components/page-list/list.tsx | 4 +-
.../components/page-list/page-group.css.ts | 5 +-
.../src/components/page-list/page-header.tsx | 7 +-
.../src/components/page-list/scoped-atoms.tsx | 13 +-
.../page-list/tags/virtualized-tag-list.tsx | 1 -
.../core/src/components/page-list/types.ts | 18 +-
.../page-list/use-page-display-properties.ts | 29 +++
.../core/src/components/page-list/utils.tsx | 2 +
.../view/edit-collection/pages-mode.tsx | 6 +-
.../view/edit-collection/rules-mode.tsx | 2 -
.../view/edit-collection/select-page.tsx | 1 -
.../workspace/all-page/all-page-header.tsx | 22 +-
.../pages/workspace/all-page/all-page.css.ts | 1 +
.../core/src/pages/workspace/trash-page.tsx | 6 +-
packages/frontend/i18n/src/resources/en.json | 12 +-
tests/affine-local/e2e/all-page.spec.ts | 52 +++++
.../src/stories/page-list.stories.tsx | 8 +-
30 files changed, 742 insertions(+), 154 deletions(-)
create mode 100644 packages/frontend/core/src/components/page-list/components/page-display-menu.css.ts
create mode 100644 packages/frontend/core/src/components/page-list/components/page-display-menu.tsx
create mode 100644 packages/frontend/core/src/components/page-list/group-definitions.css.ts
create mode 100644 packages/frontend/core/src/components/page-list/group-definitions.tsx
create mode 100644 packages/frontend/core/src/components/page-list/use-page-display-properties.ts
diff --git a/packages/frontend/core/src/components/page-list/collections/virtualized-collection-list.tsx b/packages/frontend/core/src/components/page-list/collections/virtualized-collection-list.tsx
index 400f2afa1..90b243fae 100644
--- a/packages/frontend/core/src/components/page-list/collections/virtualized-collection-list.tsx
+++ b/packages/frontend/core/src/components/page-list/collections/virtualized-collection-list.tsx
@@ -107,7 +107,6 @@ export const VirtualizedCollectionList = ({
ref={listRef}
selectable="toggle"
draggable={false}
- groupBy={false}
atTopThreshold={80}
atTopStateChange={setHideHeaderCreateNewCollection}
onSelectionActiveChange={setShowFloatingToolbar}
diff --git a/packages/frontend/core/src/components/page-list/components/list-header-cell.css.ts b/packages/frontend/core/src/components/page-list/components/list-header-cell.css.ts
index 6e70bc27e..d0cb77bf9 100644
--- a/packages/frontend/core/src/components/page-list/components/list-header-cell.css.ts
+++ b/packages/frontend/core/src/components/page-list/components/list-header-cell.css.ts
@@ -16,7 +16,6 @@ export const headerCell = style({
borderRight: `1px solid ${cssVar('hoverColorFilled')}`,
},
},
- display: 'flex',
alignItems: 'center',
columnGap: '4px',
position: 'relative',
diff --git a/packages/frontend/core/src/components/page-list/components/list-header-cell.tsx b/packages/frontend/core/src/components/page-list/components/list-header-cell.tsx
index 1fbde3409..5edde593e 100644
--- a/packages/frontend/core/src/components/page-list/components/list-header-cell.tsx
+++ b/packages/frontend/core/src/components/page-list/components/list-header-cell.tsx
@@ -22,6 +22,7 @@ export const ListHeaderCell = ({
alignment,
flex,
style,
+ hidden,
hideInSmallContainer,
children,
}: HeaderCellProps) => {
@@ -39,6 +40,7 @@ export const ListHeaderCell = ({
className={styles.headerCell}
data-sortable={sortable ? true : undefined}
data-sorting={sorting ? true : undefined}
+ hidden={hidden}
style={style}
role="columnheader"
hideInSmallContainer={hideInSmallContainer}
diff --git a/packages/frontend/core/src/components/page-list/components/page-display-menu.css.ts b/packages/frontend/core/src/components/page-list/components/page-display-menu.css.ts
new file mode 100644
index 000000000..183b3bbef
--- /dev/null
+++ b/packages/frontend/core/src/components/page-list/components/page-display-menu.css.ts
@@ -0,0 +1,75 @@
+import { cssVar } from '@toeverything/theme';
+import { style } from '@vanilla-extract/css';
+
+export const menu = style({
+ minWidth: '220px',
+});
+
+export const arrowDownSmallIcon = style({
+ width: '16px',
+ height: '16px',
+ color: cssVar('iconColor'),
+});
+
+export const headerDisplayButton = style({
+ marginLeft: '16px',
+ ['WebkitAppRegion' as string]: 'no-drag',
+});
+
+export const subMenuTrigger = style({
+ paddingRight: '8px',
+});
+
+export const subMenuItem = style({
+ fontSize: cssVar('fontXs'),
+ flexWrap: 'nowrap',
+ selectors: {
+ '&[data-active="true"]': {
+ color: cssVar('primaryColor'),
+ },
+ },
+});
+
+export const subMenuTriggerContent = style({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ gap: '8px',
+ fontWeight: 500,
+ fontSize: cssVar('fontXs'),
+});
+
+export const currentGroupType = style({
+ fontWeight: 400,
+ color: cssVar('textSecondaryColor'),
+});
+
+export const listOption = style({
+ padding: '4px 12px',
+ height: '28px',
+ fontSize: cssVar('fontXs'),
+ fontWeight: 500,
+ color: cssVar('textSecondaryColor'),
+ marginBottom: '4px',
+});
+export const properties = style({
+ padding: '4px 12px',
+ height: '28px',
+ fontSize: cssVar('fontXs'),
+});
+export const propertiesWrapper = style({
+ display: 'flex',
+ flexWrap: 'wrap',
+ maxWidth: '200px',
+ gap: '8px',
+ padding: '4px 12px',
+});
+
+export const propertyButton = style({
+ color: cssVar('textDisableColor'),
+ selectors: {
+ '&[data-active="true"]': {
+ color: cssVar('textPrimaryColor'),
+ },
+ },
+});
diff --git a/packages/frontend/core/src/components/page-list/components/page-display-menu.tsx b/packages/frontend/core/src/components/page-list/components/page-display-menu.tsx
new file mode 100644
index 000000000..ce422efb7
--- /dev/null
+++ b/packages/frontend/core/src/components/page-list/components/page-display-menu.tsx
@@ -0,0 +1,163 @@
+import {
+ Button,
+ Menu,
+ MenuItem,
+ MenuSeparator,
+ MenuSub,
+} from '@affine/component';
+import { useAFFiNEI18N } from '@affine/i18n/hooks';
+import { ArrowDownSmallIcon, DoneIcon } from '@blocksuite/icons';
+import { useAtom } from 'jotai';
+import { useCallback, useMemo } from 'react';
+
+import { pageGroupByTypeAtom } from '../group-definitions';
+import type { PageDisplayProperties, PageGroupByType } from '../types';
+import { usePageDisplayProperties } from '../use-page-display-properties';
+import * as styles from './page-display-menu.css';
+
+type GroupOption = {
+ value: PageGroupByType;
+ label: string;
+};
+
+export const PageDisplayMenu = () => {
+ const t = useAFFiNEI18N();
+ const [group, setGroup] = useAtom(pageGroupByTypeAtom);
+ const [properties, setProperties] = usePageDisplayProperties();
+ const handleSelect = useCallback(
+ (value: PageGroupByType) => {
+ setGroup(value);
+ },
+ [setGroup]
+ );
+ const propertyOptions: Array<{
+ key: keyof PageDisplayProperties;
+ onClick: () => void;
+ label: string;
+ }> = useMemo(() => {
+ return [
+ {
+ key: 'bodyNotes',
+ onClick: () => setProperties('bodyNotes', !properties['bodyNotes']),
+ label: t['com.affine.page.display.display-properties.body-notes'](),
+ },
+ {
+ key: 'tags',
+ onClick: () => setProperties('tags', !properties['tags']),
+ label: t['Tags'](),
+ },
+ {
+ key: 'createDate',
+ onClick: () => setProperties('createDate', !properties['createDate']),
+ label: t['Created'](),
+ },
+ {
+ key: 'updatedDate',
+ onClick: () => setProperties('updatedDate', !properties['updatedDate']),
+ label: t['Updated'](),
+ },
+ ];
+ }, [properties, setProperties, t]);
+
+ const items = useMemo(() => {
+ const groupOptions: GroupOption[] = [
+ {
+ value: 'createDate',
+ label: t['Created'](),
+ },
+ {
+ value: 'updatedDate',
+ label: t['Updated'](),
+ },
+ {
+ value: 'tag',
+ label: t['com.affine.page.display.grouping.group-by-tag'](),
+ },
+ {
+ value: 'favourites',
+ label: t['com.affine.page.display.grouping.group-by-favourites'](),
+ },
+ {
+ value: 'none',
+ label: t['com.affine.page.display.grouping.no-grouping'](),
+ },
+ ];
+
+ const subItems = groupOptions.map(option => (
+
+ ));
+
+ const currentGroupType = groupOptions.find(
+ option => option.value === group
+ )?.label;
+
+ return (
+ <>
+
+
+ {t['com.affine.page.display.grouping']()}
+ {currentGroupType}
+
+
+
+
+ {t['com.affine.page.display.list-option']()}
+
+
+ {t['com.affine.page.display.display-properties']()}
+
+
+ {propertyOptions.map(option => (
+
+ ))}
+
+ >
+ );
+ }, [group, handleSelect, properties, propertyOptions, t]);
+ return (
+
+ );
+};
diff --git a/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx b/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx
index 347af8120..70d0cbab3 100644
--- a/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx
+++ b/packages/frontend/core/src/components/page-list/docs/page-list-item.tsx
@@ -7,6 +7,7 @@ import { useCallback, useMemo } from 'react';
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
import type { DraggableTitleCellData, PageListItemProps } from '../types';
+import { usePageDisplayProperties } from '../use-page-display-properties';
import { ColWrapper, formatDate, stopPropagation } from '../utils';
import * as styles from './page-list-item.css';
import { PageTags } from './page-tags';
@@ -15,6 +16,7 @@ const ListTitleCell = ({
title,
preview,
}: Pick) => {
+ const [displayProperties] = usePageDisplayProperties();
return (
{title}
- {preview ? (
+ {preview && displayProperties['bodyNotes'] ? (
{
+ const [displayProperties] = usePageDisplayProperties();
const pageTitleElement = useMemo(() => {
return (
@@ -182,14 +185,29 @@ export const PageListItem = (props: PageListItemProps) => {
-
+
-
+
-
+
{props.operations ? (
diff --git a/packages/frontend/core/src/components/page-list/docs/virtualized-page-list.tsx b/packages/frontend/core/src/components/page-list/docs/virtualized-page-list.tsx
index c63feec05..3e8249c58 100644
--- a/packages/frontend/core/src/components/page-list/docs/virtualized-page-list.tsx
+++ b/packages/frontend/core/src/components/page-list/docs/virtualized-page-list.tsx
@@ -13,7 +13,8 @@ import { useCallback, useMemo, useRef, useState } from 'react';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
-import { pageHeaderColsDef } from '../header-col-def';
+import { usePageItemGroupDefinitions } from '../group-definitions';
+import { usePageHeaderColsDef } from '../header-col-def';
import { PageOperationCell } from '../operation-cell';
import { PageListItemRenderer } from '../page-group';
import { ListTableHeader } from '../page-header';
@@ -108,6 +109,7 @@ export const VirtualizedPageList = ({
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
const pageOperations = usePageOperationsRenderer();
const { isPreferredEdgeless } = usePageHelper(currentWorkspace.docCollection);
+ const pageHeaderColsDef = usePageHeaderColsDef();
const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
filters,
@@ -139,7 +141,7 @@ export const VirtualizedPageList = ({
const pageHeaderRenderer = useCallback(() => {
return ;
- }, []);
+ }, [pageHeaderColsDef]);
const pageItemRenderer = useCallback((item: ListItem) => {
return ;
@@ -179,6 +181,8 @@ export const VirtualizedPageList = ({
hideFloatingToolbar();
}, [filteredSelectedPageIds, hideFloatingToolbar, pageMetas, setTrashModal]);
+ const group = usePageItemGroupDefinitions();
+
return (
<>
(
+ 'pageGroupByType',
+ 'updatedDate'
+);
+
+const GroupLabel = ({
+ label,
+ count,
+ icon,
+ id,
+}: {
+ id: string;
+ label: string;
+ count: number;
+ icon?: ReactNode;
+}) => (
+
+ {icon}
+
+ {label}
+
+
{` ยท ${count}`}
+
+);
+
+// todo: optimize date matchers
+export const useDateGroupDefinitions = (
+ key: DateKey
+): ItemGroupDefinition[] => {
+ const t = useAFFiNEI18N();
+ return useMemo(
+ () => [
+ {
+ id: 'today',
+ label: count => (
+
+ ),
+ match: item =>
+ withinDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 1),
+ },
+ {
+ id: 'yesterday',
+ label: count => (
+
+ ),
+ match: item =>
+ betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 1, 2),
+ },
+ {
+ id: 'last7Days',
+ label: count => (
+
+ ),
+ match: item =>
+ betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 2, 7),
+ },
+ {
+ id: 'last30Days',
+ label: count => (
+
+ ),
+ match: item =>
+ betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 7, 30),
+ },
+ {
+ id: 'moreThan30Days',
+ label: count => (
+
+ ),
+ match: item =>
+ !withinDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 30),
+ },
+ ],
+ [key, t]
+ );
+};
+export const useTagGroupDefinitions = (): ItemGroupDefinition[] => {
+ const tagService = useService(TagService);
+ const tagMetas = useLiveData(tagService.tagMetas$);
+ return useMemo(() => {
+ return tagMetas.map(tag => ({
+ id: tag.id,
+ label: count => (
+
+ }
+ />
+ ),
+ match: item => (item as DocMeta).tags?.includes(tag.id),
+ }));
+ }, [tagMetas]);
+};
+
+export const useFavoriteGroupDefinitions = <
+ T extends ListItem,
+>(): ItemGroupDefinition
[] => {
+ const t = useAFFiNEI18N();
+ return useMemo(
+ () => [
+ {
+ id: 'favourited',
+ label: count => (
+ }
+ />
+ ),
+ match: item => !!(item as DocMeta).favorite,
+ },
+ {
+ id: 'notFavourited',
+ label: count => (
+ }
+ />
+ ),
+ match: item => !(item as DocMeta).favorite,
+ },
+ ],
+ [t]
+ );
+};
+
+export const usePageItemGroupDefinitions = () => {
+ const key = useAtomValue(pageGroupByTypeAtom);
+ const tagGroupDefinitions = useTagGroupDefinitions();
+ const createDateGroupDefinitions = useDateGroupDefinitions('createDate');
+ const updatedDateGroupDefinitions = useDateGroupDefinitions('updatedDate');
+ const favouriteGroupDefinitions = useFavoriteGroupDefinitions();
+
+ return useMemo(() => {
+ const itemGroupDefinitions = {
+ createDate: createDateGroupDefinitions,
+ updatedDate: updatedDateGroupDefinitions,
+ tag: tagGroupDefinitions,
+ favourites: favouriteGroupDefinitions,
+ none: undefined,
+
+ // add more here later
+ // todo: some page group definitions maybe dynamic
+ };
+ return itemGroupDefinitions[key];
+ }, [
+ createDateGroupDefinitions,
+ favouriteGroupDefinitions,
+ key,
+ tagGroupDefinitions,
+ updatedDateGroupDefinitions,
+ ]);
+};
diff --git a/packages/frontend/core/src/components/page-list/header-col-def.tsx b/packages/frontend/core/src/components/page-list/header-col-def.tsx
index 60268b834..bf9abe114 100644
--- a/packages/frontend/core/src/components/page-list/header-col-def.tsx
+++ b/packages/frontend/core/src/components/page-list/header-col-def.tsx
@@ -1,45 +1,55 @@
import { Trans } from '@affine/i18n';
+import { useMemo } from 'react';
import { ListHeaderTitleCell } from './page-header';
import type { HeaderColDef } from './types';
-
-export const pageHeaderColsDef: HeaderColDef[] = [
- {
- key: 'title',
- content: ,
- flex: 6,
- alignment: 'start',
- sortable: true,
- },
- {
- key: 'tags',
- content: ,
- flex: 3,
- alignment: 'end',
- },
- {
- key: 'createDate',
- content: ,
- flex: 1,
- sortable: true,
- alignment: 'end',
- hideInSmallContainer: true,
- },
- {
- key: 'updatedDate',
- content: ,
- flex: 1,
- sortable: true,
- alignment: 'end',
- hideInSmallContainer: true,
- },
- {
- key: 'actions',
- content: '',
- flex: 1,
- alignment: 'end',
- },
-];
+import { usePageDisplayProperties } from './use-page-display-properties';
+export const usePageHeaderColsDef = (): HeaderColDef[] => {
+ const [displayProperties] = usePageDisplayProperties();
+ return useMemo(
+ () => [
+ {
+ key: 'title',
+ content: ,
+ flex: 6,
+ alignment: 'start',
+ sortable: true,
+ },
+ {
+ key: 'tags',
+ content: ,
+ flex: 3,
+ alignment: 'end',
+ hidden: !displayProperties['tags'],
+ },
+ {
+ key: 'createDate',
+ content: ,
+ flex: 1,
+ sortable: true,
+ alignment: 'end',
+ hideInSmallContainer: true,
+ hidden: !displayProperties['createDate'],
+ },
+ {
+ key: 'updatedDate',
+ content: ,
+ flex: 1,
+ sortable: true,
+ alignment: 'end',
+ hideInSmallContainer: true,
+ hidden: !displayProperties['updatedDate'],
+ },
+ {
+ key: 'actions',
+ content: '',
+ flex: 1,
+ alignment: 'end',
+ },
+ ],
+ [displayProperties]
+ );
+};
export const collectionHeaderColsDef: HeaderColDef[] = [
{
diff --git a/packages/frontend/core/src/components/page-list/index.tsx b/packages/frontend/core/src/components/page-list/index.tsx
index 44102578b..0bd64b16b 100644
--- a/packages/frontend/core/src/components/page-list/index.tsx
+++ b/packages/frontend/core/src/components/page-list/index.tsx
@@ -2,10 +2,12 @@ export * from './collections';
export * from './components/favorite-tag';
export * from './components/floating-toolbar';
export * from './components/new-page-button';
+export * from './components/page-display-menu';
export * from './docs';
export * from './docs/page-list-item';
export * from './docs/page-tags';
export * from './filter';
+export * from './group-definitions';
export * from './header-col-def';
export * from './list';
export * from './operation-cell';
@@ -16,6 +18,7 @@ export * from './tags';
export * from './types';
export * from './use-collection-manager';
export * from './use-filtered-page-metas';
+export * from './use-page-display-properties';
export * from './utils';
export * from './view';
export * from './virtualized-list';
diff --git a/packages/frontend/core/src/components/page-list/items-to-item-group.tsx b/packages/frontend/core/src/components/page-list/items-to-item-group.tsx
index 25bdb3d50..cdac06919 100644
--- a/packages/frontend/core/src/components/page-list/items-to-item-group.tsx
+++ b/packages/frontend/core/src/components/page-list/items-to-item-group.tsx
@@ -1,61 +1,10 @@
-import { Trans } from '@affine/i18n';
-
-import type {
- DateKey,
- ItemGroupDefinition,
- ItemGroupProps,
- ListItem,
-} from './types';
-import { betweenDaysAgo, withinDaysAgo } from './utils';
-
-// todo: optimize date matchers
-const getDateGroupDefinitions = (
- key: DateKey
-): ItemGroupDefinition[] => [
- {
- id: 'today',
- label: ,
- match: item =>
- withinDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 1),
- },
- {
- id: 'yesterday',
- label: ,
- match: item =>
- betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 1, 2),
- },
- {
- id: 'last7Days',
- label: ,
- match: item =>
- betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 2, 7),
- },
- {
- id: 'last30Days',
- label: ,
- match: item =>
- betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 7, 30),
- },
- {
- id: 'moreThan30Days',
- label: ,
- match: item =>
- !withinDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 30),
- },
-];
-
-const itemGroupDefinitions = {
- createDate: getDateGroupDefinitions('createDate'),
- updatedDate: getDateGroupDefinitions('updatedDate'),
- // add more here later
- // todo: some page group definitions maybe dynamic
-};
+import type { ItemGroupDefinition, ItemGroupProps, ListItem } from './types';
export function itemsToItemGroups(
items: T[],
- key?: DateKey
+ groupDefs?: ItemGroupDefinition[] | false
): ItemGroupProps[] {
- if (!key) {
+ if (!groupDefs) {
return [
{
id: 'all',
@@ -66,8 +15,12 @@ export function itemsToItemGroups(
}
// assume pages are already sorted, we will use the page order to determine the group order
- const groupDefs = itemGroupDefinitions[key];
- const groups: ItemGroupProps[] = [];
+ let groups: ItemGroupProps[] = groupDefs.map(groupDef => ({
+ id: groupDef.id,
+ label: undefined, // Will be set later
+ items: [],
+ allItems: items,
+ }));
for (const item of items) {
// for a single page, there could be multiple groups that it belongs to
@@ -76,19 +29,24 @@ export function itemsToItemGroups(
const group = groups.find(g => g.id === groupDef.id);
if (group) {
group.items.push(item);
- } else {
- const label =
- typeof groupDef.label === 'function'
- ? groupDef.label()
- : groupDef.label;
- groups.push({
- id: groupDef.id,
- label: label,
- items: [item],
- allItems: items,
- });
}
}
}
+
+ // Now that all items have been added to groups, we can get the correct label for each group
+ groups = groups
+ .map(group => {
+ const groupDef = groupDefs.find(def => def.id === group.id);
+ if (groupDef) {
+ if (typeof groupDef.label === 'function') {
+ group.label = groupDef.label(group.items.length);
+ } else {
+ group.label = groupDef.label;
+ }
+ }
+ return group;
+ })
+ .filter(group => group.items.length > 0);
+
return groups;
}
diff --git a/packages/frontend/core/src/components/page-list/list.css.ts b/packages/frontend/core/src/components/page-list/list.css.ts
index 776bb2322..6b0af3dc6 100644
--- a/packages/frontend/core/src/components/page-list/list.css.ts
+++ b/packages/frontend/core/src/components/page-list/list.css.ts
@@ -37,6 +37,10 @@ export const hideInSmallContainer = style({
},
},
});
+
+export const hidden = style({
+ display: 'none',
+});
export const favoriteCell = style({
display: 'flex',
alignItems: 'center',
diff --git a/packages/frontend/core/src/components/page-list/list.tsx b/packages/frontend/core/src/components/page-list/list.tsx
index f13b6e9a0..52cbae8d6 100644
--- a/packages/frontend/core/src/components/page-list/list.tsx
+++ b/packages/frontend/core/src/components/page-list/list.tsx
@@ -10,7 +10,7 @@ import {
useRef,
} from 'react';
-import { pageHeaderColsDef } from './header-col-def';
+import { usePageHeaderColsDef } from './header-col-def';
import * as styles from './list.css';
import { ItemGroup } from './page-group';
import { ListTableHeader } from './page-header';
@@ -134,7 +134,7 @@ ListInnerWrapper.displayName = 'ListInnerWrapper';
const ListInner = (props: ListProps) => {
const groups = useAtomValue(groupsAtom);
-
+ const pageHeaderColsDef = usePageHeaderColsDef();
const hideHeader = props.hideHeader;
return (
diff --git a/packages/frontend/core/src/components/page-list/page-group.css.ts b/packages/frontend/core/src/components/page-list/page-group.css.ts
index 529408ac5..663c6c95e 100644
--- a/packages/frontend/core/src/components/page-list/page-group.css.ts
+++ b/packages/frontend/core/src/components/page-list/page-group.css.ts
@@ -45,7 +45,6 @@ export const header = style({
display: 'flex',
alignItems: 'center',
padding: '0px 16px 0px 6px',
- gap: 4,
height: '28px',
background: cssVar('backgroundPrimaryColor'),
':hover': {
@@ -88,6 +87,8 @@ export const selectAllButton = style({
});
export const collapsedIcon = style({
opacity: 0,
+ fontSize: '20px',
+ color: cssVar('iconColor'),
transition: 'transform 0.2s ease-in-out',
selectors: {
'&[data-collapsed="false"]': {
@@ -99,8 +100,6 @@ export const collapsedIcon = style({
},
});
export const collapsedIconContainer = style({
- width: '16px',
- height: '16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
diff --git a/packages/frontend/core/src/components/page-list/page-header.tsx b/packages/frontend/core/src/components/page-list/page-header.tsx
index 976830bc7..f9f322503 100644
--- a/packages/frontend/core/src/components/page-list/page-header.tsx
+++ b/packages/frontend/core/src/components/page-list/page-header.tsx
@@ -114,6 +114,7 @@ export const ListTableHeader = ({
data-selection-active={selectionState.selectionActive}
>
{headerCols.map(col => {
+ const isTagHidden = col.key === 'tags' && col.hidden;
return (
{col.content}
diff --git a/packages/frontend/core/src/components/page-list/scoped-atoms.tsx b/packages/frontend/core/src/components/page-list/scoped-atoms.tsx
index 83c1b9ada..aea572421 100644
--- a/packages/frontend/core/src/components/page-list/scoped-atoms.tsx
+++ b/packages/frontend/core/src/components/page-list/scoped-atoms.tsx
@@ -186,20 +186,9 @@ export const sorterAtom = atom(
);
export const groupsAtom = atom(get => {
- let groupBy = get(selectAtom(listPropsAtom, props => props.groupBy));
+ const groupBy = get(selectAtom(listPropsAtom, props => props.groupBy));
const sorter = get(sorterAtom);
- if (groupBy === false) {
- groupBy = undefined;
- } else if (groupBy === undefined) {
- groupBy =
- sorter.key === 'createDate' || sorter.key === 'updatedDate'
- ? sorter.key
- : // default sort
- !sorter.key
- ? DEFAULT_SORT_KEY
- : undefined;
- }
return itemsToItemGroups(sorter.items, groupBy);
});
diff --git a/packages/frontend/core/src/components/page-list/tags/virtualized-tag-list.tsx b/packages/frontend/core/src/components/page-list/tags/virtualized-tag-list.tsx
index d438ad010..5a7a3c18a 100644
--- a/packages/frontend/core/src/components/page-list/tags/virtualized-tag-list.tsx
+++ b/packages/frontend/core/src/components/page-list/tags/virtualized-tag-list.tsx
@@ -84,7 +84,6 @@ export const VirtualizedTagList = ({
ref={listRef}
selectable="toggle"
draggable={false}
- groupBy={false}
atTopThreshold={80}
onSelectionActiveChange={setShowFloatingToolbar}
heading={}
diff --git a/packages/frontend/core/src/components/page-list/types.ts b/packages/frontend/core/src/components/page-list/types.ts
index 6c4925536..436d55d32 100644
--- a/packages/frontend/core/src/components/page-list/types.ts
+++ b/packages/frontend/core/src/components/page-list/types.ts
@@ -82,6 +82,12 @@ export interface SortBy {
}
export type DateKey = 'createDate' | 'updatedDate';
+export type PageGroupByType =
+ | 'createDate'
+ | 'updatedDate'
+ | 'tag'
+ | 'favourites'
+ | 'none';
export interface ListProps {
// required data:
@@ -89,7 +95,7 @@ export interface ListProps {
docCollection: DocCollection;
className?: string;
hideHeader?: boolean; // whether or not to hide the header. default is false (showing header)
- groupBy?: ItemGroupByType | false;
+ groupBy?: ItemGroupDefinition[];
isPreferredEdgeless?: (pageId: string) => boolean; // determines the icon used for each row
rowAsLink?: boolean;
selectable?: 'toggle' | boolean; // show selection checkbox. toggle means showing a toggle selection in header on click; boolean == true means showing a selection checkbox for each item
@@ -117,7 +123,7 @@ export interface ItemListHandle {
export interface ItemGroupDefinition {
id: string;
// using a function to render custom group header
- label: (() => ReactNode) | ReactNode;
+ label: ((count: number) => ReactNode) | ReactNode;
match: (item: T) => boolean;
}
@@ -146,6 +152,7 @@ export type HeaderColDef = {
alignment?: ColWrapperProps['alignment'];
sortable?: boolean;
hideInSmallContainer?: boolean;
+ hidden?: boolean;
};
export type ColWrapperProps = PropsWithChildren<{
@@ -155,3 +162,10 @@ export type ColWrapperProps = PropsWithChildren<{
hideInSmallContainer?: boolean;
}> &
React.HTMLAttributes;
+
+export type PageDisplayProperties = {
+ bodyNotes: boolean;
+ tags: boolean;
+ createDate: boolean;
+ updatedDate: boolean;
+};
diff --git a/packages/frontend/core/src/components/page-list/use-page-display-properties.ts b/packages/frontend/core/src/components/page-list/use-page-display-properties.ts
new file mode 100644
index 000000000..56bf60653
--- /dev/null
+++ b/packages/frontend/core/src/components/page-list/use-page-display-properties.ts
@@ -0,0 +1,29 @@
+import { useAtom } from 'jotai';
+import { atomWithStorage } from 'jotai/utils';
+import { useCallback } from 'react';
+
+import type { PageDisplayProperties } from './types';
+
+export const pageDisplayPropertiesAtom = atomWithStorage(
+ 'pageDisplayProperties',
+ {
+ bodyNotes: true,
+ tags: true,
+ createDate: true,
+ updatedDate: true,
+ }
+);
+
+export const usePageDisplayProperties = (): [
+ PageDisplayProperties,
+ (key: keyof PageDisplayProperties, value: boolean) => void,
+] => {
+ const [properties, setProperties] = useAtom(pageDisplayPropertiesAtom);
+ const onChange = useCallback(
+ (key: keyof PageDisplayProperties, value: boolean) => {
+ setProperties(prev => ({ ...prev, [key]: value }));
+ },
+ [setProperties]
+ );
+ return [properties, onChange];
+};
diff --git a/packages/frontend/core/src/components/page-list/utils.tsx b/packages/frontend/core/src/components/page-list/utils.tsx
index af8664d0b..297681322 100644
--- a/packages/frontend/core/src/components/page-list/utils.tsx
+++ b/packages/frontend/core/src/components/page-list/utils.tsx
@@ -75,6 +75,7 @@ export const ColWrapper = forwardRef(
flex,
alignment,
hideInSmallContainer,
+ hidden,
className,
style,
children,
@@ -95,6 +96,7 @@ export const ColWrapper = forwardRef(
}}
data-hide-item={hideInSmallContainer ? true : undefined}
className={clsx(className, styles.colWrapper, {
+ [styles.hidden]: hidden,
[styles.hideInSmallContainer]: hideInSmallContainer,
})}
>
diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx
index 8cfe35f2b..a4a2b1e64 100644
--- a/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx
+++ b/packages/frontend/core/src/components/page-list/view/edit-collection/pages-mode.tsx
@@ -9,7 +9,7 @@ import { useCallback } from 'react';
import { FilterList } from '../../filter/filter-list';
import { VariableSelect } from '../../filter/vars';
-import { pageHeaderColsDef } from '../../header-col-def';
+import { usePageHeaderColsDef } from '../../header-col-def';
import { PageListItemRenderer } from '../../page-group';
import { ListTableHeader } from '../../page-header';
import type { ListItem } from '../../types';
@@ -47,6 +47,7 @@ export const PagesMode = ({
publicMode: allPageListConfig.getPublicMode(meta.id),
}))
);
+ const pageHeaderColsDef = usePageHeaderColsDef();
const { searchText, updateSearchText, searchedList } =
useSearch(filteredList);
const clearSelected = useCallback(() => {
@@ -68,7 +69,7 @@ export const PagesMode = ({
}, []);
const pageHeaderRenderer = useCallback(() => {
return ;
- }, []);
+ }, [pageHeaderColsDef]);
return (
<>
{
diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx
index 58f66ccd2..d27f127ed 100644
--- a/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx
+++ b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx
@@ -265,7 +265,6 @@ export const RulesMode = ({
hideHeader
className={styles.resultPages}
items={rulesPages}
- groupBy={false}
docCollection={allPageListConfig.docCollection}
isPreferredEdgeless={allPageListConfig.isEdgeless}
operationsRenderer={operationsRenderer}
@@ -285,7 +284,6 @@ export const RulesMode = ({
hideHeader
className={styles.resultPages}
items={allowListPages}
- groupBy={false}
docCollection={allPageListConfig.docCollection}
isPreferredEdgeless={allPageListConfig.isEdgeless}
operationsRenderer={operationsRenderer}
diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx
index 457c4c904..404a95e72 100644
--- a/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx
+++ b/packages/frontend/core/src/components/page-list/view/edit-collection/select-page.tsx
@@ -117,7 +117,6 @@ export const SelectPage = ({
items={searchedList}
docCollection={allPageListConfig.docCollection}
selectable
- groupBy={false}
onSelectedIdsChange={onChange}
selectedIds={value}
isPreferredEdgeless={allPageListConfig.isEdgeless}
diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx b/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx
index e79803386..02ef46a25 100644
--- a/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx
+++ b/packages/frontend/core/src/pages/workspace/all-page/all-page-header.tsx
@@ -1,5 +1,6 @@
import {
AllPageListOperationsMenu,
+ PageDisplayMenu,
PageListNewPageButton,
} from '@affine/core/components/page-list';
import { Header } from '@affine/core/components/pure/header';
@@ -32,15 +33,18 @@ export const AllPageHeader = ({
/>
}
right={
-
-
-
+ <>
+
+
+
+
+ >
}
center={}
/>
diff --git a/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts b/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts
index 1220d9c30..c9d274c76 100644
--- a/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts
+++ b/packages/frontend/core/src/pages/workspace/all-page/all-page.css.ts
@@ -7,6 +7,7 @@ export const scrollContainer = style({
export const headerCreateNewButton = style({
transition: 'opacity 0.1s ease-in-out',
});
+
export const headerCreateNewCollectionIconButton = style({
padding: '4px 8px',
fontSize: '16px',
diff --git a/packages/frontend/core/src/pages/workspace/trash-page.tsx b/packages/frontend/core/src/pages/workspace/trash-page.tsx
index fb02b1c46..f1d7840c1 100644
--- a/packages/frontend/core/src/pages/workspace/trash-page.tsx
+++ b/packages/frontend/core/src/pages/workspace/trash-page.tsx
@@ -8,7 +8,7 @@ import {
useFilteredPageMetas,
VirtualizedList,
} from '@affine/core/components/page-list';
-import { pageHeaderColsDef } from '@affine/core/components/page-list/header-col-def';
+import { usePageHeaderColsDef } from '@affine/core/components/page-list/header-col-def';
import { Header } from '@affine/core/components/pure/header';
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
@@ -60,6 +60,7 @@ export const TrashPage = () => {
useBlockSuiteMetaHelper(docCollection);
const { isPreferredEdgeless } = usePageHelper(docCollection);
const t = useAFFiNEI18N();
+ const pageHeaderColsDef = usePageHeaderColsDef();
const pageOperationsRenderer = useCallback(
(item: ListItem) => {
@@ -92,7 +93,7 @@ export const TrashPage = () => {
}, []);
const pageHeaderRenderer = useCallback(() => {
return ;
- }, []);
+ }, [pageHeaderColsDef]);
return (
<>
@@ -104,7 +105,6 @@ export const TrashPage = () => {
{
+ await openHomePage(page);
+ await waitForEditorLoad(page);
+ await clickNewPageButton(page);
+ await getBlockSuiteEditorTitle(page).click();
+ await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
+
+ await clickPageMoreActions(page);
+ const favoriteBtn = page.getByTestId('editor-option-menu-favorite');
+ await favoriteBtn.click();
+
+ await clickSideBarAllPageButton(page);
+ await waitForAllPagesLoad(page);
+ // click the display button
+ await page.locator('[data-testid="page-display-menu-button"]').click();
+ await page.locator('[data-testid="page-display-grouping-menuItem"]').click();
+ await page.locator('[data-testid="group-by-favourites"]').click();
+
+ // the group header should appear
+ await expect(
+ page.locator('[data-testid="group-label-favourited-1"]')
+ ).toBeVisible();
+
+ await expect(
+ page.locator('[data-testid="group-label-notFavourited-1"]')
+ ).toBeVisible();
+});
+
+test('select display properties to hide bodyNotes', async ({ page }) => {
+ await openHomePage(page);
+ await waitForEditorLoad(page);
+ await clickNewPageButton(page);
+ await getBlockSuiteEditorTitle(page).click();
+ await getBlockSuiteEditorTitle(page).fill(
+ 'this is a new page to test display properties'
+ );
+ await page.keyboard.press('Enter', { delay: 10 });
+ await page.keyboard.insertText('DRAGON BALL: Sparking! ZERO');
+ await clickSideBarAllPageButton(page);
+ await waitForAllPagesLoad(page);
+ const cell = page
+ .getByTestId('page-list-item')
+ .getByText('DRAGON BALL: Sparking! ZERO');
+ await expect(cell).toBeVisible();
+ await page.locator('[data-testid="page-display-menu-button"]').click();
+ await page.locator('[data-testid="property-bodyNotes"]').click();
+ await expect(cell).not.toBeVisible();
+ await page.locator('[data-testid="property-bodyNotes"]').click();
+ await expect(cell).toBeVisible();
+});
diff --git a/tests/storybook/src/stories/page-list.stories.tsx b/tests/storybook/src/stories/page-list.stories.tsx
index 383b6a533..9a1b54035 100644
--- a/tests/storybook/src/stories/page-list.stories.tsx
+++ b/tests/storybook/src/stories/page-list.stories.tsx
@@ -226,7 +226,13 @@ export const PageListStory: StoryFn> = (
};
PageListStory.args = {
- groupBy: 'createDate',
+ groupBy: [
+ {
+ id: 'all',
+ label: count => `All Pages (${count})`,
+ match: () => true,
+ },
+ ],
};
PageListStory.argTypes = {