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

Update Loading Overlays On Data Grid Tables #1336

Merged
merged 8 commits into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions app/src/components/data-grid/StyledDataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,6 @@ export const StyledDataGrid = <R extends GridValidRowModel = any>(props: StyledD
'&.MuiDataGrid-root--densityCompact .MuiDataGrid-cell': { py: '8px' },
'&.MuiDataGrid-root--densityStandard .MuiDataGrid-cell': { py: '15px' },
'&.MuiDataGrid-root--densityComfortable .MuiDataGrid-cell': { py: '22px' },
'& .MuiTypography-root, .MuiDataGrid-cellContent': {
fontSize: '0.9rem'
},
'& .MuiDataGrid-overlay': {
minHeight: '250px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
'& .MuiDataGrid-columnHeaderDraggableContainer': {
minWidth: '50px'
},
Expand Down
112 changes: 93 additions & 19 deletions app/src/components/loading/LoadingGuard.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,116 @@
import { PropsWithChildren, useEffect, useState } from 'react';

export interface ILoadingGuardProps {
isLoading: boolean;
fallback: JSX.Element;
delay?: number;
}
export type ILoadingGuardProps = {
/**
* Whether the component is in a loading state.
*
* @type {boolean}
*/
isLoading?: boolean;
/**
* The loading fallback component to render when `isLoading` is true.
*
* @type {JSX.Element}
*/
isLoadingFallback?: JSX.Element;
/**
* The minimum time in milliseconds to show the loading fallback component.
*
* @type {number}
*/
isLoadingFallbackDelay?: number;
/**
* Whether the component has no data to display.
*
* @type {boolean}
*/
hasNoData?: boolean;
/**
* The 'no data' fallback component to render when `isLoading` is false and `hasNoData` is true.
*
* @type {JSX.Element}
*/
hasNoDataFallback?: JSX.Element;
/**
* The minimum time in milliseconds to show the 'no data' fallback component.
*
* @type {number}
*/
hasNoDataFallbackDelay?: number;
};

/**
* Renders `props.children` if `isLoading` is false, otherwise renders `fallback`.
* Supports rendering various fallback components based on the loading/data state.
*
* Renders a loading fallback component if `isLoading` is true.
* Optionally renders a 'no data' fallback component if `isLoading` is false and `hasNoData` is true.
*
* If `delay` is provided, the fallback will be shown for at least `delay` milliseconds.
* If `isLoadingFallbackDelay` or `hasNoDataFallbackDelay` are provided, the respective fallback will be shown for at
* least `isLoadingFallbackDelay` or `hasNoDataFallbackDelay` milliseconds. Why? To prevent flickering of the UI when
* the loading state is short-lived.
*
* Fallback should be a loading spinner or skeleton component, etc.
* The fallback components are typically loading spinners, skeleton loaders, etc.
*
* @param {PropsWithChildren<ILoadingGuardProps>} props
* @return {*}
*/
export const LoadingGuard = (props: PropsWithChildren<ILoadingGuardProps>) => {
const { isLoading, fallback, delay, children } = props;
const {
isLoading,
isLoadingFallback,
isLoadingFallbackDelay,
hasNoData,
hasNoDataFallback,
hasNoDataFallbackDelay,
children
} = props;

const [showFallback, setShowFallback] = useState(isLoading);
const [showIsLoadingFallback, setShowIsLoadingFallback] = useState(isLoading ?? false);
const [showHasNoDataFallback, setShowHasNoDataFallback] = useState(hasNoData ?? false);

useEffect(() => {
if (!isLoading) {
// If the loading state changes to false, hide the fallback
if (delay) {
// If the loading state changes to false, hide the is loading fallback
if (isLoadingFallbackDelay) {
// If there is a delay, show the is loading fallback for at least `isLoadingFallbackDelay` milliseconds
setTimeout(() => {
// Disable the is loading fallback after the delay
setShowIsLoadingFallback(false);
}, isLoadingFallbackDelay);
} else {
// If there is no delay, disable the is loading fallback immediately
setShowIsLoadingFallback(false);
}
}
}, [isLoading, isLoadingFallbackDelay]);

useEffect(() => {
if (isLoading) {
// Do nothing - the loading state takes precedence over the no data state
return;
}

if (!hasNoData) {
// If there is data to display, hide the no data fallback
if (hasNoDataFallbackDelay) {
// If there is a delay, show the no data fallback for at least `hasNoDataFallbackDelay` milliseconds
setTimeout(() => {
// Show the fallback for at least `delay` milliseconds
setShowFallback(false);
}, delay);
// Disable the no data fallback after the delay
setShowHasNoDataFallback(false);
}, hasNoDataFallbackDelay);
} else {
setShowFallback(false);
// If there is no delay, disable the no data fallback immediately
setShowHasNoDataFallback(false);
}
}
}, [isLoading, delay]);
}, [hasNoData, hasNoDataFallbackDelay, isLoading]);

