Skip to content

Commit 8137860

Browse files
committed
feat: infinite scroll for file browser view
1 parent 7bceb0c commit 8137860

File tree

6 files changed

+143
-90
lines changed

6 files changed

+143
-90
lines changed

client/browser/FileSelectDialog.tsx

Lines changed: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, {
77
useRef,
88
useState,
99
} from 'react';
10-
import {InView} from 'react-intersection-observer';
10+
import {useInView} from 'react-intersection-observer';
1111
import {Tooltip} from 'react-tooltip';
1212
import FigureLabels from '../common/FigureLabels';
1313
import FileUploader from '../common/FileUploader';
@@ -50,26 +50,41 @@ function Figure(props) {
5050
}
5151

5252

53-
const FilesList = memo((props: any) => {
54-
const {files, numFiles, selectFile} = props;
55-
const [{offset, limit}, setOffset] = useState({offset: 0, limit: 10});
53+
const ScrollSpy = (props) => {
54+
const {fetchFiles} = props;
55+
const {ref, inView} = useInView({
56+
triggerOnce: true,
57+
onChange: (loadMore) => {
58+
if (loadMore && !inView) {
59+
fetchFiles();
60+
}
61+
},
62+
});
63+
console.log('ScrollSpy', inView);
64+
if (inView) {
65+
console.log('already visible');
66+
fetchFiles();
67+
}
5668

57-
console.log('FolderList', numFiles, files);
69+
return (
70+
<div className="scroll-spy" ref={ref}></div>
71+
);
72+
};
5873

59-
function loadMore(inView, entry) {
60-
if (inView) {
61-
console.log('load more:', entry.target);
62-
}
63-
}
74+
75+
const FilesList = memo((props: any) => {
76+
const {structure, fetchFiles, selectFile} = props;
77+
78+
console.log('FolderList', structure);
6479

6580
return (
6681
<ul className="files-browser">{
67-
files.length === 0 ?
82+
structure.files.length === 0 ?
6883
<li className="status">{gettext("Empty folder")}</li> : (
69-
<>{files.map(file => (
84+
<>{structure.files.map(file => (
7085
<li key={file.id} onClick={() => selectFile(file)}><Figure {...file} /></li>
7186
))}
72-
{numFiles > files.length && <InView as="li" onChange={loadMore} />}
87+
{structure.offset !== null && <ScrollSpy fetchFiles={fetchFiles} />}
7388
</>
7489
)}</ul>
7590
);
@@ -82,7 +97,8 @@ export default function FileSelectDialog(props) {
8297
root_folder: null,
8398
last_folder: null,
8499
files: null,
85-
num_files: 0,
100+
offset: null,
101+
search_query: '',
86102
labels: [],
87103
});
88104
const [uploadedFile, setUploadedFile] = useState(null);
@@ -110,6 +126,48 @@ export default function FileSelectDialog(props) {
110126
};
111127
}, [dialog]);
112128

129+
const setCurrentFolder = (folderId) => {
130+
setStructure(prevStructure => {
131+
const newStructure = Object.assign(structure, {
132+
...prevStructure,
133+
last_folder: folderId,
134+
files: [],
135+
offset: null,
136+
search_query: '',
137+
});
138+
fetchFiles();
139+
return newStructure;
140+
});
141+
};
142+
143+
const setSearchQuery = (query) => {
144+
setStructure(prevStructure => {
145+
const newStructure = Object.assign(structure, {
146+
...prevStructure,
147+
files: [],
148+
offset: null,
149+
search_query: query,
150+
});
151+
fetchFiles();
152+
return newStructure;
153+
});
154+
};
155+
156+
const refreshFilesList = () => {
157+
setStructure(prevStructure => {
158+
const newStructure = Object.assign(structure, {
159+
root_folder: prevStructure.root_folder,
160+
files: [],
161+
last_folder: prevStructure.last_folder,
162+
offset: null,
163+
search_query: prevStructure.search_query,
164+
labels: prevStructure.labels,
165+
});
166+
fetchFiles();
167+
return newStructure;
168+
});
169+
};
170+
113171
async function getStructure() {
114172
const response = await fetch(`${baseUrl}structure/${realm}`);
115173
if (response.ok) {
@@ -119,29 +177,29 @@ export default function FileSelectDialog(props) {
119177
}
120178
}
121179

122-
async function fetchFiles(folderId: string, searchQuery='') {
180+
async function fetchFiles() {
123181
const fetchUrl = (() => {
124-
if (searchQuery) {
125-
const params = new URLSearchParams({q: searchQuery});
126-
return `${baseUrl}${folderId}/search?${params.toString()}`;
182+
const params = new URLSearchParams();
183+
if (structure.offset !== null) {
184+
params.set('offset', String(structure.offset));
127185
}
128-
return `${baseUrl}${folderId}/list`;
186+
if (structure.search_query) {
187+
params.set('q', structure.search_query);
188+
return `${baseUrl}${structure.last_folder}/search?${params.toString()}`;
189+
}
190+
return `${baseUrl}${structure.last_folder}/list?${params.toString()}`;
129191
})();
130-
const newStructure = {
131-
root_folder: structure.root_folder,
132-
last_folder: folderId,
133-
files: null,
134-
num_files: 0,
135-
labels: structure.labels,
136-
};
137192
const response = await fetch(fetchUrl);
138193
if (response.ok) {
139194
const body = await response.json();
140-
newStructure.files = body.files;
195+
setStructure({
196+
...structure,
197+
files: structure.files.concat(body.files),
198+
offset: body.offset,
199+
});
141200
} else {
142201
console.error(response);
143202
}
144-
setStructure(newStructure);
145203
}
146204

147205
function refreshStructure() {
@@ -162,8 +220,8 @@ export default function FileSelectDialog(props) {
162220
settings={{csrfToken, baseUrl, selectFile, labels: structure.labels}}
163221
/> : <>
164222
<MenuBar
165-
lastFolderId={structure.last_folder}
166-
fetchFiles={fetchFiles}
223+
refreshFilesList={refreshFilesList}
224+
setSearchQuery={setSearchQuery}
167225
openUploader={() => uploaderRef.current.openUploader()}
168226
labels={structure.labels}
169227
/>
@@ -174,7 +232,7 @@ export default function FileSelectDialog(props) {
174232
baseUrl={baseUrl}
175233
folder={structure.root_folder}
176234
lastFolderId={structure.last_folder}
177-
fetchFiles={fetchFiles}
235+
setCurrentFolder={setCurrentFolder}
178236
refreshStructure={refreshStructure}
179237
/>}
180238
</ul>
@@ -187,7 +245,11 @@ export default function FileSelectDialog(props) {
187245
>{
188246
structure.files === null ?
189247
<div className="status">{gettext("Loading files…")}</div> :
190-
<FilesList files={structure.files} numFiles={structure.num_files} selectFile={selectFile} />
248+
<FilesList
249+
structure={structure}
250+
fetchFiles={fetchFiles}
251+
selectFile={selectFile}
252+
/>
191253
}</FileUploader>
192254
</div>
193255
</>}

client/browser/FolderStructure.tsx

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

99

1010
function FolderEntry(props) {
11-
const {folder, toggleOpen, fetchFiles, isCurrent} = props;
11+
const {folder, toggleOpen, setCurrentFolder, isCurrent} = props;
1212

1313
if (folder.is_root) {
14-
return (<span onClick={() => fetchFiles(folder.id)}><RootIcon/></span>);
14+
return (<span onClick={() => setCurrentFolder(folder.id)}><RootIcon/></span>);
1515
}
1616

1717
return (<>
@@ -20,7 +20,7 @@ function FolderEntry(props) {
2020
}</i>
2121
{isCurrent ?
2222
<strong><FolderOpenIcon/>{folder.name}</strong> :
23-
<span onClick={() => fetchFiles(folder.id)} role="button">
23+
<span onClick={() => setCurrentFolder(folder.id)} role="button">
2424
<FolderIcon/>{folder.name}
2525
</span>
2626
}
@@ -29,7 +29,7 @@ function FolderEntry(props) {
2929

3030

3131
export default function FolderStructure(props) {
32-
const {baseUrl, folder, lastFolderId, fetchFiles, refreshStructure} = props;
32+
const {baseUrl, folder, lastFolderId, setCurrentFolder, refreshStructure} = props;
3333

3434
async function fetchChildren() {
3535
const response = await fetch(`${baseUrl}${folder.id}/fetch`);
@@ -62,7 +62,7 @@ export default function FolderStructure(props) {
6262
<FolderEntry
6363
folder={folder}
6464
toggleOpen={toggleOpen}
65-
fetchFiles={fetchFiles}
65+
setCurrentFolder={setCurrentFolder}
6666
isCurrent={lastFolderId === folder.id}
6767
/>
6868
{folder.is_open && (
@@ -73,7 +73,7 @@ export default function FolderStructure(props) {
7373
baseUrl={baseUrl}
7474
folder={child}
7575
lastFolderId={lastFolderId}
76-
fetchFiles={fetchFiles}
76+
setCurrentFolder={setCurrentFolder}
7777
refreshStructure={refreshStructure}
7878
/>
7979
))}

client/browser/MenuBar.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import UploadIcon from '../icons/upload.svg';
88

99

1010
export default function MenuBar(props) {
11-
const {lastFolderId, fetchFiles, openUploader, labels} = props;
11+
const {refreshFilesList, setSearchQuery, openUploader, labels} = props;
1212
const searchRef = useRef(null);
1313
const [searchRealm, setSearchRealm] = useSearchRealm('current');
1414

1515
function handleSearch(event) {
1616
const performSearch = () => {
1717
const searchQuery = searchRef.current.value || '';
18-
fetchFiles(lastFolderId, searchQuery);
18+
setSearchQuery(searchQuery);
1919
};
2020
const resetSearch = () => {
21-
fetchFiles(lastFolderId);
21+
setSearchQuery('');
2222
};
2323

2424
if (event.type === 'change' && searchRef.current.value.length === 0) {
@@ -47,8 +47,6 @@ export default function MenuBar(props) {
4747
};
4848
}
4949

50-
console.log('Menu', lastFolderId);
51-
5250
return (
5351
<ul role="menubar">
5452
<li role="menuitem" className="search-field">
@@ -75,8 +73,8 @@ export default function MenuBar(props) {
7573
</DropDownMenu>
7674
</div>
7775
</li>
78-
<SortingOptions refreshFilesList={() => fetchFiles(lastFolderId)}/>
79-
{labels && <FilterByLabel refreshFilesList={() => fetchFiles(lastFolderId)} labels={labels} />}
76+
<SortingOptions refreshFilesList={refreshFilesList}/>
77+
{labels && <FilterByLabel refreshFilesList={refreshFilesList} labels={labels} />}
8078
<li role="menuitem" onClick={openUploader} data-tooltip-id="django-finder-tooltip"
8179
data-tooltip-content={gettext("Upload file")}>
8280
<UploadIcon/>

client/scss/_menubar.scss

Lines changed: 12 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@
4848
min-width: 100px;
4949
height: 100%;
5050
display: block;
51-
color: $body-fg-color;
52-
background-color: $body-bg-color;
5351
margin: 0;
5452
padding: 2px 8px;
5553
border: 1px solid $border-color;
@@ -86,28 +84,24 @@
8684

8785
.search-realm {
8886
display: flex;
89-
// [role="combobox"] {
90-
// li:nth-child(2) {
91-
// padding-bottom: 4px;
92-
// border-bottom: 1px solid $border-color;
93-
// margin-bottom: 4px;
94-
// }
95-
// }
9687
}
9788
}
9889
}
9990

91+
&:not(:has(> input:placeholder-shown)) {
92+
background-color: $selected-row-color;
93+
> input {
94+
background-color: $selected-row-color;
95+
}
96+
}
97+
10098
&:has(> input:focus-visible) {
10199
box-shadow: $focus-visible-box-shadow;
102100
}
103101
}
104102

105103
&[aria-haspopup="listbox"]:has(>[role~="listbox"]) {
106104
width: auto;
107-
108-
//&.with-caret {
109-
// padding-right: 0.25rem;
110-
//}
111105
}
112106

113107
&.sorting-options {
@@ -119,6 +113,7 @@
119113

120114
&.filter-by-label {
121115
margin-right: auto;
116+
122117
ul > li {
123118
input[type="checkbox"] {
124119
vertical-align: initial;
@@ -132,21 +127,15 @@
132127
margin-right: 0.5rem;
133128
}
134129
}
135-
}
136130

137-
//&.filter-by-label + &.sorting-options {
138-
// margin-left: auto;
139-
//}
131+
&:has(input:checked) {
132+
background-color: $selected-row-color;
133+
}
134+
}
140135

141136
&.extra-menu [role~="listbox"] {
142137
right: 0;
143138
margin-inline-end: inherit;
144-
145-
//> li:nth-child(5):not(:last-child) {
146-
// padding-bottom: 4px;
147-
// border-bottom: 1px solid $border-color;
148-
// margin-bottom: 4px;
149-
//}
150139
}
151140

152141
svg {
@@ -165,22 +154,6 @@
165154
left: -250px;
166155
right: -250px;
167156

168-
//li {
169-
// padding: 0 2rem 0 0.5rem;
170-
// width: auto;
171-
// cursor: pointer;
172-
// display: flex;
173-
// align-items: center;
174-
// line-height: 32px;
175-
// font-size: 16px;
176-
//
177-
// &.active::after {
178-
// content: "✔";
179-
// position: absolute;
180-
// right: 10px;
181-
// }
182-
//}
183-
184157
svg {
185158
color: $body-fg-color;
186159
}

0 commit comments

Comments
 (0)