@dxos/react-ui-form 0.8.3 → 0.8.4-main.1f223c7
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 +648 -581
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +648 -581
- 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 +79 -4
- 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 +76 -6
- 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 +126 -30
- 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 +1 -1
- 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 +10 -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.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +74 -3
- 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 +5 -4
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +78 -7
- 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/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +61 -57
- 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 +36 -33
- package/src/components/ControlSection/ControlSection.stories.tsx +10 -12
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +21 -20
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +17 -22
- package/src/components/Form/ArrayField.tsx +9 -9
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +131 -95
- package/src/components/Form/Form.tsx +16 -12
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +96 -34
- package/src/components/Form/FormContext.tsx +5 -5
- package/src/components/Form/Input.tsx +4 -1
- package/src/components/Form/RefField.tsx +126 -40
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +9 -8
- 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 +70 -32
- package/src/components/ViewEditor/ViewEditor.test.tsx +38 -39
- package/src/components/ViewEditor/ViewEditor.tsx +159 -155
- package/src/components/index.ts +0 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +5 -3
- package/src/hooks/useRefQueryLookupHandler.ts +1 -1
- package/src/index.ts +1 -1
- package/src/translations.ts +15 -7
- package/src/util/field.ts +8 -18
- package/dist/lib/node/index.cjs +0 -2416
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/types/src/components/ViewEditor/Test.stories.d.ts +0 -10
- package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +0 -1
- 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/ViewEditor/Test.stories.tsx +0 -45
- 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,43 @@ 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 {
|
|
12
|
+
import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
|
|
13
13
|
import { isNotFalsy } from '@dxos/util';
|
|
14
14
|
|
|
15
|
+
import { type QueryRefOptions } from '../../hooks';
|
|
16
|
+
import { getRefProps } from '../../util';
|
|
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' | 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreateFromQuery'
|
|
37
|
+
>;
|
|
35
38
|
|
|
36
39
|
export const FormField = ({
|
|
37
40
|
property,
|
|
38
41
|
path,
|
|
39
42
|
readonly,
|
|
40
43
|
inline,
|
|
44
|
+
projection,
|
|
41
45
|
onQueryRefOptions,
|
|
46
|
+
createOptionLabel,
|
|
47
|
+
createOptionIcon,
|
|
48
|
+
onCreateFromQuery,
|
|
42
49
|
lookupComponent,
|
|
43
50
|
Custom,
|
|
44
51
|
}: FormFieldProps) => {
|
|
@@ -62,7 +69,7 @@ export const FormField = ({
|
|
|
62
69
|
type,
|
|
63
70
|
format,
|
|
64
71
|
label,
|
|
65
|
-
|
|
72
|
+
readonly,
|
|
66
73
|
placeholder,
|
|
67
74
|
...inputProps,
|
|
68
75
|
},
|
|
@@ -82,7 +89,7 @@ export const FormField = ({
|
|
|
82
89
|
label={label}
|
|
83
90
|
inputOnly={inline}
|
|
84
91
|
placeholder={placeholder}
|
|
85
|
-
|
|
92
|
+
readonly={readonly}
|
|
86
93
|
{...inputProps}
|
|
87
94
|
/>
|
|
88
95
|
);
|
|
@@ -102,9 +109,12 @@ export const FormField = ({
|
|
|
102
109
|
format={format}
|
|
103
110
|
label={label}
|
|
104
111
|
placeholder={placeholder}
|
|
105
|
-
|
|
112
|
+
readonly={readonly}
|
|
106
113
|
inputOnly={inline}
|
|
107
114
|
onQueryRefOptions={onQueryRefOptions}
|
|
115
|
+
createOptionLabel={createOptionLabel}
|
|
116
|
+
createOptionIcon={createOptionIcon}
|
|
117
|
+
onCreateFromQuery={onCreateFromQuery}
|
|
108
118
|
{...inputProps}
|
|
109
119
|
/>
|
|
110
120
|
);
|
|
@@ -131,7 +141,7 @@ export const FormField = ({
|
|
|
131
141
|
label={label}
|
|
132
142
|
inputOnly={inline}
|
|
133
143
|
placeholder={placeholder}
|
|
134
|
-
|
|
144
|
+
readonly={readonly}
|
|
135
145
|
{...inputProps}
|
|
136
146
|
/>
|
|
137
147
|
);
|
|
@@ -146,7 +156,7 @@ export const FormField = ({
|
|
|
146
156
|
<SelectInput
|
|
147
157
|
type={type}
|
|
148
158
|
format={format}
|
|
149
|
-
|
|
159
|
+
readonly={readonly}
|
|
150
160
|
inputOnly={inline}
|
|
151
161
|
label={label}
|
|
152
162
|
options={options.map((option) => ({ value: option, label: String(option) }))}
|
|
@@ -174,7 +184,11 @@ export const FormField = ({
|
|
|
174
184
|
schema={Schema.make(typeLiteral)}
|
|
175
185
|
path={path}
|
|
176
186
|
readonly={readonly}
|
|
187
|
+
projection={projection}
|
|
177
188
|
onQueryRefOptions={onQueryRefOptions}
|
|
189
|
+
createOptionLabel={createOptionLabel}
|
|
190
|
+
createOptionIcon={createOptionIcon}
|
|
191
|
+
onCreateFromQuery={onCreateFromQuery}
|
|
178
192
|
Custom={Custom}
|
|
179
193
|
lookupComponent={lookupComponent}
|
|
180
194
|
/>
|
|
@@ -186,36 +200,82 @@ export const FormField = ({
|
|
|
186
200
|
return null;
|
|
187
201
|
};
|
|
188
202
|
|
|
189
|
-
export type FormFieldsProps = ThemedClassName<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
export type FormFieldsProps = ThemedClassName<
|
|
204
|
+
{
|
|
205
|
+
testId?: string;
|
|
206
|
+
schema: Schema.Schema.All;
|
|
207
|
+
/**
|
|
208
|
+
* Path to the current object from the root. Used with nested forms.
|
|
209
|
+
*/
|
|
210
|
+
path?: (string | number)[];
|
|
211
|
+
filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
|
|
212
|
+
sort?: string[];
|
|
213
|
+
/**
|
|
214
|
+
* Optional projection for projection-based field management
|
|
215
|
+
*/
|
|
216
|
+
projection?: ProjectionModel;
|
|
217
|
+
lookupComponent?: ComponentLookup;
|
|
218
|
+
/**
|
|
219
|
+
* Map of custom renderers for specific properties.
|
|
220
|
+
* Prefer lookupComponent for plugin specific input surfaces.
|
|
221
|
+
*/
|
|
222
|
+
Custom?: Partial<Record<string, InputComponent>>;
|
|
223
|
+
onQueryRefOptions?: QueryRefOptions;
|
|
224
|
+
} & Pick<FormFieldProps, 'readonly'> &
|
|
225
|
+
Pick<RefFieldProps, 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreateFromQuery'>
|
|
226
|
+
>;
|
|
207
227
|
|
|
208
228
|
export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
209
229
|
(
|
|
210
|
-
{
|
|
230
|
+
{
|
|
231
|
+
classNames,
|
|
232
|
+
schema,
|
|
233
|
+
path,
|
|
234
|
+
filter,
|
|
235
|
+
sort,
|
|
236
|
+
projection,
|
|
237
|
+
readonly,
|
|
238
|
+
lookupComponent,
|
|
239
|
+
Custom,
|
|
240
|
+
onQueryRefOptions,
|
|
241
|
+
...props
|
|
242
|
+
},
|
|
211
243
|
forwardRef,
|
|
212
244
|
) => {
|
|
213
245
|
const values = useFormValues(path);
|
|
214
246
|
const properties = useMemo(() => {
|
|
215
247
|
const props = getSchemaProperties(schema.ast, values);
|
|
248
|
+
|
|
249
|
+
// Use projection-based field management when view and projection are available
|
|
250
|
+
if (projection) {
|
|
251
|
+
const fieldProjections = projection.getFieldProjections();
|
|
252
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
253
|
+
|
|
254
|
+
// Filter properties to only include visible ones and order by projection
|
|
255
|
+
const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name));
|
|
256
|
+
const orderedProps: SchemaProperty<any>[] = [];
|
|
257
|
+
|
|
258
|
+
// Add properties in projection field order
|
|
259
|
+
for (const fieldProjection of fieldProjections) {
|
|
260
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
261
|
+
const prop = visibleProps.find((p) => p.name === fieldPath);
|
|
262
|
+
if (prop) {
|
|
263
|
+
orderedProps.push(prop);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Add any remaining properties not in projection
|
|
268
|
+
const projectionPaths = new Set(fieldProjections.map((fp) => String(fp.field.path)));
|
|
269
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name));
|
|
270
|
+
orderedProps.push(...remainingProps);
|
|
271
|
+
|
|
272
|
+
return orderedProps;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Fallback to legacy filter/sort behavior
|
|
216
276
|
const filtered = filter ? filter(props) : props;
|
|
217
277
|
return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
|
|
218
|
-
}, [schema, values, filter, sort]);
|
|
278
|
+
}, [schema, values, filter, sort, projection?.fields]);
|
|
219
279
|
|
|
220
280
|
return (
|
|
221
281
|
<div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
|
|
@@ -227,9 +287,11 @@ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
|
|
|
227
287
|
property={property}
|
|
228
288
|
path={[...(path ?? []), property.name]}
|
|
229
289
|
readonly={readonly}
|
|
290
|
+
projection={projection}
|
|
230
291
|
onQueryRefOptions={onQueryRefOptions}
|
|
231
292
|
lookupComponent={lookupComponent}
|
|
232
293
|
Custom={Custom}
|
|
294
|
+
{...props}
|
|
233
295
|
/>
|
|
234
296
|
);
|
|
235
297
|
})
|
|
@@ -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
|
|
|
@@ -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
|
/**
|
|
@@ -3,38 +3,50 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type SchemaAST } from 'effect';
|
|
6
|
-
import React, { useCallback, useMemo,
|
|
6
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
Expando,
|
|
10
|
-
getTypeAnnotation,
|
|
11
10
|
Ref,
|
|
12
11
|
ReferenceAnnotationId,
|
|
13
12
|
type ReferenceAnnotationValue,
|
|
13
|
+
type TypeAnnotation,
|
|
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 } from '@dxos/lit-ui/react';
|
|
20
|
+
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
21
|
+
import { PopoverCombobox } from '@dxos/react-ui-searchlist';
|
|
22
|
+
import { TagPickerItem } from '@dxos/react-ui-tag-picker';
|
|
20
23
|
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
21
|
-
import { isNonNullable } from '@dxos/util';
|
|
24
|
+
import { type MaybePromise, isNonNullable } from '@dxos/util';
|
|
22
25
|
|
|
23
|
-
import { TextInput } from './Defaults';
|
|
24
|
-
import { InputHeader, type InputProps } from './Input';
|
|
25
26
|
import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
|
|
26
27
|
import { translationKey } from '../../translations';
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
import { TextInput } from './Defaults';
|
|
30
|
+
import { InputHeader, type InputProps } from './Input';
|
|
31
|
+
|
|
32
|
+
export type RefFieldProps = InputProps & {
|
|
29
33
|
ast?: SchemaAST.AST;
|
|
30
34
|
array?: boolean;
|
|
31
35
|
onQueryRefOptions?: QueryRefOptions;
|
|
36
|
+
createOptionLabel?: [string, { ns: string }];
|
|
37
|
+
createOptionIcon?: string;
|
|
38
|
+
onCreateFromQuery?: (type: TypeAnnotation, query: string) => MaybePromise<void>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// TODO(thure): Is this a standard that should be better canonized?
|
|
42
|
+
const isRefSnapShot = (val: any): val is { '/': string } => {
|
|
43
|
+
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
32
44
|
};
|
|
33
45
|
|
|
34
46
|
export const RefField = ({
|
|
35
47
|
type,
|
|
36
48
|
label,
|
|
37
|
-
|
|
49
|
+
readonly,
|
|
38
50
|
placeholder,
|
|
39
51
|
inputOnly,
|
|
40
52
|
array,
|
|
@@ -42,6 +54,9 @@ export const RefField = ({
|
|
|
42
54
|
getValue,
|
|
43
55
|
onBlur,
|
|
44
56
|
onQueryRefOptions,
|
|
57
|
+
createOptionLabel,
|
|
58
|
+
createOptionIcon,
|
|
59
|
+
onCreateFromQuery,
|
|
45
60
|
onValueChange,
|
|
46
61
|
...restInputProps
|
|
47
62
|
}: RefFieldProps) => {
|
|
@@ -56,17 +71,18 @@ export const RefField = ({
|
|
|
56
71
|
// If ref type is expando, fall back to taking a DXN in string format.
|
|
57
72
|
return (
|
|
58
73
|
<RefFieldFallback
|
|
59
|
-
{...{ type, label, placeholder,
|
|
74
|
+
{...{ type, label, placeholder, readonly, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
|
|
60
75
|
/>
|
|
61
76
|
);
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
const handleGetValue = useCallback(()
|
|
79
|
+
const handleGetValue = useCallback(() => {
|
|
65
80
|
const formValue = getValue();
|
|
66
81
|
|
|
67
82
|
const unknownToRefOption = (val: unknown) => {
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
const isRef = Ref.isRef(val);
|
|
84
|
+
if (isRef || isRefSnapShot(val)) {
|
|
85
|
+
const dxnString = isRef ? val.dxn.toString() : val['/'];
|
|
70
86
|
const matchingOption = availableOptions.find((option) => option.id === dxnString);
|
|
71
87
|
if (matchingOption) {
|
|
72
88
|
return matchingOption;
|
|
@@ -87,15 +103,6 @@ export const RefField = ({
|
|
|
87
103
|
return [];
|
|
88
104
|
}, [getValue, availableOptions, array]);
|
|
89
105
|
|
|
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
106
|
const handleUpdate = useCallback(
|
|
100
107
|
(ids: string[]) => {
|
|
101
108
|
if (ids.length === 0) {
|
|
@@ -123,8 +130,6 @@ export const RefField = ({
|
|
|
123
130
|
[availableOptions, array, onValueChange],
|
|
124
131
|
);
|
|
125
132
|
|
|
126
|
-
const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
|
|
127
|
-
|
|
128
133
|
if (!refTypeInfo) {
|
|
129
134
|
return null;
|
|
130
135
|
}
|
|
@@ -132,32 +137,113 @@ export const RefField = ({
|
|
|
132
137
|
const { status, error } = restInputProps.getStatus();
|
|
133
138
|
|
|
134
139
|
const items = handleGetValue();
|
|
140
|
+
const selectedIds = useMemo(() => items.map((i: any) => i.id), [items]);
|
|
141
|
+
const labelById = useMemo(
|
|
142
|
+
() =>
|
|
143
|
+
availableOptions.reduce((acc: Record<string, string>, option) => {
|
|
144
|
+
acc[option.id.toLowerCase()] = option.label.toLowerCase();
|
|
145
|
+
return acc;
|
|
146
|
+
}, {}),
|
|
147
|
+
[availableOptions],
|
|
148
|
+
);
|
|
149
|
+
const [query, setQuery] = useState('');
|
|
150
|
+
const toggleSelect = useCallback(
|
|
151
|
+
(id: string) => {
|
|
152
|
+
if (array) {
|
|
153
|
+
const nextIds = selectedIds.includes(id) ? selectedIds.filter((x) => x !== id) : [...selectedIds, id];
|
|
154
|
+
handleUpdate(nextIds);
|
|
155
|
+
} else {
|
|
156
|
+
if (selectedIds[0] === id) {
|
|
157
|
+
handleUpdate([]);
|
|
158
|
+
} else {
|
|
159
|
+
handleUpdate([id]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[array, selectedIds, handleUpdate],
|
|
164
|
+
);
|
|
135
165
|
|
|
136
166
|
// NOTE(thure): I left both predicates in-place in case we decide to add variants which do render readonly but empty values.
|
|
137
|
-
return
|
|
167
|
+
return readonly && items.length < 1 ? null : (
|
|
138
168
|
<Input.Root validationValence={status}>
|
|
139
169
|
{!inputOnly && <InputHeader error={error} label={label} />}
|
|
140
170
|
<div data-no-submit>
|
|
141
|
-
{
|
|
171
|
+
{readonly ? (
|
|
142
172
|
items.length < 1 ? (
|
|
143
173
|
<p className={mx(descriptionText, 'mbe-2')}>{t('empty readonly ref field label')}</p>
|
|
144
174
|
) : (
|
|
145
175
|
items.map((item) => (
|
|
146
|
-
<
|
|
176
|
+
<DxAnchor key={item.id} refid={item.id} rootclassname='mie-1'>
|
|
147
177
|
{item.label}
|
|
148
|
-
</
|
|
178
|
+
</DxAnchor>
|
|
149
179
|
))
|
|
150
180
|
)
|
|
151
181
|
) : (
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
<PopoverCombobox.Root>
|
|
183
|
+
<PopoverCombobox.Trigger asChild>
|
|
184
|
+
<Button variant='ghost' classNames='is-full text-start gap-2'>
|
|
185
|
+
<div role='none' className='grow'>
|
|
186
|
+
{items?.length ? (
|
|
187
|
+
items?.map((item) => (
|
|
188
|
+
<TagPickerItem
|
|
189
|
+
key={item.id}
|
|
190
|
+
itemId={item.id}
|
|
191
|
+
label={item.label}
|
|
192
|
+
{...(item.hue ? { hue: item.hue } : {})}
|
|
193
|
+
removeLabel={t('remove item label')}
|
|
194
|
+
onItemClick={(event: DxTagPickerItemClick) => {
|
|
195
|
+
if (event.action === 'remove') {
|
|
196
|
+
toggleSelect(item.id);
|
|
197
|
+
}
|
|
198
|
+
}}
|
|
199
|
+
/>
|
|
200
|
+
))
|
|
201
|
+
) : (
|
|
202
|
+
<span className='text-description'>
|
|
203
|
+
{placeholder ?? t('ref field placeholder', { count: array ? 99 : 1 })}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
<Icon size={3} icon='ph--caret-down--bold' />
|
|
208
|
+
</Button>
|
|
209
|
+
</PopoverCombobox.Trigger>
|
|
210
|
+
<PopoverCombobox.Content
|
|
211
|
+
filter={(value, search) =>
|
|
212
|
+
value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
<PopoverCombobox.Input
|
|
216
|
+
placeholder={'Search...'}
|
|
217
|
+
value={query}
|
|
218
|
+
onValueChange={(v) => setQuery(v)}
|
|
219
|
+
autoFocus
|
|
220
|
+
/>
|
|
221
|
+
<PopoverCombobox.List>
|
|
222
|
+
{availableOptions.map((option) => (
|
|
223
|
+
<PopoverCombobox.Item
|
|
224
|
+
key={option.id}
|
|
225
|
+
value={option.id}
|
|
226
|
+
onSelect={() => toggleSelect(option.id)}
|
|
227
|
+
classNames='flex items-center gap-2'
|
|
228
|
+
>
|
|
229
|
+
<span className='grow'>{option.label}</span>
|
|
230
|
+
{selectedIds.includes(option.id) && <Icon icon='ph--check--regular' />}
|
|
231
|
+
</PopoverCombobox.Item>
|
|
232
|
+
))}
|
|
233
|
+
{query.length > 0 && createOptionLabel && createOptionIcon && onCreateFromQuery && (
|
|
234
|
+
<PopoverCombobox.Item
|
|
235
|
+
value='__create__'
|
|
236
|
+
onSelect={() => onCreateFromQuery?.(refTypeInfo, query)}
|
|
237
|
+
classNames='flex items-center gap-2'
|
|
238
|
+
>
|
|
239
|
+
<Icon icon={createOptionIcon} />
|
|
240
|
+
{t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })}
|
|
241
|
+
</PopoverCombobox.Item>
|
|
242
|
+
)}
|
|
243
|
+
</PopoverCombobox.List>
|
|
244
|
+
<PopoverCombobox.Arrow />
|
|
245
|
+
</PopoverCombobox.Content>
|
|
246
|
+
</PopoverCombobox.Root>
|
|
161
247
|
)}
|
|
162
248
|
</div>
|
|
163
249
|
{inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
@@ -168,7 +254,7 @@ export const RefField = ({
|
|
|
168
254
|
const RefFieldFallback = ({
|
|
169
255
|
type,
|
|
170
256
|
label,
|
|
171
|
-
|
|
257
|
+
readonly,
|
|
172
258
|
placeholder,
|
|
173
259
|
inputOnly,
|
|
174
260
|
getValue,
|
|
@@ -202,7 +288,7 @@ const RefFieldFallback = ({
|
|
|
202
288
|
<TextInput
|
|
203
289
|
type={type}
|
|
204
290
|
label={label}
|
|
205
|
-
|
|
291
|
+
readonly={readonly}
|
|
206
292
|
placeholder={placeholder}
|
|
207
293
|
inputOnly={inputOnly}
|
|
208
294
|
getValue={handleGetValue as <V>() => V | undefined}
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { type ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type GeoPoint
|
|
7
|
+
import { GeoLocation, type GeoPoint } from '@dxos/echo-schema';
|
|
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
|
);
|
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
|
-
import { type Meta, type StoryObj } from '@storybook/react';
|
|
7
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
8
|
import { Schema } from 'effect';
|
|
9
9
|
import React, { useState } from 'react';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { type SelectOption, SelectOptionSchema } from '@dxos/echo-schema';
|
|
12
12
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import translations from '../../../translations';
|
|
14
|
+
import { translations } from '../../../translations';
|
|
16
15
|
import { TestLayout, TestPanel } from '../../testing';
|
|
17
16
|
import { Form } from '../Form';
|
|
18
17
|
|
|
18
|
+
import { SelectOptionInput } from './SelectOptionsInput';
|
|
19
|
+
|
|
19
20
|
const schema = Schema.Struct({
|
|
20
21
|
options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
|
|
21
22
|
}).pipe(Schema.mutable);
|
|
@@ -44,18 +45,18 @@ const DefaultStory = () => {
|
|
|
44
45
|
);
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
const meta
|
|
48
|
+
const meta = {
|
|
48
49
|
title: 'ui/react-ui-form/SelectOptionsInput',
|
|
49
|
-
component: SelectOptionInput,
|
|
50
|
+
component: SelectOptionInput as any,
|
|
50
51
|
render: DefaultStory,
|
|
51
52
|
decorators: [withTheme, withLayout({ fullscreen: true })],
|
|
52
53
|
parameters: {
|
|
53
54
|
translations,
|
|
54
55
|
},
|
|
55
|
-
}
|
|
56
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
56
57
|
|
|
57
58
|
export default meta;
|
|
58
59
|
|
|
59
|
-
type Story = StoryObj
|
|
60
|
+
type Story = StoryObj<typeof meta>;
|
|
60
61
|
|
|
61
62
|
export const Default: Story = {};
|