mon-entreprise/site/components/Studio.tsx

267 lines
6.0 KiB
TypeScript
Raw Normal View History

import Engine from 'publicodes'
import { Documentation, getDocumentationSiteMap } from 'publicodes-react'
import { invertObj, last } from 'ramda'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import MonacoEditor from 'react-monaco-editor'
import { useHistory, useLocation } from 'react-router-dom'
import styled from 'styled-components'
2020-04-12 21:30:58 +00:00
const EXAMPLE_CODE = `
# Bienvenue dans le bac à sable du langage publicodes !
2020-12-15 10:24:31 +00:00
# Pour en savoir plus sur le langage :
# => https://publi.codes/documentation/principes-de-base
prix:
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
`
2020-03-12 13:55:32 +00:00
export default function Studio() {
const { search, pathname } = useLocation()
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 history = useHistory()
useEffect(() => {
history.replace({
pathname,
search: `?code=${encodeURIComponent(debouncedEditorValue)}`,
})
}, [debouncedEditorValue, history])
const handleShare = useCallback(() => {
navigator.clipboard.writeText(window.location.href)
}, [window.location.href])
return (
<Layout className="ui__ full-width">
<MonacoEditor
language="yaml"
height="90vh"
defaultValue={editorValue}
onChange={(newValue) => setEditorValue(newValue ?? '')}
options={{
minimap: { enabled: false },
}}
/>
<section
2020-03-12 13:55:32 +00:00
css={`
padding: 0 1rem;
flex: 1;
2020-03-12 13:55:32 +00:00
`}
>
<ErrorBoundary key={debouncedEditorValue}>
{/* TODO: prévoir de changer la signature de EngineProvider */}
2020-05-26 13:44:19 +00:00
<Results rules={debouncedEditorValue} onClickShare={handleShare} />
</ErrorBoundary>
</section>
</Layout>
2020-03-12 13:55:32 +00:00
)
}
type ResultsProps = {
rules: string
onClickShare: React.MouseEventHandler
}
class Logger {
messages: string[] = []
warn(message: string) {
this.messages.push(message)
}
error(message: string) {
this.messages.push(message)
}
log(message: string) {
this.messages.push(message)
}
toJSX() {
return this.messages.map((m) => (
<div
css={`
background: lightyellow;
padding: 20px;
border-radius: 5px;
`}
key={m}
>
{nl2br(m)}
</div>
))
}
}
2020-05-26 13:44:19 +00:00
export const Results = ({ onClickShare, rules }: ResultsProps) => {
const logger = useMemo(() => new Logger(), [rules])
const engine = useMemo(() => new Engine(rules, { logger }), [rules, logger])
const targets = useMemo(() => Object.keys(engine.getParsedRules()), [engine])
const documentationPath = '/studio'
2020-05-26 13:44:19 +00:00
const pathToRules = useMemo(
() => getDocumentationSiteMap({ engine, documentationPath }),
[engine, documentationPath]
)
2020-05-26 13:44:19 +00:00
const ruleToPaths = useMemo(() => invertObj(pathToRules), [pathToRules])
const { search, pathname } = useLocation()
const history = useHistory()
const setCurrentTarget = useCallback(
(target) =>
2020-05-26 13:44:19 +00:00
history.replace({
pathname: ruleToPaths[target],
search,
2020-05-26 13:44:19 +00:00
}),
[ruleToPaths, history, search]
)
2020-05-26 13:44:19 +00:00
useEffect(() => {
if (!pathToRules[pathname]) {
setCurrentTarget(last(targets))
}
})
return (
2020-03-12 13:55:32 +00:00
<>
{logger.toJSX()}
2020-03-12 13:55:32 +00:00
<div
css={`
display: flex;
justify-content: space-between;
align-items: baseline;
2020-03-12 13:55:32 +00:00
`}
>
<small>
Aller à{' '}
<select
onChange={(e) => {
setCurrentTarget(e.target.value)
}}
css={`
font-size: inherit;
color: inherit;
font-family: inherit;
`}
>
{targets.map((target) => (
<option
key={target}
value={target}
2020-05-26 13:44:19 +00:00
selected={pathToRules[pathname] === target}
>
{target}
</option>
))}
</select>
</small>
<div className="ui__ answer-group">
<button className="ui__ simple small button" onClick={onClickShare}>
Squashed 'publicodes/' changes from 10a30d32..a680ad31 a680ad31 :bug: Corrige un bug sur l'affichage des remplacements dans la doc publicodes 1e45d98d 🔥 Supprime la transformation d'emoji dans la doc publicodes 77973a9b :bug: Répare l'affichage des règles remplacées 341b2e39 :art::bug: corrige le style du remplacement dans les sommes 24dce683 :art: Améliore l'explication des règles avec remplacement 6d086823 :green_heart: fix lint 2d64d908 🏗Add export as Pdf button on simulators 163b766a ✅ Prettier linting in publicodes subtree e1507975 ✨ Reformat files 80161f95 🐛 Fix VAT example 79bde0be 🔥 Remove "classnames" dependency from publicodes-react 3e65e320 🔥 Remove ramda from publicodes-react 71b68707 📦 Publicodes v1.0.0-beta.16 bdc92216 Merge the tests and publish workflows 1c032ebc ✅ Add test for a value with a percentage in its unit d2865e8c Disable sum optimization inside comparisons f4faa35d Ajout d'un test qui casse l'implé actuelle des missing parentes f6105283 🖋 Document packages publication on NPM a79eeb86 Better Github workflows d0db4d09 Import publish action c268cff5 Type checking in CI a35403d7 Correction formattage 3022fd78 Add a separate cache for applicability 35095da9 Optimize the evaluation of applicability 7525446e Add a github action to run tests on push 39a12a13 Ajout d'un prettierrc / reformattage de quelques fichiers récents c296a25e Ajout d'un deuxièmes test non fonctionnel sur le sujet #33 9f5afb4e Désactivation d'un nouveau test pas encore résolu 76d00085 Récupération de la complexité initiale du test missing variations 93210235 :bug: Meilleures missingVariables des variations 369abeae Simplification du test missingVariables qui ne marche pas 64217d3d Nouveau test missing variables éval paresseuse variations d9c3e1f6 Conversion d'un gros test JS object en YAML 615ae5e5 Ajout d'un test râté pour #33 d290b46d Passage à mochapack pour webpack 5 5d7a5b31 Paquets NPM et conf babel manquants 1df9a8d4 Ajout d'un .gitignore et yarn.lock 6c2d0203 Uniformise l'unité des arrondis 2cbffe8a ⬆ MAJ Typescript vers 4.3 678403e4 Corrige le calcul des cotisations forfaitaires de début d'activité 8cdaac05 Simpler condition component (#1578) b7459617 🔥 Supprime les variables temporelles db62b57d 🔥 Supprime l'utilisation des temporals dans les mécanismes git-subtree-dir: publicodes git-subtree-split: a680ad31c33b93e4f35171488cec1b6f9e08179a
2021-11-05 14:52:37 +00:00
🔗 Copier le lien
</button>
</div>
2020-03-12 13:55:32 +00:00
</div>
<ErrorBoundary>
<Documentation
engine={engine}
documentationPath={documentationPath}
language="fr"
/>
</ErrorBoundary>
2020-03-12 13:55:32 +00:00
</>
)
}
2020-03-12 13:55:32 +00:00
const newlineRegex = /(\r\n|\r|\n)/g
function nl2br(str: string) {
if (typeof str !== 'string') {
return str
}
return str.split(newlineRegex).map(function (line, index) {
2020-03-12 13:55:32 +00:00
if (line.match(newlineRegex)) {
return React.createElement('br', { key: index })
}
return line
})
}
const Layout = styled.div`
2020-03-12 13:55:32 +00:00
flex-grow: 1;
display: flex;
height: 100%;
> :first-child {
width: 55% !important;
}
@media (max-width: 960px) {
flex-direction: column;
padding: 20px;
> :first-child {
width: 100% !important;
}
}
`
class ErrorBoundary extends React.Component {
state: { error: false | { message: string; name: string } } = { error: false }
static getDerivedStateFromError(error: Error) {
console.error(error)
return { error: { message: error.message, name: error.name } }
}
render() {
if (this.state.error) {
return (
<div
css={`
background: lightyellow;
padding: 20px;
border-radius: 5px;
`}
>
<strong>{this.state.error.name}</strong>
<br />
{nl2br(this.state.error.message)}
2020-05-26 13:44:19 +00:00
<br />
<br />
<a onClick={() => window.location.reload()}>Rafraichir</a>
</div>
)
}
return this.props.children
}
}
function useDebounce<T>(value: T, delay: number) {
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
}