@dxos/react-ui-form 0.8.4-main.b97322e → 0.8.4-main.c4373fc

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 (117) hide show
  1. package/dist/lib/browser/index.mjs +810 -594
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +810 -594
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/ControlSection/ControlSection.d.ts +2 -2
  8. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  9. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +81 -5
  10. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +76 -5
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/ArrayField.d.ts +2 -2
  16. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Defaults.d.ts +5 -5
  18. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.d.ts +7 -6
  20. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/Form.stories.d.ts +129 -32
  22. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormContent.d.ts +17 -11
  25. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormContext.d.ts +3 -3
  27. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/Input.d.ts +5 -2
  29. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/RefField.d.ts +14 -5
  31. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -1
  33. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +1 -1
  35. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +76 -4
  37. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
  39. package/dist/types/src/components/Form/factory.d.ts +1 -1
  40. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  41. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +41 -0
  42. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  43. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +81 -0
  44. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  45. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  46. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  47. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +16 -4
  48. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  49. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +79 -7
  50. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/index.d.ts +1 -1
  52. package/dist/types/src/components/index.d.ts.map +1 -1
  53. package/dist/types/src/components/testing.d.ts +6 -3
  54. package/dist/types/src/components/testing.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useForm.d.ts +3 -3
  56. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useQueryRefOptions.d.ts +8 -3
  58. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
  59. package/dist/types/src/translations.d.ts +11 -5
  60. package/dist/types/src/translations.d.ts.map +1 -1
  61. package/dist/types/src/util/field.d.ts +1 -1
  62. package/dist/types/src/util/field.d.ts.map +1 -1
  63. package/dist/types/src/util/formatting.d.ts +1 -1
  64. package/dist/types/src/util/formatting.d.ts.map +1 -1
  65. package/dist/types/src/util/refs.d.ts +1 -1
  66. package/dist/types/src/util/refs.d.ts.map +1 -1
  67. package/dist/types/src/util/schema.d.ts +1 -1
  68. package/dist/types/src/util/schema.d.ts.map +1 -1
  69. package/dist/types/tsconfig.tsbuildinfo +1 -1
  70. package/package.json +41 -36
  71. package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
  72. package/src/components/ControlSection/ControlSection.tsx +26 -12
  73. package/src/components/FieldEditor/FieldEditor.stories.tsx +14 -14
  74. package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
  75. package/src/components/FieldEditor/FieldEditor.tsx +26 -18
  76. package/src/components/Form/ArrayField.tsx +12 -11
  77. package/src/components/Form/Defaults.tsx +22 -20
  78. package/src/components/Form/Form.stories.tsx +135 -99
  79. package/src/components/Form/Form.tsx +26 -15
  80. package/src/components/Form/FormActions.tsx +2 -1
  81. package/src/components/Form/FormContent.tsx +121 -39
  82. package/src/components/Form/FormContext.tsx +8 -8
  83. package/src/components/Form/Input.tsx +5 -2
  84. package/src/components/Form/RefField.tsx +97 -41
  85. package/src/components/Form/custom/GeoPointInput.tsx +29 -21
  86. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +11 -11
  87. package/src/components/Form/custom/SelectOptionsInput.tsx +30 -31
  88. package/src/components/Form/custom/TupleInput.tsx +2 -2
  89. package/src/components/Form/factory.tsx +2 -2
  90. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +207 -0
  91. package/src/components/ObjectPicker/ObjectPicker.tsx +161 -0
  92. package/src/components/ObjectPicker/index.ts +5 -0
  93. package/src/components/ViewEditor/ViewEditor.stories.tsx +102 -44
  94. package/src/components/ViewEditor/ViewEditor.test.tsx +9 -12
  95. package/src/components/ViewEditor/ViewEditor.tsx +328 -220
  96. package/src/components/index.ts +1 -2
  97. package/src/components/testing.tsx +15 -9
  98. package/src/hooks/useForm.ts +8 -4
  99. package/src/hooks/useQueryRefOptions.ts +9 -3
  100. package/src/hooks/useRefQueryLookupHandler.ts +1 -1
  101. package/src/translations.ts +13 -6
  102. package/src/util/field.ts +9 -19
  103. package/src/util/formatting.ts +1 -1
  104. package/src/util/refs.ts +2 -2
  105. package/src/util/schema.ts +2 -4
  106. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  107. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  108. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  109. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  110. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  111. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  112. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  113. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  114. package/src/components/deprecated/Form.stories.tsx +0 -62
  115. package/src/components/deprecated/Form.tsx +0 -119
  116. package/src/components/deprecated/FormInput.tsx +0 -71
  117. package/src/components/deprecated/index.ts +0 -5
