Skip to content

Commit

Permalink
OpenConceptLab/ocl_issues#1363 | Quick Add Mappings from concept
Browse files Browse the repository at this point in the history
  • Loading branch information
snyaggarwal committed Nov 18, 2022
1 parent 2c98025 commit 79e8b11
Show file tree
Hide file tree
Showing 8 changed files with 513 additions and 31 deletions.
123 changes: 123 additions & 0 deletions src/components/common/ConceptSearchAutocomplete.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';
import {
LocalOfferOutlined as ConceptIcon,
} from '@mui/icons-material';
import { TextField, CircularProgress, ListItem, ListItemIcon, ListItemText, Divider, Typography } from '@mui/material';
import Autocomplete from '@mui/material/Autocomplete';
import { get, debounce, orderBy, isEmpty } from 'lodash'
import APIService from '../../services/APIService';
import { BLUE } from '../../common/constants';

const ConceptSearchAutocomplete = ({onChange, label, id, required, minCharactersForSearch, size, parentURI, disabled}) => {
const minLength = minCharactersForSearch || 1;
const [input, setInput] = React.useState('')
const [open, setOpen] = React.useState(false)
const [fetched, setFetched] = React.useState(false)
const [concepts, setConcepts] = React.useState([])
const [selected, setSelected] = React.useState(undefined)
const isSearchable = input && input.length >= minLength;
const loading = Boolean(open && !fetched && isSearchable && isEmpty(concepts))
const handleInputChange = debounce((event, value, reason) => {
setInput(value || '')
setFetched(false)
if(reason !== 'reset' && value && value.length >= minLength)
fetchConcepts(value)
}, 300)

const handleChange = (event, id, item) => {
event.preventDefault()
event.stopPropagation()
setSelected(item)
onChange(id, item)
}

const fetchConcepts = searchStr => {
const query = {limit: 10, q: searchStr}
let service = parentURI ? APIService.new().overrideURL(parentURI).appendToUrl('concepts/') : APIService.concepts()
service.get(null, null, query).then(response => {
const concepts = orderBy(response.data, ['display_name'])
setConcepts(concepts)
setFetched(true)
})
}

return (
<Autocomplete
disabled={disabled}
filterOptions={x => x}
openOnFocus
blurOnSelect
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
isOptionEqualToValue={(option, value) => option.url === get(value, 'url')}
value={selected}
id={id || 'concept'}
size={size || 'medium'}
options={concepts}
loading={loading}
loadingText={loading ? 'Loading...' : `Type atleast ${minLength} characters to search`}
noOptionsText={(isSearchable && !loading) ? "No results" : 'Start typing...'}
getOptionLabel={option => option ? option.display_name || option.id : ''}
fullWidth
required={required}
onInputChange={handleInputChange}
onChange={(event, item) => handleChange(event, id || 'concept', item)}
renderInput={
params => (
<TextField
{...params}
value={input}
required
label={label || "Concept"}
variant="outlined"
fullWidth
size={size || 'medium'}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)
}
renderOption={
(props, option) => (
<React.Fragment key={option.url}>
<ListItem {...props}>
<ListItemIcon>
<ConceptIcon style={{color: BLUE}} fontSize='large' />
</ListItemIcon>
<ListItemText
primary={
<Typography
sx={{ maxWidth: 'calc(100% - 90px)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<span>{option.display_name}</span>
<span style={{color: 'rgba(0, 0, 0, 0.6)', marginLeft: '5px'}}>
<i>{`[${option.display_locale}]`}</i>
</span>
</Typography>
}
secondary={
<Typography
sx={{ display: 'inline', color: 'rgba(0, 0, 0, 0.6)' }}
component="span"
className='flex-vertical-center'>
{option.id}
</Typography>
}
/>
</ListItem>
<Divider variant="inset" component="li" />
</React.Fragment>
)
}
/>
);
}

export default ConceptSearchAutocomplete;
133 changes: 133 additions & 0 deletions src/components/common/SourceSearchAutocomplete.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React from 'react';
import {
List as SourceIcon, LocalOfferOutlined as ConceptIcon,
Person as UserIcon, AccountBalance as OrgIcon,
} from '@mui/icons-material';
import { TextField, CircularProgress, ListItem, ListItemIcon, ListItemText, Divider, Typography } from '@mui/material';
import Autocomplete from '@mui/material/Autocomplete';
import { get, debounce, orderBy, isEmpty } from 'lodash'
import APIService from '../../services/APIService';
import { GREEN } from '../../common/constants';

const SourceSearchAutocomplete = ({onChange, label, id, required, minCharactersForSearch, size}) => {
const minLength = minCharactersForSearch || 2;
const [input, setInput] = React.useState('')
const [open, setOpen] = React.useState(false)
const [fetched, setFetched] = React.useState(false)
const [sources, setSources] = React.useState([])
const [selected, setSelected] = React.useState(undefined)
const isSearchable = input && input.length >= minLength;
const loading = Boolean(open && !fetched && isSearchable && isEmpty(sources))
const handleInputChange = debounce((event, value, reason) => {
setInput(value || '')
setFetched(false)
if(reason !== 'reset' && value && value.length >= minLength)
fetchSources(value)
}, 300)

const handleChange = (event, id, item) => {
event.preventDefault()
event.stopPropagation()
setSelected(item)
onChange(id, item)
}

const fetchSources = searchStr => {
const query = {limit: 25, q: searchStr, includeSummary: true}
APIService.sources().get(null, null, query).then(response => {
const sources = orderBy(response.data, ['name'])
setSources(sources)
setFetched(true)
})
}

return (
<Autocomplete
filterOptions={x => x}
openOnFocus
blurOnSelect
open={open}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
isOptionEqualToValue={(option, value) => option.url === get(value, 'url')}
value={selected}
id={id || 'source'}
size={size || 'medium'}
options={sources}
loading={loading}
loadingText={loading ? 'Loading...' : `Type atleast ${minLength} characters to search`}
noOptionsText={(isSearchable && !loading) ? "No results" : 'Start typing...'}
getOptionLabel={option => option ? option.name : ''}
fullWidth
required={required}
onInputChange={handleInputChange}
onChange={(event, item) => handleChange(event, id || 'source', item)}
renderInput={
params => (
<TextField
{...params}
value={input}
required
label={label || "Source"}
variant="outlined"
fullWidth
size={size || 'medium'}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)
}
renderOption={
(props, option) => (
<React.Fragment key={option.url}>
<ListItem
{...props}
secondaryAction={
<span className='flex-vertical-center'>
{
option.owner_type === 'User' ?
<UserIcon style={{marginRight: '4px', color: 'rgba(0, 0, 0, 0.26)', fontSize: '13px' }}/> :
<OrgIcon style={{marginRight: '4px', color: 'rgba(0, 0, 0, 0.26)', fontSize: '13px'}} />
}
<span className='flex-vertical-center' style={{maxWidth: '70px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', color: 'rgba(0, 0, 0, 0.26)', fontSize: '13px'}}>
{option.owner}
</span>
</span>
}>
<ListItemIcon>
<SourceIcon style={{color: GREEN}} fontSize='large' />
</ListItemIcon>
<ListItemText
primary={
<Typography
sx={{ maxWidth: 'calc(100% - 90px)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{option.name}
</Typography>
}
secondary={
<Typography
sx={{ display: 'inline', color: 'rgba(0, 0, 0, 0.6)' }}
component="span"
className='flex-vertical-center'>
<ConceptIcon size='small' style={{marginRight: '4px', fontSize: '1rem'}} />
{option.summary.active_concepts}
</Typography>
}
/>
</ListItem>
<Divider variant="inset" component="li" />
</React.Fragment>
)
}
/>
);
}

export default SourceSearchAutocomplete;
77 changes: 61 additions & 16 deletions src/components/concepts/ConceptHome.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from 'react';
import alertifyjs from 'alertifyjs';
import Split from 'react-split'
import { CircularProgress } from '@mui/material';
import { get, isObject, isBoolean, has } from 'lodash';
import { get, isObject, isBoolean, has, flatten, values, isArray } from 'lodash';
import APIService from '../../services/APIService';
import { toParentURI } from '../../common/utils'
import NotFound from '../common/NotFound';
Expand Down Expand Up @@ -184,6 +185,48 @@ class ConceptHome extends React.Component {
})
}

onCreateNewMapping = (payload, targetConcept, isDirect, successCallback) => {
const { concept, mappings, reverseMappings } = this.state
const URL = `${concept.owner_url}sources/${concept.source}/mappings/`
APIService.new().overrideURL(URL).post(payload).then(response => {
if(response.status === 201) {
alertifyjs.success('Success')
let newMapping = {
...response.data,
cascade_target_concept_code: targetConcept.id,
cascade_target_concept_url: targetConcept.url,
cascade_target_source_owner: targetConcept.owner,
cascade_target_source_name: targetConcept.source,
cascade_target_concept_name: targetConcept.display_name,
}
if(isDirect)
this.setState({
mappings: [
...mappings,
newMapping,
{...targetConcept, entries: []}
]
})
else
this.setState({
reverseMappings: [
...reverseMappings,
newMapping,
{...targetConcept, entries: []}
]
})
if(successCallback)
successCallback()
} else {
const errors = flatten(values(response))
if(isArray(errors) && errors.length)
alertifyjs.error(errors[0])
else
alertifyjs.error('Something bad happened!')
}
})
}

onIncludeRetiredAssociationsToggle = includeRetired => this.setState({includeRetiredAssociations: includeRetired}, this.getMappings)

getCollectionVersions() {
Expand All @@ -204,21 +247,22 @@ class ConceptHome extends React.Component {
onConceptClick = concept => {
this.setState({isUpdatingFromHierarchy: true, isLoading: true}, () => {
window.location.hash = concept.url
APIService.new()
.overrideURL(encodeURI(concept.url))
.get().then(response => {
this.setState(
{
isLoading: false,
concept: response.data,
isUpdatingFromHierarchy: false
},
() => {
this.getVersions()
window.scrollTo(0, 0)
}
)
})
APIService
.new()
.overrideURL(encodeURI(concept.url))
.get().then(response => {
this.setState(
{
isLoading: false,
concept: response.data,
isUpdatingFromHierarchy: false
},
() => {
this.getVersions()
window.scrollTo(0, 0)
}
)
})
})
}

Expand Down Expand Up @@ -267,6 +311,7 @@ class ConceptHome extends React.Component {
sourceVersion={get(this.props.match, 'params.version')}
parent={this.props.parent}
onIncludeRetiredAssociationsToggle={this.onIncludeRetiredAssociationsToggle}
onCreateNewMapping={isVersionedObject && this.props.scoped != 'collection' ? this.onCreateNewMapping : false}
/>
</div>
</React.Fragment>
Expand Down
12 changes: 10 additions & 2 deletions src/components/concepts/ConceptHomeDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const ACCORDIAN_DETAILS_STYLES = {
overflowX: 'auto', width: '100%', padding: '0'
}

const ConceptHomeDetails = ({ concept, isLoadingMappings, isLoadingCollections, source, singleColumn, versions, scoped, sourceVersion, parent, onIncludeRetiredAssociationsToggle }) => {
const ConceptHomeDetails = ({ concept, isLoadingMappings, isLoadingCollections, source, singleColumn, versions, scoped, sourceVersion, parent, onIncludeRetiredAssociationsToggle, onCreateNewMapping }) => {
const names = get(concept, 'names', [])
const descriptions = get(concept, 'descriptions', [])
let classes = 'col-sm-12 padding-5';
Expand Down Expand Up @@ -43,7 +43,15 @@ const ConceptHomeDetails = ({ concept, isLoadingMappings, isLoadingCollections,
/>
</div>
<div className={classes} style={{paddingTop: '10px'}}>
<HomeMappings concept={concept} isLoadingMappings={isLoadingMappings} source={source} sourceVersion={sourceVersion} parent={parent} onIncludeRetiredToggle={onIncludeRetiredAssociationsToggle} />
<HomeMappings
concept={concept}
isLoadingMappings={isLoadingMappings}
source={source}
sourceVersion={sourceVersion}
parent={parent}
onIncludeRetiredToggle={onIncludeRetiredAssociationsToggle}
onCreateNewMapping={onCreateNewMapping}
/>
{
scoped === 'collection' ?
<ResourceReferences
Expand Down
Loading

0 comments on commit 79e8b11

Please sign in to comment.