feat(core): new empty states for doc/collection/tag (#8197)

AF-1329, AF-1330
This commit is contained in:
CatsJuice
2024-09-11 10:48:52 +00:00
parent f12655655e
commit b7d05d2078
49 changed files with 656 additions and 456 deletions

View File

@@ -28,6 +28,7 @@ export * from './ui/slider';
export * from './ui/switch'; export * from './ui/switch';
export * from './ui/table'; export * from './ui/table';
export * from './ui/tabs'; export * from './ui/tabs';
export * from './ui/themed-img';
export * from './ui/toast'; export * from './ui/toast';
export * from './ui/tooltip'; export * from './ui/tooltip';
export * from './utils'; export * from './utils';

View File

@@ -16,6 +16,9 @@ export type EmptyContentProps = {
descriptionStyle?: CSSProperties; descriptionStyle?: CSSProperties;
}; };
/**
* @deprecated use different empty components for different use cases, like `EmptyDocs` for documentation empty state
*/
export const Empty = ({ export const Empty = ({
containerStyle, containerStyle,
title, title,

View File

@@ -0,0 +1,17 @@
import { useTheme } from 'next-themes';
import { forwardRef, type HTMLAttributes } from 'react';
export interface ThemedImgProps
extends Omit<HTMLAttributes<HTMLImageElement>, 'src'> {
lightSrc: string;
darkSrc?: string;
}
export const ThemedImg = forwardRef<HTMLImageElement, ThemedImgProps>(
function ThemedImg({ lightSrc, darkSrc, ...attrs }, ref) {
const { resolvedTheme } = useTheme();
const src = resolvedTheme === 'dark' && darkSrc ? darkSrc : lightSrc;
return <img ref={ref} src={src} {...attrs} />;
}
);

View File

@@ -1,2 +1,3 @@
export * from './observe-resize'; export * from './observe-resize';
export { startScopedViewTransition } from './view-transition'; export { startScopedViewTransition } from './view-transition';
export * from './with-unit';

View File

@@ -0,0 +1,30 @@
import { Button, type ButtonProps } from '@affine/component';
import clsx from 'clsx';
import {
actionButton,
actionContent,
mobileActionButton,
mobileActionContent,
} from './style.css';
export const ActionButton = ({
className,
contentClassName,
...props
}: ButtonProps) => {
return (
<Button
size="large"
className={clsx(
environment.isMobileEdition ? mobileActionButton : actionButton,
className
)}
contentClassName={clsx(
environment.isMobileEdition ? mobileActionContent : actionContent,
contentClassName
)}
{...props}
/>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,66 @@
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { CollectionService } from '@affine/core/modules/collection';
import type { Collection } from '@affine/env/filter';
import { useI18n } from '@affine/i18n';
import { AllDocsIcon, FilterIcon } from '@blocksuite/icons/rc';
import { useService } from '@toeverything/infra';
import { useEditCollection } from '../../page-list';
import { ActionButton } from './action-button';
import collectionDetailDark from './assets/collection-detail.dark.png';
import collectionDetailLight from './assets/collection-detail.light.png';
import { EmptyLayout } from './layout';
import { actionGroup } from './style.css';
import type { UniversalEmptyProps } from './types';
export interface EmptyCollectionDetailProps extends UniversalEmptyProps {
collection: Collection;
}
export const EmptyCollectionDetail = ({
collection,
...props
}: EmptyCollectionDetailProps) => {
const t = useI18n();
return (
<EmptyLayout
illustrationLight={collectionDetailLight}
illustrationDark={collectionDetailDark}
title={t['com.affine.empty.collection-detail.title']()}
description={t['com.affine.empty.collection-detail.description']()}
action={
environment.isMobileEdition ? null : <Actions collection={collection} />
}
{...props}
/>
);
};
const Actions = ({ collection }: { collection: Collection }) => {
const t = useI18n();
const collectionService = useService(CollectionService);
const { open } = useEditCollection();
const openAddDocs = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'page');
collectionService.updateCollection(ret.id, () => ret);
}, [open, collection, collectionService]);
const openAddRules = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'rule');
collectionService.updateCollection(ret.id, () => ret);
}, [collection, open, collectionService]);
return (
<div className={actionGroup}>
<ActionButton prefix={<AllDocsIcon />} onClick={openAddDocs}>
{t['com.affine.empty.collection-detail.action.add-doc']()}
</ActionButton>
<ActionButton prefix={<FilterIcon />} onClick={openAddRules}>
{t['com.affine.empty.collection-detail.action.add-rule']()}
</ActionButton>
</div>
);
};

View File

@@ -0,0 +1,60 @@
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { CollectionService } from '@affine/core/modules/collection';
import { useI18n } from '@affine/i18n';
import { ViewLayersIcon } from '@blocksuite/icons/rc';
import { useService, WorkspaceService } from '@toeverything/infra';
import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { createEmptyCollection, useEditCollectionName } from '../../page-list';
import { ActionButton } from './action-button';
import collectionListDark from './assets/collection-list.dark.png';
import collectionListLight from './assets/collection-list.light.png';
import { EmptyLayout } from './layout';
import type { UniversalEmptyProps } from './types';
export const EmptyCollections = (props: UniversalEmptyProps) => {
const t = useI18n();
const collectionService = useService(CollectionService);
const currentWorkspace = useService(WorkspaceService).workspace;
const navigateHelper = useNavigateHelper();
const { open } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](),
showTips: true,
});
const showAction = true;
const handleCreateCollection = useCallback(() => {
open('')
.then(name => {
const id = nanoid();
collectionService.addCollection(createEmptyCollection(id, { name }));
navigateHelper.jumpToCollection(currentWorkspace.id, id);
})
.catch(err => {
console.error(err);
});
}, [collectionService, currentWorkspace, navigateHelper, open]);
return (
<EmptyLayout
illustrationLight={collectionListLight}
illustrationDark={collectionListDark}
title={t['com.affine.empty.collections.title']()}
description={t['com.affine.empty.collections.description']()}
action={
showAction ? (
<ActionButton
prefix={<ViewLayersIcon />}
onClick={handleCreateCollection}
>
{t['com.affine.empty.collections.action.new-collection']()}
</ActionButton>
) : null
}
{...props}
/>
);
};

