@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,187 @@
|
|
1
|
+
import type { GeoJSON } from 'geojson'
|
2
|
+
|
3
|
+
interface IValueTypes {
|
4
|
+
text: string
|
5
|
+
number: number
|
6
|
+
date: string
|
7
|
+
datetime: string
|
8
|
+
time: string
|
9
|
+
boolean: boolean
|
10
|
+
geojson: GeoJSON
|
11
|
+
json: JSON
|
12
|
+
composite: ICompositeValueType
|
13
|
+
}
|
14
|
+
|
15
|
+
type ValueOf<T> = T[keyof T]
|
16
|
+
|
17
|
+
// export type ICompositeValueType = Record<string, string | number | boolean>
|
18
|
+
// type can't reference self
|
19
|
+
// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
|
20
|
+
export interface ICompositeValueType {
|
21
|
+
[key: string]: IValueType | IValueType[] | undefined
|
22
|
+
}
|
23
|
+
export type IValueType = undefined | null | ValueOf<IValueTypes> // | Array<ValueOf<IValueTypes>> | { [key: string]: IValueType } | Array<Record<string, IValueTypes>>
|
24
|
+
// export type IValueType2 = string | string[] | number | number[] | boolean | boolean[] | ICompositeValueType | ICompositeValueType[]
|
25
|
+
|
26
|
+
export type IFormField = ITextField | ILongTextField | IJSONField | ISelectField | IRadioField | ICheckboxField | IDateField | ITimeField | IDateTimeField | IBooleanField | IObjectField | IGeoJSONField | IFormFieldSection
|
27
|
+
|
28
|
+
export type IFormFieldType = 'section' | 'text' | 'long_text' | 'number' | 'json' | 'select' | 'radio' | 'checkbox' | 'date' | 'time' | 'datetime' | 'boolean' | 'object' | 'geojson'
|
29
|
+
interface IFieldConditions {
|
30
|
+
dependsOn: string | string[]
|
31
|
+
value: string | number | boolean
|
32
|
+
}
|
33
|
+
|
34
|
+
interface IFormFieldRoot {
|
35
|
+
id: string
|
36
|
+
type: IFormFieldType
|
37
|
+
required?: boolean
|
38
|
+
label?: string | null | undefined
|
39
|
+
multiple?: boolean
|
40
|
+
path?: string[]
|
41
|
+
fullPath?: string[]
|
42
|
+
level?: number
|
43
|
+
value?: IValueType
|
44
|
+
conditions?: IFieldConditions
|
45
|
+
}
|
46
|
+
|
47
|
+
interface IStringValueInput extends IFormFieldRoot {
|
48
|
+
value?: 'text' | 'number'
|
49
|
+
placeholder?: string
|
50
|
+
}
|
51
|
+
|
52
|
+
interface ITextField extends IStringValueInput {
|
53
|
+
type: 'text' | 'number'
|
54
|
+
}
|
55
|
+
|
56
|
+
interface ILongTextField extends IStringValueInput {
|
57
|
+
type: 'long_text'
|
58
|
+
}
|
59
|
+
|
60
|
+
interface IJSONField extends IFormFieldRoot {
|
61
|
+
type: 'json'
|
62
|
+
}
|
63
|
+
|
64
|
+
interface ISelectOption {
|
65
|
+
label: string
|
66
|
+
value: string
|
67
|
+
}
|
68
|
+
|
69
|
+
interface ISelectableInput extends IFormFieldRoot {
|
70
|
+
options?: ISelectOption[]
|
71
|
+
options_source?: {
|
72
|
+
type: 'url'
|
73
|
+
url: string
|
74
|
+
method?: 'GET' | 'POST'
|
75
|
+
headers?: Record<string, string>
|
76
|
+
body?: Record<string, string>
|
77
|
+
value_key: string
|
78
|
+
label_key: string
|
79
|
+
}
|
80
|
+
}
|
81
|
+
|
82
|
+
interface ISingleSelectableInput extends ISelectableInput {
|
83
|
+
value?: 'text' | 'number'
|
84
|
+
}
|
85
|
+
|
86
|
+
interface IMultiSelectableInput extends ISelectableInput {
|
87
|
+
values?: Array<'text' | 'number'>
|
88
|
+
}
|
89
|
+
|
90
|
+
export interface ISelectField extends ISingleSelectableInput {
|
91
|
+
type: 'select'
|
92
|
+
}
|
93
|
+
|
94
|
+
export interface IRadioField extends ISingleSelectableInput {
|
95
|
+
type: 'radio'
|
96
|
+
layout?: 'horizontal' | 'vertical'
|
97
|
+
}
|
98
|
+
|
99
|
+
export interface ICheckboxField extends IMultiSelectableInput {
|
100
|
+
type: 'checkbox'
|
101
|
+
}
|
102
|
+
|
103
|
+
export interface IBooleanField extends IFormFieldRoot {
|
104
|
+
type: 'boolean'
|
105
|
+
value?: 'boolean'
|
106
|
+
}
|
107
|
+
|
108
|
+
interface IDateField extends IFormFieldRoot {
|
109
|
+
type: 'date'
|
110
|
+
value?: 'date'
|
111
|
+
}
|
112
|
+
|
113
|
+
interface ITimeField extends IFormFieldRoot {
|
114
|
+
type: 'time'
|
115
|
+
value?: 'time'
|
116
|
+
}
|
117
|
+
|
118
|
+
interface IDateTimeField extends IFormFieldRoot {
|
119
|
+
type: 'datetime'
|
120
|
+
value?: 'datetime'
|
121
|
+
}
|
122
|
+
|
123
|
+
interface IContainerField extends IFormFieldRoot {
|
124
|
+
skip_path?: boolean
|
125
|
+
fields: IFormField[]
|
126
|
+
layout?: 'horizontal' | 'vertical' | 'grid2' | 'grid3' | 'grid4'
|
127
|
+
}
|
128
|
+
|
129
|
+
export interface IObjectField extends IContainerField {
|
130
|
+
type: 'object'
|
131
|
+
}
|
132
|
+
|
133
|
+
export interface IFormFieldSection extends IContainerField {
|
134
|
+
type: 'section'
|
135
|
+
description?: string
|
136
|
+
multiple: false
|
137
|
+
value: undefined
|
138
|
+
values: undefined
|
139
|
+
}
|
140
|
+
|
141
|
+
interface IGeoJSONField extends IFormFieldRoot {
|
142
|
+
type: 'geojson'
|
143
|
+
value?: 'geojson'
|
144
|
+
exclude_types?: string[]
|
145
|
+
include_types?: string[]
|
146
|
+
}
|
147
|
+
|
148
|
+
export interface IPage {
|
149
|
+
id: string
|
150
|
+
label: string
|
151
|
+
description?: string
|
152
|
+
sections: IFormFieldSection[]
|
153
|
+
|
154
|
+
}
|
155
|
+
|
156
|
+
export interface IForm {
|
157
|
+
id: string
|
158
|
+
label: string
|
159
|
+
description?: string
|
160
|
+
fields: IFormField[]
|
161
|
+
}
|
162
|
+
|
163
|
+
export interface IFormWithPages {
|
164
|
+
id: string
|
165
|
+
label: string
|
166
|
+
description?: string
|
167
|
+
pages: IPage[]
|
168
|
+
}
|
169
|
+
|
170
|
+
export interface IFormFieldProps {
|
171
|
+
field: IFormField
|
172
|
+
onChange?: IValueChangeFn
|
173
|
+
}
|
174
|
+
|
175
|
+
export type IFormValues = Record<string, IValueType | IValueType[]>
|
176
|
+
|
177
|
+
export type IFormInputComponent = React.FC<IFormFieldProps>
|
178
|
+
|
179
|
+
export type IValueChangeFn = (v: IValueType | IValueType[] | undefined) => void
|
180
|
+
|
181
|
+
export interface IFieldInputProps {
|
182
|
+
form: IForm
|
183
|
+
field: IFormField
|
184
|
+
onChange: IValueChangeFn
|
185
|
+
formValueState: [IFormValues, (v: IFormValues) => void]
|
186
|
+
value?: IValueType
|
187
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
export interface IFormMapping {
|
2
|
+
fields: Record<string, IFieldMapping>
|
3
|
+
$targetSchema: string
|
4
|
+
}
|
5
|
+
|
6
|
+
interface IFieldMapping {
|
7
|
+
xpath: string
|
8
|
+
}
|
9
|
+
|
10
|
+
export const example: IFormMapping = {
|
11
|
+
$targetSchema: 'testAsset',
|
12
|
+
fields: {
|
13
|
+
label: {
|
14
|
+
xpath: '/label'
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import { utils } from '@axdspub/axiom-ui-utilities'
|
2
|
+
import { CheckIcon, CopyIcon } from '@radix-ui/react-icons'
|
3
|
+
import React, { type ReactElement, useState } from 'react'
|
4
|
+
|
5
|
+
export const CopyButton = ({
|
6
|
+
string,
|
7
|
+
size = 'med',
|
8
|
+
defaultClassName = 'text-lg text-slate-400 pointer-events-none',
|
9
|
+
className,
|
10
|
+
defaultWrapperClassName,
|
11
|
+
wrapperClassName
|
12
|
+
}: {
|
13
|
+
string: string
|
14
|
+
defaultClassName?: string
|
15
|
+
className?: string
|
16
|
+
defaultWrapperClassName?: string
|
17
|
+
wrapperClassName?: string
|
18
|
+
size?: 'sm' | 'med' | 'lg' | 'xlg'
|
19
|
+
}): ReactElement => {
|
20
|
+
const [copied, setCopied] = useState(false)
|
21
|
+
return (
|
22
|
+
<button className={utils.makeClassName({
|
23
|
+
className: wrapperClassName,
|
24
|
+
defaultClassName: defaultWrapperClassName
|
25
|
+
})} onClick={() => {
|
26
|
+
navigator.clipboard.writeText(string)
|
27
|
+
.then(() => {
|
28
|
+
setCopied(true)
|
29
|
+
setTimeout(() => {
|
30
|
+
setCopied(false)
|
31
|
+
}, 1000)
|
32
|
+
})
|
33
|
+
.catch(e => {
|
34
|
+
console.log('Error!')
|
35
|
+
})
|
36
|
+
}}>
|
37
|
+
{copied
|
38
|
+
? <span className={utils.makeClassName({
|
39
|
+
className,
|
40
|
+
defaultClassName
|
41
|
+
})}><CheckIcon className={utils.makeClassName({
|
42
|
+
className: 'bg-slate-600 text-white rounded-full',
|
43
|
+
extras: [utils.getIconClassForSize(size)]
|
44
|
+
})} /></span>
|
45
|
+
: <CopyIcon className={utils.makeClassName({
|
46
|
+
className,
|
47
|
+
defaultClassName,
|
48
|
+
extras: [utils.getIconClassForSize(size)]
|
49
|
+
})} />}
|
50
|
+
|
51
|
+
<span className='sr-only'>Copy</span>
|
52
|
+
</button>
|
53
|
+
)
|
54
|
+
}
|
55
|
+
export const CopyableJSONOutput = ({ string, label }: { string: string, label: string }): ReactElement => {
|
56
|
+
return <div>
|
57
|
+
{label !== undefined
|
58
|
+
? <h2 className='text-lg pb-4 font-bold'>{label}</h2>
|
59
|
+
: ''}
|
60
|
+
<div className='relative' onClick={() => {
|
61
|
+
navigator.clipboard.writeText(string)
|
62
|
+
.then(() => {
|
63
|
+
console.log('Copied!')
|
64
|
+
})
|
65
|
+
.catch(e => {
|
66
|
+
console.log('Error!')
|
67
|
+
})
|
68
|
+
}}>
|
69
|
+
<CopyButton string={string} className='text-slate-400 absolute top-4 right-4' wrapperClassName='absolute top-0 right-0 bottom-0 left-0' />
|
70
|
+
<pre className='p-10 bg-slate-200 hover:bg-slate-300 text-slate-600 select-none cursor-pointer'>
|
71
|
+
{string}
|
72
|
+
</pre>
|
73
|
+
</div>
|
74
|
+
</div>
|
75
|
+
}
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import { type IForm, type IFormField } from '@/Form/FormCreatorTypes'
|
2
|
+
import { CopyButton } from '@/Form/Manage/CopyableJSONOutput'
|
3
|
+
import { TextArea } from '@axdspub/axiom-ui-utilities'
|
4
|
+
import React, { type ReactElement, useState, useEffect } from 'react'
|
5
|
+
|
6
|
+
const validateForm = (form: IForm): string | undefined => {
|
7
|
+
if (form.label === undefined) {
|
8
|
+
return ('Label is required')
|
9
|
+
}
|
10
|
+
if (form.fields === undefined) {
|
11
|
+
return ('At least one field is required')
|
12
|
+
}
|
13
|
+
if (form.fields.length > Object.keys(Object.fromEntries(form.fields.map((field: IFormField) => [field.id, field]))).length) {
|
14
|
+
return ('Field IDs must be unique')
|
15
|
+
}
|
16
|
+
return undefined
|
17
|
+
}
|
18
|
+
|
19
|
+
const FormConfigInput = ({ formState }: { formState: [IForm, (form: IForm) => void] }): ReactElement => {
|
20
|
+
const [form, setForm] = formState
|
21
|
+
const [error, setError] = useState<string | undefined>(validateForm(form))
|
22
|
+
const [str, setStr] = useState<string | undefined>(undefined)
|
23
|
+
useEffect(() => {
|
24
|
+
if (str !== '' && str !== undefined) {
|
25
|
+
try {
|
26
|
+
const ob = JSON.parse(str)
|
27
|
+
const newError = validateForm(ob)
|
28
|
+
setError(newError)
|
29
|
+
if (newError === undefined) {
|
30
|
+
setForm(ob)
|
31
|
+
}
|
32
|
+
} catch {
|
33
|
+
setError('Invalid JSON')
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}, [str])
|
37
|
+
return (
|
38
|
+
<div className='h-full flex flex-col'>
|
39
|
+
{ error !== undefined
|
40
|
+
? <p className='text-red-500 py-4'>
|
41
|
+
{error}
|
42
|
+
</p>
|
43
|
+
: ''
|
44
|
+
}
|
45
|
+
<div className='h-full relative'>
|
46
|
+
<CopyButton string={JSON.stringify(form, null, 2)} className='absolute right-10 top-10 pointer-events-auto' />
|
47
|
+
<TextArea
|
48
|
+
id='formManager'
|
49
|
+
testId='formManager'
|
50
|
+
value={JSON.stringify(form, null, 2)}
|
51
|
+
className='h-full mt-0 w-full flex-grow min-h-[600px] shadow-inner-xl bg-slate-100'
|
52
|
+
onChange={(e) => {
|
53
|
+
setStr(e)
|
54
|
+
}}
|
55
|
+
/>
|
56
|
+
</div>
|
57
|
+
</div>
|
58
|
+
)
|
59
|
+
}
|
60
|
+
|
61
|
+
export default FormConfigInput
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import set from 'lodash/set'
|
2
|
+
import React, { type ReactElement, useEffect, useState } from 'react'
|
3
|
+
|
4
|
+
import { CheckIcon, CopyIcon } from '@radix-ui/react-icons'
|
5
|
+
import { useAtom } from 'jotai'
|
6
|
+
|
7
|
+
import { utils } from '@axdspub/axiom-ui-utilities'
|
8
|
+
import formValuesAtom from '@/state/formValuesAtom'
|
9
|
+
import { copyAndAddPathToFields, getFields } from '@/Form/helpers'
|
10
|
+
import { type IForm } from '@/Form/FormCreatorTypes'
|
11
|
+
import { type IFormMapping } from '@/Form/FormMappingTypes'
|
12
|
+
|
13
|
+
interface IOutputRecord {
|
14
|
+
[key: string]: IOutputRecord | string | null | number
|
15
|
+
}
|
16
|
+
|
17
|
+
const CopyButton = ({
|
18
|
+
string,
|
19
|
+
size = 'med',
|
20
|
+
defaultClassName = 'text-lg text-slate-400 pointer-events-none',
|
21
|
+
className,
|
22
|
+
defaultWrapperClassName,
|
23
|
+
wrapperClassName
|
24
|
+
}: {
|
25
|
+
string: string
|
26
|
+
defaultClassName?: string
|
27
|
+
className?: string
|
28
|
+
defaultWrapperClassName?: string
|
29
|
+
wrapperClassName?: string
|
30
|
+
size?: 'sm' | 'med' | 'lg' | 'xlg'
|
31
|
+
}): ReactElement => {
|
32
|
+
const [copied, setCopied] = useState(false)
|
33
|
+
return (
|
34
|
+
<button className={utils.makeClassName({
|
35
|
+
className: wrapperClassName,
|
36
|
+
defaultClassName: defaultWrapperClassName
|
37
|
+
})} onClick={() => {
|
38
|
+
navigator.clipboard.writeText(JSON.stringify(string, null, 2))
|
39
|
+
.then(() => {
|
40
|
+
setCopied(true)
|
41
|
+
setTimeout(() => {
|
42
|
+
setCopied(false)
|
43
|
+
}, 1000)
|
44
|
+
})
|
45
|
+
.catch(e => {
|
46
|
+
console.log('Error!')
|
47
|
+
})
|
48
|
+
}
|
49
|
+
}>
|
50
|
+
{
|
51
|
+
copied
|
52
|
+
? <span className={utils.makeClassName({
|
53
|
+
className,
|
54
|
+
defaultClassName
|
55
|
+
})}><CheckIcon className={
|
56
|
+
utils.makeClassName({
|
57
|
+
className: 'bg-slate-600 text-white rounded-full',
|
58
|
+
extras: [utils.getIconClassForSize(size)]
|
59
|
+
})}/></span>
|
60
|
+
: <CopyIcon className={utils.makeClassName({
|
61
|
+
className,
|
62
|
+
defaultClassName,
|
63
|
+
extras: [utils.getIconClassForSize(size)]
|
64
|
+
})} />
|
65
|
+
}
|
66
|
+
|
67
|
+
<span className='sr-only'>Copy</span>
|
68
|
+
</button>
|
69
|
+
)
|
70
|
+
}
|
71
|
+
|
72
|
+
const CopyableJSONOutput = ({ json, label }: { json: string, label: string }): ReactElement => {
|
73
|
+
return <div>
|
74
|
+
{
|
75
|
+
label !== undefined
|
76
|
+
? <h2 className='text-lg pb-4 font-bold'>{label}</h2>
|
77
|
+
: ''
|
78
|
+
}
|
79
|
+
<div className='relative' onClick={() => {
|
80
|
+
navigator.clipboard.writeText(json)
|
81
|
+
.then(() => {
|
82
|
+
console.log('Copied!')
|
83
|
+
})
|
84
|
+
.catch(e => {
|
85
|
+
console.log('Error!')
|
86
|
+
})
|
87
|
+
}}>
|
88
|
+
<CopyButton string={json} className='text-slate-400 absolute top-4 right-4' wrapperClassName='absolute top-0 right-0 bottom-0 left-0' />
|
89
|
+
<pre className='p-10 bg-slate-200 hover:bg-slate-300 text-slate-600 select-none cursor-pointer'>
|
90
|
+
{json}
|
91
|
+
</pre>
|
92
|
+
</div>
|
93
|
+
</div>
|
94
|
+
}
|
95
|
+
|
96
|
+
const MappedOutput = ({
|
97
|
+
form,
|
98
|
+
formMapping
|
99
|
+
}: {
|
100
|
+
form: IForm
|
101
|
+
formMapping: IFormMapping
|
102
|
+
}): ReactElement => {
|
103
|
+
const [formValues] = useAtom(formValuesAtom)
|
104
|
+
const [output, setOutput] = useState<IOutputRecord | undefined>(undefined)
|
105
|
+
const [flatOutput, setFlatOutput] = useState<IOutputRecord | undefined>(undefined)
|
106
|
+
|
107
|
+
useEffect(() => {
|
108
|
+
let newOutput: IOutputRecord = {}
|
109
|
+
const newFlatOutput: IOutputRecord = {}
|
110
|
+
const { fields } = copyAndAddPathToFields<IForm>(form)
|
111
|
+
const flatFields = getFields(fields)
|
112
|
+
flatFields.forEach(field => {
|
113
|
+
const idPath = field.path?.join('.') ?? field.id
|
114
|
+
const path = formMapping.fields[idPath]?.xpath ?? field.id
|
115
|
+
const value = formValues[idPath]
|
116
|
+
newOutput = set(newOutput, path, value ?? null)
|
117
|
+
newFlatOutput[idPath] = (value === null || value === undefined) ? null : isNaN(+value) ? String(value) : Number(value)
|
118
|
+
})
|
119
|
+
|
120
|
+
setOutput(newOutput)
|
121
|
+
setFlatOutput(newFlatOutput)
|
122
|
+
}, [form, formValues, formMapping])
|
123
|
+
return (<div className='flex flex-col gap-8'>
|
124
|
+
<CopyableJSONOutput json={JSON.stringify(output, null, 2)} label='Output' />
|
125
|
+
<CopyableJSONOutput json={JSON.stringify(flatOutput, null, 2)} label='Flat Output' />
|
126
|
+
|
127
|
+
</div>
|
128
|
+
)
|
129
|
+
}
|
130
|
+
|
131
|
+
export default MappedOutput
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import { type IForm } from '@/Form/FormCreatorTypes'
|
2
|
+
import { type IFormMapping } from '@/Form/FormMappingTypes'
|
3
|
+
import { addFieldPath, getFields } from '@/Form/helpers'
|
4
|
+
import { Input } from '@axdspub/axiom-ui-utilities'
|
5
|
+
import React, { type ReactElement } from 'react'
|
6
|
+
|
7
|
+
const FormMappingInput = ({
|
8
|
+
form,
|
9
|
+
mappingState
|
10
|
+
}: {
|
11
|
+
form: IForm
|
12
|
+
mappingState: [IFormMapping, (mapping: IFormMapping) => void]
|
13
|
+
}): ReactElement => {
|
14
|
+
const [mapping, setMappings] = mappingState
|
15
|
+
const uniqueFields = getFields(form.fields.map(f => addFieldPath(structuredClone(f))))
|
16
|
+
return (
|
17
|
+
<>
|
18
|
+
<div className='flex flex-col gap-4'>
|
19
|
+
{
|
20
|
+
uniqueFields.filter(f => f.type !== 'object' && f.type !== 'section').map(field => {
|
21
|
+
const fieldId = field.path?.join('.') ?? field.id
|
22
|
+
return (
|
23
|
+
<div key={field.id} className='flex flex-col gap-2'>
|
24
|
+
<div>
|
25
|
+
<Input
|
26
|
+
placeholder='Output path'
|
27
|
+
label={<span className='pb-2'>
|
28
|
+
<span className='text-xs bg-slate-100 p-1 float-right text-rose-700'>{fieldId}</span>
|
29
|
+
{field.label}
|
30
|
+
</span>}
|
31
|
+
id={`map:${fieldId}`} testId={`map:${fieldId}`} value={mapping.fields?.[fieldId]?.xpath}
|
32
|
+
onChange={(e) => {
|
33
|
+
if (e !== undefined && e !== '') {
|
34
|
+
setMappings({
|
35
|
+
...mapping,
|
36
|
+
fields: {
|
37
|
+
...mapping.fields,
|
38
|
+
[fieldId]: {
|
39
|
+
...mapping.fields[fieldId],
|
40
|
+
xpath: e
|
41
|
+
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
})
|
46
|
+
}
|
47
|
+
}}
|
48
|
+
/>
|
49
|
+
</div>
|
50
|
+
|
51
|
+
</div>
|
52
|
+
)
|
53
|
+
})
|
54
|
+
}
|
55
|
+
</div>
|
56
|
+
</>
|
57
|
+
)
|
58
|
+
}
|
59
|
+
|
60
|
+
export default FormMappingInput
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import { Button, MultiAccordion, Tabs } from '@axdspub/axiom-ui-utilities'
|
2
|
+
import React, { type ReactNode, useState, type ReactElement } from 'react'
|
3
|
+
import FormOutput from '@/Form/Manage/FormMappedOutput'
|
4
|
+
import FormConfigInput from '@/Form/Manage/FormConfigInput'
|
5
|
+
import FormMappingInput from '@/Form/Manage/FormMappingInput'
|
6
|
+
import Form from '@/Form/FormCreator'
|
7
|
+
import { useAtom } from 'jotai'
|
8
|
+
import formAtom from '@/state/formAtom'
|
9
|
+
import { RawFormOutput } from '@/Form/Manage/RawFormOutput'
|
10
|
+
import formMappingAtom from '@/state/formMappingAtom'
|
11
|
+
import { getQueryParam, updateUrlParam } from '@/helpers'
|
12
|
+
import formValuesAtom from '@/state/formValuesAtom'
|
13
|
+
import { CheckIcon, Cross1Icon, TrashIcon } from '@radix-ui/react-icons'
|
14
|
+
import testForm from '@/Form/testData/nestedForm.json'
|
15
|
+
import { type IForm, type IFormValues } from '@/Form/FormCreatorTypes'
|
16
|
+
import { type IFormMapping } from '@/Form/FormMappingTypes'
|
17
|
+
|
18
|
+
type IDisplayType = 'stack' | 'tab'
|
19
|
+
|
20
|
+
const ClearForm = ({
|
21
|
+
message = 'Clear form',
|
22
|
+
onConfirm
|
23
|
+
}: {
|
24
|
+
message?: ReactNode
|
25
|
+
onConfirm: () => void
|
26
|
+
|
27
|
+
}): ReactElement => {
|
28
|
+
const [confirm, setConfirm] = useState(false)
|
29
|
+
|
30
|
+
return (
|
31
|
+
<>
|
32
|
+
{
|
33
|
+
confirm
|
34
|
+
? <p className='flex flex-row gap-2 text-sm'><span className='text-slate-600'>Deleting: </span> Are you sure?
|
35
|
+
<Button size='xs' type='submit'
|
36
|
+
onClick={() => {
|
37
|
+
onConfirm()
|
38
|
+
setConfirm(false)
|
39
|
+
}}>Yes <CheckIcon className='inline ml-2' />
|
40
|
+
</Button>
|
41
|
+
<Button size='xs' type='alert'
|
42
|
+
onClick={() => {
|
43
|
+
setConfirm(false)
|
44
|
+
}}>Cancel <Cross1Icon className='inline ml-2' />
|
45
|
+
</Button>
|
46
|
+
</p>
|
47
|
+
: <Button size='xs' type='alert' onClick={() => { setConfirm(true) }}>
|
48
|
+
{message} <TrashIcon className='inline ml-2 fill-white' />
|
49
|
+
</Button>
|
50
|
+
}
|
51
|
+
</>
|
52
|
+
)
|
53
|
+
}
|
54
|
+
|
55
|
+
const FormManager = ({
|
56
|
+
formValueState,
|
57
|
+
mappingState,
|
58
|
+
formState
|
59
|
+
}: {
|
60
|
+
formValueState?: [IFormValues, (v: IFormValues) => void]
|
61
|
+
mappingState?: [IFormMapping, (v: IFormMapping) => void]
|
62
|
+
formState?: [IForm, (v: IForm) => void]
|
63
|
+
}): ReactElement => {
|
64
|
+
const [form, setForm] = formState ?? useAtom(formAtom)
|
65
|
+
const [mapping, setMapping] = mappingState ?? useAtom(formMappingAtom)
|
66
|
+
const [formValues, setFormValues] = formValueState ?? useAtom(formValuesAtom)
|
67
|
+
const sections = [
|
68
|
+
{
|
69
|
+
id: 'config',
|
70
|
+
label: 'Form config',
|
71
|
+
content: <FormConfigInput
|
72
|
+
formState={[form, setForm]}
|
73
|
+
/>
|
74
|
+
},
|
75
|
+
{
|
76
|
+
id: 'mapping',
|
77
|
+
label: 'Form mapping',
|
78
|
+
content: <FormMappingInput
|
79
|
+
form={form}
|
80
|
+
mappingState={[mapping, setMapping]}
|
81
|
+
/>
|
82
|
+
},
|
83
|
+
{
|
84
|
+
id: 'output',
|
85
|
+
label: 'Mapped Output',
|
86
|
+
content: <FormOutput
|
87
|
+
form={form}
|
88
|
+
formMapping={mapping}
|
89
|
+
/>
|
90
|
+
},
|
91
|
+
{
|
92
|
+
id: 'raw_output',
|
93
|
+
label: 'Raw Output',
|
94
|
+
content: <RawFormOutput />
|
95
|
+
}
|
96
|
+
]
|
97
|
+
const params = Object.fromEntries(new URLSearchParams(window.location.search))
|
98
|
+
const display: IDisplayType = params.display === 'stack' ? 'stack' : 'tab'
|
99
|
+
return (
|
100
|
+
<div className='flex flex-col h-full gap-4 p-20'>
|
101
|
+
<div className='flex flex-row gap-4 justify-end'>
|
102
|
+
<ClearForm onConfirm={() => {
|
103
|
+
setFormValues({})
|
104
|
+
}} />
|
105
|
+
<ClearForm message='Clear form config' onConfirm={() => {
|
106
|
+
setForm(structuredClone(testForm as IForm))
|
107
|
+
}} />
|
108
|
+
</div>
|
109
|
+
<div className='grid grid-cols-2 gap-8 flex-grow'>
|
110
|
+
|
111
|
+
<Form form={form} formValueState={[formValues, setFormValues]} />
|
112
|
+
|
113
|
+
<div className='flex flex-col gap-4'>
|
114
|
+
{
|
115
|
+
display !== 'tab'
|
116
|
+
? <MultiAccordion tabs={sections} />
|
117
|
+
: <Tabs
|
118
|
+
tabs={sections}
|
119
|
+
selectedTab={getQueryParam('tab') ?? undefined}
|
120
|
+
onChange={(tab) => {
|
121
|
+
updateUrlParam('tab', tab)
|
122
|
+
}}
|
123
|
+
/>
|
124
|
+
}
|
125
|
+
|
126
|
+
</div>
|
127
|
+
</div>
|
128
|
+
</div>
|
129
|
+
)
|
130
|
+
}
|
131
|
+
|
132
|
+
export default FormManager
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import { CopyableJSONOutput } from '@/Form/Manage/CopyableJSONOutput'
|
2
|
+
import formValuesAtom from '@/state/formValuesAtom'
|
3
|
+
import { useAtom } from 'jotai'
|
4
|
+
import { set } from 'lodash'
|
5
|
+
import React, { type ReactElement } from 'react'
|
6
|
+
|
7
|
+
export const RawFormOutput = (): ReactElement => {
|
8
|
+
const [formValues] = useAtom(formValuesAtom)
|
9
|
+
const rehydrated = {}
|
10
|
+
Object.keys(formValues).forEach(path => {
|
11
|
+
set(rehydrated, path, formValues[path])
|
12
|
+
})
|
13
|
+
|
14
|
+
// const rehydratedMapped = {}
|
15
|
+
|
16
|
+
return <div className='flex flex-col gap-4'>
|
17
|
+
<CopyableJSONOutput string={JSON.stringify(formValues, null, 2)} label='As stored' />
|
18
|
+
<CopyableJSONOutput string={JSON.stringify(rehydrated, null, 2)} label='Rehydrated' />
|
19
|
+
</div>
|
20
|
+
}
|