From 377c8b90e3c0b392136025c65aee8a9f9a845bb5 Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Wed, 19 Apr 2023 16:30:52 -0300 Subject: [PATCH] feat: support dropping folders on Gltf UI component (#531) --- .../GltfInspector/GltfInspector.tsx | 35 +++++++++++++--- .../ProjectAssetExplorer/ProjectView.tsx | 2 +- .../ProjectAssetExplorer/utils.spec.ts | 41 +++++++++++++++++++ .../components/ProjectAssetExplorer/utils.ts | 15 ++++--- packages/@dcl/inspector/src/lib/logic/once.ts | 6 +-- 5 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 packages/@dcl/inspector/src/components/ProjectAssetExplorer/utils.spec.ts diff --git a/packages/@dcl/inspector/src/components/EntityInspector/GltfInspector/GltfInspector.tsx b/packages/@dcl/inspector/src/components/EntityInspector/GltfInspector/GltfInspector.tsx index 2b42ba94d..865da88a9 100644 --- a/packages/@dcl/inspector/src/components/EntityInspector/GltfInspector/GltfInspector.tsx +++ b/packages/@dcl/inspector/src/components/EntityInspector/GltfInspector/GltfInspector.tsx @@ -1,10 +1,11 @@ -import { useCallback, useState } from 'react' +import { useCallback } from 'react' import { Menu, Item } from 'react-contexify' import { useDrop } from 'react-dnd' import { AiFillDelete as DeleteIcon } from 'react-icons/ai' import cx from 'classnames' -import { AssetNodeItem } from '../../ProjectAssetExplorer/types' +import { memoize } from '../../../lib/logic/once' +import { TreeNode } from '../../ProjectAssetExplorer/ProjectView' import { withContextMenu } from '../../../hoc/withContextMenu' import { WithSdkProps, withSdk } from '../../../hoc/withSdk' @@ -18,14 +19,30 @@ import { Container } from '../../Container' import { TextField } from '../TextField' import { Props } from './types' import { fromGltf, toGltf, isValidInput } from './utils' +import { isAssetNode } from '../../ProjectAssetExplorer/utils' +import { AssetNodeItem } from '../../ProjectAssetExplorer/types' const DROP_TYPES = ['project-asset-gltf'] interface IDrop { value: string; - context: { tree: Map } + context: { tree: Map } } +const isModel = (node: TreeNode): node is AssetNodeItem => isAssetNode(node) && (node.name.endsWith('.gltf') || node.name.endsWith('.glb')) + +const getModel = memoize((node: TreeNode, tree: Map): AssetNodeItem | null => { + if (isModel(node)) return node + + const children = node.children || [] + for (const child of children) { + const childNode = tree.get(child)! + if (isModel(childNode)) return childNode + } + + return null +}) + export default withSdk( withContextMenu(({ sdk, entity, contextMenuId }) => { const { files } = useFileSystem() @@ -37,8 +54,8 @@ export default withSdk( const getInputProps = useComponentInput(entity, GltfContainer, fromGltf, toGltf, handleInputValidation) const handleRemove = useCallback(() => GltfContainer.deleteFrom(entity), []) - const handleDrop = useCallback((value: AssetNodeItem) => { - GltfContainer.createOrReplace(entity, { src: value.asset.src }) + const handleDrop = useCallback((src: string) => { + GltfContainer.createOrReplace(entity, { src }) }, []) const [{ isHover }, drop] = useDrop( @@ -46,7 +63,13 @@ export default withSdk( accept: DROP_TYPES, drop: ({ value, context }: IDrop, monitor) => { if (monitor.didDrop()) return - handleDrop(context.tree.get(value)!) + const node = context.tree.get(value)! + const model = getModel(node, context.tree) + if (model) handleDrop(model.asset.src) + }, + canDrop: ({ value, context }: IDrop) => { + const node = context.tree.get(value)! + return !!getModel(node, context.tree) }, collect: (monitor) => ({ isHover: monitor.canDrop() && monitor.isOver() diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx index 2c93606d0..ac469efb1 100644 --- a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx @@ -16,7 +16,7 @@ type Props = { const ROOT = 'File System' -type TreeNode = Omit & { children?: string[] } +export type TreeNode = Omit & { children?: string[] } function ProjectView({ folders }: Props) { const [open, setOpen] = useState(new Set()) diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/utils.spec.ts b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/utils.spec.ts new file mode 100644 index 000000000..f358db02a --- /dev/null +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/utils.spec.ts @@ -0,0 +1,41 @@ +import * as utils from './utils' + +import { AssetNodeFolder, AssetNodeItem } from './types' + +describe('ProjectAssetExplorer/utils', () => { + describe('getFullNodePath', () => { + it('should return full node path given a child', () => { + const parentA = { name: 'parentA', parent: null } + const parentB = { name: 'parentB', parent: parentA } + const child = { name: 'file.gltf', parent: parentB } as AssetNodeItem + expect(utils.getFullNodePath(child)).toBe('/parentA/parentB/file.gltf') + }) + it('should return child name if node has no parent', () => { + const child = { name: 'file.gltf', parent: null } as AssetNodeItem + expect(utils.getFullNodePath(child)).toBe('/file.gltf') + }) + }) + describe('isAssetNode', () => { + it('should return true when node type "asset"', () => { + const node: AssetNodeItem = { + name: 'some-name', + parent: null, + type: 'asset', + asset: { + src: 'some-file.glb', + type: 'gltf' + } + } + expect(utils.isAssetNode(node)).toBe(true) + }) + it('should return false when node type is different from "asset"', () => { + const node: AssetNodeFolder = { + name: 'some-name', + parent: null, + type: 'folder', + children: [] + } + expect(utils.isAssetNode(node)).toBe(false) + }) + }) +}) diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/utils.ts b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/utils.ts index 7c77994c7..3c2f13c03 100644 --- a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/utils.ts +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/utils.ts @@ -1,4 +1,5 @@ -import { AssetNode, AssetNodeFolder } from './types' +import { TreeNode } from './ProjectView' +import { AssetNode, AssetNodeFolder, AssetNodeItem } from './types' export function AssetNodeRootNull(): AssetNodeFolder { return { name: '', parent: null, type: 'folder', children: [] } @@ -44,12 +45,16 @@ export function buildAssetTree(paths: string[]): AssetNodeFolder { return root } -export function getFullNodePath(item: AssetNode) { +export function getFullNodePath(item: AssetNode | TreeNode): string { let path = '' - let it = item - while (it.parent !== null && item.name !== '') { - path = item.name + '/' + path + let it: AssetNode | TreeNode | null = item + while (it) { + path = '/' + it.name + path it = it.parent } return path } + +export function isAssetNode(node: AssetNode | TreeNode): node is AssetNodeItem { + return node.type === 'asset' +} diff --git a/packages/@dcl/inspector/src/lib/logic/once.ts b/packages/@dcl/inspector/src/lib/logic/once.ts index c1848bab0..6ef824f2e 100644 --- a/packages/@dcl/inspector/src/lib/logic/once.ts +++ b/packages/@dcl/inspector/src/lib/logic/once.ts @@ -1,8 +1,8 @@ -export function memoize(cb: (a: K) => V): (a: K) => V { +export function memoize(cb: (a: K, ...params: P[]) => V): (a: K, ...params: P[]) => V { const memoized = new WeakMap() - return (a: K) => { + return (a: K, ...params: P[]) => { if (!memoized.has(a)) { - const ret = cb(a) + const ret = cb(a, ...params) memoized.set(a, ret) return ret }