From 2adb4ffc13d757b0608bd1070e017e90fc437fb4 Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young <108430771+mauberti-bc@users.noreply.github.com> Date: Fri, 9 Aug 2024 17:10:20 -0700 Subject: [PATCH] Update Loading Overlays On Data Grid Tables (#1336) * Update data table overlays * Update LoadingGuard and wrap data tables --------- Co-authored-by: Nick Phura --- .../components/data-grid/StyledDataGrid.tsx | 9 - app/src/components/loading/LoadingGuard.tsx | 112 +++++-- app/src/components/overlay/NoDataOverlay.tsx | 6 +- .../details/FundingSourceSurveyReferences.tsx | 37 +-- .../projects/view/ProjectAttachments.test.tsx | 20 +- .../projects/view/ProjectAttachmentsList.tsx | 34 ++- .../project/ProjectsListContainer.tsx | 126 ++++---- .../list-data/survey/SurveysListContainer.tsx | 120 ++++---- .../animal/AnimalsListContainer.tsx | 122 ++++---- .../observation/ObservationsListContainer.tsx | 122 ++++---- .../telemetry/TelemetryListContainer.tsx | 122 ++++---- .../surveys/list/SurveysListPage.test.tsx | 26 +- .../features/surveys/list/SurveysListPage.tsx | 70 +++-- .../observations-table/ObservationsTable.tsx | 276 +++++++++--------- .../periods/table/SamplingPeriodTable.tsx | 55 +--- .../sites/SamplingSiteContainer.tsx | 108 +++++-- .../sites/table/SamplingSiteTable.tsx | 21 +- .../sites/table/SamplingSiteTabs.tsx | 13 +- .../techniques/SamplingTechniqueContainer.tsx | 4 +- .../table/SamplingTechniqueTable.tsx | 38 +-- .../surveys/view/SurveyAttachments.test.tsx | 36 ++- .../surveys/view/SurveyAttachmentsList.tsx | 42 ++- .../sampling-data/SurveySamplingContainer.tsx | 23 +- .../components/SurveySamplingTabs.tsx | 137 +++++++-- .../components/SurveySitesTable.tsx | 45 +-- .../components/SurveyTechniquesTable.tsx | 45 +-- .../animal/SurveySpatialAnimalTable.tsx | 80 ++--- .../SurveySpatialObservationTable.tsx | 106 +++---- .../telemetry/SurveySpatialTelemetryTable.tsx | 79 ++--- 29 files changed, 1132 insertions(+), 902 deletions(-) diff --git a/app/src/components/data-grid/StyledDataGrid.tsx b/app/src/components/data-grid/StyledDataGrid.tsx index 2f4cf18154..8ad1c1200e 100644 --- a/app/src/components/data-grid/StyledDataGrid.tsx +++ b/app/src/components/data-grid/StyledDataGrid.tsx @@ -52,15 +52,6 @@ export const StyledDataGrid = (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' }, diff --git a/app/src/components/loading/LoadingGuard.tsx b/app/src/components/loading/LoadingGuard.tsx index 3ab107dc81..9a2d563f43 100644 --- a/app/src/components/loading/LoadingGuard.tsx +++ b/app/src/components/loading/LoadingGuard.tsx @@ -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} props * @return {*} */ export const LoadingGuard = (props: PropsWithChildren) => { - 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}; diff --git a/app/src/components/overlay/NoDataOverlay.tsx b/app/src/components/overlay/NoDataOverlay.tsx index cb13371fe9..6eb8156566 100644 --- a/app/src/components/overlay/NoDataOverlay.tsx +++ b/app/src/components/overlay/NoDataOverlay.tsx @@ -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; @@ -17,7 +17,7 @@ interface INoDataOverlayProps { export const NoDataOverlay = (props: INoDataOverlayProps) => { const { title, subtitle, icon } = props; return ( - + {title} {icon && } diff --git a/app/src/features/funding-sources/details/FundingSourceSurveyReferences.tsx b/app/src/features/funding-sources/details/FundingSourceSurveyReferences.tsx index d6bd0126c3..a1b544e47a 100644 --- a/app/src/features/funding-sources/details/FundingSourceSurveyReferences.tsx +++ b/app/src/features/funding-sources/details/FundingSourceSurveyReferences.tsx @@ -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'; @@ -149,22 +150,24 @@ const FundingSourceSurveyReferences = (props: IFundingSourceSurveyReferencesProp fullWidth={true} /> - {fundingSourceSurveyReferences.length === 0 ? ( - - - - No surveys found - - - - ) : ( + + + + No surveys found + + + + }> - )} + ); diff --git a/app/src/features/projects/view/ProjectAttachments.test.tsx b/app/src/features/projects/view/ProjectAttachments.test.tsx index fea9714a5f..3e95cb6352 100644 --- a/app/src/features/projects/view/ProjectAttachments.test.tsx +++ b/app/src/features/projects/view/ProjectAttachments.test.tsx @@ -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, projectId: 1, projectDataLoader: { @@ -119,7 +121,7 @@ describe('ProjectAttachments', () => { hasLoadedParticipantInfo: true }; - const { getByText } = render( + const { getByTestId } = render( @@ -133,7 +135,7 @@ describe('ProjectAttachments', () => { ); await waitFor(() => { - expect(getByText('No shared files found')).toBeInTheDocument(); + expect(getByTestId('project-attachments-list-no-data-overlay')).toBeInTheDocument(); }); }); @@ -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 } as unknown as IProjectContext; @@ -361,12 +365,16 @@ describe('ProjectAttachments', () => { } ] }, - load: jest.fn() + load: jest.fn(), + isLoading: false, + isReady: true } as unknown as DataLoader, projectId: 1, projectDataLoader: { data: { projectData: { project: { project_name: 'name' } } }, - load: jest.fn() + load: jest.fn(), + isLoading: false, + isReady: true } as unknown as DataLoader } as unknown as IProjectContext; diff --git a/app/src/features/projects/view/ProjectAttachmentsList.tsx b/app/src/features/projects/view/ProjectAttachmentsList.tsx index f2fdbeb13f..f272a12b99 100644 --- a/app/src/features/projects/view/ProjectAttachmentsList.tsx +++ b/app/src/features/projects/view/ProjectAttachmentsList.tsx @@ -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'; @@ -120,13 +124,29 @@ const ProjectAttachmentsList = () => { open={!!currentAttachment && currentAttachment?.fileType === AttachmentType.REPORT} onClose={handleViewDetailsClose} /> - - attachments={attachmentsList} - handleDownload={handleDownload} - handleDelete={handleDelete} - handleViewDetails={handleViewDetailsOpen} - emptyStateText="No shared files found" - /> + } + isLoadingFallbackDelay={100} + hasNoData={!attachmentsList.length} + hasNoDataFallback={ + + } + hasNoDataFallbackDelay={100}> + + attachments={attachmentsList} + handleDownload={handleDownload} + handleDelete={handleDelete} + handleViewDetails={handleViewDetailsOpen} + emptyStateText="No shared files found" + /> + ); }; diff --git a/app/src/features/summary/list-data/project/ProjectsListContainer.tsx b/app/src/features/summary/list-data/project/ProjectsListContainer.tsx index 8a9d5583a5..31ac59fda6 100644 --- a/app/src/features/summary/list-data/project/ProjectsListContainer.tsx +++ b/app/src/features/summary/list-data/project/ProjectsListContainer.tsx @@ -1,3 +1,4 @@ +import { mdiArrowTopRight } from '@mdi/js'; import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; import grey from '@mui/material/colors/grey'; @@ -8,6 +9,9 @@ import Typography from '@mui/material/Typography'; import { GridColDef, GridPaginationModel, GridSortDirection, GridSortModel } from '@mui/x-data-grid'; import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; +import { LoadingGuard } from 'components/loading/LoadingGuard'; +import { SkeletonTable } from 'components/loading/SkeletonLoaders'; +import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; import { getNrmRegionColour, NrmRegionKeys } from 'constants/colours'; import { NRM_REGION_APPENDED_TEXT } from 'constants/regions'; import { TeamMemberAvatar } from 'features/projects/view/components/TeamMemberAvatar'; @@ -96,17 +100,17 @@ const ProjectsListContainer = (props: IProjectsListContainerProps) => { [paginationModel.page, paginationModel.pageSize, sort?.field, sort?.sort] ); - const { refresh, isReady, data } = useDataLoader( + const projectsDataLoader = useDataLoader( (pagination: ApiPaginationRequestOptions, filter?: IProjectAdvancedFilters) => biohubApi.project.findProjects(pagination, filter) ); // Fetch projects when either the pagination, sort, or advanced filters change useDeepCompareEffect(() => { - refresh(paginationSort, advancedFiltersModel); + projectsDataLoader.refresh(paginationSort, advancedFiltersModel); }, [advancedFiltersModel, paginationSort]); - const rows = data?.projects ?? []; + const rows = projectsDataLoader.data?.projects ?? []; // Define the columns for the DataGrid const columns: GridColDef[] = [ @@ -204,63 +208,67 @@ const ProjectsListContainer = (props: IProjectsListContainerProps) => { - - row.project_id} - // Pagination - paginationMode="server" - paginationModel={paginationModel} - pageSizeOptions={pageSizeOptions} - onPaginationModelChange={(model) => { - if (!model) { - return; - } - setSearchParams(searchParams.set('p_page', String(model.page)).set('p_limit', String(model.pageSize))); - setPaginationModel(model); - }} - // Sorting - sortingMode="server" - sortModel={sortModel} - sortingOrder={['asc', 'desc']} - onSortModelChange={(model) => { - if (!model.length) { - return; - } - setSearchParams(searchParams.set('p_sort', model[0].field).set('p_order', model[0].sort ?? 'desc')); - setSortModel(model); - }} - // Row options - rowSelection={false} - checkboxSelection={false} - disableRowSelectionOnClick - // Column options - disableColumnSelector - disableColumnFilter - disableColumnMenu - // Styling - rowHeight={70} - getRowHeight={() => 'auto'} - autoHeight={false} - sx={{ - '& .MuiDataGrid-overlay': { - background: grey[50] - }, - '& .MuiDataGrid-cell': { - py: 0.75, - background: '#fff', - '&.MuiDataGrid-cell--editing:focus-within': { - outline: 'none' + + + } + isLoadingFallbackDelay={100} + hasNoData={!rows.length} + hasNoDataFallback={ + + } + hasNoDataFallbackDelay={100}> + row.project_id} + // Pagination + paginationMode="server" + paginationModel={paginationModel} + pageSizeOptions={pageSizeOptions} + onPaginationModelChange={(model) => { + if (!model) { + return; + } + setSearchParams(searchParams.set('p_page', String(model.page)).set('p_limit', String(model.pageSize))); + setPaginationModel(model); + }} + // Sorting + sortingMode="server" + sortModel={sortModel} + sortingOrder={['asc', 'desc']} + onSortModelChange={(model) => { + if (!model.length) { + return; } - } - }} - /> + setSearchParams(searchParams.set('p_sort', model[0].field).set('p_order', model[0].sort ?? 'desc')); + setSortModel(model); + }} + // Row options + rowSelection={false} + checkboxSelection={false} + disableRowSelectionOnClick + // Column options + disableColumnSelector + disableColumnFilter + disableColumnMenu + // Styling + rowHeight={70} + getRowHeight={() => 'auto'} + autoHeight={false} + /> + ); diff --git a/app/src/features/summary/list-data/survey/SurveysListContainer.tsx b/app/src/features/summary/list-data/survey/SurveysListContainer.tsx index 0820ee272d..135b955975 100644 --- a/app/src/features/summary/list-data/survey/SurveysListContainer.tsx +++ b/app/src/features/summary/list-data/survey/SurveysListContainer.tsx @@ -1,3 +1,4 @@ +import { mdiArrowTopRight } from '@mdi/js'; import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; import grey from '@mui/material/colors/grey'; @@ -8,6 +9,9 @@ import Typography from '@mui/material/Typography'; import { GridColDef, GridPaginationModel, GridSortDirection, GridSortModel } from '@mui/x-data-grid'; import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; +import { LoadingGuard } from 'components/loading/LoadingGuard'; +import { SkeletonTable } from 'components/loading/SkeletonLoaders'; +import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; import { getNrmRegionColour, NrmRegionKeys } from 'constants/colours'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import { NRM_REGION_APPENDED_TEXT } from 'constants/regions'; @@ -212,63 +216,67 @@ const SurveysListContainer = (props: ISurveysListContainerProps) => { - - row.survey_id} - // Pagination - paginationMode="server" - paginationModel={paginationModel} - pageSizeOptions={pageSizeOptions} - onPaginationModelChange={(model) => { - if (!model) { - return; - } - setSearchParams(searchParams.set('s_page', String(model.page)).set('s_limit', String(model.pageSize))); - setPaginationModel(model); - }} - // Sorting - sortingMode="server" - sortModel={sortModel} - sortingOrder={['asc', 'desc']} - onSortModelChange={(model) => { - if (!model.length) { - return; - } - setSearchParams(searchParams.set('s_sort', model[0].field).set('s_order', model[0].sort ?? 'desc')); - setSortModel(model); - }} - // Row options - checkboxSelection={false} - disableRowSelectionOnClick - rowSelection={false} - // Column options - disableColumnSelector - disableColumnFilter - disableColumnMenu - // Styling - rowHeight={70} - getRowHeight={() => 'auto'} - autoHeight={false} - sx={{ - '& .MuiDataGrid-overlay': { - background: grey[50] - }, - '& .MuiDataGrid-cell': { - py: 0.75, - background: '#fff', - '&.MuiDataGrid-cell--editing:focus-within': { - outline: 'none' + + + } + isLoadingFallbackDelay={100} + hasNoData={!rows.length} + hasNoDataFallback={ + + } + hasNoDataFallbackDelay={100}> + row.survey_id} + // Pagination + paginationMode="server" + paginationModel={paginationModel} + pageSizeOptions={pageSizeOptions} + onPaginationModelChange={(model) => { + if (!model) { + return; + } + setSearchParams(searchParams.set('s_page', String(model.page)).set('s_limit', String(model.pageSize))); + setPaginationModel(model); + }} + // Sorting + sortingMode="server" + sortModel={sortModel} + sortingOrder={['asc', 'desc']} + onSortModelChange={(model) => { + if (!model.length) { + return; } - } - }} - /> + setSearchParams(searchParams.set('s_sort', model[0].field).set('s_order', model[0].sort ?? 'desc')); + setSortModel(model); + }} + // Row options + rowSelection={false} + checkboxSelection={false} + disableRowSelectionOnClick + // Column options + disableColumnSelector + disableColumnFilter + disableColumnMenu + // Styling + rowHeight={70} + getRowHeight={() => 'auto'} + autoHeight={false} + /> + ); diff --git a/app/src/features/summary/tabular-data/animal/AnimalsListContainer.tsx b/app/src/features/summary/tabular-data/animal/AnimalsListContainer.tsx index df5c6be4e6..dcafbcd54d 100644 --- a/app/src/features/summary/tabular-data/animal/AnimalsListContainer.tsx +++ b/app/src/features/summary/tabular-data/animal/AnimalsListContainer.tsx @@ -1,3 +1,4 @@ +import { mdiArrowTopRight } from '@mdi/js'; import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; import grey from '@mui/material/colors/grey'; @@ -5,6 +6,9 @@ import Divider from '@mui/material/Divider'; import Typography from '@mui/material/Typography'; import { GridColDef, GridPaginationModel, GridSortDirection, GridSortModel } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; +import { LoadingGuard } from 'components/loading/LoadingGuard'; +import { SkeletonTable } from 'components/loading/SkeletonLoaders'; +import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { useDeepCompareEffect } from 'hooks/useDeepCompareEffect'; @@ -91,7 +95,7 @@ const AnimalsListContainer = (props: IAnimalsListContainerProps) => { animalsDataLoader.refresh(paginationSort, advancedFiltersModel); }, [advancedFiltersModel, paginationSort]); - const animalRows = animalsDataLoader.data?.animals ?? []; + const rows = animalsDataLoader.data?.animals ?? []; const columns: GridColDef[] = [ { @@ -152,63 +156,67 @@ const AnimalsListContainer = (props: IAnimalsListContainerProps) => { - - row.critter_id} - // Pagination - paginationMode="server" - pageSizeOptions={pageSizeOptions} - paginationModel={paginationModel} - onPaginationModelChange={(model) => { - if (!model) { - return; - } - setSearchParams(searchParams.set('a_page', String(model.page)).set('a_limit', String(model.pageSize))); - setPaginationModel(model); - }} - // Sorting - sortingMode="server" - sortModel={sortModel} - sortingOrder={['asc', 'desc']} - onSortModelChange={(model) => { - if (!model.length) { - return; - } - setSearchParams(searchParams.set('a_sort', model[0].field).set('a_order', model[0].sort ?? 'desc')); - setSortModel(model); - }} - // Row options - checkboxSelection={false} - disableRowSelectionOnClick - rowSelection={false} - // Column options - disableColumnSelector - disableColumnFilter - disableColumnMenu - // Styling - rowHeight={70} - getRowHeight={() => 'auto'} - autoHeight={false} - sx={{ - '& .MuiDataGrid-overlay': { - background: grey[50] - }, - '& .MuiDataGrid-cell': { - py: 0.75, - background: '#fff', - '&.MuiDataGrid-cell--editing:focus-within': { - outline: 'none' + + + } + isLoadingFallbackDelay={100} + hasNoData={!rows.length} + hasNoDataFallback={ + + } + hasNoDataFallbackDelay={100}> + row.critter_id} + // Pagination + paginationMode="server" + paginationModel={paginationModel} + pageSizeOptions={pageSizeOptions} + onPaginationModelChange={(model) => { + if (!model) { + return; + } + setSearchParams(searchParams.set('a_page', String(model.page)).set('a_limit', String(model.pageSize))); + setPaginationModel(model); + }} + // Sorting + sortingMode="server" + sortModel={sortModel} + sortingOrder={['asc', 'desc']} + onSortModelChange={(model) => { + if (!model.length) { + return; } - } - }} - /> + setSearchParams(searchParams.set('a_sort', model[0].field).set('a_order', model[0].sort ?? 'desc')); + setSortModel(model); + }} + // Row options + rowSelection={false} + checkboxSelection={false} + disableRowSelectionOnClick + // Column options + disableColumnSelector + disableColumnFilter + disableColumnMenu + // Styling + rowHeight={70} + getRowHeight={() => 'auto'} + autoHeight={false} + /> + ); diff --git a/app/src/features/summary/tabular-data/observation/ObservationsListContainer.tsx b/app/src/features/summary/tabular-data/observation/ObservationsListContainer.tsx index 39410f3f1c..1ad980da68 100644 --- a/app/src/features/summary/tabular-data/observation/ObservationsListContainer.tsx +++ b/app/src/features/summary/tabular-data/observation/ObservationsListContainer.tsx @@ -1,3 +1,4 @@ +import { mdiArrowTopRight } from '@mdi/js'; import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; import grey from '@mui/material/colors/grey'; @@ -5,6 +6,9 @@ import Divider from '@mui/material/Divider'; import Typography from '@mui/material/Typography'; import { GridColDef, GridPaginationModel, GridSortDirection, GridSortModel } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; +import { LoadingGuard } from 'components/loading/LoadingGuard'; +import { SkeletonTable } from 'components/loading/SkeletonLoaders'; +import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import { IObservationTableRow } from 'contexts/observationsTableContext'; import dayjs from 'dayjs'; @@ -157,7 +161,7 @@ const ObservationsListContainer = (props: IObservationsListContainerProps) => { [] ); - const observationRows = observationsDataLoader.data ? getRowsFromObservations(observationsDataLoader.data) : []; + const rows = observationsDataLoader.data ? getRowsFromObservations(observationsDataLoader.data) : []; const columns: GridColDef[] = [ { @@ -241,63 +245,67 @@ const ObservationsListContainer = (props: IObservationsListContainerProps) => { - - row.observation_subcount_id} - // Pagination - paginationMode="server" - pageSizeOptions={pageSizeOptions} - paginationModel={paginationModel} - onPaginationModelChange={(model) => { - if (!model) { - return; - } - setSearchParams(searchParams.set('o_page', String(model.page)).set('o_limit', String(model.pageSize))); - setPaginationModel(model); - }} - // Sorting - sortingMode="server" - sortModel={sortModel} - sortingOrder={['asc', 'desc']} - onSortModelChange={(model) => { - if (!model.length) { - return; - } - setSearchParams(searchParams.set('o_sort', model[0].field).set('o_order', model[0].sort ?? 'desc')); - setSortModel(model); - }} - // Row options - checkboxSelection={false} - disableRowSelectionOnClick - rowSelection={false} - // Column options - disableColumnSelector - disableColumnFilter - disableColumnMenu - // Styling - rowHeight={70} - getRowHeight={() => 'auto'} - autoHeight={false} - sx={{ - '& .MuiDataGrid-overlay': { - background: grey[50] - }, - '& .MuiDataGrid-cell': { - py: 0.75, - background: '#fff', - '&.MuiDataGrid-cell--editing:focus-within': { - outline: 'none' + + + } + isLoadingFallbackDelay={100} + hasNoData={!rows.length} + hasNoDataFallback={ + + } + hasNoDataFallbackDelay={100}> + row.observation_subcount_id} + // Pagination + paginationMode="server" + paginationModel={paginationModel} + pageSizeOptions={pageSizeOptions} + onPaginationModelChange={(model) => { + if (!model) { + return; + } + setSearchParams(searchParams.set('o_page', String(model.page)).set('o_limit', String(model.pageSize))); + setPaginationModel(model); + }} + // Sorting + sortingMode="server" + sortModel={sortModel} + sortingOrder={['asc', 'desc']} + onSortModelChange={(model) => { + if (!model.length) { + return; } - } - }} - /> + setSearchParams(searchParams.set('o_sort', model[0].field).set('o_order', model[0].sort ?? 'desc')); + setSortModel(model); + }} + // Row options + rowSelection={false} + checkboxSelection={false} + disableRowSelectionOnClick + // Column options + disableColumnSelector + disableColumnFilter + disableColumnMenu + // Styling + rowHeight={70} + getRowHeight={() => 'auto'} + autoHeight={false} + /> + ); diff --git a/app/src/features/summary/tabular-data/telemetry/TelemetryListContainer.tsx b/app/src/features/summary/tabular-data/telemetry/TelemetryListContainer.tsx index 79ec4a0472..0580232275 100644 --- a/app/src/features/summary/tabular-data/telemetry/TelemetryListContainer.tsx +++ b/app/src/features/summary/tabular-data/telemetry/TelemetryListContainer.tsx @@ -1,3 +1,4 @@ +import { mdiArrowTopRight } from '@mdi/js'; import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; import grey from '@mui/material/colors/grey'; @@ -5,6 +6,9 @@ import Divider from '@mui/material/Divider'; import Typography from '@mui/material/Typography'; import { GridColDef, GridPaginationModel, GridSortDirection, GridSortModel } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; +import { LoadingGuard } from 'components/loading/LoadingGuard'; +import { SkeletonTable } from 'components/loading/SkeletonLoaders'; +import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import dayjs from 'dayjs'; import { useBiohubApi } from 'hooks/useBioHubApi'; @@ -93,7 +97,7 @@ const TelemetryListContainer = (props: ITelemetryListContainerProps) => { telemetryDataLoader.refresh(paginationSort, advancedFiltersModel); }, [advancedFiltersModel, paginationSort]); - const telemetryRows = telemetryDataLoader.data?.telemetry ?? []; + const rows = telemetryDataLoader.data?.telemetry ?? []; const columns: GridColDef[] = [ { @@ -156,63 +160,67 @@ const TelemetryListContainer = (props: ITelemetryListContainerProps) => { - - row.telemetry_id} - // Pagination - paginationMode="server" - pageSizeOptions={pageSizeOptions} - paginationModel={paginationModel} - onPaginationModelChange={(model) => { - if (!model) { - return; - } - setSearchParams(searchParams.set('t_page', String(model.page)).set('t_limit', String(model.pageSize))); - setPaginationModel(model); - }} - // Sorting - sortingMode="server" - sortModel={sortModel} - sortingOrder={['asc', 'desc']} - onSortModelChange={(model) => { - if (!model[0]) { - return; - } - setSearchParams(searchParams.set('t_sort', model[0].field).set('t_order', model[0].sort ?? 'desc')); - setSortModel(model); - }} - // Row options - checkboxSelection={false} - disableRowSelectionOnClick - rowSelection={false} - // Column options - disableColumnSelector - disableColumnFilter - disableColumnMenu - // Styling - rowHeight={70} - getRowHeight={() => 'auto'} - autoHeight={false} - sx={{ - '& .MuiDataGrid-overlay': { - background: grey[50] - }, - '& .MuiDataGrid-cell': { - py: 0.75, - background: '#fff', - '&.MuiDataGrid-cell--editing:focus-within': { - outline: 'none' + + + } + isLoadingFallbackDelay={100} + hasNoData={!rows.length} + hasNoDataFallback={ + + } + hasNoDataFallbackDelay={100}> + row.telemetry_id} + // Pagination + paginationMode="server" + paginationModel={paginationModel} + pageSizeOptions={pageSizeOptions} + onPaginationModelChange={(model) => { + if (!model) { + return; + } + setSearchParams(searchParams.set('t_page', String(model.page)).set('t_limit', String(model.pageSize))); + setPaginationModel(model); + }} + // Sorting + sortingMode="server" + sortModel={sortModel} + sortingOrder={['asc', 'desc']} + onSortModelChange={(model) => { + if (!model.length) { + return; } - } - }} - /> + setSearchParams(searchParams.set('t_sort', model[0].field).set('t_order', model[0].sort ?? 'desc')); + setSortModel(model); + }} + // Row options + rowSelection={false} + checkboxSelection={false} + disableRowSelectionOnClick + // Column options + disableColumnSelector + disableColumnFilter + disableColumnMenu + // Styling + rowHeight={70} + getRowHeight={() => 'auto'} + autoHeight={false} + /> + ); diff --git a/app/src/features/surveys/list/SurveysListPage.test.tsx b/app/src/features/surveys/list/SurveysListPage.test.tsx index 6cefc06e96..972549265c 100644 --- a/app/src/features/surveys/list/SurveysListPage.test.tsx +++ b/app/src/features/surveys/list/SurveysListPage.test.tsx @@ -10,7 +10,7 @@ import { getMockAuthState, SystemAdminAuthState } from 'test-helpers/auth-helper import { codes } from 'test-helpers/code-helpers'; import { getProjectForViewResponse } from 'test-helpers/project-helpers'; import { getSurveyForListResponse } from 'test-helpers/survey-helpers'; -import { cleanup, render, waitFor } from 'test-helpers/test-utils'; +import { cleanup, render, screen, waitFor } from 'test-helpers/test-utils'; import SurveysListPage from './SurveysListPage'; const history = createMemoryHistory(); @@ -45,7 +45,11 @@ describe('SurveysListPage', () => { projectDataLoader: { data: getProjectForViewResponse } as DataLoader, - surveysListDataLoader: { data: [], refresh: jest.fn() } as unknown as DataLoader, + surveysListDataLoader: { data: [], isLoading: false, isReady: true, refresh: jest.fn() } as unknown as DataLoader< + any, + any, + any + >, artifactDataLoader: { data: null } as DataLoader, projectId: 1 }; @@ -63,7 +67,7 @@ describe('SurveysListPage', () => { const authState = getMockAuthState({ base: SystemAdminAuthState }); - const { getByText } = render( + const { getByTestId } = render( @@ -77,10 +81,9 @@ describe('SurveysListPage', () => { ); + screen.debug(); await waitFor(() => { - expect(getByText(/^Surveys/)).toBeInTheDocument(); - expect(getByText('Create Survey')).toBeInTheDocument(); - expect(getByText('No surveys found')).toBeInTheDocument(); + expect(getByTestId('survey-list-no-data-overlay')).toBeInTheDocument(); }); }); @@ -105,11 +108,12 @@ describe('SurveysListPage', () => { projectDataLoader: { data: getProjectForViewResponse } as DataLoader, - surveysListDataLoader: { data: getSurveyForListResponse, refresh: jest.fn() } as unknown as DataLoader< - any, - any, - any - >, + surveysListDataLoader: { + data: getSurveyForListResponse, + isLoading: false, + isReady: true, + refresh: jest.fn() + } as unknown as DataLoader, artifactDataLoader: { data: null } as DataLoader, projectId: 1 }; diff --git a/app/src/features/surveys/list/SurveysListPage.tsx b/app/src/features/surveys/list/SurveysListPage.tsx index fe3cc79810..7af0bc0bce 100644 --- a/app/src/features/surveys/list/SurveysListPage.tsx +++ b/app/src/features/surveys/list/SurveysListPage.tsx @@ -1,4 +1,4 @@ -import { mdiPlus } from '@mdi/js'; +import { mdiArrowTopRight, mdiPlus } from '@mdi/js'; import Icon from '@mdi/react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -8,6 +8,9 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { GridColDef, GridPaginationModel, GridSortModel } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; +import { LoadingGuard } from 'components/loading/LoadingGuard'; +import { SkeletonTable } from 'components/loading/SkeletonLoaders'; +import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; import { ProjectRoleGuard } from 'components/security/Guards'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import { PROJECT_PERMISSION, SYSTEM_ROLE } from 'constants/roles'; @@ -53,6 +56,8 @@ const SurveysListPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [sortModel, paginationModel]); + const surveys = projectContext.surveysListDataLoader.data?.surveys ?? []; + const columns: GridColDef[] = [ { field: 'name', @@ -131,30 +136,47 @@ const SurveysListPage = () => { - + + + - row.survey_id} - pageSizeOptions={[...pageSizeOptions]} - paginationMode="server" - sortingMode="server" - sortModel={sortModel} - paginationModel={paginationModel} - onPaginationModelChange={setPaginationModel} - onSortModelChange={setSortModel} - rowSelection={false} - checkboxSelection={false} - disableRowSelectionOnClick - disableColumnSelector - disableColumnFilter - disableColumnMenu - sortingOrder={['asc', 'desc']} - /> + } + isLoadingFallbackDelay={100} + hasNoData={!surveys.length} + hasNoDataFallback={ + + } + hasNoDataFallbackDelay={100}> + row.survey_id} + pageSizeOptions={[...pageSizeOptions]} + paginationMode="server" + sortingMode="server" + sortModel={sortModel} + paginationModel={paginationModel} + onPaginationModelChange={setPaginationModel} + onSortModelChange={setSortModel} + rowSelection={false} + checkboxSelection={false} + disableRowSelectionOnClick + disableColumnSelector + disableColumnFilter + disableColumnMenu + sortingOrder={['asc', 'desc']} + /> + ); diff --git a/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx b/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx index e523621720..7c3afd2cbe 100644 --- a/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx +++ b/app/src/features/surveys/observations/observations-table/ObservationsTable.tsx @@ -26,152 +26,150 @@ const ObservationsTable = (props: ISpeciesObservationTableProps) => { const observationsTableContext = useObservationsTableContext(); return ( - <> - observationsTableContext.onRowEditStart(params.id)} - onRowEditStop={(_params, event) => { - event.defaultMuiPrevented = true; - }} - // Row selection - checkboxSelection - disableRowSelectionOnClick - rowSelectionModel={observationsTableContext.rowSelectionModel} - onRowSelectionModelChange={observationsTableContext.onRowSelectionModelChange} - // Styling - localeText={{ - noRowsLabel: 'No Records' - }} - rowHeight={56} - getRowHeight={() => 'auto'} - getRowClassName={(params) => (has(observationsTableContext.validationModel, params.row.id) ? 'error' : '')} - // Loading - loading={observationsTableContext.isLoading} - slots={{ - loadingOverlay: SkeletonTable - }} - // Styles - sx={{ - border: 'none', - borderRadius: 0, - '&:after': { - content: '" "', - position: 'absolute', - top: 0, - right: 0, - width: 100, - height: 55 - }, - '& .pinnedColumn': { - position: 'sticky', - right: 0, - top: 0, - borderLeft: '1px solid' + grey[300] - }, - '& .MuiDataGrid-columnHeaders': { - position: 'relative', - background: grey[50] - }, - '& .MuiDataGrid-columnHeader:focus-within': { - outline: 'none', - background: grey[200] - }, - '& .MuiDataGrid-columnHeaderTitle': { - fontWeight: 700, - textTransform: 'uppercase', - color: 'text.secondary' + observationsTableContext.onRowEditStart(params.id)} + onRowEditStop={(_params, event) => { + event.defaultMuiPrevented = true; + }} + // Row selection + checkboxSelection + disableRowSelectionOnClick + rowSelectionModel={observationsTableContext.rowSelectionModel} + onRowSelectionModelChange={observationsTableContext.onRowSelectionModelChange} + // Styling + localeText={{ + noRowsLabel: 'No Records' + }} + rowHeight={56} + getRowHeight={() => 'auto'} + getRowClassName={(params) => (has(observationsTableContext.validationModel, params.row.id) ? 'error' : '')} + // Loading + loading={observationsTableContext.isLoading} + slots={{ + loadingOverlay: SkeletonTable + }} + // Styles + sx={{ + border: 'none', + borderRadius: 0, + '&:after': { + content: '" "', + position: 'absolute', + top: 0, + right: 0, + width: 100, + height: 55 + }, + '& .pinnedColumn': { + position: 'sticky', + right: 0, + top: 0, + borderLeft: '1px solid' + grey[300] + }, + '& .MuiDataGrid-columnHeaders': { + position: 'relative', + background: grey[50] + }, + '& .MuiDataGrid-columnHeader:focus-within': { + outline: 'none', + background: grey[200] + }, + '& .MuiDataGrid-columnHeaderTitle': { + fontWeight: 700, + textTransform: 'uppercase', + color: 'text.secondary' + }, + '& .MuiDataGrid-cell': { + py: 0.75, + background: '#fff', + '&.MuiDataGrid-cell--editing:focus-within': { + outline: 'none' }, + '&.MuiDataGrid-cell--editing': { + p: 0.5, + backgroundColor: cyan[100] + } + }, + '& .MuiDataGrid-row--editing': { + boxShadow: 'none', + backgroundColor: cyan[50], '& .MuiDataGrid-cell': { - py: 0.75, - background: '#fff', - '&.MuiDataGrid-cell--editing:focus-within': { - outline: 'none' - }, - '&.MuiDataGrid-cell--editing': { - p: 0.5, - backgroundColor: cyan[100] - } + backgroundColor: cyan[50] }, - '& .MuiDataGrid-row--editing': { - boxShadow: 'none', - backgroundColor: cyan[50], - '& .MuiDataGrid-cell': { - backgroundColor: cyan[50] - }, - '&.error': { - '& .MuiDataGrid-cell, .MuiDataGrid-cell--editing': { - backgroundColor: 'rgb(251, 237, 238)' - } - } - }, - '& .MuiDataGrid-editInputCell': { - border: '1px solid #ccc', - '&:hover': { - borderColor: 'primary.main' - }, - '&.Mui-focused': { - borderColor: 'primary.main', - outlineWidth: '2px', - outlineStyle: 'solid', - outlineColor: 'primary.main', - outlineOffset: '-2px' - } - }, - '& .MuiInputBase-root': { - height: '40px', - borderRadius: '4px', - background: '#fff', - fontSize: '0.875rem', - '&.MuiDataGrid-editInputCell': { - padding: 0 - } - }, - '& .MuiOutlinedInput-root': { - borderRadius: '4px', - background: '#fff', - border: 'none', - '&:hover': { - borderColor: 'primary.main' - }, - '&:hover > fieldset': { - border: '1px solid primary.main' + '&.error': { + '& .MuiDataGrid-cell, .MuiDataGrid-cell--editing': { + backgroundColor: 'rgb(251, 237, 238)' } + } + }, + '& .MuiDataGrid-editInputCell': { + border: '1px solid #ccc', + '&:hover': { + borderColor: 'primary.main' }, - '& .MuiOutlinedInput-notchedOutline': { - border: '1px solid ' + grey[300], - '&.Mui-focused': { - borderColor: 'primary.main' - } + '&.Mui-focused': { + borderColor: 'primary.main', + outlineWidth: '2px', + outlineStyle: 'solid', + outlineColor: 'primary.main', + outlineOffset: '-2px' + } + }, + '& .MuiInputBase-root': { + height: '40px', + borderRadius: '4px', + background: '#fff', + fontSize: '0.875rem', + '&.MuiDataGrid-editInputCell': { + padding: 0 + } + }, + '& .MuiOutlinedInput-root': { + borderRadius: '4px', + background: '#fff', + border: 'none', + '&:hover': { + borderColor: 'primary.main' }, - '& .MuiDataGrid-virtualScrollerContent, .MuiDataGrid-overlay': { - background: grey[100] + '&:hover > fieldset': { + border: '1px solid primary.main' + } + }, + '& .MuiOutlinedInput-notchedOutline': { + border: '1px solid ' + grey[300], + '&.Mui-focused': { + borderColor: 'primary.main' } - }} - /> - + }, + '& .MuiDataGrid-virtualScrollerContent, .MuiDataGrid-overlay': { + background: grey[100] + } + }} + /> ); }; diff --git a/app/src/features/surveys/sampling-information/periods/table/SamplingPeriodTable.tsx b/app/src/features/surveys/sampling-information/periods/table/SamplingPeriodTable.tsx index d1553e6dda..ea9bcec0f8 100644 --- a/app/src/features/surveys/sampling-information/periods/table/SamplingPeriodTable.tsx +++ b/app/src/features/surveys/sampling-information/periods/table/SamplingPeriodTable.tsx @@ -1,15 +1,12 @@ -import { mdiArrowTopRight } from '@mdi/js'; import Typography from '@mui/material/Typography'; -import { GridColDef, GridOverlay } from '@mui/x-data-grid'; +import { GridColDef } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; -import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import dayjs from 'dayjs'; import { useCodesContext } from 'hooks/useContext'; -import { IGetSampleSiteResponse } from 'interfaces/useSamplingSiteApi.interface'; import { getCodesName } from 'utils/Utils'; -interface ISamplingSitePeriodRowData { +export interface ISamplingSitePeriodRowData { id: number; sample_site: string; sample_method: string; @@ -21,7 +18,7 @@ interface ISamplingSitePeriodRowData { } interface ISamplingPeriodTableProps { - sites?: IGetSampleSiteResponse; + periods: ISamplingSitePeriodRowData[]; } /** @@ -31,30 +28,10 @@ interface ISamplingPeriodTableProps { * @returns {*} */ export const SamplingPeriodTable = (props: ISamplingPeriodTableProps) => { - const { sites } = props; + const { periods } = props; const codesContext = useCodesContext(); - const rows: ISamplingSitePeriodRowData[] = []; - - // Flatten the sample sites, methods, and periods into a single array of rows - for (const site of sites?.sampleSites ?? []) { - for (const method of site.sample_methods) { - for (const period of method.sample_periods) { - rows.push({ - sample_site: site.name, - sample_method: method.technique.name, - method_response_metric_id: method.method_response_metric_id, - start_date: period.start_date, - end_date: period.end_date, - start_time: period.start_time, - end_time: period.end_time, - id: period.survey_sample_period_id - }); - } - } - } - const columns: GridColDef[] = [ { field: 'sample_site', @@ -113,7 +90,7 @@ export const SamplingPeriodTable = (props: ISamplingPeriodTableProps) => { autoHeight getRowHeight={() => 'auto'} disableColumnMenu - rows={rows} + rows={periods} getRowId={(row: ISamplingSitePeriodRowData) => row.id} columns={columns} checkboxSelection={false} @@ -124,28 +101,6 @@ export const SamplingPeriodTable = (props: ISamplingPeriodTableProps) => { } }} pageSizeOptions={[10, 25, 50]} - noRowsOverlay={ - - - - } - sx={{ - '& .MuiDataGrid-virtualScroller': { - height: rows.length === 0 ? '250px' : 'unset', - overflowY: 'auto !important', - overflowX: 'hidden' - }, - '& .MuiDataGrid-overlay': { - height: '250px', - display: 'flex', - justifyContent: 'center', - alignItems: 'center' - } - }} /> ); }; diff --git a/app/src/features/surveys/sampling-information/sites/SamplingSiteContainer.tsx b/app/src/features/surveys/sampling-information/sites/SamplingSiteContainer.tsx index a8b3b89150..ffa68724c9 100644 --- a/app/src/features/surveys/sampling-information/sites/SamplingSiteContainer.tsx +++ b/app/src/features/surveys/sampling-information/sites/SamplingSiteContainer.tsx @@ -1,4 +1,4 @@ -import { mdiDotsVertical, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; +import { mdiArrowTopRight, mdiDotsVertical, mdiPlus, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -14,17 +14,20 @@ import Typography from '@mui/material/Typography'; import { GridRowSelectionModel } from '@mui/x-data-grid'; import { LoadingGuard } from 'components/loading/LoadingGuard'; import { SkeletonMap, SkeletonTable } from 'components/loading/SkeletonLoaders'; -import { SamplingPeriodTable } from 'features/surveys/sampling-information/periods/table/SamplingPeriodTable'; +import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; +import { + ISamplingSitePeriodRowData, + SamplingPeriodTable +} from 'features/surveys/sampling-information/periods/table/SamplingPeriodTable'; import { SamplingSiteMapContainer } from 'features/surveys/sampling-information/sites/map/SamplingSiteMapContainer'; import { SamplingSiteTable } from 'features/surveys/sampling-information/sites/table/SamplingSiteTable'; import { - ISamplingSiteCount, SamplingSiteManageTableView, SamplingSiteTabs } from 'features/surveys/sampling-information/sites/table/SamplingSiteTabs'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { useDialogContext, useSurveyContext } from 'hooks/useContext'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; /** @@ -45,10 +48,34 @@ const SamplingSiteContainer = () => { // Controls whether sites, methods, or periods are shown const [activeView, setActiveView] = useState(SamplingSiteManageTableView.SITES); - const sampleSites = surveyContext.sampleSiteDataLoader.data; - const sampleSiteCount = sampleSites?.pagination.total ?? 0; - const sampleMethods = sampleSites?.sampleSites.flatMap((site) => site.sample_methods) || []; - const samplePeriods = sampleMethods.flatMap((method) => method.sample_periods); + const sampleSites = useMemo( + () => surveyContext.sampleSiteDataLoader.data?.sampleSites ?? [], + [surveyContext.sampleSiteDataLoader.data?.sampleSites] + ); + const sampleSiteCount = surveyContext.sampleSiteDataLoader.data?.pagination.total ?? 0; + + const samplePeriods: ISamplingSitePeriodRowData[] = useMemo(() => { + const data: ISamplingSitePeriodRowData[] = []; + + for (const site of sampleSites) { + for (const method of site.sample_methods) { + for (const period of method.sample_periods) { + data.push({ + id: period.survey_sample_period_id, + sample_site: site.name, + sample_method: method.technique.name, + method_response_metric_id: method.method_response_metric_id, + start_date: period.start_date, + end_date: period.end_date, + start_time: period.start_time, + end_time: period.end_time + }); + } + } + } + + return data; + }, [sampleSites]); useEffect(() => { surveyContext.sampleSiteDataLoader.load(surveyContext.projectId, surveyContext.surveyId); @@ -111,10 +138,10 @@ const SamplingSiteContainer = () => { }; // Counts for the toggle button labels - const counts: ISamplingSiteCount[] = [ - { type: SamplingSiteManageTableView.SITES, value: sampleSiteCount }, - { type: SamplingSiteManageTableView.PERIODS, value: samplePeriods.length } - ]; + const viewCounts = { + [SamplingSiteManageTableView.SITES]: sampleSiteCount, + [SamplingSiteManageTableView.PERIODS]: samplePeriods.length + }; return ( <> @@ -144,6 +171,7 @@ const SamplingSiteContainer = () => {