@dxos/react-ui-form 0.7.4 → 0.7.5-labs.071a3e2
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 +841 -521
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node/index.cjs +877 -571
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +841 -521
- 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 +14 -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/SelectOptionsInput.d.ts +4 -0
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -0
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +7 -0
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.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 +4 -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/components/deprecated/FormInput.d.ts +5 -0
- package/dist/types/src/components/deprecated/FormInput.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/src/translations.d.ts +6 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +28 -27
- package/src/components/FieldEditor/FieldEditor.stories.tsx +3 -3
- package/src/components/FieldEditor/FieldEditor.tsx +77 -51
- 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 +49 -329
- package/src/components/Form/FormActions.tsx +41 -0
- package/src/components/Form/FormContent.tsx +173 -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/SelectOptionsInput.stories.tsx +58 -0
- package/src/components/Form/custom/SelectOptionsInput.tsx +192 -0
- package/src/components/Form/custom/TupleInput.tsx +49 -0
- package/src/components/Form/custom/index.ts +7 -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 +18 -10
- package/src/components/deprecated/FormInput.tsx +20 -7
- package/src/hooks/useForm.ts +67 -79
- package/src/translations.ts +8 -0
- 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
|
@@ -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,13 +4,12 @@
|
|
|
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';
|
|
12
12
|
import { FieldSchema, type FieldType, type ViewType, ViewProjection, VIEW_FIELD_LIMIT } from '@dxos/schema';
|
|
13
|
-
import { arrayMove } from '@dxos/util';
|
|
14
13
|
|
|
15
14
|
import { translationKey } from '../../translations';
|
|
16
15
|
import { FieldEditor } from '../FieldEditor';
|
|
@@ -30,11 +29,12 @@ const ViewMetaSchema = S.Struct({
|
|
|
30
29
|
type ViewMetaType = S.Schema.Type<typeof ViewMetaSchema>;
|
|
31
30
|
|
|
32
31
|
export type ViewEditorProps = ThemedClassName<{
|
|
33
|
-
schema:
|
|
32
|
+
schema: EchoSchema;
|
|
34
33
|
view: ViewType;
|
|
35
|
-
registry?:
|
|
34
|
+
registry?: SchemaRegistry;
|
|
36
35
|
readonly?: boolean;
|
|
37
36
|
showHeading?: boolean;
|
|
37
|
+
onTypenameChanged: (typename: string) => void;
|
|
38
38
|
onDelete: (fieldId: string) => void;
|
|
39
39
|
}>;
|
|
40
40
|
|
|
@@ -48,6 +48,7 @@ export const ViewEditor = ({
|
|
|
48
48
|
registry,
|
|
49
49
|
readonly,
|
|
50
50
|
showHeading = false,
|
|
51
|
+
onTypenameChanged,
|
|
51
52
|
onDelete,
|
|
52
53
|
}: ViewEditorProps) => {
|
|
53
54
|
const { t } = useTranslation(translationKey);
|
|
@@ -67,12 +68,15 @@ export const ViewEditor = ({
|
|
|
67
68
|
const handleViewUpdate = useCallback(
|
|
68
69
|
({ name, typename }: ViewMetaType) => {
|
|
69
70
|
requestAnimationFrame(() => {
|
|
70
|
-
view.name
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
if (view.name !== name) {
|
|
72
|
+
view.name = name;
|
|
73
|
+
}
|
|
74
|
+
if (view.query.type !== typename) {
|
|
75
|
+
onTypenameChanged(typename);
|
|
76
|
+
}
|
|
73
77
|
});
|
|
74
78
|
},
|
|
75
|
-
[view, schema],
|
|
79
|
+
[view, schema, onTypenameChanged],
|
|
76
80
|
);
|
|
77
81
|
|
|
78
82
|
const handleSelect = useCallback((field: FieldType) => {
|
|
@@ -86,7 +90,11 @@ export const ViewEditor = ({
|
|
|
86
90
|
|
|
87
91
|
const handleMove = useCallback(
|
|
88
92
|
(fromIndex: number, toIndex: number) => {
|
|
89
|
-
|
|
93
|
+
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
94
|
+
const fields = [...view.fields];
|
|
95
|
+
const [moved] = fields.splice(fromIndex, 1);
|
|
96
|
+
fields.splice(toIndex, 0, moved);
|
|
97
|
+
view.fields = fields;
|
|
90
98
|
},
|
|
91
99
|
[view.fields],
|
|
92
100
|
);
|
|
@@ -4,7 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type JSX, type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { Input } from '@dxos/react-ui';
|
|
7
|
+
import { Input, type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated
|
|
12
|
+
*/
|
|
13
|
+
export const DeprecatedFormContainer = ({ children, classNames }: ThemedClassName<PropsWithChildren>) => {
|
|
14
|
+
return (
|
|
15
|
+
<div role='none' className='w-full p-4 justify-center overflow-x-hidden overflow-y-auto'>
|
|
16
|
+
<div role='form' className={mx('flex flex-col', textBlockWidth, classNames)}>
|
|
17
|
+
{children}
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
8
22
|
|
|
9
23
|
export type DeprecatedFormInputProps = {
|
|
10
24
|
label: string;
|
|
@@ -15,7 +29,6 @@ export type DeprecatedFormInputProps = {
|
|
|
15
29
|
/**
|
|
16
30
|
* @deprecated
|
|
17
31
|
*/
|
|
18
|
-
// TODO(burdon): Still used in ObservabilitySettings and all settings.
|
|
19
32
|
export const DeprecatedFormInput = ({
|
|
20
33
|
label,
|
|
21
34
|
description,
|
|
@@ -26,8 +39,9 @@ export const DeprecatedFormInput = ({
|
|
|
26
39
|
<div role='none' className='flex w-full gap-4 py-1'>
|
|
27
40
|
<Input.Root>
|
|
28
41
|
<div role='none' className='flex flex-col w-full'>
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
<Input.Label classNames='flex min-bs-[--rail-action] items-center' style={{ fontSize: 'unset' }}>
|
|
43
|
+
{label}
|
|
44
|
+
</Input.Label>
|
|
31
45
|
{description && (
|
|
32
46
|
<Input.DescriptionAndValidation classNames='mbs-0.5'>
|
|
33
47
|
<Input.Description>{description}</Input.Description>
|
|
@@ -36,7 +50,7 @@ export const DeprecatedFormInput = ({
|
|
|
36
50
|
</div>
|
|
37
51
|
|
|
38
52
|
<div role='none'>
|
|
39
|
-
<div role='none' className='flex min-
|
|
53
|
+
<div role='none' className='flex min-bs-[--rail-action] items-center'>
|
|
40
54
|
{children}
|
|
41
55
|
</div>
|
|
42
56
|
</div>
|
|
@@ -45,9 +59,8 @@ export const DeprecatedFormInput = ({
|
|
|
45
59
|
);
|
|
46
60
|
|
|
47
61
|
if (secondary) {
|
|
48
|
-
// console.log(secondary);
|
|
49
62
|
return (
|
|
50
|
-
<div role='none'
|
|
63
|
+
<div role='none'>
|
|
51
64
|
{primary}
|
|
52
65
|
{secondary}
|
|
53
66
|
</div>
|
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,30 +88,33 @@ 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
|
//
|
|
107
104
|
// Validation.
|
|
108
105
|
//
|
|
109
106
|
|
|
110
|
-
// TODO(burdon): Validate each property separately.
|
|
111
107
|
const validate = useCallback(
|
|
112
|
-
(values: T) => {
|
|
108
|
+
(values: Partial<T>): values is T => {
|
|
113
109
|
let errors: ValidationError[] = validateSchema(schema, values) ?? [];
|
|
114
110
|
if (errors.length === 0 && onValidate) {
|
|
115
|
-
|
|
111
|
+
const validatedValues = values as T;
|
|
112
|
+
errors = onValidate(validatedValues) ?? [];
|
|
116
113
|
}
|
|
117
114
|
|
|
118
115
|
setErrors(flatMap(errors));
|
|
119
|
-
|
|
116
|
+
const valid = errors.length === 0;
|
|
117
|
+
return valid;
|
|
120
118
|
},
|
|
121
119
|
[schema, onValidate],
|
|
122
120
|
);
|
|
@@ -138,10 +136,15 @@ export const useForm = <T extends BaseObject>({
|
|
|
138
136
|
const canSave = useMemo(
|
|
139
137
|
() =>
|
|
140
138
|
!saving &&
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// Check if any error paths that are touched have errors
|
|
140
|
+
!Object.entries(touched).some(
|
|
141
|
+
([path, isTouched]) =>
|
|
142
|
+
isTouched &&
|
|
143
|
+
Object.keys(errors).some(
|
|
144
|
+
(errorPath) => errorPath === path || errorPath.startsWith(`${path}.`) || errorPath.startsWith(`${path}[`),
|
|
145
|
+
),
|
|
143
146
|
),
|
|
144
|
-
[
|
|
147
|
+
[touched, errors, saving],
|
|
145
148
|
);
|
|
146
149
|
|
|
147
150
|
const handleSave = useCallback(async () => {
|
|
@@ -153,27 +156,34 @@ export const useForm = <T extends BaseObject>({
|
|
|
153
156
|
setSaving(false);
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
|
-
}, [values, validate, onSave]);
|
|
159
|
+
}, [values, validate, onSave, changed]);
|
|
157
160
|
|
|
158
161
|
//
|
|
159
162
|
// Fields.
|
|
160
163
|
//
|
|
161
164
|
|
|
162
165
|
const getStatus = useCallback<FormHandler<T>['getStatus']>(
|
|
163
|
-
(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
(path) => {
|
|
167
|
+
const jsonPath = Array.isArray(path) ? createJsonPath(path) : path;
|
|
168
|
+
const matchingError = Object.entries(errors).find(
|
|
169
|
+
([errorPath]) =>
|
|
170
|
+
errorPath === jsonPath || errorPath.startsWith(`${jsonPath}.`) || errorPath.startsWith(`${jsonPath}[`),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
status: matchingError ? 'error' : undefined,
|
|
175
|
+
error: matchingError ? matchingError[1] : undefined,
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
[errors],
|
|
168
179
|
);
|
|
169
180
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return getByPath(values, property);
|
|
181
|
+
const getFormValue = <V>(path: (string | number)[]): V | undefined => {
|
|
182
|
+
return getValue(values, createJsonPath(path));
|
|
173
183
|
};
|
|
174
184
|
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
const onValueChange = (path: (string | number)[], type: SimpleType, value: any) => {
|
|
186
|
+
const jsonPath = createJsonPath(path);
|
|
177
187
|
let parsedValue = value;
|
|
178
188
|
try {
|
|
179
189
|
if (type === 'number') {
|
|
@@ -184,9 +194,9 @@ export const useForm = <T extends BaseObject>({
|
|
|
184
194
|
parsedValue = undefined;
|
|
185
195
|
}
|
|
186
196
|
|
|
187
|
-
const newValues =
|
|
197
|
+
const newValues = { ...setValue(values, jsonPath, parsedValue) };
|
|
188
198
|
setValues(newValues);
|
|
189
|
-
setChanged((prev) => ({ ...prev, [
|
|
199
|
+
setChanged((prev) => ({ ...prev, [jsonPath]: true }));
|
|
190
200
|
onValuesChanged?.(newValues);
|
|
191
201
|
|
|
192
202
|
const isValid = validate(newValues);
|
|
@@ -195,19 +205,10 @@ export const useForm = <T extends BaseObject>({
|
|
|
195
205
|
}
|
|
196
206
|
};
|
|
197
207
|
|
|
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
|
-
|
|
208
|
+
const onTouched = useCallback(
|
|
209
|
+
(path: (string | number)[]) => {
|
|
210
|
+
const jsonPath = createJsonPath(path);
|
|
211
|
+
setTouched((touched) => ({ ...touched, [jsonPath]: true }));
|
|
211
212
|
validate(values);
|
|
212
213
|
},
|
|
213
214
|
[validate, values],
|
|
@@ -224,40 +225,27 @@ export const useForm = <T extends BaseObject>({
|
|
|
224
225
|
|
|
225
226
|
// Field utils.
|
|
226
227
|
getStatus,
|
|
227
|
-
getValue,
|
|
228
|
+
getValue: getFormValue,
|
|
228
229
|
onValueChange,
|
|
229
|
-
|
|
230
|
+
onTouched,
|
|
230
231
|
} satisfies FormHandler<T>;
|
|
231
232
|
};
|
|
232
233
|
|
|
233
|
-
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<
|
|
234
|
+
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<JsonPath, V> => {
|
|
234
235
|
invariant(obj);
|
|
235
|
-
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<
|
|
236
|
+
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
|
|
236
237
|
};
|
|
237
238
|
|
|
238
|
-
const flatMap =
|
|
239
|
+
const flatMap = (errors: ValidationError[]) => {
|
|
239
240
|
return errors.reduce(
|
|
240
241
|
(result, { path, message }) => {
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
// Convert the validation error path format to our JsonPath format
|
|
243
|
+
const jsonPath = fromEffectValidationPath(path);
|
|
244
|
+
if (!(jsonPath in result)) {
|
|
245
|
+
result[jsonPath] = message;
|
|
243
246
|
}
|
|
244
247
|
return result;
|
|
245
248
|
},
|
|
246
|
-
{} as Record<
|
|
249
|
+
{} as Record<JsonPath, string>,
|
|
247
250
|
);
|
|
248
251
|
};
|
|
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
|
-
};
|
package/src/translations.ts
CHANGED
|
@@ -42,6 +42,7 @@ export default [
|
|
|
42
42
|
'format ref': 'Reference',
|
|
43
43
|
'format regex': 'RegExp',
|
|
44
44
|
'format string': 'String',
|
|
45
|
+
'format single-select': 'Select',
|
|
45
46
|
'format text': 'Long text',
|
|
46
47
|
'format time': 'Time',
|
|
47
48
|
'format timestamp': 'Timestamp',
|
|
@@ -53,6 +54,13 @@ export default [
|
|
|
53
54
|
// Placeholders
|
|
54
55
|
'placeholder latitude': 'Latitude',
|
|
55
56
|
'placeholder longitude': 'Longitude',
|
|
57
|
+
|
|
58
|
+
// SelectOptionsInput
|
|
59
|
+
'select option label': 'Label',
|
|
60
|
+
'select option label placeholder': 'Option label',
|
|
61
|
+
'select option color': 'Color',
|
|
62
|
+
'select option delete': 'Delete',
|
|
63
|
+
'select option add': 'Add option',
|
|
56
64
|
},
|
|
57
65
|
},
|
|
58
66
|
},
|
|
@@ -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
|
-
};
|