@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,53 @@
1
+ import { ReactNode } from "react"
2
+ import { RiCheckboxLine } from "react-icons/ri"
3
+
4
+ import { ISerializedField, SelectFieldOptionValue, SerializedMultiSelectField } from "../../typings"
5
+ import { emptyBaseField } from "../BaseField"
6
+ import { GetInputProps } from "../typings"
7
+ import { BaseSelectField, BaseSelectFieldOptions } from "./BaseSelectField"
8
+ import { MultiSelectInput } from "./MultiSelectInput"
9
+
10
+ /**
11
+ * The options passed to the constructor of MultiSelectField.
12
+ */
13
+ export type MultiSelectFieldOptions = Omit<BaseSelectFieldOptions<SelectFieldOptionValue[], "multi-select">, "type">
14
+
15
+ export const emptyMultiSelectField = {
16
+ ...emptyBaseField,
17
+ type: "multi-select",
18
+ options: [],
19
+ }
20
+
21
+ export class MultiSelectField extends BaseSelectField<SelectFieldOptionValue[], "multi-select"> {
22
+ static readonly fieldTypeName = "Multi-select"
23
+ static readonly fieldTypeDescription = "Allows the user to select a multiple options from a list of options."
24
+
25
+ static Icon: typeof RiCheckboxLine = RiCheckboxLine
26
+
27
+ public constructor(options: MultiSelectFieldOptions) {
28
+ const { placeholder = "Select one or more...", ...rest } = options
29
+ super({ ...rest, placeholder, type: "multi-select" })
30
+ }
31
+
32
+ public getValueFromChangeEvent(event: React.ChangeEvent<HTMLInputElement> | string[]): string[] {
33
+ if (Array.isArray(event)) return event
34
+ throw new Error("Expected an array.")
35
+ }
36
+
37
+ protected isBlank(value: SelectFieldOptionValue[]): boolean {
38
+ return super.isBlank(value) || value.length === 0
39
+ }
40
+
41
+ serialize(): SerializedMultiSelectField {
42
+ return super._serialize()
43
+ }
44
+
45
+ static deserialize(data: ISerializedField): MultiSelectField {
46
+ if (data.type !== "multi-select") throw new Error("Type mismatch.")
47
+ return new MultiSelectField(data)
48
+ }
49
+
50
+ getInput(props: GetInputProps<this>): ReactNode {
51
+ return <MultiSelectInput field={this} {...props} />
52
+ }
53
+ }
@@ -0,0 +1,80 @@
1
+ import { Button, Menu, RiIcon } from "@overmap-ai/blocks"
2
+ import { memo, useCallback, useMemo } from "react"
3
+
4
+ import { InputWithLabel, InputWithLabelAndHelpText, useFormikInput } from "../BaseField"
5
+ import { ComponentProps } from "../typings"
6
+ import { MultiSelectField } from "./MultiSelectField"
7
+
8
+ const parseValueToArray = (value: string[] | string): string[] => {
9
+ if (!value) return []
10
+ if (Array.isArray(value)) return value
11
+ return [value]
12
+ }
13
+
14
+ export const MultiSelectInput = memo((props: ComponentProps<MultiSelectField>) => {
15
+ const [{ inputId, labelId, size, severity, showInputOnly, field, fieldProps }, rest] = useFormikInput(props)
16
+ const { onChange, onBlur } = fieldProps
17
+ let [{ helpText, label }] = useFormikInput(props)
18
+ helpText = showInputOnly ? null : helpText
19
+ label = showInputOnly ? "" : label
20
+
21
+ const value = useMemo(() => parseValueToArray(fieldProps.value), [fieldProps.value])
22
+
23
+ const handleChange = useCallback(
24
+ (value: string[]) => {
25
+ onChange(value)
26
+ onBlur(value)
27
+ },
28
+ [onChange, onBlur],
29
+ )
30
+
31
+ return (
32
+ <InputWithLabelAndHelpText helpText={helpText} severity={severity}>
33
+ <InputWithLabel
34
+ size={size}
35
+ severity={severity}
36
+ inputId={inputId}
37
+ labelId={labelId}
38
+ label={label}
39
+ image={showInputOnly ? undefined : field.image}
40
+ >
41
+ <Menu.Root>
42
+ <Menu.ClickTrigger>
43
+ <Button
44
+ id={inputId}
45
+ className="!justify-between"
46
+ name={fieldProps.name}
47
+ variant="soft"
48
+ {...rest}
49
+ >
50
+ {value.length > 0 ? value.join(", ") : field.placeholder}
51
+ <RiIcon icon="RiArrowDownSLine" />
52
+ </Button>
53
+ </Menu.ClickTrigger>
54
+ <Menu.Content>
55
+ <Menu.MultiSelectGroup values={value} onValuesChange={handleChange}>
56
+ <Menu.SelectAllItem allValues={field.options.map(({ value }) => value)}>
57
+ <Menu.SelectAllIndicator>
58
+ {(indeterminate) =>
59
+ indeterminate ? <RiIcon icon="RiSubtractLine" /> : <RiIcon icon="RiCheckLine" />
60
+ }
61
+ </Menu.SelectAllIndicator>
62
+ Select all
63
+ </Menu.SelectAllItem>
64
+ {field.options.map((option) => (
65
+ <Menu.MultiSelectItem key={option.value} value={option.value}>
66
+ <Menu.SelectedIndicator>
67
+ <RiIcon icon="RiCheckLine" />
68
+ </Menu.SelectedIndicator>
69
+ {option.label}
70
+ </Menu.MultiSelectItem>
71
+ ))}
72
+ </Menu.MultiSelectGroup>
73
+ </Menu.Content>
74
+ </Menu.Root>
75
+ </InputWithLabel>
76
+ </InputWithLabelAndHelpText>
77
+ )
78
+ })
79
+
80
+ MultiSelectInput.displayName = "MultiSelectInput"
@@ -0,0 +1,49 @@
1
+ import { ChangeEvent, ReactNode } from "react"
2
+ import { RiMenuFoldLine } from "react-icons/ri"
3
+
4
+ import { ISerializedField, SelectFieldOptionValue, SerializedSelectField } from "../../typings"
5
+ import { emptyBaseField } from "../BaseField"
6
+ import { GetInputProps } from "../typings"
7
+ import { BaseSelectField, BaseSelectFieldOptions } from "./BaseSelectField"
8
+ import { SelectInput } from "./SelectInput"
9
+
10
+ /**
11
+ * The options passed to the constructor of SelectField.
12
+ */
13
+ export type SelectFieldOptions = Omit<BaseSelectFieldOptions<SelectFieldOptionValue, "select">, "type">
14
+
15
+ export const emptySelectField = {
16
+ ...emptyBaseField,
17
+ type: "select",
18
+ options: [],
19
+ }
20
+
21
+ export class SelectField extends BaseSelectField<SelectFieldOptionValue, "select"> {
22
+ static readonly fieldTypeName = "Dropdown"
23
+ static readonly fieldTypeDescription = "Allows the user to select a single option from a list of options."
24
+
25
+ static Icon: typeof RiMenuFoldLine = RiMenuFoldLine
26
+
27
+ public constructor(options: SelectFieldOptions) {
28
+ const { placeholder = "Select one...", ...rest } = options
29
+ super({ ...rest, placeholder, type: "select" })
30
+ }
31
+
32
+ public getValueFromChangeEvent(event: ChangeEvent<HTMLInputElement> | string): SelectFieldOptionValue {
33
+ if (typeof event === "string") return event
34
+ return event.target.value
35
+ }
36
+
37
+ serialize(): SerializedSelectField {
38
+ return super._serialize()
39
+ }
40
+
41
+ static deserialize(data: ISerializedField): SelectField {
42
+ if (data.type !== "select") throw new Error("Type mismatch.")
43
+ return new SelectField(data)
44
+ }
45
+
46
+ getInput(props: GetInputProps<this>): ReactNode {
47
+ return <SelectInput field={this} {...props} />
48
+ }
49
+ }
@@ -0,0 +1,69 @@
1
+ import { Button, Menu, RiIcon } from "@overmap-ai/blocks"
2
+ import { memo, useCallback } from "react"
3
+
4
+ import { SEVERITY_COLOR_MAPPING } from "../../constants"
5
+ import { InputWithLabel, InputWithLabelAndHelpText, useFormikInput } from "../BaseField"
6
+ import { ComponentProps } from "../typings"
7
+ import { SelectField } from "./SelectField"
8
+
9
+ export const SelectInput = memo((props: ComponentProps<SelectField>) => {
10
+ const [{ inputId, labelId, size, severity, showInputOnly, field, fieldProps }, rest] = useFormikInput(props)
11
+ const { onChange, onBlur } = fieldProps
12
+ let [{ helpText, label }] = useFormikInput(props)
13
+ helpText = showInputOnly ? null : helpText
14
+ label = showInputOnly ? "" : label
15
+
16
+ const handleChange = useCallback(
17
+ (value: string | null) => {
18
+ onChange(value)
19
+ onBlur(value)
20
+ },
21
+ [onChange, onBlur],
22
+ )
23
+
24
+ const color = severity ? SEVERITY_COLOR_MAPPING[severity] : undefined
25
+
26
+ return (
27
+ <InputWithLabelAndHelpText helpText={helpText} severity={severity}>
28
+ <InputWithLabel
29
+ size={size}
30
+ severity={severity}
31
+ inputId={inputId}
32
+ labelId={labelId}
33
+ label={label}
34
+ image={showInputOnly ? undefined : field.image}
35
+ >
36
+ <Menu.Root>
37
+ <Menu.ClickTrigger>
38
+ <Button
39
+ {...fieldProps}
40
+ className="!justify-between"
41
+ id={inputId}
42
+ name={fieldProps.name}
43
+ accentColor={color}
44
+ variant="soft"
45
+ {...rest}
46
+ >
47
+ {fieldProps.value ? fieldProps.value : field.placeholder}
48
+ <RiIcon icon="RiArrowDownSLine" />
49
+ </Button>
50
+ </Menu.ClickTrigger>
51
+ <Menu.Content>
52
+ <Menu.SelectGroup value={fieldProps.value} onValueChange={handleChange}>
53
+ {field.options.map((option) => (
54
+ <Menu.SelectItem key={option.value} value={option.value}>
55
+ <Menu.SelectedIndicator>
56
+ <RiIcon icon="RiCheckLine" />
57
+ </Menu.SelectedIndicator>
58
+ {option.label}
59
+ </Menu.SelectItem>
60
+ ))}
61
+ </Menu.SelectGroup>
62
+ </Menu.Content>
63
+ </Menu.Root>
64
+ </InputWithLabel>
65
+ </InputWithLabelAndHelpText>
66
+ )
67
+ })
68
+
69
+ SelectInput.displayName = "SelectInput"
@@ -0,0 +1,4 @@
1
+ export * from "./MultiSelectField"
2
+ export * from "./MultiSelectInput"
3
+ export * from "./SelectField"
4
+ export * from "./SelectInput"
@@ -0,0 +1,61 @@
1
+ import { ReactNode } from "react"
2
+ import { RiInputField } from "react-icons/ri"
3
+
4
+ import { SHORT_TEXT_FIELD_MAX_LENGTH } from "../../../constants"
5
+ import { ISerializedField, SerializedStringField, StringInputType } from "../../../typings"
6
+ import { emptyBaseField } from "../../BaseField"
7
+ import { GetInputProps } from "../../typings"
8
+ import { StringOrTextField, StringOrTextFieldOptions } from "../StringOrTextField"
9
+ import { StringInput } from "./StringInput"
10
+
11
+ export interface StringFieldOptions extends Omit<StringOrTextFieldOptions, "type"> {
12
+ inputType?: StringInputType
13
+ }
14
+
15
+ export const emptyStringField = {
16
+ ...emptyBaseField,
17
+ type: "string",
18
+ maximum_length: SHORT_TEXT_FIELD_MAX_LENGTH,
19
+ input_type: "text",
20
+ }
21
+
22
+ export class StringField extends StringOrTextField<"string"> {
23
+ static readonly fieldTypeName = "Short Text"
24
+ static readonly fieldTypeDescription = `Short text fields can hold up to ${SHORT_TEXT_FIELD_MAX_LENGTH} characters on a single line.`
25
+ public readonly inputType: StringInputType
26
+
27
+ static Icon: typeof RiInputField = RiInputField
28
+
29
+ constructor(options: StringFieldOptions) {
30
+ const { inputType = "text", ...rest } = options
31
+ // the field supports a max length no larger than 500
32
+ const maxLength = options.maxLength
33
+ ? Math.min(SHORT_TEXT_FIELD_MAX_LENGTH, options.maxLength)
34
+ : SHORT_TEXT_FIELD_MAX_LENGTH
35
+ // the field supports a min length no larger than the max length
36
+ const minLength = options.minLength ? Math.min(options.minLength, maxLength) : undefined
37
+ super({ ...rest, maxLength, minLength, type: "string" })
38
+
39
+ this.inputType = inputType
40
+ }
41
+
42
+ serialize(): SerializedStringField {
43
+ return { ...super._serialize(), input_type: this.inputType }
44
+ }
45
+
46
+ static deserialize(data: ISerializedField): StringField {
47
+ if (data.type !== "string") throw new Error("Type mismatch.")
48
+ const { maximum_length, minimum_length, input_type, ...rest } = data
49
+ return new StringField({
50
+ ...rest,
51
+ maxLength: maximum_length,
52
+ minLength: minimum_length,
53
+ inputType: input_type,
54
+ placeholder: "Enter a short description",
55
+ })
56
+ }
57
+
58
+ getInput(props: GetInputProps<this>): ReactNode {
59
+ return <StringInput field={this} {...props} />
60
+ }
61
+ }
@@ -0,0 +1,41 @@
1
+ import { Input } from "@overmap-ai/blocks"
2
+ import { memo } from "react"
3
+
4
+ import { SEVERITY_COLOR_MAPPING } from "../../../constants"
5
+ import { InputWithLabel, InputWithLabelAndHelpText, useFormikInput } from "../../BaseField"
6
+ import { ComponentProps } from "../../typings"
7
+ import { StringField } from "./StringField"
8
+
9
+ export const StringInput = memo((props: ComponentProps<StringField>) => {
10
+ const [{ inputId, labelId, size, severity, showInputOnly, field, fieldProps }, rest] = useFormikInput(props)
11
+ let [{ helpText, label }] = useFormikInput(props)
12
+ helpText = showInputOnly ? null : helpText
13
+ label = showInputOnly ? "" : label
14
+
15
+ const color = severity ? SEVERITY_COLOR_MAPPING[severity] : undefined
16
+
17
+ return (
18
+ <InputWithLabelAndHelpText helpText={helpText} severity={severity}>
19
+ <InputWithLabel
20
+ size={size}
21
+ severity={severity}
22
+ inputId={inputId}
23
+ labelId={labelId}
24
+ label={label}
25
+ image={showInputOnly ? undefined : field.image}
26
+ >
27
+ <Input.Root accentColor={color} variant="soft">
28
+ <Input.Field
29
+ {...rest}
30
+ {...fieldProps}
31
+ type={field.inputType}
32
+ id={inputId}
33
+ placeholder={field.placeholder}
34
+ />
35
+ </Input.Root>
36
+ </InputWithLabel>
37
+ </InputWithLabelAndHelpText>
38
+ )
39
+ })
40
+
41
+ StringInput.displayName = "StringInput"
@@ -0,0 +1,2 @@
1
+ export * from "./StringField"
2
+ export * from "./StringInput"
@@ -0,0 +1,143 @@
1
+ import get from "lodash.get"
2
+
3
+ import { FormikUserFormRevision } from "../../builder"
4
+ import { LONG_TEXT_FIELD_MAX_LENGTH } from "../../constants"
5
+ import { Form, SerializedStringField } from "../../typings"
6
+ import { BaseField, FieldOptions } from "../BaseField"
7
+ import { NumberField, NumberFieldValue } from "../NumberField"
8
+ import { InputFieldLevelValidator, InputValidator } from "../typings"
9
+
10
+ export interface StringOrTextFieldOptions extends FieldOptions<string> {
11
+ minLength?: NumberFieldValue
12
+ maxLength?: NumberFieldValue
13
+ placeholder?: string
14
+ }
15
+
16
+ // NOTE: If changing, also change it in NumberField.ts (avoid circular imports)
17
+ const valueIsFormikUserFormRevision = (form: FormikUserFormRevision | Form): form is FormikUserFormRevision => {
18
+ return "fields" in form
19
+ }
20
+
21
+ export type SerializedStringOrTextField<TIdentifier extends "string" | "text"> = Omit<SerializedStringField, "type"> & {
22
+ type: TIdentifier
23
+ }
24
+
25
+ export abstract class StringOrTextField<TIdentifier extends "string" | "text"> extends BaseField<string, TIdentifier> {
26
+ public readonly minLength?: number
27
+ public readonly maxLength: number
28
+ public readonly placeholder: string
29
+
30
+ protected constructor(options: StringOrTextFieldOptions) {
31
+ const { minLength, maxLength, placeholder = "", ...base } = options
32
+ super(base)
33
+ // lengths must be greater than or equal to 0
34
+ this.minLength = minLength ? Math.max(minLength, 0) : undefined
35
+ this.maxLength = maxLength ? Math.max(maxLength, 0) : LONG_TEXT_FIELD_MAX_LENGTH
36
+ this.placeholder = placeholder
37
+ }
38
+
39
+ /**
40
+ * This function returns a function that validates that the value given for "minimum length" (when creating a new field) is less than or
41
+ * equal to the value given for "maximum length".
42
+ */
43
+ static _validateMin: (path: string) => InputValidator<NumberFieldValue> = (path: string) => (value, allValues) => {
44
+ const field = valueIsFormikUserFormRevision(allValues)
45
+ ? (get(allValues, path) as SerializedStringField)
46
+ : allValues
47
+ if (typeof field.maximum_length === "number" && typeof value === "number" && field.maximum_length < value) {
48
+ return "Minimum cannot be greater than maximum."
49
+ }
50
+ return null
51
+ }
52
+
53
+ /**
54
+ * This function returns a function that validates that the value given for "maximum length" (when creating a new field) is greater than or
55
+ * equal to the value given for "minimum length".
56
+ */
57
+ static _validateMax: (path: string) => InputValidator<NumberFieldValue> = (path: string) => (value, allValues) => {
58
+ if (typeof value !== "number") return null
59
+
60
+ const { minimum_length: minimumLength } = valueIsFormikUserFormRevision(allValues)
61
+ ? (get(allValues, path) as SerializedStringField)
62
+ : allValues
63
+
64
+ if (typeof minimumLength !== "number") {
65
+ return null
66
+ }
67
+
68
+ if (minimumLength > value) {
69
+ return "Maximum cannot be less than minimum."
70
+ }
71
+ return null
72
+ }
73
+
74
+ static getFieldCreationSchema(parentPath = "") {
75
+ const path = parentPath && `${parentPath}.`
76
+ return [
77
+ {
78
+ field:
79
+ // min, max
80
+ new NumberField({
81
+ label: "Minimum length",
82
+ description: "Minimum number of characters",
83
+ required: false,
84
+ identifier: `${path}minimum_length`,
85
+ minimum: 0,
86
+ maximum: 100,
87
+ formValidators: [this._validateMin(parentPath)],
88
+ integers: true,
89
+ }),
90
+ showDirectly: false,
91
+ },
92
+ {
93
+ field: new NumberField({
94
+ label: "Maximum length",
95
+ description: "Maximum number of characters",
96
+ required: false,
97
+ identifier: `${path}maximum_length`,
98
+ minimum: 1,
99
+ maximum: LONG_TEXT_FIELD_MAX_LENGTH, // TODO: depends on short vs long text
100
+ formValidators: [this._validateMax(parentPath)],
101
+ // TODO: default: 500 (see: "Short text fields can hold up to 500 characters on a single line.")
102
+ integers: true,
103
+ }),
104
+ showDirectly: false,
105
+ },
106
+ ]
107
+ }
108
+
109
+ getFieldValidators(): InputFieldLevelValidator<string>[] {
110
+ const validators = super.getFieldValidators()
111
+
112
+ if (this.minLength) {
113
+ validators.push((value) => {
114
+ if (this.minLength && (!value || value.length < this.minLength)) {
115
+ // One exception to this rule:
116
+ if (!this.required && !value) return null
117
+ return `Minimum ${this.minLength} character(s).`
118
+ }
119
+ })
120
+ }
121
+ if (this.maxLength) {
122
+ validators.push((value) => {
123
+ if (typeof value === "string" && this.maxLength && value.length > this.maxLength) {
124
+ return `Maximum ${this.maxLength} character(s).`
125
+ }
126
+ })
127
+ }
128
+
129
+ return validators
130
+ }
131
+
132
+ protected _serialize(): SerializedStringOrTextField<TIdentifier> {
133
+ if (!this.identifier) {
134
+ throw new Error("Field identifier must be set before serializing.")
135
+ }
136
+ return {
137
+ ...super._serialize(),
138
+ minimum_length: this.minLength,
139
+ maximum_length: this.maxLength,
140
+ placeholder: this.placeholder,
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,52 @@
1
+ import { ReactNode } from "react"
2
+ import { RiAlignJustify } from "react-icons/ri"
3
+
4
+ import { LONG_TEXT_FIELD_MAX_LENGTH } from "../../../constants"
5
+ import { ISerializedField, SerializedTextField } from "../../../typings"
6
+ import { emptyBaseField } from "../../BaseField"
7
+ import { GetInputProps } from "../../typings"
8
+ import { StringOrTextField, StringOrTextFieldOptions } from "../StringOrTextField"
9
+ import { TextInput } from "./TextInput"
10
+
11
+ export type TextFieldOptions = Omit<StringOrTextFieldOptions, "type">
12
+
13
+ export const emptyTextField = {
14
+ ...emptyBaseField,
15
+ type: "text",
16
+ maximum_length: LONG_TEXT_FIELD_MAX_LENGTH,
17
+ }
18
+
19
+ export class TextField extends StringOrTextField<"text"> {
20
+ static readonly fieldTypeName = "Paragraph"
21
+ static readonly fieldTypeDescription = `Paragraph fields can hold up to ${LONG_TEXT_FIELD_MAX_LENGTH} characters and can have multiple lines.`
22
+
23
+ static Icon: typeof RiAlignJustify = RiAlignJustify
24
+
25
+ constructor(options: TextFieldOptions) {
26
+ const maxLength = options.maxLength
27
+ ? Math.min(LONG_TEXT_FIELD_MAX_LENGTH, options.maxLength)
28
+ : LONG_TEXT_FIELD_MAX_LENGTH
29
+ // the field supports a min length no larger than the max length
30
+ const minLength = options.minLength ? Math.min(options.minLength, maxLength) : undefined
31
+ super({ ...options, maxLength, minLength, type: "text" })
32
+ }
33
+
34
+ serialize(): SerializedTextField {
35
+ return super._serialize()
36
+ }
37
+
38
+ static deserialize(data: ISerializedField) {
39
+ if (data.type !== "text") throw new Error("Type mismatch.")
40
+ const { maximum_length, minimum_length, ...rest } = data
41
+ return new TextField({
42
+ ...rest,
43
+ maxLength: maximum_length,
44
+ minLength: minimum_length,
45
+ placeholder: "Enter a description",
46
+ })
47
+ }
48
+
49
+ getInput(props: GetInputProps<this>): ReactNode {
50
+ return <TextInput field={this} {...props} />
51
+ }
52
+ }
@@ -0,0 +1,42 @@
1
+ import { TextArea } from "@overmap-ai/blocks"
2
+ import { memo } from "react"
3
+
4
+ import { SEVERITY_COLOR_MAPPING } from "../../../constants"
5
+ import { InputWithLabel, InputWithLabelAndHelpText, useFormikInput } from "../../BaseField"
6
+ import { ComponentProps } from "../../typings"
7
+ import { TextField } from "./TextField"
8
+
9
+ export const TextInput = memo((props: ComponentProps<TextField>) => {
10
+ const [{ inputId, labelId, size, severity, showInputOnly, field, fieldProps }, rest] = useFormikInput(props)
11
+ let [{ helpText, label }] = useFormikInput(props)
12
+ helpText = showInputOnly ? null : helpText
13
+ label = showInputOnly ? "" : label
14
+
15
+ const color = severity ? SEVERITY_COLOR_MAPPING[severity] : undefined
16
+
17
+ return (
18
+ <InputWithLabelAndHelpText helpText={helpText} severity={severity}>
19
+ <InputWithLabel
20
+ size={size}
21
+ severity={severity}
22
+ inputId={inputId}
23
+ labelId={labelId}
24
+ label={label}
25
+ image={showInputOnly ? undefined : field.image}
26
+ >
27
+ <TextArea
28
+ {...rest}
29
+ {...fieldProps}
30
+ className="field-sizing-content"
31
+ resize="vertical"
32
+ id={inputId}
33
+ placeholder={field.placeholder}
34
+ accentColor={color}
35
+ variant="soft"
36
+ />
37
+ </InputWithLabel>
38
+ </InputWithLabelAndHelpText>
39
+ )
40
+ })
41
+
42
+ TextInput.displayName = "TextInput"
@@ -0,0 +1,2 @@
1
+ export * from "./TextField"
2
+ export * from "./TextInput"
@@ -0,0 +1,2 @@
1
+ export * from "./StringField"
2
+ export * from "./TextField"