@dxos/react-ui-form 0.7.3-staging.cc8dd3e → 0.7.3
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/dist/lib/browser/index.mjs +191 -31
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +191 -30
- package/dist/lib/node/index.cjs.map +3 -3
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +191 -31
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +4 -4
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +8 -0
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +11 -0
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +5 -4
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +7 -6
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/package.json +23 -23
- package/src/components/Form/Defaults.tsx +28 -13
- package/src/components/Form/Form.stories.tsx +42 -10
- package/src/components/Form/Form.tsx +204 -28
- package/src/components/Form/Input.tsx +5 -4
- package/src/components/ViewEditor/ViewEditor.tsx +2 -2
- package/src/hooks/useForm.ts +36 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Form.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/Form.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AAErB,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5D,OAAO,EAAO,KAAK,UAAU,EAAU,CAAC,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAI/C,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"Form.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/Form.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AAErB,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAG5D,OAAO,EAAO,KAAK,UAAU,EAAU,CAAC,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAI/C,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,QAAQ,CAAC;AAY9C,QAAA,MAAM,UAAU;;;;;;;;;;;GAME,CAAC;AAEnB,KAAK,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,UAAU,CAAC,CAAC;AAEjD,KAAK,UAAU,CAAC,CAAC,SAAS,UAAU,IAAI;IAAE,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;CAAE,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;AAiB/E,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAQ/B,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,CAAC,CAAC,SAAS,UAAU,IAAI,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAE3D,eAAO,MAAM,OAAO,EAAE,KAAK,CAAC,QAAQ,CAWnC,CAAC;AAGF,eAAO,MAAM,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,aAAa,CAQ5C,CAAC;AAkBF,QAAA,MAAM,WAAW;;;;;;;;GAaC,CAAC;AAEnB,KAAK,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,WAAW,CAAC,CAAC;AAEnD,KAAK,4BAA4B,GAAG,SAAS,CAAC,SAAS,CAAC,CAAC;AAgCzD,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CAAC,4BAA4B,CAUrE,CAAC;AAEF,QAAA,MAAM,YAAY;;;;;;;;GAGA,CAAC;AAEnB,KAAK,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,YAAY,CAAC,CAAC;AAiBrD,eAAO,MAAM,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,UAAU,CAAC,CAQlD,CAAC"}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import React, { type FC, type PropsWithChildren } from 'react';
|
|
2
|
-
import { type BaseObject, type FormatEnum
|
|
2
|
+
import { type BaseObject, type FormatEnum } from '@dxos/echo-schema';
|
|
3
3
|
import { type SimpleType } from '@dxos/effect';
|
|
4
|
-
import { type FormHandler } from '../../hooks';
|
|
4
|
+
import { type FormPath, type FormHandler } from '../../hooks';
|
|
5
5
|
/**
|
|
6
6
|
* Props passed to input components.
|
|
7
7
|
*/
|
|
8
8
|
export type InputProps<T extends BaseObject> = {
|
|
9
|
-
property:
|
|
9
|
+
property: FormPath<T>;
|
|
10
10
|
type: SimpleType;
|
|
11
11
|
format?: FormatEnum;
|
|
12
|
-
label
|
|
12
|
+
label?: string;
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
placeholder?: string;
|
|
15
|
+
inputOnly?: boolean;
|
|
15
16
|
} & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
|
|
16
17
|
/**
|
|
17
18
|
* Form input component.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/Input.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE/D,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/Input.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAE/D,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAI/C,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAE9D;;GAEG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,UAAU,IAAI;IAC7C,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,eAAe,GAAG,QAAQ,CAAC,CAAC;AAEhF;;GAEG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,UAAU,IAAI,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAErE,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAEH,eAAO,MAAM,WAAW,wBAAyB,gBAAgB,sBAiBhE,CAAC"}
|
|
@@ -3,22 +3,23 @@ import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
|
|
|
3
3
|
import { type SimpleType, type S } from '@dxos/effect';
|
|
4
4
|
import { type ValidationError } from '@dxos/schema';
|
|
5
5
|
import { type MaybePromise } from '@dxos/util';
|
|
6
|
+
export type FormPath<T extends object = {}> = PropertyKey<T> | `${PropertyKey<T>}.${number}`;
|
|
6
7
|
/**
|
|
7
8
|
* Return type from `useForm` hook.
|
|
8
9
|
*/
|
|
9
10
|
export type FormHandler<T extends BaseObject> = {
|
|
10
11
|
values: T;
|
|
11
|
-
errors: Record<
|
|
12
|
-
touched: Record<
|
|
13
|
-
changed: Record<
|
|
12
|
+
errors: Record<FormPath<T>, string>;
|
|
13
|
+
touched: Record<FormPath<T>, boolean>;
|
|
14
|
+
changed: Record<FormPath<T>, boolean>;
|
|
14
15
|
canSave: boolean;
|
|
15
16
|
handleSave: () => void;
|
|
16
|
-
getStatus: (property:
|
|
17
|
+
getStatus: (property: FormPath<T>) => {
|
|
17
18
|
status?: 'error';
|
|
18
19
|
error?: string;
|
|
19
20
|
};
|
|
20
|
-
getValue: <V>(property:
|
|
21
|
-
onValueChange: <V>(property:
|
|
21
|
+
getValue: <V>(property: FormPath<T>) => V;
|
|
22
|
+
onValueChange: <V>(property: FormPath<T>, type: SimpleType, value: V) => void;
|
|
22
23
|
onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
|
23
24
|
};
|
|
24
25
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useForm.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAA6C,MAAM,OAAO,CAAC;AAEnF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,cAAc,CAAC;AAGvD,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../../../src/hooks/useForm.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAA6C,MAAM,OAAO,CAAC;AAEnF,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,cAAc,CAAC;AAGvD,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,MAAM,GAAG,EAAE,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC;AAE7F;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,UAAU,IAAI;IAK9C,MAAM,EAAE,CAAC,CAAC;IACV,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACpC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,IAAI,CAAC;IAMvB,SAAS,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3E,QAAQ,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC1C,aAAa,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9E,MAAM,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,gBAAgB,GAAG,mBAAmB,CAAC,KAAK,IAAI,CAAC;CAC7E,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,UAAU;IAC/C;;OAEG;IAEH,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAEpB;;OAEG;IAEH,aAAa,EAAE,CAAC,CAAC;IAEjB;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;IAEtC;;;;OAIG;IAEH,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,eAAe,EAAE,GAAG,SAAS,CAAC;IAE1D;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;KAAE,KAAK,IAAI,CAAC;IAE5E;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE;QAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;KAAE,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;CAC1F;AAED;;;GAGG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,UAAU,4EAOzC,WAAW,CAAC,CAAC,CAAC,KAAG,WAAW,CAAC,CAAC,CAwIhC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-form",
|
|
3
|
-
"version": "0.7.3
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Data components.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -29,20 +29,20 @@
|
|
|
29
29
|
"date-fns": "^3.3.1",
|
|
30
30
|
"effect": "^3.9.2",
|
|
31
31
|
"jsonpath": "^1.1.1",
|
|
32
|
-
"@dxos/async": "0.7.3
|
|
33
|
-
"@dxos/debug": "0.7.3
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/echo-
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/keys": "0.7.3
|
|
39
|
-
"@dxos/live-object": "0.7.3
|
|
40
|
-
"@dxos/react-client": "0.7.3
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/react-ui-types": "0.7.3
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
32
|
+
"@dxos/async": "0.7.3",
|
|
33
|
+
"@dxos/debug": "0.7.3",
|
|
34
|
+
"@dxos/echo-db": "0.7.3",
|
|
35
|
+
"@dxos/echo-schema": "0.7.3",
|
|
36
|
+
"@dxos/invariant": "0.7.3",
|
|
37
|
+
"@dxos/effect": "0.7.3",
|
|
38
|
+
"@dxos/keys": "0.7.3",
|
|
39
|
+
"@dxos/live-object": "0.7.3",
|
|
40
|
+
"@dxos/react-client": "0.7.3",
|
|
41
|
+
"@dxos/react-ui-list": "0.7.3",
|
|
42
|
+
"@dxos/log": "0.7.3",
|
|
43
|
+
"@dxos/react-ui-types": "0.7.3",
|
|
44
|
+
"@dxos/schema": "0.7.3",
|
|
45
|
+
"@dxos/util": "0.7.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@phosphor-icons/react": "^2.1.5",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"react": "~18.2.0",
|
|
54
54
|
"react-dom": "~18.2.0",
|
|
55
55
|
"vite": "5.4.7",
|
|
56
|
-
"@dxos/
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/react-ui": "0.7.3
|
|
59
|
-
"@dxos/react-ui-syntax-highlighter": "0.7.3
|
|
60
|
-
"@dxos/react-ui-theme": "0.7.3
|
|
61
|
-
"@dxos/storybook-utils": "0.7.3
|
|
56
|
+
"@dxos/random": "0.7.3",
|
|
57
|
+
"@dxos/echo-signals": "0.7.3",
|
|
58
|
+
"@dxos/react-ui": "0.7.3",
|
|
59
|
+
"@dxos/react-ui-syntax-highlighter": "0.7.3",
|
|
60
|
+
"@dxos/react-ui-theme": "0.7.3",
|
|
61
|
+
"@dxos/storybook-utils": "0.7.3"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"@effect/schema": "^0.75.5",
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
"effect": "^3.9.2",
|
|
67
67
|
"react": "~18.2.0",
|
|
68
68
|
"react-dom": "~18.2.0",
|
|
69
|
-
"@dxos/react-ui": "0.7.3
|
|
70
|
-
"@dxos/react-ui
|
|
69
|
+
"@dxos/react-ui-theme": "0.7.3",
|
|
70
|
+
"@dxos/react-ui": "0.7.3"
|
|
71
71
|
},
|
|
72
72
|
"publishConfig": {
|
|
73
73
|
"access": "public"
|
|
@@ -13,6 +13,7 @@ export const TextInput = <T extends BaseObject>({
|
|
|
13
13
|
property,
|
|
14
14
|
type,
|
|
15
15
|
label,
|
|
16
|
+
inputOnly,
|
|
16
17
|
disabled,
|
|
17
18
|
placeholder,
|
|
18
19
|
getStatus,
|
|
@@ -24,9 +25,11 @@ export const TextInput = <T extends BaseObject>({
|
|
|
24
25
|
|
|
25
26
|
return (
|
|
26
27
|
<Input.Root validationValence={status}>
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
28
|
+
{!inputOnly && (
|
|
29
|
+
<InputHeader error={error}>
|
|
30
|
+
<Input.Label>{label}</Input.Label>
|
|
31
|
+
</InputHeader>
|
|
32
|
+
)}
|
|
30
33
|
<Input.TextInput
|
|
31
34
|
disabled={disabled}
|
|
32
35
|
placeholder={placeholder}
|
|
@@ -34,6 +37,7 @@ export const TextInput = <T extends BaseObject>({
|
|
|
34
37
|
onChange={(event) => onValueChange(property, type, event.target.value)}
|
|
35
38
|
onBlur={onBlur}
|
|
36
39
|
/>
|
|
40
|
+
{inputOnly && <Input.Validation>{error}</Input.Validation>}
|
|
37
41
|
</Input.Root>
|
|
38
42
|
);
|
|
39
43
|
};
|
|
@@ -42,6 +46,7 @@ export const NumberInput = <T extends BaseObject>({
|
|
|
42
46
|
property,
|
|
43
47
|
type,
|
|
44
48
|
label,
|
|
49
|
+
inputOnly,
|
|
45
50
|
disabled,
|
|
46
51
|
placeholder,
|
|
47
52
|
getStatus,
|
|
@@ -51,12 +56,13 @@ export const NumberInput = <T extends BaseObject>({
|
|
|
51
56
|
}: InputProps<T>) => {
|
|
52
57
|
const { status, error } = getStatus?.(property);
|
|
53
58
|
|
|
54
|
-
// TODO(burdon): Only show stepper if bounded integer.
|
|
55
59
|
return (
|
|
56
60
|
<Input.Root validationValence={status}>
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
61
|
+
{!inputOnly && (
|
|
62
|
+
<InputHeader error={error}>
|
|
63
|
+
<Input.Label>{label}</Input.Label>
|
|
64
|
+
</InputHeader>
|
|
65
|
+
)}
|
|
60
66
|
<Input.TextInput
|
|
61
67
|
type='number'
|
|
62
68
|
disabled={disabled}
|
|
@@ -65,6 +71,7 @@ export const NumberInput = <T extends BaseObject>({
|
|
|
65
71
|
onChange={(event) => onValueChange(property, type, event.target.value)}
|
|
66
72
|
onBlur={onBlur}
|
|
67
73
|
/>
|
|
74
|
+
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
68
75
|
</Input.Root>
|
|
69
76
|
);
|
|
70
77
|
};
|
|
@@ -73,6 +80,7 @@ export const BooleanInput = <T extends BaseObject>({
|
|
|
73
80
|
property,
|
|
74
81
|
type,
|
|
75
82
|
label,
|
|
83
|
+
inputOnly,
|
|
76
84
|
getStatus,
|
|
77
85
|
getValue,
|
|
78
86
|
onValueChange,
|
|
@@ -81,13 +89,16 @@ export const BooleanInput = <T extends BaseObject>({
|
|
|
81
89
|
|
|
82
90
|
return (
|
|
83
91
|
<Input.Root validationValence={status}>
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
92
|
+
{!inputOnly && (
|
|
93
|
+
<InputHeader error={error}>
|
|
94
|
+
<Input.Label>{label}</Input.Label>
|
|
95
|
+
</InputHeader>
|
|
96
|
+
)}
|
|
87
97
|
<Input.Switch
|
|
88
98
|
checked={getValue<boolean>(property)}
|
|
89
99
|
onCheckedChange={(value) => onValueChange(property, type, value)}
|
|
90
100
|
/>
|
|
101
|
+
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
91
102
|
</Input.Root>
|
|
92
103
|
);
|
|
93
104
|
};
|
|
@@ -100,6 +111,7 @@ export const SelectInput = <T extends BaseObject>({
|
|
|
100
111
|
property,
|
|
101
112
|
type,
|
|
102
113
|
label,
|
|
114
|
+
inputOnly,
|
|
103
115
|
disabled,
|
|
104
116
|
placeholder,
|
|
105
117
|
options,
|
|
@@ -111,9 +123,11 @@ export const SelectInput = <T extends BaseObject>({
|
|
|
111
123
|
|
|
112
124
|
return (
|
|
113
125
|
<Input.Root validationValence={status}>
|
|
114
|
-
|
|
115
|
-
<
|
|
116
|
-
|
|
126
|
+
{!inputOnly && (
|
|
127
|
+
<InputHeader error={error}>
|
|
128
|
+
<Input.Label>{label}</Input.Label>
|
|
129
|
+
</InputHeader>
|
|
130
|
+
)}
|
|
117
131
|
<Select.Root value={getValue(property)} onValueChange={(value) => onValueChange(property, type, value)}>
|
|
118
132
|
{/* TODO(burdon): Placeholder not working? */}
|
|
119
133
|
<Select.TriggerButton classNames='is-full' disabled={disabled} placeholder={placeholder} />
|
|
@@ -130,6 +144,7 @@ export const SelectInput = <T extends BaseObject>({
|
|
|
130
144
|
</Select.Content>
|
|
131
145
|
</Select.Portal>
|
|
132
146
|
</Select.Root>
|
|
147
|
+
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
133
148
|
</Input.Root>
|
|
134
149
|
);
|
|
135
150
|
};
|
|
@@ -16,20 +16,20 @@ import { Form, type FormProps } from './Form';
|
|
|
16
16
|
import translations from '../../translations';
|
|
17
17
|
import { TestLayout, TestPanel } from '../testing';
|
|
18
18
|
|
|
19
|
+
const AddressSchema = S.Struct({
|
|
20
|
+
street: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'Street' })),
|
|
21
|
+
city: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'City' })),
|
|
22
|
+
zip: S.optional(S.String.pipe(S.pattern(/^\d{5}(-\d{4})?$/)).annotations({ [AST.TitleAnnotationId]: 'ZIP' })),
|
|
23
|
+
location: S.optional(Format.GeoPoint.annotations({ [AST.TitleAnnotationId]: 'Location' })),
|
|
24
|
+
}).annotations({ [AST.TitleAnnotationId]: 'Address' });
|
|
25
|
+
|
|
19
26
|
// TODO(burdon): Translations?
|
|
20
27
|
const TestSchema = S.Struct({
|
|
21
28
|
name: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'Name' })),
|
|
22
29
|
active: S.optional(S.Boolean.annotations({ [AST.TitleAnnotationId]: 'Active' })),
|
|
23
30
|
rank: S.optional(S.Number.annotations({ [AST.TitleAnnotationId]: 'Rank' })),
|
|
24
31
|
website: S.optional(Format.URL.annotations({ [AST.TitleAnnotationId]: 'Website' })),
|
|
25
|
-
address: S.optional(
|
|
26
|
-
S.Struct({
|
|
27
|
-
street: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'Street' })),
|
|
28
|
-
city: S.optional(S.String.annotations({ [AST.TitleAnnotationId]: 'City' })),
|
|
29
|
-
zip: S.optional(S.String.pipe(S.pattern(/^\d{5}(-\d{4})?$/)).annotations({ [AST.TitleAnnotationId]: 'ZIP' })),
|
|
30
|
-
location: S.optional(Format.GeoPoint.annotations({ [AST.TitleAnnotationId]: 'Location' })),
|
|
31
|
-
}).annotations({ [AST.TitleAnnotationId]: 'Address' }),
|
|
32
|
-
),
|
|
32
|
+
address: S.optional(AddressSchema),
|
|
33
33
|
}).pipe(S.mutable);
|
|
34
34
|
|
|
35
35
|
type TestType = S.Schema.Type<typeof TestSchema>;
|
|
@@ -83,8 +83,8 @@ export const Org: Story<Testing.OrgSchemaType> = {
|
|
|
83
83
|
args: {
|
|
84
84
|
schema: Testing.OrgSchema,
|
|
85
85
|
values: {
|
|
86
|
-
name: 'DXOS',
|
|
87
|
-
website: 'https://dxos.org',
|
|
86
|
+
// name: 'DXOS',
|
|
87
|
+
// website: 'https://dxos.org',
|
|
88
88
|
},
|
|
89
89
|
},
|
|
90
90
|
};
|
|
@@ -165,3 +165,35 @@ export const DiscriminatedShape: StoryObj<DiscriminatedUnionStoryProps> = {
|
|
|
165
165
|
},
|
|
166
166
|
},
|
|
167
167
|
};
|
|
168
|
+
|
|
169
|
+
const ArraysSchema = S.Struct({
|
|
170
|
+
names: S.Array(S.String.pipe(S.nonEmptyString())),
|
|
171
|
+
addresses: S.Array(AddressSchema),
|
|
172
|
+
}).pipe(S.mutable);
|
|
173
|
+
|
|
174
|
+
type ArraysType = S.Schema.Type<typeof ArraysSchema>;
|
|
175
|
+
|
|
176
|
+
const ArraysStory = ({ values: initialValues }: FormProps<ArraysType>) => {
|
|
177
|
+
const [values, setValues] = useState(initialValues);
|
|
178
|
+
const handleSave = useCallback<NonNullable<FormProps<ArraysType>['onSave']>>((values) => {
|
|
179
|
+
setValues(values);
|
|
180
|
+
}, []);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<TestLayout json={{ values, schema: ArraysSchema.ast.toJSON() }}>
|
|
184
|
+
<TestPanel>
|
|
185
|
+
<Form<ArraysType> schema={ArraysSchema} values={values} onSave={handleSave} />
|
|
186
|
+
</TestPanel>
|
|
187
|
+
</TestLayout>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const Arrays: StoryObj<FormProps<ArraysType>> = {
|
|
192
|
+
render: ArraysStory,
|
|
193
|
+
args: {
|
|
194
|
+
values: {
|
|
195
|
+
names: ['Alice', 'Bob'],
|
|
196
|
+
addresses: [],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
@@ -6,8 +6,8 @@ import { pipe } from 'effect';
|
|
|
6
6
|
import { capitalize } from 'effect/String';
|
|
7
7
|
import React, { useEffect, useMemo } from 'react';
|
|
8
8
|
|
|
9
|
-
import { AST, type BaseObject, S, type PropertyKey } from '@dxos/echo-schema';
|
|
10
|
-
import { findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
9
|
+
import { AST, type BaseObject, S, type PropertyKey, type FormatEnum } from '@dxos/echo-schema';
|
|
10
|
+
import { findNode, getDiscriminatedType, isDiscriminatedUnion, SimpleType } from '@dxos/effect';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
12
|
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
13
13
|
import { mx } from '@dxos/react-ui-theme';
|
|
@@ -15,9 +15,9 @@ import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
|
|
|
15
15
|
import { isNotFalsy } from '@dxos/util';
|
|
16
16
|
|
|
17
17
|
import { SelectInput } from './Defaults';
|
|
18
|
-
import { type InputComponent } from './Input';
|
|
18
|
+
import { InputHeader, type InputComponent } from './Input';
|
|
19
19
|
import { getInputComponent } from './factory';
|
|
20
|
-
import { type FormOptions, useForm } from '../../hooks';
|
|
20
|
+
import { type FormHandler, type FormOptions, useForm } from '../../hooks';
|
|
21
21
|
import { translationKey } from '../../translations';
|
|
22
22
|
|
|
23
23
|
// TODO(burdon): Rename package react-ui-form; delete react-ui-card.
|
|
@@ -71,7 +71,6 @@ export const Form = <T extends BaseObject>({
|
|
|
71
71
|
onCancel,
|
|
72
72
|
Custom,
|
|
73
73
|
}: FormProps<T>) => {
|
|
74
|
-
const { t } = useTranslation(translationKey);
|
|
75
74
|
const onValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
|
|
76
75
|
const { canSave, values, errors, handleSave, ...inputProps } = useForm<T>({
|
|
77
76
|
schema,
|
|
@@ -106,12 +105,13 @@ export const Form = <T extends BaseObject>({
|
|
|
106
105
|
<div role='form' className={mx('flex flex-col w-full gap-2 py-2', classNames)} data-testid={testId}>
|
|
107
106
|
{properties
|
|
108
107
|
.map((property) => {
|
|
109
|
-
const { ast, name, type, format, title, description, examples, options } = property;
|
|
108
|
+
const { ast, name, type, format, title, description, examples, options, array } = property;
|
|
110
109
|
const key = [...path, name];
|
|
111
110
|
const label = title ?? pipe(name, capitalize);
|
|
112
111
|
const placeholder = examples?.length ? `Example: "${examples[0]}"` : description;
|
|
113
112
|
|
|
114
113
|
// Get generic input.
|
|
114
|
+
// TODO(ZaymonFC): We might need to switch this to using globs since we're now indexing into arrays.
|
|
115
115
|
let InputComponent = Custom?.[key.join('.')];
|
|
116
116
|
if (!InputComponent) {
|
|
117
117
|
// Select.
|
|
@@ -133,6 +133,26 @@ export const Form = <T extends BaseObject>({
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
InputComponent = getInputComponent<T>(type, format);
|
|
136
|
+
|
|
137
|
+
// TODO(ZaymonFC): Array handling is inelegant. We need to push this complexity down into SDK/Schema.
|
|
138
|
+
if (array) {
|
|
139
|
+
return (
|
|
140
|
+
<ArrayField
|
|
141
|
+
key={name}
|
|
142
|
+
name={name}
|
|
143
|
+
type={type}
|
|
144
|
+
array={true}
|
|
145
|
+
label={label}
|
|
146
|
+
ast={ast}
|
|
147
|
+
values={values}
|
|
148
|
+
format={format}
|
|
149
|
+
readonly={readonly}
|
|
150
|
+
placeholder={placeholder}
|
|
151
|
+
inputProps={inputProps}
|
|
152
|
+
Custom={Custom}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
if (!InputComponent) {
|
|
@@ -181,31 +201,187 @@ export const Form = <T extends BaseObject>({
|
|
|
181
201
|
);
|
|
182
202
|
})
|
|
183
203
|
.filter(isNotFalsy)}
|
|
184
|
-
|
|
185
204
|
{(onCancel || onSave) && !autoSave && (
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
<FormActions onCancel={onCancel} onSubmit={handleSave} canSubmit={canSave} readonly={readonly} />
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
type ArrayFieldProps<T extends BaseObject> = {
|
|
212
|
+
name: PropertyKey<T>;
|
|
213
|
+
type: SimpleType;
|
|
214
|
+
array: true;
|
|
215
|
+
label: string;
|
|
216
|
+
ast: AST.AST;
|
|
217
|
+
values: Record<string, any>;
|
|
218
|
+
format?: FormatEnum;
|
|
219
|
+
readonly?: boolean;
|
|
220
|
+
placeholder?: string;
|
|
221
|
+
inputProps: Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
|
|
222
|
+
Custom?: Partial<Record<string, InputComponent<T>>>;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const ArrayField = <T extends BaseObject>({
|
|
226
|
+
name,
|
|
227
|
+
type,
|
|
228
|
+
label,
|
|
229
|
+
ast,
|
|
230
|
+
values,
|
|
231
|
+
format,
|
|
232
|
+
readonly,
|
|
233
|
+
placeholder,
|
|
234
|
+
inputProps,
|
|
235
|
+
Custom,
|
|
236
|
+
}: ArrayFieldProps<T>) => {
|
|
237
|
+
const { t } = useTranslation(translationKey);
|
|
238
|
+
const arrayValues = (values[name] ?? []) as any[];
|
|
239
|
+
const key = [name];
|
|
240
|
+
|
|
241
|
+
// TODO(ZaymonFC): Should this unwrapping happen at a lower level?
|
|
242
|
+
const tupleType = findNode(ast, AST.isTupleType);
|
|
243
|
+
const elementType = (tupleType as AST.TupleType | undefined)?.rest[0]?.type;
|
|
244
|
+
|
|
245
|
+
if (type === 'object' && elementType) {
|
|
246
|
+
const baseNode = findNode(elementType, isDiscriminatedUnion);
|
|
247
|
+
const typeLiteral = baseNode
|
|
248
|
+
? getDiscriminatedType(baseNode, values[name] as any)
|
|
249
|
+
: findNode(elementType, AST.isTypeLiteral);
|
|
250
|
+
|
|
251
|
+
if (typeLiteral) {
|
|
252
|
+
const schema = S.make(typeLiteral);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div key={name} role='none' className={mx(padding)}>
|
|
256
|
+
<InputHeader>{label}</InputHeader>
|
|
257
|
+
<div role='none' className='flex flex-col gap-1'>
|
|
258
|
+
{arrayValues.map((value, index) => {
|
|
259
|
+
return (
|
|
260
|
+
<div key={index} role='none' className='flex items-center gap-1'>
|
|
261
|
+
<div role='none' className='flex-1'>
|
|
262
|
+
<Form<any>
|
|
263
|
+
schema={schema}
|
|
264
|
+
path={[...key, index.toString()]}
|
|
265
|
+
values={value}
|
|
266
|
+
onValuesChanged={(childValues) => {
|
|
267
|
+
const newValues = [...arrayValues];
|
|
268
|
+
newValues[index] = childValues;
|
|
269
|
+
inputProps.onValueChange(name, 'object', newValues);
|
|
270
|
+
}}
|
|
271
|
+
Custom={Custom as any}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
<IconButton
|
|
275
|
+
icon='ph--trash--regular'
|
|
276
|
+
iconOnly
|
|
277
|
+
label={t('button remove')}
|
|
278
|
+
onClick={() => {
|
|
279
|
+
const newValues = arrayValues.filter((_, i) => i !== index);
|
|
280
|
+
inputProps.onValueChange(name, type, newValues);
|
|
281
|
+
}}
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
})}
|
|
286
|
+
</div>
|
|
287
|
+
<div role='none' className='flex justify-between items-center plb-1'>
|
|
288
|
+
<IconButton
|
|
289
|
+
icon='ph--plus--regular'
|
|
290
|
+
iconOnly
|
|
291
|
+
label={t('button add')}
|
|
292
|
+
onClick={() => {
|
|
293
|
+
const newValues = [...arrayValues, {}];
|
|
294
|
+
inputProps.onValueChange(name, type, newValues);
|
|
295
|
+
}}
|
|
296
|
+
/>
|
|
206
297
|
</div>
|
|
207
298
|
</div>
|
|
208
|
-
)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return (
|
|
302
|
+
<div key={name} role='none'>
|
|
303
|
+
Nested form not supported in arrays, yet.
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const InputComponent = getInputComponent<T>(type, format);
|
|
309
|
+
if (!InputComponent) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<div role='none' key='meta-input' className={mx(padding)}>
|
|
315
|
+
<InputHeader>{label}</InputHeader>
|
|
316
|
+
<div role='none' className='flex flex-col gap-1'>
|
|
317
|
+
{arrayValues.map((_, index) => (
|
|
318
|
+
<div key={index} role='none' className='flex items-start gap-1'>
|
|
319
|
+
<div role='none' className='flex-1'>
|
|
320
|
+
<InputComponent
|
|
321
|
+
type={type}
|
|
322
|
+
format={format}
|
|
323
|
+
label={label}
|
|
324
|
+
inputOnly
|
|
325
|
+
property={`${name}.${index}`}
|
|
326
|
+
disabled={readonly}
|
|
327
|
+
placeholder={placeholder}
|
|
328
|
+
{...inputProps}
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
331
|
+
<IconButton
|
|
332
|
+
icon='ph--trash--regular'
|
|
333
|
+
iconOnly
|
|
334
|
+
label={t('button remove')}
|
|
335
|
+
onClick={() => {
|
|
336
|
+
const newValues = arrayValues.filter((_, i) => i !== index);
|
|
337
|
+
inputProps.onValueChange(name, type, newValues);
|
|
338
|
+
}}
|
|
339
|
+
/>
|
|
340
|
+
</div>
|
|
341
|
+
))}
|
|
342
|
+
</div>
|
|
343
|
+
<div role='none' className='flex justify-between items-center plb-1'>
|
|
344
|
+
<IconButton
|
|
345
|
+
icon='ph--plus--regular'
|
|
346
|
+
iconOnly
|
|
347
|
+
label={t('button add')}
|
|
348
|
+
onClick={() => {
|
|
349
|
+
const newValues = [...arrayValues, SimpleType.getDefaultValue(type)];
|
|
350
|
+
inputProps.onValueChange(name, type, newValues);
|
|
351
|
+
}}
|
|
352
|
+
/>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
type FormActionsProps = {
|
|
359
|
+
readonly?: boolean;
|
|
360
|
+
canSubmit: boolean;
|
|
361
|
+
onCancel?: () => void;
|
|
362
|
+
onSubmit: () => void;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const FormActions = ({ onCancel, onSubmit, canSubmit, readonly }: FormActionsProps) => {
|
|
366
|
+
const { t } = useTranslation(translationKey);
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div role='none' className='flex justify-center'>
|
|
370
|
+
<div role='none' className={mx(onCancel && !readonly && 'grid grid-cols-2 gap-2')}>
|
|
371
|
+
{onCancel && !readonly && (
|
|
372
|
+
<IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
|
|
373
|
+
)}
|
|
374
|
+
{onSubmit && (
|
|
375
|
+
<IconButton
|
|
376
|
+
type='submit'
|
|
377
|
+
data-testid='save-button'
|
|
378
|
+
disabled={!canSubmit}
|
|
379
|
+
icon='ph--check--regular'
|
|
380
|
+
label={t('button save')}
|
|
381
|
+
onClick={onSubmit}
|
|
382
|
+
/>
|
|
383
|
+
)}
|
|
384
|
+
</div>
|
|
209
385
|
</div>
|
|
210
386
|
);
|
|
211
387
|
};
|
|
@@ -4,23 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC, type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type BaseObject, type FormatEnum
|
|
7
|
+
import { type BaseObject, type FormatEnum } from '@dxos/echo-schema';
|
|
8
8
|
import { type SimpleType } from '@dxos/effect';
|
|
9
9
|
import { Icon, Tooltip } from '@dxos/react-ui';
|
|
10
10
|
import { errorText } from '@dxos/react-ui-theme';
|
|
11
11
|
|
|
12
|
-
import { type FormHandler } from '../../hooks';
|
|
12
|
+
import { type FormPath, type FormHandler } from '../../hooks';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Props passed to input components.
|
|
16
16
|
*/
|
|
17
17
|
export type InputProps<T extends BaseObject> = {
|
|
18
|
-
property:
|
|
18
|
+
property: FormPath<T>;
|
|
19
19
|
type: SimpleType;
|
|
20
20
|
format?: FormatEnum;
|
|
21
|
-
label
|
|
21
|
+
label?: string;
|
|
22
22
|
disabled?: boolean;
|
|
23
23
|
placeholder?: string;
|
|
24
|
+
inputOnly?: boolean;
|
|
24
25
|
} & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
|
|
25
26
|
|
|
26
27
|
/**
|