1
0
Fork 0

yaml edit view in new ui

This commit is contained in:
Enoch Riese 2023-06-06 16:58:59 -05:00
parent fd0b0b0132
commit 84687fbf3e
6 changed files with 119 additions and 159 deletions

View file

@ -5,6 +5,3 @@ layoutThing: 'Layout {thing}'
pageSize: Page size
startBySelectingAThing: 'Start by selecting a {thing}'
testThing: 'Test {thing}'
yamlEditViewTitleThing: 'Edit Pattern Configuration for {thing}'
yamlEditViewError: Issues with YAML
yamlEditViewErrorDesc: We saved your input, but it might not work for the following reasons

View file

@ -1,116 +0,0 @@
import yaml from 'js-yaml'
import { defaultGist } from 'shared/components/workbench/gist.mjs'
import { validateGist } from './gist-validator.mjs'
import { useEffect, useState, useRef } from 'react'
import { Popout } from 'shared/components/popout.mjs'
import { useTranslation } from 'next-i18next'
import { capitalize } from '@freesewing/core'
/** a view for editing the gist as yaml */
export const EditYaml = (props) => {
let { gist, setGist, gistReady } = props
const inputRef = useRef(null)
// the gist parsed to yaml
const [gistAsYaml, setGistAsYaml] = useState(null)
// any errors as a json string
const [error, setError] = useState(null)
// success notifier
const [success, setSuccess] = useState(null)
const { t } = useTranslation(['workbench'])
// parse the current gist to yaml. this will also run when the gist gets set by input
useEffect(() => {
if (gistReady) {
// get everything but the design because it's a function and can't be serialized
// eslint-disable-next-line no-unused-vars
const { design, ...gistRest } = gist
setGistAsYaml(yaml.dump(gistRest))
}
}, [gist, gistReady])
// set the line numbers when the yaml changes
useEffect(() => {
if (gistAsYaml) {
inputRef.current.value = gistAsYaml
}
}, [gistAsYaml])
/** user-initiated save */
const onSave = () => {
// clear the errors
setError(null)
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 defaultGist) {
gistFromDefaults[d] = gist[d] === undefined ? defaultGist[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)
setError(newError)
}
// save regardless
setGist(gistToCheck)
setSuccess(true)
} catch (e) {
setError(e)
}
}
const designName = capitalize(props.design.designConfig.data.name.replace('@freesewing/', ''))
return (
<div className="max-w-screen-xl m-auto h-screen form-control">
<h2>{t('yamlEditViewTitleThing', { thing: designName })}</h2>
{error ? (
<Popout warning className="mb-4">
<h3> {t('yamlEditViewError')} </h3>
{success ? <p> {t('yamlEditViewErrorDesc')}: </p> : null}
<pre
className="language-json hljs text-base lg:text-lg whitespace-pre overflow-scroll pr-4"
dangerouslySetInnerHTML={{ __html: error }}
></pre>
</Popout>
) : null}
{success ? (
<div className="alert alert-success my-4">
<div>
<span>{t('success')}</span>
</div>
</div>
) : null}
<div id="editor" className="h-3/5 my-8">
<textarea
className="textarea textarea-primary w-full p-4 leading-7 text-lg h-full"
name="gistAsYaml"
aria-label="Configuration in YAML format"
ref={inputRef}
defaultValue={gistAsYaml}
/>
</div>
<button className="btn btn-primary" onClick={onSave}>
{' '}
Save{' '}
</button>
</div>
)
}

View file

