@dxos/react-ui-form 0.8.2-main.fbd8ed0 → 0.8.2-staging.4d6ad0f

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 (53) hide show
  1. package/dist/lib/browser/index.mjs +1440 -1244
  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 +1426 -1230
  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 +1440 -1244
  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.map +1 -1
  24. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  26. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  27. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -3
  28. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/testing.d.ts.map +1 -1
  30. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
  31. package/dist/types/src/translations.d.ts +5 -0
  32. package/dist/types/src/translations.d.ts.map +1 -1
  33. package/dist/types/src/vitest-setup.d.ts +2 -0
  34. package/dist/types/src/vitest-setup.d.ts.map +1 -0
  35. package/package.json +27 -25
  36. package/src/components/ControlSection/ControlSection.tsx +2 -2
  37. package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
  38. package/src/components/FieldEditor/FieldEditor.tsx +3 -2
  39. package/src/components/Form/ArrayField.tsx +51 -34
  40. package/src/components/Form/Defaults.tsx +74 -48
  41. package/src/components/Form/Form.stories.tsx +77 -101
  42. package/src/components/Form/Form.tsx +1 -2
  43. package/src/components/Form/FormActions.tsx +15 -18
  44. package/src/components/Form/FormContent.tsx +24 -29
  45. package/src/components/Form/FormContext.tsx +11 -7
  46. package/src/components/Form/Input.tsx +1 -1
  47. package/src/components/Form/RefField.tsx +30 -10
  48. package/src/components/ViewEditor/ViewEditor.stories.tsx +14 -5
  49. package/src/components/ViewEditor/ViewEditor.tsx +51 -24
  50. package/src/components/testing.tsx +5 -9
  51. package/src/hooks/useRefQueryLookupHandler.ts +1 -3
  52. package/src/translations.ts +9 -2
  53. 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,7 +7,6 @@ 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 { mx } from '@dxos/react-ui-theme';
11
10
  import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
12
11
  import { isNotFalsy } from '@dxos/util';
13
12
 
@@ -68,7 +67,7 @@ export const FormField = ({
68
67
  });
69
68
 
70
69
  if (FoundComponent) {
71
- return <div role='none'>{FoundComponent}</div>;
70
+ return FoundComponent;
72
71
  }
73
72
 
74
73
  const jsonPath = createJsonPath(path ?? []);
@@ -124,17 +123,15 @@ export const FormField = ({
124
123
  const InputComponent = getInputComponent(type, format);
125
124
  if (InputComponent) {
126
125
  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>
126
+ <InputComponent
127
+ type={type}
128
+ format={format}
129
+ label={label}
130
+ inputOnly={inline}
131
+ placeholder={placeholder}
132
+ disabled={readonly}
133
+ {...inputProps}
134
+ />
138
135
  );
139
136
  }
140
137
 
@@ -144,18 +141,16 @@ export const FormField = ({
144
141
 
145
142
  if (options) {
146
143
  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>
144
+ <SelectInput
145
+ type={type}
146
+ format={format}
147
+ disabled={readonly}
148
+ inputOnly={inline}
149
+ label={label}
150
+ options={options.map((option) => ({ value: option, label: String(option) }))}
151
+ placeholder={placeholder}
152
+ {...inputProps}
153
+ />
159
154
  );
160
155
  }
161
156
 
@@ -171,8 +166,8 @@ export const FormField = ({
171
166
 
172
167
  if (typeLiteral) {
173
168
  return (
174
- <div role='none'>
175
- {!inline && <h3 className='text-lg mbs-2 mbe-1'>{label}</h3>}
169
+ <>
170
+ {!inline && <h3 className='text-lg mbs-2 mbe-1 first:mbs-0'>{label}</h3>}
176
171
  <FormFields
177
172
  schema={Schema.make(typeLiteral)}
178
173
  path={path}
@@ -181,7 +176,7 @@ export const FormField = ({
181
176
  Custom={Custom}
182
177
  lookupComponent={lookupComponent}
183
178
  />
184
- </div>
179
+ </>
185
180
  );
186
181
  }
187
182
  }
@@ -220,7 +215,7 @@ export const FormFields = ({
220
215
  }, [schema, values, filter, sort]);
221
216
 
222
217
  return (
223
- <div role='form' className={mx('flex flex-col w-full gap-2')}>
218
+ <div role='form' className='is-full'>
224
219
  {properties
225
220
  .map((property) => {
226
221
  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>
@@ -15,11 +15,13 @@ import { useAsyncEffect } from '@dxos/react-ui';
15
15
  import { ViewProjection, ViewType, createView } from '@dxos/schema';
16
16
  import { withTheme, withLayout } from '@dxos/storybook-utils';
17
17
 
18
- import { ViewEditor } from './ViewEditor';
18
+ import { ViewEditor, type ViewEditorProps } from './ViewEditor';
19
19
  import translations from '../../translations';
20
20
  import { TestLayout, TestPanel } from '../testing';
21
21
 
22
- const DefaultStory = () => {
22
+ type StoryProps = Pick<ViewEditorProps, 'readonly'>;
23
+
24
+ const DefaultStory = (props: StoryProps) => {
23
25
  const space = useSpace();
24
26
  const [schema, setSchema] = useState<EchoSchema>();
25
27
  const [view, setView] = useState<ViewType>();
@@ -57,6 +59,7 @@ const DefaultStory = () => {
57
59
  },
58
60
  [views, schema],
59
61
  );
62
+
60
63
  const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
61
64
 
62
65
  if (!schema || !view || !projection) {
@@ -70,6 +73,7 @@ const DefaultStory = () => {
70
73
  schema={schema}
71
74
  view={view}
72
75
  registry={space?.db.schemaRegistry}
76
+ readonly={props.readonly}
73
77
  onTypenameChanged={updateViewTypename}
74
78
  onDelete={handleDelete}
75
79
  />
@@ -78,9 +82,8 @@ const DefaultStory = () => {
78
82
  );
79
83
  };
80
84
 
81
- const meta: Meta<typeof ViewEditor> = {
85
+ const meta: Meta<StoryProps> = {
82
86
  title: 'ui/react-ui-form/ViewEditor',
83
- component: ViewEditor,
84
87
  render: DefaultStory,
85
88
  decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
86
89
  parameters: {
@@ -90,6 +93,12 @@ const meta: Meta<typeof ViewEditor> = {
90
93
 
91
94
  export default meta;
92
95
 
93
- type Story = StoryObj;
96
+ type Story = StoryObj<StoryProps>;
94
97
 
95
98
  export const Default: Story = {};
99
+
100
+ export const Readonly: Story = {
101
+ args: {
102
+ readonly: true,
103
+ },
104
+ };