@overmap-ai/forms 0.0.1-master.3

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 (130) hide show
  1. package/.husky/pre-commit +6 -0
  2. package/.prettierrc.json +10 -0
  3. package/.storybook/StoryDecorator.tsx +22 -0
  4. package/.storybook/main.ts +20 -0
  5. package/.storybook/palettes/green.css +66 -0
  6. package/.storybook/palettes/red.css +66 -0
  7. package/.storybook/preview.css +39 -0
  8. package/.storybook/preview.tsx +31 -0
  9. package/.storybook/tailwind-theme/accentPalette.css +181 -0
  10. package/.storybook/tailwind-theme/backgrounds.css +11 -0
  11. package/.storybook/tailwind-theme/basePalette.css +178 -0
  12. package/dev/publish-alpha.sh +13 -0
  13. package/dev/publish-patch.sh +3 -0
  14. package/eslint.config.js +56 -0
  15. package/package.json +93 -0
  16. package/src/ColorPicker/ColorPicker.tsx +47 -0
  17. package/src/ColorPicker/index.ts +1 -0
  18. package/src/FileBadge/FileBadge.tsx +27 -0
  19. package/src/FileBadge/index.ts +1 -0
  20. package/src/FileCard/FileCard.stories.tsx +69 -0
  21. package/src/FileCard/FileCard.tsx +53 -0
  22. package/src/FileCard/index.ts +1 -0
  23. package/src/FileIcon/FileIcon.tsx +31 -0
  24. package/src/FileIcon/index.ts +1 -0
  25. package/src/FileViewer/FileViewerProvider.stories.tsx +50 -0
  26. package/src/FileViewer/FileViewerProvider.tsx +72 -0
  27. package/src/FileViewer/context.ts +11 -0
  28. package/src/FileViewer/index.ts +3 -0
  29. package/src/FileViewer/typings.ts +5 -0
  30. package/src/ImageCard/ImageCard.stories.tsx +94 -0
  31. package/src/ImageCard/ImageCard.tsx +82 -0
  32. package/src/ImageCard/index.ts +1 -0
  33. package/src/ImageMarkup/ImageMarkup.stories.tsx +65 -0
  34. package/src/ImageMarkup/ImageMarkup.tsx +268 -0
  35. package/src/ImageMarkup/index.ts +1 -0
  36. package/src/ImageViewer/ImageViewer.stories.tsx +57 -0
  37. package/src/ImageViewer/ImageViewer.tsx +124 -0
  38. package/src/ImageViewer/constants.ts +1 -0
  39. package/src/ImageViewer/index.ts +2 -0
  40. package/src/PDFViewer/PDFViewer.stories.tsx +55 -0
  41. package/src/PDFViewer/PDFViewer.tsx +170 -0
  42. package/src/PDFViewer/constants.ts +1 -0
  43. package/src/PDFViewer/index.ts +2 -0
  44. package/src/SpreadsheetViewer/SpreadsheetViewer.stories.tsx +55 -0
  45. package/src/SpreadsheetViewer/SpreadsheetViewer.tsx +162 -0
  46. package/src/SpreadsheetViewer/constants.ts +8 -0
  47. package/src/SpreadsheetViewer/index.ts +2 -0
  48. package/src/forms/builder/DropDispatch.ts +84 -0
  49. package/src/forms/builder/FieldActions.tsx +155 -0
  50. package/src/forms/builder/FieldBuilder.tsx +386 -0
  51. package/src/forms/builder/FieldSectionWithActions.tsx +260 -0
  52. package/src/forms/builder/FieldWithActions.tsx +129 -0
  53. package/src/forms/builder/FieldsEditor.tsx +180 -0
  54. package/src/forms/builder/FormBuilder.stories.tsx +105 -0
  55. package/src/forms/builder/FormBuilder.tsx +237 -0
  56. package/src/forms/builder/constants.ts +18 -0
  57. package/src/forms/builder/hooks.tsx +24 -0
  58. package/src/forms/builder/index.ts +2 -0
  59. package/src/forms/builder/typings.ts +18 -0
  60. package/src/forms/builder/utils.ts +229 -0
  61. package/src/forms/constants.ts +9 -0
  62. package/src/forms/constantsJsx.tsx +67 -0
  63. package/src/forms/fields/BaseField/BaseField.ts +152 -0
  64. package/src/forms/fields/BaseField/hooks.tsx +60 -0
  65. package/src/forms/fields/BaseField/index.ts +4 -0
  66. package/src/forms/fields/BaseField/layouts.tsx +100 -0
  67. package/src/forms/fields/BaseField/typings.ts +9 -0
  68. package/src/forms/fields/BooleanField/BooleanField.tsx +48 -0
  69. package/src/forms/fields/BooleanField/BooleanInput.tsx +54 -0
  70. package/src/forms/fields/BooleanField/index.ts +2 -0
  71. package/src/forms/fields/CustomField/CustomField.tsx +45 -0
  72. package/src/forms/fields/CustomField/FieldInputClonerField/FieldInputCloner.tsx +25 -0
  73. package/src/forms/fields/CustomField/FieldInputClonerField/FieldInputClonerField.tsx +26 -0
  74. package/src/forms/fields/CustomField/FieldInputClonerField/index.ts +3 -0
  75. package/src/forms/fields/CustomField/FieldInputClonerField/typings.ts +8 -0
  76. package/src/forms/fields/CustomField/index.ts +1 -0
  77. package/src/forms/fields/DateField/DateField.tsx +42 -0
  78. package/src/forms/fields/DateField/DateInput.tsx +39 -0
  79. package/src/forms/fields/DateField/index.ts +2 -0
  80. package/src/forms/fields/FieldSection/FieldSection.tsx +173 -0
  81. package/src/forms/fields/FieldSection/FieldSectionLayout.tsx +56 -0
  82. package/src/forms/fields/FieldSection/index.ts +1 -0
  83. package/src/forms/fields/MultiStringField/MultiStringField.tsx +90 -0
  84. package/src/forms/fields/MultiStringField/MultiStringInput.tsx +207 -0
  85. package/src/forms/fields/MultiStringField/index.ts +2 -0
  86. package/src/forms/fields/NumberField/NumberField.tsx +173 -0
  87. package/src/forms/fields/NumberField/NumberInput.tsx +44 -0
  88. package/src/forms/fields/NumberField/index.ts +2 -0
  89. package/src/forms/fields/QrField/QrField.tsx +38 -0
  90. package/src/forms/fields/QrField/QrInput.module.sass +5 -0
  91. package/src/forms/fields/QrField/QrInput.tsx +144 -0
  92. package/src/forms/fields/QrField/index.ts +2 -0
  93. package/src/forms/fields/SelectField/BaseSelectField.ts +73 -0
  94. package/src/forms/fields/SelectField/MultiSelectField.tsx +53 -0
  95. package/src/forms/fields/SelectField/MultiSelectInput.tsx +80 -0
  96. package/src/forms/fields/SelectField/SelectField.tsx +49 -0
  97. package/src/forms/fields/SelectField/SelectInput.tsx +69 -0
  98. package/src/forms/fields/SelectField/index.ts +4 -0
  99. package/src/forms/fields/StringOrTextFields/StringField/StringField.tsx +61 -0
  100. package/src/forms/fields/StringOrTextFields/StringField/StringInput.tsx +41 -0
  101. package/src/forms/fields/StringOrTextFields/StringField/index.ts +2 -0
  102. package/src/forms/fields/StringOrTextFields/StringOrTextField.ts +143 -0
  103. package/src/forms/fields/StringOrTextFields/TextField/TextField.tsx +52 -0
  104. package/src/forms/fields/StringOrTextFields/TextField/TextInput.tsx +42 -0
  105. package/src/forms/fields/StringOrTextFields/TextField/index.ts +2 -0
  106. package/src/forms/fields/StringOrTextFields/index.ts +2 -0
  107. package/src/forms/fields/UploadField/UploadField.tsx +156 -0
  108. package/src/forms/fields/UploadField/UploadInput.tsx +220 -0
  109. package/src/forms/fields/UploadField/index.ts +2 -0
  110. package/src/forms/fields/UploadField/utils.ts +17 -0
  111. package/src/forms/fields/constants.ts +43 -0
  112. package/src/forms/fields/hooks.tsx +26 -0
  113. package/src/forms/fields/index.ts +12 -0
  114. package/src/forms/fields/typings.ts +45 -0
  115. package/src/forms/fields/utils.ts +125 -0
  116. package/src/forms/index.ts +5 -0
  117. package/src/forms/renderer/FormRenderer/FormRenderer.stories.tsx +142 -0
  118. package/src/forms/renderer/FormRenderer/FormRenderer.tsx +135 -0
  119. package/src/forms/renderer/PatchForm/Field.tsx +41 -0
  120. package/src/forms/renderer/PatchForm/PatchForm.stories.tsx +91 -0
  121. package/src/forms/renderer/PatchForm/Provider.tsx +119 -0
  122. package/src/forms/renderer/PatchForm/index.ts +2 -0
  123. package/src/forms/renderer/index.ts +2 -0
  124. package/src/forms/typings.ts +162 -0
  125. package/src/forms/utils.ts +69 -0
  126. package/src/index.ts +11 -0
  127. package/src/vite-env.d.ts +1 -0
  128. package/tailwind.config.ts +8 -0
  129. package/tsconfig.json +26 -0
  130. package/vite.config.ts +23 -0
