@dxos/react-ui-form 0.8.4-main.72ec0f3 → 0.8.4-main.74a063c4e0

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 (189) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2305 -2352
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +2305 -2352
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +66 -59
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +87 -27
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +112 -136
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +43 -0
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +77 -0
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +18 -0
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  25. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  27. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  33. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/RefField.d.ts +16 -0
  35. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  37. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  38. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  39. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  40. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +82 -0
  41. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  43. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  45. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  47. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  49. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/index.d.ts +2 -4
  51. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  52. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +23 -10
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -58
  55. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/Settings/Settings.d.ts +39 -0
  57. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  58. package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -0
  59. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  60. package/dist/types/src/components/Settings/index.d.ts +2 -0
  61. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  62. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +11 -13
  63. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  64. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +67 -62
  65. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  66. package/dist/types/src/components/index.d.ts +1 -1
  67. package/dist/types/src/components/index.d.ts.map +1 -1
  68. package/dist/types/src/components/testing.d.ts +0 -2
  69. package/dist/types/src/components/testing.d.ts.map +1 -1
  70. package/dist/types/src/hooks/index.d.ts +2 -3
  71. package/dist/types/src/hooks/index.d.ts.map +1 -1
  72. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  73. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  74. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  75. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  76. package/dist/types/src/translations.d.ts +63 -59
  77. package/dist/types/src/translations.d.ts.map +1 -1
  78. package/dist/types/src/util/field.d.ts +3 -3
  79. package/dist/types/src/util/field.d.ts.map +1 -1
  80. package/dist/types/src/util/formatting.d.ts +2 -2
  81. package/dist/types/src/util/formatting.d.ts.map +1 -1
  82. package/dist/types/src/util/index.d.ts +1 -1
  83. package/dist/types/src/util/index.d.ts.map +1 -1
  84. package/dist/types/src/util/properties.d.ts +7 -0
  85. package/dist/types/src/util/properties.d.ts.map +1 -0
  86. package/dist/types/src/util/refs.d.ts +2 -2
  87. package/dist/types/src/util/refs.d.ts.map +1 -1
  88. package/dist/types/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +51 -48
  90. package/src/components/FieldEditor/FieldEditor.stories.tsx +43 -25
  91. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  92. package/src/components/FieldEditor/FieldEditor.tsx +74 -78
  93. package/src/components/Form/AUDIT.md +17 -0
  94. package/src/components/Form/Form.stories.tsx +148 -250
  95. package/src/components/Form/Form.tsx +324 -110
  96. package/src/components/Form/FormField.tsx +304 -0
  97. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  98. package/src/components/Form/FormFieldComponent.tsx +171 -0
  99. package/src/components/Form/FormFieldSet.tsx +128 -0
  100. package/src/components/Form/README.md +3 -0
  101. package/src/components/Form/fields/ArrayField.tsx +154 -0
  102. package/src/components/Form/fields/BooleanField.tsx +22 -0
  103. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  104. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  105. package/src/components/Form/fields/NumberField.tsx +39 -0
  106. package/src/components/Form/fields/RefField.tsx +198 -0
  107. package/src/components/Form/fields/SelectField.tsx +65 -0
  108. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +19 -17
  109. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +29 -24
  110. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  111. package/src/components/Form/fields/TextField.tsx +38 -0
  112. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  113. package/src/components/Form/fields/index.ts +15 -0
  114. package/src/components/Form/index.ts +3 -4
  115. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +18 -16
  116. package/src/components/ObjectPicker/ObjectPicker.tsx +103 -78
  117. package/src/components/Settings/Settings.stories.tsx +55 -0
  118. package/src/components/Settings/Settings.tsx +129 -0
  119. package/src/components/{ControlSection → Settings}/index.ts +1 -1
  120. package/src/components/ViewEditor/ViewEditor.stories.tsx +47 -45
  121. package/src/components/ViewEditor/ViewEditor.test.tsx +5 -5
  122. package/src/components/ViewEditor/ViewEditor.tsx +297 -210
  123. package/src/components/index.ts +1 -1
  124. package/src/components/testing.tsx +23 -9
  125. package/src/hooks/index.ts +2 -3
  126. package/src/hooks/useFormHandler.ts +380 -0
  127. package/src/hooks/useKeyHandler.tsx +37 -0
  128. package/src/translations.ts +66 -64
  129. package/src/util/field.ts +25 -25
  130. package/src/util/formatting.ts +22 -22
  131. package/src/util/index.ts +1 -1
  132. package/src/util/properties.ts +18 -0
  133. package/src/util/refs.ts +13 -15
  134. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  135. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  136. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -83
  137. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  138. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  139. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  140. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  141. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  142. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  143. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  144. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  145. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  146. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  147. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  148. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  149. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  150. package/dist/types/src/components/Form/Input.d.ts +0 -29
  151. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  152. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  153. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  154. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  155. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  156. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  157. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  158. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -78
  159. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  160. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  161. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  162. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  163. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  164. package/dist/types/src/components/Form/factory.d.ts +0 -8
  165. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  166. package/dist/types/src/hooks/useForm.d.ts +0 -67
  167. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  168. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  169. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  170. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  171. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  172. package/dist/types/src/util/schema.d.ts +0 -3
  173. package/dist/types/src/util/schema.d.ts.map +0 -1
  174. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  175. package/src/components/ControlSection/ControlSection.tsx +0 -153
  176. package/src/components/Form/ArrayField.tsx +0 -125
  177. package/src/components/Form/Defaults.tsx +0 -205
  178. package/src/components/Form/FormActions.tsx +0 -54
  179. package/src/components/Form/FormContent.tsx +0 -362
  180. package/src/components/Form/FormContext.tsx +0 -110
  181. package/src/components/Form/Input.tsx +0 -54
  182. package/src/components/Form/RefField.tsx +0 -283
  183. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  184. package/src/components/Form/custom/index.ts +0 -7
  185. package/src/components/Form/factory.tsx +0 -31
  186. package/src/hooks/useForm.ts +0 -296
  187. package/src/hooks/useQueryRefOptions.ts +0 -62
  188. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  189. package/src/util/schema.ts +0 -12
