Skip to content

Commit

Permalink
feat: infinite scroll for file browser view
Browse files Browse the repository at this point in the history
  • Loading branch information
jrief committed Nov 13, 2024
1 parent 7bceb0c commit 8137860
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 90 deletions.
126 changes: 94 additions & 32 deletions client/browser/FileSelectDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React, {
useRef,
useState,
} from 'react';
import {InView} from 'react-intersection-observer';
import {useInView} from 'react-intersection-observer';
import {Tooltip} from 'react-tooltip';
import FigureLabels from '../common/FigureLabels';
import FileUploader from '../common/FileUploader';
Expand Down Expand Up @@ -50,26 +50,41 @@ function Figure(props) {
}


const FilesList = memo((props: any) => {
const {files, numFiles, selectFile} = props;
const [{offset, limit}, setOffset] = useState({offset: 0, limit: 10});
const ScrollSpy = (props) => {
const {fetchFiles} = props;
const {ref, inView} = useInView({
triggerOnce: true,
onChange: (loadMore) => {
if (loadMore && !inView) {
fetchFiles();
}
},
});
console.log('ScrollSpy', inView);
if (inView) {
console.log('already visible');
fetchFiles();
}

console.log('FolderList', numFiles, files);
return (
<div className="scroll-spy" ref={ref}></div>
);
};

function loadMore(inView, entry) {
if (inView) {
console.log('load more:', entry.target);
}
}

const FilesList = memo((props: any) => {
const {structure, fetchFiles, selectFile} = props;

console.log('FolderList', structure);

return (
<ul className="files-browser">{
files.length === 0 ?
structure.files.length === 0 ?
<li className="status">{gettext("Empty folder")}</li> : (
<>{files.map(file => (
<>{structure.files.map(file => (
<li key={file.id} onClick={() => selectFile(file)}><Figure {...file} /></li>
))}
{numFiles > files.length && <InView as="li" onChange={loadMore} />}
{structure.offset !== null && <ScrollSpy fetchFiles={fetchFiles} />}
</>
)}</ul>
);
Expand All @@ -82,7 +97,8 @@ export default function FileSelectDialog(props) {
root_folder: null,
last_folder: null,
files: null,
num_files: 0,
offset: null,
search_query: '',
labels: [],
});
const [uploadedFile, setUploadedFile] = useState(null);
Expand Down Expand Up @@ -110,6 +126,48 @@ export default function FileSelectDialog(props) {
};
}, [dialog]);

const setCurrentFolder = (folderId) => {
setStructure(prevStructure => {
const newStructure = Object.assign(structure, {
...prevStructure,
last_folder: folderId,
files: [],
offset: null,
search_query: '',
});
fetchFiles();
return newStructure;
});
};

const setSearchQuery = (query) => {
setStructure(prevStructure => {
const newStructure = Object.assign(structure, {
...prevStructure,
files: [],
offset: null,
search_query: query,
});
fetchFiles();
return newStructure;
});
};

const refreshFilesList = () => {
setStructure(prevStructure => {
const newStructure = Object.assign(structure, {
root_folder: prevStructure.root_folder,
files: [],
last_folder: prevStructure.last_folder,
offset: null,
search_query: prevStructure.search_query,
labels: prevStructure.labels,
});
fetchFiles();
return newStructure;
});
};

async function getStructure() {
const response = await fetch(`${baseUrl}structure/${realm}`);
if (response.ok) {
Expand All @@ -119,29 +177,29 @@ export default function FileSelectDialog(props) {
}
}

async function fetchFiles(folderId: string, searchQuery='') {
async function fetchFiles() {
const fetchUrl = (() => {
if (searchQuery) {
const params = new URLSearchParams({q: searchQuery});
return `${baseUrl}${folderId}/search?${params.toString()}`;
const params = new URLSearchParams();
if (structure.offset !== null) {
params.set('offset', String(structure.offset));
}
return `${baseUrl}${folderId}/list`;
if (structure.search_query) {
params.set('q', structure.search_query);
return `${baseUrl}${structure.last_folder}/search?${params.toString()}`;
}
return `${baseUrl}${structure.last_folder}/list?${params.toString()}`;
})();
const newStructure = {
root_folder: structure.root_folder,
last_folder: folderId,
files: null,
num_files: 0,
labels: structure.labels,
};
const response = await fetch(fetchUrl);
if (response.ok) {
const body = await response.json();
newStructure.files = body.files;
setStructure({
...structure,
files: structure.files.concat(body.files),
offset: body.offset,
});
} else {
console.error(response);
}
setStructure(newStructure);
}

function refreshStructure() {
Expand All @@ -162,8 +220,8 @@ export default function FileSelectDialog(props) {
settings={{csrfToken, baseUrl, selectFile, labels: structure.labels}}
/> : <>
<MenuBar
lastFolderId={structure.last_folder}
fetchFiles={fetchFiles}
refreshFilesList={refreshFilesList}
setSearchQuery={setSearchQuery}
openUploader={() => uploaderRef.current.openUploader()}
labels={structure.labels}
/>
Expand All @@ -174,7 +232,7 @@ export default function FileSelectDialog(props) {
baseUrl={baseUrl}
folder={structure.root_folder}
lastFolderId={structure.last_folder}
fetchFiles={fetchFiles}
setCurrentFolder={setCurrentFolder}
refreshStructure={refreshStructure}
/>}
</ul>
Expand All @@ -187,7 +245,11 @@ export default function FileSelectDialog(props) {
>{
structure.files === null ?
<div className="status">{gettext("Loading files…")}</div> :
<FilesList files={structure.files} numFiles={structure.num_files} selectFile={selectFile} />
<FilesList
structure={structure}
fetchFiles={fetchFiles}
selectFile={selectFile}
/>
}</FileUploader>
</div>
</>}
Expand Down
12 changes: 6 additions & 6 deletions client/browser/FolderStructure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import RootIcon from '../icons/root.svg';


function FolderEntry(props) {
const {folder, toggleOpen, fetchFiles, isCurrent} = props;
const {folder, toggleOpen, setCurrentFolder, isCurrent} = props;

if (folder.is_root) {
return (<span onClick={() => fetchFiles(folder.id)}><RootIcon/></span>);
return (<span onClick={() => setCurrentFolder(folder.id)}><RootIcon/></span>);
}

return (<>
Expand All @@ -20,7 +20,7 @@ function FolderEntry(props) {
}</i>
{isCurrent ?
<strong><FolderOpenIcon/>{folder.name}</strong> :
<span onClick={() => fetchFiles(folder.id)} role="button">
<span onClick={() => setCurrentFolder(folder.id)} role="button">
<FolderIcon/>{folder.name}
</span>
}
Expand All @@ -29,7 +29,7 @@ function FolderEntry(props) {


export default function FolderStructure(props) {
const {baseUrl, folder, lastFolderId, fetchFiles, refreshStructure} = props;
const {baseUrl, folder, lastFolderId, setCurrentFolder, refreshStructure} = props;

async function fetchChildren() {
const response = await fetch(`${baseUrl}${folder.id}/fetch`);
Expand Down Expand Up @@ -62,7 +62,7 @@ export default function FolderStructure(props) {
<FolderEntry
folder={folder}
toggleOpen={toggleOpen}
fetchFiles={fetchFiles}
setCurrentFolder={setCurrentFolder}
isCurrent={lastFolderId === folder.id}
/>
{folder.is_open && (
Expand All @@ -73,7 +73,7 @@ export default function FolderStructure(props) {
baseUrl={baseUrl}
folder={child}
lastFolderId={lastFolderId}
fetchFiles={fetchFiles}
setCurrentFolder={setCurrentFolder}
refreshStructure={refreshStructure}
/>
))}
Expand Down
12 changes: 5 additions & 7 deletions client/browser/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import UploadIcon from '../icons/upload.svg';


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

function handleSearch(event) {
const performSearch = () => {
const searchQuery = searchRef.current.value || '';
fetchFiles(lastFolderId, searchQuery);
setSearchQuery(searchQuery);
};
const resetSearch = () => {
fetchFiles(lastFolderId);
setSearchQuery('');
};

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

console.log('Menu', lastFolderId);

return (
<ul role="menubar">
<li role="menuitem" className="search-field">
Expand All @@ -75,8 +73,8 @@ export default function MenuBar(props) {
</DropDownMenu>
</div>
</li>
<SortingOptions refreshFilesList={() => fetchFiles(lastFolderId)}/>
{labels && <FilterByLabel refreshFilesList={() => fetchFiles(lastFolderId)} labels={labels} />}
<SortingOptions refreshFilesList={refreshFilesList}/>
{labels && <FilterByLabel refreshFilesList={refreshFilesList} labels={labels} />}
<li role="menuitem" onClick={openUploader} data-tooltip-id="django-finder-tooltip"
data-tooltip-content={gettext("Upload file")}>
<UploadIcon/>
Expand Down
51 changes: 12 additions & 39 deletions client/scss/_menubar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@
min-width: 100px;
height: 100%;
display: block;
color: $body-fg-color;
background-color: $body-bg-color;
margin: 0;
padding: 2px 8px;
border: 1px solid $border-color;
Expand Down Expand Up @@ -86,28 +84,24 @@

.search-realm {
display: flex;
// [role="combobox"] {
// li:nth-child(2) {
// padding-bottom: 4px;
// border-bottom: 1px solid $border-color;
// margin-bottom: 4px;
// }
// }
}
}
}

&:not(:has(> input:placeholder-shown)) {
background-color: $selected-row-color;
> input {
background-color: $selected-row-color;
}
}

&:has(> input:focus-visible) {
box-shadow: $focus-visible-box-shadow;
}
}

&[aria-haspopup="listbox"]:has(>[role~="listbox"]) {
width: auto;

//&.with-caret {
// padding-right: 0.25rem;
//}
}

&.sorting-options {
Expand All @@ -119,6 +113,7 @@

&.filter-by-label {
margin-right: auto;

ul > li {
input[type="checkbox"] {
vertical-align: initial;
Expand All @@ -132,21 +127,15 @@
margin-right: 0.5rem;
}
}
}

//&.filter-by-label + &.sorting-options {
// margin-left: auto;
//}
&:has(input:checked) {
background-color: $selected-row-color;
}
}

&.extra-menu [role~="listbox"] {
right: 0;
margin-inline-end: inherit;

//> li:nth-child(5):not(:last-child) {
// padding-bottom: 4px;
// border-bottom: 1px solid $border-color;
// margin-bottom: 4px;
//}
}

svg {
Expand All @@ -165,22 +154,6 @@
left: -250px;
right: -250px;

//li {
// padding: 0 2rem 0 0.5rem;
// width: auto;
// cursor: pointer;
// display: flex;
// align-items: center;
// line-height: 32px;
// font-size: 16px;
//
// &.active::after {
// content: "✔";
// position: absolute;
// right: 10px;
// }
//}

svg {
color: $body-fg-color;
}
Expand Down
Loading

0 comments on commit 8137860

Please sign in to comment.