View File

@@ -0,0 +1,69 @@
import { TagService } from '@affine/core/modules/tag';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n';
import { AllDocsIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
import { type MouseEvent, useCallback } from 'react';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import { ActionButton } from './action-button';
import docsIllustrationDark from './assets/docs.dark.png';
import docsIllustrationLight from './assets/docs.light.png';
import { EmptyLayout } from './layout';
import type { UniversalEmptyProps } from './types';
export interface EmptyDocsProps extends UniversalEmptyProps {
type?: 'all' | 'trash';
/**
* Used for "New doc", if provided, new doc will be created with this tag.
*/
tagId?: string;
}
export const EmptyDocs = ({
type = 'all',
tagId,
...props
}: EmptyDocsProps) => {
const t = useI18n();
const tagService = useService(TagService);
const currentWorkspace = useService(WorkspaceService).workspace;
const pageHelper = usePageHelper(currentWorkspace.docCollection);
const tag = useLiveData(tagService.tagList.tagByTagId$(tagId));
const showActionButton = type !== 'trash'; // && !environment.isMobileEdition;
const onCreate = useCallback(
(e: MouseEvent) => {
const doc = pageHelper.createPage(
undefined,
isNewTabTrigger(e) ? 'new-tab' : true
);
doc.load();
if (tag) tag.tag(doc.id);
},
[pageHelper, tag]
);
return (
<EmptyLayout
illustrationLight={docsIllustrationLight}
illustrationDark={docsIllustrationDark}
title={t['com.affine.empty.docs.title']()}
description={
type === 'trash'
? t['com.affine.empty.docs.trash-description']()
: t['com.affine.empty.docs.all-description']()
}
action={
showActionButton ? (
<ActionButton onClick={onCreate} prefix={<AllDocsIcon />}>
{t['com.affine.empty.docs.action.new-doc']()}
</ActionButton>
) : null
}
{...props}
/>
);
};

View File

@@ -0,0 +1,4 @@
export * from './collection-detail';
export * from './collections';
export * from './docs';
export * from './tags';

View File

@@ -0,0 +1,45 @@
import { ThemedImg, withUnit } from '@affine/component';
import clsx from 'clsx';
import * as styles from './style.css';
import type { EmptyLayoutProps } from './types';
export const EmptyLayout = ({
className,
illustrationLight,
illustrationDark,
illustrationWidth = 300,
title,
description,
action,
absoluteCenter,
...attrs
}: EmptyLayoutProps) => {
return (
<div
className={clsx(
styles.root,
absoluteCenter ? styles.absoluteCenter : null,
className
)}
{...attrs}
>
<ThemedImg
style={{ width: withUnit(illustrationWidth, 'px') }}
draggable={false}
className={styles.illustration}
lightSrc={illustrationLight}
darkSrc={illustrationDark}
/>
{title || description ? (
<div>
<p className={styles.title}>{title}</p>
<p className={styles.description}>{description}</p>
</div>
) : null}
{action}
</div>
);
};

View File

@@ -0,0 +1,64 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const root = style({
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
// SVG illustration has a space from top, add some padding bottom to compensate
paddingBottom: 35,
});
export const illustration = style({
maxWidth: '100%',
width: 300,
});
export const title = style({
textAlign: 'center',
fontSize: 15,
lineHeight: '24px',
fontWeight: 500,
color: cssVarV2('text/primary'),
marginBottom: 4,
});
export const description = style({
textAlign: 'center',
fontSize: 14,
fontWeight: 400,
lineHeight: '22px',
color: cssVarV2('text/secondary'),
marginBottom: 0,
maxWidth: 300,
});
export const absoluteCenter = style({
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
});
export const actionGroup = style({
display: 'flex',
gap: 16,
});
export const actionButton = style({});
export const mobileActionButton = style({
padding: '8px 18px',
height: 'auto',
fontWeight: 600,
});
export const actionContent = style({
padding: '0 4px',
});
export const mobileActionContent = style({
padding: '0 4px',
});

View File

@@ -0,0 +1,20 @@
import { useI18n } from '@affine/i18n';
import tagsDark from './assets/tag-list.dark.png';
import tagsLight from './assets/tag-list.light.png';
import { EmptyLayout } from './layout';
import type { UniversalEmptyProps } from './types';
export const EmptyTags = (props: UniversalEmptyProps) => {
const t = useI18n();
return (
<EmptyLayout
illustrationLight={tagsLight}
illustrationDark={tagsDark}
title={t['com.affine.empty.tags.title']()}
description={t['com.affine.empty.tags.description']()}
{...props}
/>
);
};

View File

@@ -0,0 +1,18 @@
import type { HTMLAttributes, ReactNode } from 'react';
export interface EmptyLayoutProps
extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
illustrationLight: string;
illustrationDark?: string;
illustrationWidth?: number | string;
title?: ReactNode;
description?: ReactNode;
action?: ReactNode;
/**
* Absolute center the content, useful for full screen empty states (e.g. mobile page)
*/
absoluteCenter?: boolean;
}
export type UniversalEmptyProps = Partial<EmptyLayoutProps>;

View File

