feat(core): add page group and display properties (#6228)

close TOV-23

https://github.com/toeverything/AFFiNE/assets/102217452/c05474de-b73c-40ab-9f18-cc43bb9fd828
This commit is contained in:
JimmFly
2024-03-25 07:53:33 +00:00
parent 6467e10690
commit 1ff6af85f5
30 changed files with 742 additions and 154 deletions

View File

@@ -107,7 +107,6 @@ export const VirtualizedCollectionList = ({
ref={listRef}
selectable="toggle"
draggable={false}
groupBy={false}
atTopThreshold={80}
atTopStateChange={setHideHeaderCreateNewCollection}
onSelectionActiveChange={setShowFloatingToolbar}

View File

@@ -16,7 +16,6 @@ export const headerCell = style({
borderRight: `1px solid ${cssVar('hoverColorFilled')}`,
},
},
display: 'flex',
alignItems: 'center',
columnGap: '4px',
position: 'relative',

View File

@@ -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}

View File

@@ -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'),
},
},
});

View File

@@ -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 => (
<MenuItem
key={option.value}
onSelect={() => handleSelect(option.value)}
data-active={group === option.value}
endFix={group === option.value ? <DoneIcon fontSize={'20px'} /> : null}
className={styles.subMenuItem}
data-testid={`group-by-${option.value}`}
>
<span>{option.label}</span>
</MenuItem>
));
const currentGroupType = groupOptions.find(
option => option.value === group
)?.label;
return (
<>
<MenuSub
subContentOptions={{
alignOffset: -8,
sideOffset: 12,
}}
triggerOptions={{ className: styles.subMenuTrigger }}
items={subItems}
>
<div
className={styles.subMenuTriggerContent}
data-testid="page-display-grouping-menuItem"
>
<span>{t['com.affine.page.display.grouping']()}</span>
<span className={styles.currentGroupType}>{currentGroupType}</span>
</div>
</MenuSub>
<MenuSeparator />
<div className={styles.listOption}>
{t['com.affine.page.display.list-option']()}
</div>
<div className={styles.properties}>
{t['com.affine.page.display.display-properties']()}
</div>
<div className={styles.propertiesWrapper}>
{propertyOptions.map(option => (
<Button
key={option.label}
className={styles.propertyButton}
onClick={option.onClick}
data-active={properties[option.key]}
data-testid={`property-${option.key}`}
>
{option.label}
</Button>
))}
</div>
</>
);
}, [group, handleSelect, properties, propertyOptions, t]);
return (
<Menu
items={items}
contentOptions={{
className: styles.menu,
align: 'end',
}}
>
<Button
iconPosition="end"
icon={<ArrowDownSmallIcon className={styles.arrowDownSmallIcon} />}
className={styles.headerDisplayButton}
data-testid="page-display-menu-button"
>
{t['com.affine.page.display']()}
</Button>
</Menu>
);
};

View File

