Merge branch 'master' of github.com:betagouv/mon-entreprise

pull/2037/head^2
Jérémy Rialland 2022-03-02 11:41:40 +01:00
commit 519e51d785
17 changed files with 463 additions and 340 deletions

View File

@ -32,7 +32,7 @@
"test:dev-e2e:mon-entreprise": "cypress open --browser chromium",
"test:dev-e2e:mycompanyinfrance": "cypress open --browser chromium --config baseUrl=http://localhost:8080/infrance,integrationFolder=cypress/integration/mon-entreprise/english --env language=en",
"test:record-http-calls:mon-entreprise": "cypress run --env record_http=",
"algolia:update": "node scripts/search/update-data.js",
"algolia:update": "node --loader ts-node/esm scripts/search/update-data.ts",
"algolia:clean": "node scripts/search/clean.js",
"i18n:check": "yarn i18n:rules:check && yarn i18n:ui:check",
"i18n:translate": "yarn i18n:rules:translate && yarn i18n:ui:translate",
@ -93,6 +93,7 @@
"reduce-reducers": "^1.0.4",
"redux": "^4.0.4",
"styled-components": "^5.3.1",
"ts-node": "^10.5.0",
"whatwg-fetch": "^3.0.0",
"yaml": "^1.9.2"
},

View File

@ -1,157 +0,0 @@
import algoliasearch from 'algoliasearch'
import 'dotenv/config.js'
import rawRules from 'modele-social'
import { parsePublicodes } from 'publicodes'
import getSimulationData from '../../source/pages/Simulateurs/metadata-src.js'
const rules = parsePublicodes(rawRules)
const ALGOLIA_APP_ID = process.env.ALGOLIA_APP_ID
const ALGOLIA_ADMIN_KEY = process.env.ALGOLIA_ADMIN_KEY
const ALGOLIA_INDEX_PREFIX = process.env.ALGOLIA_INDEX_PREFIX || ''
const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_KEY)
const rulesIndex = client.initIndex(`${ALGOLIA_INDEX_PREFIX}rules`)
const simulateursIndex = client.initIndex(`${ALGOLIA_INDEX_PREFIX}simulateurs`)
const formatRulesToAlgolia = (rules) =>
Object.entries(rules)
.map(([n, rule]) => {
if (!rule) return
const path = n.split(' . ')
const {
title,
rawNode: { icônes = '', description, acronyme, résumé },
} = rule
const ruleName = `${title} ${' ' + icônes}`.trim()
const namespace = path.slice(0, -1)
return {
objectID: n,
path,
ruleName,
namespace,
pathDepth: path.length,
acronyme: acronyme,
titre: title,
icone: icônes,
description: description || résumé,
}
})
.filter(Boolean)
const formatSimulationDataToAlgolia = (simulations) =>
Object.entries(simulations).map(([id, simulation]) => ({
...simulation,
objectID: id,
title: simulation.title || simulation.shortName || simulation.meta.title,
tooltip: simulation.tooltip || '',
description: simulation.meta?.description,
}))
;(async function () {
try {
console.log('Algolia update START')
console.log('Clearing: rules')
await rulesIndex.clearObjects().wait()
console.log('Configure index: rules')
await rulesIndex
.setSettings({
// Parameters are documented on Algolia website https://www.algolia.com/doc/api-reference/api-parameters/
minWordSizefor1Typo: 4,
minWordSizefor2Typos: 8,
hitsPerPage: 20,
maxValuesPerFacet: 100,
attributesToIndex: ['unordered(ruleName)', 'unordered(namespace)'],
numericAttributesToIndex: null,
attributesToRetrieve: null,
unretrievableAttributes: null,
optionalWords: null,
attributesForFaceting: null,
attributesToSnippet: null,
attributesToHighlight: ['ruleName', 'namespace'],
paginationLimitedTo: 1000,
attributeForDistinct: null,
exactOnSingleWordQuery: 'attribute',
ranking: [
'typo',
'geo',
'words',
'filters',
'proximity',
'attribute',
'exact',
'custom',
],
customRanking: ['asc(pathDepth)'],
separatorsToIndex: '',
removeWordsIfNoResults: 'none',
queryType: 'prefixLast',
highlightPreTag: '<em>',
highlightPostTag: '</em>',
snippetEllipsisText: '',
alternativesAsExact: ['ignorePlurals', 'singleWordSynonym'],
})
.wait()
console.log('Uploading: rules')
await rulesIndex.saveObjects(formatRulesToAlgolia(rules)).wait()
console.log('Clearing: simulateurs')
await simulateursIndex.clearObjects().wait()
console.log('Configure index: simulateurs')
await simulateursIndex
.setSettings({
// Parameters are documented on Algolia website https://www.algolia.com/doc/api-reference/api-parameters/
minWordSizefor1Typo: 4,
minWordSizefor2Typos: 8,
hitsPerPage: 20,
maxValuesPerFacet: 100,
attributesToIndex: [
'unordered(title)',
'unordered(tooltip)',
'unordered(description)',
],
numericAttributesToIndex: null,
attributesToRetrieve: null,
unretrievableAttributes: null,
optionalWords: null,
attributesForFaceting: null,
attributesToSnippet: null,
attributesToHighlight: ['title'],
paginationLimitedTo: 1000,
attributeForDistinct: null,
exactOnSingleWordQuery: 'attribute',
ranking: [
'typo',
'geo',
'words',
'filters',
'proximity',
'attribute',
'exact',
'custom',
],
customRanking: null,
separatorsToIndex: '',
removeWordsIfNoResults: 'none',
queryType: 'prefixLast',
highlightPreTag: '<em>',
highlightPostTag: '</em>',
snippetEllipsisText: '',
alternativesAsExact: ['ignorePlurals', 'singleWordSynonym'],
})
.wait()
console.log('Uploading: simulateurs')
await simulateursIndex
.saveObjects(formatSimulationDataToAlgolia(getSimulationData()))
.wait()
console.log('Algolia update DONE')
} catch (e) {
console.log(JSON.stringify(e, null, 2))
}
})()

