Merge pull request #1008 from betagouv/split-codebase

Sépare le moteur et le code applicatif dans des package dédiés
pull/1039/head
Johan Girod 2020-05-14 15:53:39 +02:00 committed by GitHub
commit 2b04fbbc1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
520 changed files with 2251 additions and 2805 deletions

12
.gitignore vendored
View File

@ -1,11 +1,9 @@
.tags*
.tmp
.env
source/data
node_modules/
dist/
.DS_Store
package-lock.json
yarn-error.log
cypress/videos
cypress/screenshots
package-lock.json
node_modules/
# Local Netlify folder
.netlify

View File

@ -39,12 +39,21 @@ git clone --depth 100 git@github.com:betagouv/mon-entreprise.git && cd mon-entre
# Install the Javascript dependencies through Yarn
yarn install
# Run the server
# Run the server for mon-entreprise
cd mon-entreprise
yarn start
```
L'application est exécuté sur https://localhost:8080/mon-entreprise pour la version française et http://localhost:8080/infrance pour la version anglaise.
Si vous souhaitez travailler sur le package publicode, on peut créer un lien
symbolique depuis mon-entreprise en executant la commande suivante à la racine
du projet :
```
yarn run link:publicodes
```
### Messages de commit
A mettre sans retenue dans les messages de commit :
@ -90,7 +99,7 @@ Si vous souhaitez mettre à jour les snapshots vous pouvez utiliser le paramètr
Enfin pour les tests d'intégration :
```sh
$ yarn run test-cypress
$ yarn run cypress run
```
### Traduction 👽
@ -121,7 +130,7 @@ $ yarn run i18n:rules:translate
$ yarn run i18n:ui:translate
```
N'oubliez pas de vérifier le diff que rien n'est choquant.
N'oubliez pas de vérifier sur le diff que rien n'est choquant.
### CI/CD
@ -132,7 +141,7 @@ N'oubliez pas de vérifier le diff que rien n'est choquant.
### Analyse des bundles
La commande `yarn run analyze-bundle` gènere une visualisation interactive du
La commande `yarn run compile:analyse-bundle` gènere une visualisation interactive du
contenu packagé, cf.
[webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer)
@ -148,7 +157,7 @@ raisonnement ayant abouti à ce langage sont dispos sur le repository
ailleurs inutilisé.
Pour se familiariser avec les règles, vous pouvez jeter un œil aux fichiers
contenant les règles elles-mêmes (dans le dossier `source/rules`) mais cela peut
contenant les règles elles-mêmes (dans le dossier `rules`) mais cela peut
s'avérer assez abrupt.
Essayez plutôt de jeter un oeil [aux tests](./test/mécanismes/expressions.yaml)

View File

@ -2,7 +2,7 @@
This repository powers [mycompanyinfrance.fr](https://mycompanyinfrance.fr) and [mon-entreprise.fr](https://mon-entreprise.fr) and [publi.codes](https://publi.codes).
The hiring simulator, available on both websites, embeds a [model](https://github.com/betagouv/mon-entreprise/blob/master/source/rules) of the french tax system as a YAML domain specific language. It enables displaying the computing rules on the Web and having a single source of logic for both the computation engine (a JS library) and the generated end-user conversation-like form.
The hiring simulator, available on both websites, embeds a [model](https://github.com/betagouv/mon-entreprise/blob/master/mon-entreprise/source/rules) of the french tax system as a YAML domain specific language. It enables displaying the computing rules on the Web and having a single source of logic for both the computation engine (a JS library) and the generated end-user conversation-like form.
The engine with the French tax law is available as a NPM module and explained [on the wiki](https://github.com/betagouv/mon-entreprise/wiki/Librairie-de-calcul).

View File

@ -1,30 +0,0 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current'
}
}
],
'@babel/react',
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-syntax-dynamic-import',
'react-hot-loader/babel',
['webpack-alias', { config: './source/webpack.dev.js' }],
[
'ramda',
{
useES: true
}
],
'babel-plugin-styled-components'
]
}

23
babel.config.json Normal file
View File

@ -0,0 +1,23 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
"babel-plugin-styled-components",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
["ramda", { "useES": true }]
]
}

View File

