@axdspub/axiom-ui-forms 0.2.1-alpha.6 → 0.2.1-alpha.7

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 (67) hide show
  1. package/package.json +5 -2
  2. package/.env +0 -1
  3. package/.eslintrc.json +0 -37
  4. package/.gitlab-ci.yml +0 -41
  5. package/craco.config.js +0 -42
  6. package/docker-compose.yml +0 -13
  7. package/rollup.config.mjs +0 -120
  8. package/src/App.tsx +0 -57
  9. package/src/Form/Components/FieldCreator.tsx +0 -206
  10. package/src/Form/Components/FieldLabel.tsx +0 -14
  11. package/src/Form/Components/Inputs/Boolean.tsx +0 -13
  12. package/src/Form/Components/Inputs/Date.tsx +0 -111
  13. package/src/Form/Components/Inputs/DateTime.tsx +0 -104
  14. package/src/Form/Components/Inputs/GeoJSON.tsx +0 -535
  15. package/src/Form/Components/Inputs/GeoJSONInputLoader.tsx +0 -16
  16. package/src/Form/Components/Inputs/JSONString.tsx +0 -40
  17. package/src/Form/Components/Inputs/LongString.tsx +0 -22
  18. package/src/Form/Components/Inputs/Number.tsx +0 -22
  19. package/src/Form/Components/Inputs/Object.tsx +0 -56
  20. package/src/Form/Components/Inputs/RadioGroup.tsx +0 -24
  21. package/src/Form/Components/Inputs/SingleSelect.tsx +0 -24
  22. package/src/Form/Components/Inputs/String.tsx +0 -18
  23. package/src/Form/Components/Inputs/Time.tsx +0 -107
  24. package/src/Form/Components/Inputs/index.tsx +0 -10
  25. package/src/Form/Components/Inputs/inputMap.ts +0 -30
  26. package/src/Form/Components/index.tsx +0 -2
  27. package/src/Form/Creator/FormCreator.tsx +0 -60
  28. package/src/Form/Creator/FormCreatorTypes.ts +0 -225
  29. package/src/Form/Creator/FormFields.tsx +0 -37
  30. package/src/Form/Creator/FormHeader.tsx +0 -29
  31. package/src/Form/Creator/FormSection.tsx +0 -60
  32. package/src/Form/Creator/Page.tsx +0 -132
  33. package/src/Form/Creator/Wizard.tsx +0 -157
  34. package/src/Form/Creator/utils.ts +0 -0
  35. package/src/Form/FormMappingTypes.ts +0 -17
  36. package/src/Form/Manage/CopyableJSONOutput.tsx +0 -75
  37. package/src/Form/Manage/FormConfigInput.tsx +0 -56
  38. package/src/Form/Manage/FormMappedOutput.tsx +0 -133
  39. package/src/Form/Manage/FormMappingInput.tsx +0 -60
  40. package/src/Form/Manage/Manage.tsx +0 -133
  41. package/src/Form/Manage/RawFormOutput.tsx +0 -22
  42. package/src/Form/MapTester.tsx +0 -107
  43. package/src/Form/SchemaToForm.tsx +0 -176
  44. package/src/Form/formDefinition.json +0 -8
  45. package/src/Form/helpers.ts +0 -122
  46. package/src/Form/index.ts +0 -2
  47. package/src/Form/schemaToFormHelpers.ts +0 -216
  48. package/src/Form/testData/assetData.json +0 -65
  49. package/src/Form/testData/exampleParticle.json +0 -112
  50. package/src/Form/testData/fields.json +0 -151
  51. package/src/Form/testData/nestedForm.json +0 -156
  52. package/src/Form/testData/pagedForm.json +0 -182
  53. package/src/Form/testData/testSchema.json +0 -89
  54. package/src/Form/testData/wizardForm.json +0 -217
  55. package/src/SetTester.tsx +0 -61
  56. package/src/helpers.ts +0 -36
  57. package/src/index.css +0 -39
  58. package/src/index.tsx +0 -19
  59. package/src/library.ts +0 -4
  60. package/src/reportWebVitals.ts +0 -15
  61. package/src/state/formAtom.ts +0 -21
  62. package/src/state/formMappingAtom.ts +0 -21
  63. package/src/state/formValuesAtom.ts +0 -22
  64. package/src/types/generate-schema.d.ts +0 -8
  65. package/tailwind.config.js +0 -11
  66. package/tsconfig.json +0 -32
  67. package/tsconfig.paths.json +0 -19
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@axdspub/axiom-ui-forms",
3
- "version": "0.2.1-alpha.6",
3
+ "version": "0.2.1-alpha.7",
4
4
  "private": false,
