@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.a4bbb77

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 (97) hide show
  1. package/dist/lib/browser/index.mjs +710 -553
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +710 -553
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/ControlSection/ControlSection.d.ts +2 -2
  8. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  9. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +81 -5
  10. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +4 -5
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +78 -7
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/ArrayField.d.ts +2 -2
  16. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Defaults.d.ts +5 -5
  18. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.d.ts +5 -4
  20. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/Form.stories.d.ts +128 -31
  22. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormContent.d.ts +16 -10
  25. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormContext.d.ts +2 -2
  27. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/Input.d.ts +4 -1
  29. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/RefField.d.ts +13 -5
  31. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -1
  33. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +1 -1
  35. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +76 -4
  37. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
  39. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  40. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +16 -4
  41. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +80 -8
  43. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/index.d.ts +0 -1
  45. package/dist/types/src/components/index.d.ts.map +1 -1
  46. package/dist/types/src/components/testing.d.ts +6 -3
  47. package/dist/types/src/components/testing.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useForm.d.ts +1 -1
  49. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  50. package/dist/types/src/hooks/useQueryRefOptions.d.ts +7 -2
  51. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
  52. package/dist/types/src/translations.d.ts +11 -5
  53. package/dist/types/src/translations.d.ts.map +1 -1
  54. package/dist/types/src/util/field.d.ts.map +1 -1
  55. package/dist/types/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +42 -36
  57. package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
  58. package/src/components/ControlSection/ControlSection.tsx +26 -12
  59. package/src/components/FieldEditor/FieldEditor.stories.tsx +25 -23
  60. package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
  61. package/src/components/FieldEditor/FieldEditor.tsx +21 -23
  62. package/src/components/Form/ArrayField.tsx +9 -9
  63. package/src/components/Form/Defaults.tsx +22 -20
  64. package/src/components/Form/Form.stories.tsx +133 -97
  65. package/src/components/Form/Form.tsx +16 -12
  66. package/src/components/Form/FormActions.tsx +2 -1
  67. package/src/components/Form/FormContent.tsx +116 -36
  68. package/src/components/Form/FormContext.tsx +6 -6
  69. package/src/components/Form/Input.tsx +4 -1
  70. package/src/components/Form/RefField.tsx +190 -40
  71. package/src/components/Form/custom/GeoPointInput.tsx +29 -21
  72. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +10 -10
  73. package/src/components/Form/custom/SelectOptionsInput.tsx +29 -30
  74. package/src/components/Form/custom/TupleInput.tsx +2 -2
  75. package/src/components/Form/factory.tsx +1 -1
  76. package/src/components/ViewEditor/ViewEditor.stories.tsx +110 -46
  77. package/src/components/ViewEditor/ViewEditor.test.tsx +17 -18
  78. package/src/components/ViewEditor/ViewEditor.tsx +329 -235
  79. package/src/components/index.ts +0 -2
  80. package/src/components/testing.tsx +15 -9
  81. package/src/hooks/useForm.ts +6 -3
  82. package/src/hooks/useQueryRefOptions.ts +8 -2
  83. package/src/hooks/useRefQueryLookupHandler.ts +1 -1
  84. package/src/translations.ts +13 -6
  85. package/src/util/field.ts +8 -18
  86. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  87. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  88. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  89. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  90. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  91. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  92. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  93. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  94. package/src/components/deprecated/Form.stories.tsx +0 -62
  95. package/src/components/deprecated/Form.tsx +0 -119
  96. package/src/components/deprecated/FormInput.tsx +0 -71
  97. package/src/components/deprecated/index.ts +0 -5
@@ -2,16 +2,16 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useState, useEffect, useMemo, type ChangeEvent } from 'react';
5
+ import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
- import { type GeoPoint, GeoLocation } from '@dxos/echo-schema';
7
+ import { GeoLocation, type GeoPoint } from '@dxos/echo-schema';
8
8
  import { Input, useTranslation } from '@dxos/react-ui';
9
9
  import { safeParseFloat } from '@dxos/util';
10
10
 
11
11
  import { translationKey } from '../../../translations';
