feat(core): new empty states for doc/collection/tag (#8197)
AF-1329, AF-1330
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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({
|
||||
|
||||