From 75a308ac7953d5b46f25be2e24de5420c5ef6a19 Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Wed, 7 Aug 2024 08:29:19 +0000 Subject: [PATCH] fix(core): optimize explorer's dnd behaviors (#7769) close AF-1198, AF-1169, AF-1204, AF-1167, AF-1168 - **fix**: empty favorite cannot be dropped(AF-1198) - **fix**: folder close animation has a unexpected delay - **fix**: mount explorer's DropEffect to body to avoid clipping(AF-1169) - **feat**: drop on empty organize to create folder and put item into it(AF-1204) - **feat**: only show explorer section's action when hovered(AF-1168) - **feat**: animate folder icon when opened(AF-1167) - **chore**: extract dnd related `dropEffect`, `canDrop` functions outside component --- .../component/src/ui/lottie/folder-icon.json | 2 +- .../component/src/ui/lottie/folder-icon.tsx | 14 ++- .../app-sidebar/category-divider/index.css.ts | 17 ++- .../views/layouts/collapsible-section.css.ts | 9 ++ .../views/layouts/collapsible-section.tsx | 4 +- .../views/layouts/empty-section.css.ts | 7 ++ .../explorer/views/layouts/empty-section.tsx | 15 ++- .../explorer/views/nodes/collection/index.tsx | 19 +++- .../explorer/views/nodes/folder/index.tsx | 23 +++- .../explorer/views/sections/favorites/dnd.ts | 45 ++++++++ .../views/sections/favorites/empty.tsx | 41 +++---- .../views/sections/favorites/index.tsx | 107 ++++-------------- .../views/sections/favorites/styles.css.ts | 12 -- .../explorer/views/sections/organize/dnd.ts | 36 ++++++ .../views/sections/organize/empty.tsx | 59 ++++++++-- .../views/sections/organize/index.tsx | 51 +++++---- .../views/sections/organize/styles.css.ts | 11 -- .../explorer/views/tree/drop-effect.css.ts | 7 +- .../explorer/views/tree/drop-effect.tsx | 11 +- .../src/modules/explorer/views/tree/node.tsx | 18 +-- .../core/src/modules/organize/constants.ts | 12 ++ 21 files changed, 309 insertions(+), 211 deletions(-) create mode 100644 packages/frontend/core/src/modules/explorer/views/sections/favorites/dnd.ts delete mode 100644 packages/frontend/core/src/modules/explorer/views/sections/favorites/styles.css.ts create mode 100644 packages/frontend/core/src/modules/explorer/views/sections/organize/dnd.ts delete mode 100644 packages/frontend/core/src/modules/explorer/views/sections/organize/styles.css.ts create mode 100644 packages/frontend/core/src/modules/organize/constants.ts diff --git a/packages/frontend/component/src/ui/lottie/folder-icon.json b/packages/frontend/component/src/ui/lottie/folder-icon.json index 027e384fb7..285e9b6647 100644 --- a/packages/frontend/component/src/ui/lottie/folder-icon.json +++ b/packages/frontend/component/src/ui/lottie/folder-icon.json @@ -2,7 +2,7 @@ "v": "5.12.1", "fr": 60, "ip": 0, - "op": 89, + "op": 6, "w": 240, "h": 240, "nm": "folder", diff --git a/packages/frontend/component/src/ui/lottie/folder-icon.tsx b/packages/frontend/component/src/ui/lottie/folder-icon.tsx index 42d976350c..7bcd7be6df 100644 --- a/packages/frontend/component/src/ui/lottie/folder-icon.tsx +++ b/packages/frontend/component/src/ui/lottie/folder-icon.tsx @@ -7,25 +7,31 @@ import animationData from './folder-icon.json'; import * as styles from './styles.css'; export interface FolderIconProps { - closed: boolean; // eg, when folder icon is a "dragged over" state + open: boolean; // eg, when folder icon is a "dragged over" state className?: string; + speed?: number; } // animated folder icon that has two states: closed and opened -export const AnimatedFolderIcon = ({ closed, className }: FolderIconProps) => { +export const AnimatedFolderIcon = ({ + open, + className, + speed = 0.5, +}: FolderIconProps) => { const lottieRef: LottieRef = useRef(null); useEffect(() => { if (lottieRef.current) { const lottie = lottieRef.current; - if (closed) { + lottie.setSpeed(speed); + if (open) { lottie.setDirection(1); } else { lottie.setDirection(-1); } lottie.play(); } - }, [closed]); + }, [open, speed]); return ( {actions} diff --git a/packages/frontend/core/src/modules/explorer/views/layouts/empty-section.css.ts b/packages/frontend/core/src/modules/explorer/views/layouts/empty-section.css.ts index 68168e6d31..d4c92d3705 100644 --- a/packages/frontend/core/src/modules/explorer/views/layouts/empty-section.css.ts +++ b/packages/frontend/core/src/modules/explorer/views/layouts/empty-section.css.ts @@ -9,6 +9,13 @@ export const content = style({ alignItems: 'center', gap: 4, padding: '12px 0px', + borderRadius: 8, + selectors: { + // assume that the section can be dragged over + '&[data-dragged-over="true"]': { + backgroundColor: cssVarV2('layer/background/hoverOverlay'), + }, + }, }); export const iconWrapper = style({ width: 36, diff --git a/packages/frontend/core/src/modules/explorer/views/layouts/empty-section.tsx b/packages/frontend/core/src/modules/explorer/views/layouts/empty-section.tsx index 6a36542797..f4bc65a926 100644 --- a/packages/frontend/core/src/modules/explorer/views/layouts/empty-section.tsx +++ b/packages/frontend/core/src/modules/explorer/views/layouts/empty-section.tsx @@ -1,8 +1,10 @@ import { Button } from '@affine/component'; import clsx from 'clsx'; import { + cloneElement, forwardRef, type HTMLAttributes, + type ReactElement, type Ref, type SVGProps, } from 'react'; @@ -10,7 +12,7 @@ import { import * as styles from './empty-section.css'; interface ExplorerEmptySectionProps extends HTMLAttributes { - icon: (props: SVGProps) => JSX.Element; + icon: ((props: SVGProps) => JSX.Element) | ReactElement; message: string; messageTestId?: string; actionText?: string; @@ -30,11 +32,16 @@ export const ExplorerEmptySection = forwardRef(function ExplorerEmptySection( }: ExplorerEmptySectionProps, ref: Ref ) { + const icon = + typeof Icon === 'function' ? ( + + ) : ( + cloneElement(Icon, { className: styles.icon }) + ); + return (
-
- -
+
{icon}
{message}
diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx index 28bddf6087..9b695cc3f7 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/collection/index.tsx @@ -31,11 +31,23 @@ import { import { useCallback, useEffect, useMemo, useState } from 'react'; import { ExplorerTreeNode, type ExplorerTreeNodeDropEffect } from '../../tree'; +import type { ExplorerTreeNodeIcon } from '../../tree/node'; import { ExplorerDocNode } from '../doc'; import type { GenericExplorerNode } from '../types'; import { Empty } from './empty'; import { useExplorerCollectionNodeOperations } from './operations'; +const CollectionIcon: ExplorerTreeNodeIcon = ({ + className, + draggedOver, + treeInstruction, +}) => ( + +); + export const ExplorerCollectionNode = ({ collectionId, onDrop, @@ -202,12 +214,7 @@ export const ExplorerCollectionNode = ({ return ( <> ( - - )} + icon={CollectionIcon} name={collection.name || t['Untitled']()} dndData={dndData} onDrop={handleDropOnCollection} diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/folder/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/folder/index.tsx index 6a85fea3d1..61fbfa3c40 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/folder/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/folder/index.tsx @@ -39,6 +39,7 @@ import { difference } from 'lodash-es'; import { useCallback, useMemo, useState } from 'react'; import { ExplorerTreeNode, type ExplorerTreeNodeDropEffect } from '../../tree'; +import type { ExplorerTreeNodeIcon } from '../../tree/node'; import type { NodeOperation } from '../../tree/types'; import { ExplorerCollectionNode } from '../collection'; import { ExplorerDocNode } from '../doc'; @@ -150,6 +151,21 @@ export const ExplorerFolderNode = ({ return; }; +// Define outside the `ExplorerFolderNodeFolder` to avoid re-render(the close animation won't play) +const ExplorerFolderIcon: ExplorerTreeNodeIcon = ({ + collapsed, + className, + draggedOver, + treeInstruction, +}) => ( + +); + export const ExplorerFolderNodeFolder = ({ node, onDrop, @@ -758,12 +774,7 @@ export const ExplorerFolderNodeFolder = ({ return ( ( - - )} + icon={ExplorerFolderIcon} name={name} dndData={dndData} onDrop={handleDropOnFolder} diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/dnd.ts b/packages/frontend/core/src/modules/explorer/views/sections/favorites/dnd.ts new file mode 100644 index 0000000000..32aede8269 --- /dev/null +++ b/packages/frontend/core/src/modules/explorer/views/sections/favorites/dnd.ts @@ -0,0 +1,45 @@ +import type { DropTargetOptions } from '@affine/component'; +import { isFavoriteSupportType } from '@affine/core/modules/favorite'; +import type { AffineDNDData } from '@affine/core/types/dnd'; + +import type { ExplorerTreeNodeDropEffect } from '../../tree'; + +export const favoriteChildrenDropEffect: ExplorerTreeNodeDropEffect = data => { + if ( + data.treeInstruction?.type === 'reorder-above' || + data.treeInstruction?.type === 'reorder-below' + ) { + if ( + data.source.data.from?.at === 'explorer:favorite:list' && + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) + ) { + return 'move'; + } else if ( + data.source.data.entity?.type && + isFavoriteSupportType(data.source.data.entity.type) + ) { + return 'link'; + } + } + return; // not supported +}; + +export const favoriteRootDropEffect: ExplorerTreeNodeDropEffect = data => { + const sourceType = data.source.data.entity?.type; + if (sourceType && isFavoriteSupportType(sourceType)) { + return 'link'; + } + return; +}; + +export const favoriteRootCanDrop: DropTargetOptions['canDrop'] = + data => { + return data.source.data.entity?.type + ? isFavoriteSupportType(data.source.data.entity.type) + : false; + }; + +export const favoriteChildrenCanDrop: DropTargetOptions['canDrop'] = + // Same as favoriteRootCanDrop + data => favoriteRootCanDrop(data); diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx b/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx index 95ed4f631c..47ad241bb8 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/favorites/empty.tsx @@ -1,6 +1,5 @@ import { type DropTargetDropEvent, - type DropTargetOptions, Skeleton, useDropTarget, } from '@affine/component'; @@ -9,19 +8,18 @@ import { useI18n } from '@affine/i18n'; import { FavoriteIcon } from '@blocksuite/icons/rc'; import { ExplorerEmptySection } from '../../layouts/empty-section'; -import { DropEffect, type ExplorerTreeNodeDropEffect } from '../../tree'; +import { DropEffect } from '../../tree'; +import { favoriteRootCanDrop, favoriteRootDropEffect } from './dnd'; -export const RootEmpty = ({ - onDrop, - canDrop, - isLoading, - dropEffect, -}: { +interface RootEmptyProps { onDrop?: (data: DropTargetDropEvent) => void; - canDrop?: DropTargetOptions['canDrop']; - dropEffect?: ExplorerTreeNodeDropEffect; isLoading?: boolean; -}) => { +} + +const RootEmptyLoading = () => { + return ; +}; +const RootEmptyReady = ({ onDrop }: Omit) => { const t = useI18n(); const { dropTargetRef, draggedOverDraggable, draggedOverPosition } = @@ -31,15 +29,11 @@ export const RootEmpty = ({ at: 'explorer:favorite:root', }, onDrop: onDrop, - canDrop: canDrop, + canDrop: favoriteRootCanDrop, }), - [onDrop, canDrop] + [onDrop] ); - if (isLoading) { - return ; - } - return ( - {dropEffect && draggedOverDraggable && ( + {draggedOverDraggable && ( ); }; + +export const RootEmpty = ({ isLoading, ...props }: RootEmptyProps) => { + return isLoading ? : ; +}; diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/index.tsx b/packages/frontend/core/src/modules/explorer/views/sections/favorites/index.tsx index 76a87772a5..eeea7ef3f9 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/favorites/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/favorites/index.tsx @@ -1,13 +1,11 @@ import { type DropTargetDropEvent, - type DropTargetOptions, IconButton, useDropTarget, } from '@affine/component'; import { track } from '@affine/core/mixpanel'; import { DropEffect, - type ExplorerTreeNodeDropEffect, ExplorerTreeRoot, } from '@affine/core/modules/explorer/views/tree'; import type { FavoriteSupportType } from '@affine/core/modules/favorite'; @@ -20,7 +18,7 @@ import type { AffineDNDData } from '@affine/core/types/dnd'; import { useI18n } from '@affine/i18n'; import { PlusIcon } from '@blocksuite/icons/rc'; import { DocsService, useLiveData, useServices } from '@toeverything/infra'; -import { type MouseEventHandler, useCallback, useMemo } from 'react'; +import { type MouseEventHandler, useCallback } from 'react'; import { ExplorerService } from '../../../services/explorer'; import { CollapsibleSection } from '../../layouts/collapsible-section'; @@ -28,8 +26,13 @@ import { ExplorerCollectionNode } from '../../nodes/collection'; import { ExplorerDocNode } from '../../nodes/doc'; import { ExplorerFolderNode } from '../../nodes/folder'; import { ExplorerTagNode } from '../../nodes/tag'; +import { + favoriteChildrenCanDrop, + favoriteChildrenDropEffect, + favoriteRootCanDrop, + favoriteRootDropEffect, +} from './dnd'; import { RootEmpty } from './empty'; -import * as styles from './styles.css'; export const ExplorerFavorites = () => { const { favoriteService, docsService, workbenchService, explorerService } = @@ -69,25 +72,6 @@ export const ExplorerFavorites = () => { [explorerSection, favoriteService.favoriteList] ); - const handleDropEffect = useCallback(data => { - if ( - data.source.data.entity?.type && - isFavoriteSupportType(data.source.data.entity.type) - ) { - return 'link'; - } - return; - }, []); - - const handleCanDrop = useMemo['canDrop']>( - () => data => { - return data.source.data.entity?.type - ? isFavoriteSupportType(data.source.data.entity.type) - : false; - }, - [] - ); - const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback( e => { const newDoc = docsService.createDoc(); @@ -163,40 +147,6 @@ export const ExplorerFavorites = () => { [favoriteService] ); - const handleChildrenDropEffect = useCallback( - data => { - if ( - data.treeInstruction?.type === 'reorder-above' || - data.treeInstruction?.type === 'reorder-below' - ) { - if ( - data.source.data.from?.at === 'explorer:favorite:list' && - data.source.data.entity?.type && - isFavoriteSupportType(data.source.data.entity.type) - ) { - return 'move'; - } else if ( - data.source.data.entity?.type && - isFavoriteSupportType(data.source.data.entity.type) - ) { - return 'link'; - } - } - return; // not supported - }, - [] - ); - - const handleChildrenCanDrop = useMemo< - DropTargetOptions['canDrop'] - >( - () => args => - args.source.data.entity?.type - ? isFavoriteSupportType(args.source.data.entity.type) - : false, - [] - ); - const { dropTargetRef, draggedOverDraggable, draggedOverPosition } = useDropTarget( () => ({ @@ -204,9 +154,9 @@ export const ExplorerFavorites = () => { at: 'explorer:favorite:root', }, onDrop: handleDrop, - canDrop: handleCanDrop, + canDrop: favoriteRootCanDrop, }), - [handleCanDrop, handleDrop] + [handleDrop] ); return ( @@ -216,7 +166,6 @@ export const ExplorerFavorites = () => { headerRef={dropTargetRef} testId="explorer-favorites" headerTestId="explorer-favorite-category-divider" - headerClassName={styles.draggedOverHighlight} actions={ <> { {draggedOverDraggable && ( { } > - } + placeholder={} > {favorites.map(favorite => ( ))} @@ -276,14 +213,11 @@ const childLocation = { const ExplorerFavoriteNode = ({ favorite, onDrop, - canDrop, - dropEffect, }: { favorite: { id: string; type: FavoriteSupportType; }; - canDrop?: DropTargetOptions['canDrop']; onDrop: ( favorite: { id: string; @@ -291,7 +225,6 @@ const ExplorerFavoriteNode = ({ }, data: DropTargetDropEvent ) => void; - dropEffect: ExplorerTreeNodeDropEffect; }) => { const handleOnChildrenDrop = useCallback( (data: DropTargetDropEvent) => { @@ -305,8 +238,8 @@ const ExplorerFavoriteNode = ({ docId={favorite.id} location={childLocation} onDrop={handleOnChildrenDrop} - dropEffect={dropEffect} - canDrop={canDrop} + dropEffect={favoriteChildrenDropEffect} + canDrop={favoriteChildrenCanDrop} /> ) : favorite.type === 'tag' ? ( ) : favorite.type === 'folder' ? ( ) : ( ); }; diff --git a/packages/frontend/core/src/modules/explorer/views/sections/favorites/styles.css.ts b/packages/frontend/core/src/modules/explorer/views/sections/favorites/styles.css.ts deleted file mode 100644 index 201d5befac..0000000000 --- a/packages/frontend/core/src/modules/explorer/views/sections/favorites/styles.css.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { cssVar } from '@toeverything/theme'; -import { style } from '@vanilla-extract/css'; - -export const draggedOverHighlight = style({ - position: 'relative', - selectors: { - '&[data-dragged-over="true"]': { - background: cssVar('--affine-hover-color'), - borderRadius: '4px', - }, - }, -}); diff --git a/packages/frontend/core/src/modules/explorer/views/sections/organize/dnd.ts b/packages/frontend/core/src/modules/explorer/views/sections/organize/dnd.ts new file mode 100644 index 0000000000..e713ac1a25 --- /dev/null +++ b/packages/frontend/core/src/modules/explorer/views/sections/organize/dnd.ts @@ -0,0 +1,36 @@ +import type { DropTargetOptions } from '@affine/component'; +import { isOrganizeSupportType } from '@affine/core/modules/organize/constants'; +import type { AffineDNDData } from '@affine/core/types/dnd'; + +import type { ExplorerTreeNodeDropEffect } from '../../tree'; + +export const organizeChildrenDropEffect: ExplorerTreeNodeDropEffect = data => { + if ( + data.treeInstruction?.type === 'reorder-above' || + data.treeInstruction?.type === 'reorder-below' + ) { + if (data.source.data.entity?.type === 'folder') { + return 'move'; + } + } else { + return; // not supported + } + return; +}; + +export const organizeEmptyDropEffect: ExplorerTreeNodeDropEffect = data => { + const sourceType = data.source.data.entity?.type; + if (sourceType && isOrganizeSupportType(sourceType)) { + return 'link'; + } + return; +}; + +/** + * Check whether the data can be dropped on the empty state of the organize section + */ +export const organizeEmptyRootCanDrop: DropTargetOptions['canDrop'] = + data => { + const type = data.source.data.entity?.type; + return !!type && isOrganizeSupportType(type); + }; diff --git a/packages/frontend/core/src/modules/explorer/views/sections/organize/empty.tsx b/packages/frontend/core/src/modules/explorer/views/sections/organize/empty.tsx index cc877eff7b..9db448fa30 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/organize/empty.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/organize/empty.tsx @@ -1,31 +1,66 @@ -import { Skeleton } from '@affine/component'; +import { + AnimatedFolderIcon, + type DropTargetDropEvent, + Skeleton, + useDropTarget, +} from '@affine/component'; +import type { AffineDNDData } from '@affine/core/types/dnd'; import { useI18n } from '@affine/i18n'; -import { FolderIcon } from '@blocksuite/icons/rc'; import { ExplorerEmptySection } from '../../layouts/empty-section'; +import { DropEffect } from '../../tree'; +import { organizeEmptyDropEffect, organizeEmptyRootCanDrop } from './dnd'; -export const RootEmpty = ({ - onClickCreate, - isLoading, -}: { +interface RootEmptyProps { onClickCreate?: () => void; isLoading?: boolean; -}) => { + onDrop?: (data: DropTargetDropEvent) => void; +} + +export const RootEmptyLoading = () => { + return ; +}; + +export const RootEmptyReady = ({ + onClickCreate, + onDrop, +}: Omit) => { const t = useI18n(); - if (isLoading) { - return ; - } + const { dropTargetRef, draggedOverDraggable, draggedOverPosition } = + useDropTarget( + () => ({ + data: { at: 'explorer:organize:root' }, + onDrop, + canDrop: organizeEmptyRootCanDrop, + }), + [onDrop] + ); return ( } message={t['com.affine.rootAppSidebar.organize.empty']()} messageTestId="slider-bar-organize-empty-message" actionText={t[ 'com.affine.rootAppSidebar.organize.empty.new-folders-button' ]()} onActionClick={onClickCreate} - /> + > + {draggedOverDraggable && ( + + )} + ); }; + +export const RootEmpty = ({ isLoading, ...props }: RootEmptyProps) => { + return isLoading ? : ; +}; diff --git a/packages/frontend/core/src/modules/explorer/views/sections/organize/index.tsx b/packages/frontend/core/src/modules/explorer/views/sections/organize/index.tsx index 9045e984a8..16de2ae154 100644 --- a/packages/frontend/core/src/modules/explorer/views/sections/organize/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/sections/organize/index.tsx @@ -5,10 +5,7 @@ import { toast, } from '@affine/component'; import { track } from '@affine/core/mixpanel'; -import { - type ExplorerTreeNodeDropEffect, - ExplorerTreeRoot, -} from '@affine/core/modules/explorer/views/tree'; +import { ExplorerTreeRoot } from '@affine/core/modules/explorer/views/tree'; import { type FolderNode, OrganizeService, @@ -22,8 +19,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { ExplorerService } from '../../../services/explorer'; import { CollapsibleSection } from '../../layouts/collapsible-section'; import { ExplorerFolderNode } from '../../nodes/folder'; +import { organizeChildrenDropEffect } from './dnd'; import { RootEmpty } from './empty'; -import * as styles from './styles.css'; export const ExplorerOrganize = () => { const { organizeService, explorerService } = useServices({ @@ -36,10 +33,11 @@ export const ExplorerOrganize = () => { const t = useI18n(); - const rootFolder = organizeService.folderTree.rootFolder; + const folderTree = organizeService.folderTree; + const rootFolder = folderTree.rootFolder; const folders = useLiveData(rootFolder.sortedChildren$); - const isLoading = useLiveData(organizeService.folderTree.isLoading$); + const isLoading = useLiveData(folderTree.isLoading$); const handleCreateFolder = useCallback(() => { const newFolderId = rootFolder.createFolder( @@ -49,6 +47,7 @@ export const ExplorerOrganize = () => { track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' }); setNewFolderId(newFolderId); explorerSection.setCollapsed(false); + return newFolderId; }, [explorerSection, rootFolder]); const handleOnChildrenDrop = useCallback( @@ -78,21 +77,22 @@ export const ExplorerOrganize = () => { [rootFolder, t] ); - const handleChildrenDropEffect = useCallback( - data => { - if ( - data.treeInstruction?.type === 'reorder-above' || - data.treeInstruction?.type === 'reorder-below' - ) { - if (data.source.data.entity?.type === 'folder') { - return 'move'; - } - } else { - return; // not supported - } - return; + const createFolderAndDrop = useCallback( + (data: DropTargetDropEvent) => { + const newFolderId = handleCreateFolder(); + setNewFolderId(null); + const newFolder$ = folderTree.folderNode$(newFolderId); + + const entity = data.source.data.entity; + if (!entity) return; + const { type, id } = entity; + if (type === 'folder') return; + + const folder = newFolder$.value; + if (!folder) return; + folder.createLink(type, id, folder.indexAt('after')); }, - [] + [folderTree, handleCreateFolder] ); const handleChildrenCanDrop = useMemo< @@ -106,7 +106,6 @@ export const ExplorerOrganize = () => { return ( { > + } > {folders.map(child => ( @@ -132,7 +135,7 @@ export const ExplorerOrganize = () => { nodeId={child.id as string} defaultRenaming={child.id === newFolderId} onDrop={handleOnChildrenDrop} - dropEffect={handleChildrenDropEffect} + dropEffect={organizeChildrenDropEffect} canDrop={handleChildrenCanDrop} location={{ at: 'explorer:organize:folder-node', diff --git a/packages/frontend/core/src/modules/explorer/views/sections/organize/styles.css.ts b/packages/frontend/core/src/modules/explorer/views/sections/organize/styles.css.ts deleted file mode 100644 index 0fe8f49f21..0000000000 --- a/packages/frontend/core/src/modules/explorer/views/sections/organize/styles.css.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { cssVar } from '@toeverything/theme'; -import { style } from '@vanilla-extract/css'; - -export const draggedOverHighlight = style({ - selectors: { - '&[data-dragged-over="true"]': { - background: cssVar('--affine-hover-color'), - borderRadius: '4px', - }, - }, -}); diff --git a/packages/frontend/core/src/modules/explorer/views/tree/drop-effect.css.ts b/packages/frontend/core/src/modules/explorer/views/tree/drop-effect.css.ts index f54abe9385..f23cdab040 100644 --- a/packages/frontend/core/src/modules/explorer/views/tree/drop-effect.css.ts +++ b/packages/frontend/core/src/modules/explorer/views/tree/drop-effect.css.ts @@ -3,10 +3,9 @@ import { style } from '@vanilla-extract/css'; export const dropEffect = style({ zIndex: 99999, - position: 'absolute', - left: '0px', - top: '-34px', - opacity: 0.9, + position: 'fixed', + left: '10px', + top: '-20px', background: cssVar('--affine-background-primary-color'), boxShadow: cssVar('--affine-toolbar-shadow'), padding: '0px 4px', diff --git a/packages/frontend/core/src/modules/explorer/views/tree/drop-effect.tsx b/packages/frontend/core/src/modules/explorer/views/tree/drop-effect.tsx index f8b4ed8b96..51136f23dd 100644 --- a/packages/frontend/core/src/modules/explorer/views/tree/drop-effect.tsx +++ b/packages/frontend/core/src/modules/explorer/views/tree/drop-effect.tsx @@ -1,5 +1,7 @@ +import type { useDropTarget } from '@affine/component'; import { useI18n } from '@affine/i18n'; import { CopyIcon, LinkIcon, MoveToIcon } from '@blocksuite/icons/rc'; +import { createPortal } from 'react-dom'; import * as styles from './drop-effect.css'; @@ -8,15 +10,15 @@ export const DropEffect = ({ position, }: { dropEffect?: 'copy' | 'move' | 'link' | undefined; - position: { x: number; y: number }; + position: ReturnType['draggedOverPosition']; }) => { const t = useI18n(); if (dropEffect === undefined) return null; - return ( + return createPortal(
{dropEffect === 'copy' ? ( @@ -31,6 +33,7 @@ export const DropEffect = ({ : dropEffect === 'move' ? t['com.affine.rootAppSidebar.explorer.drop-effect.move']() : t['com.affine.rootAppSidebar.explorer.drop-effect.link']()} -
+
, + document.body ); }; diff --git a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx index 7102be1456..0b33cdeb98 100644 --- a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx +++ b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx @@ -47,6 +47,12 @@ export type ExplorerTreeNodeDropEffectData = { export type ExplorerTreeNodeDropEffect = ( data: ExplorerTreeNodeDropEffectData ) => 'copy' | 'move' | 'link' | undefined; +export type ExplorerTreeNodeIcon = React.ComponentType<{ + className?: string; + draggedOver?: boolean; + treeInstruction?: DropTargetTreeInstruction | null; + collapsed?: boolean; +}>; export const ExplorerTreeNode = ({ children, @@ -74,11 +80,7 @@ export const ExplorerTreeNode = ({ ...otherProps }: { name?: string; - icon?: React.ComponentType<{ - className?: string; - draggedOver?: boolean; - treeInstruction?: DropTargetTreeInstruction | null; - }>; + icon?: ExplorerTreeNodeIcon; children?: React.ReactNode; active?: boolean; reorderable?: boolean; @@ -311,6 +313,7 @@ export const ExplorerTreeNode = ({ className={styles.icon} draggedOver={draggedOver && !isSelfDraggedOver} treeInstruction={treeInstruction} + collapsed={collapsed} /> )} @@ -398,10 +401,7 @@ export const ExplorerTreeNode = ({ source: draggedOverDraggable, treeInstruction: treeInstruction, })} - position={{ - x: draggedOverPosition.relativeX, - y: draggedOverPosition.relativeY, - }} + position={draggedOverPosition} /> )} diff --git a/packages/frontend/core/src/modules/organize/constants.ts b/packages/frontend/core/src/modules/organize/constants.ts new file mode 100644 index 0000000000..2d27108907 --- /dev/null +++ b/packages/frontend/core/src/modules/organize/constants.ts @@ -0,0 +1,12 @@ +export const OrganizeSupportType = [ + 'folder', + 'doc', + 'collection', + 'tag', +] as const; +export type OrganizeSupportType = 'folder' | 'doc' | 'collection' | 'tag'; + +export const isOrganizeSupportType = ( + type: string +): type is OrganizeSupportType => + OrganizeSupportType.includes(type as OrganizeSupportType);