@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.a4bbb77

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 (97) hide show
  1. package/dist/lib/browser/index.mjs +710 -553
  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 +710 -553
  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 +4 -5
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +78 -7
  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 +5 -4
  20. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/Form.stories.d.ts +128 -31
  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 +16 -10
  25. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormContext.d.ts +2 -2
  27. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/Input.d.ts +4 -1
  29. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/RefField.d.ts +13 -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.map +1 -1
  40. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +16 -4
  41. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +80 -8
  43. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/index.d.ts +0 -1
  45. package/dist/types/src/components/index.d.ts.map +1 -1
  46. package/dist/types/src/components/testing.d.ts +6 -3
  47. package/dist/types/src/components/testing.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useForm.d.ts +1 -1
  49. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  50. package/dist/types/src/hooks/useQueryRefOptions.d.ts +7 -2
  51. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
  52. package/dist/types/src/translations.d.ts +11 -5
  53. package/dist/types/src/translations.d.ts.map +1 -1
  54. package/dist/types/src/util/field.d.ts.map +1 -1
  55. package/dist/types/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +42 -36
  57. package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
  58. package/src/components/ControlSection/ControlSection.tsx +26 -12
  59. package/src/components/FieldEditor/FieldEditor.stories.tsx +25 -23
  60. package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
  61. package/src/components/FieldEditor/FieldEditor.tsx +21 -23
  62. package/src/components/Form/ArrayField.tsx +9 -9
  63. package/src/components/Form/Defaults.tsx +22 -20
  64. package/src/components/Form/Form.stories.tsx +133 -97
  65. package/src/components/Form/Form.tsx +16 -12
  66. package/src/components/Form/FormActions.tsx +2 -1
  67. package/src/components/Form/FormContent.tsx +116 -36
  68. package/src/components/Form/FormContext.tsx +6 -6
  69. package/src/components/Form/Input.tsx +4 -1
  70. package/src/components/Form/RefField.tsx +190 -40
  71. package/src/components/Form/custom/GeoPointInput.tsx +29 -21
  72. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +10 -10
  73. package/src/components/Form/custom/SelectOptionsInput.tsx +29 -30
  74. package/src/components/Form/custom/TupleInput.tsx +2 -2
  75. package/src/components/Form/factory.tsx +1 -1
  76. package/src/components/ViewEditor/ViewEditor.stories.tsx +110 -46
  77. package/src/components/ViewEditor/ViewEditor.test.tsx +17 -18
  78. package/src/components/ViewEditor/ViewEditor.tsx +329 -235
  79. package/src/components/index.ts +0 -2
  80. package/src/components/testing.tsx +15 -9
  81. package/src/hooks/useForm.ts +6 -3
  82. package/src/hooks/useQueryRefOptions.ts +8 -2
  83. package/src/hooks/useRefQueryLookupHandler.ts +1 -1
  84. package/src/translations.ts +13 -6
  85. package/src/util/field.ts +8 -18
  86. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  87. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  88. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  89. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  90. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  91. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  92. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  93. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  94. package/src/components/deprecated/Form.stories.tsx +0 -62
  95. package/src/components/deprecated/Form.tsx +0 -119
  96. package/src/components/deprecated/FormInput.tsx +0 -71
  97. package/src/components/deprecated/index.ts +0 -5
@@ -9,36 +9,51 @@ import React, { forwardRef, useMemo } from 'react';
9
9
  import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
10
10
  import { type ThemedClassName } from '@dxos/react-ui';
11
11
  import { mx } from '@dxos/react-ui-theme';
12
- import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
13
- import { isNotFalsy } from '@dxos/util';
12
+ import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
13
+ import { isTruthy } from '@dxos/util';
14
+
15
+ import { type QueryRefOptions } from '../../hooks';
16
+ import { getRefProps } from '../../util';
14
17
 
15
18
  import { ArrayField } from './ArrayField';
16
19
  import { SelectInput } from './Defaults';
20
+ import { getInputComponent } from './factory';
17
21
  import { type ComponentLookup } from './Form';
18
- import { useInputProps, useFormValues } from './FormContext';
22
+ import { useFormValues, useInputProps } from './FormContext';
19
23
  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';
24
+ import { RefField, type RefFieldProps } from './RefField';
24
25
 
