@dxos/react-ui-form 0.8.4-main.f9ba587 → 0.8.4-main.fffef41

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 (118) hide show
  1. package/dist/lib/browser/index.mjs +884 -637
  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 +884 -637
  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 +80 -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 +77 -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 +8 -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 +128 -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 +15 -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 +75 -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 +20 -6
  48. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  49. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +80 -9
  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 +9 -3
  58. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
  59. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
  60. package/dist/types/src/translations.d.ts +13 -8
  61. package/dist/types/src/translations.d.ts.map +1 -1
  62. package/dist/types/src/util/field.d.ts +1 -1
  63. package/dist/types/src/util/field.d.ts.map +1 -1
  64. package/dist/types/src/util/formatting.d.ts +1 -1
  65. package/dist/types/src/util/formatting.d.ts.map +1 -1
  66. package/dist/types/src/util/refs.d.ts +1 -1
  67. package/dist/types/src/util/refs.d.ts.map +1 -1
  68. package/dist/types/src/util/schema.d.ts +1 -1
  69. package/dist/types/src/util/schema.d.ts.map +1 -1
  70. package/dist/types/tsconfig.tsbuildinfo +1 -1
  71. package/package.json +42 -36
  72. package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
  73. package/src/components/ControlSection/ControlSection.tsx +26 -12
  74. package/src/components/FieldEditor/FieldEditor.stories.tsx +26 -24
  75. package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
  76. package/src/components/FieldEditor/FieldEditor.tsx +21 -23
  77. package/src/components/Form/ArrayField.tsx +13 -12
  78. package/src/components/Form/Defaults.tsx +23 -21
  79. package/src/components/Form/Form.stories.tsx +135 -99
  80. package/src/components/Form/Form.tsx +30 -16
  81. package/src/components/Form/FormActions.tsx +4 -3
  82. package/src/components/Form/FormContent.tsx +171 -49
  83. package/src/components/Form/FormContext.tsx +8 -8
  84. package/src/components/Form/Input.tsx +5 -2
  85. package/src/components/Form/RefField.tsx +111 -41
  86. package/src/components/Form/custom/GeoPointInput.tsx +29 -21
  87. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +11 -11
  88. package/src/components/Form/custom/SelectOptionsInput.tsx +31 -32
  89. package/src/components/Form/custom/TupleInput.tsx +2 -2
  90. package/src/components/Form/factory.tsx +2 -2
  91. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +207 -0
  92. package/src/components/ObjectPicker/ObjectPicker.tsx +161 -0
  93. package/src/components/ObjectPicker/index.ts +5 -0
  94. package/src/components/ViewEditor/ViewEditor.stories.tsx +130 -49
  95. package/src/components/ViewEditor/ViewEditor.test.tsx +19 -19
  96. package/src/components/ViewEditor/ViewEditor.tsx +281 -233
  97. package/src/components/index.ts +1 -2
  98. package/src/components/testing.tsx +15 -9
  99. package/src/hooks/useForm.ts +11 -6
  100. package/src/hooks/useQueryRefOptions.ts +15 -6
  101. package/src/hooks/useRefQueryLookupHandler.ts +3 -3
  102. package/src/translations.ts +15 -9
  103. package/src/util/field.ts +9 -19
  104. package/src/util/formatting.ts +1 -1
  105. package/src/util/refs.ts +7 -7
  106. package/src/util/schema.ts +2 -4
  107. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  108. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  109. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  110. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  111. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  112. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  113. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  114. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  115. package/src/components/deprecated/Form.stories.tsx +0 -62
  116. package/src/components/deprecated/Form.tsx +0 -119
  117. package/src/components/deprecated/FormInput.tsx +0 -71
  118. 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';
7
- import React, { forwardRef, useMemo } from 'react';
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';
9
+ import React, { Component, type PropsWithChildren, type ReactNode, 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,55 +212,151 @@ 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}>
222
302
  {properties
223
303
  .map((property) => {
224
304
  return (
225
- <FormField
226
- key={property.name}
227
- property={property}
228
- path={[...(path ?? []), property.name]}
229
- readonly={readonly}
230
- onQueryRefOptions={onQueryRefOptions}
231
- lookupComponent={lookupComponent}
232
- Custom={Custom}
233
- />
305
+ <ErrorBoundary key={property.name} path={[...(path ?? []), property.name]}>
306
+ <FormField
307
+ property={property}
308
+ path={[...(path ?? []), property.name]}
309
+ readonly={readonly}
310
+ projection={projection}
311
+ onQueryRefOptions={onQueryRefOptions}
312
+ lookupComponent={lookupComponent}
313
+ Custom={Custom}
314
+ {...props}
315
+ />
316
+ </ErrorBoundary>
234
317
  );
235
318
  })
236
- .filter(isNotFalsy)}
319
+ .filter(isTruthy)}
237
320
  </div>
