mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
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:
@@ -107,7 +107,6 @@ export const VirtualizedCollectionList = ({
|
|||||||
ref={listRef}
|
ref={listRef}
|
||||||
selectable="toggle"
|
selectable="toggle"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
groupBy={false}
|
|
||||||
atTopThreshold={80}
|
atTopThreshold={80}
|
||||||
atTopStateChange={setHideHeaderCreateNewCollection}
|
atTopStateChange={setHideHeaderCreateNewCollection}
|
||||||
onSelectionActiveChange={setShowFloatingToolbar}
|
onSelectionActiveChange={setShowFloatingToolbar}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export const headerCell = style({
|
|||||||
borderRight: `1px solid ${cssVar('hoverColorFilled')}`,
|
borderRight: `1px solid ${cssVar('hoverColorFilled')}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
columnGap: '4px',
|
columnGap: '4px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const ListHeaderCell = ({
|
|||||||
alignment,
|
alignment,
|
||||||
flex,
|
flex,
|
||||||
style,
|
style,
|
||||||
|
hidden,
|
||||||
hideInSmallContainer,
|
hideInSmallContainer,
|
||||||
children,
|
children,
|
||||||
}: HeaderCellProps) => {
|
}: HeaderCellProps) => {
|
||||||
@@ -39,6 +40,7 @@ export const ListHeaderCell = ({
|
|||||||
className={styles.headerCell}
|
className={styles.headerCell}
|
||||||
data-sortable={sortable ? true : undefined}
|
data-sortable={sortable ? true : undefined}
|
||||||
data-sorting={sorting ? true : undefined}
|
data-sorting={sorting ? true : undefined}
|
||||||
|
hidden={hidden}
|
||||||
style={style}
|
style={style}
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
hideInSmallContainer={hideInSmallContainer}
|
hideInSmallContainer={hideInSmallContainer}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { useCallback, useMemo } from 'react';
|
|||||||
|
|
||||||
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
|
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
|
||||||
import type { DraggableTitleCellData, PageListItemProps } from '../types';
|
import type { DraggableTitleCellData, PageListItemProps } from '../types';
|
||||||
|
import { usePageDisplayProperties } from '../use-page-display-properties';
|
||||||
import { ColWrapper, formatDate, stopPropagation } from '../utils';
|
import { ColWrapper, formatDate, stopPropagation } from '../utils';
|
||||||
import * as styles from './page-list-item.css';
|
import * as styles from './page-list-item.css';
|
||||||
import { PageTags } from './page-tags';
|
import { PageTags } from './page-tags';
|
||||||
@@ -15,6 +16,7 @@ const ListTitleCell = ({
|
|||||||
title,
|
title,
|
||||||
preview,
|
preview,
|
||||||
}: Pick<PageListItemProps, 'title' | 'preview'>) => {
|
}: Pick<PageListItemProps, 'title' | 'preview'>) => {
|
||||||
|
const [displayProperties] = usePageDisplayProperties();
|
||||||
return (
|
return (
|
||||||
<div data-testid="page-list-item-title" className={styles.titleCell}>
|
<div data-testid="page-list-item-title" className={styles.titleCell}>
|
||||||
<div
|
<div
|
||||||
@@ -23,7 +25,7 @@ const ListTitleCell = ({
|
|||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
{preview ? (
|
{preview && displayProperties['bodyNotes'] ? (
|
||||||
<div
|
<div
|
||||||
data-testid="page-list-item-preview-text"
|
data-testid="page-list-item-preview-text"
|
||||||
className={styles.titleCellPreview}
|
className={styles.titleCellPreview}
|
||||||
@@ -123,6 +125,7 @@ const PageListOperationsCell = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PageListItem = (props: PageListItemProps) => {
|
export const PageListItem = (props: PageListItemProps) => {
|
||||||
|
const [displayProperties] = usePageDisplayProperties();
|
||||||
const pageTitleElement = useMemo(() => {
|
const pageTitleElement = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.dragPageItemOverlay}>
|
<div className={styles.dragPageItemOverlay}>
|
||||||
@@ -182,14 +185,29 @@ export const PageListItem = (props: PageListItemProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<ListTitleCell title={props.title} preview={props.preview} />
|
<ListTitleCell title={props.title} preview={props.preview} />
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
<ColWrapper flex={4} alignment="end" style={{ overflow: 'visible' }}>
|
<ColWrapper
|
||||||
|
flex={4}
|
||||||
|
alignment="end"
|
||||||
|
style={{ overflow: 'visible' }}
|
||||||
|
hidden={!displayProperties['tags']}
|
||||||
|
>
|
||||||
<PageTagsCell pageId={props.pageId} />
|
<PageTagsCell pageId={props.pageId} />
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
<ColWrapper flex={1} alignment="end" hideInSmallContainer>
|
<ColWrapper
|
||||||
|
flex={1}
|
||||||
|
alignment="end"
|
||||||
|
hideInSmallContainer
|
||||||
|
hidden={!displayProperties['createDate']}
|
||||||
|
>
|
||||||
<PageCreateDateCell createDate={props.createDate} />
|
<PageCreateDateCell createDate={props.createDate} />
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
<ColWrapper flex={1} alignment="end" hideInSmallContainer>
|
<ColWrapper
|
||||||
|
flex={1}
|
||||||
|
alignment="end"
|
||||||
|
hideInSmallContainer
|
||||||
|
hidden={!displayProperties['updatedDate']}
|
||||||
|
>
|
||||||
<PageUpdatedDateCell updatedDate={props.updatedDate} />
|
<PageUpdatedDateCell updatedDate={props.updatedDate} />
|
||||||
</ColWrapper>
|
</ColWrapper>
|
||||||
{props.operations ? (
|
{props.operations ? (
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { useCallback, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
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 { PageOperationCell } from '../operation-cell';
|
||||||
import { PageListItemRenderer } from '../page-group';
|
import { PageListItemRenderer } from '../page-group';
|
||||||
import { ListTableHeader } from '../page-header';
|
import { ListTableHeader } from '../page-header';
|
||||||
@@ -108,6 +109,7 @@ export const VirtualizedPageList = ({
|
|||||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||||
const pageOperations = usePageOperationsRenderer();
|
const pageOperations = usePageOperationsRenderer();
|
||||||
const { isPreferredEdgeless } = usePageHelper(currentWorkspace.docCollection);
|
const { isPreferredEdgeless } = usePageHelper(currentWorkspace.docCollection);
|
||||||
|
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||||
|
|
||||||
const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
|
const filteredPageMetas = useFilteredPageMetas(currentWorkspace, pageMetas, {
|
||||||
filters,
|
filters,
|
||||||
@@ -139,7 +141,7 @@ export const VirtualizedPageList = ({
|
|||||||
|
|
||||||
const pageHeaderRenderer = useCallback(() => {
|
const pageHeaderRenderer = useCallback(() => {
|
||||||
return <ListTableHeader headerCols={pageHeaderColsDef} />;
|
return <ListTableHeader headerCols={pageHeaderColsDef} />;
|
||||||
}, []);
|
}, [pageHeaderColsDef]);
|
||||||
|
|
||||||
const pageItemRenderer = useCallback((item: ListItem) => {
|
const pageItemRenderer = useCallback((item: ListItem) => {
|
||||||
return <PageListItemRenderer {...item} />;
|
return <PageListItemRenderer {...item} />;
|
||||||
@@ -179,6 +181,8 @@ export const VirtualizedPageList = ({
|
|||||||
hideFloatingToolbar();
|
hideFloatingToolbar();
|
||||||
}, [filteredSelectedPageIds, hideFloatingToolbar, pageMetas, setTrashModal]);
|
}, [filteredSelectedPageIds, hideFloatingToolbar, pageMetas, setTrashModal]);
|
||||||
|
|
||||||
|
const group = usePageItemGroupDefinitions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
@@ -189,6 +193,7 @@ export const VirtualizedPageList = ({
|
|||||||
atTopStateChange={setHideHeaderCreateNewPage}
|
atTopStateChange={setHideHeaderCreateNewPage}
|
||||||
onSelectionActiveChange={setShowFloatingToolbar}
|
onSelectionActiveChange={setShowFloatingToolbar}
|
||||||
heading={heading}
|
heading={heading}
|
||||||
|
groupBy={group}
|
||||||
selectedIds={filteredSelectedPageIds}
|
selectedIds={filteredSelectedPageIds}
|
||||||
onSelectedIdsChange={setSelectedPageIds}
|
onSelectedIdsChange={setSelectedPageIds}
|
||||||
items={pageMetasToRender}
|
items={pageMetasToRender}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -1,45 +1,55 @@
|
|||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { ListHeaderTitleCell } from './page-header';
|
import { ListHeaderTitleCell } from './page-header';
|
||||||
import type { HeaderColDef } from './types';
|
import type { HeaderColDef } from './types';
|
||||||
|
import { usePageDisplayProperties } from './use-page-display-properties';
|
||||||
export const pageHeaderColsDef: HeaderColDef[] = [
|
export const usePageHeaderColsDef = (): HeaderColDef[] => {
|
||||||
{
|
const [displayProperties] = usePageDisplayProperties();
|
||||||
key: 'title',
|
return useMemo(
|
||||||
content: <ListHeaderTitleCell />,
|
() => [
|
||||||
flex: 6,
|
{
|
||||||
alignment: 'start',
|
key: 'title',
|
||||||
sortable: true,
|
content: <ListHeaderTitleCell />,
|
||||||
},
|
flex: 6,
|
||||||
{
|
alignment: 'start',
|
||||||
key: 'tags',
|
sortable: true,
|
||||||
content: <Trans i18nKey="Tags" />,
|
},
|
||||||
flex: 3,
|
{
|
||||||
alignment: 'end',
|
key: 'tags',
|
||||||
},
|
content: <Trans i18nKey="Tags" />,
|
||||||
{
|
flex: 3,
|
||||||
key: 'createDate',
|
alignment: 'end',
|
||||||
content: <Trans i18nKey="Created" />,
|
hidden: !displayProperties['tags'],
|
||||||
flex: 1,
|
},
|
||||||
sortable: true,
|
{
|
||||||
alignment: 'end',
|
key: 'createDate',
|
||||||
hideInSmallContainer: true,
|
content: <Trans i18nKey="Created" />,
|
||||||
},
|
flex: 1,
|
||||||
{
|
sortable: true,
|
||||||
key: 'updatedDate',
|
alignment: 'end',
|
||||||
content: <Trans i18nKey="Updated" />,
|
hideInSmallContainer: true,
|
||||||
flex: 1,
|
hidden: !displayProperties['createDate'],
|
||||||
sortable: true,
|
},
|
||||||
alignment: 'end',
|
{
|
||||||
hideInSmallContainer: true,
|
key: 'updatedDate',
|
||||||
},
|
content: <Trans i18nKey="Updated" />,
|
||||||
{
|
flex: 1,
|
||||||
key: 'actions',
|
sortable: true,
|
||||||
content: '',
|
alignment: 'end',
|
||||||
flex: 1,
|
hideInSmallContainer: true,
|
||||||
alignment: 'end',
|
hidden: !displayProperties['updatedDate'],
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
key: 'actions',
|
||||||
|
content: '',
|
||||||
|
flex: 1,
|
||||||
|
alignment: 'end',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[displayProperties]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const collectionHeaderColsDef: HeaderColDef[] = [
|
export const collectionHeaderColsDef: HeaderColDef[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ export * from './collections';
|
|||||||
export * from './components/favorite-tag';
|
export * from './components/favorite-tag';
|
||||||
export * from './components/floating-toolbar';
|
export * from './components/floating-toolbar';
|
||||||
export * from './components/new-page-button';
|
export * from './components/new-page-button';
|
||||||
|
export * from './components/page-display-menu';
|
||||||
export * from './docs';
|
export * from './docs';
|
||||||
export * from './docs/page-list-item';
|
export * from './docs/page-list-item';
|
||||||
export * from './docs/page-tags';
|
export * from './docs/page-tags';
|
||||||
export * from './filter';
|
export * from './filter';
|
||||||
|
export * from './group-definitions';
|
||||||
export * from './header-col-def';
|
export * from './header-col-def';
|
||||||
export * from './list';
|
export * from './list';
|
||||||
export * from './operation-cell';
|
export * from './operation-cell';
|
||||||
@@ -16,6 +18,7 @@ export * from './tags';
|
|||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './use-collection-manager';
|
export * from './use-collection-manager';
|
||||||
export * from './use-filtered-page-metas';
|
export * from './use-filtered-page-metas';
|
||||||
|
export * from './use-page-display-properties';
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './view';
|
export * from './view';
|
||||||
export * from './virtualized-list';
|
export * from './virtualized-list';
|
||||||
|
|||||||
@@ -1,61 +1,10 @@
|
|||||||
import { Trans } from '@affine/i18n';
|
import type { ItemGroupDefinition, ItemGroupProps, ListItem } from './types';
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
export function itemsToItemGroups<T extends ListItem>(
|
export function itemsToItemGroups<T extends ListItem>(
|
||||||
items: T[],
|
items: T[],
|
||||||
key?: DateKey
|
groupDefs?: ItemGroupDefinition<T>[] | false
|
||||||
): ItemGroupProps<T>[] {
|
): ItemGroupProps<T>[] {
|
||||||
if (!key) {
|
if (!groupDefs) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'all',
|
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
|
// assume pages are already sorted, we will use the page order to determine the group order
|
||||||
const groupDefs = itemGroupDefinitions[key];
|
let groups: ItemGroupProps<T>[] = groupDefs.map(groupDef => ({
|
||||||
const groups: ItemGroupProps<T>[] = [];
|
id: groupDef.id,
|
||||||
|
label: undefined, // Will be set later
|
||||||
|
items: [],
|
||||||
|
allItems: items,
|
||||||
|
}));
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
// for a single page, there could be multiple groups that it belongs to
|
// 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);
|
const group = groups.find(g => g.id === groupDef.id);
|
||||||
if (group) {
|
if (group) {
|
||||||
group.items.push(item);
|
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;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ export const hideInSmallContainer = style({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const hidden = style({
|
||||||
|
display: 'none',
|
||||||
|
});
|
||||||
export const favoriteCell = style({
|
export const favoriteCell = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { pageHeaderColsDef } from './header-col-def';
|
import { usePageHeaderColsDef } from './header-col-def';
|
||||||
import * as styles from './list.css';
|
import * as styles from './list.css';
|
||||||
import { ItemGroup } from './page-group';
|
import { ItemGroup } from './page-group';
|
||||||
import { ListTableHeader } from './page-header';
|
import { ListTableHeader } from './page-header';
|
||||||
@@ -134,7 +134,7 @@ ListInnerWrapper.displayName = 'ListInnerWrapper';
|
|||||||
|
|
||||||
const ListInner = (props: ListProps<ListItem>) => {
|
const ListInner = (props: ListProps<ListItem>) => {
|
||||||
const groups = useAtomValue(groupsAtom);
|
const groups = useAtomValue(groupsAtom);
|
||||||
|
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||||
const hideHeader = props.hideHeader;
|
const hideHeader = props.hideHeader;
|
||||||
return (
|
return (
|
||||||
<div className={clsx(props.className, styles.root)}>
|
<div className={clsx(props.className, styles.root)}>
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export const header = style({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '0px 16px 0px 6px',
|
padding: '0px 16px 0px 6px',
|
||||||
gap: 4,
|
|
||||||
height: '28px',
|
height: '28px',
|
||||||
background: cssVar('backgroundPrimaryColor'),
|
background: cssVar('backgroundPrimaryColor'),
|
||||||
':hover': {
|
':hover': {
|
||||||
@@ -88,6 +87,8 @@ export const selectAllButton = style({
|
|||||||
});
|
});
|
||||||
export const collapsedIcon = style({
|
export const collapsedIcon = style({
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
fontSize: '20px',
|
||||||
|
color: cssVar('iconColor'),
|
||||||
transition: 'transform 0.2s ease-in-out',
|
transition: 'transform 0.2s ease-in-out',
|
||||||
selectors: {
|
selectors: {
|
||||||
'&[data-collapsed="false"]': {
|
'&[data-collapsed="false"]': {
|
||||||
@@ -99,8 +100,6 @@ export const collapsedIcon = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const collapsedIconContainer = style({
|
export const collapsedIconContainer = style({
|
||||||
width: '16px',
|
|
||||||
height: '16px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export const ListTableHeader = ({
|
|||||||
data-selection-active={selectionState.selectionActive}
|
data-selection-active={selectionState.selectionActive}
|
||||||
>
|
>
|
||||||
{headerCols.map(col => {
|
{headerCols.map(col => {
|
||||||
|
const isTagHidden = col.key === 'tags' && col.hidden;
|
||||||
return (
|
return (
|
||||||
<ListHeaderCell
|
<ListHeaderCell
|
||||||
flex={col.flex}
|
flex={col.flex}
|
||||||
@@ -123,8 +124,12 @@ export const ListTableHeader = ({
|
|||||||
sortable={col.sortable}
|
sortable={col.sortable}
|
||||||
sorting={sorter.key === col.key}
|
sorting={sorter.key === col.key}
|
||||||
order={sorter.order}
|
order={sorter.order}
|
||||||
|
hidden={isTagHidden ? false : col.hidden}
|
||||||
onSort={onSort}
|
onSort={onSort}
|
||||||
style={{ overflow: 'visible' }}
|
style={{
|
||||||
|
overflow: 'visible',
|
||||||
|
visibility: isTagHidden ? 'hidden' : 'visible',
|
||||||
|
}}
|
||||||
hideInSmallContainer={col.hideInSmallContainer}
|
hideInSmallContainer={col.hideInSmallContainer}
|
||||||
>
|
>
|
||||||
{col.content}
|
{col.content}
|
||||||
|
|||||||
@@ -186,20 +186,9 @@ export const sorterAtom = atom(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const groupsAtom = atom(get => {
|
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);
|
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);
|
return itemsToItemGroups<ListItem>(sorter.items, groupBy);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export const VirtualizedTagList = ({
|
|||||||
ref={listRef}
|
ref={listRef}
|
||||||
selectable="toggle"
|
selectable="toggle"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
groupBy={false}
|
|
||||||
atTopThreshold={80}
|
atTopThreshold={80}
|
||||||
onSelectionActiveChange={setShowFloatingToolbar}
|
onSelectionActiveChange={setShowFloatingToolbar}
|
||||||
heading={<TagListHeader onOpen={onOpenCreate} />}
|
heading={<TagListHeader onOpen={onOpenCreate} />}
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ export interface SortBy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type DateKey = 'createDate' | 'updatedDate';
|
export type DateKey = 'createDate' | 'updatedDate';
|
||||||
|
export type PageGroupByType =
|
||||||
|
| 'createDate'
|
||||||
|
| 'updatedDate'
|
||||||
|
| 'tag'
|
||||||
|
| 'favourites'
|
||||||
|
| 'none';
|
||||||
|
|
||||||
export interface ListProps<T> {
|
export interface ListProps<T> {
|
||||||
// required data:
|
// required data:
|
||||||
@@ -89,7 +95,7 @@ export interface ListProps<T> {
|
|||||||
docCollection: DocCollection;
|
docCollection: DocCollection;
|
||||||
className?: string;
|
className?: string;
|
||||||
hideHeader?: boolean; // whether or not to hide the header. default is false (showing header)
|
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
|
isPreferredEdgeless?: (pageId: string) => boolean; // determines the icon used for each row
|
||||||
rowAsLink?: boolean;
|
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
|
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> {
|
export interface ItemGroupDefinition<T> {
|
||||||
id: string;
|
id: string;
|
||||||
// using a function to render custom group header
|
// using a function to render custom group header
|
||||||
label: (() => ReactNode) | ReactNode;
|
label: ((count: number) => ReactNode) | ReactNode;
|
||||||
match: (item: T) => boolean;
|
match: (item: T) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +152,7 @@ export type HeaderColDef = {
|
|||||||
alignment?: ColWrapperProps['alignment'];
|
alignment?: ColWrapperProps['alignment'];
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
hideInSmallContainer?: boolean;
|
hideInSmallContainer?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ColWrapperProps = PropsWithChildren<{
|
export type ColWrapperProps = PropsWithChildren<{
|
||||||
@@ -155,3 +162,10 @@ export type ColWrapperProps = PropsWithChildren<{
|
|||||||
hideInSmallContainer?: boolean;
|
hideInSmallContainer?: boolean;
|
||||||
}> &
|
}> &
|
||||||
React.HTMLAttributes<Element>;
|
React.HTMLAttributes<Element>;
|
||||||
|
|
||||||
|
export type PageDisplayProperties = {
|
||||||
|
bodyNotes: boolean;
|
||||||
|
tags: boolean;
|
||||||
|
createDate: boolean;
|
||||||
|
updatedDate: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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];
|
||||||
|
};
|
||||||
@@ -75,6 +75,7 @@ export const ColWrapper = forwardRef<HTMLDivElement, ColWrapperProps>(
|
|||||||
flex,
|
flex,
|
||||||
alignment,
|
alignment,
|
||||||
hideInSmallContainer,
|
hideInSmallContainer,
|
||||||
|
hidden,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
children,
|
children,
|
||||||
@@ -95,6 +96,7 @@ export const ColWrapper = forwardRef<HTMLDivElement, ColWrapperProps>(
|
|||||||
}}
|
}}
|
||||||
data-hide-item={hideInSmallContainer ? true : undefined}
|
data-hide-item={hideInSmallContainer ? true : undefined}
|
||||||
className={clsx(className, styles.colWrapper, {
|
className={clsx(className, styles.colWrapper, {
|
||||||
|
[styles.hidden]: hidden,
|
||||||
[styles.hideInSmallContainer]: hideInSmallContainer,
|
[styles.hideInSmallContainer]: hideInSmallContainer,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useCallback } from 'react';
|
|||||||
|
|
||||||
import { FilterList } from '../../filter/filter-list';
|
import { FilterList } from '../../filter/filter-list';
|
||||||
import { VariableSelect } from '../../filter/vars';
|
import { VariableSelect } from '../../filter/vars';
|
||||||
import { pageHeaderColsDef } from '../../header-col-def';
|
import { usePageHeaderColsDef } from '../../header-col-def';
|
||||||
import { PageListItemRenderer } from '../../page-group';
|
import { PageListItemRenderer } from '../../page-group';
|
||||||
import { ListTableHeader } from '../../page-header';
|
import { ListTableHeader } from '../../page-header';
|
||||||
import type { ListItem } from '../../types';
|
import type { ListItem } from '../../types';
|
||||||
@@ -47,6 +47,7 @@ export const PagesMode = ({
|
|||||||
publicMode: allPageListConfig.getPublicMode(meta.id),
|
publicMode: allPageListConfig.getPublicMode(meta.id),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||||
const { searchText, updateSearchText, searchedList } =
|
const { searchText, updateSearchText, searchedList } =
|
||||||
useSearch(filteredList);
|
useSearch(filteredList);
|
||||||
const clearSelected = useCallback(() => {
|
const clearSelected = useCallback(() => {
|
||||||
@@ -68,7 +69,7 @@ export const PagesMode = ({
|
|||||||
}, []);
|
}, []);
|
||||||
const pageHeaderRenderer = useCallback(() => {
|
const pageHeaderRenderer = useCallback(() => {
|
||||||
return <ListTableHeader headerCols={pageHeaderColsDef} />;
|
return <ListTableHeader headerCols={pageHeaderColsDef} />;
|
||||||
}, []);
|
}, [pageHeaderColsDef]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
@@ -127,7 +128,6 @@ export const PagesMode = ({
|
|||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
className={styles.pageList}
|
className={styles.pageList}
|
||||||
items={searchedList}
|
items={searchedList}
|
||||||
groupBy={false}
|
|
||||||
docCollection={allPageListConfig.docCollection}
|
docCollection={allPageListConfig.docCollection}
|
||||||
selectable
|
selectable
|
||||||
onSelectedIdsChange={ids => {
|
onSelectedIdsChange={ids => {
|
||||||
|
|||||||
@@ -265,7 +265,6 @@ export const RulesMode = ({
|
|||||||
hideHeader
|
hideHeader
|
||||||
className={styles.resultPages}
|
className={styles.resultPages}
|
||||||
items={rulesPages}
|
items={rulesPages}
|
||||||
groupBy={false}
|
|
||||||
docCollection={allPageListConfig.docCollection}
|
docCollection={allPageListConfig.docCollection}
|
||||||
isPreferredEdgeless={allPageListConfig.isEdgeless}
|
isPreferredEdgeless={allPageListConfig.isEdgeless}
|
||||||
operationsRenderer={operationsRenderer}
|
operationsRenderer={operationsRenderer}
|
||||||
@@ -285,7 +284,6 @@ export const RulesMode = ({
|
|||||||
hideHeader
|
hideHeader
|
||||||
className={styles.resultPages}
|
className={styles.resultPages}
|
||||||
items={allowListPages}
|
items={allowListPages}
|
||||||
groupBy={false}
|
|
||||||
docCollection={allPageListConfig.docCollection}
|
docCollection={allPageListConfig.docCollection}
|
||||||
isPreferredEdgeless={allPageListConfig.isEdgeless}
|
isPreferredEdgeless={allPageListConfig.isEdgeless}
|
||||||
operationsRenderer={operationsRenderer}
|
operationsRenderer={operationsRenderer}
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ export const SelectPage = ({
|
|||||||
items={searchedList}
|
items={searchedList}
|
||||||
docCollection={allPageListConfig.docCollection}
|
docCollection={allPageListConfig.docCollection}
|
||||||
selectable
|
selectable
|
||||||
groupBy={false}
|
|
||||||
onSelectedIdsChange={onChange}
|
onSelectedIdsChange={onChange}
|
||||||
selectedIds={value}
|
selectedIds={value}
|
||||||
isPreferredEdgeless={allPageListConfig.isEdgeless}
|
isPreferredEdgeless={allPageListConfig.isEdgeless}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AllPageListOperationsMenu,
|
AllPageListOperationsMenu,
|
||||||
|
PageDisplayMenu,
|
||||||
PageListNewPageButton,
|
PageListNewPageButton,
|
||||||
} from '@affine/core/components/page-list';
|
} from '@affine/core/components/page-list';
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
import { Header } from '@affine/core/components/pure/header';
|
||||||
@@ -32,15 +33,18 @@ export const AllPageHeader = ({
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<PageListNewPageButton
|
<>
|
||||||
size="small"
|
<PageListNewPageButton
|
||||||
className={clsx(
|
size="small"
|
||||||
styles.headerCreateNewButton,
|
className={clsx(
|
||||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
styles.headerCreateNewButton,
|
||||||
)}
|
!showCreateNew && styles.headerCreateNewButtonHidden
|
||||||
>
|
)}
|
||||||
<PlusIcon />
|
>
|
||||||
</PageListNewPageButton>
|
<PlusIcon />
|
||||||
|
</PageListNewPageButton>
|
||||||
|
<PageDisplayMenu />
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
center={<WorkspaceModeFilterTab activeFilter={'docs'} />}
|
center={<WorkspaceModeFilterTab activeFilter={'docs'} />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const scrollContainer = style({
|
|||||||
export const headerCreateNewButton = style({
|
export const headerCreateNewButton = style({
|
||||||
transition: 'opacity 0.1s ease-in-out',
|
transition: 'opacity 0.1s ease-in-out',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const headerCreateNewCollectionIconButton = style({
|
export const headerCreateNewCollectionIconButton = style({
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useFilteredPageMetas,
|
useFilteredPageMetas,
|
||||||
VirtualizedList,
|
VirtualizedList,
|
||||||
} from '@affine/core/components/page-list';
|
} 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 { Header } from '@affine/core/components/pure/header';
|
||||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||||
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '@affine/core/hooks/affine/use-block-suite-meta-helper';
|
||||||
@@ -60,6 +60,7 @@ export const TrashPage = () => {
|
|||||||
useBlockSuiteMetaHelper(docCollection);
|
useBlockSuiteMetaHelper(docCollection);
|
||||||
const { isPreferredEdgeless } = usePageHelper(docCollection);
|
const { isPreferredEdgeless } = usePageHelper(docCollection);
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||||
|
|
||||||
const pageOperationsRenderer = useCallback(
|
const pageOperationsRenderer = useCallback(
|
||||||
(item: ListItem) => {
|
(item: ListItem) => {
|
||||||
@@ -92,7 +93,7 @@ export const TrashPage = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
const pageHeaderRenderer = useCallback(() => {
|
const pageHeaderRenderer = useCallback(() => {
|
||||||
return <ListTableHeader headerCols={pageHeaderColsDef} />;
|
return <ListTableHeader headerCols={pageHeaderColsDef} />;
|
||||||
}, []);
|
}, [pageHeaderColsDef]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeaderIsland>
|
<ViewHeaderIsland>
|
||||||
@@ -104,7 +105,6 @@ export const TrashPage = () => {
|
|||||||
<VirtualizedList
|
<VirtualizedList
|
||||||
items={filteredPageMetas}
|
items={filteredPageMetas}
|
||||||
rowAsLink
|
rowAsLink
|
||||||
groupBy={false}
|
|
||||||
isPreferredEdgeless={isPreferredEdgeless}
|
isPreferredEdgeless={isPreferredEdgeless}
|
||||||
docCollection={currentWorkspace.docCollection}
|
docCollection={currentWorkspace.docCollection}
|
||||||
operationsRenderer={pageOperationsRenderer}
|
operationsRenderer={pageOperationsRenderer}
|
||||||
|
|||||||
@@ -1172,5 +1172,15 @@
|
|||||||
"com.affine.workbench.split-view.page-menu-open": "Open in split view",
|
"com.affine.workbench.split-view.page-menu-open": "Open in split view",
|
||||||
"com.affine.search-tags.placeholder": "Type here ...",
|
"com.affine.search-tags.placeholder": "Type here ...",
|
||||||
"com.affine.resetSyncStatus.button": "Reset Sync",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
|
clickPageMoreActions,
|
||||||
getBlockSuiteEditorTitle,
|
getBlockSuiteEditorTitle,
|
||||||
waitForAllPagesLoad,
|
waitForAllPagesLoad,
|
||||||
waitForEditorLoad,
|
waitForEditorLoad,
|
||||||
@@ -256,3 +257,54 @@ test('select a group of items by clicking "Select All" in group header', async (
|
|||||||
`${selectedItemCount} doc(s) selected`
|
`${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();
|
||||||
|
});
|
||||||
|
|||||||
@@ -226,7 +226,13 @@ export const PageListStory: StoryFn<ListProps<ListItem>> = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
PageListStory.args = {
|
PageListStory.args = {
|
||||||
groupBy: 'createDate',
|
groupBy: [
|
||||||
|
{
|
||||||
|
id: 'all',
|
||||||
|
label: count => `All Pages (${count})`,
|
||||||
|
match: () => true,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
PageListStory.argTypes = {
|
PageListStory.argTypes = {
|
||||||
|
|||||||
Reference in New Issue
Block a user