@@ -2,50 +2,67 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema, SchemaAST, pipe } from 'effect';
6
- import { capitalize } from 'effect/String';
5
+ import * as Function from 'effect/Function';
6
+ import * as Schema from 'effect/Schema';
7
+ import * as SchemaAST from 'effect/SchemaAST';
8
+ import * as StringEffect from 'effect/String';
7
9
  import React, { forwardRef, useMemo } from 'react';
8
10
 
9
11
  import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
10
12
  import { type ThemedClassName } from '@dxos/react-ui';
11
13
  import { mx } from '@dxos/react-ui-theme';
12
- import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
13
- import { isNotFalsy } from '@dxos/util';
14
+ import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
15
+ import { isTruthy } from '@dxos/util';
16
+
17
+ import { type QueryRefOptions } from '../../hooks';
18
+ import { getRefProps } from '../../util';
14
19
 
15
20
  import { ArrayField } from './ArrayField';
16
21
  import { SelectInput } from './Defaults';
22
+ import { getInputComponent } from './factory';
17
23
  import { type ComponentLookup } from './Form';
18
- import { useInputProps, useFormValues } from './FormContext';
24
+ import { useFormValues, useInputProps } from './FormContext';
19
25
  import { type InputComponent } from './Input';
20
- import { RefField } from './RefField';
21
- import { getInputComponent } from './factory';
22
- import { type QueryRefOptions } from '../../hooks';
23
- import { getRefProps } from '../../util';
26
+ import { RefField, type RefFieldProps } from './RefField';
24
27
 
25
28
  export type FormFieldProps = {
26
29
  property: SchemaProperty<any>;
27
30
  path?: (string | number)[];
28
- readonly?: boolean;
29
31
  /** Used to indicate if input should be presented inline (e.g. for array items). */
30
32
  inline?: boolean;
31
- onQueryRefOptions?: QueryRefOptions;
33
+ projection?: ProjectionModel;
32
34
  lookupComponent?: ComponentLookup;
33
35
  Custom?: Partial<Record<string, InputComponent>>;
34
- };
36
+ } & Pick<
37
+ RefFieldProps,
38
+ | 'readonly'
39
+ | 'onQueryRefOptions'
40
+ | 'createOptionLabel'
41
+ | 'createOptionIcon'
42
+ | 'onCreate'
43
+ | 'createSchema'
44
+ | 'createInitialValuePath'
45
+ >;
35
46
 
