diff --git a/package.json b/package.json index 859198eb0..70b6470cc 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "react-i18next": "^11.0.0", "react-loading-skeleton": "^1.1.2", "react-markdown": "^4.1.0", - "react-number-format": "^4.0.8", + "react-number-format": "^4.3.1", "react-redux": "^7.0.3", "react-router": "^5.1.1", "react-router-dom": "^5.1.1", @@ -53,8 +53,8 @@ "react-transition-group": "^2.2.1", "react-virtualized": "^9.20.0", "react-virtualized-select": "^3.1.3", - "reduce-reducers": "^0.1.2", - "redux": "^3.7.2", + "reduce-reducers": "^1.0.4", + "redux": "^4.0.4", "redux-thunk": "^2.3.0", "regenerator-runtime": "^0.13.3", "reselect": "^4.0.0", @@ -103,17 +103,20 @@ "@babel/preset-flow": "^7.0.0-beta.51", "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.6.0", + "@types/classnames": "^2.2.9", "@types/iframe-resizer": "^3.5.7", - "@types/node": "^12.11.7", "@types/ramda": "^0.26.33", + "@types/raven-for-redux": "^1.1.1", "@types/react": "^16.9.11", "@types/react-addons-css-transition-group": "^15.0.5", + "@types/react-color": "^3.0.1", "@types/react-dom": "^16.9.3", "@types/react-helmet": "^5.0.13", "@types/react-redux": "^7.1.5", "@types/react-router": "^5.1.2", "@types/react-router-dom": "^5.1.0", "@types/styled-components": "^4.1.19", + "@types/webpack-env": "^1.14.1", "akh": "^3.1.2", "autoprefixer": "^9.3.1", "babel-eslint": "^11.0.0-beta.0", @@ -155,7 +158,7 @@ "mock-local-storage": "^1.0.5", "nearley-loader": "^2.0.0", "postcss-loader": "^2.1.2", - "prettier": "^1.16.4", + "prettier": "^1.19.1", "ramda-fantasy": "^0.8.0", "raw-loader": "^0.5.1", "react-hot-loader": "^4.12.15", @@ -167,7 +170,7 @@ "style-loader": "^0.23.1", "styled-components": "^4.2.0", "toml-loader": "^1.0.0", - "typescript": "^3.7.1-rc", + "typescript": "^3.7.2", "url-loader": "^1.0.1", "webpack": "^4.39.3", "webpack-cli": "^3.1.2", diff --git a/source/Provider.js b/source/Provider.tsx similarity index 76% rename from source/Provider.js rename to source/Provider.tsx index 94e3d7f0e..613dce56a 100644 --- a/source/Provider.js +++ b/source/Provider.tsx @@ -1,17 +1,25 @@ import { ThemeColoursProvider } from 'Components/utils/withColours' -import { SitePathProvider } from 'Components/utils/withSitePaths' +import { SitePathProvider, SitePaths } from 'Components/utils/withSitePaths' import { TrackerProvider } from 'Components/utils/withTracker' -import { createBrowserHistory } from 'history' +import { createBrowserHistory, History } from 'history' +import { AvailableLangs } from 'i18n' import i18next from 'i18next' import React, { PureComponent } from 'react' import { I18nextProvider } from 'react-i18next' 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 reducers, { RootState } from 'Reducers/rootReducer' +import { applyMiddleware, compose, createStore, Middleware, Store } from 'redux' import thunk from 'redux-thunk' +import Tracker from 'Tracker' import { inIframe } from './utils' +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any + } +} + const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose if ( @@ -33,8 +41,20 @@ if ( }) } -export default class Provider extends PureComponent { - constructor(props) { +type ProviderProps = { + tracker: Tracker + basename: string + sitePaths: SitePaths + language: AvailableLangs + initialStore: RootState + onStoreCreated: (store: Store) => void + reduxMiddlewares: Array +} + +export default class Provider extends PureComponent { + history: History + store: Store + constructor(props: ProviderProps) { super(props) this.history = createBrowserHistory({ basename: process.env.NODE_ENV === 'production' ? '' : this.props.basename @@ -56,7 +76,7 @@ export default class Provider extends PureComponent { this.props.initialStore.lang = this.props.language } this.store = createStore(reducers, this.props.initialStore, storeEnhancer) - this.props.onStoreCreated && this.props.onStoreCreated(this.store) + this.props.onStoreCreated?.(this.store) // Remove loader var css = document.createElement('style') @@ -82,7 +102,8 @@ export default class Provider extends PureComponent { // If IE < 11 display nothing + colour={iframeCouleur && decodeURIComponent(iframeCouleur)} + > diff --git a/source/Tracker.ts b/source/Tracker.ts index 7da1add4c..2c12d1564 100644 --- a/source/Tracker.ts +++ b/source/Tracker.ts @@ -1,17 +1,14 @@ +import { History, Location } from 'history' import { debounce } from './utils' declare global { - interface Window { _paq: any; } + interface Window { + _paq: any + } } -function push(args: ['trackPageView']): void -function push(args: ['trackEvent']): void -function push(args: ['trackEvent', string, string]): void -function push(args: ['trackEvent', string, string, string]): void -function push(args: ['trackEvent', string, string, string, string, string]): void -function push(args: ['trackEvent', string, string, string, number]): void -function push() {} -type PushType = typeof push +type PushArgs = ['trackPageView'] | ['trackEvent', ...Array] +type PushType = (args: PushArgs) => void export default class Tracker { push: PushType @@ -23,7 +20,7 @@ export default class Tracker { this.push = debounce(200, pushFunction) as PushType } - connectToHistory(history) { + connectToHistory(history: History) { this.unlistenFromHistory = history.listen(loc => { this.track(loc) }) @@ -52,5 +49,5 @@ export default class Tracker { } export const devTracker = new Tracker( - (console?.log?.bind(console)) ?? (() => {}) // eslint-disable-line no-console + console?.log?.bind(console) ?? (() => {}) // eslint-disable-line no-console ) diff --git a/source/actions/actions.js b/source/actions/actions.js deleted file mode 100644 index 95846d8dc..000000000 --- a/source/actions/actions.js +++ /dev/null @@ -1,98 +0,0 @@ -import type { - ResetSimulationAction, - LoadPreviousSimulationAction, - StepAction, - DeletePreviousSimulationAction, - SetSimulationConfigAction, - SetSituationBranchAction -} from 'Types/ActionsTypes' -import { deletePersistedSimulation } from '../storage/persistSimulation' -import type { Thunk } from 'Types/ActionsTypes' - -export const resetSimulation = () => (dispatch: any => void): void => { - dispatch( - ({ - type: 'RESET_SIMULATION' - }: ResetSimulationAction) - ) -} - -export const goToQuestion = (question: string): StepAction => ({ - type: 'STEP_ACTION', - name: 'unfold', - step: question -}) - -export const validateStepWithValue = ( - dottedName, - value: any -): Thunk => dispatch => { - dispatch(updateSituation(dottedName, value)) - dispatch({ - type: 'STEP_ACTION', - name: 'fold', - step: dottedName - }) -} - -export const setSituationBranch = (id: number): SetSituationBranchAction => ({ - type: 'SET_SITUATION_BRANCH', - id -}) - -export const setSimulationConfig = ( - config: Object -): Thunk => (dispatch, _, { history }): void => { - const url = history.location.pathname - dispatch({ - type: 'SET_SIMULATION', - url, - config - }) -} - -export const deletePreviousSimulation = () => ( - dispatch: DeletePreviousSimulationAction => void -) => { - dispatch({ - type: 'DELETE_PREVIOUS_SIMULATION' - }) - 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 } -} - -export const goBackToSimulation = (): Thunk => ( - dispatch, - getState, - { history } -) => { - dispatch({ type: 'SET_EXEMPLE', name: null }) - history.push(getState().simulation.url) -} - -export function loadPreviousSimulation(): LoadPreviousSimulationAction { - return { - type: 'LOAD_PREVIOUS_SIMULATION' - } -} - -export function hideControl(id: string) { - return { type: 'HIDE_CONTROL', id } -} - -export const EXPLAIN_VARIABLE = 'EXPLAIN_VARIABLE' diff --git a/source/actions/actions.ts b/source/actions/actions.ts new file mode 100644 index 000000000..9f59adb78 --- /dev/null +++ b/source/actions/actions.ts @@ -0,0 +1,157 @@ +import { SitePaths } from 'Components/utils/withSitePaths' +import { History } from 'history' +import { RootState } from 'Reducers/rootReducer' +import { ThunkAction } from 'redux-thunk' +import { DottedName } from 'Types/rule' +import { deletePersistedSimulation } from '../storage/persistSimulation' + +export type Action = + | ResetSimulationAction + | StepAction + | UpdateAction + | SetSimulationConfigAction + | DeletePreviousSimulationAction + | SetExempleAction + | ExplainVariableAction + | UpdatePeriodAction + | HideControlAction + | LoadPreviousSimulationAction + | SetSituationBranchAction + | SetActiveTargetAction + +type ThunkResult = ThunkAction< + R, + RootState, + { history: History; sitePaths: SitePaths }, + Action +> + +type StepAction = { + type: 'STEP_ACTION' + name: 'fold' | 'unfold' + step: string +} + +type SetSimulationConfigAction = { + type: 'SET_SIMULATION' + url: string + config: Object +} + +type DeletePreviousSimulationAction = { + type: 'DELETE_PREVIOUS_SIMULATION' +} + +type SetExempleAction = { + type: 'SET_EXAMPLE' + name: null | string + situation?: object + dottedName?: string +} + +type ResetSimulationAction = ReturnType +type UpdateAction = ReturnType +type UpdatePeriodAction = ReturnType +type LoadPreviousSimulationAction = ReturnType +type SetSituationBranchAction = ReturnType +type SetActiveTargetAction = ReturnType +type HideControlAction = ReturnType +type ExplainVariableAction = ReturnType + +export const resetSimulation = () => + ({ + type: 'RESET_SIMULATION' + } as const) + +export const goToQuestion = (question: string) => + ({ + type: 'STEP_ACTION', + name: 'unfold', + step: question + } as const) + +export const validateStepWithValue = ( + dottedName: DottedName, + value: any +): ThunkResult => dispatch => { + dispatch(updateSituation(dottedName, value)) + dispatch({ + type: 'STEP_ACTION', + name: 'fold', + step: dottedName + }) +} + +export const setSituationBranch = (id: number) => + ({ + type: 'SET_SITUATION_BRANCH', + id + } as const) + +export const setSimulationConfig = (config: Object): ThunkResult => ( + dispatch, + _, + { history } +): void => { + const url = history.location.pathname + dispatch({ + type: 'SET_SIMULATION', + url, + config + }) +} + +export const setActiveTarget = (targetName: string) => + ({ + type: 'SET_ACTIVE_TARGET_INPUT', + name: targetName + } as const) + +export const deletePreviousSimulation = (): ThunkResult => dispatch => { + dispatch({ + type: 'DELETE_PREVIOUS_SIMULATION' + }) + deletePersistedSimulation() +} + +export const updateSituation = (fieldName: DottedName, value: any) => + ({ + type: 'UPDATE_SITUATION', + fieldName, + value + } as const) + +export const updatePeriod = (toPeriod: string) => + ({ + type: 'UPDATE_PERIOD', + toPeriod + } as const) + +export function setExample(name: string, situation, dottedName: string) { + return { type: 'SET_EXAMPLE', name, situation, dottedName } as const +} + +export const goBackToSimulation = (): ThunkResult => ( + dispatch, + getState, + { history } +) => { + dispatch({ type: 'SET_EXAMPLE', name: null }) + history.push(getState().simulation.url) +} + +export function loadPreviousSimulation() { + return { + type: 'LOAD_PREVIOUS_SIMULATION' + } as const +} + +export function hideControl(id: string) { + return { type: 'HIDE_CONTROL', id } as const +} + +export const explainVariable = (variableName = null) => + ({ + type: 'EXPLAIN_VARIABLE', + variableName + } as const) diff --git a/source/actions/companyCreationChecklistActions.js b/source/actions/companyCreationChecklistActions.js deleted file mode 100644 index 6788cdfe2..000000000 --- a/source/actions/companyCreationChecklistActions.js +++ /dev/null @@ -1,21 +0,0 @@ -import type { - InitializeCompanyCreationChecklistAction, - CheckCompanyCreationItemAction -} from 'Types/companyCreationChecklistTypes' - -export const initializeCompanyCreationChecklist = ( - statusName: string, - checklistItems: Array -) => - ({ - type: 'INITIALIZE_COMPANY_CREATION_CHECKLIST', - checklistItems, - statusName - }: InitializeCompanyCreationChecklistAction) - -export const checkCompanyCreationItem = (name: string, checked: boolean) => - ({ - type: 'CHECK_COMPANY_CREATION_ITEM', - name, - checked - }: CheckCompanyCreationItemAction) diff --git a/source/actions/companyCreationChecklistActions.ts b/source/actions/companyCreationChecklistActions.ts new file mode 100644 index 000000000..4fc5c8b25 --- /dev/null +++ b/source/actions/companyCreationChecklistActions.ts @@ -0,0 +1,29 @@ +import { LegalStatus } from 'Selectors/companyStatusSelectors' + +export type Action = + | InitializeCompanyCreationChecklistAction + | CheckCompanyCreationItemAction + +type InitializeCompanyCreationChecklistAction = ReturnType< + typeof initializeCompanyCreationChecklist +> +type CheckCompanyCreationItemAction = ReturnType< + typeof checkCompanyCreationItem +> + +export const initializeCompanyCreationChecklist = ( + statusName: LegalStatus, + checklistItems: Array +) => + ({ + type: 'INITIALIZE_COMPANY_CREATION_CHECKLIST', + checklistItems, + statusName + } as const) + +export const checkCompanyCreationItem = (name: string, checked: boolean) => + ({ + type: 'CHECK_COMPANY_CREATION_ITEM', + name, + checked + } as const) diff --git a/source/actions/existingCompanyActions.js b/source/actions/existingCompanyActions.js index c46469c7f..0ddcfa36a 100644 --- a/source/actions/existingCompanyActions.js +++ b/source/actions/existingCompanyActions.js @@ -1,6 +1,6 @@ import { fetchCompanyDetails } from '../api/sirene' -const fetchCommuneDetails = function (codeCommune) { +const fetchCommuneDetails = function(codeCommune) { return fetch( `https://geo.api.gouv.fr/communes/${codeCommune}?fields=departement,region` ).then(response => { diff --git a/source/actions/hiringChecklistAction.js b/source/actions/hiringChecklistAction.ts similarity index 50% rename from source/actions/hiringChecklistAction.js rename to source/actions/hiringChecklistAction.ts index de2de028c..0445463d9 100644 --- a/source/actions/hiringChecklistAction.js +++ b/source/actions/hiringChecklistAction.ts @@ -1,17 +1,17 @@ -import type { - InitializeHiringChecklistAction, - CheckHiringItemAction -} from 'Types/hiringChecklistTypes' +export type Action = InitializeHiringChecklistAction | CheckHiringItemAction + +type InitializeHiringChecklistAction = ReturnType +type CheckHiringItemAction = ReturnType export const initializeHiringChecklist = (checklistItems: Array) => ({ type: 'INITIALIZE_HIRING_CHECKLIST', checklistItems - }: InitializeHiringChecklistAction) + } as const) export const checkHiringItem = (name: string, checked: boolean) => ({ type: 'CHECK_HIRING_ITEM', name, checked - }: CheckHiringItemAction) + } as const) diff --git a/source/api/sirene.js b/source/api/sirene.ts similarity index 69% rename from source/api/sirene.js rename to source/api/sirene.ts index 9493efe96..8df00bf7a 100644 --- a/source/api/sirene.js +++ b/source/api/sirene.ts @@ -1,7 +1,7 @@ -const isSIREN = input => input.match(/^[\s]*([\d][\s]*){9}$/) -const isSIRET = input => input.match(/^[\s]*([\d][\s]*){14}$/) +const isSIREN = (input: string) => input.match(/^[\s]*([\d][\s]*){9}$/) +const isSIRET = (input: string) => input.match(/^[\s]*([\d][\s]*){14}$/) -export async function fetchCompanyDetails(siren) { +export async function fetchCompanyDetails(siren: string) { const response = await fetch( `https://entreprise.data.gouv.fr/api/sirene/v3/unites_legales/${siren.replace( /[\s]/g, @@ -15,7 +15,7 @@ export async function fetchCompanyDetails(siren) { return json.unite_legale } -export async function searchDenominationOrSiren(value) { +export async function searchDenominationOrSiren(value: string) { if (isSIRET(value)) { value = value.replace(/[\s]/g, '').slice(0, 9) } @@ -25,7 +25,12 @@ export async function searchDenominationOrSiren(value) { return searchFullText(value) } -async function searchFullText(text) { +export type Etablissement = { + siren: string + denomination?: string +} + +async function searchFullText(text: string): Promise> { const response = await fetch( `https://entreprise.data.gouv.fr/api/sirene/v1/full_text/${text}?per_page=5` ) diff --git a/source/components/Banner.js b/source/components/Banner.js deleted file mode 100644 index 9514ea217..000000000 --- a/source/components/Banner.js +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react' -import emoji from 'react-easy-emoji' -import { connect } from 'react-redux' -import { firstStepCompletedSelector } from 'Selectors/analyseSelectors' -import Animate from 'Ui/animate' -import './Banner.css' -import type { Node } from 'react' -import type { State } from 'Types/State' -type PropTypes = { - hidden: boolean, - children: Node, - icon?: string -} - -let Banner = ({ hidden = false, children, icon }: PropTypes) => - !hidden ? ( - -
- {icon && emoji(icon)} -

{children}

-
-
- ) : null - -export default connect( - (state: State, { hidden }: PropTypes) => ({ - hidden: hidden || firstStepCompletedSelector(state) - }), - {} -)(Banner) diff --git a/source/components/Banner.tsx b/source/components/Banner.tsx new file mode 100644 index 000000000..d751ae5d1 --- /dev/null +++ b/source/components/Banner.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import emoji from 'react-easy-emoji' +import { useSelector } from 'react-redux' +import { firstStepCompletedSelector } from 'Selectors/analyseSelectors' +import Animate from 'Ui/animate' +import './Banner.css' + +type BannerProps = { + children: React.ReactNode + hidden?: boolean + icon?: string +} + +export default function Banner({ + children, + hidden: hiddenProp = false, + icon +}: BannerProps) { + const hiddenState = useSelector(firstStepCompletedSelector) + const hidden = hiddenProp || hiddenState + return !hidden ? ( + +
+ {icon && emoji(icon)} +

{children}

+
+
+ ) : null +} diff --git a/source/components/CompanyDetails.js b/source/components/CompanyDetails.tsx similarity index 89% rename from source/components/CompanyDetails.js rename to source/components/CompanyDetails.tsx index 96523d295..88966a7ea 100644 --- a/source/components/CompanyDetails.js +++ b/source/components/CompanyDetails.tsx @@ -2,9 +2,9 @@ import { T } from 'Components' import React, { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Skeleton from 'react-loading-skeleton' -import { fetchCompanyDetails } from '../api/sirene' +import { Etablissement, fetchCompanyDetails } from '../api/sirene' -export default function CompanyDetails({ siren, denomination }) { +export default function CompanyDetails({ siren, denomination }: Etablissement) { const { i18n } = useTranslation() const DateFormatter = useMemo( () => diff --git a/source/components/CurrencyInput/CurrencyInput.js b/source/components/CurrencyInput/CurrencyInput.tsx similarity index 83% rename from source/components/CurrencyInput/CurrencyInput.js rename to source/components/CurrencyInput/CurrencyInput.tsx index 907db76a6..f8d892146 100644 --- a/source/components/CurrencyInput/CurrencyInput.js +++ b/source/components/CurrencyInput/CurrencyInput.tsx @@ -1,10 +1,18 @@ import classnames from 'classnames' -import React, { useRef, useState } from 'react' import { currencyFormat } from 'Engine/format' -import NumberFormat from 'react-number-format' +import React, { useRef, useState } from 'react' +import NumberFormat, { NumberFormatProps } from 'react-number-format' import { debounce } from '../../utils' import './CurrencyInput.css' +type CurrencyInputProps = NumberFormatProps & { + value?: string | number + debounce?: number + onChange?: (event: React.ChangeEvent) => void + currencySymbol?: string + language?: Parameters[0] +} + export default function CurrencyInput({ value: valueProp = '', debounce: debounceTimeout, @@ -13,7 +21,7 @@ export default function CurrencyInput({ language, className, ...forwardedProps -}) { +}: CurrencyInputProps) { const [initialValue, setInitialValue] = useState(valueProp) const [currentValue, setCurrentValue] = useState(valueProp) const onChangeDebounced = useRef( @@ -23,7 +31,7 @@ export default function CurrencyInput({ // the DOM `event` in its custom `onValueChange` handler const nextValue = useRef(null) - const inputRef = useRef() + const inputRef = useRef() // When the component is rendered with a new "value" prop, we reset our local state if (valueProp !== initialValue) { @@ -31,7 +39,7 @@ export default function CurrencyInput({ setInitialValue(valueProp) } - const handleChange = event => { + const handleChange = (event: React.ChangeEvent) => { // Only trigger the `onChange` event if the value has changed -- and not // only its formating, we don't want to call it when a dot is added in `12.` // for instance @@ -63,7 +71,8 @@ export default function CurrencyInput({
5 ? { style: { width } } : {})} - onClick={() => inputRef.current.focus()}> + onClick={() => inputRef.current.focus()} + > {!currentValue && isCurrencyPrefixed && currencySymbol} void, tracker: Tracker, onCancel: () => void } +type Props = { onEnd: () => void; onCancel: () => void } -function FeedbackForm({ onEnd, onCancel, tracker }: Props) { - const formRef = useRef() +export default function FeedbackForm({ onEnd, onCancel }: Props) { + const formRef = useRef() + const tracker = useContext(TrackerContext) - const handleFormSubmit = e => { + const handleFormSubmit = (e: React.FormEvent): void => { tracker.push(['trackEvent', 'Feedback', 'written feedback submitted']) e.preventDefault() fetch('/', { method: 'POST', - // $FlowFixMe body: new FormData(formRef.current) }) onEnd() @@ -28,7 +26,8 @@ function FeedbackForm({ onEnd, onCancel, tracker }: Props) { onClick={() => onCancel()} className="ui__ link-button" style={{ textDecoration: 'none', marginLeft: '0.3rem' }} - aria-label="close"> + aria-label="close" + > X
@@ -37,7 +36,8 @@ function FeedbackForm({ onEnd, onCancel, tracker }: Props) { style={{ flex: 1 }} method="post" ref={formRef} - onSubmit={handleFormSubmit}> + onSubmit={handleFormSubmit} + >