@dxos/react-ui-form 0.8.4-main.bc674ce → 0.8.4-main.c351d160a8
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/README.md +0 -1
- package/dist/lib/browser/index.mjs +538 -458
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +538 -458
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +13 -13
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +3 -3
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +6 -4
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.d.ts +56 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/{ControlSection/ControlSection.stories.d.ts → Settings/Settings.stories.d.ts} +2 -8
- package/dist/types/src/components/Settings/Settings.stories.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 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- 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/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -37
- package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -6
- package/src/components/FieldEditor/FieldEditor.tsx +2 -3
- package/src/components/Form/Form.stories.tsx +8 -7
- package/src/components/Form/Form.tsx +82 -55
- package/src/components/Form/FormField.tsx +12 -4
- package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
- package/src/components/Form/FormFieldComponent.tsx +17 -20
- package/src/components/Form/FormFieldSet.tsx +106 -104
- package/src/components/Form/fields/ArrayField.tsx +6 -8
- package/src/components/Form/fields/MarkdownField.tsx +1 -1
- package/src/components/Form/fields/RefField.tsx +28 -15
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -2
- package/src/components/Form/fields/SelectOptionField.tsx +4 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +9 -13
- package/src/components/Settings/Settings.stories.tsx +56 -0
- package/src/components/Settings/Settings.tsx +230 -0
- package/src/components/{ControlSection → Settings}/index.ts +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +14 -12
- package/src/components/ViewEditor/ViewEditor.test.tsx +3 -6
- package/src/components/ViewEditor/ViewEditor.tsx +92 -63
- package/src/components/index.ts +1 -1
- package/src/components/testing.tsx +9 -6
- package/src/hooks/useFormHandler.ts +42 -24
- package/src/util/index.ts +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
- package/dist/types/src/util/echo.d.ts +0 -6
- package/dist/types/src/util/echo.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/components/Form/Form-old.stories.tsx +0 -314
- package/src/util/echo.ts +0 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-form",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.c351d160a8",
|
|
4
4
|
"description": "Data components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -30,58 +30,56 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@atlaskit/pragmatic-drag-and-drop": "1.7.7",
|
|
32
32
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
|
|
33
|
-
"@effect-atom/atom-react": "^0.
|
|
33
|
+
"@effect-atom/atom-react": "^0.5.0",
|
|
34
34
|
"@fluentui/react-tabster": "9.26.11",
|
|
35
35
|
"@radix-ui/react-context": "1.1.1",
|
|
36
36
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
37
37
|
"date-fns": "^3.3.1",
|
|
38
|
-
"jsonpath": "^1.1.1",
|
|
39
38
|
"tabster": "^8.5.5",
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/echo": "0.8.4-main.
|
|
43
|
-
"@dxos/echo-
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/invariant": "0.8.4-main.
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/react-
|
|
51
|
-
"@dxos/react-
|
|
52
|
-
"@dxos/react-ui-editor": "0.8.4-main.
|
|
53
|
-
"@dxos/react-ui-mosaic": "0.8.4-main.
|
|
54
|
-
"@dxos/react-ui-
|
|
55
|
-
"@dxos/react-ui-
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/react-ui-
|
|
58
|
-
"@dxos/
|
|
59
|
-
"@dxos/types": "0.8.4-main.
|
|
60
|
-
"@dxos/
|
|
61
|
-
"@dxos/
|
|
39
|
+
"@dxos/debug": "0.8.4-main.c351d160a8",
|
|
40
|
+
"@dxos/async": "0.8.4-main.c351d160a8",
|
|
41
|
+
"@dxos/echo": "0.8.4-main.c351d160a8",
|
|
42
|
+
"@dxos/echo-query": "0.8.4-main.c351d160a8",
|
|
43
|
+
"@dxos/echo-react": "0.8.4-main.c351d160a8",
|
|
44
|
+
"@dxos/invariant": "0.8.4-main.c351d160a8",
|
|
45
|
+
"@dxos/lit-ui": "0.8.4-main.c351d160a8",
|
|
46
|
+
"@dxos/keys": "0.8.4-main.c351d160a8",
|
|
47
|
+
"@dxos/log": "0.8.4-main.c351d160a8",
|
|
48
|
+
"@dxos/effect": "0.8.4-main.c351d160a8",
|
|
49
|
+
"@dxos/react-ui-components": "0.8.4-main.c351d160a8",
|
|
50
|
+
"@dxos/react-client": "0.8.4-main.c351d160a8",
|
|
51
|
+
"@dxos/react-ui-editor": "0.8.4-main.c351d160a8",
|
|
52
|
+
"@dxos/react-ui-mosaic": "0.8.4-main.c351d160a8",
|
|
53
|
+
"@dxos/react-ui-list": "0.8.4-main.c351d160a8",
|
|
54
|
+
"@dxos/react-ui-pickers": "0.8.4-main.c351d160a8",
|
|
55
|
+
"@dxos/react-ui-searchlist": "0.8.4-main.c351d160a8",
|
|
56
|
+
"@dxos/react-ui-stack": "0.8.4-main.c351d160a8",
|
|
57
|
+
"@dxos/schema": "0.8.4-main.c351d160a8",
|
|
58
|
+
"@dxos/ui-types": "0.8.4-main.c351d160a8",
|
|
59
|
+
"@dxos/util": "0.8.4-main.c351d160a8",
|
|
60
|
+
"@dxos/types": "0.8.4-main.c351d160a8"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
|
-
"@types/jsonpath": "^0.2.4",
|
|
65
63
|
"@types/react": "~19.2.7",
|
|
66
64
|
"@types/react-dom": "~19.2.3",
|
|
67
|
-
"effect": "3.19.
|
|
65
|
+
"effect": "3.19.16",
|
|
68
66
|
"fast-check": "^3.19.0",
|
|
69
67
|
"react": "~19.2.3",
|
|
70
68
|
"react-dom": "~19.2.3",
|
|
71
|
-
"vite": "7.1.
|
|
72
|
-
"@dxos/
|
|
73
|
-
"@dxos/react-
|
|
74
|
-
"@dxos/react-ui
|
|
75
|
-
"@dxos/
|
|
76
|
-
"@dxos/ui-
|
|
77
|
-
"@dxos/
|
|
69
|
+
"vite": "^7.1.11",
|
|
70
|
+
"@dxos/random": "0.8.4-main.c351d160a8",
|
|
71
|
+
"@dxos/react-client": "0.8.4-main.c351d160a8",
|
|
72
|
+
"@dxos/react-ui": "0.8.4-main.c351d160a8",
|
|
73
|
+
"@dxos/storybook-utils": "0.8.4-main.c351d160a8",
|
|
74
|
+
"@dxos/react-ui-syntax-highlighter": "0.8.4-main.c351d160a8",
|
|
75
|
+
"@dxos/ui-theme": "0.8.4-main.c351d160a8"
|
|
78
76
|
},
|
|
79
77
|
"peerDependencies": {
|
|
80
|
-
"effect": "3.19.
|
|
78
|
+
"effect": "3.19.16",
|
|
81
79
|
"react": "~19.2.3",
|
|
82
80
|
"react-dom": "~19.2.3",
|
|
83
|
-
"@dxos/ui
|
|
84
|
-
"@dxos/
|
|
81
|
+
"@dxos/react-ui": "0.8.4-main.c351d160a8",
|
|
82
|
+
"@dxos/ui-theme": "0.8.4-main.c351d160a8"
|
|
85
83
|
},
|
|
86
84
|
"publishConfig": {
|
|
87
85
|
"access": "public"
|
|
@@ -8,8 +8,8 @@ import React, { useEffect, useMemo } from 'react';
|
|
|
8
8
|
import { Filter, Query } from '@dxos/echo';
|
|
9
9
|
import { createEchoSchema } from '@dxos/echo/testing';
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
12
|
-
import { ProjectionModel,
|
|
11
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
|
+
import { ProjectionModel, ViewModel, createEchoChangeCallback } from '@dxos/schema';
|
|
13
13
|
import { Example } from '@dxos/schema/testing';
|
|
14
14
|
|
|
15
15
|
import { translations } from '../../translations';
|
|
@@ -17,17 +17,15 @@ import { FIELD_EDITOR_DEBUG_SYMBOL, TestLayout } from '../testing';
|
|
|
17
17
|
|
|
18
18
|
import { FieldEditor, type FieldEditorProps } from './FieldEditor';
|
|
19
19
|
|
|
20
|
-
// Type definition for debug objects exposed to tests.
|
|
21
20
|
export type FieldEditorDebugObjects = {
|
|
22
21
|
props: FieldEditorProps;
|
|
23
22
|
projection: ProjectionModel;
|
|
24
23
|
};
|
|
25
24
|
|
|
26
|
-
// TODO(wittjosiah): ECHO objects don't work when passed via Storybook args.
|
|
27
25
|
const useTestProjection = () => {
|
|
28
26
|
return useMemo(() => {
|
|
29
27
|
const schema = createEchoSchema(Example);
|
|
30
|
-
const view =
|
|
28
|
+
const view = ViewModel.make({
|
|
31
29
|
name: 'Test',
|
|
32
30
|
query: Query.select(Filter.type(Example)),
|
|
33
31
|
jsonSchema: schema.jsonSchema,
|
|
@@ -82,7 +80,7 @@ const meta = {
|
|
|
82
80
|
title: 'ui/react-ui-form/FieldEditor',
|
|
83
81
|
component: FieldEditor as any,
|
|
84
82
|
render: DefaultStory,
|
|
85
|
-
decorators: [withTheme],
|
|
83
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
86
84
|
parameters: {
|
|
87
85
|
layout: 'fullscreen',
|
|
88
86
|
translations,
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { Obj, type SchemaRegistry } from '@dxos/echo';
|
|
7
|
+
import { Obj, type SchemaRegistry, type View } from '@dxos/echo';
|
|
8
8
|
import { type EchoSchema, Format, FormatEnums, formatToType } from '@dxos/echo/internal';
|
|
9
9
|
import { type SchemaProperty } from '@dxos/effect';
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
11
|
import { useAsyncEffect, useTranslation } from '@dxos/react-ui';
|
|
12
12
|
import {
|
|
13
|
-
type FieldType,
|
|
14
13
|
type ProjectionModel,
|
|
15
14
|
type PropertyType,
|
|
16
15
|
formatToAdditionalPropertyAttributes,
|
|
@@ -23,7 +22,7 @@ import { Form, type FormFieldMap, type FormRootProps, SelectField, SelectOptionF
|
|
|
23
22
|
|
|
24
23
|
export type FieldEditorProps = {
|
|
25
24
|
projection: ProjectionModel;
|
|
26
|
-
field: FieldType;
|
|
25
|
+
field: View.FieldType;
|
|
27
26
|
registry?: SchemaRegistry.SchemaRegistry;
|
|
28
27
|
view?: Obj.Unknown;
|
|
29
28
|
onSave: () => void;
|
|
@@ -6,13 +6,13 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
7
|
import React, { useCallback, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { Annotation, Format, Obj, Tag, Type } from '@dxos/echo';
|
|
9
|
+
import { Annotation, Format, Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
10
10
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
import { useClient } from '@dxos/react-client';
|
|
13
13
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
14
14
|
import { Tooltip } from '@dxos/react-ui';
|
|
15
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
15
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
16
16
|
|
|
17
17
|
import { translations } from '../../translations';
|
|
18
18
|
import { TestLayout } from '../testing';
|
|
@@ -23,7 +23,7 @@ const Organization = Schema.Struct({
|
|
|
23
23
|
name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
|
|
24
24
|
}).pipe(
|
|
25
25
|
Type.object({
|
|
26
|
-
typename: 'example.
|
|
26
|
+
typename: 'com.example.type.organization',
|
|
27
27
|
version: '0.1.0',
|
|
28
28
|
}),
|
|
29
29
|
);
|
|
@@ -46,8 +46,8 @@ const Person = Schema.Struct({
|
|
|
46
46
|
zip: Schema.Number,
|
|
47
47
|
}).annotations({ title: 'Address' }),
|
|
48
48
|
),
|
|
49
|
-
employer: Schema.optional(
|
|
50
|
-
tags: Schema.optional(Schema.Array(
|
|
49
|
+
employer: Schema.optional(Ref.Ref(Organization).annotations({ title: 'Employer' })),
|
|
50
|
+
tags: Schema.optional(Schema.Array(Ref.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
|
|
51
51
|
status: Schema.optional(Schema.Literal('active', 'inactive').annotations({ title: 'Status' })),
|
|
52
52
|
notes: Schema.optional(Format.Text.annotations({ title: 'Notes' })),
|
|
53
53
|
location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
|
|
@@ -65,7 +65,7 @@ const Person = Schema.Struct({
|
|
|
65
65
|
),
|
|
66
66
|
}).pipe(
|
|
67
67
|
Type.object({
|
|
68
|
-
typename: 'dxos.
|
|
68
|
+
typename: 'org.dxos.type.person', // TODO(burdon): Change all types to /schema
|
|
69
69
|
version: '0.1.0',
|
|
70
70
|
}),
|
|
71
71
|
);
|
|
@@ -127,7 +127,8 @@ const meta = {
|
|
|
127
127
|
render: DefaultStory,
|
|
128
128
|
|
|
129
129
|
decorators: [
|
|
130
|
-
withTheme,
|
|
130
|
+
withTheme(),
|
|
131
|
+
withLayout({ layout: 'fullscreen' }),
|
|
131
132
|
withClientProvider({
|
|
132
133
|
createIdentity: true,
|
|
133
134
|
createSpace: true,
|
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import { createContext } from '@radix-ui/react-context';
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
|
-
import
|
|
7
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
8
8
|
import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
|
|
9
9
|
|
|
10
10
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
11
|
import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
IconButton,
|
|
14
|
+
type IconButtonProps,
|
|
15
|
+
ScrollArea,
|
|
16
|
+
type ScrollAreaRootProps,
|
|
17
|
+
type ThemedClassName,
|
|
18
|
+
useTranslation,
|
|
19
|
+
} from '@dxos/react-ui';
|
|
13
20
|
import { mx } from '@dxos/ui-theme';
|
|
14
21
|
|
|
15
22
|
import {
|
|
@@ -20,7 +27,6 @@ import {
|
|
|
20
27
|
useKeyHandler,
|
|
21
28
|
} from '../../hooks';
|
|
22
29
|
import { translationKey } from '../../translations';
|
|
23
|
-
import { setValueEchoAware } from '../../util';
|
|
24
30
|
|
|
25
31
|
import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
|
|
26
32
|
import {
|
|
@@ -29,20 +35,19 @@ import {
|
|
|
29
35
|
} from './FormFieldSet';
|
|
30
36
|
|
|
31
37
|
// New features/polish
|
|
32
|
-
// [x] Unify readonly/inline modes
|
|
33
|
-
// [ ] Don't call save/autoSave if value hasn't changed
|
|
34
|
-
// [ ] Fix onCancel (restore values)
|
|
35
|
-
// [ ] Fix useSchema Type.Obj.Any cast
|
|
36
|
-
// [ ] Remove @dxos/echo-db deps
|
|
37
|
-
// [ ] TableCellEditor (handleEnter/ModalController).
|
|
38
|
-
// [ ] Use FormFieldWrapper uniformly
|
|
39
|
-
// [ ] Inline tables for object arrays
|
|
40
|
-
// [ ] Defer query until popover
|
|
41
|
-
// [ ] Omit id from sub properties.
|
|
38
|
+
// [x] Unify readonly/inline modes.
|
|
42
39
|
// [x] Refs
|
|
43
40
|
// [x] Single-select (fix popover)
|
|
44
41
|
// [x] Multi-select (array)
|
|
45
|
-
// [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged)
|
|
42
|
+
// [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged).
|
|
43
|
+
// [ ] Don't call save/autoSave if value hasn't changed.
|
|
44
|
+
// [ ] Fix onCancel (restore values).
|
|
45
|
+
// [ ] Fix useSchema Type.AnyObj cast.
|
|
46
|
+
// [ ] TableCellEditor (handleEnter/ModalController).
|
|
47
|
+
// [ ] Use FormFieldWrapper uniformly
|
|
48
|
+
// [ ] Inline tables.
|
|
49
|
+
// [ ] Defer query until popover.
|
|
50
|
+
// [ ] Omit id from sub properties.
|
|
46
51
|
|
|
47
52
|
// TODO(burdon): Move to @dxos/schema (re-export here).
|
|
48
53
|
export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
|
|
@@ -70,7 +75,7 @@ type FormContextValue<T extends AnyProperties = any> = {
|
|
|
70
75
|
* Testing.
|
|
71
76
|
*/
|
|
72
77
|
testId?: string;
|
|
73
|
-
} & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider'>;
|
|
78
|
+
} & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection'>;
|
|
74
79
|
|
|
75
80
|
const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
|
|
76
81
|
|
|
@@ -84,19 +89,25 @@ const useFormValues: {
|
|
|
84
89
|
path: (string | number)[] | undefined,
|
|
85
90
|
defaultValue: () => T,
|
|
86
91
|
): T | undefined;
|
|
87
|
-
} = (componentName: string, path
|
|
88
|
-
const
|
|
92
|
+
} = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
|
|
93
|
+
const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
|
|
94
|
+
const jsonPath = createJsonPath(stablePath);
|
|
89
95
|
const {
|
|
90
|
-
form: { values },
|
|
96
|
+
form: { values, onValueChange },
|
|
91
97
|
} = useFormContext(componentName);
|
|
92
98
|
|
|
93
99
|
const value = getValue$(values, jsonPath);
|
|
94
100
|
|
|
101
|
+
// Apply default value once when the field has no value. lastAppliedPathRef prevents
|
|
102
|
+
// re-applying on every render (e.g. when defaultValue() returns null) and ensures
|
|
103
|
+
// we apply per path when the hook is used for different fields.
|
|
104
|
+
const lastAppliedPathRef = useRef<string | null>(null);
|
|
95
105
|
useEffect(() => {
|
|
96
|
-
if (
|
|
97
|
-
|
|
106
|
+
if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
|
|
107
|
+
lastAppliedPathRef.current = jsonPath;
|
|
108
|
+
onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
|
|
98
109
|
}
|
|
99
|
-
}, [value, defaultValue]);
|
|
110
|
+
}, [value, defaultValue, onValueChange, stablePath, jsonPath]);
|
|
100
111
|
|
|
101
112
|
return value;
|
|
102
113
|
};
|
|
@@ -164,65 +175,78 @@ FormRoot.displayName = 'Form.Root';
|
|
|
164
175
|
// Viewport
|
|
165
176
|
//
|
|
166
177
|
|
|
167
|
-
|
|
178
|
+
const FORM_VIEWPORT_NAME = 'Form.Viewport';
|
|
168
179
|
|
|
169
|
-
|
|
180
|
+
type FormViewportProps = PropsWithChildren<ScrollAreaRootProps>;
|
|
181
|
+
|
|
182
|
+
const FormViewport = ({
|
|
183
|
+
children,
|
|
184
|
+
classNames,
|
|
185
|
+
margin = true,
|
|
186
|
+
padding = true,
|
|
187
|
+
thin = true,
|
|
188
|
+
...props
|
|
189
|
+
}: FormViewportProps) => {
|
|
170
190
|
return (
|
|
171
|
-
<ScrollArea.Root
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
191
|
+
<ScrollArea.Root
|
|
192
|
+
orientation='vertical'
|
|
193
|
+
classNames={classNames}
|
|
194
|
+
margin={margin}
|
|
195
|
+
padding={padding}
|
|
196
|
+
thin={thin}
|
|
197
|
+
{...props}
|
|
198
|
+
>
|
|
199
|
+
<ScrollArea.Viewport>{children}</ScrollArea.Viewport>
|
|
176
200
|
</ScrollArea.Root>
|
|
177
201
|
);
|
|
178
202
|
};
|
|
179
203
|
|
|
180
|
-
FormViewport.displayName =
|
|
204
|
+
FormViewport.displayName = FORM_VIEWPORT_NAME;
|
|
181
205
|
|
|
182
206
|
//
|
|
183
207
|
// Content
|
|
184
208
|
//
|
|
185
209
|
|
|
210
|
+
const FORM_CONTENT_NAME = 'Form.Content';
|
|
211
|
+
|
|
186
212
|
type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
|
|
187
213
|
|
|
188
|
-
// TOOD(burdon): Figure out nesting (indent and testId).
|
|
189
214
|
const FormContent = ({ classNames, children }: FormContentProps) => {
|
|
190
|
-
const { form, testId } = useFormContext(
|
|
215
|
+
const { form, testId } = useFormContext(FORM_CONTENT_NAME);
|
|
191
216
|
const ref = useRef<HTMLDivElement>(null);
|
|
192
217
|
useKeyHandler(ref.current, form);
|
|
193
218
|
|
|
194
219
|
return (
|
|
195
|
-
<div
|
|
196
|
-
ref={ref}
|
|
197
|
-
role='form'
|
|
198
|
-
className={mx('flex flex-col is-full pli-cardSpacingInline density-fine', classNames)}
|
|
199
|
-
data-testid={testId}
|
|
200
|
-
>
|
|
220
|
+
<div ref={ref} role='form' className={mx('w-full flex flex-col gap-form-padding', classNames)} data-testid={testId}>
|
|
201
221
|
{children}
|
|
202
222
|
</div>
|
|
203
223
|
);
|
|
204
224
|
};
|
|
205
225
|
|
|
206
|
-
FormContent.displayName =
|
|
226
|
+
FormContent.displayName = FORM_CONTENT_NAME;
|
|
207
227
|
|
|
208
228
|
//
|
|
209
229
|
// FieldSet
|
|
210
230
|
//
|
|
211
231
|
|
|
212
|
-
|
|
232
|
+
const FORM_FIELDSET_NAME = 'Form.FieldSet';
|
|
233
|
+
|
|
234
|
+
type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
|
|
213
235
|
|
|
214
|
-
const FormFieldSet = (
|
|
215
|
-
const { form, ...
|
|
236
|
+
const FormFieldSet = (props: FormFieldSetProps) => {
|
|
237
|
+
const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
|
|
216
238
|
|
|
217
|
-
return <NaturalFormFieldSet
|
|
239
|
+
return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
|
|
218
240
|
};
|
|
219
241
|
|
|
220
|
-
FormFieldSet.displayName =
|
|
242
|
+
FormFieldSet.displayName = FORM_FIELDSET_NAME;
|
|
221
243
|
|
|
222
244
|
//
|
|
223
245
|
// Actions
|
|
224
246
|
//
|
|
225
247
|
|
|
248
|
+
const FORM_ACTIONS_NAME = 'Form.Actions';
|
|
249
|
+
|
|
226
250
|
type FormActionsProps = ThemedClassName<{}>;
|
|
227
251
|
|
|
228
252
|
const FormActions = ({ classNames }: FormActionsProps) => {
|
|
@@ -231,7 +255,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
231
255
|
form: { canSave, onSave, onCancel },
|
|
232
256
|
readonly,
|
|
233
257
|
layout,
|
|
234
|
-
} = useFormContext(
|
|
258
|
+
} = useFormContext(FORM_ACTIONS_NAME);
|
|
235
259
|
|
|
236
260
|
if (readonly || layout === 'static') {
|
|
237
261
|
return null;
|
|
@@ -241,7 +265,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
241
265
|
// Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
|
|
242
266
|
|
|
243
267
|
return (
|
|
244
|
-
<div role='none' className={mx('grid grid-flow-col auto-cols-fr
|
|
268
|
+
<div role='none' className={mx('grid grid-flow-col gap-2 auto-cols-fr py-form-padding', classNames)}>
|
|
245
269
|
{onCancel && (
|
|
246
270
|
<IconButton
|
|
247
271
|
icon='ph--x--regular'
|
|
@@ -267,33 +291,35 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
267
291
|
);
|
|
268
292
|
};
|
|
269
293
|
|
|
270
|
-
FormActions.displayName =
|
|
294
|
+
FormActions.displayName = FORM_ACTIONS_NAME;
|
|
271
295
|
|
|
272
296
|
//
|
|
273
297
|
// Submit
|
|
274
298
|
//
|
|
275
299
|
|
|
276
|
-
|
|
300
|
+
const FORM_SUBMIT_NAME = 'Form.Submit';
|
|
301
|
+
|
|
302
|
+
type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
|
|
277
303
|
|
|
278
|
-
const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
|
|
304
|
+
const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
|
|
279
305
|
const { t } = useTranslation(translationKey);
|
|
280
306
|
const {
|
|
281
307
|
form: { canSave, onSave },
|
|
282
308
|
readonly,
|
|
283
309
|
layout,
|
|
284
|
-
} = useFormContext(
|
|
310
|
+
} = useFormContext(FORM_SUBMIT_NAME);
|
|
285
311
|
|
|
286
312
|
if (readonly || layout === 'static') {
|
|
287
313
|
return null;
|
|
288
314
|
}
|
|
289
315
|
|
|
290
316
|
return (
|
|
291
|
-
<div role='none' className={mx('flex
|
|
317
|
+
<div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
|
|
292
318
|
<IconButton
|
|
293
|
-
classNames='
|
|
319
|
+
classNames='w-full'
|
|
294
320
|
type='submit'
|
|
295
321
|
variant='primary'
|
|
296
|
-
disabled={!canSave}
|
|
322
|
+
disabled={disabled ?? !canSave}
|
|
297
323
|
icon={icon ?? 'ph--check--regular'}
|
|
298
324
|
label={label ?? t('save button label')}
|
|
299
325
|
onClick={onSave}
|
|
@@ -303,7 +329,7 @@ const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
|
|
|
303
329
|
);
|
|
304
330
|
};
|
|
305
331
|
|
|
306
|
-
FormSubmit.displayName =
|
|
332
|
+
FormSubmit.displayName = FORM_SUBMIT_NAME;
|
|
307
333
|
|
|
308
334
|
//
|
|
309
335
|
// Form
|
|
@@ -315,9 +341,9 @@ export const Form = {
|
|
|
315
341
|
Viewport: FormViewport,
|
|
316
342
|
Content: FormContent,
|
|
317
343
|
FieldSet: FormFieldSet,
|
|
344
|
+
Label: FormFieldLabel,
|
|
318
345
|
Actions: FormActions,
|
|
319
346
|
Submit: FormSubmit,
|
|
320
|
-
Label: FormFieldLabel,
|
|
321
347
|
};
|
|
322
348
|
|
|
323
349
|
export { useFormContext, useFormValues, useFormFieldState };
|
|
@@ -327,6 +353,7 @@ export type {
|
|
|
327
353
|
FormViewportProps,
|
|
328
354
|
FormContentProps,
|
|
329
355
|
FormFieldSetProps,
|
|
356
|
+
FormFieldLabelProps,
|
|
330
357
|
FormActionsProps,
|
|
331
|
-
|
|
358
|
+
FormSubmitProps,
|
|
332
359
|
};
|
|
@@ -171,13 +171,21 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
171
171
|
|
|
172
172
|
const options = getOptions(type);
|
|
173
173
|
if (options) {
|
|
174
|
+
// Resolve labels from projection metadata when available.
|
|
175
|
+
const fieldProjections = projection?.getFieldProjections();
|
|
176
|
+
const fieldProjection = fieldProjections?.find((fp) => fp.field.path === name);
|
|
177
|
+
const selectOptions = fieldProjection?.props.options;
|
|
178
|
+
|
|
174
179
|
return (
|
|
175
180
|
<SelectField
|
|
176
181
|
{...fieldProps}
|
|
177
|
-
options={options.map((option) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
options={options.map((option) => {
|
|
183
|
+
const selectOption = selectOptions?.find((so) => so.id === globalThis.String(option));
|
|
184
|
+
return {
|
|
185
|
+
value: option,
|
|
186
|
+
label: selectOption?.title ?? option.toString(),
|
|
187
|
+
};
|
|
188
|
+
})}
|
|
181
189
|
/>
|
|
182
190
|
);
|
|
183
191
|
}
|
|
@@ -10,7 +10,7 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
|
10
10
|
|
|
11
11
|
import { type FormFieldComponentProps, FormFieldWrapper } from './FormFieldComponent';
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const DefaultStory = ({
|
|
14
14
|
type,
|
|
15
15
|
placeholder,
|
|
16
16
|
readonly,
|
|
@@ -24,7 +24,7 @@ const Component = ({
|
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<div className='
|
|
27
|
+
<div className='p-form-padding'>
|
|
28
28
|
<FormFieldWrapper<string> {...props}>
|
|
29
29
|
{({ value = '' }) => (
|
|
30
30
|
<Input.TextInput
|
|
@@ -41,10 +41,10 @@ const Component = ({
|
|
|
41
41
|
);
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const meta: Meta<typeof
|
|
44
|
+
const meta: Meta<typeof DefaultStory> = {
|
|
45
45
|
title: 'ui/react-ui-form/FormFieldComponent',
|
|
46
|
-
component:
|
|
47
|
-
decorators: [withTheme, withLayout({ layout: 'column' })],
|
|
46
|
+
component: DefaultStory,
|
|
47
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
48
48
|
parameters: {
|
|
49
49
|
layout: 'fullscreen',
|
|
50
50
|
},
|
|
@@ -15,12 +15,10 @@ import React, {
|
|
|
15
15
|
|
|
16
16
|
import { type Format } from '@dxos/echo/internal';
|
|
17
17
|
import { Icon, Input, Tooltip } from '@dxos/react-ui';
|
|
18
|
-
import {
|
|
18
|
+
import { inputTextLabel } from '@dxos/ui-theme';
|
|
19
19
|
|
|
20
20
|
import { type FormFieldStatus } from '../../hooks';
|
|
21
21
|
|
|
22
|
-
const labelSpacing = 'mbs-inputSpacingBlock mbe-labelSpacingBlock';
|
|
23
|
-
|
|
24
22
|
/**
|
|
25
23
|
* Dynamic props passed to input components.
|
|
26
24
|
*/
|
|
@@ -33,7 +31,6 @@ export type FormFieldStateProps<T = any> = {
|
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
33
|
* Presentation mode.
|
|
36
|
-
*
|
|
37
34
|
* - full: Show label, control, and status.
|
|
38
35
|
* - compact: Show label and control.
|
|
39
36
|
* - inline: Control only.
|
|
@@ -79,14 +76,12 @@ export type FormFieldLabelProps = {
|
|
|
79
76
|
|
|
80
77
|
export const FormFieldLabel = ({ label, error, readonly, asChild }: FormFieldLabelProps) => {
|
|
81
78
|
const Label = readonly || asChild ? 'span' : Input.Label;
|
|
82
|
-
const labelProps = readonly || asChild ? { className: 'text-description text-sm' } : { classNames: '!mlb-0 text-sm' };
|
|
83
|
-
|
|
84
79
|
return (
|
|
85
|
-
<div role='none' className=
|
|
86
|
-
<Label {
|
|
80
|
+
<div role='none' className='flex items-center justify-between'>
|
|
81
|
+
<Label className={inputTextLabel}>{label}</Label>
|
|
87
82
|
{error && (
|
|
88
83
|
<Tooltip.Trigger asChild content={error} side='bottom'>
|
|
89
|
-
<Icon icon='ph--warning--regular' size={4} classNames=
|
|
84
|
+
<Icon icon='ph--warning--regular' size={4} classNames='text-error-text' />
|
|
90
85
|
</Tooltip.Trigger>
|
|
91
86
|
)}
|
|
92
87
|
</div>
|
|
@@ -118,15 +113,17 @@ export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
|
|
|
118
113
|
const str = String(value ?? '');
|
|
119
114
|
|
|
120
115
|
return (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<Input.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
116
|
+
<div role='none' className='contents'>
|
|
117
|
+
<Input.Root validationValence={status}>
|
|
118
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
119
|
+
{layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
|
|
120
|
+
{layout === 'full' && (
|
|
121
|
+
<Input.DescriptionAndValidation>
|
|
122
|
+
<Input.Validation>{error}</Input.Validation>
|
|
123
|
+
</Input.DescriptionAndValidation>
|
|
124
|
+
)}
|
|
125
|
+
</Input.Root>
|
|
126
|
+
</div>
|
|
130
127
|
);
|
|
131
128
|
};
|
|
132
129
|
|
|
@@ -158,8 +155,8 @@ export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProp
|
|
|
158
155
|
override render() {
|
|
159
156
|
if (this.state.error) {
|
|
160
157
|
return (
|
|
161
|
-
<div className='flex gap-2 border border-
|
|
162
|
-
<span className='bg-
|
|
158
|
+
<div className='flex gap-2 border border-rose-fill font-mono text-sm'>
|
|
159
|
+
<span className='bg-rose-fill text-surface-text px-1 font-thin'>ERROR</span>
|
|
163
160
|
{String(this.props.path?.join('.'))}
|
|
164
161
|
</div>
|
|
165
162
|
);
|