@dxos/react-ui-form 0.7.4 → 0.7.5-labs.5f04cf6
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 +574 -496
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +629 -565
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +574 -496
- 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 +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts +16 -0
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/Defaults.d.ts +5 -6
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +13 -17
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +7 -1
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts +7 -0
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormContent.d.ts +26 -0
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormContext.d.ts +20 -0
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -0
- package/dist/types/src/components/Form/Input.d.ts +5 -6
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +4 -0
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -0
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +6 -0
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -0
- package/dist/types/src/components/Form/custom/index.d.ts +3 -0
- package/dist/types/src/components/Form/custom/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/factory.d.ts +2 -2
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts +2 -2
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +13 -15
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +26 -26
- package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
- package/src/components/FieldEditor/FieldEditor.tsx +75 -50
- package/src/components/Form/ArrayField.tsx +111 -0
- package/src/components/Form/Defaults.tsx +20 -33
- package/src/components/Form/Form.stories.tsx +48 -6
- package/src/components/Form/Form.tsx +39 -333
- package/src/components/Form/FormActions.tsx +41 -0
- package/src/components/Form/FormContent.tsx +155 -0
- package/src/components/Form/FormContext.tsx +48 -0
- package/src/components/Form/Input.tsx +5 -6
- package/src/components/Form/custom/GeoPointInput.tsx +44 -0
- package/src/components/Form/custom/TupleInput.tsx +49 -0
- package/src/components/Form/custom/index.ts +6 -0
- package/src/components/Form/factory.tsx +4 -7
- package/src/components/Form/index.ts +3 -2
- package/src/components/ViewEditor/ViewEditor.stories.tsx +30 -8
- package/src/components/ViewEditor/ViewEditor.tsx +13 -8
- package/src/hooks/useForm.ts +67 -78
- package/dist/types/src/components/Form/Custom.d.ts +0 -5
- package/dist/types/src/components/Form/Custom.d.ts.map +0 -1
- package/src/components/Form/Custom.tsx +0 -65
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { type GeoPoint } from '@dxos/echo-schema';
|
|
8
|
+
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
import { safeParseFloat } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
import { translationKey } from '../../../translations';
|
|
12
|
+
import { InputHeader, type InputProps } from '../Input';
|
|
13
|
+
|
|
14
|
+
export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
|
|
15
|
+
const { t } = useTranslation(translationKey);
|
|
16
|
+
const { status, error } = getStatus();
|
|
17
|
+
const [lng = 0, lat = 0] = getValue<GeoPoint>() ?? [];
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Input.Root validationValence={status}>
|
|
21
|
+
<InputHeader error={error}>
|
|
22
|
+
<Input.Label>{label}</Input.Label>
|
|
23
|
+
</InputHeader>
|
|
24
|
+
<div className='grid grid-cols-2 gap-2'>
|
|
25
|
+
<Input.TextInput
|
|
26
|
+
type='number'
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
placeholder={t('placeholder longitude')}
|
|
29
|
+
value={lng}
|
|
30
|
+
onChange={(event) => onValueChange(type, [lat, safeParseFloat(event.target.value, 0)])}
|
|
31
|
+
onBlur={onBlur}
|
|
32
|
+
/>
|
|
33
|
+
<Input.TextInput
|
|
34
|
+
type='number'
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
placeholder={t('placeholder latitude')}
|
|
37
|
+
value={lat}
|
|
38
|
+
onChange={(event) => onValueChange(type, [safeParseFloat(event.target.value, 0), lng])}
|
|
39
|
+
onBlur={onBlur}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
</Input.Root>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input } from '@dxos/react-ui';
|
|
8
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
9
|
+
import { safeParseFloat } from '@dxos/util';
|
|
10
|
+
|
|
11
|
+
import { InputHeader, type InputProps } from '../Input';
|
|
12
|
+
|
|
13
|
+
const gridCols = ['grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4'];
|
|
14
|
+
|
|
15
|
+
export const TupleInput = ({
|
|
16
|
+
binding,
|
|
17
|
+
type,
|
|
18
|
+
label,
|
|
19
|
+
disabled,
|
|
20
|
+
getStatus,
|
|
21
|
+
getValue,
|
|
22
|
+
onValueChange,
|
|
23
|
+
onBlur,
|
|
24
|
+
}: InputProps & { binding: string[] }) => {
|
|
25
|
+
const { status, error } = getStatus();
|
|
26
|
+
|
|
27
|
+
// TODO(burdon): Generalize number/float/string, etc.
|
|
28
|
+
const values: Record<string, number> = getValue<Record<string, number>>() ?? {};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Input.Root validationValence={status}>
|
|
32
|
+
<InputHeader error={error}>
|
|
33
|
+
<Input.Label>{label}</Input.Label>
|
|
34
|
+
</InputHeader>
|
|
35
|
+
<div className={mx('grid gap-2', gridCols[binding.length - 1])}>
|
|
36
|
+
{binding.map((prop) => (
|
|
37
|
+
<Input.TextInput
|
|
38
|
+
key={prop}
|
|
39
|
+
type='number'
|
|
40
|
+
disabled={disabled}
|
|
41
|
+
value={values[prop]}
|
|
42
|
+
onChange={(event) => onValueChange(type, { ...values, [prop]: safeParseFloat(event.target.value, 0) })}
|
|
43
|
+
onBlur={onBlur}
|
|
44
|
+
/>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
</Input.Root>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -2,23 +2,20 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { FormatEnum } from '@dxos/echo-schema';
|
|
6
6
|
import { type SimpleType } from '@dxos/effect';
|
|
7
7
|
|
|
8
|
-
import { GeiPointInput } from './Custom';
|
|
9
8
|
import { BooleanInput, NumberInput, TextInput } from './Defaults';
|
|
10
9
|
import { type InputComponent } from './Input';
|
|
10
|
+
import { GeoPointInput } from './custom';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Get property input component.
|
|
14
14
|
*/
|
|
15
|
-
export const getInputComponent =
|
|
16
|
-
type: SimpleType,
|
|
17
|
-
format?: FormatEnum,
|
|
18
|
-
): InputComponent<T> | undefined => {
|
|
15
|
+
export const getInputComponent = (type: SimpleType, format?: FormatEnum): InputComponent | undefined => {
|
|
19
16
|
switch (format) {
|
|
20
17
|
case FormatEnum.LatLng:
|
|
21
|
-
return
|
|
18
|
+
return GeoPointInput;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
switch (type) {
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
|
-
import React, { useCallback,
|
|
8
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
import { Format, type
|
|
11
|
-
import { useSpace } from '@dxos/react-client/echo';
|
|
10
|
+
import { Format, type EchoSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
|
|
11
|
+
import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
12
12
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
13
|
-
import {
|
|
13
|
+
import { useAsyncEffect } from '@dxos/react-ui';
|
|
14
|
+
import { ViewProjection, ViewType, createView } from '@dxos/schema';
|
|
14
15
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
15
16
|
|
|
16
17
|
import { ViewEditor } from './ViewEditor';
|
|
@@ -19,10 +20,10 @@ import { TestLayout, TestPanel } from '../testing';
|
|
|
19
20
|
|
|
20
21
|
const DefaultStory = () => {
|
|
21
22
|
const space = useSpace();
|
|
22
|
-
const [schema, setSchema] = useState<
|
|
23
|
+
const [schema, setSchema] = useState<EchoSchema>();
|
|
23
24
|
const [view, setView] = useState<ViewType>();
|
|
24
25
|
const [projection, setProjection] = useState<ViewProjection>();
|
|
25
|
-
|
|
26
|
+
useAsyncEffect(async () => {
|
|
26
27
|
if (space) {
|
|
27
28
|
class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
28
29
|
name: S.String,
|
|
@@ -30,7 +31,7 @@ const DefaultStory = () => {
|
|
|
30
31
|
salary: Format.Currency(),
|
|
31
32
|
}) {}
|
|
32
33
|
|
|
33
|
-
const schema = space.db.schemaRegistry.
|
|
34
|
+
const [schema] = await space.db.schemaRegistry.register([TestSchema]);
|
|
34
35
|
const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: toJsonSchema(TestSchema) });
|
|
35
36
|
const projection = new ViewProjection(schema, view);
|
|
36
37
|
|
|
@@ -40,6 +41,21 @@ const DefaultStory = () => {
|
|
|
40
41
|
}
|
|
41
42
|
}, [space]);
|
|
42
43
|
|
|
44
|
+
const views = useQuery(space, Filter.schema(ViewType));
|
|
45
|
+
const currentTypename = useMemo(() => view?.query?.type, [view]);
|
|
46
|
+
const updateViewTypename = useCallback(
|
|
47
|
+
(newTypename: string) => {
|
|
48
|
+
if (!schema) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const matchingViews = views.filter((view) => view.query.type === currentTypename);
|
|
52
|
+
for (const view of matchingViews) {
|
|
53
|
+
view.query.type = newTypename;
|
|
54
|
+
}
|
|
55
|
+
schema.updateTypename(newTypename);
|
|
56
|
+
},
|
|
57
|
+
[views, schema],
|
|
58
|
+
);
|
|
43
59
|
const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
|
|
44
60
|
|
|
45
61
|
if (!schema || !view || !projection) {
|
|
@@ -49,7 +65,13 @@ const DefaultStory = () => {
|
|
|
49
65
|
return (
|
|
50
66
|
<TestLayout json={{ schema, view, projection }}>
|
|
51
67
|
<TestPanel>
|
|
52
|
-
<ViewEditor
|
|
68
|
+
<ViewEditor
|
|
69
|
+
schema={schema}
|
|
70
|
+
view={view}
|
|
71
|
+
registry={space?.db.schemaRegistry}
|
|
72
|
+
onTypenameChanged={updateViewTypename}
|
|
73
|
+
onDelete={handleDelete}
|
|
74
|
+
/>
|
|
53
75
|
</TestPanel>
|
|
54
76
|
</TestLayout>
|
|
55
77
|
);
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
8
|
-
import { AST, Format, type
|
|
7
|
+
import { type SchemaRegistry } from '@dxos/echo-db';
|
|
8
|
+
import { AST, Format, type EchoSchema, S } from '@dxos/echo-schema';
|
|
9
9
|
import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
10
10
|
import { List } from '@dxos/react-ui-list';
|
|
11
11
|
import { ghostHover, inputTextLabel, mx } from '@dxos/react-ui-theme';
|
|
@@ -30,11 +30,12 @@ const ViewMetaSchema = S.Struct({
|
|
|
30
30
|
type ViewMetaType = S.Schema.Type<typeof ViewMetaSchema>;
|
|
31
31
|
|
|
32
32
|
export type ViewEditorProps = ThemedClassName<{
|
|
33
|
-
schema:
|
|
33
|
+
schema: EchoSchema;
|
|
34
34
|
view: ViewType;
|
|
35
|
-
registry?:
|
|
35
|
+
registry?: SchemaRegistry;
|
|
36
36
|
readonly?: boolean;
|
|
37
37
|
showHeading?: boolean;
|
|
38
|
+
onTypenameChanged: (typename: string) => void;
|
|
38
39
|
onDelete: (fieldId: string) => void;
|
|
39
40
|
}>;
|
|
40
41
|
|
|
@@ -48,6 +49,7 @@ export const ViewEditor = ({
|
|
|
48
49
|
registry,
|
|
49
50
|
readonly,
|
|
50
51
|
showHeading = false,
|
|
52
|
+
onTypenameChanged,
|
|
51
53
|
onDelete,
|
|
52
54
|
}: ViewEditorProps) => {
|
|
53
55
|
const { t } = useTranslation(translationKey);
|
|
@@ -67,12 +69,15 @@ export const ViewEditor = ({
|
|
|
67
69
|
const handleViewUpdate = useCallback(
|
|
68
70
|
({ name, typename }: ViewMetaType) => {
|
|
69
71
|
requestAnimationFrame(() => {
|
|
70
|
-
view.name
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
if (view.name !== name) {
|
|
73
|
+
view.name = name;
|
|
74
|
+
}
|
|
75
|
+
if (view.query.type !== typename) {
|
|
76
|
+
onTypenameChanged(typename);
|
|
77
|
+
}
|
|
73
78
|
});
|
|
74
79
|
},
|
|
75
|
-
[view, schema],
|
|
80
|
+
[view, schema, onTypenameChanged],
|
|
76
81
|
);
|
|
77
82
|
|
|
78
83
|
const handleSelect = useCallback((field: FieldType) => {
|
package/src/hooks/useForm.ts
CHANGED
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type BaseObject,
|
|
8
|
-
import { type SimpleType, type
|
|
7
|
+
import { type BaseObject, getValue, setValue } from '@dxos/echo-schema';
|
|
8
|
+
import { AST, type S, type SimpleType, type JsonPath, createJsonPath, fromEffectValidationPath } from '@dxos/effect';
|
|
9
9
|
import { invariant } from '@dxos/invariant';
|
|
10
10
|
import { log } from '@dxos/log';
|
|
11
11
|
import { validateSchema, type ValidationError } from '@dxos/schema';
|
|
12
12
|
import { type MaybePromise } from '@dxos/util';
|
|
13
13
|
|
|
14
|
-
// TODO(ZaymonFC): Where should this live?
|
|
15
|
-
export type FormPath<T extends object = {}> = PropertyKey<T> | `${PropertyKey<T>}.${number}`;
|
|
16
|
-
|
|
17
14
|
/**
|
|
18
15
|
* Return type from `useForm` hook.
|
|
19
16
|
*/
|
|
@@ -22,10 +19,10 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
22
19
|
// Form state management.
|
|
23
20
|
//
|
|
24
21
|
|
|
25
|
-
values: T
|
|
26
|
-
errors: Record<
|
|
27
|
-
touched: Record<
|
|
28
|
-
changed: Record<
|
|
22
|
+
values: Partial<T>;
|
|
23
|
+
errors: Record<JsonPath, string>;
|
|
24
|
+
touched: Record<JsonPath, boolean>;
|
|
25
|
+
changed: Record<JsonPath, boolean>;
|
|
29
26
|
canSave: boolean;
|
|
30
27
|
handleSave: () => void;
|
|
31
28
|
|
|
@@ -33,10 +30,10 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
33
30
|
// Form input component helpers.
|
|
34
31
|
//
|
|
35
32
|
|
|
36
|
-
getStatus: (
|
|
37
|
-
getValue: <V>(
|
|
38
|
-
onValueChange: <V>(
|
|
39
|
-
|
|
33
|
+
getStatus: (path: string | (string | number)[]) => { status?: 'error'; error?: string };
|
|
34
|
+
getValue: <V>(path: (string | number)[]) => V | undefined;
|
|
35
|
+
onValueChange: <V>(path: (string | number)[], type: SimpleType, value: V) => void;
|
|
36
|
+
onTouched: (path: (string | number)[]) => void;
|
|
40
37
|
};
|
|
41
38
|
|
|
42
39
|
/**
|
|
@@ -44,23 +41,21 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
44
41
|
*/
|
|
45
42
|
export interface FormOptions<T extends BaseObject> {
|
|
46
43
|
/**
|
|
47
|
-
* Effect schema.
|
|
44
|
+
* Effect schema (Type literal).
|
|
48
45
|
*/
|
|
49
|
-
// TODO(burdon): Change to S.Struct<T>?
|
|
50
46
|
schema: S.Schema<T>;
|
|
51
47
|
|
|
52
48
|
/**
|
|
53
49
|
* Initial values (which may not pass validation).
|
|
54
50
|
*/
|
|
55
|
-
|
|
56
|
-
initialValues: T;
|
|
51
|
+
initialValues: Partial<T>;
|
|
57
52
|
|
|
58
53
|
/**
|
|
59
54
|
* Callback for value changes. Note: This is called even when values are invalid.
|
|
60
55
|
* Sometimes the parent component may want to know about changes even if the form is
|
|
61
56
|
* in an invalid state.
|
|
62
57
|
*/
|
|
63
|
-
onValuesChanged?: (values: T) => void;
|
|
58
|
+
onValuesChanged?: (values: Partial<T>) => void;
|
|
64
59
|
|
|
65
60
|
/**
|
|
66
61
|
* Custom validation function that runs only after schema validation passes.
|
|
@@ -93,14 +88,16 @@ export const useForm = <T extends BaseObject>({
|
|
|
93
88
|
onValid,
|
|
94
89
|
onSave,
|
|
95
90
|
}: FormOptions<T>): FormHandler<T> => {
|
|
96
|
-
|
|
91
|
+
invariant(AST.isTypeLiteral(schema.ast));
|
|
92
|
+
|
|
93
|
+
const [values, setValues] = useState<Partial<T>>(initialValues);
|
|
97
94
|
useEffect(() => {
|
|
98
95
|
setValues(initialValues);
|
|
99
96
|
}, [initialValues]);
|
|
100
97
|
|
|
101
|
-
const [touched, setTouched] = useState<Record<
|
|
102
|
-
const [changed, setChanged] = useState<Record<
|
|
103
|
-
const [errors, setErrors] = useState<Record<
|
|
98
|
+
const [touched, setTouched] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
|
|
99
|
+
const [changed, setChanged] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
|
|
100
|
+
const [errors, setErrors] = useState<Record<JsonPath, string>>({});
|
|
104
101
|
const [saving, setSaving] = useState(false);
|
|
105
102
|
|
|
106
103
|
//
|
|
@@ -109,14 +106,16 @@ export const useForm = <T extends BaseObject>({
|
|
|
109
106
|
|
|
110
107
|
// TODO(burdon): Validate each property separately.
|
|
111
108
|
const validate = useCallback(
|
|
112
|
-
(values: T) => {
|
|
109
|
+
(values: Partial<T>): values is T => {
|
|
113
110
|
let errors: ValidationError[] = validateSchema(schema, values) ?? [];
|
|
114
111
|
if (errors.length === 0 && onValidate) {
|
|
115
|
-
|
|
112
|
+
const validatedValues = values as T;
|
|
113
|
+
errors = onValidate(validatedValues) ?? [];
|
|
116
114
|
}
|
|
117
115
|
|
|
118
116
|
setErrors(flatMap(errors));
|
|
119
|
-
|
|
117
|
+
const valid = errors.length === 0;
|
|
118
|
+
return valid;
|
|
120
119
|
},
|
|
121
120
|
[schema, onValidate],
|
|
122
121
|
);
|
|
@@ -138,10 +137,15 @@ export const useForm = <T extends BaseObject>({
|
|
|
138
137
|
const canSave = useMemo(
|
|
139
138
|
() =>
|
|
140
139
|
!saving &&
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
// Check if any error paths that are touched have errors
|
|
141
|
+
!Object.entries(touched).some(
|
|
142
|
+
([path, isTouched]) =>
|
|
143
|
+
isTouched &&
|
|
144
|
+
Object.keys(errors).some(
|
|
145
|
+
(errorPath) => errorPath === path || errorPath.startsWith(`${path}.`) || errorPath.startsWith(`${path}[`),
|
|
146
|
+
),
|
|
143
147
|
),
|
|
144
|
-
[
|
|
148
|
+
[touched, errors, saving],
|
|
145
149
|
);
|
|
146
150
|
|
|
147
151
|
const handleSave = useCallback(async () => {
|
|
@@ -153,27 +157,34 @@ export const useForm = <T extends BaseObject>({
|
|
|
153
157
|
setSaving(false);
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
|
-
}, [values, validate, onSave]);
|
|
160
|
+
}, [values, validate, onSave, changed]);
|
|
157
161
|
|
|
158
162
|
//
|
|
159
163
|
// Fields.
|
|
160
164
|
//
|
|
161
165
|
|
|
162
166
|
const getStatus = useCallback<FormHandler<T>['getStatus']>(
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
(path) => {
|
|
168
|
+
const jsonPath = Array.isArray(path) ? createJsonPath(path) : path;
|
|
169
|
+
const matchingError = Object.entries(errors).find(
|
|
170
|
+
([errorPath]) =>
|
|
171
|
+
errorPath === jsonPath || errorPath.startsWith(`${jsonPath}.`) || errorPath.startsWith(`${jsonPath}[`),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
status: matchingError ? 'error' : undefined,
|
|
176
|
+
error: matchingError ? matchingError[1] : undefined,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
[errors],
|
|
168
180
|
);
|
|
169
181
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return getByPath(values, property);
|
|
182
|
+
const getFormValue = <V>(path: (string | number)[]): V | undefined => {
|
|
183
|
+
return getValue(values, createJsonPath(path));
|
|
173
184
|
};
|
|
174
185
|
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
const onValueChange = (path: (string | number)[], type: SimpleType, value: any) => {
|
|
187
|
+
const jsonPath = createJsonPath(path);
|
|
177
188
|
let parsedValue = value;
|
|
178
189
|
try {
|
|
179
190
|
if (type === 'number') {
|
|
@@ -184,9 +195,9 @@ export const useForm = <T extends BaseObject>({
|
|
|
184
195
|
parsedValue = undefined;
|
|
185
196
|
}
|
|
186
197
|
|
|
187
|
-
const newValues =
|
|
198
|
+
const newValues = { ...setValue(values, jsonPath, parsedValue) };
|
|
188
199
|
setValues(newValues);
|
|
189
|
-
setChanged((prev) => ({ ...prev, [
|
|
200
|
+
setChanged((prev) => ({ ...prev, [jsonPath]: true }));
|
|
190
201
|
onValuesChanged?.(newValues);
|
|
191
202
|
|
|
192
203
|
const isValid = validate(newValues);
|
|
@@ -195,19 +206,10 @@ export const useForm = <T extends BaseObject>({
|
|
|
195
206
|
}
|
|
196
207
|
};
|
|
197
208
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
setTouched((touched) => ({ ...touched, [name]: true }));
|
|
203
|
-
|
|
204
|
-
// TODO(Zan): This should be configurable behavior.
|
|
205
|
-
if (event.relatedTarget?.getAttribute('type') === 'submit') {
|
|
206
|
-
// NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
|
|
207
|
-
// and results in the submit button being disabled when the form is invalid.
|
|
208
|
-
setTouched(createKeySet(values, true));
|
|
209
|
-
}
|
|
210
|
-
|
|
209
|
+
const onTouched = useCallback(
|
|
210
|
+
(path: (string | number)[]) => {
|
|
211
|
+
const jsonPath = createJsonPath(path);
|
|
212
|
+
setTouched((touched) => ({ ...touched, [jsonPath]: true }));
|
|
211
213
|
validate(values);
|
|
212
214
|
},
|
|
213
215
|
[validate, values],
|
|
@@ -224,40 +226,27 @@ export const useForm = <T extends BaseObject>({
|
|
|
224
226
|
|
|
225
227
|
// Field utils.
|
|
226
228
|
getStatus,
|
|
227
|
-
getValue,
|
|
229
|
+
getValue: getFormValue,
|
|
228
230
|
onValueChange,
|
|
229
|
-
|
|
231
|
+
onTouched,
|
|
230
232
|
} satisfies FormHandler<T>;
|
|
231
233
|
};
|
|
232
234
|
|
|
233
|
-
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<
|
|
235
|
+
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<JsonPath, V> => {
|
|
234
236
|
invariant(obj);
|
|
235
|
-
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<
|
|
237
|
+
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
|
|
236
238
|
};
|
|
237
239
|
|
|
238
|
-
const flatMap =
|
|
240
|
+
const flatMap = (errors: ValidationError[]) => {
|
|
239
241
|
return errors.reduce(
|
|
240
242
|
(result, { path, message }) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
// Convert the validation error path format to our JsonPath format
|
|
244
|
+
const jsonPath = fromEffectValidationPath(path);
|
|
245
|
+
if (!(jsonPath in result)) {
|
|
246
|
+
result[jsonPath] = message;
|
|
243
247
|
}
|
|
244
248
|
return result;
|
|
245
249
|
},
|
|
246
|
-
{} as Record<
|
|
250
|
+
{} as Record<JsonPath, string>,
|
|
247
251
|
);
|
|
248
252
|
};
|
|
249
|
-
|
|
250
|
-
// TODO(ZaymonFC): Move to util?
|
|
251
|
-
const getByPath = <T extends object, V>(obj: T, path: FormPath<T>): V => {
|
|
252
|
-
const [key, index] = path.split('.');
|
|
253
|
-
return index === undefined ? (obj[key as keyof T] as V) : ((obj[key as keyof T] as any[])[parseInt(index)] as V);
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const setByPath = <T extends object>(obj: T, path: FormPath<T>, value: any): T => {
|
|
257
|
-
const [key, index] = path.split('.');
|
|
258
|
-
const newValue =
|
|
259
|
-
index === undefined
|
|
260
|
-
? value
|
|
261
|
-
: [...(obj[key as keyof T] as any[])].map((v, i) => (i === parseInt(index) ? value : v));
|
|
262
|
-
return { ...obj, [key]: newValue };
|
|
263
|
-
};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { type BaseObject } from '@dxos/echo-schema';
|
|
3
|
-
import { type InputProps } from './Input';
|
|
4
|
-
export declare const GeiPointInput: <T extends BaseObject>({ property, type, label, disabled, getStatus, getValue, onValueChange, onBlur, }: InputProps<T>) => React.JSX.Element;
|
|
5
|
-
//# sourceMappingURL=Custom.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Custom.d.ts","sourceRoot":"","sources":["../../../../../src/components/Form/Custom.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,KAAK,UAAU,EAAiB,MAAM,mBAAmB,CAAC;AAGnE,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAOvD,eAAO,MAAM,aAAa,GAAI,CAAC,SAAS,UAAU,oFAS/C,UAAU,CAAC,CAAC,CAAC,sBA8Bf,CAAC"}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import React from 'react';
|
|
6
|
-
|
|
7
|
-
import { type BaseObject, type GeoPoint } from '@dxos/echo-schema';
|
|
8
|
-
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
|
-
|
|
10
|
-
import { InputHeader, type InputProps } from './Input';
|
|
11
|
-
import { translationKey } from '../../translations';
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
// Custom format components.
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
export const GeiPointInput = <T extends BaseObject>({
|
|
18
|
-
property,
|
|
19
|
-
type,
|
|
20
|
-
label,
|
|
21
|
-
disabled,
|
|
22
|
-
getStatus,
|
|
23
|
-
getValue,
|
|
24
|
-
onValueChange,
|
|
25
|
-
onBlur,
|
|
26
|
-
}: InputProps<T>) => {
|
|
27
|
-
const { t } = useTranslation(translationKey);
|
|
28
|
-
const { status, error } = getStatus?.(property);
|
|
29
|
-
const [lng = 0, lat = 0] = getValue<GeoPoint>(property) ?? [];
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<Input.Root validationValence={status}>
|
|
33
|
-
<InputHeader error={error}>
|
|
34
|
-
<Input.Label>{label}</Input.Label>
|
|
35
|
-
</InputHeader>
|
|
36
|
-
<div className='grid grid-cols-2 gap-2'>
|
|
37
|
-
<Input.TextInput
|
|
38
|
-
type='number'
|
|
39
|
-
disabled={disabled}
|
|
40
|
-
placeholder={t('placeholder longitude')}
|
|
41
|
-
value={lng}
|
|
42
|
-
onChange={(event) => onValueChange(property, type, [lat, safeParseFloat(event.target.value, 0)])}
|
|
43
|
-
onBlur={onBlur}
|
|
44
|
-
/>
|
|
45
|
-
<Input.TextInput
|
|
46
|
-
type='number'
|
|
47
|
-
disabled={disabled}
|
|
48
|
-
placeholder={t('placeholder latitude')}
|
|
49
|
-
value={lat}
|
|
50
|
-
onChange={(event) => onValueChange(property, type, [safeParseFloat(event.target.value, 0), lng])}
|
|
51
|
-
onBlur={onBlur}
|
|
52
|
-
/>
|
|
53
|
-
</div>
|
|
54
|
-
</Input.Root>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// TODO(burdon): Factor out.
|
|
59
|
-
const safeParseFloat = (str: string, defaultValue?: number): number | undefined => {
|
|
60
|
-
try {
|
|
61
|
-
return parseFloat(str);
|
|
62
|
-
} catch {
|
|
63
|
-
return defaultValue ?? 0;
|
|
64
|
-
}
|
|
65
|
-
};
|