fixup! Separation du gros reducer des autres

pull/209/head
Mael 2018-04-25 16:23:15 +02:00
parent 04cb772336
commit 40781b29c7
2 changed files with 236 additions and 0 deletions

View File

@ -0,0 +1,146 @@
import { path, head, reject, concat, without } from 'ramda'
import { rules, collectDefaults, rulesFr } from 'Engine/rules'
import {
getNextSteps,
collectMissingVariablesByTarget
} from 'Engine/generateQuestions'
import { analyseMany, parseAll } from 'Engine/traverse'
export default (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',
'SET_ACTIVE_TARGET_INPUT'
].includes(action.type)
)
return state
if (
path(['form', 'conversation', 'syncErrors'], state) ||
!answerSource(state)(state.activeTargetInput)
)
return state
let targetNames = reject(
name => state.activeTargetInput && state.activeTargetInput.includes(name)
)(state.targetNames)
// Most rules have default values
let rulesDefaults = collectDefaults(flatRules),
situationWithDefaults = assume(answerSource, 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)(
answerSource(state)
),
missingVariablesByTarget = collectMissingVariablesByTarget(
nextStepsAnalysis.targets
),
nextSteps = getNextSteps(missingVariablesByTarget),
currentQuestion = head(nextSteps)
let newState = {
...state,
analysis,
situationGate: situationWithDefaults(state),
explainedVariable: null,
nextSteps,
currentQuestion,
foldedSteps:
action.type === 'SET_CONVERSATION_TARGETS' && action.reset
? []
: state.foldedSteps
}
if (['START_CONVERSATION', 'SET_ACTIVE_TARGET_INPUT'].includes(action.type))
return {
...newState,
missingVariablesByTarget: {
initial: missingVariablesByTarget,
current: missingVariablesByTarget
}
}
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'
])
}
let foldedSteps = [...state.foldedSteps, state.currentQuestion]
return {
...newState,
foldedSteps,
missingVariablesByTarget: {
...state.missingVariablesByTarget,
current: missingVariablesByTarget
}
}
}
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,
rawFoldedSteps = answered
? concat(state.foldedSteps, [previous])
: state.foldedSteps,
foldedSteps = without([action.step], rawFoldedSteps)
return {
...newState,
foldedSteps,
currentQuestion: action.step,
missingVariablesByTarget: {
...state.missingVariablesByTarget,
current: missingVariablesByTarget
}
}
}
}
// 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]
}

View File

@ -0,0 +1,90 @@
import { combineReducers } from 'redux'
import reduceReducers from 'reduce-reducers'
import { reducer as formReducer, formValueSelector } from 'redux-form'
import reduceSteps from './reduceSteps'
import computeThemeColours from 'Components/themeColours'
import { formatInputs } from 'Engine/rules'
import ReactPiwik from 'Components/Tracker'
import { popularTargetNames } from 'Components/TargetSelection'
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)
)
)