From 02bf82ab49ddece9c80d27f096e9ed1b73f9f215 Mon Sep 17 00:00:00 2001 From: Jochen Klar Date: Wed, 1 May 2024 15:10:38 +0200 Subject: [PATCH] Add option providers into new react front end and add markdown functionality for option title and help (#441, #976) --- .../js/interview/actions/actionTypes.js | 3 ++ .../js/interview/actions/interviewActions.js | 33 ++++++++++++++- .../assets/js/interview/api/ProjectApi.js | 4 +- .../question/widgets/AutocompleteInput.js | 30 ++++++++++--- .../question/widgets/AutocompleteWidget.js | 3 +- .../main/question/widgets/CheckboxInput.js | 28 +++++++------ .../main/question/widgets/CheckboxWidget.js | 28 +++++++++---- .../main/question/widgets/RadioInput.js | 42 ++++++++++++------- .../main/question/widgets/RadioWidget.js | 3 +- .../main/question/widgets/SelectInput.js | 30 ++++++++++--- .../main/question/widgets/SelectWidget.js | 3 +- .../widgets/common/AdditionalTextInput.js | 27 ++++++------ .../widgets/common/AdditionalTextareaInput.js | 2 +- .../question/widgets/common/OptionHelp.js | 18 ++++++++ .../question/widgets/common/OptionText.js | 17 ++++++++ .../js/interview/reducers/interviewReducer.js | 5 +++ .../assets/js/interview/utils/options.js | 13 ++++++ .../assets/js/interview/utils/page.js | 23 +++++----- rdmo/projects/assets/scss/interview.scss | 7 ++++ rdmo/projects/serializers/v1/page.py | 4 +- 20 files changed, 247 insertions(+), 76 deletions(-) create mode 100644 rdmo/projects/assets/js/interview/components/main/question/widgets/common/OptionHelp.js create mode 100644 rdmo/projects/assets/js/interview/components/main/question/widgets/common/OptionText.js create mode 100644 rdmo/projects/assets/js/interview/utils/options.js diff --git a/rdmo/projects/assets/js/interview/actions/actionTypes.js b/rdmo/projects/assets/js/interview/actions/actionTypes.js index 257952f8e5..52fcbd06bc 100644 --- a/rdmo/projects/assets/js/interview/actions/actionTypes.js +++ b/rdmo/projects/assets/js/interview/actions/actionTypes.js @@ -15,6 +15,9 @@ export const FETCH_PROGRESS_SUCCESS = 'FETCH_PROGRESS_SUCCESS' export const FETCH_VALUES_SUCCESS = 'FETCH_VALUES_SUCCESS' export const FETCH_VALUES_ERROR = 'FETCH_VALUES_ERROR' +export const FETCH_OPTIONS_SUCCESS = 'FETCH_OPTIONS_SUCCESS' +export const FETCH_OPTIONS_ERROR = 'FETCH_OPTIONS_ERROR' + export const CREATE_VALUE = 'CREATE_VALUE' export const STORE_VALUE_SUCCESS = 'STORE_VALUE_SUCCESS' diff --git a/rdmo/projects/assets/js/interview/actions/interviewActions.js b/rdmo/projects/assets/js/interview/actions/interviewActions.js index 68d75a5ce6..da1ed05b97 100644 --- a/rdmo/projects/assets/js/interview/actions/interviewActions.js +++ b/rdmo/projects/assets/js/interview/actions/interviewActions.js @@ -22,6 +22,8 @@ import { FETCH_PAGE_SUCCESS, FETCH_VALUES_SUCCESS, FETCH_VALUES_ERROR, + FETCH_OPTIONS_SUCCESS, + FETCH_OPTIONS_ERROR, CREATE_VALUE, STORE_VALUE_SUCCESS, STORE_VALUE_ERROR, @@ -47,9 +49,15 @@ export function fetchPage(pageId) { : PageApi.fetchPage(projectId, pageId) return promise.then((page) => { updateLocation(page.id) + initPage(page) + dispatch(fetchNavigation(page)) dispatch(fetchValues(page)) + + page.optionsets.filter((optionset) => optionset.has_provider) + .forEach((optionset) => dispatch(fetchOptions(optionset))) + dispatch(fetchPageSuccess(page, false)) }) } @@ -69,7 +77,6 @@ export function fetchNavigation(page) { return ProjectApi.fetchNavigation(projectId, page && page.section.id) .then((navigation) => dispatch(fetchNavigationSuccess(navigation))) .catch((errors) => dispatch(fetchNavigationError(errors))) - } } @@ -81,6 +88,30 @@ export function fetchNavigationError(errors) { return {type: FETCH_NAVIGATION_ERROR, errors} } +export function fetchOptions(optionset) { + return (dispatch) => { + return ProjectApi.fetchOptions(projectId, optionset.id) + .then((options) => dispatch(fetchOptionsSuccess(optionset, options))) + .catch((errors) => dispatch(fetchOptionsError(errors))) + } +} + +export function fetchOptionsSuccess(optionset, options) { + return (dispatch, getStore) => { + const page = getStore().interview.page + page.optionsets.forEach((pageOptionset) => { + if (pageOptionset.id == optionset.id) { + optionset.options = options + } + }) + return {type: FETCH_OPTIONS_SUCCESS, page} + } +} + +export function fetchOptionsError(errors) { + return {type: FETCH_OPTIONS_ERROR, errors} +} + export function fetchValues(page) { return (dispatch) => { return ValueApi.fetchValues(projectId, { attribute: page.attributes }) diff --git a/rdmo/projects/assets/js/interview/api/ProjectApi.js b/rdmo/projects/assets/js/interview/api/ProjectApi.js index fa45e530b3..64d1567f8f 100644 --- a/rdmo/projects/assets/js/interview/api/ProjectApi.js +++ b/rdmo/projects/assets/js/interview/api/ProjectApi.js @@ -1,5 +1,7 @@ import { isNil } from 'lodash' +import { encodeParams } from 'rdmo/core/assets/js/utils/api' + import BaseApi from 'rdmo/core/assets/js/api/BaseApi' class ProjectsApi extends BaseApi { @@ -21,7 +23,7 @@ class ProjectsApi extends BaseApi { } static fetchOptions(projectId, optionsetId) { - return this.get(`/api/v1/projects/projects/${projectId}/options/${optionsetId}`) + return this.get(`/api/v1/projects/projects/${projectId}/options/?${encodeParams({ optionset: optionsetId })}`) } } diff --git a/rdmo/projects/assets/js/interview/components/main/question/widgets/AutocompleteInput.js b/rdmo/projects/assets/js/interview/components/main/question/widgets/AutocompleteInput.js index e46db7622b..077096ca05 100644 --- a/rdmo/projects/assets/js/interview/components/main/question/widgets/AutocompleteInput.js +++ b/rdmo/projects/assets/js/interview/components/main/question/widgets/AutocompleteInput.js @@ -4,21 +4,33 @@ import PropTypes from 'prop-types' import classNames from 'classnames' import { isNil } from 'lodash' +import OptionHelp from './common/OptionHelp' +import OptionText from './common/OptionText' + const AutocompleteInput = ({ value, options, disabled, isDefault, updateValue }) => { const selectOptions = options.map(option => ({ - value: option.id, + value: option, label: option.text })) const [inputValue, setInputValue] = useState('') useEffect(() => { - setInputValue(selectOptions.find((selectOption) => selectOption.value == value.option)) - }, [value.id, value.option]) + setInputValue(selectOptions.find((selectOption) => ( + selectOption.value.has_provider ? (value.external_id === selectOption.value.id) + : (value.option === selectOption.value.id) + ))) + }, [value.id, value.option, value.external_id]) const handleChange = (selectedOption) => { - updateValue(value, { - option: isNil(selectedOption) ? null : selectedOption.value - }) + if (isNil(selectedOption)) { + updateValue(value, {}) + } else { + if (selectedOption.value.has_provider) { + updateValue(value, { external_id: selectedOption.value.id, text: selectedOption.value.text }) + } else { + updateValue(value, { option: selectedOption.value.id }) + } + } } const classnames = classNames({ @@ -38,6 +50,12 @@ const AutocompleteInput = ({ value, options, disabled, isDefault, updateValue }) handleChange(option) }} isDisabled={disabled} + formatOptionLabel={({ value }) => ( + + + + + )} /> ) } diff --git a/rdmo/projects/assets/js/interview/components/main/question/widgets/AutocompleteWidget.js b/rdmo/projects/assets/js/interview/components/main/question/widgets/AutocompleteWidget.js index bfcd6929c3..bd64167d6f 100644 --- a/rdmo/projects/assets/js/interview/components/main/question/widgets/AutocompleteWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/question/widgets/AutocompleteWidget.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { isDefaultValue } from '../../../../utils/value' +import { gatherOptions } from '../../../../utils/options' import QuestionAddValue from '../QuestionAddValue' import QuestionDefault from '../QuestionDefault' @@ -30,7 +31,7 @@ const AutocompleteWidget = ({ question, values, currentSet, disabled, createValu { @@ -18,11 +20,15 @@ const CheckboxInput = ({ value, option, disabled, onCreate, onUpdate, onDelete } } } - const handleAdditionalValueChange = useDebouncedCallback((value, option, additionalValue) => { + const handleAdditionalValueChange = useDebouncedCallback((value, option, additionalInput) => { if (checked) { - onUpdate(value, { text: additionalValue, option: option.id }) + if (option.has_provider) { + onUpdate(value, { text: option.text, external_id: option.id }) + } else { + onUpdate(value, { option: option.id }) + } } else { - onCreate(option, additionalValue) + onCreate(option, additionalInput) } }, 500) @@ -35,20 +41,18 @@ const CheckboxInput = ({ value, option, disabled, onCreate, onUpdate, onDelete } disabled={disabled} onChange={() => handleChange()} /> - {option.text} + { isEmpty(option.additional_input) && ( - {option.help} + ) } { option.additional_input == 'text' && ( <> : - {' '} - - {' '} - {option.help} + + ) } @@ -56,10 +60,10 @@ const CheckboxInput = ({ value, option, disabled, onCreate, onUpdate, onDelete } option.additional_input == 'textarea' && ( <> : - {' '} - {' '} -
{option.help}
+
+ +
) } diff --git a/rdmo/projects/assets/js/interview/components/main/question/widgets/CheckboxWidget.js b/rdmo/projects/assets/js/interview/components/main/question/widgets/CheckboxWidget.js index 7e9ae0e1c5..d73391911e 100644 --- a/rdmo/projects/assets/js/interview/components/main/question/widgets/CheckboxWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/question/widgets/CheckboxWidget.js @@ -1,23 +1,33 @@ import React from 'react' import PropTypes from 'prop-types' -import { isNil, maxBy } from 'lodash' +import { maxBy } from 'lodash' + +import { gatherOptions } from '../../../../utils/options' import CheckboxInput from './CheckboxInput' const CheckboxWidget = ({ question, values, currentSet, disabled, createValue, updateValue, deleteValue }) => { - const handleCreateValue = (option, text) => { + const handleCreateValue = (option, additionalInput) => { const lastValue = maxBy(values, (v) => v.collection_index) const collectionIndex = lastValue ? lastValue.collection_index + 1 : 0 - createValue({ + const value = { attribute: question.attribute, set_prefix: currentSet.set_prefix, set_index: currentSet.set_index, collection_index: collectionIndex, - option: option.id, - text: isNil(text) ? '' : text - }, true) + } + + if (option.has_provider) { + value.external_id = option.id + value.text = option.text + } else { + value.option = option.id + value.text = additionalInput + } + + createValue(value, true) } return ( @@ -25,10 +35,12 @@ const CheckboxWidget = ({ question, values, currentSet, disabled, createValue, u
{ - question.options.map((option, optionIndex) => ( + gatherOptions(question).map((option, optionIndex) => ( value.option == option.id)} + value={values.find((value) => ( + option.has_provider ? (value.external_id === option.id) : (value.option === option.id) + ))} option={option} disabled={disabled} onCreate={handleCreateValue} diff --git a/rdmo/projects/assets/js/interview/components/main/question/widgets/RadioInput.js b/rdmo/projects/assets/js/interview/components/main/question/widgets/RadioInput.js index 2fca85aa62..5b0330d97c 100644 --- a/rdmo/projects/assets/js/interview/components/main/question/widgets/RadioInput.js +++ b/rdmo/projects/assets/js/interview/components/main/question/widgets/RadioInput.js @@ -6,21 +6,24 @@ import { isEmpty } from 'lodash' import AdditionalTextInput from './common/AdditionalTextInput' import AdditionalTextareaInput from './common/AdditionalTextareaInput' +import OptionHelp from './common/OptionHelp' +import OptionText from './common/OptionText' const RadioInput = ({ value, options, disabled, isDefault, updateValue }) => { + console.log(value.text) const handleChange = (option) => { - if (isEmpty(option.additional_input)) { - updateValue(value, { option: option.id, text: '' }) + if (option.has_provider) { + updateValue(value, { text: option.text, external_id: option.id }) } else { updateValue(value, { option: option.id }) } } - const handleAdditionalValueChange = useDebouncedCallback((value, option, additionalValue) => { + const handleAdditionalValueChange = useDebouncedCallback((value, option, additionalInput) => { updateValue(value, { option: option.id, - text: additionalValue + text: additionalInput }) }, 500) @@ -40,24 +43,28 @@ const RadioInput = ({ value, options, disabled, isDefault, updateValue }) => {
{ const selectOptions = options.map(option => ({ - value: option.id, + value: option, label: option.text })) const [inputValue, setInputValue] = useState('') useEffect(() => { - setInputValue(selectOptions.find((selectOption) => selectOption.value == value.option)) - }, [value.id, value.option]) + setInputValue(selectOptions.find((selectOption) => ( + selectOption.value.has_provider ? (value.external_id === selectOption.value.id) + : (value.option === selectOption.value.id) + ))) + }, [value.id, value.option, value.external_id]) const handleChange = (selectedOption) => { - updateValue(value, { - option: isNil(selectedOption) ? null : selectedOption.value - }) + if (isNil(selectedOption)) { + updateValue(value, {}) + } else { + if (selectedOption.value.has_provider) { + updateValue(value, { external_id: selectedOption.value.id, text: selectedOption.value.text }) + } else { + updateValue(value, { option: selectedOption.value.id }) + } + } } const classnames = classNames({ @@ -38,6 +50,12 @@ const SelectInput = ({ value, options, disabled, isDefault, updateValue }) => { handleChange(option) }} isDisabled={disabled} + formatOptionLabel={({ value }) => ( + + + + + )} /> ) } diff --git a/rdmo/projects/assets/js/interview/components/main/question/widgets/SelectWidget.js b/rdmo/projects/assets/js/interview/components/main/question/widgets/SelectWidget.js index 64132939f0..4788767e84 100644 --- a/rdmo/projects/assets/js/interview/components/main/question/widgets/SelectWidget.js +++ b/rdmo/projects/assets/js/interview/components/main/question/widgets/SelectWidget.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import { isDefaultValue } from '../../../../utils/value' +import { gatherOptions } from '../../../../utils/options' import QuestionAddValue from '../QuestionAddValue' import QuestionDefault from '../QuestionDefault' @@ -30,7 +31,7 @@ const SelectWidget = ({ question, values, currentSet, disabled, createValue, upd
{ +const AdditionalTextInput = ({ className, value, option, disabled, onChange }) => { const [inputValue, setInputValue] = useState('') useEffect(() => { @@ -11,23 +11,26 @@ const AdditionalTextInput = ({ value, option, disabled, onChange }) => { } else { setInputValue(value.option == option.id ? value.text : '') } - }, [get(value, 'id'), get(value, 'option')]) + }, [get(value, 'id'), get(value, 'option'), get(value, 'external_id')]) return ( - { - setInputValue(event.target.value) - onChange(value, option, event.target.value) - }} - /> + + { + setInputValue(event.target.value) + onChange(value, option, event.target.value) + }} + /> + ) } AdditionalTextInput.propTypes = { + className: PropTypes.string, value: PropTypes.object, option: PropTypes.object.isRequired, disabled: PropTypes.bool, diff --git a/rdmo/projects/assets/js/interview/components/main/question/widgets/common/AdditionalTextareaInput.js b/rdmo/projects/assets/js/interview/components/main/question/widgets/common/AdditionalTextareaInput.js index aac31ce851..83c9df6275 100644 --- a/rdmo/projects/assets/js/interview/components/main/question/widgets/common/AdditionalTextareaInput.js +++ b/rdmo/projects/assets/js/interview/components/main/question/widgets/common/AdditionalTextareaInput.js @@ -11,7 +11,7 @@ const AdditionalTextareaInput = ({ value, option, disabled, onChange }) => { } else { setInputValue(value.option == option.id ? value.text : '') } - }, [get(value, 'id'), get(value, 'option')]) + }, [get(value, 'id'), get(value, 'option'), get(value, 'external_id')]) return (