View File

@ -0,0 +1,152 @@
import algoliasearch from 'algoliasearch'
import 'dotenv/config.js'
import rawRules from 'modele-social'
import { ParsedRules, parsePublicodes } from 'publicodes'
import getSimulationData, {
MetadataSrc,
} from '../../source/pages/Simulateurs/metadata-src.js'
const rules = parsePublicodes(rawRules)
// @ts-ignore Needed by ts-node/esm
const env = process.env
const ALGOLIA_APP_ID = env.ALGOLIA_APP_ID || ''
const ALGOLIA_ADMIN_KEY = env.ALGOLIA_ADMIN_KEY || ''
const ALGOLIA_INDEX_PREFIX = env.ALGOLIA_INDEX_PREFIX || ''
const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_KEY)
const rulesIndex = client.initIndex(`${ALGOLIA_INDEX_PREFIX}rules`)
const simulateursIndex = client.initIndex(`${ALGOLIA_INDEX_PREFIX}simulateurs`)
const falsy = <T>(value: T | false): value is T => Boolean(value)
const formatRulesToAlgolia = (rules: ParsedRules<string>) =>
Object.entries(rules)
.map(([n, rule]) => {
if (!rule) return false
const path = n.split(' . ')
const {
title,
rawNode: { icônes = '', description, acronyme, résumé },
} = rule
const ruleName = `${title} ${' ' + icônes}`.trim()
const namespace = path.slice(0, -1)
return {
objectID: n,
path,
ruleName,
namespace,
pathDepth: path.length,
acronyme: acronyme,
titre: title,
icone: icônes,
description: description || résumé,
}
})
.filter(falsy)
const formatSimulationDataToAlgolia = (simulations: MetadataSrc) =>
Object.entries(simulations).map(([id, simulation]) => ({
...simulation,
objectID: id,
title:
('title' in simulation && simulation.title) ||
simulation.shortName ||
simulation.meta.title,
tooltip: ('tooltip' in simulation && simulation.tooltip) || '',
description: simulation.meta?.description,
}))
try {
console.log('Algolia update START')
console.log('Clearing: rules')
await rulesIndex.clearObjects().wait()
console.log('Configure index: rules')
await rulesIndex
.setSettings({
// Parameters are documented on Algolia website https://www.algolia.com/doc/api-reference/api-parameters/
minWordSizefor1Typo: 4,
minWordSizefor2Typos: 8,
hitsPerPage: 20,
maxValuesPerFacet: 100,
attributesToIndex: ['unordered(ruleName)', 'unordered(namespace)'],
attributesToHighlight: ['ruleName', 'namespace'],
paginationLimitedTo: 1000,
exactOnSingleWordQuery: 'attribute',
ranking: [
'typo',
'geo',
'words',
'filters',
'proximity',
'attribute',
'exact',
'custom',
],
customRanking: ['asc(pathDepth)'],
separatorsToIndex: '',
removeWordsIfNoResults: 'none',
queryType: 'prefixLast',
highlightPreTag: '<em>',
highlightPostTag: '</em>',
snippetEllipsisText: '',
alternativesAsExact: ['ignorePlurals', 'singleWordSynonym'],
})
.wait()
console.log('Uploading: rules')
await rulesIndex.saveObjects(formatRulesToAlgolia(rules)).wait()
console.log('Clearing: simulateurs')
await simulateursIndex.clearObjects().wait()
console.log('Configure index: simulateurs')
await simulateursIndex
.setSettings({
// Parameters are documented on Algolia website https://www.algolia.com/doc/api-reference/api-parameters/
minWordSizefor1Typo: 4,
minWordSizefor2Typos: 8,
hitsPerPage: 20,
maxValuesPerFacet: 100,
attributesToIndex: [
'unordered(title)',
'unordered(tooltip)',
'unordered(description)',
],
attributesToHighlight: ['title'],
paginationLimitedTo: 1000,
exactOnSingleWordQuery: 'attribute',
ranking: [
'typo',
'geo',
'words',
'filters',
'proximity',
'attribute',
'exact',
'custom',
],
separatorsToIndex: '',
removeWordsIfNoResults: 'none',
queryType: 'prefixLast',
highlightPreTag: '<em>',
highlightPostTag: '</em>',
snippetEllipsisText: '',
alternativesAsExact: ['ignorePlurals', 'singleWordSynonym'],
})
.wait()
console.log('Uploading: simulateurs')
await simulateursIndex
.saveObjects(
formatSimulationDataToAlgolia(getSimulationData((_, text) => text))
)
.wait()
console.log('Algolia update DONE')
} catch (e) {
console.log(JSON.stringify(e, null, 2))
}

