feat(core): remove old all docs code (#12558)

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

## Summary by CodeRabbit

- **Removed Features**
  - The "All Pages (Old)" workspace view and its associated header have been removed.
  - The previous page list UI, including virtualized lists, group headers, and multi-selection, is no longer available.
  - Search and tag aggregation features within the old page list have been removed.

- **Style**
  - Styles related to the old page list and its components have been deleted.

- **Navigation**
  - The "All Pages (Old)" route has been removed from workspace navigation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN
2025-05-27 08:33:43 +00:00
parent 5033142a77
commit ace5531b1f
16 changed files with 4 additions and 1875 deletions

View File

@@ -1,5 +1,2 @@
export * from './page-list-header';
export * from './page-list-item';
export * from './page-list-new-page-button';
export * from './page-tags';
export * from './virtualized-page-list';

View File

@@ -1,169 +0,0 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const docListHeader = style({
height: 100,
alignItems: 'center',
padding: '48px 16px 20px 24px',
overflow: 'hidden',
display: 'flex',
justifyContent: 'space-between',
background: cssVar('backgroundPrimaryColor'),
});
export const docListHeaderTitle = style({
fontSize: cssVar('fontH5'),
fontWeight: 500,
color: cssVar('textSecondaryColor'),
display: 'flex',
alignItems: 'center',
gap: 8,
height: '28px',
userSelect: 'none',
});
export const titleIcon = style({
color: cssVar('iconColor'),
display: 'inline-flex',
alignItems: 'center',
});
export const titleCollectionName = style({
color: cssVar('textPrimaryColor'),
});
export const tagSticky = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '1px 8px',
color: cssVar('textPrimaryColor'),
fontSize: cssVar('fontXs'),
borderRadius: '10px',
columnGap: '4px',
border: `1px solid ${cssVar('borderColor')}`,
background: cssVar('backgroundPrimaryColor'),
maxWidth: '30vw',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
height: '22px',
lineHeight: '1.67em',
cursor: 'pointer',
});
export const tagIndicator = style({
width: '8px',
height: '8px',
borderRadius: '50%',
flexShrink: 0,
});
export const tagLabel = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const arrowDownSmallIcon = style({
color: cssVar('iconColor'),
fontSize: '12px',
});
export const searchIcon = style({
color: cssVar('iconColor'),
fontSize: '20px',
});
export const tagsEditorRoot = style({
display: 'flex',
flexDirection: 'column',
width: '100%',
padding: '8px',
});
export const tagsMenu = style({
padding: 0,
width: '296px',
overflow: 'hidden',
});
export const tagsEditorSelectedTags = style({
display: 'flex',
gap: '8px',
flexWrap: 'nowrap',
padding: '6px 12px',
minHeight: 42,
alignItems: 'center',
});
export const searchInput = style({
flexGrow: 1,
padding: '10px 0',
margin: '-10px 0',
border: 'none',
outline: 'none',
fontSize: cssVar('fontSm'),
fontFamily: 'inherit',
color: 'inherit',
backgroundColor: 'transparent',
'::placeholder': {
color: cssVar('placeholderColor'),
},
overflow: 'hidden',
});
export const tagsEditorTagsSelector = style({
display: 'flex',
flexDirection: 'column',
maxHeight: '400px',
overflow: 'auto',
});
export const tagSelectorTagsScrollContainer = style({
overflowX: 'hidden',
position: 'relative',
maxHeight: '200px',
gap: '8px',
});
export const tagSelectorItem = style({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
padding: '4px 16px',
height: '32px',
gap: 8,
fontSize: cssVar('fontSm'),
cursor: 'pointer',
borderRadius: '4px',
color: cssVar('textPrimaryColor'),
':hover': {
backgroundColor: cssVar('hoverColor'),
},
':visited': {
color: cssVar('textPrimaryColor'),
},
selectors: {
'&.disable:hover': {
backgroundColor: 'unset',
cursor: 'auto',
},
},
});
export const tagIcon = style({
width: '8px',
height: '8px',
borderRadius: '50%',
flexShrink: 0,
});
export const tagSelectorItemText = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const rightButtonGroup = style({
display: 'flex',
gap: '12px',
alignItems: 'center',
justifyContent: 'center',
});
export const buttonText = style({
fontSize: cssVar('fontXs'),
fontWeight: 500,
color: cssVar('textPrimaryColor'),
});

View File