@@ -1,33 +1,27 @@
import { Button } from '@affine/component'; import { Button } from '@affine/component';
import { useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import type { ReactElement } from 'react';
import * as styles from './collection-list-header.css'; import * as styles from './collection-list-header.css';
export const CollectionListHeader = ({ export const CollectionListHeader = ({
node,
onCreate, onCreate,
}: { }: {
node: ReactElement | null;
onCreate: () => void; onCreate: () => void;
}) => { }) => {
const t = useI18n(); const t = useI18n();
return ( return (
<> <div className={styles.collectionListHeader}>
<div className={styles.collectionListHeader}> <div className={styles.collectionListHeaderTitle}>
<div className={styles.collectionListHeaderTitle}> {t['com.affine.collections.header']()}
{t['com.affine.collections.header']()}
</div>
<Button
className={styles.newCollectionButton}
onClick={onCreate}
data-testid="all-collection-new-button"
>
{t['com.affine.collections.empty.new-collection-button']()}
</Button>
</div> </div>
{node} <Button
</> className={styles.newCollectionButton}
onClick={onCreate}
data-testid="all-collection-new-button"
>
{t['com.affine.collections.empty.new-collection-button']()}
</Button>
</div>
); );
}; };

View File

@@ -2,7 +2,6 @@ import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-co
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter'; import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useService, WorkspaceService } from '@toeverything/infra'; import { useService, WorkspaceService } from '@toeverything/infra';
import type { ReactElement } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { CollectionService } from '../../../modules/collection'; import { CollectionService } from '../../../modules/collection';
@@ -42,12 +41,10 @@ export const VirtualizedCollectionList = ({
collections, collections,
collectionMetas, collectionMetas,
setHideHeaderCreateNewCollection, setHideHeaderCreateNewCollection,
node,
handleCreateCollection, handleCreateCollection,
}: { }: {
collections: Collection[]; collections: Collection[];
collectionMetas: CollectionMeta[]; collectionMetas: CollectionMeta[];
node: ReactElement | null;
handleCreateCollection: () => void; handleCreateCollection: () => void;
setHideHeaderCreateNewCollection: (hide: boolean) => void; setHideHeaderCreateNewCollection: (hide: boolean) => void;
}) => { }) => {
@@ -107,9 +104,7 @@ export const VirtualizedCollectionList = ({
atTopThreshold={80} atTopThreshold={80}
atTopStateChange={setHideHeaderCreateNewCollection} atTopStateChange={setHideHeaderCreateNewCollection}
onSelectionActiveChange={setShowFloatingToolbar} onSelectionActiveChange={setShowFloatingToolbar}
heading={ heading={<CollectionListHeader onCreate={handleCreateCollection} />}
<CollectionListHeader node={node} onCreate={handleCreateCollection} />
}
selectedIds={filteredSelectedCollectionIds} selectedIds={filteredSelectedCollectionIds}
onSelectedIdsChange={setSelectedCollectionIds} onSelectedIdsChange={setSelectedCollectionIds}
items={collectionMetas} items={collectionMetas}

View File

@@ -110,7 +110,7 @@ export const CollectionPageListHeader = ({
jumpToCollections(workspaceId); jumpToCollections(workspaceId);
}, [jumpToCollections, workspaceId]); }, [jumpToCollections, workspaceId]);
const { node, open } = useEditCollection(); const { open } = useEditCollection();
const handleEdit = useAsyncCallback(async () => { const handleEdit = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'page'); const ret = await open({ ...collection }, 'page');
@@ -162,32 +162,29 @@ export const CollectionPageListHeader = ({
}, [createPage, onConfirmAddDocument]); }, [createPage, onConfirmAddDocument]);
return ( return (
<> <div className={styles.docListHeader}>
{node} <div className={styles.docListHeaderTitle}>
<div className={styles.docListHeader}> <div style={{ cursor: 'pointer' }} onClick={handleJumpToCollections}>
<div className={styles.docListHeaderTitle}> {t['com.affine.collections.header']()} /
<div style={{ cursor: 'pointer' }} onClick={handleJumpToCollections}>
{t['com.affine.collections.header']()} /
</div>
<div className={styles.titleIcon}>
<ViewLayersIcon />
</div>
<div className={styles.titleCollectionName}>{collection.name}</div>
</div> </div>
<div className={styles.rightButtonGroup}> <div className={styles.titleIcon}>
<Button onClick={handleEdit}>{t['Edit']()}</Button> <ViewLayersIcon />
<PageListNewPageButton
size="small"
testId="new-page-button-trigger"
onCreateDoc={onCreateDoc}
onCreateEdgeless={onCreateEdgeless}
onCreatePage={onCreatePage}
>
<div className={styles.buttonText}>{t['New Page']()}</div>
</PageListNewPageButton>
</div> </div>
<div className={styles.titleCollectionName}>{collection.name}</div>
</div> </div>
</> <div className={styles.rightButtonGroup}>
<Button onClick={handleEdit}>{t['Edit']()}</Button>
<PageListNewPageButton
size="small"
testId="new-page-button-trigger"
onCreateDoc={onCreateDoc}
onCreateEdgeless={onCreateEdgeless}
onCreatePage={onCreatePage}
>
<div className={styles.buttonText}>{t['New Page']()}</div>
</PageListNewPageButton>
</div>
</div>
); );
}; };
@@ -205,7 +202,7 @@ export const TagPageListHeader = ({
const { jumpToTags, jumpToCollection } = useNavigateHelper(); const { jumpToTags, jumpToCollection } = useNavigateHelper();
const collectionService = useService(CollectionService); const collectionService = useService(CollectionService);
const [openMenu, setOpenMenu] = useState(false); const [openMenu, setOpenMenu] = useState(false);
const { open, node } = useEditCollectionName({ const { open } = useEditCollectionName({
title: t['com.affine.editCollection.saveCollection'](), title: t['com.affine.editCollection.saveCollection'](),
showTips: true, showTips: true,
}); });
@@ -235,47 +232,44 @@ export const TagPageListHeader = ({
}, [open, saveToCollection]); }, [open, saveToCollection]);
return ( return (
<> <div className={styles.docListHeader}>
{node} <div className={styles.docListHeaderTitle}>
<div className={styles.docListHeader}> <div
<div className={styles.docListHeaderTitle}> style={{ cursor: 'pointer', lineHeight: '1.4em' }}
<div onClick={handleJumpToTags}
style={{ cursor: 'pointer', lineHeight: '1.4em' }} >
onClick={handleJumpToTags} {t['Tags']()} /
>
{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> </div>
<Button onClick={handleClick}> <Menu
{t['com.affine.editCollection.saveCollection']()} rootOptions={{
</Button> 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> </div>
</> <Button onClick={handleClick}>
{t['com.affine.editCollection.saveCollection']()}
</Button>
</div>
); );
}; };

View File

@@ -317,13 +317,11 @@ export const CollectionOperationCell = ({
favAdapter.isFavorite$(collection.id, 'collection') favAdapter.isFavorite$(collection.id, 'collection')
); );
const { open: openEditCollectionModal, node: editModal } = const { open: openEditCollectionModal } = useEditCollection();
useEditCollection();
const { open: openEditCollectionNameModal, node: editNameModal } = const { open: openEditCollectionNameModal } = useEditCollectionName({
useEditCollectionName({ title: t['com.affine.editCollection.renameCollection'](),
title: t['com.affine.editCollection.renameCollection'](), });
});
const handlePropagation = useCallback((event: MouseEvent) => { const handlePropagation = useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
@@ -402,8 +400,6 @@ export const CollectionOperationCell = ({
return ( return (
<> <>
{editModal}
{editNameModal}
<ColWrapper <ColWrapper
hideInSmallContainer hideInSmallContainer
data-testid="page-list-item-favorite" data-testid="page-list-item-favorite"

View File

@@ -51,13 +51,11 @@ export const CollectionOperations = ({
}); });
const deleteInfo = useDeleteCollectionInfo(); const deleteInfo = useDeleteCollectionInfo();
const workbench = workbenchService.workbench; const workbench = workbenchService.workbench;
const { open: openEditCollectionModal, node: editModal } = const { open: openEditCollectionModal } = useEditCollection();
useEditCollection();
const t = useI18n(); const t = useI18n();
const { open: openEditCollectionNameModal, node: editNameModal } = const { open: openEditCollectionNameModal } = useEditCollectionName({
useEditCollectionName({ title: t['com.affine.editCollection.renameCollection'](),
title: t['com.affine.editCollection.renameCollection'](), });
});
const enableMultiView = useLiveData( const enableMultiView = useLiveData(
featureFlagService.flags.enable_multi_view.$ featureFlagService.flags.enable_multi_view.$
); );
@@ -193,33 +191,29 @@ export const CollectionOperations = ({
] ]
); );
return ( return (
<> <Menu
{editModal} items={
{editNameModal} <div style={{ minWidth: 150 }}>
<Menu {actions.map(action => {
items={ if (action.element) {
<div style={{ minWidth: 150 }}> return action.element;
{actions.map(action => { }
if (action.element) { return (
return action.element; <MenuItem
} data-testid="collection-option"
return ( key={action.name}
<MenuItem type={action.type}
data-testid="collection-option" prefixIcon={action.icon}
key={action.name} onClick={action.click}
type={action.type} >
prefixIcon={action.icon} {action.name}
onClick={action.click} </MenuItem>
> );
{action.name} })}
</MenuItem> </div>
); }
})} >
</div> {children}
} </Menu>
>
{children}
</Menu>
</>
); );
}; };

View File

@@ -17,7 +17,7 @@ export const SaveAsCollectionButton = ({
onConfirm, onConfirm,
}: SaveAsCollectionButtonProps) => { }: SaveAsCollectionButtonProps) => {
const t = useI18n(); const t = useI18n();
const { open, node } = useEditCollectionName({ const { open } = useEditCollectionName({
title: t['com.affine.editCollection.saveCollection'](), title: t['com.affine.editCollection.saveCollection'](),
showTips: true, showTips: true,
}); });
@@ -31,16 +31,13 @@ export const SaveAsCollectionButton = ({
}); });
}, [open, onConfirm]); }, [open, onConfirm]);
return ( return (
<> <Button
<Button onClick={handleClick}
onClick={handleClick} data-testid="save-as-collection"
data-testid="save-as-collection" prefix={<SaveIcon />}
prefix={<SaveIcon />} className={styles.button}
className={styles.button} >
> {t['com.affine.editCollection.saveCollection']()}
{t['com.affine.editCollection.saveCollection']()} </Button>
</Button>
{node}
</>
); );
}; };

