Skip to content

Commit 225506d

Browse files
committed
implement the frontend part of django-filer
1 parent 42bbd03 commit 225506d

File tree

15 files changed

+559
-243
lines changed

15 files changed

+559
-243
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import React, {
2+
lazy,
3+
memo,
4+
Suspense,
5+
useEffect,
6+
useMemo,
7+
useRef,
8+
useState,
9+
} from 'react';
10+
import {Tooltip} from 'react-tooltip';
11+
import FigureLabels from '../finder/FigureLabels';
12+
import FileUploader from '../finder/FileUploader';
13+
import FolderStructure from './FolderStructure';
14+
import Menu from './Menu';
15+
16+
17+
function StaticFigure(props) {
18+
return (<>{props.children}</>);
19+
}
20+
21+
22+
function Figure(props) {
23+
const FigBody = useMemo(() => {
24+
if (props.browser_component) {
25+
const component = `./components/folderitem/${props.browser_component}.js`;
26+
const LazyItem = lazy(() => import(component));
27+
return (props) => (
28+
<Suspense>
29+
<LazyItem {...props}>{props.children}</LazyItem>
30+
</Suspense>
31+
);
32+
}
33+
return StaticFigure;
34+
},[]);
35+
36+
return (
37+
<figure className="figure">
38+
<FigBody {...props}>
39+
<FigureLabels labels={props.labels}>
40+
<img src={props.thumbnail_url} {...props.listeners} {...props.attributes} />
41+
</FigureLabels>
42+
</FigBody>
43+
<figcaption>
44+
{props.name}
45+
</figcaption>
46+
</figure>
47+
);
48+
}
49+
50+
51+
const FilesList = memo((props: any) => {
52+
const {files, selectFile} = props;
53+
54+
console.log('FolderList', files);
55+
56+
return (
57+
<ul className="files-browser">{
58+
files.length === 0 ?
59+
<li className="status">{gettext("Empty folder")}</li> :
60+
files.map(file => (
61+
<li key={file.id} onClick={() => selectFile(file)}><Figure {...file} /></li>
62+
))}
63+
</ul>
64+
);
65+
});
66+
67+
68+
export default function FileSelectDialog(props) {
69+
const {baseUrl, csrfToken, realm, selectFile} = props;
70+
const [structure, setStructure] = useState({root_folder: null, last_folder: null, files: null});
71+
const uploaderRef = useRef(null);
72+
73+
useEffect(() => {
74+
getStructure();
75+
}, []);
76+
77+
async function getStructure() {
78+
const response = await fetch(`${baseUrl}structure/${realm}`);
79+
if (response.ok) {
80+
setStructure(await response.json());
81+
} else {
82+
console.error(response);
83+
}
84+
}
85+
86+
async function fetchFiles(folderId: string, searchQuery='') {
87+
const fetchUrl = (() => {
88+
if (searchQuery) {
89+
const params = new URLSearchParams({q: searchQuery});
90+
return `${baseUrl}${folderId}/search?${params.toString()}`;
91+
}
92+
return `${baseUrl}${folderId}/list`;
93+
})();
94+
const newStructure = {root_folder: structure.root_folder, last_folder: folderId, files: null};
95+
const response = await fetch(fetchUrl);
96+
if (response.ok) {
97+
const body = await response.json();
98+
newStructure.files = body.files;
99+
} else {
100+
console.error(response);
101+
}
102+
setStructure(newStructure);
103+
}
104+
105+
function refreshStructure() {
106+
console.log('refreshStructure');
107+
setStructure({...structure});
108+
}
109+
110+
function handleUpload(folderId, files) {
111+
fetchFiles(folderId);
112+
selectFile(files[0]);
113+
}
114+
115+
return structure.root_folder && (<>
116+
<div className="wrapper">
117+
<Menu
118+
lastFolderId={structure.last_folder}
119+
fetchFiles={fetchFiles}
120+
openUploader={() => uploaderRef.current.openUploader()}
121+
/>
122+
<div className="browser-body">
123+
<nav className="folder-structure">
124+
<ul>
125+
<FolderStructure
126+
baseUrl={baseUrl}
127+
folder={structure.root_folder}
128+
lastFolderId={structure.last_folder}
129+
fetchFiles={fetchFiles}
130+
refreshStructure={refreshStructure}
131+
/>
132+
</ul>
133+
</nav>
134+
<FileUploader
135+
folderId={structure.last_folder}
136+
handleUpload={handleUpload}
137+
ref={uploaderRef}
138+
settings={{csrf_token: csrfToken, base_url: baseUrl}}
139+
>{
140+
structure.files === null ?
141+
<div className="status">{gettext("Loading files…")}</div> :
142+
<FilesList files={structure.files} selectFile={selectFile} />
143+
}</FileUploader>
144+
</div>
145+
</div>
146+
<Tooltip id="django-finder-tooltip" place="bottom-start" />
147+
</>);
148+
}
Lines changed: 87 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,107 @@
1-
import React, {
2-
lazy,
3-
memo,
4-
Suspense,
5-
useEffect,
6-
useMemo,
7-
useRef,
8-
useState,
9-
} from 'react';
10-
import {Tooltip} from 'react-tooltip';
11-
import FigureLabels from '../finder/FigureLabels';
12-
import FileUploader from '../finder/FileUploader';
13-
import FolderStructure from './FolderStructure';
14-
import Menu from './Menu';
15-
16-
17-
function StaticFigure(props) {
18-
return (<>{props.children}</>);
19-
}
20-
21-
22-
function Figure(props) {
23-
const FigBody = useMemo(() => {
24-
if (props.browser_component) {
25-
const component = `./components/folderitem/${props.browser_component}.js`;
26-
const LazyItem = lazy(() => import(component));
27-
return (props) => (
28-
<Suspense>
29-
<LazyItem {...props}>{props.children}</LazyItem>
30-
</Suspense>
31-
);
32-
}
33-
return StaticFigure;
34-
},[]);
35-
36-
return (
37-
<figure className="figure">
38-
<FigBody {...props}>
39-
<FigureLabels labels={props.labels}>
40-
<img src={props.thumbnail_url} {...props.listeners} {...props.attributes} />
41-
</FigureLabels>
42-
</FigBody>
43-
<figcaption>
44-
{props.name}
45-
</figcaption>
46-
</figure>
47-
);
48-
}
49-
50-
51-
const FilesList = memo((props: any) => {
52-
const {files} = props;
53-
54-
function selectFile(file) {
55-
console.log(file);
56-
}
57-
58-
console.log('FolderList', files);
59-
60-
return (
61-
<ul className="files-list">{
62-
files.length === 0 ?
63-
<li className="status">{gettext("Empty folder")}</li> :
64-
files.map(file => (
65-
<li key={file.id} onClick={() => selectFile(file)}><Figure {...file} /></li>
66-
))}
67-
</ul>
68-
);
69-
});
1+
import React, {useEffect, useRef, useState} from 'react';
2+
import FileSelectDialog from './FileSelectDialog';
3+
import CloseIcon from '../icons/close.svg';
704

