diff --git a/.env b/.env new file mode 100644 index 0000000..0815b66 --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +VITE_SEARCH_ENDPOINT = https://www.ebi.ac.uk/ols4/api/search? +VITE_MONARCH_SEARCH = https://api-v3.monarchinitiative.org/v3/api/search? +# VITE_VOCAB_ENDPOINT = http://127.0.0.1:5000/api # local +VITE_VOCAB_ENDPOINT = https://locutus-110109177269.us-central1.run.app/api # dev +# VITE_VOCAB_ENDPOINT = https://locutus-1066621297011.us-central1.run.app/api # uat +VITE_CLIENT_ID = 907694787484-36jr4oaf04mmniik6482batc87ejemm8.apps.googleusercontent.com diff --git a/.gitignore b/.gitignore index dd5161b..13a3147 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ dist-ssr .env.local .env.qa .env.dev +.env diff --git a/package-lock.json b/package-lock.json index e277d09..419d30d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vocab-management-tool", "version": "0.0.0", "dependencies": { + "@ant-design/icons": "^5.5.1", "@react-oauth/google": "^0.12.1", "antd": "^5.15.2", "jwt-decode": "^4.0.0", @@ -77,13 +78,13 @@ } }, "node_modules/@ant-design/icons": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.7.tgz", - "integrity": "sha512-bCPXTAg66f5bdccM4TT21SQBDO1Ek2gho9h3nO9DAKXJP4sq+5VBjrQMSxMVXSB3HyEz+cUbHQ5+6ogxCOpaew==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.5.1.tgz", + "integrity": "sha512-0UrM02MA2iDIgvLatWrj6YTCYe0F/cwXvVE0E2SqGrL7PZireQwgEKTKBisWpZyal5eXZLvuM98kju6YtYne8w==", "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.11.2", + "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, @@ -394,9 +395,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz", + "integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==", "dependencies": { "regenerator-runtime": "^0.14.0" }, diff --git a/package.json b/package.json index 5855550..38334e7 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "preview": "vite preview" }, "dependencies": { + "@ant-design/icons": "^5.5.1", "@react-oauth/google": "^0.12.1", "antd": "^5.15.2", "jwt-decode": "^4.0.0", diff --git a/src/App.jsx b/src/App.jsx index 64d3e56..4769f37 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,6 +7,7 @@ export const myContext = createContext(); function App() { const searchUrl = import.meta.env.VITE_SEARCH_ENDPOINT; + const monarchUrl = import.meta.env.VITE_MONARCH_SEARCH; const vocabUrl = import.meta.env.VITE_VOCAB_ENDPOINT; const clientId = import.meta.env.VITE_CLIENT_ID; @@ -40,13 +41,13 @@ function App() { top: '25vh', }); return ( - { <>
+
@@ -41,17 +43,31 @@ export const AppRouter = () => { } /> } /> } /> + } /> }> }> } /> + } /> } /> - - } /> + + } /> } + path="/Study/:studyId/DataDictionary/:DDId/Table/" + element={} /> + + } /> + } + /> + } + /> + } /> + { const [filter, setFilter] = useState(null); + const [pageSize, setPageSize] = useState( + parseInt(localStorage.getItem('pageSize'), 10) || 10); + const handleTableChange = (current, size) => { + setPageSize(size); + }; + useEffect(() => { + localStorage.setItem('pageSize', pageSize); + }, [pageSize]); + const ontologyTitle = () => { return (
@@ -85,6 +94,8 @@ export const OntologyTable = ({ ontology }) => { })) ); + + return ( { scroll={{ y: 470, }} + pagination={{ + showSizeChanger: true, + pageSizeOptions: ['10', '20', '30'], + pageSize: pageSize, // Use the stored pageSize + onChange: handleTableChange, // Capture pagination changes + }} /> ); }; diff --git a/src/components/Manager/FetchManager.jsx b/src/components/Manager/FetchManager.jsx index 1b5e44e..57c5b86 100644 --- a/src/components/Manager/FetchManager.jsx +++ b/src/components/Manager/FetchManager.jsx @@ -1,3 +1,5 @@ +import { ontologyReducer } from './Utilitiy'; + // Fetches all elements at an endpoint export const getAll = (vocabUrl, name, navigate) => { return fetch(`${vocabUrl}/${name}`, { @@ -165,3 +167,116 @@ export const getOntologies = vocabUrl => { } }); }; + +export const olsFilterOntologiesSearch = ( + searchUrl, + query, + ontologiesToSearch, + page, + entriesPerPage, + pageStart, + selectedBoxes, + setTotalCount, + setResults, + setFilteredResultsCount, + setResultsCount, + setLoading, + results, + setFacetCounts +) => { + setLoading(true); + return fetch( + `${searchUrl}q=${query}&ontology=${ontologiesToSearch}&rows=${entriesPerPage}&start=${pageStart}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ) + .then(res => res.json()) + .then(data => { + // filters results through the ontologyReducer function (defined in Manager/Utility.jsx) + + let res = ontologyReducer(data?.response?.docs); + // if the page > 0 (i.e. if this is not the first batch of results), the new results + // are concatenated to the old + if (selectedBoxes) { + res.results = res.results.filter( + d => !selectedBoxes.some(box => box.obo_id === d.obo_id) + ); + } + + if (page > 0 && results.length > 0) { + res.results = results.concat(res.results); + + // Apply filtering to remove results with obo_id in selectedBoxes + } else { + // Set the total number of search results for pagination + setTotalCount(data.response.numFound); + } + + //the results are set to res (the filtered, concatenated results) + + setResults(res.results); + setFilteredResultsCount(res?.filteredResults?.length); + // resultsCount is set to the length of the filtered, concatenated results for pagination + setResultsCount(res.results.length); + setFacetCounts(data?.facet_counts?.facet_fields?.ontologyPreferredPrefix); + }) + .finally(() => setLoading(false)); +}; + +export const getFiltersByCode = ( + vocabUrl, + component, + mappingProp, + setApiPreferencesCode, + notification, + apiPreferencesCode, + setUnformattedPref, + table +) => { + return fetch( + `${vocabUrl}/${ + component === table + ? component?.terminology?.reference + : `Terminology/${component?.id}` + }/filter/${mappingProp}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ) + .then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error('An unknown error occurred.'); + } + }) + .then(data => { + setUnformattedPref(data); + + // Dynamically derive the mappingProp based on a condition or the structure of the data + const codeToSearch = Object.keys(data)[0]; // Example: get the first key in the object + if (data?.[codeToSearch]?.api_preference?.ols) { + const joinedOntologies = + data[codeToSearch].api_preference.ols.join(','); + setApiPreferencesCode(joinedOntologies); // Set state to the comma-separated string + } else { + setApiPreferencesCode(''); // Fallback if no ols found + } + }) + .catch(error => { + if (error) { + notification.error({ + message: 'Error', + description: 'An error occurred loading the ontology preferences.', + }); + } + return error; + }); +}; diff --git a/src/components/Manager/MappingsFunctions/DefaultSearch.jsx b/src/components/Manager/MappingsFunctions/DefaultSearch.jsx new file mode 100644 index 0000000..ef91ae6 --- /dev/null +++ b/src/components/Manager/MappingsFunctions/DefaultSearch.jsx @@ -0,0 +1,52 @@ +import { ontologyReducer } from '../Utilitiy'; + +export const fetchResults = ( + page, + query, + entriesPerPage, + // setLoading, + setTotalCount, + setResults, + setFilteredResultsCount, + setResultsCount, + searchUrl, + selectedBoxes +) => { + if (!query) { + return undefined; + } + // setLoading(true); + const pageStart = page * entriesPerPage; + + return fetch( + `${searchUrl}q=${query}&ontology=mondo,hp,maxo,ncit&rows=${entriesPerPage}&start=${pageStart}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ) + .then(res => res.json()) + .then(data => { + let res = ontologyReducer(data?.response?.docs); + + // Filter based on selectedBoxes + if (selectedBoxes) { + res.results = res.results.filter( + d => !selectedBoxes.some(box => box.obo_id === d.obo_id) + ); + } + + if (page > 0 && res.results.length > 0) { + res.results = results.concat(res.results); + } else { + setTotalCount(data.response.numFound); + } + + setResults(res.results); + setFilteredResultsCount(res?.filteredResults?.length); + setResultsCount(res.results.length); + }); + // .finally(() => setLoading(false)); +}; diff --git a/src/components/Manager/MappingsFunctions/FilterAPI.jsx b/src/components/Manager/MappingsFunctions/FilterAPI.jsx index b3a0941..faee0ea 100644 --- a/src/components/Manager/MappingsFunctions/FilterAPI.jsx +++ b/src/components/Manager/MappingsFunctions/FilterAPI.jsx @@ -1,5 +1,5 @@ import { useContext, useEffect, useState } from 'react'; -import { Checkbox, Form, Input } from 'antd'; +import { Checkbox, Form } from 'antd'; import { ModalSpinner, OntologySpinner } from '../Spinner'; import { myContext } from '../../../App'; import { FilterOntology } from './FilterOntology'; @@ -16,20 +16,40 @@ export const FilterAPI = ({ active, setActive, searchText, - setSearchText, currentPage, setCurrentPage, pageSize, setPageSize, paginatedOntologies, + apiPreferences, + table, }) => { - const { Search } = Input; - const { vocabUrl } = useContext(myContext); const [ontology, setOntology] = useState([]); const [loading, setLoading] = useState(false); const [tableLoading, setTableLoading] = useState(false); + // The selected ontology filters that have already been selected + const existingFilters = Object.values(apiPreferences?.self || {}).flat(); + + // Flattens the existingFilters into a single array + const flattenedFilters = existingFilters + .flatMap(item => + Object.keys(item).map(key => + item[key].map(value => ({ + api: key, + })) + ) + ) + .flat(); + + // The initial value for the form. The checkboxes for the filters that have already been selected will be checked by default + const initialChecked = flattenedFilters?.map(ef => + JSON.stringify({ + ontology: ef, + }) + ); + // Fetches the active ontologyAPI each time the active API changes useEffect(() => { active && getOntologyApiById(); @@ -54,7 +74,20 @@ export const FilterAPI = ({ } }) .then(data => { - setOntology(data); + //Alphabetizes the data + const sortedData = data.map(api => { + const sortedOntologies = Object.fromEntries( + Object.entries(api.ontologies).sort((a, b) => + a[1].ontology_code > b[1].ontology_code ? 1 : -1 + ) + ); + + return { + ...api, + ontologies: sortedOntologies, + }; + }); + setOntology(sortedData); }) .finally(() => setTableLoading(false)) ); @@ -83,51 +116,54 @@ export const FilterAPI = ({ ); }; - return loading ? ( ) : (
-
-
-
APIs
+ +
+
+
APIs
- - { - return { - value: JSON.stringify({ api_preference: api?.api_id }), - label: checkboxDisplay(api, index), - }; - })} - /> - -
-
- {tableLoading ? ( -
- -
- ) : ( - - )} + + { + return { + value: JSON.stringify({ api_preference: api?.api_id }), + label: checkboxDisplay(api, index), + }; + })} + /> + +
+
+ {tableLoading ? ( +
+ +
+ ) : ( + + )} +
diff --git a/src/components/Manager/MappingsFunctions/FilterOntology.jsx b/src/components/Manager/MappingsFunctions/FilterOntology.jsx index 327f1cd..d62a5a3 100644 --- a/src/components/Manager/MappingsFunctions/FilterOntology.jsx +++ b/src/components/Manager/MappingsFunctions/FilterOntology.jsx @@ -1,6 +1,7 @@ import { Checkbox, Form, Pagination } from 'antd'; import { useContext, useEffect, useState } from 'react'; import { myContext } from '../../../App'; +import { FilterReset } from './FilterReset'; export const FilterOntology = ({ ontology, @@ -12,10 +13,11 @@ export const FilterOntology = ({ setDisplaySelectedOntologies, searchText, paginatedOntologies, + apiPreferences, + table, }) => { const [allCheckboxes, setAllCheckboxes] = useState([]); - const { ontologyForPagination, setOntologyForPagination } = - useContext(myContext); + const { setOntologyForPagination } = useContext(myContext); useEffect(() => { setOntologyForPagination(ontology); @@ -110,28 +112,102 @@ export const FilterOntology = ({ ); }; + const existingDisplay = (ont, i) => { + return ( + <> +
+
+
+
{ont.ontology.toUpperCase()}
+
+
+
+ + ); + }; + + const existingFilters = Object.values(apiPreferences?.self || {}).flat(); + + const flattenedFilters = existingFilters + .flatMap(item => + Object.keys(item).map(key => + item[key].map(value => ({ + api: key, + ontology: value, + })) + ) + ) + .flat(); + + const initialChecked = flattenedFilters?.map(ef => + JSON.stringify({ + ontology: ef, + }) + ); return ( <>
- {displaySelectedOntologies.length > 0 && ( - -
- {displaySelectedOntologies?.map((selected, i) => ( - box?.ontology_code === selected?.ontology_code - )} - value={selected} - onChange={e => onCheckboxChange(e, selected)} - > - {selectedOntDisplay(selected, i)} - - ))} + {Object.keys(apiPreferences?.self?.api_preference || {}).some( + key => apiPreferences?.self?.api_preference[key]?.length > 0 + ) && ( + <> +
+

Ontology Filters

- + + {flattenedFilters?.length > 0 ? ( + { + return { + value: JSON.stringify({ + ontology: po, + }), + label: existingDisplay(po, index), + }; + })} + /> + ) : ( + '' + )} + + + )} + {displaySelectedOntologies.length > 0 && ( + <> +

Selected

+ +
+ {displaySelectedOntologies?.map((selected, i) => ( + box?.ontology_code === selected?.ontology_code + )} + value={selected} + onChange={e => onCheckboxChange(e, selected)} + > + {selectedOntDisplay(selected, i)} + + ))} +
+
+ )} + {(Object.keys(apiPreferences?.self?.api_preference || {}).some( + key => apiPreferences?.self?.api_preference[key]?.length > 0 + ) || + displaySelectedOntologies.length > 0) &&

Ontologies

} !displaySelectedOntologies.some( dsm => checkbox.ontology_code === dsm.ontology_code + ) && + !flattenedFilters.some( + ef => checkbox.ontology_code === ef.ontology ) ) .map((ont, i) => ({ diff --git a/src/components/Manager/MappingsFunctions/FilterReset.jsx b/src/components/Manager/MappingsFunctions/FilterReset.jsx new file mode 100644 index 0000000..d1229fe --- /dev/null +++ b/src/components/Manager/MappingsFunctions/FilterReset.jsx @@ -0,0 +1,62 @@ +import { Button, message, Modal, notification } from 'antd'; +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { useContext, useState } from 'react'; +import { myContext } from '../../../App'; +import { SearchContext } from '../../../Contexts/SearchContext'; + +export const FilterReset = ({ table }) => { + const { confirm } = Modal; + + const { user, vocabUrl } = useContext(myContext); + const { setApiPreferences } = useContext(SearchContext); + const [remove, setRemove] = useState(false); + + const deleteOntologies = evt => { + return fetch(`${vocabUrl}/${table?.terminology?.reference}/filter/self`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ editor: user.email }), + }) + .then(res => { + if (res.ok) { + return res.json().then(data => { + message.success('Ontology filters deleted successfully.'); + }); + } else { + notification.error({ + message: 'Error', + description: 'An error occurred deleting the table.', + }); + } + }) + .then(data => setApiPreferences(data)); + }; + + const showConfirm = () => { + confirm({ + className: 'delete_table_confirm', + title: 'Alert', + icon: , + content: + ' Are you sure you want to remove the ontology filters?', + onOk() { + deleteOntologies(); + setRemove(false); + }, + onCancel() { + setRemove(false); + }, + }); + }; + + return ( + <> + + {remove && showConfirm()} + + ); +}; diff --git a/src/components/Manager/MappingsFunctions/FilterSelect.jsx b/src/components/Manager/MappingsFunctions/FilterSelect.jsx index 7063a65..031396b 100644 --- a/src/components/Manager/MappingsFunctions/FilterSelect.jsx +++ b/src/components/Manager/MappingsFunctions/FilterSelect.jsx @@ -4,6 +4,8 @@ import { useContext, useEffect, useState } from 'react'; import { myContext } from '../../../App'; import { FilterAPI } from './FilterAPI'; import { getOntologies } from '../FetchManager'; +import { ModalSpinner } from '../Spinner'; +import { SearchContext } from '../../../Contexts/SearchContext'; export const FilterSelect = ({ table, apiPreferences, setApiPreferences }) => { const [form] = Form.useForm(); @@ -18,7 +20,7 @@ export const FilterSelect = ({ table, apiPreferences, setApiPreferences }) => { const [active, setActive] = useState(null); const [loading, setLoading] = useState(false); const { user, vocabUrl, ontologyForPagination } = useContext(myContext); - const [ontologyApis, setOntologyApis] = useState([]); + const { ontologyApis, setOntologyApis } = useContext(SearchContext); const [searchText, setSearchText] = useState(''); // Gets the ontologyAPIs on first load, automatically sets active to the first of the list to display on the page @@ -63,15 +65,15 @@ export const FilterSelect = ({ table, apiPreferences, setApiPreferences }) => { // If the api doesn't exist in api_preference, creates an empty array for it // If the api_preference array for the api does not include an ontology_code, pushes the code to the array for the api // If there is an api in api_preferences that is not included with the ontology_code, it's added to apiPreference with an empty array - const handleSubmit = values => { - // setLoading(true); + setLoading(true); const apiPreference = { api_preference: {}, }; if (values?.ontologies?.length > 0) { - values?.ontologies.forEach(({ ontology_code, api }) => { + // If there are ontologies, populate apiPreference with them + values.ontologies.forEach(({ ontology_code, api }) => { if (!apiPreference.api_preference[api]) { apiPreference.api_preference[api] = []; } @@ -80,28 +82,45 @@ export const FilterSelect = ({ table, apiPreferences, setApiPreferences }) => { } }); } else { - values?.selected_apis.forEach(item => { + // If no ontologies are provided, initialize api preferences from selected_apis + values?.selected_apis?.forEach(item => { const apiObj = JSON.parse(item); const apiName = apiObj.api_preference; apiPreference.api_preference[apiName] = []; // Create an empty array for each api_preference }); } - values?.selected_apis?.forEach(item => { - const apiObj = JSON.parse(item); - const apiName = apiObj.api_preference; - if (!apiPreference.api_preference[apiName]) { - apiPreference.api_preference[apiName] = []; - } - }); + // Now handle the existing_filters + if (values?.existing_filters?.length > 0) { + values.existing_filters.forEach(item => { + const apiObj = JSON.parse(item); // Parse the JSON string + const apiName = apiObj.ontology.api; // Extract the API + const ontologyCode = apiObj.ontology.ontology; // Extract the ontology code + + // Ensure the apiName exists in apiPreference.api_preference + if (!apiPreference.api_preference[apiName]) { + apiPreference.api_preference[apiName] = []; // Initialize if not already present + } + + // Add the ontologyCode to the corresponding api, if it's not already included + if (!apiPreference.api_preference[apiName].includes(ontologyCode)) { + apiPreference.api_preference[apiName].push(ontologyCode); + } + }); + } const apiPreferenceDTO = { api_preference: apiPreference?.api_preference, editor: user.email, }; + const method = + Object.keys(apiPreferences?.self?.api_preference || {}).length === 0 + ? 'POST' + : 'PUT'; + fetch(`${vocabUrl}/${table?.terminology?.reference}/filter`, { - method: 'POST', + method: method, headers: { 'Content-Type': 'application/json', }, @@ -115,7 +134,7 @@ export const FilterSelect = ({ table, apiPreferences, setApiPreferences }) => { } }) .then(() => - fetch(`${vocabUrl}/${table?.terminology?.reference}/filter`, { + fetch(`${vocabUrl}/${table?.terminology?.reference}/filter/self`, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -197,70 +216,78 @@ export const FilterSelect = ({ table, apiPreferences, setApiPreferences }) => { > API Filters {apiPrefObject ? `(${apiPrefLength})` : ''} - { - form.validateFields().then(values => { - handleSubmit(values); + {addFilter && ( + { + form.validateFields().then(values => { + handleSubmit(values); + onClose(); + }); + }} + onCancel={() => { + form.resetFields(); + setAddFilter(false); onClose(); - }); - }} - onCancel={() => { - form.resetFields(); - setAddFilter(false); - onClose(); - }} - maskClosable={false} - closeIcon={false} - footer={(_, { OkBtn, CancelBtn }) => ( - <> -
+ }} + maskClosable={false} + closeIcon={false} + footer={(_, { OkBtn, CancelBtn }) => ( + <>
- +
+ +
+
+ + +
-
- - -
-
- - )} - > - -
+ + )} + > + {loading ? ( + + ) : ( + + )} +
+ )} ); }; diff --git a/src/components/Manager/MappingsFunctions/GetMappingsModal.jsx b/src/components/Manager/MappingsFunctions/GetMappingsModal.jsx index 3c56feb..32fc530 100644 --- a/src/components/Manager/MappingsFunctions/GetMappingsModal.jsx +++ b/src/components/Manager/MappingsFunctions/GetMappingsModal.jsx @@ -1,9 +1,21 @@ -import { Checkbox, Input, message, Modal, Form, Tooltip } from 'antd'; +import { + Checkbox, + Form, + Input, + message, + Modal, + notification, + Tooltip, +} from 'antd'; import { useContext, useEffect, useRef, useState } from 'react'; import { myContext } from '../../../App'; -import { ellipsisString, ontologyReducer, systemsMatch } from '../Utilitiy'; +import { ellipsisString, systemsMatch } from '../Utilitiy'; import { ModalSpinner } from '../Spinner'; import { MappingContext } from '../../../Contexts/MappingContext'; +import { SearchContext } from '../../../Contexts/SearchContext'; +import { getFiltersByCode, olsFilterOntologiesSearch } from '../FetchManager'; +import { OntologyCheckboxes } from './OntologyCheckboxes'; +import { OntologyFilterCodeSubmit } from './OntologyFilterCodeSubmit'; export const GetMappingsModal = ({ componentString, @@ -12,14 +24,23 @@ export const GetMappingsModal = ({ searchProp, component, mappingProp, + mappingDesc, + table }) => { const [form] = Form.useForm(); const { Search } = Input; - const { searchUrl, vocabUrl, setSelectedKey, user } = useContext(myContext); + const { + apiPreferences, + defaultOntologies, + setFacetCounts, + setApiPreferencesCode, + apiPreferencesCode, + setUnformattedPref, + } = useContext(SearchContext); const [page, setPage] = useState(0); - const entriesPerPage = 15; - const [loading, setLoading] = useState(true); + const entriesPerPage = 2500; + const [loading, setLoading] = useState(false); const [results, setResults] = useState([]); const [totalCount, setTotalCount] = useState(); const [resultsCount, setResultsCount] = useState(); @@ -27,7 +48,6 @@ export const GetMappingsModal = ({ const [filteredResultsCount, setFilteredResultsCount] = useState(0); const [inputValue, setInputValue] = useState(searchProp); //Sets the value of the search bar const [currentSearchProp, setCurrentSearchProp] = useState(searchProp); - const { setSelectedMappings, displaySelectedMappings, @@ -46,13 +66,23 @@ export const GetMappingsModal = ({ setCurrentSearchProp(searchProp); setPage(0); if (!!searchProp) { - fetchResults(0, searchProp); + getFiltersByCode( + vocabUrl, + component, + mappingProp, + setApiPreferencesCode, + notification, + apiPreferencesCode, + setUnformattedPref, + table + ); } }, [searchProp]); - // 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 code and when the page changes. + useEffect(() => { + if (apiPreferencesCode !== undefined) fetchResults(0, searchProp); + }, [apiPreferencesCode, searchProp]); + useEffect(() => { if (!!currentSearchProp) { fetchResults(page, currentSearchProp); @@ -64,7 +94,7 @@ export const GetMappingsModal = ({ 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 && page > 0) { + if (results?.length > 0 && page > 0) { const container = ref.current.closest('.ant-modal-body'); const scrollTop = ref.current.offsetTop - container.offsetTop; container.scrollTop = scrollTop; @@ -92,9 +122,9 @@ export const GetMappingsModal = ({ const onClose = () => { setPage(0); setResults([]); - setLoading(true); setSelectedMappings([]); setDisplaySelectedMappings([]); + setApiPreferencesCode(undefined); setSelectedBoxes([]); setSelectedKey(null); }; @@ -104,11 +134,12 @@ export const GetMappingsModal = ({ setCurrentSearchProp(query); setPage(0); }; + // Function to send a PUT call to update the mappings. // Each mapping in the mappings array being edited is JSON.parsed and pushed to the blank mappings array. // The mappings are turned into objects in the mappings array. const handleSubmit = values => { - const selectedMappings = values?.selected_mappings?.map(item => ({ + const selectedMappings = selectedBoxes?.map(item => ({ code: item.obo_id, display: item.label, description: item.description[0], @@ -118,7 +149,7 @@ export const GetMappingsModal = ({ mappings: selectedMappings, editor: user.email, }; - + setLoading(true); fetch( `${vocabUrl}/${componentString}/${component.id}/mapping/${mappingProp}`, { @@ -141,59 +172,79 @@ export const GetMappingsModal = ({ form.resetFields(); setGetMappings(null); message.success('Changes saved successfully.'); - }); + }) + .then(() => + OntologyFilterCodeSubmit( + apiPreferencesCode, + setApiPreferencesCode, + apiPreferences, + mappingProp, + table, + vocabUrl, + component + ) + ) + .finally(() => setLoading(false)); }; - // 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; - return fetch( - `${searchUrl}q=${query}&ontology=mondo,hp,maxo,ncit&rows=${entriesPerPage}&start=${pageStart}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - } - ) - .then(res => res.json()) - .then(data => { - // filters results through the ontologyReducer function (defined in Manager/Utility.jsx) - - let res = ontologyReducer(data?.response?.docs); - // if the page > 0 (i.e. if this is not the first batch of results), the new results - // are concatenated to the old - if (selectedBoxes) { - res.results = res.results.filter( - d => !selectedBoxes.some(box => box.obo_id === d.obo_id) - ); - } - - if (page > 0 && results.length > 0) { - res.results = results.concat(res.results); - // Apply filtering to remove results with obo_id in selectedBoxes + 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 { - // Set the total number of search results for pagination - setTotalCount(data.response.numFound); + // else if there are no preferred ontologies, it uses the default ontologies + return defaultOntologies; } - - //the results are set to res (the filtered, concatenated results) - - setResults(res.results); - setFilteredResultsCount(res?.filteredResults?.length); - // resultsCount is set to the length of the filtered, concatenated results for pagination - setResultsCount(res.results.length); - }) - .then(() => setLoading(false)); + }; + //fetch call to search OLS with either preferred or default ontologies + return olsFilterOntologiesSearch( + searchUrl, + query, + apiPreferencesCode !== '' + ? apiPreferencesCode + : apiPreferenceOntologies(), + page, + entriesPerPage, + pageStart, + selectedBoxes, + setTotalCount, + setResults, + setFilteredResultsCount, + setResultsCount, + setLoading, + results, + setFacetCounts + ); + } else + return olsFilterOntologiesSearch( + searchUrl, + query, + apiPreferencesCode !== '' ? 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. @@ -269,7 +320,6 @@ export const GetMappingsModal = ({ setSelectedBoxes(prevState => prevState.filter(val => val !== code)); } }; - const onSelectedChange = checkedValues => { const selected = JSON.parse(checkedValues?.[0]); const selectedMapping = results.find( @@ -306,12 +356,13 @@ export const GetMappingsModal = ({ }; const filteredResultsArray = getFilteredResults(); + return ( <> { @@ -330,25 +381,29 @@ export const GetMappingsModal = ({ cancelButtonProps={{ disabled: loading }} okButtonProps={{ disabled: loading }} > -
- <> - {loading === false ? ( - <> -
-
-

{searchProp}

-
- -
+ {loading ? ( + + ) : ( +
+ <> +
+
+

{searchProp}

+
+
- {/* ant.design form displaying the checkboxes with the search results. */} - {results?.length > 0 ? ( -
-
+ {mappingDesc} +
+ {/* ant.design form displaying the checkboxes with the search results. */} +
+ +
+ +
{displaySelectedMappings?.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] - ), - }), - label: checkBoxDisplay(d, index), - }; - })} - onChange={onSelectedChange} - /> - ) : ( - '' - )} - - -
- {/* '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} - - {totalCount - filteredResultsCount !== resultsCount && ( - { - handleViewMore(e); - setLastCount(resultsCount); - }} - > - View More - + {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] + ), + }), + label: checkBoxDisplay(d, index), + }; + } + )} + onChange={onSelectedChange} + /> + ) : ( + '' + )} + {' '} + + ) : ( +

No results found

)}
- ) : ( -

No results found.

- )} + +
+ {/* '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} + + {totalCount - filteredResultsCount !== resultsCount && ( + { + handleViewMore(e); + setLastCount(resultsCount); + }} + > + View More + + )} +
- - ) : ( -
-
- )} - -
+ +
+ )} ); diff --git a/src/components/Manager/MappingsFunctions/MappingReset.jsx b/src/components/Manager/MappingsFunctions/MappingReset.jsx index dc9c75b..10d91fd 100644 --- a/src/components/Manager/MappingsFunctions/MappingReset.jsx +++ b/src/components/Manager/MappingsFunctions/MappingReset.jsx @@ -1,17 +1,32 @@ -import { Checkbox, Form, Input, Tooltip } from 'antd'; +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'; export const MappingReset = ({ searchProp, + mappingDesc, setEditMappings, form, onClose, + component, + mappingProp, + table, }) => { - const { searchUrl } = useContext(myContext); + const { searchUrl, vocabUrl } = useContext(myContext); + const { + apiPreferences, + defaultOntologies, + setFacetCounts, + setApiPreferencesCode, + apiPreferencesCode, + setUnformattedPref, + } = useContext(SearchContext); const [page, setPage] = useState(0); const entriesPerPage = 15; const [loading, setLoading] = useState(true); @@ -42,10 +57,23 @@ export const MappingReset = ({ setCurrentSearchProp(searchProp); setPage(0); if (!!searchProp) { - fetchResults(0, searchProp); + getFiltersByCode( + vocabUrl, + component, + mappingProp, + setApiPreferencesCode, + notification, + apiPreferencesCode, + setUnformattedPref, + table + ); } }, [searchProp]); + useEffect(() => { + if (apiPreferencesCode !== undefined) fetchResults(0, searchProp); + }, [apiPreferencesCode, searchProp]); + // 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 code and when the page changes. @@ -60,7 +88,7 @@ export const MappingReset = ({ 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 && page > 0) { + if (results?.length > 0 && page > 0) { const container = ref.current.closest('.ant-modal-body'); const scrollTop = ref.current.offsetTop - container.offsetTop; container.scrollTop = scrollTop; @@ -103,52 +131,55 @@ export const MappingReset = ({ 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; - return fetch( - `${searchUrl}q=${query}&ontology=mondo,hp,maxo,ncit&rows=${entriesPerPage}&start=${pageStart}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - } - ) - .then(res => res.json()) - .then(data => { - // filters results through the ontologyReducer function (defined in Manager/Utility.jsx) - let res = ontologyReducer(data?.response?.docs); - - // Filters out results that have already been selected in previous search if there is a change to the search term - if (selectedBoxes) { - res.results = res.results.filter( - d => !selectedBoxes.some(box => box.obo_id === d.obo_id) - ); - } - - // if the page > 0 (i.e. if this is not the first batch of results), the new results - // are concatenated to the old - if (page > 0 && results.length > 0) { - res.results = results.concat(res.results); + 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 { - // the total number of search results are set to totalCount for pagination - setTotalCount(data.response.numFound); - } - //the results are set to res (the filtered, concatenated results) - setResults(res.results); - setFilteredResultsCount(res?.filteredResults?.length); - - // resultsCount is set to the length of the filtered, concatenated results for pagination - setResultsCount(res.results.length); - }) - .catch(error => { - if (error) { - notification.error({ - message: 'Error', - description: 'An error occurred. Please try again.', - }); + // else if there are no preferred ontologies, it uses the default ontologies + return defaultOntologies; } - return error; - }) - .finally(() => setLoading(false)); + }; + //fetch call to search OLS with either preferred or default ontologies + return olsFilterOntologiesSearch( + searchUrl, + query, + apiPreferencesCode !== '' + ? apiPreferencesCode + : apiPreferenceOntologies(), + page, + entriesPerPage, + pageStart, + selectedBoxes, + setTotalCount, + setResults, + setFilteredResultsCount, + setResultsCount, + setLoading, + results, + setFacetCounts + ); + } else + return olsFilterOntologiesSearch( + searchUrl, + query, + apiPreferencesCode !== '' ? 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. @@ -285,18 +316,21 @@ export const MappingReset = ({ onChange={handleChange} />
+ {mappingDesc}
- {/* ant.design form displaying the checkboxes with the search results. */} - {results?.length > 0 ? ( -
-
+
+ {/* ant.design form displaying the checkboxes with the search results. */} +
+ +
+ +
{displaySelectedMappings?.length > 0 && ( - {' '}
{displaySelectedMappings?.map((sm, i) => ( + )}{' '} + {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] + ), + }), + label: checkBoxDisplay(d, index), + }; + } + )} + onChange={onSelectedChange} + /> + ) : ( + '' + )} + + + ) : ( +

No results found

)} - - {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]), - }), - label: checkBoxDisplay(d, index), - }; - })} - onChange={onSelectedChange} - /> - ) : ( - '' - )} - - -
- {/* 'View More' pagination displaying the number of results being displayed +
+
+ +
+ {/* '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} - - {totalCount - filteredResultsCount !== resultsCount && ( - { - handleViewMore(e); - setLastCount(resultsCount); - }} - > - View More - - )} -
-
- ) : ( -

No results found.

- )} + + Displaying {resultsCount} +  of {totalCount} + + {totalCount - filteredResultsCount !== resultsCount && ( + { + handleViewMore(e); + setLastCount(resultsCount); + }} + > + View More + + )} +
) : ( -
- -
+
+ +
)} - -
+ +
); }; + \ No newline at end of file diff --git a/src/components/Manager/MappingsFunctions/MappingSearch.jsx b/src/components/Manager/MappingsFunctions/MappingSearch.jsx index 80585e8..88d8db4 100644 --- a/src/components/Manager/MappingsFunctions/MappingSearch.jsx +++ b/src/components/Manager/MappingsFunctions/MappingSearch.jsx @@ -4,6 +4,9 @@ import { myContext } from '../../../App'; import { ellipsisString, ontologyReducer, systemsMatch } from '../Utilitiy'; import { ModalSpinner } from '../Spinner'; import { MappingContext } from '../../../Contexts/MappingContext'; +import { SearchContext } from '../../../Contexts/SearchContext'; +import { getFiltersByCode, olsFilterOntologiesSearch } from '../FetchManager'; +import { OntologyCheckboxes } from './OntologyCheckboxes'; export const MappingSearch = ({ setEditMappings, @@ -11,10 +14,23 @@ export const MappingSearch = ({ mappingsForSearch, onClose, searchProp, + mappingDesc, + component, + mappingProp, + table, }) => { - const { searchUrl } = useContext(myContext); + const { searchUrl, vocabUrl } = useContext(myContext); + const { + apiPreferences, + defaultOntologies, + setFacetCounts, + setApiPreferencesCode, + apiPreferencesCode, + setUnformattedPref, + } = useContext(SearchContext); + const [page, setPage] = useState(0); - const entriesPerPage = 15; + const entriesPerPage = 2500; const [loading, setLoading] = useState(true); const [results, setResults] = useState([]); const [totalCount, setTotalCount] = useState(); @@ -45,10 +61,23 @@ export const MappingSearch = ({ setCurrentSearchProp(searchProp); setPage(0); if (!!searchProp) { - fetchResults(0, searchProp); + getFiltersByCode( + vocabUrl, + component, + mappingProp, + setApiPreferencesCode, + notification, + apiPreferencesCode, + setUnformattedPref, + table + ); } }, [searchProp]); + useEffect(() => { + if (apiPreferencesCode !== undefined) fetchResults(0, searchProp); + }, [apiPreferencesCode, searchProp]); + // 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 code and when the page changes. @@ -63,13 +92,12 @@ export const MappingSearch = ({ 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 && page > 0) { + if (results?.length > 0 && page > 0) { 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({ @@ -105,51 +133,55 @@ export const MappingSearch = ({ 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; - return fetch( - `${searchUrl}q=${query}&ontology=mondo,hp,maxo,ncit&rows=${entriesPerPage}&start=${pageStart}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - } - ) - .then(res => res.json()) - .then(data => { - // filters results through the ontologyReducer function (defined in Manager/Utility.jsx) - let res = ontologyReducer(data?.response?.docs); - - // Filters out results that have already been selected in previous search if there is a change to the search term - if (selectedBoxes) { - res.results = res.results.filter( - d => !selectedBoxes.some(box => box.obo_id === d.obo_id) - ); - } - // if the page > 0 (i.e. if this is not the first batch of results), the new results - // are concatenated to the old - if (page > 0 && results.length > 0) { - res.results = results.concat(res.results); + 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 { - // the total number of search results are set to totalCount for pagination - setTotalCount(data.response.numFound); - } - //the results are set to res (the filtered, concatenated results) - setResults(res.results); - setFilteredResultsCount(res?.filteredResults?.length); - - // resultsCount is set to the length of the filtered, concatenated results for pagination - setResultsCount(res.results.length); - }) - .catch(error => { - if (error) { - notification.error({ - message: 'Error', - description: 'An error occurred. Please try again.', - }); + // else if there are no preferred ontologies, it uses the default ontologies + return defaultOntologies; } - return error; - }) - .finally(() => setLoading(false)); + }; + //fetch call to search OLS with either preferred or default ontologies + return olsFilterOntologiesSearch( + searchUrl, + query, + apiPreferencesCode !== '' + ? apiPreferencesCode + : apiPreferenceOntologies(), + page, + entriesPerPage, + pageStart, + selectedBoxes, + setTotalCount, + setResults, + setFilteredResultsCount, + setResultsCount, + setLoading, + results, + setFacetCounts + ); + } else + return olsFilterOntologiesSearch( + searchUrl, + query, + apiPreferencesCode !== '' ? 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. @@ -347,121 +379,130 @@ export const MappingSearch = ({ onChange={handleChange} />
+ {mappingDesc}
{/* ant.design form displaying the checkboxes with the search results. */} - {results?.length > 0 ? ( -
-
- - {mappingsForSearch?.length > 0 && ( - { - return { - value: JSON.stringify({ - code: d.code, - display: d.display, - description: d.description, - system: d.system, - }), - label: existingMappingDisplay(d, index), - }; - })} - onChange={onExistingChange} - /> - )} - - - {displaySelectedMappings?.length > 0 && ( +
+ +
+ +
- {' '} -
- {displaySelectedMappings?.map((sm, i) => ( - onCheckboxChange(e, sm)} - checked={selectedBoxes.some( - box => box.obo_id === sm.obo_id - )} - value={sm} - > - {selectedTermsDisplay(sm, i)} - - ))} -
+ {mappingsForSearch?.length > 0 && ( + { + return { + value: JSON.stringify({ + code: d.code, + display: d.display, + description: d.description, + system: d.system, + }), + label: existingMappingDisplay(d, index), + }; + })} + onChange={onExistingChange} + /> + )}
- )} - - {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] - ), - }), - label: newSearchDisplay(d, index), - }; - })} - onChange={onSelectedChange} - /> + {displaySelectedMappings?.length > 0 && ( + + {' '} +
+ {displaySelectedMappings?.map((sm, i) => ( + onCheckboxChange(e, sm)} + checked={selectedBoxes.some( + box => box.obo_id === sm.obo_id + )} + value={sm} + > + {selectedTermsDisplay(sm, i)} + + ))} +
+
+ )}{' '} + {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] + ), + }), + label: newSearchDisplay(d, index), + }; + } + )} + onChange={onSelectedChange} + /> + )} + + + ) : ( +

No results found

)} -
- -
- {/* 'View More' pagination displaying the number of results being displayed +
+
+ +
+ {/* '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} + + {totalCount - filteredResultsCount !== resultsCount && ( + { + handleViewMore(e); + setLastCount(resultsCount); + }} > - Displaying {resultsCount} -  of {totalCount} - - {totalCount - filteredResultsCount !== resultsCount && ( - { - handleViewMore(e); - setLastCount(resultsCount); - }} - > - View More - - )} -
+ View More + + )}
- ) : ( -

No results found.

- )} +
) : ( diff --git a/src/components/Manager/MappingsFunctions/MappingsFunctions.scss b/src/components/Manager/MappingsFunctions/MappingsFunctions.scss index 9be73c5..170c28b 100644 --- a/src/components/Manager/MappingsFunctions/MappingsFunctions.scss +++ b/src/components/Manager/MappingsFunctions/MappingsFunctions.scss @@ -5,21 +5,48 @@ top: 25vh; } +.all_checkboxes_container { + display: flex; + flex-direction: row; +} +.ontology_form { + width: 10vw; + height: 50vh; + overflow: auto; + position: fixed; +} +.result_form { + margin-left: 11vw; +} + .modal_search_results_header { background-color: white; - top:0; + top: 0; position: sticky; margin: 0 0 15px 0; display: flex; + flex-wrap: wrap; flex-direction: row; - align-items: center; - gap: 20px; + justify-content: flex-start; + align-items: end; z-index: 10; + flex: 1; + border-bottom: solid 1px #eeee; + h4 { + margin:0; + padding-right:1rem; + } + .mappings_search_bar { + width: 60%; + } + .search-desc { + flex: 0 0 100%; + margin-bottom:1rem; + } } -.mappings_search_bar { - width: 50%; -} + + .inactive_term, .active_term { @@ -82,3 +109,14 @@ width: 50vw; justify-content: center; } + +.onto_reset { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} + +.view_more_wrapper { + margin-left: 11vw; +} diff --git a/src/components/Manager/MappingsFunctions/OntologyCheckboxes.jsx b/src/components/Manager/MappingsFunctions/OntologyCheckboxes.jsx new file mode 100644 index 0000000..123f96c --- /dev/null +++ b/src/components/Manager/MappingsFunctions/OntologyCheckboxes.jsx @@ -0,0 +1,116 @@ +import { Checkbox, Form } from 'antd'; +import { ontologyCounts } from '../Utilitiy'; +import { useContext, useEffect, useState } from 'react'; +import { SearchContext } from '../../../Contexts/SearchContext'; + +export const OntologyCheckboxes = ({ apiPreferences }) => { + const { + apiPreferencesCode, + setApiPreferencesCode, + facetCounts, + ontologyApis, + component, + } = useContext(SearchContext); + const [checkedOntologies, setCheckedOntologies] = useState([]); + + const defaultOntologies = ['mondo', 'hp', 'maxo', 'ncit']; + + let processedApiPreferencesCode; + + if (Array.isArray(apiPreferencesCode)) { + processedApiPreferencesCode = apiPreferencesCode; + } else if (typeof apiPreferencesCode === 'string') { + processedApiPreferencesCode = apiPreferencesCode.split(','); + } + + const existingOntologies = apiPreferencesCode + ? processedApiPreferencesCode + : apiPreferences && + apiPreferences?.self && + apiPreferences?.self?.api_preference + ? Object?.values(apiPreferences?.self?.api_preference).flat() + : defaultOntologies; + + useEffect(() => { + setCheckedOntologies(existingOntologies); + }, [apiPreferences]); + + const onCheckboxChange = e => { + const { value, checked } = e.target; + + setCheckedOntologies(existingOntologies => { + const newCheckedOntologies = Array.isArray(existingOntologies) + ? checked + ? [...existingOntologies, value] + : existingOntologies.filter(key => key !== value) + : []; + + setApiPreferencesCode(newCheckedOntologies); + + return newCheckedOntologies; + }); + }; + + const formattedFacetCounts = ontologyCounts(facetCounts); + + const sortedData = ontologyApis.map(api => { + const sortedOntologies = Object.fromEntries( + Object.entries(api.ontologies).sort((a, b) => + a[1].ontology_code > b[1].ontology_code ? 1 : -1 + ) + ); + + return { + ...api, + ontologies: sortedOntologies, + }; + }); + + const countsMap = formattedFacetCounts.reduce((acc, item) => { + const key = Object.keys(item)[0]; + acc[key] = parseInt(item[key], 10); + return acc; + }, {}); + + // // Build the new data structure + const countsResult = Object.keys(sortedData[0]?.ontologies).map(key => { + return { [key]: countsMap[key] || 0, api: sortedData[0]?.api_id }; + }); + + const checkedOntologiesArray = Array.isArray(checkedOntologies) + ? checkedOntologies + : []; + + return ( +
+ +
+ {countsResult + ?.sort((a, b) => { + const aValue = Object.values(a)[0]; + const bValue = Object.values(b)[0]; + return bValue - aValue; + }) + .map((fc, i) => { + const key = Object.keys(fc)[0]; + const value = fc[key]; + return ( + + {`${key.toUpperCase()} ${value !== 0 ? `(${value})` : ''}`} + + ); + })} +
+
+
+ ); +}; diff --git a/src/components/Manager/MappingsFunctions/OntologyFilterCodeSubmit.jsx b/src/components/Manager/MappingsFunctions/OntologyFilterCodeSubmit.jsx new file mode 100644 index 0000000..84050dc --- /dev/null +++ b/src/components/Manager/MappingsFunctions/OntologyFilterCodeSubmit.jsx @@ -0,0 +1,53 @@ +import { notification } from 'antd'; + +export const OntologyFilterCodeSubmit = ( + apiPreferencesCode, + setApiPreferencesCode, + apiPreferences, + mappingProp, + table, + vocabUrl, + component +) => { + const apiPreference = { + api_preference: { 'ols': [] }, + }; + + if ( + apiPreferencesCode && + JSON.stringify( + Object.values(apiPreferences?.self?.api_preference)[0].sort() + ) !== JSON.stringify(apiPreferencesCode?.sort()) + ) { + apiPreference.api_preference.ols = apiPreferencesCode; + + fetch( + `${vocabUrl}/${(component = table + ? table?.terminology?.reference + : `Terminology/${component?.id}`)}/filter/${mappingProp}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(apiPreference), + } + ) + .then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error('An unknown error occurred.'); + } + }) + .catch(error => { + if (error) { + notification.error({ + message: 'Error', + description: 'An error occurred saving the ontology preferences.', + }); + } + return error; + }); + } +}; diff --git a/src/components/Manager/Utilitiy.jsx b/src/components/Manager/Utilitiy.jsx index 4052b6d..1515f19 100644 --- a/src/components/Manager/Utilitiy.jsx +++ b/src/components/Manager/Utilitiy.jsx @@ -40,3 +40,23 @@ export const ontologySystems = { export const systemsMatch = ont => { return ontologySystems[ont]; }; + +// Iterates over the facet counts in the result to make an object of search results per ontology +export const ontologyCounts = arr => { + let result = []; + let i = 0; + + while (i < arr.length) { + if (isNaN(arr[i])) { + // If element in array, it not a number (i.e. it's a string), it sets it as the key + const key = arr[i]; + const value = arr[i + 1]; // Gets the first number after the string + result.push({ [key]: value }); // Pushes the key (string) and value (number) pair to the result array + i += 2; // Moves to the next letter-number pair + } else { + i += 1; // If the element is a number, it keeps going until it starts over and finds a string + } + } + + return result; +}; diff --git a/src/components/Nav/Breadcrumbs.jsx b/src/components/Nav/Breadcrumbs.jsx new file mode 100644 index 0000000..f6b29ee --- /dev/null +++ b/src/components/Nav/Breadcrumbs.jsx @@ -0,0 +1,30 @@ +import { Breadcrumb } from "antd" +import { useLocation, Link } from 'react-router-dom'; +// import { Link } from 'react-router'; +export const Breadcrumbs = () => { + const location = useLocation(); + const pathParts = location.pathname.split('/').filter(Boolean); + const pathArr = [] + pathParts.forEach(path => { + pathArr.push({ title: path, path: path }); + }); + + console.log(location,'location'); + + + return +
  • Home {location.pathname != '/' &&   /  }
  • + +
    +} +function itemRender(currentRoute, params, items, pathArr) { + const isLast = currentRoute?.path === items[items.length - 1]?.path; + + return <> + {isLast ? ( + {currentRoute.title} + ) : ( + {currentRoute.title} + )} + +} \ No newline at end of file diff --git a/src/components/Nav/NavBar.jsx b/src/components/Nav/NavBar.jsx index 13a0c11..80e39ff 100644 --- a/src/components/Nav/NavBar.jsx +++ b/src/components/Nav/NavBar.jsx @@ -27,11 +27,11 @@ export const NavBar = () => { {/* Placeholder elements below. No functionality at this time.*/} -
  • Help
  • +
  • About
  • -
  • About
  • +
  • Ontologies
  • diff --git a/src/components/Nav/NavBar.scss b/src/components/Nav/NavBar.scss index 25c21e9..95459e7 100644 --- a/src/components/Nav/NavBar.scss +++ b/src/components/Nav/NavBar.scss @@ -3,7 +3,7 @@ $navColor: #080705; $linkColor: #fffc31; -nav { +nav.navbar { z-index: 1; width: 100%; background-color: $navColor; @@ -11,81 +11,122 @@ nav { display: flex; flex-direction: row; justify-content: center; - position: fixed; + position: sticky; top: 0; + li { list-style-type: none; } + a:not(.nav_logo) { text-decoration: none; color: $navColor; + font: { size: 1em; weight: 800; } } -} -.nav_body { - width: 100%; - display: grid; - grid-template-columns: 1fr auto 1fr; -} -.logo_container { - left: 0; - padding-left: 100px; -} + .nav_body { + width: 100%; + display: grid; + grid-template-columns: 1fr auto 1fr; + } -.nav_logo { - width: 81px; - color: white; - font: { - weight: 800; - size: 1.3rem; + .logo_container { + left: 0; + padding-left: 100px; } - text-align: left; - text-decoration: none; -} -.nav_links { - margin: auto; - display: flex; - flex-direction: row; - gap: 2rem; - grid-column-start: 2; -} + .nav_logo { + width: 81px; + color: white; -.nav_link { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - background-color: $linkColor; - height: 75px; - padding: 0 4px; - width: $navHeight; - border-radius: 10px; -} + font: { + weight: 800; + size: 1.3rem; + } + + text-align: left; + text-decoration: none; + } + + .nav_links { + margin: auto; + display: flex; + flex-direction: row; + gap: 2rem; + grid-column-start: 2; + } + + .nav_link { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: $linkColor; + height: 75px; + padding: 0 4px; + width: $navHeight; + border-radius: 10px; + } + + .nav_link:hover { + color: rgb(45, 42, 34); + box-shadow: 0 5px 15px rgba(178, 182, 92, 0.8); + } -.nav_link:hover { - color: rgb(45, 42, 34); - box-shadow: 0 5px 15px rgba(178, 182, 92, 0.8); + .login { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + margin: 0 100px 0 0; + } } -.login { +.breadcrumbs { display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - margin: 0 100px 0 0; + align-items: baseline; + z-index: 1; + padding: 0 10vw; + position: sticky; + top: 115px; + background:#eeeeee; + + li { + list-style: none; + font-size: 14px; + line-height: 1.5714285714285714; + color: rgba(0, 0, 0, 0.45); + + &>a { + color: rgba(0, 0, 0, 0.45); + text-decoration: none; + transition: color 0.2s; + padding: 0 4px; + border-radius: 4px; + height: 22px; + display: inline-block; + margin-inline: -4px; + + &:hover { + color: rgba(0, 0, 0, 0.88); + background-color: rgba(0, 0, 0, 0.06); + } + } + } + + & * { + text-transform: capitalize; + } + } footer { - margin-top: auto; - bottom: 0; width: 100%; height: 50px; - margin-top: -50px; background-color: $navColor; -} +} \ No newline at end of file diff --git a/src/components/Projects/Studies/StudyStyling.scss b/src/components/Projects/Studies/StudyStyling.scss index 62eca80..7a2cae2 100644 --- a/src/components/Projects/Studies/StudyStyling.scss +++ b/src/components/Projects/Studies/StudyStyling.scss @@ -1,6 +1,7 @@ .studies_container { - margin: 17vh 10vw 10vh 10vw; + margin: 5vh 10vw 10vh 10vw; line-height: 1.5rem; + height: 100vh; } .cards_container { diff --git a/src/components/Projects/Tables/EditMappingsTableModal.jsx b/src/components/Projects/Tables/EditMappingsTableModal.jsx index 3408f96..15fdc98 100644 --- a/src/components/Projects/Tables/EditMappingsTableModal.jsx +++ b/src/components/Projects/Tables/EditMappingsTableModal.jsx @@ -16,12 +16,14 @@ import { MappingReset } from '../../Manager/MappingsFunctions/MappingReset'; import { ResetTableMappings } from './ResetTableMappings'; import { ellipsisString, systemsMatch } from '../../Manager/Utilitiy'; import { getById } from '../../Manager/FetchManager'; +import { SearchContext } from '../../../Contexts/SearchContext'; export const EditMappingsTableModal = ({ editMappings, setEditMappings, tableId, setMapping, + table, }) => { const [form] = Form.useForm(); const [termMappings, setTermMappings] = useState([]); @@ -33,6 +35,7 @@ export const EditMappingsTableModal = ({ const [editSearch, setEditSearch] = useState(false); const { setSelectedMappings, setDisplaySelectedMappings } = useContext(MappingContext); + const { setApiPreferencesCode } = useContext(SearchContext); useEffect(() => { fetchMappings(); @@ -45,6 +48,7 @@ export const EditMappingsTableModal = ({ setReset(false); setEditSearch(false); setSelectedKey(null); + setApiPreferencesCode(undefined); }; const fetchMappings = () => { @@ -337,16 +341,32 @@ export const EditMappingsTableModal = ({ reset={reset} onClose={form.resetFields} searchProp={editMappings.name} + mappingDesc={ + editMappings.description + ? editMappings.description + : 'No Description' + } + component={table} + mappingProp={editMappings.code} + table={table} /> ) : ( reset && ( ) )} diff --git a/src/components/Projects/Tables/TableDetails.jsx b/src/components/Projects/Tables/TableDetails.jsx index 46b3597..ca17c9d 100644 --- a/src/components/Projects/Tables/TableDetails.jsx +++ b/src/components/Projects/Tables/TableDetails.jsx @@ -4,7 +4,8 @@ import './TableStyling.scss'; import { Link, useNavigate, useParams } from 'react-router-dom'; import { Spinner } from '../../Manager/Spinner'; import { getById } from '../../Manager/FetchManager'; -import { Card, Col, Form, notification, Row, Table, Tooltip } from 'antd'; +import { Card, Col, Form, notification, Row, Table, Button } from 'antd'; +import { CloseCircleOutlined } from '@ant-design/icons'; import { EditTableDetails } from './EditTableDetails'; import { DeleteTable } from './DeleteTable'; import { LoadVariables } from './LoadVariables'; @@ -20,12 +21,14 @@ import { Submenu } from '../../Manager/Submenu'; import { SettingsDropdownTable } from '../../Manager/Dropdown/SettingsDropdownTable'; import { RequiredLogin } from '../../Auth/RequiredLogin'; import { FilterSelect } from '../../Manager/MappingsFunctions/FilterSelect'; +import { SearchContext } from '../../../Contexts/SearchContext'; export const TableDetails = () => { const [form] = Form.useForm(); const { vocabUrl, edit, setEdit, table, setTable, user } = useContext(myContext); + const { apiPreferences, setApiPreferences } = useContext(SearchContext); const { getMappings, setGetMappings, @@ -37,8 +40,12 @@ export const TableDetails = () => { const { studyId, DDId, tableId } = useParams(); const [loading, setLoading] = useState(true); const [load, setLoad] = useState(false); - const [apiPreferences, setApiPreferences] = useState({}); + const [pageSize, setPageSize] = useState( + parseInt(localStorage.getItem('pageSize'), 10) || 10); + const handleTableChange = (current, size) => { + setPageSize(size); + }; const navigate = useNavigate(); const handleSuccess = () => { @@ -47,8 +54,68 @@ export const TableDetails = () => { const login = RequiredLogin({ handleSuccess: handleSuccess }); useEffect(() => { + setDataSource(tableData(table)); - }, [table, mapping]); + localStorage.setItem('pageSize', pageSize); + }, [table, mapping, pageSize]); + + + const updateMappings = (mapArr,mappingCode ) => { + + // setLoading(true); + const mappingsDTO = { + mappings: mapArr, + editor: user.email, + }; + console.log(mappingsDTO,"mappingsDTO"); + + fetch(`${vocabUrl}/Table/${tableId}/mapping/${mappingCode}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(mappingsDTO), + }) + .then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error('An unknown error occurred.'); + } + }) + .then(data => { + setMapping(data.codes); + setEditMappings(null); + form.resetFields(); + notification.success({ description:'Mapping removed.'}); + }) + .catch(error => { + console.log(error,'error'); + + if (error) { + notification.error({ + message: 'Error', + description: 'An error occurred. Please try again.', + }); + } + return error; + }) + .finally(() => setLoading(false)); + }; + + + const alphabetizeOntologies = ontologies => { + // Sort the keys alphabetically + const sortedKeys = Object.keys(ontologies).sort(); + + // Rebuild object using the sorted keys + const sortedOntologies = {}; + sortedKeys.forEach(key => { + sortedOntologies[key] = ontologies[key]; + }); + + return sortedOntologies; + }; // fetches the table and sets 'table' to the response useEffect(() => { @@ -64,21 +131,25 @@ export const TableDetails = () => { .then(data => setMapping(data.codes)) .catch(error => { if (error) { + console.log(error,"error"); + notification.error({ message: 'Error', - description: - 'An error occurred loading mappings. Please try again.', + description: 'An error occurred loading mappings.', }); } return error; }) .then(() => - fetch(`${vocabUrl}/${data?.terminology?.reference}/filter`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }) + fetch( + `${vocabUrl}/${data?.terminology?.reference}/filter/self`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + } + ) ) .then(res => { if (res.ok) { @@ -163,23 +234,39 @@ The variables in the mappings need to be matched up to each variable in the tabl The function maps through the mapping array. For each variable, if the mapping variable is equal to the variable in the table, AND the mappings array length for the variable is > 0, the mappings array is mapped through and returns the length of the mapping array (i.e. returns the number of variables mapped to the table variable). -There is then a tooltip that displays the variables on hover.*/ - const matchCode = variable => - mapping?.length > 0 && - mapping?.map( - (item, index) => - item?.code === variable?.code && - item?.mappings?.length > 0 && ( - { - return
    {code.code}
    ; - })} - key={index} - > - {item?.mappings?.length} -
    - ) - ); +It then shows the mappings as table data and alows the user to delete a mapping from the table.*/ + const noMapping = variable => { + return + } + + const matchCode = variable => { + + + if (!mapping?.length) { + return noMapping(variable); + } + + const variableMappings = mapping.find(item => item?.code === variable?.code); + + if (variableMappings && variableMappings.mappings?.length) { + return variableMappings.mappings.map(code =>
    {code.display} handleRemoveMapping(variableMappings,code)}>
    ); + } else { + return noMapping(variable); + } + }; + + const handleRemoveMapping = (variableMappings,code) => { + // console.log(variableMappings,"variableMappings"); + const mappingToRemove = variableMappings.mappings.indexOf(code); + //remove mapping from mappings + {mappingToRemove !== -1 && variableMappings.mappings.splice(mappingToRemove,1)} + updateMappings(variableMappings?.mappings,variableMappings?.code); + + } + + // 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. @@ -198,7 +285,9 @@ There is then a tooltip that displays the variables on hover.*/ max: variable.max, units: variable.units, enumeration: variable.data_type === 'ENUMERATION' && ( - View/Edit + console.log(variable.enumerations.reference,"variable.enumerations.reference"), + + View/Edit ), mapped_terms: matchCode(variable), }; @@ -296,6 +385,12 @@ There is then a tooltip that displays the variables on hover.*/ record.data_type === 'INTEGER' || record.data_type === 'QUANTITY', }} + pagination={{ + showSizeChanger: true, + pageSizeOptions: ['10', '20', '30'], + pageSize: pageSize, // Use the stored pageSize + onChange: handleTableChange, // Capture pagination changes + }} /> @@ -342,6 +437,7 @@ There is then a tooltip that displays the variables on hover.*/ setEditMappings={setEditMappings} tableId={tableId} setMapping={setMapping} + table={table} /> diff --git a/src/components/Projects/Tables/TableStyling.scss b/src/components/Projects/Tables/TableStyling.scss index 4cf5288..64e5b6e 100644 --- a/src/components/Projects/Tables/TableStyling.scss +++ b/src/components/Projects/Tables/TableStyling.scss @@ -2,7 +2,7 @@ @import '../../Styling/Variables'; .table_id_container { - margin: 17vh 10vw 13vh 10vw; //Make 10vh bottom without export button + margin: 5vh 10vw 13vh 10vw; //Make 10vh bottom without export button width: 66vw; } @@ -54,6 +54,7 @@ input::file-selector-button:hover { border: 1px solid darkgrey; color: gray; border-radius: 25px; + font: { size: 0.7rem; weight: 800; @@ -74,3 +75,20 @@ input::file-selector-button:hover { gap: 10px; margin-left: 50px; } + +.mapping { + &-display { + white-space: nowrap; + } + display: flex; + .remove-mapping { + padding-left:.25rem; + cursor: pointer; + opacity: 0; + + &:hover { + transition: all .4s linear; + opacity: 1; + } + } +} diff --git a/src/components/Projects/Terminologies/EditMappingModal.jsx b/src/components/Projects/Terminologies/EditMappingModal.jsx index ecc352c..4845716 100644 --- a/src/components/Projects/Terminologies/EditMappingModal.jsx +++ b/src/components/Projects/Terminologies/EditMappingModal.jsx @@ -15,17 +15,22 @@ import { ResetMappings } from './ResetMappings'; import { MappingReset } from '../../Manager/MappingsFunctions/MappingReset'; import { ellipsisString, systemsMatch } from '../../Manager/Utilitiy'; import { getById } from '../../Manager/FetchManager'; +import { SearchContext } from '../../../Contexts/SearchContext'; export const EditMappingsModal = ({ editMappings, setEditMappings, terminologyId, setMapping, + mappingDesc, + terminology, + }) => { const [form] = Form.useForm(); const [termMappings, setTermMappings] = useState([]); const [options, setOptions] = useState([]); const { vocabUrl, setSelectedKey, user } = useContext(myContext); + const { setApiPreferencesCode } = useContext(SearchContext); const [loading, setLoading] = useState(false); const [reset, setReset] = useState(false); const [mappingsForSearch, setMappingsForSearch] = useState([]); @@ -39,6 +44,7 @@ export const EditMappingsModal = ({ setTermMappings([]); setOptions([]); setSelectedKey(null); + setApiPreferencesCode(undefined); }; const fetchMappings = () => { @@ -320,6 +326,8 @@ export const EditMappingsModal = ({ ? editMappings.display : editMappings?.code} + {mappingDesc} +
    ) : ( reset && ( @@ -356,6 +365,7 @@ export const EditMappingsModal = ({ form={form} reset={reset} onClose={form.resetFields} + mappingDesc={editMappings?.description} /> ) )} diff --git a/src/components/Projects/Terminologies/Terminology.jsx b/src/components/Projects/Terminologies/Terminology.jsx index a7c49b3..5316c5f 100644 --- a/src/components/Projects/Terminologies/Terminology.jsx +++ b/src/components/Projects/Terminologies/Terminology.jsx @@ -4,7 +4,8 @@ import { myContext } from '../../../App'; import './Terminology.scss'; import { Spinner } from '../../Manager/Spinner'; import { getById } from '../../Manager/FetchManager'; -import { Col, Form, notification, Row, Table, Tooltip } from 'antd'; +import { Col, Form, notification, Row, Table, Button } from 'antd'; +import { CloseCircleOutlined } from '@ant-design/icons'; import { EditMappingsModal } from './EditMappingModal'; import { EditTerminologyDetails } from './EditTerminologyDetails'; import { SettingsDropdownTerminology } from '../../Manager/Dropdown/SettingsDropdownTerminology'; @@ -21,8 +22,8 @@ import { SearchContext } from '../../../Contexts/SearchContext'; export const Terminology = () => { const [form] = Form.useForm(); - const { terminologyId } = useParams(); - const { vocabUrl } = useContext(myContext); + const { terminologyId, tableId } = useParams(); + const { vocabUrl, user } = useContext(myContext); const { setPrefTerminologies, prefTerminologies } = useContext(SearchContext); const { editMappings, @@ -33,33 +34,121 @@ export const Terminology = () => { setMapping, } = useContext(MappingContext); + const [pageSize, setPageSize] = useState( + parseInt(localStorage.getItem('pageSize'), 10) || 10 + ); + const handleTableChange = (current, size) => { + setPageSize(size); + }; + useEffect(() => { + localStorage.setItem('pageSize', pageSize); + }, [pageSize]); + const [loading, setLoading] = useState(true); const initialTerminology = { url: '', description: '', name: '', codes: [] }; //initial state of terminology const [terminology, setTerminology] = useState(initialTerminology); const navigate = useNavigate(); + + const updateMappings = (mapArr, mappingCode) => { + // setLoading(true); + const mappingsDTO = { + mappings: mapArr, + editor: user.email, + }; + console.log(tableId, 'tableId'); + + fetch(`${vocabUrl}/Terminology/${terminologyId}/mapping/${mappingCode}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(mappingsDTO), + }) + .then(res => { + if (res.ok) { + return res.json(); + } else { + throw new Error('An unknown error occurred.'); + } + }) + .then(data => { + setMapping(data.codes); + setEditMappings(null); + form.resetFields(); + notification.success({ description: 'Mapping removed.' }); + }) + .catch(error => { + console.log(error, 'error'); + + if (error) { + console.log(error, 'error'); + + notification.error({ + message: 'Error', + description: 'An error occurred. Please try again.', + }); + } + return error; + }) + .finally(() => setLoading(false)); + }; + /* The terminology may have numerous codes. The API call to fetch the mappings returns all mappings for the terminology. The codes in the mappings need to be matched up to each code in the terminology. The function maps through the mapping array. For each code, if the mapping code is equal to the code in the terminology, AND the mappings array length for the code is > 0, the mappings array is mapped through and returns the length of the mapping array (i.e. returns the number of codes mapped to the terminology code). -There is then a tooltip that displays the codes on hover.*/ - const matchCode = code => - mapping?.length > 0 && - mapping?.map( - (item, index) => - item.code === code.code && - item?.mappings?.length > 0 && ( - { - return
    {code.code}
    ; - })} - key={index} - > - {item.mappings.length} -
    - ) +It then shows the mappings as table data and alows the user to delete a mapping from the table.*/ + + const noMapping = variable => { + return ( + + ); + }; + + const matchCode = variable => { + if (!mapping?.length) { + return noMapping(variable); + } + + const variableMappings = mapping.find( + item => item?.code === variable?.code ); + if (variableMappings && variableMappings.mappings?.length) { + return variableMappings.mappings.map(code => ( +
    + {code.display} + handleRemoveMapping(variableMappings, code)} + > + + +
    + )); + } else { + return noMapping(variable); + } + }; + + const handleRemoveMapping = (variableMappings, code) => { + // console.log(variableMappings,"variableMappings"); + const mappingToRemove = variableMappings.mappings.indexOf(code); + //remove mapping from mappings + { + mappingToRemove !== -1 && + variableMappings.mappings.splice(mappingToRemove, 1); + } + updateMappings(variableMappings?.mappings, variableMappings?.code); + }; + // data for each column in the table. // Map through the codes in the terminology and display the code, display, number of mapped terms, // and an edit or get mappings button depending on the condition. @@ -248,7 +337,16 @@ There is then a tooltip that displays the codes on hover.*/ ) : ( -
    +
    )} @@ -256,9 +354,14 @@ There is then a tooltip that displays the codes on hover.*/ {/* Displays the edit form */} diff --git a/src/components/Projects/Terminologies/Terminology.scss b/src/components/Projects/Terminologies/Terminology.scss index b640fcb..ca3ad43 100644 --- a/src/components/Projects/Terminologies/Terminology.scss +++ b/src/components/Projects/Terminologies/Terminology.scss @@ -1,6 +1,6 @@ @import '../../Styling/Variables'; .terminology_container { - margin: 17vh 10vw 10vh 10vw; + margin: 5vh 10vw 10vh 10vw; width: 66vw; } @@ -108,6 +108,7 @@ .modal_search_result { display: flex; + flex-direction: column; align-items: center; justify-content: space-between; margin: 0 10px 10px 6px; diff --git a/src/components/Projects/Terminologies/TerminologyList.jsx b/src/components/Projects/Terminologies/TerminologyList.jsx index 5b3ca6f..99d0cbf 100644 --- a/src/components/Projects/Terminologies/TerminologyList.jsx +++ b/src/components/Projects/Terminologies/TerminologyList.jsx @@ -14,7 +14,8 @@ export const TerminologyList = () => { const [terms, setTerms] = useState([]); const [filter, setFilter] = useState(null); const [deleteId, setDeleteId] = useState(null); - + const [pageSize, setPageSize] = useState( + parseInt(localStorage.getItem('pageSize'), 10) || 10); const { vocabUrl } = useContext(myContext); const navigate = useNavigate(); @@ -28,7 +29,8 @@ export const TerminologyList = () => { setTerms(data); }) .finally(() => setLoading(false)); - }, []); + localStorage.setItem('pageSize', pageSize); + }, [pageSize]); const terminologyTitle = () => { return ( @@ -44,6 +46,9 @@ export const TerminologyList = () => { {item.name ? item.name : item.id} ); + const handleTableChange = (current, size) => { + setPageSize(size); + }; const columns = [ { @@ -140,9 +145,16 @@ export const TerminologyList = () => {

    Terminology Index

    trigger.parentNode} + pagination={{ + showSizeChanger: true, + pageSizeOptions: ['10', '20', '30'], + pageSize: pageSize, // Use the stored pageSize + onChange: handleTableChange, // Capture pagination changes + }} />