@ -5,12 +5,12 @@ commands:
- checkout
- restore_cache:
keys:
- v1-deps-{{ .Branch }}-{{ checksum "package.json" }}
- v1-deps-{{ .Branch }}-{{ checksum "yarn.lock" }}
- v1-deps-{{ .Branch }}
- v1-deps
- run: yarn install --frozen-lockfile
- save_cache:
key: v1-deps-{{ .Branch }}-{{ checksum "package.json" }}
key: v1-deps-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache
cypress:
@ -27,7 +27,9 @@ commands:
type: string
default: https://mon-entreprise.fr
steps:
- run: CYPRESS_baseUrl=<< parameters.base_url >> yarn run cypress run --record --key 21660df5-36a5-4c49-b23d-801799b0c759 --env language=<< parameters.language >> --config integrationFolder=cypress/integration/<< parameters.integration_folder >>
- run: |
cd mon-entreprise
CYPRESS_baseUrl=<< parameters.base_url >> yarn run cypress run --record --key 21660df5-36a5-4c49-b23d-801799b0c759 --env language=<< parameters.language >> --config integrationFolder=cypress/integration/<< parameters.integration_folder >>
jobs:
lint:
@ -36,42 +38,45 @@ jobs:
steps:
- install
- run: |
yarn eslintrc-check
yarn eslint-check --quiet
yarn prettier-check
yarn lint:eslintrc
yarn lint:eslint --quiet
yarn lint:prettier
type-check:
docker:
- image: node:12.16.1-buster
steps:
- install
- run: |
yarn type-check
- run: yarn test:type
i18n-check:
docker:
- image: node:12.16.1-buster
steps:
- install
- run: yarn run i18n:rules:check
- run: yarn run i18n:ui:check
- run: |
cd mon-entreprise
yarn run i18n:rules:check
yarn run i18n:ui:check
unit-test:
docker:
- image: node:12.16.1-buster
steps:
- install
- run: |
git config --global core.quotepath false
yarn test
yarn test-regressions
- run: git config --global core.quotepath false
- run: yarn test
- run: yarn test:regressions
end-to-end-test:
docker:
- image: cypress/base:12.16.1
environment:
TERM: xterm
resource_class: medium+
steps:
- install
- run: yarn run compile-dev
- run: yarn workspace mon-entreprise compile:dev
- run:
command: yarn run serve-dev
command: yarn workspace mon-entreprise serve:dev
background: true
- cypress:
base_url: http://localhost:5000
@ -81,21 +86,13 @@ jobs:
- cypress:
base_url: http://localhost:5002
integration_folder: publi.codes
bundlesize-test:
docker:
- image: cypress/base:12.16.1
environment:
TERM: xterm
steps:
- install
- run: |
yarn run simple-compile
yarn test-bundlesize
production-end-to-end-test:
docker:
- image: cypress/base:12.16.1
environment:
TERM: xterm
resource_class: medium+
parallelism: 3
steps:
- install
@ -124,7 +121,6 @@ workflows:
- i18n-check
- unit-test
- end-to-end-test
- bundlesize-test
- production-end-to-end-test:
filters:
branches:

View File

