@dxos/react-ui-form 0.8.4-main.72ec0f3 → 0.8.4-main.8360d9e660

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 (185) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +2287 -2298
  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 +2287 -2298
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +13 -6
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +81 -27
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +59 -83
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +37 -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/{ControlSection/ControlSection.stories.d.ts → Form/fields/SelectOptionField.stories.d.ts} +12 -13
  41. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  43. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  44. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  45. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  46. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  47. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  48. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  49. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  50. package/dist/types/src/components/Form/index.d.ts +2 -4
  51. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  52. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +14 -9
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +9 -5
  55. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/Settings/Settings.d.ts +56 -0
  57. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  58. package/dist/types/src/components/{Form/custom/SelectOptionsInput.stories.d.ts → Settings/Settings.stories.d.ts} +10 -7
  59. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  60. package/dist/types/src/components/Settings/index.d.ts +2 -0
  61. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  62. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +11 -13
  63. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  64. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +12 -7
  65. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  66. package/dist/types/src/components/index.d.ts +1 -1
  67. package/dist/types/src/components/index.d.ts.map +1 -1
  68. package/dist/types/src/components/testing.d.ts +0 -2
  69. package/dist/types/src/components/testing.d.ts.map +1 -1
  70. package/dist/types/src/hooks/index.d.ts +2 -3
  71. package/dist/types/src/hooks/index.d.ts.map +1 -1
  72. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  73. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  74. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  75. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  76. package/dist/types/src/translations.d.ts +10 -6
  77. package/dist/types/src/translations.d.ts.map +1 -1
  78. package/dist/types/src/util/field.d.ts +3 -3
  79. package/dist/types/src/util/field.d.ts.map +1 -1
  80. package/dist/types/src/util/formatting.d.ts +2 -2
  81. package/dist/types/src/util/formatting.d.ts.map +1 -1
  82. package/dist/types/src/util/index.d.ts +1 -1
  83. package/dist/types/src/util/index.d.ts.map +1 -1
  84. package/dist/types/src/util/properties.d.ts +7 -0
  85. package/dist/types/src/util/properties.d.ts.map +1 -0
  86. package/dist/types/src/util/refs.d.ts +1 -2
  87. package/dist/types/src/util/refs.d.ts.map +1 -1
  88. package/dist/types/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +50 -47
  90. package/src/components/FieldEditor/FieldEditor.stories.tsx +43 -24
  91. package/src/components/FieldEditor/FieldEditor.tsx +74 -78
  92. package/src/components/Form/Form.stories.tsx +149 -248
  93. package/src/components/Form/Form.tsx +347 -110
  94. package/src/components/Form/FormField.tsx +294 -0
  95. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  96. package/src/components/Form/FormFieldComponent.tsx +171 -0
  97. package/src/components/Form/FormFieldSet.tsx +127 -0
  98. package/src/components/Form/README.md +3 -0
  99. package/src/components/Form/fields/ArrayField.tsx +149 -0
  100. package/src/components/Form/fields/BooleanField.tsx +22 -0
  101. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  102. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  103. package/src/components/Form/fields/NumberField.tsx +39 -0
  104. package/src/components/Form/fields/RefField.tsx +194 -0
  105. package/src/components/Form/fields/SelectField.tsx +65 -0
  106. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +19 -16
  107. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +24 -18
  108. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  109. package/src/components/Form/fields/TextField.tsx +38 -0
  110. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +15 -8
  111. package/src/components/Form/fields/index.ts +15 -0
  112. package/src/components/Form/index.ts +3 -4
  113. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +15 -12
  114. package/src/components/ObjectPicker/ObjectPicker.tsx +76 -62
  115. package/src/components/Settings/Settings.stories.tsx +56 -0
  116. package/src/components/Settings/Settings.tsx +230 -0
  117. package/src/components/{ControlSection → Settings}/index.ts +1 -1
  118. package/src/components/ViewEditor/ViewEditor.stories.tsx +45 -42
  119. package/src/components/ViewEditor/ViewEditor.test.tsx +5 -4
  120. package/src/components/ViewEditor/ViewEditor.tsx +297 -210
  121. package/src/components/index.ts +1 -1
  122. package/src/components/testing.tsx +12 -7
  123. package/src/hooks/index.ts +2 -3
  124. package/src/hooks/useFormHandler.ts +380 -0
  125. package/src/hooks/useKeyHandler.tsx +37 -0
  126. package/src/translations.ts +12 -9
  127. package/src/util/field.ts +25 -25
  128. package/src/util/formatting.ts +22 -22
  129. package/src/util/index.ts +1 -1
  130. package/src/util/properties.ts +18 -0
  131. package/src/util/refs.ts +7 -13
  132. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  133. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  134. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  135. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  136. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  137. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  138. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  139. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  140. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  141. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  142. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  143. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  144. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  145. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  146. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  147. package/dist/types/src/components/Form/Input.d.ts +0 -29
  148. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  149. package/dist/types/src/components/Form/RefField.d.ts +0 -22
  150. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  151. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  152. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  153. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  154. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  155. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  156. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  157. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  158. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  159. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  160. package/dist/types/src/components/Form/factory.d.ts +0 -8
  161. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  162. package/dist/types/src/hooks/useForm.d.ts +0 -67
  163. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  164. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
  165. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  166. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  167. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  168. package/dist/types/src/util/schema.d.ts +0 -3
  169. package/dist/types/src/util/schema.d.ts.map +0 -1
  170. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  171. package/src/components/ControlSection/ControlSection.tsx +0 -153
  172. package/src/components/Form/ArrayField.tsx +0 -125
  173. package/src/components/Form/Defaults.tsx +0 -205
  174. package/src/components/Form/FormActions.tsx +0 -54
  175. package/src/components/Form/FormContent.tsx +0 -362
  176. package/src/components/Form/FormContext.tsx +0 -110
  177. package/src/components/Form/Input.tsx +0 -54
  178. package/src/components/Form/RefField.tsx +0 -283
  179. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  180. package/src/components/Form/custom/index.ts +0 -7
  181. package/src/components/Form/factory.tsx +0 -31
  182. package/src/hooks/useForm.ts +0 -296
  183. package/src/hooks/useQueryRefOptions.ts +0 -62
  184. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  185. package/src/util/schema.ts +0 -12