@ -16,8 +16,9 @@ import { DraftView, ns as draftNs } from 'shared/components/workbench/views/draf
import { SaveView, ns as saveNs } from 'shared/components/workbench/views/save/index.mjs'
import { PrintView, ns as printNs } from 'shared/components/workbench/views/print/index.mjs'
import { CutView, ns as cutNs } from 'shared/components/workbench/views/cut/index.mjs'
import { EditView, ns as editNs } from './views/edit/index.mjs'
export const ns = ['account', 'workbench', ...draftNs, ...saveNs, ...printNs, ...cutNs]
export const ns = ['account', 'workbench', ...draftNs, ...saveNs, ...printNs, ...cutNs, ...editNs]
const defaultUi = {
renderer: 'react',
@ -27,6 +28,7 @@ const views = {
draft: DraftView,
print: PrintView,
cut: CutView,
edit: EditView,
}
const draftViews = ['draft', 'test']
@ -86,6 +88,9 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
case 'save':
viewContent = <SaveView {...viewProps} from={from} />
break
case 'edit':
viewContent = <EditView {...viewProps} setSettings={setSettings} />
break
default: {
const layout = ui.layouts?.[view] || settings.layout || true
// Generate the pattern here so we can pass it down to both the view and the options menu

View file

@ -0,0 +1,86 @@
import yaml from 'js-yaml'
import { validateSettings } from './settings-validator.mjs'
import { useEffect, useState, useRef, useMemo } from 'react'
import { useTranslation } from 'next-i18next'
import { useToast } from 'shared/hooks/use-toast.mjs'
import { CloseIcon } from 'shared/components/icons.mjs'
import { capitalize } from 'shared/utils.mjs'
export const ns = ['wbedit']
/** a view for editing the gist as yaml */
export const EditView = ({ settings, setSettings, design, Design }) => {
const inputRef = useRef(null)
const [error, setError] = useState(false)
const [success, setSuccess] = useState(false)
const { t } = useTranslation(ns)
const patternConfig = useMemo(() => new Design().getConfig(), [Design])
const toast = useToast()
// parse the settings to yaml and set them as the value on the textArea
useEffect(() => {
inputRef.current.value = yaml.dump(settings)
}, [settings])
/** user-initiated save */
const onSave = () => {
setError(false)
setSuccess(false)
try {
// parse back to json
const editedAsJson = yaml.load(inputRef.current.value)
// validate it
const validation = validateSettings(editedAsJson, patternConfig)
// if it's not valid, show a warning about errors
if (!validation.valid) {
const newError = JSON.stringify(validation.errors, null, 2)
setError(newError)
}
// save regardless
setSettings(editedAsJson)
setSuccess(true)
if (validation.valid) toast.success(t('success'))
} catch (e) {
console.log(e)
setError(e.message)
}
}
return (
<div className="max-w-screen-xl m-auto h-screen form-control mt-4 flex flex-col">
<h2>{t('yamlEditViewTitleThing', { thing: capitalize(design) })}</h2>
<div id="editor" className="h-2/3 my-2 overflow-auto flex flex-col">
{error && (
<div className={`w-full shadow bg-base-100 p-0 my-4`}>
<div className={`w-full m-0 bg-error p-4 border bg-opacity-30 rounded-lg`}>
<button
className="float-right btn btn-circle btn-outline btn-sm"
onClick={() => setError(false)}
>
<CloseIcon />
</button>
<h3> {t('yamlEditViewError')} </h3>
{success && <p>{t('yamlEditViewErrorDesc')}: </p>}
<pre
className="language-json hljs text-base lg:text-lg whitespace-pre overflow-scroll pr-4"
dangerouslySetInnerHTML={{ __html: error }}
/>
</div>
</div>
)}
<textarea
className="textarea textarea-primary w-full p-4 leading-7 text-lg grow"
name="gistAsYaml"
aria-label="Configuration in YAML format"
ref={inputRef}
/>
</div>
<button className="btn btn-primary" onClick={onSave}>
{t('save')}
</button>
</div>
)
}

View file

@ -1,60 +1,45 @@
import { defaultGist } from 'shared/components/workbench/gist.mjs'
/** A utility for validating a gist against a design */
class GistValidator {
givenGist
design
/** A utility for validating a gist against a patternConfig */
class SettingsValidator {
givenSettings
patternConfig
errors
valid = true
setGist(givenGist, design) {
this.givenGist = givenGist
this.design = design
setGist(givenSettings, patternConfig) {
this.givenSettings = givenSettings
this.patternConfig = patternConfig
this.errors = {}
this.valid = true
}
/** check that the settings all exist and are all of the right type */
validateSettings() {
for (const key in defaultGist) {
if (this.givenGist[key] === undefined) {
this.errors[key] = 'MissingSetting'
this.valid = false
} else if (typeof this.givenGist[key] !== typeof defaultGist[key]) {
this.errors[key] = 'TypeError'
this.valid = false
}
}
}
/** check that the required measurements are all there and the correct type */
validateMeasurements() {
if (!this.givenGist.measurements && this.design.patternConfig.measurements.length) {
if (!this.givenSettings.measurements && this.patternConfig.measurements.length) {
this.errors.measurements = 'MissingMeasurements'
this.valid = false
return
}
this.errors.measurements = {}
for (const m of this.design.patternConfig.measurements || []) {
if (this.givenGist.measurements[m] === undefined) {
for (const m of this.patternConfig.measurements || []) {
if (this.givenSettings.measurements[m] === undefined) {
this.errors.measurements[m] = 'MissingMeasurement'
this.valid = false
} else if (isNaN(this.givenGist.measurements[m])) {
} else if (isNaN(this.givenSettings.measurements[m])) {
this.errors.measurements[m] = 'TypeError'
this.valid = false
}
}
}
/** check validit of any options that are included */
/** check validity of any options that are included */
validateOptions() {
this.errors.options = {}
const configOpts = this.design.patternConfig.options
const gistOpts = this.givenGist.options
for (const o in gistOpts) {
const configOpts = this.patternConfig.options
const settingsOpts = this.givenSettings.options
for (const o in settingsOpts) {
const configOpt = configOpts[o]
const gistOpt = gistOpts[o]
const settingsOpt = settingsOpts[o]
// if the option doesn't exist on the pattern
if (!configOpt) {
this.errors.options[o] = 'UnknownOption'
@ -65,20 +50,20 @@ class GistValidator {
}
// if it's a list option but the selection isn't in the list, mark it an unknown selection
else if (configOpt.list !== undefined) {
if (!configOpt.list.includes(gistOpt) && gistOpt != configOpt.dflt)
if (!configOpt.list.includes(settingsOpt) && settingsOpt != configOpt.dflt)
this.error.options[o] = 'UnknownOptionSelection'
}
// if it's a boolean option but the gist value isn't a boolean. mark a type error
else if (configOpts[o].bool !== undefined) {
if (typeof gistOpt !== 'boolean') this.errors.options[o] = 'TypeError'
if (typeof settingsOpt !== 'boolean') this.errors.options[o] = 'TypeError'
}
// all other options are numbers, so check it's a number
else if (isNaN(gistOpt)) {
else if (isNaN(settingsOpt)) {
this.errors.options[o] = 'TypeError'
}
// if still no error, check the bounds
else {
const checkNum = configOpt.pct ? gistOpt * 100 : gistOpt
const checkNum = configOpt.pct ? settingsOpt * 100 : settingsOpt
if (checkNum < configOpt.min || checkNum > configOpt.max) {
this.errors.options[o] = 'RangeError'
}
@ -90,7 +75,6 @@ class GistValidator {
/** run all validations */
validate() {
this.validateSettings()
this.validateMeasurements()
this.validateOptions()
@ -98,10 +82,10 @@ class GistValidator {
}
}
const validator = new GistValidator()
const validator = new SettingsValidator()
/** make and run a gist validator */
export function validateGist(givenGist, design) {
validator.setGist(givenGist, design)
export function validateSettings(givenSettings, patternConfig) {
validator.setGist(givenSettings, patternConfig)
return validator.validate()
}

View file

@ -0,0 +1,4 @@
yamlEditViewTitleThing: 'Edit Pattern Configuration for {thing}'
yamlEditViewError: Issues with YAML
yamlEditViewErrorDesc: We saved your input, but it might not work for the following reasons
save: Save Settings