Skip to content

Commit

Permalink
Implement QuestionSets in react front-end (and more...)
Browse files Browse the repository at this point in the history
  • Loading branch information
jochenklar committed Aug 25, 2024
1 parent bd6b70d commit fb64bfe
Show file tree
Hide file tree
Showing 24 changed files with 539 additions and 445 deletions.
3 changes: 2 additions & 1 deletion rdmo/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,13 @@

TEMPLATES_API = [
'projects/project_interview_add_field_help.html',
'projects/project_interview_add_set_help.html',
'projects/project_interview_buttons_help.html',
'projects/project_interview_multiple_values_warning.html',
'projects/project_interview_navigation_help.html',
'projects/project_interview_overview_help.html',
'projects/project_interview_page_tabs_help.html',
'projects/project_interview_progress_help.html',
'projects/project_interview_multiple_values_warning.html',
]

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Expand Down
4 changes: 4 additions & 0 deletions rdmo/projects/assets/js/interview/actions/actionTypes.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const NOOP = 'NOOP'

export const FETCH_NAVIGATION_ERROR = 'FETCH_NAVIGATION_ERROR'
export const FETCH_NAVIGATION_SUCCESS = 'FETCH_NAVIGATION_SUCCESS'

Expand All @@ -22,6 +24,8 @@ export const DELETE_VALUE_SUCCESS = 'DELETE_VALUE_SUCCESS'
export const DELETE_VALUE_ERROR = 'DELETE_VALUE_ERROR'

export const ACTIVATE_SET = 'ACTIVATE_SET'

export const CREATE_SET = 'CREATE_SET'

export const DELETE_SET_SUCCESS = 'DELETE_SET_SUCCESS'
export const DELETE_SET_ERROR = 'DELETE_SET_ERROR'
79 changes: 56 additions & 23 deletions rdmo/projects/assets/js/interview/actions/interviewActions.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { isNil } from 'lodash'
import { isEmpty, isNil } from 'lodash'

import PageApi from '../api/PageApi'
import ProjectApi from '../api/ProjectApi'
import ValueApi from '../api/ValueApi'

import { updateLocation } from '../utils/location'
import { getAttributes, initPage } from '../utils/page'
import { initSets } from '../utils/set'

import { initPage } from '../utils/page'
import { gatherSets, getDescendants, initSets } from '../utils/set'
import { initValues } from '../utils/value'
import projectId from '../utils/projectId'

import ValueFactory from '../factories/ValueFactory'
import SetFactory from '../factories/SetFactory'

