mon-entreprise/source/reducers.js

236 lines
6.1 KiB
JavaScript

import { head, pathOr, without, concat, path, length, reject } from 'ramda'
import { combineReducers } from 'redux'
import reduceReducers from 'reduce-reducers'
import { reducer as formReducer, formValueSelector } from 'redux-form'
import { rules, collectDefaults, formatInputs, rulesFr } from 'Engine/rules'
import {
getNextSteps,
collectMissingVariablesByTarget
} from 'Engine/generateQuestions'
import computeThemeColours from 'Components/themeColours'
import {
STEP_ACTION,
EXPLAIN_VARIABLE,
CHANGE_THEME_COLOUR,
CHANGE_LANG
} from './actions'
import { analyseMany, parseAll } from 'Engine/traverse'
import ReactPiwik from 'Components/Tracker'
import { popularTargetNames } from './components/TargetSelection'
// assume "wraps" a given situation function with one that overrides its values with
// the given assumptions
export let assume = (evaluator, assumptions) => state => name => {
let userInput = evaluator(state)(name)
return userInput != null ? userInput : assumptions[name]
}
export let reduceSteps = (tracker, flatRules, answerSource) => (
state,
action
) => {
state.flatRules = flatRules
// Optimization - don't parse on each analysis
if (!state.parsedRules) {
state.parsedRules = parseAll(flatRules)
}
// TODO
if (action.type == CHANGE_LANG) {
if (action.lang == 'fr') {
flatRules = rulesFr
} else flatRules = rules
return {
...state,
flatRules
}
}
if (
![
'SET_CONVERSATION_TARGETS',
STEP_ACTION,
'USER_INPUT_UPDATE',
'START_CONVERSATION'
].includes(action.type)
)
return state
if (path(['form', 'conversation', 'syncErrors'], state)) return state
let targetNames = reject(
name => state.activeTargetInput && state.activeTargetInput.includes(name)
)(state.targetNames)
let sim = {},
// Hard assumptions cannot be changed, they are used to specialise a simulator
// before the user sees the first question
hardAssumptions = pathOr({}, ['simulateur', 'hypothèses'], sim),
intermediateSituation = assume(answerSource, hardAssumptions),
// Most rules have default values
rulesDefaults = collectDefaults(flatRules),
situationWithDefaults = assume(intermediateSituation, rulesDefaults)
let analysis = analyseMany(state.parsedRules, targetNames)(
situationWithDefaults(state)
)
if (action.type === 'USER_INPUT_UPDATE') {
return { ...state, analysis, situationGate: situationWithDefaults(state) }
}
let nextStepsAnalysis = analyseMany(state.parsedRules, targetNames)(
intermediateSituation(state)
),
missingVariablesByTarget = collectMissingVariablesByTarget(
nextStepsAnalysis.targets
),
nextSteps = getNextSteps(missingVariablesByTarget)
let newState = {
...state,
analysis,
situationGate: situationWithDefaults(state),
explainedVariable: null,
nextSteps,
// store the missingVariables when no question has been answered yet,
// to be able to compute a progress by objective
missingVariablesByTarget: {
initial:
state.foldedSteps.length === 0
? missingVariablesByTarget
: state.missingVariablesByTarget.initial,
current: missingVariablesByTarget
},
currentQuestion: head(nextSteps),
foldedSteps:
action.type === 'SET_CONVERSATION_TARGETS' && action.reset
? []
: state.foldedSteps
}
if (action.type == 'START_CONVERSATION') return newState
if (action.type == STEP_ACTION && action.name == 'fold') {
tracker.push([
'trackEvent',
'answer:' + action.source,
action.step + ': ' + situationWithDefaults(state)(action.step)
])
if (!newState.currentQuestion) {
tracker.push([
'trackEvent',
'done',
'after' + length(newState.foldedSteps) + 'questions'
])
}
return {
...newState,
foldedSteps: [...state.foldedSteps, state.currentQuestion]
}
}
if (action.type == STEP_ACTION && action.name == 'unfold') {
tracker.push(['trackEvent', 'unfold', action.step])
// We are possibly "refolding" a previously open question
let previous = state.currentQuestion,
// we fold it back into foldedSteps if it had been answered
answered = previous && answerSource(state)(previous) != undefined,
foldedSteps = answered
? concat(state.foldedSteps, [previous])
: state.foldedSteps
return {
...newState,
foldedSteps: without([action.step], foldedSteps),
currentQuestion: action.step
}
}
}
function themeColours(state = computeThemeColours(), { type, colour }) {
if (type == CHANGE_THEME_COLOUR) return computeThemeColours(colour)
else return state
}
function explainedVariable(state = null, { type, variableName = null }) {
switch (type) {
case EXPLAIN_VARIABLE:
return variableName
default:
return state
}
}
function currentExample(state = null, { type, situation, name }) {
switch (type) {
case 'SET_EXAMPLE':
return name != null ? { name, situation } : null
default:
return state
}
}
function conversationStarted(state = false, { type }) {
switch (type) {
case 'START_CONVERSATION':
return true
default:
return state
}
}
function activeTargetInput(state = null, { type, name }) {
switch (type) {
case 'SET_ACTIVE_TARGET_INPUT':
return name
default:
return state
}
}
export default initialRules =>
reduceReducers(
combineReducers({
sessionId: (id = Math.floor(Math.random() * 1000000000000) + '') => id,
// this is handled by redux-form, pas touche !
form: formReducer,
/* Have forms been filled or ignored ?
false means the user is reconsidering its previous input */
foldedSteps: (steps = []) => steps,
currentQuestion: (state = null) => state,
nextSteps: (state = []) => state,
missingVariablesByTarget: (state = {}) => state,
parsedRules: (state = null) => state,
flatRules: (state = null) => state,
analysis: (state = null) => state,
targetNames: (state = popularTargetNames) => state,
situationGate: (state = name => null) => state,
iframe: (state = false) => state,
themeColours,
explainedVariable,
currentExample,
conversationStarted,
activeTargetInput
}),
// cross-cutting concerns because here `state` is the whole state tree
reduceSteps(
ReactPiwik,
initialRules,
formatInputs(initialRules, formValueSelector)
)
)