feat(core): new empty states for doc/collection/tag (#8197)
AF-1329, AF-1330
@ -28,6 +28,7 @@ export * from './ui/slider';
|
||||
export * from './ui/switch';
|
||||
export * from './ui/table';
|
||||
export * from './ui/tabs';
|
||||
export * from './ui/themed-img';
|
||||
export * from './ui/toast';
|
||||
export * from './ui/tooltip';
|
||||
export * from './utils';
|
||||
|
@ -16,6 +16,9 @@ export type EmptyContentProps = {
|
||||
descriptionStyle?: CSSProperties;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use different empty components for different use cases, like `EmptyDocs` for documentation empty state
|
||||
*/
|
||||
export const Empty = ({
|
||||
containerStyle,
|
||||
title,
|
||||
|
17
packages/frontend/component/src/ui/themed-img/index.tsx
Normal 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} />;
|
||||
}
|
||||
);
|
@ -1,2 +1,3 @@
|
||||
export * from './observe-resize';
|
||||
export { startScopedViewTransition } from './view-transition';
|
||||
export * from './with-unit';
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
69
packages/frontend/core/src/components/affine/empty/docs.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export * from './collection-detail';
|
||||
export * from './collections';
|
||||
export * from './docs';
|
||||
export * from './tags';
|
@ -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>
|
||||
);
|
||||
};
|
@ -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',
|
||||
});
|
20
packages/frontend/core/src/components/affine/empty/tags.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
18
packages/frontend/core/src/components/affine/empty/types.ts
Normal 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>;
|
@ -1,33 +1,27 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import * as styles from './collection-list-header.css';
|
||||
|
||||
export const CollectionListHeader = ({
|
||||
node,
|
||||
onCreate,
|
||||
}: {
|
||||
node: ReactElement | null;
|
||||
onCreate: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.collectionListHeader}>
|
||||
<div className={styles.collectionListHeaderTitle}>
|
||||
{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 className={styles.collectionListHeader}>
|
||||
<div className={styles.collectionListHeaderTitle}>
|
||||
{t['com.affine.collections.header']()}
|
||||
</div>
|
||||
{node}
|
||||
</>
|
||||
<Button
|
||||
className={styles.newCollectionButton}
|
||||
onClick={onCreate}
|
||||
data-testid="all-collection-new-button"
|
||||
>
|
||||
{t['com.affine.collections.empty.new-collection-button']()}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,6 @@ import { useDeleteCollectionInfo } from '@affine/core/hooks/affine/use-delete-co
|
||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { CollectionService } from '../../../modules/collection';
|
||||
@ -42,12 +41,10 @@ export const VirtualizedCollectionList = ({
|
||||
collections,
|
||||
collectionMetas,
|
||||
setHideHeaderCreateNewCollection,
|
||||
node,
|
||||
handleCreateCollection,
|
||||
}: {
|
||||
collections: Collection[];
|
||||
collectionMetas: CollectionMeta[];
|
||||
node: ReactElement | null;
|
||||
handleCreateCollection: () => void;
|
||||
setHideHeaderCreateNewCollection: (hide: boolean) => void;
|
||||
}) => {
|
||||
@ -107,9 +104,7 @@ export const VirtualizedCollectionList = ({
|
||||
atTopThreshold={80}
|
||||
atTopStateChange={setHideHeaderCreateNewCollection}
|
||||
onSelectionActiveChange={setShowFloatingToolbar}
|
||||
heading={
|
||||
<CollectionListHeader node={node} onCreate={handleCreateCollection} />
|
||||
}
|
||||
heading={<CollectionListHeader onCreate={handleCreateCollection} />}
|
||||
selectedIds={filteredSelectedCollectionIds}
|
||||
onSelectedIdsChange={setSelectedCollectionIds}
|
||||
items={collectionMetas}
|
||||
|
@ -110,7 +110,7 @@ export const CollectionPageListHeader = ({
|
||||
jumpToCollections(workspaceId);
|
||||
}, [jumpToCollections, workspaceId]);
|
||||
|
||||
const { node, open } = useEditCollection();
|
||||
const { open } = useEditCollection();
|
||||
|
||||
const handleEdit = useAsyncCallback(async () => {
|
||||
const ret = await open({ ...collection }, 'page');
|
||||
@ -162,32 +162,29 @@ export const CollectionPageListHeader = ({
|
||||
}, [createPage, onConfirmAddDocument]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{node}
|
||||
<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}>{collection.name}</div>
|
||||
<div className={styles.docListHeader}>
|
||||
<div className={styles.docListHeaderTitle}>
|
||||
<div style={{ cursor: 'pointer' }} onClick={handleJumpToCollections}>
|
||||
{t['com.affine.collections.header']()} /
|
||||
</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 className={styles.titleIcon}>
|
||||
<ViewLayersIcon />
|
||||
</div>
|
||||
<div className={styles.titleCollectionName}>{collection.name}</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 collectionService = useService(CollectionService);
|
||||
const [openMenu, setOpenMenu] = useState(false);
|
||||
const { open, node } = useEditCollectionName({
|
||||
const { open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
@ -235,47 +232,44 @@ export const TagPageListHeader = ({
|
||||
}, [open, saveToCollection]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{node}
|
||||
<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 className={styles.docListHeader}>
|
||||
<div className={styles.docListHeaderTitle}>
|
||||
<div
|
||||
style={{ cursor: 'pointer', lineHeight: '1.4em' }}
|
||||
onClick={handleJumpToTags}
|
||||
>
|
||||
{t['Tags']()} /
|
||||
</div>
|
||||
<Button onClick={handleClick}>
|
||||
{t['com.affine.editCollection.saveCollection']()}
|
||||
</Button>
|
||||
<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>
|
||||
</>
|
||||
<Button onClick={handleClick}>
|
||||
{t['com.affine.editCollection.saveCollection']()}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -317,13 +317,11 @@ export const CollectionOperationCell = ({
|
||||
favAdapter.isFavorite$(collection.id, 'collection')
|
||||
);
|
||||
|
||||
const { open: openEditCollectionModal, node: editModal } =
|
||||
useEditCollection();
|
||||
const { open: openEditCollectionModal } = useEditCollection();
|
||||
|
||||
const { open: openEditCollectionNameModal, node: editNameModal } =
|
||||
useEditCollectionName({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
});
|
||||
const { open: openEditCollectionNameModal } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
});
|
||||
|
||||
const handlePropagation = useCallback((event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
@ -402,8 +400,6 @@ export const CollectionOperationCell = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{editModal}
|
||||
{editNameModal}
|
||||
<ColWrapper
|
||||
hideInSmallContainer
|
||||
data-testid="page-list-item-favorite"
|
||||
|
@ -51,13 +51,11 @@ export const CollectionOperations = ({
|
||||
});
|
||||
const deleteInfo = useDeleteCollectionInfo();
|
||||
const workbench = workbenchService.workbench;
|
||||
const { open: openEditCollectionModal, node: editModal } =
|
||||
useEditCollection();
|
||||
const { open: openEditCollectionModal } = useEditCollection();
|
||||
const t = useI18n();
|
||||
const { open: openEditCollectionNameModal, node: editNameModal } =
|
||||
useEditCollectionName({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
});
|
||||
const { open: openEditCollectionNameModal } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.renameCollection'](),
|
||||
});
|
||||
const enableMultiView = useLiveData(
|
||||
featureFlagService.flags.enable_multi_view.$
|
||||
);
|
||||
@ -193,33 +191,29 @@ export const CollectionOperations = ({
|
||||
]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{editModal}
|
||||
{editNameModal}
|
||||
<Menu
|
||||
items={
|
||||
<div style={{ minWidth: 150 }}>
|
||||
{actions.map(action => {
|
||||
if (action.element) {
|
||||
return action.element;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="collection-option"
|
||||
key={action.name}
|
||||
type={action.type}
|
||||
prefixIcon={action.icon}
|
||||
onClick={action.click}
|
||||
>
|
||||
{action.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
</>
|
||||
<Menu
|
||||
items={
|
||||
<div style={{ minWidth: 150 }}>
|
||||
{actions.map(action => {
|
||||
if (action.element) {
|
||||
return action.element;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="collection-option"
|
||||
key={action.name}
|
||||
type={action.type}
|
||||
prefixIcon={action.icon}
|
||||
onClick={action.click}
|
||||
>
|
||||
{action.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ export const SaveAsCollectionButton = ({
|
||||
onConfirm,
|
||||
}: SaveAsCollectionButtonProps) => {
|
||||
const t = useI18n();
|
||||
const { open, node } = useEditCollectionName({
|
||||
const { open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
@ -31,16 +31,13 @@ export const SaveAsCollectionButton = ({
|
||||
});
|
||||
}, [open, onConfirm]);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
data-testid="save-as-collection"
|
||||
prefix={<SaveIcon />}
|
||||
className={styles.button}
|
||||
>
|
||||
{t['com.affine.editCollection.saveCollection']()}
|
||||
</Button>
|
||||
{node}
|
||||
</>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
data-testid="save-as-collection"
|
||||
prefix={<SaveIcon />}
|
||||
className={styles.button}
|
||||
>
|
||||
{t['com.affine.editCollection.saveCollection']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
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 type { EditCollectionMode } from './edit-collection/edit-collection';
|
||||
@ -16,9 +17,11 @@ export const useEditCollection = () => {
|
||||
setData(undefined);
|
||||
}
|
||||
}, []);
|
||||
const { mount } = useMount('useEditCollection');
|
||||
|
||||
return {
|
||||
node: (
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
return mount(
|
||||
<EditCollectionModal
|
||||
init={data?.collection}
|
||||
open={!!data}
|
||||
@ -26,7 +29,10 @@ export const useEditCollection = () => {
|
||||
onOpenChange={close}
|
||||
onConfirm={data?.onConfirm ?? (() => {})}
|
||||
/>
|
||||
),
|
||||
);
|
||||
}, [close, data, mount]);
|
||||
|
||||
return {
|
||||
open: (
|
||||
collection: Collection,
|
||||
mode?: EditCollectionMode
|
||||
@ -59,9 +65,11 @@ export const useEditCollectionName = ({
|
||||
setData(undefined);
|
||||
}
|
||||
}, []);
|
||||
const { mount } = useMount('useEditCollectionName');
|
||||
|
||||
return {
|
||||
node: (
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
return mount(
|
||||
<CreateCollectionModal
|
||||
showTips={showTips}
|
||||
title={title}
|
||||
@ -70,7 +78,10 @@ export const useEditCollectionName = ({
|
||||
onOpenChange={close}
|
||||
onConfirm={data?.onConfirm ?? (() => {})}
|
||||
/>
|
||||
),
|
||||
);
|
||||
}, [close, data, mount, showTips, title]);
|
||||
|
||||
return {
|
||||
open: (name: string): Promise<string> =>
|
||||
new Promise<string>(res => {
|
||||
setData({
|
||||
|
@ -62,8 +62,7 @@ export const ExplorerCollectionNode = ({
|
||||
const { globalContextService } = useServices({
|
||||
GlobalContextService,
|
||||
});
|
||||
const { open: openEditCollectionModal, node: editModal } =
|
||||
useEditCollection();
|
||||
const { open: openEditCollectionModal } = useEditCollection();
|
||||
const active =
|
||||
useLiveData(globalContextService.globalContext.collectionId.$) ===
|
||||
collectionId;
|
||||
@ -211,29 +210,26 @@ export const ExplorerCollectionNode = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExplorerTreeNode
|
||||
icon={CollectionIcon}
|
||||
name={collection.name || t['Untitled']()}
|
||||
dndData={dndData}
|
||||
onDrop={handleDropOnCollection}
|
||||
renameable
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
to={`/collection/${collection.id}`}
|
||||
active={active}
|
||||
canDrop={handleCanDrop}
|
||||
reorderable={reorderable}
|
||||
onRename={handleRename}
|
||||
childrenPlaceholder={<Empty onDrop={handleDropOnPlaceholder} />}
|
||||
operations={finalOperations}
|
||||
dropEffect={handleDropEffectOnCollection}
|
||||
data-testid={`explorer-collection-${collectionId}`}
|
||||
>
|
||||
<ExplorerCollectionNodeChildren collection={collection} />
|
||||
</ExplorerTreeNode>
|
||||
{editModal}
|
||||
</>
|
||||
<ExplorerTreeNode
|
||||
icon={CollectionIcon}
|
||||
name={collection.name || t['Untitled']()}
|
||||
dndData={dndData}
|
||||
onDrop={handleDropOnCollection}
|
||||
renameable
|
||||
collapsed={collapsed}
|
||||
setCollapsed={setCollapsed}
|
||||
to={`/collection/${collection.id}`}
|
||||
active={active}
|
||||
canDrop={handleCanDrop}
|
||||
reorderable={reorderable}
|
||||
onRename={handleRename}
|
||||
childrenPlaceholder={<Empty onDrop={handleDropOnPlaceholder} />}
|
||||
operations={finalOperations}
|
||||
dropEffect={handleDropEffectOnCollection}
|
||||
data-testid={`explorer-collection-${collectionId}`}
|
||||
>
|
||||
<ExplorerCollectionNodeChildren collection={collection} />
|
||||
</ExplorerTreeNode>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -25,7 +25,7 @@ export const ExplorerCollections = () => {
|
||||
});
|
||||
const explorerSection = explorerService.sections.collections;
|
||||
const collections = useLiveData(collectionService.collections$);
|
||||
const { node, open: openCreateCollectionModel } = useEditCollectionName({
|
||||
const { open: openCreateCollectionModel } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.createCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
@ -52,40 +52,37 @@ export const ExplorerCollections = () => {
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CollapsibleSection
|
||||
name="collections"
|
||||
testId="explorer-collections"
|
||||
title={t['com.affine.rootAppSidebar.collections']()}
|
||||
actions={
|
||||
<IconButton
|
||||
data-testid="explorer-bar-add-collection-button"
|
||||
onClick={handleCreateCollection}
|
||||
size="16"
|
||||
tooltip={t[
|
||||
'com.affine.rootAppSidebar.explorer.collection-section-add-tooltip'
|
||||
]()}
|
||||
>
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ExplorerTreeRoot
|
||||
placeholder={<RootEmpty onClickCreate={handleCreateCollection} />}
|
||||
<CollapsibleSection
|
||||
name="collections"
|
||||
testId="explorer-collections"
|
||||
title={t['com.affine.rootAppSidebar.collections']()}
|
||||
actions={
|
||||
<IconButton
|
||||
data-testid="explorer-bar-add-collection-button"
|
||||
onClick={handleCreateCollection}
|
||||
size="16"
|
||||
tooltip={t[
|
||||
'com.affine.rootAppSidebar.explorer.collection-section-add-tooltip'
|
||||
]()}
|
||||
>
|
||||
{collections.map(collection => (
|
||||
<ExplorerCollectionNode
|
||||
key={collection.id}
|
||||
collectionId={collection.id}
|
||||
reorderable={false}
|
||||
location={{
|
||||
at: 'explorer:collection:list',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ExplorerTreeRoot>
|
||||
</CollapsibleSection>
|
||||
{node}
|
||||
</>
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ExplorerTreeRoot
|
||||
placeholder={<RootEmpty onClickCreate={handleCreateCollection} />}
|
||||
>
|
||||
{collections.map(collection => (
|
||||
<ExplorerCollectionNode
|
||||
key={collection.id}
|
||||
collectionId={collection.id}
|
||||
reorderable={false}
|
||||
location={{
|
||||
at: 'explorer:collection:list',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ExplorerTreeRoot>
|
||||
</CollapsibleSection>
|
||||
);
|
||||
};
|
||||
|
@ -40,7 +40,7 @@ export const AllCollection = () => {
|
||||
}, [collections]);
|
||||
|
||||
const navigateHelper = useNavigateHelper();
|
||||
const { open, node } = useEditCollectionName({
|
||||
const { open } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.createCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
@ -74,16 +74,12 @@ export const AllCollection = () => {
|
||||
collections={collections}
|
||||
collectionMetas={collectionMetas}
|
||||
setHideHeaderCreateNewCollection={setHideHeaderCreateNew}
|
||||
node={node}
|
||||
handleCreateCollection={handleCreateCollection}
|
||||
/>
|
||||
) : (
|
||||
<EmptyCollectionList
|
||||
heading={
|
||||
<CollectionListHeader
|
||||
node={node}
|
||||
onCreate={handleCreateCollection}
|
||||
/>
|
||||
<CollectionListHeader onCreate={handleCreateCollection} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@ -72,11 +72,7 @@ export const AllPage = () => {
|
||||
filters={filters}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList
|
||||
type="all"
|
||||
heading={<PageListHeader />}
|
||||
docCollection={currentWorkspace.docCollection}
|
||||
/>
|
||||
<EmptyPageList type="all" heading={<PageListHeader />} />
|
||||
)}
|
||||
</div>
|
||||
</ViewBody>
|
||||
|
@ -1,19 +1,14 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { EmptyCollectionDetail } from '@affine/core/components/affine/empty/collection-detail';
|
||||
import {
|
||||
AffineShapeIcon,
|
||||
useEditCollection,
|
||||
VirtualizedPageList,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import {
|
||||
CloseIcon,
|
||||
FilterIcon,
|
||||
PageIcon,
|
||||
ViewLayersIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
@ -33,7 +28,6 @@ import {
|
||||
ViewTitle,
|
||||
} from '../../../modules/workbench';
|
||||
import { WorkspaceSubPath } from '../../../shared';
|
||||
import * as styles from './collection.css';
|
||||
import { CollectionDetailHeader } from './header';
|
||||
|
||||
export const CollectionDetail = ({
|
||||
@ -41,7 +35,7 @@ export const CollectionDetail = ({
|
||||
}: {
|
||||
collection: Collection;
|
||||
}) => {
|
||||
const { node, open } = useEditCollection();
|
||||
const { open } = useEditCollection();
|
||||
const collectionService = useService(CollectionService);
|
||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||
|
||||
@ -64,7 +58,6 @@ export const CollectionDetail = ({
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
/>
|
||||
</ViewBody>
|
||||
{node}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -138,25 +131,7 @@ export const Component = function CollectionPage() {
|
||||
|
||||
const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const collectionService = useService(CollectionService);
|
||||
const { node, open } = useEditCollection();
|
||||
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 handleJumpToCollections = useCallback(() => {
|
||||
@ -206,142 +181,11 @@ const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
</div>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
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>
|
||||
<EmptyCollectionDetail
|
||||
collection={collection}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
</ViewBody>
|
||||
{node}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,13 @@ import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
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({
|
||||
cursor: 'pointer',
|
||||
|
@ -1,7 +1,5 @@
|
||||
import { Empty, IconButton } from '@affine/component';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import type { DocCollection } from '@blocksuite/store';
|
||||
import { EmptyDocs, EmptyTags } from '@affine/core/components/affine/empty';
|
||||
import { EmptyCollections } from '@affine/core/components/affine/empty/collections';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import * as styles from './page-list-empty.css';
|
||||
@ -9,79 +7,38 @@ import * as styles from './page-list-empty.css';
|
||||
export const EmptyPageList = ({
|
||||
type,
|
||||
heading,
|
||||
tagId,
|
||||
}: {
|
||||
type: 'all' | 'trash' | 'shared' | 'public';
|
||||
docCollection: DocCollection;
|
||||
type: 'all' | 'trash';
|
||||
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 (
|
||||
<div className={styles.pageListEmptyStyle}>
|
||||
{heading && <div>{heading}</div>}
|
||||
<Empty
|
||||
title={t['com.affine.emptyDesc']()}
|
||||
description={
|
||||
<span className={styles.descWrapper}>{getEmptyDescription()}</span>
|
||||
}
|
||||
<EmptyDocs
|
||||
tagId={tagId}
|
||||
type={type}
|
||||
className={styles.pageListEmptyBody}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyCollectionList = ({ heading }: { heading: ReactNode }) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<div className={styles.pageListEmptyStyle}>
|
||||
{heading && <div>{heading}</div>}
|
||||
<Empty title={t['com.affine.emptyDesc.collection']()} />
|
||||
<EmptyCollections className={styles.pageListEmptyBody} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyTagList = ({ heading }: { heading: ReactNode }) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<div className={styles.pageListEmptyStyle}>
|
||||
{heading && <div>{heading}</div>}
|
||||
<Empty title={t['com.affine.emptyDesc.tag']()} />
|
||||
<EmptyTags className={styles.pageListEmptyBody} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -79,13 +79,13 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
) : (
|
||||
<EmptyPageList
|
||||
type="all"
|
||||
tagId={tagId}
|
||||
heading={
|
||||
<TagPageListHeader
|
||||
tag={currentTag}
|
||||
workspaceId={currentWorkspace.id}
|
||||
/>
|
||||
}
|
||||
docCollection={currentWorkspace.docCollection}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -75,10 +75,7 @@ export const TrashPage = () => {
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedTrashList />
|
||||
) : (
|
||||
<EmptyPageList
|
||||
type="trash"
|
||||
docCollection={currentWorkspace.docCollection}
|
||||
/>
|
||||
<EmptyPageList type="trash" />
|
||||
)}
|
||||
</div>
|
||||
</ViewBody>
|
||||
|
@ -686,6 +686,19 @@
|
||||
"com.affine.emptyDesc": "There's no doc here yet",
|
||||
"com.affine.emptyDesc.collection": "There's no collection 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.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",
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { notify, useThemeColorV2 } from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { isEmptyCollection } from '@affine/core/pages/workspace/collection';
|
||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||
import {
|
||||
GlobalContextService,
|
||||
@ -13,7 +12,7 @@ import { useCallback, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { AppTabs } from '../../../components';
|
||||
import { CollectionDetail, EmptyCollection } from '../../../views';
|
||||
import { CollectionDetail } from '../../../views';
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/secondary');
|
||||
@ -70,10 +69,6 @@ export const Component = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEmptyCollection(collection)) {
|
||||
return <EmptyCollection collection={collection} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<CollectionDetail collection={collection} />
|
||||
|
@ -1,4 +1,6 @@
|
||||
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 { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
@ -34,6 +36,15 @@ export const CollectionDetail = ({
|
||||
}: {
|
||||
collection: Collection;
|
||||
}) => {
|
||||
if (isEmptyCollection(collection)) {
|
||||
return (
|
||||
<>
|
||||
<DetailHeader collection={collection} />
|
||||
<EmptyCollectionDetail collection={collection} absoluteCenter />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailHeader collection={collection} />
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { EmptyCollections } from '@affine/core/components/affine/empty';
|
||||
import type { CollectionMeta } from '@affine/core/components/page-list';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@ -19,6 +20,10 @@ export const CollectionList = () => {
|
||||
[collections]
|
||||
);
|
||||
|
||||
if (!collectionMetas.length) {
|
||||
return <EmptyCollections absoluteCenter />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={list}>
|
||||
{collectionMetas.map(meta => (
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { EmptyDocs } from '@affine/core/components/affine/empty';
|
||||
import {
|
||||
type ItemGroupDefinition,
|
||||
type ItemGroupProps,
|
||||
@ -78,6 +79,10 @@ export const AllDocList = ({
|
||||
return itemsToItemGroups(finalPageMetas ?? [], groupDefs);
|
||||
}, [finalPageMetas, groupDefs]);
|
||||
|
||||
if (!groups.length) {
|
||||
return <EmptyDocs absoluteCenter tagId={tag?.id} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.groups}>
|
||||
{groups.map(group => (
|
||||
|
@ -1,17 +1,10 @@
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { useLiveData } from '@toeverything/infra';
|
||||
|
||||
import { AppTabs } from '../../../components';
|
||||
import { AllDocList } from '../doc';
|
||||
import { TagDetailHeader } from './detail-header';
|
||||
import { TagEmpty } from './empty';
|
||||
|
||||
export const TagDetail = ({ tag }: { tag: Tag }) => {
|
||||
const pageIds = useLiveData(tag?.pageIds$);
|
||||
|
||||
if (!pageIds.length) {
|
||||
return <TagEmpty tag={tag} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<TagDetailHeader tag={tag} />
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { EmptyTags } from '@affine/core/components/affine/empty';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
@ -8,6 +9,10 @@ export const TagList = () => {
|
||||
const tagList = useService(TagService).tagList;
|
||||
const tags = useLiveData(tagList.tags$);
|
||||
|
||||
if (!tags.length) {
|
||||
return <EmptyTags absoluteCenter />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className={list}>
|
||||
{tags.map(tag => (
|
||||
|
@ -12,10 +12,15 @@ export const RecentDocs = ({ max = 5 }: { max?: number }) => {
|
||||
|
||||
const cardMetas = useMemo(() => {
|
||||
return [...allPageMetas]
|
||||
.filter(meta => !meta.trash)
|
||||
.sort((a, b) => (b.updatedDate ?? 0) - (a.updatedDate ?? 0))
|
||||
.slice(0, max);
|
||||
}, [allPageMetas, max]);
|
||||
|
||||
if (!cardMetas.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
name="recent"
|
||||
|