@dxos/react-ui-form 0.8.2-main.fbd8ed0 → 0.8.2-staging.42af850

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 (54) hide show
  1. package/dist/lib/browser/index.mjs +1445 -1245
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +1431 -1231
  5. package/dist/lib/node/index.cjs.map +3 -3
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +1445 -1245
  8. package/dist/lib/node-esm/index.mjs.map +3 -3
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/ControlSection/ControlSection.d.ts +1 -1
  11. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts +2 -0
  14. package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts.map +1 -0
  15. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Defaults.d.ts +4 -4
  17. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +1 -2
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormActions.d.ts +1 -1
  22. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormContent.d.ts +6 -5
  24. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  27. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  28. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -3
  29. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/testing.d.ts.map +1 -1
  31. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
  32. package/dist/types/src/translations.d.ts +5 -0
  33. package/dist/types/src/translations.d.ts.map +1 -1
  34. package/dist/types/src/vitest-setup.d.ts +2 -0
  35. package/dist/types/src/vitest-setup.d.ts.map +1 -0
  36. package/package.json +27 -25
  37. package/src/components/ControlSection/ControlSection.tsx +2 -2
  38. package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
  39. package/src/components/FieldEditor/FieldEditor.tsx +3 -2
  40. package/src/components/Form/ArrayField.tsx +51 -34
  41. package/src/components/Form/Defaults.tsx +74 -48
  42. package/src/components/Form/Form.stories.tsx +77 -101
  43. package/src/components/Form/Form.tsx +1 -2
  44. package/src/components/Form/FormActions.tsx +15 -18
  45. package/src/components/Form/FormContent.tsx +34 -35
  46. package/src/components/Form/FormContext.tsx +11 -7
  47. package/src/components/Form/Input.tsx +1 -1
  48. package/src/components/Form/RefField.tsx +30 -10
  49. package/src/components/ViewEditor/ViewEditor.stories.tsx +14 -5
  50. package/src/components/ViewEditor/ViewEditor.tsx +51 -24
  51. package/src/components/testing.tsx +5 -9
  52. package/src/hooks/useRefQueryLookupHandler.ts +1 -3
  53. package/src/translations.ts +9 -2
  54. package/src/vitest-setup.ts +11 -0
@@ -5,14 +5,15 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
- import { Schema, SchemaAST } from 'effect';
8
+ import { Schema } from 'effect';
9
9
  import React, { useCallback, useState } from 'react';
10
10
 
11
11
  import { ContactType } from '@dxos/client/testing';
12
12
  import { type BaseObject, Expando, Format, getDXN, Ref, type TypeAnnotation } from '@dxos/echo-schema';
13
13
  import { live } from '@dxos/live-object';
14
+ import { withSurfaceVariantsLayout } from '@dxos/react-ui/testing';
14
15
  import { Testing } from '@dxos/schema/testing';
15
- import { withLayout, withTheme } from '@dxos/storybook-utils';
16
+ import { withTheme } from '@dxos/storybook-utils';
16
17
 
17
18
  import { SelectInput } from './Defaults';
18
19
  import { Form, type FormProps } from './Form';
@@ -20,20 +21,18 @@ import translations from '../../translations';
20
21
  import { TestLayout, TestPanel } from '../testing';
21
22
 
22
23
  const AddressSchema = Schema.Struct({
23
- street: Schema.optional(Schema.String.annotations({ [SchemaAST.TitleAnnotationId]: 'Street' })),
24
- city: Schema.optional(Schema.String.annotations({ [SchemaAST.TitleAnnotationId]: 'City' })),
25
- zip: Schema.optional(
26
- Schema.String.pipe(Schema.pattern(/^\d{5}(-\d{4})?$/)).annotations({ [SchemaAST.TitleAnnotationId]: 'ZIP' }),
27
- ),
28
- location: Schema.optional(Format.GeoPoint.annotations({ [SchemaAST.TitleAnnotationId]: 'Location' })),
29
- }).annotations({ [SchemaAST.TitleAnnotationId]: 'Address' });
24
+ street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
25
+ city: Schema.optional(Schema.String.annotations({ title: 'City' })),
26
+ zip: Schema.optional(Schema.String.pipe(Schema.pattern(/^\d{5}(-\d{4})?$/)).annotations({ title: 'ZIP' })),
27
+ location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
28
+ }).annotations({ title: 'Address' });
30
29
 
