Skip to content

Commit 79d9d55

Browse files
committed
client side code to handle Folder selection
1 parent 7dddb4f commit 79d9d55

File tree

6 files changed

+226
-19
lines changed

6 files changed

+226
-19
lines changed

client/browser/FileSelectDialog.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const FileSelectDialog = forwardRef((props: any, forwardedRef) => {
116116
}
117117
}, [isDirty]);
118118

119-
function setCurrentFolder(folderId){
119+
function setCurrentFolderId(folderId){
120120
setStructure({
121121
...structure,
122122
last_folder: folderId,
@@ -185,7 +185,7 @@ const FileSelectDialog = forwardRef((props: any, forwardedRef) => {
185185
async function fetchFiles() {
186186
const fetchUrl = (() => {
187187
const params = new URLSearchParams();
188-
mimeTypes.forEach(type => params.append('mimetypes', type));
188+
mimeTypes?.forEach(type => params.append('mimetypes', type));
189189
if (structure.recursive) {
190190
params.set('recursive', '');
191191
}
@@ -219,12 +219,22 @@ const FileSelectDialog = forwardRef((props: any, forwardedRef) => {
219219
}
220220

221221
const selectFile = useCallback(fileInfo => {
222-
props.selectFile(fileInfo);
223-
setUploadedFile(null);
224-
refreshFilesList();
225-
props.dialogRef.current.close();
222+
if (props.selectFile) {
223+
props.selectFile(fileInfo);
224+
setUploadedFile(null);
225+
refreshFilesList();
226+
props.dialogRef.current.close();
227+
}
226228
}, []);
227229

230+
const selectFolder = useCallback(folder => {
231+
setCurrentFolderId(folder.id);
232+
if (props.selectFolder) {
233+
props.selectFolder(folder);
234+
props.dialogRef.current.close();
235+
}
236+
}, [structure.last_folder]);
237+
228238
function scrollToCurrentFolder() {
229239
if (currentFolderElement) {
230240
currentFolderElement.scrollIntoView({behavior: 'smooth', block: 'center'});
@@ -277,10 +287,11 @@ const FileSelectDialog = forwardRef((props: any, forwardedRef) => {
277287
baseUrl={baseUrl}
278288
folder={structure.root_folder}
279289
lastFolderId={structure.last_folder}
280-
setCurrentFolder={setCurrentFolder}
290+
selectFolder={selectFolder}
281291
toggleRecursive={toggleRecursive}
282292
refreshStructure={() => setStructure({...structure})}
283293
isListed={structure.recursive ? false : null}
294+
setCurrentFolderId={setCurrentFolderId}
284295
setCurrentFolderElement={setCurrentFolderElement}
285296
/>}
286297
</ul>
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import React, {useEffect, useRef, useState} from 'react';
2+
import FileSelectDialog from './FileSelectDialog';
3+
4+
5+
interface SelectedFolder {
6+
id: string;
7+
name: string;
8+
last_modified_at: string;
9+
summary: string;
10+
meta_data: object;
11+
}
12+
13+
function parseDataset(dataset: string|object) : SelectedFolder|null {
14+
const data = typeof dataset === 'string' ? JSON.parse(dataset) : dataset;
15+
if (data) {
16+
const {
17+
id,
18+
name,
19+
last_modified_at,
20+
summary,
21+
meta_data,
22+
} = data;
23+
return {
24+
id,
25+
name,
26+
last_modified_at,
27+
summary,
28+
meta_data,
29+
} as SelectedFolder;
30+
}
31+
return null;
32+
}
33+
34+
35+
export default function FinderFolderSelect(props) {
36+
const shadowRoot = props.container;
37+
const baseUrl = props['base-url'];
38+
const styleUrl = props['style-url'];
39+
const folderIconUrl = props['folder-icon-url'];
40+
const selectRef = useRef(null);
41+
const slotRef = useRef(null);
42+
const dialogRef = useRef(null);
43+
const [selectedFolder, setSelectedFolder] = useState<SelectedFolder>(null);
44+
const csrfToken = getCSRFToken();
45+
const uuid5Regex = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
46+
47+
useEffect(() => {
48+
// Create a styles element for the shadow DOM
49+
const link = document.createElement('link');
50+
link.href = styleUrl;
51+
link.media = 'all';
52+
link.rel = 'stylesheet';
53+
shadowRoot.insertBefore(link, shadowRoot.firstChild);
54+
const inputElement = slotRef.current.assignedElements()[0];
55+
if (inputElement instanceof HTMLInputElement) {
56+
setSelectedFolder(parseDataset(inputElement.dataset.selected_folder));
57+
inputElement.addEventListener('change', valueChanged);
58+
}
59+
return () => {
60+
inputElement.removeEventListener('change', valueChanged);
61+
};
62+
}, []);
63+
64+
useEffect(() => {
65+
const handleEscape = (event) => {
66+
if (event.key === 'Escape') {
67+
selectRef.current.dismissAndClose();
68+
}
69+
};
70+
const preventDefault = (event) => {
71+
event.preventDefault();
72+
};
73+
window.addEventListener('keydown', handleEscape);
74+
75+
// prevent browser from loading a drag-and-dropped file
76+
window.addEventListener('dragover', preventDefault, false);
77+
window.addEventListener('drop', preventDefault, false);
78+
79+
return () => {
80+
window.removeEventListener('keydown', handleEscape);
81+
window.removeEventListener('dragover', preventDefault);
82+
window.removeEventListener('drop', preventDefault);
83+
}
84+
}, []);
85+
86+
async function valueChanged(event) {
87+
const folderId = event.target.value;
88+
if (!uuid5Regex.test(folderId)) {
89+
setSelectedFolder(null);
90+
return;
91+
}
92+
const response = await fetch(`${baseUrl}${folderId}/fetch`);
93+
if (response.ok) {
94+
const dataset = parseDataset(await response.json());
95+
const inputElement = slotRef.current.assignedElements()[0];
96+
if (inputElement instanceof HTMLInputElement) {
97+
inputElement.dataset.selected_file = JSON.stringify(dataset);
98+
}
99+
setSelectedFolder(dataset);
100+
} else {
101+
console.error(`Failed to fetch folder info for ID ${folderId}:`, response.statusText);
102+
}
103+
}
104+
105+
function getCSRFToken() {
106+
const csrfToken = shadowRoot.host.closest('form')?.querySelector('input[name="csrfmiddlewaretoken"]')?.value;
107+
if (csrfToken)
108+
return csrfToken;
109+
const parts = `; ${document.cookie}`.split('; csrftoken=');
110+
if (parts.length === 2)
111+
return parts.pop().split(';').shift();
112+
}
113+
114+
function openDialog() {
115+
dialogRef.current.showModal();
116+
selectRef.current.scrollToCurrentFolder();
117+
}
118+
119+
function removeFolder() {
120+
setSelectedFolder(null);
121+
const inputElement = slotRef.current.assignedElements()[0];
122+
if (inputElement instanceof HTMLInputElement) {
123+
inputElement.value = '';
124+
inputElement.dataset.selected_file = null;
125+
inputElement.checkValidity();
126+
}
127+
}
128+
129+
function selectFolder(folder) {
130+
if (folder) {
131+
setSelectedFolder(folder);
132+
const inputElement = slotRef.current.assignedElements()[0];
133+
if (inputElement instanceof HTMLInputElement) {
134+
inputElement.value = folder.id;
135+
inputElement.dataset.selected_folder = JSON.stringify(parseDataset(folder));
136+
inputElement.checkValidity();
137+
}
138+
}
139+
}
140+
141+
function renderTimestamp(timestamp) {
142+
const date = new Date(timestamp);
143+
return date.toLocaleString();
144+
}
145+
146+
return (<>
147+
<slot ref={slotRef} />
148+
<div className="finder-file-select">
149+
<figure>{selectedFolder ? <>
150+
<img src={folderIconUrl} onClick={openDialog} onDragEnter={openDialog} />
151+
<figcaption>
152+
<dl>
153+
<dt>{gettext("Name")}:</dt>
154+
<dd>{selectedFolder.name}</dd>
155+
</dl>
156+
<dl>
157+
<dt>{gettext("Details")}:</dt>
158+
<dd>{selectedFolder.summary}</dd>
159+
</dl>
160+
<dl>
161+
<dt>{gettext("Modified at")}:</dt>
162+
<dd>{renderTimestamp(selectedFolder.last_modified_at)}</dd>
163+
</dl>
164+
<button className="remove-file-button" type="button" onClick={removeFolder}>{gettext("Remove")}</button>
165+
</figcaption>
166+
</> :
167+
<span onClick={openDialog} onDragEnter={openDialog}>
168+
<p>{gettext("Select Folder")}</p>
169+
</span>
170+
}</figure>
171+
</div>
172+
<dialog ref={dialogRef}>
173+
<FileSelectDialog
174+
ref={selectRef}
175+
realm={props.realm}
176+
baseUrl={baseUrl}
177+
csrfToken={csrfToken}
178+
selectFolder={selectFolder}
179+
dialogRef={dialogRef}
180+
/>
181+
</dialog>
182+
</>);
183+
}

client/browser/FolderStructure.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import RootIcon from '../icons/root.svg';
88

99

1010
function FolderEntry(props) {
11-
const {folder, toggleOpen, setCurrentFolder, openRecursive, isCurrent, isListed, setCurrentFolderElement} = props;
11+
const {folder, toggleOpen, selectFolder, setCurrentFolderId, openRecursive, isCurrent, isListed, setCurrentFolderElement} = props;
1212
const ref = useRef(null);
1313

1414
if (folder.is_root) {
15-
return (<i onClick={() => setCurrentFolder(folder.id)}><RootIcon/></i>);
15+
return (<i onClick={() => selectFolder(folder)}><RootIcon/></i>);
1616
}
1717

1818
useEffect(() => {
@@ -26,16 +26,15 @@ function FolderEntry(props) {
2626
folder.is_open ? <ArrowDownIcon/> : <ArrowRightIcon/>
2727
}</i> : <i><EmptyIcon/></i>}
2828
<i onClick={() => openRecursive()} role="button">{isListed || isCurrent ? <FolderOpenIcon/> : <FolderIcon/>}</i>
29-
{isCurrent
30-
? <strong ref={ref}>{folder.name}</strong>
31-
: <span onClick={() => setCurrentFolder(folder.id)} role="button">{folder.name}</span>
32-
}
29+
<span onClick={() => selectFolder(folder)} ref={isCurrent ? ref : null} aria-current={isCurrent} role="button">
30+
{folder.name}
31+
</span>
3332
</>);
3433
}
3534

3635

3736
export default function FolderStructure(props) {
38-
const {baseUrl, folder, lastFolderId, setCurrentFolder, toggleRecursive, refreshStructure, setCurrentFolderElement} = props;
37+
const {baseUrl, folder, lastFolderId, selectFolder, toggleRecursive, refreshStructure, setCurrentFolderId, setCurrentFolderElement} = props;
3938
const isListed = props.isListed === false ? lastFolderId === folder.id : props.isListed;
4039
const isCurrent = lastFolderId === folder.id;
4140

@@ -78,7 +77,7 @@ export default function FolderStructure(props) {
7877
}
7978
await toggleRecursive(folder.id);
8079
} else {
81-
await setCurrentFolder(folder.id);
80+
await setCurrentFolderId(folder.id);
8281
}
8382
}
8483

@@ -87,10 +86,11 @@ export default function FolderStructure(props) {
8786
<FolderEntry
8887
folder={folder}
8988
toggleOpen={toggleOpen}
90-
setCurrentFolder={setCurrentFolder}
89+
selectFolder={selectFolder}
9190
openRecursive={openRecursive}
9291
isCurrent={isCurrent}
9392
isListed={isListed}
93+
setCurrentFolderId={setCurrentFolderId}
9494
setCurrentFolderElement={setCurrentFolderElement}
9595
/>
9696
{folder.is_open && folder.children && (
@@ -101,10 +101,11 @@ export default function FolderStructure(props) {
101101
baseUrl={baseUrl}
102102
folder={child}
103103
lastFolderId={lastFolderId}
104-
setCurrentFolder={setCurrentFolder}
104+
selectFolder={selectFolder}
105105
toggleRecursive={toggleRecursive}
106106
refreshStructure={refreshStructure}
107107
isListed={isListed}
108+
setCurrentFolderId={setCurrentFolderId}
108109
setCurrentFolderElement={setCurrentFolderElement}
109110
/>
110111
))}

client/finder-select.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import r2wc from '@r2wc/react-to-web-component';
22
import FinderFileSelect from './browser/FinderFileSelect';
3+
import FinderFolderSelect from './browser/FinderFolderSelect';
34

45

56
window.addEventListener('DOMContentLoaded', (event) => {
@@ -10,4 +11,11 @@ window.addEventListener('DOMContentLoaded', (event) => {
1011
shadow: 'open',
1112
}),
1213
);
14+
window.customElements.define(
15+
'finder-folder-select',
16+
r2wc(FinderFolderSelect, {
17+
props: {'base-url': 'string', 'style-url': 'string', realm: 'string', 'folder-icon-url': 'string'},
18+
shadow: 'open',
19+
}),
20+
);
1321
});

client/scss/finder-browser.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ dialog {
144144
span[role="button"] {
145145
cursor: pointer;
146146

147+
&[aria-current="true"] {
148+
font-weight: bold;
149+
}
150+
147151
&:hover {
148152
text-decoration: underline;
149153
text-decoration-style: dashed;

client/scss/finder-select.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
@use "tooltip";
22

3-
finder-file-select {
3+
finder-file-select, finder-folder-select {
44
// hide the original input field without loosing the ability to focus it
5-
input.finder-file-field {
5+
input.finder-hidden-input {
66
width: 0 !important;
77
height: 0 !important;
88
border: none !important;

0 commit comments

Comments
 (0)