View File

@@ -1,5 +1,6 @@
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { useCallback, useState } from 'react'; import { useMount } from '@toeverything/infra';
import { useCallback, useEffect, useState } from 'react';
import { CreateCollectionModal } from './create-collection'; import { CreateCollectionModal } from './create-collection';
import type { EditCollectionMode } from './edit-collection/edit-collection'; import type { EditCollectionMode } from './edit-collection/edit-collection';
@@ -16,9 +17,11 @@ export const useEditCollection = () => {
setData(undefined); setData(undefined);
} }
}, []); }, []);
const { mount } = useMount('useEditCollection');
return { useEffect(() => {
node: ( if (!data) return;
return mount(
<EditCollectionModal <EditCollectionModal
init={data?.collection} init={data?.collection}
open={!!data} open={!!data}
@@ -26,7 +29,10 @@ export const useEditCollection = () => {
onOpenChange={close} onOpenChange={close}
onConfirm={data?.onConfirm ?? (() => {})} onConfirm={data?.onConfirm ?? (() => {})}
/> />
), );
}, [close, data, mount]);
return {
open: ( open: (
collection: Collection, collection: Collection,
mode?: EditCollectionMode mode?: EditCollectionMode
@@ -59,9 +65,11 @@ export const useEditCollectionName = ({
setData(undefined); setData(undefined);
} }
}, []); }, []);
const { mount } = useMount('useEditCollectionName');
return { useEffect(() => {
node: ( if (!data) return;
return mount(
<CreateCollectionModal <CreateCollectionModal
showTips={showTips} showTips={showTips}
title={title} title={title}
@@ -70,7 +78,10 @@ export const useEditCollectionName = ({
onOpenChange={close} onOpenChange={close}
onConfirm={data?.onConfirm ?? (() => {})} onConfirm={data?.onConfirm ?? (() => {})}
/> />
), );
}, [close, data, mount, showTips, title]);
return {
open: (name: string): Promise<string> => open: (name: string): Promise<string> =>
new Promise<string>(res => { new Promise<string>(res => {
setData({ setData({

View File

@@ -62,8 +62,7 @@ export const ExplorerCollectionNode = ({
const { globalContextService } = useServices({ const { globalContextService } = useServices({
GlobalContextService, GlobalContextService,
}); });
const { open: openEditCollectionModal, node: editModal } = const { open: openEditCollectionModal } = useEditCollection();
useEditCollection();
const active = const active =
useLiveData(globalContextService.globalContext.collectionId.$) === useLiveData(globalContextService.globalContext.collectionId.$) ===
collectionId; collectionId;
@@ -211,29 +210,26 @@ export const ExplorerCollectionNode = ({
} }
return ( return (
<> <ExplorerTreeNode
<ExplorerTreeNode icon={CollectionIcon}
icon={CollectionIcon} name={collection.name || t['Untitled']()}
name={collection.name || t['Untitled']()} dndData={dndData}
dndData={dndData} onDrop={handleDropOnCollection}
onDrop={handleDropOnCollection} renameable
renameable collapsed={collapsed}
collapsed={collapsed} setCollapsed={setCollapsed}
setCollapsed={setCollapsed} to={`/collection/${collection.id}`}
to={`/collection/${collection.id}`} active={active}
active={active} canDrop={handleCanDrop}
canDrop={handleCanDrop} reorderable={reorderable}
reorderable={reorderable} onRename={handleRename}
onRename={handleRename} childrenPlaceholder={<Empty onDrop={handleDropOnPlaceholder} />}
childrenPlaceholder={<Empty onDrop={handleDropOnPlaceholder} />} operations={finalOperations}
operations={finalOperations} dropEffect={handleDropEffectOnCollection}
dropEffect={handleDropEffectOnCollection} data-testid={`explorer-collection-${collectionId}`}
data-testid={`explorer-collection-${collectionId}`} >
> <ExplorerCollectionNodeChildren collection={collection} />
<ExplorerCollectionNodeChildren collection={collection} /> </ExplorerTreeNode>
</ExplorerTreeNode>
{editModal}
</>
); );
}; };

View File

@@ -25,7 +25,7 @@ export const ExplorerCollections = () => {
}); });
const explorerSection = explorerService.sections.collections; const explorerSection = explorerService.sections.collections;
const collections = useLiveData(collectionService.collections$); const collections = useLiveData(collectionService.collections$);
const { node, open: openCreateCollectionModel } = useEditCollectionName({ const { open: openCreateCollectionModel } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](), title: t['com.affine.editCollection.createCollection'](),
showTips: true, showTips: true,
}); });
@@ -52,40 +52,37 @@ export const ExplorerCollections = () => {
]); ]);
return ( return (
<> <CollapsibleSection
<CollapsibleSection name="collections"
name="collections" testId="explorer-collections"
testId="explorer-collections" title={t['com.affine.rootAppSidebar.collections']()}
title={t['com.affine.rootAppSidebar.collections']()} actions={
actions={ <IconButton
<IconButton data-testid="explorer-bar-add-collection-button"
data-testid="explorer-bar-add-collection-button" onClick={handleCreateCollection}
onClick={handleCreateCollection} size="16"
size="16" tooltip={t[
tooltip={t[ 'com.affine.rootAppSidebar.explorer.collection-section-add-tooltip'
'com.affine.rootAppSidebar.explorer.collection-section-add-tooltip' ]()}
]()}
>
<PlusIcon />
</IconButton>
}
>
<ExplorerTreeRoot
placeholder={<RootEmpty onClickCreate={handleCreateCollection} />}
> >
{collections.map(collection => ( <PlusIcon />
<ExplorerCollectionNode </IconButton>
key={collection.id} }
collectionId={collection.id} >
reorderable={false} <ExplorerTreeRoot
location={{ placeholder={<RootEmpty onClickCreate={handleCreateCollection} />}
at: 'explorer:collection:list', >
}} {collections.map(collection => (
/> <ExplorerCollectionNode
))} key={collection.id}
</ExplorerTreeRoot> collectionId={collection.id}
</CollapsibleSection> reorderable={false}
{node} location={{
</> at: 'explorer:collection:list',
}}
/>
))}
</ExplorerTreeRoot>
</CollapsibleSection>
); );
}; };

View File

@@ -40,7 +40,7 @@ export const AllCollection = () => {
}, [collections]); }, [collections]);
const navigateHelper = useNavigateHelper(); const navigateHelper = useNavigateHelper();
const { open, node } = useEditCollectionName({ const { open } = useEditCollectionName({
title: t['com.affine.editCollection.createCollection'](), title: t['com.affine.editCollection.createCollection'](),
showTips: true, showTips: true,
}); });
@@ -74,16 +74,12 @@ export const AllCollection = () => {
collections={collections} collections={collections}
collectionMetas={collectionMetas} collectionMetas={collectionMetas}
setHideHeaderCreateNewCollection={setHideHeaderCreateNew} setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
node={node}
handleCreateCollection={handleCreateCollection} handleCreateCollection={handleCreateCollection}
/> />
) : ( ) : (
<EmptyCollectionList <EmptyCollectionList
heading={ heading={
<CollectionListHeader <CollectionListHeader onCreate={handleCreateCollection} />
node={node}
onCreate={handleCreateCollection}
/>
} }
/> />
)} )}

