yaml edit view in new ui
This commit is contained in:
parent
fd0b0b0132
commit
84687fbf3e
6 changed files with 119 additions and 159 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
86
sites/shared/components/workbench/views/edit/index.mjs
Normal file
86
sites/shared/components/workbench/views/edit/index.mjs
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue