@dxos/react-ui-form 0.8.4-main.9735255 → 0.8.4-main.9be5663bfe

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 (119) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +989 -674
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +989 -674
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +62 -61
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +23 -18
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +68 -67
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +7 -1
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/RefField.d.ts +8 -6
  26. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +62 -61
  29. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  31. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  32. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  33. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  34. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
  35. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  36. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -61
  37. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  39. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  40. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +83 -0
  41. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  43. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  44. package/dist/types/src/components/Settings/Settings.d.ts +35 -0
  45. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  46. package/dist/types/src/components/Settings/Settings.stories.d.ts +82 -0
  47. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/Settings/index.d.ts +2 -0
  49. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  50. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
  51. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  52. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +66 -64
  53. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/index.d.ts +3 -1
  55. package/dist/types/src/components/index.d.ts.map +1 -1
  56. package/dist/types/src/components/testing.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  59. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  60. package/dist/types/src/translations.d.ts +62 -61
  61. package/dist/types/src/translations.d.ts.map +1 -1
  62. package/dist/types/src/util/index.d.ts +0 -1
  63. package/dist/types/src/util/index.d.ts.map +1 -1
  64. package/dist/types/src/util/refs.d.ts +1 -0
  65. package/dist/types/src/util/refs.d.ts.map +1 -1
  66. package/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +37 -38
  68. package/src/components/FieldEditor/FieldEditor.stories.tsx +7 -8
  69. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  70. package/src/components/FieldEditor/FieldEditor.tsx +3 -4
  71. package/src/components/Form/AUDIT.md +17 -0
  72. package/src/components/Form/Form.stories.tsx +25 -26
  73. package/src/components/Form/Form.tsx +100 -66
  74. package/src/components/Form/FormField.tsx +32 -14
  75. package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
  76. package/src/components/Form/FormFieldComponent.tsx +17 -20
  77. package/src/components/Form/FormFieldSet.tsx +126 -105
  78. package/src/components/Form/fields/ArrayField.tsx +11 -8
  79. package/src/components/Form/fields/GeoPointField.tsx +6 -6
  80. package/src/components/Form/fields/MarkdownField.tsx +1 -1
  81. package/src/components/Form/fields/RefField.tsx +37 -20
  82. package/src/components/Form/fields/SelectField.tsx +1 -1
  83. package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -3
  84. package/src/components/Form/fields/SelectOptionField.tsx +9 -10
  85. package/src/components/Form/fields/TupleField.tsx +1 -1
  86. package/src/components/ObjectForm/ObjectForm.tsx +113 -0
  87. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  88. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -18
  89. package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
  90. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
  91. package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
  92. package/src/components/ObjectProperties/index.ts +5 -0
  93. package/src/components/Settings/Settings.stories.tsx +55 -0
  94. package/src/components/Settings/Settings.tsx +129 -0
  95. package/src/components/Settings/index.ts +5 -0
  96. package/src/components/ViewEditor/ViewEditor.stories.tsx +36 -33
  97. package/src/components/ViewEditor/ViewEditor.test.tsx +3 -7
  98. package/src/components/ViewEditor/ViewEditor.tsx +99 -70
  99. package/src/components/index.ts +3 -1
  100. package/src/components/testing.tsx +20 -8
  101. package/src/hooks/useFormHandler.ts +42 -24
  102. package/src/hooks/useKeyHandler.tsx +7 -7
  103. package/src/translations.ts +63 -63
  104. package/src/util/index.ts +0 -1
  105. package/src/util/refs.ts +7 -3
  106. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  107. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  108. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
  109. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  110. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  111. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  112. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  113. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  114. package/dist/types/src/util/echo.d.ts +0 -6
  115. package/dist/types/src/util/echo.d.ts.map +0 -1
  116. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  117. package/src/components/ControlSection/ControlSection.tsx +0 -153
  118. package/src/components/Form/Form-old.stories.tsx +0 -314
  119. package/src/util/echo.ts +0 -25
