Skip to content

Commit

Permalink
Update Loading Overlays On Data Grid Tables (#1336)
Browse files Browse the repository at this point in the history
* Update data table overlays
* Update LoadingGuard and wrap data tables

---------

Co-authored-by: Nick Phura <[email protected]>
  • Loading branch information
mauberti-bc and NickPhura authored Aug 10, 2024
1 parent 8062ebb commit 2adb4ff
Show file tree
Hide file tree
Showing 29 changed files with 1,132 additions and 902 deletions.
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

0 comments on commit 2adb4ff

Please sign in to comment.