25
26
  export type FormFieldProps = {
26
27
  property: SchemaProperty<any>;
27
28
  path?: (string | number)[];
28
- readonly?: boolean;
29
29
  /** Used to indicate if input should be presented inline (e.g. for array items). */
30
30
  inline?: boolean;
31
- onQueryRefOptions?: QueryRefOptions;
31
+ projection?: ProjectionModel;
32
32
  lookupComponent?: ComponentLookup;
33
33
  Custom?: Partial<Record<string, InputComponent>>;
34
- };
34
+ } & Pick<
35
+ RefFieldProps,
36
+ | 'readonly'
37
+ | 'onQueryRefOptions'
38
+ | 'createOptionLabel'
39
+ | 'createOptionIcon'
40
+ | 'onCreate'
41
+ | 'createSchema'
42
+ | 'createInitialValuePath'
43
+ >;
35
44
 
36
45
  export const FormField = ({
37
46
  property,
38
47
  path,
39
48
  readonly,
40
49
  inline,
50
+ projection,
41
51
  onQueryRefOptions,
52
+ createOptionLabel,
53
+ createOptionIcon,
54
+ onCreate,
55
+ createSchema,
56
+ createInitialValuePath,
42
57
  lookupComponent,
43
58
  Custom,
44
59
  }: FormFieldProps) => {
@@ -62,7 +77,7 @@ export const FormField = ({
62
77
  type,
63
78
  format,
64
79
  label,
65
- disabled: readonly,
80
+ readonly,
66
81
  placeholder,
67
82
  ...inputProps,
68
83
  },
@@ -82,7 +97,7 @@ export const FormField = ({
82
97
  label={label}
83
98
  inputOnly={inline}
84
99
  placeholder={placeholder}
85
- disabled={readonly}
100
+ readonly={readonly}
86
101
  {...inputProps}
87
102
  />
88
103
  );
@@ -102,9 +117,14 @@ export const FormField = ({
102
117
  format={format}
103
118
  label={label}
104
119
  placeholder={placeholder}
105
- disabled={readonly}
120
+ readonly={readonly}
106
121
  inputOnly={inline}
107
122
  onQueryRefOptions={onQueryRefOptions}
123
+ createOptionLabel={createOptionLabel}
124
+ createOptionIcon={createOptionIcon}
125
+ onCreate={onCreate}
126
+ createSchema={createSchema}
127
+ createInitialValuePath={createInitialValuePath}
108
128
  {...inputProps}
109
129
  />
110
130
  );
@@ -131,7 +151,7 @@ export const FormField = ({
131
151
  label={label}
132
152
  inputOnly={inline}
133
153
  placeholder={placeholder}
134
- disabled={readonly}
154
+ readonly={readonly}
135
155
  {...inputProps}
136
156
  />
137
157
  );
@@ -146,7 +166,7 @@ export const FormField = ({
146
166
  <SelectInput
147
167
  type={type}
148
168
  format={format}
149
- disabled={readonly}
169
+ readonly={readonly}
150
170
  inputOnly={inline}
151
171
  label={label}
152
172
  options={options.map((option) => ({ value: option, label: String(option) }))}
@@ -174,7 +194,11 @@ export const FormField = ({
174
194
  schema={Schema.make(typeLiteral)}
175
195
  path={path}
176
196
  readonly={readonly}
197
+ projection={projection}
177
198
  onQueryRefOptions={onQueryRefOptions}
199
+ createOptionLabel={createOptionLabel}
200
+ createOptionIcon={createOptionIcon}
201
+ onCreate={onCreate}
178
202
  Custom={Custom}
179
203
  lookupComponent={lookupComponent}
180
204
  />
@@ -186,36 +210,90 @@ export const FormField = ({
186
210
  return null;
187
211
  };
188
212
 
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
- }>;
213
+ export type FormFieldsProps = ThemedClassName<
214
+ {
215
+ testId?: string;
216
+ schema: Schema.Schema.All;
217
+ /**
218
+ * Path to the current object from the root. Used with nested forms.
219
+ */
220
+ path?: (string | number)[];
221
+ filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
222
+ sort?: string[];
223
+ /**
224
+ * Optional projection for projection-based field management
225
+ */
226
+ projection?: ProjectionModel;
227
+ lookupComponent?: ComponentLookup;
228
+ /**
229
+ * Map of custom renderers for specific properties.
230
+ * Prefer lookupComponent for plugin specific input surfaces.
231
+ */
232
+ Custom?: Partial<Record<string, InputComponent>>;
233
+ onQueryRefOptions?: QueryRefOptions;
234
+ } & Pick<FormFieldProps, 'readonly'> &
235
+ Pick<
236
+ RefFieldProps,
237
+ | 'onQueryRefOptions'
238
+ | 'createOptionLabel'
239
+ | 'createOptionIcon'
240
+ | 'onCreate'
241
+ | 'createSchema'
242
+ | 'createInitialValuePath'
243
+ >
244
+ >;
207
245
 
208
246
  export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
209
247
  (
210
- { classNames, testId, schema, path, filter, sort, readonly, lookupComponent, Custom, onQueryRefOptions },
248
+ {
249
+ classNames,
250
+ schema,
251
+ path,
252
+ filter,
253
+ sort,
254
+ projection,
255
+ readonly,
256
+ lookupComponent,
257
+ Custom,
258
+ onQueryRefOptions,
259
+ ...props
260
+ },
211
261
  forwardRef,
212
262
  ) => {
213
263
  const values = useFormValues(path);
214
264
  const properties = useMemo(() => {
215
265
  const props = getSchemaProperties(schema.ast, values);
266
+
267
+ // Use projection-based field management when view and projection are available
268
+ if (projection) {
269
+ const fieldProjections = projection.getFieldProjections();
270
+ const hiddenProperties = new Set(projection.getHiddenProperties());
271
+
272
+ // Filter properties to only include visible ones and order by projection
273
+ const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name));
274
+ const orderedProps: SchemaProperty<any>[] = [];
275
+
276
+ // Add properties in projection field order
277
+ for (const fieldProjection of fieldProjections) {
278
+ const fieldPath = String(fieldProjection.field.path);
279
+ const prop = visibleProps.find((p) => p.name === fieldPath);
280
+ if (prop) {
281
+ orderedProps.push(prop);
282
+ }
283
+ }
284
+
285
+ // Add any remaining properties not in projection
286
+ const projectionPaths = new Set(fieldProjections.map((fp) => String(fp.field.path)));
287
+ const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name));
288
+ orderedProps.push(...remainingProps);
289
+
290
+ return orderedProps;
291
+ }
292
+
293
+ // Fallback to legacy filter/sort behavior
216
294
  const filtered = filter ? filter(props) : props;
217
295
  return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
218
- }, [schema, values, filter, sort]);
296
+ }, [schema, values, filter, sort, projection?.fields]);
219
297
 
220
298
  return (
221
299
  <div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
@@ -227,13 +305,15 @@ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
227
305
  property={property}
228
306
  path={[...(path ?? []), property.name]}
229
307
  readonly={readonly}
308
+ projection={projection}
230
309
  onQueryRefOptions={onQueryRefOptions}
231
310
  lookupComponent={lookupComponent}
232
311
  Custom={Custom}
312
+ {...props}
233
313
  />
234
314
  );
235
315
  })
236
- .filter(isNotFalsy)}
316
+ .filter(isTruthy)}
237
317
  </div>
