@dxos/react-ui-form 0.8.4-main.d05673bc65 → 0.8.4-main.d9fc60f731
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.
- package/LICENSE +102 -5
- package/dist/lib/browser/index.mjs +2008 -840
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +83 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +2008 -840
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +85 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +64 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +71 -17
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +148 -86
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +24 -8
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +26 -13
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +11 -9
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
- package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.d.ts +50 -0
- package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +119 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
- package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
- package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
- package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/parser.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/DateField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/DateField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/InlineRefField.d.ts +13 -0
- package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +85 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +12 -6
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts +4 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +64 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/index.d.ts +2 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts +3 -0
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
- package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
- package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
- package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +28 -6
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +64 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +128 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +87 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
- package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +28 -33
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +65 -61
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +9 -11
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +68 -66
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +13 -4
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +64 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field-type.d.ts +19 -0
- package/dist/types/src/util/field-type.d.ts.map +1 -0
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +18 -1
- package/dist/types/src/util/properties.d.ts.map +1 -1
- package/dist/types/src/util/properties.test.d.ts +2 -0
- package/dist/types/src/util/properties.test.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +48 -41
- package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -4
- package/src/components/FieldEditor/FieldEditor.test.tsx +4 -2
- package/src/components/FieldEditor/FieldEditor.tsx +23 -17
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +46 -65
- package/src/components/Form/Form.tsx +176 -60
- package/src/components/Form/FormField.tsx +107 -38
- package/src/components/Form/FormFieldComponent.tsx +77 -20
- package/src/components/Form/FormFieldSet.tsx +128 -78
- package/src/components/Form/FormTooltipsContext.ts +25 -0
- package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
- package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
- package/src/components/Form/Layout/FormLayout.test.ts +59 -0
- package/src/components/Form/Layout/FormLayout.tsx +251 -0
- package/src/components/Form/Layout/annotation.ts +38 -0
- package/src/components/Form/Layout/index.ts +7 -0
- package/src/components/Form/Layout/parser.test.ts +120 -0
- package/src/components/Form/Layout/parser.ts +167 -0
- package/src/components/Form/fields/ArrayField.tsx +64 -35
- package/src/components/Form/fields/DateField.tsx +101 -0
- package/src/components/Form/fields/GeoPointField.tsx +12 -8
- package/src/components/Form/fields/InlineRefField.tsx +146 -0
- package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
- package/src/components/Form/fields/MarkdownField.tsx +135 -45
- package/src/components/Form/fields/RefField.tsx +83 -42
- package/src/components/Form/fields/SelectField.tsx +17 -5
- package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
- package/src/components/Form/fields/SelectOptionField.tsx +14 -13
- package/src/components/Form/fields/TextAreaField.tsx +1 -1
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/Form/fields/TupleField.tsx +3 -2
- package/src/components/Form/fields/index.ts +2 -0
- package/src/components/Form/index.ts +3 -0
- package/src/components/Form/meta-tags.test.ts +47 -0
- package/src/components/Form/meta-tags.ts +33 -0
- package/src/components/ObjectForm/ObjectForm.tsx +109 -0
- package/src/components/ObjectForm/index.ts +5 -0
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -11
- package/src/components/ObjectPicker/ObjectPicker.tsx +93 -66
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +111 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +144 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
- package/src/components/ObjectTree/ObjectTree.tsx +190 -0
- package/src/components/ObjectTree/index.ts +5 -0
- package/src/components/Settings/AUDIT.md +51 -0
- package/src/components/Settings/Settings.stories.tsx +69 -21
- package/src/components/Settings/Settings.tsx +32 -181
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +58 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +2 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +45 -51
- package/src/components/ViewEditor/ViewEditor.test.tsx +11 -9
- package/src/components/ViewEditor/ViewEditor.tsx +45 -55
- package/src/components/index.ts +3 -0
- package/src/components/testing.tsx +19 -11
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/index.ts +0 -4
- package/src/translations.ts +67 -63
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -0
- package/src/util/properties.test.ts +121 -0
- package/src/util/properties.ts +47 -4
- package/src/util/refs.ts +7 -3
- package/src/vite-env.d.ts +10 -0
|
@@ -17,6 +17,7 @@ export const TupleField = ({
|
|
|
17
17
|
type,
|
|
18
18
|
readonly,
|
|
19
19
|
label,
|
|
20
|
+
jsonPath,
|
|
20
21
|
getStatus,
|
|
21
22
|
getValue,
|
|
22
23
|
onValueChange,
|
|
@@ -31,8 +32,8 @@ export const TupleField = ({
|
|
|
31
32
|
|
|
32
33
|
return (
|
|
33
34
|
<Input.Root validationValence={status}>
|
|
34
|
-
<FormFieldLabel error={error} readonly={readonly} label={label} />
|
|
35
|
-
<div className={mx('grid gap-
|
|
35
|
+
<FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />
|
|
36
|
+
<div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
|
|
36
37
|
{binding.map((prop) => (
|
|
37
38
|
<Input.TextInput
|
|
38
39
|
key={prop}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
export * from './ArrayField';
|
|
6
6
|
export * from './BooleanField';
|
|
7
|
+
export * from './DateField';
|
|
7
8
|
export * from './GeoPointField';
|
|
9
|
+
export * from './InlineRefField';
|
|
8
10
|
export * from './MarkdownField';
|
|
9
11
|
export * from './NumberField';
|
|
10
12
|
export * from './RefField';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
+
import { describe, test } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { Obj, Ref, Tag } from '@dxos/echo';
|
|
10
|
+
|
|
11
|
+
import { META_TAGS_KEY, withMetaTags } from './meta-tags';
|
|
12
|
+
|
|
13
|
+
// A feed-host schema whose own `tags` field is a `TagIndex` record (tagId -> objectId[]), not an
|
|
14
|
+
// array — mirrors plugin-commerce `Search`, plugin-inbox `Mailbox`, plugin-feed `Subscription`.
|
|
15
|
+
const TagIndexHost = Schema.Struct({
|
|
16
|
+
name: Schema.String.pipe(Schema.optional),
|
|
17
|
+
tags: Schema.Record({ key: Schema.String, value: Schema.Array(Obj.ID) }).pipe(Schema.optional),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const propertyNames = (schema: Schema.Schema.AnyNoContext): string[] =>
|
|
21
|
+
SchemaAST.getPropertySignatures(schema.ast).map((property) => property.name.toString());
|
|
22
|
+
|
|
23
|
+
describe('withMetaTags', () => {
|
|
24
|
+
test('keying meta-tags as `tags` collides with a schema-declared `tags` record (regression)', ({ expect }) => {
|
|
25
|
+
// The pre-fix shape: splice a `tags: Array<Ref<Tag>>` onto a schema that already declares `tags`.
|
|
26
|
+
// `Schema.extend` cannot merge an array with a record at the same key, hence the companion crash:
|
|
27
|
+
// "overlapping types at path: [\"tags\"]".
|
|
28
|
+
expect(() =>
|
|
29
|
+
Schema.Struct({ tags: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.optional) }).pipe(Schema.extend(TagIndexHost)),
|
|
30
|
+
).toThrow(/tags/i);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('splices meta-tags under `_tags` without colliding with a schema-declared `tags`', ({ expect }) => {
|
|
34
|
+
const schema = withMetaTags(TagIndexHost);
|
|
35
|
+
const names = propertyNames(schema);
|
|
36
|
+
expect(names).toContain(META_TAGS_KEY);
|
|
37
|
+
expect(names).toContain('tags');
|
|
38
|
+
expect(names).not.toContain('id');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('still splices meta-tags onto a schema with no `tags` field', ({ expect }) => {
|
|
42
|
+
const Plain = Schema.Struct({ name: Schema.String });
|
|
43
|
+
const names = propertyNames(withMetaTags(Plain));
|
|
44
|
+
expect(names).toContain(META_TAGS_KEY);
|
|
45
|
+
expect(names).toContain('name');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
|
+
|
|
7
|
+
import { Ref, Tag } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
import { omitId } from './Form';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Form-field key for the synthetic meta-tags array spliced into property/create forms.
|
|
13
|
+
*
|
|
14
|
+
* An object's tags live in `Obj.getMeta(obj).tags` (off-schema), so the form surfaces them as an
|
|
15
|
+
* extra field rather than reading the schema. The key is underscore-prefixed so it never collides
|
|
16
|
+
* with a `tags` field an object schema declares itself — e.g. a `TagIndex` record on a feed host,
|
|
17
|
+
* whose `tags` is `Record<tagId, objectId[]>`, not an array. Extending such a schema with a `tags`
|
|
18
|
+
* array threw "overlapping types at path: [\"tags\"]"; the disjoint `_tags` key avoids it. The
|
|
19
|
+
* human-facing label is restored via the `title` annotation on {@link withMetaTags}.
|
|
20
|
+
*/
|
|
21
|
+
export const META_TAGS_KEY = '_tags';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Splices the synthetic meta-tags array (an object's `meta.tags`, surfaced as {@link Tag} refs) onto
|
|
25
|
+
* a schema for editing and drops `id`. Keyed by {@link META_TAGS_KEY} so it never collides with a
|
|
26
|
+
* `tags` field the schema declares itself.
|
|
27
|
+
*/
|
|
28
|
+
export const withMetaTags = (schema: Schema.Schema.AnyNoContext) =>
|
|
29
|
+
omitId(
|
|
30
|
+
Schema.Struct({
|
|
31
|
+
[META_TAGS_KEY]: Schema.Array(Ref.Ref(Tag.Tag)).pipe(Schema.annotations({ title: 'Tags' }), Schema.optional),
|
|
32
|
+
}).pipe(Schema.extend(schema)),
|
|
33
|
+
);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
8
|
+
import { type JsonPath, splitJsonPath } from '@dxos/echo/internal';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
11
|
+
|
|
12
|
+
import { translationKey } from '#translations';
|
|
13
|
+
|
|
14
|
+
import { Form, type FormFieldMap, META_TAGS_KEY, withMetaTags } from '../Form';
|
|
15
|
+
|
|
16
|
+
export type ObjectFormProps = {
|
|
17
|
+
type: Type.AnyEntity;
|
|
18
|
+
object: Obj.Unknown;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const createFieldMap: FormFieldMap = {
|
|
22
|
+
hue: ({ type, label, layout, getValue, onValueChange }) => {
|
|
23
|
+
const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
|
|
24
|
+
const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{layout !== 'inline' && <Form.Label label={label} />}
|
|
28
|
+
<HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const ObjectForm = ({ object, type }: ObjectFormProps) => {
|
|
35
|
+
const db = Obj.getDatabase(object);
|
|
36
|
+
const meta = Obj.getMeta(object);
|
|
37
|
+
// `meta.tags` already holds `Ref<Tag>`s (materialized by the database handler).
|
|
38
|
+
const tags = [...meta.tags];
|
|
39
|
+
const values = useMemo(() => ({ [META_TAGS_KEY]: tags, ...object }), [object, tags]);
|
|
40
|
+
const formSchema = useMemo(() => withMetaTags(Type.getSchema(type)), [type]);
|
|
41
|
+
|
|
42
|
+
const handleCreate = useCallback((type: Type.AnyEntity, values: any) => {
|
|
43
|
+
invariant(db);
|
|
44
|
+
invariant(Type.isObject(type));
|
|
45
|
+
const newObject = db.add(Obj.make(type, values));
|
|
46
|
+
if (Obj.instanceOf(Tag.Tag, newObject)) {
|
|
47
|
+
Obj.update(object, (object) => {
|
|
48
|
+
Obj.getMeta(object).tags = [...Obj.getMeta(object).tags, Ref.make(newObject)];
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// TODO(wittjosiah): Use FormRootProps type.
|
|
54
|
+
const handleChange = useCallback(
|
|
55
|
+
(
|
|
56
|
+
{ [META_TAGS_KEY]: metaTags, ...values }: any,
|
|
57
|
+
{ isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> },
|
|
58
|
+
) => {
|
|
59
|
+
if (!isValid) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
|
|
64
|
+
|
|
65
|
+
// Handle meta-tags separately using Obj.update.
|
|
66
|
+
const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === META_TAGS_KEY);
|
|
67
|
+
if (hasTagsChange) {
|
|
68
|
+
Obj.update(object, (object) => {
|
|
69
|
+
// Copy so later in-place form mutations don't bypass the `Obj.update` boundary.
|
|
70
|
+
Obj.getMeta(object).tags = Array.isArray(metaTags) ? [...(metaTags as Ref.Ref<Tag.Tag>[])] : [];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle other property changes.
|
|
75
|
+
const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== META_TAGS_KEY);
|
|
76
|
+
if (nonTagPaths.length > 0) {
|
|
77
|
+
Obj.update(object, () => {
|
|
78
|
+
for (const path of nonTagPaths) {
|
|
79
|
+
const parts = splitJsonPath(path);
|
|
80
|
+
const value = Obj.getValue(values, parts);
|
|
81
|
+
Obj.setValue(object, parts, value);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
[object],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Form.Root
|
|
91
|
+
schema={formSchema}
|
|
92
|
+
defaultValues={values}
|
|
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>
|
|
103
|
+
<Form.Content>
|
|
104
|
+
<Form.FieldSet />
|
|
105
|
+
</Form.Content>
|
|
106
|
+
</Form.Viewport>
|
|
107
|
+
</Form.Root>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
@@ -7,28 +7,28 @@ import * as Schema from 'effect/Schema';
|
|
|
7
7
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
8
8
|
import { expect, fn, userEvent, within } from 'storybook/test';
|
|
9
9
|
|
|
10
|
-
import { Obj } from '@dxos/echo';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { Filter, Obj, Type } from '@dxos/echo';
|
|
11
|
+
import { random } from '@dxos/random';
|
|
12
|
+
import { useQuery } from '@dxos/react-client/echo';
|
|
13
13
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
14
14
|
import { Button } from '@dxos/react-ui';
|
|
15
15
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
16
16
|
import { Person } from '@dxos/types';
|
|
17
17
|
import { osTranslations } from '@dxos/ui-theme';
|
|
18
18
|
|
|
19
|
-
import { translations } from '
|
|
19
|
+
import { translations } from '#translations';
|
|
20
20
|
|
|
21
21
|
import { ObjectPicker } from './ObjectPicker';
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
random.seed(1);
|
|
24
24
|
|
|
25
25
|
const createPerson = () =>
|
|
26
26
|
Obj.make(Person.Person, {
|
|
27
|
-
fullName:
|
|
27
|
+
fullName: random.person.fullName(),
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
const omitId = Schema.omit<any, any, ['id']>('id');
|
|
31
|
-
const personSchema = omitId(Person.Person);
|
|
31
|
+
const personSchema = omitId(Type.getSchema(Person.Person));
|
|
32
32
|
|
|
33
33
|
// Mock functions for testing
|
|
34
34
|
const mockHandleSelect = fn();
|
|
@@ -48,7 +48,7 @@ const DefaultStory = () => {
|
|
|
48
48
|
}
|
|
49
49
|
}, [space]);
|
|
50
50
|
|
|
51
|
-
// Get all objects in the space (similar to
|
|
51
|
+
// Get all objects in the space (similar to BoardArticle)
|
|
52
52
|
const allObjects = useQuery(space?.db, Filter.everything());
|
|
53
53
|
|
|
54
54
|
// Map objects to options format expected by ObjectPicker
|
|
@@ -65,7 +65,9 @@ const DefaultStory = () => {
|
|
|
65
65
|
const handleCreateCallback = useCallback(
|
|
66
66
|
(values: any) => {
|
|
67
67
|
console.log('[on create]', values);
|
|
68
|
-
if (!space)
|
|
68
|
+
if (!space) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
69
71
|
const newPerson = space.db.add(Obj.make(Person.Person, values));
|
|
70
72
|
mockHandleCreate(values);
|
|
71
73
|
return newPerson;
|
|
@@ -74,7 +76,7 @@ const DefaultStory = () => {
|
|
|
74
76
|
);
|
|
75
77
|
|
|
76
78
|
return (
|
|
77
|
-
<div
|
|
79
|
+
<div className='w-96'>
|
|
78
80
|
<ObjectPicker.Root open={isOpen} onOpenChange={setIsOpen}>
|
|
79
81
|
<ObjectPicker.Trigger asChild>
|
|
80
82
|
<Button variant='primary' data-testid='trigger' classNames='w-full'>
|
|
@@ -85,7 +87,7 @@ const DefaultStory = () => {
|
|
|
85
87
|
classNames='dx-card-popover-width'
|
|
86
88
|
options={options}
|
|
87
89
|
createSchema={personSchema}
|
|
88
|
-
createOptionLabel={['create
|
|
90
|
+
createOptionLabel={['create-new-person.label', { ns: osTranslations }]}
|
|
89
91
|
createOptionIcon='ph--user-plus--regular'
|
|
90
92
|
createInitialValuePath='fullName'
|
|
91
93
|
onCreate={handleCreateCallback}
|
|
@@ -6,10 +6,13 @@ 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
|
|
9
|
+
import { Combobox } from '@dxos/react-ui-list';
|
|
10
|
+
import { useSearchListResults } from '@dxos/react-ui-search';
|
|
11
|
+
|
|
12
|
+
import { translationKey } from '#translations';
|
|
10
13
|
|
|
11
|
-
import { translationKey } from '../../translations';
|
|
12
14
|
import { Form } from '../Form';
|
|
15
|
+
import { type FormFieldMap } from '../Form/FormFieldComponent';
|
|
13
16
|
|
|
14
17
|
export type RefOption = {
|
|
15
18
|
id: string;
|
|
@@ -20,40 +23,22 @@ export type RefOption = {
|
|
|
20
23
|
export type ObjectPickerContentProps = ThemedClassName<{
|
|
21
24
|
options: RefOption[];
|
|
22
25
|
selectedIds?: string[];
|
|
23
|
-
createOptionLabel?: [string, { ns: string }];
|
|
26
|
+
createOptionLabel?: [string, { ns: string | readonly string[] }];
|
|
24
27
|
createOptionIcon?: string;
|
|
25
28
|
createSchema?: Schema.Schema.AnyNoContext;
|
|
26
29
|
createInitialValuePath?: string;
|
|
27
|
-
|
|
30
|
+
createFieldMap?: FormFieldMap;
|
|
31
|
+
/**
|
|
32
|
+
* Persist a newly-created object given the form values. May be async (e.g.
|
|
33
|
+
* to write to a database). The Promise is awaited before the inline create
|
|
34
|
+
* form is collapsed back to the picker list, so consumers can complete
|
|
35
|
+
* follow-up work (assign the new ref to a parent slot, close the popover)
|
|
36
|
+
* before the picker UI re-renders.
|
|
37
|
+
*/
|
|
38
|
+
onCreate?: (values: any) => unknown | Promise<unknown>;
|
|
28
39
|
onSelect: (id: string) => void;
|
|
29
40
|
}>;
|
|
30
41
|
|
|
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
42
|
const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
|
|
58
43
|
(
|
|
59
44
|
{
|
|
@@ -63,23 +48,24 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
63
48
|
createOptionLabel,
|
|
64
49
|
createOptionIcon,
|
|
65
50
|
createInitialValuePath,
|
|
51
|
+
createFieldMap,
|
|
66
52
|
onSelect,
|
|
67
53
|
onCreate,
|
|
68
54
|
...props
|
|
69
55
|
},
|
|
70
|
-
|
|
56
|
+
forwardedRef,
|
|
71
57
|
) => {
|
|
72
58
|
const { t } = useTranslation(translationKey);
|
|
73
59
|
const [showForm, setShowForm] = useState(false);
|
|
74
60
|
const [formInitialValue, setFormInitialValue] = useState<string>('');
|
|
75
61
|
|
|
76
|
-
const { results, handleSearch } = useSearchListResults({
|
|
62
|
+
const { results, query, handleSearch } = useSearchListResults({
|
|
77
63
|
items: options,
|
|
78
64
|
});
|
|
79
65
|
|
|
80
66
|
const handleFormSave = useCallback(
|
|
81
|
-
(values: any) => {
|
|
82
|
-
onCreate?.(values);
|
|
67
|
+
async (values: any) => {
|
|
68
|
+
await onCreate?.(values);
|
|
83
69
|
setShowForm(false);
|
|
84
70
|
setFormInitialValue('');
|
|
85
71
|
},
|
|
@@ -112,14 +98,15 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
112
98
|
[createSchema],
|
|
113
99
|
);
|
|
114
100
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{
|
|
101
|
+
if (showForm && createSchema) {
|
|
102
|
+
return (
|
|
103
|
+
<Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
|
|
118
104
|
<Popover.Viewport>
|
|
119
105
|
<Form.Root
|
|
120
106
|
testId='create-referenced-object-form'
|
|
121
107
|
schema={createSchema}
|
|
122
|
-
|
|
108
|
+
defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
|
|
109
|
+
fieldMap={createFieldMap}
|
|
123
110
|
onSave={handleFormSave}
|
|
124
111
|
onCancel={handleFormCancel}
|
|
125
112
|
>
|
|
@@ -131,40 +118,80 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
131
118
|
</Form.Viewport>
|
|
132
119
|
</Form.Root>
|
|
133
120
|
</Popover.Viewport>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
121
|
+
<Combobox.Arrow />
|
|
122
|
+
</Combobox.Content>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
|
|
128
|
+
<Combobox.Input
|
|
129
|
+
placeholder={t('ref-field-combobox-input.placeholder')}
|
|
130
|
+
autoFocus
|
|
131
|
+
value={query}
|
|
132
|
+
onValueChange={handleSearch}
|
|
133
|
+
/>
|
|
134
|
+
<Combobox.List>
|
|
135
|
+
{results.map((option) => (
|
|
136
|
+
<Combobox.Item
|
|
137
|
+
key={option.id}
|
|
138
|
+
value={option.id}
|
|
139
|
+
label={option.label}
|
|
140
|
+
checked={selectedIds?.includes(option.id)}
|
|
141
|
+
onSelect={() => onSelect(option.id)}
|
|
142
|
+
classNames='flex items-center gap-2'
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
{createSchema && onCreate && (
|
|
146
|
+
<CreateItem
|
|
147
|
+
query={query}
|
|
148
|
+
createOptionLabel={createOptionLabel}
|
|
149
|
+
createOptionIcon={createOptionIcon ?? 'ph--plus--regular'}
|
|
150
|
+
onCreateItemSelect={(currentQuery: string) => {
|
|
151
|
+
setFormInitialValue(currentQuery);
|
|
152
|
+
setShowForm(true);
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
</Combobox.List>
|
|
161
157
|
<Combobox.Arrow />
|
|
162
158
|
</Combobox.Content>
|
|
163
159
|
);
|
|
164
160
|
},
|
|
165
161
|
);
|
|
166
162
|
|
|
167
|
-
|
|
163
|
+
const CreateItem = ({
|
|
164
|
+
query,
|
|
165
|
+
createOptionLabel,
|
|
166
|
+
createOptionIcon,
|
|
167
|
+
onCreateItemSelect,
|
|
168
|
+
}: {
|
|
169
|
+
query: string;
|
|
170
|
+
createOptionLabel?: [string, { ns: string | readonly string[] }];
|
|
171
|
+
createOptionIcon: string;
|
|
172
|
+
onCreateItemSelect: (query: string) => void;
|
|
173
|
+
}) => {
|
|
174
|
+
const { t } = useTranslation(translationKey);
|
|
175
|
+
|
|
176
|
+
const label = createOptionLabel
|
|
177
|
+
? t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })
|
|
178
|
+
: t('create-option.label');
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Combobox.Item
|
|
182
|
+
value='__create__'
|
|
183
|
+
label={label}
|
|
184
|
+
icon={createOptionIcon}
|
|
185
|
+
classNames='flex items-center gap-2'
|
|
186
|
+
closeOnSelect={false}
|
|
187
|
+
onSelect={() => {
|
|
188
|
+
onCreateItemSelect(query);
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
ObjectPickerContent.displayName = 'ObjectPicker.Content';
|
|
168
195
|
|
|
169
196
|
export const ObjectPicker = {
|
|
170
197
|
Root: Combobox.Root,
|