Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

task/WG-418 - File Listing Component Enhancement and Fixes #337

Merged
merged 9 commits into from
Mar 5, 2025
1 change: 1 addition & 0 deletions react/src/components/FileBrowserModal/FileBrowserModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const FileBrowserModal = ({
}
open={isOpen}
onCancel={handleClose}
destroyOnClose={true}
footer={[
<Text key="fileCount" type="secondary" style={{ marginRight: 16 }}>
<Flex vertical justify="space-evenly">
Expand Down
145 changes: 110 additions & 35 deletions react/src/components/Files/FileListing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
} from '@hazmapper/hooks';
import { File, TTapisSystem } from '@hazmapper/types';
import { serializeToChonkyFile } from '@hazmapper/utils/fileUtils';
import { debounce } from 'lodash';

const DEFAULT_NO_FILE_EXTENSIONS: string[] = [];
const DEFAULT_FILE_LIMIT = 500;

const _FileBrowser = FileBrowser as React.MemoExoticComponent<
React.ForwardRefExoticComponent<
Expand Down Expand Up @@ -65,20 +67,28 @@

const { data: user } = useAuthenticatedUser();

const [chonkyFiles, setChonkyFiles] = React.useState<any>([]);
const [chonkyFiles, setChonkyFiles] = React.useState<any>(
new Array(8).fill(null)
);
const [folderChain, setFolderChain] = React.useState<any>([]);
const [path, setPath] = React.useState<string>('');
const [selectedSystem, setSelectedSystem] = React.useState<TTapisSystem>(
myDataSystem as TTapisSystem
);
const [hasError, setHasError] = React.useState(false);
const [loadingMoreFiles, setLoadingMoreFiles] = React.useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use the isLoading state from the useFiles hook instead?

const [hasMoreFiles, setHasMoreFiles] = React.useState(false);
const [listingState, setListingState] = React.useState({
path: '',
offset: 0,
});

const { data: files, refetch } = useFiles({
system: selectedSystem?.id || '',
path:
path || (selectedSystem?.id === myDataSystem?.id ? user?.username : ''),
offset: '0',
limit: '100' /* TODO https://tacc-main.atlassian.net/browse/WG-418 */,
listingState.path ||
(selectedSystem?.id === myDataSystem?.id ? user?.username : ''),
offset: listingState.offset.toString(),
limit: DEFAULT_FILE_LIMIT.toString(),
Comment on lines +88 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few suggestions for future improvement:

  • offset and limit should be type int, and adjust in the hook query params
  • react query has a nice infinite query hook. Used in tandem with InterSectionObserver, or react-intersection-observer, this would simplify this code a bit with the use of a ref for a node at the bottom of the listing, and a callback to the react-query built-in fetchNextPage() method. If gone that route, you'd be able to get rid of most of the useStates in this component

enabled: !!selectedSystem?.id,
});

Expand All @@ -87,33 +97,98 @@

const selectedSystemId = watch('system');

const setRootFolderChain = (rootPath, sys = selectedSystem) => {
let rootFolderName: string;

if (sys?.id === myDataSystem?.id) {
rootFolderName = 'My Data';
} else if (sys?.id === communityDataSystem?.id) {
rootFolderName = 'Community Data';
} else if (sys?.id === publishedDataSystem?.id) {
rootFolderName = 'Published Data';
} else {
rootFolderName = 'Project';
}

setFolderChain([{ id: rootPath, name: rootFolderName, isDir: true }]);
};

useEffect(() => {
if (selectedSystem?.id) {
refetch();
}
}, [selectedSystem, path, refetch]);
}, [selectedSystem, listingState, refetch]);

useEffect(() => {
setChonkyFiles(
files?.map((file) =>
serializeToChonkyFile(file, allowedFileExtensions)
) || []
);
if (files) {
setLoadingMoreFiles(false);
const fileListWrapper = document.querySelector('.chonky-fileListWrapper');

if (!fileListWrapper) return;

const dynamicListContainer = fileListWrapper.querySelector(
'[class^="listContainer-"]'
);

if (!dynamicListContainer) return;

const handleScroll = debounce(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick and you don't need to take my advice, but personally I like to avoid lodash. Is there a way to handle the scroll event in this way without the debounce frequency?

if (!hasMoreFiles) return;
const { scrollTop, scrollHeight, clientHeight } = dynamicListContainer;
if (scrollTop + clientHeight >= scrollHeight - 20) {
setLoadingMoreFiles(true);
setListingState((prev) => ({
...prev,
offset: prev.offset + DEFAULT_FILE_LIMIT,
}));
}
}, 250);

dynamicListContainer.addEventListener('scroll', handleScroll);
return () =>
dynamicListContainer.removeEventListener('scroll', handleScroll);
}
}, [files, hasMoreFiles]);