36
47
  export const FormField = ({
37
48
  property,
38
49
  path,
39
50
  readonly,
40
51
  inline,
52
+ projection,
41
53
  onQueryRefOptions,
54
+ createOptionLabel,
55
+ createOptionIcon,
56
+ onCreate,
57
+ createSchema,
58
+ createInitialValuePath,
42
59
  lookupComponent,
43
60
  Custom,
44
61
  }: FormFieldProps) => {
45
62
  const { ast, name, type, format, title, description, options, examples, array } = property;
46
63
  const inputProps = useInputProps(path);
47
64
 
48
- const label = useMemo(() => title ?? pipe(name, capitalize), [title, name]);
65
+ const label = useMemo(() => title ?? Function.pipe(name, StringEffect.capitalize), [title, name]);
49
66
  const placeholder = useMemo(
50
67
  () => (examples?.length ? `Example: "${examples[0]}"` : description),
51
68
  [examples, description],
@@ -62,7 +79,7 @@ export const FormField = ({
62
79
  type,
63
80
  format,
64
81
  label,
65
- disabled: readonly,
82
+ readonly,
66
83
  placeholder,
67
84
  ...inputProps,
68
85
  },
@@ -82,7 +99,7 @@ export const FormField = ({
82
99
  label={label}
83
100
  inputOnly={inline}
84
101
  placeholder={placeholder}
85
- disabled={readonly}
102
+ readonly={readonly}
86
103
  {...inputProps}
87
104
  />
88
105
  );
@@ -102,9 +119,14 @@ export const FormField = ({
102
119
  format={format}
103
120
  label={label}
104
121
  placeholder={placeholder}
105
- disabled={readonly}
122
+ readonly={readonly}
106
123
  inputOnly={inline}
107
124
  onQueryRefOptions={onQueryRefOptions}
125
+ createOptionLabel={createOptionLabel}
126
+ createOptionIcon={createOptionIcon}
127
+ onCreate={onCreate}
128
+ createSchema={createSchema}
129
+ createInitialValuePath={createInitialValuePath}
108
130
  {...inputProps}
109
131
  />
110
132
  );
@@ -131,7 +153,7 @@ export const FormField = ({
131
153
  label={label}
132
154
  inputOnly={inline}
133
155
  placeholder={placeholder}
134
- disabled={readonly}
156
+ readonly={readonly}
135
157
  {...inputProps}
136
158
  />
137
159
  );
@@ -146,7 +168,7 @@ export const FormField = ({
146
168
  <SelectInput
147
169
  type={type}
148
170
  format={format}
149
- disabled={readonly}
171
+ readonly={readonly}
150
172
  inputOnly={inline}
151
173
  label={label}
152
174
  options={options.map((option) => ({ value: option, label: String(option) }))}
@@ -174,7 +196,11 @@ export const FormField = ({
174
196
  schema={Schema.make(typeLiteral)}
175
197
  path={path}
176
198
  readonly={readonly}
199
+ projection={projection}
177
200
  onQueryRefOptions={onQueryRefOptions}
201
+ createOptionLabel={createOptionLabel}
202
+ createOptionIcon={createOptionIcon}
203
+ onCreate={onCreate}
178
204
  Custom={Custom}
179
205
  lookupComponent={lookupComponent}
180
206
  />
@@ -186,36 +212,90 @@ export const FormField = ({
186
212
  return null;
187
213
  };
188
214
 
189
- export type FormFieldsProps = ThemedClassName<{
190
- testId?: string;
191
- readonly?: boolean;
192
- schema: Schema.Schema.All;
193
- /**
194
- * Path to the current object from the root. Used with nested forms.
195
- */
196
- path?: (string | number)[];
197
- filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
198
- sort?: string[];
199
- lookupComponent?: ComponentLookup;
200
- /**
201
- * Map of custom renderers for specific properties.
202
- * Prefer lookupComponent for plugin specific input surfaces.
203
- */
204
- Custom?: Partial<Record<string, InputComponent>>;
205
- onQueryRefOptions?: QueryRefOptions;
206
- }>;
215
+ export type FormFieldsProps = ThemedClassName<
216
+ {
217
+ testId?: string;
218
+ schema: Schema.Schema.All;
219
+ /**
220
+ * Path to the current object from the root. Used with nested forms.
221
+ */
222
+ path?: (string | number)[];
223
+ filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
224
+ sort?: string[];
225
+ /**
226
+ * Optional projection for projection-based field management
227
+ */
228
+ projection?: ProjectionModel;
229
+ lookupComponent?: ComponentLookup;
230
+ /**
231
+ * Map of custom renderers for specific properties.
232
+ * Prefer lookupComponent for plugin specific input surfaces.
233
+ */
234
+ Custom?: Partial<Record<string, InputComponent>>;
235
+ onQueryRefOptions?: QueryRefOptions;
236
+ } & Pick<FormFieldProps, 'readonly'> &
237
+ Pick<
238
+ RefFieldProps,
239
+ | 'onQueryRefOptions'
240
+ | 'createOptionLabel'
241
+ | 'createOptionIcon'
242
+ | 'onCreate'
243
+ | 'createSchema'
244
+ | 'createInitialValuePath'
245
+ >
246
+ >;
207
247
 
208
248
  export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
209
249
  (
210
- { classNames, testId, schema, path, filter, sort, readonly, lookupComponent, Custom, onQueryRefOptions },
250
+ {
251
+ classNames,
252
+ schema,
253
+ path,
254
+ filter,
255
+ sort,
256
+ projection,
257
+ readonly,
258
+ lookupComponent,
259
+ Custom,
260
+ onQueryRefOptions,
261
+ ...props
262
+ },
211
263
  forwardRef,
212
264
  ) => {
213
265
  const values = useFormValues(path);
214
266
  const properties = useMemo(() => {
215
267
  const props = getSchemaProperties(schema.ast, values);
268
+
269
+ // Use projection-based field management when view and projection are available
270
+ if (projection) {
271
+ const fieldProjections = projection.getFieldProjections();
272
+ const hiddenProperties = new Set(projection.getHiddenProperties());
273
+
274
+ // Filter properties to only include visible ones and order by projection
275
+ const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name));
276
+ const orderedProps: SchemaProperty<any>[] = [];
277
+
278
+ // Add properties in projection field order
279
+ for (const fieldProjection of fieldProjections) {
280
+ const fieldPath = String(fieldProjection.field.path);
281
+ const prop = visibleProps.find((p) => p.name === fieldPath);
282
+ if (prop) {
283
+ orderedProps.push(prop);
284
+ }
285
+ }
286
+
287
+ // Add any remaining properties not in projection
288
+ const projectionPaths = new Set(fieldProjections.map((fp) => String(fp.field.path)));
289
+ const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name));
290
+ orderedProps.push(...remainingProps);
291
+
292
+ return orderedProps;
293
+ }
294
+
295
+ // Fallback to legacy filter/sort behavior
216
296
  const filtered = filter ? filter(props) : props;
217
297
  return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
218
- }, [schema, values, filter, sort]);
298
+ }, [schema, values, filter, sort, projection?.fields]);
219
299
 
220
300
  return (
221
301
  <div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
@@ -227,13 +307,15 @@ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
227
307
  property={property}
228
308
  path={[...(path ?? []), property.name]}
229
309
  readonly={readonly}
310
+ projection={projection}
230
311
  onQueryRefOptions={onQueryRefOptions}
231
312
  lookupComponent={lookupComponent}
232
313
  Custom={Custom}
314
+ {...props}
233
315
  />
234
316
  );
235
317
  })
236
- .filter(isNotFalsy)}
318
+ .filter(isTruthy)}
237
319
  </div>
238
320
  );
239
321
  },
