@dxos/react-ui-form 0.8.2-main.f11618f → 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 (98) hide show
  1. package/dist/lib/browser/index.mjs +1732 -1374
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +1715 -1365
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +1732 -1374
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  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 +4 -4
  19. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/Form.stories.d.ts +34 -34
  21. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormActions.d.ts +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 +9 -8
  25. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/RefField.d.ts +7 -18
  29. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  33. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/factory.d.ts +1 -1
  35. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  37. package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -2
  39. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -3
  41. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/deprecated/Form.d.ts +2 -2
  43. package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
  44. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
  46. package/dist/types/src/components/testing.d.ts.map +1 -1
  47. package/dist/types/src/hooks/index.d.ts +2 -0
  48. package/dist/types/src/hooks/index.d.ts.map +1 -1
  49. package/dist/types/src/hooks/useForm.d.ts +2 -2
  50. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useQueryRefOptions.d.ts +23 -0
  52. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -0
  53. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +8 -0
  54. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
  55. package/dist/types/src/translations.d.ts +5 -0
  56. package/dist/types/src/translations.d.ts.map +1 -1
  57. package/dist/types/src/util/field.d.ts.map +1 -1
  58. package/dist/types/src/util/formatting.d.ts.map +1 -1
  59. package/dist/types/src/util/index.d.ts +2 -0
  60. package/dist/types/src/util/index.d.ts.map +1 -1
  61. package/dist/types/src/util/refs.d.ts +9 -0
  62. package/dist/types/src/util/refs.d.ts.map +1 -0
  63. package/dist/types/src/util/schema.d.ts +3 -0
  64. package/dist/types/src/util/schema.d.ts.map +1 -0
  65. package/dist/types/src/vitest-setup.d.ts +2 -0
  66. package/dist/types/src/vitest-setup.d.ts.map +1 -0
  67. package/dist/types/tsconfig.tsbuildinfo +1 -1
  68. package/package.json +28 -25
  69. package/src/components/ControlSection/ControlSection.stories.tsx +1 -1
  70. package/src/components/ControlSection/ControlSection.tsx +2 -2
  71. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  72. package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
  73. package/src/components/FieldEditor/FieldEditor.tsx +3 -2
  74. package/src/components/Form/ArrayField.tsx +60 -49
  75. package/src/components/Form/Defaults.tsx +77 -48
  76. package/src/components/Form/Form.stories.tsx +104 -122
  77. package/src/components/Form/Form.tsx +5 -6
  78. package/src/components/Form/FormActions.tsx +15 -18
  79. package/src/components/Form/FormContent.tsx +80 -54
  80. package/src/components/Form/FormContext.tsx +14 -8
  81. package/src/components/Form/Input.tsx +4 -10
  82. package/src/components/Form/RefField.tsx +167 -101
  83. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
  84. package/src/components/Form/factory.tsx +1 -1
  85. package/src/components/Form/index.ts +0 -1
  86. package/src/components/ViewEditor/ViewEditor.stories.tsx +19 -9
  87. package/src/components/ViewEditor/ViewEditor.tsx +65 -35
  88. package/src/components/deprecated/Form.tsx +6 -6
  89. package/src/components/testing.tsx +5 -9
  90. package/src/hooks/index.ts +3 -1
  91. package/src/hooks/useForm.ts +4 -3
  92. package/src/hooks/useQueryRefOptions.ts +53 -0
  93. package/src/hooks/useRefQueryLookupHandler.ts +39 -0
  94. package/src/translations.ts +9 -2
  95. package/src/util/index.ts +3 -1
  96. package/src/util/refs.ts +38 -0
  97. package/src/util/schema.ts +14 -0
  98. package/src/vitest-setup.ts +11 -0
@@ -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,16 +68,14 @@ 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';
79
- const inputOptOut = (event.target as HTMLElement).hasAttribute('data-no-submit');
76
+ const inputOptOut =
77
+ (event.target as HTMLElement).hasAttribute('data-no-submit') ||
78
+ (event.target as HTMLElement).closest('[data-no-submit]') !== null;
80
79
 
