@axdspub/axiom-ui-forms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +4 -0
- package/.env +1 -0
- package/.eslintrc.json +37 -0
- package/.gitlab-ci.yml +72 -0
- package/.storybook/main.ts +43 -0
- package/.storybook/preview.ts +16 -0
- package/.vscode/extensions.json +5 -0
- package/.vscode/settings.json +10 -0
- package/Dockerfile +34 -0
- package/README.md +19 -0
- package/craco.config.js +42 -0
- package/docker/nginx/conf.d/default.conf +46 -0
- package/docker-compose.yml +13 -0
- package/package.json +103 -0
- package/public/exampleForm.json +77 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +43 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/rollup.config.mjs +108 -0
- package/src/App.tsx +25 -0
- package/src/Form/Components/FieldCreator.tsx +206 -0
- package/src/Form/Components/FieldLabel.tsx +14 -0
- package/src/Form/Components/Inputs/Boolean.tsx +13 -0
- package/src/Form/Components/Inputs/JSONString.tsx +40 -0
- package/src/Form/Components/Inputs/LongString.tsx +22 -0
- package/src/Form/Components/Inputs/Number.tsx +22 -0
- package/src/Form/Components/Inputs/Object.tsx +56 -0
- package/src/Form/Components/Inputs/RadioGroup.tsx +24 -0
- package/src/Form/Components/Inputs/SingleSelect.tsx +24 -0
- package/src/Form/Components/Inputs/String.tsx +18 -0
- package/src/Form/Components/Inputs/index.tsx +6 -0
- package/src/Form/Components/Inputs/inputMap.ts +23 -0
- package/src/Form/Components/index.tsx +2 -0
- package/src/Form/FormCreator.tsx +62 -0
- package/src/Form/FormCreatorTypes.ts +187 -0
- package/src/Form/FormMappingTypes.ts +17 -0
- package/src/Form/Manage/CopyableJSONOutput.tsx +75 -0
- package/src/Form/Manage/FormConfigInput.tsx +61 -0
- package/src/Form/Manage/FormMappedOutput.tsx +131 -0
- package/src/Form/Manage/FormMappingInput.tsx +60 -0
- package/src/Form/Manage/Manage.tsx +132 -0
- package/src/Form/Manage/RawFormOutput.tsx +20 -0
- package/src/Form/MapTester.tsx +107 -0
- package/src/Form/SchemaToForm.tsx +348 -0
- package/src/Form/formDefinition.json +8 -0
- package/src/Form/helpers.ts +85 -0
- package/src/Form/index.ts +1 -0
- package/src/Form/testData/assetData.json +65 -0
- package/src/Form/testData/exampleParticle.json +112 -0
- package/src/Form/testData/fields.json +151 -0
- package/src/Form/testData/nestedForm.json +156 -0
- package/src/Form/testData/testSchema.json +89 -0
- package/src/SetTester.tsx +61 -0
- package/src/helpers.ts +36 -0
- package/src/index.css +39 -0
- package/src/index.tsx +19 -0
- package/src/library.ts +3 -0
- package/src/reportWebVitals.ts +15 -0
- package/src/state/formAtom.ts +21 -0
- package/src/state/formMappingAtom.ts +21 -0
- package/src/state/formValuesAtom.ts +22 -0
- package/src/types/generate-schema.d.ts +8 -0
- package/tailwind.config.js +11 -0
- package/tsconfig.json +32 -0
- package/tsconfig.paths.json +19 -0
@@ -0,0 +1,107 @@
|
|
1
|
+
import { type IFormField, type IForm } from '@/Form/FormCreatorTypes'
|
2
|
+
import { type IFormMapping } from '@/Form/FormMappingTypes'
|
3
|
+
import { copyAndAddPathToFields, getPathFromField } from '@/Form/helpers'
|
4
|
+
import { CopyButton } from '@/Form/Manage/CopyableJSONOutput'
|
5
|
+
import FormMappingInput from '@/Form/Manage/FormMappingInput'
|
6
|
+
import testForm from '@/Form/testData/nestedForm.json'
|
7
|
+
import { Checkbox, Input, TextArea } from '@axdspub/axiom-ui-utilities'
|
8
|
+
import React, { useEffect, useState, type ReactElement } from 'react'
|
9
|
+
|
10
|
+
const FieldMap = ({ field, mappingState }: { field: IFormField, mappingState: [IFormMapping, (m: IFormMapping) => void] }): ReactElement => {
|
11
|
+
const isContainer = field.type === 'object' || field.type === 'section'
|
12
|
+
const [include, setInclude] = useState<boolean>(true)
|
13
|
+
const [path, setPath] = useState<string | undefined>(undefined)
|
14
|
+
return (
|
15
|
+
<div className={`${isContainer ? 'px-4 py-6 border-2 border-slate-400 border-dotted' : 'px-4 py-1'} m-2 bg-slate-400 [&>.bg-slate-400]:bg-slate-200 [&>.bg-slate-200]:bg-slate-100 bg-opacity-50`}>
|
16
|
+
<p>{field.label}</p>
|
17
|
+
<p className='text-xs'>{`${getPathFromField(field)}`}</p>
|
18
|
+
<div className='flex flex-row p-2'>
|
19
|
+
<Checkbox id={`${field.id}-include`} testId={`${field.id}-include`} value={include} onChange={(e) => {
|
20
|
+
setInclude(e)
|
21
|
+
}} />
|
22
|
+
<Input size='xs' wrapperClassName='flex-grow' id={`${field.id}-target`} testId={`${field.id}-target`} value={path} onChange={(e) => {
|
23
|
+
setPath(e === '' ? undefined : e)
|
24
|
+
}} />
|
25
|
+
</div>
|
26
|
+
{
|
27
|
+
field.type === 'object' && field.fields?.map(f => {
|
28
|
+
return (
|
29
|
+
<FieldMap key={f.id} field={f} mappingState={mappingState} />
|
30
|
+
)
|
31
|
+
})
|
32
|
+
}
|
33
|
+
</div>
|
34
|
+
)
|
35
|
+
}
|
36
|
+
|
37
|
+
const MapTester = (): ReactElement => {
|
38
|
+
const [inputObject, setInputObject] = useState<IForm>(copyAndAddPathToFields(testForm as IForm))
|
39
|
+
const [mapping, setMapping] = useState<IFormMapping>({
|
40
|
+
fields: {},
|
41
|
+
$targetSchema: ''
|
42
|
+
})
|
43
|
+
const [error, setError] = useState<string | undefined>(undefined)
|
44
|
+
const [str, setStr] = useState<string | undefined>(JSON.stringify(inputObject))
|
45
|
+
useEffect(() => {
|
46
|
+
try {
|
47
|
+
const ob = JSON.parse(str === '' || str === undefined ? '{}' : str)
|
48
|
+
setInputObject({
|
49
|
+
fields: [],
|
50
|
+
label: '',
|
51
|
+
id: '',
|
52
|
+
...ob
|
53
|
+
})
|
54
|
+
} catch {
|
55
|
+
setError('Invalid JSON')
|
56
|
+
}
|
57
|
+
}, [str])
|
58
|
+
return (
|
59
|
+
<div className='p-20 h-full'>
|
60
|
+
<h1 className='font-bold'>Map Tester</h1>
|
61
|
+
<div className='grid grid-cols-3 gap-8 flex-grow h-full'>
|
62
|
+
<div className='h-full'>
|
63
|
+
{ error !== undefined
|
64
|
+
? <p className='text-red-500 py-4'>
|
65
|
+
{error}
|
66
|
+
</p>
|
67
|
+
: ''
|
68
|
+
}
|
69
|
+
|
70
|
+
<TextArea
|
71
|
+
label='Input object'
|
72
|
+
id='object'
|
73
|
+
testId='object'
|
74
|
+
wrapperClassName='h-full relative'
|
75
|
+
className='h-full'
|
76
|
+
value={JSON.stringify(inputObject, null, 2)}
|
77
|
+
onChange={(e) => {
|
78
|
+
setStr(e)
|
79
|
+
}}
|
80
|
+
after={<CopyButton string={JSON.stringify(inputObject, null, 2)} className='pointer-events-auto absolute right-8 top-12' />}
|
81
|
+
/>
|
82
|
+
</div>
|
83
|
+
<div>
|
84
|
+
<div className='hidden'>
|
85
|
+
<FormMappingInput form={inputObject} mappingState={[mapping, setMapping]} />
|
86
|
+
</div>
|
87
|
+
<div className='flex flex-col gap-2'>
|
88
|
+
{
|
89
|
+
inputObject.fields.map(f => {
|
90
|
+
return (
|
91
|
+
<FieldMap key={f.id} field={f} mappingState={[mapping, setMapping]} />
|
92
|
+
)
|
93
|
+
})
|
94
|
+
}
|
95
|
+
</div>
|
96
|
+
</div>
|
97
|
+
<div>
|
98
|
+
<p>Mapped output will go here..</p>
|
99
|
+
</div>
|
100
|
+
|
101
|
+
</div>
|
102
|
+
|
103
|
+
</div>
|
104
|
+
)
|
105
|
+
}
|
106
|
+
|
107
|
+
export default MapTester
|
@@ -0,0 +1,348 @@
|
|
1
|
+
import { CopyButton } from '@/Form/Manage/CopyableJSONOutput'
|
2
|
+
import { Tabs, TextArea } from '@axdspub/axiom-ui-utilities'
|
3
|
+
import { type JSONSchema7Type, type JSONSchema7, type JSONSchema7Definition } from 'json-schema'
|
4
|
+
import React, { useEffect, useState, type ReactElement } from 'react'
|
5
|
+
import metaSchemaDraftV7 from 'ajv/lib/refs/json-schema-draft-07.json'
|
6
|
+
import metaSchemaDraftV6 from 'ajv/lib/refs/json-schema-draft-06.json'
|
7
|
+
import metaSchemaV5 from 'ajv/lib/refs/json-schema-2020-12/schema.json'
|
8
|
+
import metaSchemaV4 from 'ajv/lib/refs/json-schema-2019-09/schema.json'
|
9
|
+
import testSchema from '@/Form/testData/testSchema.json'
|
10
|
+
import Ajv, { type ValidateFunction } from 'ajv'
|
11
|
+
import GenerateSchema from 'generate-schema'
|
12
|
+
import { type IFormFieldType, type IForm, type IFormField, type IFormValues } from '@/Form/FormCreatorTypes'
|
13
|
+
import FormCreator from '@/Form/FormCreator'
|
14
|
+
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'
|
15
|
+
|
16
|
+
const getValidator = (schema: number): ValidateFunction => {
|
17
|
+
const ajv = new Ajv({ strict: false })
|
18
|
+
switch (schema) {
|
19
|
+
case 4:
|
20
|
+
return ajv.compile(metaSchemaV4)
|
21
|
+
case 5:
|
22
|
+
return ajv.compile(metaSchemaV5)
|
23
|
+
case 6:
|
24
|
+
return ajv.compile(metaSchemaDraftV6)
|
25
|
+
case 7:
|
26
|
+
return ajv.compile(metaSchemaDraftV7)
|
27
|
+
default:
|
28
|
+
return ajv.compile(metaSchemaV5)
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
export const objectToSchema = (ob: unknown): JSONSchema7 => {
|
33
|
+
return GenerateSchema.json('Schema', ob) as JSONSchema7
|
34
|
+
}
|
35
|
+
|
36
|
+
const validateSchema = (schemaOb: unknown, version: number = 6): string | undefined => {
|
37
|
+
const ajv = new Ajv({ strict: false })
|
38
|
+
const validator = getValidator(version)
|
39
|
+
const valid = validator(schemaOb)
|
40
|
+
if (!valid) {
|
41
|
+
return ajv.errorsText(validator.errors)
|
42
|
+
}
|
43
|
+
return undefined
|
44
|
+
}
|
45
|
+
|
46
|
+
const validateAgainstSchema = (schema: JSONSchema7, formValues: IFormValues): string[] | undefined => {
|
47
|
+
const ajv = new Ajv({ strict: false, allErrors: true })
|
48
|
+
const validator = ajv.compile(schema)
|
49
|
+
const valid = validator(formValues)
|
50
|
+
if (validator.errors !== null && validator.errors !== undefined && !valid) {
|
51
|
+
return validator.errors.map(e => {
|
52
|
+
return `${e.instancePath} ${e.message}`
|
53
|
+
})
|
54
|
+
}
|
55
|
+
return undefined
|
56
|
+
}
|
57
|
+
|
58
|
+
const makeId = (options: Array<string | number | undefined | null>): string => {
|
59
|
+
const validOptions = options.filter((o) => o !== undefined && o !== null)
|
60
|
+
if (validOptions.length === 0) {
|
61
|
+
return crypto !== undefined ? crypto.randomUUID() : Math.random().toString(36).substring(2)
|
62
|
+
}
|
63
|
+
return String(validOptions[0])
|
64
|
+
}
|
65
|
+
|
66
|
+
const makeLabel = (options: Array<string | number | undefined | null>): string | undefined => {
|
67
|
+
const validOptions = options.filter((o) => o !== undefined && o !== null)
|
68
|
+
if (validOptions.length === 0) {
|
69
|
+
return undefined
|
70
|
+
}
|
71
|
+
return String(validOptions[0])
|
72
|
+
.replace(/_/g, ' ')
|
73
|
+
.split(' ')
|
74
|
+
.map((s, i) => {
|
75
|
+
return i === 0 ? `${s.charAt(0).toUpperCase()}${s.slice(1)}` : s
|
76
|
+
}).join(' ')
|
77
|
+
}
|
78
|
+
|
79
|
+
const getFieldType = (schema: JSONSchema7): IFormFieldType => {
|
80
|
+
const schemaType = schema.type
|
81
|
+
if (schemaType === 'string' || schemaType === 'number' || schemaType === 'integer') {
|
82
|
+
if (schema.enum !== undefined || schema.oneOf !== undefined) {
|
83
|
+
return 'select'
|
84
|
+
} else if (schema.anyOf !== undefined) {
|
85
|
+
return 'checkbox'
|
86
|
+
} else if (schemaType === 'string' && (schema.maxLength !== undefined && schema.maxLength <= 100)) {
|
87
|
+
return 'text'
|
88
|
+
} else if (schemaType === 'number' || schemaType === 'integer') {
|
89
|
+
return 'number'
|
90
|
+
}
|
91
|
+
return 'long_text'
|
92
|
+
} else if (schemaType === 'boolean') {
|
93
|
+
return 'boolean'
|
94
|
+
} else if (schemaType === 'object') {
|
95
|
+
return 'object'
|
96
|
+
}
|
97
|
+
|
98
|
+
return 'text'
|
99
|
+
}
|
100
|
+
|
101
|
+
export const getValueFromSchema = (schema: JSONSchema7Type | JSONSchema7Definition | undefined): string | number | boolean | undefined => {
|
102
|
+
if (schema === undefined || schema === null) {
|
103
|
+
return undefined
|
104
|
+
}
|
105
|
+
if (typeof schema === 'string' || typeof schema === 'number' || typeof schema === 'boolean') {
|
106
|
+
return schema
|
107
|
+
}
|
108
|
+
if (Array.isArray(schema)) {
|
109
|
+
if (schema.length > 0) {
|
110
|
+
return getValueFromSchema(schema[0])
|
111
|
+
}
|
112
|
+
return undefined
|
113
|
+
}
|
114
|
+
if (schema.const !== undefined) {
|
115
|
+
return String(schema.const)
|
116
|
+
}
|
117
|
+
return undefined
|
118
|
+
}
|
119
|
+
|
120
|
+
export const getLabelFromSchema = (schema: JSONSchema7Type | JSONSchema7Definition | undefined): string | undefined => {
|
121
|
+
if (schema === undefined || schema === null) {
|
122
|
+
return undefined
|
123
|
+
}
|
124
|
+
if (typeof schema === 'boolean') {
|
125
|
+
return schema ? 'true' : 'false'
|
126
|
+
}
|
127
|
+
if (typeof schema === 'string' || typeof schema === 'number' || typeof schema === 'boolean') {
|
128
|
+
return String(schema)
|
129
|
+
}
|
130
|
+
if (Array.isArray(schema)) {
|
131
|
+
if (schema.length > 0) {
|
132
|
+
return getLabelFromSchema(schema[0])
|
133
|
+
}
|
134
|
+
return undefined
|
135
|
+
}
|
136
|
+
if (schema.title !== undefined && schema.title !== null) {
|
137
|
+
return getLabelFromSchema(schema.title)
|
138
|
+
}
|
139
|
+
return String(getValueFromSchema(schema))
|
140
|
+
}
|
141
|
+
|
142
|
+
const schemaToFormField = (schema: JSONSchema7, property: string, multiple?: boolean): IFormField => {
|
143
|
+
if (schema.type === 'array') {
|
144
|
+
return schemaToFormField(schema.items as JSONSchema7, property, true)
|
145
|
+
}
|
146
|
+
const type = getFieldType(schema)
|
147
|
+
const id = makeId([
|
148
|
+
schema.$id,
|
149
|
+
property,
|
150
|
+
schema.title?.toLowerCase().replace(' ', '-')
|
151
|
+
])
|
152
|
+
const label = makeLabel([
|
153
|
+
schema.title,
|
154
|
+
property
|
155
|
+
])
|
156
|
+
const ob: Pick<IFormField, 'id' | 'label' | 'multiple'> = {
|
157
|
+
id,
|
158
|
+
label,
|
159
|
+
multiple
|
160
|
+
}
|
161
|
+
if (type === 'text' || type === 'number' || type === 'long_text' || type === 'boolean') {
|
162
|
+
return {
|
163
|
+
...ob,
|
164
|
+
type
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|
168
|
+
if (type === 'select' || type === 'checkbox') {
|
169
|
+
const schemaOptions = schema.enum ?? schema.oneOf ?? schema.anyOf ?? []
|
170
|
+
const options = schemaOptions.map(e => {
|
171
|
+
const value = getValueFromSchema(e)
|
172
|
+
const label = getLabelFromSchema(e)
|
173
|
+
return value !== undefined
|
174
|
+
? {
|
175
|
+
value: String(value),
|
176
|
+
label: label ?? String(value)
|
177
|
+
}
|
178
|
+
: null
|
179
|
+
}).filter(d => d !== null)
|
180
|
+
return {
|
181
|
+
...ob,
|
182
|
+
type,
|
183
|
+
options
|
184
|
+
}
|
185
|
+
}
|
186
|
+
if (type === 'object') {
|
187
|
+
const properties = schema.properties ?? {}
|
188
|
+
const fields: IFormField[] = []
|
189
|
+
for (const key in properties) {
|
190
|
+
if (properties[key] !== undefined && typeof properties[key] !== 'boolean') {
|
191
|
+
fields.push(schemaToFormField(properties[key], key))
|
192
|
+
}
|
193
|
+
}
|
194
|
+
return {
|
195
|
+
...ob,
|
196
|
+
type,
|
197
|
+
fields,
|
198
|
+
multiple
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
return {
|
203
|
+
id,
|
204
|
+
type: 'text',
|
205
|
+
multiple
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
export const schemaToFormObject = (schema: JSONSchema7): IForm => {
|
210
|
+
const formFields: IFormField[] = []
|
211
|
+
for (const key in schema.properties) {
|
212
|
+
if (schema.properties[key] !== undefined && typeof schema.properties[key] !== 'boolean') {
|
213
|
+
formFields.push(schemaToFormField(schema.properties[key], key))
|
214
|
+
}
|
215
|
+
}
|
216
|
+
return {
|
217
|
+
id: makeId([schema.$id, schema.title?.toLowerCase().replace(' ', '-')]),
|
218
|
+
label: schema.title ?? 'Untitled',
|
219
|
+
fields: formFields
|
220
|
+
}
|
221
|
+
}
|
222
|
+
|
223
|
+
const SchemaToForm = (): ReactElement => {
|
224
|
+
const [schema, setSchema] = useState<JSONSchema7 | undefined>(undefined)
|
225
|
+
const [form, setForm] = useState<IForm | undefined>(undefined)
|
226
|
+
const [formValues, setFormValues] = useState<IFormValues>({})
|
227
|
+
const [error, setError] = useState<string | undefined>(validateSchema(schema))
|
228
|
+
const [str, setStr] = useState<string | undefined>(JSON.stringify(testSchema, null, 2))
|
229
|
+
const [formOutputErrors, setFormOutputErrors] = useState<string[] | undefined>(undefined)
|
230
|
+
useEffect(() => {
|
231
|
+
if (str !== '' && str !== undefined) {
|
232
|
+
try {
|
233
|
+
const ob = JSON.parse(str)
|
234
|
+
const validationResponse = validateSchema(ob)
|
235
|
+
setError(validationResponse)
|
236
|
+
if (validationResponse === undefined) {
|
237
|
+
setSchema(ob as JSONSchema7)
|
238
|
+
setForm(schemaToFormObject(ob as JSONSchema7))
|
239
|
+
}
|
240
|
+
} catch {
|
241
|
+
setError('Invalid JSON')
|
242
|
+
}
|
243
|
+
} else {
|
244
|
+
setSchema(undefined)
|
245
|
+
setForm(undefined)
|
246
|
+
}
|
247
|
+
}, [str])
|
248
|
+
useEffect(() => {
|
249
|
+
setFormOutputErrors(validateAgainstSchema(schema ?? {}, formValues))
|
250
|
+
}, [formValues])
|
251
|
+
return (
|
252
|
+
<div className='flex flex-col h-full gap-4 p-20'>
|
253
|
+
<h1 className='text-2xl'>Schema to Form</h1>
|
254
|
+
<p>
|
255
|
+
This page will allow you to convert a JSON schema to a form UI schema
|
256
|
+
</p>
|
257
|
+
<div className='grid grid-cols-2 gap-8 flex-grow'>
|
258
|
+
<div className='h-full bg-slate-100 p-8'>
|
259
|
+
{
|
260
|
+
form === undefined
|
261
|
+
? <p>Waiting on valid schema</p>
|
262
|
+
: <Tabs
|
263
|
+
tabs={[
|
264
|
+
{
|
265
|
+
id: 'form',
|
266
|
+
label: 'Form',
|
267
|
+
content: <FormCreator form={form} formValueState={ [formValues, setFormValues]} />
|
268
|
+
},
|
269
|
+
{
|
270
|
+
id: 'output',
|
271
|
+
label: <>Form output {formOutputErrors !== undefined ? <ExclamationTriangleIcon className='inline ml-2' /> : ''}</>,
|
272
|
+
content: <div>{
|
273
|
+
schema !== undefined
|
274
|
+
? <>
|
275
|
+
<p>{formOutputErrors !== undefined
|
276
|
+
? <>Errors: <ul className='text-rose-800 text-xs list-disc p-4'>{
|
277
|
+
formOutputErrors.map((e) => {
|
278
|
+
return <li key={e}>{e}</li>
|
279
|
+
})
|
280
|
+
}</ul></>
|
281
|
+
: 'Form output is valid'}</p>
|
282
|
+
<div className='p-10 relative bg-yellow-200'>
|
283
|
+
<CopyButton string={JSON.stringify(form ?? '', null, 2)} className='absolute right-10 top-10 pointer-events-auto' />
|
284
|
+
<pre>{JSON.stringify(formValues ?? '', null, 2)}</pre>
|
285
|
+
</div>
|
286
|
+
</>
|
287
|
+
: 'No schema'
|
288
|
+
}
|
289
|
+
</div>
|
290
|
+
}
|
291
|
+
]}
|
292
|
+
/>
|
293
|
+
}
|
294
|
+
</div>
|
295
|
+
<div className='h-full bg-slate-100 p-8 overflow-auto'>
|
296
|
+
<div className='flex flex-col gap-2'>
|
297
|
+
<p>Schema</p>
|
298
|
+
|
299
|
+
<p className={`${error !== undefined ? 'text-rose-800' : 'text-green-800'}`}>
|
300
|
+
{error ?? 'No errors'}
|
301
|
+
</p>
|
302
|
+
|
303
|
+
<div className='relative'>
|
304
|
+
<CopyButton string={JSON.stringify(schema, null, 2)} className='absolute right-10 top-10 pointer-events-auto' />
|
305
|
+
<TextArea
|
306
|
+
id='schemaInput'
|
307
|
+
testId='schemaInput'
|
308
|
+
value={JSON.stringify(schema, null, 2)}
|
309
|
+
className={`h-full mt-0 w-full flex-grow min-h-[600px] shadow-inner-x ${error !== undefined ? 'bg-rose-100' : 'bg-green-100'}`}
|
310
|
+
onChange={(e) => {
|
311
|
+
setStr(e)
|
312
|
+
}}
|
313
|
+
/>
|
314
|
+
</div>
|
315
|
+
</div>
|
316
|
+
<div className='flex flex-col gap-2'>
|
317
|
+
|
318
|
+
<p>UI Config</p>
|
319
|
+
<div className='relative'>
|
320
|
+
{
|
321
|
+
form !== undefined
|
322
|
+
? <>
|
323
|
+
<CopyButton string={JSON.stringify(form ?? '', null, 2)} className='absolute right-10 top-10 pointer-events-auto' />
|
324
|
+
<TextArea
|
325
|
+
id='formInput'
|
326
|
+
testId='formInput'
|
327
|
+
value={JSON.stringify(form, null, 2)}
|
328
|
+
className='h-full mt-0 w-full flex-grow min-h-[600px] shadow-inner-x bg-blue-900 text-white'
|
329
|
+
onChange={(e) => {
|
330
|
+
setForm(e !== undefined ? JSON.parse(e) : undefined)
|
331
|
+
}}
|
332
|
+
/>
|
333
|
+
|
334
|
+
</>
|
335
|
+
: 'Waiting on valid schema'
|
336
|
+
}
|
337
|
+
|
338
|
+
</div>
|
339
|
+
|
340
|
+
</div>
|
341
|
+
|
342
|
+
</div>
|
343
|
+
</div>
|
344
|
+
|
345
|
+
</div>
|
346
|
+
)
|
347
|
+
}
|
348
|
+
export default SchemaToForm
|
@@ -0,0 +1,85 @@
|
|
1
|
+
import { type IFormFieldSection, type IObjectField, type IForm, type IFormField, type IValueType, type IFormValues } from '@/Form/FormCreatorTypes'
|
2
|
+
|
3
|
+
export const getChildFields = (field: IFormField): IFormField[] => {
|
4
|
+
return field.type === 'object' || field.type === 'section' ? field.fields ?? [] : []
|
5
|
+
}
|
6
|
+
|
7
|
+
export const addFieldPath = (field: IFormField, parentPath?: string[]): IFormField => {
|
8
|
+
if (field.type === 'object' && field.skip_path === true) {
|
9
|
+
field.path = parentPath !== undefined ? parentPath.slice() : []
|
10
|
+
field.level = parentPath !== undefined ? parentPath.length : 0
|
11
|
+
} else {
|
12
|
+
const newSegment = field.id // `${field.id}${field.multiple === true ? '[]' : ''}`
|
13
|
+
field.path = parentPath !== undefined ? parentPath.concat(newSegment) : [newSegment]
|
14
|
+
field.level = parentPath !== undefined ? parentPath.length + 1 : 1
|
15
|
+
}
|
16
|
+
if ((field.type === 'object' || field.type === 'section') && field.fields !== undefined) {
|
17
|
+
field.fields = field.fields.map(childField => {
|
18
|
+
return addFieldPath(childField, field.path)
|
19
|
+
})
|
20
|
+
}
|
21
|
+
return field
|
22
|
+
}
|
23
|
+
|
24
|
+
export const getUniqueFormFields = (form: IForm): IFormField[] => {
|
25
|
+
const fieldMap = Object.fromEntries(form.fields.map(f => [f.id, f]))
|
26
|
+
return Object.values(fieldMap)
|
27
|
+
}
|
28
|
+
|
29
|
+
export const getFields = (fields: IFormField[]): IFormField[] => {
|
30
|
+
const all = fields.map(field => {
|
31
|
+
let fields = [field]
|
32
|
+
const children = getChildFields(field)
|
33
|
+
children.forEach(c => {
|
34
|
+
fields = fields.concat(getFields([c]))
|
35
|
+
})
|
36
|
+
return fields
|
37
|
+
}).flat(Infinity) as IFormField[]
|
38
|
+
return all
|
39
|
+
}
|
40
|
+
|
41
|
+
export function copyAndAddPathToFields<T extends IForm | IFormFieldSection | IObjectField> (formOrContainer: T): T {
|
42
|
+
const form = JSON.parse(JSON.stringify(formOrContainer)) as T
|
43
|
+
// const fields = getFields(form.fields)
|
44
|
+
form.fields = form.fields.map(field => {
|
45
|
+
return addFieldPath(field)
|
46
|
+
})
|
47
|
+
return form
|
48
|
+
}
|
49
|
+
|
50
|
+
export function getFieldValue (field: IFormField, formValues: IFormValues): IValueType | IValueType[] | undefined {
|
51
|
+
return formValues[getPathFromField(field)]
|
52
|
+
}
|
53
|
+
|
54
|
+
export function getPathFromField (field: IFormField): string {
|
55
|
+
// console.log(`${field.path !== undefined ? field.path.join('.') : 'nopath'} = ${field.id}`)
|
56
|
+
return field.path !== undefined ? field.path.join('.') : field.id
|
57
|
+
}
|
58
|
+
|
59
|
+
// THIS DOESN'T HANDLE NESTED YET
|
60
|
+
export const checkCondition = (field: IFormField, formValues: IFormValues): boolean => {
|
61
|
+
if (field.conditions !== undefined) {
|
62
|
+
const dependsOn = Array.isArray(field.conditions.dependsOn) ? field.conditions.dependsOn : [field.conditions.dependsOn]
|
63
|
+
|
64
|
+
const val = field.conditions.value
|
65
|
+
return dependsOn.every(d => val !== undefined
|
66
|
+
? formValues[d] === val
|
67
|
+
: formValues !== null && formValues[d] !== undefined && formValues[d] !== false
|
68
|
+
)
|
69
|
+
}
|
70
|
+
return true
|
71
|
+
}
|
72
|
+
|
73
|
+
export function cleanUnusedDependenciesFromFormValues (form: IForm, formValues: IFormValues): IFormValues {
|
74
|
+
Object.keys(formValues).forEach(key => {
|
75
|
+
const field = form.fields.find(f => f.id === key)
|
76
|
+
if (field !== undefined && !checkCondition(field, formValues)) {
|
77
|
+
formValues[key] = undefined
|
78
|
+
}
|
79
|
+
})
|
80
|
+
|
81
|
+
const fields = getFields(form.fields)
|
82
|
+
const fieldIds = fields.map(f => f.id)
|
83
|
+
const newFormValues = Object.fromEntries(Object.entries(formValues).filter(([key]) => fieldIds.includes(key)))
|
84
|
+
return newFormValues
|
85
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from '@/Form/helpers'
|
@@ -0,0 +1,65 @@
|
|
1
|
+
{
|
2
|
+
"source_descriptors": {
|
3
|
+
"human":{
|
4
|
+
"label":"Human",
|
5
|
+
"user":"Trevor Golden",
|
6
|
+
"update":"2024-04-20T12:31.000Z",
|
7
|
+
"create":"2022-05-23T09:32.000Z",
|
8
|
+
"priority": 1
|
9
|
+
},
|
10
|
+
"netcdf":{
|
11
|
+
"label":"NetCDF",
|
12
|
+
"update": "2023-11-02T12:31.000Z",
|
13
|
+
"create": "2022-05-23T09:32.000Z",
|
14
|
+
"priority": 2
|
15
|
+
},
|
16
|
+
"csv":{
|
17
|
+
"label":"Messy CSV",
|
18
|
+
"update": "2023-13-12T12:31.000Z",
|
19
|
+
"create": "2022-06-23T09:32.000Z",
|
20
|
+
"priority": 3
|
21
|
+
}
|
22
|
+
},
|
23
|
+
"fields": [
|
24
|
+
{
|
25
|
+
"id": "label",
|
26
|
+
"values": {
|
27
|
+
"netcdf": "Label from the NetCDF file",
|
28
|
+
"csv": "Label from the CSV file",
|
29
|
+
"human": "Label from the human"
|
30
|
+
}
|
31
|
+
},
|
32
|
+
{
|
33
|
+
"id": "description",
|
34
|
+
"values": {
|
35
|
+
"netcdf": "Description from the NetCDF file",
|
36
|
+
"csv": "Description from the CSV file",
|
37
|
+
"human": null
|
38
|
+
}
|
39
|
+
},
|
40
|
+
{
|
41
|
+
"id": "yesno",
|
42
|
+
"values": {
|
43
|
+
"netcdf": null,
|
44
|
+
"csv": true,
|
45
|
+
"human": false
|
46
|
+
}
|
47
|
+
},
|
48
|
+
{
|
49
|
+
"id": "onecolor",
|
50
|
+
"values": {
|
51
|
+
"netcdf": "red",
|
52
|
+
"csv": "green",
|
53
|
+
"human": "blue"
|
54
|
+
}
|
55
|
+
},
|
56
|
+
{
|
57
|
+
"id": "manychoices",
|
58
|
+
"values": {
|
59
|
+
"netcdf": "red",
|
60
|
+
"csv": "green",
|
61
|
+
"human": "blue"
|
62
|
+
}
|
63
|
+
}
|
64
|
+
]
|
65
|
+
}
|