From 03ab674c1d4b98b0dbd1c9f78e6b8f3aaaebd956 Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Thu, 16 Jan 2025 11:15:25 -0300 Subject: [PATCH] refactor file dropping for ImportAsset component --- .../src/components/Assets/Assets.css | 2 +- .../src/components/Assets/Assets.tsx | 30 +++-- .../src/components/FileInput/FileInput.tsx | 34 ++++-- .../src/components/FileInput/types.ts | 1 + .../components/ImportAsset/ImportAsset.css | 45 ++++--- .../components/ImportAsset/ImportAsset.tsx | 113 ++++-------------- 6 files changed, 96 insertions(+), 129 deletions(-) diff --git a/packages/@dcl/inspector/src/components/Assets/Assets.css b/packages/@dcl/inspector/src/components/Assets/Assets.css index 75db5e1de..004d5662d 100644 --- a/packages/@dcl/inspector/src/components/Assets/Assets.css +++ b/packages/@dcl/inspector/src/components/Assets/Assets.css @@ -74,7 +74,7 @@ .Assets .Assets-content { background: var(--tree-bg-color) !important; - height: calc(100% - 36px); + height: 100%; } .Assets .Assets-content.Hide { diff --git a/packages/@dcl/inspector/src/components/Assets/Assets.tsx b/packages/@dcl/inspector/src/components/Assets/Assets.tsx index 4921ff8aa..7c36e8cca 100644 --- a/packages/@dcl/inspector/src/components/Assets/Assets.tsx +++ b/packages/@dcl/inspector/src/components/Assets/Assets.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react' +import React, { useCallback, useRef } from 'react' import cx from 'classnames' import { MdImageSearch } from 'react-icons/md' import { HiOutlinePlus } from 'react-icons/hi' @@ -17,6 +17,7 @@ import { CustomAssets } from '../CustomAssets' import { selectCustomAssets } from '../../redux/app' import { RenameAsset } from '../RenameAsset' import { CreateCustomAsset } from '../CreateCustomAsset' +import { InputRef } from '../FileInput/FileInput' import './Assets.css' @@ -31,6 +32,7 @@ function Assets({ isAssetsPanelCollapsed }: { isAssetsPanelCollapsed: boolean }) const dispatch = useAppDispatch() const tab = useAppSelector(getSelectedAssetsTab) const customAssets = useAppSelector(selectCustomAssets) + const inputRef = useRef(null); const handleTabClick = useCallback( (tab: AssetsTab) => () => { @@ -47,9 +49,14 @@ function Assets({ isAssetsPanelCollapsed }: { isAssetsPanelCollapsed: boolean }) const assetToRename = useAppSelector(selectAssetToRename) const stagedCustomAsset = useAppSelector(selectStagedCustomAsset) + const handleImportClick = useCallback(() => { + inputRef.current?.onClick() + }, [inputRef]) + return (
+
@@ -76,16 +83,17 @@ function Assets({ isAssetsPanelCollapsed }: { isAssetsPanelCollapsed: boolean })
-
- {tab === AssetsTab.AssetsPack && } - {tab === AssetsTab.FileSystem && } - {tab === AssetsTab.Import && } - {tab === AssetsTab.CustomAssets && } - {tab === AssetsTab.RenameAsset && assetToRename && ( - - )} - {tab === AssetsTab.CreateCustomAsset && stagedCustomAsset && } -
+ +
+ {tab === AssetsTab.AssetsPack && } + {tab === AssetsTab.FileSystem && } + {tab === AssetsTab.CustomAssets && } + {tab === AssetsTab.RenameAsset && assetToRename && ( + + )} + {tab === AssetsTab.CreateCustomAsset && stagedCustomAsset && } +
+
) } diff --git a/packages/@dcl/inspector/src/components/FileInput/FileInput.tsx b/packages/@dcl/inspector/src/components/FileInput/FileInput.tsx index dd783b67d..0fc2ed9ec 100644 --- a/packages/@dcl/inspector/src/components/FileInput/FileInput.tsx +++ b/packages/@dcl/inspector/src/components/FileInput/FileInput.tsx @@ -1,4 +1,4 @@ -import React, { useRef, PropsWithChildren, useCallback } from 'react' +import React, { useImperativeHandle, useCallback, useEffect, useRef } from 'react' import { useDrop } from 'react-dnd' import { NativeTypes } from 'react-dnd-html5-backend' import { PropTypes } from './types' @@ -11,12 +11,16 @@ function parseAccept(accept: PropTypes['accept']) { return value } -export function FileInput(props: PropsWithChildren) { +export interface InputRef { + onClick: () => void; +} + +export const FileInput = React.forwardRef>((props, parentRef) => { const { onDrop } = props - const inputRef = useRef(null) const acceptExtensions = Object.values(props.accept ?? []).flat() + const inputRef = useRef(null) - const [_, drop] = useDrop( + const [{ isHover }, drop] = useDrop( () => ({ accept: [NativeTypes.FILE], drop(item: { files: File[] }) { @@ -24,11 +28,18 @@ export function FileInput(props: PropsWithChildren) { }, canDrop(item: { files: File[] }) { return item.files.every((file) => !!acceptExtensions.find((ext) => file.name.endsWith(ext))) - } + }, + collect: (monitor) => ({ + isHover: monitor.canDrop() && monitor.isOver() + }) }), [props] ) + const handleClick = useCallback(() => { + inputRef?.current?.click() + }, [inputRef]) + const handleFileSelected = useCallback( (e: React.ChangeEvent): void => { const files = Array.from(e.target.files ?? []) @@ -37,8 +48,17 @@ export function FileInput(props: PropsWithChildren) { [onDrop] ) + useEffect(() => { + props.onHover?.(isHover) + return () => props.onHover?.(false) + }, [isHover]) + + useImperativeHandle(parentRef, () => ({ + onClick: handleClick, + }), [handleClick]); + return ( -
inputRef?.current?.click()}> +
) { {props.children}
) -} +}) export default FileInput diff --git a/packages/@dcl/inspector/src/components/FileInput/types.ts b/packages/@dcl/inspector/src/components/FileInput/types.ts index c42e1fa6d..22b346edc 100644 --- a/packages/@dcl/inspector/src/components/FileInput/types.ts +++ b/packages/@dcl/inspector/src/components/FileInput/types.ts @@ -1,5 +1,6 @@ export interface PropTypes { onDrop(files: File[]): void + onHover?(isHover: boolean): void accept?: Record disabled?: boolean } diff --git a/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css b/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css index f011715b9..9c0b52d49 100644 --- a/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css +++ b/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css @@ -1,9 +1,16 @@ .ImportAsset { height: 100%; - padding: 8px; } .ImportAsset > div:first-child { + height: 100%; +} + +.ImportAssetHover { + padding: 8px; +} + +.ImportAssetHover > div:first-child { display: flex; flex-direction: column; align-items: center; @@ -19,11 +26,11 @@ overflow-y: auto; } -.ImportAsset > div > span:last-of-type { +.ImportAssetHover > div > span:last-of-type { text-align: center; } -.ImportAsset .upload-icon { +.ImportAssetHover .upload-icon { background-color: var(--list-item-hover-bg-color); width: 40px; height: 40px; @@ -35,19 +42,19 @@ flex-shrink: 0; } -.ImportAsset .Container { +.ImportAssetHover .Container { overflow: visible; height: unset; background-color: var(--list-item-hover-bg-color); margin-right: 20px; } -.ImportAsset .Container svg { +.ImportAssetHover .Container svg { width: 40px; height: 40px; } -.ImportAsset .Container .content { +.ImportAssetHover .Container .content { height: 100%; width: 100%; display: flex; @@ -57,7 +64,7 @@ position: relative; } -.ImportAsset .Container .remove-icon { +.ImportAssetHover .Container .remove-icon { position: absolute; display: flex; align-items: center; @@ -68,12 +75,12 @@ border-radius: 50%; } -.ImportAsset .Container .remove-icon svg { +.ImportAssetHover .Container .remove-icon svg { width: 14px; height: 14px; } -.ImportAsset .Container .file-title { +.ImportAssetHover .Container .file-title { margin-top: 8px; text-overflow: ellipsis; overflow: hidden; @@ -82,49 +89,49 @@ text-align: center; } -.ImportAsset .file-container { +.ImportAssetHover .file-container { display: flex; flex-direction: row; margin-top: 16px; } -.ImportAsset .file-container .Container { +.ImportAssetHover .file-container .Container { width: 120px; } -.ImportAsset .file-container .Text.Field { +.ImportAssetHover .file-container .Text.Field { width: auto; height: 24px; } -.ImportAsset .file-container .error .Text.Field { +.ImportAssetHover .file-container .error .Text.Field { border-color: red; } -.ImportAsset .file-container > div:nth-child(2) { +.ImportAssetHover .file-container > div:nth-child(2) { display: flex; flex-direction: column; justify-content: center; } -.ImportAsset .file-container > div:nth-child(2) button { +.ImportAssetHover .file-container > div:nth-child(2) button { background-color: var(--primary-main); } -.ImportAsset .error { +.ImportAssetHover .error { color: var(--primary-main); margin-top: 6px; } -.ImportAsset .file-container .AssetPreview { +.ImportAssetHover .file-container .AssetPreview { width: 100px; height: 100px; } -.ImportAsset .warning { +.ImportAssetHover .warning { color: var(--danger); margin-top: 6px; } -.ImportAsset .text { +.ImportAssetHover .text { text-align: center; } diff --git a/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx b/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx index b68148e4a..a78ed5cce 100644 --- a/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx +++ b/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react' +import cx from 'classnames' import { HiOutlineUpload } from 'react-icons/hi' import { RxCross2 } from 'react-icons/rx' import classNames from 'classnames' @@ -20,6 +21,7 @@ import { processAssets } from './utils' import { Asset } from './types' import './ImportAsset.css' +import { InputRef } from '../FileInput/FileInput' const ACCEPTED_FILE_TYPES = { 'model/gltf-binary': ['.gltf', '.glb', '.bin'], @@ -30,16 +32,23 @@ const ACCEPTED_FILE_TYPES = { 'video/mp4': ['.mp4'] } +const ACCEPTED_FILE_TYPES_STR = Object + .values(ACCEPTED_FILE_TYPES) + .flat().join('/') + .replaceAll('.', '') + .toUpperCase() + interface PropTypes { onSave(): void } -const ImportAsset: React.FC = ({ onSave }) => { +const ImportAsset = React.forwardRef>(({ onSave, children }, inputRef) => { const dispatch = useAppDispatch() const catalog = useAppSelector(selectAssetCatalog) const uploadFile = useAppSelector(selectUploadFile) const [files, setFiles] = useState([]) + const [isHover, setIsHover] = useState(false) const { basePath, assets } = catalog ?? { basePath: '', assets: [] } useEffect(() => { @@ -49,113 +58,35 @@ const ImportAsset: React.FC = ({ onSave }) => { } }, [uploadFile]) - const handleDrop = async (acceptedFiles: File[]) => { + const handleDrop = useCallback(async (acceptedFiles: File[]) => { const assets = await processAssets(acceptedFiles) console.log('assets: ', assets) setFiles(assets) - } - - const handleSave = () => { - // const basePath = withAssetDir(DIRECTORY.SCENE) - // const content: Map = new Map() - // const fullName = assetName + '.' + assetExtension - // content.set(fullName, new Uint8Array(binary)) - - // dispatch( - // importAsset({ - // content, - // basePath, - // assetPackageName: '', - // reload: true - // }) - // ) + }, []) - // if (thumbnail) { - // dispatch( - // saveThumbnail({ - // content: transformBase64ResourceToBinary(thumbnail), - // path: `${DIRECTORY.THUMBNAILS}/${assetName}.png` - // }) - // ) - // } - - // // Clear uploaded file from the FileUploadField - // const newUploadFile = { ...uploadFile } - // for (const key in newUploadFile) { - // newUploadFile[key] = `${basePath}/${fullName}` - // } - // dispatch(updateUploadFile(newUploadFile)) - // setFile(undefined) - - // onSave() - // } - } + const handleHover = useCallback((isHover: boolean) => { + setIsHover(isHover) + }, []) function removeAsset(asset: Asset) { // e.stopPropagation() setFiles(files.filter((file) => file.name !== asset.name)) } - // const handleNameChange = useCallback((event: React.ChangeEvent) => { - // setAssetName(event.target.value) - // }, []) - - // const isNameUnique = useCallback((name: string, ext: string) => { - // return !assets.find((asset) => { - // const [packageName, otherAssetName] = removeBasePath(basePath, asset.path).split('/') - // if (packageName === 'builder') return false - // return otherAssetName?.toLocaleLowerCase() === name?.toLocaleLowerCase() + '.' + ext - // }) - // }, []) - - // const isNameRepeated = !isNameUnique(assetName, assetExtension) - - // const handleScreenshot = useCallback( - // (value: string) => { - // setThumbnail(value) - // }, - // [files] - // ) - - const types = useMemo(() => Object.values(ACCEPTED_FILE_TYPES).flat().join('/').replaceAll('.', '').toUpperCase(), []) - return ( -
- - {!files.length && ( +
+ + {!files.length && isHover ? ( <>
- Drop {types} files + Drop {ACCEPTED_FILE_TYPES_STR} files - )} - {/* {file && ( -
- -
- -
- -
{file.name}
-
-
- - - - -
-
- )} - {validationError} - {isNameRepeated && ( - There's a file with this name already, you will overwrite it if you continue - )} */} + ) : children}
) -} +}) export default ImportAsset