@dxos/react-ui-form 0.8.4-main.f5c0578 → 0.8.4-main.fcfe5033a5

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 (229) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2753 -2121
  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 +2753 -2121
  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 -8
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +83 -5
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +86 -25
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +129 -56
  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 +83 -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/ObjectForm/ObjectForm.d.ts +9 -0
  53. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  54. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  55. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  56. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +54 -0
  57. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  58. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +86 -0
  59. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  60. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  61. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  62. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  63. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  64. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +83 -0
  65. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  67. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  68. package/dist/types/src/components/Settings/Settings.d.ts +43 -0
  69. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  70. package/dist/types/src/components/Settings/Settings.stories.d.ts +83 -0
  71. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  72. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +38 -0
  73. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  74. package/dist/types/src/components/Settings/SettingsItem.d.ts +17 -0
  75. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  76. package/dist/types/src/components/Settings/index.d.ts +3 -0
  77. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  78. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +22 -10
  79. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  80. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +85 -8
  81. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  82. package/dist/types/src/components/index.d.ts +4 -2
  83. package/dist/types/src/components/index.d.ts.map +1 -1
  84. package/dist/types/src/components/testing.d.ts +4 -4
  85. package/dist/types/src/components/testing.d.ts.map +1 -1
  86. package/dist/types/src/hooks/index.d.ts +2 -3
  87. package/dist/types/src/hooks/index.d.ts.map +1 -1
  88. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  89. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  90. package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
  91. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  92. package/dist/types/src/translations.d.ts +64 -55
  93. package/dist/types/src/translations.d.ts.map +1 -1
  94. package/dist/types/src/util/field-type.d.ts +19 -0
  95. package/dist/types/src/util/field-type.d.ts.map +1 -0
  96. package/dist/types/src/util/field.d.ts +3 -3
  97. package/dist/types/src/util/field.d.ts.map +1 -1
  98. package/dist/types/src/util/formatting.d.ts +2 -2
  99. package/dist/types/src/util/formatting.d.ts.map +1 -1
  100. package/dist/types/src/util/index.d.ts +2 -1
  101. package/dist/types/src/util/index.d.ts.map +1 -1
  102. package/dist/types/src/util/properties.d.ts +7 -0
  103. package/dist/types/src/util/properties.d.ts.map +1 -0
  104. package/dist/types/src/util/refs.d.ts +3 -3
  105. package/dist/types/src/util/refs.d.ts.map +1 -1
  106. package/dist/types/tsconfig.tsbuildinfo +1 -1
  107. package/package.json +45 -43
  108. package/src/components/FieldEditor/FieldEditor.stories.tsx +48 -31
  109. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  110. package/src/components/FieldEditor/FieldEditor.tsx +84 -90
  111. package/src/components/Form/AUDIT.md +17 -0
  112. package/src/components/Form/Form.stories.tsx +144 -222
  113. package/src/components/Form/Form.tsx +359 -96
  114. package/src/components/Form/FormField.tsx +304 -0
  115. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  116. package/src/components/Form/FormFieldComponent.tsx +171 -0
  117. package/src/components/Form/FormFieldSet.tsx +144 -0
  118. package/src/components/Form/README.md +3 -0
  119. package/src/components/Form/fields/ArrayField.tsx +154 -0
  120. package/src/components/Form/fields/BooleanField.tsx +22 -0
  121. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  122. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  123. package/src/components/Form/fields/NumberField.tsx +39 -0
  124. package/src/components/Form/fields/RefField.tsx +198 -0
  125. package/src/components/Form/fields/SelectField.tsx +65 -0
  126. package/src/components/Form/fields/SelectOptionField.stories.tsx +65 -0
  127. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +36 -31
  128. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  129. package/src/components/Form/fields/TextField.tsx +38 -0
  130. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +17 -10
  131. package/src/components/Form/fields/index.ts +15 -0
  132. package/src/components/Form/index.ts +3 -4
  133. package/src/components/ObjectForm/ObjectForm.tsx +113 -0
  134. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  135. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +209 -0
  136. package/src/components/ObjectPicker/ObjectPicker.tsx +186 -0
  137. package/src/components/ObjectPicker/index.ts +5 -0
  138. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
  139. package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
  140. package/src/components/ObjectProperties/index.ts +5 -0
  141. package/src/components/Settings/Settings.stories.tsx +103 -0
  142. package/src/components/Settings/Settings.tsx +81 -0
  143. package/src/components/Settings/SettingsFieldSet.tsx +211 -0
  144. package/src/components/Settings/SettingsItem.tsx +62 -0
  145. package/src/components/Settings/index.ts +6 -0
  146. package/src/components/ViewEditor/ViewEditor.stories.tsx +139 -63
  147. package/src/components/ViewEditor/ViewEditor.test.tsx +12 -15
  148. package/src/components/ViewEditor/ViewEditor.tsx +346 -211
  149. package/src/components/index.ts +4 -3
  150. package/src/components/testing.tsx +28 -12
  151. package/src/hooks/index.ts +2 -3
  152. package/src/hooks/useFormHandler.ts +380 -0
  153. package/src/hooks/useKeyHandler.tsx +37 -0
  154. package/src/translations.ts +67 -59
  155. package/src/util/field-type.ts +57 -0
  156. package/src/util/field.ts +33 -43
  157. package/src/util/formatting.ts +22 -22
  158. package/src/util/index.ts +2 -1
  159. package/src/util/properties.ts +18 -0
  160. package/src/util/refs.ts +17 -19
  161. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  162. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  163. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -8
  164. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  165. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  166. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  167. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  168. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  169. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  170. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  171. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  172. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  173. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  174. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  175. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  176. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  177. package/dist/types/src/components/Form/Input.d.ts +0 -26
  178. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  179. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  180. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  181. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  182. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  183. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  184. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  185. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  186. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  187. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  188. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  189. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  190. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  191. package/dist/types/src/components/Form/factory.d.ts +0 -8
  192. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  193. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  194. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  195. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  196. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  197. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  198. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  199. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  200. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  201. package/dist/types/src/hooks/useForm.d.ts +0 -67
  202. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  203. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  204. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  205. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  206. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  207. package/dist/types/src/util/schema.d.ts +0 -3
  208. package/dist/types/src/util/schema.d.ts.map +0 -1
  209. package/src/components/ControlSection/ControlSection.stories.tsx +0 -42
  210. package/src/components/ControlSection/ControlSection.tsx +0 -139
  211. package/src/components/Form/ArrayField.tsx +0 -125
  212. package/src/components/Form/Defaults.tsx +0 -204
  213. package/src/components/Form/FormActions.tsx +0 -54
  214. package/src/components/Form/FormContent.tsx +0 -241
  215. package/src/components/Form/FormContext.tsx +0 -110
  216. package/src/components/Form/Input.tsx +0 -51
  217. package/src/components/Form/RefField.tsx +0 -214
  218. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  219. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -62
  220. package/src/components/Form/custom/index.ts +0 -7
  221. package/src/components/Form/factory.tsx +0 -31
  222. package/src/components/deprecated/Form.stories.tsx +0 -63
  223. package/src/components/deprecated/Form.tsx +0 -119
  224. package/src/components/deprecated/FormInput.tsx +0 -71
  225. package/src/components/deprecated/index.ts +0 -5
  226. package/src/hooks/useForm.ts +0 -293
  227. package/src/hooks/useQueryRefOptions.ts +0 -53
  228. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  229. package/src/util/schema.ts +0 -14