View File

@@ -72,11 +72,7 @@ export const AllPage = () => {
filters={filters} filters={filters}
/> />
) : ( ) : (
<EmptyPageList <EmptyPageList type="all" heading={<PageListHeader />} />
type="all"
heading={<PageListHeader />}
docCollection={currentWorkspace.docCollection}
/>
)} )}
</div> </div>
</ViewBody> </ViewBody>

View File

@@ -1,19 +1,14 @@
import { notify } from '@affine/component'; import { notify } from '@affine/component';
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty/collection-detail';
import { import {
AffineShapeIcon,
useEditCollection, useEditCollection,
VirtualizedPageList, VirtualizedPageList,
} from '@affine/core/components/page-list'; } from '@affine/core/components/page-list';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { CollectionService } from '@affine/core/modules/collection'; import { CollectionService } from '@affine/core/modules/collection';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { Trans, useI18n } from '@affine/i18n'; import { useI18n } from '@affine/i18n';
import { import { ViewLayersIcon } from '@blocksuite/icons/rc';
CloseIcon,
FilterIcon,
PageIcon,
ViewLayersIcon,
} from '@blocksuite/icons/rc';
import { import {
GlobalContextService, GlobalContextService,
useLiveData, useLiveData,
@@ -33,7 +28,6 @@ import {
ViewTitle, ViewTitle,
} from '../../../modules/workbench'; } from '../../../modules/workbench';
import { WorkspaceSubPath } from '../../../shared'; import { WorkspaceSubPath } from '../../../shared';
import * as styles from './collection.css';
import { CollectionDetailHeader } from './header'; import { CollectionDetailHeader } from './header';
export const CollectionDetail = ({ export const CollectionDetail = ({
@@ -41,7 +35,7 @@ export const CollectionDetail = ({
}: { }: {
collection: Collection; collection: Collection;
}) => { }) => {
const { node, open } = useEditCollection(); const { open } = useEditCollection();
const collectionService = useService(CollectionService); const collectionService = useService(CollectionService);
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true); const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
@@ -64,7 +58,6 @@ export const CollectionDetail = ({
setHideHeaderCreateNewPage={setHideHeaderCreateNew} setHideHeaderCreateNewPage={setHideHeaderCreateNew}
/> />
</ViewBody> </ViewBody>
{node}
</> </>
); );
}; };
@@ -138,25 +131,7 @@ export const Component = function CollectionPage() {
const Placeholder = ({ collection }: { collection: Collection }) => { const Placeholder = ({ collection }: { collection: Collection }) => {
const workspace = useService(WorkspaceService).workspace; const workspace = useService(WorkspaceService).workspace;
const collectionService = useService(CollectionService);
const { node, open } = useEditCollection();
const { jumpToCollections } = useNavigateHelper(); const { jumpToCollections } = useNavigateHelper();
const openPageEdit = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'page');
collectionService.updateCollection(ret.id, () => ret);
}, [open, collection, collectionService]);
const openRuleEdit = useAsyncCallback(async () => {
const ret = await open({ ...collection }, 'rule');
collectionService.updateCollection(ret.id, () => ret);
}, [collection, open, collectionService]);
const [showTips, setShowTips] = useState(false);
useEffect(() => {
setShowTips(!localStorage.getItem('hide-empty-collection-help-info'));
}, []);
const hideTips = useCallback(() => {
setShowTips(false);
localStorage.setItem('hide-empty-collection-help-info', 'true');
}, []);
const t = useI18n(); const t = useI18n();
const handleJumpToCollections = useCallback(() => { const handleJumpToCollections = useCallback(() => {
@@ -206,142 +181,11 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
</div> </div>
</ViewHeader> </ViewHeader>
<ViewBody> <ViewBody>
<div <EmptyCollectionDetail
style={{ collection={collection}
display: 'flex', style={{ height: '100%' }}
flex: 1, />
flexDirection: 'column',
alignItems: 'center',
gap: 64,
}}
>
<div
style={{
maxWidth: 432,
marginTop: 118,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 18,
margin: '118px 12px 0',
}}
>
<AffineShapeIcon />
<div
style={{
fontSize: 20,
lineHeight: '28px',
fontWeight: 600,
color: 'var(--affine-text-primary-color)',
}}
>
{t['com.affine.collection.emptyCollection']()}
</div>
<div
style={{
fontSize: 12,
lineHeight: '20px',
color: 'var(--affine-text-secondary-color)',
textAlign: 'center',
}}
>
{t['com.affine.collection.emptyCollectionDescription']()}
</div>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px 32px',
flexWrap: 'wrap',
justifyContent: 'center',
}}
>
<div onClick={openPageEdit} className={styles.placeholderButton}>
<PageIcon
style={{
width: 20,
height: 20,
color: 'var(--affine-icon-color)',
}}
/>
<span style={{ padding: '0 4px' }}>
{t['com.affine.collection.addPages']()}
</span>
</div>
<div onClick={openRuleEdit} className={styles.placeholderButton}>
<FilterIcon
style={{
width: 20,
height: 20,
color: 'var(--affine-icon-color)',
}}
/>
<span style={{ padding: '0 4px' }}>
{t['com.affine.collection.addRules']()}
</span>
</div>
</div>
</div>
{showTips ? (
<div
style={{
maxWidth: 452,
borderRadius: 8,
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
padding: 10,
gap: 14,
margin: '0 12px',
}}
>
<div
style={{
fontWeight: 600,
fontSize: 12,
lineHeight: '20px',
color: 'var(--affine-text-secondary-color)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div>{t['com.affine.collection.helpInfo']()}</div>
<CloseIcon
className={styles.button}
style={{ width: 16, height: 16 }}
onClick={hideTips}
/>
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 10,
fontSize: 12,
lineHeight: '20px',
}}
>
<div>
<Trans i18nKey="com.affine.collection.addPages.tips">
<span style={{ fontWeight: 600 }}>Add pages:</span> You can
freely select pages and add them to the collection.
</Trans>
</div>
<div>
<Trans i18nKey="com.affine.collection.addRules.tips">
<span style={{ fontWeight: 600 }}>Add rules:</span> Rules
are based on filtering. After adding rules, pages that meet
the requirements will be automatically added to the current
collection.
</Trans>
</div>
</div>
</div>
) : null}
</div>
</ViewBody> </ViewBody>
{node}
</> </>
); );
}; };