@@ -1,362 +0,0 @@
import {
Button,
Divider,
Menu,
RowInput,
Scrollable,
useConfirmModal,
} from '@affine/component';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { DocRecord } from '@affine/core/modules/doc';
import type { Tag } from '@affine/core/modules/tag';
import { TagService } from '@affine/core/modules/tag';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { inferOpenMode } from '@affine/core/utils';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import type { DocMode } from '@blocksuite/affine/model';
import {
ArrowDownSmallIcon,
SearchIcon,
ViewLayersIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService, useServices } from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
import {
type Collection,
CollectionService,
} from '../../../modules/collection';
import { SaveAsCollectionButton } from '../view';
import * as styles from './page-list-header.css';
import { PageListNewPageButton } from './page-list-new-page-button';
export const PageListHeader = () => {
const t = useI18n();
const { workspaceService, workspaceDialogService, workbenchService } =
useServices({
WorkspaceService,
WorkspaceDialogService,
WorkbenchService,
});
const workbench = workbenchService.workbench;
const workspace = workspaceService.workspace;
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
const title = useMemo(() => {
return t['com.affine.all-pages.header']();
}, [t]);
const handleOpenDocs = useCallback(
(result: {
docIds: string[];
entryId?: string;
isWorkspaceFile?: boolean;
}) => {
const { docIds, entryId, isWorkspaceFile } = result;
// If the imported file is a workspace file, open the entry page.
if (isWorkspaceFile && entryId) {
workbench.openDoc(entryId);
} else if (!docIds.length) {
return;
}
// Open all the docs when there are multiple docs imported.
if (docIds.length > 1) {
workbench.openAll();
} else {
// Otherwise, open the only doc.
workbench.openDoc(docIds[0]);
}
},
[workbench]
);
const onImportFile = useCallback(() => {
track.$.header.importModal.open();
workspaceDialogService.open('import', undefined, payload => {
if (!payload) {
return;
}
handleOpenDocs(payload);
});
}, [workspaceDialogService, handleOpenDocs]);
return (
<div className={styles.docListHeader}>
<div className={styles.docListHeaderTitle}>{title}</div>
<PageListNewPageButton
size="small"
data-testid="new-page-button-trigger"
onCreateEdgeless={e => createEdgeless({ at: inferOpenMode(e) })}
onCreatePage={e =>
createPage('page' as DocMode, { at: inferOpenMode(e) })
}
onCreateDoc={e => createPage(undefined, { at: inferOpenMode(e) })}
onImportFile={onImportFile}
>
<div className={styles.buttonText}>{t['New Page']()}</div>
</PageListNewPageButton>
</div>
);
};
/**
* @deprecated
*/
export const CollectionPageListHeader = ({
collection,
workspaceId,
}: {
collection: Collection;
workspaceId: string;
}) => {
const t = useI18n();
const { jumpToCollections } = useNavigateHelper();
const { collectionService, workspaceService, workspaceDialogService } =
useServices({
CollectionService,
WorkspaceService,
WorkspaceDialogService,
});
const handleJumpToCollections = useCallback(() => {
jumpToCollections(workspaceId);
}, [jumpToCollections, workspaceId]);
const handleEdit = useCallback(() => {
workspaceDialogService.open('collection-editor', {
collectionId: collection.id,
});
}, [collection, workspaceDialogService]);
const workspace = workspaceService.workspace;
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
const { openConfirmModal } = useConfirmModal();
const name = useLiveData(collection.name$);
const createAndAddDocument = useCallback(
(createDocumentFn: () => DocRecord) => {
const newDoc = createDocumentFn();
collectionService.addDocToCollection(collection.id, newDoc.id);
},
[collection.id, collectionService]
);
const onConfirmAddDocument = useCallback(
(createDocumentFn: () => DocRecord) => {
openConfirmModal({
title: t['com.affine.collection.add-doc.confirm.title'](),
description: t['com.affine.collection.add-doc.confirm.description'](),
cancelText: t['Cancel'](),
confirmText: t['Confirm'](),
confirmButtonOptions: {
variant: 'primary',
},
onConfirm: () => createAndAddDocument(createDocumentFn),
});
},
[openConfirmModal, t, createAndAddDocument]
);
const createPageModeDoc = useCallback(
() => createPage('page' as DocMode),
[createPage]
);
const onCreateEdgeless = useCallback(
() => onConfirmAddDocument(createEdgeless),
[createEdgeless, onConfirmAddDocument]
);
const onCreatePage = useCallback(() => {
onConfirmAddDocument(createPageModeDoc);
}, [createPageModeDoc, onConfirmAddDocument]);
const onCreateDoc = useCallback(() => {
onConfirmAddDocument(createPage);
}, [createPage, onConfirmAddDocument]);
return (
<div className={styles.docListHeader}>
<div className={styles.docListHeaderTitle}>
<div style={{ cursor: 'pointer' }} onClick={handleJumpToCollections}>
{t['com.affine.collections.header']()} /
</div>
<div className={styles.titleIcon}>
<ViewLayersIcon />
</div>
<div className={styles.titleCollectionName}>{name}</div>
</div>
<div className={styles.rightButtonGroup}>
<Button onClick={handleEdit}>{t['Edit']()}</Button>
<PageListNewPageButton
size="small"
data-testid="new-page-button-trigger"
onCreateDoc={onCreateDoc}
onCreateEdgeless={onCreateEdgeless}
onCreatePage={onCreatePage}
>
<div className={styles.buttonText}>{t['New Page']()}</div>
</PageListNewPageButton>
</div>
</div>
);
};
/**
* @deprecated
*/
export const TagPageListHeader = ({
tag,
workspaceId,
}: {
tag: Tag;
workspaceId: string;
}) => {
const tagColor = useLiveData(tag.color$);
const tagTitle = useLiveData(tag.value$);
const t = useI18n();
const { jumpToTags, jumpToCollection } = useNavigateHelper();
const collectionService = useService(CollectionService);
const [openMenu, setOpenMenu] = useState(false);
const handleJumpToTags = useCallback(() => {
jumpToTags(workspaceId);
}, [jumpToTags, workspaceId]);
const saveToCollection = useCallback(
(collectionName: string) => {
const id = collectionService.createCollection({
name: collectionName,
rules: {
filters: [
{
type: 'system',
key: 'tags',
method: 'include-all',
value: tag.id,
},
],
},
});
jumpToCollection(workspaceId, id);
},
[collectionService, tag.id, jumpToCollection, workspaceId]
);
return (
<div className={styles.docListHeader}>
<div className={styles.docListHeaderTitle}>
<div
style={{ cursor: 'pointer', lineHeight: '1.4em' }}
onClick={handleJumpToTags}
>
{t['Tags']()} /
</div>
<Menu
rootOptions={{
open: openMenu,
onOpenChange: setOpenMenu,
}}
contentOptions={{
side: 'bottom',
align: 'start',
sideOffset: 18,
avoidCollisions: false,
className: styles.tagsMenu,
}}
items={<SwitchTag onClick={setOpenMenu} />}
>
<div className={styles.tagSticky}>
<div
className={styles.tagIndicator}
style={{
backgroundColor: tagColor,
}}
/>
<div className={styles.tagLabel}>{tagTitle}</div>
<ArrowDownSmallIcon className={styles.arrowDownSmallIcon} />
</div>
</Menu>
</div>
<SaveAsCollectionButton onConfirm={saveToCollection} />
</div>
);
};
interface SwitchTagProps {
onClick: (open: boolean) => void;
}
export const SwitchTag = ({ onClick }: SwitchTagProps) => {
const t = useI18n();
const [inputValue, setInputValue] = useState('');
const tagList = useService(TagService).tagList;
const filteredTags = useLiveData(
inputValue ? tagList.filterTagsByName$(inputValue) : tagList.tags$
);
const onInputChange = useCallback((value: string) => {
setInputValue(value);
}, []);
const handleClick = useCallback(() => {
setInputValue('');
onClick(false);
}, [onClick]);
return (
<div className={styles.tagsEditorRoot}>
<div className={styles.tagsEditorSelectedTags}>
<SearchIcon className={styles.searchIcon} />
<RowInput
value={inputValue}
onChange={onInputChange}
autoFocus
className={styles.searchInput}
placeholder={t['com.affine.search-tags.placeholder']()}
/>
</div>
<Divider />
<div className={styles.tagsEditorTagsSelector}>
<Scrollable.Root>
<Scrollable.Viewport
className={styles.tagSelectorTagsScrollContainer}
>
{filteredTags.map(tag => {
return <TagLink key={tag.id} tag={tag} onClick={handleClick} />;
})}
{filteredTags.length === 0 ? (
<div className={clsx(styles.tagSelectorItem, 'disable')}>
{t['Find 0 result']()}
</div>
) : null}
</Scrollable.Viewport>
<Scrollable.Scrollbar style={{ transform: 'translateX(6px)' }} />
</Scrollable.Root>
</div>
</div>
);
};
const TagLink = ({ tag, onClick }: { tag: Tag; onClick: () => void }) => {
const tagColor = useLiveData(tag.color$);
const tagTitle = useLiveData(tag.value$);
return (
<Link
key={tag.id}
className={styles.tagSelectorItem}
data-tag-id={tag.id}
data-tag-value={tagTitle}
to={`/tag/${tag.id}`}
onClick={onClick}
>
<div className={styles.tagIcon} style={{ background: tagColor }} />
<div className={styles.tagSelectorItemText}>{tagTitle}</div>
</Link>
);
};

