@dxos/react-ui-form 0.8.4-main.ead640a → 0.8.4-main.effb148878

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 (268) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +0 -1
  3. package/dist/lib/browser/index.mjs +3252 -2315
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/translations.mjs +83 -0
  7. package/dist/lib/browser/translations.mjs.map +7 -0
  8. package/dist/lib/node-esm/index.mjs +3252 -2315
  9. package/dist/lib/node-esm/index.mjs.map +4 -4
  10. package/dist/lib/node-esm/meta.json +1 -1
  11. package/dist/lib/node-esm/translations.mjs +85 -0
  12. package/dist/lib/node-esm/translations.mjs.map +7 -0
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  14. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +69 -60
  16. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.d.ts +123 -26
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +175 -138
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +48 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +95 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  26. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts +21 -0
  28. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
  30. package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/Layout/FormLayout.d.ts +25 -0
  32. package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +116 -0
  34. package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
  36. package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
  38. package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
  40. package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
  42. package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  44. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  46. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
  48. package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  50. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
  52. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +85 -0
  54. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  56. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  57. package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
  58. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  59. package/dist/types/src/components/Form/fields/SelectField.d.ts +13 -0
  60. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  61. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  62. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  63. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +85 -0
  64. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  65. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  66. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  67. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  68. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  69. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  70. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  71. package/dist/types/src/components/Form/fields/index.d.ts +13 -0
  72. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  73. package/dist/types/src/components/Form/index.d.ts +4 -4
  74. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  75. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
  76. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  77. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  78. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  79. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +37 -10
  80. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  81. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +65 -59
  82. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  83. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  84. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  85. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +128 -0
  86. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  87. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  88. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  89. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  90. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  91. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
  92. package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
  93. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
  94. package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
  95. package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
  96. package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
  97. package/dist/types/src/components/Settings/Settings.d.ts +51 -0
  98. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  99. package/dist/types/src/components/Settings/Settings.stories.d.ts +85 -0
  100. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  101. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  102. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  103. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  104. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  105. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  106. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  107. package/dist/types/src/components/Settings/index.d.ts +4 -0
  108. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  109. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +16 -20
  110. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  111. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +72 -66
  112. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  113. package/dist/types/src/components/index.d.ts +4 -1
  114. package/dist/types/src/components/index.d.ts.map +1 -1
  115. package/dist/types/src/components/testing.d.ts +13 -6
  116. package/dist/types/src/components/testing.d.ts.map +1 -1
  117. package/dist/types/src/hooks/index.d.ts +2 -3
  118. package/dist/types/src/hooks/index.d.ts.map +1 -1
  119. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  120. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  121. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  122. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  123. package/dist/types/src/index.d.ts +0 -2
  124. package/dist/types/src/index.d.ts.map +1 -1
  125. package/dist/types/src/translations.d.ts +66 -60
  126. package/dist/types/src/translations.d.ts.map +1 -1
  127. package/dist/types/src/util/field-type.d.ts +19 -0
  128. package/dist/types/src/util/field-type.d.ts.map +1 -0
  129. package/dist/types/src/util/field.d.ts +3 -3
  130. package/dist/types/src/util/field.d.ts.map +1 -1
  131. package/dist/types/src/util/formatting.d.ts +2 -2
  132. package/dist/types/src/util/formatting.d.ts.map +1 -1
  133. package/dist/types/src/util/index.d.ts +2 -1
  134. package/dist/types/src/util/index.d.ts.map +1 -1
  135. package/dist/types/src/util/properties.d.ts +17 -0
  136. package/dist/types/src/util/properties.d.ts.map +1 -0
  137. package/dist/types/src/util/properties.test.d.ts +2 -0
  138. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  139. package/dist/types/src/util/refs.d.ts +2 -2
  140. package/dist/types/src/util/refs.d.ts.map +1 -1
  141. package/dist/types/tsconfig.tsbuildinfo +1 -1
  142. package/package.json +61 -51
  143. package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
  144. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  145. package/src/components/FieldEditor/FieldEditor.tsx +84 -84
  146. package/src/components/Form/AUDIT.md +17 -0
  147. package/src/components/Form/Form.stories.tsx +134 -252
  148. package/src/components/Form/Form.tsx +417 -107
  149. package/src/components/Form/FormField.tsx +332 -0
  150. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  151. package/src/components/Form/FormFieldComponent.tsx +206 -0
  152. package/src/components/Form/FormFieldSet.tsx +173 -0
  153. package/src/components/Form/FormTooltipsContext.ts +25 -0
  154. package/src/components/Form/Layout/FormLayout.stories.tsx +339 -0
  155. package/src/components/Form/Layout/FormLayout.tsx +123 -0
  156. package/src/components/Form/Layout/annotation.ts +38 -0
  157. package/src/components/Form/Layout/index.ts +7 -0
  158. package/src/components/Form/Layout/parser.test.ts +120 -0
  159. package/src/components/Form/Layout/parser.ts +167 -0
  160. package/src/components/Form/README.md +3 -0
  161. package/src/components/Form/fields/ArrayField.tsx +185 -0
  162. package/src/components/Form/fields/BooleanField.tsx +22 -0
  163. package/src/components/Form/fields/DateField.tsx +95 -0
  164. package/src/components/Form/fields/GeoPointField.tsx +124 -0
  165. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  166. package/src/components/Form/fields/MarkdownField.tsx +168 -0
  167. package/src/components/Form/fields/NumberField.tsx +39 -0
  168. package/src/components/Form/fields/RefField.tsx +232 -0
  169. package/src/components/Form/fields/SelectField.tsx +77 -0
  170. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +23 -18
  171. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +31 -24
  172. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  173. package/src/components/Form/fields/TextField.tsx +38 -0
  174. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -9
  175. package/src/components/Form/fields/index.ts +16 -0
  176. package/src/components/Form/index.ts +5 -4
  177. package/src/components/ObjectForm/ObjectForm.tsx +115 -0
  178. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  179. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +30 -25
  180. package/src/components/ObjectPicker/ObjectPicker.tsx +122 -81
  181. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
  182. package/src/components/ObjectProperties/ObjectProperties.test.tsx +108 -0
  183. package/src/components/ObjectProperties/ObjectProperties.tsx +147 -0
  184. package/src/components/ObjectProperties/index.ts +5 -0
  185. package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
  186. package/src/components/ObjectTree/ObjectTree.tsx +190 -0
  187. package/src/components/ObjectTree/index.ts +5 -0
  188. package/src/components/Settings/AUDIT.md +51 -0
  189. package/src/components/Settings/Settings.stories.tsx +104 -0
  190. package/src/components/Settings/Settings.tsx +81 -0
  191. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  192. package/src/components/Settings/SettingsItem.tsx +58 -0
  193. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  194. package/src/components/Settings/index.ts +7 -0
  195. package/src/components/ViewEditor/ViewEditor.stories.tsx +65 -73
  196. package/src/components/ViewEditor/ViewEditor.test.tsx +11 -11
  197. package/src/components/ViewEditor/ViewEditor.tsx +308 -214
  198. package/src/components/index.ts +4 -1
  199. package/src/components/testing.tsx +24 -11
  200. package/src/hooks/index.ts +2 -3
  201. package/src/hooks/useFormHandler.ts +380 -0
  202. package/src/hooks/useKeyHandler.tsx +37 -0
  203. package/src/index.ts +0 -4
  204. package/src/translations.ts +71 -65
  205. package/src/util/field-type.ts +60 -0
  206. package/src/util/field.ts +25 -25
  207. package/src/util/formatting.ts +22 -22
  208. package/src/util/index.ts +2 -1
  209. package/src/util/properties.test.ts +78 -0
  210. package/src/util/properties.ts +38 -0
  211. package/src/util/refs.ts +16 -18
  212. package/src/vite-env.d.ts +10 -0
  213. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  214. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  215. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -84
  216. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  217. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  218. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  219. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  220. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  221. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  222. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  223. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  224. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  225. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  226. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  227. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  228. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  229. package/dist/types/src/components/Form/Input.d.ts +0 -29
  230. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  231. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  232. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  233. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  234. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  235. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  236. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  237. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -79
  238. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  239. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  240. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  241. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  242. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  243. package/dist/types/src/components/Form/factory.d.ts +0 -8
  244. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  245. package/dist/types/src/hooks/useForm.d.ts +0 -67
  246. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  247. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  248. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  249. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  250. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  251. package/dist/types/src/util/schema.d.ts +0 -3
  252. package/dist/types/src/util/schema.d.ts.map +0 -1
  253. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  254. package/src/components/ControlSection/ControlSection.tsx +0 -153
  255. package/src/components/Form/ArrayField.tsx +0 -125
  256. package/src/components/Form/Defaults.tsx +0 -205
  257. package/src/components/Form/FormActions.tsx +0 -54
  258. package/src/components/Form/FormContent.tsx +0 -322
  259. package/src/components/Form/FormContext.tsx +0 -110
  260. package/src/components/Form/Input.tsx +0 -54
  261. package/src/components/Form/RefField.tsx +0 -283
  262. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  263. package/src/components/Form/custom/index.ts +0 -7
  264. package/src/components/Form/factory.tsx +0 -31
  265. package/src/hooks/useForm.ts +0 -296
  266. package/src/hooks/useQueryRefOptions.ts +0 -62
  267. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  268. package/src/util/schema.ts +0 -12