238
321
  );
239
322
  },
240
323
  );
324
+
325
+ type ErrorState = {
326
+ error: Error | undefined;
327
+ };
328
+
329
+ type ErrorBoundaryProps = PropsWithChildren<{
330
+ path?: (string | number)[];
331
+ }>;
332
+
333
+ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorState> {
334
+ static getDerivedStateFromError(error: Error): { error: Error } {
335
+ return { error };
336
+ }
337
+
338
+ override state = { error: undefined };
339
+
340
+ override componentDidUpdate(prevProps: ErrorBoundaryProps): void {
341
+ if (prevProps.path !== this.props.path) {
342
+ this.resetError();
343
+ }
344
+ }
345
+
346
+ override render(): ReactNode {
347
+ if (this.state.error) {
348
+ return (
349
+ <div className='flex gap-2 border border-roseFill font-mono text-sm'>
350
+ <span className='bg-roseFill text-surfaceText pli-1 font-thin'>ERROR</span>
351
+ {String(this.props.path?.join('.'))}
352
+ </div>
353
+ );
354
+ }
355
+
356
+ return this.props.children;
357
+ }
358
+
359
+ private resetError(): void {
360
+ this.setState({ error: undefined });
361
+ }
362
+ }
@@ -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,67 @@
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 '@dxos/lit-ui/dx-tag-picker.pcss';
6
+
7
+ import type * as Schema from 'effect/Schema';
8
+ import type * as SchemaAST from 'effect/SchemaAST';
9
+ import React, { useCallback, useMemo } from 'react';
7
10
 
8
11
  import {
12
+ type EchoSchema,
9
13
  Expando,
10
- getTypeAnnotation,
11
14
  Ref,
12
15
  ReferenceAnnotationId,
13
16
  type ReferenceAnnotationValue,
14
- } from '@dxos/echo-schema';
17
+ getTypeAnnotation,
18
+ } from '@dxos/echo/internal';
15
19
  import { findAnnotation } from '@dxos/effect';
16
20
  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';
21
+ import { type DxTagPickerItemClick } from '@dxos/lit-ui';
22
+ import { DxAnchor, DxTagPickerItem } from '@dxos/lit-ui/react';
23
+ import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
20
24
  import { descriptionText, mx } from '@dxos/react-ui-theme';
21
25
  import { isNonNullable } from '@dxos/util';
22
26
 
23
- import { TextInput } from './Defaults';
24
- import { InputHeader, type InputProps } from './Input';
25
27
  import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
26
28
  import { translationKey } from '../../translations';
29
+ import { ObjectPicker } from '../ObjectPicker';
27
30
 
