@dxos/react-ui-form 0.8.4-main.fd6878d → 0.8.4-main.fffef41
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 +806 -572
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +806 -572
- 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 +80 -5
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +1 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +75 -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 +6 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +128 -32
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +16 -10
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +2 -2
- 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 -4
- 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.stories.d.ts +75 -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 +20 -6
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +79 -8
- 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 +2 -2
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +9 -3
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +11 -7
- 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 +10 -14
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +10 -11
- package/src/components/FieldEditor/FieldEditor.tsx +12 -14
- package/src/components/Form/ArrayField.tsx +6 -6
- package/src/components/Form/Defaults.tsx +21 -20
- package/src/components/Form/Form.stories.tsx +132 -97
- package/src/components/Form/Form.tsx +27 -14
- package/src/components/Form/FormActions.tsx +2 -2
- package/src/components/Form/FormContent.tsx +166 -45
- package/src/components/Form/FormContext.tsx +3 -3
- package/src/components/Form/Input.tsx +5 -2
- package/src/components/Form/RefField.tsx +107 -38
- package/src/components/Form/custom/GeoPointInput.tsx +28 -20
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +9 -10
- package/src/components/Form/custom/SelectOptionsInput.tsx +10 -10
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- 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 +123 -49
- package/src/components/ViewEditor/ViewEditor.test.tsx +8 -11
- package/src/components/ViewEditor/ViewEditor.tsx +281 -233
- package/src/components/index.ts +1 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +7 -4
- package/src/hooks/useQueryRefOptions.ts +15 -6
- package/src/hooks/useRefQueryLookupHandler.ts +3 -3
- package/src/translations.ts +13 -8
- package/src/util/field.ts +9 -19
- package/src/util/formatting.ts +1 -1
- package/src/util/refs.ts +7 -7
- 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 -63
- 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,15 +2,17 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
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';
|
|
9
|
+
import React, { Component, type PropsWithChildren, type ReactNode, 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 { type SchemaProperty, getSchemaProperties } from '@dxos/schema';
|
|
13
|
-
import {
|
|
14
|
+
import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
|
|
15
|
+
import { isTruthy } from '@dxos/util';
|
|
14
16
|
|
|
15
17
|
import { type QueryRefOptions } from '../../hooks';
|
|
16
18
|
import { getRefProps } from '../../util';
|
|
@@ -21,32 +23,46 @@ import { getInputComponent } from './factory';
|
|
|
21
23
|
import { type ComponentLookup } from './Form';
|
|
22
24
|
import { useFormValues, useInputProps } from './FormContext';
|
|
23
25
|
import { type InputComponent } from './Input';
|
|
24
|
-
import { RefField } from './RefField';
|
|
26
|
+
import { RefField, type RefFieldProps } from './RefField';
|
|
25
27
|
|
|
26
28
|
export type FormFieldProps = {
|
|
27
29
|
property: SchemaProperty<any>;
|
|
28
30
|
path?: (string | number)[];
|
|
29
|
-
readonly?: boolean;
|
|
30
31
|
/** Used to indicate if input should be presented inline (e.g. for array items). */
|
|
31
32
|
inline?: boolean;
|
|
32
|
-
|
|
33
|
+
projection?: ProjectionModel;
|
|
33
34
|
lookupComponent?: ComponentLookup;
|
|
34
35
|
Custom?: Partial<Record<string, InputComponent>>;
|
|
35
|
-
}
|
|
36
|
+
} & Pick<
|
|
37
|
+
RefFieldProps,
|
|
38
|
+
| 'readonly'
|
|
39
|
+
| 'onQueryRefOptions'
|
|
40
|
+
| 'createOptionLabel'
|
|
41
|
+
| 'createOptionIcon'
|
|
42
|
+
| 'onCreate'
|
|
43
|
+
| 'createSchema'
|
|
44
|
+
| 'createInitialValuePath'
|
|
45
|
+
>;
|
|
36
46
|
|
|
37
47
|
export const FormField = ({
|
|
38
48
|
property,
|
|
39
49
|
path,
|
|
40
50
|
readonly,
|
|
41
51
|
inline,
|
|
52
|
+
projection,
|
|
42
53
|
onQueryRefOptions,
|
|
54
|
+
createOptionLabel,
|
|
55
|
+
createOptionIcon,
|
|
56
|
+
onCreate,
|
|
57
|
+
createSchema,
|
|
58
|
+
createInitialValuePath,
|
|
43
59
|
lookupComponent,
|
|
44
60
|
Custom,
|
|
45
61
|
}: FormFieldProps) => {
|
|
46
62
|
const { ast, name, type, format, title, description, options, examples, array } = property;
|
|
47
63
|
const inputProps = useInputProps(path);
|
|
48
64
|
|
|
49
|
-
const label = useMemo(() => title ?? pipe(name, capitalize), [title, name]);
|
|
65
|
+
const label = useMemo(() => title ?? Function.pipe(name, StringEffect.capitalize), [title, name]);
|
|
50
66
|
const placeholder = useMemo(
|
|
51
67
|
() => (examples?.length ? `Example: "${examples[0]}"` : description),
|
|
52
68
|
[examples, description],
|
|
@@ -63,7 +79,7 @@ export const FormField = ({
|
|
|
63
79
|
type,
|
|
64
80
|
format,
|
|
65
81
|
label,
|
|
66
|
-
|
|
82
|
+
readonly,
|
|
67
83
|
placeholder,
|
|
68
84
|
...inputProps,
|
|
69
85
|
},
|
|
@@ -83,7 +99,7 @@ export const FormField = ({
|
|
|
83
99
|
label={label}
|
|
84
100
|
inputOnly={inline}
|
|
85
101
|
placeholder={placeholder}
|
|
86
|
-
|
|
102
|
+
readonly={readonly}
|
|
87
103
|
{...inputProps}
|
|
88
104
|
/>
|
|
89
105
|
);
|
|
@@ -103,9 +119,14 @@ export const FormField = ({
|
|
|
103
119
|
format={format}
|
|
104
120
|
label={label}
|
|
105
121
|
placeholder={placeholder}
|
|
106
|
-
|
|
122
|
+
readonly={readonly}
|
|
107
123
|
inputOnly={inline}
|
|
108
124
|
onQueryRefOptions={onQueryRefOptions}
|
|
125
|
+
createOptionLabel={createOptionLabel}
|
|
126
|
+
createOptionIcon={createOptionIcon}
|
|
127
|
+
onCreate={onCreate}
|
|
128
|
+
createSchema={createSchema}
|
|
129
|
+
createInitialValuePath={createInitialValuePath}
|
|
109
130
|
{...inputProps}
|
|
110
131
|
/>
|
|
111
132
|
);
|
|
@@ -132,7 +153,7 @@ export const FormField = ({
|
|
|
132
153
|
label={label}
|
|
133
154
|
inputOnly={inline}
|
|
134
155
|
placeholder={placeholder}
|
|
135
|
-
|
|
156
|
+
readonly={readonly}
|
|
136
157
|
{...inputProps}
|
|
137
158
|
/>
|
|
138
159
|
);
|
|
@@ -147,7 +168,7 @@ export const FormField = ({
|
|
|
147
168
|
<SelectInput
|
|
148
169
|
type={type}
|
|
149
170
|
format={format}
|
|
150
|
-
|
|
171
|
+
readonly={readonly}
|
|
151
172
|
inputOnly={inline}
|
|
152
173
|
label={label}
|
|
153
174
|
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
@@ -175,7 +196,11 @@ export const FormField = ({
|
|
|
175
196
|
schema={Schema.make(typeLiteral)}
|
|
176
197
|
path={path}
|
|
177
198
|
readonly={readonly}
|
|
199
|
+
projection={projection}
|
|
178
200
|
onQueryRefOptions={onQueryRefOptions}
|
|
201
|
+
createOptionLabel={createOptionLabel}
|
|
202
|
+
createOptionIcon={createOptionIcon}
|
|
203
|
+
onCreate={onCreate}
|
|
179
204
|
Custom={Custom}
|
|
180
205
|
lookupComponent={lookupComponent}
|
|
181
206
|
/>
|
|
@@ -187,55 +212,151 @@ export const FormField = ({
|
|
|
187
212
|
return null;
|
|
188
213
|
};
|
|
189
214
|
|
|
190
|
-
export type FormFieldsProps = ThemedClassName<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
>;
|
|
208
247
|
|
|
209
248
|
export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
210
249
|
(
|
|
211
|
-
{
|
|
250
|
+
{
|
|
251
|
+
classNames,
|
|
252
|
+
schema,
|
|
253
|
+
path,
|
|
254
|
+
filter,
|
|
255
|
+
sort,
|
|
256
|
+
projection,
|
|
257
|
+
readonly,
|
|
258
|
+
lookupComponent,
|
|
259
|
+
Custom,
|
|
260
|
+
onQueryRefOptions,
|
|
261
|
+
...props
|
|
262
|
+
},
|
|
212
263
|
forwardRef,
|
|
213
264
|
) => {
|
|
214
265
|
const values = useFormValues(path);
|
|
215
266
|
const properties = useMemo(() => {
|
|
216
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
|
|
217
296
|
const filtered = filter ? filter(props) : props;
|
|
218
297
|
return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
|
|
219
|
-
}, [schema, values, filter, sort]);
|
|
298
|
+
}, [schema, values, filter, sort, projection?.fields]);
|
|
220
299
|
|
|
221
300
|
return (
|
|
222
301
|
<div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
|
|
223
302
|
{properties
|
|
224
303
|
.map((property) => {
|
|
225
304
|
return (
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
305
|
+
<ErrorBoundary key={property.name} path={[...(path ?? []), property.name]}>
|
|
306
|
+
<FormField
|
|
307
|
+
property={property}
|
|
308
|
+
path={[...(path ?? []), property.name]}
|
|
309
|
+
readonly={readonly}
|
|
310
|
+
projection={projection}
|
|
311
|
+
onQueryRefOptions={onQueryRefOptions}
|
|
312
|
+
lookupComponent={lookupComponent}
|
|
313
|
+
Custom={Custom}
|
|
314
|
+
{...props}
|
|
315
|
+
/>
|
|
316
|
+
</ErrorBoundary>
|
|
235
317
|
);
|
|
236
318
|
})
|
|
237
|
-
.filter(
|
|
319
|
+
.filter(isTruthy)}
|
|
238
320
|
</div>
|
|
239
321
|
);
|
|
240
322
|
},
|
|
241
323
|
);
|
|
324
|
+
|
|
325
|
+
type ErrorState = {
|
|
326
|
+
error: Error | undefined;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
type ErrorBoundaryProps = PropsWithChildren<{
|
|
330
|
+
path?: (string | number)[];
|
|
331
|
+
}>;
|
|
332
|
+
|
|
333
|
+
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorState> {
|
|
334
|
+
static getDerivedStateFromError(error: Error): { error: Error } {
|
|
335
|
+
return { error };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
override state = { error: undefined };
|
|
339
|
+
|
|
340
|
+
override componentDidUpdate(prevProps: ErrorBoundaryProps): void {
|
|
341
|
+
if (prevProps.path !== this.props.path) {
|
|
342
|
+
this.resetError();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
override render(): ReactNode {
|
|
347
|
+
if (this.state.error) {
|
|
348
|
+
return (
|
|
349
|
+
<div className='flex gap-2 border border-roseFill font-mono text-sm'>
|
|
350
|
+
<span className='bg-roseFill text-surfaceText pli-1 font-thin'>ERROR</span>
|
|
351
|
+
{String(this.props.path?.join('.'))}
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return this.props.children;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private resetError(): void {
|
|
360
|
+
this.setState({ error: undefined });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -14,7 +14,7 @@ import React, {
|
|
|
14
14
|
} from 'react';
|
|
15
15
|
|
|
16
16
|
import { raise } from '@dxos/debug';
|
|
17
|
-
import { type BaseObject, getValue } from '@dxos/echo
|
|
17
|
+
import { type BaseObject, getValue } from '@dxos/echo/internal';
|
|
18
18
|
import { type SimpleType, createJsonPath } from '@dxos/effect';
|
|
19
19
|
|
|
20
20
|
import { type FormHandler, type FormOptions, useForm } from '../../hooks';
|
|
@@ -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,46 +2,67 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
6
|
+
|
|
7
|
+
import type * as Schema from 'effect/Schema';
|
|
8
|
+
import type * as SchemaAST from 'effect/SchemaAST';
|
|
9
|
+
import React, { useCallback, useMemo } from 'react';
|
|
7
10
|
|
|
8
11
|
import {
|
|
12
|
+
type EchoSchema,
|
|
9
13
|
Expando,
|
|
10
14
|
Ref,
|
|
11
15
|
ReferenceAnnotationId,
|
|
12
16
|
type ReferenceAnnotationValue,
|
|
13
17
|
getTypeAnnotation,
|
|
14
|
-
} from '@dxos/echo
|
|
18
|
+
} from '@dxos/echo/internal';
|
|
15
19
|
import { findAnnotation } from '@dxos/effect';
|
|
16
20
|
import { DXN } from '@dxos/keys';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
21
|
+
import { type DxTagPickerItemClick } from '@dxos/lit-ui';
|
|
22
|
+
import { DxAnchor, DxTagPickerItem } from '@dxos/lit-ui/react';
|
|
23
|
+
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
20
24
|
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
21
25
|
import { isNonNullable } from '@dxos/util';
|
|
22
26
|
|
|
23
27
|
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
24
28
|
import { translationKey } from '../../translations';
|
|
29
|
+
import { ObjectPicker } from '../ObjectPicker';
|
|
25
30
|
|
|
26
31
|
import { TextInput } from './Defaults';
|
|
27
32
|
import { InputHeader, type InputProps } from './Input';
|
|
28
33
|
|
|
29
|
-
type RefFieldProps = InputProps & {
|
|
34
|
+
export type RefFieldProps = InputProps & {
|
|
30
35
|
ast?: SchemaAST.AST;
|
|
31
36
|
array?: boolean;
|
|
37
|
+
createOptionLabel?: [string, { ns: string }];
|
|
38
|
+
createOptionIcon?: string;
|
|
39
|
+
onCreate?: (values: any) => void;
|
|
40
|
+
createSchema?: Schema.Schema.AnyNoContext;
|
|
41
|
+
createInitialValuePath?: string;
|
|
32
42
|
onQueryRefOptions?: QueryRefOptions;
|
|
43
|
+
schema?: EchoSchema;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// TODO(thure): Is this a standard that should be better canonized?
|
|
47
|
+
const isRefSnapShot = (val: any): val is { '/': string } => {
|
|
48
|
+
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
33
49
|
};
|
|
34
50
|
|
|
35
51
|
export const RefField = ({
|
|
36
52
|
type,
|
|
37
53
|
label,
|
|
38
|
-
|
|
54
|
+
readonly,
|
|
39
55
|
placeholder,
|
|
40
56
|
inputOnly,
|
|
41
57
|
array,
|
|
42
58
|
ast,
|
|
43
59
|
getValue,
|
|
60
|
+
createOptionLabel,
|
|
61
|
+
createOptionIcon,
|
|
44
62
|
onBlur,
|
|
63
|
+
onCreate,
|
|
64
|
+
createSchema,
|
|
65
|
+
createInitialValuePath,
|
|
45
66
|
onQueryRefOptions,
|
|
46
67
|
onValueChange,
|
|
47
68
|
...restInputProps
|
|
@@ -51,23 +72,28 @@ export const RefField = ({
|
|
|
51
72
|
() => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
|
|
52
73
|
[ast],
|
|
53
74
|
);
|
|
54
|
-
const {
|
|
75
|
+
const {
|
|
76
|
+
options: availableOptions,
|
|
77
|
+
update: updateOptions,
|
|
78
|
+
loading: _loading,
|
|
79
|
+
} = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
|
|
55
80
|
|
|
56
81
|
if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
|
|
57
82
|
// If ref type is expando, fall back to taking a DXN in string format.
|
|
58
83
|
return (
|
|
59
84
|
<RefFieldFallback
|
|
60
|
-
{...{ type, label, placeholder,
|
|
85
|
+
{...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
61
86
|
/>
|
|
62
87
|
);
|
|
63
88
|
}
|
|
64
89
|
|
|
65
|
-
const handleGetValue = useCallback(()
|
|
90
|
+
const handleGetValue = useCallback(() => {
|
|
66
91
|
const formValue = getValue();
|
|
67
92
|
|
|
68
93
|
const unknownToRefOption = (val: unknown) => {
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
const isRef = Ref.isRef(val);
|
|
95
|
+
if (isRef || isRefSnapShot(val)) {
|
|
96
|
+
const dxnString = isRef ? val.dxn.toString() : val['/'];
|
|
71
97
|
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
72
98
|
if (matchingOption) {
|
|
73
99
|
return matchingOption;
|
|
@@ -88,15 +114,6 @@ export const RefField = ({
|
|
|
88
114
|
return [];
|
|
89
115
|
}, [getValue, availableOptions, array]);
|
|
90
116
|
|
|
91
|
-
const handleSearch = useCallback(
|
|
92
|
-
(text: string, ids: string[]): TagPickerItemData[] =>
|
|
93
|
-
availableOptions
|
|
94
|
-
.filter((option) => !ids.includes(option.id))
|
|
95
|
-
.filter((option) => option.label.toLowerCase().includes(text.toLowerCase()))
|
|
96
|
-
.map((option) => ({ id: option.id, label: option.label, hue: option.hue as any })),
|
|
97
|
-
[availableOptions],
|
|
98
|
-
);
|
|
99
|
-
|
|
100
117
|
const handleUpdate = useCallback(
|
|
101
118
|
(ids: string[]) => {
|
|
102
119
|
if (ids.length === 0) {
|
|
@@ -124,7 +141,13 @@ export const RefField = ({
|
|
|
124
141
|
[availableOptions, array, onValueChange],
|
|
125
142
|
);
|
|
126
143
|
|
|
127
|
-
const
|
|
144
|
+
const handleCreate = useCallback(
|
|
145
|
+
(values: any) => {
|
|
146
|
+
onCreate?.(values);
|
|
147
|
+
updateOptions();
|
|
148
|
+
},
|
|
149
|
+
[onCreate, updateOptions],
|
|
150
|
+
);
|
|
128
151
|
|
|
129
152
|
if (!refTypeInfo) {
|
|
130
153
|
return null;
|
|
@@ -133,32 +156,78 @@ export const RefField = ({
|
|
|
133
156
|
const { status, error } = restInputProps.getStatus();
|
|
134
157
|
|
|
135
158
|
const items = handleGetValue();
|
|
159
|
+
const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
|
|
160
|
+
const toggleSelect = useCallback(
|
|
161
|
+
(id: string) => {
|
|
162
|
+
if (array) {
|
|
163
|
+
const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
|
|
164
|
+
handleUpdate(nextIds);
|
|
165
|
+
} else {
|
|
166
|
+
if (selectedIds[0] === id) {
|
|
167
|
+
handleUpdate([]);
|
|
168
|
+
} else {
|
|
169
|
+
handleUpdate([id]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
[array, selectedIds, handleUpdate],
|
|
174
|
+
);
|
|
136
175
|
|
|
137
176
|
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
138
|
-
return
|
|
177
|
+
return readonly && items.length < 1 ? null : (
|
|
139
178
|
<Input.Root validationValence={status}>
|
|
140
179
|
{!inputOnly && <InputHeader error={error} label={label} />}
|
|
141
180
|
<div data-no-submit>
|
|
142
|
-
{
|
|
181
|
+
{readonly ? (
|
|
143
182
|
items.length < 1 ? (
|
|
144
183
|
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
145
184
|
) : (
|
|
146
185
|
items.map((item) => (
|
|
147
|
-
<
|
|
186
|
+
<DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
|
|
148
187
|
{item.label}
|
|
149
|
-
</
|
|
188
|
+
</DxAnchor>
|
|
150
189
|
))
|
|
151
190
|
)
|
|
152
191
|
) : (
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
192
|
+
<ObjectPicker.Root>
|
|
193
|
+
<ObjectPicker.Trigger asChild>
|
|
194
|
+
<Button variant='ghost' classNames='is-full text-start gap-2'>
|
|
195
|
+
<div role='none' className='grow'>
|
|
196
|
+
{items?.length ? (
|
|
197
|
+
items?.map((item) => (
|
|
198
|
+
<DxTagPickerItem
|
|
199
|
+
key={item.id}
|
|
200
|
+
itemId={item.id}
|
|
201
|
+
label={item.label}
|
|
202
|
+
{...(item.hue ? { hue: item.hue } : {})}
|
|
203
|
+
removeLabel={t('remove item label')}
|
|
204
|
+
onItemClick={(event: DxTagPickerItemClick) => {
|
|
205
|
+
if (event.action === 'remove') {
|
|
206
|
+
toggleSelect(item.id);
|
|
207
|
+
}
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
))
|
|
211
|
+
) : (
|
|
212
|
+
<span className='text-description'>
|
|
213
|
+
{placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
|
|
214
|
+
</span>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
<Icon size={3} icon='ph--caret-down--bold' />
|
|
218
|
+
</Button>
|
|
219
|
+
</ObjectPicker.Trigger>
|
|
220
|
+
<ObjectPicker.Content
|
|
221
|
+
options={availableOptions}
|
|
222
|
+
selectedIds={selectedIds}
|
|
223
|
+
onSelect={toggleSelect}
|
|
224
|
+
createSchema={createSchema}
|
|
225
|
+
createOptionLabel={createOptionLabel}
|
|
226
|
+
createOptionIcon={createOptionIcon}
|
|
227
|
+
createInitialValuePath={createInitialValuePath}
|
|
228
|
+
onCreate={handleCreate}
|
|
229
|
+
/>
|
|
230
|
+
</ObjectPicker.Root>
|
|
162
231
|
)}
|
|
163
232
|
</div>
|
|
164
233
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
@@ -169,7 +238,7 @@ export const RefField = ({
|
|
|
169
238
|
const RefFieldFallback = ({
|
|
170
239
|
type,
|
|
171
240
|
label,
|
|
172
|
-
|
|
241
|
+
readonly,
|
|
173
242
|
placeholder,
|
|
174
243
|
inputOnly,
|
|
175
244
|
getValue,
|
|
@@ -203,7 +272,7 @@ const RefFieldFallback = ({
|
|
|
203
272
|
<TextInput
|
|
204
273
|
type={type}
|
|
205
274
|
label={label}
|
|
206
|
-
|
|
275
|
+
readonly={readonly}
|
|
207
276
|
placeholder={placeholder}
|
|
208
277
|
inputOnly={inputOnly}
|
|
209
278
|
getValue={handleGetValue as <V>() => V | undefined}
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { GeoLocation, type GeoPoint } from '@dxos/echo
|
|
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
|
);
|