12
12
  import { InputHeader, type InputProps } from '../Input';
13
13
 
14
- export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
14
+ export const GeoPointInput = ({ type, label, readonly, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
15
15
  const { t } = useTranslation(translationKey);
16
16
  const { status, error } = getStatus();
17
17
  const geoPoint = useMemo<GeoPoint>(() => getValue<GeoPoint>() ?? [0, 0], [getValue]);
@@ -48,24 +48,32 @@ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onVa
48
48
  <Input.Root validationValence={status}>
49
49
  <InputHeader error={error} label={label} />
50
50
  <div role='none' className='grid grid-cols-2 gap-2'>
51
- <Input.TextInput
52
- type='text'
53
- pattern='^-?[0-9]*\.?[0-9]*$'
54
- disabled={disabled}
55
- placeholder={t('placeholder latitude')}
56
- value={latitudeText}
57
- onChange={handleCoordinateChange('latitude', setLatitudeText)}
58
- onBlur={onBlur}
59
- />
60
- <Input.TextInput
61
- type='text'
62
- pattern='^-?[0-9]*\.?[0-9]*$'
63
- disabled={disabled}
64
- placeholder={t('placeholder longitude')}
65
- value={longitudeText}
66
- onChange={handleCoordinateChange('longitude', setLongitudeText)}
67
- onBlur={onBlur}
68
- />
51
+ <div role='none'>
52
+ <Input.Label>{t('latitude label')}</Input.Label>
53
+ <Input.TextInput
54
+ type='number'
55
+ step='0.00001'
56
+ min='-90'
57
+ max='90'
58
+ disabled={!!readonly}
59
+ value={latitudeText}
60
+ onChange={handleCoordinateChange('latitude', setLatitudeText)}
61
+ onBlur={onBlur}
62
+ />
63
+ </div>
64
+ <div role='none'>
65
+ <Input.Label>{t('longitude label')}</Input.Label>
66
+ <Input.TextInput
67
+ type='number'
68
+ step='0.00001'
69
+ min='-180'
70
+ max='180'
71
+ disabled={!!readonly}
72
+ value={longitudeText}
73
+ onChange={handleCoordinateChange('longitude', setLongitudeText)}
74
+ onBlur={onBlur}
75
+ />
76
+ </div>
69
77
  </div>
70
78
  </Input.Root>
71
79
  );
@@ -2,20 +2,19 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
  import { Schema } from 'effect';
9
7
  import React, { useState } from 'react';
10
8
 
11
- import { SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
12
- import { withLayout, withTheme } from '@dxos/storybook-utils';
9
+ import { type SelectOption, SelectOptionSchema } from '@dxos/echo-schema';
10
+ import { withTheme } from '@dxos/react-ui/testing';
13
11
 
14
- import { SelectOptionInput } from './SelectOptionsInput';
15
12
  import { translations } from '../../../translations';
16
13
  import { TestLayout, TestPanel } from '../../testing';
17
14
  import { Form } from '../Form';
18
15
 
16
+ import { SelectOptionInput } from './SelectOptionsInput';
17
+
19
18
  const schema = Schema.Struct({
20
19
  options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
21
20
  }).pipe(Schema.mutable);
@@ -44,18 +43,19 @@ const DefaultStory = () => {
44
43
  );
45
44
  };
46
45
 
47
- const meta: Meta = {
46
+ const meta = {
48
47
  title: 'ui/react-ui-form/SelectOptionsInput',
49
- component: SelectOptionInput,
48
+ component: SelectOptionInput as any,
50
49
  render: DefaultStory,
51
- decorators: [withTheme, withLayout({ fullscreen: true })],
50
+ decorators: [withTheme],
52
51
  parameters: {
52
+ layout: 'fullscreen',
53
53
  translations,
54
54
  },
55
- };
55
+ } satisfies Meta<typeof DefaultStory>;
56
56
 
57
57
  export default meta;
58
58
 
59
- type Story = StoryObj;
59
+ type Story = StoryObj<typeof meta>;
60
60
 
61
61
  export const Default: Story = {};
@@ -2,20 +2,19 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useState, useRef } from 'react';
5
+ import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
6
6
 
