diff --git a/rdmo/projects/assets/js/interview/actions/interviewActions.js b/rdmo/projects/assets/js/interview/actions/interviewActions.js index b37c6f7838..bf8bce4898 100644 --- a/rdmo/projects/assets/js/interview/actions/interviewActions.js +++ b/rdmo/projects/assets/js/interview/actions/interviewActions.js @@ -13,7 +13,7 @@ import { updateLocation } from '../utils/location' import { updateOptions } from '../utils/options' import { initPage } from '../utils/page' import { gatherSets, getDescendants, initSets } from '../utils/set' -import { activateFirstValue, gatherDefaultValues, initValues } from '../utils/value' +import { activateFirstValue, gatherDefaultValues, initValues, compareValues, isEmptyValue } from '../utils/value' import { projectId } from '../utils/meta' import ValueFactory from '../factories/ValueFactory' @@ -272,7 +272,7 @@ export function storeValue(value) { return {type: NOOP} } else { return (dispatch, getState) => { - const valueIndex = getState().interview.values.map((v) => v.id).indexOf(value.id) + const valueIndex = getState().interview.values.findIndex((v) => compareValues(v, value)) const valueFile = value.file const valueSuccess = value.success @@ -363,6 +363,31 @@ export function updateValue(value, attrs, store = true) { } } +export function copyValue(value) { + return (dispatch, getState) => { + const sets = getState().interview.sets + const values = getState().interview.values + + sets.filter((set) => ( + (set.set_prefix == value.set_prefix) && + (set.set_index != value.set_index) + )).forEach((set) => { + const sibling = values.find((v) => ( + (v.attribute == value.attribute) && + (v.set_prefix == set.set_prefix) && + (v.set_index == set.set_index) && + (v.collection_index == value.collection_index) + )) + + if (isNil(sibling)) { + dispatch(storeValue(ValueFactory.create({ ...value, set_index: set.set_index }))) + } else if (isEmptyValue(sibling)) { + dispatch(storeValue(ValueFactory.update(sibling, value))) + } + }) + } +} + export function deleteValue(value) { const pendingId = `deleteValue/${value.id}` @@ -453,9 +478,10 @@ export function createSet(attrs) { if (isNil(value)) { return createSetSuccess() } else { - return dispatch(storeValue(value)).then((action) => { - if (action.type === STORE_VALUE_SUCCESS) { - createSetSuccess(action.value) + return dispatch(storeValue(value)).then(() => { + const storedValue = getState().interview.values.find((v) => compareValues(v, value)) + if (!isNil(storedValue)) { + createSetSuccess(storedValue) } }) } diff --git a/rdmo/projects/assets/js/interview/components/main/page/Page.js b/rdmo/projects/assets/js/interview/components/main/page/Page.js index 8a6155c8cf..56bc9eba84 100644 --- a/rdmo/projects/assets/js/interview/components/main/page/Page.js +++ b/rdmo/projects/assets/js/interview/components/main/page/Page.js @@ -12,7 +12,7 @@ import PageButtons from './PageButtons' import PageHead from './PageHead' const Page = ({ config, templates, overview, page, sets, values, fetchPage, - createValue, updateValue, deleteValue, + createValue, updateValue, deleteValue, copyValue, activateSet, createSet, updateSet, deleteSet }) => { const currentSetPrefix = '' @@ -58,6 +58,7 @@ const Page = ({ config, templates, overview, page, sets, values, fetchPage, createValue={createValue} updateValue={updateValue} deleteValue={deleteValue} + copyValue={copyValue} /> ) } else { @@ -66,17 +67,26 @@ const Page = ({ config, templates, overview, page, sets, values, fetchPage, key={elementIndex} templates={templates} question={element} + sets={sets.filter((set) => ( + set.set_prefix == currentSetPrefix + ))} values={values.filter((value) => ( value.attribute == element.attribute && value.set_prefix == currentSetPrefix && value.set_index == currentSetIndex ))} + siblings={values.filter((value) => ( + value.attribute == element.attribute && + value.set_prefix == currentSetPrefix && + value.set_index != currentSetIndex + ))} disabled={overview.read_only} isManager={isManager} currentSet={currentSet} createValue={createValue} updateValue={updateValue} deleteValue={deleteValue} + copyValue={copyValue} /> ) } @@ -104,7 +114,8 @@ Page.propTypes = { activateSet: PropTypes.func.isRequired, createSet: PropTypes.func.isRequired, updateSet: PropTypes.func.isRequired, - deleteSet: PropTypes.func.isRequired + deleteSet: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default Page diff --git a/rdmo/projects/assets/js/interview/components/main/page/PageHead.js b/rdmo/projects/assets/js/interview/components/main/page/PageHead.js index 57f50e430b..2b3be16650 100644 --- a/rdmo/projects/assets/js/interview/components/main/page/PageHead.js +++ b/rdmo/projects/assets/js/interview/components/main/page/PageHead.js @@ -28,6 +28,11 @@ const PageHead = ({ templates, page, sets, values, currentSet, activateSet, crea } } + const handleOpenCreateModal = (event) => { + event.preventDefault() + openCreateModal() + } + const handleCreateSet = (text) => { createSet({ attribute: page.attribute, @@ -71,7 +76,7 @@ const PageHead = ({ templates, page, sets, values, currentSet, activateSet, crea }) }
  • - + {capitalize(page.verbose_name)}
  • diff --git a/rdmo/projects/assets/js/interview/components/main/question/Question.js b/rdmo/projects/assets/js/interview/components/main/question/Question.js index cea3c70ff4..a74e89ba96 100644 --- a/rdmo/projects/assets/js/interview/components/main/question/Question.js +++ b/rdmo/projects/assets/js/interview/components/main/question/Question.js @@ -12,8 +12,8 @@ import QuestionText from './QuestionText' import QuestionWarning from './QuestionWarning' import QuestionWidget from './QuestionWidget' -const Question = ({ templates, question, values, disabled, isManager, - currentSet, createValue, updateValue, deleteValue }) => { +const Question = ({ templates, question, sets, values, siblings, disabled, isManager, + currentSet, createValue, updateValue, deleteValue, copyValue }) => { return checkQuestion(question, currentSet) && (
    @@ -25,12 +25,15 @@ const Question = ({ templates, question, values, disabled, isManager,
    ) @@ -39,13 +42,16 @@ const Question = ({ templates, question, values, disabled, isManager, Question.propTypes = { templates: PropTypes.object.isRequired, question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool.isRequired, isManager: PropTypes.bool.isRequired, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default Question diff --git a/rdmo/projects/assets/js/interview/components/main/question/QuestionAddValue.js b/rdmo/projects/assets/js/interview/components/main/question/QuestionAddValue.js index 19ca71f249..bf2ee56604 100644 --- a/rdmo/projects/assets/js/interview/components/main/question/QuestionAddValue.js +++ b/rdmo/projects/assets/js/interview/components/main/question/QuestionAddValue.js @@ -17,7 +17,7 @@ const AddValue = ({ question, values, currentSet, disabled, createValue }) => { } return !disabled && question.is_collection && ( - ) diff --git a/rdmo/projects/assets/js/interview/components/main/question/QuestionCopyValue.js b/rdmo/projects/assets/js/interview/components/main/question/QuestionCopyValue.js new file mode 100644 index 0000000000..57c9d40879 --- /dev/null +++ b/rdmo/projects/assets/js/interview/components/main/question/QuestionCopyValue.js @@ -0,0 +1,27 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import { isEmptyValue } from '../../../utils/value' + +const QuestionCopyValue = ({ question, value, siblings, copyValue }) => { + return ( + question.set_collection && + !question.is_collection && + !isEmptyValue(value) && + siblings.some((value) => isEmptyValue(value)) && ( + + ) + ) +} + +QuestionCopyValue.propTypes = { + question: PropTypes.object.isRequired, + value: PropTypes.object.isRequired, + siblings: PropTypes.array.isRequired, + copyValue: PropTypes.func.isRequired +} + +export default QuestionCopyValue diff --git a/rdmo/projects/assets/js/interview/components/main/question/QuestionCopyValues.js b/rdmo/projects/assets/js/interview/components/main/question/QuestionCopyValues.js new file mode 100644 index 0000000000..699c22a767 --- /dev/null +++ b/rdmo/projects/assets/js/interview/components/main/question/QuestionCopyValues.js @@ -0,0 +1,57 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { isEmpty } from 'lodash' + +import { isEmptyValue } from '../../../utils/value' + +const QuestionCopyValues = ({ question, sets, values, siblings, currentSet, copyValue }) => { + const handleCopyValues = () => { + values.forEach((value) => copyValue(value)) + } + + const button = question.widget_type == 'checkbox' ? ( + + ) : ( + + ) + + const hasValues = values.some((value) => !isEmptyValue(value)) + + const hasEmptySiblings = sets.filter((set) => ( + (set.set_prefix == currentSet.set_prefix) && + (set.set_index != currentSet.set_index) + )).some((set) => { + // loop over all other sets and filter siblings accordingly + const setSiblings = siblings.filter((value) => ( + (value.set_prefix == set.set_prefix) && + (value.set_index == set.set_index) + )) + + // check if this set has no sibling at all (for checkboxes) or if they are empty + return isEmpty(setSiblings) || setSiblings.some((value) => isEmptyValue(value)) + }) + + return ( + question.is_collection && + question.set_collection && + hasValues && + hasEmptySiblings && + button + ) +} + +QuestionCopyValues.propTypes = { + question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, + values: PropTypes.array.isRequired, + siblings: PropTypes.array, + currentSet: PropTypes.object.isRequired, + copyValue: PropTypes.func.isRequired +} + +export default QuestionCopyValues diff --git a/rdmo/projects/assets/js/interview/components/main/question/QuestionEraseValues.js b/rdmo/projects/assets/js/interview/components/main/question/QuestionEraseValues.js new file mode 100644 index 0000000000..d6f66f89b0 --- /dev/null +++ b/rdmo/projects/assets/js/interview/components/main/question/QuestionEraseValues.js @@ -0,0 +1,23 @@ +import React from 'react' +import PropTypes from 'prop-types' + +const QuestionEraseValues = ({ values, disabled, deleteValue }) => { + const handleEraseValue = () => { + values.forEach((value) => deleteValue(value)) + } + + return !disabled && ( + + ) +} + +QuestionEraseValues.propTypes = { + values: PropTypes.array.isRequired, + disabled: PropTypes.bool.isRequired, + deleteValue: PropTypes.func.isRequired +} + +export default QuestionEraseValues diff --git a/rdmo/projects/assets/js/interview/components/main/questionset/QuestionSet.js b/rdmo/projects/assets/js/interview/components/main/questionset/QuestionSet.js index cf52efc93f..a0a30396ed 100644 --- a/rdmo/projects/assets/js/interview/components/main/questionset/QuestionSet.js +++ b/rdmo/projects/assets/js/interview/components/main/questionset/QuestionSet.js @@ -14,7 +14,7 @@ import QuestionSetRemoveSet from './QuestionSetRemoveSet' const QuestionSet = ({ templates, questionset, sets, values, disabled, isManager, parentSet, createSet, updateSet, deleteSet, - createValue, updateValue, deleteValue }) => { + createValue, updateValue, deleteValue, copyValue }) => { const setPrefix = getChildPrefix(parentSet) @@ -58,6 +58,7 @@ const QuestionSet = ({ templates, questionset, sets, values, disabled, isManager createValue={createValue} updateValue={updateValue} deleteValue={deleteValue} + copyValue={copyValue} /> ) } else { @@ -66,17 +67,26 @@ const QuestionSet = ({ templates, questionset, sets, values, disabled, isManager key={elementIndex} templates={templates} question={element} + sets={sets.filter((set) => ( + set.set_prefix == setPrefix + ))} values={values.filter((value) => ( value.attribute == element.attribute && value.set_prefix == set.set_prefix && value.set_index == set.set_index ))} + siblings={values.filter((value) => ( + value.attribute == element.attribute && + value.set_prefix == set.set_prefix && + value.set_index != set.set_index + ))} disabled={disabled} isManager={isManager} currentSet={set} createValue={createValue} updateValue={updateValue} deleteValue={deleteValue} + copyValue={copyValue} /> ) } @@ -107,7 +117,8 @@ QuestionSet.propTypes = { deleteSet: PropTypes.func.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default QuestionSet diff --git a/rdmo/projects/assets/js/interview/components/main/widget/CheckboxInput.js b/rdmo/projects/assets/js/interview/components/main/widget/CheckboxInput.js index 38ed08ddea..cf7b3604a5 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/CheckboxInput.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/CheckboxInput.js @@ -31,6 +31,7 @@ const CheckboxInput = ({ question, value, option, disabled, onCreate, onUpdate, }) } else { onUpdate(value, { + text: additionalInput, option: option.id, unit: question.unit, value_type: question.value_type diff --git a/rdmo/projects/assets/js/interview/components/main/widget/CheckboxWidget.js b/rdmo/projects/assets/js/interview/components/main/widget/CheckboxWidget.js index e7d8ee71cb..e1cf9975f1 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/CheckboxWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/CheckboxWidget.js @@ -4,12 +4,15 @@ import { maxBy } from 'lodash' import { gatherOptions } from '../../../utils/options' +import QuestionCopyValues from '../question/QuestionCopyValues' +import QuestionEraseValues from '../question/QuestionEraseValues' import QuestionError from '../question/QuestionError' import QuestionSuccess from '../question/QuestionSuccess' import CheckboxInput from './CheckboxInput' -const CheckboxWidget = ({ question, values, currentSet, disabled, createValue, updateValue, deleteValue }) => { +const CheckboxWidget = ({ question, sets, values, siblings, currentSet, disabled, + createValue, updateValue, deleteValue, copyValue }) => { const handleCreateValue = (option, additionalInput) => { const lastValue = maxBy(values, (v) => v.collection_index) @@ -69,6 +72,19 @@ const CheckboxWidget = ({ question, values, currentSet, disabled, createValue, u
    + +
    @@ -79,12 +95,15 @@ const CheckboxWidget = ({ question, values, currentSet, disabled, createValue, u CheckboxWidget.propTypes = { question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default CheckboxWidget diff --git a/rdmo/projects/assets/js/interview/components/main/widget/DateWidget.js b/rdmo/projects/assets/js/interview/components/main/widget/DateWidget.js index 09010cbc33..71903b3a2e 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/DateWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/DateWidget.js @@ -2,6 +2,8 @@ import React from 'react' import PropTypes from 'prop-types' import QuestionAddValue from '../question/QuestionAddValue' +import QuestionCopyValue from '../question/QuestionCopyValue' +import QuestionCopyValues from '../question/QuestionCopyValues' import QuestionDefault from '../question/QuestionDefault' import QuestionError from '../question/QuestionError' import QuestionEraseValue from '../question/QuestionEraseValue' @@ -10,7 +12,8 @@ import QuestionRemoveValue from '../question/QuestionRemoveValue' import DateInput from './DateInput' -const DateWidget = ({ question, values, currentSet, disabled, createValue, updateValue, deleteValue }) => { +const DateWidget = ({ question, sets, values, siblings, currentSet, disabled, + createValue, updateValue, deleteValue, copyValue }) => { return (
    { @@ -33,6 +36,7 @@ const DateWidget = ({ question, values, currentSet, disabled, createValue, updat disabled={disabled} deleteValue={deleteValue} /> +
    } @@ -48,6 +52,15 @@ const DateWidget = ({ question, values, currentSet, disabled, createValue, updat currentSet={currentSet} disabled={disabled} createValue={createValue} + copyValue={copyValue} + /> + ) @@ -55,12 +68,15 @@ const DateWidget = ({ question, values, currentSet, disabled, createValue, updat DateWidget.propTypes = { question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default DateWidget diff --git a/rdmo/projects/assets/js/interview/components/main/widget/RadioWidget.js b/rdmo/projects/assets/js/interview/components/main/widget/RadioWidget.js index f1dd333a52..4a43ece34a 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/RadioWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/RadioWidget.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types' import { gatherOptions } from '../../../utils/options' import QuestionAddValue from '../question/QuestionAddValue' +import QuestionCopyValue from '../question/QuestionCopyValue' +import QuestionCopyValues from '../question/QuestionCopyValues' import QuestionDefault from '../question/QuestionDefault' import QuestionError from '../question/QuestionError' import QuestionSuccess from '../question/QuestionSuccess' @@ -12,7 +14,8 @@ import QuestionRemoveValue from '../question/QuestionRemoveValue' import RadioInput from './RadioInput' -const RadioWidget = ({ question, values, currentSet, disabled, createValue, updateValue, deleteValue }) => { +const RadioWidget = ({ question, sets, values, siblings, currentSet, disabled, + createValue, updateValue, deleteValue, copyValue }) => { return (
    { @@ -36,6 +39,7 @@ const RadioWidget = ({ question, values, currentSet, disabled, createValue, upda disabled={disabled} deleteValue={deleteValue} /> +
    } @@ -51,6 +55,15 @@ const RadioWidget = ({ question, values, currentSet, disabled, createValue, upda currentSet={currentSet} disabled={disabled} createValue={createValue} + copyValue={copyValue} + /> + ) @@ -58,12 +71,15 @@ const RadioWidget = ({ question, values, currentSet, disabled, createValue, upda RadioWidget.propTypes = { question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default RadioWidget diff --git a/rdmo/projects/assets/js/interview/components/main/widget/RangeWidget.js b/rdmo/projects/assets/js/interview/components/main/widget/RangeWidget.js index 692f66d5a4..9d7d42b9bb 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/RangeWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/RangeWidget.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types' import { initRange } from '../../../utils/value' import QuestionAddValue from '../question/QuestionAddValue' +import QuestionCopyValue from '../question/QuestionCopyValue' +import QuestionCopyValues from '../question/QuestionCopyValues' import QuestionDefault from '../question/QuestionDefault' import QuestionError from '../question/QuestionError' import QuestionSuccess from '../question/QuestionSuccess' @@ -12,7 +14,8 @@ import QuestionRemoveValue from '../question/QuestionRemoveValue' import RangeInput from './RangeInput' -const RangeWidget = ({ question, values, currentSet, disabled, createValue, updateValue, deleteValue }) => { +const RangeWidget = ({ question, sets, values, siblings, currentSet, disabled, + createValue, updateValue, deleteValue, copyValue }) => { const handleCreateValue = (value) => { initRange(question, value) @@ -46,6 +49,7 @@ const RangeWidget = ({ question, values, currentSet, disabled, createValue, upda disabled={disabled} deleteValue={deleteValue} /> + } @@ -61,6 +65,15 @@ const RangeWidget = ({ question, values, currentSet, disabled, createValue, upda currentSet={currentSet} disabled={disabled} createValue={handleCreateValue} + copyValue={copyValue} + /> + ) @@ -68,12 +81,15 @@ const RangeWidget = ({ question, values, currentSet, disabled, createValue, upda RangeWidget.propTypes = { question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default RangeWidget diff --git a/rdmo/projects/assets/js/interview/components/main/widget/SelectWidget.js b/rdmo/projects/assets/js/interview/components/main/widget/SelectWidget.js index d1eb95347b..fce4140a7e 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/SelectWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/SelectWidget.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types' import { gatherOptions } from '../../../utils/options' import QuestionAddValue from '../question/QuestionAddValue' +import QuestionCopyValue from '../question/QuestionCopyValue' +import QuestionCopyValues from '../question/QuestionCopyValues' import QuestionDefault from '../question/QuestionDefault' import QuestionEraseValue from '../question/QuestionEraseValue' import QuestionError from '../question/QuestionError' @@ -12,8 +14,8 @@ import QuestionRemoveValue from '../question/QuestionRemoveValue' import SelectInput from './SelectInput' -const SelectWidget = ({ question, values, currentSet, disabled, creatable, - createValue, updateValue, deleteValue }) => { +const SelectWidget = ({ question, sets, values, siblings, currentSet, disabled, creatable, + createValue, updateValue, deleteValue, copyValue }) => { return (
    { @@ -38,6 +40,7 @@ const SelectWidget = ({ question, values, currentSet, disabled, creatable, disabled={disabled} deleteValue={deleteValue} /> +
    } @@ -54,19 +57,30 @@ const SelectWidget = ({ question, values, currentSet, disabled, creatable, disabled={disabled} createValue={createValue} /> + ) } SelectWidget.propTypes = { question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool, creatable: PropTypes.bool, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default SelectWidget diff --git a/rdmo/projects/assets/js/interview/components/main/widget/TextWidget.js b/rdmo/projects/assets/js/interview/components/main/widget/TextWidget.js index 7eb55f8032..fbee0d5235 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/TextWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/TextWidget.js @@ -2,6 +2,8 @@ import React from 'react' import PropTypes from 'prop-types' import QuestionAddValue from '../question/QuestionAddValue' +import QuestionCopyValue from '../question/QuestionCopyValue' +import QuestionCopyValues from '../question/QuestionCopyValues' import QuestionDefault from '../question/QuestionDefault' import QuestionError from '../question/QuestionError' import QuestionRemoveValue from '../question/QuestionRemoveValue' @@ -9,7 +11,8 @@ import QuestionSuccess from '../question/QuestionSuccess' import TextInput from './TextInput' -const TextWidget = ({ question, values, currentSet, disabled, createValue, updateValue, deleteValue }) => { +const TextWidget = ({ question, sets, values, siblings, currentSet, disabled, + createValue, updateValue, deleteValue, copyValue }) => { return (
    { @@ -31,6 +34,7 @@ const TextWidget = ({ question, values, currentSet, disabled, createValue, updat disabled={disabled} deleteValue={deleteValue} /> +
    } @@ -47,18 +51,29 @@ const TextWidget = ({ question, values, currentSet, disabled, createValue, updat disabled={disabled} createValue={createValue} /> + ) } TextWidget.propTypes = { question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default TextWidget diff --git a/rdmo/projects/assets/js/interview/components/main/widget/TextareaWidget.js b/rdmo/projects/assets/js/interview/components/main/widget/TextareaWidget.js index 751aef834d..cc063a3bf5 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/TextareaWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/TextareaWidget.js @@ -2,6 +2,8 @@ import React from 'react' import PropTypes from 'prop-types' import QuestionAddValue from '../question/QuestionAddValue' +import QuestionCopyValue from '../question/QuestionCopyValue' +import QuestionCopyValues from '../question/QuestionCopyValues' import QuestionDefault from '../question/QuestionDefault' import QuestionError from '../question/QuestionError' import QuestionSuccess from '../question/QuestionSuccess' @@ -9,7 +11,8 @@ import QuestionRemoveValue from '../question/QuestionRemoveValue' import TextareaInput from './TextareaInput' -const TextareaWidget = ({ question, values, currentSet, disabled, createValue, updateValue, deleteValue }) => { +const TextareaWidget = ({ question, sets, values, siblings, currentSet, disabled, + createValue, updateValue, deleteValue, copyValue }) => { return (
    { @@ -31,6 +34,7 @@ const TextareaWidget = ({ question, values, currentSet, disabled, createValue, u disabled={disabled} deleteValue={deleteValue} /> +
    } @@ -47,18 +51,29 @@ const TextareaWidget = ({ question, values, currentSet, disabled, createValue, u disabled={disabled} createValue={createValue} /> + ) } TextareaWidget.propTypes = { question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default TextareaWidget diff --git a/rdmo/projects/assets/js/interview/components/main/widget/YesNoWidget.js b/rdmo/projects/assets/js/interview/components/main/widget/YesNoWidget.js index 5ccc289ff8..ec7e965272 100644 --- a/rdmo/projects/assets/js/interview/components/main/widget/YesNoWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/widget/YesNoWidget.js @@ -2,6 +2,8 @@ import React from 'react' import PropTypes from 'prop-types' import QuestionAddValue from '../question/QuestionAddValue' +import QuestionCopyValue from '../question/QuestionCopyValue' +import QuestionCopyValues from '../question/QuestionCopyValues' import QuestionDefault from '../question/QuestionDefault' import QuestionError from '../question/QuestionError' import QuestionEraseValue from '../question/QuestionEraseValue' @@ -10,7 +12,8 @@ import QuestionRemoveValue from '../question/QuestionRemoveValue' import YesNoInput from './YesNoInput' -const YesNoWidget = ({ question, values, currentSet, disabled, createValue, updateValue, deleteValue }) => { +const YesNoWidget = ({ question, sets, values, siblings, currentSet, disabled, + createValue, updateValue, deleteValue, copyValue }) => { return (
    { @@ -33,6 +36,7 @@ const YesNoWidget = ({ question, values, currentSet, disabled, createValue, upda disabled={disabled} deleteValue={deleteValue} /> +
    } @@ -49,18 +53,29 @@ const YesNoWidget = ({ question, values, currentSet, disabled, createValue, upda disabled={disabled} createValue={createValue} /> + ) } YesNoWidget.propTypes = { question: PropTypes.object.isRequired, + sets: PropTypes.array.isRequired, values: PropTypes.array.isRequired, + siblings: PropTypes.array, disabled: PropTypes.bool, currentSet: PropTypes.object.isRequired, createValue: PropTypes.func.isRequired, updateValue: PropTypes.func.isRequired, - deleteValue: PropTypes.func.isRequired + deleteValue: PropTypes.func.isRequired, + copyValue: PropTypes.func.isRequired } export default YesNoWidget diff --git a/rdmo/projects/assets/js/interview/containers/Main.js b/rdmo/projects/assets/js/interview/containers/Main.js index 7e8c3b1c49..a8f6189fe2 100644 --- a/rdmo/projects/assets/js/interview/containers/Main.js +++ b/rdmo/projects/assets/js/interview/containers/Main.js @@ -53,6 +53,7 @@ const Main = ({ config, settings, templates, user, project, interview, configAct createSet={interviewActions.createSet} updateSet={interviewActions.updateSet} deleteSet={interviewActions.deleteSet} + copyValue={interviewActions.copyValue} /> ) } diff --git a/rdmo/projects/assets/js/interview/reducers/interviewReducer.js b/rdmo/projects/assets/js/interview/reducers/interviewReducer.js index 1b48a12e97..39ac156bc2 100644 --- a/rdmo/projects/assets/js/interview/reducers/interviewReducer.js +++ b/rdmo/projects/assets/js/interview/reducers/interviewReducer.js @@ -67,7 +67,7 @@ export default function interviewReducer(state = initialState, action) { return { ...state, values: [...state.values, action.value] } } case DELETE_VALUE_SUCCESS: - return {...state, values: state.values.filter((value) => value !== action.value)} + return {...state, values: state.values.filter((value) => value.id !== action.value.id)} case CREATE_SET: return { ...state, values: action.values, sets: action.sets } case DELETE_SET_SUCCESS: diff --git a/rdmo/projects/assets/js/interview/utils/page.js b/rdmo/projects/assets/js/interview/utils/page.js index f1d40c39ae..e254514e69 100644 --- a/rdmo/projects/assets/js/interview/utils/page.js +++ b/rdmo/projects/assets/js/interview/utils/page.js @@ -20,36 +20,36 @@ const initQuestionSet = (questionset) => { // aggregate questionsets from descendants questionset.questionsets = questionset.elements.reduce((questionsets, element) => { if (element.model == 'questions.questionset') { - return questionsets.concat(element.questionsets) + return [...questionsets, element, ...element.questionsets] // add this questionset and all it's questionsets } else { - return questionsets + return questionsets // do nothing } }, []) - // aggregate optionsets from descendants + // aggregate questions from descendants questionset.questions = questionset.elements.reduce((questions, element) => { if (element.model == 'questions.questionset') { - return questions.concat(element.questions) + return questions.concat(element.questions) // add the questions of this questionset } else { - return [...questions, element] + return [...questions, element] // add this question } }, []) // aggregate optionsets from descendants questionset.optionsets = questionset.elements.reduce((optionsets, element) => { if (element.model == 'questions.questionset') { - return optionsets.concat(element.optionsets) + return optionsets.concat(element.optionsets) // add all optionsets of the questions of this questionset } else { - return [...optionsets, ...element.optionsets] + return [...optionsets, ...element.optionsets] // add all optionsets of this question } }, []) // aggregate attributes from descendants questionset.attributes = questionset.elements.reduce((attributes, element) => { if (element.model == 'questions.questionset') { - return attributes.concat(element.attributes) + return attributes.concat(element.attributes) // add all attributes of this questionset and its questions } else { - return [...attributes, element.attribute] + return [...attributes, element.attribute] // add the attribute of this question } }, [questionset.attribute]).filter((a) => !isNil(a)) } diff --git a/rdmo/projects/assets/js/interview/utils/value.js b/rdmo/projects/assets/js/interview/utils/value.js index f66a706073..5723f8644b 100644 --- a/rdmo/projects/assets/js/interview/utils/value.js +++ b/rdmo/projects/assets/js/interview/utils/value.js @@ -39,28 +39,33 @@ const initValues = (sets, values, element, setPrefix) => { setPrefix = '' } + // loop over all sets of the current set prefix and over all questions sets.filter((set) => set.set_prefix === setPrefix).forEach((set) => { element.elements.filter((e) => (e.model === 'questions.question')).forEach((question) => { + // check if there is any value for this question and set if (isNil(values.find((value) => ( (value.attribute === question.attribute) && - (value.set_prefix == set.set_prefix) && - (value.set_index == set.set_index) + (value.set_prefix === set.set_prefix) && + (value.set_index === set.set_index) )))) { - const value = ValueFactory.create({ - attribute: question.attribute, - set_prefix: set.set_prefix, - set_index: set.set_index, - set_collection: question.set_collection, - text: question.default_text, - option: question.default_option, - external_id: question.default_external_id - }) - - if (question.widget_class === 'range') { - initRange(question, value) - } + // if there is no value, create one, but not for checkboxes + if (question.widget_class !== 'checkbox') { + const value = ValueFactory.create({ + attribute: question.attribute, + set_prefix: set.set_prefix, + set_index: set.set_index, + set_collection: question.set_collection, + text: question.default_text, + option: question.default_option, + external_id: question.default_external_id + }) + + if (question.widget_class === 'range') { + initRange(question, value) + } - values.push(value) + values.push(value) + } } }) @@ -86,4 +91,21 @@ const activateFirstValue = (page, values) => { } } -export { isDefaultValue, gatherDefaultValues, initValues, initRange, activateFirstValue } +const compareValues = (a, b) => { + if (isNil(a.id) || isNil(b.id)) { + return (a.attribute == b.attribute) && + (a.set_prefix == b.set_prefix) && + (a.set_index == b.set_index) && + (a.collection_index == b.collection_index) + } else { + return a.id == b.id + } +} + +const isEmptyValue = (value) => { + return isNil(value.id) || ( + isEmpty(value.text) && isNil(value.option) && isEmpty(value.external_id) + ) +} + +export { isDefaultValue, gatherDefaultValues, initValues, initRange, activateFirstValue, compareValues, isEmptyValue } diff --git a/rdmo/projects/assets/scss/interview.scss b/rdmo/projects/assets/scss/interview.scss index 4936fa30a7..20eb128e67 100644 --- a/rdmo/projects/assets/scss/interview.scss +++ b/rdmo/projects/assets/scss/interview.scss @@ -180,8 +180,7 @@ display: flex; gap: 10px; - .btn-erase-value, - .btn-remove-value, + .btn.btn-link, .success-indicator { opacity: 0.8; line-height: 20px; @@ -190,13 +189,11 @@ padding-right: 0; } - .btn-erase-value:hover, - .btn-remove-value:hover { + .btn.btn-link:hover { opacity: 1; } - .btn-erase-value:focus, - .btn-remove-value:focus { + .btn.btn-link:focus { outline: 0; }