import {
NOOP,
FETCH_NAVIGATION_ERROR,
FETCH_NAVIGATION_SUCCESS,
FETCH_OVERVIEW_ERROR,
Expand Down Expand Up @@ -103,15 +105,19 @@ export function fetchValues() {
return (dispatch, getStore) => {
const page = getStore().interview.page

return ValueApi.fetchValues(projectId, { attribute: getAttributes(page) })
return ValueApi.fetchValues(projectId, { attribute: page.attributes })
.then((values) => dispatch(fetchValuesSuccess(values, page)))
.catch((errors) => dispatch(fetchValuesError(errors)))
}
}

export function fetchValuesSuccess(values, page) {
const sets = initSets(values)
return {type: FETCH_VALUES_SUCCESS, values: initValues(values, sets, page), sets}
const sets = gatherSets(values)

initSets(sets, page)
initValues(sets, values, page)

return {type: FETCH_VALUES_SUCCESS, values, sets}
}

export function fetchValuesError(errors) {
Expand Down Expand Up @@ -160,47 +166,61 @@ export function updateValue(value, attrs) {
}

export function deleteValue(value) {
return (dispatch, getStore) => {
const valueIndex = getStore().interview.values.indexOf(value)

return (dispatch) => {
if (isNil(value.id)) {
return dispatch(deleteValueSuccess(valueIndex))
return dispatch(deleteValueSuccess(value))
} else {
return ValueApi.deleteValue(projectId, value)
.then(() => dispatch(deleteValueSuccess(valueIndex)))
.then(() => dispatch(deleteValueSuccess(value)))
.catch((errors) => dispatch(deleteValueError(errors)))
}
}
}

export function deleteValueSuccess(valueIndex) {
return {type: DELETE_VALUE_SUCCESS, valueIndex}
export function deleteValueSuccess(value) {
return {type: DELETE_VALUE_SUCCESS, value}
}

export function deleteValueError(errors) {
return {type: DELETE_VALUE_ERROR, errors}
}

export function activateSet(set) {
return updateConfig('rdmo.interview', 'page.currentSetIndex', set.set_index)
if (isEmpty(set.set_prefix)) {
return updateConfig('rdmo.interview', 'page.currentSetIndex', set.set_index)
} else {
return { type: NOOP }
}
}

export function createSet(attrs) {
return (dispatch) => {
return (dispatch, getState) => {
// create a new set
const set = SetFactory.create(attrs)

// create a value for the text if the page has an attribute
const value = isNil(attrs.attribute) ? null : ValueFactory.create(attrs)

if (isNil(value)) {
// create an action to be called immediately or after saving the value
const action = (value) => {
dispatch(activateSet(set))
return dispatch({type: CREATE_SET, set})

const state = getState().interview

const page = state.page
const sets = [...state.sets, set]
const values = isNil(value) ? [...state.values] : [...state.values, value]

initSets(sets, page)
initValues(sets, values, page)

return dispatch({type: CREATE_SET, values, sets})
}

if (isNil(value)) {
return action()
} else {
return dispatch(storeValue(value)).then(() => {
dispatch(activateSet(set))
return dispatch({type: CREATE_SET, set})
})
return dispatch(storeValue(value)).then((value) => action(value))
}
}
}
Expand All @@ -211,7 +231,14 @@ export function updateSet(setValue, attrs) {

export function deleteSet(set, setValue) {
if (isNil(setValue)) {
// TODO: delete all values for all questions in the set
return (dispatch, getState) => {
// gather all values for this set and it's descendants
const values = getDescendants(getState().interview.values, set)

return Promise.all(values.map((value) => ValueApi.deleteValue(projectId, value)))
.then(() => dispatch(deleteSetSuccess(set)))
.catch((errors) => dispatch(deleteValueError(errors)))
}
} else {
return (dispatch, getState) => {
return ValueApi.deleteSet(projectId, setValue)
Expand All @@ -235,7 +262,13 @@ export function deleteSet(set, setValue) {
}

export function deleteSetSuccess(set) {
return {type: DELETE_SET_SUCCESS, set}

return (dispatch, getState) => {
// again, gather all values for this set and it's descendants
const sets = [...getDescendants(getState().interview.sets, set), set]
const values = getDescendants(getState().interview.values, set)
return dispatch({type: DELETE_SET_SUCCESS, sets, values})
}
}

export function deleteSetError(errors) {
Expand Down
4 changes: 3 additions & 1 deletion rdmo/projects/assets/js/interview/api/ValueApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ class ValueApi extends BaseApi {
}

static deleteValue(projectId, value) {
return this.delete(`/api/v1/projects/projects/${projectId}/values/${value.id}/`)
if (!isUndefined(value.id)) {
return this.delete(`/api/v1/projects/projects/${projectId}/values/${value.id}/`)
}
}

static deleteSet(projectId, value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const Page = ({ config, templates, page, sets, values, fetchPage,

const currentSetPrefix = ''
const currentSetIndex = page.is_collection ? get(config, 'page.currentSetIndex', 0) : 0
const currentSet = sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == currentSetIndex))
const pageSets = sets.filter((set) => (set.set_prefix == currentSetPrefix))
const currentSet = sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == currentSetIndex)) ||
sets.find((set) => (set.set_prefix == currentSetPrefix && set.set_index == 0)) // sanity check

return (
<div className="interview-page">
Expand All @@ -28,7 +28,7 @@ const Page = ({ config, templates, page, sets, values, fetchPage,
<PageHead
page={page}
help={templates.project_interview_page_tabs_help}
sets={pageSets}
sets={sets.filter((set) => (set.set_prefix == currentSetPrefix))}
values={isNil(page.attribute) ? [] : values.filter((value) => (value.attribute == page.attribute))}
currentSet={currentSet}
activateSet={activateSet}
Expand Down
33 changes: 20 additions & 13 deletions rdmo/projects/assets/js/interview/components/main/page/PageHead.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import capitalize from 'lodash/capitalize'
import isNil from 'lodash/isNil'
import last from 'lodash/last'
import { capitalize, isNil, last } from 'lodash'

import Template from 'rdmo/core/assets/js/components/Template'
import useModal from 'rdmo/core/assets/js/hooks/useModal'
Expand All @@ -13,9 +11,11 @@ import PageHeadFormModal from './PageHeadFormModal'

const PageHead = ({ page, help, sets, values, currentSet, activateSet, createSet, updateSet, deleteSet }) => {

const currentSetValue = values.find((value) => (
value.set_prefix == currentSet.set_prefix && value.set_index == currentSet.set_index
))
const currentSetValue = isNil(currentSet) ? null : (
values.find((value) => (
value.set_prefix == currentSet.set_prefix && value.set_index == currentSet.set_index
))
)

const [showCreateModal, openCreateModal, closeCreateModal] = useModal()
const [showUpdateModal, openUpdateModal, closeUpdateModal] = useModal()
Expand All @@ -29,7 +29,11 @@ const PageHead = ({ page, help, sets, values, currentSet, activateSet, createSet
}

const handleCreateSet = (text) => {
createSet({ set_index: last(sets) ? last(sets).set_index + 1 : 0, text })
createSet({
attribute: page.attribute,
set_index: last(sets) ? last(sets).set_index + 1 : 0,
text
})
closeCreateModal()
}

Expand Down Expand Up @@ -65,13 +69,17 @@ const PageHead = ({ page, help, sets, values, currentSet, activateSet, createSet
})
}
<li>
<a href="#" className="text-success" title={gettext('Add tab')} onClick={openCreateModal}>
<a href="#" title={gettext('Add tab')} className="add-set" onClick={openCreateModal}>
<i className="fa fa-plus fa-btn"></i> {capitalize(page.verbose_name)}
</a>
</li>
</ul>
<div className="interview-page-tabs-buttons">
<button className="btn-link fa fa-pencil" title={gettext('Edit tab')} onClick={openUpdateModal} />
{
page.attribute && (
<button className="btn-link fa fa-pencil" title={gettext('Edit tab')} onClick={openUpdateModal} />
)
}
<button className="btn-link fa fa-trash" title={gettext('Remove tab')} onClick={openDeleteModal} />
</div>
</>
Expand All @@ -82,9 +90,8 @@ const PageHead = ({ page, help, sets, values, currentSet, activateSet, createSet
)
}


<PageHeadFormModal
title={gettext(page.verbose_name)}
title={capitalize(page.verbose_name)}
show={showCreateModal}
initial={isNil(page.attribute) ? null : ''}
onClose={closeCreateModal}
Expand All @@ -93,7 +100,7 @@ const PageHead = ({ page, help, sets, values, currentSet, activateSet, createSet
{
currentSetValue && (
<PageHeadFormModal
title={gettext(page.verbose_name)}
title={capitalize(page.verbose_name)}
show={showUpdateModal}
initial={currentSetValue.text}
onClose={closeUpdateModal}
Expand All @@ -102,7 +109,7 @@ const PageHead = ({ page, help, sets, values, currentSet, activateSet, createSet
)
}
<PageHeadDeleteModal
title={gettext(page.verbose_name)}
title={capitalize(page.verbose_name)}
show={showDeleteModal}
onClose={closeDeleteModal}
onSubmit={handleDeleteSet}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const PageHeadDeleteModal = ({ title, show, onClose, onSubmit }) => {
return (
<Modal title={title} show={show} submitText={gettext('Delete')} submitColor="danger"
onClose={onClose} onSubmit={onSubmit}>
<p>You are about to permanently delete this tab.</p>
<p>This includes all given answers for this tab on all pages, not just this one.</p>
<p className="text-danger">This action cannot be undone!</p>
<p>{gettext('You are about to permanently delete this tab.')}</p>
<p>{gettext('This includes all given answers for this tab on all pages, not just this one.')}</p>
<p className="text-danger">{gettext('This action cannot be undone!')}</p>
</Modal>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import React, { useState, useEffect } from 'react'
import React, { useState, useRef, useEffect } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import isNil from 'lodash/isNil'
import isEmpty from 'lodash/isEmpty'
import { isEmpty, isNil } from 'lodash'

import Modal from 'rdmo/core/assets/js/components/Modal'
import useFocusEffect from '../../../hooks/useFocusEffect'


const PageHeadFormModal = ({ title, show, initial, onClose, onSubmit }) => {

const ref = useRef(null)
const [inputValue, setInputValue] = useState('')
const [hasError, setHasError] = useState(false)
const submitText = isNil(initial) ? gettext('Create') : gettext('Update')
const submitColor = isNil(initial) ? 'success' : 'primary'
const submitText = isEmpty(initial) ? gettext('Create') : gettext('Update')
const submitColor = isEmpty(initial) ? 'success' : 'primary'

const handleSubmit = () => {
if (isEmpty(inputValue)) {
if (isEmpty(inputValue) && !isNil(initial)) {
setHasError(true)
} else {
onSubmit(inputValue)
}
}

// update the inputValue when the modal ist shown
// update the inputValue
useEffect(() => {
if (show) {
setInputValue(initial || '')
Expand All @@ -35,6 +37,9 @@ const PageHeadFormModal = ({ title, show, initial, onClose, onSubmit }) => {
}
}, [inputValue])

// focus when the modal is shown
useFocusEffect(ref, [show])

return (
<Modal title={title} show={show} submitText={submitText} submitColor={submitColor}
onClose={onClose} onSubmit={handleSubmit} disableSubmit={hasError}>
Expand All @@ -49,6 +54,7 @@ const PageHeadFormModal = ({ title, show, initial, onClose, onSubmit }) => {
{gettext('Name')}
</label>
<input
ref={ref}
className="form-control"
id="interview-page-tabs-modal-form-title"
type="text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import QuestionWidget from './QuestionWidget'

const Question = ({ templates, question, values, focus, currentSet, createValue, updateValue, deleteValue }) => {
return (
<div className="interview-question">
<div className={`interview-question col-md-${question.width || '12'}`}>
<QuestionText question={question} />
<QuestionHelp question={question} />
{
Expand All @@ -19,7 +19,9 @@ const Question = ({ templates, question, values, focus, currentSet, createValue,
)
}
{
<Template template={templates.project_interview_multiple_values_warning} />
!question.is_collection && values.length > 1 && (
<Template template={templates.project_interview_multiple_values_warning} />
)
}
{
<QuestionManagement question={question} />
Expand Down
Loading

0 comments on commit fb64bfe

Please sign in to comment.