View File

@@ -1,152 +0,0 @@
import { cssVar } from '@toeverything/theme';
import { globalStyle, style } from '@vanilla-extract/css';
export const root = style({
display: 'flex',
color: cssVar('textPrimaryColor'),
height: '54px',
// 42 + 12
flexShrink: 0,
width: '100%',
alignItems: 'stretch',
contain: 'strict',
transition: 'background-color 0.2s, opacity 0.2s',
':hover': {
backgroundColor: cssVar('hoverColor'),
},
overflow: 'hidden',
cursor: 'default',
selectors: {
'&[data-clickable=true]': {
cursor: 'pointer',
},
},
});
export const dragPageItemOverlay = style({
height: '45px',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
background: cssVar('hoverColorFilled'),
boxShadow: cssVar('menuShadow'),
maxWidth: '360px',
minWidth: '260px',
});
export const dndCell = style({
position: 'relative',
marginLeft: -8,
height: '100%',
outline: 'none',
paddingLeft: 8,
});
globalStyle(`[data-draggable=true] ${dndCell}:before`, {
content: '""',
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
left: 0,
width: 4,
height: 4,
transition: 'height 0.2s, opacity 0.2s',
backgroundColor: cssVar('placeholderColor'),
borderRadius: '2px',
opacity: 0,
});
globalStyle(`[data-draggable=true] ${dndCell}:hover:before`, {
height: 12,
opacity: 1,
});
globalStyle(`[data-draggable=true][data-dragging=true] ${dndCell}`, {
opacity: 0.5,
});
globalStyle(`[data-draggable=true][data-dragging=true] ${dndCell}:before`, {
height: 32,
width: 2,
opacity: 1,
});
globalStyle(`${root} > :first-child`, {
paddingLeft: '16px',
});
globalStyle(`${root} > :last-child`, {
paddingRight: '8px',
});
export const titleIconsWrapper = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '10px',
});
export const selectionCell = style({
display: 'flex',
alignItems: 'center',
flexShrink: 0,
fontSize: cssVar('fontH3'),
});
export const titleCell = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
padding: '0 16px',
maxWidth: 'calc(100% - 64px)',
flex: 1,
whiteSpace: 'nowrap',
userSelect: 'none',
});
export const titleCellMain = style({
overflow: 'hidden',
fontSize: cssVar('fontSm'),
fontWeight: 600,
whiteSpace: 'nowrap',
flex: 1,
textOverflow: 'ellipsis',
alignSelf: 'stretch',
});
export const titleCellPreview = style({
overflow: 'hidden',
color: cssVar('textSecondaryColor'),
fontSize: cssVar('fontXs'),
flex: 1,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
alignSelf: 'stretch',
});
export const iconCell = style({
display: 'flex',
alignItems: 'center',
fontSize: cssVar('fontH3'),
color: cssVar('iconColor'),
flexShrink: 0,
});
export const tagsCell = style({
display: 'flex',
alignItems: 'center',
fontSize: cssVar('fontXs'),
color: cssVar('textSecondaryColor'),
padding: '0 8px',
height: '60px',
width: '100%',
});
export const dateCell = style({
display: 'flex',
alignItems: 'center',
fontSize: cssVar('fontXs'),
color: cssVar('textPrimaryColor'),
flexShrink: 0,
flexWrap: 'nowrap',
padding: '0 8px',
userSelect: 'none',
});
export const actionsCellWrapper = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
flexShrink: 0,
});
export const operationsCell = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
columnGap: '6px',
flexShrink: 0,
});

View File