5
5
  "main": "./library/umd/library.js",
6
- "module": "./library/es/library.js",
6
+ "module": "./library/esm/library.js",
7
7
  "types": "./library/axiom-ui-forms.d.ts",
8
+ "files": [
9
+ "library/**"
10
+ ],
8
11
  "peerDependencies": {
9
12
  "lodash": "^4.17.21",
10
13
  "react": "^18.0.0",
package/.env DELETED
@@ -1 +0,0 @@
1
- PORT=3080
package/.eslintrc.json DELETED
@@ -1,37 +0,0 @@
1
- {
2
- "env": {
3
- "browser": true,
4
- "es2021": true
5
- },
6
- "extends": [
7
- "plugin:react/recommended",
8
- "standard-with-typescript"
9
- ],
10
- "overrides": [],
11
- "parserOptions": {
12
- "ecmaVersion": "latest",
13
- "sourceType": "module",
14
- "project": [
15
- "./tsconfig.json"
16
- ]
17
- },
18
- "plugins": [
19
- "react"
20
- ],
21
- "rules": {
22
- "ignoreIIFE": 0,
23
- "@typescript-eslint/strict-boolean-expressions": 0,
24
- "@typescript-eslint/no-misused-promises": [2, {
25
- "checksVoidReturn": {
26
- "attributes": false
27
- }
28
- }]
29
-
30
- },
31
- "ignorePatterns": [
32
- "**/build/*",
33
- "**/library/*",
34
- "**/node_modules/*",
35
- "**/.yalc/*"
36
- ]
37
- }
package/.gitlab-ci.yml DELETED
@@ -1,41 +0,0 @@
1
- stages:
2
- - build
3
- - test
4
- - push
5
- - clean
6
- - deploy
7
-
8
- build_image:
9
- stage: build
10
- script:
11
- - docker build -f Dockerfile -t $CI_PROJECT_NAME:$CI_PIPELINE_ID .
12
-
13
- test:
14
- stage: test
15
- image: $CI_PROJECT_NAME:$CI_PIPELINE_ID
16
- script:
17
- - test -e /usr/share/nginx/html/index.html
18
- - test -e /usr/share/nginx/html/asset-manifest.json
19
-
20
- push_latest:
21
- stage: push
22
- only:
23
- - main
24
- script:
25
- - docker tag $CI_PROJECT_NAME:$CI_PIPELINE_ID registry.axiom/$CI_PROJECT_NAME:latest
26
- - docker push registry.axiom/$CI_PROJECT_NAME:latest
27
-
28
- clean_latest:
29
- stage: clean
30
- only:
31
- - main
32
- script:
33
- - docker rmi --no-prune $CI_PROJECT_NAME:$CI_PIPELINE_ID
34
- - docker rmi --no-prune registry.axiom/$CI_PROJECT_NAME:latest
35
-
36
- deploy_app:
37
- stage: deploy
38
- only:
39
- - main
40
- script:
41
- - aps apps-prod axiom-ui-forms
package/craco.config.js DELETED
@@ -1,42 +0,0 @@
1
- const path = require('path')
2
-
3
- // someday, clean up the the FOUR! repeated declarations of aliases (others are is in tsconfig.paths.json => tsconfig.json and .storybook/main.ts)
4
- // https://stackoverflow.com/a/71892901
5
-
6
- module.exports = {
7
- webpack: {
8
- eslint: {
9
- enable: false
10
- },
11
- alias: {
12
- '@': path.resolve(__dirname, 'src/'),
13
- '@Components': path.resolve(__dirname, 'src/Components'),
14
- '@State': path.resolve(__dirname, 'src/State')
15
- },
16
- configure: {
17
- ignoreWarnings: [/Failed to parse source map/],
18
- resolve: {
19
- fallback: {
20
- https: false,
21
- http: false,
22
- path: false,
23
- zlib: false
24
- }
25
- },
26
- module: {
27
- // this silences a warning (Critical dependency: require function is used in a way in which dependencies cannot be statically extracted) that comes up with the cesium build included with @axdspubs/axiom-maps
28
- unknownContextCritical: false
29
- }
30
- }
31
- },
32
- jest: {
33
- configure: {
34
- verbose: true,
35
- moduleNameMapper: {
36
- '^@/(.*)$': '<rootDir>/src/$1',
37
- '^@Components/(.*)$': '<rootDir>/src/State/$1',
38
- '^@State/(.*)$': '<rootDir>/src/State/$1'
39
- }
40
- }
41
- }
42
- }
@@ -1,13 +0,0 @@
1
- services:
2
- app:
3
- image: axiom-ui-forms:latest
4
- build:
5
- context: .
6
- dockerfile: Dockerfile
7
- extra_hosts:
8
- - "npm-registry.srv.axiomptk:10.2.3.4"
9
- - "npm-registry.srv.axiomptk:10.2.10.2"
10
- ports:
11
- - "8787:80"
12
- # profiles:
13
- # - app
package/rollup.config.mjs DELETED
@@ -1,120 +0,0 @@
1
- import resolve from '@rollup/plugin-node-resolve'
2
- import commonjs from '@rollup/plugin-commonjs'
3
- import babel from '@rollup/plugin-babel'
4
- import typescript from '@rollup/plugin-typescript'
5
- import dts from 'rollup-plugin-dts'
6
- import json from '@rollup/plugin-json'
7
- import alias from '@rollup/plugin-alias'
8
- import nodePolyfills from 'rollup-plugin-polyfill-node'
9
- // import packageJson from './package.json'
10
-
11
- const globals = {
12
- 'react/jsx-runtime': 'JSXRuntime',
13
- react: 'React',
14
- 'react-dom': 'ReactDOM',
15
- lodash: 'lodash'
16
- }
17
-
18
- const external = [
19
- 'react',
20
- 'react-dom',
21
- 'react-scripts',
22
- 'react/jsx-runtime',
23
- 'lodash'
24
- ]
25
-
26
- const config = [
27
- {
28
- external,
29
- input: 'src/library.ts',
30
- onwarn: function (warning, warn) {
31
- if (warning.code === 'MODULE_LEVEL_DIRECTIVE') {
32
- return
33
- }
34
- // got same warning as with d3.. assuming same deal for now (bostock sez: it's allowed in the spec, not gonna fix. just supress it)
35
- // https://github.com/d3/d3-selection/issues/168#issuecomment-451983830
36
- if (warning.code === 'CIRCULAR_DEPENDENCY') return
37
-
38
- warn(warning)
39
- },
40
- output: [
41
- /* {
42
- file: 'library/cjs.js',
43
- format: 'cjs',
44
- sourcemap: true,
45
- globals
46
- }, */
47
- {
48
- dir: 'library/esm',
49
- format: 'esm',
50
- sourcemap: true,
51
- chunkFileNames: 'chunks/[name]-[hash].js',
52
- // inlineDynamicImports: true,
53
- globals
54
- }//,
55
- /* {
56
- file: 'library/browser.js',
57
- format: 'iife',
58
- name: 'AxiomUIforms',
59
- sourcemap: true,
60
- globals
61
- }, */
62
- /* {
63
- dir: 'library/umd',
64
- format: 'umd',
65
- name: 'AxiomUIforms',
66
- sourcemap: true,
67
- // inlineDynamicImports: true,
68
- globals
69
- } */
70
- ],
71
- plugins: [
72
- nodePolyfills(),
73
- json(),
74
- resolve({ preferBuiltins: false, browser: true }),
75
- commonjs(),
76
- /* babel({
77
- babelHelpers: 'bundled',
78
- presets: ['@babel/preset-react']
79
- }), */
80
- alias({
81
- entries: {
82
- '@/*': './src'
83
- }
84
- }),
85
- typescript({ tsconfig: './tsconfig.json' })
86
- ]
87
- },
88
- {
89
- external,
90
- input: 'src/library.ts',
91
- output: [{
92
- file: 'library/axiom-ui-forms.d.ts',
93
- format: 'esm',
94
- globals
95
- }],
96
- plugins: [
97
- json(),
98
- dts({
99
- compilerOptions: {
100
- // enabling declaration (.d.ts) emit
101
- declaration: true,
102
-
103
- // optional - in general it's a good practice to decouple declaration files from your actual transpiled JavaScript files
104
- declarationDir: 'library',
105
-
106
- // optional if you're using babel to transpile TS -> JS
107
- emitDeclarationOnly: true,
108
-
109
- paths: {
110
- '@/*': [
111
- './src/*'
112
- ]
113
- }
114
- }
115
- })
116
- ]
117
- }
118
- ]
119
-
120
- export default config
package/src/App.tsx DELETED
@@ -1,57 +0,0 @@
1
- import FormManager from '@/Form/Manage/Manage'
2
- import React, { useState, type ReactElement } from 'react'
3
- import { BrowserRouter, Route, Routes } from 'react-router-dom'
4
- import SetTester from '@/SetTester'
5
- import MapTester from '@/Form/MapTester'
6
- import SchemaToForm from '@/Form/SchemaToForm'
7
- import pagedFormJson from '@/Form/testData/pagedForm.json'
8
- import wizardFormJson from '@/Form/testData/wizardForm.json'
9
- import { type IForm, type IFormValues } from '@/Form/Creator/FormCreatorTypes'
10
- import Form from '@/Form/Creator/FormCreator'
11
-
12
- const PagedFormWrap = (): ReactElement => {
13
- const formValueState = useState<IFormValues>({})
14
- return (
15
- <div>
16
- <Form form={pagedFormJson as IForm} formValueState={formValueState} className='p-20' />
17
- <pre className='p-20 bg-slate-200 text-xs'>{JSON.stringify(formValueState[0], null, 2)}</pre>
18
- </div>
19
- )
20
- }
21
-
22
- const WizardFormWrap = (): ReactElement => {
23
- const formValueState = useState<IFormValues>({})
24
- return (
25
- <div>
26
- <Form form={wizardFormJson as IForm} formValueState={formValueState} className='p-20' />
27
- <pre className='p-20 bg-slate-200 text-xs'>{JSON.stringify(formValueState[0], null, 2)}</pre>
28
- </div>
29
- )
30
- }
31
-
32
- const App = (): ReactElement => {
33
- return (
34
-
35
- <div className='h-screen flex flex-col gap-4'>
36
- <BrowserRouter>
37
- <Routes>
38
- <Route path='/' element={<FormManager />}>
39
- <Route path='*' element={<FormManager />} />
40
- </Route>
41
- <Route path='/schema-to-form' element={<SchemaToForm />} />
42
- <Route path="/set-tester" element={<SetTester />} />
43
- <Route path="/map-tester" element={<MapTester />} />
44
- <Route path="/page-form/" element={<PagedFormWrap />}>
45
- <Route path='*' element={<PagedFormWrap />} />
46
- </Route>
47
- <Route path='/wizard-form' element={<WizardFormWrap />}>
48
- <Route path='*' element={<WizardFormWrap />} />
49
- </Route>
50
- </Routes>
51
- </BrowserRouter>
52
- </div>
53
-
54
- )
55
- }
56
-
57
- export default App
@@ -1,206 +0,0 @@
1
- import inputMap from '@/Form/Components/Inputs/inputMap'
2
- import { type IFormValues, type IFieldInputProps, type IFormField, type IValueChangeFn, type IValueType, type IForm, type IFormValueState } from '@/Form/Creator/FormCreatorTypes'
3
- import { checkCondition, cleanUnusedDependenciesFromFormValues, getFieldValue, getPathFromField } from '@/Form/helpers'
4
- import { Button, utils } from '@axdspub/axiom-ui-utilities'
5
- import { CheckIcon, CopyIcon, Cross1Icon, PlusIcon, TrashIcon } from '@radix-ui/react-icons'
6
- import { set } from 'lodash'
7
- import React, { useState, type ReactElement } from 'react'
8
-
9
- interface IFieldCreator {
10
- field: IFormField
11
- form: IForm
12
- onChange?: IValueChangeFn
13
- className?: string
14
- defaultClassName?: string
15
- value?: IValueType | IValueType[]
16
- formValueState: IFormValueState
17
- }
18
-
19
- const toolButtonClass = 'border-white hover:border-single hover:border-1 hover:border-slate-400'
20
-
21
- const DeleteMultiple = ({
22
- doDelete
23
- }: {
24
- doDelete: () => void
25
- }): ReactElement => {
26
- const [confirm, setConfirm] = useState(false)
27
-
28
- return (
29
- <>
30
- {
31
- confirm
32
- ? <p className='flex flex-row gap-2 text-sm'><span className='text-slate-600'>Deleting: </span> Are you sure?
33
- <Button size='xs' type='submit'
34
- onClick={() => {
35
- doDelete()
36
- setConfirm(false)
37
- }}>Yes <CheckIcon className='inline ml-2' />
38
- </Button>
39
- <Button size='xs' type='alert'
40
- onClick={() => {
41
- setConfirm(false)
42
- }}>Cancel <Cross1Icon className='inline ml-2' />
43
- </Button>
44
- </p>
45
- : <Button size='xs' className={toolButtonClass} onClick={() => { setConfirm(true) }}>
46
- Delete <TrashIcon className='inline ml-2 fill-white' />
47
- </Button>
48
- }
49
- </>
50
- )
51
- }
52
-
53
- const OneOfMultiple = ({
54
- InputComponent,
55
- field,
56
- form,
57
- value,
58
- index,
59
- onChange,
60
- values,
61
- formValueState
62
-
63
- }: {
64
- InputComponent: React.FC<IFieldInputProps>
65
- field: IFormField
66
- form: IForm
67
- value: IValueType
68
- index: number
69
- onChange: (v: IValueType[] | undefined) => void
70
- values: IValueType[]
71
- formValueState: [IFormValues, (v: IFormValues) => void]
72
-
73
- }): ReactElement => {
74
- const addValue = (v: IValueType | null): void => {
75
- const newValues = [...values]
76
- newValues.splice(index + 1, 0, v)
77
- onChange(newValues)
78
- }
79
-
80
- return (
81
- <div className='flex flex-col gap-2'>
82
- <InputComponent
83
- formValueState={formValueState}
84
- form={form}
85
- field={{
86
- ...field,
87
- required: false,
88
- label: index > 0 ? null : field.label,
89
- id: `${field.id}-${index}`
90
- }}
91
- value={value}
92
- onChange={(v) => {
93
- const newValues = [...values]
94
- newValues[index] = v as IValueType
95
- onChange(newValues)
96
- }}
97
- />
98
-
99
- <div className='flex flex-row justify-between w-full p-2'>
100
- {index > 0 && (
101
- <DeleteMultiple doDelete={() => {
102
- const newValues = [...values]
103
- newValues.splice(index, 1)
104
- onChange(newValues)
105
- }} />
106
- )}
107
- <div className='ml-auto flex gap-2'>
108
- <Button
109
- size='xs'
110
- className={toolButtonClass}
111
- onClick={() => {
112
- addValue(null)
113
- }}>Add <PlusIcon className='inline ml-2' /></Button>
114
- <Button
115
- size='xs'
116
- className={toolButtonClass}
117
- onClick={() => {
118
- addValue(structuredClone(value))
119
- }}>Duplicate <CopyIcon className='inline ml-2' />
120
- </Button>
121
- </div>
122
- </div>
123
- </div>
124
- )
125
- }
126
-
127
- const MultipleFieldCreator = ({ form, field, onChange, value, formValueState }: IFieldCreator): ReactElement => {
128
- const [formValues, setFormValues] = formValueState
129
- const defaultOnChange = (v: IValueType[] | undefined): void => {
130
- const formValuesCopy = structuredClone(formValues)
131
- set(formValuesCopy, getPathFromField(field), v)
132
- setFormValues(formValuesCopy)
133
- }
134
-
135
- const initialVal = value !== undefined ? value : getFieldValue(field, formValues)
136
- const initialValues = (initialVal !== undefined ? (Array.isArray(initialVal) ? initialVal : [initialVal]) : [null])
137
-
138
- /* const initialValues = (
139
- formValues[getPathFromField(field)] !== undefined
140
- ? Array.isArray(formValues[getPathFromField(field)])
141
- ? formValues[getPathFromField(field)]
142
- : [formValues[getPathFromField(field)]]
143
- : [null]
144
- ) as IValueType[] */
145
-
146
- const InputComponent = inputMap[field.type]
147
-
148
- return <div>
149
- {
150
- initialValues?.map((value, index) => {
151
- return <OneOfMultiple
152
- formValueState={formValueState}
153
- key={`${field.id}-${index}`}
154
- InputComponent={InputComponent}
155
- form={form}
156
- field={field}
157
- value={value}
158
- index={index}
159
- onChange={onChange ?? defaultOnChange}
160
- values={initialValues}
161
- />
162
- })
163
- }
164
- </div>
165
- }
166
-
167
- const FieldCreator = ({
168
- field,
169
- form,
170
- value,
171
- onChange,
172
- className,
173
- defaultClassName = 'py-2 flex flex-col gap-8',
174
- formValueState
175
- }: IFieldCreator): ReactElement | null => {
176
- const [formValues, setFormValues] = formValueState
177
- const InputComponent = inputMap[field.type]
178
-
179
- const updateFormValues = (v: IFormValues): void => {
180
- setFormValues(cleanUnusedDependenciesFromFormValues(form, v))
181
- }
182
-
183
- if (!checkCondition(field, formValues)) {
184
- return null
185
- }
186
-
187
- const defaultOnChange = (v: IValueType | IValueType[] | undefined): void => {
188
- const formValuesCopy = structuredClone(formValues)
189
- set(formValuesCopy, getPathFromField(field), v)
190
- updateFormValues(formValuesCopy)
191
- }
192
- const initialValue = value !== undefined ? value : getFieldValue(field, formValues)
193
- return InputComponent !== undefined
194
- ? <div className={utils.makeClassName({
195
- className,
196
- defaultClassName
197
- })}>{
198
- field.multiple === true
199
- ? <MultipleFieldCreator field={field} form={form} onChange={onChange} value={initialValue} formValueState={formValueState} />
200
- : <InputComponent field={field} form={form} onChange={onChange ?? defaultOnChange} value={Array.isArray(initialValue) ? initialValue[0] : initialValue} formValueState={formValueState} />
201
-
202
- }</div>
203
- : <p>No component definition for {field.type} ({field.id})</p>
204
- }
205
-
206
- export default FieldCreator
@@ -1,14 +0,0 @@
1
- import { type IFormField } from '@/Form/Creator/FormCreatorTypes'
2
- import React, { type ReactElement } from 'react'
3
-
4
- export const FieldLabelText = (field: IFormField): ReactElement => {
5
- return (
6
- <strong>{field.label} { field.required === true ? <span className='text-red-500'>*</span> : ''}</strong>
7
- )
8
- }
9
-
10
- const FieldLabel = (field: IFormField): ReactElement => {
11
- return <p className='pb-2'><FieldLabelText {...field} /></p>
12
- }
13
-
14
- export default FieldLabel
@@ -1,13 +0,0 @@
1
- import { FieldLabelText } from '@/Form/Components/FieldLabel'
2
- import { type IFieldInputProps } from '@/Form/Creator/FormCreatorTypes'
3
- import { Checkbox } from '@axdspub/axiom-ui-utilities'
4
- import React, { type ReactElement } from 'react'
5
-
6
- const BooleanInput = ({ field, onChange, value }: IFieldInputProps): ReactElement => {
7
- const initialValue = value !== undefined ? value : false
8
- return <Checkbox id={field.id} testId={field.id} label={<FieldLabelText {...field} />} value={Boolean(initialValue)} onChange={(e) => {
9
- onChange(e)
10
- }} />
11
- }
12
-
13
- export default BooleanInput
@@ -1,111 +0,0 @@
1
- import FieldLabel from '@/Form/Components/FieldLabel'
2
- import { type IFieldInputProps } from '@/Form/Creator/FormCreatorTypes'
3
- import React, { type ReactElement, useState, useEffect } from 'react'
4
-
5
- const DateInput = ({ field, onChange, value }: IFieldInputProps): ReactElement => {
6
- if (field.type !== 'date') {
7
- return <p>Field config for {field.id} is missing &apos;options&apos;</p>
8
- }
9
- const [inputValue, setInputValue] = useState('')
10
- const [error, setError] = useState<string | null>(null)
11
- const { minDate, maxDate } = field.constraints ?? {}
12
-
13
- useEffect(() => {
14
- setInputValue(formatValue(value as string))
15
- }, [value])
16
-
17
- // Convert the value to the format expected by date input (YYYY-MM-DD)
18
- const formatValue = (val: string | undefined | null): string => {
19
- if (!val) return ''
20
- try {
21
- // Ensure the value is in the correct format
22
- const date = new Date(val)
23
- if (isNaN(date.getTime())) return ''
24
-
25
- // Format to YYYY-MM-DD using UTC to avoid timezone issues
26
- const year = date.getUTCFullYear()
27
- const month = String(date.getUTCMonth() + 1).padStart(2, '0')
28
- const day = String(date.getUTCDate()).padStart(2, '0')
29
-
30
- return `${year}-${month}-${day}`
31
- } catch {
32
- return ''
33
- }
34
- }
35
-
36
- const validateDate = (dateStr: string): string | null => {
37
- if (!minDate && !maxDate) return null
38
-
39
- const inputDate = new Date(dateStr + 'T00:00:00Z')
40
- const minDateObj = minDate ? new Date(minDate) : null
41
- const maxDateObj = maxDate ? new Date(maxDate) : null
42
-
43
- if (minDateObj && inputDate < minDateObj) {
44
- return `Date must be after ${formatValue(minDate)}`
45
- }
46
- if (maxDateObj && inputDate > maxDateObj) {
47
- return `Date must be before ${formatValue(maxDate)}`
48
- }
49
- return null
50
- }
51
-
52
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
53
- const newInputValue = e.target.value
54
- setInputValue(newInputValue)
55
-
56
- // Only notify parent of change if we have a complete valid date
57
- if (newInputValue) {
58
- // Create date in UTC by appending T00:00:00Z to the input value
59
- const date = new Date(newInputValue + 'T00:00:00Z')
60
- // Check if it's a valid date and the year is reasonable (4 digits)
61
- if (!isNaN(date.getTime()) && date.getUTCFullYear() > 999) {
62
- const validationError = validateDate(newInputValue)
63
- setError(validationError)
64
- if (!validationError) {
65
- onChange(date.toISOString())
66
- }
67
- }
68
- } else {
69
- setError(null)
70
- onChange(undefined)
71
- }
72
- }
73
-
74
- const getConstraintMessage = (): string | null => {
75
- if (!minDate && !maxDate) return null
76
- const parts = []
77
- if (minDate) parts.push(`after ${formatValue(minDate)}`)
78
- if (maxDate) parts.push(`before ${formatValue(maxDate)}`)
79
- return `Must be ${parts.join(' and ')}`
80
- }
81
-
82
- const constraintMessage = getConstraintMessage()
83
-
84
- return (
85
- <div>
86
- <div className="flex flex-wrap items-baseline gap-2">
87
- <label htmlFor={field.id} className="flex-1 min-w-[200px]">
88
- <FieldLabel {...field} />
89
- </label>
90
- {constraintMessage && (
91
- <span className="text-sm text-slate-500 italic">
92
- {constraintMessage}
93
- </span>
94
- )}
95
- </div>
96
- <input
97
- id={field.id}
98
- className={`border ${error ? 'border-red-500' : 'border-slate-300'} p-2 w-full`}
99
- data-testid={field.id}
100
- type="date"
101
- value={inputValue}
102
- onChange={handleChange}
103
- min={minDate ? formatValue(minDate) : undefined}
104
- max={maxDate ? formatValue(maxDate) : undefined}
105
- />
106
- {error && <p className="text-red-500 text-sm mt-1">{error}</p>}
107
- </div>
108
- )
109
- }
110
-
111
- export default DateInput