if (showIsLoadingFallback) {
return <>{isLoadingFallback}</>;
}

if (showFallback) {
return <>{fallback}</>;
if (showHasNoDataFallback) {
return <>{hasNoDataFallback}</>;
}

return <>{children}</>;
Expand Down
6 changes: 3 additions & 3 deletions app/src/components/overlay/NoDataOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Icon from '@mdi/react';
import Box from '@mui/material/Box';
import Box, { BoxProps } from '@mui/material/Box';
import Typography from '@mui/material/Typography';

interface INoDataOverlayProps {
interface INoDataOverlayProps extends BoxProps {
title: string;
subtitle?: string;
icon?: string;
Expand All @@ -17,7 +17,7 @@ interface INoDataOverlayProps {
export const NoDataOverlay = (props: INoDataOverlayProps) => {
const { title, subtitle, icon } = props;
return (
<Box justifyContent="center" display="flex" flexDirection="column">
<Box justifyContent="center" display="flex" flexDirection="column" {...props}>
<Typography mb={1} variant="h4" color="textSecondary" textAlign="center">
{title}
{icon && <Icon path={icon} size={1} style={{ marginLeft: '8px' }} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Paper from '@mui/material/Paper';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { DataGrid, GridColDef, GridOverlay } from '@mui/x-data-grid';
import { LoadingGuard } from 'components/loading/LoadingGuard';
import { IGetFundingSourceResponse } from 'interfaces/useFundingSourceApi.interface';
import { debounce } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
Expand Down Expand Up @@ -149,22 +150,24 @@ const FundingSourceSurveyReferences = (props: IFundingSourceSurveyReferencesProp
fullWidth={true}
/>
</Box>
{fundingSourceSurveyReferences.length === 0 ? (
<Box>
<Paper
elevation={0}
variant="outlined"
sx={{
padding: '24px',
textAlign: 'center',
background: grey[100]
}}>
<Typography variant="body1" color="textSecondary">
No surveys found
</Typography>
</Paper>
</Box>
) : (
<LoadingGuard
hasNoData={!fundingSourceSurveyReferences.length}
hasNoDataFallback={
<Box>
<Paper
elevation={0}
variant="outlined"
sx={{
padding: '24px',
textAlign: 'center',
background: grey[100]
}}>
<Typography variant="body1" color="textSecondary">
No surveys found
</Typography>
</Paper>
</Box>
}>
<Paper elevation={0} variant="outlined">
<Box>
<DataGrid
Expand All @@ -188,7 +191,7 @@ const FundingSourceSurveyReferences = (props: IFundingSourceSurveyReferencesProp
/>
</Box>
</Paper>
)}
</LoadingGuard>
</Box>
</>
);
Expand Down
20 changes: 14 additions & 6 deletions app/src/features/projects/view/ProjectAttachments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ describe('ProjectAttachments', () => {
const mockProjectContext: IProjectContext = {
artifactDataLoader: {
data: null,
load: jest.fn()
load: jest.fn(),
isLoading: false,
isReady: true
} as unknown as DataLoader<any, any, any>,
projectId: 1,
projectDataLoader: {
Expand Down Expand Up @@ -119,7 +121,7 @@ describe('ProjectAttachments', () => {
hasLoadedParticipantInfo: true
};

const { getByText } = render(
const { getByTestId } = render(
<ConfigContext.Provider value={{} as IConfig}>
<AuthStateContext.Provider value={authState}>
<Router history={history}>
Expand All @@ -133,7 +135,7 @@ describe('ProjectAttachments', () => {
</ConfigContext.Provider>
);
await waitFor(() => {
expect(getByText('No shared files found')).toBeInTheDocument();
expect(getByTestId('project-attachments-list-no-data-overlay')).toBeInTheDocument();
});
});

Expand All @@ -155,7 +157,9 @@ describe('ProjectAttachments', () => {
projectId: 1,
projectDataLoader: {
data: { projectData: { project: { project_name: 'name' } } },
load: jest.fn()
load: jest.fn(),
isLoading: false,
isReady: true
} as unknown as DataLoader<any, any, any>
} as unknown as IProjectContext;

Expand Down Expand Up @@ -361,12 +365,16 @@ describe('ProjectAttachments', () => {
}
]
},
load: jest.fn()
load: jest.fn(),
isLoading: false,
isReady: true
} as unknown as DataLoader<any, any, any>,
projectId: 1,
projectDataLoader: {
data: { projectData: { project: { project_name: 'name' } } },
load: jest.fn()
load: jest.fn(),
isLoading: false,
isReady: true
} as unknown as DataLoader<any, any, any>
} as unknown as IProjectContext;

Expand Down
34 changes: 27 additions & 7 deletions app/src/features/projects/view/ProjectAttachmentsList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { mdiArrowTopRight } from '@mdi/js';
import Typography from '@mui/material/Typography';
import AttachmentsList from 'components/attachments/list/AttachmentsList';
import ProjectReportAttachmentDialog from 'components/dialog/attachments/project/ProjectReportAttachmentDialog';
import { LoadingGuard } from 'components/loading/LoadingGuard';
import { SkeletonTable } from 'components/loading/SkeletonLoaders';
import { NoDataOverlay } from 'components/overlay/NoDataOverlay';
import { AttachmentType } from 'constants/attachments';
import { AttachmentsI18N } from 'constants/i18n';
import { DialogContext, ISnackbarProps } from 'contexts/dialogContext';
Expand Down Expand Up @@ -120,13 +124,29 @@ const ProjectAttachmentsList = () => {
open={!!currentAttachment && currentAttachment?.fileType === AttachmentType.REPORT}
onClose={handleViewDetailsClose}
/>
<AttachmentsList<IGetProjectAttachment>
attachments={attachmentsList}
handleDownload={handleDownload}
handleDelete={handleDelete}
handleViewDetails={handleViewDetailsOpen}
emptyStateText="No shared files found"
/>
<LoadingGuard
isLoading={projectContext.artifactDataLoader.isLoading}
isLoadingFallback={<SkeletonTable data-testid="project-attachments-loading-skeleton" />}
isLoadingFallbackDelay={100}
hasNoData={!attachmentsList.length}
hasNoDataFallback={
<NoDataOverlay
height="200px"
title="Upload Files"
subtitle="Share information with your team by uploading files"
icon={mdiArrowTopRight}
data-testid="project-attachments-list-no-data-overlay"
/>
}
hasNoDataFallbackDelay={100}>
<AttachmentsList<IGetProjectAttachment>
attachments={attachmentsList}
handleDownload={handleDownload}
handleDelete={handleDelete}
handleViewDetails={handleViewDetailsOpen}
emptyStateText="No shared files found"
/>
</LoadingGuard>
</>
);
};
Expand Down
Loading