@@ -0,0 +1,113 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+ import React, { useCallback, useMemo } from 'react';
7
+
8
+ import { DXN, Obj, Ref, Tag, Type } from '@dxos/echo';
9
+ import { type JsonPath, splitJsonPath } from '@dxos/echo/internal';
10
+ import { invariant } from '@dxos/invariant';
11
+ import { HuePicker } from '@dxos/react-ui-pickers';
12
+ import { isNonNullable } from '@dxos/util';
13
+
14
+ import { translationKey } from '../../translations';
15
+ import { Form, type FormFieldMap, omitId } from '../Form';
16
+
17
+ export type ObjectFormProps = {
18
+ schema: Schema.Schema.AnyNoContext;
19
+ object: Obj.Unknown;
20
+ };
21
+
22
+ const createFieldMap: FormFieldMap = {
23
+ hue: ({ type, label, layout, getValue, onValueChange }) => {
24
+ const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
25
+ const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
26
+ return (
27
+ <>
28
+ {layout !== 'inline' && <Form.Label label={label} />}
29
+ <HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
30
+ </>
31
+ );
32
+ },
33
+ };
34
+
35
+ export const ObjectForm = ({ object, schema }: ObjectFormProps) => {
36
+ const db = Obj.getDatabase(object);
37
+ const meta = Obj.getMeta(object);
38
+ const tags = (meta.tags ?? []).map((tag) => db?.makeRef(DXN.parse(tag))).filter(isNonNullable);
39
+ const values = useMemo(() => ({ tags, ...object }), [object, tags]);
40
+ const formSchema = useMemo(
41
+ () =>
42
+ omitId(
43
+ Schema.Struct({
44
+ tags: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.optional),
45
+ }).pipe(Schema.extend(schema)),
46
+ ),
47
+ [schema],
48
+ );
49
+
50
+ const handleCreate = useCallback((schema: Type.AnyEntity, values: any) => {
51
+ invariant(db);
52
+ invariant(Type.isObjectSchema(schema));
53
+ const newObject = db.add(Obj.make(schema, values));
54
+ if (Obj.instanceOf(Tag.Tag, newObject)) {
55
+ Obj.change(object, (object) => {
56
+ Obj.getMeta(object).tags = [...(Obj.getMeta(object).tags ?? []), Obj.getDXN(newObject).toString()];
57
+ });
58
+ }
59
+ }, []);
60
+
61
+ // TODO(wittjosiah): Use FormRootProps type.
62
+ const handleChange = useCallback(
63
+ ({ tags, ...values }: any, { isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> }) => {
64
+ if (!isValid) {
65
+ return;
66
+ }
67
+
68
+ const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
69
+
70
+ // Handle tags separately using Obj.change.
71
+ const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
72
+ if (hasTagsChange) {
73
+ Obj.change(object, (object) => {
74
+ Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.dxn.toString()) ?? [];
75
+ });
76
+ }
77
+
78
+ // Handle other property changes.
79
+ const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
80
+ if (nonTagPaths.length > 0) {
81
+ Obj.change(object, () => {
82
+ for (const path of nonTagPaths) {
83
+ const parts = splitJsonPath(path);
84
+ const value = Obj.getValue(values, parts);
85
+ Obj.setValue(object, parts, value);
86
+ }
87
+ });
88
+ }
89
+ },
90
+ [object],
91
+ );
92
+
93
+ return (
94
+ <Form.Root
95
+ schema={formSchema}
96
+ defaultValues={values}
97
+ createTypename={Type.getTypename(Tag.Tag)}
98
+ createOptionIcon='ph--plus--regular'
99
+ createOptionLabel={['add-tag.label', { ns: translationKey }]}
100
+ createInitialValuePath='label'
101
+ createFieldMap={createFieldMap}
102
+ db={db}
103
+ onValuesChanged={handleChange}
104
+ onCreate={handleCreate}
105
+ >
106
+ <Form.Viewport>
107
+ <Form.Content>
108
+ <Form.FieldSet />
109
+ </Form.Content>
110
+ </Form.Viewport>
111
+ </Form.Root>
112
+ );
113
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './ControlSection';
5
+ export * from './ObjectForm';
@@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
8
8
  import { expect, fn, userEvent, within } from 'storybook/test';
9
9
 
10
10
  import { Obj } from '@dxos/echo';
11
- import { faker } from '@dxos/random';
11
+ import { random } from '@dxos/random';
12
12
  import { Filter, useQuery } from '@dxos/react-client/echo';
13
13
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
14
14
  import { Button } from '@dxos/react-ui';
@@ -17,14 +17,13 @@ import { Person } from '@dxos/types';
17
17
  import { osTranslations } from '@dxos/ui-theme';
18
18
 
19
19
  import { translations } from '../../translations';
20
-
21
20
  import { ObjectPicker } from './ObjectPicker';
