@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.
- package/.husky/pre-commit +6 -0
- package/.prettierrc.json +10 -0
- package/.storybook/StoryDecorator.tsx +22 -0
- package/.storybook/main.ts +20 -0
- package/.storybook/palettes/green.css +66 -0
- package/.storybook/palettes/red.css +66 -0
- package/.storybook/preview.css +39 -0
- package/.storybook/preview.tsx +31 -0
- package/.storybook/tailwind-theme/accentPalette.css +181 -0
- package/.storybook/tailwind-theme/backgrounds.css +11 -0
- package/.storybook/tailwind-theme/basePalette.css +178 -0
- package/dev/publish-alpha.sh +13 -0
- package/dev/publish-patch.sh +3 -0
- package/eslint.config.js +56 -0
- package/package.json +93 -0
- package/src/ColorPicker/ColorPicker.tsx +47 -0
- package/src/ColorPicker/index.ts +1 -0
- package/src/FileBadge/FileBadge.tsx +27 -0
- package/src/FileBadge/index.ts +1 -0
- package/src/FileCard/FileCard.stories.tsx +69 -0
- package/src/FileCard/FileCard.tsx +53 -0
- package/src/FileCard/index.ts +1 -0
- package/src/FileIcon/FileIcon.tsx +31 -0
- package/src/FileIcon/index.ts +1 -0
- package/src/FileViewer/FileViewerProvider.stories.tsx +50 -0
- package/src/FileViewer/FileViewerProvider.tsx +72 -0
- package/src/FileViewer/context.ts +11 -0
- package/src/FileViewer/index.ts +3 -0
- package/src/FileViewer/typings.ts +5 -0
- package/src/ImageCard/ImageCard.stories.tsx +94 -0
- package/src/ImageCard/ImageCard.tsx +82 -0
- package/src/ImageCard/index.ts +1 -0
- package/src/ImageMarkup/ImageMarkup.stories.tsx +65 -0
- package/src/ImageMarkup/ImageMarkup.tsx +268 -0
- package/src/ImageMarkup/index.ts +1 -0
- package/src/ImageViewer/ImageViewer.stories.tsx +57 -0
- package/src/ImageViewer/ImageViewer.tsx +124 -0
- package/src/ImageViewer/constants.ts +1 -0
- package/src/ImageViewer/index.ts +2 -0
- package/src/PDFViewer/PDFViewer.stories.tsx +55 -0
- package/src/PDFViewer/PDFViewer.tsx +170 -0
- package/src/PDFViewer/constants.ts +1 -0
- package/src/PDFViewer/index.ts +2 -0
- package/src/SpreadsheetViewer/SpreadsheetViewer.stories.tsx +55 -0
- package/src/SpreadsheetViewer/SpreadsheetViewer.tsx +162 -0
- package/src/SpreadsheetViewer/constants.ts +8 -0
- package/src/SpreadsheetViewer/index.ts +2 -0
- package/src/forms/builder/DropDispatch.ts +84 -0
- package/src/forms/builder/FieldActions.tsx +155 -0
- package/src/forms/builder/FieldBuilder.tsx +386 -0
- package/src/forms/builder/FieldSectionWithActions.tsx +260 -0
- package/src/forms/builder/FieldWithActions.tsx +129 -0
- package/src/forms/builder/FieldsEditor.tsx +180 -0
- package/src/forms/builder/FormBuilder.stories.tsx +105 -0
- package/src/forms/builder/FormBuilder.tsx +237 -0
- package/src/forms/builder/constants.ts +18 -0
- package/src/forms/builder/hooks.tsx +24 -0
- package/src/forms/builder/index.ts +2 -0
- package/src/forms/builder/typings.ts +18 -0
- package/src/forms/builder/utils.ts +229 -0
- package/src/forms/constants.ts +9 -0
- package/src/forms/constantsJsx.tsx +67 -0
- package/src/forms/fields/BaseField/BaseField.ts +152 -0
- package/src/forms/fields/BaseField/hooks.tsx +60 -0
- package/src/forms/fields/BaseField/index.ts +4 -0
- package/src/forms/fields/BaseField/layouts.tsx +100 -0
- package/src/forms/fields/BaseField/typings.ts +9 -0
- package/src/forms/fields/BooleanField/BooleanField.tsx +48 -0
- package/src/forms/fields/BooleanField/BooleanInput.tsx +54 -0
- package/src/forms/fields/BooleanField/index.ts +2 -0
- package/src/forms/fields/CustomField/CustomField.tsx +45 -0
- package/src/forms/fields/CustomField/FieldInputClonerField/FieldInputCloner.tsx +25 -0
- package/src/forms/fields/CustomField/FieldInputClonerField/FieldInputClonerField.tsx +26 -0
- package/src/forms/fields/CustomField/FieldInputClonerField/index.ts +3 -0
- package/src/forms/fields/CustomField/FieldInputClonerField/typings.ts +8 -0
- package/src/forms/fields/CustomField/index.ts +1 -0
- package/src/forms/fields/DateField/DateField.tsx +42 -0
- package/src/forms/fields/DateField/DateInput.tsx +39 -0
- package/src/forms/fields/DateField/index.ts +2 -0
- package/src/forms/fields/FieldSection/FieldSection.tsx +173 -0
- package/src/forms/fields/FieldSection/FieldSectionLayout.tsx +56 -0
- package/src/forms/fields/FieldSection/index.ts +1 -0
- package/src/forms/fields/MultiStringField/MultiStringField.tsx +90 -0
- package/src/forms/fields/MultiStringField/MultiStringInput.tsx +207 -0
- package/src/forms/fields/MultiStringField/index.ts +2 -0
- package/src/forms/fields/NumberField/NumberField.tsx +173 -0
- package/src/forms/fields/NumberField/NumberInput.tsx +44 -0
- package/src/forms/fields/NumberField/index.ts +2 -0
- package/src/forms/fields/QrField/QrField.tsx +38 -0
- package/src/forms/fields/QrField/QrInput.module.sass +5 -0
- package/src/forms/fields/QrField/QrInput.tsx +144 -0
- package/src/forms/fields/QrField/index.ts +2 -0
- package/src/forms/fields/SelectField/BaseSelectField.ts +73 -0
- package/src/forms/fields/SelectField/MultiSelectField.tsx +53 -0
- package/src/forms/fields/SelectField/MultiSelectInput.tsx +80 -0
- package/src/forms/fields/SelectField/SelectField.tsx +49 -0
- package/src/forms/fields/SelectField/SelectInput.tsx +69 -0
- package/src/forms/fields/SelectField/index.ts +4 -0
- package/src/forms/fields/StringOrTextFields/StringField/StringField.tsx +61 -0
- package/src/forms/fields/StringOrTextFields/StringField/StringInput.tsx +41 -0
- package/src/forms/fields/StringOrTextFields/StringField/index.ts +2 -0
- package/src/forms/fields/StringOrTextFields/StringOrTextField.ts +143 -0
- package/src/forms/fields/StringOrTextFields/TextField/TextField.tsx +52 -0
- package/src/forms/fields/StringOrTextFields/TextField/TextInput.tsx +42 -0
- package/src/forms/fields/StringOrTextFields/TextField/index.ts +2 -0
- package/src/forms/fields/StringOrTextFields/index.ts +2 -0
- package/src/forms/fields/UploadField/UploadField.tsx +156 -0
- package/src/forms/fields/UploadField/UploadInput.tsx +220 -0
- package/src/forms/fields/UploadField/index.ts +2 -0
- package/src/forms/fields/UploadField/utils.ts +17 -0
- package/src/forms/fields/constants.ts +43 -0
- package/src/forms/fields/hooks.tsx +26 -0
- package/src/forms/fields/index.ts +12 -0
- package/src/forms/fields/typings.ts +45 -0
- package/src/forms/fields/utils.ts +125 -0
- package/src/forms/index.ts +5 -0
- package/src/forms/renderer/FormRenderer/FormRenderer.stories.tsx +142 -0
- package/src/forms/renderer/FormRenderer/FormRenderer.tsx +135 -0
- package/src/forms/renderer/PatchForm/Field.tsx +41 -0
- package/src/forms/renderer/PatchForm/PatchForm.stories.tsx +91 -0
- package/src/forms/renderer/PatchForm/Provider.tsx +119 -0
- package/src/forms/renderer/PatchForm/index.ts +2 -0
- package/src/forms/renderer/index.ts +2 -0
- package/src/forms/typings.ts +162 -0
- package/src/forms/utils.ts +69 -0
- package/src/index.ts +11 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.ts +8 -0
- package/tsconfig.json +26 -0
- 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,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,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"
|