View File

@ -13,7 +13,7 @@ export const useShowFeedback = () => {
simulators['comparaison-statuts'],
simulators['demande-mobilité'],
]
.map((s) => s.path)
.map((s) => s.path as string)
.includes(currentPath)
) {
return true

View File

@ -4,7 +4,8 @@ import { SitePathsContext } from 'Components/utils/SitePathsContext'
import { SmallCard } from 'DesignSystem/card'
import InfoBulle from 'DesignSystem/InfoBulle'
import { H3 } from 'DesignSystem/typography/heading'
import { SimulatorData } from 'pages/Simulateurs/metadata'
import { ExtractFromSimuData } from 'pages/Simulateurs/metadata'
import { MetadataSrc } from 'pages/Simulateurs/metadata-src'
import { path } from 'ramda'
import { useContext } from 'react'
import { Trans } from 'react-i18next'
@ -15,7 +16,7 @@ import { Highlight } from './Hightlight'
type AlgoliaSimulatorHit = Hit<{
icône: string
title: string
pathId: string
pathId: MetadataSrc[keyof MetadataSrc]['pathId']
}>
type SimulatorHitsProps = {
@ -26,7 +27,9 @@ const SimulateurCardHit = ({
hit,
path,
tooltip,
}: Pick<SimulatorData[keyof SimulatorData], 'path' | 'tooltip'> & {
}: {
path: ExtractFromSimuData<'path'>
tooltip?: ExtractFromSimuData<'tooltip'>
hit: AlgoliaSimulatorHit
}) => {
return (
@ -65,7 +68,12 @@ export const SimulatorHits = connectHits<
<Grid item key={hit.objectID} xs={12} lg={6}>
<SimulateurCardHit
hit={hit}
path={path(hit.pathId.split('.'), sitePaths)}
path={
path(
hit.pathId.split('.'),
sitePaths
) as ExtractFromSimuData<'path'>
}
/>
</Grid>
)

View File

@ -9,11 +9,13 @@ export default function IntegrationTest() {
const integrableModuleNames = useMemo(
() =>
Object.values(simulators)
.map((s) => s.iframePath)
.filter(Boolean),
.map((s) => 'iframePath' in s && s.iframePath)
.filter(((el) => Boolean(el)) as <T>(x: T | false) => x is T),
[simulators]
)
const [currentModule, setCurrentModule] = useState(integrableModuleNames[0])
const [currentModule, setCurrentModule] = useState<string>(
integrableModuleNames[0]
)
const [color, setColor] = useState('#005aa1')
const [version, setVersion] = useState(0)
const domNode = useRef<HTMLDivElement>(null)

View File

@ -30,21 +30,25 @@ export default function Iframes() {
<ThemeColorsProvider>
<Switch>
{Object.values(simulators)
.filter(({ iframePath }) => !!iframePath)
.map((s) => (
<Route
key={s.iframePath}
path={`/iframes/${s.iframePath}`}
render={() => (
<>
<Helmet>
<link rel="canonical" href={s.path} />
</Helmet>
<SimulateurPage {...s} />
</>
)}
/>
))}
.filter((el) => !!('iframePath' in el && el.iframePath))
.map(
(s) =>
'iframePath' in s &&
s.iframePath && (
<Route
key={s.iframePath}
path={`/iframes/${s.iframePath}`}
render={() => (
<>
<Helmet>
<link rel="canonical" href={s.path} />
</Helmet>
<SimulateurPage {...s} />
</>
)}
/>
)
)}
</Switch>
<IframeFooter />
</ThemeColorsProvider>

View File

@ -18,7 +18,7 @@ import { ThemeProvider } from 'styled-components'
import { TrackPage } from '../../ATInternetTracking'
import Meta from '../../components/utils/Meta'
import simulatorSvg from './images/illustration-simulateur.svg'
import useSimulatorsData, { SimulatorData } from './metadata'
import useSimulatorsData, { ExtractFromSimuData } from './metadata'
export default function Simulateurs() {
const { t } = useTranslation()
@ -135,10 +135,13 @@ export function SimulateurCard({
iframePath,
fromGérer = false,
icône,
}: Pick<
SimulatorData[keyof SimulatorData],
'shortName' | 'meta' | 'path' | 'tooltip' | 'iframePath' | 'icône'
> & {
}: {
shortName: ExtractFromSimuData<'shortName'>
meta: ExtractFromSimuData<'meta'>
path: ExtractFromSimuData<'path'>
tooltip?: ExtractFromSimuData<'tooltip'>
iframePath?: ExtractFromSimuData<'iframePath'>
icône: ExtractFromSimuData<'icône'>
small?: boolean
fromGérer?: boolean
}) {

View File

@ -9,12 +9,12 @@ import { FAQAutoEntrepreneurArticle } from '../../../pages/Creer/CreationCheckli
import { GuideURSSAFCard } from '../cards/GuideURSSAFCard'
import { IframeIntegrationCard } from '../cards/IframeIntegrationCard'
import { SimulatorRessourceCard } from '../cards/SimulatorRessourceCard'
import { SimulatorData } from '../metadata'
import { ExtractFromSimuData } from '../metadata'
type NextStepsProps = Pick<
SimulatorData[keyof SimulatorData],
'iframePath' | 'nextSteps'
>
interface NextStepsProps {
iframePath: ExtractFromSimuData<'iframePath'>
nextSteps: ExtractFromSimuData<'nextSteps'>
}
export function NextSteps({ iframePath, nextSteps }: NextStepsProps) {
const sitePaths = useContext(SitePathsContext)

View File

@ -11,10 +11,28 @@ import { ComponentPropsWithoutRef, useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation } from 'react-router-dom'
import { TrackChapter } from '../../../ATInternetTracking'
import { CurrentSimulatorDataProvider, SimulatorData } from '../metadata'
import { CurrentSimulatorDataProvider, ExtractFromSimuData } from '../metadata'
import { NextSteps } from './NextSteps'
export default function PageData(props: SimulatorData[keyof SimulatorData]) {
export interface PageDataProps {
meta: ExtractFromSimuData<'meta'>
config?: ExtractFromSimuData<'config'>
tracking: ExtractFromSimuData<'tracking'>
tooltip?: ExtractFromSimuData<'tooltip'>
description?: ExtractFromSimuData<'description'>
iframePath?: ExtractFromSimuData<'iframePath'>
seoExplanations?: ExtractFromSimuData<'seoExplanations'>
nextSteps?: ExtractFromSimuData<'nextSteps'>
path: ExtractFromSimuData<'path'>
title?: ExtractFromSimuData<'title'>
private?: ExtractFromSimuData<'private'>
component: ExtractFromSimuData<'component'>
icône: ExtractFromSimuData<'icône'>
pathId: ExtractFromSimuData<'pathId'>
shortName: ExtractFromSimuData<'shortName'>
}
export default function PageData(props: PageDataProps) {
const {
meta,
config,

View File

@ -1,10 +1,12 @@
import { TFunction } from 'react-i18next'
/**
* Contient l'intégralité des données concernant les différents simulateurs
* sans dépendance qui compliquerait leur import dans le script de mise à jour
* des données pour Algolia.
*/
export default ({ t = (_, text) => text } = {}) => {
return {
const metadataSrc = (t: TFunction<'translation', string>) => {
const data = {
salarié: {
tracking: 'salarie',
icône: '🤝',
@ -359,7 +361,7 @@ export default ({ t = (_, text) => text } = {}) => {
'Déclaration de revenus indépendant : calcul du montant des cotisations'
),
},
pathId: 'simulateurs.déclarationIndépendant',
pathId: 'gérer.déclarationIndépendant',
shortName: t(
'pages.gérer.aide-déclaration-indépendant.shortname',
'Aide à la déclaration de revenu'
@ -638,7 +640,7 @@ export default ({ t = (_, text) => text } = {}) => {
),
color: '#11965f',
},
pathId: 'simulateurs.aide-embauche',
pathId: 'simulateurs.aides-embauche',
iframePath: 'aides-embauche',
shortName: t(
'pages.simulateurs.aides-embauche.meta.title',
@ -696,5 +698,10 @@ export default ({ t = (_, text) => text } = {}) => {
nextSteps: ['salarié', 'is', 'comparaison-statuts'],
},
}
} as const
return data
}
export type MetadataSrc = ReturnType<typeof metadataSrc>
export default metadataSrc

View File

@ -5,9 +5,9 @@ import { H2 } from 'DesignSystem/typography/heading'
import { Link } from 'DesignSystem/typography/link'
import { Body } from 'DesignSystem/typography/paragraphs'
import React, { createContext, useContext, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { TFunction, Trans, useTranslation } from 'react-i18next'
import { SimulationConfig } from 'Reducers/rootReducer'
import { constructLocalizedSitePath } from '../../sitePaths'
import { constructLocalizedSitePath, SitePathsType } from '../../sitePaths'
import Créer from '../Creer/Home'
import AideDéclarationIndépendant from '../Gerer/AideDéclarationIndépendant'
import FormulaireMobilitéIndépendant from '../Gerer/DemandeMobilite'
@ -43,91 +43,23 @@ import SalariéSimulation from './Salarié'
import { SASUSimulation } from './SASU'
import SchemeComparaisonPage from './SchemeComparaison'
const simulateurs = [
'salarié',
'auto-entrepreneur',
'indépendant',
'eirl',
'eurl',
'sasu',
'chômage-partiel',
'artiste-auteur',
'comparaison-statuts',
'entreprise-individuelle',
'économie-collaborative',
'aide-déclaration-indépendant',
'demande-mobilité',
'profession-libérale',
'médecin',
'choix-statut',
'pharmacien',
'chirurgien-dentiste',
'sage-femme',
'auxiliaire-médical',
'avocat',
'expert-comptable',
'pamc',
'is',
'aides-embauche',
'dividendes',
] as const
interface SimulatorsDataParams {
t?: TFunction<'translation', string>
sitePaths?: SitePathsType
language?: string
}
export type SimulatorId = typeof simulateurs[number]
export type PureSimulatorData = Record<
SimulatorId,
{
meta: {
title: string
description: string
ogTitle?: string
ogDescription?: string
ogImage?: string
color?: string
}
tracking:
| {
chapter2?: string
chapter3?: string
chapter1?: string
}
| string
icône?: string
shortName: string
path?: string
tooltip?: string
iframePath?: string
title?: string
description?: React.ReactNode
config?: SimulationConfig
seoExplanations?: React.ReactNode
private?: boolean
pathId: string
}
>
export type SimulatorData = PureSimulatorData &
Record<
SimulatorId,
{
path?: string
description?: React.ReactNode
config?: SimulationConfig
seoExplanations?: React.ReactNode
nextSteps?: Array<SimulatorId>
component: () => JSX.Element
}
>
export function getSimulatorsData({
t = (_: unknown, text: string) => text,
function getSimulatorsData({
t = (_, text) => text,
sitePaths = constructLocalizedSitePath('fr'),
language = 'fr',
} = {}): SimulatorData {
const pureSimulatorsData: PureSimulatorData = getData({ t })
}: SimulatorsDataParams = {}) {
const pureSimulatorsData = getData(t)
return {
salarié: {
...pureSimulatorsData['salarié'],
config: salariéConfig,
config: salariéConfig as SimulationConfig,
component: SalariéSimulation,
meta: {
...pureSimulatorsData['salarié'].meta,
@ -210,7 +142,7 @@ export function getSimulatorsData({
},
'entreprise-individuelle': {
...pureSimulatorsData['entreprise-individuelle'],
config: entrepriseIndividuelleConfig,
config: entrepriseIndividuelleConfig as SimulationConfig,
meta: {
...pureSimulatorsData['entreprise-individuelle']?.meta,
ogImage: AutoEntrepreneurPreview,
@ -283,7 +215,7 @@ export function getSimulatorsData({
},
eirl: {
...pureSimulatorsData['eirl'],
config: indépendantConfig,
config: indépendantConfig as SimulationConfig,
meta: {
...pureSimulatorsData['eirl']?.meta,
ogImage: AutoEntrepreneurPreview,
@ -294,7 +226,7 @@ export function getSimulatorsData({
},
sasu: {
...pureSimulatorsData['sasu'],
config: sasuConfig,
config: sasuConfig as SimulationConfig,
meta: {
...pureSimulatorsData['sasu']?.meta,
ogImage: RémunérationSASUPreview,
@ -345,7 +277,7 @@ export function getSimulatorsData({
},
eurl: {
...pureSimulatorsData['eurl'],
config: eurlConfig,
config: eurlConfig as SimulationConfig,
meta: {
...pureSimulatorsData['eurl']?.meta,
ogImage: RémunérationSASUPreview,
@ -356,7 +288,7 @@ export function getSimulatorsData({
'auto-entrepreneur': {
...pureSimulatorsData['auto-entrepreneur'],
tracking: 'auto_entrepreneur',
config: autoEntrepreneurConfig,
config: autoEntrepreneurConfig as SimulationConfig,
meta: {
...pureSimulatorsData['auto-entrepreneur']?.meta,
ogImage: AutoEntrepreneurPreview,
@ -430,7 +362,7 @@ export function getSimulatorsData({
},
indépendant: {
...pureSimulatorsData['indépendant'],
config: indépendantConfig,
config: indépendantConfig as SimulationConfig,
path: sitePaths.simulateurs.indépendant,
component: IndépendantSimulation,
},
@ -442,7 +374,7 @@ export function getSimulatorsData({
'chômage-partiel': {
...pureSimulatorsData['chômage-partiel'],
component: ChômagePartielComponent,
config: chômageParielConfig,
config: chômageParielConfig as SimulationConfig,
path: sitePaths.simulateurs['chômage-partiel'],
meta: {
...pureSimulatorsData['chômage-partiel']?.meta,
@ -591,14 +523,14 @@ export function getSimulatorsData({
},
'profession-libérale': {
...pureSimulatorsData['profession-libérale'],
config: professionLibéraleConfig,
config: professionLibéraleConfig as SimulationConfig,
path: sitePaths.simulateurs['profession-libérale'].index,
component: IndépendantPLSimulation,
},
pamc: {
...pureSimulatorsData['pamc'],
path: sitePaths.simulateurs.pamc,
config: professionLibéraleConfig,
config: professionLibéraleConfig as SimulationConfig,
component: PAMCHome,
},
'aides-embauche': {
@ -674,7 +606,7 @@ export function getSimulatorsData({
...pureSimulatorsData['dividendes'],
path: sitePaths.simulateurs.dividendes,
component: DividendesSimulation,
config: dividendesConfig,
config: dividendesConfig as SimulationConfig,
seoExplanations: (
<Trans i18nKey="pages.simulateurs.dividendes.seo">
<H2>Les dividendes et distributions</H2>
@ -724,9 +656,25 @@ export function getSimulatorsData({
</Trans>
),
},
}
} as const
}
export type SimulatorData = ReturnType<typeof getSimulatorsData>
/**
* Extract type if U extends T
* Else return an object with value undefined
*/
type ExtractOrUndefined<T, U> = T extends U ? T : Record<keyof U, undefined>
/**
* Extract type from a key of SimulatorData
*/
export type ExtractFromSimuData<T extends string> = ExtractOrUndefined<
SimulatorData[keyof SimulatorData],
Record<T, unknown>
>[T]
export default function useSimulatorsData(): SimulatorData {
const { t, i18n } = useTranslation()
const sitePaths = useContext(SitePathsContext)
@ -737,18 +685,19 @@ export default function useSimulatorsData(): SimulatorData {
)
}
export const CurrentSimulatorDataContext = createContext<
SimulatorData[keyof SimulatorData] | null
>(null)
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U
export const CurrentSimulatorDataContext = createContext<Overwrite<
SimulatorData[keyof SimulatorData],
{ path: ExtractFromSimuData<'path'> }
> | null>(null)
export const CurrentSimulatorDataProvider = CurrentSimulatorDataContext.Provider
professionLibéraleConfig as SimulationConfig
const configFromPLMetier = (metier: string): SimulationConfig => ({
...professionLibéraleConfig,
...(professionLibéraleConfig as SimulationConfig),
situation: {
...professionLibéraleConfig.situation,
...(professionLibéraleConfig as SimulationConfig).situation,
'entreprise . activité . libérale réglementée': 'oui',
'dirigeant . indépendant . PL . métier': `'${metier}'`,
},
@ -762,16 +711,16 @@ const sageFemmeConfig = configFromPLMetier('santé . sage-femme')
const avocatConfig = configFromPLMetier('avocat')
const expertComptableConfig = configFromPLMetier('expert-comptable')
const eurlConfig = {
...indépendantConfig,
...(indépendantConfig as SimulationConfig),
situation: {
...indépendantConfig.situation,
...(indépendantConfig as SimulationConfig).situation,
'entreprise . imposition': "'IS'",
},
}
const entrepriseIndividuelleConfig = {
...indépendantConfig,
...(indépendantConfig as SimulationConfig),
situation: {
...indépendantConfig.situation,
...(indépendantConfig as SimulationConfig).situation,
'entreprise . imposition': "'IR'",
},
}

View File

@ -293,7 +293,7 @@ export default function Stats() {
}
function getChapter2(s: SimulatorData[keyof SimulatorData]): Chapter2 | '' {
if (s.iframePath === 'pamc') {
if ('iframePath' in s && s.iframePath === 'pamc') {
return 'PAM'
}
if (!s.tracking) {

View File

@ -22,7 +22,7 @@ import Créer from '../Creer'
import Documentation from '../Documentation'
import Iframes from '../Iframes'
import Simulateurs from '../Simulateurs'
import useSimulatorsData from '../Simulateurs/metadata'
import useSimulatorsData, { SimulatorData } from '../Simulateurs/metadata'
import './iframe.css'
import apecLogo from './images/apec.png'
import cciLogo from './images/cci.png'
@ -31,13 +31,22 @@ import poleEmploiLogo from './images/pole-emploi.png'
const LazyColorPicker = lazy(() => import('../Dev/ColorPicker'))
const checkIframe = (obj: SimulatorData[keyof SimulatorData]) =>
'iframePath' in obj && obj.iframePath && !('private' in obj && obj.private)
const getFromSimu = <S extends SimulatorData, T extends string>(
obj: S,
key: T
) =>
key in obj &&
obj[key as keyof SimulatorData] &&
checkIframe(obj[key as keyof SimulatorData])
? obj[key as keyof SimulatorData]
: undefined
function IntegrationCustomizer() {
const { search } = useLocation()
const simulators = Object.fromEntries(
Object.entries(useSimulatorsData()).filter(
([, s]) => s.iframePath && !s.private
)
)
const simulatorsData = useSimulatorsData()
const sitePaths = useContext(SitePathsContext)
const history = useHistory()
@ -45,7 +54,9 @@ function IntegrationCustomizer() {
new URLSearchParams(search ?? '').get('module') ?? ''
const [currentModule, setCurrentModule] = useState(
simulators[defaultModuleFromUrl] ? defaultModuleFromUrl : 'salarié'
getFromSimu(simulatorsData, defaultModuleFromUrl)
? defaultModuleFromUrl
: 'salarié'
)
useEffect(() => {
@ -54,6 +65,14 @@ function IntegrationCustomizer() {
const [color, setColor] = useState<string | undefined>()
const currentSimulator = getFromSimu(simulatorsData, currentModule)
const currentIframePath =
(currentSimulator &&
'iframePath' in currentSimulator &&
currentSimulator.iframePath) ||
''
return (
<>
<H2>
@ -70,17 +89,25 @@ function IntegrationCustomizer() {
onSelectionChange={(val) => setCurrentModule(String(val))}
selectedKey={currentModule}
>
{Object.entries(simulators).map(([module, s]) => (
<Item key={module} textValue={s.shortName ?? s.title}>
{s.icône && (
<>
<Emoji emoji={s.icône} />
&nbsp;
</>
)}
{s.shortName ?? s.title}
</Item>
))}
{Object.entries(simulatorsData)
.map(
([module, s]) =>
getFromSimu(simulatorsData, module) && (
<Item
key={module}
textValue={s.shortName ?? ('title' in s ? s.title : '')}
>
{s.icône && (
<>
<Emoji emoji={s.icône} />
&nbsp;
</>
)}
{s.shortName ?? ('title' in s ? s.title : '')}
</Item>
)
)
.filter(((el) => Boolean(el)) as <T>(x: T | undefined) => x is T)}
</Select>
<H3>
@ -103,10 +130,7 @@ function IntegrationCustomizer() {
Voici le code à copier-coller sur votre site&nbsp;:
</Trans>
</Body>
<IntegrationCode
color={color}
module={simulators[currentModule].iframePath}
/>
<IntegrationCode color={color} module={currentIframePath} />
</Grid>
<Grid item lg={8}>
<H3>
@ -115,9 +139,7 @@ function IntegrationCustomizer() {
<PrevisualisationContainer columns={10}>
<MemoryRouter
key={currentModule}
initialEntries={[
`/iframes/${simulators[currentModule].iframePath ?? ''}`,
]}
initialEntries={[`/iframes/${currentIframePath}`]}
>
<ThemeColorsProvider
color={color == null ? color : hexToHSL(color)}

View File

@ -1,3 +1,4 @@
import { MetadataSrc } from 'pages/Simulateurs/metadata-src'
import { map, reduce, toPairs, zipObj } from 'ramda'
import { LegalStatus } from 'Selectors/companyStatusSelectors'
@ -14,15 +15,13 @@ export const LANDING_LEGAL_STATUS_LIST: Array<LegalStatus> = [
]
type LocalizedPath = string
type PathFactory = (...args: Array<any>) => LocalizedPath
type SitePathObject<T> = {
index: LocalizedPath
} & {
[key in keyof T]: string | PathFactory | SitePathObject<T[key]>
[key in keyof T]: string | SitePathObject<T[key]>
}
const sitePathsFr = {
const rawSitePathsFr = {
index: '',
créer: {
index: '/créer',
@ -92,10 +91,10 @@ const sitePathsFr = {
},
} as const
const sitePathsEn = {
...sitePathsFr,
const rawSitePathsEn = {
...rawSitePathsFr,
créer: {
...sitePathsFr.créer,
...rawSitePathsFr.créer,
index: '/create',
après: '/after-registration',
guideStatut: {
@ -150,30 +149,58 @@ const sitePathsEn = {
accessibilité: '/accessibility',
integration: {
...sitePathsFr.integration,
...rawSitePathsFr.integration,
index: '/integration',
library: '/library',
},
} as const
/**
* Le but des types suivants est d'obtenir un typage statique des chaînes de caractères
* comme "simulateurs.auto-entrepreneur" utilisés comme identifiants des routes (via les pathId dans metadat-src.ts).
* Cela permet de ne pas avoir de faute dans les clés comme 'aide-embauche' au lieu de 'aides-embauche'
*/
// Transfrom string type like PathToType<'simulateurs.auto-entrepreneur', number>
// into { simulateurs : { auto-entrepreneur: number }}
type PathToType<T extends string, W> = T extends `${infer U}.${infer V}`
? { [key in U]: V extends string ? PathToType<V, W> : never }
: { [key in T]: W }
// Transform type A | B into A & B
type UnionToIntersection<T> = (
T extends unknown ? (x: T) => void : never
) extends (x: infer R) => void
? R
: never
// Union of pathId
type PathIds = MetadataSrc[keyof MetadataSrc]['pathId']
type RequiredPath = Required<UnionToIntersection<PathToType<PathIds, string>>>
// If there is a type error here, check rawSitePathsFr object matches the metadata-src.ts pathId
const checkedSitePathsFr: RequiredPath & typeof rawSitePathsFr = rawSitePathsFr
// If there is a type error here, check rawSitePathsEn object matches the metadata-src.ts pathId
const checkedSitePathsEn: RequiredPath & typeof rawSitePathsEn = rawSitePathsEn
function constructSitePaths<T extends SitePathObject<T>>(
root: string,
{ index, ...sitePaths }: T
): T {
return {
index: root + index,
...map((value: LocalizedPath | PathFactory | SitePathObject<string>) =>
...map((value: LocalizedPath | SitePathObject<string>) =>
typeof value === 'string'
? root + index + value
: typeof value === 'function'
? (...args: Array<unknown>) => root + index + String(value(...args))
: constructSitePaths(root + index, value as any)
)(sitePaths as any),
} as any
}
export const constructLocalizedSitePath = (language: 'en' | 'fr') => {
const sitePaths = language === 'fr' ? sitePathsFr : sitePathsEn
const sitePaths = language === 'fr' ? checkedSitePathsFr : checkedSitePathsEn
return constructSitePaths('', sitePaths)
}

View File

@ -31,6 +31,7 @@
},
"include": [
"source",
"scripts",
"test/**/*.ts",
"vite.config.ts",
"vite-iframe-script.config.ts"

View File

@ -364,6 +364,18 @@
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==
"@cspotcode/source-map-support@0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5"
integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==
dependencies:
"@cspotcode/source-map-consumer" "0.8.0"
"@cypress/request@^2.88.5":
version "2.88.10"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
@ -1591,6 +1603,26 @@
"@sentry/types" "6.17.2"
tslib "^1.9.3"
"@tsconfig/node10@^1.0.7":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==
"@tsconfig/node12@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c"
integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==
"@tsconfig/node14@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
"@tsconfig/node16@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e"
integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==
"@types/chai-subset@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94"
@ -1982,7 +2014,12 @@ acorn-jsx@^5.3.1:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.7.0:
acorn-walk@^8.1.1:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^8.4.1, acorn@^8.7.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
@ -2088,6 +2125,11 @@ arch@^2.2.0:
resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11"
integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@ -2667,6 +2709,11 @@ cosmiconfig@^6.0.0:
path-type "^4.0.0"
yaml "^1.7.2"
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-fetch@^3.0.4:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
@ -2952,6 +2999,11 @@ dfa@^1.2.0:
resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657"
integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@ -4636,6 +4688,11 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.4"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
markdown-to-jsx@^7.1.5:
version "7.1.6"
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.1.6.tgz#421487df2a66fe4231d94db653a34da033691e62"
@ -6235,6 +6292,25 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
ts-node@^10.5.0:
version "10.5.0"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9"
integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==
dependencies:
"@cspotcode/source-map-support" "0.7.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.0"
yn "3.1.1"
ts-toolbelt@^6.3.3:
version "6.15.5"
resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz#cb3b43ed725cb63644782c64fbcad7d8f28c0a83"
@ -6392,6 +6468,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache-lib@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8"
integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
@ -6646,3 +6727,8 @@ yauzl@^2.10.0:
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==