From a85c74db09fc0273a118f3bcd309df59ea26cec5 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Wed, 22 Apr 2026 08:09:10 +0530 Subject: [PATCH 1/5] OpenConceptLab/ocl_issues#2123 | Reference view details --- .eslintrc.json | 3 +- src/components/common/Breadcrumbs.jsx | 27 ++- src/components/concepts/Associations.jsx | 9 +- .../references/ReferenceDetails.jsx | 134 +++++++++++++ .../references/ReferenceExpansionResults.jsx | 183 ++++++++++++++++++ src/components/references/ReferenceHeader.jsx | 63 ++++++ src/components/references/ReferenceHome.jsx | 79 ++++++++ .../references/ReferenceManagementList.jsx | 49 +++++ src/components/references/ReferenceTabs.jsx | 24 +++ src/components/repos/RepoHome.jsx | 11 +- src/components/search/SearchResults.jsx | 5 +- src/i18n/locales/en/translations.json | 11 +- 12 files changed, 584 insertions(+), 14 deletions(-) create mode 100644 src/components/references/ReferenceDetails.jsx create mode 100644 src/components/references/ReferenceExpansionResults.jsx create mode 100644 src/components/references/ReferenceHeader.jsx create mode 100644 src/components/references/ReferenceHome.jsx create mode 100644 src/components/references/ReferenceManagementList.jsx create mode 100644 src/components/references/ReferenceTabs.jsx diff --git a/.eslintrc.json b/.eslintrc.json index 632000428..032728dcb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -256,7 +256,8 @@ "unversioned", "reranker", "lte", - "gte" + "gte", + "resourceversions" ], "skipIfMatch": [ "http://[^s]*", diff --git a/src/components/common/Breadcrumbs.jsx b/src/components/common/Breadcrumbs.jsx index 20b2bccf9..dd04b90cc 100644 --- a/src/components/common/Breadcrumbs.jsx +++ b/src/components/common/Breadcrumbs.jsx @@ -2,15 +2,16 @@ import React from 'react'; import RepoIcon from '../repos/RepoIcon'; import ConceptIcon from '../concepts/ConceptIcon'; import MappingIcon from '../mappings/MappingIcon'; +import ReferenceIcon from '@mui/icons-material/PentagonRounded'; import DotSeparator from './DotSeparator' import RepoVersionButton from '../repos/RepoVersionButton' import RepoTooltip from '../repos/RepoTooltip' import Box from '@mui/material/Box'; import OwnerButton from './OwnerButton' -const Breadcrumbs = ({owner, ownerType, repo, repoVersion, repoURL, concept, mapping, noIcons, color, fontSize, size, ownerURL, nested}) => { +const Breadcrumbs = ({owner, ownerType, repo, repoVersion, repoURL, concept, mapping, reference, noIcons, color, fontSize, size, ownerURL, nested}) => { const iconProps = {color: 'secondary', style: {marginRight: '8px', width: '0.8em'}} - const hideParents = Boolean((concept?.id || mapping?.id) && nested) + const hideParents = Boolean((concept?.id || mapping?.id || reference.id) && nested) return ( { @@ -125,6 +126,28 @@ const Breadcrumbs = ({owner, ownerType, repo, repoVersion, repoURL, concept, map } + { + reference?.id && + + {!hideParents && } + { + !noIcons && + + } + + {reference.id} + + + } ) } diff --git a/src/components/concepts/Associations.jsx b/src/components/concepts/Associations.jsx index fe3d23d57..2f4de1ff2 100644 --- a/src/components/concepts/Associations.jsx +++ b/src/components/concepts/Associations.jsx @@ -31,7 +31,7 @@ const groupMappings = (orderedMappings, concept, mappings, forward) => { if(!mapType) mapType = forward ? 'children' : 'parent'; orderedMappings[mapType] = orderedMappings[mapType] || {order: null, direct: [], indirect: [], unknown: [], hierarchy: [], reverseHierarchy: [], self: []} - const isSelfMapping = isMapping && dropVersion(concept.url) === dropVersion(resource.cascade_target_concept_url) && toParentURI(concept.url) === dropVersion(resource.cascade_target_concept_url) + const isSelfMapping = isMapping && dropVersion(concept?.url) === dropVersion(resource.cascade_target_concept_url) && concept?.url && toParentURI(concept.url) === dropVersion(resource.cascade_target_concept_url) let _resource = isMapping ? {...resource, isSelf: isSelfMapping, cascade_target_concept_name: resource.cascade_target_concept_name || get(find(mappings, m => dropVersion(m.url) === dropVersion(resource.cascade_target_concept_url)), 'display_name')} : {...resource, cascade_target_concept_name: resource.display_name} if(isSelfMapping) { if(!map(orderedMappings[mapType].self, 'id').includes(resource.id)) @@ -131,7 +131,7 @@ const AssociationRow = ({mappings, id, mapType, isSelf, isIndirect, hide}) => { const borderColor = 'rgba(0, 0, 0, 0.12)' -const Associations = ({concept, mappings, reverseMappings, ownerMappings, reverseOwnerMappings, onLoadOwnerMappings, loadingOwnerMappings}) => { +const Associations = ({concept, mappings, reverseMappings, ownerMappings, reverseOwnerMappings, onLoadOwnerMappings, loadingOwnerMappings, nested}) => { const [scope, setScope] = React.useState('repo') const [orderedMappings, setOrderedMappings] = React.useState({}); const [orderedOwnerMappings, setOrderedOwnerMappings] = React.useState({}); @@ -190,7 +190,9 @@ const Associations = ({concept, mappings, reverseMappings, ownerMappings, revers const toggleSection = repoURI => setCollapsedSections(collapsedSections?.includes(repoURI) ? without(collapsedSections, repoURI) : [...collapsedSections, repoURI]) return ( - + + { + !nested && @@ -205,6 +207,7 @@ const Associations = ({concept, mappings, reverseMappings, ownerMappings, revers + } diff --git a/src/components/references/ReferenceDetails.jsx b/src/components/references/ReferenceDetails.jsx new file mode 100644 index 000000000..d54ec8d74 --- /dev/null +++ b/src/components/references/ReferenceDetails.jsx @@ -0,0 +1,134 @@ +import React from 'react' +import { useTranslation } from 'react-i18next'; +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableRow from '@mui/material/TableRow' +import TableCell from '@mui/material/TableCell' +import Paper from '@mui/material/Paper' +import Typography from '@mui/material/Typography' +import { startCase, isString, isPlainObject, map } from 'lodash' +import { formatDateTime } from '../../common/utils' +import Link from '../common/Link' + + +const borderColor = 'rgba(0, 0, 0, 0.12)' + +const ReferenceDetails = ({ reference, style }) => { + const { t } = useTranslation() + const getCascadeDetails = () => { + if(!reference?.cascade) + return {} + if(isString(reference.cascade)) + return {method: reference.cascade} + if(isPlainObject(reference.cascade)) + return reference.cascade + return {} + } + return ( +
+ + + {t('reference.details')} + +
+ + + + {t('reference.type')} + + + {startCase(reference.reference_type)} + + + + + {t('reference.translation')} + + + {reference.translation} + + + { + map(getCascadeDetails(), (value, key) => { + let val = isPlainObject(value) ? {JSON.stringify(value, undefined, 2)} : value + return ( + + + {`Cascade ${startCase(key)}`} + + + {val} + + + ) + }) + } + { + Boolean(reference.transform) && + + + {t('reference.transform')} + + + { + ['resourceversions', 'intensional'].includes(reference.transform?.toLowerCase()) ? + t('reference.intensional') : + ( + reference.transform?.toLowerCase() === 'extensional' ? + t('reference.extensional') : + reference.transform + ) + } + + + } + +
+
+ { + reference.filter?.length > 0 && + + + {t('repo.filters')} + + + + { + map(reference.filter, (refFilter, index) => { + return ( + + + {refFilter?.property} {refFilter?.op} + + + {refFilter?.value} + + + ) + }) + } + +
+
+ } +
+ + { + `${t('reference.last_resolved_at')} ` + } + { + reference.last_resolved_at ? formatDateTime(reference.last_resolved_at) : - + } + + + {t('common.created_on')} { + <>{formatDateTime(reference.created_at)} {t('common.by')} + } + + +
+ + ) +} + +export default ReferenceDetails; diff --git a/src/components/references/ReferenceExpansionResults.jsx b/src/components/references/ReferenceExpansionResults.jsx new file mode 100644 index 000000000..1e7a7e780 --- /dev/null +++ b/src/components/references/ReferenceExpansionResults.jsx @@ -0,0 +1,183 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import Card from '@mui/material/Card' +import CardContent from '@mui/material/CardContent' +import ListItemText from '@mui/material/ListItemText' +import Paper from '@mui/material/Paper' +import Typography from '@mui/material/Typography' +import Table from '@mui/material/Table' +import TableHead from '@mui/material/TableHead' +import TableBody from '@mui/material/TableBody' +import TableRow from '@mui/material/TableRow' +import TableCell from '@mui/material/TableCell' +import Skeleton from '@mui/material/Skeleton' +import Button from '@mui/material/Button' +import Collapse from '@mui/material/Collapse'; +import Box from '@mui/material/Box'; + + +import AddIcon from '@mui/icons-material/Add'; +import RemoveIcon from '@mui/icons-material/Remove'; + +import RepoChip from '../repos/RepoChip' +import ConceptIcon from '../concepts/ConceptIcon' +import Associations from '../concepts/Associations' + +import { map, times, without } from 'lodash' + +import { BLACK } from '../../common/colors' + +const borderColor = 'rgba(0, 0, 0, 0.12)' + +const MappingsTable = ({mappings, loading, t}) => { + return ( + + + {t('mapping.mappings')} + + + + ) +} + +const ConceptsAndMappingsTable = ({reference, concepts, loading, t}) => { + const [open, setOpen] = React.useState([]) + const toggleRow = conceptURL => { + setOpen(open.includes(conceptURL) ? without(open, conceptURL) : [...open, conceptURL]) + } + + return ( + + + {t('reference.concepts_and_mappings')} + + + + + {t('common.id')} + {t('concept.display_name')} + {t('mapping.mappings')} + {t('repo.repo')} + + + + { + loading ? + <> + { + times((reference.concepts || 1), i => ( + + + + + + )) + } + : + <> + { + map(concepts, (concept, index) => { + let key = concept.version_url || concept.url + const hasMappings = concept.mappings?.length > 0 + const isOpen = open.includes(key) + const isLastConcept = index === (concepts?.length || 0) - 1 + return ( + + .MuiTableCell-root': { + borderBottom: 0, + '&:first-of-type': { + borderBottomLeftRadius: '10px' + }, + '&:last-child': { + borderBottomRightRadius: '10px' + } + } + } : undefined} + > + + + + {concept.id} + + + + {concept.display_name} + + + { + hasMappings ? + : + '0' + } + + + + + + .MuiTableCell-root': { + borderBottom: 0, + borderBottomLeftRadius: isOpen ? '10px' : 0, + borderBottomRightRadius: isOpen ? '10px' : 0 + } + } : undefined} + > + + + + + + + + + + ) + }) + } + + } + +
+
+ ) +} + +const ReferenceExpansionResults = ({ reference, concepts, mappings, loading }) => { + const { t } = useTranslation() + + const statSx = { + '.MuiListItemText-primary': { + color: 'rgba(0, 0, 0, 0.6)', + fontSize: '1rem' + }, + '.MuiListItemText-secondary': { + color: BLACK, + fontSize: '1.5rem' + } + } + + const isMappingsOnly = Boolean(!reference.concepts && reference.mappings) + + return ( +
+ + + + + + + + { + isMappingsOnly ? + : + + } + +
+ ) +} + +export default ReferenceExpansionResults; diff --git a/src/components/references/ReferenceHeader.jsx b/src/components/references/ReferenceHeader.jsx new file mode 100644 index 000000000..47a590c3d --- /dev/null +++ b/src/components/references/ReferenceHeader.jsx @@ -0,0 +1,63 @@ +import React from 'react' +import { useTranslation } from 'react-i18next'; + +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import DownIcon from '@mui/icons-material/ArrowDropDown'; + +import { BLACK } from '../../common/colors' +import { toFullAPIURL, copyURL } from '../../common/utils' +import Breadcrumbs from '../common/Breadcrumbs' +import CloseIconButton from '../common/CloseIconButton'; +import ReferenceManagementList from './ReferenceManagementList' + +const ReferenceHeader = ({ reference, onClose }) => { + const { t } = useTranslation() + const [menu, setMenu] = React.useState(false) + const [menuAnchorEl, setMenuAnchorEl] = React.useState(false) + const onMenuOpen = event => { + setMenuAnchorEl(event.currentTarget) + setMenu(true) + } + const onMenuClose = () => { + setMenuAnchorEl(false) + setMenu(false) + } + + const onClick = option => { + if(option === 'copyExpression') + copyURL(toFullAPIURL(reference.expression)) + else if(option === 'copyURL') + copyURL(toFullAPIURL(reference.uri)) + onMenuClose() + } + + return ( +
+
+ + + + + + +
+
+ + {reference.expression} + +
+
+ + + + + +
+
+ ) +} + +export default ReferenceHeader; diff --git a/src/components/references/ReferenceHome.jsx b/src/components/references/ReferenceHome.jsx new file mode 100644 index 000000000..99b12224d --- /dev/null +++ b/src/components/references/ReferenceHome.jsx @@ -0,0 +1,79 @@ +import React from 'react' + +import APIService from '../../services/APIService'; + +import ReferenceHeader from './ReferenceHeader' +import ReferenceDetails from './ReferenceDetails' +import ReferenceTabs from './ReferenceTabs' +import ReferenceExpansionResults from './ReferenceExpansionResults' + +const ReferenceHome = props => { + const { reference } = props + const [loading, setLoading] = React.useState(false) + const [tab, setTab] = React.useState('metadata') + const [concepts, setConcepts] = React.useState(false) + const [mappings, setMappings] = React.useState(false) + + const repoURL = props?.repo?.version_url || props?.repo?.url + + React.useEffect(() => { + setConcepts(false) + setMappings(false) + if(tab === 'expansion') + setTimeout(() => getResults(true), 100) + }, [reference?.id]) + + const onTabChange = newTab => { + setTab(newTab) + if(newTab === 'expansion') + getResults() + } + const getResults = (force=false) => { + if(reference.reference_type === 'mappings' && (force || !mappings?.length)) { + fetchMappings() + } + else if (force || !concepts?.length) { + fetchConcepts() + } + } + const getRefService = () => APIService.new().overrideURL(repoURL).appendToUrl(`references/${reference.id}/`) + + const fetchConcepts = (page=1) => { + let limit = 10 + let offset = limit * (page -1) + setLoading(true) + getRefService().appendToUrl('concepts/').get(null, null, {limit: limit, offset: offset, includeMappings: true, mappingBrief: true}).then(response => { + setConcepts(page === 1 ? response.data : [...(concepts || []), ...response.data]) + setLoading(false) + }) + } + + const fetchMappings = (page=1) => { + let limit = 10 + let offset = limit * (page -1) + setLoading(true) + getRefService().appendToUrl('mappings/').get(null, null, {limit: limit, offset: offset}).then(response => { + setMappings(page === 1 ? response.data : [...(mappings || []), ...response.data]) + setLoading(false) + }) + } + + + + return ( +
+ + onTabChange(newTab)} loading={loading} /> + { + tab === 'metadata' && + + } + { + tab === 'expansion' && + + } +
+ ) +} + +export default ReferenceHome diff --git a/src/components/references/ReferenceManagementList.jsx b/src/components/references/ReferenceManagementList.jsx new file mode 100644 index 000000000..ab61a8b2b --- /dev/null +++ b/src/components/references/ReferenceManagementList.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next' +import { Menu, ListItem, ListItemButton, ListItemText, ListItemIcon, Divider} from '@mui/material' +import CopyIcon from '@mui/icons-material/ContentCopy'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; + +const ReferenceManagementList = ({ anchorEl, open, onClose, id, onClick }) => { + const { t } = useTranslation() + return ( + + + onClick('copyExpression')} sx={{padding: '8px 12px'}}> + + + + + + + + onClick('copyURL')} sx={{padding: '8px 12px'}}> + + + + + + + + + onClick('delete')} sx={{padding: '8px 12px', color: 'error.main'}}> + + + + + + + + ) +} + +export default ReferenceManagementList; diff --git a/src/components/references/ReferenceTabs.jsx b/src/components/references/ReferenceTabs.jsx new file mode 100644 index 000000000..298930a70 --- /dev/null +++ b/src/components/references/ReferenceTabs.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next' +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; + +const TAB_STYLES = {textTransform: 'none'} +const ReferenceTabs = ({ tab, onTabChange }) => { + const { t } = useTranslation() + return ( + + + + + ) +} + +export default ReferenceTabs diff --git a/src/components/repos/RepoHome.jsx b/src/components/repos/RepoHome.jsx index 915d79f99..d365ef2f1 100644 --- a/src/components/repos/RepoHome.jsx +++ b/src/components/repos/RepoHome.jsx @@ -23,6 +23,7 @@ import RepoOverview from './RepoOverview' import VersionForm from './VersionForm' import ReleaseVersion from './ReleaseVersion' import RepoHeader from './RepoHeader'; +import ReferenceHome from '../references/ReferenceHome' const RepoHome = () => { const { t } = useTranslation() @@ -77,7 +78,7 @@ const RepoHome = () => { else setTabs([...TABS]) - if(isConceptURL || isMappingURL) + if(isConceptURL || isMappingURL || isReferenceURL) setShowItem(true) }) } @@ -232,7 +233,7 @@ const RepoHome = () => { const getReferenceURLFromMainURL = () => (isReferenceURL && params.resource) ? getURL() + 'references/' + params.resource + '/' : false const showConceptURL = ((showItem?.concept_class || params.resource) && isConceptURL) ? showItem?.version_url || showItem?.url || getConceptURLFromMainURL() : false const showMappingURL = ((showItem?.map_type || params.resource) && isMappingURL) ? showItem?.version_url || showItem?.url || getMappingURLFromMainURL() : false - const showReferenceURL = ((showItem?.expression || params.resource) && isReferenceURL) ? showItem?.version_url || showItem?.url || getReferenceURLFromMainURL() : false + const showReferenceURL = ((showItem?.expression || params.resource) && isReferenceURL) ? showItem?.uri || getReferenceURLFromMainURL() : false const isSplitView = conceptForm || mappingForm || showConceptURL || showMappingURL || showReferenceURL || versionForm const onVersionEditClick = () => isVersion && setVersionForm(true) @@ -303,9 +304,13 @@ const RepoHome = () => { setShowItem(false)} repoVersions={versions} nested /> } { - showMappingURL && + Boolean(showMappingURL && !mappingForm) && setShowItem(false)} repoVersions={versions} nested /> } + { + showReferenceURL && + setShowItem(false)} repoVersions={versions} nested /> + } { conceptForm && setConceptForm(false)} /> diff --git a/src/components/search/SearchResults.jsx b/src/components/search/SearchResults.jsx index 2368c3a4d..f8aff3ca7 100644 --- a/src/components/search/SearchResults.jsx +++ b/src/components/search/SearchResults.jsx @@ -149,13 +149,10 @@ const SearchResults = props => { }; const handleRowClick = (event, id) => { - if(props.resource === 'references') - return - event.preventDefault() event.stopPropagation() const item = rows.find(row => id == (row.version_url || row.url || row.id)) || false - if(['concepts', 'mappings'].includes(props.resource)) { + if(['concepts', 'mappings', 'references'].includes(props.resource)) { props.onShowItemSelect(item) } else if (props.resource === 'repos') { history.push(item.url) diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index 86fdebe4c..a69393b2e 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -252,7 +252,16 @@ }, "reference": { "references": "References", - "reference": "Reference" + "reference": "Reference", + "type": "Reference Type", + "translation": "Translation", + "last_resolved_at": "Last resolved at", + "unresolved": "unresolved", + "details": "Reference Details", + "expansion_results": "Expansion Results", + "concepts_and_mappings": "Concepts and Mappings", + "copy_expression": "Copy Reference Expression", + "copy_url": "Copy Reference URL" }, "checksums": { "standard": "Standard Checksum", From 8f9b7430408baa9bb571b0c040a9b61f9773ca04 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Tue, 28 Apr 2026 11:33:28 +0530 Subject: [PATCH 2/5] OpenConceptLab/ocl_issues#2123 | added pagination on concepts/mappings --- .../references/ReferenceExpansionResults.jsx | 118 ++++++++++-------- src/components/references/ReferenceHome.jsx | 45 +++++-- 2 files changed, 104 insertions(+), 59 deletions(-) diff --git a/src/components/references/ReferenceExpansionResults.jsx b/src/components/references/ReferenceExpansionResults.jsx index 1e7a7e780..116921764 100644 --- a/src/components/references/ReferenceExpansionResults.jsx +++ b/src/components/references/ReferenceExpansionResults.jsx @@ -6,6 +6,7 @@ import ListItemText from '@mui/material/ListItemText' import Paper from '@mui/material/Paper' import Typography from '@mui/material/Typography' import Table from '@mui/material/Table' +import TableContainer from '@mui/material/TableContainer' import TableHead from '@mui/material/TableHead' import TableBody from '@mui/material/TableBody' import TableRow from '@mui/material/TableRow' @@ -29,42 +30,49 @@ import { BLACK } from '../../common/colors' const borderColor = 'rgba(0, 0, 0, 0.12)' -const MappingsTable = ({mappings, loading, t}) => { +const MappingsTable = ({mappings, loading, t, hasMore, onLoadMore}) => { return ( {t('mapping.mappings')} + { + hasMore ? +
+ +
: + null + }
) } -const ConceptsAndMappingsTable = ({reference, concepts, loading, t}) => { +const ConceptsAndMappingsTable = ({reference, concepts, loading, t, hasMore, onLoadMore}) => { const [open, setOpen] = React.useState([]) const toggleRow = conceptURL => { setOpen(open.includes(conceptURL) ? without(open, conceptURL) : [...open, conceptURL]) } - return ( - {t('reference.concepts_and_mappings')} - - - - - {t('common.id')} - {t('concept.display_name')} - {t('mapping.mappings')} - {t('repo.repo')} - - - - { - loading ? - <> - { + {t('reference.concepts_and_mappings')} + + +
+ + + {t('common.id')} + {t('concept.display_name')} + {t('mapping.mappings')} + {t('repo.repo')} + + + + { + (loading && !concepts?.length) ? + <> + { times((reference.concepts || 1), i => ( @@ -72,17 +80,17 @@ const ConceptsAndMappingsTable = ({reference, concepts, loading, t}) => { )) - } - : - <> - { - map(concepts, (concept, index) => { - let key = concept.version_url || concept.url - const hasMappings = concept.mappings?.length > 0 - const isOpen = open.includes(key) - const isLastConcept = index === (concepts?.length || 0) - 1 - return ( - + } + : + <> + { + map(concepts, (concept, index) => { + let key = concept.version_url || concept.url + const hasMappings = concept.mappings?.length > 0 + const isOpen = open.includes(key) + const isLastConcept = index === (concepts?.length || 0) - 1 + return ( + .MuiTableCell-root': { @@ -98,9 +106,9 @@ const ConceptsAndMappingsTable = ({reference, concepts, loading, t}) => { > - - {concept.id} - + + {concept.id} + {concept.display_name} @@ -126,26 +134,34 @@ const ConceptsAndMappingsTable = ({reference, concepts, loading, t}) => { } : undefined} > - - - - - - + + + + + + - - ) - }) - } - - } - -
+
+ ) + }) + } + + } + + + + { + hasMore ? +
+ +
: + null + } ) } -const ReferenceExpansionResults = ({ reference, concepts, mappings, loading }) => { +const ReferenceExpansionResults = ({ reference, concepts, mappings, conceptHeaders, mappingHeaders, loading, onLoadMore }) => { const { t } = useTranslation() const statSx = { @@ -160,6 +176,8 @@ const ReferenceExpansionResults = ({ reference, concepts, mappings, loading }) = } const isMappingsOnly = Boolean(!reference.concepts && reference.mappings) + const hasMoreConcepts = Boolean(conceptHeaders?.next) + const hasMoreMappings = Boolean(mappingHeaders?.next) return (
@@ -172,8 +190,8 @@ const ReferenceExpansionResults = ({ reference, concepts, mappings, loading }) = { isMappingsOnly ? - : - + : + }
diff --git a/src/components/references/ReferenceHome.jsx b/src/components/references/ReferenceHome.jsx index 99b12224d..f11a67d53 100644 --- a/src/components/references/ReferenceHome.jsx +++ b/src/components/references/ReferenceHome.jsx @@ -12,13 +12,17 @@ const ReferenceHome = props => { const [loading, setLoading] = React.useState(false) const [tab, setTab] = React.useState('metadata') const [concepts, setConcepts] = React.useState(false) + const [conceptHeaders, setConceptHeaders] = React.useState(false) const [mappings, setMappings] = React.useState(false) + const [mappingHeaders, setMappingHeaders] = React.useState(false) const repoURL = props?.repo?.version_url || props?.repo?.url React.useEffect(() => { setConcepts(false) setMappings(false) + setConceptHeaders(false) + setMappingHeaders(false) if(tab === 'expansion') setTimeout(() => getResults(true), 100) }, [reference?.id]) @@ -38,26 +42,49 @@ const ReferenceHome = props => { } const getRefService = () => APIService.new().overrideURL(repoURL).appendToUrl(`references/${reference.id}/`) - const fetchConcepts = (page=1) => { - let limit = 10 - let offset = limit * (page -1) + const fetchConcepts = () => { + const { limit, page } = getLimits(conceptHeaders) + if(limit === 0) + return + setLoading(true) - getRefService().appendToUrl('concepts/').get(null, null, {limit: limit, offset: offset, includeMappings: true, mappingBrief: true}).then(response => { + getRefService().appendToUrl('concepts/').get(null, null, {limit: limit, page: page, includeMappings: true, mappingBrief: true}).then(response => { setConcepts(page === 1 ? response.data : [...(concepts || []), ...response.data]) + setConceptHeaders(response.headers) setLoading(false) }) } - const fetchMappings = (page=1) => { - let limit = 10 - let offset = limit * (page -1) + const fetchMappings = () => { + const { limit, page } = getLimits(mappingHeaders) + if(limit === 0) + return + setLoading(true) - getRefService().appendToUrl('mappings/').get(null, null, {limit: limit, offset: offset}).then(response => { + getRefService().appendToUrl('mappings/').get(null, null, {limit: limit, page: page}).then(response => { setMappings(page === 1 ? response.data : [...(mappings || []), ...response.data]) + setMappingHeaders(response.headers) setLoading(false) }) } + const getLimits = headers => { + if(headers?.page_number && !headers?.next) + return { limit: 0, page: 0 } + + return { + limit: 10, + page: (parseInt(headers?.page_number || 0, 10)) + 1 + } + } + + const onLoadMore = (resource) => { + if(resource === 'concepts') + fetchConcepts() + else if (resource === 'mappings') + fetchMappings() + } + return ( @@ -70,7 +97,7 @@ const ReferenceHome = props => { } { tab === 'expansion' && - + } ) From c4ac2199914f726a9cf0bcad184c0f8fb885a02b Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Tue, 28 Apr 2026 11:38:32 +0530 Subject: [PATCH 3/5] OpenConceptLab/ocl_issues#2123 | fixing ref change when loaded --- src/components/references/ReferenceHome.jsx | 43 ++++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/references/ReferenceHome.jsx b/src/components/references/ReferenceHome.jsx index f11a67d53..ae8e955f6 100644 --- a/src/components/references/ReferenceHome.jsx +++ b/src/components/references/ReferenceHome.jsx @@ -15,54 +15,69 @@ const ReferenceHome = props => { const [conceptHeaders, setConceptHeaders] = React.useState(false) const [mappings, setMappings] = React.useState(false) const [mappingHeaders, setMappingHeaders] = React.useState(false) + const activeReferenceIdRef = React.useRef(reference?.id) const repoURL = props?.repo?.version_url || props?.repo?.url - React.useEffect(() => { + const resetExpansionState = () => { + setLoading(false) setConcepts(false) setMappings(false) setConceptHeaders(false) setMappingHeaders(false) - if(tab === 'expansion') - setTimeout(() => getResults(true), 100) + } + + React.useEffect(() => { + activeReferenceIdRef.current = reference?.id + resetExpansionState() }, [reference?.id]) + React.useEffect(() => { + if(tab === 'expansion') + getResults({ force: true, reset: true, currentReferenceId: reference?.id }) + }, [reference?.id, tab]) + const onTabChange = newTab => { setTab(newTab) - if(newTab === 'expansion') - getResults() } - const getResults = (force=false) => { + + const getResults = ({ force=false, reset=false, currentReferenceId=reference?.id } = {}) => { if(reference.reference_type === 'mappings' && (force || !mappings?.length)) { - fetchMappings() + fetchMappings({ reset, currentReferenceId }) } else if (force || !concepts?.length) { - fetchConcepts() + fetchConcepts({ reset, currentReferenceId }) } } const getRefService = () => APIService.new().overrideURL(repoURL).appendToUrl(`references/${reference.id}/`) - const fetchConcepts = () => { - const { limit, page } = getLimits(conceptHeaders) + const fetchConcepts = ({ reset=false, currentReferenceId=reference?.id } = {}) => { + const { limit, page } = getLimits(reset ? false : conceptHeaders) if(limit === 0) return setLoading(true) getRefService().appendToUrl('concepts/').get(null, null, {limit: limit, page: page, includeMappings: true, mappingBrief: true}).then(response => { - setConcepts(page === 1 ? response.data : [...(concepts || []), ...response.data]) + if(activeReferenceIdRef.current !== currentReferenceId) + return + + setConcepts(currentConcepts => (page === 1 ? response.data : [...(currentConcepts || []), ...response.data])) setConceptHeaders(response.headers) setLoading(false) }) } - const fetchMappings = () => { - const { limit, page } = getLimits(mappingHeaders) + const fetchMappings = ({ reset=false, currentReferenceId=reference?.id } = {}) => { + const { limit, page } = getLimits(reset ? false : mappingHeaders) if(limit === 0) return setLoading(true) getRefService().appendToUrl('mappings/').get(null, null, {limit: limit, page: page}).then(response => { - setMappings(page === 1 ? response.data : [...(mappings || []), ...response.data]) + if(activeReferenceIdRef.current !== currentReferenceId) + return + + setMappings(currentMappings => (page === 1 ? response.data : [...(currentMappings || []), ...response.data])) setMappingHeaders(response.headers) setLoading(false) }) From cc807f34c3b87fb434c730a7e3331f41ef6f17fa Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Tue, 28 Apr 2026 11:41:01 +0530 Subject: [PATCH 4/5] OpenConceptLab/ocl_issues#2123 | removed duplicate border bottom --- src/components/references/ReferenceExpansionResults.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/references/ReferenceExpansionResults.jsx b/src/components/references/ReferenceExpansionResults.jsx index 116921764..f25dbbb08 100644 --- a/src/components/references/ReferenceExpansionResults.jsx +++ b/src/components/references/ReferenceExpansionResults.jsx @@ -133,7 +133,7 @@ const ConceptsAndMappingsTable = ({reference, concepts, loading, t, hasMore, onL } } : undefined} > - + From 48610be910010bc9fe33524a48cd3b80188576c7 Mon Sep 17 00:00:00 2001 From: Sunny Aggarwal Date: Tue, 28 Apr 2026 11:41:15 +0530 Subject: [PATCH 5/5] OpenConceptLab/ocl_issues#2123 | added translations --- src/i18n/locales/en/translations.json | 1 + src/i18n/locales/es/translations.json | 3 ++- src/i18n/locales/zh/translations.json | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/en/translations.json b/src/i18n/locales/en/translations.json index a69393b2e..6172834bb 100644 --- a/src/i18n/locales/en/translations.json +++ b/src/i18n/locales/en/translations.json @@ -111,6 +111,7 @@ "your_url_will_be": "Your URL will be", "proceed": "Proceed", "loading": "Loading...", + "load_more": "Load more", "release": "Release", "unrelease": "Un-Release", "reason": "Reason", diff --git a/src/i18n/locales/es/translations.json b/src/i18n/locales/es/translations.json index 0c8f55997..cbff07ee4 100644 --- a/src/i18n/locales/es/translations.json +++ b/src/i18n/locales/es/translations.json @@ -29,7 +29,8 @@ "navigate": "Navegar", "select": "Seleccionar", "search": "Buscar", - "for": "para" + "for": "para", + "load_more": "Cargar más" }, "errors": { "404": "Lo siento, no se pudo encontrar tu página." diff --git a/src/i18n/locales/zh/translations.json b/src/i18n/locales/zh/translations.json index c448ab7de..04f8739b2 100644 --- a/src/i18n/locales/zh/translations.json +++ b/src/i18n/locales/zh/translations.json @@ -87,7 +87,8 @@ "from": "始于", "target": "目标", "properties": "特性", - "custom": "自定义" + "custom": "自定义", + "load_more": "加载更多" }, "errors": { "404": "很抱歉,未能找到您的页面。",