81
80
  // Regular inputs: Submit on Enter (no modifiers).
82
81
  const shouldSubmitRegularInput = !inputIsTextarea && keyIsEnter && !modifierUsed;
@@ -92,7 +91,14 @@ export const FormProvider = ({
92
91
  (event.target as HTMLElement).blur();
93
92
  }
94
93
  }
95
- };
94
+ },
95
+ [form, autoSave],
96
+ );
97
+
98
+ useEffect(() => {
99
+ if (!formRef?.current) {
100
+ return;
101
+ }
96
102
 
97
103
  const formElement = formRef.current;
98
104
 
@@ -34,18 +34,12 @@ 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
- <Tooltip.Root>
41
- <Tooltip.Trigger asChild>
42
- <Icon icon='ph--warning--regular' size={4} classNames={errorText} />
43
- </Tooltip.Trigger>
44
- <Tooltip.Content side='bottom'>
45
- <Tooltip.Arrow />
46
- {error}
47
- </Tooltip.Content>
48
- </Tooltip.Root>
40
+ <Tooltip.Trigger asChild content={error} side='bottom'>
41
+ <Icon icon='ph--warning--regular' size={4} classNames={errorText} />
42
+ </Tooltip.Trigger>
49
43
  )}
50
44
  </div>
51
45
  );
@@ -2,149 +2,215 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useEffect, useState } from 'react';
5
+ import { type SchemaAST } from 'effect';
6
+ import React, { useCallback, useMemo, type FocusEvent } from 'react';
6
7
 
7
8
  import {
8
- type AST,
9
9
  Expando,
10
10
  getTypeAnnotation,
11
+ Ref,
11
12
  ReferenceAnnotationId,
12
13
  type ReferenceAnnotationValue,
13
- type TypeAnnotation,
14
14
  } from '@dxos/echo-schema';
15
- import { findAnnotation, type SimpleType } from '@dxos/effect';
15
+ import { findAnnotation } from '@dxos/effect';
16
16
  import { DXN } from '@dxos/keys';