@@ -1,398 +0,0 @@
import { Checkbox, Tooltip, useDraggable } from '@affine/component';
import { TagService } from '@affine/core/modules/tag';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { stopPropagation } from '@affine/core/utils';
import { i18nTime } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import type { ForwardedRef, PropsWithChildren } from 'react';
import { forwardRef, useCallback, useEffect, useMemo } from 'react';
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
import {
anchorIndexAtom,
rangeIdsAtom,
selectionStateAtom,
useAtom,
} from '../scoped-atoms';
import type { PageListItemProps } from '../types';
import { useAllDocDisplayProperties } from '../use-all-doc-display-properties';
import { ColWrapper } from '../utils';
import * as styles from './page-list-item.css';
import { PageTags } from './page-tags';
const ListTitleCell = ({
title,
preview,
}: Pick<PageListItemProps, 'title' | 'preview'>) => {
const [displayProperties] = useAllDocDisplayProperties();
return (
<div data-testid="page-list-item-title" className={styles.titleCell}>
<div
data-testid="page-list-item-title-text"
className={styles.titleCellMain}
>
{title}
</div>
{preview && displayProperties.displayProperties.bodyNotes ? (
<div
data-testid="page-list-item-preview-text"
className={styles.titleCellPreview}
>
{preview}
</div>
) : null}
</div>
);
};
const ListIconCell = ({ icon }: Pick<PageListItemProps, 'icon'>) => {
return (
<div data-testid="page-list-item-icon" className={styles.iconCell}>
{icon}
</div>
);
};
const PageSelectionCell = ({
selectable,
onSelectedChange,
selected,
}: Pick<PageListItemProps, 'selectable' | 'onSelectedChange' | 'selected'>) => {
const onSelectionChange = useCallback(
(_event: React.ChangeEvent<HTMLInputElement>) => {
return onSelectedChange?.();
},
[onSelectedChange]
);
if (!selectable) {
return null;
}
return (
<div className={styles.selectionCell}>
<Checkbox
onClick={stopPropagation}
checked={!!selected}
onChange={onSelectionChange}
/>
</div>
);
};
export const PageTagsCell = ({ pageId }: Pick<PageListItemProps, 'pageId'>) => {
const tagList = useService(TagService).tagList;
const tags = useLiveData(tagList.tagsByPageId$(pageId));
return (
<div data-testid="page-list-item-tags" className={styles.tagsCell}>
<PageTags
tags={tags}
hoverExpandDirection="left"
widthOnHover="300%"
maxItems={5}
/>
</div>
);
};
const PageCreateDateCell = ({
createDate,
}: Pick<PageListItemProps, 'createDate'>) => {
return (
<Tooltip content={i18nTime(createDate)}>
<div
data-testid="page-list-item-date"
data-date-raw={createDate}
className={styles.dateCell}
>
{i18nTime(createDate, {
relative: true,
})}
</div>
</Tooltip>
);
};
const PageUpdatedDateCell = ({
updatedDate,
}: Pick<PageListItemProps, 'updatedDate'>) => {
return (
<Tooltip content={updatedDate ? i18nTime(updatedDate) : undefined}>
<div
data-testid="page-list-item-date"
data-date-raw={updatedDate}
className={styles.dateCell}
>
{updatedDate
? i18nTime(updatedDate, {
relative: true,
})
: '-'}
</div>
</Tooltip>
);
};
const PageListOperationsCell = ({
operations,
}: Pick<PageListItemProps, 'operations'>) => {
return operations ? (
<div onClick={stopPropagation} className={styles.operationsCell}>
{operations}
</div>
) : null;
};
export const PageListItem = (props: PageListItemProps) => {
const [displayProperties] = useAllDocDisplayProperties();
const pageTitleElement = useMemo(() => {
return (
<div className={styles.dragPageItemOverlay}>
<div className={styles.titleIconsWrapper}>
<PageSelectionCell
onSelectedChange={props.onSelectedChange}
selectable={props.selectable}
selected={props.selected}
/>
<ListIconCell icon={props.icon} />
</div>
<ListTitleCell title={props.title} preview={props.preview} />
</div>
);
}, [
props.icon,
props.onSelectedChange,
props.preview,
props.selectable,
props.selected,
props.title,
]);
const { dragRef, CustomDragPreview, dragging } = useDraggable<AffineDNDData>(
() => ({
canDrag: props.draggable,
data: {
entity: {
type: 'doc',
id: props.pageId,
},
from: {
at: 'all-docs:list',
},
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.draggable, props.pageId, props.selectable]
);
return (
<>
<PageListItemWrapper
onClick={props.onClick}
to={props.to}
pageId={props.pageId}
draggable={props.draggable}
isDragging={dragging}
ref={dragRef}
pageIds={props.pageIds || []}
>
<ColWrapper flex={9}>
<ColWrapper className={styles.dndCell} flex={8}>
<div className={styles.titleIconsWrapper}>
<PageSelectionCell
onSelectedChange={props.onSelectedChange}
selectable={props.selectable}
selected={props.selected}
/>
<ListIconCell icon={props.icon} />
</div>
<ListTitleCell title={props.title} preview={props.preview} />
</ColWrapper>
<ColWrapper
flex={4}
alignment="end"
style={{ overflow: 'visible' }}
hidden={!displayProperties.displayProperties.tags}
>
<PageTagsCell pageId={props.pageId} />
</ColWrapper>
</ColWrapper>
<ColWrapper
flex={1}
alignment="end"
hideInSmallContainer
hidden={!displayProperties.displayProperties.createDate}
>
<PageCreateDateCell createDate={props.createDate} />
</ColWrapper>
<ColWrapper
flex={1}
alignment="end"
hideInSmallContainer
hidden={!displayProperties.displayProperties.updatedDate}
>
<PageUpdatedDateCell updatedDate={props.updatedDate} />
</ColWrapper>
{props.operations ? (
<ColWrapper
className={styles.actionsCellWrapper}
flex={1}
alignment="end"
>
<PageListOperationsCell operations={props.operations} />
</ColWrapper>
) : null}
</PageListItemWrapper>
<CustomDragPreview position="pointer-outside">
{pageTitleElement}
</CustomDragPreview>
</>
);
};
type PageListWrapperProps = PropsWithChildren<
Pick<PageListItemProps, 'to' | 'pageId' | 'onClick' | 'draggable'> & {
isDragging: boolean;
pageIds: string[];
}
>;
const PageListItemWrapper = forwardRef(
(
{
to,
isDragging,
pageId,
pageIds,
onClick,
children,
draggable,
}: PageListWrapperProps,
ref: ForwardedRef<HTMLAnchorElement & HTMLDivElement>
) => {
const [selectionState, setSelectionActive] = useAtom(selectionStateAtom);
const [anchorIndex, setAnchorIndex] = useAtom(anchorIndexAtom);
const [rangeIds, setRangeIds] = useAtom(rangeIdsAtom);
const handleShiftClick = useCallback(
(currentIndex: number) => {
if (anchorIndex === undefined) {
setAnchorIndex(currentIndex);
onClick?.();
return;
}
const lowerIndex = Math.min(anchorIndex, currentIndex);
const upperIndex = Math.max(anchorIndex, currentIndex);
const newRangeIds = pageIds.slice(lowerIndex, upperIndex + 1);
const currentSelected = selectionState.selectedIds || [];
// Set operations
const setRange = new Set(rangeIds);
const newSelected = new Set(
currentSelected.filter(id => !setRange.has(id)).concat(newRangeIds)
);
selectionState.onSelectedIdsChange?.([...newSelected]);
setRangeIds(newRangeIds);
},
[
anchorIndex,
onClick,
pageIds,
selectionState,
setAnchorIndex,
rangeIds,
setRangeIds,
]
);
const handleClick = useCallback(
(e: React.MouseEvent) => {
if (!selectionState.selectable) {
return;
}
e.stopPropagation();
const currentIndex = pageIds.indexOf(pageId);
if (e.shiftKey) {
e.preventDefault();
if (!selectionState.selectionActive) {
setSelectionActive(true);
setAnchorIndex(currentIndex);
onClick?.();
} else {
handleShiftClick(currentIndex);
}
} else {
setAnchorIndex(undefined);
setRangeIds([]);
onClick?.();
return;
}
},
[
handleShiftClick,
onClick,
pageId,
pageIds,
selectionState.selectable,
selectionState.selectionActive,
setAnchorIndex,
setRangeIds,
setSelectionActive,
]
);
const commonProps = useMemo(
() => ({
role: 'list-item',
'data-testid': 'page-list-item',
'data-page-id': pageId,
'data-draggable': draggable,
className: styles.root,
'data-clickable': !!onClick || !!to,
'data-dragging': isDragging,
onClick: onClick ? handleClick : undefined,
}),
[pageId, draggable, onClick, to, isDragging, handleClick]
);
useEffect(() => {
if (selectionState.selectionActive) {
// listen for shift key up
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
setAnchorIndex(undefined);
setRangeIds([]);
}
};
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keyup', handleKeyUp);
};
}
return;
}, [
selectionState.selectionActive,
setAnchorIndex,
setRangeIds,
setSelectionActive,
]);
if (to) {
return (
<WorkbenchLink ref={ref} draggable={false} {...commonProps} to={to}>
{children}
</WorkbenchLink>
);
} else {
return (
<div ref={ref} {...commonProps}>
{children}
</div>
);
}
}
);
PageListItemWrapper.displayName = 'PageListItemWrapper';

