commit
f3e79f4251
|
@ -1,7 +1,4 @@
|
|||
rules:
|
||||
linebreak-style:
|
||||
- 2
|
||||
- unix
|
||||
quotes:
|
||||
- 1 # While https://github.com/eslint/eslint/issues/9662#issuecomment-353958854 we don't enforce this
|
||||
- single
|
||||
|
@ -14,10 +11,13 @@ rules:
|
|||
react/jsx-no-target-blank: 0
|
||||
react/no-unescaped-entities: 0
|
||||
react/display-name: 1
|
||||
react-hooks/rules-of-hooks: error
|
||||
react-hooks/exhaustive-deps: warn
|
||||
parser: babel-eslint
|
||||
|
||||
plugins:
|
||||
- react
|
||||
- react-hooks
|
||||
- flowtype
|
||||
env:
|
||||
browser: true
|
||||
|
|
|
@ -57,8 +57,6 @@
|
|||
"react-virtualized-select": "^3.1.3",
|
||||
"reduce-reducers": "^0.1.2",
|
||||
"redux": "^3.7.2",
|
||||
"redux-batched-actions": "^0.4.1",
|
||||
"redux-form": "^8.2.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"reselect": "^4.0.0",
|
||||
|
@ -125,6 +123,7 @@
|
|||
"eslint-config-prettier": "^4.0.0",
|
||||
"eslint-plugin-flowtype": "^3.2.1",
|
||||
"eslint-plugin-react": "^7.12.4",
|
||||
"eslint-plugin-react-hooks": "^2.0.1",
|
||||
"express": "^4.16.3",
|
||||
"file-loader": "^1.1.11",
|
||||
"flow-bin": "^0.92.0",
|
||||
|
|
|
@ -9,7 +9,6 @@ import { Provider as ReduxProvider } from 'react-redux'
|
|||
import { Router } from 'react-router-dom'
|
||||
import reducers from 'Reducers/rootReducer'
|
||||
import { applyMiddleware, compose, createStore } from 'redux'
|
||||
import { enableBatching } from 'redux-batched-actions'
|
||||
import thunk from 'redux-thunk'
|
||||
import { getIframeOption, inIframe } from './utils'
|
||||
|
||||
|
@ -54,11 +53,7 @@ export default class Provider extends PureComponent {
|
|||
if (this.props.initialStore)
|
||||
this.props.initialStore.lang = this.props.language
|
||||
}
|
||||
this.store = createStore(
|
||||
enableBatching(reducers),
|
||||
this.props.initialStore,
|
||||
storeEnhancer
|
||||
)
|
||||
this.store = createStore(reducers, this.props.initialStore, storeEnhancer)
|
||||
this.props.onStoreCreated && this.props.onStoreCreated(this.store)
|
||||
|
||||
// Remove loader
|
||||
|
|
|
@ -7,8 +7,6 @@ import type {
|
|||
SetSimulationConfigAction,
|
||||
SetSituationBranchAction
|
||||
} from 'Types/ActionsTypes'
|
||||
// $FlowFixMe
|
||||
import { change, reset } from 'redux-form'
|
||||
import { deletePersistedSimulation } from '../storage/persistSimulation'
|
||||
import type { Thunk } from 'Types/ActionsTypes'
|
||||
|
||||
|
@ -18,18 +16,19 @@ export const resetSimulation = () => (dispatch: any => void): void => {
|
|||
type: 'RESET_SIMULATION'
|
||||
}: ResetSimulationAction)
|
||||
)
|
||||
dispatch(reset('conversation'))
|
||||
}
|
||||
|
||||
export const goToQuestion = (question: string): StepAction => ({
|
||||
type: 'STEP_ACTION',
|
||||
name: 'unfold',
|
||||
step: question
|
||||
})
|
||||
|
||||
export const validateStepWithValue = (
|
||||
dottedName,
|
||||
value: any
|
||||
): Thunk<StepAction> => dispatch => {
|
||||
dispatch(change('conversation', dottedName, value))
|
||||
dispatch(updateSituation(dottedName, value))
|
||||
dispatch({
|
||||
type: 'STEP_ACTION',
|
||||
name: 'fold',
|
||||
|
@ -62,6 +61,17 @@ export const deletePreviousSimulation = () => (
|
|||
deletePersistedSimulation()
|
||||
}
|
||||
|
||||
export const updateSituation = (fieldName, value) => ({
|
||||
type: 'UPDATE_SITUATION',
|
||||
fieldName,
|
||||
value
|
||||
})
|
||||
|
||||
export const updatePeriod = toPeriod => ({
|
||||
type: 'UPDATE_PERIOD',
|
||||
toPeriod
|
||||
})
|
||||
|
||||
// $FlowFixMe
|
||||
export function setExample(name, situation, dottedName) {
|
||||
return { type: 'SET_EXAMPLE', name, situation, dottedName }
|
||||
|
|
|
@ -8,6 +8,7 @@ import Overlay from './Overlay'
|
|||
// Il suffit à la section d'appeler une fonction fournie en lui donnant du JSX
|
||||
export let AttachDictionary = dictionary => Decorated =>
|
||||
function withDictionary(props) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [{ explanation, term }, setState] = useState({
|
||||
term: null,
|
||||
explanation: null
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classnames from 'classnames'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import NumberFormat from 'react-number-format'
|
||||
import { debounce } from '../../utils'
|
||||
import './CurrencyInput.css'
|
||||
|
@ -22,23 +22,28 @@ let currencyFormat = language => ({
|
|||
})
|
||||
|
||||
export default function CurrencyInput({
|
||||
value: valueArg,
|
||||
value: valueProp = '',
|
||||
debounce: debounceTimeout,
|
||||
currencySymbol = '€',
|
||||
onChange,
|
||||
language,
|
||||
className,
|
||||
...forwardedProps
|
||||
}) {
|
||||
const [currentValue, setCurrentValue] = useState(valueArg)
|
||||
const [initialValue] = useState(valueArg)
|
||||
// When the component is rendered with a new "value" argument, we update our local state
|
||||
useEffect(() => {
|
||||
setCurrentValue(valueArg)
|
||||
}, [valueArg])
|
||||
const nextValue = useRef(null)
|
||||
const [initialValue, setInitialValue] = useState(valueProp)
|
||||
const [currentValue, setCurrentValue] = useState(valueProp)
|
||||
const onChangeDebounced = useRef(
|
||||
debounceTimeout ? debounce(debounceTimeout, onChange) : onChange
|
||||
)
|
||||
// We need some mutable reference because the <NumberFormat /> component doesn't provide
|
||||
// the DOM `event` in its custom `onValueChange` handler
|
||||
const nextValue = useRef(null)
|
||||
|
||||
// When the component is rendered with a new "value" prop, we reset our local state
|
||||
if (valueProp !== initialValue) {
|
||||
setCurrentValue(valueProp)
|
||||
setInitialValue(valueProp)
|
||||
}
|
||||
|
||||
const handleChange = event => {
|
||||
// Only trigger the `onChange` event if the value has changed -- and not
|
||||
|
@ -61,20 +66,18 @@ export default function CurrencyInput({
|
|||
thousandSeparator,
|
||||
decimalSeparator
|
||||
} = currencyFormat(language)
|
||||
|
||||
// We display negative numbers iff this was the provided value (but we allow the user to enter them)
|
||||
// We display negative numbers iff this was the provided value (but we disallow the user to enter them)
|
||||
const valueHasChanged = currentValue !== initialValue
|
||||
|
||||
// Autogrow the input
|
||||
const valueLength = (currentValue || '').toString().length
|
||||
const valueLength = currentValue.toString().length
|
||||
const width = `${5 + (valueLength - 5) * 0.75}em`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(className, 'currencyInput__container')}
|
||||
{...(valueLength > 5
|
||||
? { style: { width: `${5 + (valueLength - 5) * 0.75}em` } }
|
||||
: {})}>
|
||||
{isCurrencyPrefixed && '€'}
|
||||
{...(valueLength > 5 ? { style: { width } } : {})}>
|
||||
{!currentValue && isCurrencyPrefixed && currencySymbol}
|
||||
<NumberFormat
|
||||
{...forwardedProps}
|
||||
thousandSeparator={thousandSeparator}
|
||||
|
@ -82,12 +85,15 @@ export default function CurrencyInput({
|
|||
allowNegative={!valueHasChanged}
|
||||
className="currencyInput__input"
|
||||
inputMode="numeric"
|
||||
prefix={
|
||||
isCurrencyPrefixed && currencySymbol ? `${currencySymbol} ` : ''
|
||||
}
|
||||
onValueChange={({ value }) => {
|
||||
setCurrentValue(value)
|
||||
nextValue.current = value.toString().replace(/^\-/, '')
|
||||
nextValue.current = value.toString().replace(/^-/, '')
|
||||
}}
|
||||
onChange={handleChange}
|
||||
value={(currentValue || '').toString().replace('.', decimalSeparator)}
|
||||
value={currentValue.toString().replace('.', decimalSeparator)}
|
||||
/>
|
||||
{!isCurrencyPrefixed && <> €</>}
|
||||
</div>
|
||||
|
|
|
@ -24,10 +24,18 @@ describe('CurrencyInput', () => {
|
|||
})
|
||||
|
||||
it('should separate thousand groups', () => {
|
||||
const input1 = getInput(<CurrencyInput value={1000} language="fr" />)
|
||||
const input2 = getInput(<CurrencyInput value={1000} language="en" />)
|
||||
const input3 = getInput(<CurrencyInput value={1000.5} language="en" />)
|
||||
const input4 = getInput(<CurrencyInput value={1000000} language="en" />)
|
||||
const input1 = getInput(
|
||||
<CurrencyInput value={1000} language="fr" currencySymbol={''} />
|
||||
)
|
||||
const input2 = getInput(
|
||||
<CurrencyInput value={1000} language="en" currencySymbol={''} />
|
||||
)
|
||||
const input3 = getInput(
|
||||
<CurrencyInput value={1000.5} language="en" currencySymbol={''} />
|
||||
)
|
||||
const input4 = getInput(
|
||||
<CurrencyInput value={1000000} language="en" currencySymbol={''} />
|
||||
)
|
||||
expect(input1.instance().value).to.equal('1 000')
|
||||
expect(input2.instance().value).to.equal('1,000')
|
||||
expect(input3.instance().value).to.equal('1,000.5')
|
||||
|
@ -90,7 +98,7 @@ describe('CurrencyInput', () => {
|
|||
const clock = useFakeTimers()
|
||||
let onChange = spy()
|
||||
const input = getInput(
|
||||
<CurrencyInput onChange={onChange} debounce={1000} />
|
||||
<CurrencyInput onChange={onChange} debounce={1000} currencySymbol={''} />
|
||||
)
|
||||
input.simulate('change', { target: { value: '1', focus: () => {} } })
|
||||
expect(onChange).not.to.have.been.called
|
||||
|
@ -106,12 +114,12 @@ describe('CurrencyInput', () => {
|
|||
})
|
||||
|
||||
it('should initialize with value of the value prop', () => {
|
||||
const input = getInput(<CurrencyInput value={1} />)
|
||||
const input = getInput(<CurrencyInput value={1} language="fr" />)
|
||||
expect(input.instance().value).to.equal('1')
|
||||
})
|
||||
|
||||
it('should update its value if the value prop changes', () => {
|
||||
const component = mount(<CurrencyInput value={1} />)
|
||||
const component = mount(<CurrencyInput value={1} language="fr" />)
|
||||
component.setProps({ value: 2 })
|
||||
expect(component.find('input').instance().value).to.equal('2')
|
||||
})
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
import React, { useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import './PercentageField.css'
|
||||
|
||||
export default function PercentageField({ input, debounce }) {
|
||||
const [localValue, setLocalValue] = useState(input?.value)
|
||||
|
||||
const debouncedOnChange = debounce
|
||||
? debounce(debounce, input.onChange)
|
||||
: input.onChange
|
||||
|
||||
const onChange = value => {
|
||||
setLocalValue(value)
|
||||
debouncedOnChange(value)
|
||||
}
|
||||
export default function PercentageField({ onChange, value, debounce }) {
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
const debouncedOnChange = useCallback(
|
||||
debounce ? debounce(debounce, onChange) : onChange,
|
||||
[debounce, onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
className="range"
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onChange={e => {
|
||||
const value = e.target.value
|
||||
setLocalValue(value)
|
||||
debouncedOnChange(value)
|
||||
}}
|
||||
type="range"
|
||||
value={localValue}
|
||||
name="volume"
|
||||
|
|
|
@ -1,116 +1,41 @@
|
|||
import { findRuleByDottedName, nestedSituationToPathMap } from 'Engine/rules'
|
||||
import { compose, filter, map, toPairs } from 'ramda'
|
||||
import React, { useEffect } from 'react'
|
||||
import { updatePeriod } from 'Actions/actions'
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
import { batchActions } from 'redux-batched-actions'
|
||||
import { change, Field, reduxForm } from 'redux-form'
|
||||
import {
|
||||
flatRulesSelector,
|
||||
situationSelector,
|
||||
situationsWithDefaultsSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { situationSelector } from 'Selectors/analyseSelectors'
|
||||
import './PeriodSwitch.css'
|
||||
|
||||
export default compose(
|
||||
reduxForm({
|
||||
form: 'conversation',
|
||||
destroyOnUnmount: false
|
||||
}),
|
||||
connect(
|
||||
state => {
|
||||
let situation = situationsWithDefaultsSelector(state)
|
||||
if (Array.isArray(situation)) {
|
||||
situation = situation[0]
|
||||
}
|
||||
|
||||
return {
|
||||
rules: flatRulesSelector(state),
|
||||
situation: nestedSituationToPathMap(situationSelector(state)),
|
||||
initialPériode: situation.période
|
||||
}
|
||||
},
|
||||
dispatch => ({
|
||||
batchPeriodChange: actions => dispatch(batchActions(actions))
|
||||
})
|
||||
export default function PeriodSwitch() {
|
||||
const dispatch = useDispatch()
|
||||
const situation = useSelector(situationSelector)
|
||||
const defaultPeriod = useSelector(
|
||||
state => state.simulation?.config?.situation?.période || 'année'
|
||||
)
|
||||
)(function PeriodSwitch({
|
||||
situation,
|
||||
rules,
|
||||
batchPeriodChange,
|
||||
initialPériode
|
||||
}) {
|
||||
useEffect(() => {
|
||||
!situation.période &&
|
||||
updateSituation(
|
||||
initialPériode || 'année',
|
||||
batchPeriodChange,
|
||||
situation,
|
||||
rules
|
||||
)
|
||||
return
|
||||
})
|
||||
const currentPeriod = situation.période
|
||||
let periods = ['année', 'mois']
|
||||
|
||||
if (!currentPeriod) {
|
||||
dispatch(updatePeriod(defaultPeriod))
|
||||
}
|
||||
|
||||
return (
|
||||
<span id="PeriodSwitch">
|
||||
<span className="base ui__ small toggle">
|
||||
<label>
|
||||
<Field
|
||||
name="période"
|
||||
component="input"
|
||||
type="radio"
|
||||
value="année"
|
||||
onChange={() =>
|
||||
updateSituation('année', batchPeriodChange, situation, rules)
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
<Trans>année</Trans>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<Field
|
||||
name="période"
|
||||
component="input"
|
||||
type="radio"
|
||||
value="mois"
|
||||
onChange={() =>
|
||||
updateSituation('mois', batchPeriodChange, situation, rules)
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
<Trans>mois</Trans>
|
||||
</span>
|
||||
</label>
|
||||
{periods.map(period => (
|
||||
<label key={period}>
|
||||
<input
|
||||
name="période"
|
||||
type="radio"
|
||||
value={period}
|
||||
onChange={() => dispatch(updatePeriod(period))}
|
||||
checked={currentPeriod === period}
|
||||
/>
|
||||
<span>
|
||||
<Trans>{period}</Trans>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
)
|
||||
})
|
||||
|
||||
let updateSituation = (toPeriod, batchPeriodChange, situation, rules) => {
|
||||
let needConvertion = filter(([dottedName, value]) => {
|
||||
let rule = findRuleByDottedName(rules, dottedName)
|
||||
return value != null && rule?.période === 'flexible'
|
||||
})(toPairs(situation))
|
||||
let actions = [
|
||||
...map(
|
||||
([dottedName, value]) =>
|
||||
change(
|
||||
'conversation',
|
||||
dottedName,
|
||||
Math.round(
|
||||
situation.période === 'mois' && toPeriod === 'année'
|
||||
? value * 12
|
||||
: situation.période === 'année' && toPeriod === 'mois'
|
||||
? value / 12
|
||||
: (function() {
|
||||
throw new Error('Oups, changement de période invalide')
|
||||
})()
|
||||
) + ''
|
||||
),
|
||||
needConvertion
|
||||
),
|
||||
change('conversation', 'période', toPeriod)
|
||||
]
|
||||
|
||||
batchPeriodChange(actions)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import React, { useRef } from 'react'
|
|||
import emoji from 'react-easy-emoji'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
import { formValueSelector } from 'redux-form'
|
||||
import { usePeriod } from 'Selectors/analyseSelectors'
|
||||
import * as Animate from 'Ui/animate'
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
|
@ -89,20 +89,21 @@ export default compose(
|
|||
)
|
||||
})
|
||||
|
||||
const PaySlipSection = connect(state => ({
|
||||
period: formValueSelector('conversation')(state, 'période')
|
||||
}))(({ period }) => (
|
||||
<section>
|
||||
<h2>
|
||||
<Trans>
|
||||
{period === 'mois'
|
||||
? 'Fiche de paie mensuelle'
|
||||
: 'Détail annuel des cotisations'}
|
||||
</Trans>
|
||||
</h2>
|
||||
<PaySlip />
|
||||
</section>
|
||||
))
|
||||
function PaySlipSection() {
|
||||
const period = usePeriod()
|
||||
return (
|
||||
<section>
|
||||
<h2>
|
||||
<Trans>
|
||||
{period === 'mois'
|
||||
? 'Fiche de paie mensuelle'
|
||||
: 'Détail annuel des cotisations'}
|
||||
</Trans>
|
||||
</h2>
|
||||
<PaySlip />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const DistributionSection = () => (
|
||||
<section>
|
||||
|
|
|
@ -2,7 +2,7 @@ import withSitePaths from 'Components/utils/withSitePaths'
|
|||
import { encodeRuleName } from 'Engine/rules.js'
|
||||
import Fuse from 'fuse.js'
|
||||
import { compose, pick, sortBy } from 'ramda'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import Highlighter from 'react-highlight-words'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, Redirect } from 'react-router-dom'
|
||||
|
@ -19,28 +19,23 @@ function SearchBar({
|
|||
const [inputValue, setInputValue] = useState(null)
|
||||
const [selectedOption, setSelectedOption] = useState(null)
|
||||
const inputElementRef = useRef()
|
||||
const fuse = useRef()
|
||||
// This operation is expensive, we don't want to do it everytime we re-render, so we cache its result
|
||||
const fuse = useMemo(() => {
|
||||
const list = rules.map(
|
||||
pick(['title', 'espace', 'description', 'name', 'dottedName'])
|
||||
)
|
||||
const options = {
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.3 },
|
||||
{ name: 'title', weight: 0.3 },
|
||||
{ name: 'espace', weight: 0.2 },
|
||||
{ name: 'description', weight: 0.2 }
|
||||
]
|
||||
}
|
||||
return new Fuse(list, options)
|
||||
}, [rules])
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
const options = {
|
||||
keys: [
|
||||
{ name: 'name', weight: 0.3 },
|
||||
{ name: 'title', weight: 0.3 },
|
||||
{ name: 'espace', weight: 0.2 },
|
||||
{ name: 'description', weight: 0.2 }
|
||||
]
|
||||
}
|
||||
if (!fuse.current) {
|
||||
// This operation is expensive, we don't want to do it everytime we re-render, so we cache its result in a reference
|
||||
fuse.current = new Fuse(
|
||||
rules.map(pick(['title', 'espace', 'description', 'name', 'dottedName'])),
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = selectedOption => {
|
||||
setSelectedOption(selectedOption)
|
||||
}
|
||||
const renderOption = ({ title, dottedName }) => (
|
||||
<span>
|
||||
<Highlighter searchWords={[inputValue]} textToHighlight={title} />
|
||||
|
@ -49,7 +44,7 @@ function SearchBar({
|
|||
</span>
|
||||
</span>
|
||||
)
|
||||
const filterOptions = (options, filter) => fuse.current.search(filter)
|
||||
const filterOptions = (options, filter) => fuse.search(filter)
|
||||
|
||||
if (selectedOption != null) {
|
||||
finallyCallback && finallyCallback()
|
||||
|
@ -68,8 +63,8 @@ function SearchBar({
|
|||
<>
|
||||
<Select
|
||||
value={selectedOption && selectedOption.dottedName}
|
||||
onChange={handleChange}
|
||||
onInputChange={inputValue => setInputValue(inputValue)}
|
||||
onChange={setSelectedOption}
|
||||
onInputChange={setInputValue}
|
||||
valueKey="dottedName"
|
||||
labelKey="title"
|
||||
options={rules}
|
||||
|
|
|
@ -14,6 +14,13 @@ export default compose(
|
|||
)(function SearchButton({ flatRules, invisibleButton }) {
|
||||
const [visible, setVisible] = useState(false)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = e => {
|
||||
if (!(e.ctrlKey && e.key === 'k')) return
|
||||
setVisible(true)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
return () => {
|
||||
|
@ -21,14 +28,6 @@ export default compose(
|
|||
}
|
||||
}, [])
|
||||
|
||||
const handleKeyDown = e => {
|
||||
if (!(e.ctrlKey && e.key === 'k')) return
|
||||
setVisible(true)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
|
||||
const close = () => setVisible(false)
|
||||
|
||||
return visible ? (
|
||||
|
|
|
@ -106,28 +106,19 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
#targetSelection .editable:not(.attractClick) {
|
||||
border: 2px solid rgba(0, 0, 0, 0);
|
||||
border-bottom: 1px dashed #ffffff91;
|
||||
min-width: 2.5em;
|
||||
display: inline-block;
|
||||
}
|
||||
#targetSelection .targetInputOrValue > :not(.targetInput):not(.attractClick) {
|
||||
margin: 0.2rem 0.6rem;
|
||||
#targetSelection .targetInputOrValue > :not(.targetInput) {
|
||||
margin: 0 0.6rem;
|
||||
}
|
||||
|
||||
#targetSelection .attractClick.editable::before {
|
||||
content: '€';
|
||||
#targetSelection input {
|
||||
margin: 2.7px 0;
|
||||
}
|
||||
|
||||
#targetSelection .attractClick,
|
||||
#targetSelection .targetInput {
|
||||
width: 5.5em;
|
||||
max-width: 7.5em;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
cursor: text;
|
||||
padding: 0;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 0.3rem;
|
||||
|
@ -135,6 +126,25 @@
|
|||
font-size: inherit;
|
||||
}
|
||||
|
||||
#targetSelection .editableTarget {
|
||||
max-width: 7.5em;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
padding: 0 2px;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
#targetSelection .targetInputBottomBorder {
|
||||
margin: 0;
|
||||
padding: 0 2px;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#targetSelection .editableTarget + .targetInputBottomBorder {
|
||||
border-bottom: 1px dashed #ffffff91;
|
||||
}
|
||||
|
||||
#targetSelection .unit {
|
||||
margin-left: 0.4em;
|
||||
font-size: 110%;
|
||||
|
|
|
@ -1,104 +1,75 @@
|
|||
import classNames from 'classnames'
|
||||
import { updateSituation } from 'Actions/actions'
|
||||
import { T } from 'Components'
|
||||
import InputSuggestions from 'Components/conversation/InputSuggestions'
|
||||
import PercentageField from 'Components/PercentageField'
|
||||
import PeriodSwitch from 'Components/PeriodSwitch'
|
||||
import RuleLink from 'Components/RuleLink'
|
||||
import withColours from 'Components/utils/withColours'
|
||||
import { ThemeColoursContext } from 'Components/utils/withColours'
|
||||
import withSitePaths from 'Components/utils/withSitePaths'
|
||||
import { encodeRuleName } from 'Engine/rules'
|
||||
import { serialiseUnit } from 'Engine/units'
|
||||
import { compose, isEmpty, isNil, propEq } from 'ramda'
|
||||
import React, { memo, useEffect, useState } from 'react'
|
||||
import { isEmpty, isNil } from 'ramda'
|
||||
import React, { useEffect, useState, useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { change, Field, formValueSelector, reduxForm } from 'redux-form'
|
||||
import {
|
||||
analysisWithDefaultsSelector,
|
||||
flatRulesSelector
|
||||
useSituation,
|
||||
useSituationValue,
|
||||
useTarget
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import Animate from 'Ui/animate'
|
||||
import AnimatedTargetValue from 'Ui/AnimatedTargetValue'
|
||||
import CurrencyInput from './CurrencyInput/CurrencyInput'
|
||||
import './TargetSelection.css'
|
||||
|
||||
export default compose(
|
||||
withColours,
|
||||
reduxForm({
|
||||
form: 'conversation',
|
||||
destroyOnUnmount: false
|
||||
}),
|
||||
connect(
|
||||
state => ({
|
||||
getTargetValue: dottedName =>
|
||||
formValueSelector('conversation')(state, dottedName),
|
||||
analysis: analysisWithDefaultsSelector(state),
|
||||
flatRules: flatRulesSelector(state),
|
||||
activeInput: state.activeTargetInput,
|
||||
objectifs: state.simulation?.config.objectifs || [],
|
||||
secondaryObjectives:
|
||||
state.simulation?.config['objectifs secondaires'] || []
|
||||
}),
|
||||
dispatch => ({
|
||||
setFormValue: (field, name) =>
|
||||
dispatch(change('conversation', field, name)),
|
||||
setActiveInput: name =>
|
||||
dispatch({ type: 'SET_ACTIVE_TARGET_INPUT', name })
|
||||
})
|
||||
),
|
||||
memo
|
||||
)(function TargetSelection({
|
||||
secondaryObjectives,
|
||||
analysis,
|
||||
getTargetValue,
|
||||
setFormValue,
|
||||
colours,
|
||||
activeInput,
|
||||
setActiveInput,
|
||||
objectifs
|
||||
}) {
|
||||
export default function TargetSelection() {
|
||||
const [initialRender, setInitialRender] = useState(true)
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
const objectifs = useSelector(
|
||||
state => state.simulation?.config.objectifs || []
|
||||
)
|
||||
const secondaryObjectives = useSelector(
|
||||
state => state.simulation?.config['objectifs secondaires'] || []
|
||||
)
|
||||
const situation = useSituation()
|
||||
const dispatch = useDispatch()
|
||||
const colours = useContext(ThemeColoursContext)
|
||||
|
||||
const targets =
|
||||
analysis?.targets.filter(
|
||||
t =>
|
||||
!secondaryObjectives.includes(t.dottedName) &&
|
||||
t.dottedName !== 'contrat salarié . aides employeur'
|
||||
) || []
|
||||
|
||||
useEffect(() => {
|
||||
let targets = getTargets()
|
||||
// Initialize defaultValue for target that can't be computed
|
||||
// TODO: this logic shouldn't be here
|
||||
targets
|
||||
.filter(
|
||||
target =>
|
||||
(!target.formule || isEmpty(target.formule)) &&
|
||||
(!isNil(target.defaultValue) ||
|
||||
!isNil(target.explanation?.defaultValue)) &&
|
||||
!getTargetValue(target.dottedName)
|
||||
!situation[target.dottedName]
|
||||
)
|
||||
|
||||
.forEach(target => {
|
||||
setFormValue(
|
||||
target.dottedName,
|
||||
!isNil(target.defaultValue)
|
||||
? target.defaultValue
|
||||
: target.explanation?.defaultValue
|
||||
dispatch(
|
||||
updateSituation(
|
||||
target.dottedName,
|
||||
!isNil(target.defaultValue)
|
||||
? target.defaultValue
|
||||
: target.explanation?.defaultValue
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
if (initialRender) {
|
||||
setInitialRender(false)
|
||||
}
|
||||
setInitialRender(false)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const getTargets = () => {
|
||||
if (!analysis) return []
|
||||
return analysis.targets.filter(
|
||||
t =>
|
||||
!secondaryObjectives.includes(t.dottedName) &&
|
||||
t.dottedName !== 'contrat salarié . aides employeur'
|
||||
)
|
||||
}
|
||||
|
||||
let targets = getTargets()
|
||||
|
||||
return (
|
||||
<div id="targetSelection">
|
||||
{(typeof objectifs[0] === 'string' ? [{ objectifs }] : objectifs).map(
|
||||
|
@ -127,9 +98,6 @@ export default compose(
|
|||
}}>
|
||||
<Targets
|
||||
{...{
|
||||
activeInput,
|
||||
setActiveInput,
|
||||
setFormValue,
|
||||
targets: targets.filter(({ dottedName }) =>
|
||||
groupTargets.includes(dottedName)
|
||||
),
|
||||
|
@ -142,15 +110,9 @@ export default compose(
|
|||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let Targets = ({
|
||||
activeInput,
|
||||
setActiveInput,
|
||||
setFormValue,
|
||||
targets,
|
||||
initialRender
|
||||
}) => (
|
||||
let Targets = ({ targets, initialRender }) => (
|
||||
<div>
|
||||
<ul className="targets">
|
||||
{targets
|
||||
|
@ -166,11 +128,7 @@ let Targets = ({
|
|||
key={target.dottedName}
|
||||
initialRender={initialRender}
|
||||
{...{
|
||||
target,
|
||||
setFormValue,
|
||||
activeInput,
|
||||
setActiveInput,
|
||||
targets
|
||||
target
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
@ -178,18 +136,13 @@ let Targets = ({
|
|||
</div>
|
||||
)
|
||||
|
||||
const Target = ({
|
||||
target,
|
||||
activeInput,
|
||||
const Target = ({ target, initialRender }) => {
|
||||
const activeInput = useSelector(state => state.activeTargetInput)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
targets,
|
||||
setActiveInput,
|
||||
setFormValue,
|
||||
initialRender
|
||||
}) => {
|
||||
const isActiveInput = activeInput === target.dottedName
|
||||
const isSmallTarget =
|
||||
!target.question || !target.formule || isEmpty(target.formule)
|
||||
|
||||
return (
|
||||
<li
|
||||
key={target.name}
|
||||
|
@ -200,8 +153,7 @@ const Target = ({
|
|||
<Header
|
||||
{...{
|
||||
target,
|
||||
|
||||
isActiveInput: activeInput === target.dottedName
|
||||
isActiveInput
|
||||
}}
|
||||
/>
|
||||
{isSmallTarget && (
|
||||
|
@ -216,19 +168,17 @@ const Target = ({
|
|||
<TargetInputOrValue
|
||||
{...{
|
||||
target,
|
||||
targets,
|
||||
activeInput,
|
||||
setActiveInput,
|
||||
setFormValue
|
||||
isActiveInput,
|
||||
isSmallTarget
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{activeInput === target.dottedName && (
|
||||
{isActiveInput && (
|
||||
<Animate.fromTop>
|
||||
<InputSuggestions
|
||||
suggestions={target.suggestions}
|
||||
onFirstClick={value => {
|
||||
setFormValue(target.dottedName, '' + value)
|
||||
dispatch(updateSituation(target.dottedName, '' + value))
|
||||
}}
|
||||
rulePeriod={target.période}
|
||||
colouredBackground={true}
|
||||
|
@ -256,127 +206,100 @@ let Header = withSitePaths(({ target, sitePaths }) => {
|
|||
)
|
||||
})
|
||||
|
||||
let CurrencyField = withColours(props => {
|
||||
return (
|
||||
<CurrencyInput
|
||||
style={{
|
||||
color: props.colours.textColour,
|
||||
borderColor: props.colours.textColour
|
||||
}}
|
||||
debounce={600}
|
||||
className="targetInput"
|
||||
value={props.input.value}
|
||||
{...props.input}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
let DebouncedPercentageField = props => (
|
||||
<PercentageField debounce={600} {...props} />
|
||||
)
|
||||
export const formatCurrency = (value, language) => {
|
||||
return value == null
|
||||
? ''
|
||||
: Intl.NumberFormat(language, {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
maximumFractionDigits: 0,
|
||||
minimumFractionDigits: 0
|
||||
})
|
||||
.format(value)
|
||||
.replace(/^€/, '€ ')
|
||||
}
|
||||
|
||||
let TargetInputOrValue = ({ target, isActiveInput, isSmallTarget }) => {
|
||||
const { i18n } = useTranslation()
|
||||
const colors = useContext(ThemeColoursContext)
|
||||
const dispatch = useDispatch()
|
||||
const situationValue = useSituationValue(target.dottedName)
|
||||
const targetWithValue = useTarget(target.dottedName)
|
||||
const value = targetWithValue?.nodeValue?.toFixed(0)
|
||||
const inversionFail = useSelector(
|
||||
state => analysisWithDefaultsSelector(state)?.cache.inversionFail
|
||||
)
|
||||
const blurValue = inversionFail && !isActiveInput && value
|
||||
|
||||
let TargetInputOrValue = ({
|
||||
target,
|
||||
targets,
|
||||
activeInput,
|
||||
setActiveInput,
|
||||
firstStepCompleted
|
||||
}) => {
|
||||
const {
|
||||
i18n: { language }
|
||||
} = useTranslation()
|
||||
let inputIsActive = activeInput === target.dottedName
|
||||
return (
|
||||
<span className="targetInputOrValue">
|
||||
{inputIsActive || !target.formule || isEmpty(target.formule) ? (
|
||||
<Field
|
||||
name={target.dottedName}
|
||||
onBlur={event => event.preventDefault()}
|
||||
component={
|
||||
{ '€': CurrencyField, '%': DebouncedPercentageField }[
|
||||
serialiseUnit(target.unit)
|
||||
]
|
||||
}
|
||||
{...(inputIsActive ? { autoFocus: true } : {})}
|
||||
language={language}
|
||||
/>
|
||||
<span
|
||||
className="targetInputOrValue"
|
||||
style={blurValue ? { filter: 'blur(3px)' } : {}}>
|
||||
{target.question ? (
|
||||
<>
|
||||
{!isActiveInput && <AnimatedTargetValue value={value} />}
|
||||
<CurrencyInput
|
||||
style={{
|
||||
color: colors.textColour,
|
||||
borderColor: colors.textColour
|
||||
}}
|
||||
debounce={600}
|
||||
name={target.dottedName}
|
||||
value={situationValue || value}
|
||||
className={
|
||||
isActiveInput || isNil(value) ? 'targetInput' : 'editableTarget'
|
||||
}
|
||||
onChange={evt =>
|
||||
dispatch(updateSituation(target.dottedName, evt.target.value))
|
||||
}
|
||||
onBlur={event => event.preventDefault()}
|
||||
// We use onMouseDown instead of onClick because that's when the browser moves the cursor
|
||||
onMouseDown={() => {
|
||||
if (isSmallTarget) return
|
||||
dispatch({
|
||||
type: 'SET_ACTIVE_TARGET_INPUT',
|
||||
name: target.dottedName
|
||||
})
|
||||
// TODO: This shouldn't be necessary: we don't need to recalculate the situation
|
||||
// when the user just focus another field. Removing this line is almost working
|
||||
// however there is a weird bug in the selection of the next question.
|
||||
if (value) {
|
||||
dispatch(updateSituation(target.dottedName, '' + value))
|
||||
}
|
||||
}}
|
||||
{...(isActiveInput ? { autoFocus: true } : {})}
|
||||
language={i18n.language}
|
||||
/>
|
||||
<span className="targetInputBottomBorder">
|
||||
{formatCurrency(value, i18n.language)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<TargetValue
|
||||
{...{
|
||||
targets,
|
||||
target,
|
||||
activeInput,
|
||||
setActiveInput,
|
||||
firstStepCompleted
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{Number.isNaN(value) ? '—' : formatCurrency(value, i18n.language)}
|
||||
</span>
|
||||
)}
|
||||
{target.dottedName.includes('rémunération . total') && <AidesGlimpse />}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const TargetValue = connect(
|
||||
state => ({
|
||||
blurValue: analysisWithDefaultsSelector(state)?.cache.inversionFail
|
||||
}),
|
||||
dispatch => ({
|
||||
setFormValue: (field, name) => dispatch(change('conversation', field, name))
|
||||
})
|
||||
)(function TargetValue({
|
||||
targets,
|
||||
target,
|
||||
blurValue,
|
||||
setFormValue,
|
||||
activeInput,
|
||||
setActiveInput
|
||||
}) {
|
||||
let targetWithValue = targets?.find(propEq('dottedName', target.dottedName)),
|
||||
value = targetWithValue && targetWithValue.nodeValue
|
||||
|
||||
const showField = value => () => {
|
||||
if (!target.question) return
|
||||
if (value != null && !Number.isNaN(value))
|
||||
setFormValue(target.dottedName, Math.round(value) + '')
|
||||
|
||||
if (activeInput) setFormValue(activeInput, '')
|
||||
setActiveInput(target.dottedName)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
editable: target.question,
|
||||
attractClick: target.question && isNil(target.nodeValue)
|
||||
})}
|
||||
style={blurValue ? { filter: 'blur(3px)' } : {}}
|
||||
{...(target.question ? { tabIndex: 0 } : {})}
|
||||
onClick={showField(value)}
|
||||
onFocus={showField(value)}>
|
||||
<AnimatedTargetValue value={value} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const AidesGlimpse = compose(
|
||||
withRouter,
|
||||
connect(state => ({ analysis: analysisWithDefaultsSelector(state) }))
|
||||
)(({ analysis: { targets }, colours }) => {
|
||||
const aides = targets?.find(
|
||||
t => t.dottedName === 'contrat salarié . aides employeur'
|
||||
)
|
||||
if (!aides || !aides.nodeValue) return null
|
||||
function AidesGlimpse() {
|
||||
const aides = useTarget('contrat salarié . aides employeur')
|
||||
if (!aides?.nodeValue) return null
|
||||
return (
|
||||
<Animate.appear>
|
||||
<div className="aidesGlimpse">
|
||||
<RuleLink {...aides}>
|
||||
-{' '}
|
||||
<strong>
|
||||
<AnimatedTargetValue value={aides.nodeValue} />
|
||||
<AnimatedTargetValue value={aides.nodeValue}>
|
||||
<span>{formatCurrency(aides.nodeValue)}</span>
|
||||
</AnimatedTargetValue>
|
||||
</strong>{' '}
|
||||
<T>d'aides</T> {emoji(aides.icons)}
|
||||
</RuleLink>
|
||||
</div>
|
||||
</Animate.appear>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,16 +3,16 @@ import withSitePaths from 'Components/utils/withSitePaths'
|
|||
import { compose } from 'ramda'
|
||||
import React from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { connect } from 'react-redux'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
|
||||
import './Targets.css'
|
||||
|
||||
export default compose(
|
||||
connect(state => ({ analysis: analysisWithDefaultsSelector(state) })),
|
||||
withColours,
|
||||
withSitePaths
|
||||
)(function Targets({ analysis, colours, sitePaths }) {
|
||||
)(function Targets({ colours, sitePaths }) {
|
||||
const analysis = useSelector(analysisWithDefaultsSelector)
|
||||
let { nodeValue, unité: unit, dottedName } = analysis.targets[0]
|
||||
return (
|
||||
<div id="targets">
|
||||
|
|
|
@ -3,11 +3,9 @@ import { T } from 'Components'
|
|||
import QuickLinks from 'Components/QuickLinks'
|
||||
import { getInputComponent } from 'Engine/generateQuestions'
|
||||
import { findRuleByDottedName } from 'Engine/rules'
|
||||
import { compose } from 'ramda'
|
||||
import React from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { connect } from 'react-redux'
|
||||
import { reduxForm } from 'redux-form'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
currentQuestionSelector,
|
||||
flatRulesSelector,
|
||||
|
@ -17,40 +15,30 @@ import * as Animate from 'Ui/animate'
|
|||
import Aide from './Aide'
|
||||
import './conversation.css'
|
||||
|
||||
export default compose(
|
||||
reduxForm({
|
||||
form: 'conversation',
|
||||
destroyOnUnmount: false
|
||||
}),
|
||||
connect(
|
||||
state => ({
|
||||
flatRules: flatRulesSelector(state),
|
||||
currentQuestion: currentQuestionSelector(state),
|
||||
previousAnswers: state.conversationSteps.foldedSteps,
|
||||
nextSteps: nextStepsSelector(state)
|
||||
}),
|
||||
{ validateStepWithValue, goToQuestion }
|
||||
export default function Conversation({ customEndMessages }) {
|
||||
const dispatch = useDispatch()
|
||||
const flatRules = useSelector(flatRulesSelector)
|
||||
const currentQuestion = useSelector(currentQuestionSelector)
|
||||
const previousAnswers = useSelector(
|
||||
state => state.conversationSteps.foldedSteps
|
||||
)
|
||||
)(function Conversation({
|
||||
nextSteps,
|
||||
previousAnswers,
|
||||
currentQuestion,
|
||||
customEndMessages,
|
||||
flatRules,
|
||||
goToQuestion,
|
||||
validateStepWithValue
|
||||
}) {
|
||||
const nextSteps = useSelector(nextStepsSelector)
|
||||
|
||||
const setDefault = () =>
|
||||
validateStepWithValue(
|
||||
currentQuestion,
|
||||
findRuleByDottedName(flatRules, currentQuestion).defaultValue
|
||||
dispatch(
|
||||
validateStepWithValue(
|
||||
currentQuestion,
|
||||
findRuleByDottedName(flatRules, currentQuestion).defaultValue
|
||||
)
|
||||
)
|
||||
const goToPrevious = () => goToQuestion(previousAnswers.slice(-1)[0])
|
||||
const goToPrevious = () =>
|
||||
dispatch(goToQuestion(previousAnswers.slice(-1)[0]))
|
||||
const handleKeyDown = ({ key }) => {
|
||||
if (['Escape'].includes(key)) {
|
||||
setDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return nextSteps.length ? (
|
||||
<>
|
||||
<Aide />
|
||||
|
@ -98,4 +86,4 @@ export default compose(
|
|||
</p>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { updateSituation } from 'Actions/actions'
|
||||
import classNames from 'classnames'
|
||||
import Explicable from 'Components/conversation/Explicable'
|
||||
import { compose } from 'ramda'
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { change, Field } from 'redux-form'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { situationSelector } from 'Selectors/analyseSelectors'
|
||||
|
||||
/*
|
||||
This higher order component wraps "Form" components (e.g. Question.js), that represent user inputs,
|
||||
|
@ -13,47 +13,45 @@ Read https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-comp
|
|||
to understand those precious higher order components.
|
||||
*/
|
||||
|
||||
export var FormDecorator = formType => RenderField =>
|
||||
compose(
|
||||
connect(
|
||||
//... this helper directly to the redux state to avoid passing more props
|
||||
state => ({
|
||||
flatRules: state.flatRules
|
||||
}),
|
||||
dispatch => ({
|
||||
stepAction: (name, step, source) =>
|
||||
dispatch({ type: 'STEP_ACTION', name, step, source }),
|
||||
setFormValue: (field, value) =>
|
||||
dispatch(change('conversation', field, value))
|
||||
export const FormDecorator = formType => RenderField =>
|
||||
function({ fieldName, question, inversion, unit, ...otherProps }) {
|
||||
const dispatch = useDispatch()
|
||||
const situation = useSelector(situationSelector)
|
||||
|
||||
const submit = source =>
|
||||
dispatch({
|
||||
type: 'STEP_ACTION',
|
||||
name: 'fold',
|
||||
step: fieldName,
|
||||
source
|
||||
})
|
||||
)
|
||||
)(function(props) {
|
||||
let { stepAction, fieldName, inversion, setFormValue, unit } = props,
|
||||
submit = cause => stepAction('fold', fieldName, cause),
|
||||
stepProps = {
|
||||
...props,
|
||||
submit,
|
||||
setFormValue: (value, name = fieldName) => setFormValue(name, value),
|
||||
...(unit === '%'
|
||||
? {
|
||||
format: x => (x == null ? null : +(x * 100).toFixed(2)),
|
||||
normalize: x => (x == null ? null : x / 100)
|
||||
}
|
||||
: {})
|
||||
}
|
||||
const setFormValue = value => {
|
||||
dispatch(updateSituation(fieldName, normalize(value)))
|
||||
}
|
||||
|
||||
const format = x => (unit === '%' && x ? +(x * 100).toFixed(2) : x)
|
||||
const normalize = x => (unit === '%' ? x / 100 : x)
|
||||
const value = format(situation[fieldName])
|
||||
|
||||
return (
|
||||
<div className={classNames('step', formType)}>
|
||||
<div className="unfoldedHeader">
|
||||
<h3>
|
||||
{props.question}{' '}
|
||||
{!inversion && <Explicable dottedName={fieldName} />}
|
||||
{question} {!inversion && <Explicable dottedName={fieldName} />}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<Field component={RenderField} name={fieldName} {...stepProps} />
|
||||
<RenderField
|
||||
name={fieldName}
|
||||
value={value}
|
||||
setFormValue={setFormValue}
|
||||
submit={submit}
|
||||
format={format}
|
||||
unit={unit}
|
||||
{...otherProps}
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import classnames from 'classnames'
|
||||
import { React, T } from 'Components'
|
||||
import { T } from 'Components'
|
||||
import withColours from 'Components/utils/withColours'
|
||||
import { compose } from 'ramda'
|
||||
import { connect } from 'react-redux'
|
||||
import { formValueSelector } from 'redux-form'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { usePeriod } from 'Selectors/analyseSelectors'
|
||||
import { debounce } from '../../utils'
|
||||
import { FormDecorator } from './FormDecorator'
|
||||
import InputSuggestions from './InputSuggestions'
|
||||
|
@ -11,33 +11,30 @@ import SendButton from './SendButton'
|
|||
|
||||
export default compose(
|
||||
FormDecorator('input'),
|
||||
withColours,
|
||||
connect(state => ({
|
||||
period: formValueSelector('conversation')(state, 'période')
|
||||
}))
|
||||
withColours
|
||||
)(function Input({
|
||||
input,
|
||||
suggestions,
|
||||
setFormValue,
|
||||
submit,
|
||||
rulePeriod,
|
||||
dottedName,
|
||||
meta: { dirty, error },
|
||||
value,
|
||||
format,
|
||||
colours,
|
||||
period,
|
||||
unit
|
||||
}) {
|
||||
const debouncedOnChange = debounce(750, input.onChange)
|
||||
let suffixed = unit != null,
|
||||
inputError = dirty && error,
|
||||
submitDisabled = !dirty || inputError
|
||||
const period = usePeriod()
|
||||
const debouncedSetFormValue = useCallback(debounce(750, setFormValue), [])
|
||||
const suffixed = unit != null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div css="width: 100%">
|
||||
<InputSuggestions
|
||||
suggestions={suggestions}
|
||||
onFirstClick={value => setFormValue('' + value)}
|
||||
onFirstClick={value => {
|
||||
setFormValue(format(value))
|
||||
}}
|
||||
onSecondClick={() => submit('suggestion')}
|
||||
rulePeriod={rulePeriod}
|
||||
/>
|
||||
|
@ -46,12 +43,11 @@ export default compose(
|
|||
<div className="answer">
|
||||
<input
|
||||
type="text"
|
||||
key={input.value}
|
||||
key={value}
|
||||
autoFocus
|
||||
defaultValue={input.value}
|
||||
onChange={e => {
|
||||
e.persist()
|
||||
debouncedOnChange(e)
|
||||
defaultValue={value}
|
||||
onChange={evt => {
|
||||
debouncedSetFormValue(evt.target.value)
|
||||
}}
|
||||
className={classnames({ suffixed })}
|
||||
id={'step-' + dottedName}
|
||||
|
@ -76,10 +72,8 @@ export default compose(
|
|||
)}
|
||||
</label>
|
||||
)}
|
||||
<SendButton {...{ disabled: submitDisabled, error, submit }} />
|
||||
<SendButton {...{ disabled: value === undefined, submit }} />
|
||||
</div>
|
||||
|
||||
{inputError && <span className="step-input-error">{error}</span>}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import { compose, toPairs } from 'ramda'
|
||||
import { toPairs } from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
import { formValueSelector } from 'redux-form'
|
||||
import { usePeriod } from 'Selectors/analyseSelectors'
|
||||
|
||||
export default compose(
|
||||
connect(state => ({
|
||||
period: formValueSelector('conversation')(state, 'période')
|
||||
}))
|
||||
)(function InputSuggestions({
|
||||
export default function InputSuggestions({
|
||||
suggestions,
|
||||
onSecondClick,
|
||||
onFirstClick,
|
||||
rulePeriod,
|
||||
period
|
||||
rulePeriod
|
||||
}) {
|
||||
const [suggestion, setSuggestion] = useState(null)
|
||||
const period = usePeriod()
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!suggestions) return null
|
||||
|
@ -45,4 +40,4 @@ export default compose(
|
|||
})}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import classnames from 'classnames'
|
||||
import withColours from 'Components/utils/withColours'
|
||||
import { compose, is } from 'ramda'
|
||||
import React from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import Explicable from './Explicable'
|
||||
import { FormDecorator } from './FormDecorator'
|
||||
|
@ -29,46 +29,40 @@ import SendButton from './SendButton'
|
|||
export default compose(
|
||||
FormDecorator('question'),
|
||||
withColours
|
||||
)(function Question(props) {
|
||||
let {
|
||||
choices,
|
||||
submit,
|
||||
colours,
|
||||
meta: { pristine }
|
||||
} = props
|
||||
)(function Question({
|
||||
choices,
|
||||
submit,
|
||||
colours,
|
||||
name,
|
||||
setFormValue,
|
||||
value: currentValue
|
||||
}) {
|
||||
const [touched, setTouched] = useState(false)
|
||||
const onChange = useCallback(
|
||||
value => {
|
||||
setFormValue(value)
|
||||
setTouched(true)
|
||||
},
|
||||
[setFormValue]
|
||||
)
|
||||
|
||||
const renderBinaryQuestion = () => {
|
||||
let {
|
||||
input, // vient de redux-form
|
||||
submit,
|
||||
choices,
|
||||
setFormValue,
|
||||
colours
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="binaryQuestionList">
|
||||
{choices.map(({ value, label }) => (
|
||||
<RadioLabel
|
||||
key={value}
|
||||
{...{ value, label, input, submit, colours, setFormValue }}
|
||||
{...{ value, label, currentValue, submit, colours, onChange }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const renderChildren = choices => {
|
||||
let {
|
||||
input, // vient de redux-form
|
||||
submit,
|
||||
setFormValue,
|
||||
colours
|
||||
} = props,
|
||||
{ name } = input,
|
||||
// seront stockées ainsi dans le state :
|
||||
// [parent object path]: dotted name relative to parent
|
||||
relativeDottedName = radioDottedName =>
|
||||
radioDottedName.split(name + ' . ')[1]
|
||||
// seront stockées ainsi dans le state :
|
||||
// [parent object path]: dotted name relative to parent
|
||||
const relativeDottedName = radioDottedName =>
|
||||
radioDottedName.split(name + ' . ')[1]
|
||||
|
||||
return (
|
||||
<ul css="width: 100%">
|
||||
|
@ -78,11 +72,11 @@ export default compose(
|
|||
{...{
|
||||
value: 'non',
|
||||
label: 'Aucun',
|
||||
input,
|
||||
currentValue,
|
||||
submit,
|
||||
colours,
|
||||
dottedName: null,
|
||||
setFormValue
|
||||
onChange
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
|
@ -101,10 +95,10 @@ export default compose(
|
|||
value: relativeDottedName(dottedName),
|
||||
label: title,
|
||||
dottedName,
|
||||
input,
|
||||
currentValue,
|
||||
submit,
|
||||
colours,
|
||||
setFormValue
|
||||
onChange
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
|
@ -123,7 +117,7 @@ export default compose(
|
|||
{choiceElements}
|
||||
<SendButton
|
||||
{...{
|
||||
disabled: pristine,
|
||||
disabled: !touched,
|
||||
colours,
|
||||
error: false,
|
||||
submit
|
||||
|
@ -143,14 +137,15 @@ let RadioLabel = props => (
|
|||
const RadioLabelContent = compose(withColours)(function RadioLabelContent({
|
||||
value,
|
||||
label,
|
||||
input,
|
||||
currentValue,
|
||||
onChange,
|
||||
submit
|
||||
}) {
|
||||
let labelStyle = value === '_' ? { fontWeight: 'bold' } : null,
|
||||
selected = value === input.value
|
||||
selected = value === currentValue
|
||||
|
||||
const click = value => () => {
|
||||
if (input.value == value) submit('dblClick')
|
||||
if (currentValue == value) submit('dblClick')
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -161,10 +156,10 @@ const RadioLabelContent = compose(withColours)(function RadioLabelContent({
|
|||
<Trans i18nKey={`radio_${label}`}>{label}</Trans>
|
||||
<input
|
||||
type="radio"
|
||||
{...input}
|
||||
onClick={click(value)}
|
||||
value={value}
|
||||
checked={value === input.value ? 'checked' : ''}
|
||||
onChange={evt => onChange(evt.target.value)}
|
||||
checked={value === currentValue ? 'checked' : ''}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@ import FormDecorator from 'Components/conversation/FormDecorator'
|
|||
import React from 'react'
|
||||
|
||||
export default FormDecorator('rhetorical-question')(
|
||||
function RhetoricalQuestion({ input, submit, possibleChoice }) {
|
||||
function RhetoricalQuestion({ value: currentValue, submit, possibleChoice }) {
|
||||
if (!possibleChoice) return null // No action possible, don't render an answer
|
||||
|
||||
let { text, value } = possibleChoice
|
||||
|
@ -10,7 +10,12 @@ export default FormDecorator('rhetorical-question')(
|
|||
return (
|
||||
<span className="answer">
|
||||
<label key={value} className="radio userAnswerButton">
|
||||
<input type="radio" {...input} onClick={submit} value={value} />
|
||||
<input
|
||||
type="radio"
|
||||
checked={value === currentValue}
|
||||
onClick={submit}
|
||||
value={value}
|
||||
/>
|
||||
{text}
|
||||
</label>
|
||||
</span>
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
|
||||
export default function SendButton({ disabled, submit }) {
|
||||
const getAction = useCallback(cause => (!disabled ? submit(cause) : null), [
|
||||
disabled,
|
||||
submit
|
||||
])
|
||||
useEffect(() => {
|
||||
const handleKeyDown = ({ key }) => {
|
||||
if (key !== 'Enter') return
|
||||
getAction('enter')
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [])
|
||||
}, [getAction])
|
||||
|
||||
const getAction = () => {
|
||||
return cause => (!disabled ? submit(cause) : null)
|
||||
}
|
||||
|
||||
const handleKeyDown = ({ key }) => {
|
||||
if (key !== 'Enter') return
|
||||
getAction()('enter')
|
||||
}
|
||||
return (
|
||||
<button
|
||||
className="ui__ button plain"
|
||||
css="margin-left: 1.2rem"
|
||||
disabled={disabled}
|
||||
onClick={() => getAction()('accept')}>
|
||||
onClick={() => getAction('accept')}>
|
||||
<span className="text">
|
||||
<Trans>Suivant</Trans> →
|
||||
</span>
|
||||
|
|
|
@ -35,14 +35,14 @@ let getOptions = input =>
|
|||
})
|
||||
|
||||
export default FormDecorator('select')(function Select({
|
||||
input: { onChange },
|
||||
setFormValue,
|
||||
submit
|
||||
}) {
|
||||
let submitOnChange = option => {
|
||||
tauxVersementTransport(option.code)
|
||||
.then(({ taux }) => {
|
||||
// serialize to not mix our data schema and the API response's
|
||||
onChange(
|
||||
setFormValue(
|
||||
JSON.stringify({
|
||||
...option,
|
||||
...(taux != undefined
|
||||
|
@ -59,7 +59,7 @@ export default FormDecorator('select')(function Select({
|
|||
console.log(
|
||||
'Erreur dans la récupération du taux de versement transport à partir du code commune',
|
||||
error
|
||||
) || onChange(JSON.stringify({ option }))
|
||||
) || setFormValue(JSON.stringify({ option }))
|
||||
submit() // eslint-disable-line no-console
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,26 +5,22 @@ import { FormDecorator } from '../FormDecorator'
|
|||
import './Select.css'
|
||||
import SelectOption from './SelectOption.js'
|
||||
|
||||
function ReactSelectWrapper(props) {
|
||||
let {
|
||||
value,
|
||||
onBlur,
|
||||
onChange,
|
||||
submit,
|
||||
options,
|
||||
submitOnChange = option => {
|
||||
option.text = +option['Taux net'].replace(',', '.') / 100
|
||||
onChange(option.text)
|
||||
submit()
|
||||
},
|
||||
selectValue = value && value['Code risque']
|
||||
// but ReactSelect obviously needs a unique identifier
|
||||
} = props
|
||||
|
||||
function ReactSelectWrapper({
|
||||
value,
|
||||
onBlur,
|
||||
setFormValue,
|
||||
submit,
|
||||
options,
|
||||
submitOnChange = option => {
|
||||
option.text = +option['Taux net'].replace(',', '.') / 100
|
||||
setFormValue(option.text)
|
||||
submit()
|
||||
},
|
||||
selectValue = value?.['Code risque']
|
||||
}) {
|
||||
if (!options) return null
|
||||
|
||||
return (
|
||||
// For redux-form integration, checkout https://github.com/erikras/redux-form/issues/82#issuecomment-143164199
|
||||
<ReactSelect
|
||||
options={options}
|
||||
onChange={submitOnChange}
|
||||
|
@ -40,7 +36,7 @@ function ReactSelectWrapper(props) {
|
|||
)
|
||||
}
|
||||
|
||||
function Select({ input, submit }) {
|
||||
export default FormDecorator('select')(function Select(props) {
|
||||
const [options, setOptions] = useState(null)
|
||||
useEffect(() => {
|
||||
fetch(
|
||||
|
@ -63,9 +59,7 @@ function Select({ input, submit }) {
|
|||
|
||||
return (
|
||||
<div className="select-answer">
|
||||
<ReactSelectWrapper {...input} options={options} submit={submit} />
|
||||
<ReactSelectWrapper {...props} options={options} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormDecorator('select')(Select)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* @flow */
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import './AnimatedTargetValue.css'
|
||||
|
@ -8,44 +8,42 @@ type Props = {
|
|||
value: ?number
|
||||
}
|
||||
|
||||
export default function AnimatedTargetValue({ value }: Props) {
|
||||
const [difference, setDifference] = useState(0)
|
||||
const [previousValue, setPreviousValue] = useState()
|
||||
useEffect(() => {
|
||||
if (previousValue === value || Number.isNaN(value)) {
|
||||
return
|
||||
}
|
||||
setDifference((value || 0) - (previousValue || 0))
|
||||
setPreviousValue(value)
|
||||
}, [value])
|
||||
const { i18n } = useTranslation()
|
||||
function formatDifference(difference, language) {
|
||||
const prefix = difference > 0 ? '+' : ''
|
||||
const formatedValue = Intl.NumberFormat(language, {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
maximumFractionDigits: 0,
|
||||
minimumFractionDigits: 0
|
||||
}).format(difference)
|
||||
return prefix + formatedValue
|
||||
}
|
||||
|
||||
const format = value => {
|
||||
return value == null
|
||||
? ''
|
||||
: Intl.NumberFormat(i18n.language, {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
maximumFractionDigits: 0,
|
||||
minimumFractionDigits: 0
|
||||
}).format(value)
|
||||
}
|
||||
export default function AnimatedTargetValue({ value, children }: Props) {
|
||||
const previousValue = useRef()
|
||||
const { language } = useTranslation().i18n
|
||||
|
||||
const formattedDifference = format(difference)
|
||||
const difference =
|
||||
previousValue.current === value || Number.isNaN(value)
|
||||
? null
|
||||
: (value || 0) - (previousValue.current || 0)
|
||||
previousValue.current = value
|
||||
const shouldDisplayDifference =
|
||||
Math.abs(difference) > 1 && value != null && !Number.isNaN(value)
|
||||
difference !== null && Math.abs(difference) > 1
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="Rule-value">
|
||||
{shouldDisplayDifference && (
|
||||
<Evaporate
|
||||
style={{
|
||||
color: difference > 0 ? 'chartreuse' : 'red'
|
||||
color: difference > 0 ? 'chartreuse' : 'red',
|
||||
pointerEvents: 'none'
|
||||
}}>
|
||||
{(difference > 0 ? '+' : '') + formattedDifference}
|
||||
{formatDifference(difference, language)}
|
||||
</Evaporate>
|
||||
)}{' '}
|
||||
<span>{Number.isNaN(value) ? '—' : format(value)}</span>
|
||||
{children}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -131,7 +131,10 @@ export function appear({
|
|||
delay = 0,
|
||||
style
|
||||
}) {
|
||||
// TODO: We should rename this function Appear
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [show, setShow] = useState(unless)
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
window.setTimeout(() => setShow(true), 0)
|
||||
}, [])
|
||||
|
|
|
@ -88,7 +88,7 @@ const generateTheme = (themeColour?: ?string): ThemeColours => {
|
|||
}
|
||||
}
|
||||
|
||||
const ThemeColoursContext: React$Context<ThemeColours> = createContext(
|
||||
export const ThemeColoursContext: React$Context<ThemeColours> = createContext(
|
||||
generateTheme()
|
||||
)
|
||||
|
||||
|
|
|
@ -173,8 +173,6 @@ export let findRuleByNamespace = (allRules, ns) =>
|
|||
|
||||
export let queryRule = rule => query => path(query.split(' . '))(rule)
|
||||
|
||||
// Redux-form stores the form values as a nested object
|
||||
// This helper makes a dottedName => value Map
|
||||
export let nestedSituationToPathMap = situation => {
|
||||
if (situation == undefined) return {}
|
||||
let rec = (o, currentPath) =>
|
||||
|
|
|
@ -79,7 +79,7 @@ export default function uniroot(
|
|||
|
||||
if (
|
||||
p < 0.75 * cb * q - Math.abs(tol_act * q) / 2 &&
|
||||
p < Math.abs(prev_step * q / 2)
|
||||
p < Math.abs((prev_step * q) / 2)
|
||||
) {
|
||||
// If (b + p / q) falls in [b,c] and isn't too large it is accepted
|
||||
new_step = p / q
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
import {
|
||||
compose,
|
||||
defaultTo,
|
||||
identity,
|
||||
isNil,
|
||||
lensPath,
|
||||
omit,
|
||||
over,
|
||||
set,
|
||||
uniq,
|
||||
|
@ -12,11 +14,10 @@ import {
|
|||
} from 'ramda'
|
||||
import reduceReducers from 'reduce-reducers'
|
||||
import { combineReducers } from 'redux'
|
||||
// $FlowFixMe
|
||||
import { reducer as formReducer } from 'redux-form'
|
||||
import i18n from '../i18n'
|
||||
import inFranceAppReducer from './inFranceAppReducer'
|
||||
import storageReducer from './storageReducer'
|
||||
import { findRuleByDottedName } from 'Engine/rules'
|
||||
import type { Action } from 'Types/ActionsTypes'
|
||||
|
||||
function explainedVariable(state = null, { type, variableName = null }) {
|
||||
|
@ -99,15 +100,71 @@ function conversationSteps(
|
|||
return state
|
||||
}
|
||||
|
||||
function simulation(state = null, { type, config, url, id }) {
|
||||
if (type === 'SET_SIMULATION') {
|
||||
return { config, url, hiddenControls: [] }
|
||||
function updateSituation(situation, { fieldName, value, config }) {
|
||||
const removePreviousTarget = config.objectifs.includes(fieldName)
|
||||
? omit(config.objectifs)
|
||||
: identity
|
||||
return { ...removePreviousTarget(situation), [fieldName]: value }
|
||||
}
|
||||
|
||||
function updatePeriod(situation, { toPeriod, rules }) {
|
||||
const currentPeriod = situation['période']
|
||||
if (currentPeriod === toPeriod) {
|
||||
return situation
|
||||
}
|
||||
if (type === 'HIDE_CONTROL' && state !== null) {
|
||||
return { ...state, hiddenControls: [...state.hiddenControls, id] }
|
||||
if (!['mois', 'année'].includes(toPeriod)) {
|
||||
throw new Error('Oups, changement de période invalide')
|
||||
}
|
||||
if (type === 'RESET_SIMULATION' && state !== null) {
|
||||
return { ...state, hiddenControls: [] }
|
||||
|
||||
const needConversion = Object.keys(situation).filter(dottedName => {
|
||||
const rule = findRuleByDottedName(rules, dottedName)
|
||||
return rule?.période === 'flexible'
|
||||
})
|
||||
|
||||
const updatedSituation = Object.entries(situation)
|
||||
.filter(([fieldName]) => needConversion.includes(fieldName))
|
||||
.map(([fieldName, value]) => [
|
||||
fieldName,
|
||||
currentPeriod === 'mois' && toPeriod === 'année' ? value * 12 : value / 12
|
||||
])
|
||||
|
||||
return {
|
||||
...situation,
|
||||
...Object.fromEntries(updatedSituation),
|
||||
période: toPeriod
|
||||
}
|
||||
}
|
||||
|
||||
function simulation(state = null, action, rules) {
|
||||
if (action.type === 'SET_SIMULATION') {
|
||||
const { config, url } = action
|
||||
return { config, url, hiddenControls: [], situation: {} }
|
||||
}
|
||||
if (state === null) {
|
||||
return state
|
||||
}
|
||||
switch (action.type) {
|
||||
case 'HIDE_CONTROL':
|
||||
return { ...state, hiddenControls: [...state.hiddenControls, action.id] }
|
||||
case 'RESET_SIMULATION':
|
||||
return { ...state, hiddenControls: [], situation: {} }
|
||||
case 'UPDATE_SITUATION':
|
||||
return {
|
||||
...state,
|
||||
situation: updateSituation(state.situation, {
|
||||
fieldName: action.fieldName,
|
||||
value: action.value,
|
||||
config: state.config
|
||||
})
|
||||
}
|
||||
case 'UPDATE_PERIOD':
|
||||
return {
|
||||
...state,
|
||||
situation: updatePeriod(state.situation, {
|
||||
toPeriod: action.toPeriod,
|
||||
rules: rules
|
||||
})
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
@ -144,21 +201,23 @@ const existingCompanyReducer = (state, action) => {
|
|||
}
|
||||
return newState
|
||||
}
|
||||
|
||||
export default reduceReducers(
|
||||
existingCompanyReducer,
|
||||
storageReducer,
|
||||
combineReducers({
|
||||
sessionId: defaultTo(Math.floor(Math.random() * 1000000000000) + ''),
|
||||
// this is handled by redux-form, pas touche !
|
||||
form: formReducer,
|
||||
conversationSteps,
|
||||
lang,
|
||||
simulation,
|
||||
explainedVariable,
|
||||
previousSimulation: defaultTo(null),
|
||||
currentExample,
|
||||
situationBranch,
|
||||
activeTargetInput,
|
||||
inFranceApp: inFranceAppReducer
|
||||
})
|
||||
(state, action) =>
|
||||
combineReducers({
|
||||
sessionId: defaultTo(Math.floor(Math.random() * 1000000000000) + ''),
|
||||
conversationSteps,
|
||||
lang,
|
||||
rules: defaultTo(null),
|
||||
explainedVariable,
|
||||
// We need to access the `rules` in the simulation reducer
|
||||
simulation: (a, b) => simulation(a, b, state.rules),
|
||||
previousSimulation: defaultTo(null),
|
||||
currentExample,
|
||||
situationBranch,
|
||||
activeTargetInput,
|
||||
inFranceApp: inFranceAppReducer
|
||||
})(state, action)
|
||||
)
|
||||
|
|
|
@ -2,28 +2,20 @@
|
|||
|
||||
import type { State } from 'Types/State'
|
||||
import type { Action } from 'Types/ActionsTypes'
|
||||
import {
|
||||
createStateFromSavedSimulation,
|
||||
currentSimulationSelector
|
||||
} from 'Selectors/storageSelectors'
|
||||
import { createStateFromSavedSimulation } from 'Selectors/storageSelectors'
|
||||
|
||||
export default (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'LOAD_PREVIOUS_SIMULATION':
|
||||
return {
|
||||
...state,
|
||||
...createStateFromSavedSimulation(state.previousSimulation)
|
||||
...createStateFromSavedSimulation(state)
|
||||
}
|
||||
case 'DELETE_PREVIOUS_SIMULATION':
|
||||
return {
|
||||
...state,
|
||||
previousSimulation: null
|
||||
}
|
||||
case 'RESET_SIMULATION':
|
||||
return {
|
||||
...state,
|
||||
previousSimulation: currentSimulationSelector(state)
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -5,10 +5,7 @@ import {
|
|||
import {
|
||||
collectDefaults,
|
||||
disambiguateExampleSituation,
|
||||
findRuleByDottedName,
|
||||
nestedSituationToPathMap,
|
||||
rules as baseRulesEn,
|
||||
rulesFr as baseRulesFr
|
||||
findRuleByDottedName
|
||||
} from 'Engine/rules'
|
||||
import { analyse, analyseMany, parseAll } from 'Engine/traverse'
|
||||
import {
|
||||
|
@ -33,24 +30,16 @@ import {
|
|||
takeWhile,
|
||||
zipWith
|
||||
} from 'ramda'
|
||||
import { getFormValues } from 'redux-form'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'
|
||||
import { mapOrApply } from '../utils'
|
||||
// create a "selector creator" that uses deep equal instead of ===
|
||||
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, equals)
|
||||
|
||||
/*
|
||||
*
|
||||
* We must here compute parsedRules, flatRules, analyse which contains both targets and cache objects
|
||||
*
|
||||
*
|
||||
* */
|
||||
|
||||
export let flatRulesSelector = createSelector(
|
||||
state => state.lang,
|
||||
(state, props) => props && props.rules,
|
||||
(lang, rules) => rules || (lang === 'en' ? baseRulesEn : baseRulesFr)
|
||||
)
|
||||
// We must here compute parsedRules, flatRules, analyse which contains both targets and cache objects
|
||||
export let flatRulesSelector = (state, props) => {
|
||||
return props?.rules || state?.rules
|
||||
}
|
||||
|
||||
export let parsedRulesSelector = createSelector(
|
||||
[flatRulesSelector],
|
||||
|
@ -81,24 +70,29 @@ export let targetNamesSelector = state => {
|
|||
return [...targetNames, ...secondaryTargetNames]
|
||||
}
|
||||
|
||||
export let situationSelector = createDeepEqualSelector(
|
||||
getFormValues('conversation'),
|
||||
x => x
|
||||
)
|
||||
export let situationSelector = state => state.simulation?.situation || {}
|
||||
|
||||
export let formattedSituationSelector = createSelector(
|
||||
[situationSelector],
|
||||
situation => nestedSituationToPathMap(situation)
|
||||
)
|
||||
export const useSituation = () => useSelector(situationSelector)
|
||||
|
||||
export const useSituationValue = fieldName => useSituation()?.[fieldName]
|
||||
|
||||
export const usePeriod = () => useSituationValue('période')
|
||||
|
||||
export const useTarget = dottedName => {
|
||||
const targets = useSelector(
|
||||
state => analysisWithDefaultsSelector(state).targets
|
||||
)
|
||||
return targets?.find(t => t.dottedName === dottedName)
|
||||
}
|
||||
|
||||
export let noUserInputSelector = createSelector(
|
||||
[formattedSituationSelector],
|
||||
[situationSelector],
|
||||
situation => !situation || isEmpty(dissoc('période', situation))
|
||||
)
|
||||
|
||||
export let firstStepCompletedSelector = createSelector(
|
||||
[
|
||||
formattedSituationSelector,
|
||||
situationSelector,
|
||||
targetNamesSelector,
|
||||
parsedRulesSelector,
|
||||
state => state.simulation?.config?.bloquant
|
||||
|
@ -150,7 +144,7 @@ const createSituationBrancheSelector = situationSelector =>
|
|||
)
|
||||
|
||||
export let situationBranchesSelector = createSituationBrancheSelector(
|
||||
formattedSituationSelector
|
||||
situationSelector
|
||||
)
|
||||
export let situationBranchNameSelector = createSelector(
|
||||
[branchesSelector, state => state.situationBranch],
|
||||
|
@ -159,7 +153,7 @@ export let situationBranchNameSelector = createSelector(
|
|||
)
|
||||
|
||||
export let validatedSituationSelector = createSelector(
|
||||
[formattedSituationSelector, validatedStepsSelector],
|
||||
[situationSelector, validatedStepsSelector],
|
||||
(situation, validatedSteps) => pick(validatedSteps, situation)
|
||||
)
|
||||
export let validatedSituationBranchesSelector = createSituationBrancheSelector(
|
||||
|
@ -283,7 +277,7 @@ export let nextStepsSelector = createSelector(
|
|||
currentMissingVariablesByTargetSelector,
|
||||
state => state.simulation?.config.questions,
|
||||
state => state.conversationSteps.foldedSteps,
|
||||
formattedSituationSelector
|
||||
situationSelector
|
||||
],
|
||||
(
|
||||
mv,
|
||||
|
|
|
@ -1,28 +1,23 @@
|
|||
/* @flow */
|
||||
import type { Situation } from 'Types/Situation.js'
|
||||
import type { SavedSimulation, State } from 'Types/State.js'
|
||||
|
||||
const situationSelector: State => Situation = state =>
|
||||
state.form.conversation?.values
|
||||
export const currentSimulationSelector: State => SavedSimulation = state => {
|
||||
return {
|
||||
situation: state.simulation.situation,
|
||||
activeTargetInput: state.activeTargetInput,
|
||||
foldedSteps: state.conversationSteps.foldedSteps
|
||||
}
|
||||
}
|
||||
|
||||
export const currentSimulationSelector: State => SavedSimulation = state => ({
|
||||
situation: situationSelector(state),
|
||||
activeTargetInput: state.activeTargetInput,
|
||||
foldedSteps: state.conversationSteps.foldedSteps
|
||||
})
|
||||
|
||||
export const createStateFromSavedSimulation: (
|
||||
?SavedSimulation
|
||||
) => ?$Supertype<State> = simulation =>
|
||||
simulation && {
|
||||
activeTargetInput: simulation.activeTargetInput,
|
||||
form: {
|
||||
conversation: {
|
||||
values: simulation.situation
|
||||
}
|
||||
export const createStateFromSavedSimulation = state =>
|
||||
state.previousSimulation && {
|
||||
activeTargetInput: state.previousSimulation.activeTargetInput,
|
||||
simulation: {
|
||||
...state.simulation,
|
||||
situation: state.previousSimulation.situation || {}
|
||||
},
|
||||
conversationSteps: {
|
||||
foldedSteps: simulation.foldedSteps
|
||||
foldedSteps: state.previousSimulation.foldedSteps
|
||||
},
|
||||
previousSimulation: null
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import Landing from './pages/Landing/Landing.js'
|
|||
import SocialSecurity from './pages/SocialSecurity'
|
||||
import ÉconomieCollaborative from './pages/ÉconomieCollaborative'
|
||||
import { constructLocalizedSitePath } from './sitePaths'
|
||||
import { rules as baseRulesEn, rulesFr as baseRulesFr } from 'Engine/rules'
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
Raven.config(
|
||||
|
@ -58,8 +59,9 @@ const middlewares = [
|
|||
function InFranceRoute({ basename, language }) {
|
||||
useEffect(() => {
|
||||
setToSessionStorage('lang', language)
|
||||
}, [])
|
||||
}, [language])
|
||||
const paths = constructLocalizedSitePath(language)
|
||||
const rules = language === 'en' ? baseRulesEn : baseRulesFr
|
||||
return (
|
||||
<Provider
|
||||
basename={basename}
|
||||
|
@ -68,12 +70,13 @@ function InFranceRoute({ basename, language }) {
|
|||
sitePaths={paths}
|
||||
reduxMiddlewares={middlewares}
|
||||
onStoreCreated={store => {
|
||||
persistEverything()(store)
|
||||
persistEverything({ except: ['rules'] })(store)
|
||||
persistSimulation(store)
|
||||
}}
|
||||
initialStore={{
|
||||
...retrievePersistedState(),
|
||||
previousSimulation: retrievePersistedSimulation()
|
||||
previousSimulation: retrievePersistedSimulation(),
|
||||
rules
|
||||
}}>
|
||||
<RouterSwitch />
|
||||
</Provider>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import classnames from 'classnames'
|
||||
import withTracker from 'Components/utils/withTracker'
|
||||
import { compose } from 'ramda'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { withRouter } from 'react-router'
|
||||
import backSvg from './back.svg'
|
||||
import mobileMenuSvg from './mobile-menu.svg'
|
||||
|
@ -19,10 +19,6 @@ type Props = OwnProps & {
|
|||
tracker: Tracker,
|
||||
location: Location
|
||||
}
|
||||
type State = {
|
||||
opened: boolean,
|
||||
sticky: boolean
|
||||
}
|
||||
|
||||
const bigScreen = window.matchMedia('(min-width: 1500px)')
|
||||
const isParent = (parentNode, children) => {
|
||||
|
@ -35,13 +31,19 @@ const isParent = (parentNode, children) => {
|
|||
return isParent(parentNode, children.parentNode)
|
||||
}
|
||||
|
||||
function SideBar({ location, tracker, children }) {
|
||||
function SideBar({ location, tracker, children }: Props) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [sticky, setSticky] = useState(bigScreen.matches)
|
||||
const [previousLocation, setPreviousLocation] = useState(location)
|
||||
const ref = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = event => {
|
||||
if (!sticky && !isParent(ref.current, event.target) && opened) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('click', handleClick)
|
||||
bigScreen.addListener(handleMediaQueryChange)
|
||||
|
||||
|
@ -49,27 +51,22 @@ function SideBar({ location, tracker, children }) {
|
|||
window.removeEventListener('click', handleClick)
|
||||
bigScreen.removeListener(handleMediaQueryChange)
|
||||
}
|
||||
}, [opened, sticky])
|
||||
}, [handleClose, opened, sticky])
|
||||
|
||||
useEffect(() => {
|
||||
if (!sticky && previousLocation !== location) {
|
||||
setOpened(false)
|
||||
}
|
||||
setPreviousLocation(location)
|
||||
})
|
||||
}, [sticky, previousLocation, location])
|
||||
|
||||
const handleClick = event => {
|
||||
if (!sticky && !isParent(ref.current, event.target) && opened) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
const handleMediaQueryChange = () => {
|
||||
setSticky(bigScreen.matches)
|
||||
}
|
||||
const handleClose = () => {
|
||||
const handleClose = useCallback(() => {
|
||||
tracker.push(['trackEvent', 'Sidebar', 'close'])
|
||||
setOpened(false)
|
||||
}
|
||||
}, [tracker])
|
||||
const handleOpen = () => {
|
||||
tracker.push(['trackEvent', 'Sidebar', 'open'])
|
||||
setOpened(true)
|
||||
|
|
|
@ -1,26 +1,12 @@
|
|||
/* @flow */
|
||||
|
||||
// $FlowFixMe
|
||||
import { actionTypes } from 'redux-form'
|
||||
import {
|
||||
currentQuestionSelector,
|
||||
formattedSituationSelector
|
||||
situationSelector
|
||||
} from 'Selectors/analyseSelectors'
|
||||
import { debounce } from '../../../utils'
|
||||
|
||||
import type { Tracker } from 'Components/utils/withTracker'
|
||||
|
||||
export default (tracker: Tracker) => {
|
||||
const debouncedUserInputTracking = debounce(1000, action =>
|
||||
tracker.push([
|
||||
'trackEvent',
|
||||
'Simulator',
|
||||
'input',
|
||||
action.meta.field,
|
||||
action.payload
|
||||
])
|
||||
)
|
||||
|
||||
// $FlowFixMe
|
||||
return ({ getState }) => next => action => {
|
||||
next(action)
|
||||
|
@ -31,7 +17,7 @@ export default (tracker: Tracker) => {
|
|||
'Simulator::answer',
|
||||
action.source,
|
||||
action.step,
|
||||
formattedSituationSelector(newState)[action.step]
|
||||
situationSelector(newState)[action.step]
|
||||
])
|
||||
|
||||
if (!currentQuestionSelector(newState)) {
|
||||
|
@ -55,8 +41,15 @@ export default (tracker: Tracker) => {
|
|||
])
|
||||
}
|
||||
|
||||
if (action.type === actionTypes.CHANGE) {
|
||||
debouncedUserInputTracking(action)
|
||||
if (action.type === 'UPDATE_SITUATION' || action.type === 'UPDATE_PERIOD') {
|
||||
tracker.push([
|
||||
'trackEvent',
|
||||
'Simulator',
|
||||
'update situation',
|
||||
...(action.type === 'UPDATE_PERIOD'
|
||||
? ['période', action.toPeriod]
|
||||
: [action.fieldName, action.value])
|
||||
])
|
||||
}
|
||||
if (action.type === 'START_CONVERSATION') {
|
||||
tracker.push([
|
||||
|
|
|
@ -23,6 +23,7 @@ export default function IntegrationTest() {
|
|||
script.dataset.couleur = colour
|
||||
domNode.current.innerHTML = ''
|
||||
domNode.current.appendChild(script)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [version])
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import LangSwitcher from 'Components/LangSwitcher'
|
||||
import marianneSvg from 'Images/marianne.svg'
|
||||
import urssafSvg from 'Images/urssaf.svg'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
@ -9,8 +9,10 @@ import screenfull from 'screenfull'
|
|||
import { isIE } from '../../../../utils'
|
||||
|
||||
export default function IframeFooter() {
|
||||
const [isFullscreen, setIsFullscreen] = useState(screenfull.isFullscreen)
|
||||
useEffect(() => {
|
||||
screenfull.enabled && screenfull.onchange(() => this.forceUpdate())
|
||||
screenfull.enabled &&
|
||||
screenfull.onchange(() => setIsFullscreen(screenfull.isFullscreen))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
@ -44,7 +46,7 @@ export default function IframeFooter() {
|
|||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<LangSwitcher className="ui__ button simple" />
|
||||
{screenfull.enabled && !screenfull.isFullscreen && !isIE() && (
|
||||
{screenfull.enabled && !isFullscreen && !isIE() && (
|
||||
<button
|
||||
className="ui__ button small"
|
||||
onClick={() => {
|
||||
|
|
|
@ -7,7 +7,7 @@ import { deserialize, serialize } from './serializeSimulation'
|
|||
import type { State, SavedSimulation } from '../types/State'
|
||||
import type { Action } from 'Types/ActionsTypes'
|
||||
|
||||
const VERSION = 2
|
||||
const VERSION = 3
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'embauche.gouv.fr::persisted-simulation::v' + VERSION
|
||||
|
||||
|
|
|
@ -18,11 +18,6 @@ export type FlatRules = {
|
|||
}
|
||||
}
|
||||
export type State = {
|
||||
form: {
|
||||
conversation: {
|
||||
values: Situation
|
||||
}
|
||||
},
|
||||
previousSimulation: ?SavedSimulation,
|
||||
conversationSteps: {
|
||||
foldedSteps: Array<string>,
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '../source/selectors/analyseSelectors'
|
||||
let baseState = {
|
||||
conversationSteps: { foldedSteps: [] },
|
||||
form: { conversation: { values: {} } }
|
||||
simulation: { situation: {} }
|
||||
}
|
||||
|
||||
describe('conversation', function() {
|
||||
|
@ -48,14 +48,11 @@ describe('conversation', function() {
|
|||
rules = rawRules.map(enrichRule)
|
||||
|
||||
let step1 = merge(baseState, {
|
||||
rules,
|
||||
simulation: { config: { objectifs: ['startHere'] } }
|
||||
})
|
||||
let step2 = reducers(
|
||||
assocPath(
|
||||
['form', 'conversation', 'values'],
|
||||
{ top: { aa: '1' } },
|
||||
step1
|
||||
),
|
||||
assocPath(['simulation', 'situation'], { 'top . aa': '1' }, step1),
|
||||
{
|
||||
type: 'STEP_ACTION',
|
||||
name: 'fold',
|
||||
|
@ -65,8 +62,8 @@ describe('conversation', function() {
|
|||
|
||||
let step3 = reducers(
|
||||
assocPath(
|
||||
['form', 'conversation', 'values'],
|
||||
{ top: { bb: '1', aa: '1' } },
|
||||
['simulation', 'situation'],
|
||||
{ 'top . aa': '1', 'top . bb': '1' },
|
||||
step2
|
||||
),
|
||||
{
|
||||
|
@ -86,7 +83,7 @@ describe('conversation', function() {
|
|||
step: 'top . bb'
|
||||
})
|
||||
|
||||
expect(currentQuestionSelector(lastStep, { rules })).to.equal('top . bb')
|
||||
expect(currentQuestionSelector(lastStep)).to.equal('top . bb')
|
||||
expect(lastStep.conversationSteps).to.have.property('foldedSteps')
|
||||
expect(lastStep.conversationSteps.foldedSteps).to.have.lengthOf(0)
|
||||
})
|
||||
|
@ -129,12 +126,13 @@ describe('conversation', function() {
|
|||
rules = rawRules.map(enrichRule)
|
||||
|
||||
let step1 = merge(baseState, {
|
||||
rules,
|
||||
simulation: { config: { objectifs: ['net'] } }
|
||||
})
|
||||
expect(currentQuestionSelector(step1, { rules })).to.equal('brut')
|
||||
expect(currentQuestionSelector(step1)).to.equal('brut')
|
||||
|
||||
let step2 = reducers(
|
||||
assocPath(['form', 'conversation', 'values', 'brut'], '2300', step1),
|
||||
assocPath(['simulation', 'situation', 'brut'], '2300', step1),
|
||||
{
|
||||
type: 'STEP_ACTION',
|
||||
name: 'fold',
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import { expect } from 'chai'
|
||||
// $FlowFixMe
|
||||
import salariéConfig from 'Components/simulationConfigs/salarié.yaml'
|
||||
import { getRuleFromAnalysis } from 'Engine/rules'
|
||||
import { getRuleFromAnalysis, rules } from 'Engine/rules'
|
||||
import { analysisWithDefaultsSelector } from 'Selectors/analyseSelectors'
|
||||
import {
|
||||
analysisToCotisationsSelector,
|
||||
|
@ -11,16 +11,13 @@ import {
|
|||
} from 'Selectors/ficheDePaieSelectors'
|
||||
|
||||
let state = {
|
||||
form: {
|
||||
conversation: {
|
||||
values: {
|
||||
'contrat salarié': { rémunération: { 'brut de base': '2300' } },
|
||||
entreprise: { effectif: '50' }
|
||||
}
|
||||
}
|
||||
},
|
||||
rules,
|
||||
simulation: {
|
||||
config: salariéConfig
|
||||
config: salariéConfig,
|
||||
situation: {
|
||||
'contrat salarié . rémunération . brut de base': '2300',
|
||||
'entreprise . effectif': '50'
|
||||
}
|
||||
},
|
||||
conversationSteps: {
|
||||
foldedSteps: []
|
||||
|
|
|
@ -10,6 +10,7 @@ let runExamples = (examples, rule) =>
|
|||
examples.map(ex => {
|
||||
let runExample = exampleAnalysisSelector(
|
||||
{
|
||||
rules,
|
||||
currentExample: {
|
||||
situation: ex.situation,
|
||||
dottedName: rule.dottedName
|
||||
|
|
44
yarn.lock
44
yarn.lock
|
@ -751,7 +751,7 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4":
|
||||
"@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4":
|
||||
version "7.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
||||
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
||||
|
@ -3497,11 +3497,6 @@ es-to-primitive@^1.2.0:
|
|||
is-date-object "^1.0.1"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
es6-error@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
|
||||
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
|
||||
|
||||
es6-promise@^4.0.3:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
|
@ -3558,6 +3553,11 @@ eslint-plugin-flowtype@^3.2.1:
|
|||
dependencies:
|
||||
lodash "^4.17.15"
|
||||
|
||||
eslint-plugin-react-hooks@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.0.1.tgz#e898ec26a0a335af6f7b0ad1f0bedda7143ed756"
|
||||
integrity sha512-xir+3KHKo86AasxlCV8AHRtIZPHljqCRRUYgASkbatmt0fad4+5GgC7zkT7o/06hdKM6MIwp8giHVXqBPaarHQ==
|
||||
|
||||
eslint-plugin-react@^7.12.4:
|
||||
version "7.14.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13"
|
||||
|
@ -4686,7 +4686,7 @@ hoist-non-react-statics@^2.2.2:
|
|||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
|
||||
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
|
||||
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.0:
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
|
||||
integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==
|
||||
|
@ -4934,11 +4934,6 @@ ignore@^4.0.6:
|
|||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
|
||||
immutable@3.8.2:
|
||||
version "3.8.2"
|
||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
|
||||
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
|
||||
|
||||
import-cwd@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
|
||||
|
@ -5799,7 +5794,7 @@ locate-path@^3.0.0:
|
|||
p-locate "^3.0.0"
|
||||
path-exists "^3.0.0"
|
||||
|
||||
lodash-es@^4.17.15, lodash-es@^4.2.1:
|
||||
lodash-es@^4.2.1:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
|
||||
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
|
||||
|
@ -8180,29 +8175,6 @@ reduce-reducers@^0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-0.1.5.tgz#ff77ca8068ff41007319b8b4b91533c7e0e54576"
|
||||
integrity sha512-uoVmQnZQ+BtKKDKpBdbBri5SLNyIK9ULZGOA504++VbHcwouWE+fJDIo8AuESPF9/EYSkI0v05LDEQK6stCbTA==
|
||||
|
||||
redux-batched-actions@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/redux-batched-actions/-/redux-batched-actions-0.4.1.tgz#a8de8cef50a1db4f009d5222820c836515597e22"
|
||||
integrity sha512-r6tLDyBP3U9cXNLEHs0n1mX5TQfmk6xE0Y9uinYZ5HOyAWDgIJxYqRRkU/bC6XrJ4nS7tasNbxaHJHVmf9UdkA==
|
||||
|
||||
redux-form@^8.2.0:
|
||||
version "8.2.6"
|
||||
resolved "https://registry.yarnpkg.com/redux-form/-/redux-form-8.2.6.tgz#6840bbe9ed5b2aaef9dd82e6db3e5efcfddd69b1"
|
||||
integrity sha512-krmF7wl1C753BYpEpWIVJ5NM4lUJZFZc5GFUVgblT+jprB99VVBDyBcgrZM3gWWLOcncFyNsHcKNQQcFg8Uanw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.2.0"
|
||||
es6-error "^4.1.1"
|
||||
hoist-non-react-statics "^3.2.1"
|
||||
invariant "^2.2.4"
|
||||
is-promise "^2.1.0"
|
||||
lodash "^4.17.15"
|
||||
lodash-es "^4.17.15"
|
||||
prop-types "^15.6.1"
|
||||
react-is "^16.7.0"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
optionalDependencies:
|
||||
immutable "3.8.2"
|
||||
|
||||
redux-thunk@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
|
||||
|
|
Loading…
Reference in New Issue