31
30
  // TODO(burdon): Translations?
32
31
  const TestSchema = Schema.Struct({
33
- name: Schema.optional(Schema.String.annotations({ [SchemaAST.TitleAnnotationId]: 'Name' })),
34
- active: Schema.optional(Schema.Boolean.annotations({ [SchemaAST.TitleAnnotationId]: 'Active' })),
35
- rank: Schema.optional(Schema.Number.annotations({ [SchemaAST.TitleAnnotationId]: 'Rank' })),
36
- website: Schema.optional(Format.URL.annotations({ [SchemaAST.TitleAnnotationId]: 'Website' })),
32
+ name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
33
+ active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
34
+ rank: Schema.optional(Schema.Number.annotations({ title: 'Rank' })),
35
+ website: Schema.optional(Format.URL.annotations({ title: 'Website' })),
37
36
  address: Schema.optional(AddressSchema),
38
37
  }).pipe(Schema.mutable);
39
38
 
@@ -47,6 +46,15 @@ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...
47
46
  setValues(values);
48
47
  }, []);
49
48
 
49
+ return <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />;
50
+ };
51
+
52
+ const DebugStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
53
+ const [values, setValues] = useState(initialValues);
54
+ const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
55
+ setValues(values);
56
+ }, []);
57
+
50
58
  return (
51
59
  <TestLayout json={{ values, schema: schema.ast.toJSON() }}>
52
60
  <TestPanel>
@@ -59,11 +67,17 @@ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...
59
67
  const meta: Meta<StoryProps<any>> = {
60
68
  title: 'ui/react-ui-form/Form',
61
69
  component: Form,
62
- render: DefaultStory,
63
- decorators: [withLayout({ fullscreen: true }), withTheme],
70
+ render: DebugStory,
71
+ decorators: [withTheme],
64
72
  parameters: {
65
73
  translations,
66
74
  },
75
+ argTypes: {
76
+ readonly: {
77
+ control: 'boolean',
78
+ description: 'Readonly',
79
+ },
80
+ },
67
81
  };
68
82
 
69
83
  export default meta;
@@ -71,6 +85,8 @@ export default meta;
71
85
  type Story<T extends BaseObject> = StoryObj<StoryProps<T>>;
72
86
 
73
87
  export const Default: Story<TestType> = {
88
+ render: DefaultStory,
89
+ decorators: [withSurfaceVariantsLayout(), withTheme],
74
90
  args: {
75
91
  schema: TestSchema,
76
92
  values: {
@@ -80,6 +96,7 @@ export const Default: Story<TestType> = {
80
96
  zip: '11205',
81
97
  },
82
98
  },
99
+ readonly: false,
83
100
  },
84
101
  };
85
102
 
@@ -88,8 +105,9 @@ export const Organization: Story<Testing.Organization> = {
88
105
  schema: Testing.OrganizationSchema,
89
106
  values: {
90
107
  name: 'DXOS',
91
- // website: 'https://dxos.org',
108
+ website: 'https://dxos.org',
92
109
  },
110
+ readonly: false,
93
111
  },
94
112
  };
95
113
 
@@ -98,9 +116,10 @@ export const OrganizationAutoSave: Story<Testing.Organization> = {
98
116
  schema: Testing.OrganizationSchema,
99
117
  values: {
100
118
  name: 'DXOS',
101
- // website: 'https://dxos.org',
119
+ website: 'https://dxos.org',
102
120
  },
103
121
  autoSave: true,
122
+ readonly: false,
104
123
  },
105
124
  };
106
125
 
