feat(core): new doc list for collection detail (#12278)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a new document list view with support for different layouts (list, grid, masonry) and improved multi-selection and batch deletion capabilities. - Added a view toggle control for switching between document list layouts across relevant pages. - Implemented a new collection list header with breadcrumb navigation and streamlined actions for editing collections and creating new pages. - **Improvements** - Simplified and unified document list rendering by delegating logic to a shared component. - Enhanced document selection behavior, allowing toggling of individual items in select mode. - Updated styles for document lists, group headers, and collection pages for a more consistent appearance. - Refined wrapper component styling to prevent unintended HTML attribute forwarding. - **Refactor** - Replaced local implementations of view toggles and document grouping with shared components for easier maintenance. - Removed deprecated and unused styles and props to streamline components and improve code clarity. - Refactored collection detail and header components to adopt new context-driven document explorer architecture. - **Documentation** - Added deprecation notice to an outdated collection page list header component. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -46,6 +46,8 @@ export const Wrapper = styled('div', {
|
|||||||
'marginLeft',
|
'marginLeft',
|
||||||
'marginRight',
|
'marginRight',
|
||||||
'marginBottom',
|
'marginBottom',
|
||||||
|
'flexGrow',
|
||||||
|
'flexShrink',
|
||||||
].includes(prop as string);
|
].includes(prop as string);
|
||||||
},
|
},
|
||||||
})<WrapperProps>(({
|
})<WrapperProps>(({
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const viewToggle = style({
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
});
|
||||||
|
export const viewToggleItem = style({
|
||||||
|
padding: 0,
|
||||||
|
fontSize: 16,
|
||||||
|
width: 24,
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
selectors: {
|
||||||
|
'&[data-state=checked]': {
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const viewToggleIndicator = style({
|
||||||
|
backgroundColor: cssVarV2.layer.background.hoverOverlay,
|
||||||
|
boxShadow: 'none',
|
||||||
|
});
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { RadioGroup, type RadioItem } from '@affine/component';
|
||||||
|
import { useLiveData } from '@toeverything/infra';
|
||||||
|
import { useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
import { DocExplorerContext } from '../context';
|
||||||
|
import {
|
||||||
|
type DocListItemView,
|
||||||
|
DocListViewIcon,
|
||||||
|
} from '../docs-view/doc-list-item';
|
||||||
|
import * as styles from './view-toggle.css';
|
||||||
|
|
||||||
|
const views = [
|
||||||
|
{
|
||||||
|
label: <DocListViewIcon view="masonry" />,
|
||||||
|
value: 'masonry',
|
||||||
|
className: styles.viewToggleItem,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <DocListViewIcon view="grid" />,
|
||||||
|
value: 'grid',
|
||||||
|
className: styles.viewToggleItem,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <DocListViewIcon view="list" />,
|
||||||
|
value: 'list',
|
||||||
|
className: styles.viewToggleItem,
|
||||||
|
},
|
||||||
|
] satisfies RadioItem[];
|
||||||
|
|
||||||
|
export const ViewToggle = () => {
|
||||||
|
const explorerContextValue = useContext(DocExplorerContext);
|
||||||
|
|
||||||
|
const view = useLiveData(explorerContextValue.view$);
|
||||||
|
|
||||||
|
const handleViewChange = useCallback(
|
||||||
|
(view: DocListItemView) => {
|
||||||
|
explorerContextValue.view$?.next(view);
|
||||||
|
},
|
||||||
|
[explorerContextValue.view$]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
itemHeight={24}
|
||||||
|
gap={8}
|
||||||
|
padding={0}
|
||||||
|
items={views}
|
||||||
|
value={view}
|
||||||
|
onChange={handleViewChange}
|
||||||
|
className={styles.viewToggle}
|
||||||
|
borderRadius={4}
|
||||||
|
indicatorClassName={styles.viewToggleIndicator}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -124,11 +124,16 @@ export const DocListItem = ({ ...props }: DocListItemProps) => {
|
|||||||
// do multi select
|
// do multi select
|
||||||
handleMultiSelect(prevCheckAnchorId, currCursor);
|
handleMultiSelect(prevCheckAnchorId, currCursor);
|
||||||
} else {
|
} else {
|
||||||
contextValue.selectedDocIds$?.next([docId]);
|
contextValue.selectedDocIds$?.next(
|
||||||
|
contextValue.selectedDocIds$.value.includes(docId)
|
||||||
|
? contextValue.selectedDocIds$.value.filter(id => id !== docId)
|
||||||
|
: [...contextValue.selectedDocIds$.value, docId]
|
||||||
|
);
|
||||||
contextValue.prevCheckAnchorId$?.next(currCursor);
|
contextValue.prevCheckAnchorId$?.next(currCursor);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
|
contextValue.selectMode$?.next(true);
|
||||||
contextValue.selectedDocIds$?.next([docId]);
|
contextValue.selectedDocIds$?.next([docId]);
|
||||||
contextValue.prevCheckAnchorId$?.next(currCursor);
|
contextValue.prevCheckAnchorId$?.next(currCursor);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const groupHeader = style({
|
||||||
|
background: cssVarV2.layer.background.primary,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const docItem = style({
|
||||||
|
transition: 'width 0.2s ease-in-out',
|
||||||
|
});
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import { Masonry, type MasonryGroup, useConfirmModal } from '@affine/component';
|
||||||
|
import { DocsService } from '@affine/core/modules/doc';
|
||||||
|
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||||
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { memo, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ListFloatingToolbar } from '../../page-list/components/list-floating-toolbar';
|
||||||
|
import { WorkspacePropertyTypes } from '../../workspace-property-types';
|
||||||
|
import { DocExplorerContext } from '../context';
|
||||||
|
import { DocListItem } from './doc-list-item';
|
||||||
|
import * as styles from './docs-list.css';
|
||||||
|
|
||||||
|
const GroupHeader = memo(function GroupHeader({
|
||||||
|
groupId,
|
||||||
|
collapsed,
|
||||||
|
itemCount,
|
||||||
|
}: {
|
||||||
|
groupId: string;
|
||||||
|
collapsed?: boolean;
|
||||||
|
itemCount: number;
|
||||||
|
}) {
|
||||||
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const propertyService = useService(WorkspacePropertyService);
|
||||||
|
const allProperties = useLiveData(propertyService.sortedProperties$);
|
||||||
|
const groupBy = useLiveData(contextValue.groupBy$);
|
||||||
|
|
||||||
|
const groupType = groupBy?.type;
|
||||||
|
const groupKey = groupBy?.key;
|
||||||
|
|
||||||
|
const header = useMemo(() => {
|
||||||
|
if (groupType === 'property') {
|
||||||
|
const property = allProperties.find(p => p.id === groupKey);
|
||||||
|
if (!property) return null;
|
||||||
|
|
||||||
|
const config = WorkspacePropertyTypes[property.type];
|
||||||
|
if (!config?.groupHeader) return null;
|
||||||
|
return (
|
||||||
|
<config.groupHeader
|
||||||
|
groupId={groupId}
|
||||||
|
docCount={itemCount}
|
||||||
|
collapsed={!!collapsed}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn('unsupported group type', groupType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]);
|
||||||
|
|
||||||
|
if (!groupType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return header;
|
||||||
|
});
|
||||||
|
|
||||||
|
const calcCardHeightById = (id: string) => {
|
||||||
|
if (!id) {
|
||||||
|
return 250;
|
||||||
|
}
|
||||||
|
const max = 5;
|
||||||
|
const min = 1;
|
||||||
|
const code = id.charCodeAt(0);
|
||||||
|
const value = Math.floor((code % (max - min)) + min);
|
||||||
|
return 250 + value * 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DocListItemComponent = memo(function DocListItemComponent({
|
||||||
|
itemId,
|
||||||
|
groupId,
|
||||||
|
}: {
|
||||||
|
groupId: string;
|
||||||
|
itemId: string;
|
||||||
|
}) {
|
||||||
|
return <DocListItem docId={itemId} groupId={groupId} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DocsExplorer = ({
|
||||||
|
className,
|
||||||
|
disableMultiDelete,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
disableMultiDelete?: boolean;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const contextValue = useContext(DocExplorerContext);
|
||||||
|
const docsService = useService(DocsService);
|
||||||
|
|
||||||
|
const groups = useLiveData(contextValue.groups$);
|
||||||
|
const view = useLiveData(contextValue.view$);
|
||||||
|
const selectMode = useLiveData(contextValue.selectMode$);
|
||||||
|
const selectedDocIds = useLiveData(contextValue.selectedDocIds$);
|
||||||
|
const collapsedGroups = useLiveData(contextValue.collapsedGroups$);
|
||||||
|
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
|
||||||
|
const masonryItems = useMemo(() => {
|
||||||
|
const items = groups.map((group: any) => {
|
||||||
|
return {
|
||||||
|
id: group.key,
|
||||||
|
Component: groups.length > 1 ? GroupHeader : undefined,
|
||||||
|
height: groups.length > 1 ? 24 : 0,
|
||||||
|
className: styles.groupHeader,
|
||||||
|
items: group.items.map((docId: string) => {
|
||||||
|
return {
|
||||||
|
id: docId,
|
||||||
|
Component: DocListItemComponent,
|
||||||
|
height:
|
||||||
|
view === 'list'
|
||||||
|
? 42
|
||||||
|
: view === 'grid'
|
||||||
|
? 280
|
||||||
|
: calcCardHeightById(docId),
|
||||||
|
'data-view': view,
|
||||||
|
className: styles.docItem,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
} satisfies MasonryGroup;
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}, [groups, view]);
|
||||||
|
|
||||||
|
const handleCloseFloatingToolbar = useCallback(() => {
|
||||||
|
contextValue.selectMode$?.next(false);
|
||||||
|
contextValue.selectedDocIds$.next([]);
|
||||||
|
}, [contextValue]);
|
||||||
|
|
||||||
|
const handleMultiDelete = useCallback(() => {
|
||||||
|
if (disableMultiDelete) {
|
||||||
|
handleCloseFloatingToolbar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (selectedDocIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openConfirmModal({
|
||||||
|
title: t['com.affine.moveToTrash.confirmModal.title.multiple']({
|
||||||
|
number: selectedDocIds.length.toString(),
|
||||||
|
}),
|
||||||
|
description: t[
|
||||||
|
'com.affine.moveToTrash.confirmModal.description.multiple'
|
||||||
|
]({
|
||||||
|
number: selectedDocIds.length.toString(),
|
||||||
|
}),
|
||||||
|
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||||
|
confirmText: t.Delete(),
|
||||||
|
confirmButtonOptions: {
|
||||||
|
variant: 'error',
|
||||||
|
},
|
||||||
|
onConfirm: () => {
|
||||||
|
const selectedDocIds = contextValue.selectedDocIds$.value;
|
||||||
|
for (const docId of selectedDocIds) {
|
||||||
|
const doc = docsService.list.doc$(docId).value;
|
||||||
|
doc?.moveToTrash();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
contextValue.selectedDocIds$,
|
||||||
|
disableMultiDelete,
|
||||||
|
docsService.list,
|
||||||
|
handleCloseFloatingToolbar,
|
||||||
|
openConfirmModal,
|
||||||
|
selectedDocIds.length,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
contextValue.selectMode$?.next(false);
|
||||||
|
contextValue.selectedDocIds$.next([]);
|
||||||
|
contextValue.prevCheckAnchorId$?.next(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, [contextValue]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Masonry
|
||||||
|
className={className}
|
||||||
|
items={masonryItems}
|
||||||
|
gapY={12}
|
||||||
|
gapX={12}
|
||||||
|
groupsGap={12}
|
||||||
|
groupHeaderGapWithItems={12}
|
||||||
|
columns={view === 'list' ? 1 : undefined}
|
||||||
|
itemWidthMin={220}
|
||||||
|
preloadHeight={100}
|
||||||
|
itemWidth={'stretch'}
|
||||||
|
virtualScroll
|
||||||
|
collapsedGroups={collapsedGroups}
|
||||||
|
paddingX={useCallback(
|
||||||
|
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
|
||||||
|
[]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ListFloatingToolbar
|
||||||
|
open={!!selectMode}
|
||||||
|
onDelete={handleMultiDelete}
|
||||||
|
onClose={handleCloseFloatingToolbar}
|
||||||
|
content={
|
||||||
|
<Trans
|
||||||
|
i18nKey="com.affine.page.toolbar.selected"
|
||||||
|
count={selectedDocIds.length}
|
||||||
|
>
|
||||||
|
<div style={{ color: cssVarV2.text.secondary }}>
|
||||||
|
{{ count: selectedDocIds.length } as any}
|
||||||
|
</div>
|
||||||
|
selected
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -105,6 +105,9 @@ export const PageListHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export const CollectionPageListHeader = ({
|
export const CollectionPageListHeader = ({
|
||||||
collection,
|
collection,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@@ -29,7 +29,3 @@ export const viewToggleItem = style({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const viewToggleIndicator = style({
|
|
||||||
backgroundColor: cssVarV2.layer.background.hoverOverlay,
|
|
||||||
boxShadow: 'none',
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,61 +1,10 @@
|
|||||||
import { type MenuProps, RadioGroup, type RadioItem } from '@affine/component';
|
import { type MenuProps } from '@affine/component';
|
||||||
import { DocExplorerContext } from '@affine/core/components/explorer/context';
|
|
||||||
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||||
import {
|
import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle';
|
||||||
type DocListItemView,
|
|
||||||
DocListViewIcon,
|
|
||||||
} from '@affine/core/components/explorer/docs-view/doc-list-item';
|
|
||||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||||
import { useLiveData } from '@toeverything/infra';
|
|
||||||
import { useCallback, useContext } from 'react';
|
|
||||||
|
|
||||||
import * as styles from './all-page-header.css';
|
import * as styles from './all-page-header.css';
|
||||||
|
|
||||||
const views = [
|
|
||||||
{
|
|
||||||
label: <DocListViewIcon view="masonry" />,
|
|
||||||
value: 'masonry',
|
|
||||||
className: styles.viewToggleItem,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: <DocListViewIcon view="grid" />,
|
|
||||||
value: 'grid',
|
|
||||||
className: styles.viewToggleItem,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: <DocListViewIcon view="list" />,
|
|
||||||
value: 'list',
|
|
||||||
className: styles.viewToggleItem,
|
|
||||||
},
|
|
||||||
] satisfies RadioItem[];
|
|
||||||
|
|
||||||
const ViewToggle = () => {
|
|
||||||
const explorerContextValue = useContext(DocExplorerContext);
|
|
||||||
|
|
||||||
const view = useLiveData(explorerContextValue.view$);
|
|
||||||
|
|
||||||
const handleViewChange = useCallback(
|
|
||||||
(view: DocListItemView) => {
|
|
||||||
explorerContextValue.view$?.next(view);
|
|
||||||
},
|
|
||||||
[explorerContextValue.view$]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RadioGroup
|
|
||||||
itemHeight={24}
|
|
||||||
gap={8}
|
|
||||||
padding={0}
|
|
||||||
items={views}
|
|
||||||
value={view}
|
|
||||||
onChange={handleViewChange}
|
|
||||||
className={styles.viewToggle}
|
|
||||||
borderRadius={4}
|
|
||||||
indicatorClassName={styles.viewToggleIndicator}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const menuProps: Partial<MenuProps> = {
|
const menuProps: Partial<MenuProps> = {
|
||||||
contentOptions: {
|
contentOptions: {
|
||||||
side: 'bottom',
|
side: 'bottom',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
export const scrollContainer = style({
|
export const scrollContainer = style({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -38,13 +37,6 @@ export const scrollArea = style({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// group
|
// group
|
||||||
export const groupHeader = style({
|
|
||||||
background: cssVarV2.layer.background.primary,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const docItem = style({
|
|
||||||
transition: 'width 0.2s ease-in-out',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const pinnedCollection = style({
|
export const pinnedCollection = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
@@ -1,38 +1,20 @@
|
|||||||
import {
|
import { Button, usePromptModal } from '@affine/component';
|
||||||
Button,
|
|
||||||
Masonry,
|
|
||||||
type MasonryGroup,
|
|
||||||
useConfirmModal,
|
|
||||||
usePromptModal,
|
|
||||||
} from '@affine/component';
|
|
||||||
import {
|
import {
|
||||||
createDocExplorerContext,
|
createDocExplorerContext,
|
||||||
DocExplorerContext,
|
DocExplorerContext,
|
||||||
} from '@affine/core/components/explorer/context';
|
} from '@affine/core/components/explorer/context';
|
||||||
import { DocListItem } from '@affine/core/components/explorer/docs-view/doc-list-item';
|
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||||
import { Filters } from '@affine/core/components/filter';
|
import { Filters } from '@affine/core/components/filter';
|
||||||
import { ListFloatingToolbar } from '@affine/core/components/page-list/components/list-floating-toolbar';
|
|
||||||
import { WorkspacePropertyTypes } from '@affine/core/components/workspace-property-types';
|
|
||||||
import {
|
import {
|
||||||
CollectionService,
|
CollectionService,
|
||||||
PinnedCollectionService,
|
PinnedCollectionService,
|
||||||
} from '@affine/core/modules/collection';
|
} from '@affine/core/modules/collection';
|
||||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
|
import type { FilterParams } from '@affine/core/modules/collection-rules/types';
|
||||||
import { DocsService } from '@affine/core/modules/doc';
|
|
||||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||||
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import {
|
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ViewBody,
|
ViewBody,
|
||||||
@@ -47,70 +29,9 @@ import { AllDocsHeader } from './all-page-header';
|
|||||||
import { MigrationAllDocsDataNotification } from './migration-data';
|
import { MigrationAllDocsDataNotification } from './migration-data';
|
||||||
import { PinnedCollections } from './pinned-collections';
|
import { PinnedCollections } from './pinned-collections';
|
||||||
|
|
||||||
const GroupHeader = memo(function GroupHeader({
|
|
||||||
groupId,
|
|
||||||
collapsed,
|
|
||||||
itemCount,
|
|
||||||
}: {
|
|
||||||
groupId: string;
|
|
||||||
collapsed?: boolean;
|
|
||||||
itemCount: number;
|
|
||||||
}) {
|
|
||||||
const contextValue = useContext(DocExplorerContext);
|
|
||||||
const propertyService = useService(WorkspacePropertyService);
|
|
||||||
const allProperties = useLiveData(propertyService.sortedProperties$);
|
|
||||||
const groupBy = useLiveData(contextValue.groupBy$);
|
|
||||||
|
|
||||||
const groupType = groupBy?.type;
|
|
||||||
const groupKey = groupBy?.key;
|
|
||||||
|
|
||||||
const header = useMemo(() => {
|
|
||||||
if (groupType === 'property') {
|
|
||||||
const property = allProperties.find(p => p.id === groupKey);
|
|
||||||
if (!property) return null;
|
|
||||||
|
|
||||||
const config = WorkspacePropertyTypes[property.type];
|
|
||||||
if (!config?.groupHeader) return null;
|
|
||||||
return (
|
|
||||||
<config.groupHeader
|
|
||||||
groupId={groupId}
|
|
||||||
docCount={itemCount}
|
|
||||||
collapsed={!!collapsed}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return '// TODO: ' + groupType;
|
|
||||||
}
|
|
||||||
}, [allProperties, collapsed, groupId, groupKey, groupType, itemCount]);
|
|
||||||
|
|
||||||
if (!groupType) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return header;
|
|
||||||
});
|
|
||||||
|
|
||||||
const calcCardHeightById = (id: string) => {
|
|
||||||
const max = 5;
|
|
||||||
const min = 1;
|
|
||||||
const code = id.charCodeAt(0);
|
|
||||||
const value = Math.floor((code % (max - min)) + min);
|
|
||||||
return 250 + value * 10;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DocListItemComponent = memo(function DocListItemComponent({
|
|
||||||
itemId,
|
|
||||||
groupId,
|
|
||||||
}: {
|
|
||||||
groupId: string;
|
|
||||||
itemId: string;
|
|
||||||
}) {
|
|
||||||
return <DocListItem docId={itemId} groupId={groupId} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AllPage = () => {
|
export const AllPage = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const docsService = useService(DocsService);
|
|
||||||
const collectionService = useService(CollectionService);
|
const collectionService = useService(CollectionService);
|
||||||
const pinnedCollectionService = useService(PinnedCollectionService);
|
const pinnedCollectionService = useService(PinnedCollectionService);
|
||||||
|
|
||||||
@@ -138,41 +59,10 @@ export const AllPage = () => {
|
|||||||
|
|
||||||
const [explorerContextValue] = useState(createDocExplorerContext);
|
const [explorerContextValue] = useState(createDocExplorerContext);
|
||||||
|
|
||||||
const view = useLiveData(explorerContextValue.view$);
|
|
||||||
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
||||||
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
||||||
const groups = useLiveData(explorerContextValue.groups$);
|
|
||||||
const selectedDocIds = useLiveData(explorerContextValue.selectedDocIds$);
|
|
||||||
const collapsedGroups = useLiveData(explorerContextValue.collapsedGroups$);
|
|
||||||
const selectMode = useLiveData(explorerContextValue.selectMode$);
|
|
||||||
|
|
||||||
const { openPromptModal } = usePromptModal();
|
const { openPromptModal } = usePromptModal();
|
||||||
const { openConfirmModal } = useConfirmModal();
|
|
||||||
const masonryItems = useMemo(() => {
|
|
||||||
const items = groups.map((group: any) => {
|
|
||||||
return {
|
|
||||||
id: group.key,
|
|
||||||
Component: groups.length > 1 ? GroupHeader : undefined,
|
|
||||||
height: groups.length > 1 ? 24 : 0,
|
|
||||||
className: styles.groupHeader,
|
|
||||||
items: group.items.map((docId: string) => {
|
|
||||||
return {
|
|
||||||
id: docId,
|
|
||||||
Component: DocListItemComponent,
|
|
||||||
height:
|
|
||||||
view === 'list'
|
|
||||||
? 42
|
|
||||||
: view === 'grid'
|
|
||||||
? 280
|
|
||||||
: calcCardHeightById(docId),
|
|
||||||
'data-view': view,
|
|
||||||
className: styles.docItem,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
} satisfies MasonryGroup;
|
|
||||||
});
|
|
||||||
return items;
|
|
||||||
}, [groups, view]);
|
|
||||||
|
|
||||||
const collectionRulesService = useService(CollectionRulesService);
|
const collectionRulesService = useService(CollectionRulesService);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -270,39 +160,6 @@ export const AllPage = () => {
|
|||||||
setTempFilters(filters);
|
setTempFilters(filters);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCloseFloatingToolbar = useCallback(() => {
|
|
||||||
explorerContextValue.selectMode$.next(false);
|
|
||||||
explorerContextValue.selectedDocIds$.next([]);
|
|
||||||
}, [explorerContextValue]);
|
|
||||||
|
|
||||||
const handleMultiDelete = useCallback(() => {
|
|
||||||
if (selectedDocIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
openConfirmModal({
|
|
||||||
title: t['com.affine.moveToTrash.confirmModal.title.multiple']({
|
|
||||||
number: selectedDocIds.length.toString(),
|
|
||||||
}),
|
|
||||||
description: t[
|
|
||||||
'com.affine.moveToTrash.confirmModal.description.multiple'
|
|
||||||
]({
|
|
||||||
number: selectedDocIds.length.toString(),
|
|
||||||
}),
|
|
||||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
|
||||||
confirmText: t.Delete(),
|
|
||||||
confirmButtonOptions: {
|
|
||||||
variant: 'error',
|
|
||||||
},
|
|
||||||
onConfirm: () => {
|
|
||||||
for (const docId of selectedDocIds) {
|
|
||||||
const doc = docsService.list.doc$(docId).value;
|
|
||||||
doc?.moveToTrash();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [docsService.list, openConfirmModal, selectedDocIds, t]);
|
|
||||||
|
|
||||||
const handleSaveFilters = useCallback(() => {
|
const handleSaveFilters = useCallback(() => {
|
||||||
openPromptModal({
|
openPromptModal({
|
||||||
title: t['com.affine.editCollection.saveCollection'](),
|
title: t['com.affine.editCollection.saveCollection'](),
|
||||||
@@ -383,41 +240,9 @@ export const AllPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.scrollArea}>
|
<div className={styles.scrollArea}>
|
||||||
<Masonry
|
<DocsExplorer />
|
||||||
items={masonryItems}
|
|
||||||
gapY={12}
|
|
||||||
gapX={12}
|
|
||||||
groupsGap={12}
|
|
||||||
groupHeaderGapWithItems={12}
|
|
||||||
columns={view === 'list' ? 1 : undefined}
|
|
||||||
itemWidthMin={220}
|
|
||||||
preloadHeight={100}
|
|
||||||
itemWidth={'stretch'}
|
|
||||||
virtualScroll
|
|
||||||
collapsedGroups={collapsedGroups}
|
|
||||||
paddingX={useCallback(
|
|
||||||
(w: number) => (w > 500 ? 24 : w > 393 ? 20 : 16),
|
|
||||||
[]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ListFloatingToolbar
|
|
||||||
open={selectMode}
|
|
||||||
onDelete={handleMultiDelete}
|
|
||||||
onClose={handleCloseFloatingToolbar}
|
|
||||||
content={
|
|
||||||
<Trans
|
|
||||||
i18nKey="com.affine.page.toolbar.selected"
|
|
||||||
count={selectedDocIds.length}
|
|
||||||
>
|
|
||||||
<div style={{ color: cssVarV2.text.secondary }}>
|
|
||||||
{{ count: selectedDocIds.length } as any}
|
|
||||||
</div>
|
|
||||||
selected
|
|
||||||
</Trans>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ViewBody>
|
</ViewBody>
|
||||||
<AllDocSidebarTabs />
|
<AllDocSidebarTabs />
|
||||||
</DocExplorerContext.Provider>
|
</DocExplorerContext.Provider>
|
||||||
|
|||||||
@@ -1,35 +1,17 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { FlexWrapper } from '@affine/component';
|
||||||
|
import { ExplorerDisplayMenuButton } from '@affine/core/components/explorer/display-menu';
|
||||||
|
import { ViewToggle } from '@affine/core/components/explorer/display-menu/view-toggle';
|
||||||
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
import { ExplorerNavigation } from '@affine/core/components/explorer/header/navigation';
|
||||||
import { PageDisplayMenu } from '@affine/core/components/page-list';
|
|
||||||
import { Header } from '@affine/core/components/pure/header';
|
import { Header } from '@affine/core/components/pure/header';
|
||||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import * as styles from './collection.css';
|
export const CollectionDetailHeader = () => {
|
||||||
|
|
||||||
export const CollectionDetailHeader = ({
|
|
||||||
showCreateNew,
|
|
||||||
onCreate,
|
|
||||||
}: {
|
|
||||||
showCreateNew: boolean;
|
|
||||||
onCreate: () => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
right={
|
right={
|
||||||
<>
|
<FlexWrapper gap={16}>
|
||||||
<IconButton
|
<ViewToggle />
|
||||||
size="16"
|
<ExplorerDisplayMenuButton />
|
||||||
icon={<PlusIcon />}
|
</FlexWrapper>
|
||||||
onClick={onCreate}
|
|
||||||
className={clsx(
|
|
||||||
styles.headerCreateNewButton,
|
|
||||||
styles.headerCreateNewCollectionIconButton,
|
|
||||||
!showCreateNew && styles.headerCreateNewButtonHidden
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<PageDisplayMenu />
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
left={<ExplorerNavigation active="collections" />}
|
left={<ExplorerNavigation active="collections" />}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const scrollArea = style({
|
||||||
|
width: '100%',
|
||||||
|
flexGrow: 1,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collectionHeader = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '24px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const breadcrumb = style({
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: '22px',
|
||||||
|
color: cssVarV2.text.secondary,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
export const breadcrumbItem = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
cursor: 'pointer',
|
||||||
|
selectors: {
|
||||||
|
'&[data-active="true"]': {
|
||||||
|
color: cssVarV2.text.primary,
|
||||||
|
cursor: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const breadcrumbLink = style({
|
||||||
|
color: 'inherit',
|
||||||
|
textDecoration: 'none',
|
||||||
|
});
|
||||||
|
export const breadcrumbIcon = style({
|
||||||
|
fontSize: 20,
|
||||||
|
color: cssVarV2.icon.primary,
|
||||||
|
});
|
||||||
|
export const breadcrumbSeparator = style({
|
||||||
|
marginLeft: 4,
|
||||||
|
marginRight: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const headerActions = style({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const newPageButtonText = style({
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '20px',
|
||||||
|
fontWeight: 500,
|
||||||
|
});
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import { FlexWrapper } from '@affine/component';
|
||||||
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty/collection-detail';
|
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty/collection-detail';
|
||||||
import { VirtualizedPageList } from '@affine/core/components/page-list';
|
import {
|
||||||
|
createDocExplorerContext,
|
||||||
|
DocExplorerContext,
|
||||||
|
} from '@affine/core/components/explorer/context';
|
||||||
|
import { DocsExplorer } from '@affine/core/components/explorer/docs-view/docs-list';
|
||||||
import {
|
import {
|
||||||
type Collection,
|
type Collection,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
} from '@affine/core/modules/collection';
|
} from '@affine/core/modules/collection';
|
||||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
@@ -25,42 +30,82 @@ import {
|
|||||||
import { PageNotFound } from '../../404';
|
import { PageNotFound } from '../../404';
|
||||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||||
import { CollectionDetailHeader } from './header';
|
import { CollectionDetailHeader } from './header';
|
||||||
|
import * as styles from './index.css';
|
||||||
|
import { CollectionListHeader } from './list-header';
|
||||||
|
|
||||||
export const CollectionDetail = ({
|
export const CollectionDetail = ({
|
||||||
collection,
|
collection,
|
||||||
}: {
|
}: {
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
}) => {
|
}) => {
|
||||||
const { workspaceDialogService } = useServices({
|
const [explorerContextValue] = useState(createDocExplorerContext);
|
||||||
WorkspaceDialogService,
|
const collectionRulesService = useService(CollectionRulesService);
|
||||||
});
|
|
||||||
const permissionService = useService(WorkspacePermissionService);
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
|
||||||
|
|
||||||
const handleEditCollection = useCallback(() => {
|
const groupBy = useLiveData(explorerContextValue.groupBy$);
|
||||||
workspaceDialogService.open('collection-editor', {
|
const orderBy = useLiveData(explorerContextValue.orderBy$);
|
||||||
collectionId: collection.id,
|
const rules = useLiveData(collection.rules$);
|
||||||
|
const allowList = useLiveData(collection.allowList$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = collectionRulesService
|
||||||
|
.watch({
|
||||||
|
filters: rules.filters,
|
||||||
|
groupBy,
|
||||||
|
orderBy,
|
||||||
|
extraAllowList: allowList,
|
||||||
|
extraFilters: [
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'empty-journal',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'system',
|
||||||
|
key: 'trash',
|
||||||
|
method: 'is',
|
||||||
|
value: 'false',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: result => {
|
||||||
|
explorerContextValue.groups$.next(result.groups);
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}, [collection, workspaceDialogService]);
|
return () => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
allowList,
|
||||||
|
collectionRulesService,
|
||||||
|
explorerContextValue.groups$,
|
||||||
|
groupBy,
|
||||||
|
orderBy,
|
||||||
|
rules.filters,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DocExplorerContext.Provider value={explorerContextValue}>
|
||||||
<ViewHeader>
|
<ViewHeader>
|
||||||
<CollectionDetailHeader
|
<CollectionDetailHeader />
|
||||||
showCreateNew={!hideHeaderCreateNew}
|
|
||||||
onCreate={handleEditCollection}
|
|
||||||
/>
|
|
||||||
</ViewHeader>
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<VirtualizedPageList
|
<FlexWrapper flexDirection="column" alignItems="stretch" width="100%">
|
||||||
collection={collection}
|
<CollectionListHeader collection={collection} />
|
||||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
<div className={styles.scrollArea}>
|
||||||
disableMultiDelete={!isAdmin && !isOwner}
|
<DocsExplorer disableMultiDelete={!isAdmin && !isOwner} />
|
||||||
/>
|
</div>
|
||||||
|
</FlexWrapper>
|
||||||
</ViewBody>
|
</ViewBody>
|
||||||
</>
|
</DocExplorerContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Button, useConfirmModal } from '@affine/component';
|
||||||
|
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||||
|
import { PageListNewPageButton } from '@affine/core/components/page-list';
|
||||||
|
import {
|
||||||
|
type Collection,
|
||||||
|
CollectionService,
|
||||||
|
} from '@affine/core/modules/collection';
|
||||||
|
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||||
|
import type { DocRecord } from '@affine/core/modules/doc';
|
||||||
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
|
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import type { DocMode } from '@blocksuite/affine/model';
|
||||||
|
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useServices } from '@toeverything/infra';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.css';
|
||||||
|
|
||||||
|
export const CollectionListHeader = ({
|
||||||
|
collection,
|
||||||
|
}: {
|
||||||
|
collection: Collection;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const { collectionService, workspaceService, workspaceDialogService } =
|
||||||
|
useServices({
|
||||||
|
CollectionService,
|
||||||
|
WorkspaceService,
|
||||||
|
WorkspaceDialogService,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<header className={styles.collectionHeader}>
|
||||||
|
<div className={styles.breadcrumb}>
|
||||||
|
<div className={styles.breadcrumbItem}>
|
||||||
|
<WorkbenchLink to="/collection" className={styles.breadcrumbLink}>
|
||||||
|
{t['com.affine.collections.header']()}
|
||||||
|
</WorkbenchLink>
|
||||||
|
</div>
|
||||||
|
<div className={styles.breadcrumbSeparator}>/</div>
|
||||||
|
<div className={styles.breadcrumbItem} data-active={true}>
|
||||||
|
<ViewLayersIcon className={styles.breadcrumbIcon} />
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.headerActions}>
|
||||||
|
<Button onClick={handleEdit}>{t['Edit']()}</Button>
|
||||||
|
<PageListNewPageButton
|
||||||
|
size="small"
|
||||||
|
testId="new-page-button-trigger"
|
||||||
|
onCreateDoc={onCreateDoc}
|
||||||
|
onCreateEdgeless={onCreateEdgeless}
|
||||||
|
onCreatePage={onCreatePage}
|
||||||
|
>
|
||||||
|
<div className={styles.newPageButtonText}>{t['New Page']()}</div>
|
||||||
|
</PageListNewPageButton>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user