@@ -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<PageListItemProps, 'title' | 'preview'>) => {
const [displayProperties] = usePageDisplayProperties();
return (
<div data-testid="page-list-item-title" className={styles.titleCell}>
<div
@@ -23,7 +25,7 @@ const ListTitleCell = ({
>
{title}
</div>
{preview ? (
{preview && displayProperties['bodyNotes'] ? (
<div
data-testid="page-list-item-preview-text"
className={styles.titleCellPreview}
@@ -123,6 +125,7 @@ const PageListOperationsCell = ({
};
export const PageListItem = (props: PageListItemProps) => {
const [displayProperties] = usePageDisplayProperties();
const pageTitleElement = useMemo(() => {
return (
<div className={styles.dragPageItemOverlay}>
@@ -182,14 +185,29 @@ export const PageListItem = (props: PageListItemProps) => {
</div>
<ListTitleCell title={props.title} preview={props.preview} />
</ColWrapper>
<ColWrapper flex={4} alignment="end" style={{ overflow: 'visible' }}>
<ColWrapper
flex={4}
alignment="end"
style={{ overflow: 'visible' }}
hidden={!displayProperties['tags']}
>
<PageTagsCell pageId={props.pageId} />
</ColWrapper>
</ColWrapper>
<ColWrapper flex={1} alignment="end" hideInSmallContainer>
<ColWrapper
flex={1}
alignment="end"
hideInSmallContainer
hidden={!displayProperties['createDate']}
>
<PageCreateDateCell createDate={props.createDate} />
</ColWrapper>
<ColWrapper flex={1} alignment="end" hideInSmallContainer>
<ColWrapper
flex={1}
alignment="end"
hideInSmallContainer
hidden={!displayProperties['updatedDate']}
>
<PageUpdatedDateCell updatedDate={props.updatedDate} />
</ColWrapper>
{props.operations ? (

View File

@@ -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 <ListTableHeader headerCols={pageHeaderColsDef} />;
}, []);
}, [pageHeaderColsDef]);
const pageItemRenderer = useCallback((item: ListItem) => {
return <PageListItemRenderer {...item} />;
@@ -179,6 +181,8 @@ export const VirtualizedPageList = ({
hideFloatingToolbar();
}, [filteredSelectedPageIds, hideFloatingToolbar, pageMetas, setTrashModal]);
const group = usePageItemGroupDefinitions();
return (
<>
<VirtualizedList
@@ -189,6 +193,7 @@ export const VirtualizedPageList = ({
atTopStateChange={setHideHeaderCreateNewPage}
onSelectionActiveChange={setShowFloatingToolbar}
heading={heading}
groupBy={group}
selectedIds={filteredSelectedPageIds}
onSelectedIdsChange={setSelectedPageIds}
items={pageMetasToRender}

View File

@@ -0,0 +1,40 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const groupLabelWrapper = style({
display: 'flex',
alignItems: 'center',
gap: '4px',
});
export const tagIcon = style({
width: '8px',
height: '8px',
borderRadius: '50%',
marginLeft: '4px',
marginRight: '6px',
});
export const groupLabel = style({
fontSize: cssVar('fontSm'),
lineHeight: '1.5em',
color: cssVar('textPrimaryColor'),
});
export const pageCount = style({
fontSize: cssVar('fontBase'),
lineHeight: '1.6em',
color: cssVar('textSecondaryColor'),
});
export const favouritedIcon = style({
color: cssVar('primaryColor'),
marginRight: '6px',
fontSize: '16px',
});
export const notFavouritedIcon = style({
color: cssVar('iconColor'),
marginRight: '6px',
fontSize: '16px',
});

View File

@@ -0,0 +1,205 @@
import { TagService } from '@affine/core/modules/tag';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FavoritedIcon, FavoriteIcon } from '@blocksuite/icons';
import type { DocMeta } from '@blocksuite/store';
import { useLiveData, useService } from '@toeverything/infra';
import { useAtomValue } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { type ReactNode, useMemo } from 'react';
import * as styles from './group-definitions.css';
import type {
DateKey,
ItemGroupDefinition,
ListItem,
PageGroupByType,
} from './types';
import { betweenDaysAgo, withinDaysAgo } from './utils';
export const pageGroupByTypeAtom = atomWithStorage<PageGroupByType>(
'pageGroupByType',
'updatedDate'
);
const GroupLabel = ({
label,
count,
icon,
id,
}: {
id: string;
label: string;
count: number;
icon?: ReactNode;
}) => (
<div className={styles.groupLabelWrapper}>
{icon}
<div
className={styles.groupLabel}
data-testid={`group-label-${id}-${count}`}
>
{label}
</div>
<div className={styles.pageCount}>{` · ${count}`}</div>
</div>
);
// todo: optimize date matchers
export const useDateGroupDefinitions = <T extends ListItem>(
key: DateKey
): ItemGroupDefinition<T>[] => {
const t = useAFFiNEI18N();
return useMemo(
() => [
{
id: 'today',
label: count => (
<GroupLabel
id="today"
label={t['com.affine.today']()}
count={count}
/>
),
match: item =>
withinDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 1),
},
{
id: 'yesterday',
label: count => (
<GroupLabel
id="yesterday"
label={t['com.affine.yesterday']()}
count={count}
/>
),
match: item =>
betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 1, 2),
},
{
id: 'last7Days',
label: count => (
<GroupLabel
id="last7Days"
label={t['com.affine.last7Days']()}
count={count}
/>
),
match: item =>
betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 2, 7),
},
{
id: 'last30Days',
label: count => (
<GroupLabel
id="last30Days"
label={t['com.affine.last30Days']()}
count={count}
/>
),
match: item =>
betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 7, 30),
},
{
id: 'moreThan30Days',
label: count => (
<GroupLabel
id="moreThan30Days"
label={t['com.affine.moreThan30Days']()}
count={count}
/>
),
match: item =>
!withinDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 30),
},
],
[key, t]
);
};
export const useTagGroupDefinitions = (): ItemGroupDefinition<ListItem>[] => {
const tagService = useService(TagService);
const tagMetas = useLiveData(tagService.tagMetas$);
return useMemo(() => {
return tagMetas.map(tag => ({
id: tag.id,
label: count => (
<GroupLabel
id={tag.title}
label={tag.title}
count={count}
icon={
<div
className={styles.tagIcon}
style={{
backgroundColor: tag.color,
}}
></div>
}
/>
),
match: item => (item as DocMeta).tags?.includes(tag.id),
}));
}, [tagMetas]);
};
export const useFavoriteGroupDefinitions = <
T extends ListItem,
>(): ItemGroupDefinition<T>[] => {
const t = useAFFiNEI18N();
return useMemo(
() => [
{
id: 'favourited',
label: count => (
<GroupLabel
id="favourited"
label={t['com.affine.page.group-header.favourited']()}
count={count}
icon={<FavoritedIcon className={styles.favouritedIcon} />}
/>
),
match: item => !!(item as DocMeta).favorite,
},
{
id: 'notFavourited',
label: count => (
<GroupLabel
id="notFavourited"
label={t['com.affine.page.group-header.not-favourited']()}
count={count}
icon={<FavoriteIcon className={styles.notFavouritedIcon} />}
/>
),
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,
]);
};

View File

@@ -1,9 +1,13 @@
import { Trans } from '@affine/i18n';
import { useMemo } from 'react';
import { ListHeaderTitleCell } from './page-header';
import type { HeaderColDef } from './types';
export const pageHeaderColsDef: HeaderColDef[] = [
import { usePageDisplayProperties } from './use-page-display-properties';
export const usePageHeaderColsDef = (): HeaderColDef[] => {
const [displayProperties] = usePageDisplayProperties();
return useMemo(
() => [
{
key: 'title',
content: <ListHeaderTitleCell />,
@@ -16,6 +20,7 @@ export const pageHeaderColsDef: HeaderColDef[] = [
content: <Trans i18nKey="Tags" />,
flex: 3,
alignment: 'end',
hidden: !displayProperties['tags'],
},
{
key: 'createDate',
@@ -24,6 +29,7 @@ export const pageHeaderColsDef: HeaderColDef[] = [
sortable: true,
alignment: 'end',
hideInSmallContainer: true,
hidden: !displayProperties['createDate'],
},
{
key: 'updatedDate',
@@ -32,6 +38,7 @@ export const pageHeaderColsDef: HeaderColDef[] = [
sortable: true,
alignment: 'end',
hideInSmallContainer: true,
hidden: !displayProperties['updatedDate'],
},
{
key: 'actions',
@@ -39,7 +46,10 @@ export const pageHeaderColsDef: HeaderColDef[] = [
flex: 1,
alignment: 'end',
},
];
],
[displayProperties]
);
};
export const collectionHeaderColsDef: HeaderColDef[] = [
{

View File

@@ -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';

View File

@@ -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 = <T extends ListItem>(
key: DateKey
): ItemGroupDefinition<T>[] => [
{
id: 'today',
label: <Trans i18nKey="com.affine.today" />,
match: item =>
withinDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 1),
},
{
id: 'yesterday',
label: <Trans i18nKey="com.affine.yesterday" />,
match: item =>
betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 1, 2),
},
{
id: 'last7Days',
label: <Trans i18nKey="com.affine.last7Days" />,
match: item =>
betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 2, 7),
},
{
id: 'last30Days',
label: <Trans i18nKey="com.affine.last30Days" />,
match: item =>
betweenDaysAgo(new Date(item[key] ?? item.createDate ?? ''), 7, 30),
},
{
id: 'moreThan30Days',
label: <Trans i18nKey="com.affine.moreThan30Days" />,
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<T extends ListItem>(
items: T[],
key?: DateKey
groupDefs?: ItemGroupDefinition<T>[] | false
): ItemGroupProps<T>[] {
if (!key) {
if (!groupDefs) {
return [
{
id: 'all',
@@ -66,8 +15,12 @@ export function itemsToItemGroups<T extends ListItem>(
}
// assume pages are already sorted, we will use the page order to determine the group order
const groupDefs = itemGroupDefinitions[key];
const groups: ItemGroupProps<T>[] = [];
let groups: ItemGroupProps<T>[] = 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<T extends ListItem>(
const group = groups.find(g => g.id === groupDef.id);
if (group) {
group.items.push(item);
}
}
}
// 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 {
const label =
typeof groupDef.label === 'function'
? groupDef.label()
: groupDef.label;
groups.push({
id: groupDef.id,
label: label,
items: [item],
allItems: items,
});
}
group.label = groupDef.label;
}
}
return group;
})
.filter(group => group.items.length > 0);
return groups;
}

View File

@@ -37,6 +37,10 @@ export const hideInSmallContainer = style({
},
},
});
export const hidden = style({
display: 'none',
});
export const favoriteCell = style({
display: 'flex',
alignItems: 'center',

View File

@@ -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<ListItem>) => {
const groups = useAtomValue(groupsAtom);
const pageHeaderColsDef = usePageHeaderColsDef();
const hideHeader = props.hideHeader;
return (
<div className={clsx(props.className, styles.root)}>

View File

@@ -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',

View File

@@ -114,6 +114,7 @@ export const ListTableHeader = ({
data-selection-active={selectionState.selectionActive}
>
{headerCols.map(col => {
const isTagHidden = col.key === 'tags' && col.hidden;
return (
<ListHeaderCell
flex={col.flex}
@@ -123,8 +124,12 @@ export const ListTableHeader = ({
sortable={col.sortable}
sorting={sorter.key === col.key}
order={sorter.order}
hidden={isTagHidden ? false : col.hidden}
onSort={onSort}
style={{ overflow: 'visible' }}
style={{
overflow: 'visible',
visibility: isTagHidden ? 'hidden' : 'visible',
}}
hideInSmallContainer={col.hideInSmallContainer}
>
{col.content}

View File

@@ -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<ListItem>(sorter.items, groupBy);
});

View File

@@ -84,7 +84,6 @@ export const VirtualizedTagList = ({
ref={listRef}
selectable="toggle"
draggable={false}
groupBy={false}
atTopThreshold={80}
onSelectionActiveChange={setShowFloatingToolbar}
heading={<TagListHeader onOpen={onOpenCreate} />}

View File

@@ -82,6 +82,12 @@ export interface SortBy {
}
export type DateKey = 'createDate' | 'updatedDate';
export type PageGroupByType =
| 'createDate'
| 'updatedDate'
| 'tag'
| 'favourites'
| 'none';
export interface ListProps<T> {
// required data:
@@ -89,7 +95,7 @@ export interface ListProps<T> {
docCollection: DocCollection;
className?: string;
hideHeader?: boolean; // whether or not to hide the header. default is false (showing header)
groupBy?: ItemGroupByType | false;
groupBy?: ItemGroupDefinition<T>[];
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<T> {
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<Element>;
export type PageDisplayProperties = {
bodyNotes: boolean;
tags: boolean;
createDate: boolean;
updatedDate: boolean;
};

View File

@@ -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>(
'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];
};

View File

@@ -75,6 +75,7 @@ export const ColWrapper = forwardRef<HTMLDivElement, ColWrapperProps>(
flex,
alignment,
hideInSmallContainer,
hidden,
className,
style,
children,
@@ -95,6 +96,7 @@ export const ColWrapper = forwardRef<HTMLDivElement, ColWrapperProps>(
}}
data-hide-item={hideInSmallContainer ? true : undefined}
className={clsx(className, styles.colWrapper, {
[styles.hidden]: hidden,
[styles.hideInSmallContainer]: hideInSmallContainer,
})}
>

View File

@@ -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 <ListTableHeader headerCols={pageHeaderColsDef} />;
}, []);
}, [pageHeaderColsDef]);
return (
<>
<input
@@ -127,7 +128,6 @@ export const PagesMode = ({
<VirtualizedList
className={styles.pageList}
items={searchedList}
groupBy={false}
docCollection={allPageListConfig.docCollection}
selectable
onSelectedIdsChange={ids => {

View File

@@ -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}

View File

@@ -117,7 +117,6 @@ export const SelectPage = ({
items={searchedList}
docCollection={allPageListConfig.docCollection}
selectable
groupBy={false}
onSelectedIdsChange={onChange}
selectedIds={value}
isPreferredEdgeless={allPageListConfig.isEdgeless}

View File

@@ -1,5 +1,6 @@
import {
AllPageListOperationsMenu,
PageDisplayMenu,
PageListNewPageButton,
} from '@affine/core/components/page-list';
import { Header } from '@affine/core/components/pure/header';
@@ -32,6 +33,7 @@ export const AllPageHeader = ({
/>
}
right={
<>
<PageListNewPageButton
size="small"
className={clsx(
@@ -41,6 +43,8 @@ export const AllPageHeader = ({
>
<PlusIcon />
</PageListNewPageButton>
<PageDisplayMenu />
</>
}
center={<WorkspaceModeFilterTab activeFilter={'docs'} />}
/>

View File

@@ -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',

View File

@@ -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 <ListTableHeader headerCols={pageHeaderColsDef} />;
}, []);
}, [pageHeaderColsDef]);
return (
<>
<ViewHeaderIsland>
@@ -104,7 +105,6 @@ export const TrashPage = () => {
<VirtualizedList
items={filteredPageMetas}
rowAsLink
groupBy={false}
isPreferredEdgeless={isPreferredEdgeless}
docCollection={currentWorkspace.docCollection}
operationsRenderer={pageOperationsRenderer}

View File

@@ -1172,5 +1172,15 @@
"com.affine.workbench.split-view.page-menu-open": "Open in split view",
"com.affine.search-tags.placeholder": "Type here ...",
"com.affine.resetSyncStatus.button": "Reset Sync",
"com.affine.resetSyncStatus.description": "This operation may fix some synchronization issues."
"com.affine.resetSyncStatus.description": "This operation may fix some synchronization issues.",
"com.affine.page.group-header.favourited": "Favourited",
"com.affine.page.group-header.not-favourited": "Not Favourited",
"com.affine.page.display": "Display",
"com.affine.page.display.grouping": "Grouping",
"com.affine.page.display.grouping.no-grouping": "No Grouping",
"com.affine.page.display.grouping.group-by-tag": "Tag",
"com.affine.page.display.grouping.group-by-favourites": "Favourites",
"com.affine.page.display.list-option": "List option",
"com.affine.page.display.display-properties": "Display properties",
"com.affine.page.display.display-properties.body-notes": "Body notes"
}

View File

@@ -15,6 +15,7 @@ import {
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
clickPageMoreActions,
getBlockSuiteEditorTitle,
waitForAllPagesLoad,
waitForEditorLoad,
@@ -256,3 +257,54 @@ test('select a group of items by clicking "Select All" in group header', async (
`${selectedItemCount} doc(s) selected`
);
});
test('click display button to group pages', 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 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();
});

View File

@@ -226,7 +226,13 @@ export const PageListStory: StoryFn<ListProps<ListItem>> = (
};
PageListStory.args = {
groupBy: 'createDate',
groupBy: [
{
id: 'all',
label: count => `All Pages (${count})`,
match: () => true,
},
],
};
PageListStory.argTypes = {