@dxos/react-ui-form 0.7.5-main.9d26e3a → 0.7.5-main.e9bb01b

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 (70) hide show
  1. package/dist/lib/browser/index.mjs +743 -466
  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 +777 -515
  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 +743 -466
  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/FieldEditor/FieldEditor.d.ts.map +1 -1
  11. package/dist/types/src/components/Form/ArrayField.d.ts +16 -0
  12. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -0
  13. package/dist/types/src/components/Form/Defaults.d.ts +5 -6
  14. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.d.ts +13 -16
  16. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.stories.d.ts +2 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormActions.d.ts +7 -0
  20. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -0
  21. package/dist/types/src/components/Form/FormContent.d.ts +26 -0
  22. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormContext.d.ts +20 -0
  24. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/Input.d.ts +5 -6
  26. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -2
  28. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +4 -0
  30. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +7 -0
  32. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -2
  34. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  35. package/dist/types/src/components/Form/custom/index.d.ts +1 -0
  36. package/dist/types/src/components/Form/custom/index.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/factory.d.ts +2 -2
  38. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  39. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -1
  40. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/deprecated/FormInput.d.ts +5 -0
  43. package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useForm.d.ts +9 -10
  45. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  46. package/dist/types/src/translations.d.ts +6 -0
  47. package/dist/types/src/translations.d.ts.map +1 -1
  48. package/dist/types/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +25 -24
  50. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  51. package/src/components/FieldEditor/FieldEditor.tsx +58 -40
  52. package/src/components/Form/ArrayField.tsx +111 -0
  53. package/src/components/Form/Defaults.tsx +18 -33
  54. package/src/components/Form/Form.stories.tsx +15 -5
  55. package/src/components/Form/Form.tsx +39 -330
  56. package/src/components/Form/FormActions.tsx +41 -0
  57. package/src/components/Form/FormContent.tsx +173 -0
  58. package/src/components/Form/FormContext.tsx +48 -0
  59. package/src/components/Form/Input.tsx +5 -6
  60. package/src/components/Form/custom/GeoPointInput.tsx +6 -15
  61. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +58 -0
  62. package/src/components/Form/custom/SelectOptionsInput.tsx +192 -0
  63. package/src/components/Form/custom/TupleInput.tsx +5 -9
  64. package/src/components/Form/custom/index.ts +1 -0
  65. package/src/components/Form/factory.tsx +2 -5
  66. package/src/components/ViewEditor/ViewEditor.stories.tsx +25 -4
  67. package/src/components/ViewEditor/ViewEditor.tsx +14 -6
  68. package/src/components/deprecated/FormInput.tsx +19 -3
  69. package/src/hooks/useForm.ts +54 -47
  70. package/src/translations.ts +8 -0
@@ -4,30 +4,29 @@
4
4
 
5
5
  import React, { type FC, type PropsWithChildren } from 'react';
6
6
 
7
- import { type BaseObject, type FormatEnum, type PropertyKey } from '@dxos/echo-schema';
7
+ import { type FormatEnum } from '@dxos/echo-schema';
8
8
  import { type SimpleType } from '@dxos/effect';
9
9
  import { Icon, Tooltip } from '@dxos/react-ui';
10
10
  import { errorText } from '@dxos/react-ui-theme';
11
11
 
12
- import { type FormHandler } from '../../hooks';
12
+ import { type FormInputStateProps } from './FormContext';
13
13
 
14
14
  /**
15
15
  * Props passed to input components.
16
16
  */
17
- export type InputProps<T extends BaseObject> = {
18
- property: PropertyKey<T>;
17
+ export type InputProps = {
19
18
  type: SimpleType;
20
19
  format?: FormatEnum;
21
20
  label?: string;
22
21
  disabled?: boolean;
23
22
  placeholder?: string;
24
23
  inputOnly?: boolean;
25
- } & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
24
+ } & FormInputStateProps;
26
25
 
27
26
  /**
28
27
  * Form input component.
29
28
  */
30
- export type InputComponent<T extends BaseObject> = FC<InputProps<T>>;
29
+ export type InputComponent = FC<InputProps>;
31
30
 
