@dxos/react-ui-form 0.10.0 → 0.11.0
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/index.mjs +4064 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/translations.mjs +77 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +0 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +1 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Card.stories.d.ts +88 -0
- package/dist/types/src/components/Form/Card.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.d.ts +7 -7
- package/dist/types/src/components/Form/Form.stories.d.ts +21 -0
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.theme.d.ts +6 -0
- package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormControls.d.ts +7 -7
- package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormRow.d.ts +17 -3
- package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts +0 -6
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts +7 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +0 -11
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts +1 -11
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts +13 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +1 -13
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts +15 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +0 -27
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts +29 -0
- package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +10 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +8 -23
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +1 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -0
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +9 -8
- package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +6 -2
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.test.d.ts +2 -0
- package/dist/types/src/hooks/useFormHandler.test.d.ts.map +1 -0
- package/dist/types/src/testing/TestLayout.d.ts +1 -1
- package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +5 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +39 -42
- package/src/components/Form/Card.stories.tsx +93 -0
- package/src/components/Form/Form.stories.tsx +95 -3
- package/src/components/Form/Form.theme.ts +14 -8
- package/src/components/Form/Form.tsx +2 -2
- package/src/components/Form/FormControls.tsx +32 -13
- package/src/components/Form/FormField/FormField.tsx +11 -3
- package/src/components/Form/FormField/FormRow.tsx +20 -1
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +3 -46
- package/src/components/Form/FormField/fields/ArrayField/default-value.ts +49 -0
- package/src/components/Form/FormField/fields/ArrayField/index.ts +2 -0
- package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +12 -2
- package/src/components/Form/FormField/fields/DateField/DateField.tsx +10 -3
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +1 -1
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +2 -12
- package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +1 -1
- package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +1 -36
- package/src/components/Form/FormField/fields/NumberField/index.ts +2 -0
- package/src/components/Form/FormField/fields/NumberField/numeric-constraints.ts +43 -0
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +1 -0
- package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +2 -2
- package/src/components/Form/FormField/fields/RefField/RefField.test.ts +1 -1
- package/src/components/Form/FormField/fields/RefField/RefField.tsx +3 -45
- package/src/components/Form/FormField/fields/RefField/find-ref-option.ts +56 -0
- package/src/components/Form/FormField/fields/RefField/index.ts +2 -0
- package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +2 -67
- package/src/components/Form/FormField/fields/SelectField/create-select-field.tsx +80 -0
- package/src/components/Form/FormField/fields/SelectField/index.ts +2 -0
- package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +2 -1
- package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +11 -9
- package/src/components/Form/FormLayout/FormLayout.test.ts +1 -1
- package/src/components/Form/FormLayout/FormLayout.tsx +1 -86
- package/src/components/Form/FormLayout/index.ts +2 -0
- package/src/components/Form/FormLayout/resolve-layout-field.ts +98 -0
- package/src/components/ObjectForm/ObjectForm.stories.tsx +93 -3
- package/src/components/ObjectForm/ObjectForm.tsx +22 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +1 -1
- package/src/components/ObjectPicker/ObjectPicker.tsx +3 -3
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
- package/src/components/ObjectProperties/ObjectProperties.tsx +7 -1
- package/src/components/ObjectTree/ObjectTree.stories.tsx +1 -1
- package/src/components/RefEditor/RefEditor.stories.tsx +9 -1
- package/src/components/RefEditor/RefEditor.tsx +42 -6
- package/src/components/RefEditor/ref-editor-extension.ts +168 -91
- package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.tsx +9 -9
- package/src/hooks/useFormHandler.test.ts +85 -0
- package/src/hooks/useFormHandler.ts +118 -31
- package/src/translations.ts +1 -0
- package/src/types.ts +5 -0
- package/src/util/index.ts +0 -1
- package/dist/lib/browser/index.mjs +0 -4675
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/translations.mjs +0 -87
- package/dist/lib/browser/translations.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -4677
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/translations.mjs +0 -89
- package/dist/lib/node-esm/translations.mjs.map +0 -7
- package/dist/types/src/util/formatting.d.ts +0 -15
- package/dist/types/src/util/formatting.d.ts.map +0 -1
- package/src/util/formatting.ts +0 -171
|
@@ -23,6 +23,7 @@ import { useFormValues } from '../../../../../hooks';
|
|
|
23
23
|
import { getFormProperties } from '../../../../../util';
|
|
24
24
|
import { CompactIconButton, FormField, type FormFieldProps } from '../../FormField';
|
|
25
25
|
import { FormFieldHeader } from '../../FormFieldHeader';
|
|
26
|
+
import { getDefaultValue } from './default-value';
|
|
26
27
|
|
|
27
28
|
// Synthetic id assigned to each row when rendering an ordered list. Plain form
|
|
28
29
|
// values have no stable identity, so drag-and-drop (which requires a stable key
|
|
@@ -185,13 +186,7 @@ export const ArrayField = ({
|
|
|
185
186
|
// TODO(burdon): Hacky.
|
|
186
187
|
// An owned inline object form must establish its own column context: otherwise it inherits the parent
|
|
187
188
|
// form's `--dx-col` (center) placement and collapses into the array row's narrow second track.
|
|
188
|
-
return createInline ?
|
|
189
|
-
<div role='none' className='w-full min-w-0 [--dx-col:auto]'>
|
|
190
|
-
{field}
|
|
191
|
-
</div>
|
|
192
|
-
) : (
|
|
193
|
-
field
|
|
194
|
-
);
|
|
189
|
+
return createInline ? <div className='w-full min-w-0 [--dx-col:auto]'>{field}</div> : field;
|
|
195
190
|
};
|
|
196
191
|
|
|
197
192
|
const header = (layout !== 'static' || (values && values.length > 0)) && (
|
|
@@ -260,7 +255,7 @@ export const ArrayField = ({
|
|
|
260
255
|
return (
|
|
261
256
|
<>
|
|
262
257
|
{header}
|
|
263
|
-
<div className='flex flex-col gap-2'>
|
|
258
|
+
<div className='flex flex-col gap-2 pb-2'>
|
|
264
259
|
{values?.map((_, index) => {
|
|
265
260
|
const isLast = index === values.length - 1;
|
|
266
261
|
return (
|
|
@@ -288,41 +283,3 @@ export const ArrayField = ({
|
|
|
288
283
|
};
|
|
289
284
|
|
|
290
285
|
ArrayField.displayName = 'Form.ArrayField';
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Returns the default empty value for a given AST.
|
|
294
|
-
* Used for initializing new array values etc.
|
|
295
|
-
*/
|
|
296
|
-
// TODO(wittjosiah): Factor out?
|
|
297
|
-
export const getDefaultValue = (ast?: SchemaAST.AST): any => {
|
|
298
|
-
switch (ast?._tag) {
|
|
299
|
-
case 'StringKeyword': {
|
|
300
|
-
return '';
|
|
301
|
-
}
|
|
302
|
-
case 'NumberKeyword': {
|
|
303
|
-
return 0;
|
|
304
|
-
}
|
|
305
|
-
case 'BooleanKeyword': {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
case 'Suspend': {
|
|
309
|
-
return getDefaultValue(ast.f());
|
|
310
|
-
}
|
|
311
|
-
case 'Refinement': {
|
|
312
|
-
// Use minimum from JSON schema annotation (e.g. Schema.between(1, 31)) as the default
|
|
313
|
-
// so new array items start within the valid range.
|
|
314
|
-
const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(ast));
|
|
315
|
-
if (jsonSchema != null && 'minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
|
|
316
|
-
return jsonSchema.minimum;
|
|
317
|
-
}
|
|
318
|
-
return getDefaultValue(ast.from);
|
|
319
|
-
}
|
|
320
|
-
default: {
|
|
321
|
-
if (ast && SchemaEx.isNestedType(ast)) {
|
|
322
|
-
return {};
|
|
323
|
-
} else {
|
|
324
|
-
throw new Error(`Unsupported type: ${ast?._tag}`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Option from 'effect/Option';
|
|
6
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
+
|
|
8
|
+
import { SchemaEx } from '@dxos/effect';
|
|
9
|
+
|
|
10
|
+
// Kept out of `ArrayField.tsx`: react-refresh only fast-refreshes a module whose
|
|
11
|
+
// exports are all components, so values exported beside them force a full page reload on every edit.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the default empty value for a given AST.
|
|
15
|
+
* Used for initializing new array values etc.
|
|
16
|
+
*/
|
|
17
|
+
// TODO(wittjosiah): Factor out?
|
|
18
|
+
export const getDefaultValue = (ast?: SchemaAST.AST): any => {
|
|
19
|
+
switch (ast?._tag) {
|
|
20
|
+
case 'StringKeyword': {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
case 'NumberKeyword': {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
case 'BooleanKeyword': {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
case 'Suspend': {
|
|
30
|
+
return getDefaultValue(ast.f());
|
|
31
|
+
}
|
|
32
|
+
case 'Refinement': {
|
|
33
|
+
// Use minimum from JSON schema annotation (e.g. Schema.between(1, 31)) as the default
|
|
34
|
+
// so new array items start within the valid range.
|
|
35
|
+
const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(ast));
|
|
36
|
+
if (jsonSchema != null && 'minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
|
|
37
|
+
return jsonSchema.minimum;
|
|
38
|
+
}
|
|
39
|
+
return getDefaultValue(ast.from);
|
|
40
|
+
}
|
|
41
|
+
default: {
|
|
42
|
+
if (ast && SchemaEx.isNestedType(ast)) {
|
|
43
|
+
return {};
|
|
44
|
+
} else {
|
|
45
|
+
throw new Error(`Unsupported type: ${ast?._tag}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -29,6 +29,9 @@ export const ComboboxField = ({ lookup, type, readonly, placeholder, onValueChan
|
|
|
29
29
|
const values = useFormValues<AnyProperties>(ComboboxField.displayName);
|
|
30
30
|
const ownKey = props.jsonPath;
|
|
31
31
|
const [query, setQuery] = useState('');
|
|
32
|
+
// The stored value is often an opaque id; remember the selected option's label so the trigger shows
|
|
33
|
+
// it (the query-driven suggestions no longer include the option once selected).
|
|
34
|
+
const [selectedLabel, setSelectedLabel] = useState<string | undefined>();
|
|
32
35
|
|
|
33
36
|
// When the combobox searches on its own field (its name is among `deps`), feed the live query to the
|
|
34
37
|
// loader so remote suggestions track typing; otherwise the loader reads the committed sibling values.
|
|
@@ -54,7 +57,13 @@ export const ComboboxField = ({ lookup, type, readonly, placeholder, onValueChan
|
|
|
54
57
|
);
|
|
55
58
|
const hasExact = results.some((option) => option.value === trimmed);
|
|
56
59
|
|
|
57
|
-
const handleValueChange = useCallback(
|
|
60
|
+
const handleValueChange = useCallback(
|
|
61
|
+
(next: string) => {
|
|
62
|
+
setSelectedLabel((data ?? []).find((option) => option.value === next)?.label);
|
|
63
|
+
onValueChange(type, next);
|
|
64
|
+
},
|
|
65
|
+
[onValueChange, type, data],
|
|
66
|
+
);
|
|
58
67
|
const handleOpenChange = useCallback((open: boolean) => {
|
|
59
68
|
if (!open) {
|
|
60
69
|
setQuery('');
|
|
@@ -64,12 +73,13 @@ export const ComboboxField = ({ lookup, type, readonly, placeholder, onValueChan
|
|
|
64
73
|
return (
|
|
65
74
|
<FormRow<string>
|
|
66
75
|
readonly={readonly}
|
|
67
|
-
renderStatic={(value) => <p className='truncate min-w-0'>{value ?? ''}</p>}
|
|
76
|
+
renderStatic={(value) => <p className='truncate min-w-0'>{selectedLabel ?? value ?? ''}</p>}
|
|
68
77
|
{...props}
|
|
69
78
|
>
|
|
70
79
|
{({ value = '' }) => (
|
|
71
80
|
<Combobox.Root
|
|
72
81
|
value={value}
|
|
82
|
+
displayValue={selectedLabel}
|
|
73
83
|
onValueChange={handleValueChange}
|
|
74
84
|
onOpenChange={handleOpenChange}
|
|
75
85
|
placeholder={placeholder}
|
|
@@ -71,7 +71,7 @@ export const DateField = ({
|
|
|
71
71
|
switch (format) {
|
|
72
72
|
case Format.TypeFormat.Date:
|
|
73
73
|
return (
|
|
74
|
-
<div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch'>
|
|
74
|
+
<div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch tabular-nums'>
|
|
75
75
|
<Input.Date
|
|
76
76
|
classNames='min-w-0 overflow-hidden'
|
|
77
77
|
disabled={readonly}
|
|
@@ -82,11 +82,18 @@ export const DateField = ({
|
|
|
82
82
|
</div>
|
|
83
83
|
);
|
|
84
84
|
case Format.TypeFormat.Time:
|
|
85
|
-
return
|
|
85
|
+
return (
|
|
86
|
+
<Input.Time
|
|
87
|
+
classNames='tabular-nums'
|
|
88
|
+
disabled={!!readonly}
|
|
89
|
+
value={value ?? ''}
|
|
90
|
+
onValueChange={handleSimpleChange}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
86
93
|
case Format.TypeFormat.DateTime:
|
|
87
94
|
default:
|
|
88
95
|
return (
|
|
89
|
-
<div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch'>
|
|
96
|
+
<div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch tabular-nums'>
|
|
90
97
|
<Input.DateTime
|
|
91
98
|
classNames='min-w-0 overflow-hidden'
|
|
92
99
|
disabled={readonly}
|
|
@@ -8,7 +8,7 @@ import React, { useMemo } from 'react';
|
|
|
8
8
|
import { expect, userEvent } from 'storybook/test';
|
|
9
9
|
|
|
10
10
|
import { Filter, Format, Obj, Ref } from '@dxos/echo';
|
|
11
|
-
import { useQuery } from '@dxos/react
|
|
11
|
+
import { useQuery } from '@dxos/echo-react';
|
|
12
12
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
13
13
|
import { Panel } from '@dxos/react-ui';
|
|
14
14
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
@@ -17,16 +17,6 @@ import { type FormFieldRendererProps } from '#types';
|
|
|
17
17
|
|
|
18
18
|
import { FormRow } from '../../FormRow';
|
|
19
19
|
|
|
20
|
-
// Mirror the focus treatment of `Input` (default variant): a `focus-ring-subtle` ring of `dx-focus-line`
|
|
21
|
-
// width rather than a recolored 1px border, and suppress the editor wrapper's `accent-bg` border so a
|
|
22
|
-
// second click never turns the outline blue. Keyed on `focus-within` because focus lands on the inner
|
|
23
|
-
// CodeMirror contenteditable, not this wrapper.
|
|
24
|
-
const editorClassNames = [
|
|
25
|
-
'min-h-[6lh] transition-colors rounded-xs p-1 px-2 bg-input-surface border border-input-separator',
|
|
26
|
-
'focus:border-separator focus-within:border-separator focus-within:bg-focus-surface',
|
|
27
|
-
'focus-within:ring-2 focus-within:ring-offset-0 focus-within:ring-focus-ring-subtle focus-within:z-[1]',
|
|
28
|
-
];
|
|
29
|
-
|
|
30
20
|
/**
|
|
31
21
|
* Form field that edits a markdown value in a CodeMirror editor.
|
|
32
22
|
* Supports two backings:
|
|
@@ -116,7 +106,7 @@ const RefMarkdownEditor = ({ reference, placeholder, readonly }: RefMarkdownEdit
|
|
|
116
106
|
|
|
117
107
|
return (
|
|
118
108
|
<Editor.Root>
|
|
119
|
-
<Editor.View classNames=
|
|
109
|
+
<Editor.View classNames='dx-input' extensions={extensions} />
|
|
120
110
|
</Editor.Root>
|
|
121
111
|
);
|
|
122
112
|
};
|
|
@@ -135,7 +125,7 @@ const StringMarkdownEditor = ({ value, placeholder, readonly, onChange }: String
|
|
|
135
125
|
return (
|
|
136
126
|
<Editor.Root>
|
|
137
127
|
<Editor.View
|
|
138
|
-
classNames=
|
|
128
|
+
classNames='dx-input min-h-[6lh]'
|
|
139
129
|
extensions={extensions}
|
|
140
130
|
value={value}
|
|
141
131
|
onChange={readonly ? undefined : handleChange}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as Schema from 'effect/Schema';
|
|
6
6
|
import { describe, test } from 'vitest';
|
|
7
7
|
|
|
8
|
-
import { getNumericConstraints } from './
|
|
8
|
+
import { getNumericConstraints } from './numeric-constraints';
|
|
9
9
|
|
|
10
10
|
describe('getNumericConstraints', () => {
|
|
11
11
|
test('reads min/max from Schema.between (bounds live on separate nested refinements)', ({ expect }) => {
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import * as Option from 'effect/Option';
|
|
6
|
-
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
5
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
8
6
|
|
|
9
7
|
import { Input, type TextInputProps } from '@dxos/react-ui';
|
|
@@ -12,40 +10,7 @@ import { safeParseFloat } from '@dxos/util';
|
|
|
12
10
|
import { type FormFieldRendererProps } from '#types';
|
|
13
11
|
|
|
14
12
|
import { FormRow } from '../../FormRow';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Extracts numeric constraints (`minimum`/`maximum` bounds and whether the value must be an integer)
|
|
18
|
-
* by walking the chain of Refinement ASTs and reading each one's JSON schema annotation (e.g. produced
|
|
19
|
-
* by `Schema.between` / `Schema.int`). Returns empty constraints when none are declared.
|
|
20
|
-
*/
|
|
21
|
-
export const getNumericConstraints = (ast: SchemaAST.AST): { min?: number; max?: number; integer: boolean } => {
|
|
22
|
-
let node: SchemaAST.AST | undefined = ast;
|
|
23
|
-
let min: number | undefined;
|
|
24
|
-
let max: number | undefined;
|
|
25
|
-
let integer = false;
|
|
26
|
-
// Nested refinements (e.g. `Schema.int()` + `Schema.between()`) each carry their own JSON schema
|
|
27
|
-
// fragment, so accumulate across the chain rather than reading only the outermost node.
|
|
28
|
-
while (node && SchemaAST.isRefinement(node)) {
|
|
29
|
-
const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(node));
|
|
30
|
-
if (jsonSchema != null) {
|
|
31
|
-
// Stacked refinements intersect: keep the strictest bound (largest min, smallest max).
|
|
32
|
-
if ('minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
|
|
33
|
-
min = min === undefined ? jsonSchema.minimum : Math.max(min, jsonSchema.minimum);
|
|
34
|
-
}
|
|
35
|
-
if ('maximum' in jsonSchema && typeof jsonSchema.maximum === 'number') {
|
|
36
|
-
max = max === undefined ? jsonSchema.maximum : Math.min(max, jsonSchema.maximum);
|
|
37
|
-
}
|
|
38
|
-
if (
|
|
39
|
-
('type' in jsonSchema && jsonSchema.type === 'integer') ||
|
|
40
|
-
('multipleOf' in jsonSchema && jsonSchema.multipleOf === 1)
|
|
41
|
-
) {
|
|
42
|
-
integer = true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
node = node.from;
|
|
46
|
-
}
|
|
47
|
-
return { min, max, integer };
|
|
48
|
-
};
|
|
13
|
+
import { getNumericConstraints } from './numeric-constraints';
|
|
49
14
|
|
|
50
15
|
export const NumberField = ({
|
|
51
16
|
type,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Option from 'effect/Option';
|
|
6
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
+
|
|
8
|
+
// Kept out of `NumberField.tsx`: react-refresh only fast-refreshes a module whose
|
|
9
|
+
// exports are all components, so values exported beside them force a full page reload on every edit.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extracts numeric constraints (`minimum`/`maximum` bounds and whether the value must be an integer)
|
|
13
|
+
* by walking the chain of Refinement ASTs and reading each one's JSON schema annotation (e.g. produced
|
|
14
|
+
* by `Schema.between` / `Schema.int`). Returns empty constraints when none are declared.
|
|
15
|
+
*/
|
|
16
|
+
export const getNumericConstraints = (ast: SchemaAST.AST): { min?: number; max?: number; integer: boolean } => {
|
|
17
|
+
let node: SchemaAST.AST | undefined = ast;
|
|
18
|
+
let min: number | undefined;
|
|
19
|
+
let max: number | undefined;
|
|
20
|
+
let integer = false;
|
|
21
|
+
// Nested refinements (e.g. `Schema.int()` + `Schema.between()`) each carry their own JSON schema
|
|
22
|
+
// fragment, so accumulate across the chain rather than reading only the outermost node.
|
|
23
|
+
while (node && SchemaAST.isRefinement(node)) {
|
|
24
|
+
const jsonSchema = Option.getOrUndefined(SchemaAST.getJSONSchemaAnnotation(node));
|
|
25
|
+
if (jsonSchema != null) {
|
|
26
|
+
// Stacked refinements intersect: keep the strictest bound (largest min, smallest max).
|
|
27
|
+
if ('minimum' in jsonSchema && typeof jsonSchema.minimum === 'number') {
|
|
28
|
+
min = min === undefined ? jsonSchema.minimum : Math.max(min, jsonSchema.minimum);
|
|
29
|
+
}
|
|
30
|
+
if ('maximum' in jsonSchema && typeof jsonSchema.maximum === 'number') {
|
|
31
|
+
max = max === undefined ? jsonSchema.maximum : Math.min(max, jsonSchema.maximum);
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
('type' in jsonSchema && jsonSchema.type === 'integer') ||
|
|
35
|
+
('multipleOf' in jsonSchema && jsonSchema.multipleOf === 1)
|
|
36
|
+
) {
|
|
37
|
+
integer = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
node = node.from;
|
|
41
|
+
}
|
|
42
|
+
return { min, max, integer };
|
|
43
|
+
};
|
|
@@ -15,7 +15,7 @@ import { IconButton, Input, useTranslation } from '@dxos/react-ui';
|
|
|
15
15
|
import { translationKey } from '#translations';
|
|
16
16
|
|
|
17
17
|
import { omitId } from '../../../../../util';
|
|
18
|
-
import { FormContent,
|
|
18
|
+
import { FormContent, FormFieldSetContainer, FormRoot } from '../../../FormControls';
|
|
19
19
|
import { FormFieldLabel } from '../../FormRow';
|
|
20
20
|
import { presentationFor } from '../../presentation';
|
|
21
21
|
import { type RefFieldProps } from './RefField';
|
|
@@ -138,7 +138,7 @@ const InlineForm = ({ reference, db, readonly, useType = defaultUseType }: Inlin
|
|
|
138
138
|
return (
|
|
139
139
|
<FormRoot db={db} schema={formSchema} defaultValues={defaultValues as any} onValuesChanged={handleChange}>
|
|
140
140
|
<FormContent>
|
|
141
|
-
<
|
|
141
|
+
<FormFieldSetContainer collapsible readonly={readonly} />
|
|
142
142
|
</FormContent>
|
|
143
143
|
</FormRoot>
|
|
144
144
|
);
|
|
@@ -6,7 +6,7 @@ import { describe, test } from 'vitest';
|
|
|
6
6
|
|
|
7
7
|
import { type RefOption } from '#types';
|
|
8
8
|
|
|
9
|
-
import { findRefOption } from './
|
|
9
|
+
import { findRefOption } from './find-ref-option';
|
|
10
10
|
|
|
11
11
|
// Sample qualified self URIs as produced by an in-space object (Entity.getURI / `echo://<space>/<id>`).
|
|
12
12
|
const SPACE_ID = 'BA25QRC2FEWCSAMRP4RZL65LWJ7352CKE';
|
|
@@ -9,63 +9,21 @@ import { Entity, Filter, Obj, Query, Ref, Scope, Type } from '@dxos/echo';
|
|
|
9
9
|
import { useType as defaultUseType, useQuery } from '@dxos/echo-react';
|
|
10
10
|
import { ANY_OBJECT_TYPENAME, ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
|
|
11
11
|
import { SchemaEx } from '@dxos/effect';
|
|
12
|
-
import { DXN,
|
|
12
|
+
import { DXN, URI } from '@dxos/keys';
|
|
13
13
|
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
14
14
|
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
15
15
|
import { ParentLabelAnnotationId } from '@dxos/schema';
|
|
16
16
|
|
|
17
17
|
import { translationKey } from '#translations';
|
|
18
|
-
import { type CreateOptions, type FormFieldRendererProps, type RefFieldDataProps
|
|
18
|
+
import { type CreateOptions, type FormFieldRendererProps, type RefFieldDataProps } from '#types';
|
|
19
19
|
|
|
20
20
|
import { omitHiddenFormFields, omitId } from '../../../../../util';
|
|
21
21
|
import { ObjectPicker } from '../../../../ObjectPicker';
|
|
22
22
|
import { FormFieldLabel } from '../../FormRow';
|
|
23
23
|
import { presentationFor } from '../../presentation';
|
|
24
|
+
import { findRefOption } from './find-ref-option';
|
|
24
25
|
|
|
25
26
|
// TODO(burdon): Factor out.
|
|
26
|
-
const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
27
|
-
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Find the option a ref-like form value points at. Matches on the local (entity-id) form so a bare local EID
|
|
32
|
-
* (`echo:/<id>`, produced by `Ref.make`) still resolves against an option keyed by the entity's qualified
|
|
33
|
-
* self URI (`echo://<space>/<id>`). Returns `undefined` when the value is not a ref or no option matches.
|
|
34
|
-
*
|
|
35
|
-
* Comparing by entity id is only sound within one space (ids are unique there, not globally). Two EIDs that
|
|
36
|
-
* both carry a space authority must therefore agree on it: a qualified value and a qualified option from
|
|
37
|
-
* different spaces never match, even when their entity ids coincide.
|
|
38
|
-
*/
|
|
39
|
-
export const findRefOption = (value: unknown, options: RefOption[]): RefOption | undefined => {
|
|
40
|
-
const isRef = Ref.isRef(value);
|
|
41
|
-
if (!isRef && !isRefSnapshot(value)) {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
const valueUri = isRef ? value.uri : value['/'];
|
|
45
|
-
// Keyed/registry entities (skills, operations) are referenced by a named DXN rather than an
|
|
46
|
-
// entity-id, so they carry no parseable EID; match those by direct URI equality against the option id.
|
|
47
|
-
const directMatch = options.find((option) => option.id === valueUri);
|
|
48
|
-
if (directMatch) {
|
|
49
|
-
return directMatch;
|
|
50
|
-
}
|
|
51
|
-
const valueEid = EID.tryParse(valueUri);
|
|
52
|
-
if (!valueEid) {
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
const valueSpaceId = EID.getSpaceId(valueEid);
|
|
56
|
-
const valueLocal = EID.toLocal(valueEid);
|
|
57
|
-
return options.find((option) => {
|
|
58
|
-
const optionEid = EID.tryParse(option.id);
|
|
59
|
-
if (!optionEid) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
const optionSpaceId = EID.getSpaceId(optionEid);
|
|
63
|
-
if (valueSpaceId != null && optionSpaceId != null && valueSpaceId !== optionSpaceId) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
return EID.equals(EID.toLocal(optionEid), valueLocal);
|
|
67
|
-
});
|
|
68
|
-
};
|
|
69
27
|
|
|
70
28
|
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (
|
|
71
29
|
results,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
6
|
+
import { Ref } from '@dxos/echo';
|
|
7
|
+
import { EID } from '@dxos/keys';
|
|
8
|
+
|
|
9
|
+
import { type RefOption } from '#types';
|
|
10
|
+
|
|
11
|
+
// Kept out of `RefField.tsx`: react-refresh only fast-refreshes a module whose
|
|
12
|
+
// exports are all components, so values exported beside them force a full page reload on every edit.
|
|
13
|
+
|
|
14
|
+
const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
15
|
+
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find the option a ref-like form value points at. Matches on the local (entity-id) form so a bare local EID
|
|
20
|
+
* (`echo:/<id>`, produced by `Ref.make`) still resolves against an option keyed by the entity's qualified
|
|
21
|
+
* self URI (`echo://<space>/<id>`). Returns `undefined` when the value is not a ref or no option matches.
|
|
22
|
+
*
|
|
23
|
+
* Comparing by entity id is only sound within one space (ids are unique there, not globally). Two EIDs that
|
|
24
|
+
* both carry a space authority must therefore agree on it: a qualified value and a qualified option from
|
|
25
|
+
* different spaces never match, even when their entity ids coincide.
|
|
26
|
+
*/
|
|
27
|
+
export const findRefOption = (value: unknown, options: RefOption[]): RefOption | undefined => {
|
|
28
|
+
const isRef = Ref.isRef(value);
|
|
29
|
+
if (!isRef && !isRefSnapshot(value)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const valueUri = isRef ? value.uri : value['/'];
|
|
33
|
+
// Keyed/registry entities (skills, operations) are referenced by a named DXN rather than an
|
|
34
|
+
// entity-id, so they carry no parseable EID; match those by direct URI equality against the option id.
|
|
35
|
+
const directMatch = options.find((option) => option.id === valueUri);
|
|
36
|
+
if (directMatch) {
|
|
37
|
+
return directMatch;
|
|
38
|
+
}
|
|
39
|
+
const valueEid = EID.tryParse(valueUri);
|
|
40
|
+
if (!valueEid) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const valueSpaceId = EID.getSpaceId(valueEid);
|
|
44
|
+
const valueLocal = EID.toLocal(valueEid);
|
|
45
|
+
return options.find((option) => {
|
|
46
|
+
const optionEid = EID.tryParse(option.id);
|
|
47
|
+
if (!optionEid) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const optionSpaceId = EID.getSpaceId(optionEid);
|
|
51
|
+
if (valueSpaceId != null && optionSpaceId != null && valueSpaceId !== optionSpaceId) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return EID.equals(EID.toLocal(optionEid), valueLocal);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import { invariant } from '@dxos/invariant';
|
|
8
7
|
import { Icon, Select, type SelectRootProps } from '@dxos/react-ui';
|
|
9
8
|
import { getStyles } from '@dxos/ui-theme';
|
|
10
9
|
|
|
11
|
-
import { type
|
|
10
|
+
import { type FormFieldRendererProps } from '#types';
|
|
12
11
|
|
|
13
12
|
import { FormRow } from '../../FormRow';
|
|
14
13
|
|
|
@@ -33,7 +32,7 @@ export const SelectField = ({ type, readonly, placeholder, options, onValueChang
|
|
|
33
32
|
{...props}
|
|
34
33
|
>
|
|
35
34
|
{({ value }) => (
|
|
36
|
-
<Select.Root value={value} onValueChange={handleValueChange}>
|
|
35
|
+
<Select.Root value={value} onValueChange={handleValueChange} disabled={!!readonly}>
|
|
37
36
|
<Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
|
|
38
37
|
{options?.length !== 0 && (
|
|
39
38
|
<Select.Portal>
|
|
@@ -70,67 +69,3 @@ export type SelectFieldOption = {
|
|
|
70
69
|
value: string;
|
|
71
70
|
label?: string;
|
|
72
71
|
};
|
|
73
|
-
|
|
74
|
-
export type CreateSelectFieldOptions = {
|
|
75
|
-
/** Options to display. Strings are used as both value and label. */
|
|
76
|
-
options: ReadonlyArray<string | SelectFieldOption>;
|
|
77
|
-
/** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
|
|
78
|
-
defaultLabel?: string | null;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Factory for a `fieldMap` entry (keyed by JSON path) that renders a Select over a fixed list of
|
|
83
|
-
* options, with an optional sentinel mapping to `undefined`. Use for fields whose choices come from a
|
|
84
|
-
* runtime list rather than the schema (e.g. available model ids).
|
|
85
|
-
*/
|
|
86
|
-
export const createSelectField = ({
|
|
87
|
-
options,
|
|
88
|
-
defaultLabel = 'Default',
|
|
89
|
-
}: CreateSelectFieldOptions): FormFieldRenderer => {
|
|
90
|
-
const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
|
|
91
|
-
const hasDefault = defaultLabel !== null;
|
|
92
|
-
// The sentinel maps to `undefined`. Radix forbids an empty-string `Select.Item` value, so use a
|
|
93
|
-
// non-empty placeholder; reserved (a real option with this value would be unrepresentable).
|
|
94
|
-
const sentinel = '__default__';
|
|
95
|
-
invariant(
|
|
96
|
-
!normalized.some((option) => option.value === sentinel),
|
|
97
|
-
`createSelectField: option value '${sentinel}' is reserved.`,
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
return ({ type, readonly, onValueChange, ...props }: FormFieldRendererProps<string | undefined>) => (
|
|
101
|
-
<FormRow<string>
|
|
102
|
-
readonly={readonly}
|
|
103
|
-
renderStatic={(value) => (
|
|
104
|
-
<p className='truncate min-w-0'>
|
|
105
|
-
{normalized.find((option) => option.value === value)?.label ?? String(value ?? '')}
|
|
106
|
-
</p>
|
|
107
|
-
)}
|
|
108
|
-
{...props}
|
|
109
|
-
>
|
|
110
|
-
{({ value }) => (
|
|
111
|
-
<Select.Root
|
|
112
|
-
disabled={!!readonly}
|
|
113
|
-
value={value ?? sentinel}
|
|
114
|
-
onValueChange={(next) => onValueChange(type, hasDefault && next === sentinel ? undefined : next)}
|
|
115
|
-
>
|
|
116
|
-
<Select.TriggerButton classNames='w-full' disabled={!!readonly} />
|
|
117
|
-
{normalized.length > 0 && (
|
|
118
|
-
<Select.Portal>
|
|
119
|
-
<Select.Content>
|
|
120
|
-
<Select.Viewport>
|
|
121
|
-
{hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
|
|
122
|
-
{normalized.map((option) => (
|
|
123
|
-
<Select.Option key={option.value} value={option.value}>
|
|
124
|
-
{option.label ?? option.value}
|
|
125
|
-
</Select.Option>
|
|
126
|
-
))}
|
|
127
|
-
</Select.Viewport>
|
|
128
|
-
<Select.Arrow />
|
|
129
|
-
</Select.Content>
|
|
130
|
-
</Select.Portal>
|
|
131
|
-
)}
|
|
132
|
-
</Select.Root>
|
|
133
|
-
)}
|
|
134
|
-
</FormRow>
|
|
135
|
-
);
|
|
136
|
-
};
|