2022-12-14 16:37:17 -06:00
import yaml from 'js-yaml'
import hljs from 'highlight.js/lib/common'
import defaultSettings from './default-settings'
import validateGist from './gist-validator'
import { useEffect , useState , useRef } from 'react'
2022-12-15 19:27:05 -06:00
import Json from 'shared/components/json-highlight.js'
import Popout from 'shared/components/popout.js'
2022-12-16 11:58:39 -06:00
import { useTranslation } from 'next-i18next'
2022-12-16 12:19:12 -06:00
import { capitalize } from '@freesewing/core'
2022-12-14 16:37:17 -06:00
2022-12-16 11:58:39 -06:00
/** count the number of lines in some text */
2022-12-14 16:37:17 -06:00
const countLines = ( txt ) => txt . split ( '\n' ) . length
2022-12-16 11:58:39 -06:00
/** a view for editing the gist as yaml */
2022-12-14 16:37:17 -06:00
const Edit = ( props ) => {
2022-12-16 11:58:39 -06:00
let { gist , setGist , gistReady } = props
2022-12-14 16:37:17 -06:00
const inputRef = useRef ( null )
2022-12-16 11:58:39 -06:00
// the gist parsed to yaml
2022-12-14 16:37:17 -06:00
const [ gistAsYaml , setGistAsYaml ] = useState ( null )
2022-12-16 11:58:39 -06:00
// the number of lines in the yaml
2022-12-14 16:37:17 -06:00
const [ numLines , setNumLines ] = useState ( 0 )
2022-12-16 11:58:39 -06:00
// any errors, parsed by hljs
2022-12-15 19:27:05 -06:00
const [ error , setError ] = useState ( null )
2022-12-16 11:58:39 -06:00
// success notifier
const [ success , setSuccess ] = useState ( null )
const { t } = useTranslation ( [ 'app' ] )
2022-12-14 16:37:17 -06:00
2022-12-16 11:58:39 -06:00
// parse the current gist to yaml. this will also run when the gist gets set by input
2022-12-14 16:37:17 -06:00
useEffect ( ( ) => {
2022-12-16 11:58:39 -06:00
if ( gistReady ) {
// get everything but the design because it's a function and can't be serialized
const { design , ... gistRest } = gist
setGistAsYaml ( yaml . dump ( gistRest ) )
}
} , [ gist , gistReady ] )
// set the line numbers when the yaml changes
2022-12-14 16:37:17 -06:00
useEffect ( ( ) => {
2022-12-16 11:58:39 -06:00
if ( gistAsYaml ) {
setNumLines ( countLines ( gistAsYaml ) )
// update the input value to reflect what's been saved
inputRef . current . value = gistAsYaml
}
2022-12-14 16:37:17 -06:00
} , [ gistAsYaml ] )
2022-12-16 11:58:39 -06:00
/** user-initiated save */
2022-12-15 19:27:05 -06:00
const onSave = ( ) => {
2022-12-16 11:58:39 -06:00
// clear the errors
2022-12-15 19:27:05 -06:00
setError ( null )
2022-12-16 11:58:39 -06:00
try {
// parse back to json
const editedAsJson = yaml . load ( inputRef . current . value )
// make it backwards compatible so that people can paste in the yaml export from org
// the yaml export from org is missing some of the settings that are needed in the gist, and what it does have is under 'settings', so we merge that stuff with the existing gist view state and the default settings to make sure all necessary keys are accounted for, but we're not keeping stuff that was supposed to be cleared
const gistFromDefaults = { _state : gist . _state }
for ( const d in defaultSettings ) {
gistFromDefaults [ d ] = gist [ d ] === undefined ? defaultSettings [ d ] : gist [ d ]
}
// merge it all up
const gistToCheck = {
... gistFromDefaults ,
... ( editedAsJson . settings ? editedAsJson . settings : editedAsJson ) ,
}
// validate it
const validation = validateGist ( gistToCheck , props . design )
// if it's not valid, show a warning about errors
if ( ! validation . valid ) {
const newError = JSON . stringify ( validation . errors , null , 2 )
const newErrorHighlight = hljs . highlight ( newError , { language : 'json' } )
setError ( newError )
}
// save regardless
setGist ( gistToCheck )
setSuccess ( true )
} catch ( e ) {
setError ( e )
2022-12-14 16:37:17 -06:00
}
}
2022-12-16 11:58:39 -06:00
/** count lines and reset the success alert */
2022-12-14 16:37:17 -06:00
const onKeyUp = ( e ) => {
setNumLines ( countLines ( e . target . value ) )
2022-12-16 11:58:39 -06:00
setSuccess ( false )
2022-12-14 16:37:17 -06:00
}
2022-12-16 12:19:12 -06:00
const designName = capitalize ( props . design . designConfig . data . name . replace ( '@freesewing/' , '' ) )
2022-12-14 16:37:17 -06:00
return (
< div className = "max-w-screen-xl m-auto h-screen form-control" >
2022-12-16 12:19:12 -06:00
< h2 > { t ( 'workbench:editThingTitle' , { thing : designName } ) } < / h 2 >
2022-12-15 19:27:05 -06:00
{ error ? (
2022-12-16 11:58:39 -06:00
< Popout warning className = "mb-4" >
< h3 > { t ( 'editError' ) } < / h 3 >
2022-12-16 12:21:56 -06:00
{ success ? < p > { t ( 'editErrorDesc' ) } : < / p > : n u l l }
2022-12-15 19:27:05 -06:00
< pre
className = "language-json hljs text-base lg:text-lg whitespace-pre overflow-scroll pr-4"
dangerouslySetInnerHTML = { { _ _html : error } }
> < / p r e >
< / P o p o u t >
) : null }
2022-12-16 11:58:39 -06:00
{ success ? (
< div className = "alert alert-success my-4" >
< div >
< span > { t ( 'success' ) } < / s p a n >
< / d i v >
< / d i v >
) : null }
2022-12-14 16:37:17 -06:00
< div
id = "editor"
className = "h-3/5 mb-8 overflow-auto p-1 outline-1 outline-primary outline focus-within:outline-primary-focus focus-within:outline-2 rounded"
>
< div className = "font-mono flex gap-4 leading-7 text-lg items-stretch" >
< div id = "line-numbers" className = "text-right p-4 pr-0 text-primary" aria - hidden >
{ Array . from ( { length : numLines } , ( _ , i ) => (
< span className = "block" key = { i } >
{ ' ' }
{ i + 1 } { ' ' }
< / s p a n >
) ) }
< / d i v >
< textarea
className = "textarea focus:outline-none w-full p-4 leading-7 overflow-y-hidden resize-none text-lg"
name = "gistAsYaml"
aria - label = "Configuration in YAML format"
ref = { inputRef }
defaultValue = { gistAsYaml }
onKeyUp = { onKeyUp }
2022-12-16 11:58:39 -06:00
onChange = { onKeyUp }
2022-12-14 16:37:17 -06:00
/ >
< / d i v >
< / d i v >
2022-12-15 19:27:05 -06:00
< button className = "btn btn-primary" onClick = { onSave } >
2022-12-14 16:37:17 -06:00
{ ' ' }
Save { ' ' }
< / b u t t o n >
< / d i v >
)
}
export default Edit