@@ -3,19 +3,19 @@
3
3
  //
4
4
 
5
5
  import React, {
6
- createContext,
6
+ type FocusEvent,
7
+ type PropsWithChildren,
7
8
  type RefObject,
9
+ createContext,
10
+ useCallback,
8
11
  useContext,
9
12
  useEffect,
10
13
  useMemo,
11
- type FocusEvent,
12
- type PropsWithChildren,
13
- useCallback,
14
14
  } from 'react';
15
15
 
16
16
  import { raise } from '@dxos/debug';
17
- import { type BaseObject, getValue } from '@dxos/echo-schema';
18
- import { createJsonPath, type SimpleType } from '@dxos/effect';
17
+ import { type BaseObject, getValue } from '@dxos/echo/internal';
18
+ import { type SimpleType, createJsonPath } from '@dxos/effect';
19
19
 
20
20
  import { type FormHandler, type FormOptions, useForm } from '../../hooks';
21
21
 
@@ -62,7 +62,7 @@ export const FormProvider = ({
62
62
  ...formOptions
63
63
  }: PropsWithChildren<
64
64
  FormOptions<any> & {
65
- formRef?: RefObject<HTMLDivElement>;
65
+ formRef?: RefObject<HTMLDivElement | null>;
66
66
  autoSave?: boolean;
67
67
  }
68
68
  >) => {
@@ -92,7 +92,7 @@ export const FormProvider = ({
92
92
  }
93
93
  }
94
94
  },
95
- [form, autoSave],
95
+ [form.canSave, form.formIsValid, form.handleSave, autoSave],
96
96
  );
97
97
 
