diff --git a/package.json b/package.json index f51783537..979c50f20 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,7 @@ "not ie < 11" ], "dependencies": { - "@babel/polyfill": "^7.4.0", "@babel/runtime": "^7.3.4", - "@monaco-editor/react": "^3.1.1", "@rehooks/local-storage": "^2.1.1", "classnames": "^2.2.5", "color-convert": "^1.9.2", @@ -31,7 +29,6 @@ "i18next": "^18.0.1", "iframe-resizer": "^4.1.1", "js-yaml": "^3.13.1", - "monaco-editor": "^0.20.0", "moo": "^0.5.0", "nearley": "^2.19.0", "puppeteer": "^2.1.1", @@ -48,6 +45,7 @@ "react-i18next": "^11.0.0", "react-loading-skeleton": "^1.1.2", "react-markdown": "^4.1.0", + "react-monaco-editor": "^0.36.0", "react-number-format": "^4.3.1", "react-redux": "^7.0.3", "react-router": "^5.1.1", @@ -57,8 +55,6 @@ "react-syntax-highlighter": "^10.1.1", "react-to-print": "^2.5.1", "react-transition-group": "^2.2.1", - "react-virtualized": "^9.20.0", - "react-virtualized-select": "^3.1.3", "reduce-reducers": "^1.0.4", "redux": "^4.0.4", "redux-thunk": "^2.3.0", @@ -67,9 +63,6 @@ "swr": "^0.1.16", "whatwg-fetch": "^3.0.0" }, - "peerDependencies": { - "monaco-editor": "^0.20.0" - }, "scripts": { "prepare": "node source/scripts/prepare.js", "compile": "yarn run webpack --config source/webpack.prod.js && yarn run webpack --config source/webpack.prod.legacyBrowser.js", @@ -112,6 +105,7 @@ "@babel/core": "^7.6.4", "@babel/plugin-proposal-class-properties": "^7.1.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.4.4", + "monaco-editor-webpack-plugin": "^1.9.0", "@babel/plugin-proposal-object-rest-spread": "^7.0.0", "@babel/plugin-proposal-optional-chaining": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.0.0", diff --git a/source/engine/mecanisms.js b/source/engine/mecanisms.js index eb0b027d9..ad2563cef 100644 --- a/source/engine/mecanisms.js +++ b/source/engine/mecanisms.js @@ -17,7 +17,6 @@ import { toPairs } from 'ramda' import React from 'react' -import 'react-virtualized/styles.css' import { typeWarning } from './error' import { collectNodeMissing, @@ -480,24 +479,24 @@ export let mecanismProduct = (recurse, k, v) => { } let mult = (base, rate, facteur, plafond) => Math.min(base, plafond === false ? Infinity : plafond) * rate * facteur + + const nodeValue = [taux, assiette, facteur].some(n => n.nodeValue === false) + ? false + : [taux, assiette, facteur].some(n => n.nodeValue === 0) + ? 0 + : [taux, assiette, facteur].some(n => n.nodeValue === null) + ? null + : mult( + assiette.nodeValue, + taux.nodeValue, + facteur.nodeValue, + plafond.nodeValue + ) + const unit = inferUnit( '*', [assiette, taux, facteur].map(el => el.unit) ) - const nodeValue = - taux.nodeValue === 0 || - taux.nodeValue === false || - assiette.nodeValue === 0 || - facteur.nodeValue === 0 - ? 0 - : [taux, assiette, facteur, plafond].some(n => n.nodeValue === null) - ? null - : mult( - assiette.nodeValue, - taux.nodeValue, - facteur.nodeValue, - plafond.nodeValue - ) return { nodeValue, diff --git a/source/engine/mecanisms.yaml b/source/engine/mecanisms.yaml index b653cb3c9..97161e0f2 100644 --- a/source/engine/mecanisms.yaml +++ b/source/engine/mecanisms.yaml @@ -206,12 +206,50 @@ régularisation: variables numérique mensuelle cumulée. Ce mécanisme spécifique est utilisé pour le calcul des cotisations - mensuelles. + mensuelles, afin de "lisser" un plafond ou un calcul sur plusieurs mois. + + La régularisation progressive s'opère le long d'une année civile complète. + + ## Explication du calcul + + Pour chaque mois, on évalue la valeur à régulariser en faisant le cumul des + éléments nécessaires au calcul sur la période écoulée depuis le premier + jour de l’année. + + Par exemple, pour la cotisation suivante : + ```yaml + cotisation: + formule: + produit: + assiette: brut + plafond: 2000 €/mois + taux: 10% + ``` + Avec un brut de 1000 € en janvier et 3500 € en février et 1000€ en mars, le cumul sera le + suivant : + + ``` + brut plafond cotisation + JAN 1000 2000 1000 * 10% = 100 € + FEV 4500 4000 4000 * 10% = 400 € + MAR 5500 6000 5500 * 10% = 550 € + ``` + + On regarde ensuite le cumul des valeurs déjà versée les mois précédent, pour + ne garder que la différence entre les deux montant. Dans notre exemple, on + abouti aux valeurs suivantes : + + ``` + cotisation cumul cotis régularisée + JAN 100 € 100 € + FEV 400 € 400 € - 100 € = 300 € + MAR 550 € 550 € - 300 € = 150 € + ``` arguments: règle: règle à régulariser valeurs cumulées: - - liste de variables cumulée mensuellement pour calculer la régularisation. Doit être + - liste de variables cumulée mensuellement pour calculer la régularisation. Doivent être numérique, et avoir une unité `/mois` exemples: @@ -219,10 +257,9 @@ régularisation: brut: formule: somme: - - 2000 €/mois | du 01/01/2020 | au 31/05/2020 - - 4000 €/mois | du 01/06/2020 | au 31/12/2020 - plafond: - formule: 3000 €/mois + - 1000 €/mois | du 01/01/2020 | au 31/01/2020 + - 3500 €/mois | du 01/02/2020 | au 29/02/2020 + - 1000 €/mois | du 01/03/2020 | au 31/03/2020 cotisation: formule: @@ -230,20 +267,55 @@ régularisation: règle: produit: assiette: brut - plafond: plafond + plafond [ref]: 2000 €/mois taux: 10% valeurs cumulées: - brut - plafond - cotisation en mai: - formule: cotisation | du 01/05/2020 | au 31/05/2020 + valeur cumulée à partir d'une date: >- + SMIC: 1521.22 €/mois - cotisation en juin: - formule: cotisation | du 01/06/2020 | au 30/06/2020 + primes: + formule: + somme: + - 2470 €/mois | du 01/01/2019 | au 31/01/2019 + - 1470 €/mois | du 01/05/2019 | au 31/05/2019 - cotisation en novembre: - formule: cotisation | du 01/11/2020 | au 30/11/2020 + rémunération: + formule: + somme: + - 1530 €/mois | du 01/01/2019 | au 31/12/2019 + - primes + + réduction sur AC au 1er janvier: + formule: non + + réduction générale chômage . coefficient: + formule: + arrondi: + valeur: (0.0405 / 0.60 ) * (1.60 * SMIC / rémunération - 1) + décimales: 4 + + réduction générale chômage . réduction sans régularisation: + formule: + encadrement: + plancher: 0 + valeur: + arrondi: + valeur: + multiplication: + assiette: rémunération + taux: coefficient + décimales: 2 + + réduction générale chômage: + formule: + régularisation: + règle: réduction sans régularisation + valeurs cumulées: + - 'SMIC | à partir du 01/10/2019' + - 'rémunération | à partir du 01/10/2019' recalcul: description: >- diff --git a/source/engine/mecanisms/arrondi.tsx b/source/engine/mecanisms/arrondi.tsx index 27a79a2fb..24082ef59 100644 --- a/source/engine/mecanisms/arrondi.tsx +++ b/source/engine/mecanisms/arrondi.tsx @@ -5,6 +5,7 @@ import { mergeAllMissing } from 'Engine/evaluation' import { Node } from 'Engine/mecanismViews/common' +import { mapTemporal, pureTemporal, temporalAverage } from 'Engine/temporal' import { EvaluatedRule } from 'Engine/types' import { has } from 'ramda' import React from 'react' @@ -64,15 +65,20 @@ function evaluate( const value = evaluateAttribute(node.explanation.value) const decimals = evaluateAttribute(node.explanation.decimals) - const nodeValue = - typeof value.nodeValue === 'number' - ? roundWithPrecision(value.nodeValue, decimals.nodeValue) - : value.nodeValue + const temporalValue = mapTemporal( + (val: number | false | null) => + typeof val === 'number' + ? roundWithPrecision(val, decimals.nodeValue) + : val, + value.temporalValue ?? pureTemporal(value.nodeValue) + ) + const nodeValue = temporalAverage(temporalValue, value.unit) return { ...node, unit: value.unit, nodeValue, + ...(temporalValue.length > 1 && { temporalValue }), missingVariables: mergeAllMissing([value, decimals]), explanation: { value, decimals } } diff --git a/source/engine/mecanisms/encadrement.tsx b/source/engine/mecanisms/encadrement.tsx index f161a846d..ef10b143b 100644 --- a/source/engine/mecanisms/encadrement.tsx +++ b/source/engine/mecanisms/encadrement.tsx @@ -1,9 +1,8 @@ import { typeWarning } from 'Engine/error' import { defaultNode, - evaluateNode, + evaluateObject, makeJsx, - mergeAllMissing, parseObject } from 'Engine/evaluation' import { Node } from 'Engine/mecanismViews/common' @@ -60,48 +59,38 @@ const objectShape = { plancher: defaultNode(-Infinity) } -const evaluate = (cache, situation, parsedRules, node) => { - const evaluateAttribute = evaluateNode.bind( - null, - cache, - situation, - parsedRules - ) - const valeur = evaluateAttribute(node.explanation.valeur) - let plafond = evaluateAttribute(node.explanation.plafond) - if (plafond.nodeValue === false || plafond.nodeValue === null) { - plafond = objectShape.plafond - } - let plancher = evaluateAttribute(node.explanation.plancher) - if (valeur.unit) { - try { - plafond = convertNodeToUnit(valeur.unit, plafond) - plancher = convertNodeToUnit(valeur.unit, plancher) - } catch (e) { - typeWarning( - cache._meta.contextRule, - "Le plafond / plancher de l'encadrement a une unité incompatible avec celle de la valeur à encadrer", - e - ) +const evaluate = evaluateObject( + objectShape, + ({ valeur, plafond, plancher }, cache) => { + if (plafond.nodeValue === false || plafond.nodeValue === null) { + plafond = objectShape.plafond } - } - const nodeValue = Math.max( - plancher.nodeValue, - Math.min(plafond.nodeValue, valeur.nodeValue) - ) - return { - ...node, - nodeValue, - missingVariables: mergeAllMissing([valeur, plafond, plancher]), - unit: valeur.unit, - explanation: { - valeur, - plafond, - plancher + if (valeur.unit) { + try { + plafond = convertNodeToUnit(valeur.unit, plafond) + plancher = convertNodeToUnit(valeur.unit, plancher) + } catch (e) { + typeWarning( + cache._meta.contextRule, + "Le plafond / plancher de l'encadrement a une unité incompatible avec celle de la valeur à encadrer", + e + ) + } + } + return { + nodeValue: + typeof valeur.nodeValue !== 'number' + ? valeur.nodeValue + : Math.max( + plancher.nodeValue, + Math.min(plafond.nodeValue, valeur.nodeValue) + ), + unit: valeur.unit } } -} +) + export default (recurse, k, v) => { const explanation = parseObject(recurse, objectShape, v) diff --git a/source/engine/mecanisms/régularisation.ts b/source/engine/mecanisms/régularisation.ts index bdbddc906..1c92a4f0a 100644 --- a/source/engine/mecanisms/régularisation.ts +++ b/source/engine/mecanisms/régularisation.ts @@ -12,9 +12,15 @@ import { temporalCumul } from 'Engine/temporal' import { Unit } from 'Engine/units' -import { DottedName } from 'Rules' import { coerceArray } from '../../utils' +import { DottedName } from './../../rules/index' +function stripTemporalTransform(node) { + if (!node?.explanation?.period) { + return node + } + return stripTemporalTransform(node.explanation.value) +} export default function parse(parse, k, v) { const rule = parse(v.règle) if (!v['valeurs cumulées']) { @@ -23,16 +29,22 @@ export default function parse(parse, k, v) { ) } - const variables = coerceArray(v['valeurs cumulées']).map(parse) as Array<{ - dottedName: DottedName - category: string - name: 'string' - }> - if (variables.some(({ category }) => category !== 'reference')) { - throw new Error( - 'Le mécanisme régularisation attend des noms de règles sous la clé `valeurs cumulées`' - ) - } + const variables = coerceArray(v['valeurs cumulées']).map(variable => { + if (typeof variable !== 'string') { + throw new Error( + `Les \`valeurs cumulées\` du mécanisme de régularisation doivent être des noms de règles existantes` + ) + } + + const value = parse(variable) + const reference = stripTemporalTransform(value) + if (reference.category !== 'reference') { + throw new Error( + 'Le mécanisme régularisation attend des noms de règles existantes dans les `valeurs cumulées`' + ) + } + return { value, dottedName: reference.dottedName } + }) as Array<{ dottedName: DottedName; value: Object }> return { evaluate, @@ -94,17 +106,17 @@ function evaluate( const currentYear = getYear(temporalEvaluation[0].start as string) const cumulatedVariables = node.explanation.variables.reduce( - (acc, parsedVariable) => { - const evaluation = evaluate(parsedVariable) + (acc, { dottedName, value }) => { + const evaluation = evaluate(value) if (!evaluation.unit.denominators.some(unit => unit === 'mois')) { evaluationError( cache._meta.contextRule, - `Dans le mécanisme régularisation, la valeur cumulée '${parsedVariable.name}' n'est pas une variable numérique définie sur le mois` + `Dans le mécanisme régularisation, la valeur cumulée '${dottedName}' n'est pas une variable numérique définie sur le mois` ) } return { ...acc, - [parsedVariable.dottedName]: getMonthlyCumulatedValuesOverYear( + [dottedName]: getMonthlyCumulatedValuesOverYear( currentYear, evaluation.temporalValue ?? pureTemporal(evaluation.nodeValue), evaluation.unit @@ -133,19 +145,18 @@ function evaluate( } const evaluation = evaluate(node.explanation.rule) - + console.log(evaluation) const temporalValue = evaluation.temporalValue const evaluationWithRegularisation = groupByYear( temporalValue as Temporal> ) .map(regulariseYear) .flat() - return { ...node, temporalValue: evaluationWithRegularisation, explanation: evaluation, - nodeValue: temporalAverage(temporalValue), + nodeValue: temporalAverage(evaluationWithRegularisation, evaluation.unit), missingVariables: evaluation.missingVariables, unit: evaluation.unit } diff --git a/source/engine/mecanisms/variableTemporelle.ts b/source/engine/mecanisms/variableTemporelle.ts index 80f09383b..54aaaebd5 100644 --- a/source/engine/mecanisms/variableTemporelle.ts +++ b/source/engine/mecanisms/variableTemporelle.ts @@ -27,10 +27,9 @@ function evaluate( evaluateAttribute(node.explanation.period.end) const value = evaluateAttribute(node.explanation.value) const period = { - start: start?.nodeValue ?? null, - end: end?.nodeValue ?? null + start: start?.nodeValue || null, + end: end?.nodeValue || null } - const temporalValue = value.temporalValue ? narrowTemporalValue(period, value.temporalValue) : createTemporalEvaluation(value.nodeValue, period) diff --git a/source/engine/units.ts b/source/engine/units.ts index 30dca85a5..474b67c38 100644 --- a/source/engine/units.ts +++ b/source/engine/units.ts @@ -143,12 +143,15 @@ let simplify = ( const convertTable: { readonly [index: string]: number } = { 'mois/an': 12, - '€/k€': 1000, 'jour/an': 365, 'jour/mois': 365 / 12, 'trimestre/an': 4, 'mois/trimestre': 3, - 'jour/trimestre': (365 / 12) * 3 + 'jour/trimestre': (365 / 12) * 3, + '€/k€': 10 ** 3, + 'g/kg': 10 ** 3, + 'mg/g': 10 ** 3, + 'mg/kg': 10 ** 6 } function singleUnitConversionFactor( from: string, @@ -225,7 +228,8 @@ export function convertUnit( const convertibleUnitClasses = [ ['mois', 'an', 'jour', 'trimestre'], - ['€', 'k€'] + ['€', 'k€'], + ['g', 'kg', 'mg'] ] function areSameClass(a: string, b: string) { return ( diff --git a/source/locales/rules-en.yaml b/source/locales/rules-en.yaml index f55a71989..47c46ea02 100644 --- a/source/locales/rules-en.yaml +++ b/source/locales/rules-en.yaml @@ -1,3 +1,6 @@ +SMIC: + titre.en: '[automatic] SMIC' + titre.fr: SMIC SMIC horaire: titre.en: hourly minimum wage (SMIC) titre.fr: SMIC horaire @@ -4711,6 +4714,9 @@ dirigeant . rémunération totale: résumé.fr: Dépensé par l'entreprise titre.en: Director total income titre.fr: rémunération totale +effectif 20 salarié ou plus: + titre.en: '[automatic] 20 or more employees' + titre.fr: effectif 20 salarié ou plus entreprise: description.en: The contract binds a company and an employee description.fr: | @@ -5623,6 +5629,9 @@ plafond sécurité sociale temps plein: prendre en compte pour le calcul de certaines cotisations. titre.en: full-time social security ceiling titre.fr: plafond sécurité sociale temps plein +primes: + titre.en: '[automatic] premiums' + titre.fr: primes protection sociale: description.en: > Social protection in France is composed of 5 main branches: sickness, @@ -6011,6 +6020,48 @@ revenus net de cotisations: résumé.fr: Avant impôt titre.en: net contribution income titre.fr: revenus net de cotisations +réduction générale: + titre.en: '[automatic] overall decrease' + titre.fr: réduction générale +réduction générale . chômage: + titre.en: '[automatic] unemployment' + titre.fr: chômage +réduction générale . chômage . coefficient: + titre.en: '[automatic] coefficient' + titre.fr: coefficient +réduction générale . chômage . constante: + titre.en: '[automatic] constant' + titre.fr: constante +réduction générale . chômage . réduction sans régularisation: + titre.en: '[automatic] unregulated reduction' + titre.fr: réduction sans régularisation +réduction générale . début: + titre.en: '[automatic] start' + titre.fr: début +réduction générale . urssaf: + titre.en: '[automatic] urssaf' + titre.fr: urssaf +réduction générale . urssaf . coefficient: + titre.en: '[automatic] coefficient' + titre.fr: coefficient +réduction générale . urssaf . constante: + titre.en: '[automatic] constant' + titre.fr: constante +réduction générale . urssaf . réduction sans régularisation: + titre.en: '[automatic] unregulated reduction' + titre.fr: réduction sans régularisation +réduction lodeom: + titre.en: '[automatic] lodeom reduction' + titre.fr: réduction lodeom +réduction lodeom . constante: + titre.en: '[automatic] constant' + titre.fr: constante +réduction sur AC au 1er janvier: + titre.en: '[automatic] reduction on AC on January 1st' + titre.fr: réduction sur AC au 1er janvier +rémunération: + titre.en: '[automatic] remuneration' + titre.fr: rémunération situation personnelle: titre.en: personal situation titre.fr: situation personnelle diff --git a/source/rules/exemple-régularisation.yaml b/source/rules/exemple-régularisation.yaml new file mode 100644 index 000000000..d1a426a1e --- /dev/null +++ b/source/rules/exemple-régularisation.yaml @@ -0,0 +1,120 @@ +SMIC: 1521.22 €/mois + +primes: + formule: + somme: + - 2470 €/mois | du 01/01/2019 | au 31/01/2019 + - 1470 €/mois | du 01/05/2019 | au 31/05/2019 + +rémunération: + formule: + somme: + - 1530 €/mois | du 01/01/2019 | au 31/12/2019 + - primes + +effectif 20 salarié ou plus: + formule: oui + +réduction sur AC au 1er janvier: + formule: non + +réduction générale . urssaf . constante: + formule: + variations: + - si: effectif 20 salarié ou plus + alors: 0.284 + - sinon: 0.2809 + +réduction générale . urssaf . coefficient: + formule: + arrondi: + valeur: (constante / 0.60 ) * (1.60 * SMIC / rémunération - 1) + décimales: 4 + +réduction générale . urssaf . réduction sans régularisation: + formule: + encadrement: + plancher: 0 + valeur: + arrondi: + valeur: + multiplication: + assiette: rémunération + taux: coefficient + décimales: 2 + +réduction générale . urssaf: + formule: + régularisation: + règle: réduction sans régularisation + valeurs cumulées: + - SMIC + - rémunération + +réduction générale . chômage . constante: + formule: 0.0405 + +réduction générale . chômage . coefficient: + formule: + arrondi: + valeur: (constante / 0.60 ) * (1.60 * SMIC / rémunération - 1) + décimales: 4 + +réduction générale . chômage . réduction sans régularisation: + formule: + encadrement: + plancher: 0 + valeur: + arrondi: + valeur: + multiplication: + assiette: rémunération + taux: coefficient + décimales: 2 + +réduction générale . début: + non applicable si: réduction sur AC au 1er janvier + formule: 01/10/2019 + +réduction générale . chômage: + formule: + régularisation: + règle: réduction sans régularisation + valeurs cumulées: + - 'SMIC | à partir de : début' + - 'rémunération | à partir de : début' + +réduction générale: + formule: urssaf + chômage + +réduction lodeom . constante: + formule: 0.3214 + +réduction lodeom: + formule: + régularisation: + valeurs cumulées: + - SMIC + - rémunération + règle: + encadrement: + plancher: 0 + valeur: + arrondi: + décimales: 2 + valeur: + produit: + assiette: rémunération + facteur [ref coefficient]: + arrondi: + décimales: 4 + valeur: + grille: + assiette: rémunération + multiplicateur: SMIC + tranches: + - montant: constante + plafond: 1.7 + - montant: constante * 1.7 * SMIC / rémunération + plafond: 2.5 + - montant: 1.7 * constante * (3.5 * SMIC / rémunération - 1) diff --git a/source/rules/protection-sociale.yaml b/source/rules/protection-sociale.yaml index af5d643aa..dccc5230f 100644 --- a/source/rules/protection-sociale.yaml +++ b/source/rules/protection-sociale.yaml @@ -178,6 +178,7 @@ protection sociale . revenu moyen: formule: le maximum de: + - 0 - dirigeant . indépendant . revenu professionnel - dirigeant . auto-entrepreneur . impôt . revenu abattu - contrat salarié . rémunération . brut diff --git a/source/sites/publi.codes/LazyStudio.js b/source/sites/publi.codes/LazyStudio.js index 10c8012b7..19a65fe62 100644 --- a/source/sites/publi.codes/LazyStudio.js +++ b/source/sites/publi.codes/LazyStudio.js @@ -1,10 +1,34 @@ import React, { Suspense } from 'react' +import { Header } from './Header' let Studio = React.lazy(() => import('./Studio')) export default function LazyStudio() { return ( - Chargement du code source...}> - - +
+
+
+
+ + Chargement du code source... +

+ } + > + +
+
) } diff --git a/source/sites/publi.codes/Studio.tsx b/source/sites/publi.codes/Studio.tsx index e516064b1..da8c86d3b 100644 --- a/source/sites/publi.codes/Studio.tsx +++ b/source/sites/publi.codes/Studio.tsx @@ -1,96 +1,114 @@ -import { ControlledEditor } from '@monaco-editor/react' +// import { ControlledEditor } from '@monaco-editor/react' +import { formatValue } from 'Engine/format' import Engine from 'Engine/react' import { safeLoad } from 'js-yaml' import { last } from 'ramda' -import React, { useCallback, useEffect, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import emoji from 'react-easy-emoji' +import MonacoEditor from 'react-monaco-editor' import { useLocation } from 'react-router' import styled from 'styled-components' -import { Header } from './Header' +const EXAMPLE_CODE = ` +# Bienvenu dans le bac à sable du langage publicode ! +# Pour en savoir plus sur le langage, consultez le tutoriel : +# => https://publi.codes -let initialInput = `a: - formule: 10€ -b: - formule: a + 18€ -c: - formule: - produit: - assiette: 2000€ - taux: 3% -d: - formule: a + b + c +prix . carottes: 2€/kg +prix . champignons: 5€/kg +prix . avocat: 2€/avocat + +dépenses primeur: + formule: + somme: + - prix . carottes * 1.5 kg + - prix . champignons * 500g + - prix . avocat * 3 avocat ` +function useDebounce(value, delay) { + const [debouncedValue, setDebouncedValue] = useState(value) + useEffect( + () => { + // Update debounced value after delay + const handler = setTimeout(() => { + setDebouncedValue(value) + }, delay) + + // Cancel the timeout if value changes (also on delay change or unmount) + // This is how we prevent debounced value from updating if value is changed ... + // .. within the delay period. Timeout gets cleared and restarted. + return () => { + clearTimeout(handler) + } + }, + [value, delay] // Only re-call effect if value or delay changes + ) + return debouncedValue +} export default function Studio() { - const search = new URLSearchParams(useLocation().search ?? '') - const code = search.get('code') - const [editorValue, setEditorValue] = useState(code ? code : initialInput) - const [targets, setTargets] = useState([]) - const [rules, setRules] = useState(editorValue) - const handleShare = useCallback(() => { - navigator.clipboard.writeText( - `https://publi.codes/studio?code=${encodeURIComponent(editorValue)}` - ) - }, [editorValue]) + const search = useLocation().search + const initialValue = useMemo(() => { + const code = new URLSearchParams(search ?? '').get('code') + return code ? code : EXAMPLE_CODE + }, [search]) + const [editorValue, setEditorValue] = useState(initialValue) + const debouncedEditorValue = useDebounce(editorValue, 1000) + + const targets = useMemo(() => { + try { + return Object.keys(safeLoad(debouncedEditorValue) ?? {}) + } catch (e) { + console.error(e) + return [] + } + }, [debouncedEditorValue]) useEffect(() => { - try { - setTargets(Object.keys(safeLoad(editorValue) ?? {})) - } catch {} - }, [editorValue]) + history.replaceState( + null, + '', + `${window.location.pathname}?code=${encodeURIComponent( + debouncedEditorValue + )}` + ) + }, [debouncedEditorValue]) + + const handleShare = useCallback(() => { + navigator.clipboard.writeText(window.location.href) + }, [window.location.href]) return ( - -
+ setEditorValue(newValue ?? '')} + options={{ + minimap: { enabled: false } + }} + /> +
-
-
-
- -
- setEditorValue(newValue ?? '')} - options={{ minimap: { enabled: false } }} - /> -
-
- setRules(editorValue)} - onClickShare={handleShare} - /> -
-
-
-
+ + + + + ) } type ResultsProps = { targets: string[] - onClickUpdate: React.MouseEventHandler onClickShare: React.MouseEventHandler } -export const Results = ({ - targets, - onClickUpdate, - onClickShare -}: ResultsProps) => { +export const Results = ({ targets, onClickShare }: ResultsProps) => { const [rule, setCurrentTarget] = useState() const currentTarget = rule ?? (last(targets) as string) const error = Engine.useError() @@ -111,11 +129,6 @@ export const Results = ({ `} > {nl2br(error)} -
-
- ) : ( <> @@ -126,7 +139,9 @@ export const Results = ({ border-radius: 5px; `} > - +

+ +