From b9106aff6371f78c3c941860ccc3aadd48fa5bdd Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Wed, 27 Nov 2024 14:47:12 -0800 Subject: [PATCH 1/4] Add pagination to sample periods table on the manage sampling information page. Some minor tweaks to existing related components (tweaks the jsdoc, sorting the props, etc). --- .../toolbar/CustomToggleButtonGroup.tsx | 86 ++++++-- .../SamplingSiteListContainer.tsx | 2 +- .../periods/table/SamplingPeriodTable.tsx | 41 ++-- .../sites/table/SamplingSiteTable.tsx | 4 +- .../table/SamplingSiteTableContainer.tsx | 183 +++++++++++------- .../table/view/SamplingSiteTableView.tsx | 71 ------- .../SurveySamplingTableContainer.tsx | 7 +- 7 files changed, 209 insertions(+), 185 deletions(-) delete mode 100644 app/src/features/surveys/sampling-information/sites/table/view/SamplingSiteTableView.tsx diff --git a/app/src/components/toolbar/CustomToggleButtonGroup.tsx b/app/src/components/toolbar/CustomToggleButtonGroup.tsx index c0f504971d..3ee4460524 100644 --- a/app/src/components/toolbar/CustomToggleButtonGroup.tsx +++ b/app/src/components/toolbar/CustomToggleButtonGroup.tsx @@ -3,26 +3,73 @@ import Button from '@mui/material/Button'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -interface CustomToggleButtonGroupProps { - views: Array<{ value: T; label: string; icon: string }>; - activeView: T; - onViewChange: (view: T) => void; +export interface ToggleButtonView { + /** + * The value of the toggle button, which will be passed to the `onViewChange` callback. + * + * @type {ViewValueType} + * @memberof ToggleButtonView + */ + value: ViewValueType; + /** + * The label to display for the toggle button. + * + * @type {string} + * @memberof ToggleButtonView + */ + label: string; + /** + * An optional start icon. + * + * @type {string} + * @memberof ToggleButtonView + */ + icon?: string; +} + +interface CustomToggleButtonGroupProps { + /** + * An array of views to display in the toggle button group. + * + * @type {ToggleButtonView[]} + * @memberof CustomToggleButtonGroupProps + */ + views: ToggleButtonView[]; + /** + * The currently active view. + * + * @type {ViewValueType} + * @memberof CustomToggleButtonGroupProps + */ + activeView: ViewValueType; + /** + * Callback fired when a toggle button is clicked. + * + * @memberof CustomToggleButtonGroupProps + */ + onViewChange: (view: ViewValueType) => void; + /** + * The orientation of the toggle button group. + * + * @type {('horizontal' | 'vertical')} + * @memberof CustomToggleButtonGroupProps + */ + orientation: 'horizontal' | 'vertical'; } /** * A custom toggle button group that allows users to select from multiple views. * - * TODO: Update all togglebuttongroups throughout the app to use this component for consistent styling - * - * @param {CustomToggleButtonGroupProps} props + * @template ViewValueType + * @param {CustomToggleButtonGroupProps} props * @return {*} */ -const CustomToggleButtonGroup = (props: CustomToggleButtonGroupProps) => { - const { views, activeView, onViewChange } = props; +const CustomToggleButtonGroup = (props: CustomToggleButtonGroupProps) => { + const { views, activeView, onViewChange, orientation } = props; return ( { if (view) { @@ -45,16 +92,15 @@ const CustomToggleButtonGroup = (props: CustomToggleButtonGrou justifyContent: 'flex-start' } }}> - {views.map((view) => ( - } - value={view.value}> - {view.label} - - ))} + {views.map((view) => { + const startIcon = (view.icon && ) || undefined; + + return ( + + {view.label} + + ); + })} ); }; diff --git a/app/src/features/surveys/observations/sampling-sites/SamplingSiteListContainer.tsx b/app/src/features/surveys/observations/sampling-sites/SamplingSiteListContainer.tsx index 2506ec6e94..54b825fd66 100644 --- a/app/src/features/surveys/observations/sampling-sites/SamplingSiteListContainer.tsx +++ b/app/src/features/surveys/observations/sampling-sites/SamplingSiteListContainer.tsx @@ -69,7 +69,7 @@ export const SamplingSiteListContainer = () => { }; }, [sortModel, paginationModel]); - // Refresh survey list when pagination or sort changes + // Refresh survey list when pagination changes useEffect(() => { sampleSiteDataLoader.refresh(pagination); 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 9ef1a9e0a1..bc3fa998ab 100644 --- a/app/src/features/surveys/sampling-information/periods/table/SamplingPeriodTable.tsx +++ b/app/src/features/surveys/sampling-information/periods/table/SamplingPeriodTable.tsx @@ -1,53 +1,52 @@ import Typography from '@mui/material/Typography'; -import { GridColDef, GridPaginationModel, GridSortModel } from '@mui/x-data-grid'; +import { GridColDef, GridPaginationModel, GridRowSelectionModel, GridSortModel } from '@mui/x-data-grid'; import { StyledDataGrid } from 'components/data-grid/StyledDataGrid'; import { DATE_FORMAT } from 'constants/dateTimeFormats'; import dayjs from 'dayjs'; import { useCodesContext } from 'hooks/useContext'; +import { IFindSamplePeriodRecord } from 'interfaces/useSamplingSiteApi.interface'; import { formatTimeDifference } from 'utils/datetime'; import { getCodesName } from 'utils/Utils'; -export interface ISamplingSitePeriodRowData { - id: number; - sample_site: string; - sample_method: string; - method_response_metric_id: number; - start_date: string | null; - end_date: string | null; - start_time: string | null; - end_time: string | null; -} - interface ISamplingPeriodTableProps { - periods: ISamplingSitePeriodRowData[]; + periods: IFindSamplePeriodRecord[]; + selectedRows: GridRowSelectionModel; + setSelectedRows: (selection: GridRowSelectionModel) => void; paginationModel: GridPaginationModel; setPaginationModel: React.Dispatch>; sortModel: GridSortModel; setSortModel: React.Dispatch>; + pageSizeOptions: number[]; rowCount: number; } /** * Renders a table of sampling periods. * - * @param props {} + * @param {ISamplingPeriodTableProps} props * @returns {*} */ export const SamplingPeriodTable = (props: ISamplingPeriodTableProps) => { - const { periods, paginationModel, setPaginationModel, sortModel, setSortModel, rowCount } = props; + const { periods, paginationModel, setPaginationModel, sortModel, setSortModel, pageSizeOptions, rowCount } = props; const codesContext = useCodesContext(); - const columns: GridColDef[] = [ + const columns: GridColDef[] = [ { field: 'sample_site', headerName: 'Site', - flex: 1 + flex: 1, + valueGetter: (params) => { + return params.row.sample_site.name; + } }, { field: 'sample_method', headerName: 'Technique', - flex: 1 + flex: 1, + valueGetter: (params) => { + return params.row.method_technique.name; + } }, { field: 'method_response_metric_id', @@ -57,7 +56,7 @@ export const SamplingPeriodTable = (props: ISamplingPeriodTableProps) => { const value = getCodesName( codesContext.codesDataLoader.data, 'method_response_metrics', - params.row.method_response_metric_id + params.row.sample_method.method_response_metric_id ); return value; @@ -112,7 +111,7 @@ export const SamplingPeriodTable = (props: ISamplingPeriodTableProps) => { autoHeight={false} getRowHeight={() => 'auto'} rows={periods} - getRowId={(row: ISamplingSitePeriodRowData) => row.id} + getRowId={(row: IFindSamplePeriodRecord) => row.survey_sample_period_id} columns={columns} checkboxSelection={false} disableRowSelectionOnClick @@ -128,7 +127,7 @@ export const SamplingPeriodTable = (props: ISamplingPeriodTableProps) => { paginationModel } }} - pageSizeOptions={[10, 25, 50]} + pageSizeOptions={pageSizeOptions} /> ); }; diff --git a/app/src/features/surveys/sampling-information/sites/table/SamplingSiteTable.tsx b/app/src/features/surveys/sampling-information/sites/table/SamplingSiteTable.tsx index 9d03c9de16..65c8fa4402 100644 --- a/app/src/features/surveys/sampling-information/sites/table/SamplingSiteTable.tsx +++ b/app/src/features/surveys/sampling-information/sites/table/SamplingSiteTable.tsx @@ -32,8 +32,8 @@ interface ISamplingSiteTableProps { setSelectedRows: (selection: GridRowSelectionModel) => void; paginationModel: GridPaginationModel; setPaginationModel: React.Dispatch>; - setSortModel: React.Dispatch>; sortModel: GridSortModel; + setSortModel: React.Dispatch>; pageSizeOptions: number[]; rowCount: number; /** @@ -45,7 +45,7 @@ interface ISamplingSiteTableProps { /** * Returns a table of sampling sites with edit actions * - * @param props {} + * @param {ISamplingSiteTableProps} props * @returns {*} */ export const SamplingSiteTable = (props: ISamplingSiteTableProps) => { diff --git a/app/src/features/surveys/sampling-information/sites/table/SamplingSiteTableContainer.tsx b/app/src/features/surveys/sampling-information/sites/table/SamplingSiteTableContainer.tsx index ae15bd6119..80535ca85e 100644 --- a/app/src/features/surveys/sampling-information/sites/table/SamplingSiteTableContainer.tsx +++ b/app/src/features/surveys/sampling-information/sites/table/SamplingSiteTableContainer.tsx @@ -1,4 +1,4 @@ -import { mdiArrowTopRight, mdiDotsVertical, mdiTrashCanOutline } from '@mdi/js'; +import { mdiArrowTopRight, mdiCalendarRange, mdiDotsVertical, mdiMapMarker, mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; import Box from '@mui/material/Box'; import Divider from '@mui/material/Divider'; @@ -13,6 +13,7 @@ import { GridPaginationModel, GridRowSelectionModel, GridSortModel } from '@mui/ import { LoadingGuard } from 'components/loading/LoadingGuard'; import { SkeletonTable } from 'components/loading/SkeletonLoaders'; import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; +import CustomToggleButtonGroup from 'components/toolbar/CustomToggleButtonGroup'; import { SamplingPeriodTable } from 'features/surveys/sampling-information/periods/table/SamplingPeriodTable'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { useDialogContext, useSurveyContext } from 'hooks/useContext'; @@ -21,11 +22,15 @@ import { useEffect, useMemo, useState } from 'react'; import { ApiPaginationRequestOptions } from 'types/misc'; import { firstOrNull } from 'utils/Utils'; import { SamplingSiteTable } from './SamplingSiteTable'; -import { SamplingSiteManageTableView, SamplingSiteTableView } from './view/SamplingSiteTableView'; const pageSizeOptions = [10, 25, 50]; -export interface ISamplingSitePeriodRowData { +export enum SamplingViews { + SITES = 'SITES', + PERIODS = 'PERIODS' +} + +export interface ISamplingPeriodRowData { id: number; sample_site: string; sample_method: string; @@ -42,71 +47,105 @@ export interface ISamplingSitePeriodRowData { * @returns {*} */ export const SamplingSiteTableContainer = () => { - const biohubApi = useBiohubApi(); - const surveyContext = useSurveyContext(); const dialogContext = useDialogContext(); + const surveyContext = useSurveyContext(); + const biohubApi = useBiohubApi(); + + // Action menu const [headerAnchorEl, setHeaderAnchorEl] = useState(null); - const [selectedRows, setSelectedRows] = useState([]); - // Controls whether sites, methods, or periods are shown - const [activeView, setActiveView] = useState(SamplingSiteManageTableView.SITES); + // Views + const [activeView, setActiveView] = useState(SamplingViews.SITES); + + const views = [ + { value: SamplingViews.SITES, label: 'Sampling Sites', icon: mdiMapMarker }, + { value: SamplingViews.PERIODS, label: 'Sampling Periods', icon: mdiCalendarRange } + ]; - const [paginationModel, setPaginationModel] = useState({ + // Sites + const [selectedSites, setSelectedSites] = useState([]); + + const [sitesPaginationModel, setSitesPaginationModel] = useState({ page: 0, pageSize: pageSizeOptions[0] }); - const [sortModel, setSortModel] = useState([]); + + const [sitesSortModel, setSitesSortModel] = useState([]); + + const sitesPagination: ApiPaginationRequestOptions = useMemo(() => { + const sort = firstOrNull(sitesSortModel); + + return { + limit: sitesPaginationModel.pageSize, + sort: sort?.field || undefined, + order: sort?.sort || undefined, + + // API sitesPagination pages begin at 1, but MUI DataGrid sitesPagination begins at 0. + page: sitesPaginationModel.page + 1 + }; + }, [sitesSortModel, sitesPaginationModel]); const samplingSitesDataLoader = useDataLoader((pagination: ApiPaginationRequestOptions) => biohubApi.samplingSite.getSampleSites(surveyContext.projectId, surveyContext.surveyId, { pagination }) ); - const pagination: ApiPaginationRequestOptions = useMemo(() => { - const sort = firstOrNull(sortModel); + // Periods + const [selectedPeriods, setSelectedPeriods] = useState([]); + + const [periodsPaginationModel, setPeriodsPaginationModel] = useState({ + page: 0, + pageSize: pageSizeOptions[0] + }); + + const [periodsSortModel, setPeriodsSortModel] = useState([]); + const periodsPagination: ApiPaginationRequestOptions = useMemo(() => { + const sort = firstOrNull(periodsSortModel); return { - limit: paginationModel.pageSize, + limit: periodsPaginationModel.pageSize, sort: sort?.field || undefined, order: sort?.sort || undefined, - - // API pagination pages begin at 1, but MUI DataGrid pagination begins at 0. - page: paginationModel.page + 1 + page: periodsPaginationModel.page + 1 }; - }, [sortModel, paginationModel]); + }, [periodsSortModel, periodsPaginationModel]); + + const samplingPeriodsDataLoader = useDataLoader((pagination: ApiPaginationRequestOptions) => + biohubApi.samplingSite.findSamplePeriods({ survey_id: surveyContext.surveyId }, pagination) + ); - // Refresh survey list when pagination or sort changes useEffect(() => { - samplingSitesDataLoader.refresh(pagination); + // Refresh active view data loader when switching to the view for the first time + if (activeView === SamplingViews.SITES && !samplingSitesDataLoader.data) { + samplingSitesDataLoader.refresh(sitesPagination); + } - // Adding a DataLoader as a dependency causes an infinite rerender loop if a useEffect calls `.refresh` + if (activeView === SamplingViews.PERIODS && !samplingPeriodsDataLoader.data) { + samplingPeriodsDataLoader.refresh(periodsPagination); + } + // Including data loaders in the dependency array causes infinite reloads // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pagination]); - - const sampleSites = useMemo(() => samplingSitesDataLoader.data?.sampleSites ?? [], [samplingSitesDataLoader.data]); - - 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 - }); - } - } + }, [activeView]); + + useEffect(() => { + if (activeView === SamplingViews.SITES && Number(samplingSitesDataLoader.data?.pagination.total) !== 0) { + samplingSitesDataLoader.refresh(sitesPagination); } + // Including data loaders in the dependency array causes infinite reloads + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sitesPagination]); + + useEffect(() => { + if (activeView === SamplingViews.PERIODS && Number(samplingPeriodsDataLoader.data?.pagination.total) !== 0) { + samplingPeriodsDataLoader.refresh(periodsPagination); + } + // Including data loaders in the dependency array causes infinite reloads + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [periodsPagination]); - return data; - }, [sampleSites]); + // Data + const sampleSites = samplingSitesDataLoader.data?.sampleSites ?? []; + const samplePeriods = samplingPeriodsDataLoader.data?.periods ?? []; // Handler for bulk delete operation const handleBulkDelete = async () => { @@ -114,14 +153,14 @@ export const SamplingSiteTableContainer = () => { await biohubApi.samplingSite.deleteSampleSites( surveyContext.projectId, surveyContext.surveyId, - selectedRows.map((site) => Number(site)) // Convert GridRowId to number[] + selectedSites.map((site) => Number(site)) // Convert GridRowId to number[] ); dialogContext.setYesNoDialog({ open: false }); // Close confirmation dialog - setSelectedRows([]); // Clear selection - samplingSitesDataLoader.refresh(pagination); // Refresh data + setSelectedSites([]); // Clear selection + samplingSitesDataLoader.refresh(sitesPagination); // Refresh data } catch (error) { dialogContext.setYesNoDialog({ open: false }); // Close confirmation dialog on error - setSelectedRows([]); // Clear selection + setSelectedSites([]); // Clear selection // Show snackbar with error message dialogContext.setSnackbar({ snackbarMessage: ( @@ -141,7 +180,7 @@ export const SamplingSiteTableContainer = () => { const handleDelete = async (sampleSiteId: number) => { await biohubApi.samplingSite.deleteSampleSite(surveyContext.projectId, surveyContext.surveyId, sampleSiteId); - samplingSitesDataLoader.refresh(pagination); // Refresh data + samplingSitesDataLoader.refresh(sitesPagination); // Refresh data }; // Handler for clicking on header menu (bulk actions) @@ -195,13 +234,18 @@ export const SamplingSiteTableContainer = () => { width: '100%' }}> {/* Toggle buttons for changing between sites, methods, and periods */} - + setActiveView(view)} + orientation="horizontal" + /> @@ -212,7 +256,7 @@ export const SamplingSiteTableContainer = () => { {/* Data tables */} - {activeView === SamplingSiteManageTableView.SITES && ( + {activeView === SamplingViews.SITES && ( { hasNoDataFallbackDelay={100}> )} - {activeView === SamplingSiteManageTableView.PERIODS && ( + {activeView === SamplingViews.PERIODS && ( } isLoadingFallbackDelay={100} @@ -263,14 +308,14 @@ export const SamplingSiteTableContainer = () => { hasNoDataFallbackDelay={100}> {}} - sortModel={[]} - setSortModel={() => {}} - rowCount={samplePeriods.length} + selectedRows={selectedPeriods} + setSelectedRows={setSelectedPeriods} + paginationModel={periodsPaginationModel} + setPaginationModel={setPeriodsPaginationModel} + sortModel={periodsSortModel} + setSortModel={setPeriodsSortModel} + pageSizeOptions={pageSizeOptions} + rowCount={samplingPeriodsDataLoader.data?.pagination.total ?? 0} /> )} diff --git a/app/src/features/surveys/sampling-information/sites/table/view/SamplingSiteTableView.tsx b/app/src/features/surveys/sampling-information/sites/table/view/SamplingSiteTableView.tsx deleted file mode 100644 index 7f008713b9..0000000000 --- a/app/src/features/surveys/sampling-information/sites/table/view/SamplingSiteTableView.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { mdiCalendarRange, mdiMapMarker } from '@mdi/js'; -import { Icon } from '@mdi/react'; -import Button from '@mui/material/Button'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import { SetStateAction } from 'react'; - -export enum SamplingSiteManageTableView { - SITES = 'SITES', - PERIODS = 'PERIODS' -} - -interface ISamplingSiteManageTableView { - value: SamplingSiteManageTableView; - icon: React.ReactNode; -} - -export type ISamplingSiteCount = Record; - -interface ISamplingSiteTableViewProps { - activeView: SamplingSiteManageTableView; - setActiveView: React.Dispatch>; -} - -/** - * Renders tab controls for the sampling site table, which allow the user to switch between viewing sites and periods. - * - * @param {ISamplingSiteTableViewProps} props - * @return {*} - */ -export const SamplingSiteTableView = (props: ISamplingSiteTableViewProps) => { - const { activeView, setActiveView } = props; - - const views: ISamplingSiteManageTableView[] = [ - { value: SamplingSiteManageTableView.SITES, icon: }, - { value: SamplingSiteManageTableView.PERIODS, icon: } - ]; - - const updateDatasetView = (_: React.MouseEvent, view: SamplingSiteManageTableView) => { - if (view) { - setActiveView(view); - } - }; - - return ( - - {views.map((view) => ( - - {view.value} - - ))} - - ); -}; diff --git a/app/src/features/surveys/view/components/sampling-data/SurveySamplingTableContainer.tsx b/app/src/features/surveys/view/components/sampling-data/SurveySamplingTableContainer.tsx index 17e5436d4a..5dcbb61316 100644 --- a/app/src/features/surveys/view/components/sampling-data/SurveySamplingTableContainer.tsx +++ b/app/src/features/surveys/view/components/sampling-data/SurveySamplingTableContainer.tsx @@ -159,7 +159,12 @@ export const SurveySamplingTableContainer = () => { - + From 70a300ad25b950b8f46ac3241c70ed899840f38b Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Wed, 27 Nov 2024 16:39:20 -0800 Subject: [PATCH 2/4] Replace most instances of `ToggleButtonGroup` with `CustomToggleButtonGroup`. Tweak CSS for StyledDataGrid. Remove padding around uses of StyledDataGrid. --- .../components/data-grid/StyledDataGrid.tsx | 30 ++++++-- .../features/admin/alert/AlertContainer.tsx | 41 +++-------- .../AccessRequestContainer.tsx | 72 ++++--------------- .../admin/users/active/ActiveUsersList.tsx | 6 +- .../users/projects/UsersDetailProjects.tsx | 2 +- .../projects/view/ProjectAttachments.tsx | 9 ++- app/src/features/standards/StandardsPage.tsx | 31 ++++---- .../standards/components/StandardsToolbar.tsx | 64 ----------------- .../view/species/SpeciesStandardsResults.tsx | 26 +++---- .../components/SpeciesStandardsToolbar.tsx | 70 ------------------ .../list-data/ListDataTableContainer.tsx | 57 ++++----------- .../project/ProjectsListContainer.tsx | 6 +- .../list-data/survey/SurveysListContainer.tsx | 6 +- .../TabularDataTableContainer.tsx | 60 ++++------------ .../animal/AnimalsListContainer.tsx | 6 +- .../observation/ObservationsListContainer.tsx | 6 +- .../telemetry/TelemetryListContainer.tsx | 2 +- .../features/surveys/list/SurveysListPage.tsx | 4 +- .../components/ConfigureColumnsPage.tsx | 71 ++++-------------- .../device-keys/TelemetryDeviceKeysList.tsx | 4 +- .../surveys/view/SurveyAttachments.tsx | 61 ++++++++-------- .../analytics/SurveyObservationAnalytics.tsx | 29 +++----- .../SurveyObservationTabularDataContainer.tsx | 52 +++----------- .../survey-spatial/SurveySpatialContainer.tsx | 21 +----- .../components/SurveySpatialToolbar.tsx | 54 +++----------- .../components/animal/SurveySpatialAnimal.tsx | 2 +- .../telemetry/SurveySpatialTelemetry.tsx | 2 +- 27 files changed, 195 insertions(+), 599 deletions(-) delete mode 100644 app/src/features/standards/components/StandardsToolbar.tsx delete mode 100644 app/src/features/standards/view/species/components/SpeciesStandardsToolbar.tsx diff --git a/app/src/components/data-grid/StyledDataGrid.tsx b/app/src/components/data-grid/StyledDataGrid.tsx index 566f929745..c7fddb4e62 100644 --- a/app/src/components/data-grid/StyledDataGrid.tsx +++ b/app/src/components/data-grid/StyledDataGrid.tsx @@ -43,12 +43,31 @@ export const StyledDataGrid = (props: StyledD borderBottom: 'none' } }, - '& .MuiDataGrid-columnHeader:first-of-type, .MuiDataGrid-cell:first-of-type': { - pl: 2 + // Define custom header padding for the first column vs every other column + '& .MuiDataGrid-columnHeader:first-of-type:not(.MuiDataGrid-columnHeaderCheckbox)': { + pl: 3 // Add extra padding to the first header, unless it is a checkbox header }, - '& .MuiDataGrid-columnHeader:last-of-type, .MuiDataGrid-cell:last-of-type': { - pr: 2 + '& .MuiDataGrid-columnHeader:first-of-type.MuiDataGrid-columnHeaderCheckbox': { + pl: 2 // Add extra padding to the first header when it is a checkbox header }, + '& .MuiDataGrid-columnHeader:not(:first-of-type)': { + pl: 1 // Add extra padding to every other header + }, + // Define custom cell padding for the first column vs every other column + '& .MuiDataGrid-cell:first-of-type:not(.MuiDataGrid-cellCheckbox)': { + pl: 3 // Add extra padding to the first cell, unless it is a checkbox cell + }, + '& .MuiDataGrid-cell:first-of-type.MuiDataGrid-cellCheckbox': { + pl: 2 // Add extra padding to the first cell when it is a checkbox cell + }, + '& .MuiDataGrid-cell:not(:first-of-type)': { + pl: 1 // Add extra padding to every other cell + }, + // Ensure the draggable container is at least 50px wide + '& .MuiDataGrid-columnHeaderDraggableContainer': { + minWidth: '50px' + }, + // Custom styling for cell content at different densities '&.MuiDataGrid-root--densityCompact .MuiDataGrid-cell': { py: '8px', wordWrap: 'anywhere' @@ -61,9 +80,6 @@ export const StyledDataGrid = (props: StyledD py: '22px', wordWrap: 'anywhere' }, - '& .MuiDataGrid-columnHeaderDraggableContainer': { - minWidth: '50px' - }, ...props.sx }} /> diff --git a/app/src/features/admin/alert/AlertContainer.tsx b/app/src/features/admin/alert/AlertContainer.tsx index b1c9a22b71..0da1f0bbe5 100644 --- a/app/src/features/admin/alert/AlertContainer.tsx +++ b/app/src/features/admin/alert/AlertContainer.tsx @@ -4,10 +4,9 @@ import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; import Paper from '@mui/material/Paper'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; +import CustomToggleButtonGroup from 'components/toolbar/CustomToggleButtonGroup'; import dayjs from 'dayjs'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; @@ -76,38 +75,16 @@ const AlertListContainer = () => { - - view && setActiveView(view)} - exclusive - sx={{ - width: '100%', - gap: 1, - '& Button': { - py: 0.5, - px: 1.5, - border: 'none !important', - fontWeight: 700, - borderRadius: '4px !important', - fontSize: '0.875rem', - letterSpacing: '0.02rem' - } - }}> - {views.map(({ value, label, icon }) => ( - }> - {label} - - ))} - + + setActiveView(view)} + orientation="horizontal" + /> - + {/* Modals */} {alertId && modalState.edit && } diff --git a/app/src/features/admin/users/access-requests/AccessRequestContainer.tsx b/app/src/features/admin/users/access-requests/AccessRequestContainer.tsx index 154f211262..336bd4982b 100644 --- a/app/src/features/admin/users/access-requests/AccessRequestContainer.tsx +++ b/app/src/features/admin/users/access-requests/AccessRequestContainer.tsx @@ -1,13 +1,10 @@ import { mdiCancel, mdiCheck, mdiExclamationThick } from '@mdi/js'; -import Icon from '@mdi/react'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; import Paper from '@mui/material/Paper'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup/ToggleButtonGroup'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; +import CustomToggleButtonGroup from 'components/toolbar/CustomToggleButtonGroup'; import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface'; import { useState } from 'react'; import AccessRequestActionedList from './list/actioned/AccessRequestActionedList'; @@ -34,16 +31,16 @@ const AccessRequestContainer = (props: IAccessRequestContainerProps) => { const [activeView, setActiveView] = useState(AccessRequestViewEnum.PENDING); - const views = [ - { value: AccessRequestViewEnum.PENDING, label: 'Pending', icon: mdiExclamationThick }, - { value: AccessRequestViewEnum.ACTIONED, label: 'Approved', icon: mdiCheck }, - { value: AccessRequestViewEnum.REJECTED, label: 'Rejected', icon: mdiCancel } - ]; - const pendingRequests = accessRequests.filter((request) => request.status_name === 'Pending'); const actionedRequests = accessRequests.filter((request) => request.status_name === 'Actioned'); const rejectedRequests = accessRequests.filter((request) => request.status_name === 'Rejected'); + const views = [ + { value: AccessRequestViewEnum.PENDING, label: `Pending (${pendingRequests.length})`, icon: mdiExclamationThick }, + { value: AccessRequestViewEnum.ACTIONED, label: `Approved (${actionedRequests.length})`, icon: mdiCheck }, + { value: AccessRequestViewEnum.REJECTED, label: `Rejected (${rejectedRequests.length})`, icon: mdiCancel } + ]; + return ( @@ -53,58 +50,17 @@ const AccessRequestContainer = (props: IAccessRequestContainerProps) => { - { - if (!view) { - // An active view must be selected at all times - return; - } - + { setActiveView(view); }} - exclusive - sx={{ - width: '100%', - gap: 1, - '& Button': { - py: 0.5, - px: 1.5, - border: 'none !important', - fontWeight: 700, - borderRadius: '4px !important', - fontSize: '0.875rem', - letterSpacing: '0.02rem' - } - }}> - {views.map((view) => { - const getCount = () => { - switch (view.value) { - case AccessRequestViewEnum.PENDING: - return pendingRequests.length; - case AccessRequestViewEnum.ACTIONED: - return actionedRequests.length; - case AccessRequestViewEnum.REJECTED: - return rejectedRequests.length; - default: - return 0; - } - }; - return ( - }> - {view.label} ({getCount()}) - - ); - })} - + orientation="horizontal" + /> - + {activeView === AccessRequestViewEnum.PENDING && ( )} diff --git a/app/src/features/admin/users/active/ActiveUsersList.tsx b/app/src/features/admin/users/active/ActiveUsersList.tsx index 2e9073a02c..d2d988f07e 100644 --- a/app/src/features/admin/users/active/ActiveUsersList.tsx +++ b/app/src/features/admin/users/active/ActiveUsersList.tsx @@ -67,8 +67,8 @@ const ActiveUsersList = (props: IActiveUsersListProps) => { { field: 'system_user_id', headerName: 'ID', - width: 70, - minWidth: 70, + width: 85, + minWidth: 85, renderHeader: () => ( ID @@ -412,7 +412,7 @@ const ActiveUsersList = (props: IActiveUsersListProps) => { - + noRowsMessage="No Active Users" columns={activeUsersColumnDefs} diff --git a/app/src/features/admin/users/projects/UsersDetailProjects.tsx b/app/src/features/admin/users/projects/UsersDetailProjects.tsx index 47e7827515..397a80155c 100644 --- a/app/src/features/admin/users/projects/UsersDetailProjects.tsx +++ b/app/src/features/admin/users/projects/UsersDetailProjects.tsx @@ -163,7 +163,7 @@ const UsersDetailProjects: React.FC = (props) => { - + diff --git a/app/src/features/projects/view/ProjectAttachments.tsx b/app/src/features/projects/view/ProjectAttachments.tsx index 174e6924d3..480edf4579 100644 --- a/app/src/features/projects/view/ProjectAttachments.tsx +++ b/app/src/features/projects/view/ProjectAttachments.tsx @@ -1,6 +1,5 @@ import { mdiAttachment, mdiFilePdfBox, mdiTrayArrowUp } from '@mdi/js'; import Icon from '@mdi/react'; -import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; import { IReportMetaForm } from 'components/attachments/ReportMetaForm'; @@ -107,10 +106,10 @@ const ProjectAttachments = () => { )} /> - - - - + + + + ); }; diff --git a/app/src/features/standards/StandardsPage.tsx b/app/src/features/standards/StandardsPage.tsx index 9a1af27b41..1b43308f82 100644 --- a/app/src/features/standards/StandardsPage.tsx +++ b/app/src/features/standards/StandardsPage.tsx @@ -6,10 +6,10 @@ import Divider from '@mui/material/Divider'; import Paper from '@mui/material/Paper'; import Stack from '@mui/material/Stack'; import PageHeader from 'components/layout/PageHeader'; +import CustomToggleButtonGroup from 'components/toolbar/CustomToggleButtonGroup'; import { SystemAlertBanner } from 'features/alert/banner/SystemAlertBanner'; import { SystemAlertBannerEnum } from 'interfaces/useAlertApi.interface'; import { useState } from 'react'; -import { StandardsToolbar } from './components/StandardsToolbar'; import { EnvironmentStandards } from './view/environment/EnvironmentStandards'; import { MethodStandards } from './view/methods/MethodStandards'; import { SpeciesStandards } from './view/species/SpeciesStandards'; @@ -20,19 +20,13 @@ export enum StandardsPageView { ENVIRONMENT = 'ENVIRONMENT' } -export interface IStandardsPageView { - label: string; - value: StandardsPageView; - icon: string; -} - const StandardsPage = () => { - const [currentView, setCurrentView] = useState(StandardsPageView.SPECIES); + const [activeView, setActiveView] = useState(StandardsPageView.SPECIES); - const views: IStandardsPageView[] = [ - { label: 'Species', value: StandardsPageView.SPECIES, icon: mdiPaw }, - { label: 'Sampling Methods', value: StandardsPageView.METHODS, icon: mdiToolbox }, - { label: 'Environment variables', value: StandardsPageView.ENVIRONMENT, icon: mdiLeaf } + const views = [ + { value: StandardsPageView.SPECIES, label: 'Species', icon: mdiPaw }, + { value: StandardsPageView.METHODS, label: 'Sampling Methods', icon: mdiToolbox }, + { value: StandardsPageView.ENVIRONMENT, label: 'Environment variables', icon: mdiLeaf } ]; return ( @@ -43,20 +37,25 @@ const StandardsPage = () => { {/* TOOLBAR FOR SWITCHING VIEWS */} - + setActiveView(view)} + orientation="vertical" + /> {/* SPECIES STANDARDS */} - {currentView === StandardsPageView.SPECIES && } + {activeView === StandardsPageView.SPECIES && } {/* METHOD STANDARDS */} - {currentView === StandardsPageView.METHODS && } + {activeView === StandardsPageView.METHODS && } {/* ENVIRONMENT STANDARDS */} - {currentView === StandardsPageView.ENVIRONMENT && } + {activeView === StandardsPageView.ENVIRONMENT && } diff --git a/app/src/features/standards/components/StandardsToolbar.tsx b/app/src/features/standards/components/StandardsToolbar.tsx deleted file mode 100644 index 44ff76623c..0000000000 --- a/app/src/features/standards/components/StandardsToolbar.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import Icon from '@mdi/react'; -import Button from '@mui/material/Button'; -import ToggleButton from '@mui/material/ToggleButton/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import Typography from '@mui/material/Typography'; -import React, { SetStateAction } from 'react'; -import { IStandardsPageView, StandardsPageView } from '../StandardsPage'; - -interface IStandardsToolbar { - views: IStandardsPageView[]; - currentView: StandardsPageView; - setCurrentView: React.Dispatch>; -} - -/** - * Toolbar for setting the standards page view - * - * @param props - * @returns - */ -export const StandardsToolbar = (props: IStandardsToolbar) => { - const { views, currentView, setCurrentView } = props; - - return ( - <> - Data types - , view: StandardsPageView | null) => { - if (view) { - setCurrentView(view); - } - }} - exclusive - sx={{ - display: 'flex', - gap: 1, - '& Button': { - py: 1.25, - px: 2.5, - border: 'none', - borderRadius: '4px !important', - fontSize: '0.875rem', - fontWeight: 700, - letterSpacing: '0.02rem', - textAlign: 'left', - justifyContent: 'flex-start' - } - }}> - {views.map((view) => ( - } - key={view.value} - value={view.value} - color="primary"> - {view.label} - - ))} - - - ); -}; diff --git a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx index 859ae312bb..d665721a6f 100644 --- a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx +++ b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx @@ -2,11 +2,16 @@ import { mdiRuler, mdiTag } from '@mdi/js'; import { Box, Divider, Stack, Typography } from '@mui/material'; import { blueGrey, grey } from '@mui/material/colors'; import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; +import CustomToggleButtonGroup from 'components/toolbar/CustomToggleButtonGroup'; import { AccordionStandardCard } from 'features/standards/view/components/AccordionStandardCard'; import { ScientificNameTypography } from 'features/surveys/animals/components/ScientificNameTypography'; import { ISpeciesStandards } from 'interfaces/useStandardsApi.interface'; import { useState } from 'react'; -import SpeciesStandardsToolbar, { SpeciesStandardsViewEnum } from './components/SpeciesStandardsToolbar'; + +enum SpeciesStandardsViewEnum { + MEASUREMENTS = 'measurements', + MARKING_BODY_LOCATIONS = 'marking_body_locations' +} interface ISpeciesStandardsResultsProps { data?: ISpeciesStandards; @@ -35,23 +40,14 @@ const SpeciesStandardsResults = (props: ISpeciesStandardsResultsProps) => { - diff --git a/app/src/features/standards/view/species/components/SpeciesStandardsToolbar.tsx b/app/src/features/standards/view/species/components/SpeciesStandardsToolbar.tsx deleted file mode 100644 index feeca668c7..0000000000 --- a/app/src/features/standards/view/species/components/SpeciesStandardsToolbar.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import Icon from '@mdi/react'; -import { Box, Button, ToggleButton, ToggleButtonGroup } from '@mui/material'; - -export enum SpeciesStandardsViewEnum { - MEASUREMENTS = 'MEASUREMENTS', - MARKING_BODY_LOCATIONS = 'MARKING BODY LOCATIONS' -} - -interface ISurveySpatialDatasetView { - label: string; - icon: string; - value: SpeciesStandardsViewEnum; - isLoading: boolean; -} - -interface ISpeciesStandardsToolbarProps { - updateDatasetView: (view: SpeciesStandardsViewEnum) => void; - views: ISurveySpatialDatasetView[]; - activeView: SpeciesStandardsViewEnum; -} - -/** - * Toolbar for handling what species standards information is displayed - * - * @return {*} - */ -const SpeciesStandardsToolbar = (props: ISpeciesStandardsToolbarProps) => { - const updateDatasetView = (_event: React.MouseEvent, view: SpeciesStandardsViewEnum) => { - if (!view) { - return; - } - - props.updateDatasetView(view); - }; - - return ( - - - {props.views.map((view) => ( - } - value={view.value}> - {view.label} - - ))} - - - ); -}; - -export default SpeciesStandardsToolbar; diff --git a/app/src/features/summary/list-data/ListDataTableContainer.tsx b/app/src/features/summary/list-data/ListDataTableContainer.tsx index 6674848b52..40cf426227 100644 --- a/app/src/features/summary/list-data/ListDataTableContainer.tsx +++ b/app/src/features/summary/list-data/ListDataTableContainer.tsx @@ -3,10 +3,9 @@ import Icon from '@mdi/react'; import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; import Stack from '@mui/material/Stack'; -import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Toolbar from '@mui/material/Toolbar'; import HelpButtonDialog from 'components/buttons/HelpButtonDialog'; +import CustomToggleButtonGroup from 'components/toolbar/CustomToggleButtonGroup'; import ProjectsListContainer from 'features/summary/list-data/project/ProjectsListContainer'; import SurveysListContainer from 'features/summary/list-data/survey/SurveysListContainer'; import { useSearchParams } from 'hooks/useSearchParams'; @@ -31,16 +30,6 @@ type ListDataTableURLParams = { [SHOW_SEARCH_KEY]: SHOW_SEARCH_VALUE; }; -const buttonSx = { - py: 0.5, - px: 1.5, - border: 'none !important', - fontWeight: 700, - borderRadius: '4px !important', - fontSize: '0.875rem', - letterSpacing: '0.02rem' -}; - /** * Data table component for list data (ie: projects, surveys). * @@ -49,7 +38,9 @@ const buttonSx = { export const ListDataTableContainer = () => { const { searchParams, setSearchParams } = useSearchParams(); - const [activeView, setActiveView] = useState(searchParams.get(ACTIVE_VIEW_KEY) ?? ACTIVE_VIEW_VALUE.projects); + const [activeView, setActiveView] = useState( + (searchParams.get(ACTIVE_VIEW_KEY) as ACTIVE_VIEW_VALUE | null) ?? ACTIVE_VIEW_VALUE.projects + ); const [showSearch, setShowSearch] = useState(searchParams.get(SHOW_SEARCH_KEY) === SHOW_SEARCH_VALUE.true); const views = [ @@ -57,40 +48,18 @@ export const ListDataTableContainer = () => { { value: ACTIVE_VIEW_VALUE.surveys, label: 'surveys', icon: mdiListBoxOutline } ]; - const onChangeView = (_: React.MouseEvent, value: ACTIVE_VIEW_VALUE) => { - if (!value) { - // User has clicked the active view, do nothing - return; - } - - setSearchParams(searchParams.set(ACTIVE_VIEW_KEY, value)); - setActiveView(value); - }; - return ( <> - - {views.map((view) => ( - } - value={view.value}> - {view.label} - - ))} - + { + setSearchParams(searchParams.set(ACTIVE_VIEW_KEY, view)); + setActiveView(view); + }} + orientation="horizontal" + />