@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
@@ -0,0 +1,257 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as SchemaAST from 'effect/SchemaAST';
6
+ import * as String from 'effect/String';
7
+ import React, { useCallback, useMemo } from 'react';
8
+
9
+ import { getAnnotation, type SchemaProperty } from '@dxos/effect';
10
+ import { Input, Select } from '@dxos/react-ui';
11
+
12
+ import { getFormProperties, type SelectOption, detectFieldType, getSelectOptionsFromAst } from '../../util';
13
+ import { SettingsItem } from './SettingsItem';
14
+
15
+ //
16
+ // Types
17
+ //
18
+
19
+ /**
20
+ * Props for a custom field renderer.
21
+ * The renderer provides the control only; SettingsItem wrapper is handled by FieldSet.
22
+ */
23
+ export type SettingsFieldProps<T = any> = {
24
+ value: T;
25
+ onChange: (value: T) => void;
26
+ readonly?: boolean;
27
+ };
28
+
29
+ /**
30
+ * Map of property names to either a custom field renderer or a nested field map for struct fields.
31
+ * When a value type `T` is provided, the map shape is checked against the schema's value type.
32
+ */
33
+ export type SettingsFieldMap<T extends Record<string, any> = Record<string, any>> = {
34
+ [K in keyof T]?: NonNullable<T[K]> extends Record<string, any>
35
+ ? SettingsFieldMap<NonNullable<T[K]>>
36
+ : React.FC<SettingsFieldProps<T[K]>>;
37
+ };
38
+
39
+ export type SettingsFieldSetProps<T extends Record<string, any> = Record<string, any>> = {
40
+ /** Effect Schema for the settings object. */
41
+ schema: { ast: SchemaAST.AST };
42
+
43
+ /** Current settings values. */
44
+ values: T;
45
+
46
+ /** Callback when any value changes. Receives a new complete object. */
47
+ onValuesChanged?: (values: T) => void;
48
+
49
+ /** When true, all controls are disabled. */
50
+ readonly?: boolean;
51
+
52
+ /** Map of property names to custom field renderers. Nested objects map to struct sub-fields. */
53
+ fieldMap?: SettingsFieldMap<T>;
54
+
55
+ /** Control field visibility. Receives the dotted path (e.g. `'modelDefaults.edge'`) at every level. */
56
+ visible?: (path: string, values: T) => boolean;
57
+
58
+ /** Override the order of fields. Fields not listed are appended in schema order. Applied per scope. */
59
+ sort?: string[];
60
+
61
+ /** Internal: dot-path prefix used by recursive struct rendering. */
62
+ pathPrefix?: string;
63
+ };
64
+
65
+ //
66
+ // Component
67
+ //
68
+
69
+ export const SettingsFieldSet = <T extends Record<string, any>>({
70
+ schema,
71
+ values,
72
+ onValuesChanged,
73
+ readonly,
74
+ fieldMap,
75
+ visible,
76
+ sort,
77
+ pathPrefix = '',
78
+ }: SettingsFieldSetProps<T>) => {
79
+ const properties = useMemo(() => {
80
+ const props = getFormProperties(schema.ast);
81
+ if (!sort) {
82
+ return props;
83
+ }
84
+ const rank = (name: string) => {
85
+ const idx = sort.indexOf(name);
86
+ return idx === -1 ? Number.POSITIVE_INFINITY : idx;
87
+ };
88
+ return [...props].sort(({ name: a }, { name: b }) => rank(a.toString()) - rank(b.toString()));
89
+ }, [schema, sort]);
90
+
91
+ const handleChange = useCallback(
92
+ (name: string, value: unknown) => {
93
+ onValuesChanged?.({ ...values, [name]: value } as T);
94
+ },
95
+ [values, onValuesChanged],
96
+ );
97
+
98
+ return (
99
+ <>
100
+ {properties.map((property) => {
101
+ const name = property.name.toString();
102
+ const path = pathPrefix ? `${pathPrefix}.${name}` : name;
103
+ if (visible && !visible(path, values)) {
104
+ return null;
105
+ }
106
+
107
+ const entry = fieldMap?.[name as keyof typeof fieldMap] as
108
+ | React.FC<SettingsFieldProps>
109
+ | SettingsFieldMap
110
+ | undefined;
111
+ return (
112
+ <SettingsFieldItem
113
+ key={name}
114
+ path={path}
115
+ property={property}
116
+ value={values[name]}
117
+ onChange={(value) => handleChange(name, value)}
118
+ readonly={readonly}
119
+ customField={typeof entry === 'function' ? entry : undefined}
120
+ nestedFieldMap={typeof entry === 'object' ? entry : undefined}
121
+ visible={visible as (path: string, values: any) => boolean}
122
+ sort={sort}
123
+ />
124
+ );
125
+ })}
126
+ </>
127
+ );
128
+ };
129
+
130
+ SettingsFieldSet.displayName = 'Settings.FieldSet';
131
+
132
+ //
133
+ // Internal: render a single settings field.
134
+ //
135
+
136
+ type SettingsFieldItemProps = {
137
+ property: SchemaProperty;
138
+ path: string;
139
+ value: any;
140
+ onChange: (value: any) => void;
141
+ readonly?: boolean;
142
+ customField?: React.FC<SettingsFieldProps>;
143
+ nestedFieldMap?: SettingsFieldMap;
144
+ visible?: (path: string, values: any) => boolean;
145
+ sort?: string[];
146
+ };
147
+
148
+ const SettingsFieldItem = ({
149
+ property,
150
+ path,
151
+ value,
152
+ onChange,
153
+ readonly,
154
+ customField,
155
+ nestedFieldMap,
156
+ visible,
157
+ sort,
158
+ }: SettingsFieldItemProps) => {
159
+ const { type } = property;
160
+ const name = property.name.toString();
161
+ const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type) ?? String.capitalize(name);
162
+ const description = getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
163
+
164
+ // Custom field renderer.
165
+ if (customField) {
166
+ const CustomField = customField;
167
+ return (
168
+ <SettingsItem title={title} description={description}>
169
+ <CustomField value={value} onChange={onChange} readonly={readonly} />
170
+ </SettingsItem>
171
+ );
172
+ }
173
+
174
+ const fieldType = detectFieldType(type);
175
+
176
+ switch (fieldType) {
177
+ case 'struct':
178
+ return (
179
+ <SettingsFieldSet
180
+ schema={{ ast: type }}
181
+ values={value ?? {}}
182
+ onValuesChanged={onChange}
183
+ readonly={readonly}
184
+ fieldMap={nestedFieldMap}
185
+ visible={visible}
186
+ sort={sort}
187
+ pathPrefix={path}
188
+ />
189
+ );
190
+
191
+ case 'boolean':
192
+ return (
193
+ <SettingsItem title={title} description={description}>
194
+ <Input.Switch disabled={readonly} checked={!!value} onCheckedChange={(checked) => onChange(!!checked)} />
195
+ </SettingsItem>
196
+ );
197
+
198
+ case 'select': {
199
+ const options = getSelectOptionsFromAst(type) ?? [];
200
+ return (
201
+ <SettingsItem title={title} description={description}>
202
+ <Select.Root
203
+ disabled={readonly}
204
+ value={value == null ? '' : globalThis.String(value)}
205
+ onValueChange={(selected) => {
206
+ const matched = options.find((option) => globalThis.String(option.value) === selected);
207
+ onChange(matched?.value ?? selected);
208
+ }}
209
+ >
210
+ <Select.TriggerButton disabled={readonly} />
211
+ <Select.Portal>
212
+ <Select.Content>
213
+ <Select.Viewport>
214
+ {options.map((option: SelectOption) => (
215
+ <Select.Option key={globalThis.String(option.value)} value={globalThis.String(option.value)}>
216
+ {option.label ?? globalThis.String(option.value)}
217
+ </Select.Option>
218
+ ))}
219
+ </Select.Viewport>
220
+ <Select.Arrow />
221
+ </Select.Content>
222
+ </Select.Portal>
223
+ </Select.Root>
224
+ </SettingsItem>
225
+ );
226
+ }
227
+
228
+ case 'string':
229
+ return (
230
+ <SettingsItem title={title} description={description}>
231
+ <Input.TextInput disabled={readonly} value={value ?? ''} onChange={(event) => onChange(event.target.value)} />
232
+ </SettingsItem>
233
+ );
234
+
235
+ case 'number':
236
+ return (
237
+ <SettingsItem title={title} description={description}>
238
+ <Input.TextInput
239
+ disabled={readonly}
240
+ type='number'
241
+ value={value ?? ''}
242
+ onChange={(event) => {
243
+ if (event.target.value === '') {
244
+ onChange(undefined);
245
+ return;
246
+ }
247
+ const parsed = Number(event.target.value);
248
+ onChange(Number.isNaN(parsed) ? undefined : parsed);
249
+ }}
250
+ />
251
+ </SettingsItem>
252
+ );
253
+
254
+ default:
255
+ return null;
256
+ }
257
+ };
@@ -0,0 +1,58 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren } from 'react';
6
+
7
+ import { Input, type Label, type ThemedClassName, toLocalizedString, useTranslation } from '@dxos/react-ui';
8
+ import { mx } from '@dxos/ui-theme';
9
+
10
+ import { translationKey } from '#translations';
11
+
12
+ const SETTINGS_PANEL_NAME = 'Settings.Panel';
13
+ const SETTINGS_ITEM_NAME = 'Settings.Item';
14
+
15
+ //
16
+ // Panel (bordered card container).
17
+ //
18
+
19
+ type SettingsPanelProps = ThemedClassName<PropsWithChildren>;
20
+
21
+ export const SettingsPanel = ({ classNames, children }: SettingsPanelProps) => {
22
+ return (
23
+ <div className={mx('flex flex-col gap-3 p-trim-md border border-separator rounded-sm', classNames)}>{children}</div>
24
+ );
25
+ };
26
+
27
+ SettingsPanel.displayName = SETTINGS_PANEL_NAME;
28
+
29
+ //
30
+ // Item (label + optional description + control, proper label semantics).
31
+ //
32
+
33
+ type SettingsItemProps = PropsWithChildren<{
34
+ title: Label;
35
+ description?: Label;
36
+ }>;
37
+
38
+ export const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
39
+ const { t } = useTranslation(translationKey);
40
+
41
+ return (
42
+ <Input.Root>
43
+ <SettingsPanel classNames='grid grid-cols-1 md:grid-cols-[1fr_1fr] gap-x-trim-lg gap-y-0'>
44
+ <Input.Label classNames='pb-trim-md text-base-foreground text-lg md:col-span-2'>
45
+ {toLocalizedString(title, t)}
46
+ </Input.Label>
47
+ <Input.DescriptionAndValidation>
48
+ <Input.Description classNames='text-base text-description'>
49
+ {toLocalizedString(description, t)}
50
+ </Input.Description>
51
+ </Input.DescriptionAndValidation>
52
+ <div className='text-end py-1'>{children}</div>
53
+ </SettingsPanel>
54
+ </Input.Root>
55
+ );
56
+ };
57
+
58
+ SettingsItem.displayName = SETTINGS_ITEM_NAME;
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Select } from '@dxos/react-ui';
8
+
9
+ import { type SettingsFieldProps } from './SettingsFieldSet';
10
+
11
+ export type SelectFieldOption = {
12
+ value: string;
13
+ label?: string;
14
+ };
15
+
16
+ export type CreateSelectFieldOptions = {
17
+ /** Options to display. Strings are used as both value and label. */
18
+ options: ReadonlyArray<string | SelectFieldOption>;
19
+ /** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
20
+ defaultLabel?: string | null;
21
+ };
22
+
23
+ /**
24
+ * Factory for a `SettingsFieldSet` `fieldMap` entry that renders a Select control
25
+ * over a fixed list of options, with an optional sentinel for `undefined`.
26
+ */
27
+ export const createSelectField = ({
28
+ options,
29
+ defaultLabel = 'Default',
30
+ }: CreateSelectFieldOptions): React.FC<SettingsFieldProps<string | undefined>> => {
31
+ const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
32
+ const hasDefault = defaultLabel !== null;
33
+ // Per-instance sentinel avoids collisions with real option values; empty string when the
34
+ // sentinel is omitted so `undefined` maps to a value Select.Root treats as "no selection".
35
+ const sentinel = hasDefault ? `__default__${Math.random().toString(36).slice(2)}` : '';
36
+
37
+ return ({ value, onChange, readonly }) => (
38
+ <Select.Root
39
+ disabled={readonly}
40
+ value={value ?? sentinel}
41
+ onValueChange={(next) => onChange(hasDefault && next === sentinel ? undefined : next)}
42
+ >
43
+ <Select.TriggerButton disabled={readonly} />
44
+ <Select.Portal>
45
+ <Select.Content>
46
+ <Select.Viewport>
47
+ {hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
48
+ {normalized.map((option) => (
49
+ <Select.Option key={option.value} value={option.value}>
50
+ {option.label ?? option.value}
51
+ </Select.Option>
52
+ ))}
53
+ </Select.Viewport>
54
+ <Select.Arrow />
55
+ </Select.Content>
56
+ </Select.Portal>
57
+ </Select.Root>
58
+ );
59
+ };
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './Settings';
6
+ export { type SettingsFieldMap, type SettingsFieldProps, type SettingsFieldSetProps } from './SettingsFieldSet';
7
+ export { createSelectField, type CreateSelectFieldOptions, type SelectFieldOption } from './SettingsSelectField';
@@ -6,43 +6,44 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
8
 
9
- import { DXN, Filter, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
10
- import { type EchoSchema, Format } from '@dxos/echo/internal';
9
+ import { DXN, EchoURI, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type, type View } from '@dxos/echo';
10
+ import { Format, type Mutable } from '@dxos/echo/internal';
11
11
  import { useQuery } from '@dxos/react-client/echo';
12
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
12
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
13
13
  import { useAsyncEffect } from '@dxos/react-ui';
14
- import { withTheme } from '@dxos/react-ui/testing';
15
- import { DataType, type ProjectionModel, createView, getTypenameFromQuery } from '@dxos/schema';
14
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
15
+ import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
16
+ import { Employer, Organization, Person, Pipeline } from '@dxos/types';
16
17
 
17
- import { translations } from '../../translations';
18
- import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
18
+ import { translations } from '#translations';
19
19
 
20
+ import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
20
21
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
21
22
 
22
23
  const types = [
23
24
  // TODO(burdon): Get label from annotation.
24
- { value: Type.getTypename(DataType.Organization), label: 'Organization' },
25
- { value: Type.getTypename(DataType.Person), label: 'Person' },
26
- { value: Type.getTypename(DataType.Project), label: 'Project' },
27
- { value: Type.getTypename(DataType.Employer), label: 'Employer' },
25
+ { value: Type.getTypename(Organization.Organization), label: 'Organization' },
26
+ { value: Type.getTypename(Person.Person), label: 'Person' },
27
+ { value: Type.getTypename(Pipeline.Pipeline), label: 'Project' },
28
+ { value: Type.getTypename(Employer.Employer), label: 'Employer' },
28
29
  ];
29
30
 
30
31
  // Type definition for debug objects exposed to tests.
31
32
  export type ViewEditorDebugObjects = {
32
- schema: EchoSchema;
33
- view: DataType.View;
33
+ type: Type.AnyEntity;
34
+ view: View.View;
34
35
  projection: ProjectionModel;
35
36
  };
36
37
 
37
- type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
38
+ type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
38
39
 
39
- const DefaultStory = (props: StoryProps) => {
40
- const { space } = useClientProvider();
41
- const [schema, setSchema] = useState<EchoSchema>();
42
- const [view, setView] = useState<DataType.View>();
40
+ const DefaultStory = (props: DefaultStoryProps) => {
41
+ const { space } = useClientStory();
42
+ const [type, setType] = useState<Type.AnyEntity>();
43
+ const [view, setView] = useState<View.View>();
43
44
  const projectionRef = useRef<ProjectionModel>(null);
44
45
 
45
- const tags = useQuery(space, Filter.type(Tag.Tag));
46
+ const tags = useQuery(space?.db, Filter.type(Tag.Tag));
46
47
 
47
48
  useAsyncEffect(async () => {
48
49
  if (space) {
@@ -50,47 +51,38 @@ const DefaultStory = (props: StoryProps) => {
50
51
  name: Schema.String,
51
52
  email: Format.Email,
52
53
  salary: Format.Currency(),
53
- }).pipe(
54
- Type.Obj({
55
- typename: 'example.com/type/Test',
56
- version: '0.1.0',
57
- }),
58
- );
54
+ }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
59
55
 
60
56
  const AlternateSchema = Schema.Struct({
61
57
  title: Schema.String,
62
58
  description: Schema.String,
63
59
  completed: Schema.Boolean,
64
- }).pipe(
65
- Type.Obj({
66
- typename: 'example.com/type/Alternate',
67
- version: '0.1.0',
68
- }),
69
- );
60
+ }).pipe(Type.makeObject(DXN.make('com.example.type.alternate', '0.1.0')));
70
61
 
71
62
  const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
72
- const view = createView({
63
+ const view = ViewModel.make({
73
64
  name: 'Test',
74
- query: Query.select(Filter.typename(TestSchema.typename)),
75
- jsonSchema: Type.toJsonSchema(TestSchema),
76
- presentation: Obj.make(Type.Expando, {}),
65
+ query: Query.select(Filter.type(TestSchema)),
66
+ jsonSchema: JsonSchema.toJsonSchema(TestSchema),
77
67
  });
78
68
 
79
- setSchema(testSchema);
69
+ setType(testSchema);
80
70
  setView(view);
81
71
  }
82
72
  }, [space]);
83
73
 
84
74
  const updateViewQuery = useCallback(
85
- async (newQuery: QueryAST.Query, target?: string) => {
86
- if (!schema || !view || !space) {
75
+ async (newQuery: QueryAST.Query, target?: EchoURI.EchoURI) => {
76
+ if (!type || !view || !space) {
87
77
  return;
88
78
  }
89
79
 
90
- if (props.mode === 'query') {
91
- const queue = target && DXN.tryParse(target) ? target : undefined;
92
- const query = queue ? Query.fromAst(newQuery).options({ queues: [queue] }) : Query.fromAst(newQuery);
93
- view.query.ast = query.ast;
80
+ if (props.mode === 'tag') {
81
+ const queue = target;
82
+ const query = queue ? Query.fromAst(newQuery).from({ feeds: [queue] }) : Query.fromAst(newQuery);
83
+ Obj.update(view, (view) => {
84
+ view.query.ast = query.ast as Mutable<typeof query.ast>;
85
+ });
94
86
 
95
87
  const typename = getTypenameFromQuery(query.ast);
96
88
  const [newSchema] = await space.db.schemaRegistry.query({ typename }).run();
@@ -98,20 +90,21 @@ const DefaultStory = (props: StoryProps) => {
98
90
  return;
99
91
  }
100
92
 
101
- const newView = createView({
93
+ const newView = ViewModel.make({
102
94
  query,
103
95
  jsonSchema: newSchema.jsonSchema,
104
- presentation: Obj.make(Type.Expando, {}),
105
96
  });
106
- view.projection = Obj.getSnapshot(newView).projection;
107
- setSchema(() => newSchema);
97
+ Obj.update(view, (view) => {
98
+ view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
99
+ });
100
+ setType(() => newSchema);
108
101
  } else {
109
- console.log('updateViewQuery', { newQuery });
110
- view.query.ast = newQuery;
111
- schema.updateTypename(getTypenameFromQuery(newQuery));
102
+ Obj.update(view, (view) => {
103
+ view.query.ast = newQuery as Mutable<typeof newQuery>;
104
+ });
112
105
  }
113
106
  },
114
- [view, schema],
107
+ [view, type],
115
108
  );
116
109
 
117
110
  const handleDelete = useCallback(
@@ -121,41 +114,39 @@ const DefaultStory = (props: StoryProps) => {
121
114
 
122
115
  // Expose objects on window for test access.
123
116
  useEffect(() => {
124
- if (typeof window !== 'undefined' && schema && view && projectionRef.current) {
117
+ if (typeof window !== 'undefined' && type && view && projectionRef.current) {
125
118
  (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
126
- schema,
119
+ type,
127
120
  view,
128
121
  projection: projectionRef.current,
129
122
  } satisfies ViewEditorDebugObjects;
130
123
  }
131
- }, [schema, view]);
124
+ }, [type, view]);
132
125
 
133
126
  // NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
134
127
  const json = useMemo(
135
- () => JSON.parse(JSON.stringify({ schema, view, projection: projectionRef.current })),
136
- [JSON.stringify(schema), JSON.stringify(view)],
128
+ () => JSON.parse(JSON.stringify({ schema: type, view, projection: projectionRef.current })),
129
+ [JSON.stringify(type), JSON.stringify(view)],
137
130
  );
138
131
 
139
- if (!schema || !view) {
132
+ if (!type || !view) {
140
133
  return <div />;
141
134
  }
142
135
 
143
136
  return (
144
137
  <TestLayout json={json}>
145
- <TestPanel>
146
- <ViewEditor
147
- ref={projectionRef}
148
- schema={schema}
149
- view={view}
150
- registry={space?.db.schemaRegistry}
151
- mode={props.mode}
152
- readonly={props.readonly}
153
- types={types}
154
- tags={tags}
155
- onQueryChanged={updateViewQuery}
156
- onDelete={handleDelete}
157
- />
158
- </TestPanel>
138
+ <ViewEditor
139
+ ref={projectionRef}
140
+ type={type}
141
+ view={view}
142
+ registry={space?.db.schemaRegistry}
143
+ mode={props.mode}
144
+ readonly={props.readonly}
145
+ types={types}
146
+ tags={tags}
147
+ onQueryChanged={updateViewQuery}
148
+ onDelete={handleDelete}
149
+ />
159
150
  </TestLayout>
160
151
  );
161
152
  };
@@ -173,7 +164,8 @@ const meta = {
173
164
  space.db.add(Tag.make({ label: 'New' }));
174
165
  },
175
166
  }),
176
- withTheme,
167
+ withTheme(),
168
+ withLayout({ layout: 'fullscreen' }),
177
169
  ],
178
170
  parameters: {
179
171
  layout: 'fullscreen',
@@ -193,8 +185,8 @@ export const Readonly: Story = {
193
185
  },
194
186
  };
195
187
 
196
- export const QueryMode: Story = {
188
+ export const TagQueryMode: Story = {
197
189
  args: {
198
- mode: 'query',
190
+ mode: 'tag',
199
191
  },
200
192
  };