From eba107ef969b56be04cee795d1592e2ea598120d Mon Sep 17 00:00:00 2001 From: Akhil Kala Date: Mon, 6 Sep 2021 14:01:23 +0530 Subject: [PATCH 1/6] Common comparison component added --- src/components/app/App.jsx | 20 +- src/components/common/Comparison.jsx | 376 +++++++++++ .../ComparisonAttributes.jsx | 0 .../concepts/ConceptsComparison.jsx | 589 +++++------------- .../mappings/MappingsComparison.jsx | 139 +++++ .../search/SelectedResourceControls.jsx | 4 +- 6 files changed, 684 insertions(+), 444 deletions(-) create mode 100644 src/components/common/Comparison.jsx rename src/components/{concepts => common}/ComparisonAttributes.jsx (100%) create mode 100644 src/components/mappings/MappingsComparison.jsx diff --git a/src/components/app/App.jsx b/src/components/app/App.jsx index 5c6f9f9c..86429cbe 100644 --- a/src/components/app/App.jsx +++ b/src/components/app/App.jsx @@ -6,10 +6,11 @@ import { get } from 'lodash'; import { isFHIRServer, isLoggedIn } from '../../common/utils'; import Search from '../search/Search'; import ConceptHome from '../concepts/ConceptHome'; -import ConceptsComparison from '../concepts/ConceptsComparison'; import MappingHome from '../mappings/MappingHome'; import SourceHome from '../sources/SourceHome'; import CollectionHome from '../collections/CollectionHome'; +import ConceptsComparison from '../concepts/ConceptsComparison'; +import MappingsComparison from '../mappings/MappingsComparison'; import OrgHome from '../orgs/OrgHome'; import UserHome from '../users/UserHome'; import Login from '../users/Login'; @@ -107,11 +108,6 @@ const App = props => { path="/orgs/:org([a-zA-Z0-9\-\.\_\@]+)/sources/:source([a-zA-Z0-9\-\.\_\@]+)/:version([a-zA-Z0-9\-\.\_\@]+)/concepts/:concept" component={ConceptHome} /> - { /* Mapping Home */ } { component={CollectionHome} /> + {/* Comparison */} + + + {/* Organization Home */} diff --git a/src/components/common/Comparison.jsx b/src/components/common/Comparison.jsx new file mode 100644 index 00000000..6c0796a4 --- /dev/null +++ b/src/components/common/Comparison.jsx @@ -0,0 +1,376 @@ +import React from 'react'; +import ReactDiffViewer from 'react-diff-viewer'; +import { Link } from 'react-router-dom'; +import { + TableContainer, Table, TableHead, TableBody, TableCell, TableRow, + CircularProgress, IconButton, Tooltip +} from '@material-ui/core'; +import { + ArrowDropDown as ArrowDownIcon, ArrowDropUp as ArrowUpIcon, + Settings as SettingsIcon, +} from '@material-ui/icons'; +import { + get, startCase, map, isEmpty, isEqual, size, keys, maxBy, pickBy, forEach, includes, has, values +} from 'lodash'; +import { + formatDate, toParentURI, sortObjectBy, + memorySizeOf, formatByteSize +} from '../../common/utils'; +import { + DIFF_BG_RED, +} from '../../common/constants'; +import ComparisonAttributes from './ComparisonAttributes'; +import ExtrasDiff from '../common/ExtrasDiff'; + +class Comparison extends React.Component { + constructor(props) { + super(props); + this.state = { + isVersion: false, + isLoadingLHS: true, + isLoadingRHS: true, + lhs: {}, + rhs: {}, + drawer: false, + attributes: {} + } + } + + componentDidMount() { + this.setObjectsForComparison() + } + + componentDidUpdate(prevProps) { + if(prevProps.search !== this.props.search) + this.setObjectsForComparison() + } + + onDrawerClick = () => { + this.setState({drawer: !this.state.drawer}) + } + + reorder = (startIndex, endIndex) => { + const { attributes } = this.state; + const attrs = keys(attributes); + const result = Array.from(attrs); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + const orderedAttrs = {}; + forEach(result, (attr, index) => { + orderedAttrs[attr] = attributes[attr] + orderedAttrs[attr].position = index + 1 + }) + + return orderedAttrs; + }; + + + onAttributeDragEnd = result => { + if(result.destination && result.source.index !== result.destination.index) + this.setState({attributes: this.reorder(result.source.index, result.destination.index)}) + } + + onToggleAttributeClick = attr => { + this.setState({ + attributes: { + ...this.state.attributes, + [attr]: { + ...this.state.attributes[attr], + show: !this.state.attributes[attr].show + } + } + }) + } + + onCollapseIconClick(attr) { + this.setState({ + attributes: { + ...this.state.attributes, + [attr]: { + ...this.state.attributes[attr], + collapsed: !this.state.attributes[attr].collapsed + } + } + }) + } + + setObjectsForComparison() { + const queryParams = new URLSearchParams(this.props.search) + this.props.fetcher(queryParams.get('lhs'), 'lhs', 'isLoadingLHS', this.state).then((lhsData) => { + this.props.fetcher(queryParams.get('rhs'), 'rhs', 'isLoadingRHS', lhsData).then((data) => { + this.setState(data, ()=>{ + if(!this.props.postFetch) return + const formatted = this.props.postFetch(data) + this.setState(formatted) + }) + }) + }) + } + + getAttributeValue(concept, attr, type) { + let value = get(concept, attr) + if (attr === 'extras') + return JSON.stringify(value, undefined, 2) + if(type === 'list') { + if(isEmpty(value)) return ''; + else return value + } else if(type === 'date') { + if(attr === 'created_on') + value ||= get(concept, 'created_at') + if(attr === 'updated_on') + value ||= get(concept, 'updated_at') + + return value ? formatDate(value) : ''; + } else if (type === 'textFormatted') { + if(attr === 'owner') + return `${concept.owner_type}: ${concept.owner}` + } else if (type === 'bool') { + return value ? 'True' : 'False' + } else { + if(includes(['created_by', 'updated_by'], attr)) + value ||= get(concept, `version_${attr}`) + if(attr === 'updated_by' && has(concept, 'version_created_by')) + value ||= concept.version_created_by + return value || ''; + } + } + + getExtraAttributeLabel(val) { + if(!val) + return '' + return `${keys(val)[0]}: ${JSON.stringify(values(val)[0])}` + } + + getListAttributeValue(attr, val) { + if(includes(['extras'], attr)) + return this.getExtraAttributeLabel(val) + if(includes(['parent_concept_urls', 'child_concept_urls'], attr)) + return val + } + + getHeaderSubAttributes(concept) { + if (!this.props.getHeaderSubAttributeValues) return null + return ( +
+ {this.props.getHeaderSubAttributeValues(concept, this.state.isVersion).map((attribute, i) => { + if(attribute.url) { + return ( + + {attribute.name} + + {attribute.value} + + + ) + } + else{ + return ( + + {attribute.name} + {attribute.value} + + ) + } + })} +
+ ) + } + + getHeaderCell = resource => { + return ( + +
+ {this.getHeaderSubAttributes(resource)} +
+
+ {resource.display_name || resource.id} +
+
+ ) + } + + maxArrayElement(v1, v2) { + return maxBy([v1, v2], size) + } + + getAttributeDOM(attr, type, lhsValue, rhsValue, isDiff) { + const { lhs, rhs } = this.state; + const maxLengthAttr = type === 'list' ? this.maxArrayElement(get(lhs, attr), get(rhs, attr)) : []; + const rowSpan = size(maxLengthAttr); + const isExtras = attr === 'extras'; + return ( + + { + isExtras ? + : + ( + type === 'list' ? + map(maxLengthAttr, (_attr, index) => { + const _lhsVal = get(lhs, `${attr}.${index}`, '') + const _rhsVal = get(rhs, `${attr}.${index}`, '') + const _lhsValCleaned = this.props.getListAttributeValue ? this.props.getListAttributeValue(attr, _lhsVal): this.getListAttributeValue(attr, _lhsVal) + const _rhsValCleaned = this.props.getListAttributeValue ? this.props.getListAttributeValue(attr, _rhsVal): this.getListAttributeValue(attr, _rhsVal) + const _isDiff = !isEqual(_lhsValCleaned, _rhsValCleaned); + return ( + + { + index === 0 && + + {type !== 'list' && startCase(attr)} + + } + { + _isDiff ? + + + : + + + {this.props.getListAttributeValue ? this.props.getListAttributeValue(attr, _lhsVal, true): this.getListAttributeValue(attr, _lhsVal)} + + + {this.props.getListAttributeValue ? this.props.getListAttributeValue(attr, _rhsVal, true): this.getListAttributeValue(attr, _rhsVal)} + + + } + + ) + }) : + + + {startCase(attr)} + + { + isDiff ? + + + : + + + {this.props.getAttributeValue ? this.props.getAttributeValue(lhs, attr, type, true): this.getAttributeValue(lhs, attr, type) } + + + {this.props.getAttributeValue ? this.props.getAttributeValue(rhs, attr, type, true): this.getAttributeValue(rhs, attr, type)} + + + } + + ) + } + + ) + } + + render() { + const { lhs, rhs, isLoadingLHS, isLoadingRHS, attributes, drawer } = this.state; + const isLoading = isLoadingLHS || isLoadingRHS; + const visibleAttributes = sortObjectBy(pickBy(attributes, {show: true}), config => config.position) + return ( + + { + isLoading ? +
+ +
: +
+ + + + + + + + + + + + { + map([lhs, rhs], this.getHeaderCell) + } + + + + { + map(visibleAttributes, (config, attr) => { + const type = config.type; + const lhsValue = this.props.getAttributeValue ? this.props.getAttributeValue(lhs, attr, type): this.getAttributeValue(lhs, attr, type) + const rhsValue = this.props.getAttributeValue ? this.props.getAttributeValue(rhs, attr, type): this.getAttributeValue(rhs, attr, type) + const isDiff = !isEqual(lhsValue, rhsValue); + const children = this.getAttributeDOM(attr, type, lhsValue, rhsValue, isDiff); + if(type === 'list') { + const lhsRawValue = lhs[attr]; + const rhsRawValue = rhs[attr]; + const lhsCount = lhsRawValue.length; + const rhsCount = rhsRawValue.length; + const hasKids = Boolean(lhsCount || rhsCount); + const styles = isDiff ? {background: DIFF_BG_RED} : {}; + const isExpanded = !config.collapsed || !hasKids; + const isExtras = attr === 'extras'; + let lhsSize, rhsSize; + let size = ''; + if(isExtras && (!isEmpty(lhsRawValue) || !isEmpty(rhsRawValue))) { + lhsSize = memorySizeOf(lhsRawValue, false) + rhsSize = memorySizeOf(rhsRawValue, false) + const totalSize = lhsSize + rhsSize; + const tooMany = totalSize/1024 >= 99; // More than 95KB + size = `${formatByteSize(totalSize)}`; + size = tooMany ? `${size} (this may take some time)` : size + } + return ( + + this.onCollapseIconClick(attr)} style={{cursor: 'pointer'}}> + + + {`${startCase(attr)} (${lhsCount}/${rhsCount})`} + { size && {size} } + { + isExpanded ? : + } + + + + { + isExpanded && + + {children} + + } + + ) + } else { + return children; + } + }) + } + +
+
+
+ } + +
+ ) + } +} + +export default Comparison; diff --git a/src/components/concepts/ComparisonAttributes.jsx b/src/components/common/ComparisonAttributes.jsx similarity index 100% rename from src/components/concepts/ComparisonAttributes.jsx rename to src/components/common/ComparisonAttributes.jsx diff --git a/src/components/concepts/ConceptsComparison.jsx b/src/components/concepts/ConceptsComparison.jsx index dad33be2..e1c60068 100644 --- a/src/components/concepts/ConceptsComparison.jsx +++ b/src/components/concepts/ConceptsComparison.jsx @@ -1,28 +1,9 @@ -import React from 'react'; -import ReactDiffViewer from 'react-diff-viewer'; -import { Link } from 'react-router-dom'; -import { - TableContainer, Table, TableHead, TableBody, TableCell, TableRow, - CircularProgress, IconButton, Tooltip -} from '@material-ui/core'; -import { - ArrowDropDown as ArrowDownIcon, ArrowDropUp as ArrowUpIcon, - Settings as SettingsIcon, -} from '@material-ui/icons'; -import { - get, startCase, map, isEmpty, includes, isEqual, size, filter, reject, keys, values, - sortBy, findIndex, uniqBy, has, maxBy, cloneDeep, pickBy, forEach -} from 'lodash'; +import React from 'react' +import { cloneDeep, get, map, isEmpty, sortBy, filter, reject, uniqBy, findIndex, includes, has, keys, values } from 'lodash'; +import Comparison from '../common/Comparison' import APIService from '../../services/APIService'; -import { - formatDate, toObjectArray, toParentURI, sortObjectBy, - memorySizeOf, formatByteSize -} from '../../common/utils'; -import { - DIFF_BG_RED, -} from '../../common/constants'; -import ComparisonAttributes from './ComparisonAttributes'; -import ExtrasDiff from '../common/ExtrasDiff'; +import { toObjectArray, toParentURI, formatDate } from '../../common/utils'; +import { useLocation } from 'react-router-dom'; const getLocaleLabelExpanded = (locale, formatted=false) => { if(!locale) @@ -70,435 +51,171 @@ const getMappingLabel = (mapping, formatted=false) => { return label } - -class ConceptsComparison extends React.Component { - constructor(props) { - super(props); - this.attributeState = {show: true, type: 'text'} - this.state = { - isVersion: false, - isLoadingLHS: true, - isLoadingRHS: true, - lhs: {}, - rhs: {}, - drawer: false, - attributes: { - datatype: {...cloneDeep(this.attributeState), position: 1}, - display_locale: {...cloneDeep(this.attributeState), position: 2}, - external_id: {...cloneDeep(this.attributeState), position: 3}, - owner: {...cloneDeep(this.attributeState), type: 'textFormatted', position: 4}, - names: {...cloneDeep(this.attributeState), collapsed: true, type: 'list', position: 5}, - descriptions: {...cloneDeep(this.attributeState), collapsed: true, type: 'list', position: 6}, - parent_concept_urls: {...cloneDeep(this.attributeState), collapsed: true, type: 'list', position: 14}, - child_concept_urls: {...cloneDeep(this.attributeState), collapsed: true, type: 'list', position: 15}, - mappings: {...cloneDeep(this.attributeState), collapsed: true, type: 'list', position: 7}, - extras: {...cloneDeep(this.attributeState), collapsed: true, type: 'list', position: 8}, - retired: {...cloneDeep(this.attributeState), type: 'bool', position: 9}, - created_by: {...cloneDeep(this.attributeState), position: 10}, - updated_by: {...cloneDeep(this.attributeState), position: 11}, - created_on: {...cloneDeep(this.attributeState), type: 'date', position: 12}, - updated_on: {...cloneDeep(this.attributeState), type: 'date', position: 13}, - }, +export default function ConceptsComparison() { + const location = useLocation() + const attributeState = {show: true, type: 'text'} + const attributes = { + datatype: {...cloneDeep(attributeState), position: 1}, + display_locale: {...cloneDeep(attributeState), position: 2}, + external_id: {...cloneDeep(attributeState), position: 3}, + owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 4}, + names: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 5}, + descriptions: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 6}, + parent_concept_urls: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 14}, + child_concept_urls: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 15}, + mappings: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 7}, + extras: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 8}, + retired: {...cloneDeep(attributeState), type: 'bool', position: 9}, + created_by: {...cloneDeep(attributeState), position: 10}, + updated_by: {...cloneDeep(attributeState), position: 11}, + created_on: {...cloneDeep(attributeState), type: 'date', position: 12}, + updated_on: {...cloneDeep(attributeState), type: 'date', position: 13}, } - } - - componentDidMount() { - this.setObjectsForComparison() - } - - componentDidUpdate(prevProps) { - if(prevProps.location.search !== this.props.location.search) - this.setObjectsForComparison() - } - - onDrawerClick = () => { - this.setState({drawer: !this.state.drawer}) - } - - reorder = (startIndex, endIndex) => { - const { attributes } = this.state; - const attrs = keys(attributes); - const result = Array.from(attrs); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - const orderedAttrs = {}; - forEach(result, (attr, index) => { - orderedAttrs[attr] = attributes[attr] - orderedAttrs[attr].position = index + 1 - }) - return orderedAttrs; - }; + const fetcher = (uri, attr, loadingAttr, state) => { + if(uri && attr && loadingAttr) { + const { isVersion } = state; + const isAnyVersion = isVersion || uri.match(/\//g).length === 8; + return APIService + .new() + .overrideURL(encodeURI(uri)) + .get(null, null, {includeInverseMappings: true, includeHierarchyPath: true, includeParentConceptURLs: true, includeChildConceptURLs: true}) + .then(response => { + if(get(response, 'status') === 200) { + const newState = {...state} + newState[attr] = formatter(response.data) + newState[loadingAttr] = false + newState.isVersion = isAnyVersion + newState.attributes = attributes + if(isAnyVersion) { + newState.attributes['is_latest_version'] = {...cloneDeep(attributeState), type: 'bool', position: 14} + newState.attributes['update_comment'] = {...cloneDeep(attributeState), position: 15} + } + return newState + } + }) + } + } + const formatter = (concept) => { + concept.names = sortLocales(concept.names) + concept.descriptions = sortLocales(concept.descriptions) + concept.originalExtras = concept.extras + concept.extras = toObjectArray(concept.extras) + return concept + } - onAttributeDragEnd = result => { - if(result.destination && result.source.index !== result.destination.index) - this.setState({attributes: this.reorder(result.source.index, result.destination.index)}) - } + const sortLocales = locales => { + return sortBy([ + ...filter(locales, {name_type: 'FULLY_SPECIFIED', locale_preferred: true}), + ...filter(reject(locales, {name_type: 'FULLY_SPECIFIED'}), {locale_preferred: true}), + ...filter(locales, {name_type: 'FULLY_SPECIFIED', locale_preferred: false}), + ...reject(reject(locales, {name_type: 'FULLY_SPECIFIED'}), {locale_preferred: true}), + ], 'locale') + } - onToggleAttributeClick = attr => { - this.setState({ - attributes: { - ...this.state.attributes, - [attr]: { - ...this.state.attributes[attr], - show: !this.state.attributes[attr].show + const sortMappings = (state) => { + if(!isEmpty(get(state.lhs, 'mappings')) && !isEmpty(get(state.rhs, 'mappings'))) { + const newState = {...state}; + if(newState.lhs.mappings.length > newState.rhs.mappings.length) { + newState.lhs.mappings = uniqBy([...sortBy( + newState.rhs.mappings, m1 => findIndex(newState.lhs.mappings, m2 => m1.id === m2.id) + ), ...newState.lhs.mappings], 'id') + } else { + newState.rhs.mappings = uniqBy([...sortBy( + newState.lhs.mappings, m1 => findIndex(newState.rhs.mappings, m2 => m1.id === m2.id) + ), ...newState.rhs.mappings], 'id') } - } - }) - } - onCollapseIconClick(attr) { - this.setState({ - attributes: { - ...this.state.attributes, - [attr]: { - ...this.state.attributes[attr], - collapsed: !this.state.attributes[attr].collapsed - } + return newState } - }) - } + } - setObjectsForComparison() { - const queryParams = new URLSearchParams(this.props.location.search) - this.fetchConcept(queryParams.get('lhs'), 'lhs', 'isLoadingLHS') - this.fetchConcept(queryParams.get('rhs'), 'rhs', 'isLoadingRHS') - } + const getHeaderSubAttributeValues = (concept, isVersion) => { + const attributes = [ + { + name: "Source:", + value: concept.source, + url: toParentURI(concept.url) + }, + { + name: "Type:", + value: concept.concept_class, + url: null + }, + { + name: "UID:", + value: concept.id, + url: null + }, + ] + if (isVersion) { + attributes.push({ + name: "VERSION:", + value: concept.version, + url: null + }) + } - fetchConcept(uri, attr, loadingAttr) { - if(uri && attr && loadingAttr) { - const { isVersion } = this.state; - const isAnyVersion = isVersion || uri.match(/\//g).length === 8; - APIService - .new() - .overrideURL(encodeURI(uri)) - .get(null, null, {includeInverseMappings: true, includeHierarchyPath: true, includeParentConceptURLs: true, includeChildConceptURLs: true}) - .then(response => { - if(get(response, 'status') === 200) { - const newState = {...this.state} - newState[attr] = this.formatConcept(response.data) - newState[loadingAttr] = false - newState.isVersion = isAnyVersion - if(isAnyVersion) { - newState.attributes['is_latest_version'] = {...cloneDeep(this.attributeState), type: 'bool', position: 14} - newState.attributes['update_comment'] = {...cloneDeep(this.attributeState), position: 15} - } - this.setState(newState, this.sortMappings) - } - }) + return attributes } - } - - formatConcept(concept) { - concept.names = this.sortLocales(concept.names) - concept.descriptions = this.sortLocales(concept.descriptions) - concept.originalExtras = concept.extras - concept.extras = toObjectArray(concept.extras) - return concept - } - sortMappings() { - if(!isEmpty(get(this.state.lhs, 'mappings')) && !isEmpty(get(this.state.rhs, 'mappings'))) { - const newState = {...this.state}; - if(newState.lhs.mappings.length > newState.rhs.mappings.length) { - newState.lhs.mappings = uniqBy([...sortBy( - newState.rhs.mappings, m1 => findIndex(newState.lhs.mappings, m2 => m1.id === m2.id) - ), ...newState.lhs.mappings], 'id') + const getAttributeValue = (concept, attr, type, formatted=false) => { + let value = get(concept, attr) + if (attr === 'extras') + return JSON.stringify(value, undefined, 2) + if(type === 'list') { + if(isEmpty(value)) return ''; + if(includes(['names', 'descriptions'], attr)) + return map(value, locale => getLocaleLabelExpanded(locale, formatted)) + if (attr === 'mappings') + return map(value, mapping => getMappingLabel(mapping, formatted)); + else + return value + } else if(type === 'date') { + if(attr === 'created_on') + value ||= get(concept, 'created_at') + if(attr === 'updated_on') + value ||= get(concept, 'updated_at') + + return value ? formatDate(value) : ''; + } else if (type === 'textFormatted') { + if(attr === 'owner') + return `${concept.owner_type}: ${concept.owner}` + } else if (type === 'bool') { + return value ? 'True' : 'False' } else { - newState.rhs.mappings = uniqBy([...sortBy( - newState.lhs.mappings, m1 => findIndex(newState.rhs.mappings, m2 => m1.id === m2.id) - ), ...newState.rhs.mappings], 'id') + if(includes(['created_by', 'updated_by'], attr)) + value ||= get(concept, `version_${attr}`) + if(attr === 'updated_by' && has(concept, 'version_created_by')) + value ||= concept.version_created_by + return value || ''; } - - this.setState(newState) } - } - - sortLocales = locales => { - return sortBy([ - ...filter(locales, {name_type: 'FULLY_SPECIFIED', locale_preferred: true}), - ...filter(reject(locales, {name_type: 'FULLY_SPECIFIED'}), {locale_preferred: true}), - ...filter(locales, {name_type: 'FULLY_SPECIFIED', locale_preferred: false}), - ...reject(reject(locales, {name_type: 'FULLY_SPECIFIED'}), {locale_preferred: true}), - ], 'locale') - } - - getHeaderSubAttributes(concept) { - return ( - -
- - Source: - - {concept.source} - - - - Type: - {concept.concept_class} - - - UID: - {concept.id} - - { - this.state.isVersion && - - VERSION: - {concept.version} - - } -
-
- ) - } - getValue(concept, attr, type, formatted=false) { - let value = get(concept, attr) - if (attr === 'extras') - return JSON.stringify(value, undefined, 2) - if(type === 'list') { - if(isEmpty(value)) return ''; + const getExtraAttributeLabel = (val) => { + if(!val) + return '' + return `${keys(val)[0]}: ${JSON.stringify(values(val)[0])}` + } + + const getListAttributeValue = (attr, val, formatted=false) => { if(includes(['names', 'descriptions'], attr)) - return map(value, locale => getLocaleLabelExpanded(locale, formatted)) - if (attr === 'mappings') - return map(value, mapping => getMappingLabel(mapping, formatted)); - else - return value - } else if(type === 'date') { - if(attr === 'created_on') - value ||= get(concept, 'created_at') - if(attr === 'updated_on') - value ||= get(concept, 'updated_at') - - return value ? formatDate(value) : ''; - } else if (type === 'textFormatted') { - if(attr === 'owner') - return `${concept.owner_type}: ${concept.owner}` - } else if (type === 'bool') { - return value ? 'True' : 'False' - } else { - if(includes(['created_by', 'updated_by'], attr)) - value ||= get(concept, `version_${attr}`) - if(attr === 'updated_by' && has(concept, 'version_created_by')) - value ||= concept.version_created_by - return value || ''; + return getLocaleLabelExpanded(val, formatted) + if(includes(['mappings'], attr)) + return getMappingLabel(val, formatted) + if(includes(['extras'], attr)) + return getExtraAttributeLabel(val) + if(includes(['parent_concept_urls', 'child_concept_urls'], attr)) + return val } - } - - maxArrayElement(v1, v2) { - return maxBy([v1, v2], size) - } - - getListAttrValue(attr, val, formatted=false) { - if(includes(['names', 'descriptions'], attr)) - return getLocaleLabelExpanded(val, formatted) - if(includes(['mappings'], attr)) - return getMappingLabel(val, formatted) - if(includes(['extras'], attr)) - return this.getExtraAttributeLabel(val) - if(includes(['parent_concept_urls', 'child_concept_urls'], attr)) - return val - } - getExtraAttributeLabel(val) { - if(!val) - return '' - return `${keys(val)[0]}: ${JSON.stringify(values(val)[0])}` - } - - getAttributeDOM(attr, type, lhsValue, rhsValue, isDiff) { - const { lhs, rhs } = this.state; - const maxLengthAttr = type === 'list' ? this.maxArrayElement(get(lhs, attr), get(rhs, attr)) : []; - const rowSpan = size(maxLengthAttr); - const isExtras = attr === 'extras'; - return ( - - { - isExtras ? - : - ( - type === 'list' ? - map(maxLengthAttr, (_attr, index) => { - const _lhsVal = get(lhs, `${attr}.${index}`, '') - const _rhsVal = get(rhs, `${attr}.${index}`, '') - const _lhsValCleaned = this.getListAttrValue(attr, _lhsVal) - const _rhsValCleaned = this.getListAttrValue(attr, _rhsVal) - const _isDiff = !isEqual(_lhsValCleaned, _rhsValCleaned); - return ( - - { - index === 0 && - - {type !== 'list' && startCase(attr)} - - } - { - _isDiff ? - - - : - - - {this.getListAttrValue(attr, _lhsVal, true)} - - - {this.getListAttrValue(attr, _rhsVal, true)} - - - } - - ) - }) : - - - {startCase(attr)} - - { - isDiff ? - - - : - - - {this.getValue(lhs, attr, type, true)} - - - {this.getValue(rhs, attr, type, true)} - - - } - - ) - } - - ) - } - - getHeaderCell = concept => { - return ( - -
- {this.getHeaderSubAttributes(concept)} -
-
- {concept.display_name} -
-
- ) - } - - render() { - const { lhs, rhs, isLoadingLHS, isLoadingRHS, attributes, drawer } = this.state; - const isLoading = isLoadingLHS || isLoadingRHS; - const visibleAttributes = sortObjectBy(pickBy(attributes, {show: true}), config => config.position) - return ( - - { - isLoading ? -
- -
: -
- - - - - - - - - - - - { - map([lhs, rhs], this.getHeaderCell) - } - - - - { - map(visibleAttributes, (config, attr) => { - const type = config.type; - const lhsValue = this.getValue(lhs, attr, type); - const rhsValue = this.getValue(rhs, attr, type); - const isDiff = !isEqual(lhsValue, rhsValue); - const children = this.getAttributeDOM(attr, type, lhsValue, rhsValue, isDiff); - if(type === 'list') { - const lhsRawValue = lhs[attr]; - const rhsRawValue = rhs[attr]; - const lhsCount = lhsRawValue.length; - const rhsCount = rhsRawValue.length; - const hasKids = Boolean(lhsCount || rhsCount); - const styles = isDiff ? {background: DIFF_BG_RED} : {}; - const isExpanded = !config.collapsed || !hasKids; - const isExtras = attr === 'extras'; - let lhsSize, rhsSize; - let size = ''; - if(isExtras && (!isEmpty(lhsRawValue) || !isEmpty(rhsRawValue))) { - lhsSize = memorySizeOf(lhsRawValue, false) - rhsSize = memorySizeOf(rhsRawValue, false) - const totalSize = lhsSize + rhsSize; - const tooMany = totalSize/1024 >= 99; // More than 95KB - size = `${formatByteSize(totalSize)}`; - size = tooMany ? `${size} (this may take some time)` : size - } - return ( - - this.onCollapseIconClick(attr)} style={{cursor: 'pointer'}}> - - - {`${startCase(attr)} (${lhsCount}/${rhsCount})`} - { size && {size} } - { - isExpanded ? : - } - - - - { - isExpanded && - - {children} - - } - - ) - } else { - return children; - } - }) - } - -
-
-
- } - -
- ) - } + return } - -export default ConceptsComparison; diff --git a/src/components/mappings/MappingsComparison.jsx b/src/components/mappings/MappingsComparison.jsx new file mode 100644 index 00000000..d150fb4c --- /dev/null +++ b/src/components/mappings/MappingsComparison.jsx @@ -0,0 +1,139 @@ +import React from 'react' +import Comparison from '../common/Comparison' +import { cloneDeep, get, isEmpty, includes, has } from 'lodash' +import { useLocation } from 'react-router-dom' +import APIService from '../../services/APIService'; +import { formatDate, toObjectArray, toParentURI } from '../../common/utils'; + + +export default function MappingsComparison() { + const location = useLocation() + const attributeState = {show: true, type: 'text'} + const attributes = { + map_type: {...cloneDeep(attributeState), position: 1}, + external_id: {...cloneDeep(attributeState), position: 2}, + owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 3}, + from_source_owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 4}, + to_source_owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 5}, + extras: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 6}, + retired: {...cloneDeep(attributeState), type: 'bool', position: 7}, + created_by: {...cloneDeep(attributeState), position: 8}, + updated_by: {...cloneDeep(attributeState), position: 9}, + created_on: {...cloneDeep(attributeState), type: 'date', position: 10}, + updated_on: {...cloneDeep(attributeState), type: 'date', position: 11}, + } + + const fetcher = (uri, attr, loadingAttr, state) => { + if(uri && attr && loadingAttr) { + const { isVersion } = state; + const isAnyVersion = isVersion || uri.match(/\//g).length === 8; + return APIService + .new() + .overrideURL(encodeURI(uri)) + .get() + .then(response => { + if(get(response, 'status') === 200) { + const newState = {...state} + newState[attr] = formatter(response.data) + newState[loadingAttr] = false + newState.isVersion = isAnyVersion + newState.attributes = attributes + if(isAnyVersion) { + newState.attributes['is_latest_version'] = {...cloneDeep(this.attributeState), type: 'bool', position: 14} + newState.attributes['update_comment'] = {...cloneDeep(this.attributeState), position: 15} + } + return newState + } + }) + } + } + + const formatter = (mapping) => { + mapping.originalExtras = mapping.extras + mapping.extras = toObjectArray(mapping.extras) + return mapping + } + + const getAttributeValue = (concept, attr, type) => { + let value = get(concept, attr) + if (attr === 'extras') + return JSON.stringify(value, undefined, 2) + if(type === 'list') { + if(isEmpty(value)) return ''; + else return value + } else if(type === 'date') { + if(attr === 'created_on') + value ||= get(concept, 'created_at') + if(attr === 'updated_on') + value ||= get(concept, 'updated_at') + + return value ? formatDate(value) : ''; + } else if (type === 'textFormatted') { + if(attr === 'owner') + return `${concept.owner_type}: ${concept.owner}` + if(attr === 'to_source_owner') + return `${concept.to_source_owner_type}: ${concept.to_source_owner}` + if(attr === 'from_source_owner') + return `${concept.from_source_owner_type}: ${concept.from_source_owner}` + } else if (type === 'bool') { + return value ? 'True' : 'False' + } else { + if(includes(['created_by', 'updated_by'], attr)) + value ||= get(concept, `version_${attr}`) + if(attr === 'updated_by' && has(concept, 'version_created_by')) + value ||= concept.version_created_by + return value || ''; + } + } + + const getHeaderSubAttributeValues = (mapping, isVersion) => { + const attributes = [ + { + name: "Source:", + value: mapping.source, + url: toParentURI(mapping.url) + }, + { + name: "UID:", + value: mapping.id, + url: null + }, + { + name: "From Concept:", + value: mapping.from_concept_name, + url: mapping.from_concept_url + }, + { + name: "To Concept:", + value: mapping.to_concept_name, + url: mapping.to_concept_url + }, + { + name: "From Source:", + value: mapping.from_source_name, + url: mapping.from_source_url + }, + { + name: "To Source:", + value: mapping.to_source_name, + url: mapping.to_source_url + }, + ] + if (isVersion) { + attributes.push({ + name: "VERSION:", + value: mapping.version, + url: null + }) + } + + return attributes + } + + return +} diff --git a/src/components/search/SelectedResourceControls.jsx b/src/components/search/SelectedResourceControls.jsx index a8616714..004848b2 100644 --- a/src/components/search/SelectedResourceControls.jsx +++ b/src/components/search/SelectedResourceControls.jsx @@ -22,7 +22,7 @@ const SelectedResourceControls = ({ const isSourceChild = includes(['concepts', 'mappings'], resource); const hasSelectedItems = selectedItems.length > 0; const shouldShowDownloadOption = isSourceChild && hasSelectedItems; - const shouldShowCompareOption = isConceptResource && selectedItems.length === 2; + const shouldShowCompareOption = isSourceChild && selectedItems.length === 2; const shouldShowCreateSimilarOption = isSourceChild && hasAccess && selectedItems.length == 1 && onCreateSimilarClick; const shouldShowAddToCollection = isSourceChild && isAuthenticated && hasSelectedItems; const shouldShowCreateMappingOption = isConceptResource && hasAccess && hasSelectedItems && selectedItems.length <= 2 && onCreateMappingClick; @@ -41,7 +41,7 @@ const SelectedResourceControls = ({ event.preventDefault() const urls = map(selectedItems, 'url') if(urls.length == 2) { - const url = `#/concepts/compare?lhs=${urls[0]}&rhs=${urls[1]}` + const url = `#/${resource}/compare?lhs=${urls[0]}&rhs=${urls[1]}` window.open(url, '_blank') } } From 316e27c1bdb6fa1b6bf80693a33c70035e944e0f Mon Sep 17 00:00:00 2001 From: Akhil Kala Date: Tue, 7 Sep 2021 15:12:55 +0530 Subject: [PATCH 2/6] Mapping Comparison modified --- .../mappings/MappingsComparison.jsx | 64 ++++++++----------- 1 file changed, 27 insertions(+), 37 deletions(-) diff --git a/src/components/mappings/MappingsComparison.jsx b/src/components/mappings/MappingsComparison.jsx index d150fb4c..bd53114a 100644 --- a/src/components/mappings/MappingsComparison.jsx +++ b/src/components/mappings/MappingsComparison.jsx @@ -14,13 +14,19 @@ export default function MappingsComparison() { external_id: {...cloneDeep(attributeState), position: 2}, owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 3}, from_source_owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 4}, - to_source_owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 5}, - extras: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 6}, - retired: {...cloneDeep(attributeState), type: 'bool', position: 7}, - created_by: {...cloneDeep(attributeState), position: 8}, - updated_by: {...cloneDeep(attributeState), position: 9}, - created_on: {...cloneDeep(attributeState), type: 'date', position: 10}, - updated_on: {...cloneDeep(attributeState), type: 'date', position: 11}, + from_source_name: {...cloneDeep(attributeState), position: 5}, + to_source_url: {...cloneDeep(attributeState), position: 6}, + to_source_owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 7}, + to_source_name: {...cloneDeep(attributeState), position: 8}, + from_concept_name: {...cloneDeep(attributeState), position: 9}, + from_concept_url: {...cloneDeep(attributeState), position: 10}, + to_concept_name: {...cloneDeep(attributeState), position: 11}, + to_concept_url: {...cloneDeep(attributeState), position: 12}, + retired: {...cloneDeep(attributeState), type: 'bool', position: 13}, + created_by: {...cloneDeep(attributeState), position: 14}, + updated_by: {...cloneDeep(attributeState), position: 15}, + created_on: {...cloneDeep(attributeState), type: 'date', position: 16}, + updated_on: {...cloneDeep(attributeState), type: 'date', position: 17}, } const fetcher = (uri, attr, loadingAttr, state) => { @@ -54,34 +60,38 @@ export default function MappingsComparison() { return mapping } - const getAttributeValue = (concept, attr, type) => { - let value = get(concept, attr) + const getAttributeValue = (mapping, attr, type) => { + let value = get(mapping, attr) if (attr === 'extras') return JSON.stringify(value, undefined, 2) + if (attr === 'from_concept_name') + return value ||= get(mapping, 'from_concept_code') + if (attr === 'to_concept_name') + return value ||= get(mapping, 'to_concept_code') if(type === 'list') { if(isEmpty(value)) return ''; else return value } else if(type === 'date') { if(attr === 'created_on') - value ||= get(concept, 'created_at') + value ||= get(mapping, 'created_at') if(attr === 'updated_on') - value ||= get(concept, 'updated_at') + value ||= get(mapping, 'updated_at') return value ? formatDate(value) : ''; } else if (type === 'textFormatted') { if(attr === 'owner') - return `${concept.owner_type}: ${concept.owner}` + return `${mapping.owner_type}: ${mapping.owner}` if(attr === 'to_source_owner') - return `${concept.to_source_owner_type}: ${concept.to_source_owner}` + return `${mapping.to_source_owner_type}: ${mapping.to_source_owner}` if(attr === 'from_source_owner') - return `${concept.from_source_owner_type}: ${concept.from_source_owner}` + return `${mapping.from_source_owner_type}: ${mapping.from_source_owner}` } else if (type === 'bool') { return value ? 'True' : 'False' } else { if(includes(['created_by', 'updated_by'], attr)) - value ||= get(concept, `version_${attr}`) - if(attr === 'updated_by' && has(concept, 'version_created_by')) - value ||= concept.version_created_by + value ||= get(mapping, `version_${attr}`) + if(attr === 'updated_by' && has(mapping, 'version_created_by')) + value ||= mapping.version_created_by return value || ''; } } @@ -98,26 +108,6 @@ export default function MappingsComparison() { value: mapping.id, url: null }, - { - name: "From Concept:", - value: mapping.from_concept_name, - url: mapping.from_concept_url - }, - { - name: "To Concept:", - value: mapping.to_concept_name, - url: mapping.to_concept_url - }, - { - name: "From Source:", - value: mapping.from_source_name, - url: mapping.from_source_url - }, - { - name: "To Source:", - value: mapping.to_source_name, - url: mapping.to_source_url - }, ] if (isVersion) { attributes.push({ From 84fcdbfbca289c5f228278223c874272effb805e Mon Sep 17 00:00:00 2001 From: Akhil Kala Date: Tue, 14 Sep 2021 04:09:21 +0530 Subject: [PATCH 3/6] mapping version comparison added --- src/components/common/VersionList.jsx | 7 ++++--- src/components/mappings/MappingsComparison.jsx | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/common/VersionList.jsx b/src/components/common/VersionList.jsx index 2d003977..d4ee276e 100644 --- a/src/components/common/VersionList.jsx +++ b/src/components/common/VersionList.jsx @@ -35,14 +35,15 @@ const VersionList = ({ versions, resource }) => { setSelectedList(newSelectedList) } const isConcept = resource === 'concept'; - const canSelect = isConcept && sortedVersions.length > 1; + const isMapping = resource === 'mapping'; + const canSelect = (isConcept || isMapping) && sortedVersions.length > 1; const gridClass = canSelect ? 'col-md-11' : 'col-md-12' - const showCompareOption = isConcept && selectedList.length === 2; + const showCompareOption = (isConcept || isMapping) && selectedList.length === 2; const onCompareClick = event => { event.stopPropagation() event.preventDefault() - const url = `#/concepts/compare?lhs=${selectedList[0]}&rhs=${selectedList[1]}` + const url = `#/${resource}s/compare?lhs=${selectedList[0]}&rhs=${selectedList[1]}` window.open(url, '_blank') } diff --git a/src/components/mappings/MappingsComparison.jsx b/src/components/mappings/MappingsComparison.jsx index bd53114a..07ce65af 100644 --- a/src/components/mappings/MappingsComparison.jsx +++ b/src/components/mappings/MappingsComparison.jsx @@ -45,8 +45,8 @@ export default function MappingsComparison() { newState.isVersion = isAnyVersion newState.attributes = attributes if(isAnyVersion) { - newState.attributes['is_latest_version'] = {...cloneDeep(this.attributeState), type: 'bool', position: 14} - newState.attributes['update_comment'] = {...cloneDeep(this.attributeState), position: 15} + newState.attributes['is_latest_version'] = {...cloneDeep(attributeState), type: 'bool', position: 14} + newState.attributes['update_comment'] = {...cloneDeep(attributeState), position: 15} } return newState } From 7535004d5556e20b9731fdc31081f8a00d10087c Mon Sep 17 00:00:00 2001 From: Akhil Kala Date: Wed, 15 Sep 2021 10:50:02 +0530 Subject: [PATCH 4/6] extras added in mapping comparison --- src/components/mappings/MappingsComparison.jsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/mappings/MappingsComparison.jsx b/src/components/mappings/MappingsComparison.jsx index 07ce65af..d435e069 100644 --- a/src/components/mappings/MappingsComparison.jsx +++ b/src/components/mappings/MappingsComparison.jsx @@ -22,11 +22,12 @@ export default function MappingsComparison() { from_concept_url: {...cloneDeep(attributeState), position: 10}, to_concept_name: {...cloneDeep(attributeState), position: 11}, to_concept_url: {...cloneDeep(attributeState), position: 12}, - retired: {...cloneDeep(attributeState), type: 'bool', position: 13}, - created_by: {...cloneDeep(attributeState), position: 14}, - updated_by: {...cloneDeep(attributeState), position: 15}, - created_on: {...cloneDeep(attributeState), type: 'date', position: 16}, - updated_on: {...cloneDeep(attributeState), type: 'date', position: 17}, + extras: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 13}, + retired: {...cloneDeep(attributeState), type: 'bool', position: 14}, + created_by: {...cloneDeep(attributeState), position: 15}, + updated_by: {...cloneDeep(attributeState), position: 16}, + created_on: {...cloneDeep(attributeState), type: 'date', position: 17}, + updated_on: {...cloneDeep(attributeState), type: 'date', position: 18}, } const fetcher = (uri, attr, loadingAttr, state) => { From cf6b0d18254d6c3301f59e05943a4b9d2f070fa9 Mon Sep 17 00:00:00 2001 From: Akhil Kala Date: Mon, 27 Sep 2021 08:29:57 +0530 Subject: [PATCH 5/6] Source and Collection comparison progress --- src/components/app/App.jsx | 12 + .../collections/CollectionComparison.jsx | 148 ++++ src/components/common/Comparison.jsx | 12 +- .../common/ConceptContainerVersionList.jsx | 48 +- .../mappings/MappingsComparison.jsx | 4 +- src/components/search/ResultConstants.js | 828 ++++++++++++++---- src/components/search/ResultsTable.jsx | 8 +- .../search/SelectedResourceControls.jsx | 3 +- src/components/sources/SourceComparison.jsx | 151 ++++ 9 files changed, 1029 insertions(+), 185 deletions(-) create mode 100644 src/components/collections/CollectionComparison.jsx create mode 100644 src/components/sources/SourceComparison.jsx diff --git a/src/components/app/App.jsx b/src/components/app/App.jsx index 86429cbe..9091f263 100644 --- a/src/components/app/App.jsx +++ b/src/components/app/App.jsx @@ -11,6 +11,8 @@ import SourceHome from '../sources/SourceHome'; import CollectionHome from '../collections/CollectionHome'; import ConceptsComparison from '../concepts/ConceptsComparison'; import MappingsComparison from '../mappings/MappingsComparison'; +import SourceComparison from '../sources/SourceComparison'; +import CollectionComparison from '../collections/CollectionComparison'; import OrgHome from '../orgs/OrgHome'; import UserHome from '../users/UserHome'; import Login from '../users/Login'; @@ -189,6 +191,16 @@ const App = props => { path="/mappings/compare" component={MappingsComparison} /> + + {/* Organization Home */} diff --git a/src/components/collections/CollectionComparison.jsx b/src/components/collections/CollectionComparison.jsx new file mode 100644 index 00000000..76478599 --- /dev/null +++ b/src/components/collections/CollectionComparison.jsx @@ -0,0 +1,148 @@ +import React from 'react' +import Comparison from '../common/Comparison' +import { cloneDeep, get, isEmpty, includes, has } from 'lodash' +import { useLocation } from 'react-router-dom' +import APIService from '../../services/APIService'; +import { formatDate, toObjectArray } from '../../common/utils'; + + +export default function CollectionComparison() { + const location = useLocation() + const attributeState = {show: true, type: 'text'} + const attributes = { + description: {...cloneDeep(attributeState), position: 1}, + collection_type: {...cloneDeep(attributeState), position: 2}, + public_access: {...cloneDeep(attributeState), position: 3}, + default_locale: {...cloneDeep(attributeState), position: 4}, + supported_locales: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 5}, + website: {...cloneDeep(attributeState), type: "url", position: 6}, + custom_validation_schema: {...cloneDeep(attributeState), position: 7}, + custom_resources_linked_source: {...cloneDeep(attributeState), position: 8}, + repository_type: {...cloneDeep(attributeState), position: 9}, + preferred_source: {...cloneDeep(attributeState), position: 10}, + canonical_url: {...cloneDeep(attributeState), type: "url", position: 11}, + purpose: {...cloneDeep(attributeState), position: 12}, + copyright: {...cloneDeep(attributeState), position: 13}, + meta: {...cloneDeep(attributeState), position: 14}, + immutable: {...cloneDeep(attributeState), position: 15}, + revision_date: {...cloneDeep(attributeState), type: 'date', position: 16}, + logo_url: {...cloneDeep(attributeState), type: "url", position: 17}, + text: {...cloneDeep(attributeState), position: 18}, + experimental: {...cloneDeep(attributeState), position: 19}, + locked_date: {...cloneDeep(attributeState), type: 'date', position: 20}, + map_type: {...cloneDeep(attributeState), position: 21}, + external_id: {...cloneDeep(attributeState), position: 22}, + owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 23}, + extras: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 24}, + summary: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 25}, + created_by: {...cloneDeep(attributeState), position: 26}, + updated_by: {...cloneDeep(attributeState), position: 27}, + created_on: {...cloneDeep(attributeState), type: 'date', position: 28}, + updated_on: {...cloneDeep(attributeState), type: 'date', position: 19}, + } + + const fetcher = (uri, attr, loadingAttr, state) => { + if(uri && attr && loadingAttr) { + const { isVersion } = state; + const isAnyVersion = isVersion || uri.match(/\//g).length === 8; + return APIService + .new() + .overrideURL(encodeURI(uri)) + .get(null, null, {includeSummary: true}) + .then(response => { + if(get(response, 'status') === 200) { + const newState = {...state} + newState[attr] = formatter(response.data) + newState[loadingAttr] = false + newState.isVersion = isAnyVersion + newState.attributes = attributes + if(isAnyVersion) { + newState.attributes['is_latest_version'] = {...cloneDeep(attributeState), type: 'bool', position: 14} + newState.attributes['update_comment'] = {...cloneDeep(attributeState), position: 15} + } + return newState + } + }) + } + } + + const formatter = (collection) => { + collection.originalExtras = collection.extras + collection.originalSummary = collection.summary + collection.extras = toObjectArray(collection.extras) + collection.summary = toObjectArray(collection.summary) + return collection + } + + const getAttributeValue = (collection, attr, type) => { + let value = get(collection, attr) + if (attr === 'extras') + return JSON.stringify(value, undefined, 2) + if(type === 'list') { + if(isEmpty(value)) return ''; + else return value + } else if(type === 'date') { + if(attr === 'created_on') + value ||= get(collection, 'created_at') + if(attr === 'updated_on') + value ||= get(collection, 'updated_at') + return value ? formatDate(value) : ''; + } else if (type === 'textFormatted') { + if(attr === 'owner') + return `${collection.owner_type}: ${collection.owner}` + } else if (type === 'bool') { + return value ? 'True' : 'False' + } else { + if(includes(['created_by', 'updated_by'], attr)) + value ||= get(collection, `version_${attr}`) + if(attr === 'updated_by' && has(collection, 'version_created_by')) + value ||= collection.version_created_by + return value || ''; + } + } + + const getHeaderSubAttributeValues = (collection, isVersion) => { + const attributes = [ + { + name: "UID:", + value: collection.id, + url: null + }, + { + name: "Short Code:", + value: collection.short_code, + url: null + }, + ] + if (isVersion) { + attributes.push({ + name: "VERSION:", + value: collection.version, + url: null + }) + } + + return attributes + } + + const getState = () => { + if (!location.state) return null + return { + isVersion: true, + isLoadingLHS: false, + isLoadingRHS: false, + lhs: formatter(location.state[0]), + rhs: formatter(location.state[1]), + drawer: false, + attributes + } + } + + return +} diff --git a/src/components/common/Comparison.jsx b/src/components/common/Comparison.jsx index 6c0796a4..fb5d3dc7 100644 --- a/src/components/common/Comparison.jsx +++ b/src/components/common/Comparison.jsx @@ -95,6 +95,7 @@ class Comparison extends React.Component { } setObjectsForComparison() { + if (this.props.getState()) return this.setState(this.props.getState()) const queryParams = new URLSearchParams(this.props.search) this.props.fetcher(queryParams.get('lhs'), 'lhs', 'isLoadingLHS', this.state).then((lhsData) => { this.props.fetcher(queryParams.get('rhs'), 'rhs', 'isLoadingRHS', lhsData).then((data) => { @@ -109,7 +110,7 @@ class Comparison extends React.Component { getAttributeValue(concept, attr, type) { let value = get(concept, attr) - if (attr === 'extras') + if (attr === 'extras' || attr === 'summary') return JSON.stringify(value, undefined, 2) if(type === 'list') { if(isEmpty(value)) return ''; @@ -142,7 +143,7 @@ class Comparison extends React.Component { } getListAttributeValue(attr, val) { - if(includes(['extras'], attr)) + if(includes(['extras', 'summary'], attr)) return this.getExtraAttributeLabel(val) if(includes(['parent_concept_urls', 'child_concept_urls'], attr)) return val @@ -198,11 +199,12 @@ class Comparison extends React.Component { const maxLengthAttr = type === 'list' ? this.maxArrayElement(get(lhs, attr), get(rhs, attr)) : []; const rowSpan = size(maxLengthAttr); const isExtras = attr === 'extras'; + const isSummary = attr === 'summary'; return ( { - isExtras ? - : + (isExtras || isSummary) ? + : ( type === 'list' ? map(maxLengthAttr, (_attr, index) => { @@ -319,7 +321,7 @@ class Comparison extends React.Component { const hasKids = Boolean(lhsCount || rhsCount); const styles = isDiff ? {background: DIFF_BG_RED} : {}; const isExpanded = !config.collapsed || !hasKids; - const isExtras = attr === 'extras'; + const isExtras = attr === 'extras' || attr === 'summary'; let lhsSize, rhsSize; let size = ''; if(isExtras && (!isEmpty(lhsRawValue) || !isEmpty(rhsRawValue))) { diff --git a/src/components/common/ConceptContainerVersionList.jsx b/src/components/common/ConceptContainerVersionList.jsx index edd18868..13b69659 100644 --- a/src/components/common/ConceptContainerVersionList.jsx +++ b/src/components/common/ConceptContainerVersionList.jsx @@ -3,13 +3,14 @@ import { Link } from 'react-router-dom'; import alertifyjs from 'alertifyjs'; import { Accordion, AccordionSummary, AccordionDetails, Typography, Divider, Tooltip, - IconButton, CircularProgress + IconButton, CircularProgress, Button, Checkbox } from '@material-ui/core'; -import { map, isEmpty, startCase, get, includes, merge } from 'lodash'; +import { map, isEmpty, startCase, get, includes, merge, uniq, without } from 'lodash'; import { ExpandMore as ExpandMoreIcon, Search as SearchIcon, Edit as EditIcon, Delete as DeleteIcon, Block as RetireIcon, NewReleases as ReleaseIcon, FileCopy as CopyIcon, + CompareArrows as CompareArrowsIcon, } from '@material-ui/icons'; import APIService from '../../services/APIService'; import { headFirst, copyURL, toFullAPIURL } from '../../common/utils'; @@ -20,6 +21,7 @@ import ConceptContainerVersionForm from './ConceptContainerVersionForm'; import CommonFormDrawer from './CommonFormDrawer'; import ConceptContainerExport from './ConceptContainerExport'; import { CONCEPT_CONTAINER_RESOURCE_CHILDREN_TAGS } from '../search/ResultConstants'; +import { useHistory } from "react-router-dom" const ACCORDIAN_HEADING_STYLES = { fontWeight: 'bold', @@ -65,6 +67,7 @@ const updateVersion = (version, data, verb, successCallback) => getService(versi const ConceptContainerVersionList = ({ versions, resource, canEdit, onUpdate, fhir, isLoading }) => { const sortedVersions = headFirst(versions); const [versionForm, setVersionForm] = React.useState(false); + const history = useHistory() const [selectedVersion, setSelectedVersion] = React.useState(); const onEditClick = version => { setSelectedVersion(version) @@ -96,6 +99,24 @@ const ConceptContainerVersionList = ({ versions, resource, canEdit, onUpdate, fh handleOnClick(title, message, () => updateVersion(version, {[attr]: newValue}, resLabel, onUpdate)) } + const [selectedList, setSelectedList] = React.useState([]); + const onSelectChange = (event, id) => { + const newSelectedList = event.target.checked ? uniq([...selectedList, id]) : without(selectedList, id); + + setSelectedList(newSelectedList) + } + const showCompareOption = selectedList.length === 2; + const canSelect = versions.length > 1; + const onCompareClick = event => { + event.stopPropagation() + event.preventDefault() + history.push({ + pathname: `/${resource}s/compare`, + search: `?lhs=${selectedList[0]}&rhs=${selectedList[1]}`, + state: sortedVersions + }) + } + return (
@@ -105,7 +126,24 @@ const ConceptContainerVersionList = ({ versions, resource, canEdit, onUpdate, fh expandIcon={} aria-controls="panel1a-content" > + {`${startCase(resource)} Version History`} + + { + showCompareOption && + + + + } { @@ -121,6 +159,12 @@ const ConceptContainerVersionList = ({ versions, resource, canEdit, onUpdate, fh return (
+ { + canSelect && +
+ onSelectChange(event, version.version_url)} /> +
+ }
{ diff --git a/src/components/mappings/MappingsComparison.jsx b/src/components/mappings/MappingsComparison.jsx index d435e069..b5eaadb5 100644 --- a/src/components/mappings/MappingsComparison.jsx +++ b/src/components/mappings/MappingsComparison.jsx @@ -46,8 +46,8 @@ export default function MappingsComparison() { newState.isVersion = isAnyVersion newState.attributes = attributes if(isAnyVersion) { - newState.attributes['is_latest_version'] = {...cloneDeep(attributeState), type: 'bool', position: 14} - newState.attributes['update_comment'] = {...cloneDeep(attributeState), position: 15} + newState.attributes['is_latest_version'] = {...cloneDeep(attributeState), type: 'bool', position: 19} + newState.attributes['update_comment'] = {...cloneDeep(attributeState), position: 20} } return newState } diff --git a/src/components/search/ResultConstants.js b/src/components/search/ResultConstants.js index 04c2d2e9..2e541690 100644 --- a/src/components/search/ResultConstants.js +++ b/src/components/search/ResultConstants.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; import { LocalOffer as LocalOfferIcon, Link as LinkIcon, @@ -7,237 +7,723 @@ import { Person as PersonIcon, Home as HomeIcon, Loyalty as LoyaltyIcon, -} from '@material-ui/icons' -import { get, find, isEmpty, flatten, compact } from 'lodash'; +} from "@material-ui/icons"; +import { get, find, isEmpty, flatten, compact } from "lodash"; import { - formatDate, formatWebsiteLink, formatDateTime -} from '../../common/utils'; -import ReferenceChip from '../common/ReferenceChip'; -import OwnerChip from '../common/OwnerChip'; -import ToConceptLabelVertical from '../mappings/ToConceptLabelVertical'; -import FromConceptLabelVertical from '../mappings/FromConceptLabelVertical'; -import ConceptDisplayName from '../concepts/ConceptDisplayName'; + formatDate, + formatWebsiteLink, + formatDateTime, +} from "../../common/utils"; +import ReferenceChip from "../common/ReferenceChip"; +import OwnerChip from "../common/OwnerChip"; +import ToConceptLabelVertical from "../mappings/ToConceptLabelVertical"; +import FromConceptLabelVertical from "../mappings/FromConceptLabelVertical"; +import ConceptDisplayName from "../concepts/ConceptDisplayName"; export const ALL_COLUMNS = { concepts: [ - {id: 'owner', label: 'Owner', value: 'owner', sortOn: 'owner', renderer: concept => , essential: false}, - {id: 'parent', label: 'Source', value: 'source', sortOn: 'source', essential: false}, - {id: 'id', label: 'ID', value: 'id', sortOn: 'id', className: 'small'}, - {id: 'name', label: 'Display Name', value: 'display_name', sortOn: '_name', renderer: concept => (), className: 'medium', sortBy: 'asc', tooltip: 'The display name is the preferred name for a source’s default locale.'}, - {id: 'class', label: 'Class', value: 'concept_class', sortOn: 'concept_class'}, - {id: 'datatype', label: 'Datatype', value: 'datatype', sortOn: 'datatype'}, - {id: 'updatedOn', label: 'UpdatedOn', value: 'version_created_on', formatter: formatDate, sortOn: 'last_update'}, + { + id: "owner", + label: "Owner", + value: "owner", + sortOn: "owner", + renderer: (concept) => ( + + ), + essential: false, + }, + { + id: "parent", + label: "Source", + value: "source", + sortOn: "source", + essential: false, + }, + { id: "id", label: "ID", value: "id", sortOn: "id", className: "small" }, + { + id: "name", + label: "Display Name", + value: "display_name", + sortOn: "_name", + renderer: (concept) => , + className: "medium", + sortBy: "asc", + tooltip: + "The display name is the preferred name for a source’s default locale.", + }, + { + id: "class", + label: "Class", + value: "concept_class", + sortOn: "concept_class", + }, + { + id: "datatype", + label: "Datatype", + value: "datatype", + sortOn: "datatype", + }, + { + id: "updatedOn", + label: "UpdatedOn", + value: "version_created_on", + formatter: formatDate, + sortOn: "last_update", + }, ], mappings: [ - {id: 'owner', label: 'Owner', value: 'owner', sortOn: 'owner', renderer: mapping => , essential: false}, - {id: 'parent', label: 'Source', value: 'source', sortOn: 'source', essential: false, className: 'xsmall'}, - {id: 'id', label: 'ID', value: 'id', sortOn: 'id', className: 'small'}, - {id: 'from', label: 'From Concept', renderer: mapping => , className: 'medium'}, - {id: 'mapType', label: 'Type', value: 'map_type', sortOn: 'map_type', className: 'xxsmall'}, - {id: 'to', label: 'To Concept', renderer: mapping => , className: 'medium'}, - {id: 'updatedOn', label: 'UpdatedOn', value: 'version_created_on', formatter: formatDate, sortOn: 'last_update', className: 'xxsmall'}, + { + id: "owner", + label: "Owner", + value: "owner", + sortOn: "owner", + renderer: (mapping) => ( + + ), + essential: false, + }, + { + id: "parent", + label: "Source", + value: "source", + sortOn: "source", + essential: false, + className: "xsmall", + }, + { id: "id", label: "ID", value: "id", sortOn: "id", className: "small" }, + { + id: "from", + label: "From Concept", + renderer: (mapping) => ( + + ), + className: "medium", + }, + { + id: "mapType", + label: "Type", + value: "map_type", + sortOn: "map_type", + className: "xxsmall", + }, + { + id: "to", + label: "To Concept", + renderer: (mapping) => , + className: "medium", + }, + { + id: "updatedOn", + label: "UpdatedOn", + value: "version_created_on", + formatter: formatDate, + sortOn: "last_update", + className: "xxsmall", + }, ], sources: [ - {id: 'owner', label: 'Owner', value: 'owner', sortOn: 'owner', renderer: source => , essential: false}, - {id: 'id', label: 'ID', value: 'short_code', sortOn: 'mnemonic'}, - {id: 'name', label: 'Name', value: 'name', sortOn: 'name', sortBy: 'asc'}, - {id: 'sourceType', label: 'Type', value: 'source_type', sortOn: 'source_type'}, - {id: 'uuid', label: 'UUID', value: 'uuid', sortable: false}, - {id: 'full_name', label: 'Full Name', value: 'full_name', sortOn: 'full_name', sortBy: 'asc'}, - {id: 'description', label: 'Description', value: 'description', sortable: false}, - {id: 'public_access', label: 'Public Access', value: 'public_access', sortable: false}, - {id: 'default_locale', label: 'Default Locale', value: 'default_locale', sortable: false}, - {id: 'website', label: 'Website', value: 'website', sortable: false, formatter: formatWebsiteLink}, - {id: 'external_id', label: 'External ID', value: 'external_id', sortable: false}, - {id: 'canonical_url', label: 'Canonical URL', value: 'canonical_url', sortable: false, formatter: formatWebsiteLink}, - {id: 'publisher', label: 'Publisher', value: 'publisher', sortable: false}, - {id: 'purpose', label: 'Purpose', value: 'purpose', sortable: false}, - {id: 'copyright', label: 'Copyright', value: 'copyright', sortable: false}, - {id: 'content_type', label: 'Content Type', value: 'content_type', sortable: false}, - {id: 'revision_date', label: 'Revision Date', value: 'revision_date', sortable: false, formatter: formatDate}, + { + id: "owner", + label: "Owner", + value: "owner", + sortOn: "owner", + renderer: (source) => ( + + ), + essential: false, + }, + { id: "id", label: "ID", value: "short_code", sortOn: "mnemonic" }, + { id: "name", label: "Name", value: "name", sortOn: "name", sortBy: "asc" }, + { + id: "sourceType", + label: "Type", + value: "source_type", + sortOn: "source_type", + }, + { id: "uuid", label: "UUID", value: "uuid", sortable: false }, + { + id: "full_name", + label: "Full Name", + value: "full_name", + sortOn: "full_name", + sortBy: "asc", + }, + { + id: "description", + label: "Description", + value: "description", + sortable: false, + }, + { + id: "public_access", + label: "Public Access", + value: "public_access", + sortable: false, + }, + { + id: "default_locale", + label: "Default Locale", + value: "default_locale", + sortable: false, + }, + { + id: "website", + label: "Website", + value: "website", + sortable: false, + formatter: formatWebsiteLink, + }, + { + id: "external_id", + label: "External ID", + value: "external_id", + sortable: false, + }, + { + id: "canonical_url", + label: "Canonical URL", + value: "canonical_url", + sortable: false, + formatter: formatWebsiteLink, + }, + { + id: "publisher", + label: "Publisher", + value: "publisher", + sortable: false, + }, + { id: "purpose", label: "Purpose", value: "purpose", sortable: false }, + { + id: "copyright", + label: "Copyright", + value: "copyright", + sortable: false, + }, + { + id: "content_type", + label: "Content Type", + value: "content_type", + sortable: false, + }, + { + id: "revision_date", + label: "Revision Date", + value: "revision_date", + sortable: false, + formatter: formatDate, + }, ], collections: [ - {id: 'owner', label: 'Owner', value: 'owner', sortOn: 'owner', renderer: coll => , essential: false}, - {id: 'id', label: 'ID', value: 'short_code', sortOn: 'mnemonic'}, - {id: 'name', label: 'Name', value: 'name', sortOn: 'name', sortBy: 'asc'}, - {id: 'collectionType', label: 'Type', value: 'collection_type', sortOn: 'collection_type'}, - {id: 'full_name', label: 'Full Name', value: 'full_name', sortOn: 'full_name', sortBy: 'asc'}, - {id: 'uuid', label: 'UUID', value: 'uuid', sortable: false}, - {id: 'description', label: 'Description', value: 'description', sortable: false}, - {id: 'public_access', label: 'Public Access', value: 'public_access', sortable: false}, - {id: 'default_locale', label: 'Default Locale', value: 'default_locale', sortable: false}, - {id: 'website', label: 'Website', value: 'website', sortable: false, formatter: formatWebsiteLink}, - {id: 'external_id', label: 'External ID', value: 'external_id', sortable: false}, - {id: 'canonical_url', label: 'Canonical URL', value: 'canonical_url', sortable: false, formatter: formatWebsiteLink}, - {id: 'publisher', label: 'Publisher', value: 'publisher', sortable: false}, - {id: 'purpose', label: 'Purpose', value: 'purpose', sortable: false}, - {id: 'copyright', label: 'Copyright', value: 'copyright', sortable: false}, - {id: 'revision_date', label: 'Revision Date', value: 'revision_date', sortable: false, formatter: formatDate}, + { + id: "owner", + label: "Owner", + value: "owner", + sortOn: "owner", + renderer: (coll) => ( + + ), + essential: false, + }, + { id: "id", label: "ID", value: "short_code", sortOn: "mnemonic" }, + { id: "name", label: "Name", value: "name", sortOn: "name", sortBy: "asc" }, + { + id: "collectionType", + label: "Type", + value: "collection_type", + sortOn: "collection_type", + }, + { + id: "full_name", + label: "Full Name", + value: "full_name", + sortOn: "full_name", + sortBy: "asc", + }, + { id: "uuid", label: "UUID", value: "uuid", sortable: false }, + { + id: "description", + label: "Description", + value: "description", + sortable: false, + }, + { + id: "public_access", + label: "Public Access", + value: "public_access", + sortable: false, + }, + { + id: "default_locale", + label: "Default Locale", + value: "default_locale", + sortable: false, + }, + { + id: "website", + label: "Website", + value: "website", + sortable: false, + formatter: formatWebsiteLink, + }, + { + id: "external_id", + label: "External ID", + value: "external_id", + sortable: false, + }, + { + id: "canonical_url", + label: "Canonical URL", + value: "canonical_url", + sortable: false, + formatter: formatWebsiteLink, + }, + { + id: "publisher", + label: "Publisher", + value: "publisher", + sortable: false, + }, + { id: "purpose", label: "Purpose", value: "purpose", sortable: false }, + { + id: "copyright", + label: "Copyright", + value: "copyright", + sortable: false, + }, + { + id: "revision_date", + label: "Revision Date", + value: "revision_date", + sortable: false, + formatter: formatDate, + }, ], organizations: [ - {id: 'id', label: 'ID', value: 'id', sortOn: 'mnemonic', renderer: org => }, - {id: 'name', label: 'Name', value: 'name', sortOn: 'name', sortBy: 'asc'}, - {id: 'createdOn', label: 'Created On', value: 'created_on', formatter: formatDate, sortOn: 'created_on'}, + { + id: "id", + label: "ID", + value: "id", + sortOn: "mnemonic", + renderer: (org) => , + }, + { id: "name", label: "Name", value: "name", sortOn: "name", sortBy: "asc" }, + { + id: "createdOn", + label: "Created On", + value: "created_on", + formatter: formatDate, + sortOn: "created_on", + }, ], users: [ - {id: 'username', label: 'Username', value: 'username', sortOn: 'username', renderer: user => , sortBy: 'asc'}, - {id: 'name', label: 'Name', value: 'name', sortOn: 'name', sortBy: 'asc'}, - {id: 'date_joined', label: 'Joined On', value: 'date_joined', formatter: formatDate, sortOn: 'date_joined'}, - {id: 'email', label: 'Email', value: 'email', sortable: false}, - {id: 'company', label: 'Company', value: 'company'}, - {id: 'location', label: 'Location', value: 'location'}, - {id: 'website', label: 'Website', value: 'website', sortable: false, formatter: formatWebsiteLink}, - {id: 'last_login', label: 'Last Login', value: 'last_login', sortable: false, formatter: formatDateTime}, + { + id: "username", + label: "Username", + value: "username", + sortOn: "username", + renderer: (user) => , + sortBy: "asc", + }, + { id: "name", label: "Name", value: "name", sortOn: "name", sortBy: "asc" }, + { + id: "date_joined", + label: "Joined On", + value: "date_joined", + formatter: formatDate, + sortOn: "date_joined", + }, + { id: "email", label: "Email", value: "email", sortable: false }, + { id: "company", label: "Company", value: "company" }, + { id: "location", label: "Location", value: "location" }, + { + id: "website", + label: "Website", + value: "website", + sortable: false, + formatter: formatWebsiteLink, + }, + { + id: "last_login", + label: "Last Login", + value: "last_login", + sortable: false, + formatter: formatDateTime, + }, ], references: [ - {id: 'expression', label: 'Reference', value: 'expression', sortable: false, renderer: reference => }, + { + id: "expression", + label: "Reference", + value: "expression", + sortable: false, + renderer: (reference) => , + }, ], CodeSystem: [ - {id: '_id', label: 'ID', value: 'resource.id', sortOn: '_id', sortBy: 'asc'}, - {id: 'url', label: 'Canonical URL', value: 'resource.url', sortable: false}, - {id: 'name', label: 'Name', value: 'resource.name', renderer: codeSystem => {codeSystem.resource.name}, sortOn: 'name', sortBy: 'asc'}, - {id: 'version', label: 'Latest Version', value: 'resource.version', sortOn: 'version', sortBy: 'asc'}, - {id: 'status', label: 'Status', value: 'resource.status', sortOn: 'status', sortBy: 'asc'}, - {id: 'content', label: 'Content', value: 'resource.content', sortOn: 'content', sortBy: 'asc'}, - {id: 'date', label: 'Release Date', value: 'resource.date', sortOn: 'date', formatter: formatDate}, - {id: 'publisher', label: 'Publisher', value: 'resource.publisher', sortOn: 'publisher', sortBy: 'asc'}, + { + id: "_id", + label: "ID", + value: "resource.id", + sortOn: "_id", + sortBy: "asc", + }, + { + id: "url", + label: "Canonical URL", + value: "resource.url", + sortable: false, + }, + { + id: "name", + label: "Name", + value: "resource.name", + renderer: (codeSystem) => ( + + {codeSystem.resource.name} + + ), + sortOn: "name", + sortBy: "asc", + }, + { + id: "version", + label: "Latest Version", + value: "resource.version", + sortOn: "version", + sortBy: "asc", + }, + { + id: "status", + label: "Status", + value: "resource.status", + sortOn: "status", + sortBy: "asc", + }, + { + id: "content", + label: "Content", + value: "resource.content", + sortOn: "content", + sortBy: "asc", + }, + { + id: "date", + label: "Release Date", + value: "resource.date", + sortOn: "date", + formatter: formatDate, + }, + { + id: "publisher", + label: "Publisher", + value: "resource.publisher", + sortOn: "publisher", + sortBy: "asc", + }, ], ValueSet: [ - {id: '_id', label: 'ID', value: 'resource.id', sortOn: '_id', sortBy: 'asc'}, - {id: 'url', label: 'Canonical URL', value: 'resource.url', sortable: false}, - {id: 'name', label: 'Name', value: 'resource.name', renderer: codeSystem => {codeSystem.resource.name}, sortOn: 'name', sortBy: 'asc'}, - {id: 'version', label: 'Latest Version', value: 'resource.version', sortOn: 'version', sortBy: 'asc'}, - {id: 'status', label: 'Status', value: 'resource.status', sortOn: 'status', sortBy: 'asc'}, - {id: 'date', label: 'Release Date', value: 'resource.date', sortOn: 'date', formatter: formatDate}, - {id: 'publisher', label: 'Publisher', value: 'resource.publisher', sortOn: 'publisher', sortBy: 'asc'}, + { + id: "_id", + label: "ID", + value: "resource.id", + sortOn: "_id", + sortBy: "asc", + }, + { + id: "url", + label: "Canonical URL", + value: "resource.url", + sortable: false, + }, + { + id: "name", + label: "Name", + value: "resource.name", + renderer: (codeSystem) => ( + + {codeSystem.resource.name} + + ), + sortOn: "name", + sortBy: "asc", + }, + { + id: "version", + label: "Latest Version", + value: "resource.version", + sortOn: "version", + sortBy: "asc", + }, + { + id: "status", + label: "Status", + value: "resource.status", + sortOn: "status", + sortBy: "asc", + }, + { + id: "date", + label: "Release Date", + value: "resource.date", + sortOn: "date", + formatter: formatDate, + }, + { + id: "publisher", + label: "Publisher", + value: "resource.publisher", + sortOn: "publisher", + sortBy: "asc", + }, ], ConceptMap: [ - {id: '_id', label: 'ID', value: 'resource.id', sortOn: '_id', sortBy: 'asc'}, - {id: 'url', label: 'Canonical URL', value: 'resource.url', sortable: false}, - {id: 'name', label: 'Name', value: 'resource.title', renderer: codeSystem => {codeSystem.resource.title}, sortOn: 'title', sortBy: 'asc'}, - {id: 'version', label: 'Latest Version', value: 'resource.version', sortOn: 'version', sortBy: 'asc'}, - {id: 'status', label: 'Status', value: 'resource.status', sortOn: 'status', sortBy: 'asc'}, - {id: 'date', label: 'Release Date', value: 'resource.date', sortOn: 'date', formatter: formatDate}, - {id: 'publisher', label: 'Publisher', value: 'resource.publisher', sortOn: 'publisher', sortBy: 'asc'}, - ] + { + id: "_id", + label: "ID", + value: "resource.id", + sortOn: "_id", + sortBy: "asc", + }, + { + id: "url", + label: "Canonical URL", + value: "resource.url", + sortable: false, + }, + { + id: "name", + label: "Name", + value: "resource.title", + renderer: (codeSystem) => ( + + {codeSystem.resource.title} + + ), + sortOn: "title", + sortBy: "asc", + }, + { + id: "version", + label: "Latest Version", + value: "resource.version", + sortOn: "version", + sortBy: "asc", + }, + { + id: "status", + label: "Status", + value: "resource.status", + sortOn: "status", + sortBy: "asc", + }, + { + id: "date", + label: "Release Date", + value: "resource.date", + sortOn: "date", + formatter: formatDate, + }, + { + id: "publisher", + label: "Publisher", + value: "resource.publisher", + sortOn: "publisher", + sortBy: "asc", + }, + ], }; -const TAG_ICON_STYLES = {width: '12px', marginRight: '2px', marginTop: '2px'} +const TAG_ICON_STYLES = { width: "12px", marginRight: "2px", marginTop: "2px" }; export const CONCEPT_CONTAINER_RESOURCE_CHILDREN_TAGS = [ { - id: 'activeConcepts', - value: 'summary.active_concepts', - label: 'Concepts', - icon: , - hrefAttr: 'concepts_url' + id: "activeConcepts", + value: "summary.active_concepts", + label: "Concepts", + icon: , + hrefAttr: "concepts_url", }, { - id: 'activeMappings', - value: 'summary.active_mappings', - label: 'Mappings', - icon: , - hrefAttr: 'mappings_url' + id: "activeMappings", + value: "summary.active_mappings", + label: "Mappings", + icon: , + hrefAttr: "mappings_url", }, -] +]; const CONCEPT_CONTAINER_TAGS = [ ...CONCEPT_CONTAINER_RESOURCE_CHILDREN_TAGS, { - id: 'versions', - value: 'summary.versions', - label: 'Versions', - icon: , - hrefAttr: 'versions_url' + id: "versions", + value: "summary.versions", + label: "Versions", + icon: , + hrefAttr: "versions_url", }, -] +]; -const getOCLFHIRResourceURL = item => { - const identifiers = flatten([get(item, 'resource.identifier', [])]) - return '/fhir/' + compact(get(find(identifiers, ident => get(ident, 'system', '').match('fhir.')), 'value', '').split('/')).splice(0, 4).join('/') -} +const getOCLFHIRResourceURL = (item) => { + const identifiers = flatten([get(item, "resource.identifier", [])]); + return ( + "/fhir/" + + compact( + get( + find(identifiers, (ident) => get(ident, "system", "").match("fhir.")), + "value", + "" + ).split("/") + ) + .splice(0, 4) + .join("/") + ); +}; const CODE_SYSTEM_TAGS = [ { - id: 'count', - getValue: item => (get(item, 'resource.count') || (get(item, 'resource.concept', []) || []).length).toLocaleString(), - label: 'Concepts', - icon: , - hrefAttr: (item, hapi) => hapi ? `/fhir/CodeSystem/${item.resource.id}/` : getOCLFHIRResourceURL(item) + id: "count", + getValue: (item) => + ( + get(item, "resource.count") || + (get(item, "resource.concept", []) || []).length + ).toLocaleString(), + label: "Concepts", + icon: , + hrefAttr: (item, hapi) => + hapi + ? `/fhir/CodeSystem/${item.resource.id}/` + : getOCLFHIRResourceURL(item), }, -] +]; const VALUE_SET_TAGS = [ { - id: 'count', - getValue: item => { - const concepts = get(item, 'resource.count') || - get(find(get(item, 'resource.compose.include', []), inc => !isEmpty(get(inc, 'concept'))), 'concept', []).length || - 0; - return concepts.toLocaleString() - }, - label: 'Concepts', - icon: , - hrefAttr: (item, hapi) => hapi ? `/fhir/ValueSet/${item.resource.id}/` : getOCLFHIRResourceURL(item) + id: "count", + getValue: (item) => { + const concepts = + get(item, "resource.count") || + get( + find( + get(item, "resource.compose.include", []), + (inc) => !isEmpty(get(inc, "concept")) + ), + "concept", + [] + ).length || + 0; + return concepts.toLocaleString(); + }, + label: "Concepts", + icon: , + hrefAttr: (item, hapi) => + hapi + ? `/fhir/ValueSet/${item.resource.id}/` + : getOCLFHIRResourceURL(item), }, -] +]; -export const CODE_SYSTEM_VERSION_TAGS = [...CODE_SYSTEM_TAGS] -export const VALUE_SET_VERSION_TAGS = [...VALUE_SET_TAGS] +export const CODE_SYSTEM_VERSION_TAGS = [...CODE_SYSTEM_TAGS]; +export const VALUE_SET_VERSION_TAGS = [...VALUE_SET_TAGS]; const SOURCE_TAG = { - id: 'sources', - value: 'public_sources', - label: 'Public Sources', - icon: , - hrefAttr: 'sources_url' -} + id: "sources", + value: "public_sources", + label: "Public Sources", + icon: , + hrefAttr: "sources_url", +}; const COLLECTION_TAG = { - id: 'collections', - value: 'public_collections', - label: 'Public Collections', - icon: , - hrefAttr: 'collections_url' -} + id: "collections", + value: "public_collections", + label: "Public Collections", + icon: , + hrefAttr: "collections_url", +}; export const TAGS = { sources: [...CONCEPT_CONTAINER_TAGS], collections: [...CONCEPT_CONTAINER_TAGS], organizations: [ { - id: 'members', - value: 'members', - label: 'Members', - icon: , - hrefAttr: 'url' + id: "members", + value: "members", + label: "Members", + icon: , + hrefAttr: "url", }, SOURCE_TAG, COLLECTION_TAG, ], users: [ { - id: 'orgs', - value: 'orgs', - label: 'Organizations', - icon: , - hrefAttr: 'organizations_url' + id: "orgs", + value: "orgs", + label: "Organizations", + icon: , + hrefAttr: "organizations_url", }, SOURCE_TAG, COLLECTION_TAG, ], CodeSystem: [...CODE_SYSTEM_TAGS], - ValueSet: [...VALUE_SET_TAGS] -} + ValueSet: [...VALUE_SET_TAGS], +}; export const FACET_ORDER = { - concepts: ['owner', 'ownerType', 'source', 'conceptClass', 'datatype', 'locale', 'retired', 'collection_membership'], + concepts: [ + "owner", + "ownerType", + "source", + "conceptClass", + "datatype", + "locale", + "retired", + "collection_membership", + ], mappings: [ - 'owner', 'ownerType', 'source', 'mapType', - 'fromConceptOwner', 'fromConceptOwnerType', 'fromConceptSource', 'fromConcept', - 'toConceptOwner', 'toConceptOwnerType', 'toConceptSource', 'toConcept', - 'retired', 'collection_membership', - ] -} + "owner", + "ownerType", + "source", + "mapType", + "fromConceptOwner", + "fromConceptOwnerType", + "fromConceptSource", + "fromConcept", + "toConceptOwner", + "toConceptOwnerType", + "toConceptSource", + "toConcept", + "retired", + "collection_membership", + ], +}; export const SORT_ATTRS = { - concepts: ['score', 'last_update', 'id', 'numeric_id', '_name', 'concept_class', 'datatype', 'source', 'owner'], - mappings: ['score', 'last_update', 'id', 'map_type', 'source', 'owner'], - users: ['score', 'username', 'date_joined', 'company', 'location'], - organizations: ['score', 'last_update', 'name', 'mnemonic'], - sources: ['score', 'last_update', 'mnemonic', 'source_type', 'name', 'owner', 'canonical_url'], - collections: ['score', 'last_update', 'mnemonic', 'collection_type', 'name', 'owner', 'canonical_url'], -} + concepts: [ + "score", + "last_update", + "id", + "numeric_id", + "_name", + "concept_class", + "datatype", + "source", + "owner", + ], + mappings: ["score", "last_update", "id", "map_type", "source", "owner"], + users: ["score", "username", "date_joined", "company", "location"], + organizations: ["score", "last_update", "name", "mnemonic"], + sources: [ + "score", + "last_update", + "mnemonic", + "source_type", + "name", + "owner", + "canonical_url", + ], + collections: [ + "score", + "last_update", + "mnemonic", + "collection_type", + "name", + "owner", + "canonical_url", + ], +}; diff --git a/src/components/search/ResultsTable.jsx b/src/components/search/ResultsTable.jsx index 924ec92e..b900fe73 100644 --- a/src/components/search/ResultsTable.jsx +++ b/src/components/search/ResultsTable.jsx @@ -622,7 +622,7 @@ const ExpandibleRow = props => { )) } { - !isSelectable && + !isSelectable || true && { resourceDefinition.tagWaitAttribute && !has(item, resourceDefinition.tagWaitAttribute) ? @@ -770,7 +770,7 @@ const ResultsTable = ( const [orderBy, setOrderBy] = React.useState(defaultOrderBy) const [order, setOrder] = React.useState(defaultOrder) const hasAccess = currentUserHasAccess() - const isSourceChild = includes(['concepts', 'mappings'], resource); + const isSourceChild = includes(['concepts', 'mappings', 'sources', 'collections'], resource); const isReferenceResource = resource === 'references'; const isSelectable = (isReferenceResource && hasAccess && isVersionedObject) || isSourceChild; @@ -896,7 +896,7 @@ const ResultsTable = ( }) } { - !isSelectable && + !isSelectable || true && } { @@ -952,4 +952,4 @@ const ResultsTable = ( ) } -export default ResultsTable +export default ResultsTable \ No newline at end of file diff --git a/src/components/search/SelectedResourceControls.jsx b/src/components/search/SelectedResourceControls.jsx index 004848b2..aea41582 100644 --- a/src/components/search/SelectedResourceControls.jsx +++ b/src/components/search/SelectedResourceControls.jsx @@ -20,9 +20,10 @@ const SelectedResourceControls = ({ const isReferenceResource = resource === 'references'; const isConceptResource = resource === 'concepts'; const isSourceChild = includes(['concepts', 'mappings'], resource); + const isComparable = includes(['concepts', 'mappings', 'sources', 'collections'], resource); const hasSelectedItems = selectedItems.length > 0; const shouldShowDownloadOption = isSourceChild && hasSelectedItems; - const shouldShowCompareOption = isSourceChild && selectedItems.length === 2; + const shouldShowCompareOption = isComparable && selectedItems.length === 2; const shouldShowCreateSimilarOption = isSourceChild && hasAccess && selectedItems.length == 1 && onCreateSimilarClick; const shouldShowAddToCollection = isSourceChild && isAuthenticated && hasSelectedItems; const shouldShowCreateMappingOption = isConceptResource && hasAccess && hasSelectedItems && selectedItems.length <= 2 && onCreateMappingClick; diff --git a/src/components/sources/SourceComparison.jsx b/src/components/sources/SourceComparison.jsx new file mode 100644 index 00000000..4a38735a --- /dev/null +++ b/src/components/sources/SourceComparison.jsx @@ -0,0 +1,151 @@ +import React from 'react' +import Comparison from '../common/Comparison' +import { cloneDeep, get, isEmpty, includes, has } from 'lodash' +import { useLocation } from 'react-router-dom' +import APIService from '../../services/APIService'; +import { formatDate, toObjectArray, toParentURI } from '../../common/utils'; + + +export default function SourceComparison() { + const location = useLocation() + const attributeState = {show: true, type: 'text'} + const attributes = { + description: {...cloneDeep(attributeState), position: 1}, + source_type: {...cloneDeep(attributeState), position: 2}, + custom_validation_schema: {...cloneDeep(attributeState), position: 3}, + public_access: {...cloneDeep(attributeState), position: 4}, + default_locale: {...cloneDeep(attributeState), position: 5}, + website: {...cloneDeep(attributeState), type:"url", position: 6}, + custom_resources_linked_source: {...cloneDeep(attributeState), position: 7}, + repository_type: {...cloneDeep(attributeState), position: 8}, + preferred_source: {...cloneDeep(attributeState), position: 9}, + canonical_url: {...cloneDeep(attributeState), type:"url", position: 10}, + publisher: {...cloneDeep(attributeState), position: 11}, + purpose: {...cloneDeep(attributeState), position: 12}, + copyright: {...cloneDeep(attributeState), position: 13}, + meta: {...cloneDeep(attributeState), position: 14}, + immutable: {...cloneDeep(attributeState), position: 15}, + revision_date: {...cloneDeep(attributeState), type:"url", position: 16}, + logo_url: {...cloneDeep(attributeState), type:"url", position: 17}, + text: {...cloneDeep(attributeState), position: 18}, + experimental: {...cloneDeep(attributeState), position: 19}, + locked_date: {...cloneDeep(attributeState), type:"url", position: 20}, + map_type: {...cloneDeep(attributeState), position: 21}, + external_id: {...cloneDeep(attributeState), position: 22}, + owner: {...cloneDeep(attributeState), type: 'textFormatted', position: 23}, + extras: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 24}, + summary: {...cloneDeep(attributeState), collapsed: true, type: 'list', position: 25}, + created_by: {...cloneDeep(attributeState), position: 26}, + updated_by: {...cloneDeep(attributeState), position: 27}, + created_on: {...cloneDeep(attributeState), type: 'date', position: 28}, + updated_on: {...cloneDeep(attributeState), type: 'date', position: 29}, + } + + const fetcher = (uri, attr, loadingAttr, state) => { + if(uri && attr && loadingAttr) { + const { isVersion } = state; + const isAnyVersion = isVersion || uri.match(/\//g).length === 8; + return APIService + .new() + .overrideURL(encodeURI(uri)) + .get(null, null, {includeSummary: true}) + .then(response => { + if(get(response, 'status') === 200) { + const newState = {...state} + newState[attr] = formatter(response.data) + newState[loadingAttr] = false + newState.isVersion = isAnyVersion + newState.attributes = attributes + if(isAnyVersion) { + newState.attributes['is_latest_version'] = {...cloneDeep(attributeState), type: 'bool', position: 14} + newState.attributes['update_comment'] = {...cloneDeep(attributeState), position: 15} + } + return newState + } + }) + } + } + + const formatter = (source) => { + source.originalExtras = source.extras + source.extras = toObjectArray(source.extras) + return source + } + + const getAttributeValue = (source, attr, type) => { + let value = get(source, attr) + if (attr === 'extras') + return JSON.stringify(value, undefined, 2) + if(type === 'list') { + if(isEmpty(value)) return ''; + else return value + } else if(type === 'date') { + if(attr === 'created_on') + value ||= get(source, 'created_at') + if(attr === 'updated_on') + value ||= get(source, 'updated_at') + return value ? formatDate(value) : ''; + } else if (type === 'textFormatted') { + if(attr === 'owner') + return `${source.owner_type}: ${source.owner}` + } else if (type === 'bool') { + return value ? 'True' : 'False' + } else { + if(includes(['created_by', 'updated_by'], attr)) + value ||= get(source, `version_${attr}`) + if(attr === 'updated_by' && has(source, 'version_created_by')) + value ||= source.version_created_by + return value || ''; + } + } + + const getHeaderSubAttributeValues = (source, isVersion) => { + const attributes = [ + { + name: "Source:", + value: source.source, + url: toParentURI(source.url) + }, + { + name: "UID:", + value: source.id, + url: null + }, + { + name: "Short Code:", + value: source.short_code, + url: null + }, + ] + if (isVersion) { + attributes.push({ + name: "VERSION:", + value: source.version, + url: null + }) + } + + return attributes + } + + const getState = () => { + if (!location.state) return null + return { + isVersion: true, + isLoadingLHS: false, + isLoadingRHS: false, + lhs: formatter(location.state[0]), + rhs: formatter(location.state[1]), + drawer: false, + attributes + } + } + + return +} From 4f9121799fe2f7570bb8201e8978623fa48430ac Mon Sep 17 00:00:00 2001 From: Akhil Kala Date: Mon, 27 Sep 2021 09:07:59 +0530 Subject: [PATCH 6/6] Summary fix --- src/components/collections/CollectionComparison.jsx | 2 +- src/components/common/Comparison.jsx | 2 +- src/components/sources/SourceComparison.jsx | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/collections/CollectionComparison.jsx b/src/components/collections/CollectionComparison.jsx index 76478599..8aa734e6 100644 --- a/src/components/collections/CollectionComparison.jsx +++ b/src/components/collections/CollectionComparison.jsx @@ -76,7 +76,7 @@ export default function CollectionComparison() { const getAttributeValue = (collection, attr, type) => { let value = get(collection, attr) - if (attr === 'extras') + if (attr === 'extras' || attr === 'summary') return JSON.stringify(value, undefined, 2) if(type === 'list') { if(isEmpty(value)) return ''; diff --git a/src/components/common/Comparison.jsx b/src/components/common/Comparison.jsx index fb5d3dc7..a750a375 100644 --- a/src/components/common/Comparison.jsx +++ b/src/components/common/Comparison.jsx @@ -95,7 +95,7 @@ class Comparison extends React.Component { } setObjectsForComparison() { - if (this.props.getState()) return this.setState(this.props.getState()) + if (this.props.getState && this.props.getState()) return this.setState(this.props.getState()) const queryParams = new URLSearchParams(this.props.search) this.props.fetcher(queryParams.get('lhs'), 'lhs', 'isLoadingLHS', this.state).then((lhsData) => { this.props.fetcher(queryParams.get('rhs'), 'rhs', 'isLoadingRHS', lhsData).then((data) => { diff --git a/src/components/sources/SourceComparison.jsx b/src/components/sources/SourceComparison.jsx index 4a38735a..da45a374 100644 --- a/src/components/sources/SourceComparison.jsx +++ b/src/components/sources/SourceComparison.jsx @@ -67,14 +67,16 @@ export default function SourceComparison() { } const formatter = (source) => { - source.originalExtras = source.extras - source.extras = toObjectArray(source.extras) - return source + source.originalExtras = source.extras + source.originalSummary = source.summary + source.extras = toObjectArray(source.extras) + source.summary = toObjectArray(source.summary) + return source } const getAttributeValue = (source, attr, type) => { let value = get(source, attr) - if (attr === 'extras') + if (attr === 'extras' || attr === 'summary') return JSON.stringify(value, undefined, 2) if(type === 'list') { if(isEmpty(value)) return '';