@dxos/react-ui-form 0.8.4-main.3eb6e50203 → 0.8.4-main.3fbcb4aa9b

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 (153) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +1134 -682
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +82 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +1134 -682
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +84 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +44 -16
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +75 -73
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField.d.ts +11 -5
  21. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormFieldSet.d.ts +6 -10
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
  33. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/fields/RefField.d.ts +15 -7
  38. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  40. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
  42. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
  44. package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
  45. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
  46. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  47. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  48. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  49. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  50. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  51. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
  52. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  55. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  56. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  57. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  58. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  59. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  60. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  61. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  62. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  63. package/dist/types/src/components/Settings/Settings.d.ts +28 -33
  64. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  65. package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
  66. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  68. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  69. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  70. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  71. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  72. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  73. package/dist/types/src/components/Settings/index.d.ts +2 -0
  74. package/dist/types/src/components/Settings/index.d.ts.map +1 -1
  75. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -6
  76. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  77. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +67 -64
  78. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  79. package/dist/types/src/components/index.d.ts +2 -0
  80. package/dist/types/src/components/index.d.ts.map +1 -1
  81. package/dist/types/src/components/testing.d.ts +4 -4
  82. package/dist/types/src/components/testing.d.ts.map +1 -1
  83. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  84. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  85. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  86. package/dist/types/src/index.d.ts +0 -2
  87. package/dist/types/src/index.d.ts.map +1 -1
  88. package/dist/types/src/translations.d.ts +63 -61
  89. package/dist/types/src/translations.d.ts.map +1 -1
  90. package/dist/types/src/util/field-type.d.ts +19 -0
  91. package/dist/types/src/util/field-type.d.ts.map +1 -0
  92. package/dist/types/src/util/field.d.ts.map +1 -1
  93. package/dist/types/src/util/formatting.d.ts.map +1 -1
  94. package/dist/types/src/util/index.d.ts +1 -1
  95. package/dist/types/src/util/index.d.ts.map +1 -1
  96. package/dist/types/src/util/properties.d.ts.map +1 -1
  97. package/dist/types/src/util/refs.d.ts +1 -0
  98. package/dist/types/src/util/refs.d.ts.map +1 -1
  99. package/dist/types/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +42 -42
  101. package/src/components/FieldEditor/FieldEditor.stories.tsx +6 -8
  102. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  103. package/src/components/FieldEditor/FieldEditor.tsx +6 -6
  104. package/src/components/Form/AUDIT.md +17 -0
  105. package/src/components/Form/Form.stories.tsx +29 -44
  106. package/src/components/Form/Form.tsx +101 -54
  107. package/src/components/Form/FormField.tsx +39 -19
  108. package/src/components/Form/FormFieldComponent.stories.tsx +4 -4
  109. package/src/components/Form/FormFieldComponent.tsx +11 -11
  110. package/src/components/Form/FormFieldSet.tsx +121 -104
  111. package/src/components/Form/fields/ArrayField.tsx +10 -9
  112. package/src/components/Form/fields/GeoPointField.tsx +8 -7
  113. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  114. package/src/components/Form/fields/MarkdownField.tsx +135 -45
  115. package/src/components/Form/fields/RefField.tsx +85 -45
  116. package/src/components/Form/fields/SelectField.tsx +1 -1
  117. package/src/components/Form/fields/SelectOptionField.stories.tsx +10 -8
  118. package/src/components/Form/fields/SelectOptionField.tsx +13 -13
  119. package/src/components/Form/fields/TextField.tsx +2 -2
  120. package/src/components/Form/fields/TupleField.tsx +1 -1
  121. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  122. package/src/components/ObjectForm/index.ts +5 -0
  123. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +16 -18
  124. package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
  125. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  126. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  127. package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
  128. package/src/components/ObjectProperties/index.ts +5 -0
  129. package/src/components/Settings/AUDIT.md +52 -0
  130. package/src/components/Settings/Settings.stories.tsx +69 -21
  131. package/src/components/Settings/Settings.tsx +33 -187
  132. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  133. package/src/components/Settings/SettingsItem.tsx +58 -0
  134. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  135. package/src/components/Settings/index.ts +2 -0
  136. package/src/components/ViewEditor/ViewEditor.stories.tsx +21 -19
  137. package/src/components/ViewEditor/ViewEditor.test.tsx +2 -2
  138. package/src/components/ViewEditor/ViewEditor.tsx +87 -50
  139. package/src/components/index.ts +2 -0
  140. package/src/components/testing.tsx +23 -12
  141. package/src/hooks/useFormHandler.ts +42 -24
  142. package/src/hooks/useKeyHandler.tsx +7 -7
  143. package/src/index.ts +0 -4
  144. package/src/translations.ts +66 -63
  145. package/src/util/field-type.ts +60 -0
  146. package/src/util/index.ts +1 -1
  147. package/src/util/refs.ts +7 -3
  148. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  149. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  150. package/dist/types/src/util/echo.d.ts +0 -6
  151. package/dist/types/src/util/echo.d.ts.map +0 -1
  152. package/src/components/Form/Form-old.stories.tsx +0 -314
  153. package/src/util/echo.ts +0 -25