@@ -1,205 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import React, { useCallback, useEffect, useRef } from 'react';
6
-
7
- import { Input, Select, useTranslation } from '@dxos/react-ui';
8
-
9
- import { translationKey } from '../../translations';
10
-
11
- import { InputHeader, type InputProps } from './Input';
12
-
13
- export const TextInput = ({
14
- type,
15
- label,
16
- inputOnly,
17
- readonly,
18
- placeholder,
19
- getStatus,
20
- getValue,
21
- onValueChange,
22
- onBlur,
23
- }: InputProps) => {
24
- const { status, error } = getStatus();
25
-
26
- return readonly && !getValue() ? null : readonly === 'static' && inputOnly ? (
27
- <p>{getValue() ?? ''}</p>
28
- ) : (
29
- <Input.Root validationValence={status}>
30
- {!inputOnly && <InputHeader error={error} label={label} />}
31
- {readonly === 'static' ? (
32
- <p>{getValue() ?? ''}</p>
33
- ) : (
34
- <Input.TextInput
35
- disabled={!!readonly}
36
- placeholder={placeholder}
37
- value={getValue() ?? ''}
38
- onChange={(event) => onValueChange(type, event.target.value)}
39
- onBlur={onBlur}
40
- />
41
- )}
42
- {inputOnly && <Input.Validation>{error}</Input.Validation>}
43
- </Input.Root>
44
- );
45
- };
46
-
47
- export const NumberInput = ({
48
- type,
49
- label,
50
- inputOnly,
51
- readonly,
52
- placeholder,
53
- getStatus,
54
- getValue,
55
- onValueChange,
56
- onBlur,
57
- }: InputProps) => {
58
- const { status, error } = getStatus();
59
-
60
- return readonly && !getValue() ? null : readonly === 'static' && inputOnly ? (
61
- <p>{getValue() ?? ''}</p>
62
- ) : (
63
- <Input.Root validationValence={status}>
64
- {!inputOnly && <InputHeader error={error} label={label} />}
65
- {readonly === 'static' ? (
66
- <p>{getValue() ?? ''}</p>
67
- ) : (
68
- <Input.TextInput
69
- type='number'
70
- disabled={!!readonly}
71
- placeholder={placeholder}
72
- value={getValue()}
73
- onChange={(event) => onValueChange(type, event.target.value)}
74
- onBlur={onBlur}
75
- />
76
- )}
77
- {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
78
- </Input.Root>
79
- );
80
- };
81
-
82
- export const BooleanInput = ({ type, label, inputOnly, getStatus, getValue, onValueChange, readonly }: InputProps) => {
83
- const { status, error } = getStatus();
84
- const checked = Boolean(getValue());
85
- const { t } = useTranslation(translationKey);
86
-
87
- return (
88
- <Input.Root validationValence={status}>
89
- {!inputOnly && <InputHeader error={error} label={label} />}
90
- {readonly === 'static' ? (
91
- <p>{t(checked ? 'boolean input true value' : 'boolean input false value')}</p>
92
- ) : (
93
- <Input.Switch disabled={!!readonly} checked={checked} onCheckedChange={(value) => onValueChange(type, value)} />
94
- )}
95
- {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
96
- </Input.Root>
97
- );
98
- };
99
-
100
- export type SelectInputOptions = InputProps & {
101
- options?: Array<{ value: string | number; label?: string }>;
102
- };
103
-
104
- export const SelectInput = ({
105
- type,
106
- label,
107
- inputOnly,
108
- readonly,
109
- placeholder,
110
- options,
111
- getStatus,
112
- getValue,
113
- onValueChange,
114
- }: SelectInputOptions) => {
115
- const { status, error } = getStatus();
116
-
117
- const value = getValue() as string | undefined;
118
- const handleValueChange = useCallback((value: string | number) => onValueChange(type, value), [type, onValueChange]);
119
-
120
- return readonly && !value ? null : (
121
- <Input.Root validationValence={status}>
122
- {!inputOnly && <InputHeader error={error} label={label} />}
123
- {readonly === 'static' ? (
124
- <p>{options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)}</p>
125
- ) : (
126
- <Select.Root value={value} onValueChange={handleValueChange}>
127
- {/* TODO(burdon): Placeholder not working? */}
128
- <Select.TriggerButton classNames='is-full' disabled={!!readonly} placeholder={placeholder} />
129
- <Select.Portal>
130
- <Select.Content>
131
- <Select.Viewport>
132
- {options?.map(({ value, label }) => (
133
- // NOTE: Numeric values are converted to and from strings.
134
- <Select.Option key={String(value)} value={String(value)}>
135
- {label ?? String(value)}
136
- </Select.Option>
137
- ))}
138
- </Select.Viewport>
139
- <Select.Arrow />
140
- </Select.Content>
141
- </Select.Portal>
142
- </Select.Root>
143
- )}
144
- {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
145
- </Input.Root>
146
- );
147
- };
148
-
149
- export const MarkdownInput = ({
150
- type,
151
- label,
152
- inputOnly,
153
- readonly,
154
- placeholder,
155
- getStatus,
156
- getValue,
157
- onValueChange,
158
- onBlur,
159
- }: InputProps) => {
160
- const { status, error } = getStatus();
161
- const textareaRef = useRef<HTMLTextAreaElement>(null);
162
-
163
- // TODO(ZaymonFC): If we start using `Enter` for form submission, we should prevent default behavior here
164
- // since we're using a textarea element and the user needs to be able to enter new lines.
165
-
166
- const adjustHeight = useCallback(() => {
167
- const textarea = textareaRef.current;
168
- if (textarea) {
169
- textarea.style.blockSize = 'auto'; // For measurement.
170
- textarea.style.blockSize = `${textarea.scrollHeight + 2}px`;
171
- }
172
- }, []);
173
-
174
- // Adjust height on initial render.
175
- useEffect(() => {
176
- adjustHeight();
177
- }, [adjustHeight]);
178
-
179
- // Adjust height when the content changes.
180
- useEffect(() => {
181
- adjustHeight();
182
- }, [getValue(), adjustHeight]);
183
-
184
- return (
185
- <Input.Root validationValence={status}>
186
- {!inputOnly && <InputHeader error={error} label={label} />}
187
- {readonly === 'static' ? (
188
- <p>{getValue() ?? ''}</p>
189
- ) : (
190
- <Input.TextArea
191
- ref={textareaRef}
192
- disabled={!!readonly}
193
- placeholder={placeholder}
194
- value={getValue() ?? ''}
195
- classNames={'min-bs-auto max-bs-40 overflow-auto'}
196
- onChange={(event) => onValueChange(type, event.target.value)}
197
- onBlur={onBlur}
198
- style={{ resize: 'none' }}
199
- spellCheck={false}
200
- />
201
- )}
202
- {inputOnly && <Input.Validation>{error}</Input.Validation>}
203
- </Input.Root>
204
- );
205
- };
@@ -1,54 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import React from 'react';
6
-
7
- import { IconButton, useTranslation } from '@dxos/react-ui';
8
- import { cardSpacing } from '@dxos/react-ui-stack';
9
- import { mx } from '@dxos/react-ui-theme';
10
-
11
- import { translationKey } from '../../translations';
12
-
13
- import { useFormContext } from './FormContext';
14
-
15
- export type FormOuterSpacing = boolean | 'blockStart-0' | 'scroll-fields';
16
-
17
- export type FormActionsProps = {
18
- readonly?: boolean;
19
- onCancel?: () => void;
20
- outerSpacing?: FormOuterSpacing;
21
- };
22
-
23
- export const FormActions = ({ readonly, onCancel, outerSpacing = true }: FormActionsProps) => {
24
- const { t } = useTranslation(translationKey);
25
- const { canSave, handleSave } = useFormContext();
26
-
27
- return (
28
- <div
29
- role='none'
30
- className={mx(
31
- outerSpacing === 'scroll-fields'
32
- ? 'pli-cardSpacingInline mbe-cardSpacingBlock'
33
- : outerSpacing
34
- ? cardSpacing
35
- : 'mbs-cardSpacingBlock',
36
- 'flex [&_button]:grow gap-1 first:mbs-0',
37
- )}
38
- >
39
- {onCancel && !readonly && (
40
- <IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('cancel button')} onClick={onCancel} />
41
- )}
42
- {handleSave && (
43
- <IconButton
44
- type='submit'
45
- data-testid='save-button'
46
- disabled={!canSave}
47
- icon='ph--check--regular'
48
- label={t('save button')}
49
- onClick={handleSave}
50
- />
51
- )}
52
- </div>
53
- );
54
- };
@@ -1,362 +0,0 @@
1
- //
2
- // Copyright 2025 DXOS.org
3
- //
4
-
5
- import * as Function from 'effect/Function';
6
- import * as Schema from 'effect/Schema';
7
- import * as SchemaAST from 'effect/SchemaAST';
8
- import * as StringEffect from 'effect/String';
9
- import React, { Component, type PropsWithChildren, type ReactNode, forwardRef, useMemo } from 'react';
10
-
11
- import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
12
- import { type ThemedClassName } from '@dxos/react-ui';
13
- import { mx } from '@dxos/react-ui-theme';
14
- import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
15
- import { isTruthy } from '@dxos/util';
16
-
17
- import { type QueryRefOptions } from '../../hooks';
18
- import { getRefProps } from '../../util';
19
-
20
- import { ArrayField } from './ArrayField';
21
- import { SelectInput } from './Defaults';
22
- import { getInputComponent } from './factory';
23
- import { type ComponentLookup } from './Form';
24
- import { useFormValues, useInputProps } from './FormContext';
25
- import { type InputComponent } from './Input';
26
- import { RefField, type RefFieldProps } from './RefField';
27
-
28
- export type FormFieldProps = {
29
- property: SchemaProperty<any>;
30
- path?: (string | number)[];
31
- /** Used to indicate if input should be presented inline (e.g. for array items). */
32
- inline?: boolean;
33
- projection?: ProjectionModel;
34
- lookupComponent?: ComponentLookup;
35
- Custom?: Partial<Record<string, InputComponent>>;
36
- } & Pick<
37
- RefFieldProps,
38
- | 'readonly'
39
- | 'onQueryRefOptions'
40
- | 'createOptionLabel'
41
- | 'createOptionIcon'
42
- | 'onCreate'
43
- | 'createSchema'
44
- | 'createInitialValuePath'
45
- >;
46
-
47
- export const FormField = ({
48
- property,
49
- path,
50
- readonly,
51
- inline,
52
- projection,
53
- onQueryRefOptions,
54
- createOptionLabel,
55
- createOptionIcon,
56
- onCreate,
57
- createSchema,
58
- createInitialValuePath,
59
- lookupComponent,
60
- Custom,
61
- }: FormFieldProps) => {
62
- const { ast, name, type, format, title, description, options, examples, array } = property;
63
- const inputProps = useInputProps(path);
64
-
65
- const label = useMemo(() => title ?? Function.pipe(name, StringEffect.capitalize), [title, name]);
66
- const placeholder = useMemo(
67
- () => (examples?.length ? `Example: "${examples[0]}"` : description),
68
- [examples, description],
69
- );
70
-
71
- //
72
- // Registry and Custom.
73
- //
74
-
75
- const FoundComponent = lookupComponent?.({
76
- prop: name,
77
- schema: Schema.make(ast),
78
- inputProps: {
79
- type,
80
- format,
81
- label,
82
- readonly,
83
- placeholder,
84
- ...inputProps,
85
- },
86
- });
87
-
88
- if (FoundComponent) {
89
- return FoundComponent;
90
- }
91
-
92
- const jsonPath = createJsonPath(path ?? []);
93
- const CustomComponent = Custom?.[jsonPath];
94
- if (CustomComponent) {
95
- return (
96
- <CustomComponent
97
- type={type}
98
- format={format}
99
- label={label}
100
- inputOnly={inline}
101
- placeholder={placeholder}
102
- readonly={readonly}
103
- {...inputProps}
104
- />
105
- );
106
- }
107
-
108
- //
109
- // Refs.
110
- //
111
-
112
- const refProps = getRefProps(property);
113
- if (refProps) {
114
- return (
115
- <RefField
116
- ast={refProps.ast}
117
- array={refProps.isArray}
118
- type={type}
119
- format={format}
120
- label={label}
121
- placeholder={placeholder}
122
- readonly={readonly}
123
- inputOnly={inline}
124
- onQueryRefOptions={onQueryRefOptions}
125
- createOptionLabel={createOptionLabel}
126
- createOptionIcon={createOptionIcon}
127
- onCreate={onCreate}
128
- createSchema={createSchema}
129
- createInitialValuePath={createInitialValuePath}
130
- {...inputProps}
131
- />
132
- );
133
- }
134
-
135
- //
136
- // Arrays.
137
- //
138
-
139
- if (array) {
140
- return <ArrayField property={property} path={path} inputProps={inputProps} readonly={readonly} Custom={Custom} />;
141
- }
142
-
143
- //
144
- // Standard Inputs.
145
- //
146
-
147
- const InputComponent = getInputComponent(type, format);
148
- if (InputComponent) {
149
- return (
150
- <InputComponent
151
- type={type}
152
- format={format}
153
- label={label}
154
- inputOnly={inline}
155
- placeholder={placeholder}
156
- readonly={readonly}
157
- {...inputProps}
158
- />
159
- );
160
- }
161
-
162
- //
163
- // Select.
164
- //
165
-
166
- if (options) {
167
- return (
168
- <SelectInput
169
- type={type}
170
- format={format}
171
- readonly={readonly}
172
- inputOnly={inline}
173
- label={label}
174
- options={options.map((option) => ({ value: option, label: String(option) }))}
175
- placeholder={placeholder}
176
- {...inputProps}
177
- />
178
- );
179
- }
180
-
181
- //
182
- // Nested Objects.
183
- //
184
-
185
- if (type === 'object') {
186
- const baseNode = findNode(ast, isDiscriminatedUnion);
187
- const typeLiteral = baseNode
188
- ? getDiscriminatedType(baseNode, inputProps.getValue() as any)
189
- : findNode(ast, SchemaAST.isTypeLiteral);
190
-
191
- if (typeLiteral) {
192
- return (
193
- <>
194
- {!inline && <h3 className={mx('text-lg mlb-inputSpacingBlock first:mbs-0')}>{label}</h3>}
195
- <FormFields
196
- schema={Schema.make(typeLiteral)}
197
- path={path}
198
- readonly={readonly}
199
- projection={projection}
200
- onQueryRefOptions={onQueryRefOptions}
201
- createOptionLabel={createOptionLabel}
202
- createOptionIcon={createOptionIcon}
203
- onCreate={onCreate}
204
- Custom={Custom}
205
- lookupComponent={lookupComponent}
206
- />
207
- </>
208
- );
209
- }
210
- }
211
-
212
- return null;
213
- };
214
-
215
- export type FormFieldsProps = ThemedClassName<
216
- {
217
- testId?: string;
218
- schema: Schema.Schema.All;
219
- /**
220
- * Path to the current object from the root. Used with nested forms.
221
- */
222
- path?: (string | number)[];
223
- filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
224
- sort?: string[];
225
- /**
226
- * Optional projection for projection-based field management
227
- */
228
- projection?: ProjectionModel;
229
- lookupComponent?: ComponentLookup;
230
- /**
231
- * Map of custom renderers for specific properties.
232
- * Prefer lookupComponent for plugin specific input surfaces.
233
- */
234
- Custom?: Partial<Record<string, InputComponent>>;
235
- onQueryRefOptions?: QueryRefOptions;
236
- } & Pick<FormFieldProps, 'readonly'> &
237
- Pick<
238
- RefFieldProps,
239
- | 'onQueryRefOptions'
240
- | 'createOptionLabel'
241
- | 'createOptionIcon'
242
- | 'onCreate'
243
- | 'createSchema'
244
- | 'createInitialValuePath'
245
- >
246
- >;
247
-
248
- export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
249
- (
250
- {
251
- classNames,
252
- schema,
253
- path,
254
- filter,
255
- sort,
256
- projection,
257
- readonly,
258
- lookupComponent,
259
- Custom,
260
- onQueryRefOptions,
261
- ...props
262
- },
263
- forwardRef,
264
- ) => {
265
- const values = useFormValues(path);
266
- const properties = useMemo(() => {
267
- const props = getSchemaProperties(schema.ast, values);
268
-
269
- // Use projection-based field management when view and projection are available
270
- if (projection) {
271
- const fieldProjections = projection.getFieldProjections();
272
- const hiddenProperties = new Set(projection.getHiddenProperties());
273
-
274
- // Filter properties to only include visible ones and order by projection
275
- const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name));
276
- const orderedProps: SchemaProperty<any>[] = [];
277
-
278
- // Add properties in projection field order
279
- for (const fieldProjection of fieldProjections) {
280
- const fieldPath = String(fieldProjection.field.path);
281
- const prop = visibleProps.find((p) => p.name === fieldPath);
282
- if (prop) {
283
- orderedProps.push(prop);
284
- }
285
- }
286
-
287
- // Add any remaining properties not in projection
288
- const projectionPaths = new Set(fieldProjections.map((fp) => String(fp.field.path)));
289
- const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name));
290
- orderedProps.push(...remainingProps);
291
-
292
- return orderedProps;
293
- }
294
-
295
- // Fallback to legacy filter/sort behavior
296
- const filtered = filter ? filter(props) : props;
297
- return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
298
- }, [schema, values, filter, sort, projection?.fields]);
299
-
300
- return (
301
- <div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
302
- {properties
303
- .map((property) => {
304
- return (
305
- <ErrorBoundary key={property.name} path={[...(path ?? []), property.name]}>
306
- <FormField
307
- property={property}
308
- path={[...(path ?? []), property.name]}
309
- readonly={readonly}
310
- projection={projection}
311
- onQueryRefOptions={onQueryRefOptions}
312
- lookupComponent={lookupComponent}
313
- Custom={Custom}
314
- {...props}
315
- />
316
- </ErrorBoundary>
317
- );
318
- })
319
- .filter(isTruthy)}
320
- </div>
321
- );
322
- },
323
- );
324
-
325
- type ErrorState = {
326
- error: Error | undefined;
327
- };
328
-
329
- type ErrorBoundaryProps = PropsWithChildren<{
330
- path?: (string | number)[];
331
- }>;
332
-
333
- class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorState> {
334
- static getDerivedStateFromError(error: Error): { error: Error } {
335
- return { error };
336
- }
337
-
338
- override state = { error: undefined };
339
-
340
- override componentDidUpdate(prevProps: ErrorBoundaryProps): void {
341
- if (prevProps.path !== this.props.path) {
342
- this.resetError();
343
- }
344
- }
345
-
346
- override render(): ReactNode {
347
- if (this.state.error) {
348
- return (
349
- <div className='flex gap-2 border border-roseFill font-mono text-sm'>
350
- <span className='bg-roseFill text-surfaceText pli-1 font-thin'>ERROR</span>
351
- {String(this.props.path?.join('.'))}
352
- </div>
353
- );
354
- }
355
-
356
- return this.props.children;
357
- }
358
-
359
- private resetError(): void {
360
- this.setState({ error: undefined });
361
- }
362
- }