32
31
  export type InputHeaderProps = PropsWithChildren<{
33
32
  error?: string;
@@ -4,26 +4,17 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { type BaseObject, type GeoPoint } from '@dxos/echo-schema';
7
+ import { 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 = <T extends BaseObject>({
15
- property,
16
- type,
17
- label,
18
- disabled,
19
- getStatus,
20
- getValue,
21
- onValueChange,
22
- onBlur,
23
- }: InputProps<T>) => {
14
+ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
24
15
  const { t } = useTranslation(translationKey);
25
- const { status, error } = getStatus?.(property);
26
- const [lng = 0, lat = 0] = getValue<GeoPoint>(property) ?? [];
16
+ const { status, error } = getStatus();
17
+ const [lng = 0, lat = 0] = getValue<GeoPoint>() ?? [];
27
18
 
28
19
  return (
29
20
  <Input.Root validationValence={status}>
@@ -36,7 +27,7 @@ export const GeoPointInput = <T extends BaseObject>({
36
27
  disabled={disabled}
37
28
  placeholder={t('placeholder longitude')}
38
29
  value={lng}
39
- onChange={(event) => onValueChange(property, type, [lat, safeParseFloat(event.target.value, 0)])}
30
+ onChange={(event) => onValueChange(type, [lat, safeParseFloat(event.target.value, 0)])}
40
31
  onBlur={onBlur}
41
32
  />
42
33
  <Input.TextInput
@@ -44,7 +35,7 @@ export const GeoPointInput = <T extends BaseObject>({
44
35
  disabled={disabled}
45
36
  placeholder={t('placeholder latitude')}
46
37
  value={lat}
47
- onChange={(event) => onValueChange(property, type, [safeParseFloat(event.target.value, 0), lng])}
38
+ onChange={(event) => onValueChange(type, [safeParseFloat(event.target.value, 0), lng])}
48
39
  onBlur={onBlur}
49
40
  />
50
41
  </div>
@@ -0,0 +1,58 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type Meta, type StoryObj } from '@storybook/react';
8
+ import React, { useState } from 'react';
9
+
10
+ import { S, SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
11
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
12
+
13
+ import { SelectOptionInput } from './SelectOptionsInput';
14
+ import translations from '../../../translations';
15
+ import { TestLayout, TestPanel } from '../../testing';
16
+ import { Form } from '../Form';
17
+
18
+ const schema = S.Struct({
19
+ options: S.Array(SelectOptionSchema).pipe(S.mutable),
20
+ }).pipe(S.mutable);
21
+
22
+ const DefaultStory = () => {
23
+ const [values, setValues] = useState<{ options: SelectOption[] }>({
24
+ // TODO(Zaymon): Pick colors from our limited set.
25
+ options: [
26
+ { id: 'opt_1', title: 'First option', color: 'amber' },
27
+ { id: 'opt_2', title: 'Second option', color: 'emerald' },
28
+ { id: 'opt_3', title: 'Third option', color: 'rose' },
29
+ ],
30
+ });
31
+
32
+ return (
33
+ <TestLayout>
34
+ <TestPanel>
35
+ <Form
36
+ schema={schema}
37
+ values={values}
38
+ onSave={(vals) => setValues(vals)}
39
+ Custom={{ options: SelectOptionInput }}
40
+ />
41
+ </TestPanel>
42
+ </TestLayout>
43
+ );
44
+ };
45
+
46
+ const meta: Meta = {
47
+ title: 'ui/react-ui-form/SelectOptionsInput',
48
+ component: SelectOptionInput,
49
+ decorators: [withTheme, withLayout({ tooltips: true })],
50
+ parameters: { translations },
51
+ };
52
+
53
+ export default meta;
54
+ type Story = StoryObj;
55
+
56
+ export const Default: Story = {
57
+ render: DefaultStory,
58
+ };
@@ -0,0 +1,192 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useEffect, useState, type ChangeEvent, useRef } from 'react';
6
+
7
+ import { type SelectOption } from '@dxos/echo-schema';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { type ChromaticPalette, Icon, IconButton, Input, Tag, Toolbar, useTranslation } from '@dxos/react-ui';
10
+ import { List } from '@dxos/react-ui-list';
11
+ import { HuePickerToolbarButton } from '@dxos/react-ui-pickers';
12
+ import { hueTokenThemes, mx } from '@dxos/react-ui-theme';
13
+
14
+ import { translationKey } from '../../../translations';
15
+ import { InputHeader, type InputProps } from '../Input';
16
+
17
+ // TODO(ZaymonFC): Handle disabled.
18
+ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue, onValueChange }: InputProps) => {
19
+ const [selected, setSelectedId] = useState<string | null>(null);
20
+ const [isNewOption, setIsNewOption] = useState(false);
21
+ const inputRef = useRef<HTMLInputElement>(null);
22
+ const { t } = useTranslation(translationKey);
23
+ const { status, error } = getStatus();
24
+ const options = getValue<SelectOption[] | undefined>();
25
+
26
+ // Initialization.
27
+ useEffect(() => {
28
+ if (options === undefined) {
29
+ onValueChange(type, []);
30
+ }
31
+ }, [options, onValueChange, type]);
32
+
33
+ const randomHue = () => {
34
+ const usedHues = new Set(options?.map((option) => option.color) ?? []);
35
+ const hueKeys = Object.keys(hueTokenThemes) as Array<keyof typeof hueTokenThemes>;
36
+
37
+ // Pick a random unused option from hueTokenThemes.
38
+ const unusedHues = hueKeys.filter((hue) => !usedHues.has(hue));
39
+ if (unusedHues.length > 0) {
40
+ return unusedHues[Math.floor(Math.random() * unusedHues.length)];
41
+ }
42
+
43
+ // Pick a random one that hasn't been used in the last three options
44
+ const lastThree = new Set(options?.slice(-3).map((option) => option.color) ?? []);
45
+ const availableHues = hueKeys.filter((hue) => !lastThree.has(hue));
46
+ return (
47
+ availableHues[Math.floor(Math.random() * availableHues.length)] ??
48
+ hueKeys[Math.floor(Math.random() * hueKeys.length)]
49
+ );
50
+ };
51
+
52
+ const handleAdd = useCallback(() => {
53
+ const newId = PublicKey.random().truncate();
54
+ const newOption = { id: newId, title: '', color: randomHue() };
55
+ onValueChange(type, [...(options ?? []), newOption]);
56
+ setSelectedId(newId);
57
+ setIsNewOption(true);
58
+ }, [options, type, onValueChange]);
59
+
60
+ const handleDelete = useCallback(
61
+ (id: string) => {
62
+ const newOptions = options?.filter((option) => option.id !== id) ?? [];
63
+ onValueChange(type, newOptions);
64
+ setSelectedId(null);
65
+ },
66
+ [options, type, onValueChange],
67
+ );
68
+
69
+ const handleMove = useCallback(
70
+ (from: number, to: number) => {
71
+ if (!options) {
72
+ return;
73
+ }
74
+
75
+ const newOptions = [...options];
76
+ const [removed] = newOptions.splice(from, 1);
77
+ newOptions.splice(to, 0, removed);
78
+ onValueChange(type, newOptions);
79
+ },
80
+ [options, type, onValueChange],
81
+ );
82
+
83
+ const handleClick = useCallback((id: string) => {
84
+ setSelectedId((current) => (current === id ? null : id));
85
+ }, []);
86
+
87
+ const handleTitleChange = useCallback(
88
+ (id: string) => (e: ChangeEvent<HTMLInputElement>) => {
89
+ const newOptions = options?.map((o) => (o.id === id ? { ...o, title: e.target.value } : o));
90
+ onValueChange(type, newOptions ?? []);
91
+ },
92
+ [options, type, onValueChange],
93
+ );
94
+
95
+ const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
96
+ if (e.key === 'Enter') {
97
+ setSelectedId(null);
98
+ }
99
+ }, []);
100
+
101
+ const handleColorChange = useCallback(
102
+ (id: string) => (hue: string) => {
103
+ const newOptions = options?.map((o) => (o.id === id ? { ...o, color: hue } : o));
104
+ onValueChange(type, newOptions ?? []);
105
+ },
106
+ [options, type, onValueChange],
107
+ );
108
+
109
+ useEffect(() => {
110
+ if (selected && isNewOption && inputRef.current) {
111
+ inputRef.current.focus();
112
+ setIsNewOption(false);
113
+ }
114
+ }, [selected, isNewOption]);
115
+
116
+ return (
117
+ <Input.Root validationValence={status}>
118
+ <InputHeader error={error}>
119
+ <Input.Label>{label}</Input.Label>
120
+ </InputHeader>
121
+ <div role='none'>
122
+ {options && (
123
+ <List.Root items={options} isItem={(item) => true} onMove={handleMove}>
124
+ {({ items }) => (
125
+ <div role='list' className='w-full overflow-auto'>
126
+ {items?.map((item) => (
127
+ <div key={item.id}>
128
+ <List.Item
129
+ role='button'
130
+ item={item}
131
+ classNames={mx('p-1 plb-2', 'flex flex-col', 'cursor-pointer', 'hover:bg-hoverOverlay')}
132
+ aria-expanded={selected === item.id}
133
+ >
134
+ <div className='flex items-center'>
135
+ <List.ItemDragHandle />
136
+ <List.ItemTitle onClick={() => handleClick(item.id)} classNames='flex-1'>
137
+ {/* TODO(ZaymonFC): Move spacer into Tag component. */}
138
+ <Tag palette={item.color as ChromaticPalette}>{item.title || '\u200b'}</Tag>
139
+ </List.ItemTitle>
140
+ <Icon
141
+ icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
142
+ onClick={() => handleClick(item.id)}
143
+ />
144
+ </div>
145
+ {selected === item.id && (
146
+ <div className='mis-4 mbs-2 flex flex-col gap-1'>
147
+ {/* 16px to match drag handle width. */}
148
+ <Input.Label classNames='text-xs'>{t('select option label')}</Input.Label>
149
+ <div className='flex flex-row items-center gap-1'>
150
+ <Input.TextInput
151
+ placeholder={t('select option label placeholder')}
152
+ ref={selected === item.id ? inputRef : undefined}
153
+ value={item.title}
154
+ onChange={handleTitleChange(item.id)}
155
+ onKeyDown={handleKeyDown}
156
+ classNames='flex-1'
157
+ />
158
+ <Toolbar.Root classNames='p-0 m-0 !is-auto'>
159
+ <HuePickerToolbarButton hue={item.color} onChangeHue={handleColorChange(item.id)} />
160
+ </Toolbar.Root>
161
+ <IconButton
162
+ icon='ph--trash--fill'
163
+ iconOnly
164
+ label={t('select option delete')}
165
+ onClick={() => handleDelete(item.id)}
166
+ />
167
+ </div>
168
+ </div>
169
+ )}
170
+ </List.Item>
171
+ </div>
172
+ ))}
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>
185
+ </div>
186
+ )}
187
+ </List.Root>
188
+ )}
189
+ </div>
190
+ </Input.Root>
191
+ );
192
+ };
@@ -4,7 +4,6 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { type BaseObject } from '@dxos/echo-schema';
8
7
  import { Input } from '@dxos/react-ui';
9
8
  import { mx } from '@dxos/react-ui-theme';
10
9
  import { safeParseFloat } from '@dxos/util';
@@ -13,9 +12,8 @@ import { InputHeader, type InputProps } from '../Input';
13
12
 
14
13
  const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
15
14
 
16
- export const TupleInput = <T extends BaseObject>({
15
+ export const TupleInput = ({
17
16
  binding,
18
- property,
19
17
  type,
20
18
  label,
21
19
  disabled,
@@ -23,11 +21,11 @@ export const TupleInput = <T extends BaseObject>({
23
21
  getValue,
24
22
  onValueChange,
25
23
  onBlur,
26
- }: InputProps<T> & { binding: string[] }) => {
27
- const { status, error } = getStatus?.(property);
24
+ }: InputProps & { binding: string[] }) => {
25
+ const { status, error } = getStatus();
28
26
 
29
27
  // TODO(burdon): Generalize number/float/string, etc.
30
- const values: Record<string, number> = getValue<Record<string, number>>(property) ?? {};
28
+ const values: Record<string, number> = getValue<Record<string, number>>() ?? {};
31
29
 
32
30
  return (
33
31
  <Input.Root validationValence={status}>
@@ -41,9 +39,7 @@ export const TupleInput = <T extends BaseObject>({
41
39
  type='number'
42
40
  disabled={disabled}
43
41
  value={values[prop]}
44
- onChange={(event) =>
45
- onValueChange(property, type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })
46
- }
42
+ onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
47
43
  onBlur={onBlur}
48
44
  />
49
45
  ))}
@@ -3,4 +3,5 @@
3
3
  //
4
4
 
5
5
  export * from './GeoPointInput';
6
+ export * from './SelectOptionsInput';
6
7
  export * from './TupleInput';
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type BaseObject, FormatEnum } from '@dxos/echo-schema';
5
+ import { FormatEnum } from '@dxos/echo-schema';
6
6
  import { type SimpleType } from '@dxos/effect';
7
7
 
8
8
  import { BooleanInput, NumberInput, TextInput } from './Defaults';
@@ -12,10 +12,7 @@ import { GeoPointInput } from './custom';
12
12
  /**
13
13
  * Get property input component.
14
14
  */
15
- export const getInputComponent = <T extends BaseObject>(
16
- type: SimpleType,
17
- format?: FormatEnum,
18
- ): InputComponent<T> | undefined => {
15
+ export const getInputComponent = (type: SimpleType, format?: FormatEnum): InputComponent | undefined => {
19
16
  switch (format) {
20
17
  case FormatEnum.LatLng:
21
18
  return GeoPointInput;
@@ -5,13 +5,13 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
- import React, { useCallback, useState } from 'react';
8
+ import React, { useCallback, useMemo, useState } from 'react';
9
9
 
10
10
  import { Format, type EchoSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
11
- import { useSpace } from '@dxos/react-client/echo';
11
+ import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
12
12
  import { withClientProvider } from '@dxos/react-client/testing';
13
13
  import { useAsyncEffect } from '@dxos/react-ui';
14
- import { type ViewType, ViewProjection, createView } from '@dxos/schema';
14
+ import { ViewProjection, ViewType, createView } from '@dxos/schema';
15
15
  import { withTheme, withLayout } from '@dxos/storybook-utils';
16
16
 
17
17
  import { ViewEditor } from './ViewEditor';
@@ -41,6 +41,21 @@ const DefaultStory = () => {
41
41
  }
42
42
  }, [space]);
43
43
 
44
+ const views = useQuery(space, Filter.schema(ViewType));
45
+ const currentTypename = useMemo(() => view?.query?.type, [view]);
46
+ const updateViewTypename = useCallback(
47
+ (newTypename: string) => {
48
+ if (!schema) {
49
+ return;
50
+ }
51
+ const matchingViews = views.filter((view) => view.query.type === currentTypename);
52
+ for (const view of matchingViews) {
53
+ view.query.type = newTypename;
54
+ }
55
+ schema.updateTypename(newTypename);
56
+ },
57
+ [views, schema],
58
+ );
44
59
  const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
45
60
 
46
61
  if (!schema || !view || !projection) {
@@ -50,7 +65,13 @@ const DefaultStory = () => {
50
65
  return (
51
66
  <TestLayout json={{ schema, view, projection }}>
52
67
  <TestPanel>
53
- <ViewEditor schema={schema} view={view} registry={space?.db.schemaRegistry} onDelete={handleDelete} />
68
+ <ViewEditor
69
+ schema={schema}
70
+ view={view}
71
+ registry={space?.db.schemaRegistry}
72
+ onTypenameChanged={updateViewTypename}
73
+ onDelete={handleDelete}
74
+ />
54
75
  </TestPanel>
55
76
  </TestLayout>
56
77
  );
@@ -10,7 +10,6 @@ import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui
10
10
  import { List } from '@dxos/react-ui-list';
11
11
  import { ghostHover, inputTextLabel, mx } from '@dxos/react-ui-theme';
12
12
  import { FieldSchema, type FieldType, type ViewType, ViewProjection, VIEW_FIELD_LIMIT } from '@dxos/schema';
13
- import { arrayMove } from '@dxos/util';
14
13
 
15
14
  import { translationKey } from '../../translations';
16
15
  import { FieldEditor } from '../FieldEditor';
@@ -35,6 +34,7 @@ export type ViewEditorProps = ThemedClassName<{
35
34
  registry?: SchemaRegistry;
36
35
  readonly?: boolean;
37
36
  showHeading?: boolean;
37
+ onTypenameChanged: (typename: string) => void;
38
38
  onDelete: (fieldId: string) => void;
39
39
  }>;
40
40
 
@@ -48,6 +48,7 @@ export const ViewEditor = ({
48
48
  registry,
49
49
  readonly,
50
50
  showHeading = false,
51
+ onTypenameChanged,
51
52
  onDelete,
52
53
  }: ViewEditorProps) => {
53
54
  const { t } = useTranslation(translationKey);
@@ -67,12 +68,15 @@ export const ViewEditor = ({
67
68
  const handleViewUpdate = useCallback(
68
69
  ({ name, typename }: ViewMetaType) => {
69
70
  requestAnimationFrame(() => {
70
- view.name = name;
71
- view.query.type = typename;
72
- schema.updateTypename(typename);
71
+ if (view.name !== name) {
72
+ view.name = name;
73
+ }
74
+ if (view.query.type !== typename) {
75
+ onTypenameChanged(typename);
76
+ }
73
77
  });
74
78
  },
75
- [view, schema],
79
+ [view, schema, onTypenameChanged],
76
80
  );
77
81
 
78
82
  const handleSelect = useCallback((field: FieldType) => {
@@ -86,7 +90,11 @@ export const ViewEditor = ({
86
90
 
87
91
  const handleMove = useCallback(
88
92
  (fromIndex: number, toIndex: number) => {
89
- arrayMove(view.fields, fromIndex, toIndex);
93
+ // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
94
+ const fields = [...view.fields];
95
+ const [moved] = fields.splice(fromIndex, 1);
96
+ fields.splice(toIndex, 0, moved);
97
+ view.fields = fields;
90
98
  },
91
99
  [view.fields],
92
100
  );
@@ -4,7 +4,21 @@
4
4
 
5
5
  import React, { type JSX, type PropsWithChildren } from 'react';
6
6
 
7
- import { Input } from '@dxos/react-ui';
7
+ import { Input, type ThemedClassName } from '@dxos/react-ui';
8
+ import { mx, textBlockWidth } from '@dxos/react-ui-theme';
9
+
10
+ /**
11
+ * @deprecated
12
+ */
13
+ export const DeprecatedFormContainer = ({ children, classNames }: ThemedClassName<PropsWithChildren>) => {
14
+ return (
15
+ <div role='none' className='w-full p-4 justify-center overflow-x-hidden overflow-y-auto'>
16
+ <div role='form' className={mx('flex flex-col', textBlockWidth, classNames)}>
17
+ {children}
18
+ </div>
19
+ </div>
20
+ );
21
+ };
8
22
 
9
23
  export type DeprecatedFormInputProps = {
10
24
  label: string;
@@ -27,7 +41,9 @@ export const DeprecatedFormInput = ({
27
41
  <Input.Root>
28
42
  <div role='none' className='flex flex-col w-full'>
29
43
  {/* TODO(burdon): Consistent height for controls (e.g., Select, Textbox, and Checkbox are all different). */}
30
- <Input.Label classNames='flex min-h-[40px] items-center'>{label}</Input.Label>
44
+ <Input.Label classNames='flex min-bs-[--rail-action] items-center' style={{ fontSize: 'unset' }}>
45
+ {label}
46
+ </Input.Label>
31
47
  {description && (
32
48
  <Input.DescriptionAndValidation classNames='mbs-0.5'>
33
49
  <Input.Description>{description}</Input.Description>
@@ -36,7 +52,7 @@ export const DeprecatedFormInput = ({
36
52
  </div>
37
53
 
38
54
  <div role='none'>
39
- <div role='none' className='flex min-h-[40px] items-center'>
55
+ <div role='none' className='flex min-bs-[--rail-action] items-center'>
40
56
  {children}
41
57
  </div>
42
58
  </div>