View File

@@ -2,7 +2,13 @@ import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2'; import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css'; import { style } from '@vanilla-extract/css';
export const pageListEmptyStyle = style({ export const pageListEmptyStyle = style({
height: 'calc(100% - 52px)', height: '100%',
display: 'flex',
flexDirection: 'column',
});
export const pageListEmptyBody = style({
height: 0,
flex: 1,
}); });
export const emptyDescButton = style({ export const emptyDescButton = style({
cursor: 'pointer', cursor: 'pointer',

View File

@@ -1,7 +1,5 @@
import { Empty, IconButton } from '@affine/component'; import { EmptyDocs, EmptyTags } from '@affine/core/components/affine/empty';
import { Trans, useI18n } from '@affine/i18n'; import { EmptyCollections } from '@affine/core/components/affine/empty/collections';
import { PlusIcon } from '@blocksuite/icons/rc';
import type { DocCollection } from '@blocksuite/store';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import * as styles from './page-list-empty.css'; import * as styles from './page-list-empty.css';
@@ -9,79 +7,38 @@ import * as styles from './page-list-empty.css';
export const EmptyPageList = ({ export const EmptyPageList = ({
type, type,
heading, heading,
tagId,
}: { }: {
type: 'all' | 'trash' | 'shared' | 'public'; type: 'all' | 'trash';
docCollection: DocCollection;
heading?: ReactNode; heading?: ReactNode;
tagId?: string;
}) => { }) => {
const t = useI18n();
const getEmptyDescription = () => {
if (type === 'all') {
const createNewPageButton = (
<IconButton
withoutHover
className={styles.plusButton}
variant="solid"
icon={<PlusIcon />}
/>
);
if (environment.isElectron) {
const shortcut = environment.isMacOs ? '⌘ + N' : 'Ctrl + N';
return (
<Trans i18nKey="emptyAllPagesClient">
Click on the {createNewPageButton} button Or press
<kbd className={styles.emptyDescKbd}>{{ shortcut } as any}</kbd> to
create your first page.
</Trans>
);
}
return (
<Trans i18nKey="emptyAllPages">
Click on the
{createNewPageButton}
button to create your first page.
</Trans>
);
}
if (type === 'trash') {
return t['com.affine.workspaceSubPath.trash.empty-description']();
}
if (type === 'shared') {
return t['emptySharedPages']();
}
return;
};
return ( return (
<div className={styles.pageListEmptyStyle}> <div className={styles.pageListEmptyStyle}>
{heading && <div>{heading}</div>} {heading && <div>{heading}</div>}
<Empty <EmptyDocs
title={t['com.affine.emptyDesc']()} tagId={tagId}
description={ type={type}
<span className={styles.descWrapper}>{getEmptyDescription()}</span> className={styles.pageListEmptyBody}
}
/> />
</div> </div>
); );
}; };
export const EmptyCollectionList = ({ heading }: { heading: ReactNode }) => { export const EmptyCollectionList = ({ heading }: { heading: ReactNode }) => {
const t = useI18n();
return ( return (
<div className={styles.pageListEmptyStyle}> <div className={styles.pageListEmptyStyle}>
{heading && <div>{heading}</div>} {heading && <div>{heading}</div>}
<Empty title={t['com.affine.emptyDesc.collection']()} /> <EmptyCollections className={styles.pageListEmptyBody} />
</div> </div>
); );
}; };
export const EmptyTagList = ({ heading }: { heading: ReactNode }) => { export const EmptyTagList = ({ heading }: { heading: ReactNode }) => {
const t = useI18n();
return ( return (
<div className={styles.pageListEmptyStyle}> <div className={styles.pageListEmptyStyle}>
{heading && <div>{heading}</div>} {heading && <div>{heading}</div>}
<Empty title={t['com.affine.emptyDesc.tag']()} /> <EmptyTags className={styles.pageListEmptyBody} />
</div> </div>
); );
}; };

View File

@@ -79,13 +79,13 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
) : ( ) : (
<EmptyPageList <EmptyPageList
type="all" type="all"
tagId={tagId}
heading={ heading={
<TagPageListHeader <TagPageListHeader
tag={currentTag} tag={currentTag}
workspaceId={currentWorkspace.id} workspaceId={currentWorkspace.id}
/> />
} }
docCollection={currentWorkspace.docCollection}
/> />
)} )}
</div> </div>

