mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 21:41:52 +08:00
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:
@@ -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';
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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%',
|
||||
});
|
||||
@@ -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 />;
|
||||
};
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user