View File

@@ -1,14 +1,6 @@
import { Menu } from '@affine/component';
import { TagItem as TagItemComponent } from '@affine/core/components/tags';
import type { Tag } from '@affine/core/modules/tag';
import { stopPropagation } from '@affine/core/utils';
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
import { LiveData, useLiveData } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { useMemo } from 'react';
import * as styles from './page-tags.css';
import { useLiveData } from '@toeverything/infra';
export interface PageTagsProps {
tags: Tag[];
@@ -47,90 +39,3 @@ export const TagItem = ({ tag, ...props }: TagItemProps) => {
/>
);
};
const TagItemNormal = ({
tags,
maxItems,
}: {
tags: Tag[];
maxItems?: number;
}) => {
const nTags = useMemo(() => {
return maxItems ? tags.slice(0, maxItems) : tags;
}, [maxItems, tags]);
const tagsOrdered = useLiveData(
useMemo(() => {
return LiveData.computed(get =>
[...nTags].sort((a, b) => get(a.value$).length - get(b.value$).length)
);
}, [nTags])
);
return useMemo(
() =>
tagsOrdered.map((tag, idx) => (
<TagItem key={tag.id} tag={tag} idx={idx} mode="inline" />
)),
[tagsOrdered]
);
};
export const PageTags = ({
tags,
widthOnHover,
maxItems,
hoverExpandDirection,
}: PageTagsProps) => {
const sanitizedWidthOnHover = widthOnHover
? typeof widthOnHover === 'string'
? widthOnHover
: `${widthOnHover}px`
: 'auto';
const tagsInPopover = useMemo(() => {
const lastTags = tags.slice(maxItems);
return (
<div className={styles.tagsListContainer}>
{lastTags.map((tag, idx) => (
<TagItem key={tag.id} tag={tag} idx={idx} mode="list-item" />
))}
</div>
);
}, [maxItems, tags]);
return (
<div
data-testid="page-tags"
className={styles.root}
style={assignInlineVars({
[styles.hoverMaxWidth]: sanitizedWidthOnHover,
})}
>
<div
style={{
right: hoverExpandDirection === 'left' ? 0 : 'auto',
left: hoverExpandDirection === 'right' ? 0 : 'auto',
}}
className={clsx(styles.innerContainer)}
>
<div className={styles.innerBackdrop} />
<div className={styles.tagsScrollContainer}>
<TagItemNormal tags={tags} maxItems={maxItems} />
</div>
{maxItems && tags.length > maxItems ? (
<Menu
items={tagsInPopover}
contentOptions={{
onClick: stopPropagation,
}}
>
<div className={styles.showMoreTag}>
<MoreHorizontalIcon />
</div>
</Menu>
) : null}
</div>
</div>
);
};

View File

@@ -1,13 +0,0 @@
import type { DocMeta } from '@blocksuite/affine/store';
import { useState } from 'react';
export const useSearch = (list: DocMeta[]) => {
const [value, onChange] = useState('');
return {
searchText: value,
updateSearchText: onChange,
searchedList: value
? list.filter(v => v.title.toLowerCase().includes(value.toLowerCase()))
: list,
};
};

View File

