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
|
pageSize: Page size
|
||||||
startBySelectingAThing: 'Start by selecting a {thing}'
|
startBySelectingAThing: 'Start by selecting a {thing}'
|
||||||
testThing: 'Test {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 { 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 { 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 { 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 = {
|
const defaultUi = {
|
||||||
renderer: 'react',
|
renderer: 'react',
|
||||||
|
@ -27,6 +28,7 @@ const views = {
|
||||||
draft: DraftView,
|
draft: DraftView,
|
||||||
print: PrintView,
|
print: PrintView,
|
||||||
cut: CutView,
|
cut: CutView,
|
||||||
|
edit: EditView,
|
||||||
}
|
}
|
||||||
|
|
||||||
const draftViews = ['draft', 'test']
|
const draftViews = ['draft', 'test']
|
||||||
|
@ -86,6 +88,9 @@ export const Workbench = ({ design, Design, baseSettings, DynamicDocs, from }) =
|
||||||
case 'save':
|
case 'save':
|
||||||
viewContent = <SaveView {...viewProps} from={from} />
|
viewContent = <SaveView {...viewProps} from={from} />
|
||||||
break
|
break
|
||||||
|
case 'edit':
|
||||||
|
viewContent = <EditView {...viewProps} setSettings={setSettings} />
|
||||||
|
break
|
||||||
default: {
|
default: {
|
||||||
const layout = ui.layouts?.[view] || settings.layout || true
|
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
|
// 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 patternConfig */
|
||||||
|
class SettingsValidator {
|
||||||
/** A utility for validating a gist against a design */
|
givenSettings
|
||||||
class GistValidator {
|
patternConfig
|
||||||
givenGist
|
|
||||||
design
|
|
||||||
errors
|
errors
|
||||||
valid = true
|
valid = true
|
||||||
|
|
||||||
setGist(givenGist, design) {
|
setGist(givenSettings, patternConfig) {
|
||||||
this.givenGist = givenGist
|
this.givenSettings = givenSettings
|
||||||
this.design = design
|
this.patternConfig = patternConfig
|
||||||
this.errors = {}
|
this.errors = {}
|
||||||
this.valid = true
|
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 */
|
/** check that the required measurements are all there and the correct type */
|
||||||
validateMeasurements() {
|
validateMeasurements() {
|
||||||
if (!this.givenGist.measurements && this.design.patternConfig.measurements.length) {
|
if (!this.givenSettings.measurements && this.patternConfig.measurements.length) {
|
||||||
this.errors.measurements = 'MissingMeasurements'
|
this.errors.measurements = 'MissingMeasurements'
|
||||||
this.valid = false
|
this.valid = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.errors.measurements = {}
|
this.errors.measurements = {}
|
||||||
for (const m of this.design.patternConfig.measurements || []) {
|
for (const m of this.patternConfig.measurements || []) {
|
||||||
if (this.givenGist.measurements[m] === undefined) {
|
if (this.givenSettings.measurements[m] === undefined) {
|
||||||
this.errors.measurements[m] = 'MissingMeasurement'
|
this.errors.measurements[m] = 'MissingMeasurement'
|
||||||
this.valid = false
|
this.valid = false
|
||||||
} else if (isNaN(this.givenGist.measurements[m])) {
|
} else if (isNaN(this.givenSettings.measurements[m])) {
|
||||||
this.errors.measurements[m] = 'TypeError'
|
this.errors.measurements[m] = 'TypeError'
|
||||||
this.valid = false
|
this.valid = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** check validit of any options that are included */
|
/** check validity of any options that are included */
|
||||||
validateOptions() {
|
validateOptions() {
|
||||||
this.errors.options = {}
|
this.errors.options = {}
|
||||||
const configOpts = this.design.patternConfig.options
|
const configOpts = this.patternConfig.options
|
||||||
const gistOpts = this.givenGist.options
|
const settingsOpts = this.givenSettings.options
|
||||||
for (const o in gistOpts) {
|
for (const o in settingsOpts) {
|
||||||
const configOpt = configOpts[o]
|
const configOpt = configOpts[o]
|
||||||
const gistOpt = gistOpts[o]
|
const settingsOpt = settingsOpts[o]
|
||||||
// if the option doesn't exist on the pattern
|
// if the option doesn't exist on the pattern
|
||||||
if (!configOpt) {
|
if (!configOpt) {
|
||||||
this.errors.options[o] = 'UnknownOption'
|
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
|
// if it's a list option but the selection isn't in the list, mark it an unknown selection
|
||||||
else if (configOpt.list !== undefined) {
|
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'
|
this.error.options[o] = 'UnknownOptionSelection'
|
||||||
}
|
}
|
||||||
// if it's a boolean option but the gist value isn't a boolean. mark a type error
|
// if it's a boolean option but the gist value isn't a boolean. mark a type error
|
||||||
else if (configOpts[o].bool !== undefined) {
|
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
|
// all other options are numbers, so check it's a number
|
||||||
else if (isNaN(gistOpt)) {
|
else if (isNaN(settingsOpt)) {
|
||||||
this.errors.options[o] = 'TypeError'
|
this.errors.options[o] = 'TypeError'
|
||||||
}
|
}
|
||||||
// if still no error, check the bounds
|
// if still no error, check the bounds
|
||||||
else {
|
else {
|
||||||
const checkNum = configOpt.pct ? gistOpt * 100 : gistOpt
|
const checkNum = configOpt.pct ? settingsOpt * 100 : settingsOpt
|
||||||
if (checkNum < configOpt.min || checkNum > configOpt.max) {
|
if (checkNum < configOpt.min || checkNum > configOpt.max) {
|
||||||
this.errors.options[o] = 'RangeError'
|
this.errors.options[o] = 'RangeError'
|
||||||
}
|
}
|
||||||
|
@ -90,7 +75,6 @@ class GistValidator {
|
||||||
|
|
||||||
/** run all validations */
|
/** run all validations */
|
||||||
validate() {
|
validate() {
|
||||||
this.validateSettings()
|
|
||||||
this.validateMeasurements()
|
this.validateMeasurements()
|
||||||
this.validateOptions()
|
this.validateOptions()
|
||||||
|
|
||||||
|
@ -98,10 +82,10 @@ class GistValidator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const validator = new GistValidator()
|
const validator = new SettingsValidator()
|
||||||
|
|
||||||
/** make and run a gist validator */
|
/** make and run a gist validator */
|
||||||
export function validateGist(givenGist, design) {
|
export function validateSettings(givenSettings, patternConfig) {
|
||||||
validator.setGist(givenGist, design)
|
validator.setGist(givenSettings, patternConfig)
|
||||||
return validator.validate()
|
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