@dxos/react-ui-form 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe

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 (113) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +1003 -668
  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 +1003 -668
  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 +3 -3
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +62 -61
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +23 -18
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +68 -67
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +7 -1
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/RefField.d.ts +8 -6
  26. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +62 -61
  29. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  31. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  32. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  33. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  34. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
  35. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  36. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -61
  37. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  39. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  40. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +83 -0
  41. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  43. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  44. package/dist/types/src/components/Settings/Settings.d.ts +35 -0
  45. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  46. package/dist/types/src/components/Settings/Settings.stories.d.ts +82 -0
  47. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/Settings/index.d.ts +2 -0
  49. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  50. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
  51. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  52. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +66 -64
  53. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/index.d.ts +3 -1
  55. package/dist/types/src/components/index.d.ts.map +1 -1
  56. package/dist/types/src/components/testing.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  59. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  60. package/dist/types/src/translations.d.ts +62 -61
  61. package/dist/types/src/translations.d.ts.map +1 -1
  62. package/dist/types/src/util/refs.d.ts +1 -0
  63. package/dist/types/src/util/refs.d.ts.map +1 -1
  64. package/dist/types/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +37 -38
  66. package/src/components/FieldEditor/FieldEditor.stories.tsx +7 -8
  67. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  68. package/src/components/FieldEditor/FieldEditor.tsx +3 -4
  69. package/src/components/Form/AUDIT.md +17 -0
  70. package/src/components/Form/Form.stories.tsx +25 -26
  71. package/src/components/Form/Form.tsx +101 -66
  72. package/src/components/Form/FormField.tsx +32 -14
  73. package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
  74. package/src/components/Form/FormFieldComponent.tsx +17 -20
  75. package/src/components/Form/FormFieldSet.tsx +126 -105
  76. package/src/components/Form/fields/ArrayField.tsx +11 -8
  77. package/src/components/Form/fields/GeoPointField.tsx +6 -6
  78. package/src/components/Form/fields/MarkdownField.tsx +1 -1
  79. package/src/components/Form/fields/RefField.tsx +37 -20
  80. package/src/components/Form/fields/SelectField.tsx +1 -1
  81. package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -3
  82. package/src/components/Form/fields/SelectOptionField.tsx +9 -10
  83. package/src/components/Form/fields/TupleField.tsx +1 -1
  84. package/src/components/ObjectForm/ObjectForm.tsx +113 -0
  85. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  86. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -18
  87. package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
  88. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
  89. package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
  90. package/src/components/ObjectProperties/index.ts +5 -0
  91. package/src/components/Settings/Settings.stories.tsx +55 -0
  92. package/src/components/Settings/Settings.tsx +129 -0
  93. package/src/components/Settings/index.ts +5 -0
  94. package/src/components/ViewEditor/ViewEditor.stories.tsx +36 -33
  95. package/src/components/ViewEditor/ViewEditor.test.tsx +3 -7
  96. package/src/components/ViewEditor/ViewEditor.tsx +117 -85
  97. package/src/components/index.ts +3 -1
  98. package/src/components/testing.tsx +20 -8
  99. package/src/hooks/useFormHandler.ts +42 -18
  100. package/src/hooks/useKeyHandler.tsx +7 -7
  101. package/src/translations.ts +63 -63
  102. package/src/util/refs.ts +7 -3
  103. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  104. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  105. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
  106. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  107. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  108. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  109. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  110. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  111. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  112. package/src/components/ControlSection/ControlSection.tsx +0 -153
  113. package/src/components/Form/Form-old.stories.tsx +0 -314
@@ -2,124 +2,145 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { forwardRef, useMemo } from 'react';
5
+ import React, { useMemo } from 'react';
6
6
 
7
7
  import { type AnyProperties } from '@dxos/echo/internal';
8
8
  import { type SchemaProperty } from '@dxos/effect';
9
- import { type ThemedClassName } from '@dxos/react-ui';
10
- import { mx } from '@dxos/ui-theme';
11
- import { isTruthy } from '@dxos/util';
12
9
 