@@ -1,211 +0,0 @@
import { toast, useConfirmModal } from '@affine/component';
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
import { type Collection } from '@affine/core/modules/collection';
import { DocsService } from '@affine/core/modules/doc';
import type { Tag } from '@affine/core/modules/tag';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { Trans, useI18n } from '@affine/i18n';
import type { DocMeta } from '@blocksuite/affine/store';
import { useLiveData, useService } from '@toeverything/infra';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
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';
import type { ItemListHandle, ListItem } from '../types';
import { VirtualizedList } from '../virtualized-list';
import {
CollectionPageListHeader,
PageListHeader,
TagPageListHeader,
} from './page-list-header';
const usePageOperationsRenderer = (collection?: Collection) => {
const t = useI18n();
const removeFromAllowList = useCallback(
(id: string) => {
collection?.removeDoc(id);
toast(t['com.affine.collection.removePage.success']());
},
[collection, t]
);
const pageOperationsRenderer = useCallback(
(page: DocMeta, isInAllowList?: boolean) => {
return (
<PageOperationCell
page={page}
isInAllowList={isInAllowList}
onRemoveFromAllowList={() => removeFromAllowList(page.id)}
/>
);
},
[removeFromAllowList]
);
return pageOperationsRenderer;
};
export const VirtualizedPageList = memo(function VirtualizedPageList({
tag,
collection,
listItem,
setHideHeaderCreateNewPage,
disableMultiDelete,
}: {
tag?: Tag;
collection?: Collection;
listItem?: DocMeta[];
setHideHeaderCreateNewPage?: (hide: boolean) => void;
disableMultiDelete?: boolean;
}) {
const t = useI18n();
const listRef = useRef<ItemListHandle>(null);
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
const currentWorkspace = useService(WorkspaceService).workspace;
const docsService = useService(DocsService);
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
const pageOperations = usePageOperationsRenderer(collection);
const pageHeaderColsDef = usePageHeaderColsDef();
const [filteredPageIds, setFilteredPageIds] = useState<string[]>([]);
useEffect(() => {
const subscription = collection?.watch().subscribe(docIds => {
setFilteredPageIds(docIds);
});
return () => subscription?.unsubscribe();
}, [collection]);
const allowList = useLiveData(collection?.info$.map(info => info.allowList));
const pageMetasToRender = useMemo(() => {
if (listItem) {
return listItem;
}
if (collection) {
return pageMetas.filter(
page => filteredPageIds.includes(page.id) && !page.trash
);
}
return pageMetas.filter(page => !page.trash);
}, [collection, filteredPageIds, listItem, pageMetas]);
const filteredSelectedPageIds = useMemo(() => {
const ids = new Set(pageMetasToRender.map(page => page.id));
return selectedPageIds.filter(id => ids.has(id));
}, [pageMetasToRender, selectedPageIds]);
const hideFloatingToolbar = useCallback(() => {
listRef.current?.toggleSelectable();
}, []);
const pageOperationRenderer = useCallback(
(item: ListItem) => {
const page = item as DocMeta;
const isInAllowList = allowList?.includes(page.id);
return pageOperations(page, isInAllowList);
},
[allowList, pageOperations]
);
const pageHeaderRenderer = useCallback(() => {
return <ListTableHeader headerCols={pageHeaderColsDef} />;
}, [pageHeaderColsDef]);
const pageItemRenderer = useCallback((item: ListItem) => {
return <PageListItemRenderer {...item} />;
}, []);
const heading = useMemo(() => {
if (tag) {
return <TagPageListHeader workspaceId={currentWorkspace.id} tag={tag} />;
}
if (collection) {
return (
<CollectionPageListHeader
workspaceId={currentWorkspace.id}
collection={collection}
/>
);
}
return <PageListHeader />;
}, [collection, currentWorkspace.id, tag]);
const { openConfirmModal } = useConfirmModal();
const handleMultiDelete = useCallback(() => {
if (filteredSelectedPageIds.length === 0) {
return;
}
openConfirmModal({
title: t['com.affine.moveToTrash.confirmModal.title.multiple']({
number: filteredSelectedPageIds.length.toString(),
}),
description: t[
'com.affine.moveToTrash.confirmModal.description.multiple'
]({
number: filteredSelectedPageIds.length.toString(),
}),
cancelText: t['com.affine.confirmModal.button.cancel'](),
confirmText: t.Delete(),
confirmButtonOptions: {
variant: 'error',
},
onConfirm: () => {
for (const docId of filteredSelectedPageIds) {
const doc = docsService.list.doc$(docId).value;
doc?.moveToTrash();
}
},
});
hideFloatingToolbar();
}, [
docsService.list,
filteredSelectedPageIds,
hideFloatingToolbar,
openConfirmModal,
t,
]);
const group = usePageItemGroupDefinitions();
return (
<>
<VirtualizedList
ref={listRef}
selectable="toggle"
draggable
atTopThreshold={80}
atTopStateChange={setHideHeaderCreateNewPage}
onSelectionActiveChange={setShowFloatingToolbar}
heading={heading}
groupBy={group}
selectedIds={filteredSelectedPageIds}
onSelectedIdsChange={setSelectedPageIds}
items={pageMetasToRender}
rowAsLink
docCollection={currentWorkspace.docCollection}
operationsRenderer={pageOperationRenderer}
itemRenderer={pageItemRenderer}
headerRenderer={pageHeaderRenderer}
/>
<ListFloatingToolbar
open={showFloatingToolbar}
onDelete={disableMultiDelete ? undefined : handleMultiDelete}
onClose={hideFloatingToolbar}
content={
<Trans
i18nKey="com.affine.page.toolbar.selected"
count={filteredSelectedPageIds.length}
>
<div style={{ color: 'var(--affine-text-secondary-color)' }}>
{{ count: filteredSelectedPageIds.length } as any}
</div>
selected
</Trans>
}
/>
</>
);
});

View File

@@ -4,7 +4,6 @@ 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 './group-definitions';
export * from './header-col-def';

View File

