Skip to content

Commit e2de4bb

Browse files
committed
Add search and upload capability to <finder-file-select>
1 parent 6b268b9 commit e2de4bb

File tree

9 files changed

+341
-147
lines changed

9 files changed

+341
-147
lines changed

client/browser/FinderFileSelect.tsx

Lines changed: 44 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,16 @@ import React, {
22
lazy,
33
memo,
44
Suspense,
5-
useCallback,
65
useEffect,
76
useMemo,
87
useRef,
98
useState,
109
} from 'react';
10+
import {Tooltip} from 'react-tooltip';
1111
import FigureLabels from '../finder/FigureLabels';
1212
import FileUploader from '../finder/FileUploader';
13-
import ArrowDownIcon from '../icons/arrow-down.svg';
14-
import ArrowRightIcon from '../icons/arrow-right.svg';
15-
import EmptyIcon from '../icons/empty.svg';
16-
import FolderIcon from '../icons/folder.svg';
17-
import FolderOpenIcon from '../icons/folder-open.svg';
18-
import RootIcon from '../icons/root.svg';
13+
import FolderStructure from './FolderStructure';
14+
import Menu from './Menu';
1915

2016

2117
function StaticFigure(props) {
@@ -53,13 +49,14 @@ function Figure(props) {
5349

5450

5551
const FilesList = memo((props: any) => {
56-
const {files, isLoading} = props;
52+
const {files} = props;
5753

5854
function selectFile(file) {
5955
console.log(file);
6056
}
6157

6258
console.log('FolderList', files);
59+
6360
return (
6461
<ul className="files-list">{
6562
files.length === 0 ?
@@ -72,91 +69,11 @@ const FilesList = memo((props: any) => {
7269
});
7370

7471

75-
function FolderEntry(props) {
76-
const {folder, toggleOpen, fetchFiles, isCurrent} = props;
77-
78-
if (folder.is_root) {
79-
return (<span onClick={() => fetchFiles(folder.id)}><RootIcon/></span>);
80-
}
81-
82-
return (<>
83-
<i onClick={toggleOpen}>{
84-
folder.has_subfolders ? folder.is_open ? <ArrowDownIcon/> : <ArrowRightIcon/> : <EmptyIcon/>
85-
}</i>
86-
{isCurrent ?
87-
<strong><FolderOpenIcon/>{folder.name}</strong> :
88-
<span onClick={() => fetchFiles(folder.id)} role="button">
89-
<FolderIcon/>{folder.name}
90-
</span>
91-
}
92-
</>);
93-
}
94-
95-
96-
function FolderStructure(props) {
97-
const {baseUrl, folder, lastFolderId, fetchFiles, refreshStructure} = props;
98-
99-
async function fetchChildren() {
100-
const response = await fetch(`${baseUrl}fetch/${folder.id}`);
101-
if (response.ok) {
102-
const reply = await response.json();
103-
folder.name = reply.name;
104-
folder.has_subfolders = reply.has_subfolders;
105-
folder.children = reply.children;
106-
} else {
107-
console.error(response);
108-
}
109-
}
110-
111-
async function toggleOpen() {
112-
folder.is_open = !folder.is_open;
113-
if (folder.is_open) {
114-
if (folder.children === null) {
115-
await fetchChildren();
116-
} else {
117-
await fetch(`${baseUrl}open/${folder.id}`);
118-
}
119-
} else {
120-
await fetch(`${baseUrl}close/${folder.id}`);
121-
}
122-
refreshStructure();
123-
}
124-
125-
return folder ? (
126-
<li>
127-
<FolderEntry
128-
folder={folder}
129-
toggleOpen={toggleOpen}
130-
fetchFiles={fetchFiles}
131-
isCurrent={lastFolderId === folder.id}
132-
/>
133-
{folder.is_open && (
134-
<ul>
135-
{folder.children.map(child => (
136-
<FolderStructure
137-
key={child.id}
138-
baseUrl={baseUrl}
139-
folder={child}
140-
lastFolderId={lastFolderId}
141-
fetchFiles={fetchFiles}
142-
refreshStructure={refreshStructure}
143-
/>
144-
))}
145-
</ul>)}
146-
</li>
147-
) : null;
148-
}
149-
150-
15172
export default function FinderFileSelect(props) {
15273
const baseUrl = props['base-url'];
153-
const [structure, setStructure] = useState({root_folder: null, last_folder: null, files: []});
154-
const [isLoading, setLoading] = useState(false);
155-
const [searchQuery, setSearchQuery] = useState(() => {
156-
const params = new URLSearchParams(window.location.search);
157-
return params.get('q');
158-
});
74+
const [structure, setStructure] = useState({root_folder: null, last_folder: null, files: null});
15975
const folderListRef = useRef(null);
76+
const uploaderRef = useRef(null);
16077

16178
useEffect(() => {
16279
getStructure();
@@ -171,19 +88,22 @@ export default function FinderFileSelect(props) {
17188
}
17289
}
17390

174-
async function fetchFiles(folderId){
175-
const params = new URLSearchParams({q: searchQuery});
176-
const listFilesUrl = `${baseUrl}list/${folderId}${searchQuery ? `?${params.toString()}` : ''}`;
177-
setLoading(true);
178-
const newStructure = {root_folder: structure.root_folder, last_folder: folderId, files: []};
179-
const response = await fetch(listFilesUrl);
91+
async function fetchFiles(folderId: string, searchQuery='') {
92+
const fetchUrl = (() => {
93+
if (searchQuery) {
94+
const params = new URLSearchParams({q: searchQuery});
95+
return `${baseUrl}${folderId}/search?${params.toString()}`;
96+
}
97+
return `${baseUrl}${folderId}/list`;
98+
})();
99+
const newStructure = {root_folder: structure.root_folder, last_folder: folderId, files: null};
100+
const response = await fetch(fetchUrl);
180101
if (response.ok) {
181102
const body = await response.json();
182103
newStructure.files = body.files;
183104
} else {
184105
console.error(response);
185106
}
186-
setLoading(false);
187107
setStructure(newStructure);
188108
}
189109

@@ -193,24 +113,38 @@ export default function FinderFileSelect(props) {
193113
}
194114

195115
function handleUpload(folderId) {
196-
folderListRef.current.fetchFiles
116+
fetchFiles(folderId);
197117
}
198118

199119
return structure.root_folder && (<>
200-
<ul className="folder-structure">
201-
<FolderStructure
202-
baseUrl={baseUrl}
203-
folder={structure.root_folder}
120+
<nav className="folder-structure">
121+
<ul>
122+
<FolderStructure
123+
baseUrl={baseUrl}
124+
folder={structure.root_folder}
125+
lastFolderId={structure.last_folder}
126+
fetchFiles={fetchFiles}
127+
refreshStructure={refreshStructure}
128+
/>
129+
</ul>
130+
</nav>
131+
<div className="file-browser">
132+
<Menu
204133
lastFolderId={structure.last_folder}
205134
fetchFiles={fetchFiles}
206-
// folderListRef={folderListRef}
207-
refreshStructure={refreshStructure}
135+
openUploader={() => uploaderRef.current.openUploader()}
208136
/>
209-
</ul>
210-
<FileUploader folderId={structure.root_folder} handleUpload={handleUpload}>{
211-
isLoading ?
212-
<div className="status">{gettext("Loading…")}</div> :
213-
<FilesList files={structure.files} />
214-
}</FileUploader>
137+
<FileUploader
138+
folderId={structure.last_folder}
139+
handleUpload={handleUpload}
140+
ref={uploaderRef}
141+
settings={{csrf_token: props['csrf-token'], base_url: props['base-url']}}
142+
>{
143+
structure.files === null ?
144+
<div className="status">{gettext("Loading files…")}</div> :
145+
<FilesList files={structure.files} />
146+
}</FileUploader>
147+
</div>
148+
<Tooltip id="django-finder-tooltip" place="bottom-start" />
215149
</>);
216150
}

client/browser/FolderStructure.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
import ArrowDownIcon from '../icons/arrow-down.svg';
3+
import ArrowRightIcon from '../icons/arrow-right.svg';
4+
import EmptyIcon from '../icons/empty.svg';
5+
import FolderIcon from '../icons/folder.svg';
6+
import FolderOpenIcon from '../icons/folder-open.svg';
7+
import RootIcon from '../icons/root.svg';
8+
9+
10+
function FolderEntry(props) {
11+
const {folder, toggleOpen, fetchFiles, isCurrent} = props;
12+
13+
if (folder.is_root) {
14+
return (<span onClick={() => fetchFiles(folder.id)}><RootIcon/></span>);
15+
}
16+
17+
return (<>
18+
<i onClick={toggleOpen}>{
19+
folder.has_subfolders ? folder.is_open ? <ArrowDownIcon/> : <ArrowRightIcon/> : <EmptyIcon/>
20+
}</i>
21+
{isCurrent ?
22+
<strong><FolderOpenIcon/>{folder.name}</strong> :
23+
<span onClick={() => fetchFiles(folder.id)} role="button">
24+
<FolderIcon/>{folder.name}
25+
</span>
26+
}
27+
</>);
28+
}
29+
30+
31+
export default function FolderStructure(props) {
32+
const {baseUrl, folder, lastFolderId, fetchFiles, refreshStructure} = props;
33+
34+
async function fetchChildren() {
35+
const response = await fetch(`${baseUrl}${folder.id}/fetch`);
36+
if (response.ok) {
37+
const reply = await response.json();
38+
folder.name = reply.name;
39+
folder.has_subfolders = reply.has_subfolders;
40+
folder.children = reply.children;
41+
} else {
42+
console.error(response);
43+
}
44+
}
45+
46+
async function toggleOpen() {
47+
folder.is_open = !folder.is_open;
48+
if (folder.is_open) {
49+
if (folder.children === null) {
50+
await fetchChildren();
51+
} else {
52+
await fetch(`${baseUrl}${folder.id}/open`);
53+
}
54+
} else {
55+
await fetch(`${baseUrl}${folder.id}/close`);
56+
}
57+
refreshStructure();
58+
}
59+
60+
return folder ? (
61+
<li>
62+
<FolderEntry
63+
folder={folder}
64+
toggleOpen={toggleOpen}
65+
fetchFiles={fetchFiles}
66+
isCurrent={lastFolderId === folder.id}
67+
/>
68+
{folder.is_open && (
69+
<ul>
70+
{folder.children.map(child => (
71+
<FolderStructure
72+
key={child.id}
73+
baseUrl={baseUrl}
74+
folder={child}
75+
lastFolderId={lastFolderId}
76+
fetchFiles={fetchFiles}
77+
refreshStructure={refreshStructure}
78+
/>
79+
))}
80+
</ul>)}
81+
</li>
82+
) : null;
83+
}

client/browser/Menu.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React, {useRef} from 'react';
2+
import DropDownMenu from '../finder/DropDownMenu';
3+
import {useSearchRealm} from '../finder/Storage';
4+
import SearchIcon from '../icons/search.svg';
5+
import UploadIcon from '../icons/upload.svg';
6+
7+
8+
export default function Menu(props) {
9+
const {lastFolderId, fetchFiles, openUploader} = props;
10+
const searchRef = useRef(null);
11+
const [searchRealm, setSearchRealm] = useSearchRealm('current');
12+
13+
function handleSearch(event) {
14+
const performSearch = () => {
15+
const searchQuery = searchRef.current.value || '';
16+
fetchFiles(lastFolderId, searchQuery);
17+
};
18+
const resetSearch = () => {
19+
fetchFiles(lastFolderId);
20+
};
21+
22+
if (event.type === 'change' && searchRef.current.value.length === 0) {
23+
// clicked on the X button
24+
resetSearch();
25+
} else if (event.type === 'keydown' && event.key === 'Enter') {
26+
// pressed Enter
27+
searchRef.current.value.length === 0 ? resetSearch() : performSearch();
28+
} else if (event.type === 'click' && searchRef.current.value.length > 2) {
29+
// clicked on the search button
30+
performSearch();
31+
}
32+
}
33+
34+
function changeSearchRealm(value) {
35+
if (value !== searchRealm) {
36+
setSearchRealm(value);
37+
}
38+
}
39+
40+
function isActive(value) {
41+
return searchRealm === value ? 'active' : null;
42+
}
43+
44+
console.log('Menu', lastFolderId);
45+
46+
return (
47+
<nav role="menubar">
48+
<menu>
49+
<li className="search-field">
50+
<input ref={searchRef}
51+
type="search"
52+
placeholder={gettext("Search for …")}
53+
onChange={handleSearch}
54+
onKeyDown={handleSearch}
55+
/>
56+
<div>
57+
<span className="search-icon" onClick={handleSearch}><SearchIcon/></span>
58+
<DropDownMenu
59+
wrapperElement="span"
60+
className="search-realm with-caret"
61+
tooltip={gettext("Restrict search")}
62+
>
63+
<li
64+
onClick={() => changeSearchRealm('current')}
65+
className={isActive('current')}>{gettext("From current folder")}
66+
</li>
67+
<li
68+
onClick={() => changeSearchRealm('everywhere')}
69+
className={isActive('everywhere')}>{gettext("In all folders")}
70+
</li>
71+
<hr/>
72+
<li
73+
onClick={() => changeSearchRealm('filename')}
74+
className={isActive('filename')}>{gettext("Filename only")}
75+
</li>
76+
<li
77+
onClick={() => changeSearchRealm('content')}
78+
className={isActive('content')}><s>{gettext("Also file content")}</s>
79+
</li>
80+
</DropDownMenu>
81+
</div>
82+
</li>
83+
<li onClick={openUploader} data-tooltip-id="django-finder-tooltip" data-tooltip-content={gettext("Upload file")}>
84+
<UploadIcon/>
85+
</li>
86+
</menu>
87+
</nav>
88+
);
89+
}

0 commit comments

Comments
 (0)