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..f25dbbb08
--- /dev/null
+++ b/src/components/references/ReferenceExpansionResults.jsx
@@ -0,0 +1,201 @@
+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 TableContainer from '@mui/material/TableContainer'
+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, hasMore, onLoadMore}) => {
+ return (
+
+
+ {t('mapping.mappings')}
+
+
+ {
+ hasMore ?
+
+
+
:
+ null
+ }
+
+ )
+}
+
+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 && !concepts?.length) ?
+ <>
+ {
+ 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}
+ >
+
+
+
+
+
+
+
+
+
+ )
+ })
+ }
+ >
+ }
+
+
+
+ {
+ hasMore ?
+
+
+
:
+ null
+ }
+
+ )
+}
+
+const ReferenceExpansionResults = ({ reference, concepts, mappings, conceptHeaders, mappingHeaders, loading, onLoadMore }) => {
+ 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)
+ const hasMoreConcepts = Boolean(conceptHeaders?.next)
+ const hasMoreMappings = Boolean(mappingHeaders?.next)
+
+ 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}
+
+
+
+
+
+ } variant='text' sx={{textTransform: 'none', color: 'surface.contrastText'}} onClick={onMenuOpen} id='reference-actions'>
+ {t('common.actions')}
+
+
+
+
+
+ )
+}
+
+export default ReferenceHeader;
diff --git a/src/components/references/ReferenceHome.jsx b/src/components/references/ReferenceHome.jsx
new file mode 100644
index 000000000..ae8e955f6
--- /dev/null
+++ b/src/components/references/ReferenceHome.jsx
@@ -0,0 +1,121 @@
+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 [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
+
+ const resetExpansionState = () => {
+ setLoading(false)
+ setConcepts(false)
+ setMappings(false)
+ setConceptHeaders(false)
+ setMappingHeaders(false)
+ }
+
+ 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)
+ }
+
+ const getResults = ({ force=false, reset=false, currentReferenceId=reference?.id } = {}) => {
+ if(reference.reference_type === 'mappings' && (force || !mappings?.length)) {
+ fetchMappings({ reset, currentReferenceId })
+ }
+ else if (force || !concepts?.length) {
+ fetchConcepts({ reset, currentReferenceId })
+ }
+ }
+ const getRefService = () => APIService.new().overrideURL(repoURL).appendToUrl(`references/${reference.id}/`)
+
+ 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 => {
+ if(activeReferenceIdRef.current !== currentReferenceId)
+ return
+
+ setConcepts(currentConcepts => (page === 1 ? response.data : [...(currentConcepts || []), ...response.data]))
+ setConceptHeaders(response.headers)
+ setLoading(false)
+ })
+ }
+
+ 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 => {
+ if(activeReferenceIdRef.current !== currentReferenceId)
+ return
+
+ setMappings(currentMappings => (page === 1 ? response.data : [...(currentMappings || []), ...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 (
+
+
+ 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 (
+
+ )
+}
+
+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 6021d6185..8dcac5da6 100644
--- a/src/components/search/SearchResults.jsx
+++ b/src/components/search/SearchResults.jsx
@@ -159,13 +159,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 18bb140de..6d9195f13 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",
@@ -255,6 +256,14 @@
"expression": "Expression",
"references": "References",
"reference": "Reference",
+ "type": "Reference Type",
+ "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",
"reference_type": "Ref Type",
"exclude": "Exclude",
"intensional": "Intensional",
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": "很抱歉,未能找到您的页面。",