@@ -2,7 +2,6 @@ import { cssVar } from '@toeverything/theme';
import { createContainer, style } from '@vanilla-extract/css';
import { root as collectionItemRoot } from './collections/collection-list-item.css';
import { root as pageItemRoot } from './docs/page-list-item.css';
import { root as tagItemRoot } from './tags/tag-list-item.css';
export const listRootContainer = createContainer('list-root-container');
export const pageListScrollContainer = style({
@@ -50,7 +49,7 @@ export const favoriteCell = style({
flexShrink: 0,
opacity: 0,
selectors: {
[`&[data-favorite], ${pageItemRoot}:hover &, ${collectionItemRoot}:hover &, ${tagItemRoot}:hover &`]:
[`&[data-favorite], ${collectionItemRoot}:hover &, ${tagItemRoot}:hover &`]:
{
opacity: 1,
},

View File

@@ -1,47 +1,14 @@
import { Scrollable, useHasScrollTop } from '@affine/component';
import clsx from 'clsx';
import type { ForwardedRef, PropsWithChildren } from 'react';
import {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
} from 'react';
import { memo, useEffect, useImperativeHandle } from 'react';
import { usePageHeaderColsDef } from './header-col-def';
import * as styles from './list.css';
import { ItemGroup } from './page-group';
import { ListTableHeader } from './page-header';
import {
groupsAtom,
listPropsAtom,
ListProvider,
selectionStateAtom,
useAtom,
useAtomValue,
useSetAtom,
} from './scoped-atoms';
import type { ItemListHandle, ListItem, ListProps } from './types';
/**
* Given a list of pages, render a list of pages
*/
export const List = forwardRef<ItemListHandle, ListProps<ListItem>>(
function List(props, ref) {
return (
// push pageListProps to the atom so that downstream components can consume it
// this makes sure pageListPropsAtom is always populated
// @ts-expect-error jotai-scope is not well typed, AnyWritableAtom is should be any rather than unknown
<ListProvider initialValues={[[listPropsAtom, props]]}>
<ListInnerWrapper {...props} handleRef={ref}>
<ListInner {...props} />
</ListInnerWrapper>
</ListProvider>
);
}
);
// when pressing ESC or double clicking outside of the page list, close the selection mode
// TODO(@Peng): use jotai-effect instead but it seems it does not work with jotai-scope?
const useItemSelectionStateEffect = () => {
@@ -132,58 +99,3 @@ export const ListInnerWrapper = memo(
);
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)}>
{!hideHeader ? <ListTableHeader headerCols={pageHeaderColsDef} /> : null}
<div className={styles.groupsContainer}>
{groups.map(group => (
<ItemGroup key={group.id} {...group} />
))}
</div>
</div>
);
};
interface ListScrollContainerProps {
className?: string;
style?: React.CSSProperties;
}
export const ListScrollContainer = forwardRef<
HTMLDivElement,
PropsWithChildren<ListScrollContainerProps>
>(({ className, children, style }, ref) => {
const [setContainer, hasScrollTop] = useHasScrollTop();
const setNodeRef = useCallback(
(r: HTMLDivElement) => {
if (ref) {
if (typeof ref === 'function') {
ref(r);
} else {
ref.current = r;
}
}
return setContainer(r);
},
[ref, setContainer]
);
return (
<Scrollable.Root
style={style}
data-has-scroll-top={hasScrollTop}
className={clsx(styles.pageListScrollContainer, className)}
>
<Scrollable.Viewport ref={setNodeRef}>{children}</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
);
});
ListScrollContainer.displayName = 'ListScrollContainer';

View File

@@ -1,23 +1,15 @@
import { shallowEqual } from '@affine/component';
import type { CollectionMeta } from '@affine/core/modules/collection';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { useI18n } from '@affine/i18n';
import type { DocMeta } from '@blocksuite/affine/store';
import { ToggleRightIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
import * as Collapsible from '@radix-ui/react-collapsible';
import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import { selectAtom } from 'jotai/utils';
import type { MouseEventHandler } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { CollectionListItem } from './collections/collection-list-item';
import { PageListItem } from './docs/page-list-item';
import { PagePreview } from './page-content-preview';
import * as styles from './page-group.css';
import {
groupCollapseStateAtom,
groupsAtom,
listPropsAtom,
selectionStateAtom,
useAtom,
@@ -29,7 +21,6 @@ import type {
ItemGroupProps,
ListItem,
ListProps,
PageListItemProps,
TagListItemProps,
TagMeta,
} from './types';
@@ -111,82 +102,6 @@ export const ItemGroupHeader = memo(function ItemGroupHeader<
) : null;
});
export const ItemGroup = <T extends ListItem>({
id,
items,
label,
}: ItemGroupProps<T>) => {
const [collapsed, setCollapsed] = useState(false);
const onExpandedClicked: MouseEventHandler = useCallback(e => {
e.stopPropagation();
e.preventDefault();
setCollapsed(v => !v);
}, []);
const selectionState = useAtomValue(selectionStateAtom);
const selectedItems = useMemo(() => {
const selectedIds = selectionState.selectedIds ?? [];
return items.filter(item => selectedIds.includes(item.id));
}, [items, selectionState.selectedIds]);
const onSelectAll = useCallback(() => {
const nonCurrentGroupIds =
selectionState.selectedIds?.filter(
id => !items.map(item => item.id).includes(id)
) ?? [];
selectionState.onSelectedIdsChange?.([
...nonCurrentGroupIds,
...items.map(item => item.id),
]);
}, [items, selectionState]);
const t = useI18n();
return (
<Collapsible.Root
data-testid="page-list-group"
data-group-id={id}
open={!collapsed}
className={clsx(styles.root)}
>
{label ? (
<div data-testid="page-list-group-header" className={styles.header}>
<Collapsible.Trigger
role="button"
onClick={onExpandedClicked}
data-testid="page-list-group-header-collapsed-button"
className={styles.collapsedIconContainer}
>
<ToggleRightIcon
className={styles.collapsedIcon}
data-collapsed={collapsed !== false}
/>
</Collapsible.Trigger>
<div className={styles.headerLabel}>{label}</div>
{selectionState.selectionActive ? (
<div className={styles.headerCount}>
{selectedItems.length}/{items.length}
</div>
) : null}
<div className={styles.spacer} />
{selectionState.selectionActive ? (
<button className={styles.selectAllButton} onClick={onSelectAll}>
{t['com.affine.page.group-header.select-all']()}
</button>
) : null}
</div>
) : null}
<Collapsible.Content
className={styles.collapsibleContent}
data-state={!collapsed ? 'open' : 'closed'}
>
<div className={styles.collapsibleContentInner}>
{items.map(item => (
<PageListItemRenderer key={item.id} {...item} />
))}
</div>
</Collapsible.Content>
</Collapsible.Root>
);
};
// TODO(@Peng): optimize how to render page meta list item
const requiredPropNames = [
'docCollection',
@@ -214,30 +129,6 @@ const listsPropsAtom = selectAtom(
shallowEqual
);
export const PageListItemRenderer = memo(function PageListItemRenderer(
item: ListItem
) {
const props = useAtomValue(listsPropsAtom);
const { selectionActive } = useAtomValue(selectionStateAtom);
const groups = useAtomValue(groupsAtom);
const pageItems = groups.flatMap(group => group.items).map(item => item.id);
const page = item as DocMeta;
return (
<PageListItem
{...pageMetaToListItemProp(
page,
{
...props,
selectable: !!selectionActive,
},
pageItems
)}
/>
);
});
export const CollectionListItemRenderer = memo((item: ListItem) => {
const props = useAtomValue(listsPropsAtom);
const { selectionActive } = useAtomValue(selectionStateAtom);
@@ -270,62 +161,6 @@ export const TagListItemRenderer = memo(function TagListItemRenderer(
);
});
const PageTitle = ({ id }: { id: string }) => {
const i18n = useI18n();
const docDisplayMetaService = useService(DocDisplayMetaService);
const title = useLiveData(docDisplayMetaService.title$(id));
return i18n.t(title);
};
const UnifiedPageIcon = ({ id }: { id: string }) => {
const docDisplayMetaService = useService(DocDisplayMetaService);
const Icon = useLiveData(docDisplayMetaService.icon$(id));
return <Icon />;
};
function pageMetaToListItemProp(
item: DocMeta,
props: RequiredProps<DocMeta>,
pageIds?: string[]
): PageListItemProps {
const toggleSelection = props.onSelectedIdsChange
? () => {
if (!props.selectedIds) {
throw new Error('selectedIds is not found');
}
const prevSelected = props.selectedIds.includes(item.id);
const shouldAdd = !prevSelected;
const shouldRemove = prevSelected;
if (shouldAdd) {
props.onSelectedIdsChange?.([...props.selectedIds, item.id]);
} else if (shouldRemove) {
props.onSelectedIdsChange?.(
props.selectedIds.filter(id => id !== item.id)
);
}
}
: undefined;
const itemProps: PageListItemProps = {
pageId: item.id,
pageIds,
title: <PageTitle id={item.id} />,
preview: <PagePreview pageId={item.id} />,
createDate: new Date(item.createDate),
updatedDate: item.updatedDate ? new Date(item.updatedDate) : undefined,
to: props.rowAsLink && !props.selectable ? `/${item.id}` : undefined,
onClick: toggleSelection,
icon: <UnifiedPageIcon id={item.id} />,
operations: props.operationsRenderer?.(item),
selectable: props.selectable,
selected: props.selectedIds?.includes(item.id),
onSelectedChange: toggleSelection,
draggable: props.draggable,
isPublicPage: !!item.isPublic,
};
return itemProps;
}
function collectionMetaToListItemProp(
item: CollectionMeta,
props: RequiredProps<CollectionMeta>

View File

@@ -1,92 +0,0 @@
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
import {
PageDisplayMenu,
PageListNewPageButton,
} from '@affine/core/components/page-list';
import { Header } from '@affine/core/components/pure/header';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { inferOpenMode } from '@affine/core/utils';
import { track } from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc';
import { useServices } from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback } from 'react';
import * as styles from './all-page.css';
export const AllPageHeader = ({
showCreateNew,
}: {
showCreateNew: boolean;
}) => {
const { workspaceService, workspaceDialogService, workbenchService } =
useServices({
WorkspaceService,
WorkspaceDialogService,
WorkbenchService,
});
const workbench = workbenchService.workbench;
const workspace = workspaceService.workspace;
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
const handleOpenDocs = useCallback(
(result: {
docIds: string[];
entryId?: string;
isWorkspaceFile?: boolean;
}) => {
const { docIds, entryId, isWorkspaceFile } = result;
// If the imported file is a workspace file, open the entry page.
if (isWorkspaceFile && entryId) {
workbench.openDoc(entryId);
} else if (!docIds.length) {
return;
}
// Open all the docs when there are multiple docs imported.
if (docIds.length > 1) {
workbench.openAll();
} else {
// Otherwise, open the only doc.
workbench.openDoc(docIds[0]);
}
},
[workbench]
);
const onImportFile = useCallback(() => {
track.$.header.importModal.open();
workspaceDialogService.open('import', undefined, payload => {
if (!payload) {
return;
}
handleOpenDocs(payload);
});
}, [workspaceDialogService, handleOpenDocs]);
return (
<Header
left={<ExplorerNavigation active={'docs'} />}
right={
<>
<PageListNewPageButton
size="small"
className={clsx(
styles.headerCreateNewButton,
!showCreateNew && styles.headerCreateNewButtonHidden
)}
onCreateEdgeless={e => createEdgeless({ at: inferOpenMode(e) })}
onCreatePage={e => createPage('page', { at: inferOpenMode(e) })}
onCreateDoc={e => createPage(undefined, { at: inferOpenMode(e) })}
onImportFile={onImportFile}
>
<PlusIcon />
</PageListNewPageButton>
<PageDisplayMenu />
</>
}
/>
);
};

View File

@@ -1,30 +0,0 @@
import { style } from '@vanilla-extract/css';
export const scrollContainer = style({
flex: 1,
width: '100%',
paddingBottom: '32px',
});
export const headerCreateNewButton = style({
transition: 'opacity 0.1s ease-in-out',
marginRight: 16,
});
export const headerCreateNewCollectionIconButton = style({
padding: '4px 8px',
fontSize: '16px',
width: '32px',
height: '28px',
borderRadius: '8px',
});
export const headerCreateNewButtonHidden = style({
opacity: 0,
pointerEvents: 'none',
});
export const body = style({
display: 'flex',
flexDirection: 'column',
flex: 1,
height: '100%',
width: '100%',
});

View File

@@ -1,87 +0,0 @@
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
import {
PageListHeader,
VirtualizedPageList,
} from '@affine/core/components/page-list';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { IntegrationService } from '@affine/core/modules/integration';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect, useMemo, useState } from 'react';
import {
useIsActiveView,
ViewBody,
ViewHeader,
ViewIcon,
ViewTitle,
} from '../../../../modules/workbench';
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
import { EmptyPageList } from '../page-list-empty';
import * as styles from './all-page.css';
import { AllPageHeader } from './all-page-header';
export const AllPage = () => {
const currentWorkspace = useService(WorkspaceService).workspace;
const globalContext = useService(GlobalContextService).globalContext;
const permissionService = useService(WorkspacePermissionService);
const integrationService = useService(IntegrationService);
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
const isOwner = useLiveData(permissionService.permission.isOwner$);
const importing = useLiveData(integrationService.importing$);
const filteredPageMetas = useMemo(
() => pageMetas.filter(page => !page.trash),
[pageMetas]
);
const isActiveView = useIsActiveView();
useEffect(() => {
if (isActiveView) {
globalContext.isAllDocs.set(true);
return () => {
globalContext.isAllDocs.set(false);
};
}
return;
}, [globalContext, isActiveView]);
const t = useI18n();
if (importing) {
return null;
}
return (
<>
<ViewTitle title={t['All pages']()} />
<ViewIcon icon="allDocs" />
<ViewHeader>
<AllPageHeader showCreateNew={!hideHeaderCreateNew} />
</ViewHeader>
<ViewBody>
<div className={styles.body}>
{filteredPageMetas.length > 0 ? (
<VirtualizedPageList
disableMultiDelete={!isAdmin && !isOwner}
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/>
) : (
<EmptyPageList type="all" heading={<PageListHeader />} />
)}
</div>
</ViewBody>
<AllDocSidebarTabs />
</>
);
};
export const Component = () => {
return <AllPage />;
};

View File

@@ -5,10 +5,6 @@ export const workbenchRoutes = [
path: '/all',
lazy: () => import('./pages/workspace/all-page/all-page'),
},
{
path: '/all-old',
lazy: () => import('./pages/workspace/all-page-old/all-page'),
},
{
path: '/collection',
lazy: () => import('./pages/workspace/all-collection'),