238
318
  );
239
319
  },
@@ -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
17
  import { type BaseObject, getValue } from '@dxos/echo-schema';
18
- import { createJsonPath, type SimpleType } from '@dxos/effect';
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
  >) => {
@@ -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 Schema, type SchemaAST } from 'effect';
6
+ import React, { type KeyboardEvent, type MouseEvent, useCallback, useMemo, useState } from 'react';
7
7
 
8
8
  import {
9
+ type EchoSchema,
9
10
  Expando,
10
- getTypeAnnotation,
11
11
  Ref,
12
12
  ReferenceAnnotationId,
13
13
  type ReferenceAnnotationValue,
14
+ getTypeAnnotation,
14
15
  } from '@dxos/echo-schema';
15
16
  import { findAnnotation } from '@dxos/effect';
16
17
  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';
18
+ import { type DxTagPickerItemClick } from '@dxos/lit-ui';
19
+ import { DxAnchor, DxTagPickerItem } from '@dxos/lit-ui/react';
20
+ import { Button, Icon, Input, Popover, useTranslation } from '@dxos/react-ui';
21
+ import { PopoverCombobox } from '@dxos/react-ui-searchlist';
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
27
 
28
- type RefFieldProps = InputProps & {
28
+ import { TextInput } from './Defaults';
29
+ import { Form } from './Form';
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
@@ -52,21 +72,70 @@ export const RefField = ({
52
72
  );
53
73
  const { options: availableOptions, loading: _loading } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
54
74
 
75
+ const [showForm, setShowForm] = useState(false);
76
+ const [searchString, setSearchString] = useState('');
77
+
78
+ const handleFormSave = useCallback(
79
+ (values: any) => {
80
+ onCreate?.(values);
81
+ setShowForm(false);
82
+ setSearchString('');
83
+ },
84
+ [refTypeInfo, onCreate],
85
+ );
86
+
87
+ const handleFormCancel = useCallback(() => {
88
+ setShowForm(false);
89
+ setSearchString('');
90
+ }, []);
91
+
92
+ // TODO(thure): The following workarounds are necessary because `onSelect` is called after the Popover is already
93
+ // closed. Augment/refactor CmdK, if possible, to facilitate stopping event default & propagation.
94
+
95
+ const handleClick = useCallback(
96
+ (event: MouseEvent) => {
97
+ if (createSchema && (event.target as HTMLElement).closest('[data-value="__create__"]')) {
98
+ event.stopPropagation();
99
+ event.preventDefault();
100
+ setShowForm(true);
101
+ }
102
+ },
103
+ [createSchema],
104
+ );
105
+
106
+ const handleKeyDown = useCallback(
107
+ (event: KeyboardEvent) => {
108
+ if (
109
+ event.key === 'Enter' &&
110
+ createSchema &&
111
+ (event.currentTarget as HTMLElement).querySelector(
112
+ '[role="option"][aria-selected="true"][data-value="__create__"]',
113
+ )
114
+ ) {
115
+ event.stopPropagation();
116
+ event.preventDefault();
117
+ setShowForm(true);
118
+ }
119
+ },
120
+ [createSchema],
121
+ );
122
+
55
123
  if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
56
124
  // If ref type is expando, fall back to taking a DXN in string format.
57
125
  return (
58
126
  <RefFieldFallback
59
- {...{ type, label, placeholder, disabled, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
127
+ {...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
60
128
  />
61
129
  );
62
130
  }
63
131
 
64
- const handleGetValue = useCallback((): TagPickerItemData[] => {
132
+ const handleGetValue = useCallback(() => {
65
133
  const formValue = getValue();
66
134
 
67
135
  const unknownToRefOption = (val: unknown) => {
68
- if (Ref.isRef(val)) {
69
- const dxnString = val.dxn.toString();
136
+ const isRef = Ref.isRef(val);
137
+ if (isRef || isRefSnapShot(val)) {
138
+ const dxnString = isRef ? val.dxn.toString() : val['/'];
70
139
  const matchingOption = availableOptions.find((option) => option.id === dxnString);
71
140
  if (matchingOption) {
72
141
  return matchingOption;
@@ -87,15 +156,6 @@ export const RefField = ({
87
156
  return [];
88
157
  }, [getValue, availableOptions, array]);
89
158
 
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
159
  const handleUpdate = useCallback(
100
160
  (ids: string[]) => {
101
161
  if (ids.length === 0) {
@@ -123,8 +183,6 @@ export const RefField = ({
123
183
  [availableOptions, array, onValueChange],
124
184
  );
125
185
 
126
- const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
127
-
128
186
  if (!refTypeInfo) {
129
187
  return null;
130
188
  }
@@ -132,32 +190,124 @@ export const RefField = ({
132
190
  const { status, error } = restInputProps.getStatus();
133
191
 
134
192
  const items = handleGetValue();
193
+ const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
194
+ const labelById = useMemo(
195
+ () =>
196
+ availableOptions.reduce((acc: Record<string, string>, option) => {
197
+ acc[option.id.toLowerCase()] = option.label.toLowerCase();
198
+ return acc;
199
+ }, {}),
200
+ [availableOptions],
201
+ );
202
+ const toggleSelect = useCallback(
203
+ (id: string) => {
204
+ if (array) {
205
+ const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
206
+ handleUpdate(nextIds);
207
+ } else {
208
+ if (selectedIds[0] === id) {
209
+ handleUpdate([]);
210
+ } else {
211
+ handleUpdate([id]);
212
+ }
213
+ }
214
+ },
215
+ [array, selectedIds, handleUpdate],
216
+ );
135
217
 
136
218
  // 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 : (
219
+ return readonly && items.length < 1 ? null : (
138
220
  <Input.Root validationValence={status}>
139
221
  {!inputOnly && <InputHeader error={error} label={label} />}
140
222
  <div data-no-submit>
141
- {disabled ? (
223
+ {readonly ? (
142
224
  items.length < 1 ? (
143
225
  <p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
144
226
  ) : (
145
227
  items.map((item) => (
146
- <DxRefTag key={item.id} refid={item.id} rootclassname='mie-1' style={{ marginInlineEnd: '.25rem' }}>
228
+ <DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
147
229
  {item.label}
148
- </DxRefTag>
230
+ </DxAnchor>
149
231
  ))
150
232
  )
151
233
  ) : (
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
- />
234
+ <PopoverCombobox.Root>
235
+ <PopoverCombobox.Trigger asChild>
236
+ <Button variant='ghost' classNames='is-full text-start gap-2'>
237
+ <div role='none' className='grow'>
238
+ {items?.length ? (
239
+ items?.map((item) => (
240
+ <DxTagPickerItem
241
+ key={item.id}
242
+ itemId={item.id}
243
+ label={item.label}
244
+ {...(item.hue ? { hue: item.hue } : {})}
245
+ removeLabel={t('remove item label')}
246
+ onItemClick={(event: DxTagPickerItemClick) => {
247
+ if (event.action === 'remove') {
248
+ toggleSelect(item.id);
249
+ }
250
+ }}
251
+ />
252
+ ))
253
+ ) : (
254
+ <span className='text-description'>
255
+ {placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
256
+ </span>
257
+ )}
258
+ </div>
259
+ <Icon size={3} icon='ph--caret-down--bold' />
260
+ </Button>
261
+ </PopoverCombobox.Trigger>
262
+ <PopoverCombobox.Content
263
+ filter={(value, search) =>
264
+ value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0
265
+ }
266
+ onClickCapture={handleClick}
267
+ onKeyDownCapture={handleKeyDown}
268
+ >
269
+ {showForm && createSchema ? (
270
+ <Popover.Viewport>
271
+ <Form
272
+ schema={createSchema}
273
+ values={createInitialValuePath ? { [createInitialValuePath]: searchString } : {}}
274
+ onSave={handleFormSave}
275
+ onCancel={handleFormCancel}
276
+ testId='create-referenced-object-form'
277
+ />
278
+ </Popover.Viewport>
279
+ ) : (
280
+ <>
281
+ <PopoverCombobox.Input
282
+ placeholder={t('ref field combobox input placeholder')}
283
+ value={searchString}
284
+ onValueChange={(v) => setSearchString(v)}
285
+ autoFocus
286
+ />
287
+ <PopoverCombobox.List>
288
+ {availableOptions.map((option) => (
289
+ <PopoverCombobox.Item
290
+ key={option.id}
291
+ value={option.id}
292
+ onSelect={() => toggleSelect(option.id)}
293
+ classNames='flex items-center gap-2'
294
+ >
295
+ <span className='grow'>{option.label}</span>
296
+ {selectedIds.includes(option.id) && <Icon icon='ph--check--regular' />}
297
+ </PopoverCombobox.Item>
298
+ ))}
299
+ {searchString.length > 0 && createOptionLabel && createOptionIcon && createSchema && (
300
+ <PopoverCombobox.Item value='__create__' classNames='flex items-center gap-2'>
301
+ <Icon icon={createOptionIcon} />
302
+ {t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: searchString })}
303
+ </PopoverCombobox.Item>
304
+ )}
305
+ </PopoverCombobox.List>
306
+ </>
307
+ )}
308
+ <PopoverCombobox.Arrow />
309
+ </PopoverCombobox.Content>
310
+ </PopoverCombobox.Root>
161
311
  )}
162
312
  </div>
163
313
  {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
@@ -168,7 +318,7 @@ export const RefField = ({
168
318
  const RefFieldFallback = ({
169
319
  type,
170
320
  label,
171
- disabled,
321
+ readonly,
172
322
  placeholder,
173
323
  inputOnly,
174
324
  getValue,
@@ -202,7 +352,7 @@ const RefFieldFallback = ({
202
352
  <TextInput
203
353
  type={type}
204
354
  label={label}
205
- disabled={disabled}
355
+ readonly={readonly}
206
356
  placeholder={placeholder}
207
357
  inputOnly={inputOnly}
208
358
  getValue={handleGetValue as <V>() => V | undefined}