98
98
  useEffect(() => {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React, { type FC } from 'react';
6
6
 
7
- import { type FormatEnum } from '@dxos/echo-schema';
7
+ import { type FormatEnum } from '@dxos/echo/internal';
8
8
  import { type SimpleType } from '@dxos/effect';
9
9
  import { Icon, Input, Tooltip } from '@dxos/react-ui';
10
10
  import { labelSpacing } from '@dxos/react-ui-stack';
@@ -19,9 +19,12 @@ export type InputProps = {
19
19
  type: SimpleType;
20
20
  format?: FormatEnum;
21
21
  label: string;
22
- disabled?: boolean;
23
22
  placeholder?: string;
24
23
  inputOnly?: boolean;
24
+ /**
25
+ * Specifies the readonly variant: either disabled inputs, elements indicating they are usually editable but currently are not; or `static`, a field’s representation as regular content without signifiers that it is ever editable.
26
+ */
27
+ readonly?: 'disabled-input' | 'static' | false;
25
28
  } & FormInputStateProps;
26
29
 
27
30
  /**
@@ -2,45 +2,65 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type SchemaAST } from 'effect';
6
- import React, { useCallback, useMemo, type FocusEvent } from 'react';
5
+ import type * as Schema from 'effect/Schema';
6
+ import type * as SchemaAST from 'effect/SchemaAST';
7
+ import React, { useCallback, useMemo } from 'react';
7
8
 
8
9
  import {
10
+ type EchoSchema,
9
11
  Expando,
10
- getTypeAnnotation,
11
12
  Ref,
12
13
  ReferenceAnnotationId,
13
14
  type ReferenceAnnotationValue,
14
- } from '@dxos/echo-schema';
15
+ getTypeAnnotation,
16
+ } from '@dxos/echo/internal';
15
17
  import { findAnnotation } from '@dxos/effect';
16
18
  import { DXN } from '@dxos/keys';
17
- import { DxRefTag } from '@dxos/lit-ui/react';
18
- import { Input, useTranslation } from '@dxos/react-ui';
19
- import { TagPicker, type TagPickerMode, type TagPickerItemData } from '@dxos/react-ui-tag-picker';
19
+ import { type DxTagPickerItemClick } from '@dxos/lit-ui';
20
+ import { DxAnchor, DxTagPickerItem } from '@dxos/lit-ui/react';
21
+ import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
20
22
  import { descriptionText, mx } from '@dxos/react-ui-theme';
21
23
  import { isNonNullable } from '@dxos/util';
22
24
 
23
- import { TextInput } from './Defaults';
24
- import { InputHeader, type InputProps } from './Input';
25
25
  import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
26
26
  import { translationKey } from '../../translations';
27
+ import { ObjectPicker } from '../ObjectPicker';
27
28
 
28
- type RefFieldProps = InputProps & {
29
+ import { TextInput } from './Defaults';
30
+ import { InputHeader, type InputProps } from './Input';
31
+
32
+ export type RefFieldProps = InputProps & {
29
33
  ast?: SchemaAST.AST;
30
34
  array?: boolean;
35
+ createOptionLabel?: [string, { ns: string }];
36
+ createOptionIcon?: string;
37
+ onCreate?: (values: any) => void;
38
+ createSchema?: Schema.Schema.AnyNoContext;
39
+ createInitialValuePath?: string;
31
40
  onQueryRefOptions?: QueryRefOptions;
41
+ schema?: EchoSchema;
42
+ };
43
+
44
+ // TODO(thure): Is this a standard that should be better canonized?
45
+ const isRefSnapShot = (val: any): val is { '/': string } => {
46
+ return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
32
47
  };
33
48
 
34
49
  export const RefField = ({
35
50
  type,
36
51
  label,
37
- disabled,
52
+ readonly,
38
53
  placeholder,
39
54
  inputOnly,
40
55
  array,
41
56
  ast,
42
57
  getValue,
58
+ createOptionLabel,
59
+ createOptionIcon,
43
60
  onBlur,
61
+ onCreate,
62
+ createSchema,
63
+ createInitialValuePath,
44
64
  onQueryRefOptions,
45
65
  onValueChange,
46
66
  ...restInputProps
@@ -56,17 +76,18 @@ export const RefField = ({
56
76
  // If ref type is expando, fall back to taking a DXN in string format.
57
77
  return (
58
78
  <RefFieldFallback
59
- {...{ type, label, placeholder, disabled, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
79
+ {...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
60
80
  />
61
81
  );
62
82
  }
63
83
 
64
- const handleGetValue = useCallback((): TagPickerItemData[] => {
84
+ const handleGetValue = useCallback(() => {
65
85
  const formValue = getValue();
66
86
 
67
87
  const unknownToRefOption = (val: unknown) => {
68
- if (Ref.isRef(val)) {
69
- const dxnString = val.dxn.toString();
88
+ const isRef = Ref.isRef(val);
89
+ if (isRef || isRefSnapShot(val)) {
90
+ const dxnString = isRef ? val.dxn.toString() : val['/'];
70
91
  const matchingOption = availableOptions.find((option) => option.id === dxnString);
71
92
  if (matchingOption) {
72
93
  return matchingOption;
@@ -87,15 +108,6 @@ export const RefField = ({
87
108
  return [];
88
109
  }, [getValue, availableOptions, array]);
89
110
 
90
- const handleSearch = useCallback(
91
- (text: string, ids: string[]): TagPickerItemData[] =>
92
- availableOptions
93
- .filter((option) => !ids.includes(option.id))
94
- .filter((option) => option.label.toLowerCase().includes(text.toLowerCase()))
95
- .map((option) => ({ id: option.id, label: option.label, hue: option.hue as any })),
96
- [availableOptions],
97
- );
98
-
99
111
  const handleUpdate = useCallback(
100
112
  (ids: string[]) => {
101
113
  if (ids.length === 0) {
@@ -123,8 +135,6 @@ export const RefField = ({
123
135
  [availableOptions, array, onValueChange],
124
136
  );
125
137
 
126
- const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
127
-
128
138
  if (!refTypeInfo) {
129
139
  return null;
130
140
  }
@@ -132,32 +142,78 @@ export const RefField = ({
132
142
  const { status, error } = restInputProps.getStatus();
133
143
 
134
144
  const items = handleGetValue();
145
+ const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
146
+ const toggleSelect = useCallback(
147
+ (id: string) => {
148
+ if (array) {
149
+ const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
150
+ handleUpdate(nextIds);
151
+ } else {
152
+ if (selectedIds[0] === id) {
153
+ handleUpdate([]);
154
+ } else {
155
+ handleUpdate([id]);
156
+ }
157
+ }
158
+ },
159
+ [array, selectedIds, handleUpdate],
160
+ );
135
161
 
136
162
  // NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
137
- return disabled && items.length < 1 ? null : (
163
+ return readonly && items.length < 1 ? null : (
138
164
  <Input.Root validationValence={status}>
139
165
  {!inputOnly && <InputHeader error={error} label={label} />}
140
166
  <div data-no-submit>
141
- {disabled ? (
167
+ {readonly ? (
142
168
  items.length < 1 ? (
143
169
  <p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
144
170
  ) : (
145
171
  items.map((item) => (
146
- <DxRefTag key={item.id} refid={item.id} rootclassname='mie-1' style={{ marginInlineEnd: '.25rem' }}>
172
+ <DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
147
173
  {item.label}
148
- </DxRefTag>
174
+ </DxAnchor>
149
175
  ))
150
176
  )
151
177
  ) : (
152
- <TagPicker
153
- readonly={disabled}
154
- items={items}
155
- mode={tagPickerMode}
156
- onBlur={(event) => onBlur(event as unknown as FocusEvent<HTMLElement>)}
157
- onUpdate={handleUpdate}
158
- onSearch={handleSearch}
159
- classNames='bg-inputSurface p-1.5 rounded-sm'
160
- />
178
+ <ObjectPicker.Root>
179
+ <ObjectPicker.Trigger asChild>
180
+ <Button variant='ghost' classNames='is-full text-start gap-2'>
181
+ <div role='none' className='grow'>
182
+ {items?.length ? (
183
+ items?.map((item) => (
184
+ <DxTagPickerItem
185
+ key={item.id}
186
+ itemId={item.id}
187
+ label={item.label}
188
+ {...(item.hue ? { hue: item.hue } : {})}
189
+ removeLabel={t('remove item label')}
190
+ onItemClick={(event: DxTagPickerItemClick) => {
191
+ if (event.action === 'remove') {
192
+ toggleSelect(item.id);
193
+ }
194
+ }}
195
+ />
196
+ ))
197
+ ) : (
198
+ <span className='text-description'>
199
+ {placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
200
+ </span>
201
+ )}
202
+ </div>
203
+ <Icon size={3} icon='ph--caret-down--bold' />
204
+ </Button>
205
+ </ObjectPicker.Trigger>
206
+ <ObjectPicker.Content
207
+ options={availableOptions}
208
+ selectedIds={selectedIds}
209
+ onSelect={toggleSelect}
210
+ createSchema={createSchema}
211
+ createOptionLabel={createOptionLabel}
212
+ createOptionIcon={createOptionIcon}
213
+ createInitialValuePath={createInitialValuePath}
214
+ onCreate={onCreate}
215
+ />
216
+ </ObjectPicker.Root>
161
217
  )}
162
218
  </div>
163
219
  {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
@@ -168,7 +224,7 @@ export const RefField = ({
168
224
  const RefFieldFallback = ({
169
225
  type,
170
226
  label,
171
- disabled,
227
+ readonly,
172
228
  placeholder,
173
229
  inputOnly,
174
230
  getValue,
@@ -202,7 +258,7 @@ const RefFieldFallback = ({
202
258
  <TextInput
203
259
  type={type}
204
260
  label={label}
205
- disabled={disabled}
261
+ readonly={readonly}
206
262
  placeholder={placeholder}
207
263
  inputOnly={inputOnly}
208
264
  getValue={handleGetValue as <V>() => V | undefined}
@@ -2,16 +2,16 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useState, useEffect, useMemo, type ChangeEvent } from 'react';
5
+ import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
- import { type GeoPoint, GeoLocation } from '@dxos/echo-schema';
7
+ import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
8
8
  import { Input, useTranslation } from '@dxos/react-ui';
9
9
  import { safeParseFloat } from '@dxos/util';
10
10
 
11
11
  import { translationKey } from '../../../translations';
12
12
  import { InputHeader, type InputProps } from '../Input';
13
13
 
14
- export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
14
+ export const GeoPointInput = ({ type, label, readonly, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
15
15
  const { t } = useTranslation(translationKey);
16
16
  const { status, error } = getStatus();
17
17
  const geoPoint = useMemo<GeoPoint>(() => getValue<GeoPoint>() ?? [0, 0], [getValue]);
@@ -48,24 +48,32 @@ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onVa
48
48
  <Input.Root validationValence={status}>
49
49
  <InputHeader error={error} label={label} />
50
50
  <div role='none' className='grid grid-cols-2 gap-2'>
51
- <Input.TextInput
52
- type='text'
53
- pattern='^-?[0-9]*\.?[0-9]*$'
54
- disabled={disabled}
55
- placeholder={t('placeholder latitude')}
56
- value={latitudeText}
57
- onChange={handleCoordinateChange('latitude', setLatitudeText)}
58
- onBlur={onBlur}
59
- />
60
- <Input.TextInput
61
- type='text'
62
- pattern='^-?[0-9]*\.?[0-9]*$'
63
- disabled={disabled}
64
- placeholder={t('placeholder longitude')}
65
- value={longitudeText}
66
- onChange={handleCoordinateChange('longitude', setLongitudeText)}
67
- onBlur={onBlur}
68
- />
51
+ <div role='none'>
52
+ <Input.Label>{t('latitude label')}</Input.Label>
53
+ <Input.TextInput
54
+ type='number'
55
+ step='0.00001'
56
+ min='-90'
57
+ max='90'
58
+ disabled={!!readonly}
59
+ value={latitudeText}
60
+ onChange={handleCoordinateChange('latitude', setLatitudeText)}
61
+ onBlur={onBlur}
62
+ />
63
+ </div>
64
+ <div role='none'>
65
+ <Input.Label>{t('longitude label')}</Input.Label>
66
+ <Input.TextInput
67
+ type='number'
68
+ step='0.00001'
69
+ min='-180'
70
+ max='180'
71
+ disabled={!!readonly}
72
+ value={longitudeText}
73
+ onChange={handleCoordinateChange('longitude', setLongitudeText)}
74
+ onBlur={onBlur}
75
+ />
76
+ </div>
69
77
  </div>
70
78
  </Input.Root>
71
79
  );