@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.
Files changed (68) hide show
  1. package/.dockerignore +4 -0
  2. package/.env +1 -0
  3. package/.eslintrc.json +37 -0
  4. package/.gitlab-ci.yml +72 -0
  5. package/.storybook/main.ts +43 -0
  6. package/.storybook/preview.ts +16 -0
  7. package/.vscode/extensions.json +5 -0
  8. package/.vscode/settings.json +10 -0
  9. package/Dockerfile +34 -0
  10. package/README.md +19 -0
  11. package/craco.config.js +42 -0
  12. package/docker/nginx/conf.d/default.conf +46 -0
  13. package/docker-compose.yml +13 -0
  14. package/package.json +103 -0
  15. package/public/exampleForm.json +77 -0
  16. package/public/favicon.ico +0 -0
  17. package/public/index.html +43 -0
  18. package/public/logo192.png +0 -0
  19. package/public/logo512.png +0 -0
  20. package/public/manifest.json +25 -0
  21. package/public/robots.txt +3 -0
  22. package/rollup.config.mjs +108 -0
  23. package/src/App.tsx +25 -0
  24. package/src/Form/Components/FieldCreator.tsx +206 -0
  25. package/src/Form/Components/FieldLabel.tsx +14 -0
  26. package/src/Form/Components/Inputs/Boolean.tsx +13 -0
  27. package/src/Form/Components/Inputs/JSONString.tsx +40 -0
  28. package/src/Form/Components/Inputs/LongString.tsx +22 -0
  29. package/src/Form/Components/Inputs/Number.tsx +22 -0
  30. package/src/Form/Components/Inputs/Object.tsx +56 -0
  31. package/src/Form/Components/Inputs/RadioGroup.tsx +24 -0
  32. package/src/Form/Components/Inputs/SingleSelect.tsx +24 -0
  33. package/src/Form/Components/Inputs/String.tsx +18 -0
  34. package/src/Form/Components/Inputs/index.tsx +6 -0
  35. package/src/Form/Components/Inputs/inputMap.ts +23 -0
  36. package/src/Form/Components/index.tsx +2 -0
  37. package/src/Form/FormCreator.tsx +62 -0
  38. package/src/Form/FormCreatorTypes.ts +187 -0
  39. package/src/Form/FormMappingTypes.ts +17 -0
  40. package/src/Form/Manage/CopyableJSONOutput.tsx +75 -0
  41. package/src/Form/Manage/FormConfigInput.tsx +61 -0
  42. package/src/Form/Manage/FormMappedOutput.tsx +131 -0
  43. package/src/Form/Manage/FormMappingInput.tsx +60 -0
  44. package/src/Form/Manage/Manage.tsx +132 -0
  45. package/src/Form/Manage/RawFormOutput.tsx +20 -0
  46. package/src/Form/MapTester.tsx +107 -0
  47. package/src/Form/SchemaToForm.tsx +348 -0
  48. package/src/Form/formDefinition.json +8 -0
  49. package/src/Form/helpers.ts +85 -0
  50. package/src/Form/index.ts +1 -0
  51. package/src/Form/testData/assetData.json +65 -0
  52. package/src/Form/testData/exampleParticle.json +112 -0
  53. package/src/Form/testData/fields.json +151 -0
  54. package/src/Form/testData/nestedForm.json +156 -0
  55. package/src/Form/testData/testSchema.json +89 -0
  56. package/src/SetTester.tsx +61 -0
  57. package/src/helpers.ts +36 -0
  58. package/src/index.css +39 -0
  59. package/src/index.tsx +19 -0
  60. package/src/library.ts +3 -0
  61. package/src/reportWebVitals.ts +15 -0
  62. package/src/state/formAtom.ts +21 -0
  63. package/src/state/formMappingAtom.ts +21 -0
  64. package/src/state/formValuesAtom.ts +22 -0
  65. package/src/types/generate-schema.d.ts +8 -0
  66. package/tailwind.config.js +11 -0
  67. package/tsconfig.json +32 -0
  68. 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
+ }