@ -67,18 +67,11 @@ module.exports = {
moduleDirectories: ['node_modules', 'sources'],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],
// A map from regular expressions to module names that allow to stub out resources with a single module
moduleNameMapper: {
'\\.css$': '<rootDir>/test/regressions/styleMock.js'
'\\.css$': 'mon-entreprise/test/regressions/styleMock.js'
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
@ -115,9 +108,7 @@ module.exports = {
// rootDir: null,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// roots: ['<rootDir>'],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
@ -141,13 +132,11 @@ module.exports = {
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: [
// We have 2 test runners (Mocha and Jest), so we create a custom extension without `.test.js`
// to dissociate the two.
'**/*.jest.js'
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
],
testMatch: ['**/*.jest.js'],
// [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
@ -173,13 +162,14 @@ module.exports = {
transform: {
// It's not possible to have 2 piped transformers like in webpack
// ie ['jest-transform-nearley', 'babel-jest'], so we removed ES6 module from nearley output.
'\\.ne$': 'jest-transform-nearley',
'\\.yaml$': 'yaml-jest',
'\\.(js|tsx?)$': 'babel-jest'
'\\.ne$': require.resolve('jest-transform-nearley'),
'\\.yaml$': require.resolve('yaml-jest'),
'\\.(js|tsx?)$': require.resolve('babel-jest')
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
transformIgnorePatterns: ['/node_modules/(?!ramda).+\\.js$']
// An array of regexp pattern strings that are matched against all source file
// paths, matched files will skip transformation
transformIgnorePatterns: ['node_modules/(?!ramda|publicodes)/']
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,

View File

@ -0,0 +1,6 @@
{
"plugins": [
"react-hot-loader/babel",
["webpack-alias", { "config": "./webpack.dev.js" }]
]
}

View File

@ -0,0 +1,5 @@
extends: '../.eslintrc'
overrides:
- files: ['*.test.js', 'cypress/integration/**/*.js']
env:
mocha: true

8
mon-entreprise/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.env
node_modules/
package-lock.json
yarn-error.log
source/data
dist/
cypress/videos
cypress/screenshots

View File

@ -14,7 +14,7 @@ describe('Simulateurs', function() {
})
it('should display a result when entering a value in any of the currency input', () => {
cy.contains('€ / an').click()
cy.contains('€/an').click()
if (['indépendant', 'assimilé-salarié'].includes(simulateur)) {
cy.get(chargeInputSelector).type(1000)
}
@ -33,7 +33,7 @@ describe('Simulateurs', function() {
})
it('should allow to change period', function() {
cy.contains('€ / an').click()
cy.contains('€/an').click()
cy.wait(200)
cy.get(inputSelector)
.first()
@ -42,7 +42,7 @@ describe('Simulateurs', function() {
cy.get(chargeInputSelector).type('{selectall}6000')
}
cy.wait(800)
cy.contains('€ / mois').click()
cy.contains('€/mois').click()
cy.get(inputSelector)
.first()
.invoke('val')
@ -82,11 +82,12 @@ describe('Simulateurs', function() {
cy.contains('Passer').click()
cy.contains('Passer').click()
cy.contains('Début 2020').click()
cy.wait(200)
cy.contains('Suivant').click()
cy.contains('ACRE')
})
it('should not have negative value', () => {
cy.contains('€ / mois').click()
cy.contains('€/mois').click()
cy.get(inputSelector)
.first()
.type('{selectall}5000')
@ -101,7 +102,7 @@ describe('Simulateurs', function() {
)
})
describe.only('Simulateur salarié', () => {
describe('Simulateur salarié', () => {
if (!fr) {
return
}

View File

@ -0,0 +1,90 @@
{
"name": "mon-entreprise",
"license": "MIT",
"version": "1.2.8",
"description": "Library to compute the french social security contributions. Also a website that explains the calculations, and a generic engine to build cool forms that asks the question needed to compute an objective.",
"repository": {
"type": "git",
"url": "https://github.com/betagouv/mon-entreprise.git",
"directory": "mon-entreprise"
},
"engines": {
"node": ">=12.16.1"
},
"browserslist": [
"> 1% in FR",
"not ie < 11"
],
"devDependencies": {
"i18next-parser": "https://github.com/i18next/i18next-parser#master"
},
"dependencies": {
"@babel/runtime": "^7.3.4",
"@rehooks/local-storage": "^2.1.1",
"@sentry/browser": "5.15.5",
"classnames": "^2.2.5",
"color-convert": "^1.9.2",
"core-js": "^3.2.1",
"focus-trap-react": "^3.1.2",
"fuse.js": "5.2.1",
"iframe-resizer": "^4.1.1",
"js-yaml": "^3.13.1",
"moo": "^0.5.0",
"nearley": "^2.19.0",
"publicodes": "file:../publicodes",
"puppeteer": "^2.1.1",
"ramda": "^0.27.0",
"react": "^16.13.1",
"react-color": "^2.14.0",
"react-dom": "npm:@hot-loader/react-dom",
"react-easy-emoji": "^1.2.0",
"react-helmet": "6.0.0-beta",
"react-i18next": "^11.0.0",
"react-loading-skeleton": "^2.0.1",
"react-markdown": "^4.1.0",
"react-monaco-editor": "^0.36.0",
"react-number-format": "^4.3.1",
"react-redux": "^7.0.3",
"react-router-dom": "^5.1.1",
"react-router-hash-link": "^1.2.2",
"react-spring": "=8.0.27",
"react-syntax-highlighter": "^10.1.1",
"react-to-print": "^2.5.1",
"react-transition-group": "^2.2.1",
"recharts": "^1.8.5",
"reduce-reducers": "^1.0.4",
"redux": "^4.0.4",
"redux-sentry-middleware": "^0.1.8",
"redux-thunk": "^2.3.0",
"regenerator-runtime": "^0.13.3",
"reselect": "^4.0.0",
"styled-components": "^5.1.0",
"swr": "^0.1.16",
"whatwg-fetch": "^3.0.0",
"yaml": "^1.9.2"
},
"scripts": {
"prepare": "node scripts/prepare.js",
"compile": "yarn run compile:prod && yarn run compile:legacy",
"compile:prod": "yarn run webpack --config webpack.prod.js",
"compile:legacy": "yarn run webpack --config webpack.prod.legacyBrowser.js",
"compile:stats": "webpack --config webpack.prod.js --profile --json > stats.json",
"compile:analyze-bundle": "ANALYZE_BUNDLE=1 yarn run compile",
"compile:dev": "FR_SITE='http://localhost:5000${path}' EN_SITE='http://localhost:5001${path}' yarn run compile",
"test": "yarn test:file \"./{,!(node_modules)/**/}!(webpack).test.js\"",
"test:file": "yarn mocha-webpack --webpack-config ../webpack.test.js --require source-map-support/register --include test/componentTestSetup.js --require mock-local-storage --require test/helpers/browser.js",
"test:bundlesize": "bundlesize",
"test:dev-e2e:publicode": "cypress open --browser chromium --config baseUrl=http://localhost:8080/publicodes,integrationFolder=cypress/integration/publi.codes",
"test:dev-e2e:mon-entreprise": "cypress open --browser chromium",
"test:dev-e2e:mycompanyinfrance": "cypress open --browser chromium --config baseUrl=http://localhost:8080/infrance",
"i18n:rules:check": "node scripts/i18n/check-missing-rule-translation.js",
"i18n:rules:translate": "node scripts/i18n/translate-rules.js",
"i18n:ui:check": "yarn run i18next -c scripts/i18n/parser.config.js && node scripts/i18n/check-missing-UI-translation",
"i18n:ui:translate": "rm -rf source/locales/static-analysis-fr.json && yarn run i18next -c script/i18n/parser.config.js && node scripts/i18n/translate-ui.js",
"start": "node source/server.js",
"serve:dev": "yarn run serve:dev:mon-entreprise & yarn run serve:dev:mycompanyinfrance & yarn run serve:dev:publicodes",
"serve:dev:mon-entreprise": "PORT=5000 serve --config serve.mon-entreprise.json --no-clipboard",
"serve:dev:publicodes": "PORT=5002 serve --config serve.publicodes.json --no-clipboard",
"serve:dev:mycompanyinfrance": "PORT=5001 serve --config serve.infrance.json --no-clipboard"
}
}

View File

@ -10,9 +10,9 @@ const fs = require('fs')
const path = require('path')
const { readRules } = require('./rules')
const sourceDirPath = path.resolve(__dirname, '../rules')
const sourceDirPath = path.resolve(__dirname, '../source/rules')
// Note: we can't put the output file in the fs.watched directory
const outPath = path.resolve(__dirname, '../types/dottednames.json')
const outPath = path.resolve(__dirname, '../source/types/dottednames.json')
function persistJsonFileFromYaml() {
const rules = readRules()

View File

@ -50,12 +50,12 @@ module.exports = {
// Namespace separator used in your translation keys
// If you want to use plain english keys, separators such as `.` and `:` will conflict. You might want to set `keySeparator: false` and `namespaceSeparator: false`. That way, `t('Status: Loading...')` will not think that there are a namespace and three separator dots for instance.
output: 'source/locales/static-analysis-$LOCALE.json',
output: '../../source/locales/static-analysis-$LOCALE.json',
// Supports $LOCALE and $NAMESPACE injection
// Supports JSON (.json) and YAML (.yml) file formats
// Where to write the locale files relative to process.cwd()
input: './source/**/*.{jsx,tsx,js,ts}',
input: '../../source/**/*.{jsx,tsx,js,ts}',
// An array of globs that describe where to look for source files
// relative to the location of the configuration file

View File

@ -117,7 +117,7 @@ function getRulesMissingTranslations() {
const getUiMissingTranslations = () => {
const staticKeys = require(path.resolve(
'source/locales/static-analysis-fr.json'
'../../source/locales/static-analysis-fr.json'
))
const translatedKeys = parse(fs.readFileSync(UiTranslationPath, 'utf-8'))

View File

@ -5,7 +5,7 @@ const fs = require('fs')
const path = require('path')
const yaml = require('yaml')
const publicodesDir = path.resolve(__dirname, '../rules')
const publicodesDir = path.resolve(__dirname, '../source/rules')
function concatenateFilesInDir(dirPath = publicodesDir) {
return fs

View File

@ -1,6 +1,6 @@
const path = require('path')
const fs = require('fs')
const dataDir = path.resolve(__dirname, '../data/')
const dataDir = path.resolve(__dirname, '../source/data/')
exports.createDataDir = () => {
if (!fs.existsSync(dataDir)) {

View File

@ -2,16 +2,15 @@ import { ThemeColorsProvider } from 'Components/utils/colors'
import { SitePathProvider, SitePaths } from 'Components/utils/SitePathsContext'
import { TrackerProvider } from 'Components/utils/withTracker'
import { createBrowserHistory } from 'history'
import { AvailableLangs } from 'i18n'
import i18next from 'i18next'
import React, { createContext, useEffect, useMemo } from 'react'
import { I18nextProvider, useTranslation } from 'react-i18next'
import { I18nextProvider } from 'react-i18next'
import { Provider as ReduxProvider } from 'react-redux'
import { Router } from 'react-router-dom'
import reducers, { RootState } from 'Reducers/rootReducer'
import { applyMiddleware, compose, createStore, Middleware, Store } from 'redux'
import thunk from 'redux-thunk'
import Tracker from 'Tracker'
import Tracker from './Tracker'
import { inIframe } from './utils'
declare global {
@ -56,13 +55,13 @@ export type ProviderProps = {
}
export default function Provider({
tracker,
tracker = new Tracker(),
basename,
sitePaths,
reduxMiddlewares,
initialStore,
onStoreCreated,
children
children,
sitePaths = {} as SitePaths
}: ProviderProps) {
const history = useMemo(
() =>
@ -118,9 +117,9 @@ export default function Provider({
<ThemeColorsProvider
color={iframeCouleur && decodeURIComponent(iframeCouleur)}
>
<TrackerProvider value={tracker!}>
<TrackerProvider value={tracker}>
<SiteNameContext.Provider value={basename}>
<SitePathProvider value={sitePaths as any}>
<SitePathProvider value={sitePaths}>
<I18nextProvider i18n={i18next}>
<Router history={history}>
<>{children}</>

View File

@ -2,7 +2,7 @@ import { SitePaths } from 'Components/utils/SitePathsContext'
import { History } from 'history'
import { RootState, SimulationConfig } from 'Reducers/rootReducer'
import { ThunkAction } from 'redux-thunk'
import { DottedName, Situation } from 'Rules'
import { DottedName } from 'Rules'
import { deletePersistedSimulation } from '../storage/persistSimulation'
import { CompanyStatusAction } from './companyStatusActions'

View File

@ -2,7 +2,7 @@ import React from 'react'
import emoji from 'react-easy-emoji'
import { useSelector } from 'react-redux'
import { firstStepCompletedSelector } from 'Selectors/simulationSelectors'
import Animate from 'Ui/animate'
import Animate from 'Components/ui/animate'
import './Banner.css'
type BannerProps = {

View File

@ -3,7 +3,7 @@ import emoji from 'react-easy-emoji'
import { animated, config, useSpring } from 'react-spring'
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
import { ThemeColorsContext } from 'Components/utils/colors'
import { formatValue } from 'Engine/format'
import { formatValue } from 'publicodes'
import { useTranslation } from 'react-i18next'
const ANIMATION_SPRING = config.gentle

View File

@ -1,16 +1,15 @@
import { goToQuestion, hideControl } from 'Actions/actions'
import animate from 'Components/ui/animate'
import { useControls, useInversionFail } from 'Components/utils/EngineContext'
import { makeJsx } from 'Engine/evaluation'
import React from 'react'
import emoji from 'react-easy-emoji'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import animate from 'Ui/animate'
import { answeredQuestionsSelector } from 'Selectors/simulationSelectors'
import './Controls.css'
import { Markdown } from './utils/markdown'
import { ScrollToElement } from './utils/Scroll'
import { answeredQuestionsSelector } from 'Selectors/simulationSelectors'
export default function Controls() {
const { t } = useTranslation()
@ -48,12 +47,7 @@ export default function Controls() {
<div className="control">
{emoji(level == 'avertissement' ? '⚠️' : '')}
<div className="controlText ui__ card">
{message ? (
<Markdown source={message} />
) : (
<span id="controlExplanation">{makeJsx(evaluated)}</span>
)}
<Markdown source={message} />
{solution && !answeredQuestions?.includes(solution.cible) && (
<div>
<button

View File

@ -1,8 +1,7 @@
import classnames from 'classnames'
import { currencyFormat } from 'Engine/format'
import React, { useMemo, useRef, useState } from 'react'
import NumberFormat, { NumberFormatProps } from 'react-number-format'
import { debounce } from '../../utils'
import { debounce, currencyFormat } from '../../utils'
import './CurrencyInput.css'
type CurrencyInputProps = NumberFormatProps & {
@ -10,7 +9,7 @@ type CurrencyInputProps = NumberFormatProps & {
debounce?: number
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
currencySymbol?: string
language?: Parameters<typeof currencyFormat>[0]
language: string
}
export default function CurrencyInput({

View File

@ -64,7 +64,7 @@ export function DistributionBranch({
<BarChartBranch
value={value}
maximum={maximum}
title={<RuleLink {...branche} />}
title={<RuleLink dottedName={dottedName} />}
icon={icon ?? branche.icons}
description={branche.summary}
unit="€"

View File

@ -1,6 +1,4 @@
import Engine from 'Engine'
import { formatValue } from 'Engine/format'
import { EvaluatedNode } from 'Engine/types'
import Engine, { EvaluatedNode, formatValue } from 'publicodes'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { DottedName } from 'Rules'
@ -44,7 +42,7 @@ export default function Value({
})
if ('dottedName' in evaluation && linkToRule) {
return (
<RuleLink {...evaluation}>
<RuleLink dottedName={evaluation.dottedName}>
<span {...props}>{value}</span>
</RuleLink>
)

View File

@ -1,7 +1,7 @@
import { TrackerContext } from 'Components/utils/withTracker'
import React, { useCallback, useContext, useState } from 'react'
import { Trans } from 'react-i18next'
import { useLocation } from 'react-router'
import { useLocation } from 'react-router-dom'
import safeLocalStorage from '../../storage/safeLocalStorage'
import './Feedback.css'
import Form from './FeedbackForm'

View File

@ -1,7 +1,7 @@
import * as animate from 'Components/ui/animate'
import { LinkButton } from 'Components/ui/Button'
import FocusTrap from 'focus-trap-react'
import React, { useEffect } from 'react'
import * as animate from 'Ui/animate'
import { LinkButton } from 'Ui/Button'
import './Overlay.css'
type OverlayProps = React.HTMLAttributes<HTMLDivElement> & {

View File

@ -1,13 +1,12 @@
import { useEvaluation, EngineContext } from 'Components/utils/EngineContext'
import Value from 'Components/EngineValue'
import { formatValue } from 'Engine/format'
import RuleLink from 'Components/RuleLink'
import { EngineContext, useEvaluation } from 'Components/utils/EngineContext'
import { formatValue, ParsedRule, ParsedRules } from 'publicodes'
import React, { Fragment, useContext } from 'react'
import { Trans } from 'react-i18next'
import { DottedName } from 'Rules'
import './PaySlip.css'
import { Line, SalaireBrutSection, SalaireNetSection } from './PaySlipSections'
import RuleLink from './RuleLink'
import { ParsedRules, ParsedRule } from 'Rules'
export const SECTION_ORDER = [
'protection sociale . santé',
@ -105,7 +104,7 @@ export default function PaySlip() {
return (
<Fragment key={section.dottedName}>
<h5 className="payslip__cotisationTitle">
<RuleLink {...section} />
<RuleLink dottedName={section.dottedName} />
</h5>
{cotisations.map(cotisation => (
<Cotisation key={cotisation} dottedName={cotisation} />
@ -150,8 +149,6 @@ export default function PaySlip() {
}
function Cotisation({ dottedName }: { dottedName: DottedName }) {
const parsedRules = useContext(EngineContext).getParsedRules()
const partSalariale = useEvaluation(
'contrat salarié . cotisations . salariales'
)?.formule.explanation.explanation.find(
@ -168,7 +165,7 @@ function Cotisation({ dottedName }: { dottedName: DottedName }) {
return (
<>
<RuleLink
{...parsedRules[dottedName]}
dottedName={dottedName}
style={{ backgroundColor: 'var(--lightestColor)' }}
/>
<span style={{ backgroundColor: 'var(--lightestColor)' }}>

View File

@ -1,10 +1,9 @@
import Value, { Condition, ValueProps } from 'Components/EngineValue'
import RuleLink from 'Components/RuleLink'
import { EngineContext } from 'Components/utils/EngineContext'
import Value, { ValueProps, Condition } from 'Components/EngineValue'
import React, { useContext } from 'react'
import { Trans } from 'react-i18next'
import { DottedName } from 'Rules'
import { coerceArray } from '../utils'
import RuleLink from './RuleLink'
export const SalaireBrutSection = () => {
return (

View File

@ -1,4 +1,4 @@
import { formatValue } from 'Engine/format'
import { formatValue } from 'publicodes'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { debounce as debounceFn } from '../utils'

View File

@ -1,7 +1,6 @@
import { updateUnit } from 'Actions/actions'
import { parseUnit, serializeUnit } from 'Engine/units'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { targetUnitSelector } from 'Selectors/simulationSelectors'
import './PeriodSwitch.css'
@ -24,7 +23,9 @@ export default function PeriodSwitch() {
onChange={() => dispatch(updateUnit(unit))}
checked={currentUnit === unit}
/>
<span>{serializeUnit(parseUnit(unit), 1, language)}</span>
<span>
<Trans>{unit}</Trans>
</span>
</label>
))}
</span>

View File

@ -3,7 +3,7 @@ import React from 'react'
import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import { LinkButton } from 'Ui/Button'
import { LinkButton } from 'Components/ui/Button'
import Banner from './Banner'
import { firstStepCompletedSelector } from 'Selectors/simulationSelectors'

View File

@ -0,0 +1,25 @@
import Engine, { RuleLink as EngineRuleLink } from 'publicodes'
import React, { useContext } from 'react'
import { Link } from 'react-router-dom'
import { DottedName } from 'Rules'
import { EngineContext } from './utils/EngineContext'
import { SitePathsContext } from './utils/SitePathsContext'
export default function RuleLink(
props: {
dottedName: DottedName
useDefaultValues?: boolean
displayIcon?: boolean
} & Omit<React.ComponentProps<Link>, 'to'>
) {
const sitePaths = useContext(SitePathsContext)
const engine = useContext(EngineContext)
return (
<EngineRuleLink
{...props}
engine={engine}
useDefaultValues={props.useDefaultValues ?? true}
documentationPath={sitePaths.documentation.index}
/>
)
}

View File

@ -1,7 +1,8 @@
import { ParsedRule } from 'publicodes'
import yaml from 'yaml'
import React from 'react'
import rules, { ParsedRule } from 'Rules'
import PublicodeHighlighter from '../ui/PublicodeHighlighter'
import rules from 'Rules'
import PublicodeHighlighter from './ui/PublicodeHighlighter'
type RuleSourceProps = Pick<ParsedRule, 'dottedName'>

View File

@ -8,7 +8,7 @@ import emoji from 'react-easy-emoji'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { RootState } from 'Reducers/rootReducer'
import * as Animate from 'Ui/animate'
import * as Animate from 'Components/ui/animate'
import { answeredQuestionsSelector } from 'Selectors/simulationSelectors'
export default function SalaryExplanation() {

View File

@ -8,7 +8,7 @@ import Conversation from 'Components/conversation/Conversation'
import SeeAnswersButton from 'Components/conversation/SeeAnswersButton'
import Value from 'Components/EngineValue'
import dirigeantComparaison from 'Components/simulationConfigs/rémunération-dirigeant.yaml'
import Engine from 'Engine'
import Engine from 'publicodes'
import revenusSVG from 'Images/revenus.svg'
import {
default as React,
@ -22,7 +22,7 @@ import emoji from 'react-easy-emoji'
import { Trans } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { situationSelector } from 'Selectors/simulationSelectors'
import InfoBulle from 'Ui/InfoBulle'
import InfoBulle from 'Components/ui/InfoBulle'
import './SchemeComparaison.css'
import { EngineContext } from './utils/EngineContext'

View File

@ -0,0 +1,182 @@
import { ParsedRules } from 'publicodes'
import React, { useContext, useEffect, useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { DottedName } from 'Rules'
import Worker from 'worker-loader!./SearchBar.worker.js'
import RuleLink from './RuleLink'
import './SearchBar.css'
const worker = new Worker()
type SearchBarProps = {
rules: ParsedRules<DottedName>
showDefaultList: boolean
}
type SearchItem = {
title: string
dottedName: DottedName
espace: Array<string>
}
type Matches = Array<{
key: string
value: string
indices: Array<[number, number]>
}>
function highlightMatches(str: string, matches: Matches) {
if (!matches?.length) {
return str
}
const indices = matches[0].indices
.sort(([a], [b]) => a - b)
.map(([x, y]) => [x, y + 1])
.reduce(
(acc, value) =>
acc[acc.length - 1][1] <= value[0] ? [...acc, value] : acc,
[[0, 0]]
)
.flat()
return [...indices, str.length].reduce(
([highlight, prevIndice, acc], currentIndice, i) => {
const currentStr = str.slice(prevIndice, currentIndice)
return [
!highlight,
currentIndice,
[
...acc,
<span
style={highlight ? { fontWeight: 'bold' } : {}}
className={highlight ? 'ui__ light-bg' : ''}
key={i}
>
{currentStr}
</span>
]
] as [boolean, number, Array<React.ReactNode>]
},
[false, 0, []] as [boolean, number, Array<React.ReactNode>]
)[2]
}
export default function SearchBar({ rules, showDefaultList }: SearchBarProps) {
const [input, setInput] = useState('')
const [results, setResults] = useState<
Array<{
item: SearchItem
matches: Matches
}>
>([])
const { i18n } = useTranslation()
const searchIndex: Array<SearchItem> = useMemo(
() =>
Object.values(rules).map(rule => ({
title:
rule.title ??
rule.name + (rule.acronyme ? ` (${rule.acronyme})` : ''),
dottedName: rule.dottedName,
espace: rule.dottedName.split(' . ').reverse()
})),
[rules]
)
useEffect(() => {
worker.postMessage({
rules: searchIndex
})
worker.onmessage = ({ data: results }) => setResults(results)
return () => {
worker.onmessage = null
}
}, [searchIndex, setResults])
return (
<>
<input
type="search"
css={`
padding: 0.4rem;
margin: 0.2rem 0;
width: 100%;
border: 1px solid var(--lighterTextColor);
border-radius: 0.3rem;
color: inherit;
font-size: inherit;
transition: border-color 0.1s;
position: relative;
:focus {
border-color: var(--color);
}
`}
value={input}
placeholder={i18n.t('Entrez des mots clefs ici')}
onChange={e => {
const input = e.target.value
if (input.length > 0) worker.postMessage({ input })
setInput(input)
}}
/>
{!!input.length && !showDefaultList && !results.length ? (
<p
className="ui__ notice light-bg"
css={`
padding: 0.4rem;
border-radius: 0.3rem;
margin-top: 0.6rem;
`}
>
<Trans i18nKey="noresults">
Aucun résultat ne correspond à cette recherche
</Trans>
</p>
) : (
<ul
css={`
padding: 0;
margin: 0;
list-style: none;
`}
>
{(showDefaultList && !results.length
? searchIndex.map(item => ({ item, matches: [] }))
: results
)
.slice(0, 6)
.map(({ item, matches }) => (
<li key={item.dottedName}>
<RuleLink
dottedName={item.dottedName}
style={{ width: '100%', textDecoration: 'none' }}
>
<small>
{item.espace
.slice(1)
.reverse()
.map(name => (
<span key={name}>
{highlightMatches(
name,
matches.filter(
m => m.key === 'espace' && m.value === name
)
)}{' '}
{' '}
</span>
))}
</small>{' '}
<br />
{highlightMatches(
item.title,
matches.filter(m => m.key === 'title')
)}
</RuleLink>
</li>
))}
</ul>
)}
</>
)
}

View File

@ -0,0 +1,34 @@
import Fuse from 'fuse.js'
let searchWeights = [
{
name: 'espace',
weight: 0.6
},
{
name: 'title',
weight: 0.4
}
]
let fuse = null
onmessage = function(event) {
if (event.data.rules)
fuse = new Fuse(event.data.rules, {
keys: searchWeights,
includeMatches: true,
minMatchCharLength: 2,
useExtendedSearch: true,
distance: 50,
threshold: 0.3
})
if (event.data.input) {
let results = [
...fuse.search(
event.data.input + '|' + event.data.input.replace(/ /g, '|')
)
]
postMessage(results)
}
}

View File

@ -5,6 +5,7 @@ import { useSelector } from 'react-redux'
import Overlay from './Overlay'
import { EngineContext } from 'Components/utils/EngineContext'
import SearchBar from './SearchBar'
import { useLocation } from 'react-router'
type SearchButtonProps = {
invisibleButton?: boolean
@ -13,6 +14,7 @@ type SearchButtonProps = {
export default function SearchButton({ invisibleButton }: SearchButtonProps) {
const rules = useContext(EngineContext).getParsedRules()
const [visible, setVisible] = useState(false)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!(e.ctrlKey && e.key === 'k')) return
@ -34,7 +36,7 @@ export default function SearchButton({ invisibleButton }: SearchButtonProps) {
<h1>
<Trans>Chercher dans la documentation</Trans>
</h1>
<SearchBar showDefaultList={false} finally={close} rules={rules} />
<SearchBar showDefaultList={false} rules={rules} />
</Overlay>
) : invisibleButton ? null : (
<button

View File

@ -1,3 +1,4 @@
import { setSimulationConfig } from 'Actions/actions'
import Controls from 'Components/Controls'
import Conversation, {
ConversationProps
@ -6,16 +7,15 @@ import SeeAnswersButton from 'Components/conversation/SeeAnswersButton'
import PageFeedback from 'Components/Feedback/PageFeedback'
import SearchButton from 'Components/SearchButton'
import TargetSelection from 'Components/TargetSelection'
import { useSimulationProgress } from 'Components/utils/useNextQuestion'
import React, { useEffect } from 'react'
import { Trans } from 'react-i18next'
import { useSelector, useDispatch } from 'react-redux'
import { firstStepCompletedSelector } from 'Selectors/simulationSelectors'
import { useSimulationProgress } from 'Components/utils/useNextQuestion'
import * as Animate from 'Ui/animate'
import Progress from 'Ui/Progress'
import { setSimulationConfig } from 'Actions/actions'
import { useDispatch, useSelector } from 'react-redux'
import { useLocation } from 'react-router'
import { SimulationConfig } from 'Reducers/rootReducer'
import { firstStepCompletedSelector } from 'Selectors/simulationSelectors'
import * as Animate from 'Components/ui/animate'
import Progress from 'Components/ui/Progress'
type SimulationProps = {
config: SimulationConfig

View File

@ -1,6 +1,6 @@
import RuleLink from 'Components/RuleLink'
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
import { EvaluatedRule, Evaluation, Types } from 'Engine/types'
import { EvaluatedRule, Evaluation, Types } from 'publicodes'
import React from 'react'
import { animated, useSpring } from 'react-spring'
import { DottedName } from 'Rules'
@ -143,7 +143,7 @@ export default function StackedRulesChart({ data }: StackedRulesChartProps) {
...rule,
key: rule.dottedName,
value: rule.nodeValue,
legend: <RuleLink {...rule}>{capitalise0(rule.title)}</RuleLink>
legend: <RuleLink dottedName={rule.dottedName} />
}))}
/>
)

Some files were not shown because too many files have changed in this diff Show More