28
- type RefFieldProps = InputProps & {
31
+ import { TextInput } from './Defaults';
32
+ import { InputHeader, type InputProps } from './Input';
33
+
34
+ export type RefFieldProps = InputProps & {
29
35
  ast?: SchemaAST.AST;
30
36
  array?: boolean;
37
+ createOptionLabel?: [string, { ns: string }];
38
+ createOptionIcon?: string;
39
+ onCreate?: (values: any) => void;
40
+ createSchema?: Schema.Schema.AnyNoContext;
41
+ createInitialValuePath?: string;
31
42
  onQueryRefOptions?: QueryRefOptions;
43
+ schema?: EchoSchema;
44
+ };
45
+
46
+ // TODO(thure): Is this a standard that should be better canonized?
47
+ const isRefSnapShot = (val: any): val is { '/': string } => {
48
+ return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
32
49
  };
33
50
 
34
51
  export const RefField = ({
35
52
  type,
36
53
  label,
37
- disabled,
54
+ readonly,
38
55
  placeholder,
39
56
  inputOnly,
40
57
  array,
41
58
  ast,
42
59
  getValue,
60
+ createOptionLabel,
61
+ createOptionIcon,
43
62
  onBlur,
63
+ onCreate,
64
+ createSchema,
65
+ createInitialValuePath,
44
66
  onQueryRefOptions,
45
67
  onValueChange,
46
68
  ...restInputProps
@@ -50,23 +72,28 @@ export const RefField = ({
50
72
  () => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
51
73
  [ast],
52
74
  );
53
- const { options: availableOptions, loading: _loading } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
75
+ const {
76
+ options: availableOptions,
77
+ update: updateOptions,
78
+ loading: _loading,
79
+ } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
54
80
 
55
81
  if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
56
82
  // If ref type is expando, fall back to taking a DXN in string format.
57
83
  return (
58
84
  <RefFieldFallback
59
- {...{ type, label, placeholder, disabled, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
85
+ {...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
60
86
  />
61
87
  );
62
88
  }
63
89
 
64
- const handleGetValue = useCallback((): TagPickerItemData[] => {
90
+ const handleGetValue = useCallback(() => {
65
91
  const formValue = getValue();
66
92
 
67
93
  const unknownToRefOption = (val: unknown) => {
68
- if (Ref.isRef(val)) {
69
- const dxnString = val.dxn.toString();
94
+ const isRef = Ref.isRef(val);
95
+ if (isRef || isRefSnapShot(val)) {
96
+ const dxnString = isRef ? val.dxn.toString() : val['/'];
70
97
  const matchingOption = availableOptions.find((option) => option.id === dxnString);
71
98
  if (matchingOption) {
72
99
  return matchingOption;
@@ -87,15 +114,6 @@ export const RefField = ({
87
114
  return [];
88
115
  }, [getValue, availableOptions, array]);
89
116
 
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
117
  const handleUpdate = useCallback(
100
118
  (ids: string[]) => {
101
119
  if (ids.length === 0) {
@@ -123,7 +141,13 @@ export const RefField = ({
123
141
  [availableOptions, array, onValueChange],
124
142
  );
125
143
 
126
- const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
144
+ const handleCreate = useCallback(
145
+ (values: any) => {
146
+ onCreate?.(values);
147
+ updateOptions();
148
+ },
149
+ [onCreate, updateOptions],
150
+ );
127
151
 
128
152
  if (!refTypeInfo) {
129
153
  return null;
@@ -132,32 +156,78 @@ export const RefField = ({
132
156
  const { status, error } = restInputProps.getStatus();
133
157
 
134
158
  const items = handleGetValue();
159
+ const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
160
+ const toggleSelect = useCallback(
161
+ (id: string) => {
162
+ if (array) {
163
+ const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
164
+ handleUpdate(nextIds);
165
+ } else {
166
+ if (selectedIds[0] === id) {
167
+ handleUpdate([]);
168
+ } else {
169
+ handleUpdate([id]);
170
+ }
171
+ }
172
+ },
173
+ [array, selectedIds, handleUpdate],
174
+ );
135
175
 
136
176
  // 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 : (
177
+ return readonly && items.length < 1 ? null : (
138
178
  <Input.Root validationValence={status}>
139
179
  {!inputOnly && <InputHeader error={error} label={label} />}
140
180
  <div data-no-submit>
141
- {disabled ? (
181
+ {readonly ? (
142
182
  items.length < 1 ? (
143
183
  <p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
144
184
  ) : (
145
185
  items.map((item) => (
146
- <DxRefTag key={item.id} refid={item.id} rootclassname='mie-1' style={{ marginInlineEnd: '.25rem' }}>
186
+ <DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
147
187
  {item.label}
148
- </DxRefTag>
188
+ </DxAnchor>
149
189
  ))
150
190
  )
151
191
  ) : (
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
- />
192
+ <ObjectPicker.Root>
193
+ <ObjectPicker.Trigger asChild>
194
+ <Button variant='ghost' classNames='is-full text-start gap-2'>
195
+ <div role='none' className='grow'>
196
+ {items?.length ? (
197
+ items?.map((item) => (
198
+ <DxTagPickerItem
199
+ key={item.id}
200
+ itemId={item.id}
201
+ label={item.label}
202
+ {...(item.hue ? { hue: item.hue } : {})}
203
+ removeLabel={t('remove item label')}
204
+ onItemClick={(event: DxTagPickerItemClick) => {
205
+ if (event.action === 'remove') {
206
+ toggleSelect(item.id);
207
+ }
208
+ }}
209
+ />
210
+ ))
211
+ ) : (
212
+ <span className='text-description'>
213
+ {placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
214
+ </span>
215
+ )}
216
+ </div>
217
+ <Icon size={3} icon='ph--caret-down--bold' />
218
+ </Button>
219
+ </ObjectPicker.Trigger>
220
+ <ObjectPicker.Content
221
+ options={availableOptions}
222
+ selectedIds={selectedIds}
223
+ onSelect={toggleSelect}
224
+ createSchema={createSchema}
225
+ createOptionLabel={createOptionLabel}
226
+ createOptionIcon={createOptionIcon}
227
+ createInitialValuePath={createInitialValuePath}
228
+ onCreate={handleCreate}
229
+ />
230
+ </ObjectPicker.Root>
161
231
  )}
162
232
  </div>
163
233
  {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
@@ -168,7 +238,7 @@ export const RefField = ({
168
238
  const RefFieldFallback = ({
169
239
  type,
170
240
  label,
171
- disabled,
241
+ readonly,
172
242
  placeholder,
173
243
  inputOnly,
174
244
  getValue,
@@ -202,7 +272,7 @@ const RefFieldFallback = ({
202
272
  <TextInput
203
273
  type={type}
204
274
  label={label}
205
- disabled={disabled}
275
+ readonly={readonly}
206
276
  placeholder={placeholder}
207
277
  inputOnly={inputOnly}
208
278
  getValue={handleGetValue as <V>() => V | undefined}