13
10
  import { type FormHandlerProps } from '../../hooks';
14
11
  import { getFormProperties } from '../../util';
15
-
16
12
  import { useFormValues } from './Form';
17
13
  import { FormField, type FormFieldProps } from './FormField';
18
14
  import { FormFieldErrorBoundary, FormFieldLabel } from './FormFieldComponent';
19
15
 
20
- export type FormFieldSetProps<T extends AnyProperties> = ThemedClassName<
21
- {
22
- label?: string;
23
- exclude?: (props: SchemaProperty[]) => SchemaProperty[];
24
- sort?: string[];
25
- } & Pick<FormHandlerProps<T>, 'schema'> &
26
- Pick<
27
- FormFieldProps,
28
- | 'path'
29
- | 'autoFocus'
30
- | 'readonly'
31
- | 'layout'
32
- | 'projection'
33
- | 'fieldMap'
34
- | 'fieldProvider'
35
- | 'createOptionLabel'
36
- | 'createOptionIcon'
37
- | 'createInitialValuePath'
38
- | 'db'
39
- | 'schemaHook'
40
- | 'getOptions'
41
- | 'onCreate'
42
- >
43
- >;
44
-
45
- export const FormFieldSet = forwardRef<HTMLDivElement, FormFieldSetProps<any>>(
46
- ({ classNames, label, schema, readonly, path, exclude, sort, projection, layout, ...props }, forwardRef) => {
47
- const values = useFormValues(FormFieldSet.displayName!, path);
48
-
49
- // TODO(burdon): Updates on every value change.
50
- // Remove values dep if can remove from getSchemaProperties.
51
- const properties = useMemo(() => {
52
- if (!schema) {
53
- return [];
54
- }
16
+ const FORM_FIELDSET_NAME = 'Form.FieldSet';
17
+
18
+ export type FormFieldSetProps<T extends AnyProperties> = {
19
+ label?: string;
20
+ sort?: string[];
21
+ exclude?: (props: SchemaProperty[]) => SchemaProperty[];
22
+ } & Pick<FormHandlerProps<T>, 'schema'> &
23
+ Pick<
24
+ FormFieldProps,
25
+ | 'path'
26
+ | 'autoFocus'
27
+ | 'readonly'
28
+ | 'layout'
29
+ | 'projection'
30
+ | 'fieldMap'
31
+ | 'fieldProvider'
32
+ | 'createTypename'
33
+ | 'createOptionLabel'
34
+ | 'createOptionIcon'
35
+ | 'createInitialValuePath'
36
+ | 'createFieldMap'
37
+ | 'db'
38
+ | 'schemaHook'
39
+ | 'getOptions'
40
+ | 'onCreate'
41
+ >;
42
+
43
+ /**
44
+ * Renders a set of form fields derived from a schema object.
45
+ */
46
+ export const FormFieldSet = ({
47
+ label,
48
+ schema,
49
+ readonly,
50
+ path,
51
+ sort,
52
+ exclude,
53
+ projection,
54
+ layout,
55
+ ...props
56
+ }: FormFieldSetProps<any>) => {
57
+ const values = useFormValues(FORM_FIELDSET_NAME, path);
58
+
59
+ const properties = useFormFieldSetProperties({ schema, values, exclude, sort, projection });
60
+
61
+ if ((readonly || layout === 'static') && values == null) {
62
+ return null;
63
+ }
64
+
65
+ return (
66
+ <>
67
+ {layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
68
+ {properties.map((property) => {
69
+ const name = property.name.toString();
70
+ return (
71
+ <FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
72
+ <FormField
73
+ type={property.type}
74
+ name={name}
75
+ path={[...(path ?? []), name]}
76
+ readonly={readonly}
77
+ layout={layout}
78
+ projection={projection}
79
+ {...props}
80
+ />
81
+ </FormFieldErrorBoundary>
82
+ );
83
+ })}
84
+ </>
85
+ );
86
+ };
87
+
88
+ FormFieldSet.displayName = FORM_FIELDSET_NAME;
89
+
90
+ type UseFormFieldSetPropertiesParams = Pick<FormFieldSetProps<any>, 'schema' | 'exclude' | 'projection' | 'sort'> & {
91
+ values: AnyProperties | undefined;
92
+ sort?: string[];
93
+ };
94
+
95
+ /**
96
+ * Resolves ordered schema properties for a field set (projection order, exclude, or sort).
97
+ */
98
+ const useFormFieldSetProperties = ({
99
+ schema,
100
+ exclude,
101
+ projection,
102
+ values,
103
+ sort,
104
+ }: UseFormFieldSetPropertiesParams): SchemaProperty[] => {
105
+ // TODO(burdon): Updates on every value change.
106
+ // Remove values dep if can remove from getSchemaProperties.
107
+ return useMemo(() => {
108
+ if (!schema) {
109
+ return [];
110
+ }
55
111
 
56
- // TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
57
- const props = getFormProperties(schema.ast);
58
-
59
- // Use projection-based field management when view and projection are available.
60
- if (projection) {
61
- const fieldProjections = projection.getFieldProjections();
62
- const hiddenProperties = new Set(projection.getHiddenProperties());
63
-
64
- // Filter properties to only include visible ones and order by projection.
65
- const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name.toString()));
66
- const orderedProps: SchemaProperty[] = [];
67
-
68
- // Add properties in projection field order.
69
- for (const fieldProjection of fieldProjections) {
70
- const fieldPath = String(fieldProjection.field.path);
71
- const prop = visibleProps.find((p) => p.name === fieldPath);
72
- if (prop) {
73
- orderedProps.push(prop);
74
- }
112
+ // TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
113
+ const schemaProps = getFormProperties(schema.ast);
114
+ const filteredProps = exclude ? exclude(schemaProps) : schemaProps;
115
+
116
+ // Use projection-based field management when view and projection are available.
117
+ if (projection) {
118
+ const fieldProjections = projection.getFieldProjections();
119
+ const hiddenProperties = new Set(projection.getHiddenProperties());
120
+
121
+ // Filter properties to only include visible ones and order by projection.
122
+ const visibleProps = filteredProps.filter((prop) => !hiddenProperties.has(prop.name.toString()));
123
+ const orderedProps: SchemaProperty[] = [];
124
+
125
+ // Add properties in projection field order.
126
+ for (const fieldProjection of fieldProjections) {
127
+ const fieldPath = String(fieldProjection.field.path);
128
+ const prop = visibleProps.find((prop) => prop.name === fieldPath);
129
+ if (prop) {
130
+ orderedProps.push(prop);
75
131
  }
76
-
77
- // Add any remaining properties not in projection.
78
- const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
79
- const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
80
- orderedProps.push(...remainingProps);
81
- return orderedProps;
82
132
  }
83
133
 
84
- // Fallback to legacy filter/sort behavior.
85
- const filtered = exclude ? exclude(props) : props;
86
- return sort
87
- ? filtered.sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
88
- : filtered;
89
- }, [schema, values, exclude, sort, projection?.fields]);
90
-
91
- if ((readonly || layout === 'static') && values == null) {
92
- return null;
134
+ // Add any remaining properties not in projection.
135
+ const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
136
+ const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
137
+ orderedProps.push(...remainingProps);
138
+ return orderedProps;
93
139
  }
94
140
 
95
- return (
96
- <div
97
- role='form'
98
- className={mx('is-full', layout === 'inline' && 'flex flex-col gap-2', classNames)}
99
- ref={forwardRef}
100
- >
101
- {layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
102
- {properties
103
- .map((property) => {
104
- const name = property.name.toString();
105
- return (
106
- <FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
107
- <FormField
108
- type={property.type}
109
- name={name}
110
- path={[...(path ?? []), name]}
111
- readonly={readonly}
112
- layout={layout}
113
- projection={projection}
114
- {...props}
115
- />
116
- </FormFieldErrorBoundary>
117
- );
118
- })
119
- .filter(isTruthy)}
120
- </div>
121
- );
122
- },
123
- );
124
-
125
- FormFieldSet.displayName = 'Form.Fields';
141
+ // Fallback to legacy filter/sort behavior.
142
+ return sort
143
+ ? [...filteredProps].sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
144
+ : filteredProps;
145
+ }, [schema, values, exclude, sort, projection]);
146
+ };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import * as Option from 'effect/Option';
6
6
  import * as SchemaAST from 'effect/SchemaAST';
7
- import React, { Fragment, useCallback } from 'react';
7
+ import React, { useCallback } from 'react';
8
8
 
9
9
  import { findNode, getArrayElementType, getDiscriminatedType, isDiscriminatedUnion, isNestedType } from '@dxos/effect';
10
10
  import { IconButton, useTranslation } from '@dxos/react-ui';
@@ -77,10 +77,10 @@ export const ArrayField = ({
77
77
  <FormFieldLabel readonly={readonly} label={label} asChild />
78
78
  )}
79
79
 
80
- <div role='none' className='flex flex-col gap-2'>
80
+ <div role='none' className='flex flex-col gap-form-gap'>
81
81
  {values?.map((_, index) => {
82
82
  return (
83
- <div role='none' key={index} className='grid grid-cols-[1fr_min-content] gap-2 last:mb-3'>
83
+ <div role='none' key={index} className='flex gap-form-gap last:mb-form-gap items-center'>
84
84
  <FormField
85
85
  autoFocus={index === values.length - 1}
86
86
  type={elementType}
@@ -91,13 +91,13 @@ export const ArrayField = ({
91
91
  />
92
92
 
93
93
  {!readonly && layout !== 'static' && (
94
- <div role='none' className='flex flex-col bs-full justify-end'>
94
+ <div role='none' className='flex flex-col h-full justify-end'>
95
95
  {/* NOTE: Aligns with center of last field if multi-field object. */}
96
- <div role='none' className='flex items-center bs-[var(--line-height)]'>
96
+ <div role='none' className='flex items-center h-[2rem]'>
97
97
  <IconButton
98
98
  icon='ph--x--regular'
99
99
  iconOnly
100
- label={t('button remove')}
100
+ label={t('remove.button')}
101
101
  onClick={() => handleDelete(index)}
102
102
  classNames='self-center'
103
103
  />
@@ -112,9 +112,9 @@ export const ArrayField = ({
112
112
  {/* TODO(burdon): Get label from schema. */}
113
113
  {!readonly && layout !== 'static' && (
114
114
  <IconButton
115
- classNames='flex is-full _mlb-cardSpacingBlock'
115
+ classNames='flex w-full'
116
116
  icon='ph--plus--regular'
117
- label={t('add field')}
117
+ label={t('add-field.button')}
118
118
  onClick={handleAdd}
119
119
  />
120
120
  )}
@@ -140,6 +140,9 @@ export const getDefaultValue = (ast?: SchemaAST.AST): any => {
140
140
  case 'BooleanKeyword': {
141
141
  return false;
142
142
  }
143
+ case 'Suspend': {
144
+ return getDefaultValue(ast.f());
145
+ }
143
146
  default: {
144
147
  if (ast && isNestedType(ast)) {
145
148
  return {};
@@ -62,17 +62,17 @@ export const GeoPointField = ({
62
62
  {layout === 'static' ? (
63
63
  <LatLng {...value} />
64
64
  ) : (
65
- <div role='none' className='grid grid-cols-2 gap-2'>
65
+ <div role='none' className='grid grid-cols-2 gap-form-gap'>
66
66
  <div>
67
67
  <Input.Root>
68
- {layout !== 'inline' && <Input.Label>{t('latitude label')}</Input.Label>}
68
+ {layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
69
69
  <Input.TextInput
70
70
  type='number'
71
71
  step='0.00001'
72
72
  min='-90'
73
73
  max='90'
74
74
  disabled={!!readonly}
75
- placeholder={t('latitude placeholder')}
75
+ placeholder={t('latitude.placeholder')}
76
76
  value={latitudeText ?? ''}
77
77
  onChange={handleChange('latitude', setLatitudeText)}
78
78
  onBlur={onBlur}
@@ -81,14 +81,14 @@ export const GeoPointField = ({
81
81
  </div>
82
82
  <div>
83
83
  <Input.Root>
84
- {layout !== 'inline' && <Input.Label>{t('longitude label')}</Input.Label>}
84
+ {layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
85
85
  <Input.TextInput
86
86
  type='number'
87
87
  step='0.00001'
88
88
  min='-180'
89
89
  max='180'
90
90
  disabled={!!readonly}
91
- placeholder={t('longitude placeholder')}
91
+ placeholder={t('longitude.placeholder')}
92
92
  value={longitudeText ?? ''}
93
93
  onChange={handleChange('longitude', setLongitudeText)}
94
94
  onBlur={onBlur}
@@ -106,7 +106,7 @@ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
106
106
  const lngHem = longitude >= 0 ? 'E' : 'W';
107
107
 
108
108
  return (
109
- <span className='inline-flex items-center gap-1'>
109
+ <span className='inline-flex items-center gap-form-gap'>
110
110
  <span>
111
111
  <span>{Math.abs(latitude).toFixed(5)}</span>
112
112
  <span className='text-subdued'>°{latHem}</span>
@@ -65,7 +65,7 @@ export const MarkdownField = ({
65
65
  disabled={!!readonly}
66
66
  placeholder={placeholder}
67
67
  value={value ?? ''}
68
- classNames={'min-bs-auto max-bs-40 overflow-auto'}
68
+ classNames={'min-h-auto max-h-40 overflow-auto'}
69
69
  onChange={handleChange}
70
70
  onBlur={onBlur}
71
71
  style={{ resize: 'none' }}
@@ -2,18 +2,18 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos/lit-ui/dx-tag-picker.pcss';
6
-
7
5
  import React, { useCallback, useMemo } from 'react';
8
6
 
9
- import { type Database, Entity, Filter, Ref, type Type } from '@dxos/echo';
10
- import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
7
+ import '@dxos/lit-ui/dx-tag-picker.pcss';
8
+ import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
11
9
  import { useQuery, useSchema as useSchema$ } from '@dxos/echo-react';
10
+ import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
12
11
  import { findAnnotation } from '@dxos/effect';
13
12
  import { DXN } from '@dxos/keys';
14
13
  import { DxAnchor } from '@dxos/lit-ui/react';
15
14
  import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
16
- import { descriptionText, mx } from '@dxos/ui-theme';
15
+ import { ParentLabelAnnotationId } from '@dxos/schema';
16
+ import { mx } from '@dxos/ui-theme';
17
17
 
18
18
  import { translationKey } from '../../../translations';
19
19
  import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
@@ -25,23 +25,35 @@ const isRefSnapshot = (val: any): val is { '/': string } => {
25
25
  return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
26
26
  };
27
27
 
28
- const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results) =>
28
+ const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
29
29
  results.map((result) => {
30
30
  const id = Entity.getDXN(result).toString();
31
- const label = Entity.getLabel(result);
31
+ const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
32
+ const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
32
33
  return { id, label: label ?? id };
33
34
  });
34
35
 
35
36
  const defaultResultsHook: NonNullable<RefFieldProps['resultsHook']> = (db, typename) =>
36
- useQuery(db, typename ? Filter.typename(typename) : Filter.nothing());
37
+ useQuery(
38
+ db,
39
+ typename
40
+ ? // For Ref.Ref(Obj.Unknown) we want to show all objects.
41
+ typename === Type.getTypename(Obj.Unknown)
42
+ ? Filter.everything()
43
+ : Filter.typename(typename)
44
+ : Filter.nothing(),
45
+ );
37
46
 
38
47
  export type RefFieldProps = FormFieldComponentProps &
39
- Pick<ObjectPickerContentProps, 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath'> & {
48
+ Pick<
49
+ ObjectPickerContentProps,
50
+ 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
51
+ > & {
40
52
  db?: Database.Database;
41
53
  resultsHook?: (db?: Database.Database, typename?: string) => Entity.Any[];
42
- schemaHook?: (db?: Database.Database, typename?: string) => Type.Entity.Any;
43
- getOptions?: (objects: Entity.Any[]) => RefOption[];
44
- onCreate?: (schema: Type.Entity.Any, values: any) => void;
54
+ schemaHook?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
55
+ getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
56
+ onCreate?: (schema: Type.AnyEntity, values: any) => void;
45
57
  };
46
58
 
47
59
  export const RefField = (props: RefFieldProps) => {
@@ -56,6 +68,7 @@ export const RefField = (props: RefFieldProps) => {
56
68
  createOptionLabel,
57
69
  createOptionIcon,
58
70
  createInitialValuePath,
71
+ createFieldMap,
59
72
  db,
60
73
  resultsHook: useResults = defaultResultsHook,
61
74
  schemaHook: useSchema = useSchema$,
@@ -72,7 +85,10 @@ export const RefField = (props: RefFieldProps) => {
72
85
  );
73
86
 
74
87
  const results = useResults(db, typename);
75
- const options = useMemo(() => getOptions(results), [results, getOptions]);
88
+ const options = useMemo(() => {
89
+ const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
90
+ return getOptions(results, { parentLabel });
91
+ }, [results, getOptions, type]);
76
92
 
77
93
  const handleGetValue = useCallback(() => {
78
94
  const formValue = getValue();
@@ -135,9 +151,9 @@ export const RefField = (props: RefFieldProps) => {
135
151
  <div>
136
152
  {readonly ? (
137
153
  !item ? (
138
- <p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
154
+ <p className={mx('text-description', 'mb-2')}>{t('empty-readonly-ref-field.label')}</p>
139
155
  ) : (
140
- <DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
156
+ <DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
141
157
  {item.label}
142
158
  </DxAnchor>
143
159
  )
@@ -145,15 +161,15 @@ export const RefField = (props: RefFieldProps) => {
145
161
  <ObjectPicker.Root>
146
162
  <ObjectPicker.Trigger asChild classNames='p-0'>
147
163
  {item ? (
148
- <div className='flex gap-2 is-full'>
164
+ <div className='flex gap-form-gap w-full'>
149
165
  <Input.Root key={item.id}>
150
- <Input.TextInput value={item.label} readOnly classNames='is-full' />
166
+ <Input.TextInput value={item.label} readOnly classNames='w-full' />
151
167
  </Input.Root>
152
168
  </div>
153
169
  ) : (
154
- <Button classNames='is-full text-start gap-2'>
170
+ <Button classNames='w-full text-start gap-form-gap'>
155
171
  <div role='none' className='grow overflow-hidden'>
156
- <span className='flex truncate text-description'>{placeholder ?? t('ref field placeholder')}</span>
172
+ <span className='flex truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
157
173
  </div>
158
174
  <Icon size={3} icon='ph--caret-down--bold' />
159
175
  </Button>
@@ -161,13 +177,14 @@ export const RefField = (props: RefFieldProps) => {
161
177
  </ObjectPicker.Trigger>
162
178
  <ObjectPicker.Portal>
163
179
  <ObjectPicker.Content
164
- classNames='popover-card-width'
180
+ classNames='dx-card-popover-width'
165
181
  options={options}
166
182
  selectedIds={selectedIds}
167
183
  createSchema={createSchema && omitId(createSchema)}
168
184
  createOptionLabel={createOptionLabel}
169
185
  createOptionIcon={createOptionIcon}
170
186
  createInitialValuePath={createInitialValuePath}
187
+ createFieldMap={createFieldMap}
171
188
  onCreate={handleCreate}
172
189
  onSelect={handleSelect}
173
190
  />
@@ -43,7 +43,7 @@ export const SelectField = ({
43
43
  ) : (
44
44
  <Select.Root value={value} onValueChange={handleValueChange}>
45
45
  {/* TODO(burdon): Placeholder not working? */}
46
- <Select.TriggerButton classNames='is-full' disabled={!!readonly} placeholder={placeholder} />
46
+ <Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
47
47
  <Select.Portal>
48
48
  <Select.Content>
49
49
  <Select.Viewport>
@@ -7,12 +7,11 @@ import * as Schema from 'effect/Schema';
7
7
  import React, { useState } from 'react';
8
8
 
9
9
  import { SelectOption } from '@dxos/echo/internal';
10
- import { withTheme } from '@dxos/react-ui/testing';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
 
12
12
  import { translations } from '../../../translations';
13
13
  import { TestLayout } from '../../testing';
14
14
  import { Form } from '../Form';
15
-
16
15
  import { SelectOptionField } from './SelectOptionField';
17
16
 
18
17
  const schema = Schema.Struct({
@@ -50,7 +49,7 @@ const meta = {
50
49
  title: 'ui/react-ui-form/SelectOptionField',
51
50
  component: SelectOptionField as any,
52
51
  render: DefaultStory,
53
- decorators: [withTheme],
52
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
54
53
  parameters: {
55
54
  layout: 'fullscreen',
56
55
  translations,
@@ -128,13 +128,13 @@ export const SelectOptionField = ({
128
128
  readonly={!!readonly}
129
129
  >
130
130
  {({ items }) => (
131
- <div role='list' className='is-full overflow-auto'>
131
+ <div role='list' className='w-full overflow-auto'>
132
132
  {items?.map((item) => (
133
133
  <div key={item.id}>
134
134
  <List.Item
135
135
  role='button'
136
136
  item={item}
137
- classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-sm']}
137
+ classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
138
138
  aria-expanded={selected === item.id}
139
139
  >
140
140
  <div className='flex items-center'>
@@ -152,13 +152,12 @@ export const SelectOptionField = ({
152
152
  />
153
153
  </div>
154
154
  {selected === item.id && (
155
- <div className='pli-cardSpacingInline plb-cardSpacingBlock flex flex-col gap-1 density-fine'>
156
- {/* 16px to match drag handle width. */}
157
- <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
158
- <div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
155
+ <div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
156
+ <Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
157
+ <div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
159
158
  <Input.TextInput
160
159
  disabled={!!readonly}
161
- placeholder={t('select option label placeholder')}
160
+ placeholder={t('select-option-label.placeholder')}
162
161
  ref={selected === item.id ? inputRef : undefined}
163
162
  value={item.title}
164
163
  onChange={handleTitleChange(item.id)}
@@ -170,7 +169,7 @@ export const SelectOptionField = ({
170
169
  disabled={!!readonly}
171
170
  icon='ph--trash--fill'
172
171
  iconOnly
173
- label={t('select option delete')}
172
+ label={t('select-option-delete.button')}
174
173
  onClick={() => handleDelete(item.id)}
175
174
  />
176
175
  </div>
@@ -182,10 +181,10 @@ export const SelectOptionField = ({
182
181
  <IconButton
183
182
  variant='ghost'
184
183
  icon='ph--plus--regular'
185
- label={t('select option add')}
184
+ label={t('select-option-add.button')}
186
185
  onClick={handleAdd}
187
186
  disabled={!!readonly}
188
- classNames='is-full'
187
+ classNames='w-full'
189
188
  />
190
189
  </div>
191
190
  )}
@@ -32,7 +32,7 @@ export const TupleField = ({
32
32
  return (
33
33
  <Input.Root validationValence={status}>
34
34
  <FormFieldLabel error={error} readonly={readonly} label={label} />
35
- <div className={mx('grid gap-2', gridCols[binding.length - 1])}>
35
+ <div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
36
36
  {binding.map((prop) => (
37
37
  <Input.TextInput
38
38
  key={prop}