@@ -0,0 +1,59 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Select } from '@dxos/react-ui';
8
+
9
+ import { type SettingsFieldProps } from './SettingsFieldSet';
10
+
11
+ export type SelectFieldOption = {
12
+ value: string;
13
+ label?: string;
14
+ };
15
+
16
+ export type CreateSelectFieldOptions = {
17
+ /** Options to display. Strings are used as both value and label. */
18
+ options: ReadonlyArray<string | SelectFieldOption>;
19
+ /** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
20
+ defaultLabel?: string | null;
21
+ };
22
+
23
+ /**
24
+ * Factory for a `SettingsFieldSet` `fieldMap` entry that renders a Select control
25
+ * over a fixed list of options, with an optional sentinel for `undefined`.
26
+ */
27
+ export const createSelectField = ({
28
+ options,
29
+ defaultLabel = 'Default',
30
+ }: CreateSelectFieldOptions): React.FC<SettingsFieldProps<string | undefined>> => {
31
+ const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
32
+ const hasDefault = defaultLabel !== null;
33
+ // Per-instance sentinel avoids collisions with real option values; empty string when the
34
+ // sentinel is omitted so `undefined` maps to a value Select.Root treats as "no selection".
35
+ const sentinel = hasDefault ? `__default__${Math.random().toString(36).slice(2)}` : '';
36
+
37
+ return ({ value, onChange, readonly }) => (
38
+ <Select.Root
39
+ disabled={readonly}
40
+ value={value ?? sentinel}
41
+ onValueChange={(next) => onChange(hasDefault && next === sentinel ? undefined : next)}
42
+ >
43
+ <Select.TriggerButton disabled={readonly} />
44
+ <Select.Portal>
45
+ <Select.Content>
46
+ <Select.Viewport>
47
+ {hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
48
+ {normalized.map((option) => (
49
+ <Select.Option key={option.value} value={option.value}>
50
+ {option.label ?? option.value}
51
+ </Select.Option>
52
+ ))}
53
+ </Select.Viewport>
54
+ <Select.Arrow />
55
+ </Select.Content>
56
+ </Select.Portal>
57
+ </Select.Root>
58
+ );
59
+ };
@@ -3,3 +3,5 @@
3
3
  //
4
4
 
5
5
  export * from './Settings';
6
+ export { type SettingsFieldMap, type SettingsFieldProps, type SettingsFieldSetProps } from './SettingsFieldSet';
7
+ export { createSelectField, type CreateSelectFieldOptions, type SelectFieldOption } from './SettingsSelectField';
@@ -6,18 +6,19 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
8
 
9
- import { DXN, Filter, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
9
+ import { DXN, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
10
+ import { type View } from '@dxos/echo';
10
11
  import { type EchoSchema, Format, type Mutable } from '@dxos/echo/internal';
11
12
  import { useQuery } from '@dxos/react-client/echo';
12
13
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
13
14
  import { useAsyncEffect } from '@dxos/react-ui';
14
- import { withTheme } from '@dxos/react-ui/testing';
15
- import { type ProjectionModel, View, getTypenameFromQuery } from '@dxos/schema';
15
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
+ import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
16
17
  import { Employer, Organization, Person, Pipeline } from '@dxos/types';
17
18
 
18
- import { translations } from '../../translations';
19
- import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
19
+ import { translations } from '#translations';
20
20
 
21
+ import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
21
22
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
22
23
 
23
24
  const types = [
@@ -35,9 +36,9 @@ export type ViewEditorDebugObjects = {
35
36
  projection: ProjectionModel;
36
37
  };
37
38
 
38
- type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
39
+ type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
39
40
 
40
- const DefaultStory = (props: StoryProps) => {
41
+ const DefaultStory = (props: DefaultStoryProps) => {
41
42
  const { space } = useClientStory();
42
43
  const [schema, setSchema] = useState<EchoSchema>();
43
44
  const [view, setView] = useState<View.View>();
@@ -53,7 +54,7 @@ const DefaultStory = (props: StoryProps) => {
53
54
  salary: Format.Currency(),
54
55
  }).pipe(
55
56
  Type.object({
56
- typename: 'example.com/type/Test',
57
+ typename: 'com.example.type.test',
57
58
  version: '0.1.0',
58
59
  }),
59
60
  );
@@ -64,16 +65,16 @@ const DefaultStory = (props: StoryProps) => {
64
65
  completed: Schema.Boolean,
65
66
  }).pipe(
66
67
  Type.object({
67
- typename: 'example.com/type/Alternate',
68
+ typename: 'com.example.type.alternate',
68
69
  version: '0.1.0',
69
70
  }),
70
71
  );
71
72
 
72
73
  const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
73
- const view = View.make({
74
+ const view = ViewModel.make({
74
75
  name: 'Test',
75
76
  query: Query.select(Filter.type(TestSchema)),
76
- jsonSchema: Type.toJsonSchema(TestSchema),
77
+ jsonSchema: JsonSchema.toJsonSchema(TestSchema),
77
78
  });
78
79
 
79
80
  setSchema(testSchema);
@@ -89,9 +90,9 @@ const DefaultStory = (props: StoryProps) => {
89
90
 
90
91
  if (props.mode === 'tag') {
91
92
  const queue = target && DXN.tryParse(target) ? target : undefined;
92
- const query = queue ? Query.fromAst(newQuery).options({ queues: [queue] }) : Query.fromAst(newQuery);
93
- Obj.change(view, (v) => {
94
- v.query.ast = query.ast as Mutable<typeof query.ast>;
93
+ const query = queue ? Query.fromAst(newQuery).from({ queues: [queue] }) : Query.fromAst(newQuery);
94
+ Obj.update(view, (view) => {
95
+ view.query.ast = query.ast as Mutable<typeof query.ast>;
95
96
  });
96
97
 
97
98
  const typename = getTypenameFromQuery(query.ast);
@@ -100,17 +101,17 @@ const DefaultStory = (props: StoryProps) => {
100
101
  return;
101
102
  }
102
103
 
103
- const newView = View.make({
104
+ const newView = ViewModel.make({
104
105
  query,
105
106
  jsonSchema: newSchema.jsonSchema,
106
107
  });
107
- Obj.change(view, (v) => {
108
- v.projection = Obj.getSnapshot(newView).projection as Mutable<typeof v.projection>;
108
+ Obj.update(view, (view) => {
109
+ view.projection = Obj.getSnapshot(newView).projection as Mutable<typeof view.projection>;
109
110
  });
110
111
  setSchema(() => newSchema);
111
112
  } else {
112
- Obj.change(view, (v) => {
113
- v.query.ast = newQuery as Mutable<typeof newQuery>;
113
+ Obj.update(view, (view) => {
114
+ view.query.ast = newQuery as Mutable<typeof newQuery>;
114
115
  });
115
116
  schema.updateTypename(getTypenameFromQuery(newQuery));
116
117
  }
@@ -176,6 +177,7 @@ const meta = {
176
177
  },
177
178
  }),
178
179
  withTheme(),
180
+ withLayout({ layout: 'fullscreen' }),
179
181
  ],
180
182
  parameters: {
181
183
  layout: 'fullscreen',
@@ -6,11 +6,11 @@ import { composeStories } from '@storybook/react';
6
6
  import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
7
7
  import { afterEach, describe, expect, test } from 'vitest';
8
8
 
9
+ import { View } from '@dxos/echo';
9
10
  import { EchoSchema, isInstanceOf } from '@dxos/echo/internal';
10
- import { ProjectionModel, View } from '@dxos/schema';
11
+ import { ProjectionModel } from '@dxos/schema';
11
12
 
12
13
  import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
13
-
14
14
  import * as stories from './ViewEditor.stories';
15
15
  import { type ViewEditorDebugObjects } from './ViewEditor.stories';
16
16
 
@@ -9,25 +9,25 @@ import * as Option from 'effect/Option';
9
9
  import * as Schema from 'effect/Schema';
10
10
  import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
11
11
 
12
- import { Filter, Format, Obj, Query, QueryAST, type SchemaRegistry } from '@dxos/echo';
12
+ import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry } from '@dxos/echo';
13
+ import { View } from '@dxos/echo';
13
14
  import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
14
15
  import { invariant } from '@dxos/invariant';
15
- import { useObject } from '@dxos/react-client/echo';
16
- import { Callout, IconButton, Input, type ThemedClassName, useTranslation } from '@dxos/react-ui';
16
+ import { useObject, useQuery } from '@dxos/react-client/echo';
17
+ import { IconButton, Input, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
17
18
  import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
18
19
  import { List } from '@dxos/react-ui-list';
19
20
  import {
20
- FieldSchema,
21
- type FieldType,
21
+ ParentLabelAnnotation,
22
22
  ProjectionModel,
23
23
  VIEW_FIELD_LIMIT,
24
- type View,
25
24
  createEchoChangeCallback,
26
25
  getTypenameFromQuery,
27
26
  } from '@dxos/schema';
28
27
  import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
29
28
 
30
- import { translationKey } from '../../translations';
29
+ import { translationKey } from '#translations';
30
+
31
31
  import { FieldEditor } from '../FieldEditor';
32
32
  import {
33
33
  Form,
@@ -35,22 +35,20 @@ import {
35
35
  type FormFieldComponentProps,
36
36
  FormFieldLabel,
37
37
  type FormFieldMap,
38
+ type FormRootProps,
38
39
  } from '../Form';
39
40
 
40
- const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
41
- const listItemGrid = 'grid grid-cols-subgrid col-span-5';
42
-
43
41
  export type ViewEditorProps = ThemedClassName<
44
42
  {
45
43
  schema: Schema.Schema.AnyNoContext;
46
44
  view: View.View;
47
45
  mode?: 'schema' | 'tag';
48
46
  registry?: SchemaRegistry.SchemaRegistry;
49
- readonly?: boolean;
50
47
  showHeading?: boolean;
51
48
  onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
52
49
  onDelete?: (fieldId: string) => void;
53
- } & Pick<QueryFormProps, 'types' | 'tags'>
50
+ } & Pick<QueryFormProps, 'types' | 'tags'> &
51
+ Pick<FormRootProps<any>, 'readonly' | 'db'>
54
52
  >;
55
53
 
56
54
  /**
@@ -64,6 +62,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
64
62
  view,
65
63
  mode = 'schema',
66
64
  registry,
65
+ db,
67
66
  readonly,
68
67
  showHeading = false,
69
68
  types,
@@ -98,15 +97,29 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
98
97
  useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
99
98
 
100
99
  const queueTarget = Match.value(view.query.ast).pipe(
101
- Match.when({ type: 'options' }, ({ options }) => {
102
- return Option.fromNullable(options.queues).pipe(
100
+ Match.when({ type: 'from' }, ({ from }) => {
101
+ if (from._tag !== 'scope') {
102
+ return undefined;
103
+ }
104
+ return Option.fromNullable(from.scope.queues).pipe(
103
105
  Option.flatMap((queues) => Array.head(queues)),
106
+ Option.map(String),
104
107
  Option.getOrUndefined,
105
108
  );
106
109
  }),
107
110
  Match.orElse(() => undefined),
108
111
  );
109
112
 
113
+ const feeds = useQuery(db, Filter.type(Feed.Feed));
114
+
115
+ const targetRef = useMemo(() => {
116
+ if (!queueTarget) {
117
+ return undefined;
118
+ }
119
+ const feed = feeds.find((feed) => Feed.getQueueDxn(feed)?.toString() === queueTarget);
120
+ return feed ? Ref.fromDXN(Entity.getDXN(feed)) : undefined;
121
+ }, [queueTarget, feeds]);
122
+
110
123
  const viewSchema = useMemo(() => {
111
124
  const base = Schema.Struct({
112
125
  query:
@@ -118,7 +131,12 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
118
131
  if (mode === 'tag') {
119
132
  return Schema.Struct({
120
133
  ...base.fields,
121
- target: Schema.optional(Schema.String.annotations({ title: 'Target Queue' })),
134
+ // TODO(wittjosiah): Replace Type.Feed with Dataset.Dataset when Ref.Ref supports unions.
135
+ target: Ref.Ref(Feed.Feed).pipe(
136
+ Schema.annotations({ title: 'Target Feed' }),
137
+ ParentLabelAnnotation.set(true),
138
+ Schema.optional,
139
+ ),
122
140
  }).pipe(Schema.mutable);
123
141
  }
124
142
 
@@ -130,9 +148,9 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
130
148
  const viewValues = useMemo(
131
149
  () => ({
132
150
  query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
133
- target: queueTarget,
151
+ target: targetRef,
134
152
  }),
135
- [mode, view.query.ast, queueTarget],
153
+ [mode, view.query.ast, targetRef],
136
154
  );
137
155
 
138
156
  const fieldMap = useMemo<FormFieldMap | undefined>(
@@ -142,12 +160,25 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
142
160
 
143
161
  const handleUpdate = useCallback(
144
162
  (values: any) => {
145
- requestAnimationFrame(() => {
146
- const query = mode === 'schema' ? Query.select(Filter.typename(values.query)).ast : values.query;
147
- onQueryChanged?.(query, values.target);
148
- });
163
+ const targetValue = values.target;
164
+ let queueDxn: string | undefined;
165
+
166
+ if (Ref.isRef(targetValue)) {
167
+ const feedDxn = targetValue.dxn.toString();
168
+ const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === feedDxn);
169
+ if (feed) {
170
+ queueDxn = Feed.getQueueDxn(feed)?.toString();
171
+ }
172
+ }
173
+
174
+ // TODO(wittjosiah): Deep-clone the AST to plain JS or ECHO proxy arrays become objects with numeric keys.
175
+ const query =
176
+ mode === 'schema'
177
+ ? Query.select(Filter.typename(values.query)).ast
178
+ : JSON.parse(JSON.stringify(values.query));
179
+ onQueryChanged?.(query, queueDxn);
149
180
  },
150
- [onQueryChanged, view, queueTarget, mode],
181
+ [onQueryChanged, mode, feeds],
151
182
  );
152
183
 
153
184
  const handleDelete = useCallback(
@@ -159,19 +190,19 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
159
190
  );
160
191
 
161
192
  return (
162
- <div role='none' className={mx(classNames)}>
193
+ <div className={mx(classNames)}>
163
194
  {/* If readonly is set, then the callout is not needed. */}
164
195
  {schemaReadonly && !readonly && (
165
- <Callout.Root valence='info' classNames='mlb-cardSpacingBlock'>
166
- <Callout.Title>{t('system schema description')}</Callout.Title>
167
- </Callout.Root>
196
+ <Message.Root valence='info' classNames='my-form-padding'>
197
+ <Message.Title>{t('system-schema.description')}</Message.Title>
198
+ </Message.Root>
168
199
  )}
169
200
 
170
201
  {/* TODO(burdon): Is the form read-only or just the schema? */}
171
- <Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} autoSave onSave={handleUpdate}>
202
+ <Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} db={db} onValuesChanged={handleUpdate}>
172
203
  <Form.FieldSet />
173
204
 
174
- <FormFieldLabel label={t('fields label')} asChild />
205
+ <FormFieldLabel label={t('fields.label')} asChild />
175
206
  <FieldList
176
207
  schema={schema}
177
208
  view={view}
@@ -214,7 +245,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
214
245
  return model;
215
246
  }, [atomRegistry, schema, view]);
216
247
 
217
- const [expandedField, setExpandedField] = useState<FieldType['id']>();
248
+ const [expandedField, setExpandedField] = useState<View.FieldType['id']>();
218
249
 
219
250
  const handleAdd = useCallback(() => {
220
251
  invariant(!readonly);
@@ -223,7 +254,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
223
254
  }, [projectionModel, readonly]);
224
255
 
225
256
  const handleToggleField = useCallback(
226
- (field: FieldType) => {
257
+ (field: View.FieldType) => {
227
258
  setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
228
259
  },
229
260
  [readonly],
@@ -244,12 +275,12 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
244
275
  const handleMove = useCallback(
245
276
  (fromIndex: number, toIndex: number) => {
246
277
  invariant(!readonly);
247
- Obj.change(view, (v) => {
278
+ Obj.update(view, (view) => {
248
279
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
249
- const fields = [...v.projection.fields];
280
+ const fields = [...view.projection.fields];
250
281
  const [moved] = fields.splice(fromIndex, 1);
251
282
  fields.splice(toIndex, 0, moved);
252
- v.projection.fields = fields;
283
+ view.projection.fields = fields;
253
284
  });
254
285
  },
255
286
  [view, readonly],
@@ -278,33 +309,39 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
278
309
  }
279
310
 
280
311
  return (
281
- <List.Root<FieldType>
282
- items={viewSnapshot.projection.fields as FieldType[]}
283
- isItem={Schema.is(FieldSchema)}
312
+ <List.Root<View.FieldType>
313
+ items={viewSnapshot.projection.fields as View.FieldType[]}
314
+ isItem={Schema.is(View.FieldSchema)}
284
315
  getId={(field) => field.id}
285
316
  onMove={readonly ? undefined : handleMove}
286
317
  readonly={readonly}
287
318
  >
288
319
  {({ items: fields }) => (
289
320
  <>
290
- {showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
291
- <div role='list' className={listGrid}>
321
+ {showHeading && <h3 className='text-sm'>{t('field-path.label')}</h3>}
322
+ <div role='list' className='grid grid-cols-[min-content_1fr_min-content_min-content_min-content]'>
292
323
  {fields?.map((field) => {
293
324
  const hidden = field.visible === false;
294
325
  return (
295
- <List.Item<FieldType>
326
+ <List.Item<View.FieldType>
296
327
  key={field.id}
297
328
  item={field}
298
- classNames={listItemGrid}
329
+ classNames={'grid grid-cols-subgrid col-span-5'}
299
330
  aria-expanded={expandedField === field.id}
300
331
  >
301
- <div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
332
+ <div
333
+ className={mx(
334
+ subtleHover,
335
+ 'grid grid-cols-subgrid col-span-5',
336
+ 'rounded-xs cursor-pointer min-h-10',
337
+ )}
338
+ >
302
339
  <List.ItemDragHandle disabled={readonly || schemaReadonly} />
303
340
  <List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
304
341
  {field.path}
305
342
  </List.ItemTitle>
306
- <List.ItemButton
307
- label={t(hidden ? 'show field label' : 'hide field label')}
343
+ <List.ItemIconButton
344
+ label={t(hidden ? 'show-field.label' : 'hide-field.label')}
308
345
  data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
309
346
  icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
310
347
  autoHide={false}
@@ -314,16 +351,16 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
314
351
  {!readonly && (
315
352
  <>
316
353
  <List.ItemDeleteButton
317
- label={t('delete field label')}
354
+ label={t('delete-field.label')}
318
355
  autoHide={false}
319
356
  disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
320
357
  onClick={() => handleDelete(field.id)}
321
358
  data-testid='field.delete'
322
359
  />
323
- <IconButton
360
+ <List.ItemIconButton
324
361
  iconOnly
325
362
  variant='ghost'
326
- label={t('toggle expand label', { ns: osTranslations })}
363
+ label={t('toggle-expand.label', { ns: osTranslations })}
327
364
  icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
328
365
  onClick={() => handleToggleField(field)}
329
366
  data-testid='field.toggle'
@@ -332,7 +369,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
332
369
  )}
333
370
  </div>
334
371
  {expandedField === field.id && !readonly && (
335
- <div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
372
+ <div className='col-span-full mt-1 mb-1 border border-separator rounded-md'>
336
373
  <FieldEditor
337
374
  readonly={readonly || schemaReadonly}
338
375
  registry={registry}
@@ -347,13 +384,13 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
347
384
  })}
348
385
  </div>
349
386
  {!readonly && !expandedField && (
350
- <div role='none' className='mlb-cardSpacingBlock'>
387
+ <div className='my-form-padding'>
351
388
  <IconButton
352
389
  icon='ph--plus--regular'
353
- label={t('add property button label')}
390
+ label={t('add-property-button.label')}
354
391
  onClick={handleAdd}
355
392
  disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
356
- classNames='is-full'
393
+ classNames='w-full'
357
394
  />
358
395
  </div>
359
396
  )}
@@ -4,6 +4,8 @@
4
4
 
5
5
  export * from './FieldEditor';
6
6
  export * from './Form';
7
+ export * from './ObjectForm';
7
8
  export * from './ObjectPicker';
9
+ export * from './ObjectProperties';
8
10
  export * from './Settings';
9
11
  export * from './ViewEditor';
@@ -5,18 +5,24 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
- import { Card } from '@dxos/react-ui-mosaic';
9
- import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
10
- import { textBlockWidth } from '@dxos/ui-theme';
8
+ import { Syntax } from '@dxos/react-ui-syntax-highlighter';
9
+ import { composableProps, slottable } from '@dxos/ui-theme';
11
10
 
12
- type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
11
+ type TestLayoutProps = PropsWithChildren<{ json?: any }>;
13
12
 
14
- export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
13
+ export const TestLayout = ({ children, json }: TestLayoutProps) => {
15
14
  return (
16
- <div className='is-full bs-full grid grid-cols-[1fr_1fr] p-4 gap-4 overflow-hidden'>
17
- <TestPanel classNames={['flex flex-col bs-full overflow-y-auto', classNames]}>{children}</TestPanel>
18
- <TestPanel classNames={'overflow-hidden'}>
19
- <JsonFilter testId='debug' data={json} classNames='bs-full text-xs' />
15
+ <div className='dx-container grid grid-cols-[1fr_1fr] p-4 gap-4'>
16
+ <TestPanel>{children}</TestPanel>
17
+ <TestPanel>
18
+ <Syntax.Root data={json}>
19
+ <Syntax.Content>
20
+ <Syntax.Filter />
21
+ <Syntax.Viewport>
22
+ <Syntax.Code testId='debug' classNames='text-xs' />
23
+ </Syntax.Viewport>
24
+ </Syntax.Content>
25
+ </Syntax.Root>
20
26
  </TestPanel>
21
27
  </div>
22
28
  );
@@ -24,10 +30,15 @@ export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
24
30
 
25
31
  type TestPanelProps = ThemedClassName<PropsWithChildren>;
26
32
 
27
- const TestPanel = ({ classNames, children }: TestPanelProps) => {
28
- return <Card.Root classNames={['bs-full overflow-y-auto', textBlockWidth, classNames]}>{children}</Card.Root>;
29
- };
33
+ const TestPanel = slottable<HTMLDivElement, TestPanelProps>(({ children }, forwardedRef) => {
34
+ return (
35
+ <div {...composableProps({ classNames: 'dx-container bg-modal-surface rounded-md' })} ref={forwardedRef}>
36
+ {children}
37
+ </div>
38
+ );
39
+ });
30
40
 
31
41
  // Symbol for accessing debug objects in tests.
32
42
  export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
33
43
  export const FIELD_EDITOR_DEBUG_SYMBOL = Symbol('fieldEditorDebug');
44
+ export const OBJECT_PROPERTIES_DEBUG_SYMBOL = Symbol('objectPropertiesDebug');