@sanity/form-toolkit 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -1
- package/dist/index.d.mts +66 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +332 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +335 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -2
- package/src/form-schema/components/default-field.tsx +113 -0
- package/src/form-schema/components/form-renderer.tsx +53 -0
- package/src/form-schema/components/types.ts +53 -0
- package/src/form-schema/index.ts +9 -8
- package/src/form-schema/schema-types/form-field.ts +173 -109
- package/src/form-schema/schema-types/form.ts +43 -1
- package/src/form-schema/schema-types/index.ts +1 -1
- package/src/form-schema/structure/document-view.tsx +78 -0
- package/src/form-schema/structure/index.ts +11 -0
- package/src/index.ts +5 -2
- package/src/shared/create-handler.ts +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type {ChangeEvent, FC, LegacyRef} from 'react'
|
|
2
|
+
|
|
3
|
+
import type {FieldComponentProps} from './types'
|
|
4
|
+
|
|
5
|
+
export const DefaultField: FC<FieldComponentProps> = ({field, fieldState, error}) => {
|
|
6
|
+
const {type, label, name, options = {}, choices = []} = field
|
|
7
|
+
if (!type || !name) return null
|
|
8
|
+
|
|
9
|
+
const {value, onChange, onBlur, ref} = fieldState
|
|
10
|
+
|
|
11
|
+
const handleChange = (
|
|
12
|
+
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
|
|
13
|
+
) => {
|
|
14
|
+
onChange(e.target.value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const handleCheckboxChange = (e: ChangeEvent<HTMLInputElement>, choiceValue: string) => {
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
const newValue = e.target.checked
|
|
20
|
+
? [...value, choiceValue]
|
|
21
|
+
: value.filter((v: string) => v !== choiceValue)
|
|
22
|
+
onChange(newValue)
|
|
23
|
+
} else {
|
|
24
|
+
onChange(e.target.checked ? choiceValue : '')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const renderInput = () => {
|
|
29
|
+
switch (type) {
|
|
30
|
+
case 'textarea':
|
|
31
|
+
return (
|
|
32
|
+
<textarea
|
|
33
|
+
ref={ref as LegacyRef<HTMLTextAreaElement>}
|
|
34
|
+
name={name}
|
|
35
|
+
value={value ?? ''}
|
|
36
|
+
onChange={handleChange}
|
|
37
|
+
onBlur={onBlur}
|
|
38
|
+
placeholder={options.placeholder}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
case 'select':
|
|
43
|
+
return (
|
|
44
|
+
<select
|
|
45
|
+
ref={ref as LegacyRef<HTMLSelectElement>}
|
|
46
|
+
name={name}
|
|
47
|
+
value={value ?? ''}
|
|
48
|
+
onChange={handleChange}
|
|
49
|
+
onBlur={onBlur}
|
|
50
|
+
>
|
|
51
|
+
{choices?.map((choice, i) => (
|
|
52
|
+
<option key={i} value={choice.value}>
|
|
53
|
+
{choice.label}
|
|
54
|
+
</option>
|
|
55
|
+
))}
|
|
56
|
+
</select>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
case 'radio':
|
|
60
|
+
return choices?.map((choice, i) => (
|
|
61
|
+
<label key={i}>
|
|
62
|
+
<input
|
|
63
|
+
type="radio"
|
|
64
|
+
name={name}
|
|
65
|
+
ref={ref as LegacyRef<HTMLInputElement>}
|
|
66
|
+
value={choice.value}
|
|
67
|
+
checked={value === choice.value}
|
|
68
|
+
onChange={handleChange}
|
|
69
|
+
onBlur={onBlur}
|
|
70
|
+
/>
|
|
71
|
+
{choice.label}
|
|
72
|
+
</label>
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
case 'checkbox':
|
|
76
|
+
return choices?.map((choice, i) => (
|
|
77
|
+
<label key={i}>
|
|
78
|
+
<input
|
|
79
|
+
type="checkbox"
|
|
80
|
+
name={name}
|
|
81
|
+
ref={ref as LegacyRef<HTMLInputElement>}
|
|
82
|
+
value={choice.value}
|
|
83
|
+
checked={Array.isArray(value) ? value.includes(choice.value) : value === choice.value}
|
|
84
|
+
onChange={(e) => handleCheckboxChange(e, choice.value)}
|
|
85
|
+
onBlur={onBlur}
|
|
86
|
+
/>
|
|
87
|
+
{choice.label}
|
|
88
|
+
</label>
|
|
89
|
+
))
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
return (
|
|
93
|
+
<input
|
|
94
|
+
type={type}
|
|
95
|
+
ref={ref as LegacyRef<HTMLInputElement>}
|
|
96
|
+
name={name}
|
|
97
|
+
value={value ?? options.defaultValue ?? ''}
|
|
98
|
+
onChange={handleChange}
|
|
99
|
+
onBlur={onBlur}
|
|
100
|
+
placeholder={options.placeholder}
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
{label && type != 'hidden' && <label htmlFor={name}>{label}</label>}
|
|
109
|
+
{renderInput()}
|
|
110
|
+
{error && <span className="error">{error}</span>}
|
|
111
|
+
</>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type {ComponentType, FC, HTMLProps} from 'react'
|
|
2
|
+
|
|
3
|
+
import {DefaultField} from './default-field'
|
|
4
|
+
import type {FieldComponentProps, FieldState, FormDataProps, FormField} from './types'
|
|
5
|
+
|
|
6
|
+
interface FormRendererProps extends HTMLProps<HTMLFormElement> {
|
|
7
|
+
formData: FormDataProps
|
|
8
|
+
// Function to get field state for a given field name
|
|
9
|
+
getFieldState?: (fieldName: string) => FieldState
|
|
10
|
+
// Function to get field error for a given field name
|
|
11
|
+
getFieldError?: (fieldName: string) => string | undefined
|
|
12
|
+
// Override default field components
|
|
13
|
+
fieldComponents?: Record<string, ComponentType<FieldComponentProps>>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const FormRenderer: FC<FormRendererProps> = (props) => {
|
|
17
|
+
const {
|
|
18
|
+
formData,
|
|
19
|
+
getFieldState = (name) => ({
|
|
20
|
+
value: undefined,
|
|
21
|
+
onChange: () => {},
|
|
22
|
+
name, // Pass name to field for native form handling
|
|
23
|
+
}),
|
|
24
|
+
getFieldError,
|
|
25
|
+
fieldComponents = {},
|
|
26
|
+
children,
|
|
27
|
+
} = props
|
|
28
|
+
const renderField = (field: FormField) => {
|
|
29
|
+
const CustomComponent = fieldComponents[field.type]
|
|
30
|
+
const fieldState = getFieldState(field.name)
|
|
31
|
+
const error = getFieldError?.(field.name)
|
|
32
|
+
|
|
33
|
+
if (CustomComponent) {
|
|
34
|
+
return <CustomComponent field={field} fieldState={fieldState} error={error} />
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return <DefaultField field={field} fieldState={fieldState} error={error} />
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<form {...props} id={props.id ?? formData?.id?.current}>
|
|
42
|
+
{formData.fields?.map((field) => (
|
|
43
|
+
<div key={field._key} className="form-field">
|
|
44
|
+
{renderField(field)}
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
|
|
48
|
+
{children}
|
|
49
|
+
|
|
50
|
+
<button type="submit">{formData.submitButton?.text || 'Submit'}</button>
|
|
51
|
+
</form>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// types.ts
|
|
2
|
+
export type ValidationRule = {
|
|
3
|
+
type: string
|
|
4
|
+
value: string
|
|
5
|
+
message: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type FieldChoice = {
|
|
9
|
+
label: string
|
|
10
|
+
value: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FieldOptions = {
|
|
14
|
+
placeholder?: string
|
|
15
|
+
defaultValue?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type FormField = {
|
|
19
|
+
type: string
|
|
20
|
+
label?: string
|
|
21
|
+
name: string
|
|
22
|
+
required: boolean
|
|
23
|
+
validation?: ValidationRule[]
|
|
24
|
+
options?: FieldOptions
|
|
25
|
+
choices?: FieldChoice[]
|
|
26
|
+
_key: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FormDataProps = {
|
|
30
|
+
title: string
|
|
31
|
+
id: {
|
|
32
|
+
current: string
|
|
33
|
+
}
|
|
34
|
+
fields?: FormField[]
|
|
35
|
+
|
|
36
|
+
submitButton?: {
|
|
37
|
+
text: string
|
|
38
|
+
position: 'left' | 'center' | 'right'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FieldState {
|
|
43
|
+
value?: string | number | readonly string[]
|
|
44
|
+
onChange: (value: unknown) => void
|
|
45
|
+
onBlur?: () => void
|
|
46
|
+
ref?: unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FieldComponentProps {
|
|
50
|
+
field: FormField
|
|
51
|
+
fieldState: FieldState
|
|
52
|
+
error?: string
|
|
53
|
+
}
|
package/src/form-schema/index.ts
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import {definePlugin} from 'sanity'
|
|
2
|
+
// import {structureTool} from 'sanity/structure'
|
|
2
3
|
|
|
4
|
+
import {FormRenderer} from './components/form-renderer'
|
|
3
5
|
import {schema} from './schema-types'
|
|
4
|
-
|
|
5
|
-
/* nothing here yet */
|
|
6
|
-
}
|
|
6
|
+
// import {defaultDocumentNode} from './structure'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Usage in `sanity.config.ts` (or .js)
|
|
10
10
|
*
|
|
11
11
|
* ```ts
|
|
12
12
|
* import {defineConfig} from 'sanity'
|
|
13
|
-
* import {
|
|
13
|
+
* import {formSchema} from '@sanity/form-toolkit'
|
|
14
14
|
*
|
|
15
15
|
* export default defineConfig({
|
|
16
16
|
* // ...
|
|
17
|
-
* plugins: [
|
|
17
|
+
* plugins: [formSchema()],
|
|
18
18
|
* })
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
|
|
22
|
-
export const formSchema = definePlugin
|
|
21
|
+
export {FormRenderer}
|
|
22
|
+
export const formSchema = definePlugin(() => {
|
|
23
23
|
return {
|
|
24
|
-
name: '
|
|
24
|
+
name: 'form-toolkit_form-schema',
|
|
25
25
|
schema,
|
|
26
|
+
// plugins: [structureTool({defaultDocumentNode})],
|
|
26
27
|
}
|
|
27
28
|
})
|
|
@@ -1,146 +1,210 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CalendarIcon,
|
|
3
|
-
ClockIcon,
|
|
4
|
-
ColorWheelIcon,
|
|
5
|
-
DocumentIcon,
|
|
6
|
-
EarthGlobeIcon,
|
|
7
|
-
EnvelopeIcon,
|
|
8
|
-
HashIcon,
|
|
9
|
-
NumberIcon,
|
|
10
|
-
TextIcon,
|
|
11
|
-
} from '@sanity/icons'
|
|
1
|
+
import {LuTextCursorInput} from 'react-icons/lu'
|
|
12
2
|
import {defineField, defineType} from 'sanity'
|
|
13
3
|
|
|
4
|
+
interface ValidationContextDocument {
|
|
5
|
+
fields?: Array<{
|
|
6
|
+
name: string
|
|
7
|
+
type?: string
|
|
8
|
+
}>
|
|
9
|
+
}
|
|
10
|
+
// Validation options by field type
|
|
11
|
+
export const validationTypesByFieldType = {
|
|
12
|
+
checkbox: ['minSelectedCount', 'maxSelectedCount', 'custom'],
|
|
13
|
+
color: ['custom'],
|
|
14
|
+
date: ['minDate', 'maxDate', 'custom'],
|
|
15
|
+
'datetime-local': ['minDate', 'maxDate', 'custom'],
|
|
16
|
+
email: ['pattern', 'custom'],
|
|
17
|
+
file: ['maxSize', 'fileType', 'custom'],
|
|
18
|
+
hidden: ['custom'],
|
|
19
|
+
number: ['min', 'max', 'custom'],
|
|
20
|
+
// password: ['minLength', 'pattern', 'custom'],
|
|
21
|
+
radio: ['custom'],
|
|
22
|
+
range: ['min', 'max', 'step', 'custom'],
|
|
23
|
+
select: ['custom'],
|
|
24
|
+
tel: ['pattern', 'custom'],
|
|
25
|
+
text: ['minLength', 'maxLength', 'pattern', 'custom'],
|
|
26
|
+
textarea: ['minLength', 'maxLength', 'custom'],
|
|
27
|
+
time: ['custom'],
|
|
28
|
+
url: ['pattern', 'custom'],
|
|
29
|
+
}
|
|
14
30
|
export const formFieldType = defineType({
|
|
15
31
|
name: 'formField',
|
|
32
|
+
title: 'Form Field',
|
|
16
33
|
type: 'object',
|
|
34
|
+
icon: LuTextCursorInput,
|
|
17
35
|
fields: [
|
|
18
36
|
defineField({
|
|
19
|
-
name: '
|
|
37
|
+
name: 'type',
|
|
38
|
+
title: 'Field Type',
|
|
20
39
|
type: 'string',
|
|
21
|
-
group: 'info',
|
|
22
|
-
}),
|
|
23
|
-
defineField({
|
|
24
|
-
name: 'name',
|
|
25
|
-
type: 'slug',
|
|
26
|
-
group: 'info',
|
|
27
|
-
validation: (rule) => rule.required(),
|
|
28
40
|
options: {
|
|
29
|
-
|
|
41
|
+
list: Object.keys(validationTypesByFieldType).map((type) => {
|
|
42
|
+
const title = (fieldType: string) => {
|
|
43
|
+
switch (fieldType) {
|
|
44
|
+
case 'datetime-local':
|
|
45
|
+
return 'Date & Time'
|
|
46
|
+
case 'textarea':
|
|
47
|
+
return 'Text Area'
|
|
48
|
+
case 'tel':
|
|
49
|
+
return 'Phone Number'
|
|
50
|
+
default:
|
|
51
|
+
return fieldType.charAt(0).toUpperCase() + fieldType.slice(1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {title: title(type), value: type}
|
|
55
|
+
}),
|
|
30
56
|
},
|
|
31
57
|
}),
|
|
32
58
|
defineField({
|
|
33
|
-
name: '
|
|
59
|
+
name: 'label',
|
|
60
|
+
title: 'Field Label',
|
|
34
61
|
type: 'string',
|
|
35
|
-
group: 'info',
|
|
36
|
-
validation: (rule) => rule.required(),
|
|
37
|
-
options: {
|
|
38
|
-
list: [
|
|
39
|
-
{value: 'color', title: 'Color'},
|
|
40
|
-
{value: 'date', title: 'Date'},
|
|
41
|
-
{value: 'datetime-local', title: 'Date-time'},
|
|
42
|
-
'email',
|
|
43
|
-
'file',
|
|
44
|
-
{value: 'month', title: 'Month & year'},
|
|
45
|
-
'number',
|
|
46
|
-
{value: 'tel', title: 'Telephone'},
|
|
47
|
-
'text',
|
|
48
|
-
'time',
|
|
49
|
-
'range',
|
|
50
|
-
{value: 'url', title: 'URL'},
|
|
51
|
-
'week',
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
62
|
}),
|
|
55
63
|
defineField({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
type: '
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
name: 'name',
|
|
65
|
+
title: 'Field Name',
|
|
66
|
+
type: 'string',
|
|
67
|
+
description:
|
|
68
|
+
'Must start with a letter and contain only letters, numbers, underscores, or hyphens. Must be unique within the form.',
|
|
69
|
+
validation: (Rule) =>
|
|
70
|
+
Rule.required().custom((name, context) => {
|
|
71
|
+
if (!name) {
|
|
72
|
+
return 'Required'
|
|
73
|
+
}
|
|
74
|
+
// Check format (HTML ID/name rules)
|
|
75
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
|
|
76
|
+
return 'Field name must start with a letter and contain only letters, numbers, underscores, or hyphens'
|
|
77
|
+
}
|
|
61
78
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
defineField({
|
|
66
|
-
group: 'validation',
|
|
67
|
-
name: 'max',
|
|
68
|
-
type: 'number',
|
|
69
|
-
hidden: ({parent}) => {
|
|
70
|
-
const allowedTypes = ['date', 'month', 'week', 'time', 'datetime-local', 'number', 'range']
|
|
79
|
+
// Check uniqueness across all fields
|
|
80
|
+
const doc = context.document as ValidationContextDocument
|
|
81
|
+
const allFieldNames = doc?.fields?.map((field) => field.name) || []
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}),
|
|
75
|
-
defineField({
|
|
76
|
-
group: 'validation',
|
|
77
|
-
name: 'min',
|
|
78
|
-
type: 'number',
|
|
79
|
-
hidden: ({parent}) => {
|
|
80
|
-
const allowedTypes = ['date', 'month', 'week', 'time', 'datetime-local', 'number', 'range']
|
|
83
|
+
// Count occurrences of this name
|
|
84
|
+
const nameCount = allFieldNames.filter((n) => n === name).length
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
// If we find more than one occurrence (including current field), it's not unique
|
|
87
|
+
if (nameCount > 1) {
|
|
88
|
+
return 'Field name must be unique across all form fields'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check for reserved HTML form attributes
|
|
92
|
+
const reservedNames = [
|
|
93
|
+
'action',
|
|
94
|
+
'method',
|
|
95
|
+
'target',
|
|
96
|
+
'enctype',
|
|
97
|
+
'accept-charset',
|
|
98
|
+
'autocomplete',
|
|
99
|
+
'novalidate',
|
|
100
|
+
'rel',
|
|
101
|
+
'submit',
|
|
102
|
+
'reset',
|
|
103
|
+
]
|
|
104
|
+
if (reservedNames.includes(name.toLowerCase())) {
|
|
105
|
+
return 'This name is reserved for HTML form attributes. Please choose a different name.'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true
|
|
109
|
+
}),
|
|
84
110
|
}),
|
|
85
111
|
defineField({
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
type: '
|
|
89
|
-
|
|
90
|
-
const allowedTypes = ['date', 'month', 'week', 'time', 'datetime-local', 'number', 'range']
|
|
91
|
-
|
|
92
|
-
return !allowedTypes.includes(parent.type)
|
|
93
|
-
},
|
|
112
|
+
name: 'required',
|
|
113
|
+
title: 'Required',
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
initialValue: false,
|
|
94
116
|
}),
|
|
117
|
+
// defineField({
|
|
118
|
+
// name: 'validation',
|
|
119
|
+
// title: 'Validation Rules',
|
|
120
|
+
// type: 'array',
|
|
121
|
+
// of: [
|
|
122
|
+
// {
|
|
123
|
+
// type: 'object',
|
|
124
|
+
// fields: [
|
|
125
|
+
// defineField({
|
|
126
|
+
// name: 'type',
|
|
127
|
+
// title: 'Validation Type',
|
|
128
|
+
// type: 'string',
|
|
129
|
+
|
|
130
|
+
// hidden: ({parent}) => !parent?.type,
|
|
131
|
+
// options: {
|
|
132
|
+
// // TODO: I think this needs to be a custom input component?
|
|
133
|
+
// // list: ({parent}) => (parent?.type ? validationTypesByFieldType[parent.type] : []),
|
|
134
|
+
// list: [],
|
|
135
|
+
// },
|
|
136
|
+
// }),
|
|
137
|
+
// defineField({
|
|
138
|
+
// name: 'value',
|
|
139
|
+
// title: 'Value',
|
|
140
|
+
// type: 'string',
|
|
141
|
+
// }),
|
|
142
|
+
// defineField({
|
|
143
|
+
// name: 'message',
|
|
144
|
+
// title: 'Error Message',
|
|
145
|
+
// type: 'string',
|
|
146
|
+
// }),
|
|
147
|
+
// ],
|
|
148
|
+
// },
|
|
149
|
+
// ],
|
|
150
|
+
// }),
|
|
95
151
|
defineField({
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
type: 'number',
|
|
152
|
+
name: 'choices',
|
|
153
|
+
title: 'Choices',
|
|
154
|
+
type: 'array',
|
|
100
155
|
hidden: ({parent}) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return !allowedTypes.includes(parent.type)
|
|
156
|
+
return !['select', 'radio', 'checkbox'].includes(parent?.type)
|
|
104
157
|
},
|
|
158
|
+
of: [
|
|
159
|
+
{
|
|
160
|
+
type: 'object',
|
|
161
|
+
fields: [
|
|
162
|
+
defineField({
|
|
163
|
+
name: 'label',
|
|
164
|
+
title: 'Label',
|
|
165
|
+
type: 'string',
|
|
166
|
+
}),
|
|
167
|
+
defineField({
|
|
168
|
+
name: 'value',
|
|
169
|
+
title: 'Value',
|
|
170
|
+
type: 'string',
|
|
171
|
+
}),
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
105
175
|
}),
|
|
106
176
|
defineField({
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
type: 'number',
|
|
177
|
+
name: 'options',
|
|
178
|
+
title: 'Field Options',
|
|
179
|
+
type: 'object',
|
|
111
180
|
hidden: ({parent}) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
return !allowedTypes.includes(parent.type)
|
|
181
|
+
return ['select', 'radio', 'checkbox', 'file'].includes(parent?.type)
|
|
115
182
|
},
|
|
183
|
+
fields: [
|
|
184
|
+
defineField({
|
|
185
|
+
name: 'placeholder',
|
|
186
|
+
title: 'Placeholder',
|
|
187
|
+
type: 'string',
|
|
188
|
+
}),
|
|
189
|
+
defineField({
|
|
190
|
+
name: 'defaultValue',
|
|
191
|
+
title: 'Default Value',
|
|
192
|
+
type: 'string',
|
|
193
|
+
}),
|
|
194
|
+
],
|
|
116
195
|
}),
|
|
117
196
|
],
|
|
118
|
-
groups: [{name: 'info'}, {name: 'validation'}],
|
|
119
197
|
preview: {
|
|
120
198
|
select: {
|
|
121
|
-
|
|
122
|
-
|
|
199
|
+
label: 'label',
|
|
200
|
+
name: 'name',
|
|
201
|
+
type: 'type',
|
|
123
202
|
},
|
|
124
|
-
prepare
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
> = {
|
|
129
|
-
text: TextIcon,
|
|
130
|
-
number: NumberIcon,
|
|
131
|
-
date: CalendarIcon,
|
|
132
|
-
'datetime-local': CalendarIcon,
|
|
133
|
-
month: CalendarIcon,
|
|
134
|
-
time: ClockIcon,
|
|
135
|
-
color: ColorWheelIcon,
|
|
136
|
-
email: EnvelopeIcon,
|
|
137
|
-
url: EarthGlobeIcon,
|
|
138
|
-
file: DocumentIcon,
|
|
139
|
-
tel: HashIcon,
|
|
140
|
-
week: CalendarIcon,
|
|
141
|
-
range: NumberIcon,
|
|
203
|
+
prepare({label, name, type}) {
|
|
204
|
+
return {
|
|
205
|
+
title: label || name,
|
|
206
|
+
subtitle: type,
|
|
142
207
|
}
|
|
143
|
-
return {title, subtitle, media: icon[subtitle]}
|
|
144
208
|
},
|
|
145
209
|
},
|
|
146
210
|
})
|
|
@@ -1,13 +1,55 @@
|
|
|
1
|
+
import {FaWpforms} from 'react-icons/fa'
|
|
1
2
|
import {defineField, defineType} from 'sanity'
|
|
2
3
|
|
|
3
4
|
export const formType = defineType({
|
|
4
5
|
name: 'form',
|
|
5
|
-
|
|
6
|
+
title: 'Form',
|
|
7
|
+
type: 'document',
|
|
8
|
+
icon: FaWpforms,
|
|
6
9
|
fields: [
|
|
10
|
+
defineField({
|
|
11
|
+
name: 'title',
|
|
12
|
+
title: 'Form Title',
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Internal title for the form',
|
|
15
|
+
validation: (Rule) => Rule.required(),
|
|
16
|
+
}),
|
|
17
|
+
defineField({
|
|
18
|
+
name: 'id',
|
|
19
|
+
title: 'Form ID',
|
|
20
|
+
type: 'slug',
|
|
21
|
+
options: {
|
|
22
|
+
source: 'title',
|
|
23
|
+
},
|
|
24
|
+
// validation: (Rule) => Rule.required(),
|
|
25
|
+
}),
|
|
7
26
|
defineField({
|
|
8
27
|
name: 'fields',
|
|
28
|
+
title: 'Form Fields',
|
|
9
29
|
type: 'array',
|
|
10
30
|
of: [{type: 'formField'}],
|
|
11
31
|
}),
|
|
32
|
+
defineField({
|
|
33
|
+
name: 'submitButton',
|
|
34
|
+
title: 'Submit Button',
|
|
35
|
+
type: 'object',
|
|
36
|
+
fields: [
|
|
37
|
+
defineField({
|
|
38
|
+
name: 'text',
|
|
39
|
+
title: 'Button Text',
|
|
40
|
+
type: 'string',
|
|
41
|
+
initialValue: 'Submit',
|
|
42
|
+
}),
|
|
43
|
+
// defineField({
|
|
44
|
+
// name: 'position',
|
|
45
|
+
// title: 'Button Position',
|
|
46
|
+
// type: 'string',
|
|
47
|
+
// options: {
|
|
48
|
+
// list: ['left', 'center', 'right'],
|
|
49
|
+
// },
|
|
50
|
+
// initialValue: 'center',
|
|
51
|
+
// }),
|
|
52
|
+
],
|
|
53
|
+
}),
|
|
12
54
|
],
|
|
13
55
|
})
|