@@ -6,17 +6,17 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useState } from 'react';
8
8
 
9
- import { type SelectOption, SelectOptionSchema } from '@dxos/echo/internal';
10
- import { withTheme } from '@dxos/react-ui/testing';
9
+ import { SelectOption } from '@dxos/echo/internal';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
 
12
12
  import { translations } from '../../../translations';
13
- import { TestLayout, TestPanel } from '../../testing';
13
+ import { TestLayout } from '../../testing';
14
14
  import { Form } from '../Form';
15
15
 
16
- import { SelectOptionInput } from './SelectOptionsInput';
16
+ import { SelectOptionField } from './SelectOptionField';
17
17
 
18
18
  const schema = Schema.Struct({
19
- options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
19
+ options: Schema.Array(SelectOption).pipe(Schema.mutable),
20
20
  }).pipe(Schema.mutable);
21
21
 
22
22
  const DefaultStory = () => {
@@ -31,23 +31,26 @@ const DefaultStory = () => {
31
31
 
32
32
  return (
33
33
  <TestLayout>
34
- <TestPanel>
35
- <Form
36
- schema={schema}
37
- values={values}
38
- onSave={(vals) => setValues(vals)}
39
- Custom={{ options: SelectOptionInput }}
40
- />
41
- </TestPanel>
34
+ <Form.Root
35
+ schema={schema}
36
+ values={values}
37
+ fieldMap={{ options: SelectOptionField }}
38
+ onSave={(values) => setValues(values)}
39
+ >
40
+ <Form.Content>
41
+ <Form.FieldSet />
42
+ <Form.Actions />
43
+ </Form.Content>
44
+ </Form.Root>
42
45
  </TestLayout>
43
46
  );
44
47
  };
45
48
 
46
49
  const meta = {
47
- title: 'ui/react-ui-form/SelectOptionsInput',
48
- component: SelectOptionInput as any,
50
+ title: 'ui/react-ui-form/SelectOptionField',
51
+ component: SelectOptionField as any,
49
52
  render: DefaultStory,
50
- decorators: [withTheme],
53
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
51
54
  parameters: {
52
55
  layout: 'fullscreen',
53
56
  translations,
@@ -9,25 +9,32 @@ import { PublicKey } from '@dxos/keys';
9
9
  import { type ChromaticPalette, IconButton, Input, Tag, useTranslation } from '@dxos/react-ui';
10
10
  import { List } from '@dxos/react-ui-list';
11
11
  import { HuePicker } from '@dxos/react-ui-pickers';
12
- import { hues, subtleHover } from '@dxos/react-ui-theme';
12
+ import { hues, osTranslations, subtleHover } from '@dxos/ui-theme';
13
13
 
14
14
  import { translationKey } from '../../../translations';
15
- import { InputHeader, type InputProps } from '../Input';
16
-
17
- export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue, onValueChange }: InputProps) => {
15
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
16
+
17
+ export const SelectOptionField = ({
18
+ type,
19
+ readonly,
20
+ label,
21
+ getStatus,
22
+ getValue,
23
+ onValueChange,
24
+ }: FormFieldComponentProps<SelectOption[] | undefined>) => {
25
+ const { t } = useTranslation(translationKey);
26
+ const { status, error } = getStatus();
18
27
  const [selected, setSelectedId] = useState<string | null>(null);
19
28
  const [isNewOption, setIsNewOption] = useState(false);
20
29
  const inputRef = useRef<HTMLInputElement>(null);
21
- const { t } = useTranslation(translationKey);
22
- const { status, error } = getStatus();
23
- const options = getValue<SelectOption[] | undefined>();
30
+ const options = getValue();
24
31
 
25
32
  // Initialization.
26
33
  useEffect(() => {
27
34
  if (options === undefined) {
28
35
  onValueChange(type, []);
29
36
  }
30
- }, [options, onValueChange, type]);
37
+ }, [options, type, onValueChange]);
31
38
 
32
39
  const randomHue = () => {
33
40
  const usedHues = new Set(options?.map((option) => option.color) ?? []);
@@ -88,8 +95,8 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
88
95
  [options, type, onValueChange],
89
96
  );
90
97
 
91
- const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
92
- if (e.key === 'Enter') {
98
+ const handleKeyDown = useCallback((ev: KeyboardEvent<HTMLInputElement>) => {
99
+ if (ev.key === 'Enter') {
93
100
  setSelectedId(null);
94
101
  }
95
102
  }, []);
@@ -111,23 +118,23 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
111
118
 
112
119
  return (
113
120
  <Input.Root validationValence={status}>
114
- <InputHeader error={error} label={label} />
121
+ <FormFieldLabel error={error} readonly={readonly} label={label} />
115
122
  <div role='none'>
116
123
  {options && (
117
124
  <List.Root
118
125
  items={options}
119
- isItem={(item) => true}
126
+ isItem={(_item) => true}
120
127
  onMove={readonly ? undefined : handleMove}
121
128
  readonly={!!readonly}
122
129
  >
123
130
  {({ items }) => (
124
- <div role='list' className='is-full overflow-auto'>
131
+ <div role='list' className='w-full overflow-auto'>
125
132
  {items?.map((item) => (
126
133
  <div key={item.id}>
127
134
  <List.Item
128
135
  role='button'
129
136
  item={item}
130
- classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-sm']}
137
+ classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
131
138
  aria-expanded={selected === item.id}
132
139
  >
133
140
  <div className='flex items-center'>
@@ -139,13 +146,13 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
139
146
  <IconButton
140
147
  iconOnly
141
148
  variant='ghost'
142
- label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: 'os' })}
149
+ label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: osTranslations })}
143
150
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
144
151
  onClick={() => handleClick(item.id)}
145
152
  />
146
153
  </div>
147
154
  {selected === item.id && (
148
- <div className='pli-cardSpacingInline plb-cardSpacingBlock flex flex-col gap-1 density-fine'>
155
+ <div className='flex flex-col p-form-padding gap-1 dx-density-fine'>
149
156
  {/* 16px to match drag handle width. */}
150
157
  <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
151
158
  <div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
@@ -157,7 +164,6 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
157
164
  onChange={handleTitleChange(item.id)}
158
165
  onKeyDown={handleKeyDown}
159
166
  classNames='flex-1'
160
- data-no-submit
161
167
  />
162
168
  <HuePicker disabled={!!readonly} value={item.color} onChange={handleColorChange(item.id)} />
163
169
  <IconButton
@@ -179,7 +185,7 @@ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue,
179
185
  label={t('select option add')}
180
186
  onClick={handleAdd}
181
187
  disabled={!!readonly}
182
- classNames='is-full'
188
+ classNames='w-full'
183
189
  />
184
190
  </div>
185
191
  )}
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { Input, type TextAreaProps } from '@dxos/react-ui';
8
+
9
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
10
+
11
+ export const TextAreaField = ({
12
+ type,
13
+ readonly,
14
+ placeholder,
15
+ onValueChange,
16
+ onBlur,
17
+ ...props
18
+ }: FormFieldComponentProps<string>) => {
19
+ const handleChange = useCallback<NonNullable<TextAreaProps['onChange']>>(
20
+ (event) => onValueChange(type, event.target.value),
21
+ [type, onValueChange],
22
+ );
23
+
24
+ return (
25
+ <FormFieldWrapper<string> readonly={readonly} {...props}>
26
+ {({ value = '' }) => (
27
+ <Input.TextArea
28
+ rows={3}
29
+ disabled={!!readonly}
30
+ placeholder={placeholder}
31
+ value={value}
32
+ onChange={handleChange}
33
+ onBlur={onBlur}
34
+ />
35
+ )}
36
+ </FormFieldWrapper>
37
+ );
38
+ };
@@ -0,0 +1,38 @@
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
+
9
+ import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
10
+
11
+ export const TextField = ({
12
+ type,
13
+ readonly,
14
+ placeholder,
15
+ onValueChange,
16
+ onBlur,
17
+ ...props
18
+ }: FormFieldComponentProps<string>) => {
19
+ const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
20
+ (event) => onValueChange(type, event.target.value),
21
+ [type, onValueChange],
22
+ );
23
+
24
+ return (
25
+ <FormFieldWrapper<string> readonly={readonly} {...props}>
26
+ {({ value = '' }) => (
27
+ <Input.TextInput
28
+ noAutoFill
29
+ disabled={!!readonly}
30
+ placeholder={placeholder}
31
+ value={value}
32
+ onChange={handleChange}
33
+ onBlur={onBlur}
34
+ />
35
+ )}
36
+ </FormFieldWrapper>
37
+ );
38
+ };
@@ -5,31 +5,33 @@
5
5
  import React from 'react';
6
6
 
7
7
  import { Input } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
8
+ import { mx } from '@dxos/ui-theme';
9
9
  import { safeParseFloat } from '@dxos/util';
10
10
 
11
- import { InputHeader, type InputProps } from '../Input';
11
+ import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
12
12
 
13
13
  const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
14
14
 
15
- export const TupleInput = ({
15
+ export const TupleField = ({
16
16
  binding,
17
17
  type,
18
- label,
19
18
  readonly,
19
+ label,
20
20
  getStatus,
21
21
  getValue,
22
22
  onValueChange,
23
23
  onBlur,
24
- }: InputProps & { binding: string[] }) => {
24
+ }: FormFieldComponentProps<Record<string, number>> & {
25
+ binding: string[];
26
+ }) => {
25
27
  const { status, error } = getStatus();
26
28
 
27
29
  // TODO(burdon): Generalize number/float/string, etc.
28
- const values: Record<string, number> = getValue<Record<string, number>>() ?? {};
30
+ const values: Record<string, number> = getValue() ?? {};
29
31
 
30
32
  return (
31
33
  <Input.Root validationValence={status}>
32
- <InputHeader error={error} label={label} />
34
+ <FormFieldLabel error={error} readonly={readonly} label={label} />
33
35
  <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
34
36
  {binding.map((prop) => (
35
37
  <Input.TextInput
@@ -37,7 +39,12 @@ export const TupleInput = ({
37
39
  type='number'
38
40
  disabled={!!readonly}
39
41
  value={values[prop]}
40
- onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
42
+ onChange={(event) => {
43
+ onValueChange(type, {
44
+ ...values,
45
+ [prop]: safeParseFloat(event.target.value, 0),
46
+ });
47
+ }}
41
48
  onBlur={onBlur}
42
49
  />
43
50
  ))}
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './ArrayField';
6
+ export * from './BooleanField';
7
+ export * from './GeoPointField';
8
+ export * from './MarkdownField';
9
+ export * from './NumberField';
10
+ export * from './RefField';
11
+ export * from './SelectField';
12
+ export * from './SelectOptionField';
13
+ export * from './TextField';
14
+ export * from './TextAreaField';
15
+ export * from './TupleField';
@@ -2,8 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './custom';
6
- export * from './Defaults';
5
+ export * from './fields';
6
+
7
7
  export * from './Form';
8
- export * from './FormContext';
9
- export * from './Input';
8
+ export * from './FormFieldComponent';
@@ -10,10 +10,11 @@ import { expect, fn, userEvent, within } from 'storybook/test';
10
10
  import { Obj } from '@dxos/echo';
11
11
  import { faker } from '@dxos/random';
12
12
  import { Filter, useQuery } from '@dxos/react-client/echo';
13
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
13
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
14
14
  import { Button } from '@dxos/react-ui';
15
15
  import { withTheme } from '@dxos/react-ui/testing';
16
16
  import { Person } from '@dxos/types';
17
+ import { osTranslations } from '@dxos/ui-theme';
17
18
 
18
19
  import { translations } from '../../translations';
19
20
 
@@ -34,7 +35,7 @@ const mockHandleSelect = fn();
34
35
  const mockHandleCreate = fn();
35
36
 
36
37
  const DefaultStory = () => {
37
- const { space } = useClientProvider();
38
+ const { space } = useClientStory();
38
39
  const [isOpen, setIsOpen] = useState(false);
39
40
 
40
41
  // Create sample Person objects
@@ -48,7 +49,7 @@ const DefaultStory = () => {
48
49
  }, [space]);
49
50
 
50
51
  // Get all objects in the space (similar to BoardContainer)
51
- const allObjects = useQuery(space, Filter.everything());
52
+ const allObjects = useQuery(space?.db, Filter.everything());
52
53
 
53
54
  // Map objects to options format expected by ObjectPicker
54
55
  const options = useMemo(
@@ -73,22 +74,22 @@ const DefaultStory = () => {
73
74
  );
74
75
 
75
76
  return (
76
- <div className='p-4'>
77
+ <div role='none' className='flex w-96'>
77
78
  <ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
78
79
  <ObjectPicker.Trigger asChild>
79
- <Button variant='primary' data-testid='trigger'>
80
+ <Button variant='primary' data-testid='trigger' classNames='w-full'>
80
81
  Select Person
81
82
  </Button>
82
83
  </ObjectPicker.Trigger>
83
84
  <ObjectPicker.Content
85
+ classNames='dx-card-popover-width'
84
86
  options={options}
85
- onSelect={mockHandleSelect}
86
87
  createSchema={personSchema}
87
- createOptionLabel={['create new person label', { ns: 'os' }]}
88
+ createOptionLabel={['create new person label', { ns: osTranslations }]}
88
89
  createOptionIcon='ph--user-plus--regular'
89
90
  createInitialValuePath='fullName'
90
91
  onCreate={handleCreateCallback}
91
- classNames='popover-card-width'
92
+ onSelect={mockHandleSelect}
92
93
  />
93
94
  </ObjectPicker.Root>
94
95
  </div>
@@ -99,7 +100,7 @@ const meta = {
99
100
  title: 'ui/react-ui-form/ObjectPicker',
100
101
  render: DefaultStory,
101
102
  decorators: [
102
- withTheme,
103
+ withTheme(),
103
104
  withClientProvider({
104
105
  types: [Person.Person],
105
106
  createIdentity: true,
@@ -200,8 +201,10 @@ export const WithTest: Story = {
200
201
  await userEvent.click(saveButton);
201
202
 
202
203
  // Check that clicking the save button calls onCreate with expected values.
203
- await expect(mockHandleCreate).toHaveBeenCalledWith({
204
- fullName: unrelatedTerm,
205
- });
204
+ await expect(mockHandleCreate).toHaveBeenCalledWith(
205
+ expect.objectContaining({
206
+ fullName: unrelatedTerm,
207
+ }),
208
+ );
206
209
  },
207
210
  };
@@ -3,26 +3,57 @@
3
3
  //
4
4
 
5
5
  import type * as Schema from 'effect/Schema';
6
- import React, { type KeyboardEvent, type MouseEvent, forwardRef, useCallback, useMemo, useState } from 'react';
6
+ import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
7
 
8
- import { Icon, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
- import { Combobox } from '@dxos/react-ui-searchlist';
8
+ import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
+ import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-searchlist';
10
10
 
11
- import { type QueryTag } from '../../hooks';
12
11
  import { translationKey } from '../../translations';
13
12
  import { Form } from '../Form';
14
13
 
14
+ export type RefOption = {
15
+ id: string;
16
+ label: string;
17
+ hue?: Palette;
18
+ };
19
+
15
20
  export type ObjectPickerContentProps = ThemedClassName<{
16
- options: QueryTag[];
21
+ options: RefOption[];
17
22
  selectedIds?: string[];
18
- createSchema?: Schema.Schema.AnyNoContext;
19
23
  createOptionLabel?: [string, { ns: string }];
20
24
  createOptionIcon?: string;
25
+ createSchema?: Schema.Schema.AnyNoContext;
21
26
  createInitialValuePath?: string;
22
- onSelect: (id: string) => void;
23
27
  onCreate?: (values: any) => void;
28
+ onSelect: (id: string) => void;
24
29
  }>;
25
30
 
31
+ const CreateItem = ({
32
+ createOptionLabel,
33
+ createOptionIcon,
34
+ onCreateItemSelect,
35
+ }: {
36
+ createOptionLabel: [string, { ns: string }];
37
+ createOptionIcon: string;
38
+ onCreateItemSelect: (query: string) => void;
39
+ }) => {
40
+ const { t } = useTranslation(translationKey);
41
+ const { query } = useSearchListInput();
42
+
43
+ return (
44
+ <Combobox.Item
45
+ value='__create__'
46
+ label={t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })}
47
+ icon={createOptionIcon}
48
+ classNames='flex items-center gap-2'
49
+ closeOnSelect={false}
50
+ onSelect={() => {
51
+ onCreateItemSelect(query);
52
+ }}
53
+ />
54
+ );
55
+ };
56
+
26
57
  const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
27
58
  (
28
59
  {
@@ -39,39 +70,27 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
39
70
  ref,
40
71
  ) => {
41
72
  const { t } = useTranslation(translationKey);
42
-
43
- const [searchString, setSearchString] = useState('');
44
73
  const [showForm, setShowForm] = useState(false);
74
+ const [formInitialValue, setFormInitialValue] = useState<string>('');
75
+
76
+ const { results, handleSearch } = useSearchListResults({
77
+ items: options,
78
+ });
45
79
 
46
80
  const handleFormSave = useCallback(
47
81
  (values: any) => {
48
82
  onCreate?.(values);
49
83
  setShowForm(false);
50
- setSearchString('');
84
+ setFormInitialValue('');
51
85
  },
52
86
  [onCreate],
53
87
  );
54
88
 
55
89
  const handleFormCancel = useCallback(() => {
56
90
  setShowForm(false);
57
- setSearchString('');
91
+ setFormInitialValue('');
58
92
  }, []);
59
93
 
60
- // TODO(thure): The following click and keydown handlers are necessary because `onSelect` is called after the
61
- // Popover is already closed. Augment/refactor CmdK, if possible, to facilitate stopping event default
62
- // and propagation.
63
-
64
- const handleClick = useCallback(
65
- (event: MouseEvent) => {
66
- if (createSchema && (event.target as HTMLElement).closest('[data-value="__create__"]')) {
67
- event.stopPropagation();
68
- event.preventDefault();
69
- setShowForm(true);
70
- }
71
- },
72
- [createSchema],
73
- );
74
-
75
94
  const handleKeyDown = useCallback(
76
95
  (event: KeyboardEvent) => {
77
96
  if (
@@ -83,64 +102,58 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
83
102
  ) {
84
103
  event.stopPropagation();
85
104
  event.preventDefault();
105
+ // Get the current query from the input element
106
+ const input = (event.currentTarget as HTMLElement).querySelector('input[type="text"]') as HTMLInputElement;
107
+ const currentQuery = input?.value || '';
108
+ setFormInitialValue(currentQuery);
86
109
  setShowForm(true);
87
110
  }
88
111
  },
89
112
  [createSchema],
90
113
  );
91
114
 
92
- const labelById = useMemo(
93
- () =>
94
- options.reduce((acc: Record<string, string>, option) => {
95
- acc[option.id.toLowerCase()] = option.label.toLowerCase();
96
- return acc;
97
- }, {}),
98
- [options],
99
- );
100
-
101
115
  return (
102
- <Combobox.Content
103
- {...props}
104
- ref={ref}
105
- filter={(value, search) => (value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0)}
106
- onClickCapture={handleClick}
107
- onKeyDownCapture={handleKeyDown}
108
- >
116
+ <Combobox.Content {...props} ref={ref} onSearch={handleSearch} onKeyDownCapture={handleKeyDown}>
109
117
  {showForm && createSchema ? (
110
118
  <Popover.Viewport>
111
- <Form
119
+ <Form.Root
120
+ testId='create-referenced-object-form'
112
121
  schema={createSchema}
113
- values={createInitialValuePath ? { [createInitialValuePath]: searchString } : {}}
122
+ values={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
114
123
  onSave={handleFormSave}
115
124
  onCancel={handleFormCancel}
116
- testId='create-referenced-object-form'
117
- />
125
+ >
126
+ <Form.Viewport>
127
+ <Form.Content>
128
+ <Form.FieldSet />
129
+ <Form.Actions />
130
+ </Form.Content>
131
+ </Form.Viewport>
132
+ </Form.Root>
118
133
  </Popover.Viewport>
119
134
  ) : (
120
135
  <>
121
- <Combobox.Input
122
- placeholder={t('ref field combobox input placeholder')}
123
- value={searchString}
124
- onValueChange={setSearchString}
125
- autoFocus
126
- />
136
+ <Combobox.Input placeholder={t('ref field combobox input placeholder')} autoFocus />
127
137
  <Combobox.List>
128
- {options.map((option) => (
138
+ {results.map((option) => (
129
139
  <Combobox.Item
130
140
  key={option.id}
131
141
  value={option.id}
142
+ label={option.label}
143
+ checked={selectedIds?.includes(option.id)}
132
144
  onSelect={() => onSelect(option.id)}
133
145
  classNames='flex items-center gap-2'
134
- >
135
- <span className='grow'>{option.label}</span>
136
- {selectedIds?.includes(option.id) && <Icon icon='ph--check--regular' />}
137
- </Combobox.Item>
146
+ />
138
147
  ))}
139
- {searchString.length > 0 && createOptionLabel && createOptionIcon && createSchema && onCreate && (
140
- <Combobox.Item value='__create__' classNames='flex items-center gap-2'>
141
- <Icon icon={createOptionIcon} />
142
- {t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: searchString })}
143
- </Combobox.Item>
148
+ {createOptionLabel && createOptionIcon && createSchema && onCreate && (
149
+ <CreateItem
150
+ createOptionLabel={createOptionLabel}
151
+ createOptionIcon={createOptionIcon}
152
+ onCreateItemSelect={(query: string) => {
153
+ setFormInitialValue(query);
154
+ setShowForm(true);
155
+ }}
156
+ />
144
157
  )}
145
158
  </Combobox.List>
146
159
  </>
@@ -155,6 +168,7 @@ ObjectPickerContent.displayName = 'ObjectPickerContent';
155
168
 
156
169
  export const ObjectPicker = {
157
170
  Root: Combobox.Root,
171
+ Portal: Combobox.Portal,
158
172
  Trigger: Combobox.Trigger,
159
173
  VirtualTrigger: Combobox.VirtualTrigger,
160
174
  Content: ObjectPickerContent,