react/src/pages/InstrumentView.js
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { Instrument, CcConditions, CcLoops, CcSequences, CcStatements, CcQuestions, QuestionItems, QuestionGrids, Variables, Topics } from '../actions'
import { Dashboard } from '../components/Dashboard'
import { InstrumentHeading } from '../components/InstrumentHeading'
import { Loader } from '../components/Loader'
import { get, isEmpty, isNil, times } from "lodash";
import { makeStyles, withStyles } from '@material-ui/core/styles';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Collapse from '@material-ui/core/Collapse';
import ExpandLess from '@material-ui/icons/ExpandLess';
import ExpandMore from '@material-ui/icons/ExpandMore';
import Chip from '@material-ui/core/Chip';
import { Grid, Typography } from '@material-ui/core'
import { ObjectColour } from '../support/ObjectColour'
import TextFieldsIcon from '@material-ui/icons/TextFields';
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline';
import TodayIcon from '@material-ui/icons/Today';
import Filter1Icon from '@material-ui/icons/Filter1';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Tooltip from '@material-ui/core/Tooltip';
const useStyles = makeStyles((theme) => ({
root: {
width: '100%'
},
control: {
width: '100%',
padding: theme.spacing(2),
},
nested: {
paddingLeft: theme.spacing(4),
},
rosterLabel: {
backgroundColor: 'lightgray',
height: 25
},
}));
const ObjectFinder = (instrumentId, type, id) => {
const sequences = useSelector(state => state.cc_sequences);
const cc_sequences = get(sequences, instrumentId, {})
const statements = useSelector(state => state.cc_statements);
const cc_statements = get(statements, instrumentId, {})
const conditions = useSelector(state => state.cc_conditions);
const cc_conditions = get(conditions, instrumentId, {})
const questions = useSelector(state => state.cc_questions);
const cc_questions = get(questions, instrumentId, {})
const allQuestionItems = useSelector(state => state.questionItems);
const questionItems = get(allQuestionItems, instrumentId, {})
const allQuestionGrids = useSelector(state => state.questionGrids);
const questionGrids = get(allQuestionGrids, instrumentId, {});
const loops = useSelector(state => state.cc_loops);
const cc_loops = get(loops, instrumentId, {})
var item = {children: []}
if(type === 'CcSequence'){
item = get(cc_sequences, id.toString(), {})
}
if(type === 'CcStatement'){
item = get(cc_statements, id.toString(), {})
}
if(type === 'CcCondition'){
item = get(cc_conditions, id.toString(), {})
}
if(type === 'CcLoop'){
item = get(cc_loops, id.toString(), {})
}
if(type === 'CcQuestion'){
item = get(cc_questions, id.toString(), {})
if(item.question_type === 'QuestionItem'){
item.question = get(questionItems, item.question_id.toString(), {})
}else if(item.question_type === 'QuestionGrid'){
item.question = get(questionGrids, item.question_id.toString(), {})
}
}
return item
}
const constructLabelClasses = makeStyles((theme) => ({
CcCondition: {
background: `#${ObjectColour('condition')}`,
color: 'white'
},
CcStatement: {
background: `#${ObjectColour('statement')}`,
color: 'white'
},
CcQuestion: {
background: `#${ObjectColour('question')}`,
color: 'white'
}
}));
const ConstructLabel = ({item, type}) => {
const classes = constructLabelClasses();
return (<Chip label={`${item.label}`} className={classes[type]}/>)
}
const QuestionItemListItem = (props) => {
const {item} = props;
if(isNil(item) || isNil(item.question)){
return ''
}
return (
<Grid container spacing={3}>
<Grid item xs={3} sm={6}>
<ConstructLabel item={item} type={'CcQuestion'} />
</Grid>
<Grid item xs={9} sm={6}>
{!isEmpty(item.interviewee) && (
<p>Interviewee : <b>{item.interviewee}</b></p>
)}
{item.question.literal}
{!isEmpty(item.question.instruction) && (
<p><i>{item.question.instruction}</i></p>
)}
{(item.question.rds) && (
<ResponseDomains rds={item.question.rds} />
)}
<VariableItems variables={item.variables} />
</Grid>
</Grid>
)
}
const QuestionGridListItem = (props) => {
const {item} = props;
const classes = useStyles();
if(isNil(item) || isNil(item.question)){
return ''
}
const rows = times(item.question.roster_rows, String)
const question_rows = get(item.question, 'rows', [])
return (
<Grid container spacing={3}>
<Grid item xs={3}>
<ConstructLabel item={item} type={'CcQuestion'} />
</Grid>
<Grid item xs={9}>
{item.question.literal}
{!isEmpty(item.question.instruction) && (
<p><i>{item.question.instruction}</i></p>
)}
<Table size="small">
<TableHead>
<TableRow>
<TableCell><strong>{item.question.pretty_corner_label}</strong></TableCell>
{item.question.cols.map((header)=>(
<TableCell><strong>{header.label}</strong><ResponseDomains rds={[header.rd]} /></TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{item.question && question_rows.map((row)=>(
<TableRow key={row.label}>
<TableCell><strong>{row.label}</strong></TableCell>
</TableRow>
))}
{item.question && rows.map((row, i) => (
<TableRow>
<TableCell className={classes.rosterLabel}><strong>{(i == 0) ? item.question.roster_label : '' }</strong></TableCell>
</TableRow>
))}
</TableBody>
</Table>
<VariableItems variables={item.variables} />
</Grid>
</Grid>
)
}
const QuestionListItem = (props) => {
const {type, id, instrumentId} = props
const item = ObjectFinder(instrumentId, type, id)
if(isNil(item.question)){
return ''
}
if(item.question_type === 'QuestionGrid'){
return <QuestionGridListItem item={item} />
}else{
return <QuestionItemListItem item={item} />
}
}
const responseDomainClasses = makeStyles((theme) => ({
root: {
listStyleType:'none'
}
}));
const VariableItems = ({ variables }) => {
if(isEmpty(variables)){
return ''
}else{
return (
<>
<h3>Variables</h3>
<ul>
{ variables.map((variable) => {
return (
<li>
<Tooltip arrow title={variable.label}><span>{variable.name}</span></Tooltip>
</li>
)
})
}
</ul>
</>
)
}
}
const ResponseDomains = ({ rds }) => {
const classes = responseDomainClasses();
return rds.filter((rd) => { return !isNil(rd) }).map((rd) => {
switch (rd.type) {
case 'ResponseDomainCode':
return(<><ul className={classes.root}><ResponseDomainCodes codes={rd.codes} /></ul><span>Min Responses : <strong>{ rd.min_responses }</strong> Max Responses : <strong>{ rd.max_responses }</strong></span></>)
case 'ResponseDomainText':
return(<ul className={classes.root}><li><TextFieldsIcon /> {rd.label} ({`${(isNil(rd.maxlen)) ? 'no' : rd.maxlen} maximum length`})</li></ul>)
case 'ResponseDomainNumeric':
return(<ul className={classes.root}><li><Filter1Icon /> {rd.label} {rd.params} {rd.subtype}</li></ul>)
case 'ResponseDomainDatetime':
return(<ul className={classes.root}><li><TodayIcon /> {rd.label} {rd.params} {rd.subtype}</li></ul>)
default:
return '';
}
})
}
const ResponseDomainCodes = ({ codes }) => {
return codes.map((code) => {
return(<li><CheckCircleOutlineIcon /> <em>{code.value} </em> = {code.label}</li>)
})
}
const StatementListItem = (props) => {
const {type, id, instrumentId} = props
const item = ObjectFinder(instrumentId, type, id)
return (
<Grid container spacing={3}>
<Grid item xs={3}>
<ConstructLabel item={item} type={type} />
</Grid>
<Grid item xs={9}>
{item.literal}
</Grid>
</Grid>
)
}
const ConditionChildren = (props) => {
const {children, instrumentId, title} = props
const classes = useStyles();
const [open, setOpen] = React.useState(true);
const handleClick = () => {
setOpen(!open);
};
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
className={classes.root}
>
<ListItem>
<Grid container spacing={3}>
<Grid item xs={12}>
<ListItemText primary={title} />
</Grid>
</Grid>
{open ? <ExpandLess onClick={handleClick}/> : <ExpandMore onClick={handleClick}/>}
</ListItem>
{!isEmpty(children) && (
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child) => (
<StyledListItem className={classes.nested}>
{(function() {
switch (child.type) {
case 'CcSequence':
return <SequenceItem instrumentId={instrumentId} id={child.id} type={child.type} title={child.type} children={get(child,'children',[])} />;
case 'CcQuestion':
return <QuestionListItem instrumentId={instrumentId} id={child.id} type={child.type} />
case 'CcCondition':
return <ConditionItem instrumentId={instrumentId} id={child.id} type={child.type} />
case 'CcStatement':
return <StatementListItem instrumentId={instrumentId} id={child.id} type={child.type} />
case 'CcLoop':
return <LoopItem instrumentId={instrumentId} id={child.id} type={child.type} />
default:
return null;
}
})()}
</StyledListItem>
))}
</List>
</Collapse>
)}
</List>
);
}
const ConditionItem = (props) => {
const {type, id, instrumentId, title} = props
const classes = useStyles();
const [open, setOpen] = React.useState(true);
const handleClick = () => {
setOpen(!open);
};
var item = ObjectFinder(instrumentId, type, id)
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
className={classes.root}
>
<ListItem>
<Grid container spacing={3}>
<Grid item xs={3}>
<ConstructLabel item={item} type={type} />
</Grid>
<Grid item xs={9}>
<ListItemText primary={get(item, 'literal', title)} secondary={(item.logic && item.logic.match(/^ *$/) == null) ? item.logic : '[]'} />
</Grid>
</Grid>
{open ? <ExpandLess onClick={handleClick}/> : <ExpandMore onClick={handleClick}/>}
</ListItem>
<ConditionChildren instrumentId={instrumentId} title={'True'} children={item.children} />
<ConditionChildren instrumentId={instrumentId} title={'Else'} children={item.fchildren} />
</List>
);
}
const LoopItem = (props) => {
const {type, id, instrumentId, title} = props
const classes = useStyles();
const [open, setOpen] = React.useState(true);
const handleClick = () => {
setOpen(!open);
};
var item = ObjectFinder(instrumentId, type, id)
var loop_description = `${item.loop_var} from ${item.start_val} while`
if(item.end_val){
loop_description += ` ${item.loop_var} <= ${item.end_val}`
}
if (item.loop_while) {
loop_description += ` ${(item.end_val) ? '&& ' : ''}${item.loop_while}`
}
if (isNil(item.end_val) && isNil(item.loop_while)) {
loop_description += ` []`
}
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
className={classes.root}
>
<ListItem>
<Grid container spacing={3}>
<Grid item xs={3}>
<ConstructLabel item={item} type={type} />
</Grid>
<Grid item xs={9}>
<Typography variant="h6" component="h6">{loop_description}</Typography>
</Grid>
</Grid>
{open ? <ExpandLess onClick={handleClick}/> : <ExpandMore onClick={handleClick}/>}
</ListItem>
{!isEmpty(item.children) && (
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.children.map((child) => (
(function() {
switch (child.type) {
case 'CcSequence':
return (
<StyledListItem className={classes.nested}>
<SequenceItem instrumentId={instrumentId} id={child.id} type={child.type} title={child.type} children={get(child,'children',[])} />
</StyledListItem>)
case 'CcQuestion':
return (
<StyledListItem className={classes.nested}>
<QuestionListItem instrumentId={instrumentId} id={child.id} type={child.type} />
</StyledListItem>)
case 'CcStatement':
return (
<StyledListItem className={classes.nested}>
<StatementListItem instrumentId={instrumentId} id={child.id} type={child.type} />
</StyledListItem>)
case 'CcCondition':
return (
<StyledListItem className={classes.nested}>
<ConditionItem instrumentId={instrumentId} id={child.id} type={child.type} children={get(child,'children',[])} />
</StyledListItem>)
case 'CcLoop':
return (
<StyledListItem className={classes.nested}>
<LoopItem instrumentId={instrumentId} id={child.id} type={child.type} />
</StyledListItem>)
default:
return null;
}
})()
))}
</List>
</Collapse>
)}
</List>
);
}
const SequenceItem = (props) => {
const {type, id, instrumentId} = props
var {title} = props;
const classes = useStyles();
const [open, setOpen] = React.useState(true);
const handleClick = () => {
setOpen(!open);
};
var item = ObjectFinder(instrumentId, props.type, props.id)
title = get(item, 'label', props.title)
return (
<List
className={classes.sequence}
>
<ListItem className={classes.sequence}>
<Typography variant="h5" component="h5">{title}</Typography>
{open ? <ExpandLess onClick={handleClick} /> : <ExpandMore onClick={handleClick} />}
</ListItem >
{!isEmpty(item.children) && (
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.children.map((child) => (
(function() {
switch (child.type) {
case 'CcSequence':
return (
<StyledListItem className={classes.nested}>
<SequenceItem instrumentId={instrumentId} id={child.id} type={child.type} title={child.type} children={get(child,'children',[])} />
</StyledListItem>)
case 'CcQuestion':
return (
<StyledListItem className={classes.nested}>
<QuestionListItem instrumentId={instrumentId} id={child.id} type={child.type} />
</StyledListItem>)
case 'CcStatement':
return (
<StyledListItem className={classes.nested}>
<StatementListItem instrumentId={instrumentId} id={child.id} type={child.type} />
</StyledListItem>)
case 'CcCondition':
return (
<StyledListItem className={classes.nested}>
<ConditionItem instrumentId={instrumentId} id={child.id} type={child.type} children={get(child,'children',[])} />
</StyledListItem>)
case 'CcLoop':
return (
<StyledListItem className={classes.nested}>
<LoopItem instrumentId={instrumentId} id={child.id} type={child.type} children={get(child,'children',[])} />
</StyledListItem>)
default:
return null;
}
})()
))}
</List>
</Collapse>
)}
</List>
);
}
const InstrumentView = (props) => {
const dispatch = useDispatch()
const instrumentId = get(props, "match.params.instrument_id", "")
const instrument = useSelector(state => get(state.instruments, instrumentId));
const sequences = useSelector(state => state.cc_sequences);
const cc_sequences = get(sequences, instrumentId, {})
const [dataLoaded, setDataLoaded] = useState(false);
useEffect(() => {
Promise.all([
dispatch(Instrument.show(instrumentId)),
dispatch(CcSequences.all(instrumentId)),
dispatch(CcStatements.all(instrumentId)),
dispatch(CcConditions.all(instrumentId)),
dispatch(CcLoops.all(instrumentId)),
dispatch(CcQuestions.all(instrumentId)),
dispatch(QuestionItems.all(instrumentId)),
dispatch(QuestionGrids.all(instrumentId)),
dispatch(Variables.all(instrumentId)),
dispatch(Topics.all())
]).then(() => {
setDataLoaded(true)
});
// eslint-disable-next-line react-hooks/exhaustive-deps
},[]);
const sequence = (isEmpty(cc_sequences)) ? undefined : Object.values(cc_sequences).find(element => element.top == true)
return (
<div style={{ height: 500, width: '100%' }}>
<Dashboard title={'View'} instrumentId={instrumentId}>
<InstrumentHeading instrument={instrument} mode={'view'} />
{!dataLoaded
? <Loader />
: <SequenceItem instrumentId={instrumentId} type={'CcSequence'} id={sequence.children[0].id} title={sequence.children[0].label} children={sequence.children[0].children}/>
}
</Dashboard>
</div>
);
}
const StyledChip = withStyles({
root: {
background: 'linear-gradient(45deg, #00adee 30%, #00adee 90%)',
borderRadius: 3,
border: 0,
color: 'white',
height: 30,
'margin-right': 5,
padding: '0 30px',
boxShadow: '0 3px 5px 2px #00adee',
},
label: {
textTransform: '',
},
})(Chip);
const StyledListItem = withStyles({
root: {
borderRadius: 5,
border: '2px solid #00adee',
backgroundColor: 'rgba(0,173,238, 0.1)',
'margin-bottom': '10px'
},
label: {
textTransform: '',
},
})(ListItem);
export default InstrumentView;