diff --git a/.yarnclean b/.yarnclean new file mode 100644 index 000000000..7bbe5ebab --- /dev/null +++ b/.yarnclean @@ -0,0 +1 @@ +@types/react-native diff --git a/mon-entreprise/package.json b/mon-entreprise/package.json index e1064de26..6069d7462 100644 --- a/mon-entreprise/package.json +++ b/mon-entreprise/package.json @@ -26,14 +26,15 @@ "@babel/preset-typescript": "^7.9.0", "@types/cheerio": "^0.22.18", "@types/js-yaml": "^3.12.2", - "@types/react": "^16.9.11", + "@types/react": "^17.0.0", "@types/react-color": "^3.0.1", - "@types/react-dom": "^16.9.3", + "@types/react-dom": "^17.0.0", "@types/react-helmet": "^5.0.13", - "@types/react-redux": "^7.1.5", + "@types/react-redux": "^7.1.11", "@types/react-router": "^5.1.2", - "@types/recharts": "^1.8.9", - "@types/styled-components": "^5.1.0", + "@types/recharts": "^1.8.16", + "@types/redux-sentry-middleware": "^0.1.2", + "@types/styled-components": "^5.1.4", "@types/webpack": "^4.41.10", "@typescript-eslint/eslint-plugin": "^4.0.1", "@typescript-eslint/parser": "^4.0.1", @@ -44,7 +45,7 @@ "cypress-plugin-tab": "^1.0.5", "eslint-plugin-react": "^7.12.4", "html-webpack-plugin": "^3.2.0", - "i18next-parser": "^1.0.6", + "i18next-parser": "^3.3.0", "intl-locales-supported": "^1.0.0", "mock-local-storage": "^1.0.5", "monaco-editor-webpack-plugin": "^1.9.0", @@ -79,7 +80,7 @@ "react-helmet": "6.0.0-beta", "react-i18next": "^11.0.0", "react-markdown": "^4.1.0", - "react-monaco-editor": "^0.36.0", + "react-monaco-editor": "^0.40.0", "react-number-format": "^4.3.1", "react-redux": "^7.0.3", "react-router-dom": "^5.1.1", diff --git a/mon-entreprise/source/actions/existingCompanyActions.js b/mon-entreprise/source/actions/existingCompanyActions.js deleted file mode 100644 index 22f60c338..000000000 --- a/mon-entreprise/source/actions/existingCompanyActions.js +++ /dev/null @@ -1,43 +0,0 @@ -import { fetchCompanyDetails } from '../api/sirene' - -const fetchCommuneDetails = function(codeCommune) { - return fetch( - `https://geo.api.gouv.fr/communes/${codeCommune}?fields=departement,region` - ).then(response => { - return response.json() - }) -} - -export const setEntreprise = siren => async dispatch => { - dispatch({ - type: 'EXISTING_COMPANY::SET_SIREN', - siren - }) - const companyDetails = await fetchCompanyDetails(siren) - dispatch({ - type: 'EXISTING_COMPANY::SET_DETAILS', - catégorieJuridique: companyDetails.categorie_juridique, - dateDeCréation: companyDetails.date_creation - }) - const communeDetails = await fetchCommuneDetails( - companyDetails.etablissement_siege.code_commune - ) - dispatch({ - type: 'EXISTING_COMPANY::ADD_COMMUNE_DETAILS', - details: communeDetails - }) -} - -export const specifyIfAutoEntrepreneur = isAutoEntrepreneur => ({ - type: 'EXISTING_COMPANY::SPECIFY_AUTO_ENTREPRENEUR', - isAutoEntrepreneur -}) - -export const specifyIfDirigeantMajoritaire = isDirigeantMajoritaire => ({ - type: 'EXISTING_COMPANY::SPECIFY_DIRIGEANT_MAJORITAIRE', - isDirigeantMajoritaire -}) - -export const resetEntreprise = () => ({ - type: 'EXISTING_COMPANY::RESET' -}) diff --git a/mon-entreprise/source/actions/existingCompanyActions.ts b/mon-entreprise/source/actions/existingCompanyActions.ts new file mode 100644 index 000000000..8eeefe4b7 --- /dev/null +++ b/mon-entreprise/source/actions/existingCompanyActions.ts @@ -0,0 +1,69 @@ +import { ApiCommuneJson } from 'Components/conversation/select/SelectCommune' +import { fetchCompanyDetails } from '../api/sirene' + +const fetchCommuneDetails = function(codeCommune: string) { + return fetch( + `https://geo.api.gouv.fr/communes/${codeCommune}?fields=departement,region` + ).then(response => { + return response.json() + }) +} + +export type ActionExistingCompany = + | ReturnType + | ReturnType + | ReturnType + | { + type: 'EXISTING_COMPANY::SET_SIREN' + siren: string + } + | { + type: 'EXISTING_COMPANY::SET_DETAILS' + catégorieJuridique: string + dateDeCréation: string + } + | { + type: 'EXISTING_COMPANY::ADD_COMMUNE_DETAILS' + details: ApiCommuneJson + } + +export const setEntreprise = (siren: string) => async ( + dispatch: (action: ActionExistingCompany) => void +) => { + dispatch({ + type: 'EXISTING_COMPANY::SET_SIREN', + siren + } as ActionExistingCompany) + const companyDetails = await fetchCompanyDetails(siren) + dispatch({ + type: 'EXISTING_COMPANY::SET_DETAILS', + catégorieJuridique: companyDetails.categorie_juridique, + dateDeCréation: companyDetails.date_creation + }) + const communeDetails: ApiCommuneJson = await fetchCommuneDetails( + companyDetails.etablissement_siege.code_commune + ) + dispatch({ + type: 'EXISTING_COMPANY::ADD_COMMUNE_DETAILS', + details: communeDetails + } as ActionExistingCompany) +} + +export const specifyIfAutoEntrepreneur = (isAutoEntrepreneur: boolean) => + ({ + type: 'EXISTING_COMPANY::SPECIFY_AUTO_ENTREPRENEUR', + isAutoEntrepreneur + } as const) + +export const specifyIfDirigeantMajoritaire = ( + isDirigeantMajoritaire: boolean +) => + ({ + type: 'EXISTING_COMPANY::SPECIFY_DIRIGEANT_MAJORITAIRE', + isDirigeantMajoritaire + } as const) + +export const resetEntreprise = () => + ({ + type: 'EXISTING_COMPANY::RESET' + } as const) diff --git a/mon-entreprise/source/components/NewsletterRegister.tsx b/mon-entreprise/source/components/NewsletterRegister.tsx index 88177f6e1..ba1d7a06e 100644 --- a/mon-entreprise/source/components/NewsletterRegister.tsx +++ b/mon-entreprise/source/components/NewsletterRegister.tsx @@ -14,7 +14,7 @@ const formInfos = { } export default function NewsletterRegister() { - const [userIsRegistered, setUserIsRegistered] = usePersistingState( + const [userIsRegistered, setUserIsRegistered] = usePersistingState( 'app::newsletter::registered', false ) diff --git a/mon-entreprise/source/components/Overlay.tsx b/mon-entreprise/source/components/Overlay.tsx index 44fa44e01..680b15151 100644 --- a/mon-entreprise/source/components/Overlay.tsx +++ b/mon-entreprise/source/components/Overlay.tsx @@ -1,5 +1,6 @@ import * as animate from 'Components/ui/animate' import FocusTrap from 'focus-trap-react' +import { PageInfo } from 'iframe-resizer' import React, { useEffect, useState } from 'react' import styled, { css } from 'styled-components' @@ -16,7 +17,7 @@ const useIFrameOffset = () => { setOffset(0) return } - window.parentIFrame.getPageInfo(({ scrollTop, offsetTop }) => { + window.parentIFrame.getPageInfo(({ scrollTop, offsetTop }: PageInfo) => { setOffset(scrollTop - offsetTop) window.parentIFrame.getPageInfo(false) }) @@ -127,8 +128,9 @@ const StyledOverlayWrapper = styled.div<{ offsetTop: number | null }>` max-width: 40em; min-height: 6em; } - .ui__.card[aria-modal='true'] { - padding-bottom: 2rem; - margin-bottom: 2rem; + .ui__.card[aria-modal='true'] { + padding-bottom: 2rem; + margin-bottom: 2rem; + } } ` diff --git a/mon-entreprise/source/components/PercentageField.tsx b/mon-entreprise/source/components/PercentageField.tsx index 5bb76ffca..cc9d4203b 100644 --- a/mon-entreprise/source/components/PercentageField.tsx +++ b/mon-entreprise/source/components/PercentageField.tsx @@ -2,9 +2,16 @@ import { formatValue } from 'publicodes' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { debounce as debounceFn } from '../utils' +import { InputCommonProps } from './conversation/RuleInput' import './PercentageField.css' -export default function PercentageField({ onChange, value, debounce = 0 }) { +type PercentageFieldProps = InputCommonProps & { debounce: number } + +export default function PercentageField({ + onChange, + value, + debounce = 0 +}: PercentageFieldProps) { const [localValue, setLocalValue] = useState(value) const debouncedOnChange = useCallback( debounce ? debounceFn(debounce, onChange) : onChange, diff --git a/mon-entreprise/source/components/TargetSelection.tsx b/mon-entreprise/source/components/TargetSelection.tsx index a3e8f8829..3070381f2 100644 --- a/mon-entreprise/source/components/TargetSelection.tsx +++ b/mon-entreprise/source/components/TargetSelection.tsx @@ -11,6 +11,7 @@ import { useInversionFail } from 'Components/utils/EngineContext' import { SitePathsContext } from 'Components/utils/SitePathsContext' +import { EvaluatedNode } from 'publicodes' import { EvaluatedRule, formatValue } from 'publicodes' import { isNil } from 'ramda' import { Fragment, useCallback, useContext } from 'react' @@ -300,7 +301,9 @@ function AidesGlimpse() { // faisons un lien direct vers cette aide, plutôt qu'un lien vers la liste qui // est une somme des aides qui sont toutes nulle sauf l'aide active. const aidesDetail = aides?.formule.explanation.explanation - const aidesNotNul = aidesDetail?.filter(node => node.nodeValue !== false) + const aidesNotNul = aidesDetail?.filter( + (node: EvaluatedNode) => node.nodeValue !== false + ) const aideLink = aidesNotNul?.length === 1 ? aidesNotNul[0] : aides if (!aides?.nodeValue) return null diff --git a/mon-entreprise/source/components/conversation/Conversation.tsx b/mon-entreprise/source/components/conversation/Conversation.tsx index ab273cbe2..4482c6581 100644 --- a/mon-entreprise/source/components/conversation/Conversation.tsx +++ b/mon-entreprise/source/components/conversation/Conversation.tsx @@ -3,7 +3,7 @@ import { updateSituation, validateStepWithValue } from 'Actions/actions' -import RuleInput from 'Components/conversation/RuleInput' +import RuleInput, { RuleInputProps } from 'Components/conversation/RuleInput' import QuickLinks from 'Components/QuickLinks' import * as Animate from 'Components/ui/animate' import { EngineContext } from 'Components/utils/EngineContext' @@ -38,9 +38,12 @@ export default function Conversation({ customEndMessages }: ConversationProps) { }, [dispatch, currentQuestion]) const setDefault = () => dispatch( + // TODO: Skiping a question shouldn't be equivalent to answering the + // default value (for instance the question shouldn't appear in the + // answered questions). validateStepWithValue( currentQuestion, - rules[currentQuestion]['par défaut'] + rules[currentQuestion].defaultValue ) ) const goToPrevious = () => @@ -55,7 +58,7 @@ export default function Conversation({ customEndMessages }: ConversationProps) { }) } - const onChange = value => { + const onChange: RuleInputProps['onChange'] = value => { dispatch(updateSituation(currentQuestion, value)) } diff --git a/mon-entreprise/source/components/conversation/Input.tsx b/mon-entreprise/source/components/conversation/Input.tsx index 284fc99f1..8ec6002c2 100644 --- a/mon-entreprise/source/components/conversation/Input.tsx +++ b/mon-entreprise/source/components/conversation/Input.tsx @@ -1,22 +1,22 @@ -import { formatValue } from 'publicodes' +import { formatValue, Unit } from 'publicodes' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import NumberFormat from 'react-number-format' import { currencyFormat, debounce } from '../../utils' import InputSuggestions from './InputSuggestions' +import { InputCommonProps } from './RuleInput' // TODO: fusionner Input.js et CurrencyInput.js export default function Input({ suggestions, onChange, onSubmit, - dottedName, id, value, defaultValue, autoFocus, unit -}) { +}: InputCommonProps & { unit?: Unit; onSubmit: (source: string) => void }) { const debouncedOnChange = useCallback(debounce(550, onChange), []) const { language } = useTranslation().i18n const { thousandSeparator, decimalSeparator } = currencyFormat(language) diff --git a/mon-entreprise/source/components/conversation/ParagrapheInput.tsx b/mon-entreprise/source/components/conversation/ParagrapheInput.tsx index b339bce0a..00a785a4e 100644 --- a/mon-entreprise/source/components/conversation/ParagrapheInput.tsx +++ b/mon-entreprise/source/components/conversation/ParagrapheInput.tsx @@ -1,14 +1,14 @@ import { useCallback } from 'react' import { debounce } from '../../utils' +import { InputCommonProps } from './RuleInput' export default function ParagrapheInput({ onChange, - dottedName, value, id, defaultValue, autoFocus -}) { +}: InputCommonProps) { const debouncedOnChange = useCallback(debounce(1000, onChange), []) return ( diff --git a/mon-entreprise/source/components/conversation/Question.tsx b/mon-entreprise/source/components/conversation/Question.tsx index 44837befb..5edbeed96 100644 --- a/mon-entreprise/source/components/conversation/Question.tsx +++ b/mon-entreprise/source/components/conversation/Question.tsx @@ -1,11 +1,11 @@ import classnames from 'classnames' import { Markdown } from 'Components/utils/markdown' -import { is } from 'ramda' import { useCallback, useEffect, useState } from 'react' import emoji from 'react-easy-emoji' import { Trans } from 'react-i18next' import { Explicable } from './Explicable' -import { References } from 'publicodes' +import { References, ParsedRule, Rule } from 'publicodes' +import { binaryQuestion, InputCommonProps, RuleInputProps } from './RuleInput' /* Ceci est une saisie de type "radio" : l'utilisateur choisit une réponse dans une liste, ou une liste de listes. Les données @choices sont un arbre de type: @@ -23,13 +23,23 @@ import { References } from 'publicodes' */ +export type Choice = ParsedRule & { + canGiveUp?: boolean + children: Array +} + +type QuestionProps = InputCommonProps & { + onSubmit: (source: string) => void + choices: Choice | typeof binaryQuestion +} + export default function Question({ choices, onSubmit, dottedName: questionDottedName, onChange, value: currentValue -}) { +}: QuestionProps) { const [currentSelection, setCurrentSelection] = useState(currentValue) const handleChange = useCallback( value => { @@ -53,7 +63,7 @@ export default function Question({ } }, [currentSelection]) - const renderBinaryQuestion = () => { + const renderBinaryQuestion = (choices: typeof binaryQuestion) => { return choices.map(({ value, label }) => ( )) } - const renderChildren = choices => { + const renderChildren = (choices: Choice) => { // seront stockées ainsi dans le state : // [parent object path]: dotted fieldName relative to parent - const relativeDottedName = radioDottedName => + const relativeDottedName = (radioDottedName: string) => radioDottedName.split(questionDottedName + ' . ')[1] return (
    @@ -110,7 +120,7 @@ export default function Question({ children ? (
  • {title}
    - {renderChildren({ children })} + {renderChildren({ children } as Choice)}
  • ) : (
  • @@ -135,8 +145,8 @@ export default function Question({ ) } - const choiceElements = is(Array)(choices) - ? renderBinaryQuestion() + const choiceElements = Array.isArray(choices) + ? renderBinaryQuestion(choices) : renderChildren(choices) return ( @@ -154,7 +164,13 @@ export default function Question({ ) } -export const RadioLabel = props => ( +type RadioLabelProps = RadioLabelContentProps & { + description?: string + label?: string + références?: Rule['références'] +} + +export const RadioLabel = (props: RadioLabelProps) => ( <> {props.description && ( @@ -174,6 +190,16 @@ export const RadioLabel = props => ( ) +type RadioLabelContentProps = { + value: string + label: string + name: string + currentSelection?: string + icons?: string + onChange: RuleInputProps['onChange'] + onSubmit: (src: string, value: string) => void +} + function RadioLabelContent({ value, label, @@ -182,7 +208,7 @@ function RadioLabelContent({ icons, onChange, onSubmit -}) { +}: RadioLabelContentProps) { const labelStyle = value === '_' ? ({ fontWeight: 'bold' } as const) : {} const selected = value === currentSelection diff --git a/mon-entreprise/source/components/conversation/RuleInput.tsx b/mon-entreprise/source/components/conversation/RuleInput.tsx index 84feb3c8d..eee44202a 100644 --- a/mon-entreprise/source/components/conversation/RuleInput.tsx +++ b/mon-entreprise/source/components/conversation/RuleInput.tsx @@ -1,5 +1,5 @@ import Input from 'Components/conversation/Input' -import Question from 'Components/conversation/Question' +import Question, { Choice } from 'Components/conversation/Question' import SelectCommune from 'Components/conversation/select/SelectCommune' import SelectAtmp from 'Components/conversation/select/SelectTauxRisque' import CurrencyInput from 'Components/CurrencyInput/CurrencyInput' @@ -15,7 +15,7 @@ import TextInput from './TextInput' import SelectEuropeCountry from './select/SelectEuropeCountry' import ParagrapheInput from './ParagrapheInput' -type Value = string | number | Record | boolean | null +type Value = any export type RuleInputProps = { rules: ParsedRules dottedName: Name @@ -24,11 +24,29 @@ export type RuleInputProps = { isTarget?: boolean autoFocus?: boolean id?: string - value?: Value + value: Value className?: string onSubmit?: (source: string) => void } +export type InputCommonProps = Pick< + RuleInputProps, + 'dottedName' | 'value' | 'onChange' | 'autoFocus' | 'className' +> & + Pick< + ParsedRule, + 'title' | 'question' | 'defaultValue' | 'suggestions' + > & { + key: string + id: string + required: boolean + } + +export const binaryQuestion = [ + { value: 'oui', label: 'Oui' }, + { value: 'non', label: 'Non' } +] as const + // This function takes the unknown rule and finds which React component should // be displayed to get a user input through successive if statements // That's not great, but we won't invest more time until we have more diverse @@ -49,7 +67,7 @@ export default function RuleInput({ const unit = rule.unit const language = useTranslation().i18n.language const engine = useContext(EngineContext) - const commonProps = { + const commonProps: InputCommonProps = { key: dottedName, dottedName, value, @@ -153,7 +171,7 @@ const getVariant = (rule: ParsedRule) => export const buildVariantTree = ( allRules: ParsedRules, path: Name -) => { +): Choice => { const rec = (path: Name) => { const node = allRules[path] if (!node) throw new Error(`La règle ${path} est introuvable`) @@ -168,7 +186,7 @@ export const buildVariantTree = ( children: variants.map((v: string) => rec(`${path} . ${v}` as Name)) } : null - ) + ) as Choice } return rec(path) } diff --git a/mon-entreprise/source/components/conversation/TextInput.tsx b/mon-entreprise/source/components/conversation/TextInput.tsx index c8a3d3d2f..c076d0994 100644 --- a/mon-entreprise/source/components/conversation/TextInput.tsx +++ b/mon-entreprise/source/components/conversation/TextInput.tsx @@ -1,15 +1,14 @@ -import { ThemeColorsContext } from 'Components/utils/colors' -import { useCallback, useContext } from 'react' +import { useCallback } from 'react' import { debounce } from '../../utils' +import { InputCommonProps } from './RuleInput' export default function TextInput({ onChange, - dottedName, value, id, defaultValue, autoFocus -}) { +}: InputCommonProps) { const debouncedOnChange = useCallback(debounce(1000, onChange), []) return ( diff --git a/mon-entreprise/source/components/conversation/select/SelectCommune.tsx b/mon-entreprise/source/components/conversation/select/SelectCommune.tsx index 9a50e5326..f2d10de1d 100644 --- a/mon-entreprise/source/components/conversation/select/SelectCommune.tsx +++ b/mon-entreprise/source/components/conversation/select/SelectCommune.tsx @@ -3,6 +3,22 @@ import React, { useCallback, useMemo, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { debounce } from '../../../utils' import styled, { css } from 'styled-components' +import { InputCommonProps } from '../RuleInput' + +export type ApiCommuneJson = { + _score: number + code: string + codesPostaux: Array + departement: { + code: string + nom: string + } + nom: string + region: { + code: string + nom: string + } +} type Commune = { code: string @@ -10,7 +26,7 @@ type Commune = { nom: string } -async function tauxVersementTransport(codeCommune) { +async function tauxVersementTransport(codeCommune: Commune['code']) { const response = await fetch( 'https://versement-transport.netlify.app/.netlify/functions/taux-par-code-commune?codeCommune=' + codeCommune @@ -35,7 +51,7 @@ async function searchCommunes(input: string): Promise | null> { if (!response.ok) { return null } - const json = await response.json() + const json: Array = await response.json() return json .flatMap(({ codesPostaux, ...commune }) => codesPostaux @@ -46,7 +62,7 @@ async function searchCommunes(input: string): Promise | null> { .slice(0, 10) } -export default function Select({ onChange, value, id }) { +export default function Select({ onChange, value, id }: InputCommonProps) { const [name, setName] = useState(formatCommune(value)) const [searchResults, setSearchResults] = useState>( null diff --git a/mon-entreprise/source/components/conversation/select/SelectEuropeCountry.tsx b/mon-entreprise/source/components/conversation/select/SelectEuropeCountry.tsx index 83673a033..193135059 100644 --- a/mon-entreprise/source/components/conversation/select/SelectEuropeCountry.tsx +++ b/mon-entreprise/source/components/conversation/select/SelectEuropeCountry.tsx @@ -1,3 +1,5 @@ +import { InputCommonProps } from '../RuleInput' + const STATES = [ 'Allemagne', 'Autriche', @@ -32,7 +34,11 @@ const STATES = [ 'Suisse' ] as const -export default function SelectEuropeCountry({ value, onChange, id }) { +export default function SelectEuropeCountry({ + value, + onChange, + id +}: InputCommonProps) { return (