useEffect(() => {
if (files) {
if (files.length < DEFAULT_FILE_LIMIT || files.length === 0) {
setHasMoreFiles(false);
} else {
setHasMoreFiles(true);
}

setChonkyFiles((prevFiles) => {
const filteredPrevFiles = prevFiles.filter(Boolean); // Filter out nulls
const newChonkyFiles = files.map((file) =>
serializeToChonkyFile(file, allowedFileExtensions)
);

if (
JSON.stringify(prevFiles) ===
JSON.stringify([...prevFiles, ...newChonkyFiles])
) {
setHasMoreFiles(false);
return prevFiles;
}

return listingState.offset === 0
? newChonkyFiles
: [...filteredPrevFiles, ...newChonkyFiles];
});
}
if (!folderChain.length && user?.username) {
setFolderChain([{ id: user.username, name: user.username, isDir: true }]);
setRootFolderChain(user.username);
}
}, [
files,
folderChain.length,
user?.username,
allowedFileExtensions,
hasError,
]);
}, [files, folderChain.length, user?.username, allowedFileExtensions]);

Check warning on line 183 in react/src/components/Files/FileListing.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

React Hook useEffect has missing dependencies: 'listingState.offset' and 'setRootFolderChain'. Either include them or remove the dependency array. You can also replace multiple useState variables with useReducer if 'setChonkyFiles' needs the current value of 'listingState.offset'

const FileListingIcon = (props: any) => <Icon name={props.icon} />;

useEffect(() => {
if (!selectedSystemId || selectedSystem?.id === selectedSystemId) {
return;
}

setHasError(false);
setChonkyFiles(new Array(8).fill(null));
setFolderChain([null]);
Expand All @@ -127,24 +202,16 @@

setSelectedSystem(sys);
const rootFolder = sys.id === myDataSystem?.id ? user?.username : '/';
setPath(rootFolder);

setListingState({
path: rootFolder,
offset: 0,
});

onSystemSelect?.(sys.id);
onFolderSelect?.(rootFolder);
let rootFolderName: string;

if (sys.id === myDataSystem?.id) {
rootFolderName = 'My Data';
} else if (sys.id === communityDataSystem?.id) {
rootFolderName = 'Community Data';
} else if (sys.id === publishedDataSystem?.id) {
rootFolderName = 'Published Data';
} else {
rootFolderName = 'Project';
}

setFolderChain([{ id: rootFolder, name: rootFolderName, isDir: true }]);
setRootFolderChain(rootFolder, sys);
}, [selectedSystemId]);

Check warning on line 214 in react/src/components/Files/FileListing.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

React Hook useEffect has missing dependencies: 'myDataSystem?.id', 'onFolderSelect', 'onSystemSelect', 'selectedSystem?.id', 'setRootFolderChain', 'systems', and 'user?.username'. Either include them or remove the dependency array. If 'onSystemSelect' changes too often, find the parent component that defines it and wrap that definition in useCallback

const handleFileAction = useCallback(
(data: ChonkyFileActionData) => {
Expand All @@ -152,14 +219,18 @@
const file: FileData = data.payload.targetFile as FileData;

if (file.isDir) {
setPath(file.id);
setListingState({
path: file.id,
offset: 0,
});
setFolderChain((prev) => {
const index = prev.findIndex((folder) => folder.id === file.id);
return index === -1 ? [...prev, file] : prev.slice(0, index + 1);
});

onFolderSelect?.(file.id);
} else {
if (!file.selectable) return;
const selectedFile = files?.find((f) => f.path === file.id);
if (selectedFile) {
onFileSelect?.([selectedFile]);
Expand All @@ -178,7 +249,7 @@
(f) => filePaths.includes(f.path) && f.type !== 'dir'
) || [];

onFileSelect?.(newSelectedFiles); // P
onFileSelect?.(newSelectedFiles);
}
},
[files, onFileSelect, onFolderSelect]
Expand Down Expand Up @@ -233,9 +304,13 @@
clearSelectionOnOutsideClick
iconComponent={FileListingIcon}
onFileAction={handleFileAction}
defaultSortActionId={null}
>
<FileNavbar />
<FileList />
{loadingMoreFiles && (
<p style={{ textAlign: 'center' }}>Loading...</p>
)}
Comment on lines +311 to +313
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use a bit more vertical padding here

</_FileBrowser>
)}
</div>
Expand Down
Loading