@dxos/react-ui-form 0.10.0 → 0.11.1
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
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as Equal from 'effect/Equal';
|
|
6
6
|
import type * as Schema from 'effect/Schema';
|
|
7
7
|
import type * as SchemaAST from 'effect/SchemaAST';
|
|
8
|
+
import * as Utils from 'effect/Utils';
|
|
8
9
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
10
|
|
|
11
|
+
import { Ref } from '@dxos/echo';
|
|
10
12
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
13
|
import { SchemaEx } from '@dxos/effect';
|
|
12
14
|
import { log } from '@dxos/log';
|
|
@@ -26,12 +28,16 @@ export interface FormHandlerProps<T extends AnyProperties> {
|
|
|
26
28
|
schema?: Schema.Schema<T, any>;
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
|
-
*
|
|
31
|
+
* Source values. Fields the user is not editing reflect this value; in-progress edits (including intermediate
|
|
32
|
+
* invalid values) are held locally and never snap back until they are committed and the source catches up. Persist
|
|
33
|
+
* committed values from `onValuesChanged`/`onSave`. The form re-renders when a new value is passed, so to reflect
|
|
34
|
+
* external/remote mutations pass a reactive source (e.g. a `useObject` snapshot that changes on mutation) rather
|
|
35
|
+
* than a raw live object — a render-time read of an ECHO object does not itself subscribe to it.
|
|
30
36
|
*/
|
|
31
37
|
values?: Partial<T>;
|
|
32
38
|
|
|
33
39
|
/**
|
|
34
|
-
*
|
|
40
|
+
* Seeds an uncontrolled form once at mount when `values` is not provided; not reconciled thereafter.
|
|
35
41
|
*/
|
|
36
42
|
defaultValues?: Partial<T>;
|
|
37
43
|
|
|
@@ -127,23 +133,21 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
127
133
|
const [errors, setErrors] = useState<Record<SchemaEx.JsonPath, string>>({});
|
|
128
134
|
const [saving, setSaving] = useState(false);
|
|
129
135
|
const defaultValues = useDefaultValue<Partial<T>>(defaultValuesProp, () => ({}));
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// TODO(wittjosiah): Upgrade @radix-ui/react-use-controllable-state.
|
|
146
|
-
const values = values$ as Partial<T>;
|
|
136
|
+
|
|
137
|
+
// The source the form reads from for every field the user is not actively editing. The form is a pure function of
|
|
138
|
+
// this value and re-renders when the parent passes a new one; to reflect external/remote mutations the parent must
|
|
139
|
+
// supply a reactive source (e.g. a `useObject` snapshot that changes on mutation). `defaultValues` seeds an
|
|
140
|
+
// uncontrolled form once when no `values` are provided.
|
|
141
|
+
const source = valuesProp ?? defaultValues;
|
|
142
|
+
|
|
143
|
+
// Sparse local edits keyed by json-path. A path stays here — holding even an intermediate invalid value, so it
|
|
144
|
+
// never snaps back — until the source catches up to it (typically once a valid edit has been persisted and echoed
|
|
145
|
+
// back through `values`). Un-edited paths are never buffered, so they always reflect the current source.
|
|
146
|
+
const [overrides, setOverrides] = useState<Record<SchemaEx.JsonPath, unknown>>({});
|
|
147
|
+
|
|
148
|
+
// Merged view (source overlaid with local edits) for whole-form consumers: validation, save, and context/debug.
|
|
149
|
+
// Computed each render (not memoized) so it stays fresh when the source value changes.
|
|
150
|
+
const values = applyOverrides(source, overrides);
|
|
147
151
|
|
|
148
152
|
// Validate.
|
|
149
153
|
const validate = useCallback(
|
|
@@ -176,6 +180,22 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
176
180
|
}
|
|
177
181
|
}, [validate, values, schemaChanged]);
|
|
178
182
|
|
|
183
|
+
// Drop an override once the source has caught up to it (the committed value has been echoed back), so the field
|
|
184
|
+
// returns to reflecting the source. Edits the source has not yet reflected — including intermediate invalid values,
|
|
185
|
+
// and in-progress edits to a field that changed remotely — are kept, so the form never snaps back. Runs on every
|
|
186
|
+
// render (a new source value re-renders the form), reconciling against whatever the source currently holds.
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const reconciled = (Object.keys(overrides) as SchemaEx.JsonPath[]).filter((path) =>
|
|
189
|
+
valuesEqual(SchemaEx.getValue(source, path), overrides[path]),
|
|
190
|
+
);
|
|
191
|
+
if (reconciled.length === 0) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
setOverrides((prev) => omitPaths(prev, reconciled));
|
|
195
|
+
setChanged((prev) => omitPaths(prev, reconciled));
|
|
196
|
+
setTouched((prev) => omitPaths(prev, reconciled));
|
|
197
|
+
});
|
|
198
|
+
|
|
179
199
|
const isValid = useMemo(() => {
|
|
180
200
|
return Object.keys(errors).length === 0;
|
|
181
201
|
}, [errors]);
|
|
@@ -245,9 +265,14 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
245
265
|
|
|
246
266
|
const getValue = useCallback<FormHandler<T>['getValue']>(
|
|
247
267
|
(path) => {
|
|
248
|
-
|
|
268
|
+
const jsonPath = SchemaEx.createJsonPath(path);
|
|
269
|
+
if (Object.prototype.hasOwnProperty.call(overrides, jsonPath)) {
|
|
270
|
+
return overrides[jsonPath] as any;
|
|
271
|
+
}
|
|
272
|
+
// Un-edited fields reflect the current source value.
|
|
273
|
+
return SchemaEx.getValue(source, jsonPath);
|
|
249
274
|
},
|
|
250
|
-
[
|
|
275
|
+
[source, overrides],
|
|
251
276
|
);
|
|
252
277
|
|
|
253
278
|
const onValueChange = useCallback<FormHandler<T>['onValueChange']>(
|
|
@@ -266,25 +291,25 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
266
291
|
parsedValue = undefined;
|
|
267
292
|
}
|
|
268
293
|
|
|
269
|
-
//
|
|
270
|
-
|
|
294
|
+
// Hold the edit locally as an override (including invalid values, so it never snaps back); un-edited paths keep
|
|
295
|
+
// tracking the live source. The prune effect drops the override once the source catches up.
|
|
296
|
+
const newOverrides = { ...overrides, [jsonPath]: parsedValue };
|
|
297
|
+
setOverrides(newOverrides);
|
|
271
298
|
|
|
272
299
|
// TODO(burdon): Check value has changed from original.
|
|
273
300
|
const newChanged = { [jsonPath]: true };
|
|
274
301
|
setChanged((prev) => ({ ...prev, ...newChanged }));
|
|
275
302
|
|
|
303
|
+
// Merged values for validation and the callback. Form never mutates the source; the parent persists.
|
|
304
|
+
const newValues = applyOverrides(source, newOverrides);
|
|
305
|
+
|
|
276
306
|
// Validate.
|
|
277
307
|
const isValid = validate(newValues);
|
|
278
308
|
|
|
279
|
-
// Notify parent; parent is responsible for
|
|
309
|
+
// Notify parent; parent is responsible for persisting the change (typically once valid).
|
|
280
310
|
onValuesChanged?.(newValues, { isValid, changed: newChanged });
|
|
281
|
-
|
|
282
|
-
// For uncontrolled mode, update internal state. Controlled mode relies on parent to pass new values.
|
|
283
|
-
if (valuesProp === undefined) {
|
|
284
|
-
setValues(newValues);
|
|
285
|
-
}
|
|
286
311
|
},
|
|
287
|
-
[
|
|
312
|
+
[source, overrides, validate, onValuesChanged],
|
|
288
313
|
);
|
|
289
314
|
|
|
290
315
|
const onBlur = useCallback(
|
|
@@ -367,6 +392,68 @@ const mergeAtPath = (obj: any, path: readonly (string | number)[], value: any):
|
|
|
367
392
|
return { ...obj, [head]: child };
|
|
368
393
|
};
|
|
369
394
|
|
|
395
|
+
/** Overlays sparse json-path edits onto a base value without mutating it; un-edited paths keep the base reference. */
|
|
396
|
+
const applyOverrides = <T>(base: Partial<T>, overrides: Record<SchemaEx.JsonPath, unknown>): Partial<T> => {
|
|
397
|
+
let result: Partial<T> = base;
|
|
398
|
+
for (const path of Object.keys(overrides) as SchemaEx.JsonPath[]) {
|
|
399
|
+
result = mergeAtPath(result, SchemaEx.splitJsonPath(path), overrides[path]) as Partial<T>;
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Copied from `@dxos/echo` (internal `Obj.valuesEqual`): references compare by target URI, arrays and plain
|
|
405
|
+
// object-shaped property bags (excluding `id`) compare recursively, and leaves fall back to Effect `Equal.equals`
|
|
406
|
+
// inside a structural region. Effect's `Schema.equivalence` is not a safe substitute — it returns false-positive
|
|
407
|
+
// equality for dynamic/union/ref-array schemas, which would silently prune edits.
|
|
408
|
+
// TODO(wittjosiah): Factor out into a shared util rather than duplicating echo's internal implementation.
|
|
409
|
+
const valuesEqual = (left: unknown, right: unknown): boolean => {
|
|
410
|
+
if (left === right) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
if (left === null || right === null) {
|
|
414
|
+
return left === right;
|
|
415
|
+
}
|
|
416
|
+
if (typeof left !== 'object' || typeof right !== 'object') {
|
|
417
|
+
return Utils.structuralRegion(() => Equal.equals(left, right));
|
|
418
|
+
}
|
|
419
|
+
if (Ref.isRef(left) && Ref.isRef(right)) {
|
|
420
|
+
return left.uri === right.uri;
|
|
421
|
+
}
|
|
422
|
+
if (Ref.isRef(left) || Ref.isRef(right)) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
426
|
+
if (left.length !== right.length) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
return left.every((value, index) => valuesEqual(value, right[index]));
|
|
430
|
+
}
|
|
431
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
const leftRecord = left as Record<string, unknown>;
|
|
435
|
+
const rightRecord = right as Record<string, unknown>;
|
|
436
|
+
const keys = new Set([
|
|
437
|
+
...Object.keys(leftRecord).filter((key) => key !== 'id'),
|
|
438
|
+
...Object.keys(rightRecord).filter((key) => key !== 'id'),
|
|
439
|
+
]);
|
|
440
|
+
for (const key of keys) {
|
|
441
|
+
if (!valuesEqual(leftRecord[key], rightRecord[key])) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return true;
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
/** Returns a copy of `record` without the given json-path keys. */
|
|
449
|
+
const omitPaths = <V>(
|
|
450
|
+
record: Record<SchemaEx.JsonPath, V>,
|
|
451
|
+
paths: SchemaEx.JsonPath[],
|
|
452
|
+
): Record<SchemaEx.JsonPath, V> => {
|
|
453
|
+
const omit = new Set<string>(paths);
|
|
454
|
+
return Object.fromEntries(Object.entries(record).filter(([key]) => !omit.has(key))) as Record<SchemaEx.JsonPath, V>;
|
|
455
|
+
};
|
|
456
|
+
|
|
370
457
|
const flatMap = (errors: ValidationError[]) => {
|
|
371
458
|
return errors.reduce(
|
|
372
459
|
(result, { path, message }) => {
|
package/src/translations.ts
CHANGED
|
@@ -63,6 +63,7 @@ export const translations = [
|
|
|
63
63
|
'format.json.label': 'JSON',
|
|
64
64
|
'format.lnglat.label': 'Geopoint',
|
|
65
65
|
'format.markdown.label': 'Markdown',
|
|
66
|
+
'format.password.label': 'Password',
|
|
66
67
|
'format.percent.label': 'Percent',
|
|
67
68
|
'format.ref.label': 'Reference',
|
|
68
69
|
'format.regex.label': 'RegExp',
|
package/src/types.ts
CHANGED
|
@@ -158,6 +158,11 @@ export type RefFieldDataProps = {
|
|
|
158
158
|
export type FormFieldOptions = {
|
|
159
159
|
db?: Database.Database;
|
|
160
160
|
readonly?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* When `readonly`, omit fields whose value is empty (an empty read-only row is just noise).
|
|
163
|
+
* Defaults to `true`; set `false` to keep every schema field visible as a static row.
|
|
164
|
+
*/
|
|
165
|
+
hideEmpty?: boolean;
|
|
161
166
|
layout?: FormPresentation;
|
|
162
167
|
projection?: ProjectionModel;
|
|
163
168
|
fieldMap?: FormFieldMap;
|