@@ -1,120 +1,430 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import { useFocusFinders } from '@fluentui/react-tabster';
6
- import type * as Schema from 'effect/Schema';
7
- import React, { type ReactElement, useCallback, useEffect, useRef } from 'react';
8
-
9
- import { type BaseObject, type PropertyKey } from '@dxos/echo/internal';
10
- import { type ThemedClassName } from '@dxos/react-ui';
11
- import { cardDialogOverflow, cardSpacing } from '@dxos/react-ui-stack';
12
- import { type ProjectionModel, type SchemaProperty } from '@dxos/schema';
13
-
14
- import { type FormHandler, type FormOptions } from '../../hooks';
15
-
16
- import { FormActions, type FormOuterSpacing } from './FormActions';
17
- import { FormFields, type FormFieldsProps } from './FormContent';
18
- import { FormProvider } from './FormContext';
19
- import { type InputComponent, type InputProps } from './Input';
20
-
21
- export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
22
-
23
- export type ComponentLookup = (args: {
24
- prop: string;
25
- schema: Schema.Schema<any>;
26
- inputProps: InputProps;
27
- }) => ReactElement | undefined;
28
-
29
- export type CustomInputMap = Partial<Record<string, InputComponent>>;
30
-
31
- export type FormProps<T extends BaseObject> = ThemedClassName<{
32
- values: Partial<T>;
33
- // TODO(burdon): Autofocus first input.
34
- autoFocus?: boolean;
35
- // TODO(burdon): Change to JsonPath includes/excludes.
36
- filter?: PropsFilter<T>;
37
- sort?: PropertyKey<T>[];
38
- projection?: ProjectionModel;
39
- autoSave?: boolean;
40
- outerSpacing?: FormOuterSpacing;
41
- onCancel?: () => void;
42
- }> &
43
- Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'> &
44
- // TODO(wittjosiah): This needs to support different ref field options per field.
45
- FormFieldsProps;
46
-
47
- export const Form = <T extends BaseObject>({
48
- classNames,
49
- testId,
50
- values: initialValues,
51
- autoFocus,
52
- readonly,
53
- autoSave,
54
- outerSpacing = true,
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import * as Option from 'effect/Option';
7
+ import * as Schema from 'effect/Schema';
8
+ import * as SchemaAST from 'effect/SchemaAST';
9
+ import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
10
+
11
+ import { Annotation as EchoAnnotation, Type } from '@dxos/echo';
12
+ import { type AnyProperties } from '@dxos/echo/internal';
13
+ import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
14
+ import {
15
+ IconButton,
16
+ type IconButtonProps,
17
+ ScrollArea,
18
+ type ThemedClassName,
19
+ useMergeRefs,
20
+ useTranslation,
21
+ } from '@dxos/react-ui';
22
+ import { composable, composableProps, withColumn } from '@dxos/react-ui';
23
+ import { mx } from '@dxos/ui-theme';
24
+
25
+ import { translationKey } from '#translations';
26
+
27
+ import {
28
+ type FormHandler,
29
+ type FormHandlerProps,
30
+ type FormUpdateMeta,
31
+ useFormHandler,
32
+ useKeyHandler,
33
+ } from '../../hooks';
34
+ import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
35
+ import {
36
+ FormFieldSet as NaturalFormFieldSet,
37
+ type FormFieldSetProps as NaturalFormFieldSetProps,
38
+ } from './FormFieldSet';
39
+ import { FormTooltipsContext } from './FormTooltipsContext';
40
+ import { FormLayout as NaturalFormLayout } from './Layout';
41
+ import { type FormLayoutProps as NaturalFormLayoutProps } from './Layout/FormLayout';
42
+
43
+ // TODO(burdon): Move styles to form.ts (as with ui-theme).
44
+
45
+ // TODO(burdon): Reconcile with @dxos/echo.
46
+ export type ExcludeId<S extends Schema.Schema.AnyNoContext | Type.AnyEntity> = Omit<
47
+ S extends Type.AnyEntity
48
+ ? Type.InstanceType<S>
49
+ : S extends Schema.Schema.AnyNoContext
50
+ ? Schema.Schema.Type<S>
51
+ : never,
52
+ 'id'
53
+ >;
54
+
55
+ // TODO(burdon): Move to @dxos/schema (re-export here).
56
+ export const omitId = <S extends Schema.Schema.AnyNoContext | Type.AnyEntity>(
57
+ schemaOrType: S,
58
+ ): Schema.Schema<ExcludeId<S>, ExcludeId<S>> => {
59
+ const schema = Type.isType(schemaOrType)
60
+ ? Type.getSchema(schemaOrType)
61
+ : (schemaOrType as Schema.Schema.AnyNoContext);
62
+ return schema.pipe(Schema.omit('id')) as any;
63
+ };
64
+
65
+ /**
66
+ * Drop fields annotated with `FormInputAnnotation.set(false)` from a schema so
67
+ * the form's validator doesn't trip on required-but-hidden fields. Used by
68
+ * the picker's inline create form, where a `FactoryAnnotation` typically
69
+ * supplies the hidden values (e.g. a backing-object Ref) outside the form.
70
+ */
71
+ export const omitHiddenFormFields = <S extends Schema.Schema.AnyNoContext>(schema: S): S => {
72
+ const properties = SchemaAST.getPropertySignatures(schema.ast);
73
+ const hidden = properties
74
+ .filter((prop) => Option.getOrElse(EchoAnnotation.FormInputAnnotation.getFromAst(prop.type), () => true) === false)
75
+ .map((prop) => prop.name as string);
76
+ return hidden.length === 0 ? schema : (schema.pipe(Schema.omit(...(hidden as [string, ...string[]]))) as any);
77
+ };
78
+
79
+ //
80
+ // Context
81
+ //
82
+
83
+ type FormContextValue<T extends AnyProperties = any> = {
84
+ /**
85
+ * Form handler.
86
+ */
87
+ form: FormHandler<T>;
88
+
89
+ /**
90
+ * Show field tooltips (currently: JSON path on each label). Defaults to
91
+ * `true`; pass `false` to suppress.
92
+ */
93
+ tooltips?: boolean;
94
+
95
+ /**
96
+ * Testing.
97
+ */
98
+ testId?: string;
99
+ } & Pick<
100
+ NaturalFormFieldSetProps<T>,
101
+ 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap' | 'db'
102
+ >;
103
+
104
+ const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
105
+
106
+ /**
107
+ * Get the current form values.
108
+ */
109
+ const useFormValues: {
110
+ <T extends AnyProperties>(componentName: string, path?: (string | number)[]): T;
111
+ <T extends AnyProperties>(
112
+ componentName: string,
113
+ path: (string | number)[] | undefined,
114
+ defaultValue: () => T,
115
+ ): T | undefined;
116
+ } = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
117
+ const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
118
+ const jsonPath = createJsonPath(stablePath);
119
+ const {
120
+ form: { values, onValueChange },
121
+ } = useFormContext(componentName);
122
+
123
+ const value = getValue$(values, jsonPath);
124
+
125
+ // Apply default value once when the field has no value. lastAppliedPathRef prevents
126
+ // re-applying on every render (e.g. when defaultValue() returns null) and ensures
127
+ // we apply per path when the hook is used for different fields.
128
+ const lastAppliedPathRef = useRef<string | null>(null);
129
+ useEffect(() => {
130
+ if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
131
+ lastAppliedPathRef.current = jsonPath;
132
+ onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
133
+ }
134
+ }, [value, defaultValue, onValueChange, stablePath, jsonPath]);
135
+
136
+ return value;
137
+ };
138
+
139
+ /**
140
+ * Get the state props for the given field.
141
+ */
142
+ const useFormFieldState = (componentName: string, path: (string | number)[] = []): FormFieldStateProps => {
143
+ const stablePath = useMemo(() => path, [Array.isArray(path) ? path.join('.') : path]);
144
+ const {
145
+ form: { getStatus, getValue, onBlur, onValueChange },
146
+ } = useFormContext(componentName);
147
+
148
+ return useMemo(
149
+ () => ({
150
+ getStatus: () => getStatus(stablePath),
151
+ getValue: () => getValue(stablePath),
152
+ onBlur: () => onBlur(stablePath),
153
+ onValueChange: (ast: SchemaAST.AST, value: any) => onValueChange(stablePath, ast, value),
154
+ }),
155
+ [getStatus, getValue, onBlur, onValueChange, stablePath],
156
+ );
157
+ };
158
+
159
+ //
160
+ // Root
161
+ //
162
+
163
+ type FormRootProps<T extends AnyProperties = AnyProperties> = PropsWithChildren<
164
+ {
165
+ /**
166
+ * Called when the form is submitted and passes validation.
167
+ */
168
+ onSave?: (values: T, meta: FormUpdateMeta<T>) => void;
169
+
170
+ /**
171
+ * Called when the form is canceled to abandon/undo any pending changes.
172
+ */
173
+ onCancel?: () => void;
174
+ } & Omit<FormContextValue<T>, 'form'> &
175
+ Pick<FormHandlerProps<T>, 'schema' | 'autoSave' | 'values' | 'defaultValues' | 'onValidate' | 'onValuesChanged'> &
176
+ Omit<NaturalFormFieldSetProps<T>, 'schema' | 'path'>
177
+ >;
178
+
179
+ const FormRoot = <T extends AnyProperties = AnyProperties>({
180
+ children,
55
181
  schema,
56
- onCancel,
57
- onValuesChanged,
58
- onValidate,
182
+ values,
59
183
  onSave,
184
+ onCancel,
60
185
  ...props
61
- }: FormProps<T>) => {
62
- const formRef = useRef<HTMLDivElement>(null);
186
+ }: FormRootProps<T>) => {
187
+ const form = useFormHandler({ schema, values, onSave, onCancel, ...props });
63
188
 
64
- // Focus the first focusable element within this form.
65
- const { findFirstFocusable } = useFocusFinders();
66
- useEffect(() => {
67
- if (autoFocus && formRef.current) {
68
- const firstFocusable = findFirstFocusable(formRef.current);
69
- if (firstFocusable) {
70
- firstFocusable.focus();
71
- }
72
- }
73
- }, [autoFocus]);
74
-
75
- // TODO(burdon): Name?
76
- const handleValid = useCallback(
77
- async (values: any, meta: { changed: FormHandler<T>['changed'] }) => {
78
- if (autoSave) {
79
- await onSave?.(values, meta);
80
- }
81
- },
82
- [autoSave, onSave],
189
+ return (
190
+ <FormTooltipsContext.Provider value={props.tooltips ?? true}>
191
+ <FormContextProvider form={form} {...props}>
192
+ {children}
193
+ </FormContextProvider>
194
+ </FormTooltipsContext.Provider>
83
195
  );
196
+ };
197
+
198
+ FormRoot.displayName = 'Form.Root';
199
+
200
+ //
201
+ // Viewport
202
+ //
203
+
204
+ const FORM_VIEWPORT_NAME = 'Form.Viewport';
205
+
206
+ type FormViewportProps = {};
207
+
208
+ const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
209
+ return (
210
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
211
+ <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
212
+ </ScrollArea.Root>
213
+ );
214
+ });
215
+
216
+ FormViewport.displayName = FORM_VIEWPORT_NAME;
217
+
218
+ //
219
+ // Content
220
+ //
221
+
222
+ const FORM_CONTENT_NAME = 'Form.Content';
223
+
224
+ type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
225
+
226
+ const FormContent = composable<HTMLDivElement, FormContentProps>(({ children, ...props }, forwardedRef) => {
227
+ const { form, testId } = useFormContext(FORM_CONTENT_NAME);
228
+ const localRef = useRef<HTMLDivElement>(null);
229
+ const mergedRef = useMergeRefs([forwardedRef, localRef]);
230
+ useKeyHandler(localRef, form);
84
231
 
85
232
  return (
86
- <FormProvider
87
- formRef={formRef}
88
- schema={schema}
89
- autoSave={autoSave}
90
- initialValues={initialValues}
91
- onValuesChanged={onValuesChanged}
92
- onValidate={onValidate}
93
- onValid={handleValid}
94
- onSave={onSave}
233
+ <div
234
+ {...composableProps(props, {
235
+ role: 'form',
236
+ classNames: mx(withColumn.center(), 'flex flex-col w-full pb-form-gap'),
237
+ })}
238
+ data-testid={testId}
239
+ ref={mergedRef}
95
240
  >
96
- <div role='none' className='contents' data-testid={testId}>
97
- <FormFields
98
- {...props}
99
- ref={formRef}
100
- classNames={[
101
- outerSpacing === 'blockStart-0'
102
- ? 'pli-cardSpacingInline mbe-cardSpacingBlock [&>.mbs-inputSpacingBlock:first-child]:!mbs-0'
103
- : outerSpacing === 'scroll-fields'
104
- ? 'pli-cardSpacingInline pbe-cardSpacingBlock'
105
- : outerSpacing
106
- ? cardSpacing
107
- : false,
108
- outerSpacing === 'scroll-fields' && cardDialogOverflow,
109
- classNames,
110
- ]}
111
- readonly={readonly}
112
- schema={schema}
241
+ {children}
242
+ </div>
243
+ );
244
+ });
245
+
246
+ FormContent.displayName = FORM_CONTENT_NAME;
247
+
248
+ //
249
+ // FieldSet
250
+ //
251
+
252
+ const FORM_FIELDSET_NAME = 'Form.FieldSet';
253
+
254
+ type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
255
+
256
+ const FormFieldSet = (props: FormFieldSetProps) => {
257
+ const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
258
+
259
+ return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
260
+ };
261
+
262
+ FormFieldSet.displayName = FORM_FIELDSET_NAME;
263
+
264
+ //
265
+ // Layout
266
+ //
267
+
268
+ const FORM_LAYOUT_NAME = 'Form.Layout';
269
+
270
+ type FormLayoutProps = Omit<NaturalFormLayoutProps, 'schema'> & { schema?: NaturalFormLayoutProps['schema'] };
271
+
272
+ const FormLayout = ({ schema, ...props }: FormLayoutProps) => {
273
+ const { form, ...contextProps } = useFormContext(FORM_LAYOUT_NAME);
274
+ const resolvedSchema = schema ?? form.schema;
275
+ if (!resolvedSchema) {
276
+ return null;
277
+ }
278
+
279
+ return <NaturalFormLayout schema={resolvedSchema} {...contextProps} {...props} />;
280
+ };
281
+
282
+ FormLayout.displayName = FORM_LAYOUT_NAME;
283
+
284
+ //
285
+ // Actions
286
+ //
287
+
288
+ const FORM_ACTIONS_NAME = 'Form.Actions';
289
+
290
+ type FormActionsProps = ThemedClassName<{}>;
291
+
292
+ const FormActions = ({ classNames }: FormActionsProps) => {
293
+ const { t } = useTranslation(translationKey);
294
+ const {
295
+ form: { canSave, onSave, onCancel },
296
+ readonly,
297
+ layout,
298
+ } = useFormContext(FORM_ACTIONS_NAME);
299
+
300
+ if (readonly || layout === 'static') {
301
+ return null;
302
+ }
303
+
304
+ // TODO(burdon): Currently onCancel is a no-op; implement "revert values".
305
+ // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
306
+
307
+ return (
308
+ <div
309
+ className={mx(withColumn.center(), 'grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}
310
+ >
311
+ {onCancel && (
312
+ <IconButton
313
+ icon='ph--x--regular'
314
+ iconEnd
315
+ label={t('cancel-button.label')}
316
+ onClick={onCancel}
317
+ data-testid='cancel-button'
113
318
  />
114
- {(onCancel || onSave) && !autoSave && !readonly && (
115
- <FormActions readonly={readonly} onCancel={onCancel} outerSpacing={outerSpacing} />
116
- )}
319
+ )}
320
+ {onSave && (
321
+ <IconButton
322
+ type='submit'
323
+ variant='primary'
324
+ disabled={!canSave}
325
+ icon='ph--check--regular'
326
+ iconEnd
327
+ label={t('save-button.label')}
328
+ onClick={onSave}
329
+ data-testid='save-button'
330
+ />
331
+ )}
332
+ </div>
333
+ );
334
+ };
335
+
336
+ FormActions.displayName = FORM_ACTIONS_NAME;
337
+
338
+ //
339
+ // Section
340
+ //
341
+
342
+ const FORM_SECTION_NAME = 'Form.Section';
343
+
344
+ type FormSectionProps = ThemedClassName<{ label?: string; description?: string }>;
345
+
346
+ const FormSection = composable<HTMLDivElement, FormSectionProps>(
347
+ ({ children, label, description, ...props }, forwardedRef) => {
348
+ return (
349
+ <div
350
+ {...composableProps(props, { classNames: 'flex flex-col pt-form-section-gap first:pt-0' })}
351
+ ref={forwardedRef}
352
+ >
353
+ {label && <h2 className='text-lg'>{label}</h2>}
354
+ {description && <p className='text-description'>{description}</p>}
355
+ {children}
117
356
  </div>
118
- </FormProvider>
357
+ );
358
+ },
359
+ );
360
+
361
+ FormSection.displayName = FORM_SECTION_NAME;
362
+
363
+ //
364
+ // Submit
365
+ //
366
+
367
+ const FORM_SUBMIT_NAME = 'Form.Submit';
368
+
369
+ type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
370
+
371
+ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
372
+ const { t } = useTranslation(translationKey);
373
+ const {
374
+ form: { canSave, onSave },
375
+ readonly,
376
+ layout,
377
+ } = useFormContext(FORM_SUBMIT_NAME);
378
+
379
+ if (readonly || layout === 'static') {
380
+ return null;
381
+ }
382
+
383
+ return (
384
+ <div className={mx('flex w-full pt-form-padding', classNames)}>
385
+ <IconButton
386
+ classNames='w-full'
387
+ type='submit'
388
+ variant='primary'
389
+ disabled={disabled ?? !canSave}
390
+ icon={icon ?? 'ph--check--regular'}
391
+ label={label ?? t('save-button.label')}
392
+ onClick={onSave}
393
+ data-testid='save-button'
394
+ />
395
+ </div>
119
396
  );
120
397
  };
398
+
399
+ FormSubmit.displayName = FORM_SUBMIT_NAME;
400
+
401
+ //
402
+ // Form
403
+ // https://www.radix-ui.com/primitives/docs/guides/composition
404
+ //
405
+
406
+ export const Form = {
407
+ Root: FormRoot,
408
+ Viewport: FormViewport,
409
+ Content: FormContent,
410
+ Section: FormSection,
411
+ FieldSet: FormFieldSet,
412
+ Layout: FormLayout,
413
+ Label: FormFieldLabel,
414
+ Actions: FormActions,
415
+ Submit: FormSubmit,
416
+ };
417
+
418
+ export { useFormContext, useFormValues, useFormFieldState };
419
+
420
+ export type {
421
+ FormRootProps,
422
+ FormViewportProps,
423
+ FormContentProps,
424
+ FormSectionProps,
425
+ FormFieldSetProps,
426
+ FormLayoutProps,
427
+ FormFieldLabelProps,
428
+ FormActionsProps,
429
+ FormSubmitProps,
430
+ };