diff --git a/src/App.jsx b/src/App.jsx index 6363339..a1db92e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -37,6 +37,7 @@ function App() { const [selectedKey, setSelectedKey] = useState(null); const [user, setUser] = useState(null); const [ontologyForPagination, setOntologyForPagination] = useState([]); + const [ucumCodes, setUcumCodes] = useState([]); message.config({ top: '25vh', @@ -79,6 +80,8 @@ function App() { setImportState, ontologyForPagination, setOntologyForPagination, + ucumCodes, + setUcumCodes, }} > diff --git a/src/components/Manager/FetchManager.jsx b/src/components/Manager/FetchManager.jsx index 374d1cd..ff6bdfc 100644 --- a/src/components/Manager/FetchManager.jsx +++ b/src/components/Manager/FetchManager.jsx @@ -4,6 +4,7 @@ import { ontologyReducer } from './Utilitiy'; export const getAll = (vocabUrl, name, navigate) => { return fetch(`${vocabUrl}/${name}`, { method: 'GET', + credentials: 'include', headers: { 'Content-Type': 'application/json', }, diff --git a/src/components/Manager/MappingsFunctions/MappingReset.jsx b/src/components/Manager/MappingsFunctions/MappingReset.jsx deleted file mode 100644 index f93ab9f..0000000 --- a/src/components/Manager/MappingsFunctions/MappingReset.jsx +++ /dev/null @@ -1,647 +0,0 @@ -import { Checkbox, Form, Input, notification, Tooltip } from 'antd'; -import { useContext, useEffect, useRef, useState } from 'react'; -import { myContext } from '../../../App'; -import { ellipsisString, ontologyReducer, systemsMatch } from '../Utilitiy'; -import { ModalSpinner } from '../Spinner'; -import { MappingContext } from '../../../Contexts/MappingContext'; -import { getFiltersByCode, olsFilterOntologiesSearch } from '../FetchManager'; -import { SearchContext } from '../../../Contexts/SearchContext'; -import { OntologyCheckboxes } from './OntologyCheckboxes'; -import { MappingRelationship } from './MappingRelationship'; - -export const MappingReset = ({ - searchProp, - mappingDesc, - setEditMappings, - form, - onClose, - mappingProp, - table, - terminology, -}) => { - const { searchUrl, vocabUrl } = useContext(myContext); - const { - apiPreferences, - defaultOntologies, - setFacetCounts, - setApiPreferencesCode, - apiPreferencesCode, - setUnformattedPref, - ontologyApis, - prefTerminologies, - } = useContext(SearchContext); - const [page, setPage] = useState(0); - const entriesPerPage = 1000; - const [loading, setLoading] = useState(true); - const [results, setResults] = useState([]); - const [totalCount, setTotalCount] = useState(); - const [resultsCount, setResultsCount] = useState(); // - const [lastCount, setLastCount] = useState(0); //save last count as count of the results before you fetch data again - const [filteredResultsCount, setFilteredResultsCount] = useState(0); - const [inputValue, setInputValue] = useState(searchProp); //Sets the value of the search bar - const [currentSearchProp, setCurrentSearchProp] = useState(searchProp); - const [terminologiesToMap, setTerminologiesToMap] = useState([]); - const [active, setActive] = useState(null); - const [allCheckboxes, setAllCheckboxes] = useState([]); - - const { - setSelectedMappings, - displaySelectedMappings, - setDisplaySelectedMappings, - selectedBoxes, - setSelectedBoxes, - } = useContext(MappingContext); - let ref = useRef(); - const { Search } = Input; - - const fetchTerminologies = () => { - setLoading(true); - const fetchPromises = prefTerminologies?.map(pref => - fetch(`${vocabUrl}/${pref?.reference}`).then(response => response.json()) - ); - - Promise.all(fetchPromises) - .then(results => { - // Once all fetch calls are resolved, set the combined data - setTerminologiesToMap(results); - }) - .catch(error => { - notification.error({ - message: 'Error', - description: 'An error occurred. Please try again.', - }); - }) - .finally(() => setLoading(false)); - }; - - // since the code is passed through searchProp, the '!!' forces it to be evaluated as a boolean. - // if there is a searchProp being passed, it evaluates to true and runs the search function. - // inputValue and currentSearchProp for the search bar is set to the passed searchProp. - // The function is run when the code changes. - useEffect(() => { - setInputValue(searchProp); - setCurrentSearchProp(searchProp); - setPage(0); - if (!!searchProp) { - getFiltersByCode( - vocabUrl, - mappingProp, - setApiPreferencesCode, - notification, - setUnformattedPref, - table, - terminology, - setLoading - ); - } - }, [searchProp]); - - useEffect(() => { - if (apiPreferencesCode !== undefined) { - fetchResults(0, searchProp); - } - }, [searchProp]); - - useEffect(() => { - if (apiPreferencesCode !== undefined) { - fetchResults(page, currentSearchProp); - } - }, [page]); - - useEffect(() => { - if (prefTerminologies.length > 0) { - fetchTerminologies(); - } - }, []); - - useEffect(() => { - setAllCheckboxes( - terminologiesToMap.find(term => term.id === active)?.codes ?? [] - ); - }, [active]); - - useEffect(() => { - setActive(terminologiesToMap?.[0]?.id); - }, [terminologiesToMap]); - - // The '!!' forces currentSearchProp to be evaluated as a boolean. - // If there is a currentSearchProp in the search bar, it evaluates to true and runs the search function. - // The function is run when the query changes and when the preferred ontology changes. - // If there are preferred terminologies, it runs when the OLS search bar is clicked (i.e. active) - useEffect(() => { - if ( - prefTerminologies.length > 0 && - active === 'search' && - !!currentSearchProp && - apiPreferencesCode !== undefined - ) { - fetchResults(page, currentSearchProp); - } else if ( - prefTerminologies.length === 0 && - !!currentSearchProp && - apiPreferencesCode !== undefined - ) { - fetchResults(page, currentSearchProp); - } - }, [currentSearchProp, apiPreferencesCode, active]); - - /* Pagination is handled via a "View More" link at the bottom of the page. - Each click on the "View More" link makes an API call to fetch the next 15 results. - This useEffect moves the scroll bar on the modal to the first index of the new batch of results. - Because the content is in a modal and not the window, the closest class name to the modal is used for the location of the ref. */ - useEffect(() => { - if (results?.length > 0 && page > 0 && ref.current) { - const container = ref.current.closest('.ant-modal-body'); - const scrollTop = ref.current.offsetTop - container.offsetTop; - container.scrollTop = scrollTop; - } - }, [results]); - - // Sets the value of the selected_mappings in the form to the checkboxes that are selected - useEffect(() => { - form.setFieldsValue({ - selected_mappings: selectedBoxes, - }); - }, [selectedBoxes, form]); - - // sets the code to null on dismount. - useEffect( - () => () => { - onClose(); - setEditMappings(null); - setSelectedMappings([]); - setDisplaySelectedMappings([]); - setSelectedBoxes([]); - }, - [] - ); - - // Sets currentSearchProp to the value of the search bar and sets page to 0. - const handleSearch = query => { - setCurrentSearchProp(query); - setPage(0); - }; - - // The function that makes the API call to search for the passed code. - const fetchResults = (page, query) => { - if (!!!query) { - return undefined; - } - setLoading(true); - - /* The OLS API returns 10 results by default unless specified otherwise. The fetch call includes a specified - number of results to return per page (entriesPerPage) and a calculation of the first index to start the results - on each new batch of results (pageStart, calculated as the number of the page * the number of entries per page */ - const pageStart = page * entriesPerPage; - if ( - //If there are api preferences and one of them is OLS, it gets the preferred ontologies - apiPreferences?.self?.api_preference && - 'ols' in apiPreferences?.self?.api_preference - ) { - const apiPreferenceOntologies = () => { - if (apiPreferences?.self?.api_preference?.ols) { - return apiPreferences.self.api_preference.ols.join(','); - } else { - // else if there are no preferred ontologies, it uses the default ontologies - return defaultOntologies; - } - }; - //fetch call to search OLS with either preferred or default ontologies - return olsFilterOntologiesSearch( - searchUrl, - query, - apiPreferencesCode?.length > 0 - ? apiPreferencesCode - : apiPreferenceOntologies(), - page, - entriesPerPage, - pageStart, - selectedBoxes, - setTotalCount, - setResults, - setFilteredResultsCount, - setResultsCount, - setLoading, - results, - setFacetCounts - ); - } else - return olsFilterOntologiesSearch( - searchUrl, - query, - apiPreferencesCode?.length > 0 ? apiPreferencesCode : defaultOntologies, - page, - entriesPerPage, - pageStart, - selectedBoxes, - setTotalCount, - setResults, - setFilteredResultsCount, - setResultsCount, - setLoading, - results, - setFacetCounts - ); - }; - - // the 'View More' pagination onClick increments the page. The search function is triggered to run on page change in the useEffect. - const handleViewMore = e => { - e.preventDefault(); - setPage(prevPage => prevPage + 1); - }; - - // The display for the checkboxes. The index is set to the count of the results before you fetch the new batch of results - // again + 1, to move the scrollbar to the first result of the new batch. - const checkBoxDisplay = (item, index) => { - return ( - <> -
-
-
-
-
{item.code}
-
-
{item.display}
-
- {item?.description?.length > 85 ? ( - - {ellipsisString(item?.description, '85')} - - ) : ( - ellipsisString(item?.description, '85') - )} -
-
-
-
- - ); - }; - - const handleChange = e => { - setInputValue(e.target.value); - }; - - // The display for the checkboxes. The index is set to the count of the results before you fetch the new batch of results - // again + 1, to move the scrollbar to the first result of the new batch. - const newSearchDisplay = (d, index) => { - index === lastCount + 1; - return ( - <> -
-
-
-
- {d?.label} -
-
- - {d?.obo_id} - -
-
- {d?.description?.length > 100 ? ( - - {ellipsisString(d?.description[0], '100')} - - ) : ( - ellipsisString(d?.description[0], '100') - )}{' '} -
-
- - ); - }; - - const selectedTermsDisplay = (d, index) => { - return ( - <> -
-
-
-
-
- {d?.display || d?.label} -
-
-
- {d?.code || ( - - {d?.obo_id} - - )} -
-
- -
-
-
- {d?.description?.length > 85 ? ( - - {ellipsisString( - Array.isArray(d.description) - ? d.description[0] - : d.description, - '85' - )} - - ) : ( - ellipsisString( - Array.isArray(d.description) - ? d.description[0] - : d.description, - '85' - ) - )} -
-
-
- - ); - }; - // If the checkbox is checked, it adds the object to the selectedBoxes array - // If it is unchecked, it filters it out of the selectedBoxes array. - const onCheckboxChange = (event, code) => { - if (event.target.checked) { - setSelectedBoxes(prevState => [...prevState, code]); - } else { - setSelectedBoxes(prevState => prevState.filter(val => val !== code)); - } - }; - - const onSelectedChange = checkedValues => { - const selected = JSON.parse(checkedValues?.[checkedValues.length - 1]); - - // Adds the selectedMappings to the selectedBoxes to ensure they are checked - setSelectedBoxes(prevState => { - const updated = [...prevState, selected]; - // Sets the values for the form to the selectedMappings checkboxes that are checked - form.setFieldsValue({ selected_mappings: updated }); - return updated; - }); - - setDisplaySelectedMappings(prevState => [...prevState, selected]); - }; - - // Creates a Set that excludes the mappings that have already been selected. - // Then filteres the existing mappings out of the results to only display results that have not yet been selected. - const getFilteredResults = () => { - const codesToExclude = new Set([ - ...displaySelectedMappings?.map(m => m?.code), - ]); - return results.filter(r => !codesToExclude?.has(r.obo_id)); - }; - - const filteredResultsArray = getFilteredResults(); - // Peforms search on Tab key press - const searchOnTab = e => { - if (e.key === 'Tab') { - e.preventDefault(); - handleSearch(e.target.value); - } - }; - return ( - <> -
- <> - {loading === false ? ( - <> -
-
-

{searchProp}

-
- {!prefTerminologies.length > 0 && ( - - )} -
- {mappingDesc} -
- {/* ant.design form displaying the checkboxes with the search results. */} -
-
-
-
- {terminologiesToMap?.length > 0 && ( -
-
-
- setActive(terminologiesToMap?.[0]?.id) - } - > - Preferred Terminologies -
-
- {terminologiesToMap.map((term, i) => ( -
setActive(term.id)} - > - {term.name} -
- ))} -
-
setActive('search')} - className={ - active === 'search' - ? 'active_term' - : 'inactive_term' - } - > - -
-
-
- )} - {((prefTerminologies.length > 0 && - active === 'search') || - prefTerminologies.length === 0) && ( - - )} -
-
- {displaySelectedMappings?.length > 0 && ( - -
- {displaySelectedMappings?.map((sm, i) => ( - onCheckboxChange(e, sm)} - checked={ - active === 'search' - ? selectedBoxes.some( - box => box.obo_id === sm.obo_id - ) - : selectedBoxes.some( - box => box.code === sm.code - ) - } - value={sm} - > - {selectedTermsDisplay(sm, i)} - - ))} -
-
- )} - {(prefTerminologies.length > 0 && - active === 'search') || - prefTerminologies.length === 0 ? ( - results?.length > 0 ? ( - <> - - {filteredResultsArray?.length > 0 && ( - { - return { - value: JSON.stringify({ - code: d.obo_id, - display: d.label, - description: d.description[0], - system: systemsMatch( - d?.obo_id?.split(':')[0], - ontologyApis - ), - }), - label: newSearchDisplay(d, index), - }; - } - )} - onChange={onSelectedChange} - /> - )} - - - ) : ( -

No results found

- ) - ) : ( - - - !displaySelectedMappings.some( - dsm => checkbox.code === dsm.code - ) - ) - .map((code, index) => ({ - value: JSON.stringify({ - code: code.code, - display: code.display, - description: code.description, - system: code.system, - }), - label: checkBoxDisplay(code, index), - }))} - onChange={onSelectedChange} - /> - - )} -
-
-
- {((prefTerminologies.length > 0 && active === 'search') || - prefTerminologies.length === 0) && ( -
- {/* 'View More' pagination displaying the number of results being displayed - out of the total number of results. Because of the filter to filter out the duplicates, - there is a tooltip informing the user that redundant entries have been removed to explain any - inconsistencies in results numbers per page. */} - - Displaying {resultsCount} -  of {totalCount} - - {resultsCount < totalCount - filteredResultsCount && ( - { - handleViewMore(e); - setLastCount(resultsCount); - }} - > - View More - - )} -
- )} -
-
- - ) : ( -
- -
- )} - -
- - ); -}; diff --git a/src/components/Projects/Studies/StudyList.jsx b/src/components/Projects/Studies/StudyList.jsx index ffd7fdf..4a76215 100644 --- a/src/components/Projects/Studies/StudyList.jsx +++ b/src/components/Projects/Studies/StudyList.jsx @@ -32,7 +32,7 @@ export const StudyList = () => { if (error) { notification.error({ message: 'Error', - description: 'An error occurred. Please try again.', + description: 'An error occurred loading studies.', }); } return error; diff --git a/src/components/Projects/Tables/AddVariable.jsx b/src/components/Projects/Tables/AddVariable.jsx index 8b22e62..54cf7db 100644 --- a/src/components/Projects/Tables/AddVariable.jsx +++ b/src/components/Projects/Tables/AddVariable.jsx @@ -89,6 +89,8 @@ export const AddVariable = ({ table, setTable }) => { }} maskClosable={false} closeIcon={false} + cancelButtonProps={{ disabled: loading }} + okButtonProps={{ disabled: loading }} > {loading ? ( diff --git a/src/components/Projects/Tables/DataTypeNumerical.jsx b/src/components/Projects/Tables/DataTypeNumerical.jsx index 4836348..88e6ec2 100644 --- a/src/components/Projects/Tables/DataTypeNumerical.jsx +++ b/src/components/Projects/Tables/DataTypeNumerical.jsx @@ -1,8 +1,20 @@ -import { Form, Input, InputNumber, Space } from 'antd'; +import { Form, Input, InputNumber, Select, Space } from 'antd'; +import { getById } from '../../Manager/FetchManager'; +import { useContext, useEffect, useState } from 'react'; +import { myContext } from '../../../App'; export const DataTypeNumerical = ({ form, type }) => { - // Validation function to ensure values are numbers and min is less than max + const { ucumCodes } = useContext(myContext); + + const options = ucumCodes.map((uc, i) => { + return { + key: i, + value: `ucum:${uc.code}`, + label: uc.display, + }; + }); + // Validation function to ensure values are numbers and min is less than max const validateMinMax = () => { const min = parseFloat(form.getFieldValue('min')); const max = parseFloat(form.getFieldValue('max')); @@ -75,11 +87,24 @@ export const DataTypeNumerical = ({ form, type }) => { /> - { + const labelMatch = (option?.label ?? '') + .toLowerCase() + .includes(input.toLowerCase()); + const valueMatch = (option?.value ?? '') + .toLowerCase() + .includes(input.toLowerCase()); + return labelMatch || valueMatch; }} - placeholder="Units" + options={options} /> diff --git a/src/components/Projects/Tables/EditDataTypeNumerical.jsx b/src/components/Projects/Tables/EditDataTypeNumerical.jsx index 1c88578..c619f6a 100644 --- a/src/components/Projects/Tables/EditDataTypeNumerical.jsx +++ b/src/components/Projects/Tables/EditDataTypeNumerical.jsx @@ -1,6 +1,18 @@ -import { Form, Input, InputNumber, Space } from 'antd'; +import { Form, InputNumber, Select, Space } from 'antd'; +import { useContext } from 'react'; +import { myContext } from '../../../App'; + +export const EditDataTypeNumerical = ({ type, form, tableData }) => { + const { ucumCodes } = useContext(myContext); + + const options = ucumCodes.map((uc, i) => { + return { + key: i, + value: `ucum:${uc.code}`, + label: uc.display, + }; + }); -export const EditDataTypeNumerical = ({ type, form }) => { // Validation function to ensure values are numbers and min is less than max const validateMinMax = () => { const min = parseFloat(form.getFieldValue('min')); @@ -82,11 +94,25 @@ export const EditDataTypeNumerical = ({ type, form }) => { /> - { + const labelMatch = (option?.label ?? '') + .toLowerCase() + .includes(input.toLowerCase()); + const valueMatch = (option?.value ?? '') + .toLowerCase() + .includes(input.toLowerCase()); + return labelMatch || valueMatch; }} - placeholder="Units" + options={options} /> diff --git a/src/components/Projects/Tables/EditDataTypeSubForm.jsx b/src/components/Projects/Tables/EditDataTypeSubForm.jsx index 3b61b49..d07b141 100644 --- a/src/components/Projects/Tables/EditDataTypeSubForm.jsx +++ b/src/components/Projects/Tables/EditDataTypeSubForm.jsx @@ -41,7 +41,7 @@ function EditDataTypeSubForm({ type, form, editRow, tableData }) { return ( <> {type === 'INTEGER' || type === 'QUANTITY' ? ( - + ) : ( type === 'ENUMERATION' && (!terminologyLoading ? ( diff --git a/src/components/Projects/Tables/TableDetails.jsx b/src/components/Projects/Tables/TableDetails.jsx index fdaf8ec..d6e57f2 100644 --- a/src/components/Projects/Tables/TableDetails.jsx +++ b/src/components/Projects/Tables/TableDetails.jsx @@ -36,8 +36,16 @@ import { ellipsisString, mappingTooltip } from '../../Manager/Utilitiy'; export const TableDetails = () => { const [form] = Form.useForm(); - const { vocabUrl, edit, setEdit, table, setTable, user } = - useContext(myContext); + const { + vocabUrl, + edit, + setEdit, + table, + setTable, + user, + ucumCodes, + setUcumCodes, + } = useContext(myContext); const { apiPreferences, setApiPreferences } = useContext(SearchContext); const { getMappings, @@ -75,7 +83,7 @@ export const TableDetails = () => { }, [table, mapping, pageSize]); const updateMappings = (mapArr, mappingCode) => { - // setLoading(true); + setLoading(true); const mappingsDTO = { mappings: mapArr, editor: user.email, @@ -117,6 +125,10 @@ export const TableDetails = () => { // fetches the table and sets 'table' to the response useEffect(() => { + tableApiCalls(); + }, []); + + const tableApiCalls = async () => { setLoading(true); getById(vocabUrl, 'Table', tableId) .then(data => { @@ -162,7 +174,8 @@ export const TableDetails = () => { }) .then(data => { setApiPreferences(data); - }); + }) + .finally(() => setLoading(false)); } else { setLoading(false); } @@ -172,13 +185,26 @@ export const TableDetails = () => { if (error) { notification.error({ message: 'Error', - description: 'An error occurred loading the ontology preferences.', + description: 'An error occurred loading table details.', }); } return error; }) .finally(() => setLoading(false)); - }, []); + }; + + useEffect(() => { + table && tableTypes(); + }, [table]); + + const tableTypes = () => { + const varTypes = table?.variables?.map(tv => tv.data_type); + if (varTypes?.includes('INTEGER' || 'QUANTITY')) { + getById(vocabUrl, 'Terminology', 'ucum-common').then(data => + setUcumCodes(data.codes) + ); + } + }; // sets table to an empty object on dismount useEffect( @@ -296,6 +322,11 @@ It then shows the mappings as table data and alows the user to delete a mapping updateMappings(variableMappings?.mappings, variableMappings?.code); }; + const findType = variable => { + const unit = variable?.units?.split(':')[1]; + const foundType = ucumCodes.filter(item => item.code === unit); + return foundType.length > 0 ? foundType[0].display : variable?.units; + }; // data for the table columns. Each table has an array of variables. Each variable has a name, description, and data type. // The integer and quantity data types include additional details. // The enumeration data type includes a reference to a terminology, which includes further codes with the capability to match the @@ -311,7 +342,7 @@ It then shows the mappings as table data and alows the user to delete a mapping data_type: variable.data_type, min: variable.min, max: variable.max, - units: variable.units, + units: findType(variable), enumeration: variable.data_type === 'ENUMERATION' && ( ); }; + +// const tableApiCalls = async () => { +// const tableData = await getById(vocabUrl, 'Table', tableId); + +// if (!tableData) { +// navigate('/404'); +// } else { +// setTable(tableData); + +// const [tableMappings, tableFilters, mappingRelationships] = +// await Promise.all([ +// getById(vocabUrl, 'Table', `${tableId}/mapping`), +// fetch(`${vocabUrl}/Table/${tableId}/filter/self`, { +// method: 'GET', +// headers: { +// 'Content-Type': 'application/json', +// }, +// }), +// getById(vocabUrl, 'Terminology', 'ftd-concept-map-relationship'), +// ]); + +// if (tableMappings) { +// setMapping(tableMappings.codes); +// } + +// if (tableFilters.ok) { +// setApiPreferences(tableFilters.json()); +// } else { +// throw new Error('An unknown error occurred.'); +// } +// if (mappingRelationships) { +// setRelationshipOptions(mappingRelationships.codes); +// } +// } +// setLoading(false); +// }; diff --git a/src/components/Projects/Terminologies/TerminologyList.jsx b/src/components/Projects/Terminologies/TerminologyList.jsx index 743d806..8b1309e 100644 --- a/src/components/Projects/Terminologies/TerminologyList.jsx +++ b/src/components/Projects/Terminologies/TerminologyList.jsx @@ -1,4 +1,4 @@ -import { Button, Input, Space, Table } from 'antd'; +import { Button, Input, notification, Space, Table } from 'antd'; import { SearchOutlined } from '@ant-design/icons'; import { useContext, useEffect, useRef, useState } from 'react'; import { myContext } from '../../../App'; @@ -33,6 +33,15 @@ export const TerminologyList = () => { .then(data => { setTerms(data); }) + .catch(error => { + if (error) { + notification.error({ + message: 'Error', + description: 'An error occurred loading terminologies.', + }); + } + return error; + }) .finally(() => setLoading(false)); localStorage.setItem('pageSize', pageSize); }, [pageSize]);