Skip to content

Commit 96cec9f

Browse files
committed
add filters for asset explorer
1 parent f85e704 commit 96cec9f

File tree

12 files changed

+381
-105
lines changed

12 files changed

+381
-105
lines changed

packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
height: 100%;
1111
}
1212

13+
.ImportAsset .children.hidden {
14+
display: none;
15+
}
16+
1317
.ImportAssetHover {
1418
padding: 8px;
1519
}

packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react'
1+
import React, { PropsWithChildren, useCallback, useEffect, useState, useMemo } from 'react'
22
import cx from 'classnames'
33
import { HiOutlineUpload } from 'react-icons/hi'
44

@@ -117,6 +117,8 @@ const ImportAsset = React.forwardRef<InputRef, PropsWithChildren<PropTypes>>(({
117117
[assets]
118118
)
119119

120+
const isImportActive = useMemo(() => !files.length && isHover, [files, isHover])
121+
120122
return (
121123
<div className={cx('ImportAsset', { ImportAssetHover: isHover })}>
122124
<FileInput
@@ -127,16 +129,15 @@ const ImportAsset = React.forwardRef<InputRef, PropsWithChildren<PropTypes>>(({
127129
accept={ACCEPTED_FILE_TYPES}
128130
multiple
129131
>
130-
{!files.length && isHover ? (
132+
{isImportActive && (
131133
<>
132134
<div className="upload-icon">
133135
<HiOutlineUpload />
134136
</div>
135137
<span className="text">Drop {ACCEPTED_FILE_TYPES_STR} files</span>
136138
</>
137-
) : (
138-
children
139139
)}
140+
<div className={cx('children', { hidden: isImportActive })}>{children}</div>
140141
<Modal
141142
isOpen={!!files.length}
142143
onRequestClose={handleCloseModal}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.ProjectFilters {
2+
display: flex;
3+
flex-direction: column;
4+
padding: 10px;
5+
height: calc(100% - 30px);
6+
overflow-y: auto;
7+
user-select: none;
8+
}
9+
10+
.ProjectFilters .filter {
11+
display: flex;
12+
flex-direction: column;
13+
align-items: center;
14+
padding: 10px;
15+
cursor: pointer;
16+
margin-bottom: 10px;
17+
border-radius: 8px;
18+
}
19+
20+
.ProjectFilters .filter:last-child {
21+
margin: 0;
22+
}
23+
24+
.ProjectFilters .filter:hover,
25+
.ProjectFilters .filter.active {
26+
background-color: var(--accent-blue-07);
27+
}
28+
29+
.ProjectFilters .filter svg {
30+
width: 20px;
31+
height: 20px;
32+
margin-bottom: 4px;
33+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useCallback } from 'react'
2+
import cx from 'classnames'
3+
import { AiOutlineSound as AudioIcon } from 'react-icons/ai'
4+
import { IoIosImage as ImageIcon } from 'react-icons/io'
5+
import { IoCubeOutline as ModelIcon, IoVideocamOutline as VideoIcon } from 'react-icons/io5'
6+
import { FaFile as AssetIcon } from 'react-icons/fa'
7+
import { GoClock as RecentIcon } from 'react-icons/go'
8+
9+
import { PropTypes, Filter } from './types'
10+
11+
import './Filters.css'
12+
13+
export function Filters({ filters, active, onClick }: PropTypes) {
14+
const getFilter = useCallback((type: Filter) => {
15+
switch (type) {
16+
case 'all':
17+
return { title: 'All Assets', icon: AssetIcon }
18+
case 'recents':
19+
return { title: 'Recents', icon: RecentIcon }
20+
case 'models':
21+
return { title: 'Models', icon: ModelIcon }
22+
case 'images':
23+
return { title: 'Images', icon: ImageIcon }
24+
case 'audio':
25+
return { title: 'Audio', icon: AudioIcon }
26+
case 'video':
27+
return { title: 'Video', icon: VideoIcon }
28+
case 'other':
29+
return { title: 'Other', icon: AssetIcon }
30+
}
31+
}, [])
32+
33+
const handleClick = useCallback((type: Filter) => () => onClick(type), [])
34+
35+
return (
36+
<div className="ProjectFilters">
37+
{filters.map(($) => {
38+
const _filter = getFilter($)
39+
return (
40+
<div className={cx('filter', { active: $ === active })} onClick={handleClick($)}>
41+
{<_filter.icon />}
42+
{_filter.title}
43+
</div>
44+
)
45+
})}
46+
</div>
47+
)
48+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Filters } from './Filters'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type Filter = 'all' | 'recents' | 'models' | 'images' | 'video' | 'audio' | 'other'
2+
3+
export type PropTypes = {
4+
filters: Filter[]
5+
active?: Filter
6+
onClick(type: Filter): void
7+
}

packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectAssetExplorer.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
}
55

66
.ProjectView > .Tree-View {
7-
border-right: 0.1px solid;
8-
border-color: var(--border-gray);
7+
border-right: 0.1px solid var(--border-gray);
8+
border-left: 0.1px solid var(--border-gray);
99
}
1010

1111
.ProjectView > .Tree-View > .with-context-menu > .editor-assets-tree {

packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx

Lines changed: 44 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import { Tree } from '../Tree'
1111
import { Modal } from '../Modal'
1212
import { Button } from '../Button'
1313
import FolderIcon from '../Icons/Folder'
14-
import { AssetNode, AssetNodeFolder } from './types'
15-
import { getFullNodePath } from './utils'
14+
import { AssetNodeFolder } from './types'
15+
import { getFilterFromTree, getFullNodePath } from './utils'
1616
import Search from '../Search'
1717
import { withAssetDir } from '../../lib/data-layer/host/fs-utils'
1818
import { removeAsset } from '../../redux/data-layer'
1919
import { useAppDispatch } from '../../redux/hooks'
2020
import { determineAssetType, extractFileExtension } from '../ImportAsset/utils'
21+
import { generateAssetTree, getChildren as _getChildren, TreeNode, ROOT, getTiles } from './tree'
22+
import { Filters } from './Filters'
23+
import { Filter } from './Filters/types'
24+
25+
export { TreeNode }
2126

2227
function noop() {}
2328

@@ -32,81 +37,34 @@ interface ModalState {
3237
entities: Entity[]
3338
}
3439

35-
export const ROOT = 'File System'
36-
3740
export const DRAG_N_DROP_ASSET_KEY = 'local-asset'
3841

39-
export type TreeNode = Omit<AssetNode, 'children'> & { children?: string[]; matches?: string[] }
40-
4142
const FilesTree = Tree<string>()
4243

4344
function ProjectView({ folders, thumbnails }: Props) {
4445
const sdk = useSdk()
4546
const dispatch = useAppDispatch()
4647
const [open, setOpen] = useState(new Set<string>())
4748
const [modal, setModal] = useState<ModalState | undefined>(undefined)
48-
const [lastSelected, setLastSelected] = useState<string>()
49+
const [lastSelected, setLastSelected] = useState<string>(ROOT)
4950
const [search, setSearch] = useState<string>('')
5051
const [tree, setTree] = useState<Map<string, TreeNode>>(new Map())
51-
52-
const getTree = useCallback(() => {
53-
function getPath(node: string, children: string) {
54-
if (!node) return children
55-
return `${node}/${children}`
56-
}
57-
const tree = new Map<string, TreeNode>()
58-
tree.set(ROOT, { children: folders.map((f) => f.name), name: ROOT, type: 'folder', parent: null })
59-
open.add(ROOT)
60-
61-
function hasMatch(name: string) {
62-
return search && name.toLocaleLowerCase().includes(search.toLocaleLowerCase())
63-
}
64-
65-
function generateTree(node: AssetNodeFolder, parentName: string = ''): string[] {
66-
const namePath = getPath(parentName, node.name)
67-
const childrens = node.children.map((c) => `${namePath}/${c.name}`)
68-
const matchesList: string[] = []
69-
for (const children of node.children) {
70-
if (children.type === 'folder') {
71-
matchesList.push(...generateTree(children, namePath))
72-
} else {
73-
const name = getPath(namePath, children.name)
74-
const matches = hasMatch(name)
75-
if (matches) {
76-
open.add(name)
77-
matchesList.push(name)
78-
}
79-
tree.set(name, { ...children, matches: matches ? [name] : [], parent: node })
80-
}
81-
}
82-
if (matchesList.length) {
83-
open.add(namePath)
84-
}
85-
tree.set(namePath, { ...node, children: childrens, parent: null, matches: matchesList })
86-
return matchesList
87-
}
88-
89-
for (const f of folders) {
90-
generateTree(f)
91-
}
92-
return tree
93-
}, [folders, search])
52+
const [filters, setFilters] = useState<Filter[]>([])
53+
const [activeFilter, setActiveFilter] = useState<Filter>('all')
9454

9555
useEffect(() => {
96-
setTree(getTree())
97-
}, [folders, search])
98-
99-
/**
100-
* Values
101-
*/
102-
const selectedTreeNode = tree.get(lastSelected ?? ROOT)
56+
const { tree, filters } = generateAssetTree(folders, open, search, activeFilter)
57+
setTree(tree)
58+
setFilters(getFilterFromTree(filters))
59+
}, [folders, search, activeFilter])
10360

10461
/**
10562
* Callbacks
10663
*/
10764

10865
const onSelect = useCallback(
10966
(value: string) => {
67+
open.add(value)
11068
setLastSelected(value)
11169
},
11270
[setLastSelected]
@@ -148,7 +106,7 @@ function ProjectView({ folders, thumbnails }: Props) {
148106
}
149107
dispatch(removeAsset({ path }))
150108
},
151-
[open, setOpen, selectedTreeNode, lastSelected]
109+
[open, setOpen, lastSelected]
152110
)
153111

154112
const handleConfirm = useCallback(async () => {
@@ -158,11 +116,13 @@ function ProjectView({ folders, thumbnails }: Props) {
158116
}, [modal, setModal])
159117

160118
const handleModalClose = useCallback(() => setModal(undefined), [])
119+
161120
const handleClickFolder = useCallback(
162-
(val: string) => () => {
163-
if (lastSelected === val) return
164-
open.add(val)
165-
setLastSelected(val)
121+
(node: TreeNode) => () => {
122+
if (node.type === 'asset') return
123+
const path = getFullNodePath(node).slice(1)
124+
open.add(path)
125+
setLastSelected(path)
166126
},
167127
[setLastSelected]
168128
)
@@ -171,16 +131,10 @@ function ProjectView({ folders, thumbnails }: Props) {
171131

172132
const getChildren = useCallback(
173133
(val: string) => {
174-
const value = tree.get(val)
175-
if (!value?.children?.length) return []
176-
if (!search.length) return value.children
177-
178-
return value.children.filter(($) => {
179-
const childrenValue = tree.get($)
180-
return !!childrenValue?.matches?.length
181-
})
134+
const childs = _getChildren(val, tree, search, activeFilter)
135+
return childs
182136
},
183-
[tree, search]
137+
[tree, search, activeFilter]
184138
)
185139

186140
const getThumbnail = useCallback(
@@ -192,6 +146,12 @@ function ProjectView({ folders, thumbnails }: Props) {
192146
[thumbnails]
193147
)
194148

149+
const handleFilterClick = useCallback((type: Filter) => {
150+
setActiveFilter(type)
151+
}, [])
152+
153+
const tiles = getTiles(lastSelected, tree, search, activeFilter)
154+
195155
return (
196156
<>
197157
<Modal isOpen={!!modal?.isOpen} onRequestClose={handleModalClose} className="RemoveAsset">
@@ -206,6 +166,7 @@ function ProjectView({ folders, thumbnails }: Props) {
206166
</div>
207167
</Modal>
208168
<div className="ProjectView">
169+
<Filters filters={filters} active={activeFilter} onClick={handleFilterClick} />
209170
<div className="Tree-View">
210171
<Search
211172
value={search}
@@ -241,31 +202,18 @@ function ProjectView({ folders, thumbnails }: Props) {
241202
/>
242203
</div>
243204
<div className="FolderView">
244-
{selectedTreeNode?.type === 'folder'
245-
? selectedTreeNode?.children?.map(($) => (
246-
<Tile
247-
key={$}
248-
valueId={$}
249-
value={tree.get($)}
250-
getDragContext={handleDragContext}
251-
onSelect={handleClickFolder($)}
252-
onRemove={handleRemove}
253-
getThumbnail={getThumbnail}
254-
dndType={DRAG_N_DROP_ASSET_KEY}
255-
/>
256-
))
257-
: !!selectedTreeNode &&
258-
lastSelected && (
259-
<Tile
260-
valueId={lastSelected}
261-
value={selectedTreeNode}
262-
getDragContext={handleDragContext}
263-
onSelect={handleClickFolder(selectedTreeNode.name)}
264-
onRemove={handleRemove}
265-
getThumbnail={getThumbnail}
266-
dndType={DRAG_N_DROP_ASSET_KEY}
267-
/>
268-
)}
205+
{tiles.map((node) => (
206+
<Tile
207+
key={node.name}
208+
valueId={getFullNodePath(node).slice(1)}
209+
value={node}
210+
getDragContext={handleDragContext}
211+
onSelect={handleClickFolder(node)}
212+
onRemove={handleRemove}
213+
getThumbnail={getThumbnail}
214+
dndType={DRAG_N_DROP_ASSET_KEY}
215+
/>
216+
))}
269217
</div>
270218
</div>
271219
</>

packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import { ContextMenu as Menu } from '../../ContexMenu'
1414
import FolderIcon from '../../Icons/Folder'
1515
import { withContextMenu } from '../../../hoc/withContextMenu'
1616
import { useContextMenu } from '../../../hooks/sdk/useContextMenu'
17+
import { determineAssetType, extractFileExtension } from '../../ImportAsset/utils'
1718
import { Props } from './types'
1819

1920
import './Tile.css'
20-
import { determineAssetType, extractFileExtension } from '../../ImportAsset/utils'
2121

2222
export const Tile = withContextMenu<Props>(
2323
({ valueId, value, getDragContext, onSelect, onRemove, contextMenuId, dndType, getThumbnail }) => {

0 commit comments

Comments
 (0)