22
21
 
23
- faker.seed(1);
22
+ random.seed(1);
24
23
 
25
24
  const createPerson = () =>
26
25
  Obj.make(Person.Person, {
27
- fullName: faker.person.fullName(),
26
+ fullName: random.person.fullName(),
28
27
  });
29
28
 
30
29
  const omitId = Schema.omit<any, any, ['id']>('id');
@@ -74,18 +73,18 @@ const DefaultStory = () => {
74
73
  );
75
74
 
76
75
  return (
77
- <div role='none' className='flex is-96'>
76
+ <div role='none' className='w-96'>
78
77
  <ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
79
78
  <ObjectPicker.Trigger asChild>
80
- <Button variant='primary' data-testid='trigger' classNames='is-full'>
79
+ <Button variant='primary' data-testid='trigger' classNames='w-full'>
81
80
  Select Person
82
81
  </Button>
83
82
  </ObjectPicker.Trigger>
84
83
  <ObjectPicker.Content
85
- classNames='popover-card-width'
84
+ classNames='dx-card-popover-width'
86
85
  options={options}
87
86
  createSchema={personSchema}
88
- createOptionLabel={['create new person label', { ns: osTranslations }]}
87
+ createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
89
88
  createOptionIcon='ph--user-plus--regular'
90
89
  createInitialValuePath='fullName'
91
90
  onCreate={handleCreateCallback}
@@ -100,7 +99,7 @@ const meta = {
100
99
  title: 'ui/react-ui-form/ObjectPicker',
101
100
  render: DefaultStory,
102
101
  decorators: [
103
- withTheme,
102
+ withTheme(),
104
103
  withClientProvider({
105
104
  types: [Person.Person],
106
105
  createIdentity: true,
@@ -201,14 +200,10 @@ export const WithTest: Story = {
201
200
  await userEvent.click(saveButton);
202
201
 
203
202
  // Check that clicking the save button calls onCreate with expected values.
204
- await expect(mockHandleCreate).toHaveBeenCalledWith({
205
- fullName: unrelatedTerm,
206
- addresses: [],
207
- phoneNumbers: [],
208
- emails: [],
209
- identities: [],
210
- urls: [],
211
- fields: [],
212
- });
203
+ await expect(mockHandleCreate).toHaveBeenCalledWith(
204
+ expect.objectContaining({
205
+ fullName: unrelatedTerm,
206
+ }),
207
+ );
213
208
  },
214
209
  };
@@ -6,10 +6,11 @@ import type * as Schema from 'effect/Schema';
6
6
  import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
7
 
8
8
  import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
- import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-searchlist';
9
+ import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-search';
10
10
 
11
11
  import { translationKey } from '../../translations';
12
12
  import { Form } from '../Form';
13
+ import { type FormFieldMap } from '../Form/FormFieldComponent';
13
14
 
14
15
  export type RefOption = {
15
16
  id: string;
@@ -24,36 +25,11 @@ export type ObjectPickerContentProps = ThemedClassName<{
24
25
  createOptionIcon?: string;
25
26
  createSchema?: Schema.Schema.AnyNoContext;
26
27
  createInitialValuePath?: string;
28
+ createFieldMap?: FormFieldMap;
27
29
  onCreate?: (values: any) => void;
28
30
  onSelect: (id: string) => void;
29
31
  }>;
30
32
 
31
- const CreateItem = ({
32
- createOptionLabel,
33
- createOptionIcon,
34
- onCreateItemSelect,
35
- }: {
36
- createOptionLabel: [string, { ns: string }];
37
- createOptionIcon: string;
38
- onCreateItemSelect: (query: string) => void;
39
- }) => {
40
- const { t } = useTranslation(translationKey);
41
- const { query } = useSearchListInput();
42
-
43
- return (
44
- <Combobox.Item
45
- value='__create__'
46
- label={t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })}
47
- icon={createOptionIcon}
48
- classNames='flex items-center gap-2'
49
- closeOnSelect={false}
50
- onSelect={() => {
51
- onCreateItemSelect(query);
52
- }}
53
- />
54
- );
55
- };
56
-
57
33
  const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
58
34
  (
59
35
  {
@@ -63,11 +39,12 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
63
39
  createOptionLabel,
64
40
  createOptionIcon,
65
41
  createInitialValuePath,
42
+ createFieldMap,
66
43
  onSelect,
67
44
  onCreate,
68
45
  ...props
69
46
  },
70
- ref,
47
+ forwardedRef,
71
48
  ) => {
72
49
  const { t } = useTranslation(translationKey);
73
50
  const [showForm, setShowForm] = useState(false);
@@ -112,14 +89,15 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
112
89
  [createSchema],
113
90
  );
114
91
 
115
- return (
116
- <Combobox.Content {...props} ref={ref} onSearch={handleSearch} onKeyDownCapture={handleKeyDown}>
117
- {showForm && createSchema ? (
92
+ if (showForm && createSchema) {
93
+ return (
94
+ <Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
118
95
  <Popover.Viewport>
119
96
  <Form.Root
120
97
  testId='create-referenced-object-form'
121
98
  schema={createSchema}
122
- values={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
99
+ defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
100
+ fieldMap={createFieldMap}
123
101
  onSave={handleFormSave}
124
102
  onCancel={handleFormCancel}
125
103
  >
@@ -131,40 +109,73 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
131
109
  </Form.Viewport>
132
110
  </Form.Root>
133
111
  </Popover.Viewport>
134
- ) : (
135
- <>
136
- <Combobox.Input placeholder={t('ref field combobox input placeholder')} autoFocus />
137
- <Combobox.List>
138
- {results.map((option) => (
139
- <Combobox.Item
140
- key={option.id}
141
- value={option.id}
142
- label={option.label}
143
- checked={selectedIds?.includes(option.id)}
144
- onSelect={() => onSelect(option.id)}
145
- classNames='flex items-center gap-2'
146
- />
147
- ))}
148
- {createOptionLabel && createOptionIcon && createSchema && onCreate && (
149
- <CreateItem
150
- createOptionLabel={createOptionLabel}
151
- createOptionIcon={createOptionIcon}
152
- onCreateItemSelect={(query: string) => {
153
- setFormInitialValue(query);
154
- setShowForm(true);
155
- }}
156
- />
157
- )}
158
- </Combobox.List>
159
- </>
160
- )}
112
+ <Combobox.Arrow />
113
+ </Combobox.Content>
114
+ );
115
+ }
116
+
117
+ return (
118
+ <Combobox.Content {...props} onSearch={handleSearch} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
119
+ <Combobox.Input placeholder={t('ref-field-combobox-input.placeholder')} autoFocus />
120
+ <Combobox.List>
121
+ {results.map((option) => (
122
+ <Combobox.Item
123
+ key={option.id}
124
+ value={option.id}
125
+ label={option.label}
126
+ checked={selectedIds?.includes(option.id)}
127
+ onSelect={() => onSelect(option.id)}
128
+ classNames='flex items-center gap-2'
129
+ />
130
+ ))}
131
+ {createSchema && onCreate && (
132
+ <CreateItem
133
+ createOptionLabel={createOptionLabel}
134
+ createOptionIcon={createOptionIcon ?? 'ph--plus--regular'}
135
+ onCreateItemSelect={(query: string) => {
136
+ setFormInitialValue(query);
137
+ setShowForm(true);
138
+ }}
139
+ />
140
+ )}
141
+ </Combobox.List>
161
142
  <Combobox.Arrow />
162
143
  </Combobox.Content>
163
144
  );
164
145
  },
165
146
  );
166
147
 
167
- ObjectPickerContent.displayName = 'ObjectPickerContent';
148
+ const CreateItem = ({
149
+ createOptionLabel,
150
+ createOptionIcon,
151
+ onCreateItemSelect,
152
+ }: {
153
+ createOptionLabel?: [string, { ns: string }];
154
+ createOptionIcon: string;
155
+ onCreateItemSelect: (query: string) => void;
156
+ }) => {
157
+ const { t } = useTranslation(translationKey);
158
+ const { query } = useSearchListInput();
159
+
160
+ const label = createOptionLabel
161
+ ? t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })
162
+ : t('create-option.label');
163
+
164
+ return (
165
+ <Combobox.Item
166
+ value='__create__'
167
+ label={label}
168
+ icon={createOptionIcon}
169
+ classNames='flex items-center gap-2'
170
+ closeOnSelect={false}
171
+ onSelect={() => {
172
+ onCreateItemSelect(query);
173
+ }}
174
+ />
175
+ );
176
+ };
177
+
178
+ ObjectPickerContent.displayName = 'ObjectPicker.Content';
168
179
 
169
180
  export const ObjectPicker = {
170
181
  Root: Combobox.Root,
@@ -0,0 +1,63 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { Filter, Tag } from '@dxos/echo';
9
+ import { useQuery } from '@dxos/react-client/echo';
10
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
11
+ import { Panel } from '@dxos/react-ui';
12
+ import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
13
+ import { Pipeline } from '@dxos/types';
14
+
15
+ import { translations } from '../../translations';
16
+ import { ObjectProperties } from './ObjectProperties';
17
+
18
+ const DefaultStory = () => {
19
+ const { space } = useClientStory();
20
+ const [object] = useQuery(space?.db, Filter.type(Pipeline.Pipeline));
21
+ if (!object) {
22
+ return <Loading />;
23
+ }
24
+
25
+ return (
26
+ <Panel.Root>
27
+ <Panel.Content asChild>
28
+ <ObjectProperties object={object} />
29
+ </Panel.Content>
30
+ </Panel.Root>
31
+ );
32
+ };
33
+
34
+ const meta = {
35
+ title: 'ui/react-ui-form/ObjectProperties',
36
+ component: ObjectProperties as any,
37
+ render: DefaultStory,
38
+ decorators: [
39
+ withTheme(),
40
+ withLayout({ layout: 'column' }),
41
+ withClientProvider({
42
+ createIdentity: true,
43
+ createSpace: true,
44
+ types: [Pipeline.Pipeline, Tag.Tag],
45
+ onCreateSpace: async ({ space }) => {
46
+ space.db.add(Pipeline.make());
47
+ space.db.add(Tag.make({ label: 'Tag 1' }));
48
+ space.db.add(Tag.make({ label: 'Tag 2' }));
49
+ space.db.add(Tag.make({ label: 'Tag 3' }));
50
+ },
51
+ }),
52
+ ],
53
+ parameters: {
54
+ layout: 'fullscreen',
55
+ translations,
56
+ },
57
+ } satisfies Meta<typeof DefaultStory>;
58
+
59
+ export default meta;
60
+
61
+ type Story = StoryObj<typeof meta>;
62
+
63
+ export const Default: Story = {};
@@ -0,0 +1,129 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import * as Function from 'effect/Function';
6
+ import * as Option from 'effect/Option';
7
+ import * as Schema from 'effect/Schema';
8
+ import React, { type PropsWithChildren, useCallback, useMemo } from 'react';
9
+
10
+ import { DXN, Obj, Ref, Tag, Type } from '@dxos/echo';
11
+ import { type JsonPath, splitJsonPath } from '@dxos/echo/internal';
12
+ import { invariant } from '@dxos/invariant';
13
+ import { HuePicker } from '@dxos/react-ui-pickers';
14
+ import { composable, composableProps } from '@dxos/ui-theme';
15
+ import { isNonNullable } from '@dxos/util';
16
+
17
+ import { translationKey } from '../../translations';
18
+ import { Form, type FormFieldMap, omitId } from '../Form';
19
+
20
+ export type ObjectPropertiesProps = PropsWithChildren<{ object: Obj.Unknown }>;
21
+
22
+ // TODO(wittjosiah): Reconcile w/ ObjectForm.
23
+ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps>(
24
+ ({ children, object, ...props }, forwardedRef) => {
25
+ const db = Obj.getDatabase(object);
26
+ const meta = Obj.getMeta(object);
27
+ const tags = (meta.tags ?? []).map((tag) => db?.makeRef(DXN.parse(tag))).filter(isNonNullable);
28
+ const values = useMemo(() => ({ tags, ...object }), [object, tags]);
29
+
30
+ const formSchema = useMemo(() => {
31
+ return Function.pipe(
32
+ Obj.getSchema(object),
33
+ Option.fromNullable,
34
+ Option.map((schema) => omitId(BaseSchema.pipe(Schema.extend(schema)))),
35
+ Option.getOrUndefined,
36
+ );
37
+ }, [object]);
38
+
39
+ const handleCreate = useCallback((schema: Type.AnyEntity, values: any) => {
40
+ invariant(db);
41
+ invariant(Type.isObjectSchema(schema));
42
+ const newObject = db.add(Obj.make(schema, values));
43
+ if (Obj.instanceOf(Tag.Tag, newObject)) {
44
+ Obj.change(object, (object) => {
45
+ Obj.getMeta(object).tags = [...(Obj.getMeta(object).tags ?? []), Obj.getDXN(newObject).toString()];
46
+ });
47
+ }
48
+ }, []);
49
+
50
+ // TODO(wittjosiah): Use FormRootProps type.
51
+ const handleChange = useCallback(
52
+ (
53
+ { tags, ...values }: Schema.Schema.Type<typeof formSchema>,
54
+ { isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> },
55
+ ) => {
56
+ if (!isValid) {
57
+ return;
58
+ }
59
+
60
+ const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
61
+
62
+ // Handle tags separately using Obj.change.
63
+ const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
64
+ if (hasTagsChange) {
65
+ Obj.change(object, (object) => {
66
+ Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.dxn.toString()) ?? [];
67
+ });
68
+ }
69
+
70
+ // Handle other property changes.
71
+ const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
72
+ if (nonTagPaths.length > 0) {
73
+ Obj.change(object, () => {
74
+ for (const path of nonTagPaths) {
75
+ const parts = splitJsonPath(path);
76
+ const value = Obj.getValue(values as any, parts);
77
+ Obj.setValue(object, parts, value);
78
+ }
79
+ });
80
+ }
81
+ },
82
+ [object],
83
+ );
84
+
85
+ if (!formSchema) {
86
+ return null;
87
+ }
88
+
89
+ return (
90
+ <Form.Root
91
+ schema={formSchema}
92
+ defaultValues={values as any}
93
+ createTypename={Type.getTypename(Tag.Tag)}
94
+ createOptionIcon='ph--plus--regular'
95
+ createOptionLabel={['add-tag.label', { ns: translationKey }]}
96
+ createInitialValuePath='label'
97
+ createFieldMap={createFieldMap}
98
+ db={db}
99
+ onValuesChanged={handleChange}
100
+ onCreate={handleCreate}
101
+ >
102
+ <Form.Viewport {...composableProps(props)} ref={forwardedRef}>
103
+ <Form.Content>
104
+ <Form.FieldSet />
105
+ {children}
106
+ </Form.Content>
107
+ </Form.Viewport>
108
+ </Form.Root>
109
+ );
110
+ },
111
+ );
112
+
113
+ const createFieldMap: FormFieldMap = {
114
+ hue: ({ type, label, layout, getValue, onValueChange }) => {
115
+ const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
116
+ const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
117
+ return (
118
+ <>
119
+ {layout !== 'inline' && <Form.Label label={label} />}
120
+ <HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
121
+ </>
122
+ );
123
+ },
124
+ };
125
+
126
+ // TODO(wittjosiah): Would be nice to control order when extending so this isn't always first/last.
127
+ const BaseSchema = Schema.Struct({
128
+ tags: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.optional),
129
+ });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './ObjectProperties';
@@ -0,0 +1,55 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { random } from '@dxos/random';
9
+ import { Button, Input } from '@dxos/react-ui';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+
12
+ import { translations } from '../../translations';
13
+ import { Settings } from './Settings';
14
+
15
+ random.seed(132);
16
+
17
+ const DefaultStory = () => {
18
+ return (
19
+ <Settings.Viewport>
20
+ <Settings.Section title='Settings' description={random.lorem.paragraphs(1)}>
21
+ <Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(1)}>
22
+ <Input.TextInput placeholder='Input' />
23
+ </Settings.Item>
24
+ <Settings.Item title={random.lorem.sentence(2)} description={random.lorem.paragraphs(2)}>
25
+ <Input.Switch />
26
+ </Settings.Item>
27
+ <Settings.Item title={random.lorem.sentence(3)} description={random.lorem.paragraphs(2)}>
28
+ <Button>Test</Button>
29
+ </Settings.Item>
30
+ </Settings.Section>
31
+ <Settings.Section title='Panel Example'>
32
+ <Settings.Panel>
33
+ <h3 className='text-lg mb-2'>Members</h3>
34
+ <p className='text-description'>Content inside a panel.</p>
35
+ </Settings.Panel>
36
+ </Settings.Section>
37
+ </Settings.Viewport>
38
+ );
39
+ };
40
+
41
+ const meta = {
42
+ title: 'ui/react-ui-form/Settings',
43
+ render: DefaultStory,
44
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
45
+ parameters: {
46
+ layout: 'fullscreen',
47
+ translations,
48
+ },
49
+ } satisfies Meta;
50
+
51
+ export default meta;
52
+
53
+ type Story = StoryObj<typeof meta>;
54
+
55
+ export const Default: Story = {};