@@ -0,0 +1,154 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Option from 'effect/Option';
6
+ import * as SchemaAST from 'effect/SchemaAST';
7
+ import React, { useCallback } from 'react';
8
+
9
+ import { findNode, getArrayElementType, getDiscriminatedType, isDiscriminatedUnion, isNestedType } from '@dxos/effect';
10
+ import { IconButton, useTranslation } from '@dxos/react-ui';
11
+
12
+ import { translationKey } from '../../../translations';
13
+ import { getFormProperties } from '../../../util';
14
+ import { useFormValues } from '../Form';
15
+ import { FormField, type FormFieldProps } from '../FormField';
16
+ import { FormFieldLabel, type FormFieldStateProps } from '../FormFieldComponent';
17
+
18
+ export type ArrayFieldProps = {
19
+ label: string;
20
+ fieldProps: FormFieldStateProps;
21
+ } & FormFieldProps;
22
+
23
+ export const ArrayField = ({
24
+ type,
25
+ path,
26
+ label,
27
+ readonly,
28
+ layout,
29
+ fieldProps: inputProps,
30
+ ...props
31
+ }: ArrayFieldProps) => {
32
+ const { t } = useTranslation(translationKey);
33
+ const elementType = getArrayElementType(type);
34
+ const { onValueChange } = inputProps;
35
+ const values = useFormValues(ArrayField.displayName, path, () => []);
36
+
37
+ const getDefaultObjectValue = (typeNode: SchemaAST.AST): any => {
38
+ const baseNode = findNode(typeNode, isDiscriminatedUnion);
39
+ const typeLiteral = baseNode ? getDiscriminatedType(baseNode, {}) : findNode(typeNode, SchemaAST.isTypeLiteral);
40
+ if (!typeLiteral) {
41
+ return {};
42
+ }
43
+
44
+ return Object.fromEntries(
45
+ getFormProperties(typeLiteral).map((prop) => {
46
+ const defaultValue = SchemaAST.getDefaultAnnotation(prop.type).pipe((annotation) =>
47
+ Option.getOrUndefined(annotation),
48
+ );
49
+ return [prop.name, defaultValue];
50
+ }),
51
+ );
52
+ };
53
+
54
+ const handleAdd = useCallback(() => {
55
+ const defaultValue =
56
+ isNestedType(type) && elementType ? getDefaultObjectValue(elementType) : getDefaultValue(elementType);
57
+ values && onValueChange(type, [...values, defaultValue]);
58
+ }, [onValueChange, type, elementType, values]);
59
+
60
+ const handleDelete = useCallback(
61
+ (idx: number) => {
62
+ onValueChange(
63
+ type,
64
+ values?.filter((_, i) => i !== idx),
65
+ );
66
+ },
67
+ [onValueChange, type, values],
68
+ );
69
+
70
+ if (!elementType || ((readonly || layout === 'static') && values && values.length < 1)) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <>
76
+ {(layout !== 'static' || (values && values.length > 0)) && (
77
+ <FormFieldLabel readonly={readonly} label={label} asChild />
78
+ )}
79
+
80
+ <div role='none' className='flex flex-col gap-form-gap'>
81
+ {values?.map((_, index) => {
82
+ return (
83
+ <div role='none' key={index} className='flex gap-form-gap last:mb-form-gap items-center'>
84
+ <FormField
85
+ autoFocus={index === values.length - 1}
86
+ type={elementType}
87
+ path={[...(path ?? []), index]}
88
+ readonly={readonly || layout === 'static'}
89
+ layout='inline'
90
+ {...props}
91
+ />
92
+
93
+ {!readonly && layout !== 'static' && (
94
+ <div role='none' className='flex flex-col h-full justify-end'>
95
+ {/* NOTE: Aligns with center of last field if multi-field object. */}
96
+ <div role='none' className='flex items-center h-[2rem]'>
97
+ <IconButton
98
+ icon='ph--x--regular'
99
+ iconOnly
100
+ label={t('remove.button')}
101
+ onClick={() => handleDelete(index)}
102
+ classNames='self-center'
103
+ />
104
+ </div>
105
+ </div>
106
+ )}
107
+ </div>
108
+ );
109
+ })}
110
+ </div>
111
+
112
+ {/* TODO(burdon): Get label from schema. */}
113
+ {!readonly && layout !== 'static' && (
114
+ <IconButton
115
+ classNames='flex w-full'
116
+ icon='ph--plus--regular'
117
+ label={t('add-field.button')}
118
+ onClick={handleAdd}
119
+ />
120
+ )}
121
+ </>
122
+ );
123
+ };
124
+
125
+ ArrayField.displayName = 'Form.ArrayField';
126
+
127
+ /**
128
+ * Returns the default empty value for a given AST.
129
+ * Used for initializing new array values etc.
130
+ */
131
+ // TODO(wittjosiah): Factor out?
132
+ export const getDefaultValue = (ast?: SchemaAST.AST): any => {
133
+ switch (ast?._tag) {
134
+ case 'StringKeyword': {
135
+ return '';
136
+ }
137
+ case 'NumberKeyword': {
138
+ return 0;
139
+ }
140
+ case 'BooleanKeyword': {
141
+ return false;
142
+ }
143
+ case 'Suspend': {
144
+ return getDefaultValue(ast.f());
145
+ }
146
+ default: {
147
+ if (ast && isNestedType(ast)) {
148
+ return {};
149
+ } else {
150
+ throw new Error(`Unsupported type: ${ast?._tag}`);
151
+ }
152
+ }
153
+ }
154
+ };
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type SwitchProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
10
+
11
+ export const BooleanField = ({ type, readonly, onValueChange, ...props }: FormFieldComponentProps<boolean>) => {
12
+ const handleChange = useCallback<NonNullable<SwitchProps['onCheckedChange']>>(
13
+ (value) => onValueChange?.(type, value),
14
+ [type, onValueChange],
15
+ );
16
+
17
+ return (
18
+ <FormFieldWrapper<boolean> readonly={readonly} {...props}>
19
+ {({ value }) => <Input.Switch disabled={!!readonly} checked={value} onCheckedChange={handleChange} />}
20
+ </FormFieldWrapper>
21
+ );
22
+ };
@@ -0,0 +1,120 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
6
+
7
+ import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
8
+ import { Input, useTranslation } from '@dxos/react-ui';
9
+ import { safeParseFloat } from '@dxos/util';
10
+
11
+ import { translationKey } from '../../../translations';
12
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
13
+
14
+ export const GeoPointField = ({
15
+ type,
16
+ label,
17
+ readonly,
18
+ layout,
19
+ getStatus,
20
+ getValue,
21
+ onValueChange,
22
+ onBlur,
23
+ }: FormFieldComponentProps<GeoPoint>) => {
24
+ const { t } = useTranslation(translationKey);
25
+ const { status, error } = getStatus();
26
+ const geoPoint = useMemo<GeoPoint>(() => getValue() ?? [0, 0], [getValue]);
27
+ const value = useMemo(() => GeoLocation.fromGeoPoint(geoPoint), [geoPoint]);
28
+
29
+ const [longitudeText, setLongitudeText] = useState(value.longitude?.toString());
30
+ const [latitudeText, setLatitudeText] = useState(value.latitude?.toString());
31
+ useEffect(() => {
32
+ const location = GeoLocation.fromGeoPoint(geoPoint);
33
+ setLongitudeText(location.longitude?.toString());
34
+ setLatitudeText(location.latitude?.toString());
35
+ }, [geoPoint]);
36
+
37
+ const handleChange = useCallback(
38
+ (coordinateType: keyof Pick<GeoLocation, 'longitude' | 'latitude'>, setText: (text: string) => void) =>
39
+ (event: ChangeEvent<HTMLInputElement>) => {
40
+ const inputText = event.target.value;
41
+ setText(inputText);
42
+ if (inputText !== '' && inputText !== '-') {
43
+ const coord = safeParseFloat(inputText);
44
+ if (coord !== undefined && !isNaN(coord)) {
45
+ const currentLocation = GeoLocation.fromGeoPoint(getValue() ?? [0, 0]);
46
+ const newLocation = { ...currentLocation, [coordinateType]: coord };
47
+ const newValue = GeoLocation.toGeoPoint(newLocation);
48
+ onValueChange(type, newValue);
49
+ }
50
+ }
51
+ },
52
+ [type, getValue, onValueChange],
53
+ );
54
+
55
+ if ((readonly || layout === 'static') && !value?.latitude && !value?.longitude) {
56
+ return null;
57
+ }
58
+
59
+ return (
60
+ <Input.Root validationValence={status}>
61
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} asChild />}
62
+ {layout === 'static' ? (
63
+ <LatLng {...value} />
64
+ ) : (
65
+ <div role='none' className='grid grid-cols-2 gap-form-gap'>
66
+ <div>
67
+ <Input.Root>
68
+ {layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
69
+ <Input.TextInput
70
+ type='number'
71
+ step='0.00001'
72
+ min='-90'
73
+ max='90'
74
+ disabled={!!readonly}
75
+ placeholder={t('latitude.placeholder')}
76
+ value={latitudeText ?? ''}
77
+ onChange={handleChange('latitude', setLatitudeText)}
78
+ onBlur={onBlur}
79
+ />
80
+ </Input.Root>
81
+ </div>
82
+ <div>
83
+ <Input.Root>
84
+ {layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
85
+ <Input.TextInput
86
+ type='number'
87
+ step='0.00001'
88
+ min='-180'
89
+ max='180'
90
+ disabled={!!readonly}
91
+ placeholder={t('longitude.placeholder')}
92
+ value={longitudeText ?? ''}
93
+ onChange={handleChange('longitude', setLongitudeText)}
94
+ onBlur={onBlur}
95
+ />
96
+ </Input.Root>
97
+ </div>
98
+ </div>
99
+ )}
100
+ </Input.Root>
101
+ );
102
+ };
103
+
104
+ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
105
+ const latHem = latitude >= 0 ? 'N' : 'S';
106
+ const lngHem = longitude >= 0 ? 'E' : 'W';
107
+
108
+ return (
109
+ <span className='inline-flex items-center gap-form-gap'>
110
+ <span>
111
+ <span>{Math.abs(latitude).toFixed(5)}</span>
112
+ <span className='text-subdued'>°{latHem}</span>
113
+ </span>
114
+ <span>
115
+ <span>{Math.abs(longitude).toFixed(5)}</span>
116
+ <span className='text-subdued'>°{lngHem}</span>
117
+ </span>
118
+ </span>
119
+ );
120
+ };
@@ -0,0 +1,78 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useEffect, useRef } from 'react';
6
+
7
+ import { Input, type TextAreaProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
10
+
11
+ export const MarkdownField = ({
12
+ type,
13
+ readonly,
14
+ label,
15
+ placeholder,
16
+ layout,
17
+ getStatus,
18
+ getValue,
19
+ onValueChange,
20
+ onBlur,
21
+ }: FormFieldComponentProps) => {
22
+ const { status, error } = getStatus();
23
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
24
+
25
+ // TODO(ZaymonFC): If we start using `Enter` for form submission, we should prevent default behavior here
26
+ // since we're using a textarea element and the user needs to be able to enter new lines.
27
+
28
+ const adjustHeight = useCallback(() => {
29
+ const textarea = textareaRef.current;
30
+ if (textarea) {
31
+ textarea.style.blockSize = 'auto'; // For measurement.
32
+ textarea.style.blockSize = `${textarea.scrollHeight + 2}px`;
33
+ }
34
+ }, []);
35
+
36
+ // Adjust height on initial render.
37
+ useEffect(() => {
38
+ adjustHeight();
39
+ }, [adjustHeight]);
40
+
41
+ // Adjust height when the content changes.
42
+ useEffect(() => {
43
+ adjustHeight();
44
+ }, [getValue(), adjustHeight]);
45
+
46
+ const handleChange = useCallback<NonNullable<TextAreaProps['onChange']>>(
47
+ (event) => onValueChange(type, event.target.value),
48
+ [type, onValueChange],
49
+ );
50
+
51
+ const value = getValue();
52
+ if ((readonly || layout === 'static') && !value) {
53
+ return null;
54
+ }
55
+
56
+ // TODO(burdon): Use Markdown Editor.
57
+ return (
58
+ <Input.Root validationValence={status}>
59
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
60
+ {layout === 'static' ? (
61
+ <p>{value}</p>
62
+ ) : (
63
+ <Input.TextArea
64
+ ref={textareaRef}
65
+ disabled={!!readonly}
66
+ placeholder={placeholder}
67
+ value={value ?? ''}
68
+ classNames={'min-h-auto max-h-40 overflow-auto'}
69
+ onChange={handleChange}
70
+ onBlur={onBlur}
71
+ style={{ resize: 'none' }}
72
+ spellCheck={false}
73
+ />
74
+ )}
75
+ {layout === 'full' && <Input.Validation>{error}</Input.Validation>}
76
+ </Input.Root>
77
+ );
78
+ };
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type TextInputProps } from '@dxos/react-ui';
8
+ import { safeParseFloat } from '@dxos/util';
9
+
10
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
11
+
12
+ export const NumberField = ({
13
+ type,
14
+ readonly,
15
+ placeholder,
16
+ onValueChange,
17
+ onBlur,
18
+ ...props
19
+ }: FormFieldComponentProps<number>) => {
20
+ const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
21
+ (event) => onValueChange(type, safeParseFloat(event.target.value) || 0),
22
+ [type, onValueChange],
23
+ );
24
+
25
+ return (
26
+ <FormFieldWrapper<number> readonly={readonly} {...props}>
27
+ {({ value = '' }) => (
28
+ <Input.TextInput
29
+ type='number'
30
+ disabled={!!readonly}
31
+ placeholder={placeholder}
32
+ value={value}
33
+ onChange={handleChange}
34
+ onBlur={onBlur}
35
+ />
36
+ )}
37
+ </FormFieldWrapper>
38
+ );
39
+ };
@@ -0,0 +1,198 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useMemo } from 'react';
6
+
7
+ import '@dxos/lit-ui/dx-tag-picker.pcss';
8
+ import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
9
+ import { useQuery, useSchema as defaultUseSchema } from '@dxos/echo-react';
10
+ import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
11
+ import { findAnnotation } from '@dxos/effect';
12
+ import { DXN } from '@dxos/keys';
13
+ import { DxAnchor } from '@dxos/lit-ui/react';
14
+ import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
15
+ import { ParentLabelAnnotationId } from '@dxos/schema';
16
+
17
+ import { translationKey } from '../../../translations';
18
+ import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
19
+ import { omitId } from '../Form';
20
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
21
+
22
+ // TODO(burdon): Factor out.
23
+ const isRefSnapshot = (val: any): val is { '/': string } => {
24
+ return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
25
+ };
26
+
27
+ const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
28
+ results.map((result) => {
29
+ const id = Entity.getDXN(result).toString();
30
+ const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
31
+ const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
32
+ return { id, label: label ?? id };
33
+ });
34
+
35
+ const defaultUseResults: NonNullable<RefFieldProps['useResults']> = (db, typename) =>
36
+ useQuery(
37
+ db,
38
+ typename
39
+ ? // For Ref.Ref(Obj.Unknown) we want to show all objects.
40
+ typename === Type.getTypename(Obj.Unknown)
41
+ ? Filter.everything()
42
+ : Filter.typename(typename)
43
+ : Filter.nothing(),
44
+ );
45
+
46
+ export type RefFieldProps = FormFieldComponentProps &
47
+ Pick<
48
+ ObjectPickerContentProps,
49
+ 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
50
+ > & {
51
+ db?: Database.Database;
52
+ // TODO(burdon): Replace hooks with callbacks.
53
+ useSchema?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
54
+ useResults?: (db?: Database.Database, typename?: string) => Entity.Any[];
55
+ getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
56
+ onCreate?: (schema: Type.AnyEntity, values: any) => void;
57
+ };
58
+
59
+ export const RefField = (props: RefFieldProps) => {
60
+ const {
61
+ type,
62
+ readonly,
63
+ label,
64
+ placeholder,
65
+ layout,
66
+ getStatus,
67
+ getValue,
68
+ createOptionLabel,
69
+ createOptionIcon,
70
+ createInitialValuePath,
71
+ createFieldMap,
72
+ db,
73
+ useSchema = defaultUseSchema,
74
+ useResults = defaultUseResults,
75
+ getOptions = defaultGetOptions,
76
+ onCreate,
77
+ onValueChange,
78
+ } = props;
79
+ const { t } = useTranslation(translationKey);
80
+ const { status, error } = getStatus();
81
+
82
+ const typename = useMemo(
83
+ () => (type ? findAnnotation<ReferenceAnnotationValue>(type, ReferenceAnnotationId)?.typename : undefined),
84
+ [type],
85
+ );
86
+
87
+ const results = useResults(db, typename);
88
+ const options = useMemo(() => {
89
+ const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
90
+ return getOptions(results, { parentLabel });
91
+ }, [results, getOptions, type]);
92
+
93
+ const handleGetValue = useCallback(() => {
94
+ const formValue = getValue();
95
+ const unknownToRefOption = (value: unknown) => {
96
+ const isRef = Ref.isRef(value);
97
+ if (isRef || isRefSnapshot(value)) {
98
+ const dxnString = isRef ? value.dxn.toString() : value['/'];
99
+ const matchingOption = options.find((option) => option.id === dxnString);
100
+ if (matchingOption) {
101
+ return matchingOption;
102
+ }
103
+ }
104
+
105
+ return undefined;
106
+ };
107
+
108
+ return unknownToRefOption(formValue);
109
+ }, [options, getValue]);
110
+
111
+ const item = handleGetValue();
112
+ const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
113
+ const createSchema = useSchema(db, typename);
114
+
115
+ const handleCreate = useCallback(
116
+ (values: any) => {
117
+ if (createSchema && onCreate) {
118
+ onCreate(createSchema, values);
119
+ }
120
+ },
121
+ [createSchema, onCreate],
122
+ );
123
+
124
+ const handleUpdate = useCallback(
125
+ (id: string | undefined) => {
126
+ const item = options.find((option) => option.id === id);
127
+ const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
128
+ onValueChange(type, ref);
129
+ },
130
+ [options, type, onValueChange],
131
+ );
132
+
133
+ const handleSelect = useCallback(
134
+ (id: string) => {
135
+ if (item?.id === id) {
136
+ handleUpdate(undefined);
137
+ } else {
138
+ handleUpdate(id);
139
+ }
140
+ },
141
+ [item, handleUpdate],
142
+ );
143
+
144
+ if (!typename || ((readonly || layout === 'static') && !item)) {
145
+ return null;
146
+ }
147
+
148
+ return (
149
+ <Input.Root validationValence={status}>
150
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
151
+ <div>
152
+ {readonly ? (
153
+ !item ? (
154
+ <p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
155
+ ) : (
156
+ <DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
157
+ {item.label}
158
+ </DxAnchor>
159
+ )
160
+ ) : (
161
+ <ObjectPicker.Root>
162
+ <ObjectPicker.Trigger asChild classNames='p-0'>
163
+ {item ? (
164
+ <div role='none' className='flex gap-form-gap w-full'>
165
+ <Input.Root key={item.id}>
166
+ <Input.TextInput value={item.label} readOnly classNames='w-full' />
167
+ </Input.Root>
168
+ </div>
169
+ ) : (
170
+ <Button classNames='w-full text-start gap-form-gap'>
171
+ <div role='none' className='grow overflow-hidden'>
172
+ <span className='truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
173
+ </div>
174
+ <Icon size={3} icon='ph--caret-down--bold' />
175
+ </Button>
176
+ )}
177
+ </ObjectPicker.Trigger>
178
+ <ObjectPicker.Portal>
179
+ <ObjectPicker.Content
180
+ classNames='dx-card-popover-width'
181
+ options={options}
182
+ selectedIds={selectedIds}
183
+ createSchema={createSchema && omitId(createSchema)}
184
+ createOptionLabel={createOptionLabel}
185
+ createOptionIcon={createOptionIcon}
186
+ createInitialValuePath={createInitialValuePath}
187
+ createFieldMap={createFieldMap}
188
+ onCreate={handleCreate}
189
+ onSelect={handleSelect}
190
+ />
191
+ </ObjectPicker.Portal>
192
+ </ObjectPicker.Root>
193
+ )}
194
+ </div>
195
+ {layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
196
+ </Input.Root>
197
+ );
198
+ };
@@ -0,0 +1,65 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, Select, type SelectRootProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
10
+
11
+ export type SelectFieldOptions = FormFieldComponentProps & {
12
+ options?: Array<{ value: string | number; label?: string }>;
13
+ };
14
+
15
+ export const SelectField = ({
16
+ type,
17
+ readonly,
18
+ label,
19
+ layout,
20
+ placeholder,
21
+ options,
22
+ getStatus,
23
+ getValue,
24
+ onValueChange,
25
+ }: SelectFieldOptions) => {
26
+ const { status, error } = getStatus();
27
+ const value = getValue() as string | undefined;
28
+
29
+ const handleValueChange = useCallback<NonNullable<SelectRootProps['onValueChange']>>(
30
+ (value) => onValueChange(type, value),
31
+ [type, onValueChange],
32
+ );
33
+
34
+ if ((readonly || layout === 'static') && value == null) {
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <Input.Root validationValence={status}>
40
+ {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
41
+ {layout === 'static' ? (
42
+ <p>{options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)}</p>
43
+ ) : (
44
+ <Select.Root value={value} onValueChange={handleValueChange}>
45
+ {/* TODO(burdon): Placeholder not working? */}
46
+ <Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
47
+ <Select.Portal>
48
+ <Select.Content>
49
+ <Select.Viewport>
50
+ {options?.map(({ value, label }) => (
51
+ // NOTE: Numeric values are converted to and from strings.
52
+ <Select.Option key={String(value)} value={String(value)}>
53
+ {label ?? String(value)}
54
+ </Select.Option>
55
+ ))}
56
+ </Select.Viewport>
57
+ <Select.Arrow />
58
+ </Select.Content>
59
+ </Select.Portal>
60
+ </Select.Root>
61
+ )}
62
+ {layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
63
+ </Input.Root>
64
+ );
65
+ };