@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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Option from 'effect/Option';
|
|
6
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
+
import React, { Fragment, useCallback } from 'react';
|
|
8
|
+
|
|
9
|
+
import { findNode, getArrayElementType, getDiscriminatedType, isDiscriminatedUnion, isNestedType } from '@dxos/effect';
|
|
10
|
+
import { IconButton, useTranslation } from '@dxos/react-ui';
|
|
11
|
+
|
|
12
|
+
import { translationKey } from '../../../translations';
|
|
13
|
+
import { getFormProperties } from '../../../util';
|
|
14
|
+
import { useFormValues } from '../Form';
|
|
15
|
+
import { FormField, type FormFieldProps } from '../FormField';
|
|
16
|
+
import { FormFieldLabel, type FormFieldStateProps } from '../FormFieldComponent';
|
|
17
|
+
|
|
18
|
+
export type ArrayFieldProps = {
|
|
19
|
+
label: string;
|
|
20
|
+
fieldProps: FormFieldStateProps;
|
|
21
|
+
} & FormFieldProps;
|
|
22
|
+
|
|
23
|
+
export const ArrayField = ({
|
|
24
|
+
type,
|
|
25
|
+
path,
|
|
26
|
+
label,
|
|
27
|
+
readonly,
|
|
28
|
+
layout,
|
|
29
|
+
fieldProps: inputProps,
|
|
30
|
+
...props
|
|
31
|
+
}: ArrayFieldProps) => {
|
|
32
|
+
const { t } = useTranslation(translationKey);
|
|
33
|
+
const elementType = getArrayElementType(type);
|
|
34
|
+
const { onValueChange } = inputProps;
|
|
35
|
+
const values = useFormValues(ArrayField.displayName, path, () => []);
|
|
36
|
+
|
|
37
|
+
const getDefaultObjectValue = (typeNode: SchemaAST.AST): any => {
|
|
38
|
+
const baseNode = findNode(typeNode, isDiscriminatedUnion);
|
|
39
|
+
const typeLiteral = baseNode ? getDiscriminatedType(baseNode, {}) : findNode(typeNode, SchemaAST.isTypeLiteral);
|
|
40
|
+
if (!typeLiteral) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Object.fromEntries(
|
|
45
|
+
getFormProperties(typeLiteral).map((prop) => {
|
|
46
|
+
const defaultValue = SchemaAST.getDefaultAnnotation(prop.type).pipe((annotation) =>
|
|
47
|
+
Option.getOrUndefined(annotation),
|
|
48
|
+
);
|
|
49
|
+
return [prop.name, defaultValue];
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleAdd = useCallback(() => {
|
|
55
|
+
const defaultValue =
|
|
56
|
+
isNestedType(type) && elementType ? getDefaultObjectValue(elementType) : getDefaultValue(elementType);
|
|
57
|
+
values && onValueChange(type, [...values, defaultValue]);
|
|
58
|
+
}, [onValueChange, type, elementType, values]);
|
|
59
|
+
|
|
60
|
+
const handleDelete = useCallback(
|
|
61
|
+
(idx: number) => {
|
|
62
|
+
onValueChange(
|
|
63
|
+
type,
|
|
64
|
+
values?.filter((_, i) => i !== idx),
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
[onValueChange, type, values],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!elementType || ((readonly || layout === 'static') && values && values.length < 1)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
{(layout !== 'static' || (values && values.length > 0)) && (
|
|
77
|
+
<FormFieldLabel readonly={readonly} label={label} asChild />
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
<div role='none' className='flex flex-col gap-2'>
|
|
81
|
+
{values?.map((_, index) => {
|
|
82
|
+
return (
|
|
83
|
+
<div role='none' key={index} className='grid grid-cols-[1fr_min-content] gap-2 last:mb-3'>
|
|
84
|
+
<FormField
|
|
85
|
+
autoFocus={index === values.length - 1}
|
|
86
|
+
type={elementType}
|
|
87
|
+
path={[...(path ?? []), index]}
|
|
88
|
+
readonly={readonly || layout === 'static'}
|
|
89
|
+
layout='inline'
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{!readonly && layout !== 'static' && (
|
|
94
|
+
<div role='none' className='flex flex-col h-full justify-end'>
|
|
95
|
+
{/* NOTE: Aligns with center of last field if multi-field object. */}
|
|
96
|
+
<div role='none' className='flex items-center h-[2rem]'>
|
|
97
|
+
<IconButton
|
|
98
|
+
icon='ph--x--regular'
|
|
99
|
+
iconOnly
|
|
100
|
+
label={t('button remove')}
|
|
101
|
+
onClick={() => handleDelete(index)}
|
|
102
|
+
classNames='self-center'
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* TODO(burdon): Get label from schema. */}
|
|
113
|
+
{!readonly && layout !== 'static' && (
|
|
114
|
+
<IconButton classNames='flex w-full' icon='ph--plus--regular' label={t('add field')} onClick={handleAdd} />
|
|
115
|
+
)}
|
|
116
|
+
</>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
ArrayField.displayName = 'Form.ArrayField';
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns the default empty value for a given AST.
|
|
124
|
+
* Used for initializing new array values etc.
|
|
125
|
+
*/
|
|
126
|
+
// TODO(wittjosiah): Factor out?
|
|
127
|
+
export const getDefaultValue = (ast?: SchemaAST.AST): any => {
|
|
128
|
+
switch (ast?._tag) {
|
|
129
|
+
case 'StringKeyword': {
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
case 'NumberKeyword': {
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
case 'BooleanKeyword': {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
case 'Suspend': {
|
|
139
|
+
return getDefaultValue(ast.f());
|
|
140
|
+
}
|
|
141
|
+
default: {
|
|
142
|
+
if (ast && isNestedType(ast)) {
|
|
143
|
+
return {};
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error(`Unsupported type: ${ast?._tag}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type SwitchProps } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
|
|
10
|
+
|
|
11
|
+
export const BooleanField = ({ type, readonly, onValueChange, ...props }: FormFieldComponentProps<boolean>) => {
|
|
12
|
+
const handleChange = useCallback<NonNullable<SwitchProps['onCheckedChange']>>(
|
|
13
|
+
(value) => onValueChange?.(type, value),
|
|
14
|
+
[type, onValueChange],
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<FormFieldWrapper<boolean> readonly={readonly} {...props}>
|
|
19
|
+
{({ value }) => <Input.Switch disabled={!!readonly} checked={value} onCheckedChange={handleChange} />}
|
|
20
|
+
</FormFieldWrapper>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
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 { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
13
|
+
|
|
14
|
+
export const GeoPointField = ({
|
|
15
|
+
type,
|
|
16
|
+
label,
|
|
17
|
+
readonly,
|
|
18
|
+
layout,
|
|
19
|
+
getStatus,
|
|
20
|
+
getValue,
|
|
21
|
+
onValueChange,
|
|
22
|
+
onBlur,
|
|
23
|
+
}: FormFieldComponentProps<GeoPoint>) => {
|
|
24
|
+
const { t } = useTranslation(translationKey);
|
|
25
|
+
const { status, error } = getStatus();
|
|
26
|
+
const geoPoint = useMemo<GeoPoint>(() => getValue() ?? [0, 0], [getValue]);
|
|
27
|
+
const value = useMemo(() => GeoLocation.fromGeoPoint(geoPoint), [geoPoint]);
|
|
28
|
+
|
|
29
|
+
const [longitudeText, setLongitudeText] = useState(value.longitude?.toString());
|
|
30
|
+
const [latitudeText, setLatitudeText] = useState(value.latitude?.toString());
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const location = GeoLocation.fromGeoPoint(geoPoint);
|
|
33
|
+
setLongitudeText(location.longitude?.toString());
|
|
34
|
+
setLatitudeText(location.latitude?.toString());
|
|
35
|
+
}, [geoPoint]);
|
|
36
|
+
|
|
37
|
+
const handleChange = useCallback(
|
|
38
|
+
(coordinateType: keyof Pick<GeoLocation, 'longitude' | 'latitude'>, setText: (text: string) => void) =>
|
|
39
|
+
(event: ChangeEvent<HTMLInputElement>) => {
|
|
40
|
+
const inputText = event.target.value;
|
|
41
|
+
setText(inputText);
|
|
42
|
+
if (inputText !== '' && inputText !== '-') {
|
|
43
|
+
const coord = safeParseFloat(inputText);
|
|
44
|
+
if (coord !== undefined && !isNaN(coord)) {
|
|
45
|
+
const currentLocation = GeoLocation.fromGeoPoint(getValue() ?? [0, 0]);
|
|
46
|
+
const newLocation = { ...currentLocation, [coordinateType]: coord };
|
|
47
|
+
const newValue = GeoLocation.toGeoPoint(newLocation);
|
|
48
|
+
onValueChange(type, newValue);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[type, getValue, onValueChange],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if ((readonly || layout === 'static') && !value?.latitude && !value?.longitude) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Input.Root validationValence={status}>
|
|
61
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} asChild />}
|
|
62
|
+
{layout === 'static' ? (
|
|
63
|
+
<LatLng {...value} />
|
|
64
|
+
) : (
|
|
65
|
+
<div role='none' className='grid grid-cols-2 gap-2'>
|
|
66
|
+
<div>
|
|
67
|
+
<Input.Root>
|
|
68
|
+
{layout !== 'inline' && <Input.Label>{t('latitude label')}</Input.Label>}
|
|
69
|
+
<Input.TextInput
|
|
70
|
+
type='number'
|
|
71
|
+
step='0.00001'
|
|
72
|
+
min='-90'
|
|
73
|
+
max='90'
|
|
74
|
+
disabled={!!readonly}
|
|
75
|
+
placeholder={t('latitude placeholder')}
|
|
76
|
+
value={latitudeText ?? ''}
|
|
77
|
+
onChange={handleChange('latitude', setLatitudeText)}
|
|
78
|
+
onBlur={onBlur}
|
|
79
|
+
/>
|
|
80
|
+
</Input.Root>
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<Input.Root>
|
|
84
|
+
{layout !== 'inline' && <Input.Label>{t('longitude label')}</Input.Label>}
|
|
85
|
+
<Input.TextInput
|
|
86
|
+
type='number'
|
|
87
|
+
step='0.00001'
|
|
88
|
+
min='-180'
|
|
89
|
+
max='180'
|
|
90
|
+
disabled={!!readonly}
|
|
91
|
+
placeholder={t('longitude placeholder')}
|
|
92
|
+
value={longitudeText ?? ''}
|
|
93
|
+
onChange={handleChange('longitude', setLongitudeText)}
|
|
94
|
+
onBlur={onBlur}
|
|
95
|
+
/>
|
|
96
|
+
</Input.Root>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</Input.Root>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
|
|
105
|
+
const latHem = latitude >= 0 ? 'N' : 'S';
|
|
106
|
+
const lngHem = longitude >= 0 ? 'E' : 'W';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<span className='inline-flex items-center gap-1'>
|
|
110
|
+
<span>
|
|
111
|
+
<span>{Math.abs(latitude).toFixed(5)}</span>
|
|
112
|
+
<span className='text-subdued'>°{latHem}</span>
|
|
113
|
+
</span>
|
|
114
|
+
<span>
|
|
115
|
+
<span>{Math.abs(longitude).toFixed(5)}</span>
|
|
116
|
+
<span className='text-subdued'>°{lngHem}</span>
|
|
117
|
+
</span>
|
|
118
|
+
</span>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type TextAreaProps } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
10
|
+
|
|
11
|
+
export const MarkdownField = ({
|
|
12
|
+
type,
|
|
13
|
+
readonly,
|
|
14
|
+
label,
|
|
15
|
+
placeholder,
|
|
16
|
+
layout,
|
|
17
|
+
getStatus,
|
|
18
|
+
getValue,
|
|
19
|
+
onValueChange,
|
|
20
|
+
onBlur,
|
|
21
|
+
}: FormFieldComponentProps) => {
|
|
22
|
+
const { status, error } = getStatus();
|
|
23
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
24
|
+
|
|
25
|
+
// TODO(ZaymonFC): If we start using `Enter` for form submission, we should prevent default behavior here
|
|
26
|
+
// since we're using a textarea element and the user needs to be able to enter new lines.
|
|
27
|
+
|
|
28
|
+
const adjustHeight = useCallback(() => {
|
|
29
|
+
const textarea = textareaRef.current;
|
|
30
|
+
if (textarea) {
|
|
31
|
+
textarea.style.blockSize = 'auto'; // For measurement.
|
|
32
|
+
textarea.style.blockSize = `${textarea.scrollHeight + 2}px`;
|
|
33
|
+
}
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
// Adjust height on initial render.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
adjustHeight();
|
|
39
|
+
}, [adjustHeight]);
|
|
40
|
+
|
|
41
|
+
// Adjust height when the content changes.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
adjustHeight();
|
|
44
|
+
}, [getValue(), adjustHeight]);
|
|
45
|
+
|
|
46
|
+
const handleChange = useCallback<NonNullable<TextAreaProps['onChange']>>(
|
|
47
|
+
(event) => onValueChange(type, event.target.value),
|
|
48
|
+
[type, onValueChange],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const value = getValue();
|
|
52
|
+
if ((readonly || layout === 'static') && !value) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// TODO(burdon): Use Markdown Editor.
|
|
57
|
+
return (
|
|
58
|
+
<Input.Root validationValence={status}>
|
|
59
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
60
|
+
{layout === 'static' ? (
|
|
61
|
+
<p>{value}</p>
|
|
62
|
+
) : (
|
|
63
|
+
<Input.TextArea
|
|
64
|
+
ref={textareaRef}
|
|
65
|
+
disabled={!!readonly}
|
|
66
|
+
placeholder={placeholder}
|
|
67
|
+
value={value ?? ''}
|
|
68
|
+
classNames={'min-h-auto max-h-40 overflow-auto'}
|
|
69
|
+
onChange={handleChange}
|
|
70
|
+
onBlur={onBlur}
|
|
71
|
+
style={{ resize: 'none' }}
|
|
72
|
+
spellCheck={false}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
{layout === 'full' && <Input.Validation>{error}</Input.Validation>}
|
|
76
|
+
</Input.Root>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type TextInputProps } from '@dxos/react-ui';
|
|
8
|
+
import { safeParseFloat } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
|
|
11
|
+
|
|
12
|
+
export const NumberField = ({
|
|
13
|
+
type,
|
|
14
|
+
readonly,
|
|
15
|
+
placeholder,
|
|
16
|
+
onValueChange,
|
|
17
|
+
onBlur,
|
|
18
|
+
...props
|
|
19
|
+
}: FormFieldComponentProps<number>) => {
|
|
20
|
+
const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
|
|
21
|
+
(event) => onValueChange(type, safeParseFloat(event.target.value) || 0),
|
|
22
|
+
[type, onValueChange],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<FormFieldWrapper<number> readonly={readonly} {...props}>
|
|
27
|
+
{({ value = '' }) => (
|
|
28
|
+
<Input.TextInput
|
|
29
|
+
type='number'
|
|
30
|
+
disabled={!!readonly}
|
|
31
|
+
placeholder={placeholder}
|
|
32
|
+
value={value}
|
|
33
|
+
onChange={handleChange}
|
|
34
|
+
onBlur={onBlur}
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
</FormFieldWrapper>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
6
|
+
|
|
7
|
+
import React, { useCallback, useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
|
|
10
|
+
import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
|
|
11
|
+
import { useQuery, useSchema as useSchema$ } from '@dxos/echo-react';
|
|
12
|
+
import { findAnnotation } from '@dxos/effect';
|
|
13
|
+
import { DXN } from '@dxos/keys';
|
|
14
|
+
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
15
|
+
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
16
|
+
import { mx } from '@dxos/ui-theme';
|
|
17
|
+
|
|
18
|
+
import { translationKey } from '../../../translations';
|
|
19
|
+
import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
|
|
20
|
+
import { omitId } from '../Form';
|
|
21
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
22
|
+
|
|
23
|
+
// TODO(burdon): Factor out.
|
|
24
|
+
const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
25
|
+
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results) =>
|
|
29
|
+
results.map((result) => {
|
|
30
|
+
const id = Entity.getDXN(result).toString();
|
|
31
|
+
const label = Entity.getLabel(result);
|
|
32
|
+
return { id, label: label ?? id };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const defaultResultsHook: NonNullable<RefFieldProps['resultsHook']> = (db, typename) =>
|
|
36
|
+
useQuery(
|
|
37
|
+
db,
|
|
38
|
+
typename
|
|
39
|
+
? // For Ref.Ref(Obj.Unknown) we want to show all objects.
|
|
40
|
+
typename === Type.getTypename(Obj.Unknown)
|
|
41
|
+
? Filter.everything()
|
|
42
|
+
: Filter.typename(typename)
|
|
43
|
+
: Filter.nothing(),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export type RefFieldProps = FormFieldComponentProps &
|
|
47
|
+
Pick<ObjectPickerContentProps, 'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath'> & {
|
|
48
|
+
db?: Database.Database;
|
|
49
|
+
resultsHook?: (db?: Database.Database, typename?: string) => Entity.Any[];
|
|
50
|
+
schemaHook?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
|
|
51
|
+
getOptions?: (objects: Entity.Any[]) => RefOption[];
|
|
52
|
+
onCreate?: (schema: Type.AnyEntity, values: any) => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const RefField = (props: RefFieldProps) => {
|
|
56
|
+
const {
|
|
57
|
+
type,
|
|
58
|
+
readonly,
|
|
59
|
+
label,
|
|
60
|
+
placeholder,
|
|
61
|
+
layout,
|
|
62
|
+
getStatus,
|
|
63
|
+
getValue,
|
|
64
|
+
createOptionLabel,
|
|
65
|
+
createOptionIcon,
|
|
66
|
+
createInitialValuePath,
|
|
67
|
+
db,
|
|
68
|
+
resultsHook: useResults = defaultResultsHook,
|
|
69
|
+
schemaHook: useSchema = useSchema$,
|
|
70
|
+
getOptions = defaultGetOptions,
|
|
71
|
+
onCreate,
|
|
72
|
+
onValueChange,
|
|
73
|
+
} = props;
|
|
74
|
+
const { t } = useTranslation(translationKey);
|
|
75
|
+
const { status, error } = getStatus();
|
|
76
|
+
|
|
77
|
+
const typename = useMemo(
|
|
78
|
+
() => (type ? findAnnotation<ReferenceAnnotationValue>(type, ReferenceAnnotationId)?.typename : undefined),
|
|
79
|
+
[type],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const results = useResults(db, typename);
|
|
83
|
+
const options = useMemo(() => getOptions(results), [results, getOptions]);
|
|
84
|
+
|
|
85
|
+
const handleGetValue = useCallback(() => {
|
|
86
|
+
const formValue = getValue();
|
|
87
|
+
|
|
88
|
+
const unknownToRefOption = (value: unknown) => {
|
|
89
|
+
const isRef = Ref.isRef(value);
|
|
90
|
+
if (isRef || isRefSnapshot(value)) {
|
|
91
|
+
const dxnString = isRef ? value.dxn.toString() : value['/'];
|
|
92
|
+
const matchingOption = options.find((option) => option.id === dxnString);
|
|
93
|
+
if (matchingOption) {
|
|
94
|
+
return matchingOption;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return unknownToRefOption(formValue);
|
|
101
|
+
}, [options, getValue]);
|
|
102
|
+
|
|
103
|
+
const handleUpdate = useCallback(
|
|
104
|
+
(id: string | undefined) => {
|
|
105
|
+
const item = options.find((option) => option.id === id);
|
|
106
|
+
const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
|
|
107
|
+
onValueChange(type, ref);
|
|
108
|
+
},
|
|
109
|
+
[options, type, onValueChange],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const item = handleGetValue();
|
|
113
|
+
const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
|
|
114
|
+
const createSchema = useSchema(db, typename);
|
|
115
|
+
|
|
116
|
+
const handleCreate = useCallback(
|
|
117
|
+
(values: any) => {
|
|
118
|
+
if (createSchema && onCreate) {
|
|
119
|
+
onCreate(createSchema, values);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
[createSchema, onCreate],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const handleSelect = useCallback(
|
|
126
|
+
(id: string) => {
|
|
127
|
+
if (item?.id === id) {
|
|
128
|
+
handleUpdate(undefined);
|
|
129
|
+
} else {
|
|
130
|
+
handleUpdate(id);
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
[item, handleUpdate],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!typename || ((readonly || layout === 'static') && !item)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Input.Root validationValence={status}>
|
|
142
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
143
|
+
<div>
|
|
144
|
+
{readonly ? (
|
|
145
|
+
!item ? (
|
|
146
|
+
<p className={mx('text-description', 'mb-2')}>{t('empty readonly ref field label')}</p>
|
|
147
|
+
) : (
|
|
148
|
+
<DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
|
|
149
|
+
{item.label}
|
|
150
|
+
</DxAnchor>
|
|
151
|
+
)
|
|
152
|
+
) : (
|
|
153
|
+
<ObjectPicker.Root>
|
|
154
|
+
<ObjectPicker.Trigger asChild classNames='p-0'>
|
|
155
|
+
{item ? (
|
|
156
|
+
<div className='flex gap-2 w-full'>
|
|
157
|
+
<Input.Root key={item.id}>
|
|
158
|
+
<Input.TextInput value={item.label} readOnly classNames='w-full' />
|
|
159
|
+
</Input.Root>
|
|
160
|
+
</div>
|
|
161
|
+
) : (
|
|
162
|
+
<Button classNames='w-full text-start gap-2'>
|
|
163
|
+
<div role='none' className='grow overflow-hidden'>
|
|
164
|
+
<span className='flex truncate text-description'>{placeholder ?? t('ref field placeholder')}</span>
|
|
165
|
+
</div>
|
|
166
|
+
<Icon size={3} icon='ph--caret-down--bold' />
|
|
167
|
+
</Button>
|
|
168
|
+
)}
|
|
169
|
+
</ObjectPicker.Trigger>
|
|
170
|
+
<ObjectPicker.Portal>
|
|
171
|
+
<ObjectPicker.Content
|
|
172
|
+
classNames='dx-card-popover-width'
|
|
173
|
+
options={options}
|
|
174
|
+
selectedIds={selectedIds}
|
|
175
|
+
createSchema={createSchema && omitId(createSchema)}
|
|
176
|
+
createOptionLabel={createOptionLabel}
|
|
177
|
+
createOptionIcon={createOptionIcon}
|
|
178
|
+
createInitialValuePath={createInitialValuePath}
|
|
179
|
+
onCreate={handleCreate}
|
|
180
|
+
onSelect={handleSelect}
|
|
181
|
+
/>
|
|
182
|
+
</ObjectPicker.Portal>
|
|
183
|
+
</ObjectPicker.Root>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
{layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
187
|
+
</Input.Root>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, Select, type SelectRootProps } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
10
|
+
|
|
11
|
+
export type SelectFieldOptions = FormFieldComponentProps & {
|
|
12
|
+
options?: Array<{ value: string | number; label?: string }>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const SelectField = ({
|
|
16
|
+
type,
|
|
17
|
+
readonly,
|
|
18
|
+
label,
|
|
19
|
+
layout,
|
|
20
|
+
placeholder,
|
|
21
|
+
options,
|
|
22
|
+
getStatus,
|
|
23
|
+
getValue,
|
|
24
|
+
onValueChange,
|
|
25
|
+
}: SelectFieldOptions) => {
|
|
26
|
+
const { status, error } = getStatus();
|
|
27
|
+
const value = getValue() as string | undefined;
|
|
28
|
+
|
|
29
|
+
const handleValueChange = useCallback<NonNullable<SelectRootProps['onValueChange']>>(
|
|
30
|
+
(value) => onValueChange(type, value),
|
|
31
|
+
[type, onValueChange],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if ((readonly || layout === 'static') && value == null) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Input.Root validationValence={status}>
|
|
40
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
41
|
+
{layout === 'static' ? (
|
|
42
|
+
<p>{options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)}</p>
|
|
43
|
+
) : (
|
|
44
|
+
<Select.Root value={value} onValueChange={handleValueChange}>
|
|
45
|
+
{/* TODO(burdon): Placeholder not working? */}
|
|
46
|
+
<Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
|
|
47
|
+
<Select.Portal>
|
|
48
|
+
<Select.Content>
|
|
49
|
+
<Select.Viewport>
|
|
50
|
+
{options?.map(({ value, label }) => (
|
|
51
|
+
// NOTE: Numeric values are converted to and from strings.
|
|
52
|
+
<Select.Option key={String(value)} value={String(value)}>
|
|
53
|
+
{label ?? String(value)}
|
|
54
|
+
</Select.Option>
|
|
55
|
+
))}
|
|
56
|
+
</Select.Viewport>
|
|
57
|
+
<Select.Arrow />
|
|
58
|
+
</Select.Content>
|
|
59
|
+
</Select.Portal>
|
|
60
|
+
</Select.Root>
|
|
61
|
+
)}
|
|
62
|
+
{layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
63
|
+
</Input.Root>
|
|
64
|
+
);
|
|
65
|
+
};
|