@dxos/react-ui-form 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae
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 +2289 -2269
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2289 -2269
- 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 +8 -7
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +13 -7
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +81 -26
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +59 -84
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +37 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
- package/dist/types/src/components/{ControlSection/ControlSection.stories.d.ts → Form/fields/SelectOptionField.stories.d.ts} +12 -14
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/index.d.ts +12 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/index.d.ts +2 -4
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +15 -10
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +10 -6
- 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/{Form/custom/SelectOptionsInput.stories.d.ts → Settings/Settings.stories.d.ts} +10 -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 +16 -16
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +13 -9
- 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 +0 -2
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -3
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +10 -7
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts +3 -3
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +2 -2
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -0
- package/dist/types/src/util/properties.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +1 -2
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +50 -46
- package/src/components/FieldEditor/FieldEditor.stories.tsx +43 -24
- package/src/components/FieldEditor/FieldEditor.tsx +74 -78
- package/src/components/Form/Form.stories.tsx +149 -248
- package/src/components/Form/Form.tsx +347 -107
- package/src/components/Form/FormField.tsx +294 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +172 -0
- package/src/components/Form/FormFieldSet.tsx +123 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +149 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/GeoPointField.tsx +120 -0
- package/src/components/Form/fields/MarkdownField.tsx +78 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +189 -0
- package/src/components/Form/fields/SelectField.tsx +65 -0
- package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +19 -16
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +23 -17
- package/src/components/Form/fields/TextAreaField.tsx +38 -0
- package/src/components/Form/fields/TextField.tsx +38 -0
- package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +15 -8
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +40 -37
- package/src/components/ObjectPicker/ObjectPicker.tsx +84 -70
- 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 +69 -49
- package/src/components/ViewEditor/ViewEditor.test.tsx +8 -6
- package/src/components/ViewEditor/ViewEditor.tsx +302 -271
- package/src/components/index.ts +1 -1
- package/src/components/testing.tsx +12 -7
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +380 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/translations.ts +12 -10
- package/src/util/field.ts +25 -25
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +1 -1
- package/src/util/properties.ts +18 -0
- package/src/util/refs.ts +11 -17
- 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/ArrayField.d.ts +0 -16
- package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
- package/dist/types/src/components/Form/Defaults.d.ts +0 -14
- package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormActions.d.ts +0 -9
- package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContent.d.ts +0 -63
- package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContext.d.ts +0 -23
- package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
- package/dist/types/src/components/Form/Input.d.ts +0 -29
- package/dist/types/src/components/Form/Input.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -21
- package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/index.d.ts +0 -4
- package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/factory.d.ts +0 -8
- package/dist/types/src/components/Form/factory.d.ts.map +0 -1
- package/dist/types/src/hooks/useForm.d.ts +0 -67
- package/dist/types/src/hooks/useForm.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -28
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
- package/dist/types/src/util/schema.d.ts +0 -3
- package/dist/types/src/util/schema.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/ArrayField.tsx +0 -125
- package/src/components/Form/Defaults.tsx +0 -205
- package/src/components/Form/FormActions.tsx +0 -54
- package/src/components/Form/FormContent.tsx +0 -322
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -54
- package/src/components/Form/RefField.tsx +0 -269
- package/src/components/Form/custom/GeoPointInput.tsx +0 -80
- package/src/components/Form/custom/index.ts +0 -7
- package/src/components/Form/factory.tsx +0 -31
- package/src/hooks/useForm.ts +0 -295
- package/src/hooks/useQueryRefOptions.ts +0 -59
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -12
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import type * as Schema from 'effect/Schema';
|
|
6
|
-
import type * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
-
import React, { useCallback, useMemo } from 'react';
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
type EchoSchema,
|
|
11
|
-
Expando,
|
|
12
|
-
Ref,
|
|
13
|
-
ReferenceAnnotationId,
|
|
14
|
-
type ReferenceAnnotationValue,
|
|
15
|
-
getTypeAnnotation,
|
|
16
|
-
} from '@dxos/echo/internal';
|
|
17
|
-
import { findAnnotation } from '@dxos/effect';
|
|
18
|
-
import { DXN } from '@dxos/keys';
|
|
19
|
-
import { type DxTagPickerItemClick } from '@dxos/lit-ui';
|
|
20
|
-
import { DxAnchor, DxTagPickerItem } from '@dxos/lit-ui/react';
|
|
21
|
-
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
22
|
-
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
23
|
-
import { isNonNullable } from '@dxos/util';
|
|
24
|
-
|
|
25
|
-
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
26
|
-
import { translationKey } from '../../translations';
|
|
27
|
-
import { ObjectPicker } from '../ObjectPicker';
|
|
28
|
-
|
|
29
|
-
import { TextInput } from './Defaults';
|
|
30
|
-
import { InputHeader, type InputProps } from './Input';
|
|
31
|
-
|
|
32
|
-
export type RefFieldProps = InputProps & {
|
|
33
|
-
ast?: SchemaAST.AST;
|
|
34
|
-
array?: boolean;
|
|
35
|
-
createOptionLabel?: [string, { ns: string }];
|
|
36
|
-
createOptionIcon?: string;
|
|
37
|
-
onCreate?: (values: any) => void;
|
|
38
|
-
createSchema?: Schema.Schema.AnyNoContext;
|
|
39
|
-
createInitialValuePath?: string;
|
|
40
|
-
onQueryRefOptions?: QueryRefOptions;
|
|
41
|
-
schema?: EchoSchema;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// TODO(thure): Is this a standard that should be better canonized?
|
|
45
|
-
const isRefSnapShot = (val: any): val is { '/': string } => {
|
|
46
|
-
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const RefField = ({
|
|
50
|
-
type,
|
|
51
|
-
label,
|
|
52
|
-
readonly,
|
|
53
|
-
placeholder,
|
|
54
|
-
inputOnly,
|
|
55
|
-
array,
|
|
56
|
-
ast,
|
|
57
|
-
getValue,
|
|
58
|
-
createOptionLabel,
|
|
59
|
-
createOptionIcon,
|
|
60
|
-
onBlur,
|
|
61
|
-
onCreate,
|
|
62
|
-
createSchema,
|
|
63
|
-
createInitialValuePath,
|
|
64
|
-
onQueryRefOptions,
|
|
65
|
-
onValueChange,
|
|
66
|
-
...restInputProps
|
|
67
|
-
}: RefFieldProps) => {
|
|
68
|
-
const { t } = useTranslation(translationKey);
|
|
69
|
-
const refTypeInfo = useMemo(
|
|
70
|
-
() => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
|
|
71
|
-
[ast],
|
|
72
|
-
);
|
|
73
|
-
const { options: availableOptions, loading: _loading } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
|
|
74
|
-
|
|
75
|
-
if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
|
|
76
|
-
// If ref type is expando, fall back to taking a DXN in string format.
|
|
77
|
-
return (
|
|
78
|
-
<RefFieldFallback
|
|
79
|
-
{...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
80
|
-
/>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const handleGetValue = useCallback(() => {
|
|
85
|
-
const formValue = getValue();
|
|
86
|
-
|
|
87
|
-
const unknownToRefOption = (val: unknown) => {
|
|
88
|
-
const isRef = Ref.isRef(val);
|
|
89
|
-
if (isRef || isRefSnapShot(val)) {
|
|
90
|
-
const dxnString = isRef ? val.dxn.toString() : val['/'];
|
|
91
|
-
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
92
|
-
if (matchingOption) {
|
|
93
|
-
return matchingOption;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return undefined;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
if (array && Array.isArray(formValue)) {
|
|
100
|
-
return formValue.map(unknownToRefOption).filter(isNonNullable) ?? [];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const option = unknownToRefOption(formValue);
|
|
104
|
-
if (option) {
|
|
105
|
-
return [option];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return [];
|
|
109
|
-
}, [getValue, availableOptions, array]);
|
|
110
|
-
|
|
111
|
-
const handleUpdate = useCallback(
|
|
112
|
-
(ids: string[]) => {
|
|
113
|
-
if (ids.length === 0) {
|
|
114
|
-
onValueChange('object', undefined);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const refs = ids
|
|
119
|
-
.map((id) => {
|
|
120
|
-
const item = availableOptions.find((option) => option.id === id);
|
|
121
|
-
if (item) {
|
|
122
|
-
const dxn = DXN.parse(item.id);
|
|
123
|
-
return Ref.fromDXN(dxn);
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
})
|
|
127
|
-
.filter(isNonNullable);
|
|
128
|
-
|
|
129
|
-
if (array) {
|
|
130
|
-
onValueChange('object', refs);
|
|
131
|
-
} else {
|
|
132
|
-
onValueChange('object', refs[0]);
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
[availableOptions, array, onValueChange],
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
if (!refTypeInfo) {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const { status, error } = restInputProps.getStatus();
|
|
143
|
-
|
|
144
|
-
const items = handleGetValue();
|
|
145
|
-
const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
|
|
146
|
-
const toggleSelect = useCallback(
|
|
147
|
-
(id: string) => {
|
|
148
|
-
if (array) {
|
|
149
|
-
const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
|
|
150
|
-
handleUpdate(nextIds);
|
|
151
|
-
} else {
|
|
152
|
-
if (selectedIds[0] === id) {
|
|
153
|
-
handleUpdate([]);
|
|
154
|
-
} else {
|
|
155
|
-
handleUpdate([id]);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
},
|
|
159
|
-
[array, selectedIds, handleUpdate],
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
163
|
-
return readonly && items.length < 1 ? null : (
|
|
164
|
-
<Input.Root validationValence={status}>
|
|
165
|
-
{!inputOnly && <InputHeader error={error} label={label} />}
|
|
166
|
-
<div data-no-submit>
|
|
167
|
-
{readonly ? (
|
|
168
|
-
items.length < 1 ? (
|
|
169
|
-
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
170
|
-
) : (
|
|
171
|
-
items.map((item) => (
|
|
172
|
-
<DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
|
|
173
|
-
{item.label}
|
|
174
|
-
</DxAnchor>
|
|
175
|
-
))
|
|
176
|
-
)
|
|
177
|
-
) : (
|
|
178
|
-
<ObjectPicker.Root>
|
|
179
|
-
<ObjectPicker.Trigger asChild>
|
|
180
|
-
<Button variant='ghost' classNames='is-full text-start gap-2'>
|
|
181
|
-
<div role='none' className='grow'>
|
|
182
|
-
{items?.length ? (
|
|
183
|
-
items?.map((item) => (
|
|
184
|
-
<DxTagPickerItem
|
|
185
|
-
key={item.id}
|
|
186
|
-
itemId={item.id}
|
|
187
|
-
label={item.label}
|
|
188
|
-
{...(item.hue ? { hue: item.hue } : {})}
|
|
189
|
-
removeLabel={t('remove item label')}
|
|
190
|
-
onItemClick={(event: DxTagPickerItemClick) => {
|
|
191
|
-
if (event.action === 'remove') {
|
|
192
|
-
toggleSelect(item.id);
|
|
193
|
-
}
|
|
194
|
-
}}
|
|
195
|
-
/>
|
|
196
|
-
))
|
|
197
|
-
) : (
|
|
198
|
-
<span className='text-description'>
|
|
199
|
-
{placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
|
|
200
|
-
</span>
|
|
201
|
-
)}
|
|
202
|
-
</div>
|
|
203
|
-
<Icon size={3} icon='ph--caret-down--bold' />
|
|
204
|
-
</Button>
|
|
205
|
-
</ObjectPicker.Trigger>
|
|
206
|
-
<ObjectPicker.Content
|
|
207
|
-
options={availableOptions}
|
|
208
|
-
selectedIds={selectedIds}
|
|
209
|
-
onSelect={toggleSelect}
|
|
210
|
-
createSchema={createSchema}
|
|
211
|
-
createOptionLabel={createOptionLabel}
|
|
212
|
-
createOptionIcon={createOptionIcon}
|
|
213
|
-
createInitialValuePath={createInitialValuePath}
|
|
214
|
-
onCreate={onCreate}
|
|
215
|
-
/>
|
|
216
|
-
</ObjectPicker.Root>
|
|
217
|
-
)}
|
|
218
|
-
</div>
|
|
219
|
-
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
220
|
-
</Input.Root>
|
|
221
|
-
);
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
const RefFieldFallback = ({
|
|
225
|
-
type,
|
|
226
|
-
label,
|
|
227
|
-
readonly,
|
|
228
|
-
placeholder,
|
|
229
|
-
inputOnly,
|
|
230
|
-
getValue,
|
|
231
|
-
onValueChange,
|
|
232
|
-
...restInputProps
|
|
233
|
-
}: InputProps) => {
|
|
234
|
-
const handleOnValueChange = (_type: any, dxnString: string) => {
|
|
235
|
-
const dxn = DXN.tryParse(dxnString);
|
|
236
|
-
if (dxn) {
|
|
237
|
-
onValueChange?.('object', Ref.fromDXN(dxn));
|
|
238
|
-
} else if (dxnString === '') {
|
|
239
|
-
onValueChange?.('object', undefined);
|
|
240
|
-
} else {
|
|
241
|
-
onValueChange?.('string', dxnString);
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const handleGetValue = () => {
|
|
246
|
-
const formValue = getValue();
|
|
247
|
-
if (typeof formValue === 'string') {
|
|
248
|
-
return formValue;
|
|
249
|
-
}
|
|
250
|
-
if (Ref.isRef(formValue)) {
|
|
251
|
-
return formValue.dxn.toString();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return undefined;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
return (
|
|
258
|
-
<TextInput
|
|
259
|
-
type={type}
|
|
260
|
-
label={label}
|
|
261
|
-
readonly={readonly}
|
|
262
|
-
placeholder={placeholder}
|
|
263
|
-
inputOnly={inputOnly}
|
|
264
|
-
getValue={handleGetValue as <V>() => V | undefined}
|
|
265
|
-
onValueChange={handleOnValueChange}
|
|
266
|
-
{...restInputProps}
|
|
267
|
-
/>
|
|
268
|
-
);
|
|
269
|
-
};
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
|
-
|
|
7
|
-
import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
|
|
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, readonly, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
|
|
15
|
-
const { t } = useTranslation(translationKey);
|
|
16
|
-
const { status, error } = getStatus();
|
|
17
|
-
const geoPoint = useMemo<GeoPoint>(() => getValue<GeoPoint>() ?? [0, 0], [getValue]);
|
|
18
|
-
const geoLocation = useMemo(() => GeoLocation.fromGeoPoint(geoPoint), [geoPoint]);
|
|
19
|
-
|
|
20
|
-
const [longitudeText, setLongitudeText] = useState(geoLocation.longitude?.toString());
|
|
21
|
-
const [latitudeText, setLatitudeText] = useState(geoLocation.latitude?.toString());
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
const location = GeoLocation.fromGeoPoint(geoPoint);
|
|
25
|
-
setLongitudeText(location.longitude.toString());
|
|
26
|
-
setLatitudeText(location.latitude.toString());
|
|
27
|
-
}, [geoPoint]);
|
|
28
|
-
|
|
29
|
-
const handleCoordinateChange = useCallback(
|
|
30
|
-
(coordinateType: keyof Pick<GeoLocation, 'longitude' | 'latitude'>, setText: (text: string) => void) =>
|
|
31
|
-
(event: ChangeEvent<HTMLInputElement>) => {
|
|
32
|
-
const inputText = event.target.value;
|
|
33
|
-
setText(inputText);
|
|
34
|
-
if (inputText !== '' && inputText !== '-') {
|
|
35
|
-
const coord = safeParseFloat(inputText);
|
|
36
|
-
if (coord !== undefined && !isNaN(coord)) {
|
|
37
|
-
const currentLocation = GeoLocation.fromGeoPoint(getValue<GeoPoint>() ?? [0, 0]);
|
|
38
|
-
const newLocation = { ...currentLocation, [coordinateType]: coord };
|
|
39
|
-
const newValue = GeoLocation.toGeoPoint(newLocation);
|
|
40
|
-
onValueChange(type, newValue);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
[getValue, onValueChange, type],
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<Input.Root validationValence={status}>
|
|
49
|
-
<InputHeader error={error} label={label} />
|
|
50
|
-
<div role='none' className='grid grid-cols-2 gap-2'>
|
|
51
|
-
<div role='none'>
|
|
52
|
-
<Input.Label>{t('latitude label')}</Input.Label>
|
|
53
|
-
<Input.TextInput
|
|
54
|
-
type='number'
|
|
55
|
-
step='0.00001'
|
|
56
|
-
min='-90'
|
|
57
|
-
max='90'
|
|
58
|
-
disabled={!!readonly}
|
|
59
|
-
value={latitudeText}
|
|
60
|
-
onChange={handleCoordinateChange('latitude', setLatitudeText)}
|
|
61
|
-
onBlur={onBlur}
|
|
62
|
-
/>
|
|
63
|
-
</div>
|
|
64
|
-
<div role='none'>
|
|
65
|
-
<Input.Label>{t('longitude label')}</Input.Label>
|
|
66
|
-
<Input.TextInput
|
|
67
|
-
type='number'
|
|
68
|
-
step='0.00001'
|
|
69
|
-
min='-180'
|
|
70
|
-
max='180'
|
|
71
|
-
disabled={!!readonly}
|
|
72
|
-
value={longitudeText}
|
|
73
|
-
onChange={handleCoordinateChange('longitude', setLongitudeText)}
|
|
74
|
-
onBlur={onBlur}
|
|
75
|
-
/>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</Input.Root>
|
|
79
|
-
);
|
|
80
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { FormatEnum } from '@dxos/echo/internal';
|
|
6
|
-
import { type SimpleType } from '@dxos/effect';
|
|
7
|
-
|
|
8
|
-
import { GeoPointInput } from './custom';
|
|
9
|
-
import { BooleanInput, MarkdownInput, NumberInput, TextInput } from './Defaults';
|
|
10
|
-
import { type InputComponent } from './Input';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Get property input component.
|
|
14
|
-
*/
|
|
15
|
-
export const getInputComponent = (type: SimpleType, format?: FormatEnum | string): InputComponent | undefined => {
|
|
16
|
-
switch (format) {
|
|
17
|
-
case FormatEnum.GeoPoint:
|
|
18
|
-
return GeoPointInput;
|
|
19
|
-
case FormatEnum.Markdown:
|
|
20
|
-
return MarkdownInput;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
switch (type) {
|
|
24
|
-
case 'string':
|
|
25
|
-
return TextInput;
|
|
26
|
-
case 'number':
|
|
27
|
-
return NumberInput;
|
|
28
|
-
case 'boolean':
|
|
29
|
-
return BooleanInput;
|
|
30
|
-
}
|
|
31
|
-
};
|
package/src/hooks/useForm.ts
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import type * as Schema from 'effect/Schema';
|
|
6
|
-
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
8
|
-
|
|
9
|
-
import { type BaseObject, getValue, setValue } from '@dxos/echo/internal';
|
|
10
|
-
import { type JsonPath, type SimpleType, createJsonPath, fromEffectValidationPath } from '@dxos/effect';
|
|
11
|
-
import { invariant } from '@dxos/invariant';
|
|
12
|
-
import { log } from '@dxos/log';
|
|
13
|
-
import { type ValidationError, validateSchema } from '@dxos/schema';
|
|
14
|
-
import { type MaybePromise } from '@dxos/util';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Return type from `useForm` hook.
|
|
18
|
-
*/
|
|
19
|
-
export type FormHandler<T extends BaseObject> = {
|
|
20
|
-
//
|
|
21
|
-
// Form state management.
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
values: Partial<T>;
|
|
25
|
-
errors: Record<JsonPath, string>;
|
|
26
|
-
touched: Record<JsonPath, boolean>;
|
|
27
|
-
changed: Record<JsonPath, boolean>;
|
|
28
|
-
canSave: boolean;
|
|
29
|
-
formIsValid: boolean;
|
|
30
|
-
|
|
31
|
-
// TODO(burdon): Return onSave.
|
|
32
|
-
handleSave: () => void;
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
// Form input component helpers.
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
getStatus: (path: string | (string | number)[]) => { status?: 'error'; error?: string };
|
|
39
|
-
getValue: <V>(path: (string | number)[]) => V | undefined;
|
|
40
|
-
onValueChange: <V>(path: (string | number)[], type: SimpleType, value: V) => void;
|
|
41
|
-
onTouched: (path: (string | number)[]) => void;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Hook options.
|
|
46
|
-
*/
|
|
47
|
-
export interface FormOptions<T extends BaseObject> {
|
|
48
|
-
/**
|
|
49
|
-
* Effect schema (Type literal).
|
|
50
|
-
*/
|
|
51
|
-
schema: Schema.Schema<T, any>;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Initial values (which may not pass validation).
|
|
55
|
-
*/
|
|
56
|
-
initialValues: Partial<T>;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Callback for value changes. Note: This is called even when values are invalid.
|
|
60
|
-
* Sometimes the parent component may want to know about changes even if the form is
|
|
61
|
-
* in an invalid state.
|
|
62
|
-
*/
|
|
63
|
-
onValuesChanged?: (values: Partial<T>) => void;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Custom validation function that runs only after schema validation passes.
|
|
67
|
-
* Use this for complex validation logic that can't be expressed in the schema.
|
|
68
|
-
* @returns Array of validation errors, or undefined if validation passes
|
|
69
|
-
*/
|
|
70
|
-
// TODO(burdon): Change to key x value?
|
|
71
|
-
onValidate?: (values: T) => ValidationError[] | undefined;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Called when the form is valid.
|
|
75
|
-
*/
|
|
76
|
-
onValid?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => void;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Called when the form is submitted and passes validation.
|
|
80
|
-
*/
|
|
81
|
-
onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => MaybePromise<void>;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Creates a hook for managing form state, including values, validation, and submission.
|
|
86
|
-
* Deeply integrated with `@dxos/schema` for schema-based validation.
|
|
87
|
-
*/
|
|
88
|
-
export const useForm = <T extends BaseObject>({
|
|
89
|
-
schema,
|
|
90
|
-
initialValues,
|
|
91
|
-
onValuesChanged,
|
|
92
|
-
onValidate,
|
|
93
|
-
onValid,
|
|
94
|
-
onSave,
|
|
95
|
-
}: FormOptions<T>): FormHandler<T> => {
|
|
96
|
-
invariant(SchemaAST.isTypeLiteral(schema.ast));
|
|
97
|
-
|
|
98
|
-
const [values, setValues] = useState<Partial<T>>(initialValues);
|
|
99
|
-
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
setValues(initialValues);
|
|
102
|
-
}, [initialValues]);
|
|
103
|
-
|
|
104
|
-
const [touched, setTouched] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
|
|
105
|
-
const [changed, setChanged] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
|
|
106
|
-
const [errors, setErrors] = useState<Record<JsonPath, string>>({});
|
|
107
|
-
const [saving, setSaving] = useState(false);
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
// Validation.
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
const validate = useCallback(
|
|
114
|
-
(values: Partial<T>): values is T => {
|
|
115
|
-
let errors: ValidationError[] = validateSchema(schema, values) ?? [];
|
|
116
|
-
if (errors.length === 0 && onValidate) {
|
|
117
|
-
const validatedValues = values as T;
|
|
118
|
-
errors = onValidate(validatedValues) ?? [];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
setErrors(flatMap(errors));
|
|
122
|
-
const valid = errors.length === 0;
|
|
123
|
-
return valid;
|
|
124
|
-
},
|
|
125
|
-
[schema, onValidate],
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// Validate on schema change.
|
|
129
|
-
const [schemaDirty, setSchemaDirty] = useState(false);
|
|
130
|
-
useEffect(() => setSchemaDirty(true), [schema]);
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
if (schemaDirty) {
|
|
133
|
-
validate(values);
|
|
134
|
-
setSchemaDirty(false);
|
|
135
|
-
}
|
|
136
|
-
}, [values, schemaDirty]);
|
|
137
|
-
|
|
138
|
-
const formIsValid = useMemo(() => {
|
|
139
|
-
return Object.keys(errors).length === 0;
|
|
140
|
-
}, [errors]);
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* NOTE: We can submit if there is no touched field that has an error.
|
|
144
|
-
* Basically, if there's a validation message visible in the form, submit should be disabled.
|
|
145
|
-
*/
|
|
146
|
-
const canSave = useMemo(
|
|
147
|
-
() =>
|
|
148
|
-
!saving &&
|
|
149
|
-
// Check if any error paths that are touched have errors.
|
|
150
|
-
!Object.entries(touched).some(
|
|
151
|
-
([path, isTouched]) =>
|
|
152
|
-
isTouched &&
|
|
153
|
-
Object.keys(errors).some(
|
|
154
|
-
(errorPath) => errorPath === path || errorPath.startsWith(`${path}.`) || errorPath.startsWith(`${path}[`),
|
|
155
|
-
),
|
|
156
|
-
),
|
|
157
|
-
[touched, errors, saving],
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
const handleSave = useCallback(async () => {
|
|
161
|
-
if (validate(values)) {
|
|
162
|
-
setSaving(true);
|
|
163
|
-
try {
|
|
164
|
-
await onSave?.(values, { changed });
|
|
165
|
-
} finally {
|
|
166
|
-
setSaving(false);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}, [values, validate, onSave, changed]);
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
// Fields.
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
const getStatus = useCallback<FormHandler<T>['getStatus']>(
|
|
176
|
-
(path) => {
|
|
177
|
-
const jsonPath = Array.isArray(path) ? createJsonPath(path) : path;
|
|
178
|
-
const matchingError = Object.entries(errors).find(
|
|
179
|
-
([errorPath]) =>
|
|
180
|
-
errorPath === jsonPath || errorPath.startsWith(`${jsonPath}.`) || errorPath.startsWith(`${jsonPath}[`),
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
// Only show errors for touched fields.
|
|
184
|
-
const isTouched = touched[jsonPath as JsonPath];
|
|
185
|
-
if (!isTouched) {
|
|
186
|
-
return {
|
|
187
|
-
status: undefined,
|
|
188
|
-
error: undefined,
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
status: matchingError ? 'error' : undefined,
|
|
194
|
-
error: matchingError ? matchingError[1] : undefined,
|
|
195
|
-
};
|
|
196
|
-
},
|
|
197
|
-
[errors, touched],
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const getFormValue = useCallback<FormHandler<T>['getValue']>(
|
|
201
|
-
<V>(path: (string | number)[]): V | undefined => {
|
|
202
|
-
return getValue(values, createJsonPath(path));
|
|
203
|
-
},
|
|
204
|
-
[values],
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
// TODO(wittjosiah): This is causing the entire form to re-render on every change.
|
|
208
|
-
const onValueChange = useCallback<FormHandler<T>['onValueChange']>(
|
|
209
|
-
(path: (string | number)[], type: SimpleType, value: any) => {
|
|
210
|
-
const jsonPath = createJsonPath(path);
|
|
211
|
-
let parsedValue = value;
|
|
212
|
-
try {
|
|
213
|
-
if (type === 'number') {
|
|
214
|
-
parsedValue = parseFloat(value as string) || 0;
|
|
215
|
-
}
|
|
216
|
-
} catch (err) {
|
|
217
|
-
log.catch(err);
|
|
218
|
-
parsedValue = undefined;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const newValues = { ...setValue(values, jsonPath, parsedValue) };
|
|
222
|
-
setValues(newValues);
|
|
223
|
-
setChanged((prev) => ({ ...prev, [jsonPath]: true }));
|
|
224
|
-
onValuesChanged?.(newValues);
|
|
225
|
-
|
|
226
|
-
const isValid = validate(newValues);
|
|
227
|
-
if (isValid && onValid) {
|
|
228
|
-
onValid(newValues, { changed });
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
[values, onValuesChanged, validate, onValid, changed],
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
const onTouched = useCallback(
|
|
235
|
-
(path: (string | number)[]) => {
|
|
236
|
-
const jsonPath = createJsonPath(path);
|
|
237
|
-
setTouched((touched) => ({ ...touched, [jsonPath]: true }));
|
|
238
|
-
validate(values);
|
|
239
|
-
},
|
|
240
|
-
[validate, values],
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
return useMemo(
|
|
244
|
-
() => ({
|
|
245
|
-
// State.
|
|
246
|
-
values,
|
|
247
|
-
errors,
|
|
248
|
-
touched,
|
|
249
|
-
changed,
|
|
250
|
-
canSave,
|
|
251
|
-
formIsValid,
|
|
252
|
-
|
|
253
|
-
// Actions.
|
|
254
|
-
handleSave,
|
|
255
|
-
|
|
256
|
-
// Field utils.
|
|
257
|
-
getStatus,
|
|
258
|
-
getValue: getFormValue,
|
|
259
|
-
onValueChange,
|
|
260
|
-
onTouched,
|
|
261
|
-
}),
|
|
262
|
-
[
|
|
263
|
-
values,
|
|
264
|
-
errors,
|
|
265
|
-
touched,
|
|
266
|
-
changed,
|
|
267
|
-
canSave,
|
|
268
|
-
formIsValid,
|
|
269
|
-
handleSave,
|
|
270
|
-
getStatus,
|
|
271
|
-
getFormValue,
|
|
272
|
-
onValueChange,
|
|
273
|
-
onTouched,
|
|
274
|
-
],
|
|
275
|
-
);
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<JsonPath, V> => {
|
|
279
|
-
invariant(obj);
|
|
280
|
-
return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const flatMap = (errors: ValidationError[]) => {
|
|
284
|
-
return errors.reduce(
|
|
285
|
-
(result, { path, message }) => {
|
|
286
|
-
// Convert the validation error path format to our JsonPath format.
|
|
287
|
-
const jsonPath = fromEffectValidationPath(path);
|
|
288
|
-
if (!(jsonPath in result)) {
|
|
289
|
-
result[jsonPath] = message;
|
|
290
|
-
}
|
|
291
|
-
return result;
|
|
292
|
-
},
|
|
293
|
-
{} as Record<JsonPath, string>,
|
|
294
|
-
);
|
|
295
|
-
};
|