@dxos/react-ui-form 0.9.1-main.c7dcc2e112 → 0.9.1-staging.ee54ba693a
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 +1883 -1539
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1883 -1539
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/annotations.d.ts +47 -0
- package/dist/types/src/annotations.d.ts.map +1 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +12 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +46 -65
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.theme.d.ts +252 -0
- package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.theme.test.d.ts +2 -0
- package/dist/types/src/components/Form/Form.theme.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormControls.d.ts +17 -8
- package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts +16 -2
- package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts +27 -0
- package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/FormRow.d.ts +85 -0
- package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/{FormFieldWrapper.stories.d.ts → FormRow.stories.d.ts} +4 -1
- package/dist/types/src/components/Form/FormField/FormRow.stories.d.ts.map +1 -0
- 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/AsyncSelectField/AsyncSelectField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts +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/NumberField/NumberField.d.ts +11 -0
- 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.test.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts +4 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -0
- package/dist/types/src/components/{Settings/Settings.stories.d.ts → Form/FormField/fields/PasswordField/PasswordField.stories.d.ts} +1 -2
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/InlineRefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +18 -2
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/index.d.ts +4 -0
- package/dist/types/src/components/Form/FormField/fields/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/index.d.ts +4 -3
- package/dist/types/src/components/Form/FormField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/presentation.d.ts +22 -0
- package/dist/types/src/components/Form/FormField/presentation.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts +8 -5
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +22 -0
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormFieldSet/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +2 -0
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +5 -5
- 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 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/meta-tags.d.ts +1 -12
- package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +2 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -2
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -2
- 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/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAsyncFieldEffect.d.ts +24 -0
- package/dist/types/src/hooks/useAsyncFieldEffect.d.ts.map +1 -0
- package/dist/types/src/hooks/useFormContext.d.ts +8 -3
- package/dist/types/src/hooks/useFormContext.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +8 -2
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- 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/testing/schema.d.ts +13 -16
- package/dist/types/src/testing/schema.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +50 -1
- 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/src/util/omit.d.ts +1 -1
- package/dist/types/src/util/omit.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -40
- package/src/annotations.ts +63 -0
- package/src/components/FieldEditor/FieldEditor.tsx +5 -2
- package/src/components/Form/DESIGN.md +24 -0
- package/src/components/Form/Form.stories.tsx +257 -13
- package/src/components/Form/Form.theme.test.ts +33 -0
- package/src/components/Form/Form.theme.ts +86 -0
- package/src/components/Form/Form.tsx +15 -9
- package/src/components/Form/FormControls.tsx +94 -37
- package/src/components/Form/FormField/FormField.tsx +83 -32
- package/src/components/Form/FormField/FormFieldHeader.tsx +68 -0
- package/src/components/Form/FormField/{FormFieldWrapper.stories.tsx → FormRow.stories.tsx} +34 -9
- package/src/components/Form/FormField/FormRow.tsx +298 -0
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.tsx +25 -9
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +83 -24
- package/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.tsx +59 -0
- package/src/components/Form/FormField/fields/AsyncSelectField/index.ts +5 -0
- package/src/components/Form/FormField/fields/AutofillField/AutofillField.tsx +67 -0
- package/src/components/Form/FormField/fields/AutofillField/index.ts +5 -0
- package/src/components/Form/FormField/fields/BooleanField/BooleanField.tsx +5 -6
- package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +104 -0
- package/src/components/Form/FormField/fields/ComboboxField/index.ts +5 -0
- package/src/components/Form/FormField/fields/DateField/DateField.tsx +9 -9
- package/src/components/Form/FormField/fields/GeoPointField/GeoPointField.tsx +17 -20
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +2 -2
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +39 -40
- package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +29 -0
- package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +60 -21
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.tsx +53 -0
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +41 -0
- package/src/components/Form/FormField/fields/PasswordField/index.ts +5 -0
- package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +16 -10
- package/src/components/Form/FormField/fields/RefField/RefField.test.ts +8 -0
- package/src/components/Form/FormField/fields/RefField/RefField.tsx +50 -23
- package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +104 -47
- package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +3 -3
- package/src/components/Form/FormField/fields/TextAreaField/TextAreaField.tsx +3 -3
- package/src/components/Form/FormField/fields/TextField/TextField.tsx +3 -3
- package/src/components/Form/FormField/fields/TupleField/TupleField.tsx +1 -1
- package/src/components/Form/FormField/fields/index.ts +4 -0
- package/src/components/Form/FormField/index.ts +5 -3
- package/src/components/Form/FormField/presentation.tsx +32 -0
- package/src/components/Form/FormFieldSet/FormFieldSet.tsx +49 -70
- package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +93 -0
- package/src/components/Form/FormFieldSet/index.ts +1 -0
- package/src/components/Form/FormLayout/FormLayout.stories.tsx +7 -5
- package/src/components/Form/FormLayout/FormLayout.tsx +24 -14
- package/src/components/Form/FormLayout/index.ts +1 -1
- package/src/components/Form/meta-tags.test.ts +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +2 -2
- package/src/components/ObjectPicker/ObjectPicker.tsx +1 -0
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
- package/src/components/ObjectProperties/ObjectProperties.tsx +12 -8
- package/src/components/RefEditor/RefEditor.stories.tsx +2 -2
- package/src/components/RefEditor/RefEditor.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +2 -2
- package/src/components/ViewEditor/ViewEditor.test.tsx +8 -2
- package/src/components/ViewEditor/ViewEditor.tsx +3 -3
- package/src/components/index.ts +0 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAsyncFieldEffect.ts +81 -0
- package/src/hooks/useFormContext.ts +6 -3
- package/src/hooks/useFormHandler.ts +11 -6
- package/src/index.ts +1 -0
- package/src/testing/TestLayout.tsx +15 -12
- package/src/testing/schema.ts +43 -43
- package/src/types.ts +53 -1
- package/src/util/index.ts +0 -1
- package/src/util/omit.ts +25 -11
- package/src/util/properties.test.ts +110 -28
- package/src/util/refs.ts +1 -1
- package/dist/types/src/components/Form/FormField/FieldHeader.d.ts +0 -19
- package/dist/types/src/components/Form/FormField/FieldHeader.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts +0 -47
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.stories.d.ts.map +0 -1
- package/dist/types/src/components/Settings/Settings.d.ts +0 -51
- package/dist/types/src/components/Settings/Settings.d.ts.map +0 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +0 -43
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsItem.d.ts +0 -16
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +0 -18
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +0 -1
- package/dist/types/src/components/Settings/index.d.ts +0 -4
- package/dist/types/src/components/Settings/index.d.ts.map +0 -1
- package/dist/types/src/util/field-type.d.ts +0 -19
- package/dist/types/src/util/field-type.d.ts.map +0 -1
- package/src/components/Form/FormField/FieldHeader.tsx +0 -36
- package/src/components/Form/FormField/FormFieldWrapper.tsx +0 -179
- package/src/components/Settings/AUDIT.md +0 -51
- package/src/components/Settings/Settings.stories.tsx +0 -104
- package/src/components/Settings/Settings.tsx +0 -81
- package/src/components/Settings/SettingsFieldSet.tsx +0 -257
- package/src/components/Settings/SettingsItem.tsx +0 -58
- package/src/components/Settings/SettingsSelectField.tsx +0 -59
- package/src/components/Settings/index.ts +0 -7
- package/src/util/field-type.ts +0 -60
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type AnyProperties } from '@dxos/echo/internal';
|
|
8
|
+
|
|
9
|
+
import { type FormFieldRendererProps } from '#types';
|
|
10
|
+
|
|
11
|
+
import { type OptionsLookup, type OptionsLookupEntry } from '../../../../../annotations';
|
|
12
|
+
import { pickValues, useAsyncFieldEffect, useFormValues } from '../../../../../hooks';
|
|
13
|
+
import { SelectField } from '../SelectField';
|
|
14
|
+
|
|
15
|
+
export type AsyncSelectFieldProps = FormFieldRendererProps<string> & {
|
|
16
|
+
/** Loads the selectable options from the lookup's declared dependency fields. */
|
|
17
|
+
lookup: OptionsLookup;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A select whose options are loaded dynamically (debounced) from the fields the lookup declares in its
|
|
22
|
+
* `deps`, via an {@link Annotation.OptionsLookup}. Re-runs only when one of those values changes.
|
|
23
|
+
* Auto-selects the sole option when exactly one resolves and nothing is chosen yet.
|
|
24
|
+
*/
|
|
25
|
+
export const AsyncSelectField = ({ lookup, ...fieldProps }: AsyncSelectFieldProps) => {
|
|
26
|
+
const values = useFormValues<AnyProperties>(AsyncSelectField.displayName);
|
|
27
|
+
const subset = useMemo(() => pickValues(values, lookup.deps), [values, lookup.deps]);
|
|
28
|
+
const key = useMemo(() => JSON.stringify(subset), [subset]);
|
|
29
|
+
const { loading, data } = useAsyncFieldEffect<readonly OptionsLookupEntry[]>(() => lookup.load(subset), key);
|
|
30
|
+
const options = data ?? [];
|
|
31
|
+
|
|
32
|
+
const { type, placeholder } = fieldProps;
|
|
33
|
+
const current = fieldProps.getValue();
|
|
34
|
+
// `onValueChange` changes identity on every edit; hold it in a ref so auto-select depends only on the
|
|
35
|
+
// resolved `data` and current value (avoids an effect-refire loop).
|
|
36
|
+
const onValueChangeRef = useRef(fieldProps.onValueChange);
|
|
37
|
+
onValueChangeRef.current = fieldProps.onValueChange;
|
|
38
|
+
// Track which `data` reference has already been auto-selected so clearing the field does not trigger
|
|
39
|
+
// repeated re-selection of the same single option.
|
|
40
|
+
const autoSelectedDataRef = useRef<typeof data>(undefined);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if ((current == null || current === '') && data?.length === 1 && data !== autoSelectedDataRef.current) {
|
|
43
|
+
autoSelectedDataRef.current = data;
|
|
44
|
+
onValueChangeRef.current(type, data[0].value);
|
|
45
|
+
}
|
|
46
|
+
}, [current, data, type]);
|
|
47
|
+
|
|
48
|
+
const resolvedPlaceholder = loading ? 'Loading…' : options.length === 0 ? 'No options available' : placeholder;
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<SelectField
|
|
52
|
+
{...fieldProps}
|
|
53
|
+
placeholder={resolvedPlaceholder}
|
|
54
|
+
options={options.map(({ value, label, secondaryLabel, icon }) => ({ value, label, secondaryLabel, icon }))}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
AsyncSelectField.displayName = 'Form.AsyncSelectField';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type AnyProperties } from '@dxos/echo/internal';
|
|
8
|
+
|
|
9
|
+
import { type FormFieldRendererProps } from '#types';
|
|
10
|
+
|
|
11
|
+
import { type Autofill } from '../../../../../annotations';
|
|
12
|
+
import { pickValues, useAsyncFieldEffect, useFormValues } from '../../../../../hooks';
|
|
13
|
+
import { TextField } from '../TextField';
|
|
14
|
+
|
|
15
|
+
export type AutofillFieldProps = FormFieldRendererProps<string> & {
|
|
16
|
+
/** Derives this field's value from the autofill's declared dependency fields (e.g. a sibling URL). */
|
|
17
|
+
autofill: Autofill;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An editable text field that pre-fills its value (debounced) from the fields the autofill declares in
|
|
22
|
+
* its `deps`, via an {@link Autofill}. Re-runs only when one of those values changes. The
|
|
23
|
+
* derived value is written only while the user has not typed their own (the field is empty or still
|
|
24
|
+
* holds the last auto-filled value), so a manual edit is never clobbered.
|
|
25
|
+
*/
|
|
26
|
+
export const AutofillField = ({ autofill, ...fieldProps }: AutofillFieldProps) => {
|
|
27
|
+
const values = useFormValues<AnyProperties>(AutofillField.displayName);
|
|
28
|
+
const subset = useMemo(() => pickValues(values, autofill.deps), [values, autofill.deps]);
|
|
29
|
+
const key = useMemo(() => JSON.stringify(subset), [subset]);
|
|
30
|
+
const { data } = useAsyncFieldEffect<string | undefined>(() => autofill.derive(subset), key);
|
|
31
|
+
|
|
32
|
+
// Read field state through refs so the write effect depends only on the derived `data` — `getValue`
|
|
33
|
+
// and `onValueChange` change identity on every value edit, and depending on them would re-fire the
|
|
34
|
+
// effect after it writes, re-applying the same value in an infinite loop.
|
|
35
|
+
const { type } = fieldProps;
|
|
36
|
+
const getValueRef = useRef(fieldProps.getValue);
|
|
37
|
+
getValueRef.current = fieldProps.getValue;
|
|
38
|
+
const onValueChangeRef = useRef(fieldProps.onValueChange);
|
|
39
|
+
onValueChangeRef.current = fieldProps.onValueChange;
|
|
40
|
+
const lastFilledRef = useRef<string | undefined>(undefined);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const current = getValueRef.current();
|
|
44
|
+
if (typeof data !== 'string' || data.length === 0) {
|
|
45
|
+
// Derivation stopped returning a value — clear the field if it still holds our last fill so stale
|
|
46
|
+
// auto-filled text doesn't persist after the source field is cleared.
|
|
47
|
+
if (lastFilledRef.current != null && current === lastFilledRef.current) {
|
|
48
|
+
lastFilledRef.current = undefined;
|
|
49
|
+
onValueChangeRef.current(type, '');
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (current === data) {
|
|
54
|
+
lastFilledRef.current = data;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Only fill while the user has not typed their own value (empty, or still our last fill).
|
|
58
|
+
if (current == null || current === '' || current === lastFilledRef.current) {
|
|
59
|
+
lastFilledRef.current = data;
|
|
60
|
+
onValueChangeRef.current(type, data);
|
|
61
|
+
}
|
|
62
|
+
}, [data, type]);
|
|
63
|
+
|
|
64
|
+
return <TextField {...fieldProps} />;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
AutofillField.displayName = 'Form.AutofillField';
|
|
@@ -8,7 +8,7 @@ import { Input, type SwitchProps } from '@dxos/react-ui';
|
|
|
8
8
|
|
|
9
9
|
import { type FormFieldRendererProps } from '#types';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { FormRow } from '../../FormRow';
|
|
12
12
|
|
|
13
13
|
export const BooleanField = ({ type, readonly, onValueChange, ...props }: FormFieldRendererProps<boolean>) => {
|
|
14
14
|
const handleChange = useCallback<NonNullable<SwitchProps['onCheckedChange']>>(
|
|
@@ -17,13 +17,12 @@ export const BooleanField = ({ type, readonly, onValueChange, ...props }: FormFi
|
|
|
17
17
|
);
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
|
-
<
|
|
20
|
+
<FormRow<boolean> readonly={readonly} {...props}>
|
|
21
21
|
{({ value }) => (
|
|
22
|
-
|
|
23
|
-
<div className='flex items-center px-0.5 h-8'>
|
|
22
|
+
<Input.Block>
|
|
24
23
|
<Input.Switch disabled={!!readonly} checked={value} onCheckedChange={handleChange} />
|
|
25
|
-
</
|
|
24
|
+
</Input.Block>
|
|
26
25
|
)}
|
|
27
|
-
</
|
|
26
|
+
</FormRow>
|
|
28
27
|
);
|
|
29
28
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type AnyProperties } from '@dxos/echo/internal';
|
|
8
|
+
import { Combobox } from '@dxos/react-ui-list';
|
|
9
|
+
|
|
10
|
+
import { type FormFieldRendererProps } from '#types';
|
|
11
|
+
|
|
12
|
+
import { type OptionsLookup, type OptionsLookupEntry } from '../../../../../annotations';
|
|
13
|
+
import { pickValues, useAsyncFieldEffect, useFormValues } from '../../../../../hooks';
|
|
14
|
+
import { FormRow } from '../../FormRow';
|
|
15
|
+
|
|
16
|
+
export type ComboboxFieldProps = FormFieldRendererProps<string> & {
|
|
17
|
+
/** Loads suggestions from the lookup's declared dependency fields (typically the field's own value). */
|
|
18
|
+
lookup: OptionsLookup;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An editable combobox built on the shared {@link Combobox} primitive (trigger + popover search list,
|
|
23
|
+
* the same family as `ObjectPicker`/`SearchList`). The trigger shows the current value; opening it reveals
|
|
24
|
+
* a search input that loads suggestions (debounced) via an {@link OptionsLookup} and filters
|
|
25
|
+
* them as you type. The literal typed text is offered as a fallback option at the bottom (deduped), so a
|
|
26
|
+
* value not among the suggestions can still be selected. No options are shown until something is typed.
|
|
27
|
+
*/
|
|
28
|
+
export const ComboboxField = ({ lookup, type, readonly, placeholder, onValueChange, ...props }: ComboboxFieldProps) => {
|
|
29
|
+
const values = useFormValues<AnyProperties>(ComboboxField.displayName);
|
|
30
|
+
const ownKey = props.jsonPath;
|
|
31
|
+
const [query, setQuery] = useState('');
|
|
32
|
+
|
|
33
|
+
// When the combobox searches on its own field (its name is among `deps`), feed the live query to the
|
|
34
|
+
// loader so remote suggestions track typing; otherwise the loader reads the committed sibling values.
|
|
35
|
+
const lookupValues = useMemo(() => {
|
|
36
|
+
const subset = pickValues(values, lookup.deps);
|
|
37
|
+
if (ownKey && lookup.deps.includes(ownKey)) {
|
|
38
|
+
subset[ownKey] = query;
|
|
39
|
+
}
|
|
40
|
+
return subset;
|
|
41
|
+
}, [values, lookup.deps, ownKey, query]);
|
|
42
|
+
const key = useMemo(() => JSON.stringify(lookupValues), [lookupValues]);
|
|
43
|
+
const { data } = useAsyncFieldEffect<readonly OptionsLookupEntry[]>(() => lookup.load(lookupValues), key);
|
|
44
|
+
|
|
45
|
+
const trimmed = query.trim();
|
|
46
|
+
const normalized = trimmed.toLowerCase();
|
|
47
|
+
// No options until something is typed; then filter the loaded suggestions by the input text.
|
|
48
|
+
const results = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
normalized.length === 0
|
|
51
|
+
? []
|
|
52
|
+
: (data ?? []).filter((option) => (option.label ?? option.value).toLowerCase().includes(normalized)),
|
|
53
|
+
[data, normalized],
|
|
54
|
+
);
|
|
55
|
+
const hasExact = results.some((option) => option.value === trimmed);
|
|
56
|
+
|
|
57
|
+
const handleValueChange = useCallback((next: string) => onValueChange(type, next), [onValueChange, type]);
|
|
58
|
+
const handleOpenChange = useCallback((open: boolean) => {
|
|
59
|
+
if (!open) {
|
|
60
|
+
setQuery('');
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<FormRow<string>
|
|
66
|
+
readonly={readonly}
|
|
67
|
+
renderStatic={(value) => <p className='truncate min-w-0'>{value ?? ''}</p>}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
{({ value = '' }) => (
|
|
71
|
+
<Combobox.Root
|
|
72
|
+
value={value}
|
|
73
|
+
onValueChange={handleValueChange}
|
|
74
|
+
onOpenChange={handleOpenChange}
|
|
75
|
+
placeholder={placeholder}
|
|
76
|
+
>
|
|
77
|
+
{/* Full-width trigger (default renders value/placeholder + caret) to match the other fields. */}
|
|
78
|
+
<Combobox.Trigger disabled={!!readonly} classNames='w-full' />
|
|
79
|
+
<Combobox.Portal>
|
|
80
|
+
{/* Keep the first result highlighted as the list changes while typing. */}
|
|
81
|
+
<Combobox.Content resetSelectionOnChange>
|
|
82
|
+
<Combobox.Input autoFocus value={query} onValueChange={setQuery} placeholder={placeholder} />
|
|
83
|
+
<Combobox.List>
|
|
84
|
+
{results.map((option) => (
|
|
85
|
+
<Combobox.Item
|
|
86
|
+
key={option.value}
|
|
87
|
+
value={option.value}
|
|
88
|
+
label={option.label ?? option.value}
|
|
89
|
+
suffix={option.secondaryLabel}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
{/* The literal typed text as a fallback option at the bottom, unless a suggestion already is it. */}
|
|
93
|
+
{normalized.length > 0 && !hasExact && <Combobox.Item value={trimmed} label={trimmed} />}
|
|
94
|
+
</Combobox.List>
|
|
95
|
+
<Combobox.Arrow />
|
|
96
|
+
</Combobox.Content>
|
|
97
|
+
</Combobox.Portal>
|
|
98
|
+
</Combobox.Root>
|
|
99
|
+
)}
|
|
100
|
+
</FormRow>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
ComboboxField.displayName = 'Form.ComboboxField';
|
|
@@ -10,7 +10,7 @@ import { Input } from '@dxos/react-ui';
|
|
|
10
10
|
|
|
11
11
|
import { type FormFieldRendererProps } from '#types';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { FormRow } from '../../FormRow';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Stored value shapes:
|
|
@@ -66,19 +66,19 @@ export const DateField = ({
|
|
|
66
66
|
);
|
|
67
67
|
|
|
68
68
|
return (
|
|
69
|
-
<
|
|
69
|
+
<FormRow<string> {...props} readonly={readonly} format={format}>
|
|
70
70
|
{({ value }) => {
|
|
71
71
|
switch (format) {
|
|
72
72
|
case Format.TypeFormat.Date:
|
|
73
73
|
return (
|
|
74
|
-
<div className='grid grid-cols-[minmax(0,1fr)_min-content] items-stretch
|
|
74
|
+
<div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch'>
|
|
75
75
|
<Input.Date
|
|
76
76
|
classNames='min-w-0 overflow-hidden'
|
|
77
|
-
disabled={
|
|
77
|
+
disabled={readonly}
|
|
78
78
|
value={value ?? ''}
|
|
79
79
|
onValueChange={handleSimpleChange}
|
|
80
80
|
/>
|
|
81
|
-
<Input.TriggerIcon
|
|
81
|
+
<Input.TriggerIcon />
|
|
82
82
|
</div>
|
|
83
83
|
);
|
|
84
84
|
case Format.TypeFormat.Time:
|
|
@@ -86,18 +86,18 @@ export const DateField = ({
|
|
|
86
86
|
case Format.TypeFormat.DateTime:
|
|
87
87
|
default:
|
|
88
88
|
return (
|
|
89
|
-
<div className='grid grid-cols-[minmax(0,1fr)_min-content] items-stretch
|
|
89
|
+
<div className='grid grid-cols-[minmax(0,1fr)_min-content] gap-1 items-stretch'>
|
|
90
90
|
<Input.DateTime
|
|
91
91
|
classNames='min-w-0 overflow-hidden'
|
|
92
|
-
disabled={
|
|
92
|
+
disabled={readonly}
|
|
93
93
|
value={isoToLocalDateTime(value)}
|
|
94
94
|
onValueChange={handleDateTimeChange}
|
|
95
95
|
/>
|
|
96
|
-
<Input.TriggerIcon
|
|
96
|
+
<Input.TriggerIcon />
|
|
97
97
|
</div>
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
}}
|
|
101
|
-
</
|
|
101
|
+
</FormRow>
|
|
102
102
|
);
|
|
103
103
|
};
|
|
@@ -11,21 +11,17 @@ import { safeParseFloat } from '@dxos/util';
|
|
|
11
11
|
import { translationKey } from '#translations';
|
|
12
12
|
import { type FormFieldRendererProps } from '#types';
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { FormRow } from '../../FormRow';
|
|
15
15
|
|
|
16
16
|
export const GeoPointField = ({
|
|
17
17
|
type,
|
|
18
|
-
label,
|
|
19
|
-
jsonPath,
|
|
20
18
|
readonly,
|
|
21
|
-
layout,
|
|
22
|
-
getStatus,
|
|
23
19
|
getValue,
|
|
24
20
|
onValueChange,
|
|
25
21
|
onBlur,
|
|
22
|
+
...props
|
|
26
23
|
}: FormFieldRendererProps<GeoPoint>) => {
|
|
27
24
|
const { t } = useTranslation(translationKey);
|
|
28
|
-
const { status, error } = getStatus();
|
|
29
25
|
const geoPoint = useMemo<GeoPoint>(() => getValue() ?? [0, 0], [getValue]);
|
|
30
26
|
const value = useMemo(() => GeoLocation.fromGeoPoint(geoPoint), [geoPoint]);
|
|
31
27
|
|
|
@@ -55,22 +51,23 @@ export const GeoPointField = ({
|
|
|
55
51
|
[type, getValue, onValueChange],
|
|
56
52
|
);
|
|
57
53
|
|
|
58
|
-
if ((readonly || layout === 'static') && !value?.latitude && !value?.longitude) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
54
|
return (
|
|
63
|
-
<
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
<FormRow<GeoPoint>
|
|
56
|
+
readonly={readonly}
|
|
57
|
+
getValue={getValue}
|
|
58
|
+
standalone
|
|
59
|
+
renderStatic={(point) => {
|
|
60
|
+
const location = GeoLocation.fromGeoPoint(point ?? [0, 0]);
|
|
61
|
+
// Treat a zero coordinate pair as empty (no meaningful location to display).
|
|
62
|
+
return !location.latitude && !location.longitude ? null : <LatLng {...location} />;
|
|
63
|
+
}}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
{({ presentation }) => (
|
|
70
67
|
<div className='grid grid-cols-2 gap-form-gap'>
|
|
71
68
|
<div>
|
|
72
69
|
<Input.Root>
|
|
73
|
-
{
|
|
70
|
+
{presentation.showLabel && <Input.Label>{t('latitude.label')}</Input.Label>}
|
|
74
71
|
<Input.TextInput
|
|
75
72
|
type='number'
|
|
76
73
|
step='0.00001'
|
|
@@ -86,7 +83,7 @@ export const GeoPointField = ({
|
|
|
86
83
|
</div>
|
|
87
84
|
<div>
|
|
88
85
|
<Input.Root>
|
|
89
|
-
{
|
|
86
|
+
{presentation.showLabel && <Input.Label>{t('longitude.label')}</Input.Label>}
|
|
90
87
|
<Input.TextInput
|
|
91
88
|
type='number'
|
|
92
89
|
step='0.00001'
|
|
@@ -102,7 +99,7 @@ export const GeoPointField = ({
|
|
|
102
99
|
</div>
|
|
103
100
|
</div>
|
|
104
101
|
)}
|
|
105
|
-
</
|
|
102
|
+
</FormRow>
|
|
106
103
|
);
|
|
107
104
|
};
|
|
108
105
|
|
|
@@ -61,7 +61,7 @@ const RefStory = () => {
|
|
|
61
61
|
return (
|
|
62
62
|
<TestLayout>
|
|
63
63
|
<Panel.Root>
|
|
64
|
-
<Panel.Content
|
|
64
|
+
<Panel.Content>
|
|
65
65
|
<Form.Root schema={TextNoteHolder} values={values as any} db={space.db}>
|
|
66
66
|
<Form.Viewport>
|
|
67
67
|
<Form.Content>
|
|
@@ -96,7 +96,7 @@ const EmptyRefStory = () => {
|
|
|
96
96
|
return (
|
|
97
97
|
<TestLayout>
|
|
98
98
|
<Panel.Root>
|
|
99
|
-
<Panel.Content
|
|
99
|
+
<Panel.Content>
|
|
100
100
|
<Form.Root schema={EmptyRefSchema} defaultValues={{}} db={space.db}>
|
|
101
101
|
<Form.Viewport>
|
|
102
102
|
<Form.Content>
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
6
|
import React, { useCallback, useMemo } from 'react';
|
|
7
7
|
|
|
8
|
-
import { type Database,
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { type Database, Ref } from '@dxos/echo';
|
|
9
|
+
import { Doc } from '@dxos/echo-doc';
|
|
10
|
+
import { IconButton, useTranslation } from '@dxos/react-ui';
|
|
11
11
|
import { Editor, useBasicMarkdownExtensions } from '@dxos/react-ui-editor';
|
|
12
12
|
import { Text } from '@dxos/schema';
|
|
13
13
|
import { createDataExtensions } from '@dxos/ui-editor';
|
|
@@ -15,10 +15,17 @@ import { createDataExtensions } from '@dxos/ui-editor';
|
|
|
15
15
|
import { translationKey } from '#translations';
|
|
16
16
|
import { type FormFieldRendererProps } from '#types';
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { FormRow } from '../../FormRow';
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
+
];
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
* Form field that edits a markdown value in a CodeMirror editor.
|
|
@@ -30,28 +37,30 @@ const editorClassNames =
|
|
|
30
37
|
export const MarkdownField = ({
|
|
31
38
|
type,
|
|
32
39
|
readonly,
|
|
33
|
-
label,
|
|
34
|
-
jsonPath,
|
|
35
40
|
placeholder,
|
|
36
|
-
layout,
|
|
37
41
|
db,
|
|
38
|
-
getStatus,
|
|
39
42
|
getValue,
|
|
40
43
|
onValueChange,
|
|
44
|
+
...props
|
|
41
45
|
}: FormFieldRendererProps) => {
|
|
42
|
-
const { status, error } = getStatus();
|
|
43
46
|
const isRef = Ref.isRefType(type);
|
|
44
|
-
const value = getValue();
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// The field value is a runtime union the form's value type can't express: a `Ref<Text>` when the
|
|
49
|
+
// property is a markdown ref, or a plain string when it is annotated markdown text. `isRef`
|
|
50
|
+
// selects the branch, so the casts below narrow the opaque value accordingly.
|
|
51
|
+
const renderStatic = (value: unknown) => {
|
|
52
|
+
if (isRef) {
|
|
53
|
+
const reference = value as Ref.Ref<any> | undefined;
|
|
54
|
+
return reference ? <RefStaticText reference={reference} /> : null;
|
|
55
|
+
}
|
|
56
|
+
return <p className='whitespace-pre-wrap'>{(value as string | undefined) ?? ''}</p>;
|
|
57
|
+
};
|
|
49
58
|
|
|
50
|
-
const renderEditor = () => {
|
|
59
|
+
const renderEditor = (value: unknown) => {
|
|
51
60
|
if (isRef) {
|
|
52
61
|
const reference = value as Ref.Ref<any> | undefined;
|
|
53
62
|
if (reference) {
|
|
54
|
-
return <RefMarkdownEditor reference={reference} placeholder={placeholder} />;
|
|
63
|
+
return <RefMarkdownEditor reference={reference} placeholder={placeholder} readonly={!!readonly} />;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
if (readonly) {
|
|
@@ -71,20 +80,10 @@ export const MarkdownField = ({
|
|
|
71
80
|
);
|
|
72
81
|
};
|
|
73
82
|
|
|
74
|
-
const renderStatic = () => {
|
|
75
|
-
if (isRef) {
|
|
76
|
-
const reference = value as Ref.Ref<any> | undefined;
|
|
77
|
-
return reference ? <RefStaticText reference={reference} /> : null;
|
|
78
|
-
}
|
|
79
|
-
return <p className='whitespace-pre-wrap'>{(value as string | undefined) ?? ''}</p>;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
83
|
return (
|
|
83
|
-
<
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
{layout === 'full' && <Input.Validation>{error}</Input.Validation>}
|
|
87
|
-
</Input.Root>
|
|
84
|
+
<FormRow readonly={readonly} getValue={getValue} renderStatic={renderStatic} {...props}>
|
|
85
|
+
{({ value }) => renderEditor(value)}
|
|
86
|
+
</FormRow>
|
|
88
87
|
);
|
|
89
88
|
};
|
|
90
89
|
|
|
@@ -94,21 +93,23 @@ const RefStaticText = ({ reference }: { reference: Ref.Ref<any> }) => {
|
|
|
94
93
|
if (!text?.content) {
|
|
95
94
|
return null;
|
|
96
95
|
}
|
|
96
|
+
|
|
97
97
|
return <p className='whitespace-pre-wrap'>{text.content}</p>;
|
|
98
98
|
};
|
|
99
99
|
|
|
100
100
|
type RefMarkdownEditorProps = {
|
|
101
101
|
reference: Ref.Ref<any>;
|
|
102
102
|
placeholder?: string;
|
|
103
|
+
readonly?: boolean;
|
|
103
104
|
};
|
|
104
105
|
|
|
105
|
-
const RefMarkdownEditor = ({ reference, placeholder }: RefMarkdownEditorProps) => {
|
|
106
|
+
const RefMarkdownEditor = ({ reference, placeholder, readonly }: RefMarkdownEditorProps) => {
|
|
106
107
|
const text = useAtomValue(useMemo(() => reference.atom, [reference]));
|
|
107
108
|
const dataExtensions = useMemo(
|
|
108
|
-
() => (text ? [createDataExtensions({ id: reference.uri, text:
|
|
109
|
+
() => (text ? [createDataExtensions({ id: reference.uri, text: Doc.createAccessor(text, ['content']) })] : []),
|
|
109
110
|
[text, reference],
|
|
110
111
|
);
|
|
111
|
-
const extensions = useBasicMarkdownExtensions({ placeholder, extensions: dataExtensions });
|
|
112
|
+
const extensions = useBasicMarkdownExtensions({ placeholder, readonly, extensions: dataExtensions });
|
|
112
113
|
if (!text) {
|
|
113
114
|
return null;
|
|
114
115
|
}
|
|
@@ -128,7 +129,7 @@ type StringMarkdownEditorProps = {
|
|
|
128
129
|
};
|
|
129
130
|
|
|
130
131
|
const StringMarkdownEditor = ({ value, placeholder, readonly, onChange }: StringMarkdownEditorProps) => {
|
|
131
|
-
const extensions = useBasicMarkdownExtensions({ placeholder });
|
|
132
|
+
const extensions = useBasicMarkdownExtensions({ placeholder, readonly });
|
|
132
133
|
const handleChange = useCallback((next: string) => onChange(next), [onChange]);
|
|
133
134
|
|
|
134
135
|
return (
|
|
@@ -151,18 +152,16 @@ type CreateTextButtonProps = {
|
|
|
151
152
|
|
|
152
153
|
const CreateTextButton = ({ db, disabled, onCreate }: CreateTextButtonProps) => {
|
|
153
154
|
const { t } = useTranslation(translationKey);
|
|
155
|
+
|
|
154
156
|
const handleClick = useCallback(() => {
|
|
155
157
|
if (!db) {
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
158
|
-
|
|
159
|
-
onCreate(Ref.make(
|
|
160
|
+
|
|
161
|
+
onCreate(Ref.make(db.add(Text.make())));
|
|
160
162
|
}, [db, onCreate]);
|
|
161
163
|
|
|
162
164
|
return (
|
|
163
|
-
<
|
|
164
|
-
<Icon icon='ph--plus--regular' size={4} />
|
|
165
|
-
<span>{t('create-text.label')}</span>
|
|
166
|
-
</Button>
|
|
165
|
+
<IconButton icon='ph--plus--regular' label={t('create-text.label')} onClick={handleClick} disabled={disabled} />
|
|
167
166
|
);
|
|
168
167
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
|
+
import { describe, test } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { getNumericConstraints } from './NumberField';
|
|
9
|
+
|
|
10
|
+
describe('getNumericConstraints', () => {
|
|
11
|
+
test('reads min/max from Schema.between (bounds live on separate nested refinements)', ({ expect }) => {
|
|
12
|
+
const { ast } = Schema.Number.pipe(Schema.between(0, 23));
|
|
13
|
+
expect(getNumericConstraints(ast)).toEqual({ min: 0, max: 23, integer: false });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('detects integer from Schema.int', ({ expect }) => {
|
|
17
|
+
const { ast } = Schema.Number.pipe(Schema.int(), Schema.between(0, 23));
|
|
18
|
+
expect(getNumericConstraints(ast)).toEqual({ min: 0, max: 23, integer: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('returns empty constraints for an unconstrained number', ({ expect }) => {
|
|
22
|
+
expect(getNumericConstraints(Schema.Number.ast)).toEqual({ integer: false });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('aggregates stacked bounds to the strictest (intersection semantics)', ({ expect }) => {
|
|
26
|
+
const { ast } = Schema.Number.pipe(Schema.greaterThanOrEqualTo(5), Schema.greaterThanOrEqualTo(10));
|
|
27
|
+
expect(getNumericConstraints(ast)).toEqual({ min: 10, max: undefined, integer: false });
|
|
28
|
+
});
|
|
29
|
+
});
|