mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-07 20:31:33 +08:00
refactor(mobile): use separate explorer components for mobile (#8503)
close AF-1488 - remove dnd related logic - separate styles - remove empty status, always show a `New` button in each level of tree
This commit is contained in:
@@ -64,27 +64,3 @@ export const collapseIcon = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ------------- mobile -------------
|
||||
export const mobileRoot = style([
|
||||
root,
|
||||
{
|
||||
height: 25,
|
||||
padding: '0 16px',
|
||||
selectors: {
|
||||
'&[data-collapsible="true"]:hover': {
|
||||
backgroundColor: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const mobileLabel = style([
|
||||
label,
|
||||
{
|
||||
color: cssVarV2('text/primary'),
|
||||
fontSize: 20,
|
||||
lineHeight: '25px',
|
||||
letterSpacing: -0.45,
|
||||
fontWeight: 400,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -9,7 +9,6 @@ export type CategoryDividerProps = PropsWithChildren<
|
||||
label: string;
|
||||
className?: string;
|
||||
collapsed?: boolean;
|
||||
mobile?: boolean;
|
||||
setCollapsed?: (collapsed: boolean) => void;
|
||||
} & {
|
||||
[key: `data-${string}`]: unknown;
|
||||
@@ -23,7 +22,6 @@ export const CategoryDivider = forwardRef(
|
||||
children,
|
||||
className,
|
||||
collapsed,
|
||||
mobile,
|
||||
setCollapsed,
|
||||
...otherProps
|
||||
}: CategoryDividerProps,
|
||||
@@ -33,16 +31,15 @@ export const CategoryDivider = forwardRef(
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(mobile ? styles.mobileRoot : styles.root, className)}
|
||||
className={clsx(styles.root, className)}
|
||||
ref={ref}
|
||||
role="switch"
|
||||
onClick={() => setCollapsed?.(!collapsed)}
|
||||
data-mobile={mobile}
|
||||
data-collapsed={collapsed}
|
||||
data-collapsible={collapsible}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={mobile ? styles.mobileLabel : styles.label}>
|
||||
<div className={styles.label}>
|
||||
{label}
|
||||
{collapsible ? (
|
||||
<ToggleCollapseIcon
|
||||
@@ -53,11 +50,9 @@ export const CategoryDivider = forwardRef(
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{mobile ? null : (
|
||||
<div className={styles.actions} onClick={e => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.actions} onClick={e => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,18 @@ import { ExplorerService } from './services/explorer';
|
||||
export { ExplorerService } from './services/explorer';
|
||||
export type { CollapsibleSectionName } from './types';
|
||||
export { CollapsibleSection } from './views/layouts/collapsible-section';
|
||||
export { ExplorerMobileContext } from './views/mobile.context';
|
||||
export { ExplorerCollections } from './views/sections/collections';
|
||||
export { ExplorerFavorites } from './views/sections/favorites';
|
||||
export { ExplorerMigrationFavorites } from './views/sections/migration-favorites';
|
||||
export { ExplorerOrganize } from './views/sections/organize';
|
||||
// for mobile
|
||||
export { ExplorerTreeRoot } from './views/tree';
|
||||
export { ExplorerTreeContext } from './views/tree/context';
|
||||
export type {
|
||||
BaseExplorerTreeNodeProps,
|
||||
ExplorerTreeNodeIcon,
|
||||
} from './views/tree/node';
|
||||
export type { NodeOperation } from './views/tree/types';
|
||||
|
||||
export function configureExplorerModule(framework: Framework) {
|
||||
framework
|
||||
|
||||
@@ -13,8 +13,3 @@ export const header = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// mobile
|
||||
export const mobileContent = style({
|
||||
paddingTop: 8,
|
||||
});
|
||||
|
||||
@@ -7,18 +7,11 @@ import {
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
} from 'react';
|
||||
|
||||
import { ExplorerService } from '../../services/explorer';
|
||||
import type { CollapsibleSectionName } from '../../types';
|
||||
import { ExplorerMobileContext } from '../mobile.context';
|
||||
import {
|
||||
content,
|
||||
header,
|
||||
mobileContent,
|
||||
root,
|
||||
} from './collapsible-section.css';
|
||||
import { content, header, root } from './collapsible-section.css';
|
||||
|
||||
interface CollapsibleSectionProps extends PropsWithChildren {
|
||||
name: CollapsibleSectionName;
|
||||
@@ -50,7 +43,6 @@ export const CollapsibleSection = ({
|
||||
|
||||
contentClassName,
|
||||
}: CollapsibleSectionProps) => {
|
||||
const mobile = useContext(ExplorerMobileContext);
|
||||
const section = useService(ExplorerService).sections[name];
|
||||
|
||||
const collapsed = useLiveData(section.collapsed$);
|
||||
@@ -70,7 +62,6 @@ export const CollapsibleSection = ({
|
||||
data-testid={testId}
|
||||
>
|
||||
<CategoryDivider
|
||||
mobile={mobile}
|
||||
data-testid={headerTestId}
|
||||
label={title}
|
||||
setCollapsed={setCollapsed}
|
||||
@@ -82,7 +73,7 @@ export const CollapsibleSection = ({
|
||||
</CategoryDivider>
|
||||
<Collapsible.Content
|
||||
data-testid="collapsible-section-content"
|
||||
className={clsx(mobile ? mobileContent : content, contentClassName)}
|
||||
className={clsx(content, contentClassName)}
|
||||
>
|
||||
{children}
|
||||
</Collapsible.Content>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
/**
|
||||
* To enable mobile manually
|
||||
* > Using `environment.isMobile` directly will affect current web entry on mobile
|
||||
* > So we control it manually for now
|
||||
*/
|
||||
export const ExplorerMobileContext = createContext(false);
|
||||
@@ -73,15 +73,8 @@ export const useExplorerCollectionNodeOperations = (
|
||||
track.$.navigationPanel.collections.addDocToCollection({
|
||||
control: 'button',
|
||||
});
|
||||
workbenchService.workbench.openDoc(newDoc.id);
|
||||
onOpenCollapsed();
|
||||
}, [
|
||||
collectionId,
|
||||
collectionService,
|
||||
createPage,
|
||||
onOpenCollapsed,
|
||||
workbenchService.workbench,
|
||||
]);
|
||||
}, [collectionId, collectionService, createPage, onOpenCollapsed]);
|
||||
|
||||
const handleToggleFavoriteCollection = useCallback(() => {
|
||||
compatibleFavoriteItemsAdapter.toggle(collectionId, 'collection');
|
||||
|
||||
@@ -129,9 +129,8 @@ export const useExplorerDocNodeOperations = (
|
||||
await docsService.addLinkedDoc(docId, newDoc.id);
|
||||
track.$.navigationPanel.docs.createDoc({ control: 'linkDoc' });
|
||||
track.$.navigationPanel.docs.linkDoc({ control: 'createDoc' });
|
||||
workbenchService.workbench.openDoc(newDoc.id);
|
||||
options.openNodeCollapsed();
|
||||
}, [createPage, docsService, docId, workbenchService.workbench, options]);
|
||||
}, [createPage, docsService, docId, options]);
|
||||
|
||||
const handleToggleFavoriteDoc = useCallback(() => {
|
||||
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
type FolderNode,
|
||||
OrganizeService,
|
||||
} from '@affine/core/modules/organize';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -173,7 +172,7 @@ const ExplorerFolderIcon: ExplorerTreeNodeIcon = ({
|
||||
/>
|
||||
);
|
||||
|
||||
export const ExplorerFolderNodeFolder = ({
|
||||
const ExplorerFolderNodeFolder = ({
|
||||
node,
|
||||
onDrop,
|
||||
defaultRenaming,
|
||||
@@ -187,13 +186,11 @@ export const ExplorerFolderNodeFolder = ({
|
||||
node: FolderNode;
|
||||
} & GenericExplorerNode) => {
|
||||
const t = useI18n();
|
||||
const { workbenchService, workspaceService, featureFlagService } =
|
||||
useServices({
|
||||
WorkbenchService,
|
||||
WorkspaceService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const { workspaceService, featureFlagService } = useServices({
|
||||
WorkspaceService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const openDocsSelector = useSelectDoc();
|
||||
const openTagsSelector = useSelectTag();
|
||||
const openCollectionsSelector = useSelectCollection();
|
||||
@@ -552,14 +549,13 @@ export const ExplorerFolderNodeFolder = ({
|
||||
const handleNewDoc = useCallback(() => {
|
||||
const newDoc = createPage();
|
||||
node.createLink('doc', newDoc.id, node.indexAt('before'));
|
||||
workbenchService.workbench.openDoc(newDoc.id);
|
||||
track.$.navigationPanel.folders.createDoc();
|
||||
track.$.navigationPanel.organize.createOrganizeItem({
|
||||
type: 'link',
|
||||
target: 'doc',
|
||||
});
|
||||
setCollapsed(false);
|
||||
}, [createPage, node, workbenchService.workbench]);
|
||||
}, [createPage, node]);
|
||||
|
||||
const handleCreateSubfolder = useCallback(() => {
|
||||
const newFolderId = node.createFolder(
|
||||
|
||||
@@ -64,10 +64,9 @@ export const useExplorerTagNodeOperations = (
|
||||
const newDoc = createPage();
|
||||
tagRecord?.tag(newDoc.id);
|
||||
track.$.navigationPanel.tags.createDoc();
|
||||
workbenchService.workbench.openDoc(newDoc.id);
|
||||
openNodeCollapsed();
|
||||
}
|
||||
}, [createPage, openNodeCollapsed, tagRecord, workbenchService.workbench]);
|
||||
}, [createPage, openNodeCollapsed, tagRecord]);
|
||||
|
||||
const handleMoveToTrash = useCallback(() => {
|
||||
tagService.tagList.deleteTag(tagId);
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
FavoriteService,
|
||||
isFavoriteSupportType,
|
||||
} from '@affine/core/modules/favorite';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { isNewTabTrigger } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -41,14 +40,8 @@ import {
|
||||
import { RootEmpty } from './empty';
|
||||
|
||||
export const ExplorerFavorites = () => {
|
||||
const {
|
||||
favoriteService,
|
||||
workspaceService,
|
||||
workbenchService,
|
||||
explorerService,
|
||||
} = useServices({
|
||||
const { favoriteService, workspaceService, explorerService } = useServices({
|
||||
FavoriteService,
|
||||
WorkbenchService,
|
||||
WorkspaceService,
|
||||
ExplorerService,
|
||||
});
|
||||
@@ -88,23 +81,18 @@ export const ExplorerFavorites = () => {
|
||||
|
||||
const handleCreateNewFavoriteDoc: MouseEventHandler = useCallback(
|
||||
e => {
|
||||
const newDoc = createPage();
|
||||
const newDoc = createPage(
|
||||
undefined,
|
||||
isNewTabTrigger(e) ? 'new-tab' : true
|
||||
);
|
||||
favoriteService.favoriteList.add(
|
||||
'doc',
|
||||
newDoc.id,
|
||||
favoriteService.favoriteList.indexAt('before')
|
||||
);
|
||||
workbenchService.workbench.openDoc(newDoc.id, {
|
||||
at: isNewTabTrigger(e) ? 'new-tab' : 'active',
|
||||
});
|
||||
explorerSection.setCollapsed(false);
|
||||
},
|
||||
[
|
||||
createPage,
|
||||
explorerSection,
|
||||
favoriteService.favoriteList,
|
||||
workbenchService.workbench,
|
||||
]
|
||||
[createPage, explorerSection, favoriteService.favoriteList]
|
||||
);
|
||||
|
||||
const handleOnChildrenDrop = useCallback(
|
||||
|
||||
@@ -179,67 +179,3 @@ export const draggedOverEffect = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ---------- mobile ----------
|
||||
export const mobileItemRoot = style([
|
||||
itemRoot,
|
||||
{
|
||||
padding: '8px',
|
||||
borderRadius: 0,
|
||||
flexDirection: 'row-reverse',
|
||||
gap: 12,
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'none',
|
||||
},
|
||||
'&:active': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
'&[data-active="true"]': {
|
||||
background: 'transparent',
|
||||
},
|
||||
},
|
||||
|
||||
':after': {
|
||||
content: '',
|
||||
width: `calc(100% + ${levelIndent})`,
|
||||
height: 0.5,
|
||||
background: cssVar('borderColor'),
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
},
|
||||
},
|
||||
]);
|
||||
export const mobileItemMain = style([itemMain, {}]);
|
||||
|
||||
export const mobileIconContainer = style([
|
||||
iconContainer,
|
||||
{
|
||||
width: 32,
|
||||
height: 32,
|
||||
fontSize: 24,
|
||||
},
|
||||
]);
|
||||
|
||||
export const mobileCollapsedIconContainer = style([
|
||||
collapsedIconContainer,
|
||||
{
|
||||
fontSize: 16,
|
||||
},
|
||||
]);
|
||||
export const mobileItemContent = style([
|
||||
itemContent,
|
||||
{
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
letterSpacing: -0.43,
|
||||
fontWeight: 400,
|
||||
},
|
||||
]);
|
||||
export const mobileContentContainer = style([
|
||||
contentContainer,
|
||||
{
|
||||
marginTop: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { ExplorerMobileContext } from '../mobile.context';
|
||||
import { ExplorerTreeContext } from './context';
|
||||
import { DropEffect } from './drop-effect';
|
||||
import * as styles from './node.css';
|
||||
@@ -57,6 +56,38 @@ export type ExplorerTreeNodeIcon = React.ComponentType<{
|
||||
collapsed?: boolean;
|
||||
}>;
|
||||
|
||||
export interface BaseExplorerTreeNodeProps {
|
||||
name?: string;
|
||||
icon?: ExplorerTreeNodeIcon;
|
||||
children?: React.ReactNode;
|
||||
active?: boolean;
|
||||
defaultRenaming?: boolean;
|
||||
extractEmojiAsIcon?: boolean;
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
renameable?: boolean;
|
||||
onRename?: (newName: string) => void;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
to?: To;
|
||||
postfix?: React.ReactNode;
|
||||
operations?: NodeOperation[];
|
||||
childrenOperations?: NodeOperation[];
|
||||
childrenPlaceholder?: React.ReactNode;
|
||||
linkComponent?: React.ComponentType<
|
||||
React.PropsWithChildren<{ to: To; className?: string }> & RefAttributes<any>
|
||||
>;
|
||||
[key: `data-${string}`]: any;
|
||||
}
|
||||
|
||||
interface WebExplorerTreeNodeProps extends BaseExplorerTreeNodeProps {
|
||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||
reorderable?: boolean;
|
||||
dndData?: AffineDNDData;
|
||||
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
||||
dropEffect?: ExplorerTreeNodeDropEffect;
|
||||
}
|
||||
|
||||
export const ExplorerTreeNode = ({
|
||||
children,
|
||||
icon: Icon,
|
||||
@@ -82,34 +113,7 @@ export const ExplorerTreeNode = ({
|
||||
onDrop,
|
||||
dropEffect,
|
||||
...otherProps
|
||||
}: {
|
||||
name?: string;
|
||||
icon?: ExplorerTreeNodeIcon;
|
||||
children?: React.ReactNode;
|
||||
active?: boolean;
|
||||
reorderable?: boolean;
|
||||
defaultRenaming?: boolean;
|
||||
extractEmojiAsIcon?: boolean;
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
renameable?: boolean;
|
||||
onRename?: (newName: string) => void;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
to?: To;
|
||||
postfix?: React.ReactNode;
|
||||
canDrop?: DropTargetOptions<AffineDNDData>['canDrop'];
|
||||
operations?: NodeOperation[];
|
||||
childrenOperations?: NodeOperation[];
|
||||
childrenPlaceholder?: React.ReactNode;
|
||||
linkComponent?: React.ComponentType<
|
||||
React.PropsWithChildren<{ to: To; className?: string }> & RefAttributes<any>
|
||||
>;
|
||||
dndData?: AffineDNDData;
|
||||
onDrop?: (data: DropTargetDropEvent<AffineDNDData>) => void;
|
||||
dropEffect?: ExplorerTreeNodeDropEffect;
|
||||
} & { [key in `data-${string}`]?: any }) => {
|
||||
const mobile = useContext(ExplorerMobileContext);
|
||||
}: WebExplorerTreeNodeProps) => {
|
||||
const t = useI18n();
|
||||
const cid = useId();
|
||||
const context = useContext(ExplorerTreeContext);
|
||||
@@ -141,21 +145,19 @@ export const ExplorerTreeNode = ({
|
||||
AffineDNDData & { draggable: { __cid: string } }
|
||||
>(
|
||||
() => ({
|
||||
canDrag: () => !mobile,
|
||||
data: { ...dndData?.draggable, __cid: cid },
|
||||
dragPreviewPosition: 'pointer-outside',
|
||||
}),
|
||||
[cid, dndData, mobile]
|
||||
[cid, dndData]
|
||||
);
|
||||
const handleCanDrop = useMemo<DropTargetOptions<AffineDNDData>['canDrop']>(
|
||||
() => args => {
|
||||
if (mobile) return false;
|
||||
if (!reorderable && args.treeInstruction?.type !== 'make-child') {
|
||||
return false;
|
||||
}
|
||||
return (typeof canDrop === 'function' ? canDrop(args) : canDrop) ?? true;
|
||||
},
|
||||
[canDrop, mobile, reorderable]
|
||||
[canDrop, reorderable]
|
||||
);
|
||||
const {
|
||||
dropTargetRef,
|
||||
@@ -319,7 +321,7 @@ export const ExplorerTreeNode = ({
|
||||
const content = (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={mobile ? styles.mobileItemRoot : styles.itemRoot}
|
||||
className={styles.itemRoot}
|
||||
data-active={active}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
@@ -327,11 +329,7 @@ export const ExplorerTreeNode = ({
|
||||
data-disabled={disabled}
|
||||
onClick={handleCollapsedChange}
|
||||
data-testid="explorer-collapsed-button"
|
||||
className={
|
||||
mobile
|
||||
? styles.mobileCollapsedIconContainer
|
||||
: styles.collapsedIconContainer
|
||||
}
|
||||
className={styles.collapsedIconContainer}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
@@ -339,10 +337,8 @@ export const ExplorerTreeNode = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={clsx(mobile ? styles.mobileItemMain : styles.itemMain)}>
|
||||
<div
|
||||
className={mobile ? styles.mobileIconContainer : styles.iconContainer}
|
||||
>
|
||||
<div className={styles.itemMain}>
|
||||
<div className={styles.iconContainer}>
|
||||
{emoji ??
|
||||
(Icon && (
|
||||
<Icon
|
||||
@@ -352,38 +348,34 @@ export const ExplorerTreeNode = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={mobile ? styles.mobileItemContent : styles.itemContent}>
|
||||
{name}
|
||||
</div>
|
||||
<div className={styles.itemContent}>{name}</div>
|
||||
{postfix}
|
||||
{mobile ? null : (
|
||||
<div
|
||||
className={styles.postfix}
|
||||
onClick={e => {
|
||||
// prevent jump to page
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{inlineOperations.map(({ view }, index) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
{menuOperations.length > 0 && (
|
||||
<Menu
|
||||
items={menuOperations.map(({ view }, index) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
<div
|
||||
className={styles.postfix}
|
||||
onClick={e => {
|
||||
// prevent jump to page
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{inlineOperations.map(({ view }, index) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
{menuOperations.length > 0 && (
|
||||
<Menu
|
||||
items={menuOperations.map(({ view }, index) => (
|
||||
<Fragment key={index}>{view}</Fragment>
|
||||
))}
|
||||
>
|
||||
<IconButton
|
||||
size="16"
|
||||
data-testid="explorer-tree-node-operation-button"
|
||||
style={{ marginLeft: 4 }}
|
||||
>
|
||||
<IconButton
|
||||
size="16"
|
||||
data-testid="explorer-tree-node-operation-button"
|
||||
style={{ marginLeft: 4 }}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{renameable && (
|
||||
@@ -411,10 +403,7 @@ export const ExplorerTreeNode = ({
|
||||
{...otherProps}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
mobile ? styles.mobileContentContainer : styles.contentContainer,
|
||||
styles.draggedOverEffect
|
||||
)}
|
||||
className={clsx(styles.contentContainer, styles.draggedOverEffect)}
|
||||
data-open={!collapsed}
|
||||
data-self-dragged-over={isSelfDraggedOver}
|
||||
ref={dropTargetRef}
|
||||
|
||||
Reference in New Issue
Block a user