17
- import { refFromDXN, RefImpl } from '@dxos/live-object';
18
- import { log } from '@dxos/log';
19
- import { type MaybePromise } from '@dxos/util';
20
-
21
- import { SelectInput, TextInput } from './Defaults';
22
- import { type FormInputStateProps } from './FormContext';
23
-
24
- export type QueryRefOptions = (type: TypeAnnotation) => MaybePromise<{ dxn: DXN; label?: string }[]>;
25
-
26
- type RefFieldProps = {
27
- ast: AST.AST;
28
- type: SimpleType;
29
- label: string;
30
- readonly?: boolean;
31
- placeholder?: string;
32
- inline?: boolean;
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';
20
+ import { descriptionText, mx } from '@dxos/react-ui-theme';
21
+ import { isNonNullable } from '@dxos/util';
22
+
23
+ import { TextInput } from './Defaults';
24
+ import { InputHeader, type InputProps } from './Input';
25
+ import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
26
+ import { translationKey } from '../../translations';
27
+
28
+ type RefFieldProps = InputProps & {
29
+ ast?: SchemaAST.AST;
30
+ array?: boolean;
33
31
  onQueryRefOptions?: QueryRefOptions;
34
- inputProps: FormInputStateProps;
35
32
  };
36
33
 
37
34
  export const RefField = ({
38
- ast,
39
35
  type,
40
36
  label,
41
- readonly,
37
+ disabled,
42
38
  placeholder,
43
- inline,
39
+ inputOnly,
40
+ array,
41
+ ast,
42
+ getValue,
43
+ onBlur,
44
44
  onQueryRefOptions,
45
- inputProps,
45
+ onValueChange,
46
+ ...restInputProps
46
47
  }: RefFieldProps) => {
47
- const { getValue, onValueChange, ...restInputProps } = inputProps;
48
- const refTypeInfo = findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId);
49
- const [refOptions, setRefOptions] = useState<Array<{ value: string; label?: string }>>([]);
50
- const [loading, setLoading] = useState(false);
51
-
52
- useEffect(() => {
53
- if (!refTypeInfo || !onQueryRefOptions) {
54
- return;
55
- }
48
+ const { t } = useTranslation(translationKey);
49
+ const refTypeInfo = useMemo(
50
+ () => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
51
+ [ast],
52
+ );
53
+ const { options: availableOptions, loading: _loading } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
54
+
55
+ if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
56
+ // If ref type is expando, fall back to taking a DXN in string format.
57
+ return (
58
+ <RefFieldFallback
59
+ {...{ type, label, placeholder, disabled, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
60
+ />
61
+ );
62
+ }
56
63
 
57
- const fetchOptions = async () => {
58
- setLoading(true);
59
- try {
60
- const options = await onQueryRefOptions(refTypeInfo);
61
- setRefOptions(
62
- options.map((option) => ({
63
- label: option.label,
64
- value: option.dxn.toString(),
65
- })),
66
- );
67
- } catch (error) {
68
- log.error('Failed to fetch ref options:', error);
69
- setRefOptions([]);
70
- } finally {
71
- setLoading(false);
64
+ const handleGetValue = useCallback((): TagPickerItemData[] => {
65
+ const formValue = getValue();
66
+
67
+ const unknownToRefOption = (val: unknown) => {
68
+ if (Ref.isRef(val)) {
69
+ const dxnString = val.dxn.toString();
70
+ const matchingOption = availableOptions.find((option) => option.id === dxnString);
71
+ if (matchingOption) {
72
+ return matchingOption;
73
+ }
72
74
  }
75
+ return undefined;
73
76
  };
74
77
 
75
- void fetchOptions();
76
- }, [refTypeInfo, onQueryRefOptions]);
78
+ if (array && Array.isArray(formValue)) {
79
+ return formValue.map(unknownToRefOption).filter(isNonNullable) ?? [];
80
+ }
77
81
 
78
- if (!refTypeInfo) {
79
- return null;
80
- }
82
+ const option = unknownToRefOption(formValue);
83
+ if (option) {
84
+ return [option];
85
+ }
81
86
 
82
- // If ref type is expando, fall back to taking a DXN in string format.
83
- if (refTypeInfo.typename === getTypeAnnotation(Expando)?.typename || !onQueryRefOptions) {
84
- const handleOnValueChange = (_type: any, dxnString: string) => {
85
- const dxn = DXN.tryParse(dxnString);
86
- if (dxn) {
87
- onValueChange?.('object', refFromDXN(dxn));
88
- } else if (dxnString === '') {
89
- onValueChange?.('object', undefined);
90
- } else {
91
- onValueChange?.('string', dxnString);
92
- }
93
- };
87
+ return [];
88
+ }, [getValue, availableOptions, array]);
94
89
 
95
- const handleGetValue = () => {
96
- const formValue = getValue();
97
- if (typeof formValue === 'string') {
98
- return formValue;
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
+ const handleUpdate = useCallback(
100
+ (ids: string[]) => {
101
+ if (ids.length === 0) {
102
+ onValueChange('object', undefined);
103
+ return;
99
104
  }
100
- if (formValue instanceof RefImpl) {
101
- return formValue.dxn.toString();
105
+
106
+ const refs = ids
107
+ .map((id) => {
108
+ const item = availableOptions.find((option) => option.id === id);
109
+ if (item) {
110
+ const dxn = DXN.parse(item.id);
111
+ return Ref.fromDXN(dxn);
112
+ }
113
+ return null;
114
+ })
115
+ .filter(isNonNullable);
116
+
117
+ if (array) {
118
+ onValueChange('object', refs);
119
+ } else {
120
+ onValueChange('object', refs[0]);
102
121
  }
122
+ },
123
+ [availableOptions, array, onValueChange],
124
+ );
103
125
 
104
- return undefined;
105
- };
126
+ const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
106
127
 
107
- return (
108
- <TextInput
109
- type={type}
110
- label={label}
111
- disabled={readonly}
112
- placeholder={placeholder}
113
- inputOnly={inline}
114
- getValue={handleGetValue as <V>() => V | undefined}
115
- onValueChange={handleOnValueChange}
116
- {...restInputProps}
117
- />
118
- );
128
+ if (!refTypeInfo) {
129
+ return null;
119
130
  }
120
131
 
132
+ const { status, error } = restInputProps.getStatus();
133
+
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 : (
138
+ <Input.Root validationValence={status}>
139
+ {!inputOnly && (
140
+ <InputHeader error={error}>
141
+ <Input.Label>{label}</Input.Label>
142
+ </InputHeader>
143
+ )}
144
+ <div data-no-submit>
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
+ )}
166
+ </div>
167
+ {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
168
+ </Input.Root>
169
+ );
170
+ };
171
+
172
+ const RefFieldFallback = ({
173
+ type,
174
+ label,
175
+ disabled,
176
+ placeholder,
177
+ inputOnly,
178
+ getValue,
179
+ onValueChange,
180
+ ...restInputProps
181
+ }: InputProps) => {
182
+ const handleOnValueChange = (_type: any, dxnString: string) => {
183
+ const dxn = DXN.tryParse(dxnString);
184
+ if (dxn) {
185
+ onValueChange?.('object', Ref.fromDXN(dxn));
186
+ } else if (dxnString === '') {
187
+ onValueChange?.('object', undefined);
188
+ } else {
189
+ onValueChange?.('string', dxnString);
190
+ }
191
+ };
192
+
121
193
  const handleGetValue = () => {
122
194
  const formValue = getValue();
123
-
124
- if (formValue instanceof RefImpl) {
195
+ if (typeof formValue === 'string') {
196
+ return formValue;
197
+ }
198
+ if (Ref.isRef(formValue)) {
125
199
  return formValue.dxn.toString();
126
200
  }
127
201
 
128
202
  return undefined;
129
203
  };
130
204
 
131
- const handleValueChanged = (_type: any, dxnString: string) => {
132
- const dxn = DXN.parse(dxnString);
133
- const ref = refFromDXN(dxn);
134
-
135
- onValueChange('object', ref);
136
- };
137
-
138
205
  return (
139
- <SelectInput
206
+ <TextInput
140
207
  type={type}
141
208
  label={label}
142
- disabled={readonly || loading}
143
- placeholder={loading ? 'Loading options...' : placeholder}
144
- inputOnly={inline}
209
+ disabled={disabled}
210
+ placeholder={placeholder}
211
+ inputOnly={inputOnly}
145
212
  getValue={handleGetValue as <V>() => V | undefined}
146
- onValueChange={handleValueChanged}
147
- options={refOptions}
213
+ onValueChange={handleOnValueChange}
148
214
  {...restInputProps}
149
215
  />
150
216
  );
@@ -5,9 +5,10 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
+ import { Schema } from 'effect';
8
9
  import React, { useState } from 'react';
9
10
 
10
- import { S, SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
11
+ import { SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
11
12
  import { withLayout, withTheme } from '@dxos/storybook-utils';
12
13
 
13
14
  import { SelectOptionInput } from './SelectOptionsInput';
@@ -15,9 +16,9 @@ import translations from '../../../translations';
15
16
  import { TestLayout, TestPanel } from '../../testing';
16
17
  import { Form } from '../Form';
17
18
 
18
- const schema = S.Struct({
19
- options: S.Array(SelectOptionSchema).pipe(S.mutable),
20
- }).pipe(S.mutable);
19
+ const schema = Schema.Struct({
20
+ options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
21
+ }).pipe(Schema.mutable);
21
22
 
22
23
  const DefaultStory = () => {
23
24
  const [values, setValues] = useState<{ options: SelectOption[] }>({
@@ -46,7 +47,7 @@ const DefaultStory = () => {
46
47
  const meta: Meta = {
47
48
  title: 'ui/react-ui-form/SelectOptionsInput',
48
49
  component: SelectOptionInput,
49
- decorators: [withTheme, withLayout({ tooltips: true })],
50
+ decorators: [withTheme, withLayout()],
50
51
  parameters: { translations },
51
52
  };
52
53
 
@@ -12,7 +12,7 @@ import { GeoPointInput } from './custom';
12
12
  /**
13
13
  * Get property input component.
14
14
  */
15
- export const getInputComponent = (type: SimpleType, format?: FormatEnum): InputComponent | undefined => {
15
+ export const getInputComponent = (type: SimpleType, format?: FormatEnum | string): InputComponent | undefined => {
16
16
  switch (format) {
17
17
  case FormatEnum.GeoPoint:
18
18
  return GeoPointInput;
@@ -3,7 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './custom';
6
-
7
6
  export * from './Defaults';
8
7
  export * from './Form';
9
8
  export * from './FormContext';
@@ -5,20 +5,23 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
+ import { Schema } from 'effect';
8
9
  import React, { useCallback, useMemo, useState } from 'react';
9
10
 
10
- import { Format, type EchoSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
11
+ import { Format, type EchoSchema, toJsonSchema, TypedObject } from '@dxos/echo-schema';
11
12
  import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
12
13
  import { withClientProvider } from '@dxos/react-client/testing';
13
14
  import { useAsyncEffect } from '@dxos/react-ui';
14
15
  import { ViewProjection, ViewType, createView } from '@dxos/schema';
15
16
  import { withTheme, withLayout } from '@dxos/storybook-utils';
16
17
 
17
- import { ViewEditor } from './ViewEditor';
18
+ import { ViewEditor, type ViewEditorProps } from './ViewEditor';
18
19
  import translations from '../../translations';
19
20
  import { TestLayout, TestPanel } from '../testing';
20
21
 
21
- const DefaultStory = () => {
22
+ type StoryProps = Pick<ViewEditorProps, 'readonly'>;
23
+
24
+ const DefaultStory = (props: StoryProps) => {
22
25
  const space = useSpace();
23
26
  const [schema, setSchema] = useState<EchoSchema>();
24
27
  const [view, setView] = useState<ViewType>();
@@ -26,7 +29,7 @@ const DefaultStory = () => {
26
29
  useAsyncEffect(async () => {
27
30
  if (space) {
28
31
  class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
29
- name: S.String,
32
+ name: Schema.String,
30
33
  email: Format.Email,
31
34
  salary: Format.Currency(),
32
35
  }) {}
@@ -41,7 +44,7 @@ const DefaultStory = () => {
41
44
  }
42
45
  }, [space]);
43
46
 
44
- const views = useQuery(space, Filter.schema(ViewType));
47
+ const views = useQuery(space, Filter.type(ViewType));
45
48
  const currentTypename = useMemo(() => view?.query?.typename, [view]);
46
49
  const updateViewTypename = useCallback(
47
50
  (newTypename: string) => {
@@ -56,6 +59,7 @@ const DefaultStory = () => {
56
59
  },
57
60
  [views, schema],
58
61
  );
62
+
59
63
  const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
60
64
 
61
65
  if (!schema || !view || !projection) {
@@ -69,6 +73,7 @@ const DefaultStory = () => {
69
73
  schema={schema}
70
74
  view={view}
71
75
  registry={space?.db.schemaRegistry}
76
+ readonly={props.readonly}
72
77
  onTypenameChanged={updateViewTypename}
73
78
  onDelete={handleDelete}
74
79
  />
@@ -77,11 +82,10 @@ const DefaultStory = () => {
77
82
  );
78
83
  };
79
84
 
80
- const meta: Meta<typeof ViewEditor> = {
85
+ const meta: Meta<StoryProps> = {
81
86
  title: 'ui/react-ui-form/ViewEditor',
82
- component: ViewEditor,
83
87
  render: DefaultStory,
84
- decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true, tooltips: true }), withTheme],
88
+ decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
85
89
  parameters: {
86
90
  translations,
87
91
  },
@@ -89,6 +93,12 @@ const meta: Meta<typeof ViewEditor> = {
89
93
 
90
94
  export default meta;
91
95
 
92
- type Story = StoryObj;
96
+ type Story = StoryObj<StoryProps>;
93
97
 
94
98
  export const Default: Story = {};
99
+
100
+ export const Readonly: Story = {
101
+ args: {
102
+ readonly: true,
103
+ },
104
+ };