View File

@@ -75,10 +75,7 @@ export const TrashPage = () => {
{filteredPageMetas.length > 0 ? ( {filteredPageMetas.length > 0 ? (
<VirtualizedTrashList /> <VirtualizedTrashList />
) : ( ) : (
<EmptyPageList <EmptyPageList type="trash" />
type="trash"
docCollection={currentWorkspace.docCollection}
/>
)} )}
</div> </div>
</ViewBody> </ViewBody>

View File

@@ -686,6 +686,19 @@
"com.affine.emptyDesc": "There's no doc here yet", "com.affine.emptyDesc": "There's no doc here yet",
"com.affine.emptyDesc.collection": "There's no collection here yet", "com.affine.emptyDesc.collection": "There's no collection here yet",
"com.affine.emptyDesc.tag": "There's no tag here yet", "com.affine.emptyDesc.tag": "There's no tag here yet",
"com.affine.empty.docs.title": "Docs management",
"com.affine.empty.docs.all-description": "Create your first doc here.",
"com.affine.empty.docs.trash-description": "Deleted docs will appear here.",
"com.affine.empty.docs.action.new-doc": "New doc",
"com.affine.empty.collections.title": "Collection management",
"com.affine.empty.collections.description": "Create your first collection here.",
"com.affine.empty.collections.action.new-collection": "Add collection",
"com.affine.empty.tags.title": "Tag management",
"com.affine.empty.tags.description": "Create a new tag for your documents.",
"com.affine.empty.collection-detail.title": "Empty collection",
"com.affine.empty.collection-detail.description": "Collection is a smart folder where you can manually add docs or automatically add docs through rules.",
"com.affine.empty.collection-detail.action.add-doc": "Add docs",
"com.affine.empty.collection-detail.action.add-rule": "Add rules",
"com.affine.enableAffineCloudModal.button.cancel": "Cancel", "com.affine.enableAffineCloudModal.button.cancel": "Cancel",
"com.affine.error.contact.description": "If you are still experiencing this issue, please <1>contact us through the community.</1>", "com.affine.error.contact.description": "If you are still experiencing this issue, please <1>contact us through the community.</1>",
"com.affine.error.no-page-root.title": "Doc content is missing", "com.affine.error.no-page-root.title": "Doc content is missing",

View File

@@ -1,7 +1,6 @@
import { notify, useThemeColorV2 } from '@affine/component'; import { notify, useThemeColorV2 } from '@affine/component';
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper'; import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
import { CollectionService } from '@affine/core/modules/collection'; import { CollectionService } from '@affine/core/modules/collection';
import { isEmptyCollection } from '@affine/core/pages/workspace/collection';
import { WorkspaceSubPath } from '@affine/core/shared'; import { WorkspaceSubPath } from '@affine/core/shared';
import { import {
GlobalContextService, GlobalContextService,
@@ -13,7 +12,7 @@ import { useCallback, useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { AppTabs } from '../../../components'; import { AppTabs } from '../../../components';
import { CollectionDetail, EmptyCollection } from '../../../views'; import { CollectionDetail } from '../../../views';
export const Component = () => { export const Component = () => {
useThemeColorV2('layer/background/secondary'); useThemeColorV2('layer/background/secondary');
@@ -70,10 +69,6 @@ export const Component = () => {
return null; return null;
} }
if (isEmptyCollection(collection)) {
return <EmptyCollection collection={collection} />;
}
return ( return (
<> <>
<CollectionDetail collection={collection} /> <CollectionDetail collection={collection} />

View File

@@ -1,4 +1,6 @@
import { IconButton, MobileMenu } from '@affine/component'; import { IconButton, MobileMenu } from '@affine/component';
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty';
import { isEmptyCollection } from '@affine/core/pages/workspace/collection';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc'; import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
@@ -34,6 +36,15 @@ export const CollectionDetail = ({
}: { }: {
collection: Collection; collection: Collection;
}) => { }) => {
if (isEmptyCollection(collection)) {
return (
<>
<DetailHeader collection={collection} />
<EmptyCollectionDetail collection={collection} absoluteCenter />
</>
);
}
return ( return (
<> <>
<DetailHeader collection={collection} /> <DetailHeader collection={collection} />

View File

@@ -1,3 +1,4 @@
import { EmptyCollections } from '@affine/core/components/affine/empty';
import type { CollectionMeta } from '@affine/core/components/page-list'; import type { CollectionMeta } from '@affine/core/components/page-list';
import { CollectionService } from '@affine/core/modules/collection'; import { CollectionService } from '@affine/core/modules/collection';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
@@ -19,6 +20,10 @@ export const CollectionList = () => {
[collections] [collections]
); );
if (!collectionMetas.length) {
return <EmptyCollections absoluteCenter />;
}
return ( return (
<ul className={list}> <ul className={list}>
{collectionMetas.map(meta => ( {collectionMetas.map(meta => (

View File

@@ -1,3 +1,4 @@
import { EmptyDocs } from '@affine/core/components/affine/empty';
import { import {
type ItemGroupDefinition, type ItemGroupDefinition,
type ItemGroupProps, type ItemGroupProps,
@@ -78,6 +79,10 @@ export const AllDocList = ({
return itemsToItemGroups(finalPageMetas ?? [], groupDefs); return itemsToItemGroups(finalPageMetas ?? [], groupDefs);
}, [finalPageMetas, groupDefs]); }, [finalPageMetas, groupDefs]);
if (!groups.length) {
return <EmptyDocs absoluteCenter tagId={tag?.id} />;
}
return ( return (
<div className={styles.groups}> <div className={styles.groups}>
{groups.map(group => ( {groups.map(group => (

View File

@@ -1,17 +1,10 @@
import type { Tag } from '@affine/core/modules/tag'; import type { Tag } from '@affine/core/modules/tag';
import { useLiveData } from '@toeverything/infra';
import { AppTabs } from '../../../components'; import { AppTabs } from '../../../components';
import { AllDocList } from '../doc'; import { AllDocList } from '../doc';
import { TagDetailHeader } from './detail-header'; import { TagDetailHeader } from './detail-header';
import { TagEmpty } from './empty';
export const TagDetail = ({ tag }: { tag: Tag }) => { export const TagDetail = ({ tag }: { tag: Tag }) => {
const pageIds = useLiveData(tag?.pageIds$);
if (!pageIds.length) {
return <TagEmpty tag={tag} />;
}
return ( return (
<> <>
<TagDetailHeader tag={tag} /> <TagDetailHeader tag={tag} />

View File

@@ -1,3 +1,4 @@
import { EmptyTags } from '@affine/core/components/affine/empty';
import { TagService } from '@affine/core/modules/tag'; import { TagService } from '@affine/core/modules/tag';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
@@ -8,6 +9,10 @@ export const TagList = () => {
const tagList = useService(TagService).tagList; const tagList = useService(TagService).tagList;
const tags = useLiveData(tagList.tags$); const tags = useLiveData(tagList.tags$);
if (!tags.length) {
return <EmptyTags absoluteCenter />;
}
return ( return (
<ul className={list}> <ul className={list}>
{tags.map(tag => ( {tags.map(tag => (

View File

@@ -12,10 +12,15 @@ export const RecentDocs = ({ max = 5 }: { max?: number }) => {
const cardMetas = useMemo(() => { const cardMetas = useMemo(() => {
return [...allPageMetas] return [...allPageMetas]
.filter(meta => !meta.trash)
.sort((a, b) => (b.updatedDate ?? 0) - (a.updatedDate ?? 0)) .sort((a, b) => (b.updatedDate ?? 0) - (a.updatedDate ?? 0))
.slice(0, max); .slice(0, max);
}, [allPageMetas, max]); }, [allPageMetas, max]);
if (!cardMetas.length) {
return null;
}
return ( return (
<CollapsibleSection <CollapsibleSection
name="recent" name="recent"