@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.a4bbb77
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 +710 -553
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +710 -553
- 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 +4 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +78 -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 +5 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +128 -31
- 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 +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 +4 -1
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +13 -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.map +1 -1
- 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 +80 -8
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -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 +1 -1
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +7 -2
- 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.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 +25 -23
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +21 -23
- package/src/components/Form/ArrayField.tsx +9 -9
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +133 -97
- package/src/components/Form/Form.tsx +16 -12
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +116 -36
- package/src/components/Form/FormContext.tsx +6 -6
- package/src/components/Form/Input.tsx +4 -1
- package/src/components/Form/RefField.tsx +190 -40
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +10 -10
- package/src/components/Form/custom/SelectOptionsInput.tsx +29 -30
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +110 -46
- package/src/components/ViewEditor/ViewEditor.test.tsx +17 -18
- package/src/components/ViewEditor/ViewEditor.tsx +329 -235
- package/src/components/index.ts +0 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +6 -3
- package/src/hooks/useQueryRefOptions.ts +8 -2
- package/src/hooks/useRefQueryLookupHandler.ts +1 -1
- package/src/translations.ts +13 -6
- package/src/util/field.ts +8 -18
- 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
|
@@ -9,36 +9,51 @@ import React, { forwardRef, useMemo } from 'react';
|
|
|
9
9
|
import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
|
|
10
10
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
11
|
import { mx } from '@dxos/react-ui-theme';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
|
|
13
|
+
import { isTruthy } from '@dxos/util';
|
|
14
|
+
|
|
15
|
+
import { type QueryRefOptions } from '../../hooks';
|
|
16
|
+
import { getRefProps } from '../../util';
|
|
14
17
|
|
|
15
18
|
import { ArrayField } from './ArrayField';
|
|
16
19
|
import { SelectInput } from './Defaults';
|
|
20
|
+
import { getInputComponent } from './factory';
|
|
17
21
|
import { type ComponentLookup } from './Form';
|
|
18
|
-
import {
|
|
22
|
+
import { useFormValues, useInputProps } from './FormContext';
|
|
19
23
|
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';
|
|
24
|
+
import { RefField, type RefFieldProps } from './RefField';
|
|
24
25
|
|
|
25
26
|
export type FormFieldProps = {
|
|
26
27
|
property: SchemaProperty<any>;
|
|
27
28
|
path?: (string | number)[];
|
|
28
|
-
readonly?: boolean;
|
|
29
29
|
/** Used to indicate if input should be presented inline (e.g. for array items). */
|
|
30
30
|
inline?: boolean;
|
|
31
|
-
|
|
31
|
+
projection?: ProjectionModel;
|
|
32
32
|
lookupComponent?: ComponentLookup;
|
|
33
33
|
Custom?: Partial<Record<string, InputComponent>>;
|
|
34
|
-
}
|
|
34
|
+
} & Pick<
|
|
35
|
+
RefFieldProps,
|
|
36
|
+
| 'readonly'
|
|
37
|
+
| 'onQueryRefOptions'
|
|
38
|
+
| 'createOptionLabel'
|
|
39
|
+
| 'createOptionIcon'
|
|
40
|
+
| 'onCreate'
|
|
41
|
+
| 'createSchema'
|
|
42
|
+
| 'createInitialValuePath'
|
|
43
|
+
>;
|
|
35
44
|
|
|
36
45
|
export const FormField = ({
|
|
37
46
|
property,
|
|
38
47
|
path,
|
|
39
48
|
readonly,
|
|
40
49
|
inline,
|
|
50
|
+
projection,
|
|
41
51
|
onQueryRefOptions,
|
|
52
|
+
createOptionLabel,
|
|
53
|
+
createOptionIcon,
|
|
54
|
+
onCreate,
|
|
55
|
+
createSchema,
|
|
56
|
+
createInitialValuePath,
|
|
42
57
|
lookupComponent,
|
|
43
58
|
Custom,
|
|
44
59
|
}: FormFieldProps) => {
|
|
@@ -62,7 +77,7 @@ export const FormField = ({
|
|
|
62
77
|
type,
|
|
63
78
|
format,
|
|
64
79
|
label,
|
|
65
|
-
|
|
80
|
+
readonly,
|
|
66
81
|
placeholder,
|
|
67
82
|
...inputProps,
|
|
68
83
|
},
|
|
@@ -82,7 +97,7 @@ export const FormField = ({
|
|
|
82
97
|
label={label}
|
|
83
98
|
inputOnly={inline}
|
|
84
99
|
placeholder={placeholder}
|
|
85
|
-
|
|
100
|
+
readonly={readonly}
|
|
86
101
|
{...inputProps}
|
|
87
102
|
/>
|
|
88
103
|
);
|
|
@@ -102,9 +117,14 @@ export const FormField = ({
|
|
|
102
117
|
format={format}
|
|
103
118
|
label={label}
|
|
104
119
|
placeholder={placeholder}
|
|
105
|
-
|
|
120
|
+
readonly={readonly}
|
|
106
121
|
inputOnly={inline}
|
|
107
122
|
onQueryRefOptions={onQueryRefOptions}
|
|
123
|
+
createOptionLabel={createOptionLabel}
|
|
124
|
+
createOptionIcon={createOptionIcon}
|
|
125
|
+
onCreate={onCreate}
|
|
126
|
+
createSchema={createSchema}
|
|
127
|
+
createInitialValuePath={createInitialValuePath}
|
|
108
128
|
{...inputProps}
|
|
109
129
|
/>
|
|
110
130
|
);
|
|
@@ -131,7 +151,7 @@ export const FormField = ({
|
|
|
131
151
|
label={label}
|
|
132
152
|
inputOnly={inline}
|
|
133
153
|
placeholder={placeholder}
|
|
134
|
-
|
|
154
|
+
readonly={readonly}
|
|
135
155
|
{...inputProps}
|
|
136
156
|
/>
|
|
137
157
|
);
|
|
@@ -146,7 +166,7 @@ export const FormField = ({
|
|
|
146
166
|
<SelectInput
|
|
147
167
|
type={type}
|
|
148
168
|
format={format}
|
|
149
|
-
|
|
169
|
+
readonly={readonly}
|
|
150
170
|
inputOnly={inline}
|
|
151
171
|
label={label}
|
|
152
172
|
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
@@ -174,7 +194,11 @@ export const FormField = ({
|
|
|
174
194
|
schema={Schema.make(typeLiteral)}
|
|
175
195
|
path={path}
|
|
176
196
|
readonly={readonly}
|
|
197
|
+
projection={projection}
|
|
177
198
|
onQueryRefOptions={onQueryRefOptions}
|
|
199
|
+
createOptionLabel={createOptionLabel}
|
|
200
|
+
createOptionIcon={createOptionIcon}
|
|
201
|
+
onCreate={onCreate}
|
|
178
202
|
Custom={Custom}
|
|
179
203
|
lookupComponent={lookupComponent}
|
|
180
204
|
/>
|
|
@@ -186,36 +210,90 @@ export const FormField = ({
|
|
|
186
210
|
return null;
|
|
187
211
|
};
|
|
188
212
|
|
|
189
|
-
export type FormFieldsProps = ThemedClassName<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
export type FormFieldsProps = ThemedClassName<
|
|
214
|
+
{
|
|
215
|
+
testId?: string;
|
|
216
|
+
schema: Schema.Schema.All;
|
|
217
|
+
/**
|
|
218
|
+
* Path to the current object from the root. Used with nested forms.
|
|
219
|
+
*/
|
|
220
|
+
path?: (string | number)[];
|
|
221
|
+
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
222
|
+
sort?: string[];
|
|
223
|
+
/**
|
|
224
|
+
* Optional projection for projection-based field management
|
|
225
|
+
*/
|
|
226
|
+
projection?: ProjectionModel;
|
|
227
|
+
lookupComponent?: ComponentLookup;
|
|
228
|
+
/**
|
|
229
|
+
* Map of custom renderers for specific properties.
|
|
230
|
+
* Prefer lookupComponent for plugin specific input surfaces.
|
|
231
|
+
*/
|
|
232
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
233
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
234
|
+
} & Pick<FormFieldProps, 'readonly'> &
|
|
235
|
+
Pick<
|
|
236
|
+
RefFieldProps,
|
|
237
|
+
| 'onQueryRefOptions'
|
|
238
|
+
| 'createOptionLabel'
|
|
239
|
+
| 'createOptionIcon'
|
|
240
|
+
| 'onCreate'
|
|
241
|
+
| 'createSchema'
|
|
242
|
+
| 'createInitialValuePath'
|
|
243
|
+
>
|
|
244
|
+
>;
|
|
207
245
|
|
|
208
246
|
export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
209
247
|
(
|
|
210
|
-
{
|
|
248
|
+
{
|
|
249
|
+
classNames,
|
|
250
|
+
schema,
|
|
251
|
+
path,
|
|
252
|
+
filter,
|
|
253
|
+
sort,
|
|
254
|
+
projection,
|
|
255
|
+
readonly,
|
|
256
|
+
lookupComponent,
|
|
257
|
+
Custom,
|
|
258
|
+
onQueryRefOptions,
|
|
259
|
+
...props
|
|
260
|
+
},
|
|
211
261
|
forwardRef,
|
|
212
262
|
) => {
|
|
213
263
|
const values = useFormValues(path);
|
|
214
264
|
const properties = useMemo(() => {
|
|
215
265
|
const props = getSchemaProperties(schema.ast, values);
|
|
266
|
+
|
|
267
|
+
// Use projection-based field management when view and projection are available
|
|
268
|
+
if (projection) {
|
|
269
|
+
const fieldProjections = projection.getFieldProjections();
|
|
270
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
271
|
+
|
|
272
|
+
// Filter properties to only include visible ones and order by projection
|
|
273
|
+
const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name));
|
|
274
|
+
const orderedProps: SchemaProperty<any>[] = [];
|
|
275
|
+
|
|
276
|
+
// Add properties in projection field order
|
|
277
|
+
for (const fieldProjection of fieldProjections) {
|
|
278
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
279
|
+
const prop = visibleProps.find((p) => p.name === fieldPath);
|
|
280
|
+
if (prop) {
|
|
281
|
+
orderedProps.push(prop);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Add any remaining properties not in projection
|
|
286
|
+
const projectionPaths = new Set(fieldProjections.map((fp) => String(fp.field.path)));
|
|
287
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name));
|
|
288
|
+
orderedProps.push(...remainingProps);
|
|
289
|
+
|
|
290
|
+
return orderedProps;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Fallback to legacy filter/sort behavior
|
|
216
294
|
const filtered = filter ? filter(props) : props;
|
|
217
295
|
return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
|
|
218
|
-
}, [schema, values, filter, sort]);
|
|
296
|
+
}, [schema, values, filter, sort, projection?.fields]);
|
|
219
297
|
|
|
220
298
|
return (
|
|
221
299
|
<div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
|
|
@@ -227,13 +305,15 @@ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
|
227
305
|
property={property}
|
|
228
306
|
path={[...(path ?? []), property.name]}
|
|
229
307
|
readonly={readonly}
|
|
308
|
+
projection={projection}
|
|
230
309
|
onQueryRefOptions={onQueryRefOptions}
|
|
231
310
|
lookupComponent={lookupComponent}
|
|
232
311
|
Custom={Custom}
|
|
312
|
+
{...props}
|
|
233
313
|
/>
|
|
234
314
|
);
|
|
235
315
|
})
|
|
236
|
-
.filter(
|
|
316
|
+
.filter(isTruthy)}
|
|
237
317
|
</div>
|
|
238
318
|
);
|
|
239
319
|
},
|
|
@@ -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
17
|
import { type BaseObject, getValue } from '@dxos/echo-schema';
|
|
18
|
-
import {
|
|
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
|
>) => {
|
|
@@ -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 { type SchemaAST } from 'effect';
|
|
6
|
-
import React, { useCallback, useMemo,
|
|
5
|
+
import { type Schema, type SchemaAST } from 'effect';
|
|
6
|
+
import React, { type KeyboardEvent, type MouseEvent, useCallback, useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
type EchoSchema,
|
|
9
10
|
Expando,
|
|
10
|
-
getTypeAnnotation,
|
|
11
11
|
Ref,
|
|
12
12
|
ReferenceAnnotationId,
|
|
13
13
|
type ReferenceAnnotationValue,
|
|
14
|
+
getTypeAnnotation,
|
|
14
15
|
} from '@dxos/echo-schema';
|
|
15
16
|
import { findAnnotation } from '@dxos/effect';
|
|
16
17
|
import { DXN } from '@dxos/keys';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { type DxTagPickerItemClick } from '@dxos/lit-ui';
|
|
19
|
+
import { DxAnchor, DxTagPickerItem } from '@dxos/lit-ui/react';
|
|
20
|
+
import { Button, Icon, Input, Popover, useTranslation } from '@dxos/react-ui';
|
|
21
|
+
import { PopoverCombobox } from '@dxos/react-ui-searchlist';
|
|
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
27
|
|
|
28
|
-
|
|
28
|
+
import { TextInput } from './Defaults';
|
|
29
|
+
import { Form } from './Form';
|
|
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
|
|
@@ -52,21 +72,70 @@ export const RefField = ({
|
|
|
52
72
|
);
|
|
53
73
|
const { options: availableOptions, loading: _loading } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
|
|
54
74
|
|
|
75
|
+
const [showForm, setShowForm] = useState(false);
|
|
76
|
+
const [searchString, setSearchString] = useState('');
|
|
77
|
+
|
|
78
|
+
const handleFormSave = useCallback(
|
|
79
|
+
(values: any) => {
|
|
80
|
+
onCreate?.(values);
|
|
81
|
+
setShowForm(false);
|
|
82
|
+
setSearchString('');
|
|
83
|
+
},
|
|
84
|
+
[refTypeInfo, onCreate],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const handleFormCancel = useCallback(() => {
|
|
88
|
+
setShowForm(false);
|
|
89
|
+
setSearchString('');
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
// TODO(thure): The following workarounds are necessary because `onSelect` is called after the Popover is already
|
|
93
|
+
// closed. Augment/refactor CmdK, if possible, to facilitate stopping event default & propagation.
|
|
94
|
+
|
|
95
|
+
const handleClick = useCallback(
|
|
96
|
+
(event: MouseEvent) => {
|
|
97
|
+
if (createSchema && (event.target as HTMLElement).closest('[data-value="__create__"]')) {
|
|
98
|
+
event.stopPropagation();
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
setShowForm(true);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[createSchema],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const handleKeyDown = useCallback(
|
|
107
|
+
(event: KeyboardEvent) => {
|
|
108
|
+
if (
|
|
109
|
+
event.key === 'Enter' &&
|
|
110
|
+
createSchema &&
|
|
111
|
+
(event.currentTarget as HTMLElement).querySelector(
|
|
112
|
+
'[role="option"][aria-selected="true"][data-value="__create__"]',
|
|
113
|
+
)
|
|
114
|
+
) {
|
|
115
|
+
event.stopPropagation();
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
setShowForm(true);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
[createSchema],
|
|
121
|
+
);
|
|
122
|
+
|
|
55
123
|
if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
|
|
56
124
|
// If ref type is expando, fall back to taking a DXN in string format.
|
|
57
125
|
return (
|
|
58
126
|
<RefFieldFallback
|
|
59
|
-
{...{ type, label, placeholder,
|
|
127
|
+
{...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
60
128
|
/>
|
|
61
129
|
);
|
|
62
130
|
}
|
|
63
131
|
|
|
64
|
-
const handleGetValue = useCallback(()
|
|
132
|
+
const handleGetValue = useCallback(() => {
|
|
65
133
|
const formValue = getValue();
|
|
66
134
|
|
|
67
135
|
const unknownToRefOption = (val: unknown) => {
|
|
68
|
-
|
|
69
|
-
|
|
136
|
+
const isRef = Ref.isRef(val);
|
|
137
|
+
if (isRef || isRefSnapShot(val)) {
|
|
138
|
+
const dxnString = isRef ? val.dxn.toString() : val['/'];
|
|
70
139
|
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
71
140
|
if (matchingOption) {
|
|
72
141
|
return matchingOption;
|
|
@@ -87,15 +156,6 @@ export const RefField = ({
|
|
|
87
156
|
return [];
|
|
88
157
|
}, [getValue, availableOptions, array]);
|
|
89
158
|
|
|
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
159
|
const handleUpdate = useCallback(
|
|
100
160
|
(ids: string[]) => {
|
|
101
161
|
if (ids.length === 0) {
|
|
@@ -123,8 +183,6 @@ export const RefField = ({
|
|
|
123
183
|
[availableOptions, array, onValueChange],
|
|
124
184
|
);
|
|
125
185
|
|
|
126
|
-
const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
|
|
127
|
-
|
|
128
186
|
if (!refTypeInfo) {
|
|
129
187
|
return null;
|
|
130
188
|
}
|
|
@@ -132,32 +190,124 @@ export const RefField = ({
|
|
|
132
190
|
const { status, error } = restInputProps.getStatus();
|
|
133
191
|
|
|
134
192
|
const items = handleGetValue();
|
|
193
|
+
const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
|
|
194
|
+
const labelById = useMemo(
|
|
195
|
+
() =>
|
|
196
|
+
availableOptions.reduce((acc: Record<string, string>, option) => {
|
|
197
|
+
acc[option.id.toLowerCase()] = option.label.toLowerCase();
|
|
198
|
+
return acc;
|
|
199
|
+
}, {}),
|
|
200
|
+
[availableOptions],
|
|
201
|
+
);
|
|
202
|
+
const toggleSelect = useCallback(
|
|
203
|
+
(id: string) => {
|
|
204
|
+
if (array) {
|
|
205
|
+
const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
|
|
206
|
+
handleUpdate(nextIds);
|
|
207
|
+
} else {
|
|
208
|
+
if (selectedIds[0] === id) {
|
|
209
|
+
handleUpdate([]);
|
|
210
|
+
} else {
|
|
211
|
+
handleUpdate([id]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
[array, selectedIds, handleUpdate],
|
|
216
|
+
);
|
|
135
217
|
|
|
136
218
|
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
137
|
-
return
|
|
219
|
+
return readonly && items.length < 1 ? null : (
|
|
138
220
|
<Input.Root validationValence={status}>
|
|
139
221
|
{!inputOnly && <InputHeader error={error} label={label} />}
|
|
140
222
|
<div data-no-submit>
|
|
141
|
-
{
|
|
223
|
+
{readonly ? (
|
|
142
224
|
items.length < 1 ? (
|
|
143
225
|
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
144
226
|
) : (
|
|
145
227
|
items.map((item) => (
|
|
146
|
-
<
|
|
228
|
+
<DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
|
|
147
229
|
{item.label}
|
|
148
|
-
</
|
|
230
|
+
</DxAnchor>
|
|
149
231
|
))
|
|
150
232
|
)
|
|
151
233
|
) : (
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
234
|
+
<PopoverCombobox.Root>
|
|
235
|
+
<PopoverCombobox.Trigger asChild>
|
|
236
|
+
<Button variant='ghost' classNames='is-full text-start gap-2'>
|
|
237
|
+
<div role='none' className='grow'>
|
|
238
|
+
{items?.length ? (
|
|
239
|
+
items?.map((item) => (
|
|
240
|
+
<DxTagPickerItem
|
|
241
|
+
key={item.id}
|
|
242
|
+
itemId={item.id}
|
|
243
|
+
label={item.label}
|
|
244
|
+
{...(item.hue ? { hue: item.hue } : {})}
|
|
245
|
+
removeLabel={t('remove item label')}
|
|
246
|
+
onItemClick={(event: DxTagPickerItemClick) => {
|
|
247
|
+
if (event.action === 'remove') {
|
|
248
|
+
toggleSelect(item.id);
|
|
249
|
+
}
|
|
250
|
+
}}
|
|
251
|
+
/>
|
|
252
|
+
))
|
|
253
|
+
) : (
|
|
254
|
+
<span className='text-description'>
|
|
255
|
+
{placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
|
|
256
|
+
</span>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
<Icon size={3} icon='ph--caret-down--bold' />
|
|
260
|
+
</Button>
|
|
261
|
+
</PopoverCombobox.Trigger>
|
|
262
|
+
<PopoverCombobox.Content
|
|
263
|
+
filter={(value, search) =>
|
|
264
|
+
value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0
|
|
265
|
+
}
|
|
266
|
+
onClickCapture={handleClick}
|
|
267
|
+
onKeyDownCapture={handleKeyDown}
|
|
268
|
+
>
|
|
269
|
+
{showForm && createSchema ? (
|
|
270
|
+
<Popover.Viewport>
|
|
271
|
+
<Form
|
|
272
|
+
schema={createSchema}
|
|
273
|
+
values={createInitialValuePath ? { [createInitialValuePath]: searchString } : {}}
|
|
274
|
+
onSave={handleFormSave}
|
|
275
|
+
onCancel={handleFormCancel}
|
|
276
|
+
testId='create-referenced-object-form'
|
|
277
|
+
/>
|
|
278
|
+
</Popover.Viewport>
|
|
279
|
+
) : (
|
|
280
|
+
<>
|
|
281
|
+
<PopoverCombobox.Input
|
|
282
|
+
placeholder={t('ref field combobox input placeholder')}
|
|
283
|
+
value={searchString}
|
|
284
|
+
onValueChange={(v) => setSearchString(v)}
|
|
285
|
+
autoFocus
|
|
286
|
+
/>
|
|
287
|
+
<PopoverCombobox.List>
|
|
288
|
+
{availableOptions.map((option) => (
|
|
289
|
+
<PopoverCombobox.Item
|
|
290
|
+
key={option.id}
|
|
291
|
+
value={option.id}
|
|
292
|
+
onSelect={() => toggleSelect(option.id)}
|
|
293
|
+
classNames='flex items-center gap-2'
|
|
294
|
+
>
|
|
295
|
+
<span className='grow'>{option.label}</span>
|
|
296
|
+
{selectedIds.includes(option.id) && <Icon icon='ph--check--regular' />}
|
|
297
|
+
</PopoverCombobox.Item>
|
|
298
|
+
))}
|
|
299
|
+
{searchString.length > 0 && createOptionLabel && createOptionIcon && createSchema && (
|
|
300
|
+
<PopoverCombobox.Item value='__create__' classNames='flex items-center gap-2'>
|
|
301
|
+
<Icon icon={createOptionIcon} />
|
|
302
|
+
{t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: searchString })}
|
|
303
|
+
</PopoverCombobox.Item>
|
|
304
|
+
)}
|
|
305
|
+
</PopoverCombobox.List>
|
|
306
|
+
</>
|
|
307
|
+
)}
|
|
308
|
+
<PopoverCombobox.Arrow />
|
|
309
|
+
</PopoverCombobox.Content>
|
|
310
|
+
</PopoverCombobox.Root>
|
|
161
311
|
)}
|
|
162
312
|
</div>
|
|
163
313
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
@@ -168,7 +318,7 @@ export const RefField = ({
|
|
|
168
318
|
const RefFieldFallback = ({
|
|
169
319
|
type,
|
|
170
320
|
label,
|
|
171
|
-
|
|
321
|
+
readonly,
|
|
172
322
|
placeholder,
|
|
173
323
|
inputOnly,
|
|
174
324
|
getValue,
|
|
@@ -202,7 +352,7 @@ const RefFieldFallback = ({
|
|
|
202
352
|
<TextInput
|
|
203
353
|
type={type}
|
|
204
354
|
label={label}
|
|
205
|
-
|
|
355
|
+
readonly={readonly}
|
|
206
356
|
placeholder={placeholder}
|
|
207
357
|
inputOnly={inputOnly}
|
|
208
358
|
getValue={handleGetValue as <V>() => V | undefined}
|