@@ -116,70 +135,44 @@ export const OrganizationAutoSave: Story<Testing.Organization> = {
116
135
  // },
117
136
  // };
118
137
 
119
- //
120
- // TODO(burdon): Move into separate storybook and use test types.
121
- //
122
-
123
138
  const ShapeSchema = Schema.Struct({
124
139
  shape: Schema.optional(
125
140
  Schema.Union(
126
141
  Schema.Struct({
127
- type: Schema.Literal('circle').annotations({ [SchemaAST.TitleAnnotationId]: 'Type' }),
128
- radius: Schema.optional(Schema.Number.annotations({ [SchemaAST.TitleAnnotationId]: 'Radius' })),
142
+ type: Schema.Literal('circle').annotations({ title: 'Type' }),
143
+ radius: Schema.optional(Schema.Number.annotations({ title: 'Radius' })),
129
144
  }),
130
145
  Schema.Struct({
131
- type: Schema.Literal('square').annotations({ [SchemaAST.TitleAnnotationId]: 'Type' }),
132
- size: Schema.optional(
133
- Schema.Number.pipe(Schema.nonNegative()).annotations({ [SchemaAST.TitleAnnotationId]: 'Size' }),
134
- ),
146
+ type: Schema.Literal('square').annotations({ title: 'Type' }),
147
+ size: Schema.optional(Schema.Number.pipe(Schema.nonNegative()).annotations({ title: 'Size' })),
135
148
  }),
136
- ).annotations({ [SchemaAST.TitleAnnotationId]: 'Shape' }),
149
+ ).annotations({ title: 'Shape' }),
137
150
  ),
138
151
  }).pipe(Schema.mutable);
139
152
 
140
153
  type ShapeType = Schema.Schema.Type<typeof ShapeSchema>;
141
154
 