7
7
  import { type SelectOption } from '@dxos/echo-schema';
8
8
  import { PublicKey } from '@dxos/keys';
9
- import { type ChromaticPalette, Icon, IconButton, Input, Tag, Toolbar, useTranslation } from '@dxos/react-ui';
9
+ import { type ChromaticPalette, IconButton, Input, Tag, useTranslation } from '@dxos/react-ui';
10
10
  import { List } from '@dxos/react-ui-list';
11
11
  import { HuePicker } from '@dxos/react-ui-pickers';
12
- import { hues, mx } from '@dxos/react-ui-theme';
12
+ import { hues, subtleHover } from '@dxos/react-ui-theme';
13
13
 
14
14
  import { translationKey } from '../../../translations';
15
15
  import { InputHeader, type InputProps } from '../Input';
16
16
 
17
- // TODO(ZaymonFC): Handle disabled.
18
- export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue, onValueChange }: InputProps) => {
17
+ export const SelectOptionInput = ({ type, label, readonly, getStatus, getValue, onValueChange }: InputProps) => {
19
18
  const [selected, setSelectedId] = useState<string | null>(null);
20
19
  const [isNewOption, setIsNewOption] = useState(false);
21
20
  const inputRef = useRef<HTMLInputElement>(null);
@@ -115,7 +114,12 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
115
114
  <InputHeader error={error} label={label} />
116
115
  <div role='none'>
117
116
  {options && (
118
- <List.Root items={options} isItem={(item) => true} onMove={handleMove}>
117
+ <List.Root
118
+ items={options}
119
+ isItem={(item) => true}
120
+ onMove={readonly ? undefined : handleMove}
121
+ readonly={!!readonly}
122
+ >
119
123
  {({ items }) => (
120
124
  <div role='list' className='w-full overflow-auto'>
121
125
  {items?.map((item) => (
@@ -123,26 +127,30 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
123
127
  <List.Item
124
128
  role='button'
125
129
  item={item}
126
- classNames={mx('p-1 plb-2', 'flex flex-col', 'cursor-pointer', 'hover:bg-hoverOverlay')}
130
+ classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-sm']}
127
131
  aria-expanded={selected === item.id}
128
132
  >
129
133
  <div className='flex items-center'>
130
- <List.ItemDragHandle />
134
+ <List.ItemDragHandle disabled={!!readonly} />
131
135
  <List.ItemTitle onClick={() => handleClick(item.id)} classNames='flex-1'>
132
136
  {/* TODO(ZaymonFC): Move spacer into Tag component. */}
133
137
  <Tag palette={item.color as ChromaticPalette}>{item.title || '\u200b'}</Tag>
134
138
  </List.ItemTitle>
135
- <Icon
139
+ <IconButton
140
+ iconOnly
141
+ variant='ghost'
142
+ label={t(selected === item.id ? 'collapse label' : 'expand label', { ns: 'os' })}
136
143
  icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
137
144
  onClick={() => handleClick(item.id)}
138
145
  />
139
146
  </div>
140
147
  {selected === item.id && (
141
- <div className='mis-4 mbs-2 flex flex-col gap-1'>
148
+ <div className='pli-cardSpacingInline plb-cardSpacingBlock flex flex-col gap-1 density-fine'>
142
149
  {/* 16px to match drag handle width. */}
143
150
  <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
144
- <div className='flex flex-row items-center gap-1'>
151
+ <div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
145
152
  <Input.TextInput
153
+ disabled={!!readonly}
146
154
  placeholder={t('select option label placeholder')}
147
155
  ref={selected === item.id ? inputRef : undefined}
148
156
  value={item.title}
@@ -151,14 +159,9 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
151
159
  classNames='flex-1'
152
160
  data-no-submit
153
161
  />
154
- <Toolbar.Root classNames='p-0 m-0 !is-auto'>
155
- <HuePicker
156
- value={item.color}
157
- onChange={handleColorChange(item.id)}
158
- rootVariant='toolbar-button'
159
- />
160
- </Toolbar.Root>
162
+ <HuePicker disabled={!!readonly} value={item.color} onChange={handleColorChange(item.id)} />
161
163
  <IconButton
164
+ disabled={!!readonly}
162
165
  icon='ph--trash--fill'
163
166
  iconOnly
164
167
  label={t('select option delete')}
@@ -170,18 +173,14 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
170
173
  </List.Item>
171
174
  </div>
172
175
  ))}
173
- <div role='listitem'>
174
- <div
175
- role='button'
176
- className={mx('p-1 plb-2', 'flex flex-col', 'cursor-pointer', 'hover:bg-hoverOverlay')}
177
- onClick={handleAdd}
178
- >
179
- <div className='flex items-center gap-1'>
180
- <Icon icon='ph--plus--regular' />
181
- <span className='text-sm'>{t('select option add')}</span>
182
- </div>
183
- </div>
184
- </div>
176
+ <IconButton
177
+ variant='ghost'
178
+ icon='ph--plus--regular'
179
+ label={t('select option add')}
180
+ onClick={handleAdd}
181
+ disabled={!!readonly}
182
+ classNames='is-full'
183
+ />
185
184
  </div>
186
185
  )}
187
186
  </List.Root>
@@ -16,7 +16,7 @@ export const TupleInput = ({
16
16
  binding,
17
17
  type,
18
18
  label,
19
- disabled,
19
+ readonly,
20
20
  getStatus,
21
21
  getValue,
22
22
  onValueChange,
@@ -35,7 +35,7 @@ export const TupleInput = ({
35
35
  <Input.TextInput
36
36
  key={prop}
37
37
  type='number'
38
- disabled={disabled}
38
+ disabled={!!readonly}
39
39
  value={values[prop]}
40
40
  onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
41
41
  onBlur={onBlur}
@@ -5,9 +5,9 @@
5
5
  import { FormatEnum } from '@dxos/echo-schema';
6
6
  import { type SimpleType } from '@dxos/effect';
7
7
 
8
+ import { GeoPointInput } from './custom';
8
9
  import { BooleanInput, MarkdownInput, NumberInput, TextInput } from './Defaults';
9
10
  import { type InputComponent } from './Input';
10
- import { GeoPointInput } from './custom';
11
11
 
12
12
  /**
13
13
  * Get property input component.
@@ -2,87 +2,138 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
  import { Schema } from 'effect';
9
- import React, { useCallback, useMemo, useState, useEffect } from 'react';
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
10
8
 
11
- import { Format, type EchoSchema, toJsonSchema, TypedObject } from '@dxos/echo-schema';
12
- import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
9
+ import { DXN, Filter, Obj, Query, Type } from '@dxos/echo';
10
+ import { QueryBuilder } from '@dxos/echo-query';
11
+ import { type EchoSchema, Format, toJsonSchema } from '@dxos/echo-schema';
12
+ import { useSpace } from '@dxos/react-client/echo';
13
13
  import { withClientProvider } from '@dxos/react-client/testing';
14
14
  import { useAsyncEffect } from '@dxos/react-ui';
15
- import { ViewProjection, ViewType, createView } from '@dxos/schema';
16
- import { withTheme, withLayout } from '@dxos/storybook-utils';
15
+ import { withTheme } from '@dxos/react-ui/testing';
16
+ import { type DataType, type ProjectionModel, createView, typenameFromQuery } from '@dxos/schema';
17
17
 
18
- import { ViewEditor, type ViewEditorProps } from './ViewEditor';
19
18
  import { translations } from '../../translations';
20
19
  import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
21
20
 
21
+ import { ViewEditor, type ViewEditorProps } from './ViewEditor';
22
+
22
23
  // Type definition for debug objects exposed to tests.
23
24
  export type ViewEditorDebugObjects = {
24
25
  schema: EchoSchema;
25
- view: ViewType;
26
- projection: ViewProjection;
26
+ view: DataType.View;
27
+ projection: ProjectionModel;
27
28
  };
28
29
 
29
- type StoryProps = Pick<ViewEditorProps, 'readonly'>;
30
+ type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
30
31
 
31
32
  const DefaultStory = (props: StoryProps) => {
32
33
  const space = useSpace();
33
34
  const [schema, setSchema] = useState<EchoSchema>();
34
- const [view, setView] = useState<ViewType>();
35
- const [projection, setProjection] = useState<ViewProjection>();
35
+ const [view, setView] = useState<DataType.View>();
36
+ const projectionRef = useRef<ProjectionModel>(null);
37
+
36
38
  useAsyncEffect(async () => {
37
39
  if (space) {
38
- class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
40
+ const TestSchema = Schema.Struct({
39
41
  name: Schema.String,
40
42
  email: Format.Email,
41
43
  salary: Format.Currency(),
42
- }) {}
43
-
44
- const [schema] = await space.db.schemaRegistry.register([TestSchema]);
45
- const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: toJsonSchema(TestSchema) });
46
- const projection = new ViewProjection(schema.jsonSchema, view);
47
-
48
- setSchema(schema);
44
+ }).pipe(
45
+ Type.Obj({
46
+ typename: 'example.com/type/Test',
47
+ version: '0.1.0',
48
+ }),
49
+ );
50
+
51
+ const AlternateSchema = Schema.Struct({
52
+ title: Schema.String,
53
+ description: Schema.String,
54
+ completed: Schema.Boolean,
55
+ }).pipe(
56
+ Type.Obj({
57
+ typename: 'example.com/type/Alternate',
58
+ version: '0.1.0',
59
+ }),
60
+ );
61
+
62
+ const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
63
+ const view = createView({
64
+ name: 'Test',
65
+ query: Query.select(Filter.type(TestSchema)),
66
+ queryString: 'Query.select(Filter.type(TestSchema))',
67
+ jsonSchema: toJsonSchema(TestSchema),
68
+ presentation: Obj.make(Type.Expando, {}),
69
+ });
70
+
71
+ setSchema(testSchema);
49
72
  setView(view);
50
- setProjection(projection);
51
73
  }
52
74
  }, [space]);
53
75
 
54
- const views = useQuery(space, Filter.type(ViewType));
55
- const currentTypename = useMemo(() => view?.query?.typename, [view]);
56
- const updateViewTypename = useCallback(
57
- (newTypename: string) => {
58
- if (!schema) {
76
+ const updateViewQuery = useCallback(
77
+ async (queryString: string, target?: string) => {
78
+ if (!schema || !view || !space) {
59
79
  return;
60
80
  }
61
- const matchingViews = views.filter((view) => view.query.typename === currentTypename);
62
- for (const view of matchingViews) {
63
- view.query.typename = newTypename;
81
+
82
+ if (props.mode === 'query') {
83
+ view.query.string = queryString;
84
+
85
+ const builder = new QueryBuilder();
86
+ const filter = builder.build(queryString) ?? Filter.nothing();
87
+ const queue = target && DXN.tryParse(target) ? target : undefined;
88
+ const newQuery = queue ? Query.select(filter).options({ queues: [queue] }) : Query.select(filter);
89
+ view.query.ast = newQuery.ast;
90
+
91
+ const typename = typenameFromQuery(newQuery.ast);
92
+ const [newSchema] = await space.db.schemaRegistry.query({ typename }).run();
93
+ if (!newSchema) {
94
+ return;
95
+ }
96
+
97
+ const newView = createView({
98
+ query: newQuery,
99
+ jsonSchema: newSchema.jsonSchema,
100
+ presentation: Obj.make(Type.Expando, {}),
101
+ });
102
+ view.projection = Obj.getSnapshot(newView).projection;
103
+ setSchema(() => newSchema);
104
+ } else {
105
+ const typename = queryString;
106
+ const newQuery = Query.select(Filter.typename(typename));
107
+ view.query.ast = newQuery.ast;
108
+ schema.updateTypename(typename);
64
109
  }
65
- schema.updateTypename(newTypename);
66
110
  },
67
- [views, schema],
111
+ [view, schema],
68
112
  );
69
113
 
70
- const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
114
+ const handleDelete = useCallback(
115
+ (property: string) => projectionRef.current?.deleteFieldProjection(property),
116
+ [projectionRef],
117
+ );
71
118
 
72
119
  // Expose objects on window for test access.
73
120
  useEffect(() => {
74
- if (typeof window !== 'undefined' && schema && view && projection) {
75
- (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = { schema, view, projection } satisfies ViewEditorDebugObjects;
121
+ if (typeof window !== 'undefined' && schema && view && projectionRef.current) {
122
+ (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
123
+ schema,
124
+ view,
125
+ projection: projectionRef.current,
126
+ } satisfies ViewEditorDebugObjects;
76
127
  }
77
- }, [schema, view, projection]);
128
+ }, [schema, view]);
78
129
 
79
130
  // NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
80
131
  const json = useMemo(
81
- () => JSON.parse(JSON.stringify({ schema, view, projection })),
82
- [JSON.stringify(schema), JSON.stringify(view), JSON.stringify(projection)],
132
+ () => JSON.parse(JSON.stringify({ schema, view, projection: projectionRef.current })),
133
+ [JSON.stringify(schema), JSON.stringify(view)],
83
134
  );
84
135
 
85
- if (!schema || !view || !projection) {
136
+ if (!schema || !view) {
86
137
  return <div />;
87
138
  }
88
139
 
@@ -90,11 +141,13 @@ const DefaultStory = (props: StoryProps) => {
90
141
  <TestLayout json={json}>
91
142
  <TestPanel>
92
143
  <ViewEditor
144
+ ref={projectionRef}
93
145
  schema={schema}
94
146
  view={view}
95
147
  registry={space?.db.schemaRegistry}
148
+ mode={props.mode}
96
149
  readonly={props.readonly}
97
- onTypenameChanged={updateViewTypename}
150
+ onQueryChanged={updateViewQuery}
98
151
  onDelete={handleDelete}
99
152
  />
100
153
  </TestPanel>
@@ -102,19 +155,30 @@ const DefaultStory = (props: StoryProps) => {
102
155
  );
103
156
  };
104
157
 
105
- const meta: Meta<StoryProps> = {
158
+ const meta = {
106
159
  title: 'ui/react-ui-form/ViewEditor',
107
160
  render: DefaultStory,
108
- decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
109
- parameters: { translations },
110
- };
161
+ decorators: [withClientProvider({ createSpace: true }), withTheme],
162
+ parameters: {
163
+ layout: 'fullscreen',
164
+ translations,
165
+ },
166
+ } satisfies Meta<typeof DefaultStory>;
111
167
 
112
168
  export default meta;
113
169
 
114
- type Story = StoryObj<StoryProps>;
170
+ type Story = StoryObj<typeof meta>;
115
171
 
116
172
  export const Default: Story = {};
117
173
 
118
174
  export const Readonly: Story = {
119
- args: { readonly: true },
175
+ args: {
176
+ readonly: true,
177
+ },
178
+ };
179
+
180
+ export const QueryMode: Story = {
181
+ args: {
182
+ mode: 'query',
183
+ },
120
184
  };
@@ -3,15 +3,16 @@
3
3
  //
4
4
 
5
5
  import { composeStories } from '@storybook/react';
6
- import { screen, cleanup, fireEvent, waitFor } from '@testing-library/react';
6
+ import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
7
7
  import { afterEach, describe, expect, test } from 'vitest';
8
8
 
9
- import { EchoSchema } from '@dxos/echo-schema';
10
- import { ViewProjection, ViewType } from '@dxos/schema';
9
+ import { EchoSchema, isInstanceOf } from '@dxos/echo-schema';
10
+ import { DataType, ProjectionModel } from '@dxos/schema';
11
+
12
+ import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
11
13
 
12
14
  import * as stories from './ViewEditor.stories';
13
15
  import { type ViewEditorDebugObjects } from './ViewEditor.stories';
14
- import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
15
16
 
16
17
  const { Default } = composeStories(stories);
17
18
 
@@ -19,8 +20,8 @@ const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
19
20
  const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
20
21
  expect(debugObjects).toBeDefined();
21
22
  expect(debugObjects.schema).toBeInstanceOf(EchoSchema);
22
- expect(debugObjects.view).toBeInstanceOf(ViewType);
23
- expect(debugObjects.projection).toBeInstanceOf(ViewProjection);
23
+ expect(isInstanceOf(DataType.View, debugObjects.view)).toBeTruthy();
24
+ expect(debugObjects.projection).toBeInstanceOf(ProjectionModel);
24
25
  return debugObjects;
25
26
  };
26
27
 
@@ -41,7 +42,7 @@ describe('ViewEditor', () => {
41
42
  test('renders view editor', async () => {
42
43
  await Default.run();
43
44
  await waitForViewEditor();
44
- expect(screen.getByText('View')).toBeInTheDocument();
45
+ expect(screen.getByText('Record type')).toBeInTheDocument();
45
46
  });
46
47
 
47
48
  test('change property name', async () => {
@@ -73,7 +74,7 @@ describe('ViewEditor', () => {
73
74
  expect(schemaProperties!.name).toBeUndefined();
74
75
 
75
76
  // Check view contains new_property field.
76
- const newPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'new_property');
77
+ const newPropertyField = debugObjects.view.projection.fields.find((field: any) => field.path === 'new_property');
77
78
  expect(newPropertyField).toBeDefined();
78
79
  expect(newPropertyField!.path).toBe('new_property');
79
80
 
@@ -120,7 +121,9 @@ describe('ViewEditor', () => {
120
121
  expect(schemaProperties!.added_property).toBeDefined();
121
122
 
122
123
  // Check view contains added_property field.
123
- const addedPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'added_property');
124
+ const addedPropertyField = debugObjects.view.projection.fields.find(
125
+ (field: any) => field.path === 'added_property',
126
+ );
124
127
  expect(addedPropertyField).toBeDefined();
125
128
  expect(addedPropertyField!.path).toBe('added_property');
126
129
 
@@ -138,7 +141,7 @@ describe('ViewEditor', () => {
138
141
  // Find the delete button for the 'name' property.
139
142
  const nameProperty = screen.getByText('name');
140
143
  const propertyRow = nameProperty.closest('[role="listitem"]');
141
- const deleteButton = propertyRow?.querySelector('button:last-child');
144
+ const deleteButton = propertyRow?.querySelector('[data-testid="field.delete"]');
142
145
 
143
146
  if (!deleteButton) {
144
147
  throw new Error('Delete button not found');
@@ -161,7 +164,7 @@ describe('ViewEditor', () => {
161
164
  expect(schemaProperties!.name).toBeUndefined();
162
165
 
163
166
  // Check view no longer contains the name field.
164
- const nameField = debugObjects.view.fields.find((field: any) => field.path === 'name');
167
+ const nameField = debugObjects.view.projection.fields.find((field: any) => field.path === 'name');
165
168
  expect(nameField).toBeUndefined();
166
169
 
167
170
  // Check projection no longer contains the name property.
@@ -186,19 +189,15 @@ describe('ViewEditor', () => {
186
189
  // Access the live objects directly from window using symbol.
187
190
  const debugObjects = getViewEditorDebugObjects();
188
191
 
189
- // Check that hiddenFields is non-empty.
190
- expect(debugObjects.view.hiddenFields).toBeDefined();
191
- expect(debugObjects.view.hiddenFields!.length).toBeGreaterThan(0);
192
-
193
192
  // Click the show button
194
193
  fireEvent.click(screen.getByTestId('show-field-button'));
195
194
 
196
195
  // Wait for the data to update and check that hiddenFields is empty.
197
196
  await waitFor(() => {
198
- expect(debugObjects.view.hiddenFields!.length).toBe(0);
197
+ expect(debugObjects.view.projection.fields.filter(({ visible }) => visible === false)!.length).toBe(0);
199
198
  });
200
199
 
201
200
  // Also verify that the field is back in the visible fields list.
202
- expect(debugObjects.view.fields.length).toBeGreaterThan(0);
201
+ expect(debugObjects.view.projection.fields.filter(({ visible }) => visible !== false).length).toBeGreaterThan(0);
203
202
  });
204
- });
203
+ }, 30_000);