@@ -0,0 +1,142 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+
3
+ import { formRevisionToSchema, PartialFormRevision } from "../../fields"
4
+ import { FormRenderer } from "./FormRenderer"
5
+
6
+ const meta = {
7
+ title: "Renderer/FromRenderer",
8
+ component: FormRenderer,
9
+ tags: ["autodocs"],
10
+ } satisfies Meta<typeof FormRenderer>
11
+
12
+ export default meta
13
+ type Story = StoryObj<typeof meta>
14
+
15
+ const partialFormRevision: PartialFormRevision = {
16
+ title: "Test form",
17
+ description: "This is a test form",
18
+ fields: [
19
+ {
20
+ type: "section",
21
+ label: "Section",
22
+ condition: null,
23
+ conditional: false,
24
+ identifier: "section",
25
+ fields: [
26
+ {
27
+ type: "string",
28
+ label: "String field",
29
+ description: "Hint: enter more than 1 characters",
30
+ required: true,
31
+ identifier: "string",
32
+ minimum_length: 2,
33
+ maximum_length: 10,
34
+ },
35
+ {
36
+ type: "text",
37
+ label: "Text field",
38
+ required: true,
39
+ identifier: "text",
40
+ maximum_length: 400,
41
+ },
42
+ {
43
+ type: "boolean",
44
+ label: "Boolean field",
45
+ required: true,
46
+ identifier: "boolean",
47
+ description: "Hint: enter true to pass validation and open and new section",
48
+ },
49
+ {
50
+ type: "select",
51
+ label: "Select field",
52
+ required: true,
53
+ identifier: "select",
54
+ options: [
55
+ { label: "option 1", value: "option 1" },
56
+ { label: "option 2", value: "option 2" },
57
+ ],
58
+ },
59
+ {
60
+ type: "multi-select",
61
+ label: "Multi-select field",
62
+ required: true,
63
+ identifier: "multi_select",
64
+ options: [
65
+ { label: "option 1", value: "option 1" },
66
+ { label: "option 2", value: "option 2" },
67
+ ],
68
+ },
69
+ {
70
+ type: "multi-string",
71
+ label: "Multi-string field",
72
+ description: "Used to generate options",
73
+ required: true,
74
+ identifier: "multi_string",
75
+ minimum_length: 2,
76
+ maximum_length: 10,
77
+ },
78
+ {
79
+ type: "upload",
80
+ label: "Upload Field",
81
+ description: "Used to upload files",
82
+ required: true,
83
+ identifier: "upload",
84
+ maximum_files: 10,
85
+ },
86
+ {
87
+ type: "qr",
88
+ label: "QR Field",
89
+ description: "Used to scan QR codes",
90
+ required: true,
91
+ identifier: "qr",
92
+ },
93
+ ],
94
+ },
95
+ {
96
+ type: "section",
97
+ label: "Another section",
98
+ identifier: "another_section",
99
+ condition: {
100
+ identifier: "boolean",
101
+ value: true,
102
+ },
103
+ conditional: true,
104
+ fields: [
105
+ {
106
+ type: "select",
107
+ label: "Select field",
108
+ required: true,
109
+ identifier: "select2",
110
+ options: [
111
+ { label: "option 1", value: "option 1" },
112
+ { label: "option 2", value: "option 2" },
113
+ ],
114
+ },
115
+ ],
116
+ },
117
+ ],
118
+ }
119
+
120
+ export const RenderForSubmission: Story = {
121
+ args: {
122
+ schema: formRevisionToSchema(partialFormRevision),
123
+ cancelText: "Cancel",
124
+ onSubmit: () => {
125
+ alert("validation passed")
126
+ },
127
+ },
128
+ }
129
+
130
+ export const RenderForViewing: Story = {
131
+ args: {
132
+ schema: formRevisionToSchema(partialFormRevision, { readonly: true }),
133
+ values: {
134
+ string: "hello",
135
+ text: "world",
136
+ boolean: "true",
137
+ select: "option 1",
138
+ multi_string: ["hello", "world"],
139
+ multi_select: ["option 1", "option 2"],
140
+ },
141
+ },
142
+ }
@@ -0,0 +1,135 @@
1
+ import { Button, ButtonProps, Card, Heading, RiIcon, Text } from "@overmap-ai/blocks"
2
+ import { cx } from "class-variance-authority"
3
+ import { FormikProvider, useFormik } from "formik"
4
+ import { forwardRef, memo, useEffect, useMemo } from "react"
5
+
6
+ import { SEVERITY_COLOR_MAPPING } from "../../constants"
7
+ import { ISchema, useFieldInputs } from "../../fields"
8
+ import { Form } from "../../typings"
9
+ import { initialFormValues, validateForm } from "../../utils"
10
+
11
+ interface FormRendererProps {
12
+ /** The schema of the form the render */
13
+ schema: ISchema
14
+ /** Initial values of the form */
15
+ values?: Form
16
+ onSubmit?: (values: Form) => Promise<void> | void
17
+ /** @default "Submit" */
18
+ submitText?: string
19
+ /** The text of the cancel button (hidden by default)
20
+ * @default null
21
+ */
22
+ cancelText?: string
23
+ onCancel?: () => void
24
+ onDirty?: () => void
25
+ onDirtyChange?: (dirty: boolean) => void
26
+ /** Hide the form description
27
+ * @default false */
28
+ hideDescription?: boolean
29
+ /** Hide the title (and description)
30
+ * @default false
31
+ */
32
+ hideTitle?: boolean
33
+ className?: string
34
+ buttonProps?: Omit<ButtonProps, "children">
35
+ }
36
+
37
+ const defaultHandleSubmit = () => {
38
+ throw new Error("onSubmit must be provided if form is not readonly.")
39
+ }
40
+
41
+ export const FormRenderer = memo(
42
+ forwardRef<HTMLFormElement, FormRendererProps>((props, ref) => {
43
+ const {
44
+ schema,
45
+ values = {},
46
+ onSubmit = defaultHandleSubmit,
47
+ submitText = "Submit",
48
+ cancelText,
49
+ onCancel,
50
+ onDirty,
51
+ onDirtyChange,
52
+ // if the title isn't provided, hide it by default
53
+ hideTitle = !schema.title,
54
+ hideDescription,
55
+ className,
56
+ buttonProps,
57
+ } = props
58
+ const { readonly } = schema.meta
59
+
60
+ // randomly generate a form id to ensure field ids are unique
61
+ const formId = useMemo(() => crypto.randomUUID(), [])
62
+
63
+ const formik = useFormik<Form>({
64
+ initialValues: initialFormValues(schema.fields, values),
65
+ onSubmit,
66
+ validate: (form) => validateForm(schema, form),
67
+ // only validate the entire form on submit
68
+ validateOnBlur: false,
69
+ validateOnChange: false,
70
+ })
71
+ const { dirty } = formik
72
+
73
+ const Title = useMemo(
74
+ () => (typeof schema.title === "string" ? <Heading size="lg">{schema.title}</Heading> : schema.title),
75
+ [schema.title],
76
+ )
77
+
78
+ const Description = useMemo(
79
+ () =>
80
+ typeof schema.description === "string" ? (
81
+ <Text accentColor="base">{schema.description}</Text>
82
+ ) : (
83
+ schema.description
84
+ ),
85
+ [schema.description],
86
+ )
87
+
88
+ const inputs = useFieldInputs(schema.fields, { formId, disabled: readonly })
89
+
90
+ useEffect(() => {
91
+ if (dirty && onDirty) onDirty()
92
+ if (onDirtyChange) onDirtyChange(dirty)
93
+ }, [dirty, onDirty, onDirtyChange])
94
+
95
+ return (
96
+ <FormikProvider value={formik}>
97
+ <form
98
+ id={formId}
99
+ ref={ref}
100
+ className={cx(className, "flex flex-col gap-2")}
101
+ onSubmit={formik.handleSubmit}
102
+ >
103
+ {!hideTitle && (
104
+ <Card>
105
+ <div className="flex flex-col gap-1">
106
+ {Title}
107
+ {!hideDescription && Description}
108
+ </div>
109
+ </Card>
110
+ )}
111
+ {inputs}
112
+ {!readonly && (
113
+ <div className="flex items-center justify-end gap-2">
114
+ {cancelText && (
115
+ <Button
116
+ accentColor={SEVERITY_COLOR_MAPPING.danger}
117
+ {...buttonProps}
118
+ type="button"
119
+ onClick={onCancel}
120
+ >
121
+ <RiIcon icon="RiCloseLine" />
122
+ {cancelText}
123
+ </Button>
124
+ )}
125
+ <Button {...buttonProps} type="submit" disabled={!formik.isValid} accentColor="success">
126
+ <RiIcon icon="RiCheckLine" />
127
+ {submitText}
128
+ </Button>
129
+ </div>
130
+ )}
131
+ </form>
132
+ </FormikProvider>
133
+ )
134
+ }),
135
+ )
@@ -0,0 +1,41 @@
1
+ import { FieldMetaProps, useField, useFormikContext } from "formik"
2
+ import { memo, ReactNode, useMemo } from "react"
3
+
4
+ import { FieldValue } from "../../typings"
5
+
6
+ interface RenderArgs {
7
+ value: FieldValue
8
+ meta: FieldMetaProps<unknown>
9
+ /** Intermediate changes to the field value */
10
+ setValue: (value: FieldValue) => void
11
+ /** EX: when the onBlur event is fired */
12
+ patchValue: () => void
13
+ }
14
+
15
+ interface PatchFieldProps {
16
+ name: string
17
+ render: (args: RenderArgs) => ReactNode
18
+ }
19
+
20
+ export const PatchField = memo((props: PatchFieldProps) => {
21
+ const { name, render } = props
22
+ const { submitForm } = useFormikContext()
23
+ const [fieldProps, _meta, helpers] = useField<FieldValue>(name)
24
+
25
+ const ret = useMemo(() => {
26
+ const setValue = (value: FieldValue) => {
27
+ void helpers.setValue(value, false)
28
+ }
29
+ return render({
30
+ value: fieldProps.value,
31
+ meta: _meta,
32
+ setValue,
33
+ patchValue: () => {
34
+ void submitForm()
35
+ },
36
+ })
37
+ }, [render, fieldProps.value, _meta, submitForm, helpers])
38
+ return <>{ret}</>
39
+ })
40
+
41
+ PatchField.displayName = "PatchField"
@@ -0,0 +1,91 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+
3
+ import { formRevisionToSchema, PartialFormRevision } from "../../fields"
4
+ import { PatchField } from "./Field"
5
+ import { PatchFormProvider } from "./Provider"
6
+
7
+ const partialFormRevision: PartialFormRevision = {
8
+ title: "Test form",
9
+ description: "This is a test form",
10
+ fields: [
11
+ {
12
+ type: "section",
13
+ label: "Section",
14
+ condition: null,
15
+ conditional: false,
16
+ identifier: "section",
17
+ fields: [
18
+ {
19
+ type: "string",
20
+ label: "String field",
21
+ required: true,
22
+ identifier: "string",
23
+ maximum_length: 10,
24
+ },
25
+ {
26
+ type: "string",
27
+ label: "String field",
28
+ required: true,
29
+ identifier: "another_string",
30
+ minimum_length: 2,
31
+ maximum_length: 10,
32
+ },
33
+ ],
34
+ },
35
+ ],
36
+ }
37
+ const schema = formRevisionToSchema(partialFormRevision)
38
+ const values = { string: "string", another_string: "another" }
39
+
40
+ const meta = {
41
+ title: "Renderer/PatchForm",
42
+ component: PatchFormProvider,
43
+ tags: ["autodocs"],
44
+ args: {
45
+ values,
46
+ schema,
47
+ },
48
+ } satisfies Meta<typeof PatchFormProvider>
49
+
50
+ export default meta
51
+ type Story = StoryObj<typeof meta>
52
+
53
+ /** Use the `Actions` tab to see when the `onError` or `onPatch` events fire. */
54
+ export const Basic: Story["render"] = (props) => {
55
+ return (
56
+ <PatchFormProvider {...props}>
57
+ <div className="flex flex-col gap-2">
58
+ <label className="flex gap-1">
59
+ string
60
+ <PatchField
61
+ name="string"
62
+ render={(props) => (
63
+ <input
64
+ value={props.value as string}
65
+ onChange={(e) => {
66
+ props.setValue(e.target.value)
67
+ }}
68
+ onBlur={props.patchValue}
69
+ />
70
+ )}
71
+ />
72
+ </label>
73
+ <label className="flex gap-1">
74
+ another_string
75
+ <PatchField
76
+ name="another_string"
77
+ render={(props) => (
78
+ <input
79
+ value={props.value as string}
80
+ onChange={(e) => {
81
+ props.setValue(e.target.value)
82
+ }}
83
+ onBlur={props.patchValue}
84
+ />
85
+ )}
86
+ />
87
+ </label>
88
+ </div>
89
+ </PatchFormProvider>
90
+ )
91
+ }
@@ -0,0 +1,119 @@
1
+ import { FormikErrors, FormikProvider, useFormik } from "formik"
2
+ import { FormEventHandler, forwardRef, memo, useCallback, useEffect, useMemo } from "react"
3
+
4
+ import { NewForm } from "../../builder"
5
+ import { ISchema } from "../../fields"
6
+ import { Form } from "../../typings"
7
+ import { hasKeys, initialFormValues, validateForm } from "../../utils"
8
+
9
+ interface PatchFormProviderProps {
10
+ children: React.ReactNode
11
+ schema: ISchema
12
+ values: Form
13
+ /** Called when `patchValue` is called on a child `PatchField` and the form is valid.
14
+ * @example ```js
15
+ * {"field name": "field value"}
16
+ * ``` */
17
+ onPatch: (values: Form) => void
18
+ /** Called when `patchValue` is called on a child `PatchField` and the form is not valid.
19
+ * After this event is fired, the form is reset to the initial values.
20
+ * @example ```js
21
+ * {"field name": "error message"}
22
+ * ``` */
23
+ onError: (error: FormikErrors<Form | NewForm>) => void
24
+ className?: string
25
+ /** If true (default), the form will only submit if there are changes. */
26
+ requiresDiff?: boolean
27
+ /** Called when the form's dirty state changes. */
28
+ onDirtyChange?: (dirty: boolean) => void
29
+ }
30
+
31
+ /** Use PatchForms to create patch edits to existing forms rather than editing the entire form. */
32
+ export const PatchFormProvider = memo(
33
+ forwardRef<HTMLFormElement, PatchFormProviderProps>((props, ref) => {
34
+ const { children, schema, values, onPatch, onError, requiresDiff = true, onDirtyChange, ...rest } = props
35
+
36
+ const initialValues = useMemo(() => initialFormValues(schema.fields, values), [schema.fields, values])
37
+
38
+ const getDiff = useCallback(
39
+ (values: Form): Form => {
40
+ const diff: Form = {}
41
+
42
+ for (const key in values) {
43
+ const value = values[key]
44
+ if (value !== initialValues[key] && value !== undefined) {
45
+ diff[key] = value
46
+ }
47
+ }
48
+
49
+ return diff
50
+ },
51
+ [initialValues],
52
+ )
53
+
54
+ const handlePatch = useCallback(
55
+ (values: Form) => {
56
+ const diff: Form = getDiff(values)
57
+ // skip if no changes
58
+ if (requiresDiff && !hasKeys(diff)) return
59
+ onPatch(diff)
60
+ },
61
+ [getDiff, onPatch, requiresDiff],
62
+ )
63
+
64
+ const validate = useCallback(
65
+ (form: Form) => {
66
+ const error = validateForm(schema, form)
67
+
68
+ if (error) {
69
+ // report the error
70
+ onError(error)
71
+ }
72
+
73
+ if (onDirtyChange) {
74
+ const diff = getDiff(form)
75
+ onDirtyChange(hasKeys(diff))
76
+ }
77
+
78
+ return error
79
+ },
80
+ [schema, onDirtyChange, onError, getDiff],
81
+ )
82
+
83
+ const formik = useFormik<Form>({
84
+ initialValues,
85
+ onSubmit: handlePatch,
86
+ validate,
87
+ // only validate the entire form on submit
88
+ validateOnBlur: false,
89
+ validateOnChange: false,
90
+ })
91
+
92
+ const handleChange: FormEventHandler<HTMLFormElement> = useCallback(() => {
93
+ if (onDirtyChange) {
94
+ const diff = getDiff(formik.values)
95
+ if (hasKeys(diff)) {
96
+ onDirtyChange(true)
97
+ }
98
+ }
99
+ }, [formik.values, getDiff, onDirtyChange])
100
+
101
+ const { errors, resetForm } = formik
102
+
103
+ useEffect(() => {
104
+ // on errors, reset the form the initial values
105
+ if (hasKeys(errors)) {
106
+ resetForm({ values: initialValues, errors: {} })
107
+ }
108
+ }, [errors, initialValues, resetForm])
109
+
110
+ return (
111
+ <FormikProvider value={formik}>
112
+ {/* the `form` captures any submit events that are generated */}
113
+ <form {...rest} ref={ref} onSubmit={formik.handleSubmit} onChange={handleChange}>
114
+ {children}
115
+ </form>
116
+ </FormikProvider>
117
+ )
118
+ }),
119
+ )
@@ -0,0 +1,2 @@
1
+ export * from "./Field"
2
+ export * from "./Provider"
@@ -0,0 +1,2 @@
1
+ export * from "./FormRenderer/FormRenderer"
2
+ export * from "./PatchForm"
@@ -0,0 +1,162 @@
1
+ import { Marker } from "@overmap-ai/core"
2
+ import { HTMLInputTypeAttribute } from "react"
3
+
4
+ import { AnyField, BaseField } from "./fields"
5
+
6
+ /**
7
+ * When upload fields are given initial values, that value will be
8
+ * a `Promise<File>[]` instead of a `File[]` as the files must be downloaded.
9
+ * `Promise<File>[]` is not add to the `FieldValue` type as it is not a valid
10
+ * value for submitted forms as the files must be present to be submitted
11
+ * TODO: This causes type errors in hemora-web. Maybe we should change this to:
12
+ * export type Form = {
13
+ * data: Record<string, FieldValue>,
14
+ * attachments: Record<string, Promise<File>[]>
15
+ * }
16
+ * Then, we could just use `data` for creating attachment instances.
17
+ */
18
+ export type Form = Record<string, FieldValue | Promise<File>[]>
19
+
20
+ /** Helper type that extracts the TValue type from a BaseField. */
21
+ export type ValueOfField<Type extends AnyField> = Type extends BaseField<infer TValue> ? TValue : never
22
+
23
+ // Register all allowed field types here.
24
+ // Note: a serialized type does not exist for custom, as it is only used for internal forms
25
+ export type FieldTypeIdentifier =
26
+ | "string"
27
+ | "text"
28
+ | "boolean"
29
+ | "number"
30
+ | "date"
31
+ | "select"
32
+ | "custom"
33
+ | "section"
34
+ | "multi-string"
35
+ | "multi-select"
36
+ | "upload"
37
+ | "qr"
38
+
39
+ export interface BaseSerializedObject<TIdentifier extends FieldTypeIdentifier = FieldTypeIdentifier> {
40
+ description?: string | null
41
+ identifier: string
42
+ type: TIdentifier
43
+ }
44
+
45
+ export interface BaseSerializedField<TIdentifier extends FieldTypeIdentifier = FieldTypeIdentifier>
46
+ extends BaseSerializedObject<TIdentifier> {
47
+ label: string
48
+ required: boolean
49
+ image?: File | Promise<File>
50
+ }
51
+
52
+ /** All the possible field values */
53
+ export type FieldValue = string | number | boolean | string[] | File[] | Date | Marker | null
54
+
55
+ export interface SerializedCondition<TValue extends FieldValue = FieldValue> {
56
+ identifier: string
57
+ // multi-string fields would produce an array of `SelectFieldOption`
58
+ value: TValue
59
+ }
60
+
61
+ export interface SerializedFieldSection extends BaseSerializedObject {
62
+ label: string | null
63
+ type: "section"
64
+ conditional: boolean
65
+ condition: SerializedCondition | null
66
+ fields: Exclude<ISerializedField, SerializedFieldSection>[]
67
+ }
68
+
69
+ interface BaseSerializedStringField extends BaseSerializedField {
70
+ minimum_length?: number
71
+ maximum_length: number
72
+ placeholder?: string
73
+ }
74
+
75
+ export type StringInputType = Exclude<HTMLInputTypeAttribute, "checkbox" | "number" | "button">
76
+
77
+ export interface SerializedStringField extends BaseSerializedStringField {
78
+ type: "string"
79
+ input_type?: StringInputType
80
+ }
81
+
82
+ export interface SerializedQrField extends BaseSerializedField {
83
+ type: "qr"
84
+ }
85
+
86
+ export interface SerializedTextField extends BaseSerializedStringField {
87
+ type: "text"
88
+ }
89
+
90
+ export interface SerializedMultiStringField extends BaseSerializedStringField {
91
+ type: "multi-string"
92
+ }
93
+
94
+ export interface SerializedBooleanField extends BaseSerializedField {
95
+ type: "boolean"
96
+ // TODO:
97
+ // default?: boolean
98
+ }
99
+
100
+ export interface SerializedNumberField extends BaseSerializedField {
101
+ type: "number"
102
+ minimum: number | undefined
103
+ maximum: number | undefined
104
+ integers: boolean
105
+ placeholder?: string
106
+ }
107
+
108
+ export interface SerializedDateField extends BaseSerializedField {
109
+ type: "date"
110
+ }
111
+
112
+ // The types of values that can be selected in the select input.
113
+ export type SelectFieldOptionValue = string
114
+
115
+ /** Represents an option in the select input. Not to be confused with the 'field options' of SelectField. */
116
+ export interface SelectFieldOption {
117
+ value: SelectFieldOptionValue
118
+ label: string
119
+ }
120
+
121
+ export interface SerializedSelectField extends BaseSerializedField {
122
+ type: "select"
123
+ options: SelectFieldOption[]
124
+ placeholder?: string
125
+ }
126
+
127
+ export interface SerializedMultiSelectField extends BaseSerializedField {
128
+ type: "multi-select"
129
+ options: SelectFieldOption[]
130
+ placeholder?: string
131
+ }
132
+
133
+ export interface SerializedUploadField extends BaseSerializedField {
134
+ type: "upload"
135
+ /** @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept */
136
+ extensions?: string[]
137
+ /** in bytes */
138
+ maximum_size?: number
139
+ /** how many files the user can upload to this field
140
+ * @default 1
141
+ */
142
+ maximum_files?: number
143
+ }
144
+
145
+ export type ISerializedField =
146
+ // Add type hints for final field interfaces
147
+ | SerializedTextField
148
+ | SerializedBooleanField
149
+ | SerializedNumberField
150
+ | SerializedDateField
151
+ | SerializedStringField
152
+ | SerializedSelectField
153
+ | SerializedFieldSection
154
+ | SerializedMultiStringField
155
+ | SerializedMultiSelectField
156
+ | SerializedUploadField
157
+ | SerializedQrField
158
+
159
+ // REASON: Sometimes, we don't accept a FieldSection
160
+ export type ISerializedOnlyField = Exclude<ISerializedField, SerializedFieldSection>
161
+
162
+ export type Severity = "danger" | "warning" | "info" | "success"