@dxos/react-ui-form 0.8.4-main.f9ba587 → 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 +884 -637
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +884 -637
- 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 +4 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +77 -7
- 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 +8 -6
- 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/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 +15 -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 +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 +80 -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 +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 +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 +13 -8
- 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 +42 -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 +26 -24
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +21 -23
- package/src/components/Form/ArrayField.tsx +13 -12
- package/src/components/Form/Defaults.tsx +23 -21
- package/src/components/Form/Form.stories.tsx +135 -99
- package/src/components/Form/Form.tsx +30 -16
- package/src/components/Form/FormActions.tsx +4 -3
- package/src/components/Form/FormContent.tsx +171 -49
- package/src/components/Form/FormContext.tsx +8 -8
- package/src/components/Form/Input.tsx +5 -2
- package/src/components/Form/RefField.tsx +111 -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 +31 -32
- 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 +130 -49
- package/src/components/ViewEditor/ViewEditor.test.tsx +19 -19
- 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 +11 -6
- package/src/hooks/useQueryRefOptions.ts +15 -6
- package/src/hooks/useRefQueryLookupHandler.ts +3 -3
- package/src/translations.ts +15 -9
- 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 -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
|
|
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 {
|
|
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,55 +212,151 @@ 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}>
|
|
222
302
|
{properties
|
|
223
303
|
.map((property) => {
|
|
224
304
|
return (
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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>
|
|
234
317
|
);
|
|
235
318
|
})
|
|
236
|
-
.filter(
|
|
319
|
+
.filter(isTruthy)}
|
|
237
320
|
</div>
|
|
238
321
|
);
|
|
239
322
|
},
|
|
240
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
|
+
}
|
|
@@ -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,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
|
-
getTypeAnnotation,
|
|
11
14
|
Ref,
|
|
12
15
|
ReferenceAnnotationId,
|
|
13
16
|
type ReferenceAnnotationValue,
|
|
14
|
-
|
|
17
|
+
getTypeAnnotation,
|
|
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
|
-
import { TextInput } from './Defaults';
|
|
24
|
-
import { InputHeader, type InputProps } from './Input';
|
|
25
27
|
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
26
28
|
import { translationKey } from '../../translations';
|
|
29
|
+
import { ObjectPicker } from '../ObjectPicker';
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
import { TextInput } from './Defaults';
|
|
32
|
+
import { InputHeader, type InputProps } from './Input';
|
|
33
|
+
|
|
34
|
+
export type RefFieldProps = InputProps & {
|
|
29
35
|
ast?: SchemaAST.AST;
|
|
30
36
|
array?: boolean;
|
|
37
|
+
createOptionLabel?: [string, { ns: string }];
|
|
38
|
+
createOptionIcon?: string;
|
|
39
|
+
onCreate?: (values: any) => void;
|
|
40
|
+
createSchema?: Schema.Schema.AnyNoContext;
|
|
41
|
+
createInitialValuePath?: string;
|
|
31
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';
|
|
32
49
|
};
|
|
33
50
|
|
|
34
51
|
export const RefField = ({
|
|
35
52
|
type,
|
|
36
53
|
label,
|
|
37
|
-
|
|
54
|
+
readonly,
|
|
38
55
|
placeholder,
|
|
39
56
|
inputOnly,
|
|
40
57
|
array,
|
|
41
58
|
ast,
|
|
42
59
|
getValue,
|
|
60
|
+
createOptionLabel,
|
|
61
|
+
createOptionIcon,
|
|
43
62
|
onBlur,
|
|
63
|
+
onCreate,
|
|
64
|
+
createSchema,
|
|
65
|
+
createInitialValuePath,
|
|
44
66
|
onQueryRefOptions,
|
|
45
67
|
onValueChange,
|
|
46
68
|
...restInputProps
|
|
@@ -50,23 +72,28 @@ export const RefField = ({
|
|
|
50
72
|
() => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
|
|
51
73
|
[ast],
|
|
52
74
|
);
|
|
53
|
-
const {
|
|
75
|
+
const {
|
|
76
|
+
options: availableOptions,
|
|
77
|
+
update: updateOptions,
|
|
78
|
+
loading: _loading,
|
|
79
|
+
} = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
|
|
54
80
|
|
|
55
81
|
if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
|
|
56
82
|
// If ref type is expando, fall back to taking a DXN in string format.
|
|
57
83
|
return (
|
|
58
84
|
<RefFieldFallback
|
|
59
|
-
{...{ type, label, placeholder,
|
|
85
|
+
{...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
60
86
|
/>
|
|
61
87
|
);
|
|
62
88
|
}
|
|
63
89
|
|
|
64
|
-
const handleGetValue = useCallback(()
|
|
90
|
+
const handleGetValue = useCallback(() => {
|
|
65
91
|
const formValue = getValue();
|
|
66
92
|
|
|
67
93
|
const unknownToRefOption = (val: unknown) => {
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
const isRef = Ref.isRef(val);
|
|
95
|
+
if (isRef || isRefSnapShot(val)) {
|
|
96
|
+
const dxnString = isRef ? val.dxn.toString() : val['/'];
|
|
70
97
|
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
71
98
|
if (matchingOption) {
|
|
72
99
|
return matchingOption;
|
|
@@ -87,15 +114,6 @@ export const RefField = ({
|
|
|
87
114
|
return [];
|
|
88
115
|
}, [getValue, availableOptions, array]);
|
|
89
116
|
|
|
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
117
|
const handleUpdate = useCallback(
|
|
100
118
|
(ids: string[]) => {
|
|
101
119
|
if (ids.length === 0) {
|
|
@@ -123,7 +141,13 @@ export const RefField = ({
|
|
|
123
141
|
[availableOptions, array, onValueChange],
|
|
124
142
|
);
|
|
125
143
|
|
|
126
|
-
const
|
|
144
|
+
const handleCreate = useCallback(
|
|
145
|
+
(values: any) => {
|
|
146
|
+
onCreate?.(values);
|
|
147
|
+
updateOptions();
|
|
148
|
+
},
|
|
149
|
+
[onCreate, updateOptions],
|
|
150
|
+
);
|
|
127
151
|
|
|
128
152
|
if (!refTypeInfo) {
|
|
129
153
|
return null;
|
|
@@ -132,32 +156,78 @@ export const RefField = ({
|
|
|
132
156
|
const { status, error } = restInputProps.getStatus();
|
|
133
157
|
|
|
134
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
|
+
);
|
|
135
175
|
|
|
136
176
|
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
137
|
-
return
|
|
177
|
+
return readonly && items.length < 1 ? null : (
|
|
138
178
|
<Input.Root validationValence={status}>
|
|
139
179
|
{!inputOnly && <InputHeader error={error} label={label} />}
|
|
140
180
|
<div data-no-submit>
|
|
141
|
-
{
|
|
181
|
+
{readonly ? (
|
|
142
182
|
items.length < 1 ? (
|
|
143
183
|
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
144
184
|
) : (
|
|
145
185
|
items.map((item) => (
|
|
146
|
-
<
|
|
186
|
+
<DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
|
|
147
187
|
{item.label}
|
|
148
|
-
</
|
|
188
|
+
</DxAnchor>
|
|
149
189
|
))
|
|
150
190
|
)
|
|
151
191
|
) : (
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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>
|
|
161
231
|
)}
|
|
162
232
|
</div>
|
|
163
233
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
@@ -168,7 +238,7 @@ export const RefField = ({
|
|
|
168
238
|
const RefFieldFallback = ({
|
|
169
239
|
type,
|
|
170
240
|
label,
|
|
171
|
-
|
|
241
|
+
readonly,
|
|
172
242
|
placeholder,
|
|
173
243
|
inputOnly,
|
|
174
244
|
getValue,
|
|
@@ -202,7 +272,7 @@ const RefFieldFallback = ({
|
|
|
202
272
|
<TextInput
|
|
203
273
|
type={type}
|
|
204
274
|
label={label}
|
|
205
|
-
|
|
275
|
+
readonly={readonly}
|
|
206
276
|
placeholder={placeholder}
|
|
207
277
|
inputOnly={inputOnly}
|
|
208
278
|
getValue={handleGetValue as <V>() => V | undefined}
|