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
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"v": "5.12.1",
|
"v": "5.12.1",
|
||||||
"fr": 60,
|
"fr": 60,
|
||||||
"ip": 0,
|
"ip": 0,
|
||||||
"op": 89,
|
"op": 6,
|
||||||
"w": 240,
|
"w": 240,
|
||||||
"h": 240,
|
"h": 240,
|
||||||
"nm": "folder",
|
"nm": "folder",
|
||||||
|
|||||||
@@ -7,25 +7,31 @@ import animationData from './folder-icon.json';
|
|||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export interface FolderIconProps {
|
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;
|
className?: string;
|
||||||
|
speed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// animated folder icon that has two states: closed and opened
|
// 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);
|
const lottieRef: LottieRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lottieRef.current) {
|
if (lottieRef.current) {
|
||||||
const lottie = lottieRef.current;
|
const lottie = lottieRef.current;
|
||||||
if (closed) {
|
lottie.setSpeed(speed);
|
||||||
|
if (open) {
|
||||||
lottie.setDirection(1);
|
lottie.setDirection(1);
|
||||||
} else {
|
} else {
|
||||||
lottie.setDirection(-1);
|
lottie.setDirection(-1);
|
||||||
}
|
}
|
||||||
lottie.play();
|
lottie.play();
|
||||||
}
|
}
|
||||||
}, [closed]);
|
}, [open, speed]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Lottie
|
<Lottie
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { cssVar } from '@toeverything/theme';
|
|||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const actions = style({
|
const baseAction = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
opacity: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const root = style({
|
export const root = style({
|
||||||
fontSize: cssVar('fontXs'),
|
fontSize: cssVar('fontXs'),
|
||||||
height: 20,
|
height: 20,
|
||||||
@@ -23,11 +25,22 @@ export const root = style({
|
|||||||
[`&[data-collapsible="true"]:hover`]: {
|
[`&[data-collapsible="true"]:hover`]: {
|
||||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||||
},
|
},
|
||||||
[`&[data-collapsible="true"]:hover:has(${actions}:hover)`]: {
|
[`&[data-collapsible="true"]:hover:has(${baseAction}:hover)`]: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actions = style([
|
||||||
|
baseAction,
|
||||||
|
{
|
||||||
|
selectors: {
|
||||||
|
[`${root}:hover &`]: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
export const label = style({
|
export const label = style({
|
||||||
color: cssVarV2('text/tertiary'),
|
color: cssVarV2('text/tertiary'),
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const root = style({});
|
export const root = style({});
|
||||||
export const content = style({
|
export const content = style({
|
||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const header = style({
|
||||||
|
selectors: {
|
||||||
|
'&[data-dragged-over="true"]': {
|
||||||
|
background: cssVarV2('layer/background/hoverOverlay'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
|
|
||||||
import { ExplorerService } from '../../services/explorer';
|
import { ExplorerService } from '../../services/explorer';
|
||||||
import type { CollapsibleSectionName } from '../../types';
|
import type { CollapsibleSectionName } from '../../types';
|
||||||
import { content, root } from './collapsible-section.css';
|
import { content, header, root } from './collapsible-section.css';
|
||||||
|
|
||||||
interface CollapsibleSectionProps extends PropsWithChildren {
|
interface CollapsibleSectionProps extends PropsWithChildren {
|
||||||
name: CollapsibleSectionName;
|
name: CollapsibleSectionName;
|
||||||
@@ -67,7 +67,7 @@ export const CollapsibleSection = ({
|
|||||||
setCollapsed={setCollapsed}
|
setCollapsed={setCollapsed}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
ref={headerRef}
|
ref={headerRef}
|
||||||
className={headerClassName}
|
className={clsx(header, headerClassName)}
|
||||||
>
|
>
|
||||||
{actions}
|
{actions}
|
||||||
</CategoryDivider>
|
</CategoryDivider>
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ export const content = style({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
padding: '12px 0px',
|
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({
|
export const iconWrapper = style({
|
||||||
width: 36,
|
width: 36,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Button } from '@affine/component';
|
import { Button } from '@affine/component';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
|
cloneElement,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
type HTMLAttributes,
|
type HTMLAttributes,
|
||||||
|
type ReactElement,
|
||||||
type Ref,
|
type Ref,
|
||||||
type SVGProps,
|
type SVGProps,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
@@ -10,7 +12,7 @@ import {
|
|||||||
import * as styles from './empty-section.css';
|
import * as styles from './empty-section.css';
|
||||||
|
|
||||||
interface ExplorerEmptySectionProps extends HTMLAttributes<HTMLDivElement> {
|
interface ExplorerEmptySectionProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
icon: (props: SVGProps<SVGSVGElement>) => JSX.Element;
|
icon: ((props: SVGProps<SVGSVGElement>) => JSX.Element) | ReactElement;
|
||||||
message: string;
|
message: string;
|
||||||
messageTestId?: string;
|
messageTestId?: string;
|
||||||
actionText?: string;
|
actionText?: string;
|
||||||
@@ -30,11 +32,16 @@ export const ExplorerEmptySection = forwardRef(function ExplorerEmptySection(
|
|||||||
}: ExplorerEmptySectionProps,
|
}: ExplorerEmptySectionProps,
|
||||||
ref: Ref<HTMLDivElement>
|
ref: Ref<HTMLDivElement>
|
||||||
) {
|
) {
|
||||||
|
const icon =
|
||||||
|
typeof Icon === 'function' ? (
|
||||||
|
<Icon className={styles.icon} />
|
||||||
|
) : (
|
||||||
|
cloneElement(Icon, { className: styles.icon })
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.content, className)} ref={ref} {...attrs}>
|
<div className={clsx(styles.content, className)} ref={ref} {...attrs}>
|
||||||
<div className={styles.iconWrapper}>
|
<div className={styles.iconWrapper}>{icon}</div>
|
||||||
<Icon className={styles.icon} />
|
|
||||||
</div>
|
|
||||||
<div data-testid={messageTestId} className={styles.message}>
|
<div data-testid={messageTestId} className={styles.message}>
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,11 +31,23 @@ import {
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ExplorerTreeNode, type ExplorerTreeNodeDropEffect } from '../../tree';
|
import { ExplorerTreeNode, type ExplorerTreeNodeDropEffect } from '../../tree';
|
||||||
|
import type { ExplorerTreeNodeIcon } from '../../tree/node';
|
||||||
import { ExplorerDocNode } from '../doc';
|
import { ExplorerDocNode } from '../doc';
|
||||||
import type { GenericExplorerNode } from '../types';
|
import type { GenericExplorerNode } from '../types';
|
||||||
import { Empty } from './empty';
|
import { Empty } from './empty';
|
||||||
import { useExplorerCollectionNodeOperations } from './operations';
|
import { useExplorerCollectionNodeOperations } from './operations';
|
||||||
|
|
||||||
|
const CollectionIcon: ExplorerTreeNodeIcon = ({
|
||||||
|
className,
|
||||||
|
draggedOver,
|
||||||
|
treeInstruction,
|
||||||
|
}) => (
|
||||||
|
<AnimatedCollectionsIcon
|
||||||
|
className={className}
|
||||||
|
closed={!!draggedOver && treeInstruction?.type === 'make-child'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
export const ExplorerCollectionNode = ({
|
export const ExplorerCollectionNode = ({
|
||||||
collectionId,
|
collectionId,
|
||||||
onDrop,
|
onDrop,
|
||||||
@@ -202,12 +214,7 @@ export const ExplorerCollectionNode = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExplorerTreeNode
|
<ExplorerTreeNode
|
||||||
icon={({ draggedOver, className, treeInstruction }) => (
|
icon={CollectionIcon}
|
||||||
<AnimatedCollectionsIcon
|
|
||||||
className={className}
|
|
||||||
closed={!!draggedOver && treeInstruction?.type === 'make-child'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
name={collection.name || t['Untitled']()}
|
name={collection.name || t['Untitled']()}
|
||||||
dndData={dndData}
|
dndData={dndData}
|
||||||
onDrop={handleDropOnCollection}
|
onDrop={handleDropOnCollection}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { difference } from 'lodash-es';
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ExplorerTreeNode, type ExplorerTreeNodeDropEffect } from '../../tree';
|
import { ExplorerTreeNode, type ExplorerTreeNodeDropEffect } from '../../tree';
|
||||||
|
import type { ExplorerTreeNodeIcon } from '../../tree/node';
|
||||||
import type { NodeOperation } from '../../tree/types';
|
import type { NodeOperation } from '../../tree/types';
|
||||||
import { ExplorerCollectionNode } from '../collection';
|
import { ExplorerCollectionNode } from '../collection';
|
||||||
import { ExplorerDocNode } from '../doc';
|
import { ExplorerDocNode } from '../doc';
|
||||||
@@ -150,6 +151,21 @@ export const ExplorerFolderNode = ({
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Define outside the `ExplorerFolderNodeFolder` to avoid re-render(the close animation won't play)
|
||||||
|
const ExplorerFolderIcon: ExplorerTreeNodeIcon = ({
|
||||||
|
collapsed,
|
||||||
|
className,
|
||||||
|
draggedOver,
|
||||||
|
treeInstruction,
|
||||||
|
}) => (
|
||||||
|
<AnimatedFolderIcon
|
||||||
|
className={className}
|
||||||
|
open={
|
||||||
|
!collapsed || (!!draggedOver && treeInstruction?.type === 'make-child')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
export const ExplorerFolderNodeFolder = ({
|
export const ExplorerFolderNodeFolder = ({
|
||||||
node,
|
node,
|
||||||
onDrop,
|
onDrop,
|
||||||
@@ -758,12 +774,7 @@ export const ExplorerFolderNodeFolder = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ExplorerTreeNode
|
<ExplorerTreeNode
|
||||||
icon={({ draggedOver, className, treeInstruction }) => (
|
icon={ExplorerFolderIcon}
|
||||||
<AnimatedFolderIcon
|
|
||||||
className={className}
|
|
||||||
closed={!!draggedOver && treeInstruction?.type === 'make-child'}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
name={name}
|
name={name}
|
||||||
dndData={dndData}
|
dndData={dndData}
|
||||||
onDrop={handleDropOnFolder}
|
onDrop={handleDropOnFolder}
|
||||||
|
|||||||
@@ -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<AffineDNDData>['canDrop'] =
|
||||||
|
data => {
|
||||||
|
return data.source.data.entity?.type
|
||||||
|
? isFavoriteSupportType(data.source.data.entity.type)
|
||||||
|
: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const favoriteChildrenCanDrop: DropTargetOptions<AffineDNDData>['canDrop'] =
|
||||||
|
// Same as favoriteRootCanDrop
|
||||||
|
data => favoriteRootCanDrop(data);
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
type DropTargetDropEvent,
|
type DropTargetDropEvent,
|
||||||
type DropTargetOptions,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
useDropTarget,
|
useDropTarget,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
@@ -9,19 +8,18 @@ import { useI18n } from '@affine/i18n';
|
|||||||
import { FavoriteIcon } from '@blocksuite/icons/rc';
|
import { FavoriteIcon } from '@blocksuite/icons/rc';
|
||||||
|
|
||||||
import { ExplorerEmptySection } from '../../layouts/empty-section';
|
import { ExplorerEmptySection } from '../../layouts/empty-section';
|
||||||
import { DropEffect, type ExplorerTreeNodeDropEffect } from '../../tree';
|
import { DropEffect } from '../../tree';
|
||||||
|
import { favoriteRootCanDrop, favoriteRootDropEffect } from './dnd';
|
||||||
|
|
||||||
export const RootEmpty = ({
|
interface RootEmptyProps {
|
||||||
onDrop,
|
|
||||||
canDrop,
|
|
||||||
isLoading,
|
|
||||||
dropEffect,
|
|
||||||
}: {
|
|
||||||
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
||||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
|
||||||
dropEffect?: ExplorerTreeNodeDropEffect;
|
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}) => {
|
}
|
||||||
|
|
||||||
|
const RootEmptyLoading = () => {
|
||||||
|
return <Skeleton />;
|
||||||
|
};
|
||||||
|
const RootEmptyReady = ({ onDrop }: Omit<RootEmptyProps, 'isLoading'>) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
||||||
@@ -31,15 +29,11 @@ export const RootEmpty = ({
|
|||||||
at: 'explorer:favorite:root',
|
at: 'explorer:favorite:root',
|
||||||
},
|
},
|
||||||
onDrop: onDrop,
|
onDrop: onDrop,
|
||||||
canDrop: canDrop,
|
canDrop: favoriteRootCanDrop,
|
||||||
}),
|
}),
|
||||||
[onDrop, canDrop]
|
[onDrop]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Skeleton />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExplorerEmptySection
|
<ExplorerEmptySection
|
||||||
ref={dropTargetRef}
|
ref={dropTargetRef}
|
||||||
@@ -47,13 +41,10 @@ export const RootEmpty = ({
|
|||||||
message={t['com.affine.rootAppSidebar.favorites.empty']()}
|
message={t['com.affine.rootAppSidebar.favorites.empty']()}
|
||||||
messageTestId="slider-bar-favorites-empty-message"
|
messageTestId="slider-bar-favorites-empty-message"
|
||||||
>
|
>
|
||||||
{dropEffect && draggedOverDraggable && (
|
{draggedOverDraggable && (
|
||||||
<DropEffect
|
<DropEffect
|
||||||
position={{
|
position={draggedOverPosition}
|
||||||
x: draggedOverPosition.relativeX,
|
dropEffect={favoriteRootDropEffect({
|
||||||
y: draggedOverPosition.relativeY,
|
|
||||||
}}
|
|
||||||
dropEffect={dropEffect({
|
|
||||||
source: draggedOverDraggable,
|
source: draggedOverDraggable,
|
||||||
treeInstruction: null,
|
treeInstruction: null,
|
||||||
})}
|
})}
|
||||||
@@ -62,3 +53,7 @@ export const RootEmpty = ({
|
|||||||
</ExplorerEmptySection>
|
</ExplorerEmptySection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RootEmpty = ({ isLoading, ...props }: RootEmptyProps) => {
|
||||||
|
return isLoading ? <RootEmptyLoading /> : <RootEmptyReady {...props} />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
type DropTargetDropEvent,
|
type DropTargetDropEvent,
|
||||||
type DropTargetOptions,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
useDropTarget,
|
useDropTarget,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import {
|
import {
|
||||||
DropEffect,
|
DropEffect,
|
||||||
type ExplorerTreeNodeDropEffect,
|
|
||||||
ExplorerTreeRoot,
|
ExplorerTreeRoot,
|
||||||
} from '@affine/core/modules/explorer/views/tree';
|
} from '@affine/core/modules/explorer/views/tree';
|
||||||
import type { FavoriteSupportType } from '@affine/core/modules/favorite';
|
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 { useI18n } from '@affine/i18n';
|
||||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import { DocsService, useLiveData, useServices } from '@toeverything/infra';
|
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 { ExplorerService } from '../../../services/explorer';
|
||||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||||
@@ -28,8 +26,13 @@ import { ExplorerCollectionNode } from '../../nodes/collection';
|
|||||||
import { ExplorerDocNode } from '../../nodes/doc';
|
import { ExplorerDocNode } from '../../nodes/doc';
|
||||||
import { ExplorerFolderNode } from '../../nodes/folder';
|
import { ExplorerFolderNode } from '../../nodes/folder';
|
||||||
import { ExplorerTagNode } from '../../nodes/tag';
|
import { ExplorerTagNode } from '../../nodes/tag';
|
||||||
|
import {
|
||||||
|
favoriteChildrenCanDrop,
|
||||||
|
favoriteChildrenDropEffect,
|
||||||
|
favoriteRootCanDrop,
|
||||||
|
favoriteRootDropEffect,
|
||||||
|
} from './dnd';
|
||||||
import { RootEmpty } from './empty';
|
import { RootEmpty } from './empty';
|
||||||
import * as styles from './styles.css';
|
|
||||||
|
|
||||||
export const ExplorerFavorites = () => {
|
export const ExplorerFavorites = () => {
|
||||||
const { favoriteService, docsService, workbenchService, explorerService } =
|
const { favoriteService, docsService, workbenchService, explorerService } =
|
||||||
@@ -69,25 +72,6 @@ export const ExplorerFavorites = () => {
|
|||||||
[explorerSection, favoriteService.favoriteList]
|
[explorerSection, favoriteService.favoriteList]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDropEffect = useCallback<ExplorerTreeNodeDropEffect>(data => {
|
|
||||||
if (
|
|
||||||
data.source.data.entity?.type &&
|
|
||||||
isFavoriteSupportType(data.source.data.entity.type)
|
|
||||||
) {
|
|
||||||
return 'link';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
|
||||||
() => data => {
|
|
||||||
return data.source.data.entity?.type
|
|
||||||
? isFavoriteSupportType(data.source.data.entity.type)
|
|
||||||
: false;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback(
|
const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback(
|
||||||
e => {
|
e => {
|
||||||
const newDoc = docsService.createDoc();
|
const newDoc = docsService.createDoc();
|
||||||
@@ -163,40 +147,6 @@ export const ExplorerFavorites = () => {
|
|||||||
[favoriteService]
|
[favoriteService]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChildrenDropEffect = useCallback<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
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChildrenCanDrop = useMemo<
|
|
||||||
DropTargetOptions<AffineDNDData>['canDrop']
|
|
||||||
>(
|
|
||||||
() => args =>
|
|
||||||
args.source.data.entity?.type
|
|
||||||
? isFavoriteSupportType(args.source.data.entity.type)
|
|
||||||
: false,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
||||||
useDropTarget<AffineDNDData>(
|
useDropTarget<AffineDNDData>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -204,9 +154,9 @@ export const ExplorerFavorites = () => {
|
|||||||
at: 'explorer:favorite:root',
|
at: 'explorer:favorite:root',
|
||||||
},
|
},
|
||||||
onDrop: handleDrop,
|
onDrop: handleDrop,
|
||||||
canDrop: handleCanDrop,
|
canDrop: favoriteRootCanDrop,
|
||||||
}),
|
}),
|
||||||
[handleCanDrop, handleDrop]
|
[handleDrop]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -216,7 +166,6 @@ export const ExplorerFavorites = () => {
|
|||||||
headerRef={dropTargetRef}
|
headerRef={dropTargetRef}
|
||||||
testId="explorer-favorites"
|
testId="explorer-favorites"
|
||||||
headerTestId="explorer-favorite-category-divider"
|
headerTestId="explorer-favorite-category-divider"
|
||||||
headerClassName={styles.draggedOverHighlight}
|
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -233,11 +182,8 @@ export const ExplorerFavorites = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
{draggedOverDraggable && (
|
{draggedOverDraggable && (
|
||||||
<DropEffect
|
<DropEffect
|
||||||
position={{
|
position={draggedOverPosition}
|
||||||
x: draggedOverPosition.relativeX,
|
dropEffect={favoriteRootDropEffect({
|
||||||
y: draggedOverPosition.relativeY,
|
|
||||||
}}
|
|
||||||
dropEffect={handleDropEffect({
|
|
||||||
source: draggedOverDraggable,
|
source: draggedOverDraggable,
|
||||||
treeInstruction: null,
|
treeInstruction: null,
|
||||||
})}
|
})}
|
||||||
@@ -247,22 +193,13 @@ export const ExplorerFavorites = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ExplorerTreeRoot
|
<ExplorerTreeRoot
|
||||||
placeholder={
|
placeholder={<RootEmpty onDrop={handleDrop} isLoading={isLoading} />}
|
||||||
<RootEmpty
|
|
||||||
onDrop={handleDrop}
|
|
||||||
canDrop={handleCanDrop}
|
|
||||||
dropEffect={handleDropEffect}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{favorites.map(favorite => (
|
{favorites.map(favorite => (
|
||||||
<ExplorerFavoriteNode
|
<ExplorerFavoriteNode
|
||||||
key={favorite.id}
|
key={favorite.id}
|
||||||
favorite={favorite}
|
favorite={favorite}
|
||||||
onDrop={handleOnChildrenDrop}
|
onDrop={handleOnChildrenDrop}
|
||||||
dropEffect={handleChildrenDropEffect}
|
|
||||||
canDrop={handleChildrenCanDrop}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ExplorerTreeRoot>
|
</ExplorerTreeRoot>
|
||||||
@@ -276,14 +213,11 @@ const childLocation = {
|
|||||||
const ExplorerFavoriteNode = ({
|
const ExplorerFavoriteNode = ({
|
||||||
favorite,
|
favorite,
|
||||||
onDrop,
|
onDrop,
|
||||||
canDrop,
|
|
||||||
dropEffect,
|
|
||||||
}: {
|
}: {
|
||||||
favorite: {
|
favorite: {
|
||||||
id: string;
|
id: string;
|
||||||
type: FavoriteSupportType;
|
type: FavoriteSupportType;
|
||||||
};
|
};
|
||||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
|
||||||
onDrop: (
|
onDrop: (
|
||||||
favorite: {
|
favorite: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -291,7 +225,6 @@ const ExplorerFavoriteNode = ({
|
|||||||
},
|
},
|
||||||
data: DropTargetDropEvent<AffineDNDData>
|
data: DropTargetDropEvent<AffineDNDData>
|
||||||
) => void;
|
) => void;
|
||||||
dropEffect: ExplorerTreeNodeDropEffect;
|
|
||||||
}) => {
|
}) => {
|
||||||
const handleOnChildrenDrop = useCallback(
|
const handleOnChildrenDrop = useCallback(
|
||||||
(data: DropTargetDropEvent<AffineDNDData>) => {
|
(data: DropTargetDropEvent<AffineDNDData>) => {
|
||||||
@@ -305,8 +238,8 @@ const ExplorerFavoriteNode = ({
|
|||||||
docId={favorite.id}
|
docId={favorite.id}
|
||||||
location={childLocation}
|
location={childLocation}
|
||||||
onDrop={handleOnChildrenDrop}
|
onDrop={handleOnChildrenDrop}
|
||||||
dropEffect={dropEffect}
|
dropEffect={favoriteChildrenDropEffect}
|
||||||
canDrop={canDrop}
|
canDrop={favoriteChildrenCanDrop}
|
||||||
/>
|
/>
|
||||||
) : favorite.type === 'tag' ? (
|
) : favorite.type === 'tag' ? (
|
||||||
<ExplorerTagNode
|
<ExplorerTagNode
|
||||||
@@ -314,8 +247,8 @@ const ExplorerFavoriteNode = ({
|
|||||||
tagId={favorite.id}
|
tagId={favorite.id}
|
||||||
location={childLocation}
|
location={childLocation}
|
||||||
onDrop={handleOnChildrenDrop}
|
onDrop={handleOnChildrenDrop}
|
||||||
dropEffect={dropEffect}
|
dropEffect={favoriteChildrenDropEffect}
|
||||||
canDrop={canDrop}
|
canDrop={favoriteChildrenCanDrop}
|
||||||
/>
|
/>
|
||||||
) : favorite.type === 'folder' ? (
|
) : favorite.type === 'folder' ? (
|
||||||
<ExplorerFolderNode
|
<ExplorerFolderNode
|
||||||
@@ -323,8 +256,8 @@ const ExplorerFavoriteNode = ({
|
|||||||
nodeId={favorite.id}
|
nodeId={favorite.id}
|
||||||
location={childLocation}
|
location={childLocation}
|
||||||
onDrop={handleOnChildrenDrop}
|
onDrop={handleOnChildrenDrop}
|
||||||
dropEffect={dropEffect}
|
dropEffect={favoriteChildrenDropEffect}
|
||||||
canDrop={canDrop}
|
canDrop={favoriteChildrenCanDrop}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ExplorerCollectionNode
|
<ExplorerCollectionNode
|
||||||
@@ -332,8 +265,8 @@ const ExplorerFavoriteNode = ({
|
|||||||
collectionId={favorite.id}
|
collectionId={favorite.id}
|
||||||
location={childLocation}
|
location={childLocation}
|
||||||
onDrop={handleOnChildrenDrop}
|
onDrop={handleOnChildrenDrop}
|
||||||
dropEffect={dropEffect}
|
dropEffect={favoriteChildrenDropEffect}
|
||||||
canDrop={canDrop}
|
canDrop={favoriteChildrenCanDrop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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<AffineDNDData>['canDrop'] =
|
||||||
|
data => {
|
||||||
|
const type = data.source.data.entity?.type;
|
||||||
|
return !!type && isOrganizeSupportType(type);
|
||||||
|
};
|
||||||
@@ -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 { useI18n } from '@affine/i18n';
|
||||||
import { FolderIcon } from '@blocksuite/icons/rc';
|
|
||||||
|
|
||||||
import { ExplorerEmptySection } from '../../layouts/empty-section';
|
import { ExplorerEmptySection } from '../../layouts/empty-section';
|
||||||
|
import { DropEffect } from '../../tree';
|
||||||
|
import { organizeEmptyDropEffect, organizeEmptyRootCanDrop } from './dnd';
|
||||||
|
|
||||||
export const RootEmpty = ({
|
interface RootEmptyProps {
|
||||||
onClickCreate,
|
|
||||||
isLoading,
|
|
||||||
}: {
|
|
||||||
onClickCreate?: () => void;
|
onClickCreate?: () => void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}) => {
|
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RootEmptyLoading = () => {
|
||||||
|
return <Skeleton />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RootEmptyReady = ({
|
||||||
|
onClickCreate,
|
||||||
|
onDrop,
|
||||||
|
}: Omit<RootEmptyProps, 'isLoading'>) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
if (isLoading) {
|
const { dropTargetRef, draggedOverDraggable, draggedOverPosition } =
|
||||||
return <Skeleton />;
|
useDropTarget<AffineDNDData>(
|
||||||
}
|
() => ({
|
||||||
|
data: { at: 'explorer:organize:root' },
|
||||||
|
onDrop,
|
||||||
|
canDrop: organizeEmptyRootCanDrop,
|
||||||
|
}),
|
||||||
|
[onDrop]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExplorerEmptySection
|
<ExplorerEmptySection
|
||||||
icon={FolderIcon}
|
ref={dropTargetRef}
|
||||||
|
icon={<AnimatedFolderIcon open={!!draggedOverDraggable} />}
|
||||||
message={t['com.affine.rootAppSidebar.organize.empty']()}
|
message={t['com.affine.rootAppSidebar.organize.empty']()}
|
||||||
messageTestId="slider-bar-organize-empty-message"
|
messageTestId="slider-bar-organize-empty-message"
|
||||||
actionText={t[
|
actionText={t[
|
||||||
'com.affine.rootAppSidebar.organize.empty.new-folders-button'
|
'com.affine.rootAppSidebar.organize.empty.new-folders-button'
|
||||||
]()}
|
]()}
|
||||||
onActionClick={onClickCreate}
|
onActionClick={onClickCreate}
|
||||||
/>
|
>
|
||||||
|
{draggedOverDraggable && (
|
||||||
|
<DropEffect
|
||||||
|
position={draggedOverPosition}
|
||||||
|
dropEffect={organizeEmptyDropEffect({
|
||||||
|
source: draggedOverDraggable,
|
||||||
|
treeInstruction: null,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ExplorerEmptySection>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RootEmpty = ({ isLoading, ...props }: RootEmptyProps) => {
|
||||||
|
return isLoading ? <RootEmptyLoading /> : <RootEmptyReady {...props} />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import {
|
|||||||
toast,
|
toast,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import {
|
import { ExplorerTreeRoot } from '@affine/core/modules/explorer/views/tree';
|
||||||
type ExplorerTreeNodeDropEffect,
|
|
||||||
ExplorerTreeRoot,
|
|
||||||
} from '@affine/core/modules/explorer/views/tree';
|
|
||||||
import {
|
import {
|
||||||
type FolderNode,
|
type FolderNode,
|
||||||
OrganizeService,
|
OrganizeService,
|
||||||
@@ -22,8 +19,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { ExplorerService } from '../../../services/explorer';
|
import { ExplorerService } from '../../../services/explorer';
|
||||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||||
import { ExplorerFolderNode } from '../../nodes/folder';
|
import { ExplorerFolderNode } from '../../nodes/folder';
|
||||||
|
import { organizeChildrenDropEffect } from './dnd';
|
||||||
import { RootEmpty } from './empty';
|
import { RootEmpty } from './empty';
|
||||||
import * as styles from './styles.css';
|
|
||||||
|
|
||||||
export const ExplorerOrganize = () => {
|
export const ExplorerOrganize = () => {
|
||||||
const { organizeService, explorerService } = useServices({
|
const { organizeService, explorerService } = useServices({
|
||||||
@@ -36,10 +33,11 @@ export const ExplorerOrganize = () => {
|
|||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const rootFolder = organizeService.folderTree.rootFolder;
|
const folderTree = organizeService.folderTree;
|
||||||
|
const rootFolder = folderTree.rootFolder;
|
||||||
|
|
||||||
const folders = useLiveData(rootFolder.sortedChildren$);
|
const folders = useLiveData(rootFolder.sortedChildren$);
|
||||||
const isLoading = useLiveData(organizeService.folderTree.isLoading$);
|
const isLoading = useLiveData(folderTree.isLoading$);
|
||||||
|
|
||||||
const handleCreateFolder = useCallback(() => {
|
const handleCreateFolder = useCallback(() => {
|
||||||
const newFolderId = rootFolder.createFolder(
|
const newFolderId = rootFolder.createFolder(
|
||||||
@@ -49,6 +47,7 @@ export const ExplorerOrganize = () => {
|
|||||||
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
|
track.$.navigationPanel.organize.createOrganizeItem({ type: 'folder' });
|
||||||
setNewFolderId(newFolderId);
|
setNewFolderId(newFolderId);
|
||||||
explorerSection.setCollapsed(false);
|
explorerSection.setCollapsed(false);
|
||||||
|
return newFolderId;
|
||||||
}, [explorerSection, rootFolder]);
|
}, [explorerSection, rootFolder]);
|
||||||
|
|
||||||
const handleOnChildrenDrop = useCallback(
|
const handleOnChildrenDrop = useCallback(
|
||||||
@@ -78,21 +77,22 @@ export const ExplorerOrganize = () => {
|
|||||||
[rootFolder, t]
|
[rootFolder, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChildrenDropEffect = useCallback<ExplorerTreeNodeDropEffect>(
|
const createFolderAndDrop = useCallback(
|
||||||
data => {
|
(data: DropTargetDropEvent<AffineDNDData>) => {
|
||||||
if (
|
const newFolderId = handleCreateFolder();
|
||||||
data.treeInstruction?.type === 'reorder-above' ||
|
setNewFolderId(null);
|
||||||
data.treeInstruction?.type === 'reorder-below'
|
const newFolder$ = folderTree.folderNode$(newFolderId);
|
||||||
) {
|
|
||||||
if (data.source.data.entity?.type === 'folder') {
|
const entity = data.source.data.entity;
|
||||||
return 'move';
|
if (!entity) return;
|
||||||
}
|
const { type, id } = entity;
|
||||||
} else {
|
if (type === 'folder') return;
|
||||||
return; // not supported
|
|
||||||
}
|
const folder = newFolder$.value;
|
||||||
return;
|
if (!folder) return;
|
||||||
|
folder.createLink(type, id, folder.indexAt('after'));
|
||||||
},
|
},
|
||||||
[]
|
[folderTree, handleCreateFolder]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChildrenCanDrop = useMemo<
|
const handleChildrenCanDrop = useMemo<
|
||||||
@@ -106,7 +106,6 @@ export const ExplorerOrganize = () => {
|
|||||||
return (
|
return (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
name="organize"
|
name="organize"
|
||||||
headerClassName={styles.draggedOverHighlight}
|
|
||||||
title={t['com.affine.rootAppSidebar.organize']()}
|
title={t['com.affine.rootAppSidebar.organize']()}
|
||||||
actions={
|
actions={
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -123,7 +122,11 @@ export const ExplorerOrganize = () => {
|
|||||||
>
|
>
|
||||||
<ExplorerTreeRoot
|
<ExplorerTreeRoot
|
||||||
placeholder={
|
placeholder={
|
||||||
<RootEmpty onClickCreate={handleCreateFolder} isLoading={isLoading} />
|
<RootEmpty
|
||||||
|
onClickCreate={handleCreateFolder}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onDrop={createFolderAndDrop}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{folders.map(child => (
|
{folders.map(child => (
|
||||||
@@ -132,7 +135,7 @@ export const ExplorerOrganize = () => {
|
|||||||
nodeId={child.id as string}
|
nodeId={child.id as string}
|
||||||
defaultRenaming={child.id === newFolderId}
|
defaultRenaming={child.id === newFolderId}
|
||||||
onDrop={handleOnChildrenDrop}
|
onDrop={handleOnChildrenDrop}
|
||||||
dropEffect={handleChildrenDropEffect}
|
dropEffect={organizeChildrenDropEffect}
|
||||||
canDrop={handleChildrenCanDrop}
|
canDrop={handleChildrenCanDrop}
|
||||||
location={{
|
location={{
|
||||||
at: 'explorer:organize:folder-node',
|
at: 'explorer:organize:folder-node',
|
||||||
|
|||||||
@@ -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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -3,10 +3,9 @@ import { style } from '@vanilla-extract/css';
|
|||||||
|
|
||||||
export const dropEffect = style({
|
export const dropEffect = style({
|
||||||
zIndex: 99999,
|
zIndex: 99999,
|
||||||
position: 'absolute',
|
position: 'fixed',
|
||||||
left: '0px',
|
left: '10px',
|
||||||
top: '-34px',
|
top: '-20px',
|
||||||
opacity: 0.9,
|
|
||||||
background: cssVar('--affine-background-primary-color'),
|
background: cssVar('--affine-background-primary-color'),
|
||||||
boxShadow: cssVar('--affine-toolbar-shadow'),
|
boxShadow: cssVar('--affine-toolbar-shadow'),
|
||||||
padding: '0px 4px',
|
padding: '0px 4px',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type { useDropTarget } from '@affine/component';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { CopyIcon, LinkIcon, MoveToIcon } from '@blocksuite/icons/rc';
|
import { CopyIcon, LinkIcon, MoveToIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import * as styles from './drop-effect.css';
|
import * as styles from './drop-effect.css';
|
||||||
|
|
||||||
@@ -8,15 +10,15 @@ export const DropEffect = ({
|
|||||||
position,
|
position,
|
||||||
}: {
|
}: {
|
||||||
dropEffect?: 'copy' | 'move' | 'link' | undefined;
|
dropEffect?: 'copy' | 'move' | 'link' | undefined;
|
||||||
position: { x: number; y: number };
|
position: ReturnType<typeof useDropTarget>['draggedOverPosition'];
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
if (dropEffect === undefined) return null;
|
if (dropEffect === undefined) return null;
|
||||||
return (
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className={styles.dropEffect}
|
className={styles.dropEffect}
|
||||||
style={{
|
style={{
|
||||||
transform: `translate(${position.x + 10}px, ${position.y + 10}px)`,
|
transform: `translate(${position.clientX}px, ${position.clientY}px)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{dropEffect === 'copy' ? (
|
{dropEffect === 'copy' ? (
|
||||||
@@ -31,6 +33,7 @@ export const DropEffect = ({
|
|||||||
: dropEffect === 'move'
|
: dropEffect === 'move'
|
||||||
? t['com.affine.rootAppSidebar.explorer.drop-effect.move']()
|
? t['com.affine.rootAppSidebar.explorer.drop-effect.move']()
|
||||||
: t['com.affine.rootAppSidebar.explorer.drop-effect.link']()}
|
: t['com.affine.rootAppSidebar.explorer.drop-effect.link']()}
|
||||||
</div>
|
</div>,
|
||||||
|
document.body
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ export type ExplorerTreeNodeDropEffectData = {
|
|||||||
export type ExplorerTreeNodeDropEffect = (
|
export type ExplorerTreeNodeDropEffect = (
|
||||||
data: ExplorerTreeNodeDropEffectData
|
data: ExplorerTreeNodeDropEffectData
|
||||||
) => 'copy' | 'move' | 'link' | undefined;
|
) => 'copy' | 'move' | 'link' | undefined;
|
||||||
|
export type ExplorerTreeNodeIcon = React.ComponentType<{
|
||||||
|
className?: string;
|
||||||
|
draggedOver?: boolean;
|
||||||
|
treeInstruction?: DropTargetTreeInstruction | null;
|
||||||
|
collapsed?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
export const ExplorerTreeNode = ({
|
export const ExplorerTreeNode = ({
|
||||||
children,
|
children,
|
||||||
@@ -74,11 +80,7 @@ export const ExplorerTreeNode = ({
|
|||||||
...otherProps
|
...otherProps
|
||||||
}: {
|
}: {
|
||||||
name?: string;
|
name?: string;
|
||||||
icon?: React.ComponentType<{
|
icon?: ExplorerTreeNodeIcon;
|
||||||
className?: string;
|
|
||||||
draggedOver?: boolean;
|
|
||||||
treeInstruction?: DropTargetTreeInstruction | null;
|
|
||||||
}>;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
reorderable?: boolean;
|
reorderable?: boolean;
|
||||||
@@ -311,6 +313,7 @@ export const ExplorerTreeNode = ({
|
|||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
draggedOver={draggedOver && !isSelfDraggedOver}
|
draggedOver={draggedOver && !isSelfDraggedOver}
|
||||||
treeInstruction={treeInstruction}
|
treeInstruction={treeInstruction}
|
||||||
|
collapsed={collapsed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -398,10 +401,7 @@ export const ExplorerTreeNode = ({
|
|||||||
source: draggedOverDraggable,
|
source: draggedOverDraggable,
|
||||||
treeInstruction: treeInstruction,
|
treeInstruction: treeInstruction,
|
||||||
})}
|
})}
|
||||||
position={{
|
position={draggedOverPosition}
|
||||||
x: draggedOverPosition.relativeX,
|
|
||||||
y: draggedOverPosition.relativeY,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
12
packages/frontend/core/src/modules/organize/constants.ts
Normal file
12
packages/frontend/core/src/modules/organize/constants.ts
Normal file
@@ -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);
|
||||||
Reference in New Issue
Block a user