715

726
export default function FinderFileSelect(props) {
7+
const shadowRoot = props.container;
738
const baseUrl = props['base-url'];
74-
const [structure, setStructure] = useState({root_folder: null, last_folder: null, files: null});
75-
const folderListRef = useRef(null);
76-
const uploaderRef = useRef(null);
9+
const styleUrl = props['style-url'];
10+
const [selectedFile, setSelectedFile] = useState(props['selected-file']);
11+
const slotRef = useRef(null);
12+
const dialogRef = useRef(null);
13+
const csrfToken = shadowRoot.host.closest('form')?.querySelector('input[name="csrfmiddlewaretoken"]')?.value;
7714

7815
useEffect(() => {
79-
getStructure();
16+
// Create a styles element for the shadow DOM
17+
const link = document.createElement('link');
18+
link.href = styleUrl;
19+
link.media = 'all';
20+
link.rel = 'stylesheet';
21+
shadowRoot.insertBefore(link, shadowRoot.firstChild);
8022
}, []);
8123

82-
async function getStructure() {
83-
const response = await fetch(`${baseUrl}structure/${props.realm}`);
84-
if (response.ok) {
85-
setStructure(await response.json());
86-
} else {
87-
console.error(response);
24+
useEffect(() => {
25+
const handleEscape = (event) => {
26+
if (event.key === 'Escape') {
27+
closeDialog();
28+
}
29+
};
30+
window.addEventListener('keydown', handleEscape);
31+
return () => {
32+
window.removeEventListener('keydown', handleEscape);
8833
}
34+
}, []);
35+
36+
function openDialog() {
37+
dialogRef.current.showModal();
8938
}
9039

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);
101-
if (response.ok) {
102-
const body = await response.json();
103-
newStructure.files = body.files;
104-
} else {
105-
console.error(response);
106-
}
107-
setStructure(newStructure);
40+
function closeDialog() {
41+
dialogRef.current.close();
10842
}
10943

110-
function refreshStructure() {
111-
console.log('refreshStructure');
112-
setStructure({...structure});
44+
function deleteFile() {
45+
setSelectedFile(null);
11346
}
11447

115-
function handleUpload(folderId) {
116-
fetchFiles(folderId);
48+
function selectFile(file) {
49+
setSelectedFile(file);
50+
const inputElement = slotRef.current.assignedElements()[0];
51+
if (inputElement instanceof HTMLInputElement) {
52+
inputElement.value = file.id;
53+
}
54+
closeDialog();
11755
}
11856

119-
return 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
133-
lastFolderId={structure.last_folder}
134-
fetchFiles={fetchFiles}
135-
openUploader={() => uploaderRef.current.openUploader()}
136-
/>
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>
57+
function renderTimestamp(timestamp) {
58+
const date = new Date(timestamp);
59+
return date.toLocaleString();
60+
}
61+
62+
return (<>
63+
<slot ref={slotRef} />
64+
<div className="finder-file-select">
65+
<figure>{selectedFile ? <>
66+
<img src={selectedFile.thumbnail_url} onClick={openDialog} onDragEnter={openDialog} />
67+
<figcaption>
68+
<dl>
69+
<dt>{gettext("Name")}:</dt>
70+
<dd>{selectedFile.name}</dd>
71+
</dl>
72+
<dl>
73+
<dt>{gettext("Content-Type (Size)")}:</dt>
74+
<dd>{selectedFile.mime_type} ({selectedFile.file_size})</dd>
75+
</dl>
76+
<dl>
77+
<dt>{gettext("Modified at")}:</dt>
78+
<dd>{renderTimestamp(selectedFile.last_modified_at)}</dd>
79+
</dl>
80+
<div>
81+
<button type="button" onClick={deleteFile}>{gettext("Delete")}</button>
82+
</div>
83+
</figcaption>
84+
</> :
85+
<span onClick={openDialog} onDragEnter={openDialog}>
86+
<p>{gettext("Select File")}</p>
87+
</span>
88+
}</figure>
14789
</div>
148-
<Tooltip id="django-finder-tooltip" place="bottom-start" />
90+
<dialog ref={dialogRef}>
91+
<FileSelectDialog
92+
baseUrl={baseUrl}
93+
csrfToken={csrfToken}
94+
realm={props.realm}
95+
selectFile={selectFile}
96+
/>
97+
<div
98+
className="close-button"
99+
role="button"
100+
onClick={closeDialog}
101+
aria-label={gettext("Close dialog")}
102+
>
103+
<CloseIcon/>
104+
</div>
105+
</dialog>
149106
</>);
150107
}

0 commit comments

Comments
 (0)