142
- type DiscriminatedUnionStoryProps = FormProps<ShapeType>;
143
-
144
- const DiscriminatedUnionStory = ({ values: initialValues }: DiscriminatedUnionStoryProps) => {
145
- const [values, setValues] = useState(initialValues);
146
- const handleSave = useCallback<NonNullable<FormProps<ShapeType>['onSave']>>((values) => {
147
- setValues(values);
148
- }, []);
149
-
150
- return (
151
- <TestLayout json={{ values, schema: ShapeSchema.ast.toJSON() }}>
152
- <TestPanel>
153
- <Form<ShapeType>
154
- schema={ShapeSchema}
155
- values={values}
156
- onSave={handleSave}
157
- Custom={{
158
- ['shape.type' as const]: (props) => (
159
- <SelectInput
160
- {...props}
161
- options={['circle', 'square'].map((value) => ({
162
- value,
163
- label: value,
164
- }))}
165
- />
166
- ),
167
- }}
168
- />
169
- </TestPanel>
170
- </TestLayout>
171
- );
172
- };
173
-
174
- export const DiscriminatedShape: StoryObj<DiscriminatedUnionStoryProps> = {
175
- render: DiscriminatedUnionStory,
155
+ export const DiscriminatedShape: Story<ShapeType> = {
176
156
  args: {
157
+ schema: ShapeSchema,
158
+ readonly: false,
177
159
  values: {
178
160
  shape: {
179
161
  type: 'circle',
180
162
  radius: 5,
181
163
  },
182
164
  },
165
+ Custom: {
166
+ ['shape.type' as const]: (props) => (
167
+ <SelectInput
168
+ {...props}
169
+ options={['circle', 'square'].map((value) => ({
170
+ value,
171
+ label: value,
172
+ }))}
173
+ />
174
+ ),
175
+ },
183
176
  },
184
177
  };
185
178
 
@@ -190,24 +183,10 @@ const ArraysSchema = Schema.Struct({
190
183
 
191
184
  type ArraysType = Schema.Schema.Type<typeof ArraysSchema>;
192
185
 
193
- const ArraysStory = ({ values: initialValues }: FormProps<ArraysType>) => {
194
- const [values, setValues] = useState(initialValues);
195
- const handleSave = useCallback<NonNullable<FormProps<ArraysType>['onSave']>>((values) => {
196
- setValues(values);
197
- }, []);
198
-
199
- return (
200
- <TestLayout json={{ values, schema: ArraysSchema.ast.toJSON() }}>
201
- <TestPanel>
202
- <Form<ArraysType> schema={ArraysSchema} values={values} onSave={handleSave} />
203
- </TestPanel>
204
- </TestLayout>
205
- );
206
- };
207
-
208
186
  export const Arrays: StoryObj<FormProps<ArraysType>> = {
209
- render: ArraysStory,
210
187
  args: {
188
+ schema: ArraysSchema,
189
+ readonly: false,
211
190
  values: {
212
191
  names: ['Alice', 'Bob'],
213
192
  addresses: [],
@@ -217,30 +196,16 @@ export const Arrays: StoryObj<FormProps<ArraysType>> = {
217
196
 
218
197
  const ColorSchema = Schema.Struct({
219
198
  color: Schema.Union(Schema.Literal('red'), Schema.Literal('green'), Schema.Literal('blue')).annotations({
220
- [SchemaAST.TitleAnnotationId]: 'Color',
199
+ title: 'Color',
221
200
  }),
222
201
  }).pipe(Schema.mutable);
223
202
 
224
203
  type ColorType = Schema.Schema.Type<typeof ColorSchema>;
225
204
 
226
- const EnumStory = ({ values: initialValues }: FormProps<ColorType>) => {
227
- const [values, setValues] = useState(initialValues);
228
- const handleSave = useCallback<NonNullable<FormProps<ColorType>['onSave']>>((values) => {
229
- setValues(values);
230
- }, []);
231
-
232
- return (
233
- <TestLayout json={{ values, schema: ColorSchema.ast.toJSON() }}>
234
- <TestPanel>
235
- <Form<ColorType> schema={ColorSchema} values={values} onSave={handleSave} />
236
- </TestPanel>
237
- </TestLayout>
238
- );
239
- };
240
-
241
205
  export const Enum: StoryObj<FormProps<ColorType>> = {
242
- render: EnumStory,
243
206
  args: {
207
+ schema: ColorSchema,
208
+ readonly: false,
244
209
  values: {
245
210
  color: 'red',
246
211
  },
@@ -254,15 +219,15 @@ const RefSchema = Schema.Struct({
254
219
  unknownExpando: Schema.optional(Ref(Expando).annotations({ title: 'Optional Ref to an Expando (DXN Input)' })),
255
220
  });
256
221
 
257
- const RefStory = ({ values: initialValues }: FormProps<any>) => {
222
+ const contact1 = live(ContactType, { identifiers: [] });
223
+ const contact2 = live(ContactType, { identifiers: [] });
224
+
225
+ const RefStory = ({ values: initialValues, readonly }: FormProps<any>) => {
258
226
  const [values, setValues] = useState(initialValues);
259
227
  const handleSave = useCallback<NonNullable<FormProps<any>['onSave']>>((values) => {
260
228
  setValues(values);
261
229
  }, []);
262
230
 
263
- const contact1 = live(ContactType, { identifiers: [] });
264
- const contact2 = live(ContactType, { identifiers: [] });
265
-
266
231
  const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
267
232
  switch (typeInfo.typename) {
268
233
  case ContactType.typename:
@@ -278,7 +243,13 @@ const RefStory = ({ values: initialValues }: FormProps<any>) => {
278
243
  return (
279
244
  <TestLayout json={{ values, schema: RefSchema.ast.toJSON() }}>
280
245
  <TestPanel>
281
- <Form schema={RefSchema} values={values} onSave={handleSave} onQueryRefOptions={onQueryRefOptions} />
246
+ <Form
247
+ schema={RefSchema}
248
+ values={values}
249
+ readonly={readonly}
250
+ onSave={handleSave}
251
+ onQueryRefOptions={onQueryRefOptions}
252
+ />
282
253
  </TestPanel>
283
254
  </TestLayout>
284
255
  );
@@ -286,5 +257,10 @@ const RefStory = ({ values: initialValues }: FormProps<any>) => {
286
257
 
287
258
  export const Refs: StoryObj<FormProps<ContactType>> = {
288
259
  render: RefStory,
289
- args: { values: {} },
260
+ args: {
261
+ readonly: false,
262
+ values: {
263
+ refArray: [Ref.make(contact1), Ref.make(contact2)],
264
+ } as any,
265
+ },
290
266
  };
@@ -95,8 +95,7 @@ export const Form = <T extends BaseObject>({
95
95
  onValid={onValid}
96
96
  onSave={onSave}
97
97
  >
98
- {/* TODO(burdon): Remove padding. */}
99
- <div ref={formRef} role='none' className={mx('p-2', classNames)} data-testid={testId}>
98
+ <div ref={formRef} role='none' className={mx('flex flex-col gap-4', classNames)} data-testid={testId}>
100
99
  <FormFields
101
100
  schema={schema}
102
101
  path={path}
@@ -5,7 +5,6 @@
5
5
  import React from 'react';
6
6
 
7
7
  import { IconButton, useTranslation } from '@dxos/react-ui';
8
- import { mx } from '@dxos/react-ui-theme';
9
8
 
10
9
  import { useFormContext } from './FormContext';
11
10
  import { translationKey } from '../../translations';
@@ -15,27 +14,25 @@ export type FormActionsProps = {
15
14
  onCancel?: () => void;
16
15
  };
17
16
 
18
- export const FormActions = ({ onCancel, readonly }: FormActionsProps) => {
17
+ export const FormActions = ({ readonly, onCancel }: FormActionsProps) => {
19
18
  const { t } = useTranslation(translationKey);
20
19
  const { canSave, handleSave } = useFormContext();
21
20
 
22
21
  return (
23
- <div role='none' className='mt-2 flex justify-center'>
24
- <div role='none' className={mx(onCancel && !readonly && 'grid grid-cols-2 gap-2')}>
25
- {onCancel && !readonly && (
26
- <IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
27
- )}
28
- {handleSave && (
29
- <IconButton
30
- type='submit'
31
- data-testid='save-button'
32
- disabled={!canSave}
33
- icon='ph--check--regular'
34
- label={t('button save')}
35
- onClick={handleSave}
36
- />
37
- )}
38
- </div>
22
+ <div role='none' className='flex [&_button]:grow gap-1'>
23
+ {onCancel && !readonly && (
24
+ <IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
25
+ )}
26
+ {handleSave && (
27
+ <IconButton
28
+ type='submit'
29
+ data-testid='save-button'
30
+ disabled={!canSave}
31
+ icon='ph--check--regular'
32
+ label={t('button save')}
33
+ onClick={handleSave}
34
+ />
35
+ )}
39
36
  </div>
40
37
  );
41
38
  };
@@ -7,6 +7,7 @@ import { capitalize } from 'effect/String';
7
7
  import React, { useMemo } from 'react';
8
8
 
9
9
  import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
10
+ import { type ThemedClassName } from '@dxos/react-ui';
10
11
  import { mx } from '@dxos/react-ui-theme';
11
12
  import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
12
13
  import { isNotFalsy } from '@dxos/util';
@@ -68,7 +69,7 @@ export const FormField = ({
68
69
  });
69
70
 
70
71
  if (FoundComponent) {
71
- return <div role='none'>{FoundComponent}</div>;
72
+ return FoundComponent;
72
73
  }
73
74
 
74
75
  const jsonPath = createJsonPath(path ?? []);
@@ -124,17 +125,15 @@ export const FormField = ({
124
125
  const InputComponent = getInputComponent(type, format);
125
126
  if (InputComponent) {
126
127
  return (
127
- <div role='none'>
128
- <InputComponent
129
- type={type}
130
- format={format}
131
- label={label}
132
- inputOnly={inline}
133
- placeholder={placeholder}
134
- disabled={readonly}
135
- {...inputProps}
136
- />
137
- </div>
128
+ <InputComponent
129
+ type={type}
130
+ format={format}
131
+ label={label}
132
+ inputOnly={inline}
133
+ placeholder={placeholder}
134
+ disabled={readonly}
135
+ {...inputProps}
136
+ />
138
137
  );
139
138
  }
140
139
 
@@ -144,18 +143,16 @@ export const FormField = ({
144
143
 
145
144
  if (options) {
146
145
  return (
147
- <div role='none'>
148
- <SelectInput
149
- type={type}
150
- format={format}
151
- disabled={readonly}
152
- inputOnly={inline}
153
- label={label}
154
- options={options.map((option) => ({ value: option, label: String(option) }))}
155
- placeholder={placeholder}
156
- {...inputProps}
157
- />
158
- </div>
146
+ <SelectInput
147
+ type={type}
148
+ format={format}
149
+ disabled={readonly}
150
+ inputOnly={inline}
151
+ label={label}
152
+ options={options.map((option) => ({ value: option, label: String(option) }))}
153
+ placeholder={placeholder}
154
+ {...inputProps}
155
+ />
159
156
  );
160
157
  }
161
158
 
@@ -171,8 +168,8 @@ export const FormField = ({
171
168
 
172
169
  if (typeLiteral) {
173
170
  return (
174
- <div role='none'>
175
- {!inline && <h3 className='text-lg mbs-2 mbe-1'>{label}</h3>}
171
+ <>
172
+ {!inline && <h3 className='text-lg mbs-2 mbe-1 first:mbs-0'>{label}</h3>}
176
173
  <FormFields
177
174
  schema={Schema.make(typeLiteral)}
178
175
  path={path}
@@ -181,7 +178,7 @@ export const FormField = ({
181
178
  Custom={Custom}
182
179
  lookupComponent={lookupComponent}
183
180
  />
184
- </div>
181
+ </>
185
182
  );
186
183
  }
187
184
  }
@@ -189,27 +186,28 @@ export const FormField = ({
189
186
  return null;
190
187
  };
191
188
 
192
- export type FormContentProps = {
189
+ export type FormFieldsProps = ThemedClassName<{
190
+ Custom?: Partial<Record<string, InputComponent>>;
193
191
  schema: Schema.Schema.All;
194
192
  path?: (string | number)[];
195
193
  filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
196
194
  sort?: string[];
197
195
  readonly?: boolean;
198
- onQueryRefOptions?: QueryRefOptions;
199
196
  lookupComponent?: ComponentLookup;
200
- Custom?: Partial<Record<string, InputComponent>>;
201
- };
197
+ onQueryRefOptions?: QueryRefOptions;
198
+ }>;
202
199
 
203
200
  export const FormFields = ({
201
+ classNames,
202
+ Custom,
204
203
  schema,
205
204
  path,
206
205
  filter,
207
206
  sort,
208
207
  readonly,
209
- onQueryRefOptions,
210
208
  lookupComponent,
211
- Custom,
212
- }: FormContentProps) => {
209
+ onQueryRefOptions,
210
+ }: FormFieldsProps) => {
213
211
  const values = useFormValues(path);
214
212
 
215
213
  const properties = useMemo(() => {
@@ -220,7 +218,8 @@ export const FormFields = ({
220
218
  }, [schema, values, filter, sort]);
221
219
 
222
220
  return (
223
- <div role='form' className={mx('flex flex-col w-full gap-2')}>
221
+ // TODO(burdon): Consistent padding (with composite forms).
222
+ <div role='form' className={mx('p-2 is-full', classNames)}>
224
223
  {properties
225
224
  .map((property) => {
226
225
  return (
@@ -10,6 +10,7 @@ import React, {
10
10
  useMemo,
11
11
  type FocusEvent,
12
12
  type PropsWithChildren,
13
+ useCallback,
13
14
  } from 'react';
14
15
 
15
16
  import { raise } from '@dxos/debug';
@@ -67,12 +68,8 @@ export const FormProvider = ({
67
68
  >) => {
68
69
  const form = useForm(formOptions);
69
70
 
70
- useEffect(() => {
71
- if (!formRef?.current) {
72
- return;
73
- }
74
-
75
- const handleKeyDown = (event: KeyboardEvent) => {
71
+ const handleKeyDown = useCallback(
72
+ (event: KeyboardEvent) => {
76
73
  const keyIsEnter = event.key === 'Enter';
77
74
  const modifierUsed = event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
78
75
  const inputIsTextarea = (event.target as HTMLElement).tagName.toLowerCase() === 'textarea';
@@ -94,7 +91,14 @@ export const FormProvider = ({
94
91
  (event.target as HTMLElement).blur();
95
92
  }
96
93
  }
97
- };
94
+ },
95
+ [form, autoSave],
96
+ );
97
+
98
+ useEffect(() => {
99
+ if (!formRef?.current) {
100
+ return;
101
+ }
98
102
 
99
103
  const formElement = formRef.current;
100
104
 
@@ -34,7 +34,7 @@ export type InputHeaderProps = PropsWithChildren<{
34
34
 
35
35
  export const InputHeader = ({ children, error }: InputHeaderProps) => {
36
36
  return (
37
- <div role='none' className='flex justify-between items-center mb-1'>
37
+ <div role='none' className='flex justify-between items-center mbs-2 mbe-1 first:mbs-0'>
38
38
  {children}
39
39
  {error && (
40
40
  <Tooltip.Trigger asChild content={error} side='bottom'>
@@ -14,13 +14,16 @@ import {
14
14
  } from '@dxos/echo-schema';
15
15
  import { findAnnotation } from '@dxos/effect';
16
16
  import { DXN } from '@dxos/keys';
17
- import { Input } from '@dxos/react-ui';
17
+ import { DxRefTag } from '@dxos/lit-ui/react';
18
+ import { Input, useTranslation } from '@dxos/react-ui';
18
19
  import { TagPicker, type TagPickerMode, type TagPickerItemData } from '@dxos/react-ui-tag-picker';
20
+ import { descriptionText, mx } from '@dxos/react-ui-theme';
19
21
  import { isNonNullable } from '@dxos/util';
20
22
 
21
23
  import { TextInput } from './Defaults';
22
24
  import { InputHeader, type InputProps } from './Input';
23
25
  import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
26
+ import { translationKey } from '../../translations';
24
27
 
25
28
  type RefFieldProps = InputProps & {
26
29
  ast?: SchemaAST.AST;
@@ -42,6 +45,7 @@ export const RefField = ({
42
45
  onValueChange,
43
46
  ...restInputProps
44
47
  }: RefFieldProps) => {
48
+ const { t } = useTranslation(translationKey);
45
49
  const refTypeInfo = useMemo(
46
50
  () => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
47
51
  [ast],
@@ -127,7 +131,10 @@ export const RefField = ({
127
131
 
128
132
  const { status, error } = restInputProps.getStatus();
129
133
 
130
- return (
134
+ const items = handleGetValue();
135
+
136
+ // 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 : (
131
138
  <Input.Root validationValence={status}>
132
139
  {!inputOnly && (
133
140
  <InputHeader error={error}>
@@ -135,14 +142,27 @@ export const RefField = ({
135
142
  </InputHeader>
136
143
  )}
137
144
  <div data-no-submit>
138
- <TagPicker
139
- items={handleGetValue()}
140
- mode={tagPickerMode}
141
- onBlur={(event) => onBlur(event as unknown as FocusEvent<HTMLElement>)}
142
- onUpdate={handleUpdate}
143
- onSearch={handleSearch}
144
- classNames='rounded-sm bg-input p-1.5'
145
- />
145
+ {disabled ? (
146
+ items.length < 1 ? (
147
+ <p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
148
+ ) : (
149
+ items.map((item) => (
150
+ <DxRefTag key={item.id} refid={item.id} rootclassname='mie-1' style={{ marginInlineEnd: '.25rem' }}>
151
+ {item.label}
152
+ </DxRefTag>
153
+ ))
154
+ )
155
+ ) : (
156
+ <TagPicker
157
+ readonly={disabled}
158
+ items={items}
159
+ mode={tagPickerMode}
160
+ onBlur={(event) => onBlur(event as unknown as FocusEvent<HTMLElement>)}
161
+ onUpdate={handleUpdate}
162
+ onSearch={handleSearch}
163
+ classNames='rounded-sm bg-inputSurface p-1.5'
164
+ />
165
+ )}
146
166
  </div>
147
167
  {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
148
168
  </Input.Root>