From c7b17b748705779bb09cca668f4ee45c62ea642a Mon Sep 17 00:00:00 2001 From: Andrew <105487051+LouisThedroux@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:59:46 -0700 Subject: [PATCH 01/19] initial commit from transferring otu of method-standards --- app/src/AppRouter.tsx | 7 ++ .../standards/DoubleStandardsPage.tsx | 103 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 app/src/features/standards/DoubleStandardsPage.tsx diff --git a/app/src/AppRouter.tsx b/app/src/AppRouter.tsx index 3a0ce4c006..f3d6e7ad81 100644 --- a/app/src/AppRouter.tsx +++ b/app/src/AppRouter.tsx @@ -7,6 +7,7 @@ import FundingSourcesRouter from 'features/funding-sources/FundingSourcesRouter' import ProjectsRouter from 'features/projects/ProjectsRouter'; import ResourcesPage from 'features/resources/ResourcesPage'; import SpeciesStandardsPage from 'features/standards/SpeciesStandardsPage'; +import DoubleStandardsPage from 'features/standards/DoubleStandardsPage'; import BaseLayout from 'layouts/BaseLayout'; import AccessDenied from 'pages/403/AccessDenied'; import NotFoundPage from 'pages/404/NotFoundPage'; @@ -105,6 +106,12 @@ const AppRouter: React.FC = () => { + + + + + + diff --git a/app/src/features/standards/DoubleStandardsPage.tsx b/app/src/features/standards/DoubleStandardsPage.tsx new file mode 100644 index 0000000000..e6ef003aa8 --- /dev/null +++ b/app/src/features/standards/DoubleStandardsPage.tsx @@ -0,0 +1,103 @@ +import { Box, Container, Paper, ToggleButton, ToggleButtonGroup, Toolbar, Typography } from '@mui/material'; +import { styled } from '@mui/system'; +import PageHeader from 'components/layout/PageHeader'; +import SpeciesAutocompleteField from 'components/species/components/SpeciesAutocompleteField'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; +import { ITaxonomy } from 'interfaces/useTaxonomyApi.interface'; +import React, { useState } from 'react'; +import SpeciesStandardsResults from './view/SpeciesStandardsResults'; + +/** + * Page to display both species and method standards and data capture standards + * + * @return {*} + */ + +// Custom styled ToggleButton +const StyledToggleButton = styled(ToggleButton)(({ theme }) => ({ + color: theme.palette.text.primary, + borderColor: theme.palette.primary.main, + '&.Mui-selected': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.common.white + }, + '&:hover': { + backgroundColor: theme.palette.primary.light, + color: theme.palette.common.primary + } +})); + +const DoubleStandardsPage = () => { + const [currentTab, setCurrentTab] = useState(''); + + const biohubApi = useBiohubApi(); + const standardsDataLoader = useDataLoader((species: ITaxonomy) => + biohubApi.standards.getSpeciesStandards(species.tsn) + ); + + const views = [ + { label: 'Species Data & Variables', value: 'SPECIES' }, + { label: 'Data Capture & Methodologies', value: 'METHODS' } + ]; + + return ( + <> + + + + +
+ + Standards for Species and Methodologies + +
+
+ + + , view: any) => setCurrentTab(view)} + exclusive + sx={{ mb: 2 }}> + {views.map((view) => ( + + {view.label} + + ))} + + + {currentTab === 'SPECIES' && ( + { + if (value) { + standardsDataLoader.refresh(value); + } + }} + /> + )} + {/* This is te bit of code that shoes the results for search bar. Is there a way to make sure this is only showing when currentTab is species? */} + + + {/* Nothing really going on in this part riht now. Using the search bar to search methodologies in */} + {currentTab === 'METHODS' && ( + + + Data Capture & Methodologies Placeholder + + + This is a placeholder for future functionality related to Data Capture & Methodologies API calls, + where ever that might come from (technique_attribute_quantitativ etc) + + + )} + +
+
+ + ); +}; + +export default DoubleStandardsPage; From 46e1f10133fd03bd644d36fb91d8b3df032e6ec0 Mon Sep 17 00:00:00 2001 From: Andrew <105487051+LouisThedroux@users.noreply.github.com> Date: Thu, 18 Jul 2024 14:01:57 -0700 Subject: [PATCH 02/19] quick div to box fix for header not showing --- app/src/features/standards/DoubleStandardsPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/features/standards/DoubleStandardsPage.tsx b/app/src/features/standards/DoubleStandardsPage.tsx index e6ef003aa8..c1002da2a6 100644 --- a/app/src/features/standards/DoubleStandardsPage.tsx +++ b/app/src/features/standards/DoubleStandardsPage.tsx @@ -47,11 +47,12 @@ const DoubleStandardsPage = () => { -
+ Standards for Species and Methodologies -
+
From 5327db52fd6408f4083d437a9cf1014517df11d8 Mon Sep 17 00:00:00 2001 From: Andrew <105487051+LouisThedroux@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:24:01 -0700 Subject: [PATCH 03/19] Moving components around based off of Mac's suggestions. --- app/src/AppRouter.tsx | 2 +- app/src/features/standards/DoubleStandardsPage.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/AppRouter.tsx b/app/src/AppRouter.tsx index f3d6e7ad81..198667f44f 100644 --- a/app/src/AppRouter.tsx +++ b/app/src/AppRouter.tsx @@ -6,8 +6,8 @@ import AdminUsersRouter from 'features/admin/AdminUsersRouter'; import FundingSourcesRouter from 'features/funding-sources/FundingSourcesRouter'; import ProjectsRouter from 'features/projects/ProjectsRouter'; import ResourcesPage from 'features/resources/ResourcesPage'; -import SpeciesStandardsPage from 'features/standards/SpeciesStandardsPage'; import DoubleStandardsPage from 'features/standards/DoubleStandardsPage'; +import SpeciesStandardsPage from 'features/standards/SpeciesStandardsPage'; import BaseLayout from 'layouts/BaseLayout'; import AccessDenied from 'pages/403/AccessDenied'; import NotFoundPage from 'pages/404/NotFoundPage'; diff --git a/app/src/features/standards/DoubleStandardsPage.tsx b/app/src/features/standards/DoubleStandardsPage.tsx index c1002da2a6..c33d6c091e 100644 --- a/app/src/features/standards/DoubleStandardsPage.tsx +++ b/app/src/features/standards/DoubleStandardsPage.tsx @@ -69,6 +69,7 @@ const DoubleStandardsPage = () => { {currentTab === 'SPECIES' && ( + <> { } }} /> + + )} - {/* This is te bit of code that shoes the results for search bar. Is there a way to make sure this is only showing when currentTab is species? */} - + {/* Nothing really going on in this part riht now. Using the search bar to search methodologies in */} {currentTab === 'METHODS' && ( From 1be615f0da07b707be4b6a614d0bf639c50b52a7 Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Wed, 24 Jul 2024 10:50:22 -0700 Subject: [PATCH 04/19] update standards page layout --- app/src/AppRouter.tsx | 11 +- .../standards/DoubleStandardsPage.tsx | 106 ------------------ .../standards/SpeciesStandardsPage.tsx | 50 --------- app/src/features/standards/StandardsPage.tsx | 53 +++++++++ .../standards/components/StandardsToolbar.tsx | 51 +++++++++ .../components/EnvironmentStandardCard.tsx | 20 +++- .../components/MeasurementStandardCard.tsx | 13 ++- .../view/environment/EnvironmentStandards.tsx | 11 ++ .../view/methods/MethodStandards.tsx | 28 +++++ .../view/methods/MethodStandardsResults.tsx | 25 +++++ .../view/species/SpeciesStandards.tsx | 40 +++++++ .../{ => species}/SpeciesStandardsResults.tsx | 25 ++--- 12 files changed, 241 insertions(+), 192 deletions(-) delete mode 100644 app/src/features/standards/DoubleStandardsPage.tsx delete mode 100644 app/src/features/standards/SpeciesStandardsPage.tsx create mode 100644 app/src/features/standards/StandardsPage.tsx create mode 100644 app/src/features/standards/components/StandardsToolbar.tsx create mode 100644 app/src/features/standards/view/environment/EnvironmentStandards.tsx create mode 100644 app/src/features/standards/view/methods/MethodStandards.tsx create mode 100644 app/src/features/standards/view/methods/MethodStandardsResults.tsx create mode 100644 app/src/features/standards/view/species/SpeciesStandards.tsx rename app/src/features/standards/view/{ => species}/SpeciesStandardsResults.tsx (79%) diff --git a/app/src/AppRouter.tsx b/app/src/AppRouter.tsx index 198667f44f..dd30711acc 100644 --- a/app/src/AppRouter.tsx +++ b/app/src/AppRouter.tsx @@ -6,8 +6,7 @@ import AdminUsersRouter from 'features/admin/AdminUsersRouter'; import FundingSourcesRouter from 'features/funding-sources/FundingSourcesRouter'; import ProjectsRouter from 'features/projects/ProjectsRouter'; import ResourcesPage from 'features/resources/ResourcesPage'; -import DoubleStandardsPage from 'features/standards/DoubleStandardsPage'; -import SpeciesStandardsPage from 'features/standards/SpeciesStandardsPage'; +import StandardsPage from 'features/standards/StandardsPage'; import BaseLayout from 'layouts/BaseLayout'; import AccessDenied from 'pages/403/AccessDenied'; import NotFoundPage from 'pages/404/NotFoundPage'; @@ -102,13 +101,7 @@ const AppRouter: React.FC = () => { - - - - - - - + diff --git a/app/src/features/standards/DoubleStandardsPage.tsx b/app/src/features/standards/DoubleStandardsPage.tsx deleted file mode 100644 index c33d6c091e..0000000000 --- a/app/src/features/standards/DoubleStandardsPage.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { Box, Container, Paper, ToggleButton, ToggleButtonGroup, Toolbar, Typography } from '@mui/material'; -import { styled } from '@mui/system'; -import PageHeader from 'components/layout/PageHeader'; -import SpeciesAutocompleteField from 'components/species/components/SpeciesAutocompleteField'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import useDataLoader from 'hooks/useDataLoader'; -import { ITaxonomy } from 'interfaces/useTaxonomyApi.interface'; -import React, { useState } from 'react'; -import SpeciesStandardsResults from './view/SpeciesStandardsResults'; - -/** - * Page to display both species and method standards and data capture standards - * - * @return {*} - */ - -// Custom styled ToggleButton -const StyledToggleButton = styled(ToggleButton)(({ theme }) => ({ - color: theme.palette.text.primary, - borderColor: theme.palette.primary.main, - '&.Mui-selected': { - backgroundColor: theme.palette.primary.main, - color: theme.palette.common.white - }, - '&:hover': { - backgroundColor: theme.palette.primary.light, - color: theme.palette.common.primary - } -})); - -const DoubleStandardsPage = () => { - const [currentTab, setCurrentTab] = useState(''); - - const biohubApi = useBiohubApi(); - const standardsDataLoader = useDataLoader((species: ITaxonomy) => - biohubApi.standards.getSpeciesStandards(species.tsn) - ); - - const views = [ - { label: 'Species Data & Variables', value: 'SPECIES' }, - { label: 'Data Capture & Methodologies', value: 'METHODS' } - ]; - - return ( - <> - - - - - - - Standards for Species and Methodologies - - - - - - , view: any) => setCurrentTab(view)} - exclusive - sx={{ mb: 2 }}> - {views.map((view) => ( - - {view.label} - - ))} - - - {currentTab === 'SPECIES' && ( - <> - { - if (value) { - standardsDataLoader.refresh(value); - } - }} - /> - - - )} - - - {/* Nothing really going on in this part riht now. Using the search bar to search methodologies in */} - {currentTab === 'METHODS' && ( - - - Data Capture & Methodologies Placeholder - - - This is a placeholder for future functionality related to Data Capture & Methodologies API calls, - where ever that might come from (technique_attribute_quantitativ etc) - - - )} - - - - - ); -}; - -export default DoubleStandardsPage; diff --git a/app/src/features/standards/SpeciesStandardsPage.tsx b/app/src/features/standards/SpeciesStandardsPage.tsx deleted file mode 100644 index b3f7b8a0af..0000000000 --- a/app/src/features/standards/SpeciesStandardsPage.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Box, Container, Paper, Toolbar, Typography } from '@mui/material'; -import PageHeader from 'components/layout/PageHeader'; -import SpeciesAutocompleteField from 'components/species/components/SpeciesAutocompleteField'; -import { useBiohubApi } from 'hooks/useBioHubApi'; -import useDataLoader from 'hooks/useDataLoader'; -import { ITaxonomy } from 'interfaces/useTaxonomyApi.interface'; -import SpeciesStandardsResults from './view/SpeciesStandardsResults'; - -/** - * Page to display species standards, which describes what data can be entered and in what structure - * - * @return {*} - */ -const SpeciesStandardsPage = () => { - const biohubApi = useBiohubApi(); - const standardsDataLoader = useDataLoader((species: ITaxonomy) => - biohubApi.standards.getSpeciesStandards(species.tsn) - ); - - return ( - <> - - - - - - Discover data standards for species - - - - { - if (value) { - standardsDataLoader.refresh(value); - } - }} - /> - - - - - - - - ); -}; - -export default SpeciesStandardsPage; diff --git a/app/src/features/standards/StandardsPage.tsx b/app/src/features/standards/StandardsPage.tsx new file mode 100644 index 0000000000..260aab39ba --- /dev/null +++ b/app/src/features/standards/StandardsPage.tsx @@ -0,0 +1,53 @@ +import Container from '@mui/material/Container'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import PageHeader from 'components/layout/PageHeader'; +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'; + +export enum StandardsPageView { + SPECIES = 'SPECIES', + METHODS = 'METHODS', + ENVIRONMENT = 'ENVIRONMENT' +} + +export interface IStandardsPageView { + label: string; + value: StandardsPageView; +} + +const StandardsPage = () => { + const [currentView, setCurrentView] = useState(StandardsPageView.SPECIES); + + const views: IStandardsPageView[] = [ + { label: 'Species', value: StandardsPageView.SPECIES }, + { label: 'Sampling Methods', value: StandardsPageView.METHODS }, + { label: 'Environment variables', value: StandardsPageView.ENVIRONMENT } + ]; + + return ( + <> + + + + {/* TOOLBAR FOR SWITCHING VIEWS */} + + + {/* SPECIES STANDARDS */} + {currentView === StandardsPageView.SPECIES && } + + {/* METHOD STANDARDS */} + {currentView === StandardsPageView.METHODS && } + + {/* ENVIRONMENT STANDARDS */} + {currentView === StandardsPageView.ENVIRONMENT && } + + + + ); +}; + +export default StandardsPage; diff --git a/app/src/features/standards/components/StandardsToolbar.tsx b/app/src/features/standards/components/StandardsToolbar.tsx new file mode 100644 index 0000000000..5a4806c850 --- /dev/null +++ b/app/src/features/standards/components/StandardsToolbar.tsx @@ -0,0 +1,51 @@ +import ToggleButton from '@mui/material/ToggleButton'; +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +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 ( + <> + , view: StandardsPageView) => setCurrentView(view)} + exclusive + sx={{ + minWidth: '200px', + 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' + } + }}> + {views.map((view) => ( + + {view.label} + + ))} + + + ); +}; diff --git a/app/src/features/standards/view/components/EnvironmentStandardCard.tsx b/app/src/features/standards/view/components/EnvironmentStandardCard.tsx index 036309e7c5..1549dae796 100644 --- a/app/src/features/standards/view/components/EnvironmentStandardCard.tsx +++ b/app/src/features/standards/view/components/EnvironmentStandardCard.tsx @@ -1,7 +1,12 @@ import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; import Icon from '@mdi/react'; -import { Box, Card, Collapse, Paper, Stack, Typography } from '@mui/material'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import Collapse from '@mui/material/Collapse'; import { grey } from '@mui/material/colors'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; import { EnvironmentQualitativeOption } from 'interfaces/useReferenceApi.interface'; import { useState } from 'react'; @@ -23,11 +28,14 @@ const EnvironmentStandardCard = (props: IEnvironmentStandardCard) => { const { small } = props; return ( - setIsCollapsed(!isCollapsed)}> - + + setIsCollapsed(!isCollapsed)}> { const { small } = props; return ( - setIsCollapsed(!isCollapsed)}> - + + setIsCollapsed(!isCollapsed)}> { + return Placeholder for environment standards; +}; diff --git a/app/src/features/standards/view/methods/MethodStandards.tsx b/app/src/features/standards/view/methods/MethodStandards.tsx new file mode 100644 index 0000000000..2f0aab6ace --- /dev/null +++ b/app/src/features/standards/view/methods/MethodStandards.tsx @@ -0,0 +1,28 @@ +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + +/** + * + * This component will handle the data request, then pass the data to its children components. + * + * @returns + */ +export const MethodStandards = () => { + // TODO: Fetch information about methods, like below + // + // const biohubApi = useBiohubApi() + // const methodsDataLoader = useDataLoader(() => ...) + // useEffect(() => {methodsDataLoader.load()}) + + return ( + + + Data Capture & Methodologies Placeholder + + + This is a placeholder for future functionality related to Data Capture & Methodologies API calls, where ever + that might come from (technique_attribute_quantitativ etc) + + + ); +}; diff --git a/app/src/features/standards/view/methods/MethodStandardsResults.tsx b/app/src/features/standards/view/methods/MethodStandardsResults.tsx new file mode 100644 index 0000000000..0dc4833479 --- /dev/null +++ b/app/src/features/standards/view/methods/MethodStandardsResults.tsx @@ -0,0 +1,25 @@ +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + +interface ISpeciesStandardsResultsProps { + data?: any; // Change to IGetMethodStandardsResults or similar when it exists + isLoading: boolean; +} + +/** + * Component to display methods standards results + * + * @return {*} + */ +export const MethodStandardsResults = (props: ISpeciesStandardsResultsProps) => { + const { data, isLoading } = props; + + return ( + <> + + {data} + Loading: {isLoading} + + + ); +}; diff --git a/app/src/features/standards/view/species/SpeciesStandards.tsx b/app/src/features/standards/view/species/SpeciesStandards.tsx new file mode 100644 index 0000000000..ff7dbb1431 --- /dev/null +++ b/app/src/features/standards/view/species/SpeciesStandards.tsx @@ -0,0 +1,40 @@ +import Box from '@mui/material/Box'; +import SpeciesAutocompleteField from 'components/species/components/SpeciesAutocompleteField'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; +import { ITaxonomy } from 'interfaces/useTaxonomyApi.interface'; +import { isDefined } from 'utils/Utils'; +import SpeciesStandardsResults from './SpeciesStandardsResults'; + +/** + * Returns species standards page for searching species and viewing lookup values available for selected species. + * This component handles the data request, then passes the data to its children components. + * + * @returns + */ +export const SpeciesStandards = () => { + const biohubApi = useBiohubApi(); + + const standardsDataLoader = useDataLoader((species: ITaxonomy) => + biohubApi.standards.getSpeciesStandards(species.tsn) + ); + + return ( + + { + if (value) { + standardsDataLoader.refresh(value); + } + }} + /> + {isDefined(standardsDataLoader.data) && ( + + + + )} + + ); +}; diff --git a/app/src/features/standards/view/SpeciesStandardsResults.tsx b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx similarity index 79% rename from app/src/features/standards/view/SpeciesStandardsResults.tsx rename to app/src/features/standards/view/species/SpeciesStandardsResults.tsx index 520adc80e7..f817c8e81d 100644 --- a/app/src/features/standards/view/SpeciesStandardsResults.tsx +++ b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx @@ -1,13 +1,14 @@ import { mdiRuler, mdiTag } from '@mdi/js'; import { Box, CircularProgress, Divider, Stack, Typography } from '@mui/material'; +import { ScientificNameTypography } from 'features/surveys/animals/components/ScientificNameTypography'; import { IGetSpeciesStandardsResponse } from 'interfaces/useStandardsApi.interface'; import { useState } from 'react'; -import MarkingBodyLocationStandardCard from './components/MarkingBodyLocationStandardCard'; -import MeasurementStandardCard from './components/MeasurementStandardCard'; -import SpeciesStandardsToolbar, { SpeciesStandardsViewEnum } from './components/SpeciesStandardsToolbar'; +import MarkingBodyLocationStandardCard from '../components/MarkingBodyLocationStandardCard'; +import MeasurementStandardCard from '../components/MeasurementStandardCard'; +import SpeciesStandardsToolbar, { SpeciesStandardsViewEnum } from '../components/SpeciesStandardsToolbar'; interface ISpeciesStandardsResultsProps { - data?: IGetSpeciesStandardsResponse; + data: IGetSpeciesStandardsResponse; isLoading: boolean; } @@ -27,20 +28,12 @@ const SpeciesStandardsResults = (props: ISpeciesStandardsResultsProps) => { ); } - if (!props.data) { - return <>; - } - return ( <> - Showing results for{' '} - {props.data.scientificName.split(' ').length >= 2 ? ( - {props.data.scientificName} - ) : ( - props.data.scientificName - )} + Showing results for  + @@ -65,7 +58,7 @@ const SpeciesStandardsResults = (props: ISpeciesStandardsResultsProps) => { /> - {activeView === 'MEASUREMENTS' && ( + {activeView === SpeciesStandardsViewEnum.MEASUREMENTS && ( {props.data.measurements.qualitative.map((measurement) => ( { ))} )} - {activeView === 'MARKING BODY LOCATIONS' && ( + {activeView === SpeciesStandardsViewEnum.MARKING_BODY_LOCATIONS && ( {props.data.markingBodyLocations.map((location) => ( From 02fb58352603b8c0f79bd410c07480a5936c828f Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Wed, 24 Jul 2024 11:09:48 -0700 Subject: [PATCH 05/19] mock method data --- .../standards/components/StandardsToolbar.tsx | 58 ++++++++++--------- .../view/methods/MethodStandards.tsx | 45 +++++++++++--- .../view/methods/MethodStandardsResults.tsx | 21 ++++--- 3 files changed, 78 insertions(+), 46 deletions(-) diff --git a/app/src/features/standards/components/StandardsToolbar.tsx b/app/src/features/standards/components/StandardsToolbar.tsx index 5a4806c850..626a817d05 100644 --- a/app/src/features/standards/components/StandardsToolbar.tsx +++ b/app/src/features/standards/components/StandardsToolbar.tsx @@ -19,33 +19,35 @@ export const StandardsToolbar = (props: IStandardsToolbar) => { const { views, currentView, setCurrentView } = props; return ( - <> - , view: StandardsPageView) => setCurrentView(view)} - exclusive - sx={{ - minWidth: '200px', - 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' - } - }}> - {views.map((view) => ( - - {view.label} - - ))} - - + , view: StandardsPageView) => { + if (view) { + setCurrentView(view); + } + }} + exclusive + sx={{ + minWidth: '200px', + 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' + } + }}> + {views.map((view) => ( + + {view.label} + + ))} + ); }; diff --git a/app/src/features/standards/view/methods/MethodStandards.tsx b/app/src/features/standards/view/methods/MethodStandards.tsx index 2f0aab6ace..d6988fcc3f 100644 --- a/app/src/features/standards/view/methods/MethodStandards.tsx +++ b/app/src/features/standards/view/methods/MethodStandards.tsx @@ -1,5 +1,12 @@ import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; +import { MethodStandardsResults } from './MethodStandardsResults'; + +export interface IMethodStandardResult { + method_type_id: number; + label: string; + description: string; + attributes: { attribute_id: number; label: string; description: string }[]; +} /** * @@ -13,16 +20,36 @@ export const MethodStandards = () => { // const biohubApi = useBiohubApi() // const methodsDataLoader = useDataLoader(() => ...) // useEffect(() => {methodsDataLoader.load()}) + const data: IMethodStandardResult[] = [ + { + method_type_id: 1, + label: 'camera trap', + description: 'camera trap is a camera', + attributes: [ + { + attribute_id: 1, + label: 'height above ground', + description: 'The distance the camera is placed above the ground' + } + ] + }, + { + method_type_id: 2, + label: 'dip net', + description: 'dip net is a net', + attributes: [ + { + attribute_id: 2, + label: 'mesh size', + description: 'size of the mesh' + } + ] + } + ]; return ( - - - Data Capture & Methodologies Placeholder - - - This is a placeholder for future functionality related to Data Capture & Methodologies API calls, where ever - that might come from (technique_attribute_quantitativ etc) - + + ); }; diff --git a/app/src/features/standards/view/methods/MethodStandardsResults.tsx b/app/src/features/standards/view/methods/MethodStandardsResults.tsx index 0dc4833479..972516f0c4 100644 --- a/app/src/features/standards/view/methods/MethodStandardsResults.tsx +++ b/app/src/features/standards/view/methods/MethodStandardsResults.tsx @@ -1,9 +1,10 @@ -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import MeasurementStandardCard from '../components/MeasurementStandardCard'; +import { IMethodStandardResult } from './MethodStandards'; interface ISpeciesStandardsResultsProps { - data?: any; // Change to IGetMethodStandardsResults or similar when it exists - isLoading: boolean; + data: IMethodStandardResult[]; // Change to IGetMethodStandardsResults or similar when it exists + isLoading?: boolean; } /** @@ -12,14 +13,16 @@ interface ISpeciesStandardsResultsProps { * @return {*} */ export const MethodStandardsResults = (props: ISpeciesStandardsResultsProps) => { - const { data, isLoading } = props; + const { data} = props; return ( <> - - {data} - Loading: {isLoading} - + + {/* Quantitative attributes */} + {data.map((method) => ( + + ))} + ); }; From 3521fbb3180ff59066a173a5bafe13e653ebcd8c Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Wed, 24 Jul 2024 17:52:16 -0700 Subject: [PATCH 06/19] environment standards --- api/src/models/standards-view.ts | 40 +++++++++++ api/src/openapi/schemas/standards.ts | 50 +++++++++++++ api/src/paths/standards/environment/index.ts | 72 +++++++++++++++++++ api/src/repositories/standards-repository.ts | 59 +++++++++++++++ api/src/services/standards-service.ts | 44 ++++++------ .../view/environment/EnvironmentStandards.tsx | 21 +++++- .../EnvironmentStandardsResults.tsx | 30 ++++++++ .../list-data/survey/SurveysListContainer.tsx | 2 +- app/src/hooks/api/useStandardsApi.ts | 2 +- .../interfaces/useStandardsApi.interface.ts | 15 ++-- 10 files changed, 303 insertions(+), 32 deletions(-) create mode 100644 api/src/models/standards-view.ts create mode 100644 api/src/openapi/schemas/standards.ts create mode 100644 api/src/paths/standards/environment/index.ts create mode 100644 api/src/repositories/standards-repository.ts create mode 100644 app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx diff --git a/api/src/models/standards-view.ts b/api/src/models/standards-view.ts new file mode 100644 index 0000000000..4eae3610e1 --- /dev/null +++ b/api/src/models/standards-view.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { + CBQualitativeMeasurementTypeDefinition, + CBQuantitativeMeasurementTypeDefinition +} from '../services/critterbase-service'; + +export interface ISpeciesStandards { + tsn: number; + scientificName: string; + measurements: { + quantitative: CBQuantitativeMeasurementTypeDefinition[]; + qualitative: CBQualitativeMeasurementTypeDefinition[]; + }; + markingBodyLocations: { id: string; key: string; value: string }[]; +} + +// Define Zod schema +export const EnvironmentStandardsSchema = z.object({ + qualitative: z.array( + z.object({ + name: z.string(), + description: z.string(), + options: z.array( + z.object({ + name: z.string(), + description: z.string() + }) + ) + }) + ), + quantitative: z.array( + z.object({ + name: z.string(), + description: z.string() + }) + ) +}); + +// Infer the TypeScript type from the Zod schema +export type EnvironmentStandards = z.infer; diff --git a/api/src/openapi/schemas/standards.ts b/api/src/openapi/schemas/standards.ts new file mode 100644 index 0000000000..b80748e665 --- /dev/null +++ b/api/src/openapi/schemas/standards.ts @@ -0,0 +1,50 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const EnvironmentStandardsSchema: OpenAPIV3.SchemaObject = { + type: 'object', + additionalProperties: false, + properties: { + qualitative: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + }, + options: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + } + } + } + } + }, + quantitative: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + } + } + } +}; diff --git a/api/src/paths/standards/environment/index.ts b/api/src/paths/standards/environment/index.ts new file mode 100644 index 0000000000..9f4c610e33 --- /dev/null +++ b/api/src/paths/standards/environment/index.ts @@ -0,0 +1,72 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { getAPIUserDBConnection } from '../../../database/db'; +import { EnvironmentStandardsSchema } from '../../../openapi/schemas/standards'; +import { StandardsService } from '../../../services/standards-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/projects'); + +export const GET: Operation = [getEnvironmentStandards()]; + +GET.apiDoc = { + description: 'Gets lookup values for environment variables', + tags: ['standards'], + parameters: [ + + ], + security: [{ Bearer: [] }], + responses: { + 200: { + description: 'Species data standards response object.', + content: { + 'application/json': { + schema: EnvironmentStandardsSchema + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get species data standards + * + * @returns {RequestHandler} + */ +export function getEnvironmentStandards(): RequestHandler { + return async (_, res) => { + const connection = getAPIUserDBConnection(); + + try { + await connection.open(); + + const standardsService = new StandardsService(connection); + + const response = await standardsService.getEnvironmentStandards(); + + await connection.commit(); + + return res.status(200).json(response); + } catch (error) { + defaultLog.error({ label: 'getEnvironmentStandards', message: 'error', error }); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/repositories/standards-repository.ts b/api/src/repositories/standards-repository.ts new file mode 100644 index 0000000000..d549b93e72 --- /dev/null +++ b/api/src/repositories/standards-repository.ts @@ -0,0 +1,59 @@ +import SQL from 'sql-template-strings'; +import { ApiExecuteSQLError } from '../errors/api-error'; +import { EnvironmentStandards, EnvironmentStandardsSchema } from '../models/standards-view'; +import { BaseRepository } from './base-repository'; + +/** + * Standards repository + * + * @export + * @class standardsRepository + * @extends {BaseRepository} + */ +export class StandardsRepository extends BaseRepository { + /** + * Gets environment standards + * + * @return {*} + * @memberof standardsRepository + */ + async getEnvironmentStandards(): Promise { + const sql = SQL` + WITH + quan AS ( + SELECT + name AS quant_name, + description AS quant_description + FROM + environment_quantitative + ), + qual AS ( + SELECT + eq.name AS qual_name, + eq.description AS qual_description, + json_agg(json_build_object('name', eqo.name, 'description', eqo.description)) as options + FROM + environment_qualitative_option eqo + LEFT JOIN + environment_qualitative eq ON eqo.environment_qualitative_id = eq.environment_qualitative_id + GROUP BY + eq.name, + eq.description + ) + SELECT + (SELECT json_agg(json_build_object('name', quant_name, 'description', quant_description)) FROM quan) as quantitative, + (SELECT json_agg(json_build_object('name', qual_name, 'description', qual_description, 'options', options)) FROM qual) as qualitative; + `; + + const response = await this.connection.sql(sql, EnvironmentStandardsSchema); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to get environment standards', [ + 'standardsRepository->getEnvironmentStandards', + 'rows was null or undefined, expected rows != null' + ]); + } + + return response.rows[0]; + } +} diff --git a/api/src/services/standards-service.ts b/api/src/services/standards-service.ts index 76bea02f87..337c24f4f0 100644 --- a/api/src/services/standards-service.ts +++ b/api/src/services/standards-service.ts @@ -1,32 +1,21 @@ import { IDBConnection } from '../database/db'; -import { - CBQualitativeMeasurementTypeDefinition, - CBQuantitativeMeasurementTypeDefinition, - CritterbaseService -} from './critterbase-service'; +import { EnvironmentStandards, ISpeciesStandards } from '../models/standards-view'; +import { StandardsRepository } from '../repositories/standards-repository'; +import { CritterbaseService } from './critterbase-service'; import { DBService } from './db-service'; import { PlatformService } from './platform-service'; -export interface ISpeciesStandardsResponse { - tsn: number; - scientificName: string; - measurements: { - quantitative: CBQuantitativeMeasurementTypeDefinition[]; - qualitative: CBQualitativeMeasurementTypeDefinition[]; - }; - markingBodyLocations: { id: string; key: string; value: string }[]; -} - /** - * Sample Stratum Repository + * Standards Repository * * @export - * @class SampleStratumService + * @class StandardsService * @extends {DBService} */ export class StandardsService extends DBService { platformService: PlatformService; critterbaseService: CritterbaseService; + standardsRepository: StandardsRepository; constructor(connection: IDBConnection) { super(connection); @@ -35,16 +24,17 @@ export class StandardsService extends DBService { keycloak_guid: this.connection.systemUserGUID(), username: this.connection.systemUserIdentifier() }); + this.standardsRepository = new StandardsRepository(connection); } /** - * Gets all survey Sample Stratums. + * Gets species standards * - * @param {number} surveySampleSiteId - * @return {*} {Promise} + * @param {number} tsn + * @return {ISpeciesStandards} * @memberof standardsService */ - async getSpeciesStandards(tsn: number): Promise { + async getSpeciesStandards(tsn: number): Promise { // Fetch all measurement type definitions from Critterbase for the unique taxon_measurement_ids const response = await Promise.all([ this.platformService.getTaxonomyByTsns([tsn]), @@ -59,4 +49,16 @@ export class StandardsService extends DBService { measurements: response[2] }; } + + /** + * Gets environment standards + * + * @return {EnvironmentStandard[]} + * @memberof standardsService + */ + async getEnvironmentStandards(): Promise { + const response = await this.standardsRepository.getEnvironmentStandards(); + + return response; + } } diff --git a/app/src/features/standards/view/environment/EnvironmentStandards.tsx b/app/src/features/standards/view/environment/EnvironmentStandards.tsx index f61e2be878..28a26299bd 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandards.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandards.tsx @@ -1,11 +1,26 @@ import Box from '@mui/material/Box'; +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; +import { useEffect } from 'react'; +import { EnvironmentStandardsResults } from './EnvironmentStandardsResults'; /** - * Returns species standards page for searching species and viewing lookup values available for selected species. - * This component handles the data request, then passes the data to its children components. + * Returns environmental variable lookup values for the standards page * * @returns */ export const EnvironmentStandards = () => { - return Placeholder for environment standards; + const biohubApi = useBiohubApi(); + + const environmentDataLoader = useDataLoader(() => biohubApi.standards.getEnvironmentStandards()); + + useEffect(() => { + environmentDataLoader.load(); + }, [environmentDataLoader]); + + return ( + + {environmentDataLoader.data && } + + ); }; diff --git a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx new file mode 100644 index 0000000000..327b2111b4 --- /dev/null +++ b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx @@ -0,0 +1,30 @@ +import Stack from '@mui/material/Stack'; +import { IEnvironmentStandards } from 'interfaces/useStandardsApi.interface'; +import MeasurementStandardCard from '../components/MeasurementStandardCard'; + +interface ISpeciesStandardsResultsProps { + data: IEnvironmentStandards; + isLoading?: boolean; +} + +/** + * Component to display environments standards results + * + * @return {*} + */ +export const EnvironmentStandardsResults = (props: ISpeciesStandardsResultsProps) => { + const { data } = props; + + return ( + <> + + {data.quantitative.map((environment) => ( + + ))} + {data.qualitative.map((environment) => ( + + ))} + + + ); +}; diff --git a/app/src/features/summary/list-data/survey/SurveysListContainer.tsx b/app/src/features/summary/list-data/survey/SurveysListContainer.tsx index 0f9dccb619..414dbd8644 100644 --- a/app/src/features/summary/list-data/survey/SurveysListContainer.tsx +++ b/app/src/features/summary/list-data/survey/SurveysListContainer.tsx @@ -14,6 +14,7 @@ import { DATE_FORMAT } from 'constants/dateTimeFormats'; import { NRM_REGION_APPENDED_TEXT } from 'constants/regions'; import { SYSTEM_ROLE } from 'constants/roles'; import dayjs from 'dayjs'; +import { SurveyProgressChip } from 'features/surveys/components/SurveyProgressChip'; import { useBiohubApi } from 'hooks/useBioHubApi'; import { useCodesContext, useTaxonomyContext } from 'hooks/useContext'; import useDataLoader from 'hooks/useDataLoader'; @@ -24,7 +25,6 @@ import { useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { ApiPaginationRequestOptions, StringValues } from 'types/misc'; import { firstOrNull, getCodesName } from 'utils/Utils'; -import { SurveyProgressChip } from '../../../surveys/components/SurveyProgressChip'; import SurveysListFilterForm, { ISurveyAdvancedFilters, SurveyAdvancedFiltersInitialValues diff --git a/app/src/hooks/api/useStandardsApi.ts b/app/src/hooks/api/useStandardsApi.ts index 6795de4eda..6930f16a87 100644 --- a/app/src/hooks/api/useStandardsApi.ts +++ b/app/src/hooks/api/useStandardsApi.ts @@ -25,7 +25,7 @@ const useStandardsApi = (axios: AxiosInstance) => { * * @return {*} {Promise} */ - const getEnvironmentStandards = async (tsn: number): Promise => { + const getEnvironmentStandards = async (): Promise => { const { data } = await axios.get(`/api/standards/environment`); return data; diff --git a/app/src/interfaces/useStandardsApi.interface.ts b/app/src/interfaces/useStandardsApi.interface.ts index de5563004d..06f3963ced 100644 --- a/app/src/interfaces/useStandardsApi.interface.ts +++ b/app/src/interfaces/useStandardsApi.interface.ts @@ -26,10 +26,13 @@ export interface ISpeciesStandards { } export interface IEnvironmentStandards { - name: string; - description: string; - options: { + qualitative: { name: string; - description: string - } -} \ No newline at end of file + description: string; + options: { + name: string; + description: string; + }; + }[]; + quantitative: { name: string; description: string }[]; +} From ff84b2119d434687fd49fde00e11f326b27db72a Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Thu, 25 Jul 2024 10:22:27 -0700 Subject: [PATCH 07/19] add backend tests --- .../paths/standards/environment/index.test.ts | 93 +++++++++++++++++++ api/src/paths/standards/environment/index.ts | 7 +- api/src/paths/standards/taxon/{tsn}/index.ts | 6 +- .../repositories/standards-repository.test.ts | 80 ++++++++++++++++ api/src/services/standards-service.test.ts | 22 +++++ 5 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 api/src/paths/standards/environment/index.test.ts create mode 100644 api/src/repositories/standards-repository.test.ts diff --git a/api/src/paths/standards/environment/index.test.ts b/api/src/paths/standards/environment/index.test.ts new file mode 100644 index 0000000000..3b033ccdac --- /dev/null +++ b/api/src/paths/standards/environment/index.test.ts @@ -0,0 +1,93 @@ +import chai, { expect } from 'chai'; +import { afterEach, describe, it } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/http-error'; +import { StandardsService } from '../../../services/standards-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import { getEnvironmentStandards } from './index'; // Adjust the import path based on your file structure + +chai.use(sinonChai); + +describe('standards/environment', () => { + describe('getEnvironmentStandards', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should retrieve environment standards successfully', async () => { + const mockResponse = { + quantitative: [ + { name: 'Quantitative Standard 1', description: 'Description 1' }, + { name: 'Quantitative Standard 2', description: 'Description 2' } + ], + qualitative: [ + { + name: 'Qualitative Standard 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative Standard 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + }; + + const mockDBConnection = getMockDBConnection(); + + sinon.stub(db, 'getAPIUserDBConnection').returns(mockDBConnection); + + sinon.stub(StandardsService.prototype, 'getEnvironmentStandards').resolves(mockResponse); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getEnvironmentStandards(); + + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); + } + + expect(mockRes.status).to.have.been.calledWith(200); + expect(mockRes.json).to.have.been.calledWith(mockResponse); + }); + + it('catches and re-throws error', async () => { + const mockDBConnection = getMockDBConnection({ + open: sinon.stub(), + commit: sinon.stub(), + rollback: sinon.stub(), + release: sinon.stub() + }); + + sinon.stub(db, 'getAPIUserDBConnection').returns(mockDBConnection); + + sinon + .stub(StandardsService.prototype, 'getEnvironmentStandards') + .rejects(new Error('Failed to retrieve environment standards')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getEnvironmentStandards(); + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(mockDBConnection.open).to.have.been.calledOnce; + expect(mockDBConnection.rollback).to.have.been.calledOnce; + expect(mockDBConnection.release).to.have.been.calledOnce; + expect((actualError as HTTPError).message).to.equal('Failed to retrieve environment standards'); + } + }); + }); +}); diff --git a/api/src/paths/standards/environment/index.ts b/api/src/paths/standards/environment/index.ts index 9f4c610e33..8632b20c1a 100644 --- a/api/src/paths/standards/environment/index.ts +++ b/api/src/paths/standards/environment/index.ts @@ -12,13 +12,11 @@ export const GET: Operation = [getEnvironmentStandards()]; GET.apiDoc = { description: 'Gets lookup values for environment variables', tags: ['standards'], - parameters: [ - - ], + parameters: [], security: [{ Bearer: [] }], responses: { 200: { - description: 'Species data standards response object.', + description: 'Environment data standards response object.', content: { 'application/json': { schema: EnvironmentStandardsSchema @@ -64,6 +62,7 @@ export function getEnvironmentStandards(): RequestHandler { return res.status(200).json(response); } catch (error) { defaultLog.error({ label: 'getEnvironmentStandards', message: 'error', error }); + connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/paths/standards/taxon/{tsn}/index.ts b/api/src/paths/standards/taxon/{tsn}/index.ts index a46bb597a1..f12ba5473d 100644 --- a/api/src/paths/standards/taxon/{tsn}/index.ts +++ b/api/src/paths/standards/taxon/{tsn}/index.ts @@ -1,7 +1,7 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../../../../constants/roles'; -import { getDBConnection } from '../../../../database/db'; +import { getAPIUserDBConnection } from '../../../../database/db'; import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; import { StandardsService } from '../../../../services/standards-service'; import { getLogger } from '../../../../utils/logger'; @@ -199,8 +199,7 @@ GET.apiDoc = { */ export function getSpeciesStandards(): RequestHandler { return async (req, res) => { - // TODO: const connection = getAPIUserDBConnection(); - const connection = getDBConnection(req.keycloak_token); + const connection = getAPIUserDBConnection(); try { const tsn = Number(req.params.tsn); @@ -216,6 +215,7 @@ export function getSpeciesStandards(): RequestHandler { return res.status(200).json(getSpeciesStandardsResponse); } catch (error) { defaultLog.error({ label: 'getSpeciesStandards', message: 'error', error }); + connection.rollback() throw error; } finally { connection.release(); diff --git a/api/src/repositories/standards-repository.test.ts b/api/src/repositories/standards-repository.test.ts new file mode 100644 index 0000000000..7eff0efc7b --- /dev/null +++ b/api/src/repositories/standards-repository.test.ts @@ -0,0 +1,80 @@ +import chai, { expect } from 'chai'; +import { afterEach, describe, it } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { ApiExecuteSQLError } from '../errors/api-error'; +import { getMockDBConnection } from '../__mocks__/db'; +import { StandardsRepository } from './standards-repository'; + +chai.use(sinonChai); + +describe('StandardsRepository', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getEnvironmentStandards', () => { + it('should successfully retrieve environment standards', async () => { + const mockData = { + quantitative: [ + { name: 'Quantitative Standard 1', description: 'Description 1' }, + { name: 'Quantitative Standard 2', description: 'Description 2' } + ], + qualitative: [ + { + name: 'Qualitative Standard 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative Standard 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + }; + + const mockResponse = { + rows: [mockData], + rowCount: 1 + } as any as Promise>; + + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repository = new StandardsRepository(dbConnection); + + const result = await repository.getEnvironmentStandards(); + + expect(result).to.deep.equal(mockData); + }); + + it('should handle empty result and throw ApiExecuteSQLError', async () => { + const mockResponse = { + rows: [], + rowCount: 0 + } as any as Promise>; + + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repository = new StandardsRepository(dbConnection); + + try { + await repository.getEnvironmentStandards(); + expect.fail(); + } catch (error) { + expect((error as ApiExecuteSQLError).message).to.be.eq('Failed to get environment standards'); + } + }); + }); +}); diff --git a/api/src/services/standards-service.test.ts b/api/src/services/standards-service.test.ts index aad35dfbd7..8340e2b2b4 100644 --- a/api/src/services/standards-service.test.ts +++ b/api/src/services/standards-service.test.ts @@ -68,4 +68,26 @@ describe('StandardsService', () => { expect(response.measurements.qualitative[0].measurement_desc).to.eql('description'); }); }); + + describe('getEnvironmentStandards', async () => { + const mockData = { + qualitative: [{ name: 'name', description: 'name', options: [{ name: 'name', description: 'description' }] }], + quantitative: [ + { name: 'name', description: 'description' }, + { name: 'name', description: 'description' } + ] + }; + const mockDbConnection = getMockDBConnection(); + + const standardsService = new StandardsService(mockDbConnection); + + const getEnvironmentStandardsStub = sinon + .stub(standardsService.standardsRepository, 'getEnvironmentStandards') + .resolves(mockData); + + const response = await standardsService.getEnvironmentStandards(); + + expect(getEnvironmentStandardsStub).to.be.calledOnce; + expect(response).to.eql(mockData); + }); }); From 01232a468ccd454c8ca0e9a88cf1f7ef71cd19dc Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Thu, 25 Jul 2024 10:28:00 -0700 Subject: [PATCH 08/19] replace getApiUserDBConnection with getDBConnection temporarily --- api/src/paths/standards/taxon/{tsn}/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/paths/standards/taxon/{tsn}/index.ts b/api/src/paths/standards/taxon/{tsn}/index.ts index f12ba5473d..7d86350a6d 100644 --- a/api/src/paths/standards/taxon/{tsn}/index.ts +++ b/api/src/paths/standards/taxon/{tsn}/index.ts @@ -1,7 +1,7 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { SYSTEM_ROLE } from '../../../../constants/roles'; -import { getAPIUserDBConnection } from '../../../../database/db'; +import { getDBConnection } from '../../../../database/db'; import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; import { StandardsService } from '../../../../services/standards-service'; import { getLogger } from '../../../../utils/logger'; @@ -199,7 +199,9 @@ GET.apiDoc = { */ export function getSpeciesStandards(): RequestHandler { return async (req, res) => { - const connection = getAPIUserDBConnection(); + // API user DB connection does not work, possible because user does not exist in Critterbase? + // const connection = getAPIUserDBConnection(); + const connection = getDBConnection(req.keycloak_token); try { const tsn = Number(req.params.tsn); @@ -215,7 +217,7 @@ export function getSpeciesStandards(): RequestHandler { return res.status(200).json(getSpeciesStandardsResponse); } catch (error) { defaultLog.error({ label: 'getSpeciesStandards', message: 'error', error }); - connection.rollback() + connection.rollback(); throw error; } finally { connection.release(); From b4747da66b439483a29054871677fb94969e066d Mon Sep 17 00:00:00 2001 From: Andrew <105487051+LouisThedroux@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:43:32 -0700 Subject: [PATCH 09/19] lots of changes, MethodStandards Card and results, API path, sessions wih Macgregor --- api/src/models/standards-view.ts | 37 ++++++++ api/src/openapi/schemas/standards.ts | 65 +++++++++++++ api/src/paths/standards/methods/index.test.ts | 0 api/src/paths/standards/methods/index.ts | 71 ++++++++++++++ api/src/repositories/standards-repository.ts | 92 +++++++++++++++++- api/src/services/standards-service.ts | 17 +++- .../view/methods/MethodStandards.tsx | 52 +++------- .../view/methods/MethodStandardsResults.tsx | 19 ++-- .../components/MethodStandardsCard.tsx | 94 +++++++++++++++++++ .../MethodStandardsCardAttribute.tsx | 81 ++++++++++++++++ app/src/hooks/api/useStandardsApi.ts | 24 +++-- .../interfaces/useStandardsApi.interface.ts | 26 ++++- 12 files changed, 523 insertions(+), 55 deletions(-) create mode 100644 api/src/paths/standards/methods/index.test.ts create mode 100644 api/src/paths/standards/methods/index.ts create mode 100644 app/src/features/standards/view/methods/components/MethodStandardsCard.tsx create mode 100644 app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx diff --git a/api/src/models/standards-view.ts b/api/src/models/standards-view.ts index 4eae3610e1..f391e054f0 100644 --- a/api/src/models/standards-view.ts +++ b/api/src/models/standards-view.ts @@ -38,3 +38,40 @@ export const EnvironmentStandardsSchema = z.object({ // Infer the TypeScript type from the Zod schema export type EnvironmentStandards = z.infer; + +// THIS AINT WORKING OR BEING IMPORTED CORRECTLY IN STANDARSD SERVICE + +// export interface MethodStandards { +// name: string; +// description: string; + +// } + +export const MethodStandardSchema = z.object({ + method_lookup_id: z.number(), + name: z.string(), + description: z.string(), + attributes: z.object({ + qualitative: z.array( + z.object({ + name: z.string(), + description: z.string(), + options: z.array( + z.object({ + name: z.string(), + description: z.string() + }) + ) + }) + ), + quantitative: z.array( + z.object({ + name: z.string(), + description: z.string(), + units: z.string() + }) + ) + }) +}); + +export type MethodStandard = z.infer; diff --git a/api/src/openapi/schemas/standards.ts b/api/src/openapi/schemas/standards.ts index b80748e665..9d79ecd3a8 100644 --- a/api/src/openapi/schemas/standards.ts +++ b/api/src/openapi/schemas/standards.ts @@ -48,3 +48,68 @@ export const EnvironmentStandardsSchema: OpenAPIV3.SchemaObject = { } } }; + +export const MethodStandardsSchema: OpenAPIV3.SchemaObject = { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + method_lookup_id: { type: 'number' }, + name: { type: 'string' }, + description: { type: 'string' }, + attributes: { + type: 'object', + additionalProperties: false, + properties: { + qualitative: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + }, + options: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + } + } + } + } + } + } + }, + quantitative: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string' + }, + description: { + type: 'string' + }, + unit: { type: 'string' } + } + } + } + } + } + } + } +}; diff --git a/api/src/paths/standards/methods/index.test.ts b/api/src/paths/standards/methods/index.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/src/paths/standards/methods/index.ts b/api/src/paths/standards/methods/index.ts new file mode 100644 index 0000000000..8d15c43858 --- /dev/null +++ b/api/src/paths/standards/methods/index.ts @@ -0,0 +1,71 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { getAPIUserDBConnection } from '../../../database/db'; +import { MethodStandardsSchema } from '../../../openapi/schemas/standards'; +import { StandardsService } from '../../../services/standards-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/standards/methods'); + +export const GET: Operation = [getMethodStandards()]; + +GET.apiDoc = { + description: 'Gets lookup values for method variables', + tags: ['standards'], + parameters: [], + security: [{ Bearer: [] }], + responses: { + 200: { + description: 'Method data standards response object.', + content: { + 'application/json': { + schema: MethodStandardsSchema + } + } + }, + 400: { + $ref: '#/components/responses/400' + }, + 401: { + $ref: '#/components/responses/401' + }, + 403: { + $ref: '#/components/responses/403' + }, + 500: { + $ref: '#/components/responses/500' + }, + default: { + $ref: '#/components/responses/default' + } + } +}; + +/** + * Get species data standards + * + * @returns {RequestHandler} + */ +export function getMethodStandards(): RequestHandler { + return async (_, res) => { + const connection = getAPIUserDBConnection(); + + try { + await connection.open(); + + const standardsService = new StandardsService(connection); + + const response = await standardsService.getMethodStandards(); + + await connection.commit(); + + return res.status(200).json(response); + } catch (error) { + defaultLog.error({ label: 'getMethodStandards', message: 'error', error }); + connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/repositories/standards-repository.ts b/api/src/repositories/standards-repository.ts index d549b93e72..b22930356e 100644 --- a/api/src/repositories/standards-repository.ts +++ b/api/src/repositories/standards-repository.ts @@ -1,6 +1,11 @@ import SQL from 'sql-template-strings'; import { ApiExecuteSQLError } from '../errors/api-error'; -import { EnvironmentStandards, EnvironmentStandardsSchema } from '../models/standards-view'; +import { + EnvironmentStandards, + EnvironmentStandardsSchema, + MethodStandard, + MethodStandardSchema +} from '../models/standards-view'; import { BaseRepository } from './base-repository'; /** @@ -56,4 +61,89 @@ export class StandardsRepository extends BaseRepository { return response.rows[0]; } + + // FROM HERE DOWN + + async getMethodStandards(): Promise { + const sql = SQL` + WITH + quan AS ( + SELECT + mlaq.method_lookup_id, + tq.name AS quant_name, + tq.description AS quant_description, + mlaq.unit + FROM + method_lookup_attribute_quantitative mlaq + LEFT JOIN + technique_attribute_quantitative tq ON mlaq.technique_attribute_quantitative_id = tq.technique_attribute_quantitative_id + ORDER BY tq.name + ), + qual AS ( + SELECT + mlaq.method_lookup_id, + taq.name AS qual_name, + taq.description AS qual_description, + json_agg(json_build_object('name', mlaqo.name, 'description', mlaqo.description) ORDER BY mlaqo.name) AS options + FROM + method_lookup_attribute_qualitative_option mlaqo + LEFT JOIN + method_lookup_attribute_qualitative mlaq ON mlaqo.method_lookup_attribute_qualitative_id = mlaq.method_lookup_attribute_qualitative_id + LEFT JOIN + technique_attribute_qualitative taq ON mlaq.technique_attribute_qualitative_id = taq.technique_attribute_qualitative_id + GROUP BY + mlaq.method_lookup_id, + taq.name, + taq.description + ORDER BY + taq.name + ), + method_lookup AS ( + SELECT + ml.method_lookup_id, + ml.name, + ml.description, + json_build_object( + 'quantitative', ( + SELECT json_agg( + json_build_object( + 'name', quan.quant_name, + 'description', quan.quant_description, + 'unit', quan.unit + ) ORDER BY quan.quant_name + ) FROM quan + WHERE quan.method_lookup_id = ml.method_lookup_id + ), + 'qualitative', ( + SELECT json_agg( + json_build_object( + 'name', qual.qual_name, + 'description', qual.qual_description, + 'options', qual.options + ) ORDER BY qual.qual_name + ) FROM qual + WHERE qual.method_lookup_id = ml.method_lookup_id + ) + ) AS attributes + FROM + method_lookup ml + ORDER BY + ml.name + ) +SELECT * FROM method_lookup; + + + `; + + const response = await this.connection.sql(sql, MethodStandardSchema); + + if (!response.rowCount) { + throw new ApiExecuteSQLError('Failed to get Method standards', [ + 'standardsRepository->getMethodStandards', + 'rows was null or undefined, expected rows != null' + ]); + } + + return response.rows; + } } diff --git a/api/src/services/standards-service.ts b/api/src/services/standards-service.ts index 337c24f4f0..9871ec7e3d 100644 --- a/api/src/services/standards-service.ts +++ b/api/src/services/standards-service.ts @@ -1,5 +1,5 @@ import { IDBConnection } from '../database/db'; -import { EnvironmentStandards, ISpeciesStandards } from '../models/standards-view'; +import { EnvironmentStandards, ISpeciesStandards, MethodStandard } from '../models/standards-view'; import { StandardsRepository } from '../repositories/standards-repository'; import { CritterbaseService } from './critterbase-service'; import { DBService } from './db-service'; @@ -61,4 +61,17 @@ export class StandardsService extends DBService { return response; } -} + + +// THE BELOW IS NOT IMPORTING MethodStandards correctly. STANDARDS VIEW INTERFACE FOR METHODS IS PROBABLY WRONG + + /** + * Gets METHOD standards + * @return {MethodStandards} + * @memberof standardsService + */ + async getMethodStandards(): Promise { + const response = await this.standardsRepository.getMethodStandards(); + return response; + +}} diff --git a/app/src/features/standards/view/methods/MethodStandards.tsx b/app/src/features/standards/view/methods/MethodStandards.tsx index d6988fcc3f..9cd1c4fd9d 100644 --- a/app/src/features/standards/view/methods/MethodStandards.tsx +++ b/app/src/features/standards/view/methods/MethodStandards.tsx @@ -1,12 +1,8 @@ import Box from '@mui/material/Box'; import { MethodStandardsResults } from './MethodStandardsResults'; - -export interface IMethodStandardResult { - method_type_id: number; - label: string; - description: string; - attributes: { attribute_id: number; label: string; description: string }[]; -} +import { useBiohubApi } from 'hooks/useBioHubApi'; +import useDataLoader from 'hooks/useDataLoader'; +import { useEffect } from 'react'; /** * @@ -14,42 +10,20 @@ export interface IMethodStandardResult { * * @returns */ + + export const MethodStandards = () => { - // TODO: Fetch information about methods, like below - // - // const biohubApi = useBiohubApi() - // const methodsDataLoader = useDataLoader(() => ...) - // useEffect(() => {methodsDataLoader.load()}) - const data: IMethodStandardResult[] = [ - { - method_type_id: 1, - label: 'camera trap', - description: 'camera trap is a camera', - attributes: [ - { - attribute_id: 1, - label: 'height above ground', - description: 'The distance the camera is placed above the ground' - } - ] - }, - { - method_type_id: 2, - label: 'dip net', - description: 'dip net is a net', - attributes: [ - { - attribute_id: 2, - label: 'mesh size', - description: 'size of the mesh' - } - ] - } - ]; + const biohubApi = useBiohubApi(); + + const methodDataLoader = useDataLoader(() => biohubApi.standards.getMethodStandards()); + + useEffect(() => { + methodDataLoader.load(); + }, [methodDataLoader]); return ( - + {methodDataLoader.data && } ); }; diff --git a/app/src/features/standards/view/methods/MethodStandardsResults.tsx b/app/src/features/standards/view/methods/MethodStandardsResults.tsx index 972516f0c4..ed5f2a9486 100644 --- a/app/src/features/standards/view/methods/MethodStandardsResults.tsx +++ b/app/src/features/standards/view/methods/MethodStandardsResults.tsx @@ -1,9 +1,9 @@ import Stack from '@mui/material/Stack'; -import MeasurementStandardCard from '../components/MeasurementStandardCard'; -import { IMethodStandardResult } from './MethodStandards'; +import { IMethodStandard } from 'interfaces/useStandardsApi.interface'; +import MethodStandardCard from './components/MethodStandardsCard'; interface ISpeciesStandardsResultsProps { - data: IMethodStandardResult[]; // Change to IGetMethodStandardsResults or similar when it exists + data: IMethodStandard[]; isLoading?: boolean; } @@ -13,16 +13,23 @@ interface ISpeciesStandardsResultsProps { * @return {*} */ export const MethodStandardsResults = (props: ISpeciesStandardsResultsProps) => { - const { data} = props; + const { data } = props; return ( <> - {/* Quantitative attributes */} {data.map((method) => ( - + ))} ); }; + +export default MethodStandardsResults; diff --git a/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx b/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx new file mode 100644 index 0000000000..e6fa43918d --- /dev/null +++ b/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx @@ -0,0 +1,94 @@ +import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; +import Icon from '@mdi/react'; +import { Box, Collapse, Paper, Stack, Typography } from '@mui/material'; +import { grey } from '@mui/material/colors'; +import { useState } from 'react'; +import { MethodStandardsCardAttribute } from './MethodStandardsCardAttribute'; + +interface IMethodStandardCard { + name: string; + description: string; + quantitativeAttributes: Attribute[]; + qualitativeAttributes: QualitativeAttribute[]; + small?: boolean; +} + +interface Attribute { + name: string; + description: string; + unit?: string; +} + +interface QualitativeAttribute { + name: string; + description: string; + options: { + name: string; + description: string; + }[]; +} + +/** + * Card to display method information for species standards + * + * @return {*} + */ +const MethodStandardCard = (props: IMethodStandardCard) => { + const [isCollapsed, setIsCollapsed] = useState(true); + + return ( + + {/* METHOD */} + { + setIsCollapsed(!isCollapsed); + }}> + + {props.name} + + + + + + {props.description} + + + {/* QUANTITATIVE ATTRIBUTES */} + + {props.quantitativeAttributes?.map((attribute) => ( + + ))} + {props.qualitativeAttributes?.map((attribute) => ( + + ))} + + + + ); +}; + +export default MethodStandardCard; diff --git a/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx b/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx new file mode 100644 index 0000000000..a78a5448d9 --- /dev/null +++ b/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx @@ -0,0 +1,81 @@ +import Collapse from '@mui/material/Collapse'; +import { grey } from '@mui/material/colors'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import { useState } from 'react'; +import MethodStandardCard from './MethodStandardsCard'; + +interface IMethodStandardsCardAttributeProps { + name: string; + description: string; + options?: { name: string; description: string }[]; +} +export const MethodStandardsCardAttribute = (props: IMethodStandardsCardAttributeProps) => { + const [isCollapsed, setIsCollapsed] = useState(true); + // const [collapsedOptions, setCollapsedOptions] = useState<{ [key: string]: boolean }>({}); + + // const handleOptionClick = (name: string) => { + // setCollapsedOptions((prev) => ({ + // ...prev, + // [name]: !prev[name] + // })); + // }; + + const { name, description, options } = props; + + return ( + + { + setIsCollapsed((prev) => !prev); + }}> + {name} + + + + {description} + + {options?.map((option) => ( + + ))} + {/* {options?.map((option) => ( + handleOptionClick(option.name)}> + + + + {option.name} + + + + + {option.description} + + + + )) || No options available} + */} + + + ); +}; diff --git a/app/src/hooks/api/useStandardsApi.ts b/app/src/hooks/api/useStandardsApi.ts index 6930f16a87..35aa377ce0 100644 --- a/app/src/hooks/api/useStandardsApi.ts +++ b/app/src/hooks/api/useStandardsApi.ts @@ -1,5 +1,5 @@ import { AxiosInstance } from 'axios'; -import { IEnvironmentStandards, ISpeciesStandards } from 'interfaces/useStandardsApi.interface'; +import { IEnvironmentStandards, ISpeciesStandards, IMethodStandard } from 'interfaces/useStandardsApi.interface'; /** * Returns information about what data can be uploaded for a given species, @@ -21,20 +21,32 @@ const useStandardsApi = (axios: AxiosInstance) => { }; /** - * Fetch environment standards + * Fetch method standards * - * @return {*} {Promise} + * @return {*} {Promise} */ - const getEnvironmentStandards = async (): Promise => { - const { data } = await axios.get(`/api/standards/environment`); + const getMethodStandards = async (): Promise => { + const { data } = await axios.get(`/api/standards/methods`); return data; }; + /** + * Fetch environment standards + * + * @return {*} {Promise} + */ + const getEnvironmentStandards = async (): Promise => { + const { data } = await axios.get(`/api/standards/environment`); + + return data; + }; + return { getSpeciesStandards, - getEnvironmentStandards + getEnvironmentStandards, + getMethodStandards }; }; diff --git a/app/src/interfaces/useStandardsApi.interface.ts b/app/src/interfaces/useStandardsApi.interface.ts index 06f3963ced..0369548ddb 100644 --- a/app/src/interfaces/useStandardsApi.interface.ts +++ b/app/src/interfaces/useStandardsApi.interface.ts @@ -34,5 +34,29 @@ export interface IEnvironmentStandards { description: string; }; }[]; - quantitative: { name: string; description: string }[]; + quantitative: { name: string; description: string; units: string }[]; } + +export interface IMethodStandard { + method_lookup_id: number; + name: string; + description: string; + attributes: { + qualitative: { + name: string; + description: string; + options: { + name: string; + description: string; + }[]; + }[]; + quantitative: { name: string; description: string }[]; + }; +} + +// export interface IMethodStandards { +// methods: Array<{ +// name: string; +// description: string; +// }>; +// } From 00cf5fa0b7a821dd2cf4c332fd91e20f59a1902f Mon Sep 17 00:00:00 2001 From: Andrew <105487051+LouisThedroux@users.noreply.github.com> Date: Thu, 1 Aug 2024 16:57:41 -0700 Subject: [PATCH 10/19] formatting, fixing SQL, adding unit chip --- api/src/repositories/standards-repository.ts | 3 +- .../components/MethodStandardsCard.tsx | 7 +- .../MethodStandardsCardAttribute.tsx | 121 ++++++++++-------- 3 files changed, 70 insertions(+), 61 deletions(-) diff --git a/api/src/repositories/standards-repository.ts b/api/src/repositories/standards-repository.ts index b22930356e..4d94385dc6 100644 --- a/api/src/repositories/standards-repository.ts +++ b/api/src/repositories/standards-repository.ts @@ -65,8 +65,7 @@ export class StandardsRepository extends BaseRepository { // FROM HERE DOWN async getMethodStandards(): Promise { - const sql = SQL` - WITH + const sql = SQL`WITH quan AS ( SELECT mlaq.method_lookup_id, diff --git a/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx b/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx index e6fa43918d..2dfd789074 100644 --- a/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx +++ b/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx @@ -37,7 +37,7 @@ const MethodStandardCard = (props: IMethodStandardCard) => { const [isCollapsed, setIsCollapsed] = useState(true); return ( - + {/* METHOD */} { flex="1 1 auto" alignItems="center" sx={{ cursor: 'pointer' }} - onClick={() => { - setIsCollapsed(!isCollapsed); - }}> + onClick={() => setIsCollapsed(!isCollapsed)}> { key={attribute.name} name={attribute.name} description={attribute.description} + unit={attribute.unit} // Pass unit prop /> ))} {props.qualitativeAttributes?.map((attribute) => ( diff --git a/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx b/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx index a78a5448d9..46e69631e5 100644 --- a/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx +++ b/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx @@ -1,80 +1,91 @@ +import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; +import Icon from '@mdi/react'; +import Box from '@mui/material/Box'; +import Chip from '@mui/material/Chip'; // Import Chip component import Collapse from '@mui/material/Collapse'; -import { grey } from '@mui/material/colors'; +import { green, grey } from '@mui/material/colors'; // Import green color import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { useState } from 'react'; -import MethodStandardCard from './MethodStandardsCard'; interface IMethodStandardsCardAttributeProps { name: string; description: string; + unit?: string; // Add unit prop options?: { name: string; description: string }[]; } + export const MethodStandardsCardAttribute = (props: IMethodStandardsCardAttributeProps) => { const [isCollapsed, setIsCollapsed] = useState(true); - // const [collapsedOptions, setCollapsedOptions] = useState<{ [key: string]: boolean }>({}); + const [collapsedOptions, setCollapsedOptions] = useState<{ [key: string]: boolean }>({}); - // const handleOptionClick = (name: string) => { - // setCollapsedOptions((prev) => ({ - // ...prev, - // [name]: !prev[name] - // })); - // }; + const { name, description, unit, options } = props; - const { name, description, options } = props; + const handleOptionClick = (name: string) => { + setCollapsedOptions((prev) => ({ + ...prev, + [name]: !prev[name] + })); + }; return ( - - + { - setIsCollapsed((prev) => !prev); - }}> - {name} - + onClick={() => setIsCollapsed((prev) => !prev)}> + + {name} + + {unit && ( + + )} + - {description} + {description || 'No description available'} {options?.map((option) => ( - + + handleOptionClick(option.name)}> + + {option.name} + + + + + + {option.description || 'No description available'} + + + ))} - {/* {options?.map((option) => ( - handleOptionClick(option.name)}> - - - - {option.name} - - - - - {option.description} - - - - )) || No options available} - */} ); From 66855f3b05132f84e5ff712e00b40f0bf9b73abd Mon Sep 17 00:00:00 2001 From: Andrew <105487051+LouisThedroux@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:17:56 -0700 Subject: [PATCH 11/19] some fies to font, color, chevrons, chip etc --- .../view/methods/components/MethodStandardsCard.tsx | 10 ++++------ .../components/MethodStandardsCardAttribute.tsx | 11 ++++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx b/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx index 2dfd789074..333b68889a 100644 --- a/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx +++ b/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx @@ -45,18 +45,16 @@ const MethodStandardCard = (props: IMethodStandardCard) => { flex="1 1 auto" alignItems="center" sx={{ cursor: 'pointer' }} - onClick={() => setIsCollapsed(!isCollapsed)}> + onClick={() => setIsCollapsed(!isCollapsed)} + > + }} + > {props.name} diff --git a/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx b/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx index 46e69631e5..70744c25c7 100644 --- a/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx +++ b/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx @@ -1,9 +1,9 @@ import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; import Icon from '@mdi/react'; import Box from '@mui/material/Box'; -import Chip from '@mui/material/Chip'; // Import Chip component +import Chip from '@mui/material/Chip'; import Collapse from '@mui/material/Collapse'; -import { green, grey } from '@mui/material/colors'; // Import green color +import { green, grey } from '@mui/material/colors'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { useState } from 'react'; @@ -11,7 +11,7 @@ import { useState } from 'react'; interface IMethodStandardsCardAttributeProps { name: string; description: string; - unit?: string; // Add unit prop + unit?: string; options?: { name: string; description: string }[]; } @@ -41,7 +41,7 @@ export const MethodStandardsCardAttribute = (props: IMethodStandardsCardAttribut {unit && ( )} + {/* Smaller chevron */} @@ -75,7 +76,7 @@ export const MethodStandardsCardAttribute = (props: IMethodStandardsCardAttribut From aabd1e3d41f5974141133b1d1d11c6d4e4946d18 Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Tue, 6 Aug 2024 14:02:20 -0700 Subject: [PATCH 12/19] simplify standard cards --- api/src/models/standards-view.ts | 27 +-- api/src/openapi/schemas/standards.ts | 27 +-- api/src/paths/standards/environment/index.ts | 18 +- api/src/paths/standards/methods/index.ts | 22 ++- api/src/repositories/standards-repository.ts | 170 +++++++++--------- api/src/services/standards-service.ts | 15 +- .../components/loading/SkeletonLoaders.tsx | 9 +- app/src/features/standards/StandardsPage.tsx | 27 +-- .../components/AccordionStandardCard.tsx | 65 +++++++ .../standards/components/StandardsToolbar.tsx | 73 ++++---- .../view/environment/EnvironmentStandards.tsx | 59 +++++- .../EnvironmentStandardsResults.tsx | 7 +- .../view/methods/MethodStandards.tsx | 55 +++++- .../view/methods/MethodStandardsResults.tsx | 60 +++++-- .../components/MethodStandardsCard.tsx | 91 ---------- .../MethodStandardsCardAttribute.tsx | 93 ---------- .../view/species/SpeciesStandards.tsx | 27 ++- .../view/species/SpeciesStandardsResults.tsx | 26 ++- app/src/hooks/api/useStandardsApi.ts | 59 +++--- .../interfaces/useStandardsApi.interface.ts | 49 +++-- 20 files changed, 524 insertions(+), 455 deletions(-) create mode 100644 app/src/features/standards/components/AccordionStandardCard.tsx delete mode 100644 app/src/features/standards/view/methods/components/MethodStandardsCard.tsx delete mode 100644 app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx diff --git a/api/src/models/standards-view.ts b/api/src/models/standards-view.ts index f391e054f0..88b0f9db68 100644 --- a/api/src/models/standards-view.ts +++ b/api/src/models/standards-view.ts @@ -14,16 +14,15 @@ export interface ISpeciesStandards { markingBodyLocations: { id: string; key: string; value: string }[]; } -// Define Zod schema export const EnvironmentStandardsSchema = z.object({ qualitative: z.array( z.object({ name: z.string(), - description: z.string(), + description: z.string().nullable(), options: z.array( z.object({ name: z.string(), - description: z.string() + description: z.string().nullable() }) ) }) @@ -31,35 +30,27 @@ export const EnvironmentStandardsSchema = z.object({ quantitative: z.array( z.object({ name: z.string(), - description: z.string() + description: z.string().nullable(), + unit: z.string().nullable() }) ) }); -// Infer the TypeScript type from the Zod schema export type EnvironmentStandards = z.infer; -// THIS AINT WORKING OR BEING IMPORTED CORRECTLY IN STANDARSD SERVICE - -// export interface MethodStandards { -// name: string; -// description: string; - -// } - export const MethodStandardSchema = z.object({ method_lookup_id: z.number(), name: z.string(), - description: z.string(), + description: z.string().nullable(), attributes: z.object({ qualitative: z.array( z.object({ name: z.string(), - description: z.string(), + description: z.string().nullable(), options: z.array( z.object({ name: z.string(), - description: z.string() + description: z.string().nullable() }) ) }) @@ -67,8 +58,8 @@ export const MethodStandardSchema = z.object({ quantitative: z.array( z.object({ name: z.string(), - description: z.string(), - units: z.string() + description: z.string().nullable(), + unit: z.string().nullable() }) ) }) diff --git a/api/src/openapi/schemas/standards.ts b/api/src/openapi/schemas/standards.ts index 9d79ecd3a8..fdece6b26d 100644 --- a/api/src/openapi/schemas/standards.ts +++ b/api/src/openapi/schemas/standards.ts @@ -13,7 +13,8 @@ export const EnvironmentStandardsSchema: OpenAPIV3.SchemaObject = { type: 'string' }, description: { - type: 'string' + type: 'string', + nullable: true }, options: { type: 'array', @@ -24,7 +25,8 @@ export const EnvironmentStandardsSchema: OpenAPIV3.SchemaObject = { type: 'string' }, description: { - type: 'string' + type: 'string', + nullable: true } } } @@ -41,15 +43,17 @@ export const EnvironmentStandardsSchema: OpenAPIV3.SchemaObject = { type: 'string' }, description: { - type: 'string' - } + type: 'string', + nullable: true + }, + unit: { type: 'string', nullable: true } } } } } }; -export const MethodStandardsSchema: OpenAPIV3.SchemaObject = { +export const MethodStandardSchema: OpenAPIV3.SchemaObject = { type: 'array', items: { type: 'object', @@ -57,7 +61,7 @@ export const MethodStandardsSchema: OpenAPIV3.SchemaObject = { properties: { method_lookup_id: { type: 'number' }, name: { type: 'string' }, - description: { type: 'string' }, + description: { type: 'string', nullable: true }, attributes: { type: 'object', additionalProperties: false, @@ -72,7 +76,8 @@ export const MethodStandardsSchema: OpenAPIV3.SchemaObject = { type: 'string' }, description: { - type: 'string' + type: 'string', + nullable: true }, options: { type: 'array', @@ -84,7 +89,8 @@ export const MethodStandardsSchema: OpenAPIV3.SchemaObject = { type: 'string' }, description: { - type: 'string' + type: 'string', + nullable: true } } } @@ -102,9 +108,10 @@ export const MethodStandardsSchema: OpenAPIV3.SchemaObject = { type: 'string' }, description: { - type: 'string' + type: 'string', + nullable: true }, - unit: { type: 'string' } + unit: { type: 'string', nullable: true } } } } diff --git a/api/src/paths/standards/environment/index.ts b/api/src/paths/standards/environment/index.ts index 8632b20c1a..7021c9eb37 100644 --- a/api/src/paths/standards/environment/index.ts +++ b/api/src/paths/standards/environment/index.ts @@ -12,7 +12,17 @@ export const GET: Operation = [getEnvironmentStandards()]; GET.apiDoc = { description: 'Gets lookup values for environment variables', tags: ['standards'], - parameters: [], + parameters: [ + { + in: 'query', + name: 'keyword', + required: false, + schema: { + type: 'string', + nullable: true + } + } + ], security: [{ Bearer: [] }], responses: { 200: { @@ -47,7 +57,7 @@ GET.apiDoc = { * @returns {RequestHandler} */ export function getEnvironmentStandards(): RequestHandler { - return async (_, res) => { + return async (req, res) => { const connection = getAPIUserDBConnection(); try { @@ -55,7 +65,9 @@ export function getEnvironmentStandards(): RequestHandler { const standardsService = new StandardsService(connection); - const response = await standardsService.getEnvironmentStandards(); + const keyword = (req.query.keyword as string) ?? ''; + + const response = await standardsService.getEnvironmentStandards(keyword); await connection.commit(); diff --git a/api/src/paths/standards/methods/index.ts b/api/src/paths/standards/methods/index.ts index 8d15c43858..b9ed53c364 100644 --- a/api/src/paths/standards/methods/index.ts +++ b/api/src/paths/standards/methods/index.ts @@ -1,7 +1,7 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; import { getAPIUserDBConnection } from '../../../database/db'; -import { MethodStandardsSchema } from '../../../openapi/schemas/standards'; +import { MethodStandardSchema } from '../../../openapi/schemas/standards'; import { StandardsService } from '../../../services/standards-service'; import { getLogger } from '../../../utils/logger'; @@ -12,14 +12,24 @@ export const GET: Operation = [getMethodStandards()]; GET.apiDoc = { description: 'Gets lookup values for method variables', tags: ['standards'], - parameters: [], + parameters: [ + { + in: 'query', + name: 'keyword', + required: false, + schema: { + type: 'string', + nullable: true + } + } + ], security: [{ Bearer: [] }], responses: { 200: { description: 'Method data standards response object.', content: { 'application/json': { - schema: MethodStandardsSchema + schema: MethodStandardSchema } } }, @@ -47,7 +57,7 @@ GET.apiDoc = { * @returns {RequestHandler} */ export function getMethodStandards(): RequestHandler { - return async (_, res) => { + return async (req, res) => { const connection = getAPIUserDBConnection(); try { @@ -55,7 +65,9 @@ export function getMethodStandards(): RequestHandler { const standardsService = new StandardsService(connection); - const response = await standardsService.getMethodStandards(); + const keyword = (req.query.keyword as string) ?? ''; + + const response = await standardsService.getMethodStandards(keyword); await connection.commit(); diff --git a/api/src/repositories/standards-repository.ts b/api/src/repositories/standards-repository.ts index 4d94385dc6..4fb7666169 100644 --- a/api/src/repositories/standards-repository.ts +++ b/api/src/repositories/standards-repository.ts @@ -1,5 +1,4 @@ import SQL from 'sql-template-strings'; -import { ApiExecuteSQLError } from '../errors/api-error'; import { EnvironmentStandards, EnvironmentStandardsSchema, @@ -19,18 +18,22 @@ export class StandardsRepository extends BaseRepository { /** * Gets environment standards * + * @param {string} keyword * @return {*} * @memberof standardsRepository */ - async getEnvironmentStandards(): Promise { + async getEnvironmentStandards(keyword?: string): Promise { const sql = SQL` WITH quan AS ( SELECT - name AS quant_name, - description AS quant_description + eq.name AS quant_name, + eq.description AS quant_description, + eq.unit FROM - environment_quantitative + environment_quantitative eq + WHERE + eq.name ILIKE '%' || ${keyword ?? ''} || '%' ), qual AS ( SELECT @@ -41,108 +44,95 @@ export class StandardsRepository extends BaseRepository { environment_qualitative_option eqo LEFT JOIN environment_qualitative eq ON eqo.environment_qualitative_id = eq.environment_qualitative_id + WHERE + eq.name ILIKE '%' || ${keyword ?? ''} || '%' GROUP BY eq.name, eq.description ) SELECT - (SELECT json_agg(json_build_object('name', quant_name, 'description', quant_description)) FROM quan) as quantitative, + (SELECT json_agg(json_build_object('name', quant_name, 'description', quant_description, 'unit', unit)) FROM quan) as quantitative, (SELECT json_agg(json_build_object('name', qual_name, 'description', qual_description, 'options', options)) FROM qual) as qualitative; `; const response = await this.connection.sql(sql, EnvironmentStandardsSchema); - if (!response.rowCount) { - throw new ApiExecuteSQLError('Failed to get environment standards', [ - 'standardsRepository->getEnvironmentStandards', - 'rows was null or undefined, expected rows != null' - ]); - } - return response.rows[0]; } - // FROM HERE DOWN - - async getMethodStandards(): Promise { - const sql = SQL`WITH - quan AS ( - SELECT - mlaq.method_lookup_id, - tq.name AS quant_name, - tq.description AS quant_description, - mlaq.unit - FROM - method_lookup_attribute_quantitative mlaq - LEFT JOIN - technique_attribute_quantitative tq ON mlaq.technique_attribute_quantitative_id = tq.technique_attribute_quantitative_id - ORDER BY tq.name - ), - qual AS ( - SELECT - mlaq.method_lookup_id, - taq.name AS qual_name, - taq.description AS qual_description, - json_agg(json_build_object('name', mlaqo.name, 'description', mlaqo.description) ORDER BY mlaqo.name) AS options - FROM - method_lookup_attribute_qualitative_option mlaqo - LEFT JOIN - method_lookup_attribute_qualitative mlaq ON mlaqo.method_lookup_attribute_qualitative_id = mlaq.method_lookup_attribute_qualitative_id - LEFT JOIN - technique_attribute_qualitative taq ON mlaq.technique_attribute_qualitative_id = taq.technique_attribute_qualitative_id - GROUP BY - mlaq.method_lookup_id, - taq.name, - taq.description - ORDER BY - taq.name - ), - method_lookup AS ( - SELECT - ml.method_lookup_id, - ml.name, - ml.description, - json_build_object( - 'quantitative', ( - SELECT json_agg( - json_build_object( - 'name', quan.quant_name, - 'description', quan.quant_description, - 'unit', quan.unit - ) ORDER BY quan.quant_name - ) FROM quan - WHERE quan.method_lookup_id = ml.method_lookup_id - ), - 'qualitative', ( - SELECT json_agg( - json_build_object( - 'name', qual.qual_name, - 'description', qual.qual_description, - 'options', qual.options - ) ORDER BY qual.qual_name - ) FROM qual - WHERE qual.method_lookup_id = ml.method_lookup_id - ) - ) AS attributes - FROM - method_lookup ml - ORDER BY - ml.name - ) -SELECT * FROM method_lookup; - - + async getMethodStandards(keyword?: string): Promise { + const sql = SQL` + WITH + quan AS ( + SELECT + mlaq.method_lookup_id, + tq.name AS quant_name, + tq.description AS quant_description, + mlaq.unit + FROM + method_lookup_attribute_quantitative mlaq + LEFT JOIN + technique_attribute_quantitative tq ON mlaq.technique_attribute_quantitative_id = tq.technique_attribute_quantitative_id + ), + qual AS ( + SELECT + mlaq.method_lookup_id, + taq.name AS qual_name, + taq.description AS qual_description, + COALESCE(json_agg( + json_build_object( + 'name', mlaqo.name, + 'description', mlaqo.description + ) ORDER BY mlaqo.name + ), '[]'::json) AS options + FROM + method_lookup_attribute_qualitative_option mlaqo + LEFT JOIN + method_lookup_attribute_qualitative mlaq ON mlaqo.method_lookup_attribute_qualitative_id = mlaq.method_lookup_attribute_qualitative_id + LEFT JOIN + technique_attribute_qualitative taq ON mlaq.technique_attribute_qualitative_id = taq.technique_attribute_qualitative_id + GROUP BY + mlaq.method_lookup_id, + taq.name, + taq.description + ), + method_lookup AS ( + SELECT + ml.method_lookup_id, + ml.name, + ml.description, + json_build_object( + 'quantitative', ( + SELECT COALESCE(json_agg( + json_build_object( + 'name', quan.quant_name, + 'description', quan.quant_description, + 'unit', quan.unit + ) ORDER BY quan.quant_name + ), '[]'::json) FROM quan + WHERE quan.method_lookup_id = ml.method_lookup_id + ), + 'qualitative', ( + SELECT COALESCE(json_agg( + json_build_object( + 'name', qual.qual_name, + 'description', qual.qual_description, + 'options', qual.options + ) ORDER BY qual.qual_name + ), '[]'::json) FROM qual + WHERE qual.method_lookup_id = ml.method_lookup_id + ) + ) AS attributes + FROM + method_lookup ml + WHERE + ml.name ILIKE '%' || ${keyword ?? ''} || '%' + ) + SELECT * FROM method_lookup; `; const response = await this.connection.sql(sql, MethodStandardSchema); - if (!response.rowCount) { - throw new ApiExecuteSQLError('Failed to get Method standards', [ - 'standardsRepository->getMethodStandards', - 'rows was null or undefined, expected rows != null' - ]); - } - return response.rows; } } diff --git a/api/src/services/standards-service.ts b/api/src/services/standards-service.ts index 9871ec7e3d..0437cb6bd9 100644 --- a/api/src/services/standards-service.ts +++ b/api/src/services/standards-service.ts @@ -53,25 +53,26 @@ export class StandardsService extends DBService { /** * Gets environment standards * + * @param {string} keyword * @return {EnvironmentStandard[]} * @memberof standardsService */ - async getEnvironmentStandards(): Promise { - const response = await this.standardsRepository.getEnvironmentStandards(); + async getEnvironmentStandards(keyword?: string): Promise { + const response = await this.standardsRepository.getEnvironmentStandards(keyword); return response; } -// THE BELOW IS NOT IMPORTING MethodStandards correctly. STANDARDS VIEW INTERFACE FOR METHODS IS PROBABLY WRONG - /** - * Gets METHOD standards + * Gets standards for method lookups + * + * @param {string} keyword * @return {MethodStandards} * @memberof standardsService */ - async getMethodStandards(): Promise { - const response = await this.standardsRepository.getMethodStandards(); + async getMethodStandards(keyword?: string): Promise { + const response = await this.standardsRepository.getMethodStandards(keyword); return response; }} diff --git a/app/src/components/loading/SkeletonLoaders.tsx b/app/src/components/loading/SkeletonLoaders.tsx index 032477d6b5..fa75b8ca93 100644 --- a/app/src/components/loading/SkeletonLoaders.tsx +++ b/app/src/components/loading/SkeletonLoaders.tsx @@ -125,4 +125,11 @@ const SkeletonMap = () => ( ); -export { SkeletonList, SkeletonListStack, SkeletonRow, SkeletonTable, SkeletonMap, SkeletonHorizontalStack }; +export { + SkeletonHorizontalStack, + SkeletonList, + SkeletonListStack, + SkeletonMap, + SkeletonRow, + SkeletonTable +}; diff --git a/app/src/features/standards/StandardsPage.tsx b/app/src/features/standards/StandardsPage.tsx index 260aab39ba..038479645e 100644 --- a/app/src/features/standards/StandardsPage.tsx +++ b/app/src/features/standards/StandardsPage.tsx @@ -1,3 +1,5 @@ +import { mdiLeaf, mdiPaw, mdiToolbox } from '@mdi/js'; +import Box from '@mui/material/Box'; import Container from '@mui/material/Container'; import Paper from '@mui/material/Paper'; import Stack from '@mui/material/Stack'; @@ -17,15 +19,16 @@ export enum StandardsPageView { export interface IStandardsPageView { label: string; value: StandardsPageView; + icon: string; } const StandardsPage = () => { const [currentView, setCurrentView] = useState(StandardsPageView.SPECIES); const views: IStandardsPageView[] = [ - { label: 'Species', value: StandardsPageView.SPECIES }, - { label: 'Sampling Methods', value: StandardsPageView.METHODS }, - { label: 'Environment variables', value: StandardsPageView.ENVIRONMENT } + { label: 'Species', value: StandardsPageView.SPECIES, icon: mdiPaw }, + { label: 'Sampling Methods', value: StandardsPageView.METHODS, icon: mdiToolbox }, + { label: 'Environment variables', value: StandardsPageView.ENVIRONMENT, icon: mdiLeaf } ]; return ( @@ -34,16 +37,20 @@ const StandardsPage = () => { {/* TOOLBAR FOR SWITCHING VIEWS */} - + + + - {/* SPECIES STANDARDS */} - {currentView === StandardsPageView.SPECIES && } + + {/* SPECIES STANDARDS */} + {currentView === StandardsPageView.SPECIES && } - {/* METHOD STANDARDS */} - {currentView === StandardsPageView.METHODS && } + {/* METHOD STANDARDS */} + {currentView === StandardsPageView.METHODS && } - {/* ENVIRONMENT STANDARDS */} - {currentView === StandardsPageView.ENVIRONMENT && } + {/* ENVIRONMENT STANDARDS */} + {currentView === StandardsPageView.ENVIRONMENT && } + diff --git a/app/src/features/standards/components/AccordionStandardCard.tsx b/app/src/features/standards/components/AccordionStandardCard.tsx new file mode 100644 index 0000000000..edf7f7b640 --- /dev/null +++ b/app/src/features/standards/components/AccordionStandardCard.tsx @@ -0,0 +1,65 @@ +import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; +import { Icon } from '@mdi/react'; +import { Collapse } from '@mui/material'; +import Box, { BoxProps } from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import { useState } from 'react'; + +interface IAccordionStandardCardProps extends BoxProps { + label: string; + subtitle: string; + ornament?: JSX.Element; + children?: JSX.Element; + colour: string; + disableCollapse?: boolean; +} + +/** + * Returns a collapsible paper component for displaying lookup values + * @param props + * @returns + */ +export const AccordionStandardCard = (props: IAccordionStandardCardProps) => { + const { label, subtitle, children, colour, ornament, disableCollapse } = props; + + const [isCollapsed, setIsCollapsed] = useState(true); + + return ( + + { + if (!disableCollapse) { + setIsCollapsed(!isCollapsed); + } + }}> + + + {label} + + {ornament} + + {!disableCollapse && } + + + + + {subtitle} + + {children} + + + + ); +}; diff --git a/app/src/features/standards/components/StandardsToolbar.tsx b/app/src/features/standards/components/StandardsToolbar.tsx index 626a817d05..993bc05cb4 100644 --- a/app/src/features/standards/components/StandardsToolbar.tsx +++ b/app/src/features/standards/components/StandardsToolbar.tsx @@ -1,5 +1,8 @@ -import ToggleButton from '@mui/material/ToggleButton'; +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'; @@ -19,35 +22,43 @@ export const StandardsToolbar = (props: IStandardsToolbar) => { const { views, currentView, setCurrentView } = props; return ( - , view: StandardsPageView) => { - if (view) { - setCurrentView(view); - } - }} - exclusive - sx={{ - minWidth: '200px', - 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' - } - }}> - {views.map((view) => ( - - {view.label} - - ))} - + <> + Data types + , view: StandardsPageView) => { + 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/environment/EnvironmentStandards.tsx b/app/src/features/standards/view/environment/EnvironmentStandards.tsx index 28a26299bd..b0c7b98b9c 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandards.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandards.tsx @@ -1,7 +1,11 @@ import Box from '@mui/material/Box'; +import Skeleton from '@mui/material/Skeleton'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; -import { useEffect } from 'react'; +import { debounce } from 'lodash-es'; +import { useEffect, useMemo, useState } from 'react'; import { EnvironmentStandardsResults } from './EnvironmentStandardsResults'; /** @@ -12,15 +16,56 @@ import { EnvironmentStandardsResults } from './EnvironmentStandardsResults'; export const EnvironmentStandards = () => { const biohubApi = useBiohubApi(); - const environmentDataLoader = useDataLoader(() => biohubApi.standards.getEnvironmentStandards()); + const [searchTerm, setSearchTerm] = useState(''); + + const environmentsDataLoader = useDataLoader((keyword?: string) => + biohubApi.standards.getEnvironmentStandards(keyword) + ); + + const debouncedRefresh = useMemo( + () => + debounce((value: string) => { + environmentsDataLoader.refresh(value); + }, 500), + [] + ); useEffect(() => { - environmentDataLoader.load(); - }, [environmentDataLoader]); + environmentsDataLoader.load(); + }, [environmentsDataLoader]); return ( - - {environmentDataLoader.data && } - + <> + { + const value = event.currentTarget.value; + setSearchTerm(value); + debouncedRefresh(value); + }} + /> + + {environmentsDataLoader.data ? ( + + ) : ( + + + + + + + + + + + + + )} + + ); }; diff --git a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx index 327b2111b4..021c633c3a 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx @@ -1,6 +1,7 @@ +import { grey } from '@mui/material/colors'; import Stack from '@mui/material/Stack'; +import { AccordionStandardCard } from 'features/standards/components/AccordionStandardCard'; import { IEnvironmentStandards } from 'interfaces/useStandardsApi.interface'; -import MeasurementStandardCard from '../components/MeasurementStandardCard'; interface ISpeciesStandardsResultsProps { data: IEnvironmentStandards; @@ -19,10 +20,10 @@ export const EnvironmentStandardsResults = (props: ISpeciesStandardsResultsProps <> {data.quantitative.map((environment) => ( - + ))} {data.qualitative.map((environment) => ( - + ))} diff --git a/app/src/features/standards/view/methods/MethodStandards.tsx b/app/src/features/standards/view/methods/MethodStandards.tsx index 9cd1c4fd9d..90dc0a33f9 100644 --- a/app/src/features/standards/view/methods/MethodStandards.tsx +++ b/app/src/features/standards/view/methods/MethodStandards.tsx @@ -1,29 +1,68 @@ +import { Skeleton, Stack, TextField } from '@mui/material'; import Box from '@mui/material/Box'; -import { MethodStandardsResults } from './MethodStandardsResults'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; -import { useEffect } from 'react'; +import { debounce } from 'lodash-es'; +import { useEffect, useMemo, useState } from 'react'; +import { MethodStandardsResults } from './MethodStandardsResults'; /** * - * This component will handle the data request, then pass the data to its children components. + * Returns information about method lookup options * * @returns */ - export const MethodStandards = () => { const biohubApi = useBiohubApi(); + const [searchTerm, setSearchTerm] = useState(''); - const methodDataLoader = useDataLoader(() => biohubApi.standards.getMethodStandards()); + const methodDataLoader = useDataLoader((keyword?: string) => biohubApi.standards.getMethodStandards(keyword)); + + const debouncedRefresh = useMemo( + () => + debounce((value: string) => { + methodDataLoader.refresh(value); + }, 500), + [] + ); useEffect(() => { methodDataLoader.load(); }, [methodDataLoader]); return ( - - {methodDataLoader.data && } - + <> + { + const value = event.currentTarget.value; + setSearchTerm(value); + debouncedRefresh(value); + }} + /> + + {methodDataLoader.data ? ( + + ) : ( + + + + + + + + + + + + + )} + + ); }; diff --git a/app/src/features/standards/view/methods/MethodStandardsResults.tsx b/app/src/features/standards/view/methods/MethodStandardsResults.tsx index ed5f2a9486..882ed8656c 100644 --- a/app/src/features/standards/view/methods/MethodStandardsResults.tsx +++ b/app/src/features/standards/view/methods/MethodStandardsResults.tsx @@ -1,6 +1,8 @@ +import { blueGrey, grey } from '@mui/material/colors'; import Stack from '@mui/material/Stack'; +import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; import { IMethodStandard } from 'interfaces/useStandardsApi.interface'; -import MethodStandardCard from './components/MethodStandardsCard'; +import { AccordionStandardCard } from '../../components/AccordionStandardCard'; interface ISpeciesStandardsResultsProps { data: IMethodStandard[]; @@ -16,19 +18,49 @@ export const MethodStandardsResults = (props: ISpeciesStandardsResultsProps) => const { data } = props; return ( - <> - - {data.map((method) => ( - - ))} - - + + {data.map((method) => ( + + {method.attributes.qualitative.map((attribute) => ( + + {attribute.options.map((option) => ( + + ))} + + } + /> + ))} + {method.attributes.quantitative.map((attribute) => ( + } + /> + ))} + + } + /> + ))} + ); }; diff --git a/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx b/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx deleted file mode 100644 index 333b68889a..0000000000 --- a/app/src/features/standards/view/methods/components/MethodStandardsCard.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; -import Icon from '@mdi/react'; -import { Box, Collapse, Paper, Stack, Typography } from '@mui/material'; -import { grey } from '@mui/material/colors'; -import { useState } from 'react'; -import { MethodStandardsCardAttribute } from './MethodStandardsCardAttribute'; - -interface IMethodStandardCard { - name: string; - description: string; - quantitativeAttributes: Attribute[]; - qualitativeAttributes: QualitativeAttribute[]; - small?: boolean; -} - -interface Attribute { - name: string; - description: string; - unit?: string; -} - -interface QualitativeAttribute { - name: string; - description: string; - options: { - name: string; - description: string; - }[]; -} - -/** - * Card to display method information for species standards - * - * @return {*} - */ -const MethodStandardCard = (props: IMethodStandardCard) => { - const [isCollapsed, setIsCollapsed] = useState(true); - - return ( - - {/* METHOD */} - setIsCollapsed(!isCollapsed)} - > - - {props.name} - - - - - - {props.description} - - - {/* QUANTITATIVE ATTRIBUTES */} - - {props.quantitativeAttributes?.map((attribute) => ( - - ))} - {props.qualitativeAttributes?.map((attribute) => ( - - ))} - - - - ); -}; - -export default MethodStandardCard; diff --git a/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx b/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx deleted file mode 100644 index 70744c25c7..0000000000 --- a/app/src/features/standards/view/methods/components/MethodStandardsCardAttribute.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; -import Icon from '@mdi/react'; -import Box from '@mui/material/Box'; -import Chip from '@mui/material/Chip'; -import Collapse from '@mui/material/Collapse'; -import { green, grey } from '@mui/material/colors'; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; -import { useState } from 'react'; - -interface IMethodStandardsCardAttributeProps { - name: string; - description: string; - unit?: string; - options?: { name: string; description: string }[]; -} - -export const MethodStandardsCardAttribute = (props: IMethodStandardsCardAttributeProps) => { - const [isCollapsed, setIsCollapsed] = useState(true); - const [collapsedOptions, setCollapsedOptions] = useState<{ [key: string]: boolean }>({}); - - const { name, description, unit, options } = props; - - const handleOptionClick = (name: string) => { - setCollapsedOptions((prev) => ({ - ...prev, - [name]: !prev[name] - })); - }; - - return ( - - setIsCollapsed((prev) => !prev)}> - - {name} - - {unit && ( - - )} - {/* Smaller chevron */} - - - - {description || 'No description available'} - - {options?.map((option) => ( - - handleOptionClick(option.name)}> - - {option.name} - - - - - - {option.description || 'No description available'} - - - - ))} - - - ); -}; diff --git a/app/src/features/standards/view/species/SpeciesStandards.tsx b/app/src/features/standards/view/species/SpeciesStandards.tsx index 6b0c853e65..5e51bf22b5 100644 --- a/app/src/features/standards/view/species/SpeciesStandards.tsx +++ b/app/src/features/standards/view/species/SpeciesStandards.tsx @@ -1,4 +1,6 @@ +import { Skeleton } from '@mui/material'; import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; import SpeciesAutocompleteField from 'components/species/components/SpeciesAutocompleteField'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; @@ -20,7 +22,7 @@ export const SpeciesStandards = () => { ); return ( - + <> { } }} /> - {isDefined(standardsDataLoader.data) && ( - + + {isDefined(standardsDataLoader.data) ? ( - - )} - + ) : ( + + + + + + + + + + + + + )} + + ); }; diff --git a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx index 4386254ce8..dc9190c798 100644 --- a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx +++ b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx @@ -1,10 +1,11 @@ import { mdiRuler, mdiTag } from '@mdi/js'; import { Box, CircularProgress, Divider, Stack, Typography } from '@mui/material'; +import { grey } from '@mui/material/colors'; +import { AccordionStandardCard } from 'features/standards/components/AccordionStandardCard'; import { ScientificNameTypography } from 'features/surveys/animals/components/ScientificNameTypography'; import { ISpeciesStandards } from 'interfaces/useStandardsApi.interface'; import { useState } from 'react'; import MarkingBodyLocationStandardCard from '../components/MarkingBodyLocationStandardCard'; -import MeasurementStandardCard from '../components/MeasurementStandardCard'; import SpeciesStandardsToolbar, { SpeciesStandardsViewEnum } from '../components/SpeciesStandardsToolbar'; interface ISpeciesStandardsResultsProps { @@ -61,16 +62,29 @@ const SpeciesStandardsResults = (props: ISpeciesStandardsResultsProps) => { {activeView === SpeciesStandardsViewEnum.MEASUREMENTS && ( {props.data.measurements.qualitative.map((measurement) => ( - + {measurement.options.map((option) => ( + + ))} + + } /> ))} {props.data.measurements.quantitative.map((measurement) => ( - ))} diff --git a/app/src/hooks/api/useStandardsApi.ts b/app/src/hooks/api/useStandardsApi.ts index 35aa377ce0..6920409de7 100644 --- a/app/src/hooks/api/useStandardsApi.ts +++ b/app/src/hooks/api/useStandardsApi.ts @@ -1,5 +1,5 @@ import { AxiosInstance } from 'axios'; -import { IEnvironmentStandards, ISpeciesStandards, IMethodStandard } from 'interfaces/useStandardsApi.interface'; +import { IEnvironmentStandards, IMethodStandard, ISpeciesStandards } from 'interfaces/useStandardsApi.interface'; /** * Returns information about what data can be uploaded for a given species, @@ -20,28 +20,41 @@ const useStandardsApi = (axios: AxiosInstance) => { return data; }; - /** - * Fetch method standards - * - * @return {*} {Promise} - */ - const getMethodStandards = async (): Promise => { - const { data } = await axios.get(`/api/standards/methods`); - - return data; - }; - - /** - * Fetch environment standards - * - * @return {*} {Promise} - */ - const getEnvironmentStandards = async (): Promise => { - const { data } = await axios.get(`/api/standards/environment`); - - return data; - }; - + /** + * Fetch method standards + * + * @param {string} keyword + * @return {*} {Promise} + */ + const getMethodStandards = async (keyword?: string): Promise => { + let url = '/api/standards/methods'; + + if (keyword) { + url += `?keyword=${keyword}`; + } + + const { data } = await axios.get(url); + + return data; + }; + + /** + * Fetch environment standards + * + * @param {string} keyword + * @return {*} {Promise} + */ + const getEnvironmentStandards = async (keyword?: string): Promise => { + let url = '/api/standards/environment'; + + if (keyword) { + url += `?keyword=${keyword}`; + } + + const { data } = await axios.get(url); + + return data; + }; return { getSpeciesStandards, diff --git a/app/src/interfaces/useStandardsApi.interface.ts b/app/src/interfaces/useStandardsApi.interface.ts index 0369548ddb..9066e880aa 100644 --- a/app/src/interfaces/useStandardsApi.interface.ts +++ b/app/src/interfaces/useStandardsApi.interface.ts @@ -25,38 +25,29 @@ export interface ISpeciesStandards { ecologicalUnits: ICollectionUnit[]; } -export interface IEnvironmentStandards { - qualitative: { - name: string; - description: string; - options: { - name: string; - description: string; - }; - }[]; - quantitative: { name: string; description: string; units: string }[]; +export interface IStandardNameDescription { + name: string; + description: string; } -export interface IMethodStandard { - method_lookup_id: number; +export interface IQualitativeAttributeStandard { name: string; description: string; - attributes: { - qualitative: { - name: string; - description: string; - options: { - name: string; - description: string; - }[]; - }[]; - quantitative: { name: string; description: string }[]; - }; + options: IStandardNameDescription[]; } -// export interface IMethodStandards { -// methods: Array<{ -// name: string; -// description: string; -// }>; -// } +export interface IQuantitativeAttributeStandard { + name: string; + description: string; + unit: string; +} + +export interface IMethodStandard extends IStandardNameDescription { + method_lookup_id: number; + attributes: { qualitative: IQualitativeAttributeStandard[]; quantitative: IQuantitativeAttributeStandard[] }; +} + +export interface IEnvironmentStandards { + qualitative: IQualitativeAttributeStandard[]; + quantitative: IQuantitativeAttributeStandard[]; +} From e74c10a8db00cb8c6a6791e83ba2cc3690aca553 Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Tue, 6 Aug 2024 15:10:23 -0700 Subject: [PATCH 13/19] write tests --- api/src/models/standards-view.ts | 79 ++++++-------- .../paths/standards/environment/index.test.ts | 4 +- api/src/paths/standards/methods/index.test.ts | 100 ++++++++++++++++++ .../repositories/standards-repository.test.ts | 50 +++++++-- api/src/services/standards-service.test.ts | 51 ++++++++- api/src/services/standards-service.ts | 7 +- .../components/loading/SkeletonLoaders.tsx | 9 +- .../components/AccordionStandardCard.tsx | 0 .../components/EnvironmentStandardCard.tsx | 83 --------------- .../components/MeasurementStandardCard.tsx | 78 -------------- .../view/environment/EnvironmentStandards.tsx | 13 +-- .../EnvironmentStandardsResults.tsx | 2 +- .../view/methods/MethodStandards.tsx | 13 +-- .../view/methods/MethodStandardsResults.tsx | 2 +- .../view/species/SpeciesStandards.tsx | 13 +-- .../view/species/SpeciesStandardsResults.tsx | 4 +- .../components/SpeciesStandardsToolbar.tsx | 0 .../ConfigureEnvironmentColumns.tsx | 30 ++++-- .../ConfigureMeasurementColumns.tsx | 32 +++++- 19 files changed, 291 insertions(+), 279 deletions(-) rename app/src/features/standards/{ => view}/components/AccordionStandardCard.tsx (100%) delete mode 100644 app/src/features/standards/view/components/EnvironmentStandardCard.tsx delete mode 100644 app/src/features/standards/view/components/MeasurementStandardCard.tsx rename app/src/features/standards/view/{ => species}/components/SpeciesStandardsToolbar.tsx (100%) diff --git a/api/src/models/standards-view.ts b/api/src/models/standards-view.ts index 88b0f9db68..b611855b15 100644 --- a/api/src/models/standards-view.ts +++ b/api/src/models/standards-view.ts @@ -4,65 +4,50 @@ import { CBQuantitativeMeasurementTypeDefinition } from '../services/critterbase-service'; -export interface ISpeciesStandards { - tsn: number; - scientificName: string; - measurements: { - quantitative: CBQuantitativeMeasurementTypeDefinition[]; - qualitative: CBQualitativeMeasurementTypeDefinition[]; - }; - markingBodyLocations: { id: string; key: string; value: string }[]; -} - -export const EnvironmentStandardsSchema = z.object({ - qualitative: z.array( - z.object({ - name: z.string(), - description: z.string().nullable(), - options: z.array( - z.object({ - name: z.string(), - description: z.string().nullable() - }) - ) - }) - ), - quantitative: z.array( +const QualitativeMeasurementSchema = z.object({ + name: z.string(), + description: z.string().nullable(), + options: z.array( z.object({ name: z.string(), - description: z.string().nullable(), - unit: z.string().nullable() + description: z.string().nullable() }) ) }); +const QuantitativeMeasurementSchema = z.object({ + name: z.string(), + description: z.string().nullable(), + unit: z.string().nullable() +}); + +const MethodAttributesSchema = z.object({ + qualitative: z.array(QualitativeMeasurementSchema), + quantitative: z.array(QuantitativeMeasurementSchema) +}); + +export const EnvironmentStandardsSchema = z.object({ + qualitative: z.array(QualitativeMeasurementSchema), + quantitative: z.array(QuantitativeMeasurementSchema) +}); + export type EnvironmentStandards = z.infer; export const MethodStandardSchema = z.object({ method_lookup_id: z.number(), name: z.string(), description: z.string().nullable(), - attributes: z.object({ - qualitative: z.array( - z.object({ - name: z.string(), - description: z.string().nullable(), - options: z.array( - z.object({ - name: z.string(), - description: z.string().nullable() - }) - ) - }) - ), - quantitative: z.array( - z.object({ - name: z.string(), - description: z.string().nullable(), - unit: z.string().nullable() - }) - ) - }) + attributes: MethodAttributesSchema }); export type MethodStandard = z.infer; + +export interface ISpeciesStandards { + tsn: number; + scientificName: string; + measurements: { + quantitative: CBQuantitativeMeasurementTypeDefinition[]; + qualitative: CBQualitativeMeasurementTypeDefinition[]; + }; + markingBodyLocations: { id: string; key: string; value: string }[]; +} diff --git a/api/src/paths/standards/environment/index.test.ts b/api/src/paths/standards/environment/index.test.ts index 3b033ccdac..fedcd97eba 100644 --- a/api/src/paths/standards/environment/index.test.ts +++ b/api/src/paths/standards/environment/index.test.ts @@ -19,8 +19,8 @@ describe('standards/environment', () => { it('should retrieve environment standards successfully', async () => { const mockResponse = { quantitative: [ - { name: 'Quantitative Standard 1', description: 'Description 1' }, - { name: 'Quantitative Standard 2', description: 'Description 2' } + { name: 'Quantitative Standard 1', description: 'Description 1', unit: 'Unit' }, + { name: 'Quantitative Standard 2', description: 'Description 2', unit: 'Unit' } ], qualitative: [ { diff --git a/api/src/paths/standards/methods/index.test.ts b/api/src/paths/standards/methods/index.test.ts index e69de29bb2..35517a011f 100644 --- a/api/src/paths/standards/methods/index.test.ts +++ b/api/src/paths/standards/methods/index.test.ts @@ -0,0 +1,100 @@ +import chai, { expect } from 'chai'; +import { afterEach, describe, it } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { getMethodStandards } from '.'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/http-error'; +import { StandardsService } from '../../../services/standards-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; + +chai.use(sinonChai); + +describe('standards/environment', () => { + describe('getMethodStandards', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should retrieve environment standards successfully', async () => { + const mockResponse = [ + { + method_lookup_id: 1, + name: 'Name', + description: 'Description', + attributes: { + quantitative: [ + { name: 'Quantitative Standard 1', description: 'Description 1', unit: 'Unit' }, + { name: 'Quantitative Standard 2', description: 'Description 2', unit: 'Unit' } + ], + qualitative: [ + { + name: 'Qualitative Standard 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative Standard 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + } + } + ]; + + const mockDBConnection = getMockDBConnection(); + + sinon.stub(db, 'getAPIUserDBConnection').returns(mockDBConnection); + + sinon.stub(StandardsService.prototype, 'getMethodStandards').resolves(mockResponse); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getMethodStandards(); + + await requestHandler(mockReq, mockRes, mockNext); + } catch (actualError) { + expect.fail(); + } + + expect(mockRes.status).to.have.been.calledWith(200); + expect(mockRes.json).to.have.been.calledWith(mockResponse); + }); + + it('catches and re-throws error', async () => { + const mockDBConnection = getMockDBConnection({ + open: sinon.stub(), + commit: sinon.stub(), + rollback: sinon.stub(), + release: sinon.stub() + }); + + sinon.stub(db, 'getAPIUserDBConnection').returns(mockDBConnection); + + sinon + .stub(StandardsService.prototype, 'getMethodStandards') + .rejects(new Error('Failed to retrieve environment standards')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getMethodStandards(); + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(mockDBConnection.open).to.have.been.calledOnce; + expect(mockDBConnection.rollback).to.have.been.calledOnce; + expect(mockDBConnection.release).to.have.been.calledOnce; + expect((actualError as HTTPError).message).to.equal('Failed to retrieve environment standards'); + } + }); + }); +}); diff --git a/api/src/repositories/standards-repository.test.ts b/api/src/repositories/standards-repository.test.ts index 7eff0efc7b..c37c5a0b91 100644 --- a/api/src/repositories/standards-repository.test.ts +++ b/api/src/repositories/standards-repository.test.ts @@ -3,7 +3,6 @@ import { afterEach, describe, it } from 'mocha'; import { QueryResult } from 'pg'; import sinon from 'sinon'; import sinonChai from 'sinon-chai'; -import { ApiExecuteSQLError } from '../errors/api-error'; import { getMockDBConnection } from '../__mocks__/db'; import { StandardsRepository } from './standards-repository'; @@ -56,11 +55,45 @@ describe('StandardsRepository', () => { expect(result).to.deep.equal(mockData); }); + }); + + describe('getMethodStandards', () => { + it('should successfully retrieve method standards', async () => { + const mockData = [ + { + method_lookup_id: 1, + name: 'Method 1', + description: ' Description 1', + attributes: { + quantitative: [ + { name: 'Method Standard 1', description: 'Description 1', unit: 'Unit 1' }, + { name: 'Method Standard 2', description: 'Description 2', unit: 'Unit 2' } + ], + qualitative: [ + { + name: 'Qualitative 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + } + } + ]; - it('should handle empty result and throw ApiExecuteSQLError', async () => { const mockResponse = { - rows: [], - rowCount: 0 + rows: mockData, + rowCount: 1 } as any as Promise>; const dbConnection = getMockDBConnection({ @@ -69,12 +102,9 @@ describe('StandardsRepository', () => { const repository = new StandardsRepository(dbConnection); - try { - await repository.getEnvironmentStandards(); - expect.fail(); - } catch (error) { - expect((error as ApiExecuteSQLError).message).to.be.eq('Failed to get environment standards'); - } + const result = await repository.getMethodStandards(); + + expect(result).to.deep.equal(mockData); }); }); }); diff --git a/api/src/services/standards-service.test.ts b/api/src/services/standards-service.test.ts index 8340e2b2b4..0bc050d72e 100644 --- a/api/src/services/standards-service.test.ts +++ b/api/src/services/standards-service.test.ts @@ -73,8 +73,8 @@ describe('StandardsService', () => { const mockData = { qualitative: [{ name: 'name', description: 'name', options: [{ name: 'name', description: 'description' }] }], quantitative: [ - { name: 'name', description: 'description' }, - { name: 'name', description: 'description' } + { name: 'name', description: 'description', unit: 'unit' }, + { name: 'name', description: 'description', unit: 'unit' } ] }; const mockDbConnection = getMockDBConnection(); @@ -90,4 +90,51 @@ describe('StandardsService', () => { expect(getEnvironmentStandardsStub).to.be.calledOnce; expect(response).to.eql(mockData); }); + + describe('getMethodStandards', async () => { + const mockData = [ + { + method_lookup_id: 1, + name: 'Method 1', + description: ' Description 1', + attributes: { + quantitative: [ + { name: 'Method Standard 1', description: 'Description 1', unit: 'Unit 1' }, + { name: 'Method Standard 2', description: 'Description 2', unit: 'Unit 2' } + ], + qualitative: [ + { + name: 'Qualitative 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + } + } + ]; + + const mockDbConnection = getMockDBConnection(); + + const standardsService = new StandardsService(mockDbConnection); + + const getMethodStandardsStub = sinon + .stub(standardsService.standardsRepository, 'getMethodStandards') + .resolves(mockData); + + const response = await standardsService.getMethodStandards(); + + expect(getMethodStandardsStub).to.be.calledOnce; + expect(response).to.eql(mockData); + }); }); diff --git a/api/src/services/standards-service.ts b/api/src/services/standards-service.ts index 0437cb6bd9..24684c4608 100644 --- a/api/src/services/standards-service.ts +++ b/api/src/services/standards-service.ts @@ -63,10 +63,9 @@ export class StandardsService extends DBService { return response; } - /** * Gets standards for method lookups - * + * * @param {string} keyword * @return {MethodStandards} * @memberof standardsService @@ -74,5 +73,5 @@ export class StandardsService extends DBService { async getMethodStandards(keyword?: string): Promise { const response = await this.standardsRepository.getMethodStandards(keyword); return response; - -}} + } +} diff --git a/app/src/components/loading/SkeletonLoaders.tsx b/app/src/components/loading/SkeletonLoaders.tsx index fa75b8ca93..688bcc0158 100644 --- a/app/src/components/loading/SkeletonLoaders.tsx +++ b/app/src/components/loading/SkeletonLoaders.tsx @@ -125,11 +125,4 @@ const SkeletonMap = () => ( ); -export { - SkeletonHorizontalStack, - SkeletonList, - SkeletonListStack, - SkeletonMap, - SkeletonRow, - SkeletonTable -}; +export { SkeletonHorizontalStack, SkeletonList, SkeletonListStack, SkeletonMap, SkeletonRow, SkeletonTable }; diff --git a/app/src/features/standards/components/AccordionStandardCard.tsx b/app/src/features/standards/view/components/AccordionStandardCard.tsx similarity index 100% rename from app/src/features/standards/components/AccordionStandardCard.tsx rename to app/src/features/standards/view/components/AccordionStandardCard.tsx diff --git a/app/src/features/standards/view/components/EnvironmentStandardCard.tsx b/app/src/features/standards/view/components/EnvironmentStandardCard.tsx deleted file mode 100644 index 1549dae796..0000000000 --- a/app/src/features/standards/view/components/EnvironmentStandardCard.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; -import Icon from '@mdi/react'; -import Box from '@mui/material/Box'; -import Card from '@mui/material/Card'; -import Collapse from '@mui/material/Collapse'; -import { grey } from '@mui/material/colors'; -import Paper from '@mui/material/Paper'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { EnvironmentQualitativeOption } from 'interfaces/useReferenceApi.interface'; -import { useState } from 'react'; - -interface IEnvironmentStandardCard { - label: string; - description?: string; - options?: EnvironmentQualitativeOption[]; - unit?: string; - small?: boolean; -} - -/** - * Card to display environment information. - * - * @return {*} - */ -const EnvironmentStandardCard = (props: IEnvironmentStandardCard) => { - const [isCollapsed, setIsCollapsed] = useState(true); - const { small } = props; - - return ( - - setIsCollapsed(!isCollapsed)}> - - {props.label} - - - - - - - {props.description ? props.description : 'No description'} - - - - {props.options?.map((option) => ( - - - {option.name} - - - {option?.description} - - - ))} - - - - ); -}; - -export default EnvironmentStandardCard; diff --git a/app/src/features/standards/view/components/MeasurementStandardCard.tsx b/app/src/features/standards/view/components/MeasurementStandardCard.tsx deleted file mode 100644 index 0d79432b55..0000000000 --- a/app/src/features/standards/view/components/MeasurementStandardCard.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { mdiChevronDown, mdiChevronUp } from '@mdi/js'; -import Icon from '@mdi/react'; -import { Box, Card, Collapse, Paper, Stack, Typography } from '@mui/material'; -import { grey } from '@mui/material/colors'; -import { CBQualitativeOption } from 'interfaces/useCritterApi.interface'; -import { useState } from 'react'; - -interface IMeasurementStandardCard { - label: string; - description?: string; - options?: CBQualitativeOption[]; - unit?: string; - small?: boolean; -} - -/** - * Card to display measurements information for species standards - * - * @return {*} - */ -const MeasurementStandardCard = (props: IMeasurementStandardCard) => { - const [isCollapsed, setIsCollapsed] = useState(true); - const { small } = props; - - return ( - - setIsCollapsed(!isCollapsed)}> - - {props.label} - - - - - - - {props.description ? props.description : 'No description'} - - - - {props.options?.map((option) => ( - - - {option.option_label} - - - {option?.option_desc} - - - ))} - - - - ); -}; - -export default MeasurementStandardCard; diff --git a/app/src/features/standards/view/environment/EnvironmentStandards.tsx b/app/src/features/standards/view/environment/EnvironmentStandards.tsx index b0c7b98b9c..6848419706 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandards.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandards.tsx @@ -53,16 +53,9 @@ export const EnvironmentStandards = () => { ) : ( - - - - - - - - - - + {[...Array(10)].map((_, index) => ( + + ))} )} diff --git a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx index 021c633c3a..0a2398a3e6 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx @@ -1,6 +1,6 @@ import { grey } from '@mui/material/colors'; import Stack from '@mui/material/Stack'; -import { AccordionStandardCard } from 'features/standards/components/AccordionStandardCard'; +import { AccordionStandardCard } from 'features/standards/view/components/AccordionStandardCard'; import { IEnvironmentStandards } from 'interfaces/useStandardsApi.interface'; interface ISpeciesStandardsResultsProps { diff --git a/app/src/features/standards/view/methods/MethodStandards.tsx b/app/src/features/standards/view/methods/MethodStandards.tsx index 90dc0a33f9..06b9cf597f 100644 --- a/app/src/features/standards/view/methods/MethodStandards.tsx +++ b/app/src/features/standards/view/methods/MethodStandards.tsx @@ -50,16 +50,9 @@ export const MethodStandards = () => { ) : ( - - - - - - - - - - + {[...Array(10)].map((_, index) => ( + + ))} )} diff --git a/app/src/features/standards/view/methods/MethodStandardsResults.tsx b/app/src/features/standards/view/methods/MethodStandardsResults.tsx index 882ed8656c..1cea4ebf51 100644 --- a/app/src/features/standards/view/methods/MethodStandardsResults.tsx +++ b/app/src/features/standards/view/methods/MethodStandardsResults.tsx @@ -2,7 +2,7 @@ import { blueGrey, grey } from '@mui/material/colors'; import Stack from '@mui/material/Stack'; import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; import { IMethodStandard } from 'interfaces/useStandardsApi.interface'; -import { AccordionStandardCard } from '../../components/AccordionStandardCard'; +import { AccordionStandardCard } from '../components/AccordionStandardCard'; interface ISpeciesStandardsResultsProps { data: IMethodStandard[]; diff --git a/app/src/features/standards/view/species/SpeciesStandards.tsx b/app/src/features/standards/view/species/SpeciesStandards.tsx index 5e51bf22b5..be411efb57 100644 --- a/app/src/features/standards/view/species/SpeciesStandards.tsx +++ b/app/src/features/standards/view/species/SpeciesStandards.tsx @@ -37,16 +37,9 @@ export const SpeciesStandards = () => { ) : ( - - - - - - - - - - + {[...Array(10)].map((_, index) => ( + + ))} )} diff --git a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx index dc9190c798..a630e7fa1e 100644 --- a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx +++ b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx @@ -1,12 +1,12 @@ import { mdiRuler, mdiTag } from '@mdi/js'; import { Box, CircularProgress, Divider, Stack, Typography } from '@mui/material'; import { grey } from '@mui/material/colors'; -import { AccordionStandardCard } from 'features/standards/components/AccordionStandardCard'; +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 MarkingBodyLocationStandardCard from '../components/MarkingBodyLocationStandardCard'; -import SpeciesStandardsToolbar, { SpeciesStandardsViewEnum } from '../components/SpeciesStandardsToolbar'; +import SpeciesStandardsToolbar, { SpeciesStandardsViewEnum } from './components/SpeciesStandardsToolbar'; interface ISpeciesStandardsResultsProps { data: ISpeciesStandards; diff --git a/app/src/features/standards/view/components/SpeciesStandardsToolbar.tsx b/app/src/features/standards/view/species/components/SpeciesStandardsToolbar.tsx similarity index 100% rename from app/src/features/standards/view/components/SpeciesStandardsToolbar.tsx rename to app/src/features/standards/view/species/components/SpeciesStandardsToolbar.tsx diff --git a/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx b/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx index 4d4b6a26ae..32b88ef438 100644 --- a/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx +++ b/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx @@ -1,7 +1,9 @@ import { mdiTrashCanOutline } from '@mdi/js'; import Icon from '@mdi/react'; import { Box, IconButton, Stack, Typography } from '@mui/material'; -import EnvironmentStandardCard from 'features/standards/view/components/EnvironmentStandardCard'; +import { blueGrey, grey } from '@mui/material/colors'; +import ColouredRectangleChip from 'components/chips/ColouredRectangleChip'; +import { AccordionStandardCard } from 'features/standards/view/components/AccordionStandardCard'; import { EnvironmentType, EnvironmentTypeIds } from 'interfaces/useReferenceApi.interface'; import { EnvironmentsSearch } from './search/EnvironmentsSearch'; @@ -60,11 +62,22 @@ export const ConfigureEnvironmentColumns = (props: IConfigureEnvironmentColumnsP display="flex" alignItems="flex-start" key={`qualitative_environment_item_${environment.environment_qualitative_id}`}> - + {environment.options.map((option) => ( + + ))} + + } /> - + } + /> - + {measurement.options.map((option) => ( + + ))} + + ) : undefined + } + ornament={ + 'unit' in measurement ? ( + + ) : ( + <> + ) + } /> Date: Tue, 6 Aug 2024 15:57:45 -0700 Subject: [PATCH 14/19] species standards loading state --- .../paths/standards/environment/index.test.ts | 8 +- api/src/paths/standards/methods/index.test.ts | 8 +- .../view/components/AccordionStandardCard.tsx | 28 +++--- .../MarkingBodyLocationStandardCard.tsx | 29 ------ .../view/environment/EnvironmentStandards.tsx | 11 ++- .../EnvironmentStandardsResults.tsx | 29 +++--- .../view/methods/MethodStandards.tsx | 11 ++- .../view/methods/MethodStandardsResults.tsx | 1 - .../view/species/SpeciesStandards.tsx | 31 +++++-- .../view/species/SpeciesStandardsResults.tsx | 90 +++++++++---------- .../ConfigureEnvironmentColumns.tsx | 12 ++- .../ConfigureMeasurementColumns.tsx | 10 +-- 12 files changed, 129 insertions(+), 139 deletions(-) delete mode 100644 app/src/features/standards/view/components/MarkingBodyLocationStandardCard.tsx diff --git a/api/src/paths/standards/environment/index.test.ts b/api/src/paths/standards/environment/index.test.ts index fedcd97eba..2c7a3c0568 100644 --- a/api/src/paths/standards/environment/index.test.ts +++ b/api/src/paths/standards/environment/index.test.ts @@ -50,13 +50,9 @@ describe('standards/environment', () => { const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - try { - const requestHandler = getEnvironmentStandards(); + const requestHandler = getEnvironmentStandards(); - await requestHandler(mockReq, mockRes, mockNext); - } catch (actualError) { - expect.fail(); - } + await requestHandler(mockReq, mockRes, mockNext); expect(mockRes.status).to.have.been.calledWith(200); expect(mockRes.json).to.have.been.calledWith(mockResponse); diff --git a/api/src/paths/standards/methods/index.test.ts b/api/src/paths/standards/methods/index.test.ts index 35517a011f..3a5814a930 100644 --- a/api/src/paths/standards/methods/index.test.ts +++ b/api/src/paths/standards/methods/index.test.ts @@ -57,13 +57,9 @@ describe('standards/environment', () => { const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); - try { - const requestHandler = getMethodStandards(); + const requestHandler = getMethodStandards(); - await requestHandler(mockReq, mockRes, mockNext); - } catch (actualError) { - expect.fail(); - } + await requestHandler(mockReq, mockRes, mockNext); expect(mockRes.status).to.have.been.calledWith(200); expect(mockRes.json).to.have.been.calledWith(mockResponse); diff --git a/app/src/features/standards/view/components/AccordionStandardCard.tsx b/app/src/features/standards/view/components/AccordionStandardCard.tsx index edf7f7b640..1779b4841a 100644 --- a/app/src/features/standards/view/components/AccordionStandardCard.tsx +++ b/app/src/features/standards/view/components/AccordionStandardCard.tsx @@ -8,7 +8,7 @@ import { useState } from 'react'; interface IAccordionStandardCardProps extends BoxProps { label: string; - subtitle: string; + subtitle?: string | null; ornament?: JSX.Element; children?: JSX.Element; colour: string; @@ -25,6 +25,14 @@ export const AccordionStandardCard = (props: IAccordionStandardCardProps) => { const [isCollapsed, setIsCollapsed] = useState(true); + const expandable = (children || subtitle) && !disableCollapse; + + const handleHeaderClick = () => { + if (expandable) { + setIsCollapsed(!isCollapsed); + } + }; + return ( { justifyContent="space-between" flex="1 1 auto" alignItems="center" - sx={{ cursor: disableCollapse ? 'default' : 'pointer', px: 3, py: 2 }} - onClick={() => { - if (!disableCollapse) { - setIsCollapsed(!isCollapsed); - } - }}> + sx={{ cursor: expandable ? 'pointer' : 'default', px: 3, py: 2 }} + onClick={handleHeaderClick}> { {ornament} - {!disableCollapse && } + {expandable && } - - {subtitle} - + {subtitle && ( + + {subtitle} + + )} {children} diff --git a/app/src/features/standards/view/components/MarkingBodyLocationStandardCard.tsx b/app/src/features/standards/view/components/MarkingBodyLocationStandardCard.tsx deleted file mode 100644 index d271785418..0000000000 --- a/app/src/features/standards/view/components/MarkingBodyLocationStandardCard.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Paper, Typography } from '@mui/material'; -import { grey } from '@mui/material/colors'; - -interface IMarkingBodyLocationStandardCard { - label: string; -} - -/** - * Card to display marking body location for species standards - * - * @return {*} - */ -const MarkingBodyLocationStandardCard = (props: IMarkingBodyLocationStandardCard) => { - return ( - - - {props.label} - - - ); -}; - -export default MarkingBodyLocationStandardCard; diff --git a/app/src/features/standards/view/environment/EnvironmentStandards.tsx b/app/src/features/standards/view/environment/EnvironmentStandards.tsx index 6848419706..3e89b87a97 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandards.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandards.tsx @@ -48,14 +48,17 @@ export const EnvironmentStandards = () => { debouncedRefresh(value); }} /> - + {environmentsDataLoader.data ? ( ) : ( - {[...Array(10)].map((_, index) => ( - - ))} + + + + + + )} diff --git a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx index 0a2398a3e6..f7833c4ce2 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx @@ -5,7 +5,6 @@ import { IEnvironmentStandards } from 'interfaces/useStandardsApi.interface'; interface ISpeciesStandardsResultsProps { data: IEnvironmentStandards; - isLoading?: boolean; } /** @@ -17,15 +16,23 @@ export const EnvironmentStandardsResults = (props: ISpeciesStandardsResultsProps const { data } = props; return ( - <> - - {data.quantitative.map((environment) => ( - - ))} - {data.qualitative.map((environment) => ( - - ))} - - + + {data.quantitative.map((environment) => ( + + ))} + {data.qualitative.map((environment) => ( + + ))} + ); }; diff --git a/app/src/features/standards/view/methods/MethodStandards.tsx b/app/src/features/standards/view/methods/MethodStandards.tsx index 06b9cf597f..f29556538d 100644 --- a/app/src/features/standards/view/methods/MethodStandards.tsx +++ b/app/src/features/standards/view/methods/MethodStandards.tsx @@ -45,14 +45,17 @@ export const MethodStandards = () => { debouncedRefresh(value); }} /> - + {methodDataLoader.data ? ( ) : ( - {[...Array(10)].map((_, index) => ( - - ))} + + + + + + )} diff --git a/app/src/features/standards/view/methods/MethodStandardsResults.tsx b/app/src/features/standards/view/methods/MethodStandardsResults.tsx index 1cea4ebf51..001e9bb640 100644 --- a/app/src/features/standards/view/methods/MethodStandardsResults.tsx +++ b/app/src/features/standards/view/methods/MethodStandardsResults.tsx @@ -6,7 +6,6 @@ import { AccordionStandardCard } from '../components/AccordionStandardCard'; interface ISpeciesStandardsResultsProps { data: IMethodStandard[]; - isLoading?: boolean; } /** diff --git a/app/src/features/standards/view/species/SpeciesStandards.tsx b/app/src/features/standards/view/species/SpeciesStandards.tsx index be411efb57..44602950d5 100644 --- a/app/src/features/standards/view/species/SpeciesStandards.tsx +++ b/app/src/features/standards/view/species/SpeciesStandards.tsx @@ -1,11 +1,12 @@ +import { mdiArrowTopRight } from '@mdi/js'; import { Skeleton } from '@mui/material'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; +import { NoDataOverlay } from 'components/overlay/NoDataOverlay'; import SpeciesAutocompleteField from 'components/species/components/SpeciesAutocompleteField'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { IPartialTaxonomy } from 'interfaces/useTaxonomyApi.interface'; -import { isDefined } from 'utils/Utils'; import SpeciesStandardsResults from './SpeciesStandardsResults'; /** @@ -25,7 +26,10 @@ export const SpeciesStandards = () => { <> { + standardsDataLoader.clearData(); + }} handleSpecies={(value) => { if (value) { standardsDataLoader.refresh(value); @@ -33,14 +37,25 @@ export const SpeciesStandards = () => { }} /> - {isDefined(standardsDataLoader.data) ? ( - - ) : ( + {standardsDataLoader.data && } + {standardsDataLoader.isLoading ? ( - {[...Array(10)].map((_, index) => ( - - ))} + + + + + + + + ) : ( + + + )} diff --git a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx index a630e7fa1e..520dfafeaf 100644 --- a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx +++ b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx @@ -1,16 +1,14 @@ import { mdiRuler, mdiTag } from '@mdi/js'; -import { Box, CircularProgress, Divider, Stack, Typography } from '@mui/material'; +import { Box, Divider, Stack, Typography } from '@mui/material'; import { grey } from '@mui/material/colors'; 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 MarkingBodyLocationStandardCard from '../components/MarkingBodyLocationStandardCard'; import SpeciesStandardsToolbar, { SpeciesStandardsViewEnum } from './components/SpeciesStandardsToolbar'; interface ISpeciesStandardsResultsProps { data: ISpeciesStandards; - isLoading: boolean; } /** @@ -21,14 +19,6 @@ interface ISpeciesStandardsResultsProps { const SpeciesStandardsResults = (props: ISpeciesStandardsResultsProps) => { const [activeView, setActiveView] = useState(SpeciesStandardsViewEnum.MEASUREMENTS); - if (props.isLoading) { - return ( - - - - ); - } - return ( <> @@ -58,44 +48,46 @@ const SpeciesStandardsResults = (props: ISpeciesStandardsResultsProps) => { updateDatasetView={setActiveView} /> - - {activeView === SpeciesStandardsViewEnum.MEASUREMENTS && ( - - {props.data.measurements.qualitative.map((measurement) => ( - - {measurement.options.map((option) => ( - - ))} - - } - /> - ))} - {props.data.measurements.quantitative.map((measurement) => ( - - ))} - - )} - {activeView === SpeciesStandardsViewEnum.MARKING_BODY_LOCATIONS && ( - - {props.data.markingBodyLocations.map((location) => ( - - ))} - - )} + + {activeView === SpeciesStandardsViewEnum.MEASUREMENTS && ( + <> + {props.data.measurements.qualitative.map((measurement) => ( + + {measurement.options.map((option) => ( + + ))} + + } + /> + ))} + {props.data.measurements.quantitative.map((measurement) => ( + + ))} + + )} + {activeView === SpeciesStandardsViewEnum.MARKING_BODY_LOCATIONS && ( + <> + {props.data.markingBodyLocations.map((location) => ( + + ))} + + )} + ); }; diff --git a/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx b/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx index 32b88ef438..a75ca913d7 100644 --- a/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx +++ b/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx @@ -64,14 +64,14 @@ export const ConfigureEnvironmentColumns = (props: IConfigureEnvironmentColumnsP key={`qualitative_environment_item_${environment.environment_qualitative_id}`}> {environment.options.map((option) => ( @@ -101,9 +101,13 @@ export const ConfigureEnvironmentColumns = (props: IConfigureEnvironmentColumnsP key={`quantitative_environment_item_${environment.environment_quantitative_id}`}> } + ornament={ + environment.unit ? ( + + ) : undefined + } /> ))} @@ -78,11 +78,9 @@ export const ConfigureMeasurementColumns = (props: IConfigureMeasurementColumnsP ) : undefined } ornament={ - 'unit' in measurement ? ( - - ) : ( - <> - ) + 'unit' in measurement && measurement.unit ? ( + + ) : undefined } /> From 5f39c35fad579ff5ef3cbd7f7819359419cd4360 Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Tue, 6 Aug 2024 16:24:16 -0700 Subject: [PATCH 15/19] fix missing keys --- .../standards/view/environment/EnvironmentStandards.tsx | 6 +----- .../features/standards/view/methods/MethodStandards.tsx | 7 ++----- .../standards/view/species/SpeciesStandardsResults.tsx | 6 ++++-- .../components/environment/ConfigureEnvironmentColumns.tsx | 1 + 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/app/src/features/standards/view/environment/EnvironmentStandards.tsx b/app/src/features/standards/view/environment/EnvironmentStandards.tsx index 3e89b87a97..a19309e3d9 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandards.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandards.tsx @@ -5,7 +5,7 @@ import TextField from '@mui/material/TextField'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { debounce } from 'lodash-es'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { EnvironmentStandardsResults } from './EnvironmentStandardsResults'; /** @@ -16,8 +16,6 @@ import { EnvironmentStandardsResults } from './EnvironmentStandardsResults'; export const EnvironmentStandards = () => { const biohubApi = useBiohubApi(); - const [searchTerm, setSearchTerm] = useState(''); - const environmentsDataLoader = useDataLoader((keyword?: string) => biohubApi.standards.getEnvironmentStandards(keyword) ); @@ -40,11 +38,9 @@ export const EnvironmentStandards = () => { name="name" label="Environmental variable name" key="environments-name-search" - value={searchTerm} fullWidth onChange={(event) => { const value = event.currentTarget.value; - setSearchTerm(value); debouncedRefresh(value); }} /> diff --git a/app/src/features/standards/view/methods/MethodStandards.tsx b/app/src/features/standards/view/methods/MethodStandards.tsx index f29556538d..2d540e13a9 100644 --- a/app/src/features/standards/view/methods/MethodStandards.tsx +++ b/app/src/features/standards/view/methods/MethodStandards.tsx @@ -3,7 +3,7 @@ import Box from '@mui/material/Box'; import { useBiohubApi } from 'hooks/useBioHubApi'; import useDataLoader from 'hooks/useDataLoader'; import { debounce } from 'lodash-es'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { MethodStandardsResults } from './MethodStandardsResults'; /** @@ -12,10 +12,8 @@ import { MethodStandardsResults } from './MethodStandardsResults'; * * @returns */ - export const MethodStandards = () => { const biohubApi = useBiohubApi(); - const [searchTerm, setSearchTerm] = useState(''); const methodDataLoader = useDataLoader((keyword?: string) => biohubApi.standards.getMethodStandards(keyword)); @@ -24,6 +22,7 @@ export const MethodStandards = () => { debounce((value: string) => { methodDataLoader.refresh(value); }, 500), + // eslint-disable-next-line react-hooks/exhaustive-deps [] ); @@ -37,11 +36,9 @@ export const MethodStandards = () => { name="name" label="Method name" key="method-name-search" - value={searchTerm} fullWidth onChange={(event) => { const value = event.currentTarget.value; - setSearchTerm(value); debouncedRefresh(value); }} /> diff --git a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx index 520dfafeaf..92ca47e97b 100644 --- a/app/src/features/standards/view/species/SpeciesStandardsResults.tsx +++ b/app/src/features/standards/view/species/SpeciesStandardsResults.tsx @@ -53,6 +53,7 @@ const SpeciesStandardsResults = (props: ISpeciesStandardsResultsProps) => { <> {props.data.measurements.qualitative.map((measurement) => ( { {measurement.options.map((option) => ( { ))} {props.data.measurements.quantitative.map((measurement) => ( { {activeView === SpeciesStandardsViewEnum.MARKING_BODY_LOCATIONS && ( <> {props.data.markingBodyLocations.map((location) => ( - + ))} )} diff --git a/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx b/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx index a75ca913d7..0bda5089cb 100644 --- a/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx +++ b/app/src/features/surveys/observations/observations-table/configure-columns/components/environment/ConfigureEnvironmentColumns.tsx @@ -70,6 +70,7 @@ export const ConfigureEnvironmentColumns = (props: IConfigureEnvironmentColumnsP {environment.options.map((option) => ( Date: Tue, 6 Aug 2024 16:25:31 -0700 Subject: [PATCH 16/19] shade of grey --- .../view/environment/EnvironmentStandardsResults.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx index f7833c4ce2..d4d22d0e57 100644 --- a/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx +++ b/app/src/features/standards/view/environment/EnvironmentStandardsResults.tsx @@ -22,7 +22,7 @@ export const EnvironmentStandardsResults = (props: ISpeciesStandardsResultsProps key={environment.name} label={environment.name} subtitle={environment.description} - colour={grey[200]} + colour={grey[100]} /> ))} {data.qualitative.map((environment) => ( @@ -30,7 +30,7 @@ export const EnvironmentStandardsResults = (props: ISpeciesStandardsResultsProps key={environment.name} label={environment.name} subtitle={environment.description} - colour={grey[200]} + colour={grey[100]} /> ))} From cfafec3434a193760b3d5b42dbf8502bde0f8c60 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Wed, 7 Aug 2024 11:52:25 -0700 Subject: [PATCH 17/19] ignore-skip From 25a20baa5644eadcc29a6688228918ec4fff31a8 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Wed, 7 Aug 2024 11:54:42 -0700 Subject: [PATCH 18/19] fix: removed bad import from merge conflict --- .../features/summary/list-data/survey/SurveysListContainer.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/features/summary/list-data/survey/SurveysListContainer.tsx b/app/src/features/summary/list-data/survey/SurveysListContainer.tsx index 4f8e15f520..1c6672a964 100644 --- a/app/src/features/summary/list-data/survey/SurveysListContainer.tsx +++ b/app/src/features/summary/list-data/survey/SurveysListContainer.tsx @@ -22,9 +22,7 @@ import { SurveyBasicFieldsObject } from 'interfaces/useSurveyApi.interface'; import { useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { ApiPaginationRequestOptions, StringValues } from 'types/misc'; -import { firstOrNull, getCodesName } from 'utils/Utils'; import { firstOrNull } from 'utils/Utils'; -import { SurveyProgressChip } from '../../../surveys/components/SurveyProgressChip'; import SurveysListFilterForm, { ISurveyAdvancedFilters, SurveyAdvancedFiltersInitialValues From 253ef45ec7a2abcd074f8367f500f58d978a85d9 Mon Sep 17 00:00:00 2001 From: Macgregor Aubertin-Young Date: Wed, 7 Aug 2024 20:06:19 -0700 Subject: [PATCH 19/19] remove security from standards endpoints & address PR comments --- api/src/openapi/schemas/standards.ts | 23 ++++++++-- api/src/paths/standards/environment/index.ts | 2 +- api/src/paths/standards/methods/index.ts | 2 +- api/src/paths/standards/taxon/{tsn}/index.ts | 18 +------- api/src/repositories/standards-repository.ts | 11 ++++- api/src/services/standards-service.ts | 4 +- app/src/components/layout/Header.tsx | 16 +++---- .../standards/components/StandardsToolbar.tsx | 2 +- app/src/hooks/api/useStandardsApi.ts | 23 +++++----- .../interfaces/useStandardsApi.interface.ts | 42 +++++++++++-------- 10 files changed, 77 insertions(+), 66 deletions(-) diff --git a/api/src/openapi/schemas/standards.ts b/api/src/openapi/schemas/standards.ts index fdece6b26d..6457fcdba7 100644 --- a/api/src/openapi/schemas/standards.ts +++ b/api/src/openapi/schemas/standards.ts @@ -2,30 +2,38 @@ import { OpenAPIV3 } from 'openapi-types'; export const EnvironmentStandardsSchema: OpenAPIV3.SchemaObject = { type: 'object', + description: + 'Environment standards response object showing supported environmental variables and associated information', additionalProperties: false, properties: { qualitative: { type: 'array', + description: 'Array of qualitative environmental variables', items: { type: 'object', properties: { name: { - type: 'string' + type: 'string', + description: 'Name of the environmental variable' }, description: { type: 'string', + description: 'Description of the environmental variable', nullable: true }, options: { type: 'array', + description: 'Array of options for the qualitative variable', items: { type: 'object', properties: { name: { - type: 'string' + type: 'string', + description: 'Description of the environmental variable option' }, description: { type: 'string', + description: 'Description of the environmental variable option', nullable: true } } @@ -36,17 +44,24 @@ export const EnvironmentStandardsSchema: OpenAPIV3.SchemaObject = { }, quantitative: { type: 'array', + description: 'Array of quantitative environmental variables', items: { type: 'object', properties: { name: { - type: 'string' + type: 'string', + description: 'Name of the quantitative environmental variable' }, description: { type: 'string', + description: 'Description of the quantitative environmental variable', nullable: true }, - unit: { type: 'string', nullable: true } + unit: { + type: 'string', + description: 'Unit of measurement of the quantitative environmental variable', + nullable: true + } } } } diff --git a/api/src/paths/standards/environment/index.ts b/api/src/paths/standards/environment/index.ts index 7021c9eb37..42e2e24176 100644 --- a/api/src/paths/standards/environment/index.ts +++ b/api/src/paths/standards/environment/index.ts @@ -23,7 +23,7 @@ GET.apiDoc = { } } ], - security: [{ Bearer: [] }], + security: [], responses: { 200: { description: 'Environment data standards response object.', diff --git a/api/src/paths/standards/methods/index.ts b/api/src/paths/standards/methods/index.ts index b9ed53c364..f95e4d6bca 100644 --- a/api/src/paths/standards/methods/index.ts +++ b/api/src/paths/standards/methods/index.ts @@ -23,7 +23,7 @@ GET.apiDoc = { } } ], - security: [{ Bearer: [] }], + security: [], responses: { 200: { description: 'Method data standards response object.', diff --git a/api/src/paths/standards/taxon/{tsn}/index.ts b/api/src/paths/standards/taxon/{tsn}/index.ts index 7d86350a6d..483233ae06 100644 --- a/api/src/paths/standards/taxon/{tsn}/index.ts +++ b/api/src/paths/standards/taxon/{tsn}/index.ts @@ -1,26 +1,12 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../constants/roles'; import { getDBConnection } from '../../../../database/db'; -import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; import { StandardsService } from '../../../../services/standards-service'; import { getLogger } from '../../../../utils/logger'; const defaultLog = getLogger('paths/projects'); -export const GET: Operation = [ - authorizeRequestHandler(() => { - return { - and: [ - { - validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR], - discriminator: 'SystemRole' - } - ] - }; - }), - getSpeciesStandards() -]; +export const GET: Operation = [getSpeciesStandards()]; GET.apiDoc = { description: 'Gets lookup values for a tsn to describe what information can be uploaded for a given species.', @@ -35,7 +21,7 @@ GET.apiDoc = { required: true } ], - security: [{ Bearer: [] }], + security: [], responses: { 200: { description: 'Species data standards response object.', diff --git a/api/src/repositories/standards-repository.ts b/api/src/repositories/standards-repository.ts index 4fb7666169..6e7893df94 100644 --- a/api/src/repositories/standards-repository.ts +++ b/api/src/repositories/standards-repository.ts @@ -18,9 +18,9 @@ export class StandardsRepository extends BaseRepository { /** * Gets environment standards * - * @param {string} keyword + * @param {string} keyword - search term for filtering the response based on environmental variable name * @return {*} - * @memberof standardsRepository + * @memberof StandardsRepository */ async getEnvironmentStandards(keyword?: string): Promise { const sql = SQL` @@ -60,6 +60,13 @@ export class StandardsRepository extends BaseRepository { return response.rows[0]; } + /** + * Gets method standards + * + * @param {string} keyword - search term for filtering the response based on method lookup name + * @return {*} + * @memberof StandardsRepository + */ async getMethodStandards(keyword?: string): Promise { const sql = SQL` WITH diff --git a/api/src/services/standards-service.ts b/api/src/services/standards-service.ts index 24684c4608..eed1217efb 100644 --- a/api/src/services/standards-service.ts +++ b/api/src/services/standards-service.ts @@ -53,7 +53,7 @@ export class StandardsService extends DBService { /** * Gets environment standards * - * @param {string} keyword + * @param {string} keyword - search term for filtering the response based on environemntal variable name * @return {EnvironmentStandard[]} * @memberof standardsService */ @@ -66,7 +66,7 @@ export class StandardsService extends DBService { /** * Gets standards for method lookups * - * @param {string} keyword + * @param {string} keyword - search term for filtering the response based on method lookup name * @return {MethodStandards} * @memberof standardsService */ diff --git a/app/src/components/layout/Header.tsx b/app/src/components/layout/Header.tsx index 3366c386fa..0e52a07142 100644 --- a/app/src/components/layout/Header.tsx +++ b/app/src/components/layout/Header.tsx @@ -277,11 +277,9 @@ const Header: React.FC = () => { Funding Sources - - - Standards - - + + Standards + Support @@ -354,11 +352,9 @@ const Header: React.FC = () => { Funding Sources - - - Standards - - + + Standards +