@dxos/react-ui-form 0.8.4-main.b97322e → 0.8.4-main.c4373fc
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 +810 -594
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +810 -594
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +2 -2
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +81 -5
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +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/FieldEditor/FieldEditor.stories.d.ts +76 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts +2 -2
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +5 -5
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +7 -6
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +129 -32
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +17 -11
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +3 -3
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +5 -2
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +14 -5
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +76 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +41 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +81 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +16 -4
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +79 -7
- 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 +6 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +3 -3
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +8 -3
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +11 -5
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts +1 -1
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +1 -1
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +1 -1
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/src/util/schema.d.ts +1 -1
- package/dist/types/src/util/schema.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -36
- package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +14 -14
- package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
- package/src/components/FieldEditor/FieldEditor.tsx +26 -18
- package/src/components/Form/ArrayField.tsx +12 -11
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +135 -99
- package/src/components/Form/Form.tsx +26 -15
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +121 -39
- package/src/components/Form/FormContext.tsx +8 -8
- package/src/components/Form/Input.tsx +5 -2
- package/src/components/Form/RefField.tsx +97 -41
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +11 -11
- package/src/components/Form/custom/SelectOptionsInput.tsx +30 -31
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +2 -2
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +207 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +161 -0
- package/src/components/ObjectPicker/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +102 -44
- package/src/components/ViewEditor/ViewEditor.test.tsx +9 -12
- package/src/components/ViewEditor/ViewEditor.tsx +328 -220
- package/src/components/index.ts +1 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +8 -4
- package/src/hooks/useQueryRefOptions.ts +9 -3
- package/src/hooks/useRefQueryLookupHandler.ts +1 -1
- package/src/translations.ts +13 -6
- package/src/util/field.ts +9 -19
- package/src/util/formatting.ts +1 -1
- package/src/util/refs.ts +2 -2
- package/src/util/schema.ts +2 -4
- package/dist/types/src/components/deprecated/Form.d.ts +0 -15
- package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/index.d.ts +0 -2
- package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
- package/src/components/deprecated/Form.stories.tsx +0 -62
- package/src/components/deprecated/Form.tsx +0 -119
- package/src/components/deprecated/FormInput.tsx +0 -71
- package/src/components/deprecated/index.ts +0 -5
|
@@ -2,50 +2,67 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import * as Function from 'effect/Function';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
8
|
+
import * as StringEffect from 'effect/String';
|
|
7
9
|
import React, { forwardRef, useMemo } from 'react';
|
|
8
10
|
|
|
9
11
|
import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
10
12
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
13
|
import { mx } from '@dxos/react-ui-theme';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
+
import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
|
|
15
|
+
import { isTruthy } from '@dxos/util';
|
|
16
|
+
|
|
17
|
+
import { type QueryRefOptions } from '../../hooks';
|
|
18
|
+
import { getRefProps } from '../../util';
|
|
14
19
|
|
|
15
20
|
import { ArrayField } from './ArrayField';
|
|
16
21
|
import { SelectInput } from './Defaults';
|
|
22
|
+
import { getInputComponent } from './factory';
|
|
17
23
|
import { type ComponentLookup } from './Form';
|
|
18
|
-
import {
|
|
24
|
+
import { useFormValues, useInputProps } from './FormContext';
|
|
19
25
|
import { type InputComponent } from './Input';
|
|
20
|
-
import { RefField } from './RefField';
|
|
21
|
-
import { getInputComponent } from './factory';
|
|
22
|
-
import { type QueryRefOptions } from '../../hooks';
|
|
23
|
-
import { getRefProps } from '../../util';
|
|
26
|
+
import { RefField, type RefFieldProps } from './RefField';
|
|
24
27
|
|
|
25
28
|
export type FormFieldProps = {
|
|
26
29
|
property: SchemaProperty<any>;
|
|
27
30
|
path?: (string | number)[];
|
|
28
|
-
readonly?: boolean;
|
|
29
31
|
/** Used to indicate if input should be presented inline (e.g. for array items). */
|
|
30
32
|
inline?: boolean;
|
|
31
|
-
|
|
33
|
+
projection?: ProjectionModel;
|
|
32
34
|
lookupComponent?: ComponentLookup;
|
|
33
35
|
Custom?: Partial<Record<string, InputComponent>>;
|
|
34
|
-
}
|
|
36
|
+
} & Pick<
|
|
37
|
+
RefFieldProps,
|
|
38
|
+
| 'readonly'
|
|
39
|
+
| 'onQueryRefOptions'
|
|
40
|
+
| 'createOptionLabel'
|
|
41
|
+
| 'createOptionIcon'
|
|
42
|
+
| 'onCreate'
|
|
43
|
+
| 'createSchema'
|
|
44
|
+
| 'createInitialValuePath'
|
|
45
|
+
>;
|
|
35
46
|
|
|
36
47
|
export const FormField = ({
|
|
37
48
|
property,
|
|
38
49
|
path,
|
|
39
50
|
readonly,
|
|
40
51
|
inline,
|
|
52
|
+
projection,
|
|
41
53
|
onQueryRefOptions,
|
|
54
|
+
createOptionLabel,
|
|
55
|
+
createOptionIcon,
|
|
56
|
+
onCreate,
|
|
57
|
+
createSchema,
|
|
58
|
+
createInitialValuePath,
|
|
42
59
|
lookupComponent,
|
|
43
60
|
Custom,
|
|
44
61
|
}: FormFieldProps) => {
|
|
45
62
|
const { ast, name, type, format, title, description, options, examples, array } = property;
|
|
46
63
|
const inputProps = useInputProps(path);
|
|
47
64
|
|
|
48
|
-
const label = useMemo(() => title ?? pipe(name, capitalize), [title, name]);
|
|
65
|
+
const label = useMemo(() => title ?? Function.pipe(name, StringEffect.capitalize), [title, name]);
|
|
49
66
|
const placeholder = useMemo(
|
|
50
67
|
() => (examples?.length ? `Example: "${examples[0]}"` : description),
|
|
51
68
|
[examples, description],
|
|
@@ -62,7 +79,7 @@ export const FormField = ({
|
|
|
62
79
|
type,
|
|
63
80
|
format,
|
|
64
81
|
label,
|
|
65
|
-
|
|
82
|
+
readonly,
|
|
66
83
|
placeholder,
|
|
67
84
|
...inputProps,
|
|
68
85
|
},
|
|
@@ -82,7 +99,7 @@ export const FormField = ({
|
|
|
82
99
|
label={label}
|
|
83
100
|
inputOnly={inline}
|
|
84
101
|
placeholder={placeholder}
|
|
85
|
-
|
|
102
|
+
readonly={readonly}
|
|
86
103
|
{...inputProps}
|
|
87
104
|
/>
|
|
88
105
|
);
|
|
@@ -102,9 +119,14 @@ export const FormField = ({
|
|
|
102
119
|
format={format}
|
|
103
120
|
label={label}
|
|
104
121
|
placeholder={placeholder}
|
|
105
|
-
|
|
122
|
+
readonly={readonly}
|
|
106
123
|
inputOnly={inline}
|
|
107
124
|
onQueryRefOptions={onQueryRefOptions}
|
|
125
|
+
createOptionLabel={createOptionLabel}
|
|
126
|
+
createOptionIcon={createOptionIcon}
|
|
127
|
+
onCreate={onCreate}
|
|
128
|
+
createSchema={createSchema}
|
|
129
|
+
createInitialValuePath={createInitialValuePath}
|
|
108
130
|
{...inputProps}
|
|
109
131
|
/>
|
|
110
132
|
);
|
|
@@ -131,7 +153,7 @@ export const FormField = ({
|
|
|
131
153
|
label={label}
|
|
132
154
|
inputOnly={inline}
|
|
133
155
|
placeholder={placeholder}
|
|
134
|
-
|
|
156
|
+
readonly={readonly}
|
|
135
157
|
{...inputProps}
|
|
136
158
|
/>
|
|
137
159
|
);
|
|
@@ -146,7 +168,7 @@ export const FormField = ({
|
|
|
146
168
|
<SelectInput
|
|
147
169
|
type={type}
|
|
148
170
|
format={format}
|
|
149
|
-
|
|
171
|
+
readonly={readonly}
|
|
150
172
|
inputOnly={inline}
|
|
151
173
|
label={label}
|
|
152
174
|
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
@@ -174,7 +196,11 @@ export const FormField = ({
|
|
|
174
196
|
schema={Schema.make(typeLiteral)}
|
|
175
197
|
path={path}
|
|
176
198
|
readonly={readonly}
|
|
199
|
+
projection={projection}
|
|
177
200
|
onQueryRefOptions={onQueryRefOptions}
|
|
201
|
+
createOptionLabel={createOptionLabel}
|
|
202
|
+
createOptionIcon={createOptionIcon}
|
|
203
|
+
onCreate={onCreate}
|
|
178
204
|
Custom={Custom}
|
|
179
205
|
lookupComponent={lookupComponent}
|
|
180
206
|
/>
|
|
@@ -186,36 +212,90 @@ export const FormField = ({
|
|
|
186
212
|
return null;
|
|
187
213
|
};
|
|
188
214
|
|
|
189
|
-
export type FormFieldsProps = ThemedClassName<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
export type FormFieldsProps = ThemedClassName<
|
|
216
|
+
{
|
|
217
|
+
testId?: string;
|
|
218
|
+
schema: Schema.Schema.All;
|
|
219
|
+
/**
|
|
220
|
+
* Path to the current object from the root. Used with nested forms.
|
|
221
|
+
*/
|
|
222
|
+
path?: (string | number)[];
|
|
223
|
+
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
224
|
+
sort?: string[];
|
|
225
|
+
/**
|
|
226
|
+
* Optional projection for projection-based field management
|
|
227
|
+
*/
|
|
228
|
+
projection?: ProjectionModel;
|
|
229
|
+
lookupComponent?: ComponentLookup;
|
|
230
|
+
/**
|
|
231
|
+
* Map of custom renderers for specific properties.
|
|
232
|
+
* Prefer lookupComponent for plugin specific input surfaces.
|
|
233
|
+
*/
|
|
234
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
235
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
236
|
+
} & Pick<FormFieldProps, 'readonly'> &
|
|
237
|
+
Pick<
|
|
238
|
+
RefFieldProps,
|
|
239
|
+
| 'onQueryRefOptions'
|
|
240
|
+
| 'createOptionLabel'
|
|
241
|
+
| 'createOptionIcon'
|
|
242
|
+
| 'onCreate'
|
|
243
|
+
| 'createSchema'
|
|
244
|
+
| 'createInitialValuePath'
|
|
245
|
+
>
|
|
246
|
+
>;
|
|
207
247
|
|
|
208
248
|
export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
209
249
|
(
|
|
210
|
-
{
|
|
250
|
+
{
|
|
251
|
+
classNames,
|
|
252
|
+
schema,
|
|
253
|
+
path,
|
|
254
|
+
filter,
|
|
255
|
+
sort,
|
|
256
|
+
projection,
|
|
257
|
+
readonly,
|
|
258
|
+
lookupComponent,
|
|
259
|
+
Custom,
|
|
260
|
+
onQueryRefOptions,
|
|
261
|
+
...props
|
|
262
|
+
},
|
|
211
263
|
forwardRef,
|
|
212
264
|
) => {
|
|
213
265
|
const values = useFormValues(path);
|
|
214
266
|
const properties = useMemo(() => {
|
|
215
267
|
const props = getSchemaProperties(schema.ast, values);
|
|
268
|
+
|
|
269
|
+
// Use projection-based field management when view and projection are available
|
|
270
|
+
if (projection) {
|
|
271
|
+
const fieldProjections = projection.getFieldProjections();
|
|
272
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
273
|
+
|
|
274
|
+
// Filter properties to only include visible ones and order by projection
|
|
275
|
+
const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name));
|
|
276
|
+
const orderedProps: SchemaProperty<any>[] = [];
|
|
277
|
+
|
|
278
|
+
// Add properties in projection field order
|
|
279
|
+
for (const fieldProjection of fieldProjections) {
|
|
280
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
281
|
+
const prop = visibleProps.find((p) => p.name === fieldPath);
|
|
282
|
+
if (prop) {
|
|
283
|
+
orderedProps.push(prop);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Add any remaining properties not in projection
|
|
288
|
+
const projectionPaths = new Set(fieldProjections.map((fp) => String(fp.field.path)));
|
|
289
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name));
|
|
290
|
+
orderedProps.push(...remainingProps);
|
|
291
|
+
|
|
292
|
+
return orderedProps;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Fallback to legacy filter/sort behavior
|
|
216
296
|
const filtered = filter ? filter(props) : props;
|
|
217
297
|
return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
|
|
218
|
-
}, [schema, values, filter, sort]);
|
|
298
|
+
}, [schema, values, filter, sort, projection?.fields]);
|
|
219
299
|
|
|
220
300
|
return (
|
|
221
301
|
<div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
|
|
@@ -227,13 +307,15 @@ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
|
227
307
|
property={property}
|
|
228
308
|
path={[...(path ?? []), property.name]}
|
|
229
309
|
readonly={readonly}
|
|
310
|
+
projection={projection}
|
|
230
311
|
onQueryRefOptions={onQueryRefOptions}
|
|
231
312
|
lookupComponent={lookupComponent}
|
|
232
313
|
Custom={Custom}
|
|
314
|
+
{...props}
|
|
233
315
|
/>
|
|
234
316
|
);
|
|
235
317
|
})
|
|
236
|
-
.filter(
|
|
318
|
+
.filter(isTruthy)}
|
|
237
319
|
</div>
|
|
238
320
|
);
|
|
239
321
|
},
|
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import React, {
|
|
6
|
-
|
|
6
|
+
type FocusEvent,
|
|
7
|
+
type PropsWithChildren,
|
|
7
8
|
type RefObject,
|
|
9
|
+
createContext,
|
|
10
|
+
useCallback,
|
|
8
11
|
useContext,
|
|
9
12
|
useEffect,
|
|
10
13
|
useMemo,
|
|
11
|
-
type FocusEvent,
|
|
12
|
-
type PropsWithChildren,
|
|
13
|
-
useCallback,
|
|
14
14
|
} from 'react';
|
|
15
15
|
|
|
16
16
|
import { raise } from '@dxos/debug';
|
|
17
|
-
import { type BaseObject, getValue } from '@dxos/echo
|
|
18
|
-
import {
|
|
17
|
+
import { type BaseObject, getValue } from '@dxos/echo/internal';
|
|
18
|
+
import { type SimpleType, createJsonPath } from '@dxos/effect';
|
|
19
19
|
|
|
20
20
|
import { type FormHandler, type FormOptions, useForm } from '../../hooks';
|
|
21
21
|
|
|
@@ -62,7 +62,7 @@ export const FormProvider = ({
|
|
|
62
62
|
...formOptions
|
|
63
63
|
}: PropsWithChildren<
|
|
64
64
|
FormOptions<any> & {
|
|
65
|
-
formRef?: RefObject<HTMLDivElement>;
|
|
65
|
+
formRef?: RefObject<HTMLDivElement | null>;
|
|
66
66
|
autoSave?: boolean;
|
|
67
67
|
}
|
|
68
68
|
>) => {
|
|
@@ -92,7 +92,7 @@ export const FormProvider = ({
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
},
|
|
95
|
-
[form, autoSave],
|
|
95
|
+
[form.canSave, form.formIsValid, form.handleSave, autoSave],
|
|
96
96
|
);
|
|
97
97
|
|
|
98
98
|
useEffect(() => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type FormatEnum } from '@dxos/echo
|
|
7
|
+
import { type FormatEnum } from '@dxos/echo/internal';
|
|
8
8
|
import { type SimpleType } from '@dxos/effect';
|
|
9
9
|
import { Icon, Input, Tooltip } from '@dxos/react-ui';
|
|
10
10
|
import { labelSpacing } from '@dxos/react-ui-stack';
|
|
@@ -19,9 +19,12 @@ export type InputProps = {
|
|
|
19
19
|
type: SimpleType;
|
|
20
20
|
format?: FormatEnum;
|
|
21
21
|
label: string;
|
|
22
|
-
disabled?: boolean;
|
|
23
22
|
placeholder?: string;
|
|
24
23
|
inputOnly?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Specifies the readonly variant: either disabled inputs, elements indicating they are usually editable but currently are not; or `static`, a field’s representation as regular content without signifiers that it is ever editable.
|
|
26
|
+
*/
|
|
27
|
+
readonly?: 'disabled-input' | 'static' | false;
|
|
25
28
|
} & FormInputStateProps;
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -2,45 +2,65 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
import type * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
+
import React, { useCallback, useMemo } from 'react';
|
|
7
8
|
|
|
8
9
|
import {
|
|
10
|
+
type EchoSchema,
|
|
9
11
|
Expando,
|
|
10
|
-
getTypeAnnotation,
|
|
11
12
|
Ref,
|
|
12
13
|
ReferenceAnnotationId,
|
|
13
14
|
type ReferenceAnnotationValue,
|
|
14
|
-
|
|
15
|
+
getTypeAnnotation,
|
|
16
|
+
} from '@dxos/echo/internal';
|
|
15
17
|
import { findAnnotation } from '@dxos/effect';
|
|
16
18
|
import { DXN } from '@dxos/keys';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
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';
|
|
20
22
|
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
21
23
|
import { isNonNullable } from '@dxos/util';
|
|
22
24
|
|
|
23
|
-
import { TextInput } from './Defaults';
|
|
24
|
-
import { InputHeader, type InputProps } from './Input';
|
|
25
25
|
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
26
26
|
import { translationKey } from '../../translations';
|
|
27
|
+
import { ObjectPicker } from '../ObjectPicker';
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
import { TextInput } from './Defaults';
|
|
30
|
+
import { InputHeader, type InputProps } from './Input';
|
|
31
|
+
|
|
32
|
+
export type RefFieldProps = InputProps & {
|
|
29
33
|
ast?: SchemaAST.AST;
|
|
30
34
|
array?: boolean;
|
|
35
|
+
createOptionLabel?: [string, { ns: string }];
|
|
36
|
+
createOptionIcon?: string;
|
|
37
|
+
onCreate?: (values: any) => void;
|
|
38
|
+
createSchema?: Schema.Schema.AnyNoContext;
|
|
39
|
+
createInitialValuePath?: string;
|
|
31
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';
|
|
32
47
|
};
|
|
33
48
|
|
|
34
49
|
export const RefField = ({
|
|
35
50
|
type,
|
|
36
51
|
label,
|
|
37
|
-
|
|
52
|
+
readonly,
|
|
38
53
|
placeholder,
|
|
39
54
|
inputOnly,
|
|
40
55
|
array,
|
|
41
56
|
ast,
|
|
42
57
|
getValue,
|
|
58
|
+
createOptionLabel,
|
|
59
|
+
createOptionIcon,
|
|
43
60
|
onBlur,
|
|
61
|
+
onCreate,
|
|
62
|
+
createSchema,
|
|
63
|
+
createInitialValuePath,
|
|
44
64
|
onQueryRefOptions,
|
|
45
65
|
onValueChange,
|
|
46
66
|
...restInputProps
|
|
@@ -56,17 +76,18 @@ export const RefField = ({
|
|
|
56
76
|
// If ref type is expando, fall back to taking a DXN in string format.
|
|
57
77
|
return (
|
|
58
78
|
<RefFieldFallback
|
|
59
|
-
{...{ type, label, placeholder,
|
|
79
|
+
{...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
60
80
|
/>
|
|
61
81
|
);
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
const handleGetValue = useCallback(()
|
|
84
|
+
const handleGetValue = useCallback(() => {
|
|
65
85
|
const formValue = getValue();
|
|
66
86
|
|
|
67
87
|
const unknownToRefOption = (val: unknown) => {
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
const isRef = Ref.isRef(val);
|
|
89
|
+
if (isRef || isRefSnapShot(val)) {
|
|
90
|
+
const dxnString = isRef ? val.dxn.toString() : val['/'];
|
|
70
91
|
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
71
92
|
if (matchingOption) {
|
|
72
93
|
return matchingOption;
|
|
@@ -87,15 +108,6 @@ export const RefField = ({
|
|
|
87
108
|
return [];
|
|
88
109
|
}, [getValue, availableOptions, array]);
|
|
89
110
|
|
|
90
|
-
const handleSearch = useCallback(
|
|
91
|
-
(text: string, ids: string[]): TagPickerItemData[] =>
|
|
92
|
-
availableOptions
|
|
93
|
-
.filter((option) => !ids.includes(option.id))
|
|
94
|
-
.filter((option) => option.label.toLowerCase().includes(text.toLowerCase()))
|
|
95
|
-
.map((option) => ({ id: option.id, label: option.label, hue: option.hue as any })),
|
|
96
|
-
[availableOptions],
|
|
97
|
-
);
|
|
98
|
-
|
|
99
111
|
const handleUpdate = useCallback(
|
|
100
112
|
(ids: string[]) => {
|
|
101
113
|
if (ids.length === 0) {
|
|
@@ -123,8 +135,6 @@ export const RefField = ({
|
|
|
123
135
|
[availableOptions, array, onValueChange],
|
|
124
136
|
);
|
|
125
137
|
|
|
126
|
-
const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
|
|
127
|
-
|
|
128
138
|
if (!refTypeInfo) {
|
|
129
139
|
return null;
|
|
130
140
|
}
|
|
@@ -132,32 +142,78 @@ export const RefField = ({
|
|
|
132
142
|
const { status, error } = restInputProps.getStatus();
|
|
133
143
|
|
|
134
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
|
+
);
|
|
135
161
|
|
|
136
162
|
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
137
|
-
return
|
|
163
|
+
return readonly && items.length < 1 ? null : (
|
|
138
164
|
<Input.Root validationValence={status}>
|
|
139
165
|
{!inputOnly && <InputHeader error={error} label={label} />}
|
|
140
166
|
<div data-no-submit>
|
|
141
|
-
{
|
|
167
|
+
{readonly ? (
|
|
142
168
|
items.length < 1 ? (
|
|
143
169
|
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
144
170
|
) : (
|
|
145
171
|
items.map((item) => (
|
|
146
|
-
<
|
|
172
|
+
<DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
|
|
147
173
|
{item.label}
|
|
148
|
-
</
|
|
174
|
+
</DxAnchor>
|
|
149
175
|
))
|
|
150
176
|
)
|
|
151
177
|
) : (
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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>
|
|
161
217
|
)}
|
|
162
218
|
</div>
|
|
163
219
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
@@ -168,7 +224,7 @@ export const RefField = ({
|
|
|
168
224
|
const RefFieldFallback = ({
|
|
169
225
|
type,
|
|
170
226
|
label,
|
|
171
|
-
|
|
227
|
+
readonly,
|
|
172
228
|
placeholder,
|
|
173
229
|
inputOnly,
|
|
174
230
|
getValue,
|
|
@@ -202,7 +258,7 @@ const RefFieldFallback = ({
|
|
|
202
258
|
<TextInput
|
|
203
259
|
type={type}
|
|
204
260
|
label={label}
|
|
205
|
-
|
|
261
|
+
readonly={readonly}
|
|
206
262
|
placeholder={placeholder}
|
|
207
263
|
inputOnly={inputOnly}
|
|
208
264
|
getValue={handleGetValue as <V>() => V | undefined}
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type GeoPoint
|
|
7
|
+
import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
|
|
8
8
|
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
9
|
import { safeParseFloat } from '@dxos/util';
|
|
10
10
|
|
|
11
11
|
import { translationKey } from '../../../translations';
|
|
12
12
|
import { InputHeader, type InputProps } from '../Input';
|
|
13
13
|
|
|
14
|
-
export const GeoPointInput = ({ type, label,
|
|
14
|
+
export const GeoPointInput = ({ type, label, readonly, getStatus, getValue, onValueChange, onBlur }: InputProps) => {
|
|
15
15
|
const { t } = useTranslation(translationKey);
|
|
16
16
|
const { status, error } = getStatus();
|
|
17
17
|
const geoPoint = useMemo<GeoPoint>(() => getValue<GeoPoint>() ?? [0, 0], [getValue]);
|
|
@@ -48,24 +48,32 @@ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onVa
|
|
|
48
48
|
<Input.Root validationValence={status}>
|
|
49
49
|
<InputHeader error={error} label={label} />
|
|
50
50
|
<div role='none' className='grid grid-cols-2 gap-2'>
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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>
|
|
69
77
|
</div>
|
|
70
78
|
</Input.Root>
|
|
71
79
|
);
|