@dxos/react-ui-form 0.13.0 → 0.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-form",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Data components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -51,24 +51,24 @@
51
51
  "@codemirror/view": "^6.43.13",
52
52
  "@effect/atom-react": "4.0.0",
53
53
  "date-fns": "^3.6.0",
54
- "@dxos/async": "0.13.0",
55
- "@dxos/echo-doc": "0.13.0",
56
- "@dxos/echo": "0.13.0",
57
- "@dxos/echo-react": "0.13.0",
58
- "@dxos/effect": "0.13.0",
59
- "@dxos/invariant": "0.13.0",
60
- "@dxos/keys": "0.13.0",
61
- "@dxos/lit-ui": "0.13.0",
62
- "@dxos/log": "0.13.0",
63
- "@dxos/react-hooks": "0.13.0",
64
- "@dxos/react-ui-editor": "0.13.0",
65
- "@dxos/react-ui-list": "0.13.0",
66
- "@dxos/react-ui-query": "0.13.0",
67
- "@dxos/schema": "0.13.0",
68
- "@dxos/ui": "0.13.0",
69
- "@dxos/ui-editor": "0.13.0",
70
- "@dxos/ui-types": "0.13.0",
71
- "@dxos/util": "0.13.0"
54
+ "@dxos/async": "0.14.0",
55
+ "@dxos/echo": "0.14.0",
56
+ "@dxos/echo-react": "0.14.0",
57
+ "@dxos/echo-doc": "0.14.0",
58
+ "@dxos/effect": "0.14.0",
59
+ "@dxos/invariant": "0.14.0",
60
+ "@dxos/keys": "0.14.0",
61
+ "@dxos/log": "0.14.0",
62
+ "@dxos/react-hooks": "0.14.0",
63
+ "@dxos/react-ui-editor": "0.14.0",
64
+ "@dxos/react-ui-list": "0.14.0",
65
+ "@dxos/react-ui-query": "0.14.0",
66
+ "@dxos/schema": "0.14.0",
67
+ "@dxos/lit-ui": "0.14.0",
68
+ "@dxos/ui-editor": "0.14.0",
69
+ "@dxos/ui": "0.14.0",
70
+ "@dxos/util": "0.14.0",
71
+ "@dxos/ui-types": "0.14.0"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@storybook/react-vite": "^10.6.0",
@@ -78,18 +78,18 @@
78
78
  "react": "~19.2.7",
79
79
  "react-dom": "~19.2.7",
80
80
  "vite": "^8.2.2",
81
- "@dxos/react-client": "0.13.0",
82
- "@dxos/react-ui": "0.13.0",
83
- "@dxos/react-ui-syntax-highlighter": "0.13.0",
84
- "@dxos/types": "0.13.0",
85
- "@dxos/ui-theme": "0.13.0"
81
+ "@dxos/react-client": "0.14.0",
82
+ "@dxos/react-ui": "0.14.0",
83
+ "@dxos/react-ui-syntax-highlighter": "0.14.0",
84
+ "@dxos/types": "0.14.0",
85
+ "@dxos/ui-theme": "0.14.0"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "effect": "4.0.0",
89
89
  "react": "~19.2.7",
90
90
  "react-dom": "~19.2.7",
91
- "@dxos/react-ui": "^0.13.0",
92
- "@dxos/ui-theme": "^0.13.0"
91
+ "@dxos/react-ui": "^0.14.0",
92
+ "@dxos/ui-theme": "^0.14.0"
93
93
  },
94
94
  "publishConfig": {
95
95
  "access": "public"
@@ -72,7 +72,14 @@ export const FormViewport = Util.composable<HTMLDivElement, FormViewportProps>(
72
72
  </Panel.Body>
73
73
  </Panel.Root>
74
74
  ) : (
75
- <Layout.Container {...props} gutter={gutter ?? defaultGutter ?? 'sm'} width={documentWidth} ref={forwardedRef}>
75
+ // Padded on the block axis as the scrolling form is, so a form in a host that scrolls it still ends a gutter in.
76
+ <Layout.Container
77
+ {...props}
78
+ gutter={gutter ?? defaultGutter ?? 'sm'}
79
+ width={documentWidth}
80
+ padBlock
81
+ ref={forwardedRef}
82
+ >
76
83
  {children}
77
84
  </Layout.Container>
78
85
  );
@@ -144,6 +144,8 @@ export const ObjectPicker = ({
144
144
  <Combobox.Root
145
145
  items={items}
146
146
  loading={loading}
147
+ // Pinned to the field's width: object labels (or URIs) can be long enough to widen the popup past the form.
148
+ positioning={{ sameWidth: true }}
147
149
  open={open}
148
150
  onOpenChange={handleOpenChange}
149
151
  value={value ? [value] : []}
@@ -32,6 +32,7 @@ export const RefField = ({
32
32
  presentation,
33
33
  getValue,
34
34
  onValueChange,
35
+ onBlur,
35
36
  createInitialValuePath,
36
37
  createFieldMap,
37
38
  createOptionIcon,
@@ -67,10 +68,15 @@ export const RefField = ({
67
68
  const created = await create(values);
68
69
  if (created) {
69
70
  onValueChange(type, Ref.make(created));
71
+ onBlur();
70
72
  }
71
73
  })
72
74
  }
73
- onSelect={(id) => onValueChange(type, id ? Ref.fromURI(URI.make(id)) : undefined)}
75
+ // A pick is the whole edit, so it commits as a select does; without the blur an auto-saving form never saves it.
76
+ onSelect={(id) => {
77
+ onValueChange(type, id ? Ref.fromURI(URI.make(id)) : undefined);
78
+ onBlur();
79
+ }}
74
80
  />
75
81
  );
76
82
  };
@@ -5,7 +5,9 @@
5
5
  import { act, renderHook } from '@testing-library/react';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import * as Struct from 'effect/Struct';
8
- import { describe, test } from 'vitest';
8
+ import { describe, onTestFinished, test } from 'vitest';
9
+
10
+ import { type LogEntry, type LogProcessor, log } from '@dxos/log';
9
11
 
10
12
  import { useFormHandler } from './useFormHandler.ts';
11
13
 
@@ -101,3 +103,27 @@ describe('useFormHandler field overrides', () => {
101
103
  expect(result.current.isValid).toBe(true);
102
104
  });
103
105
  });
106
+
107
+ describe('useFormHandler logging', () => {
108
+ test('never writes field values to the log', ({ expect }) => {
109
+ const entries: LogEntry[] = [];
110
+ const capture: LogProcessor = (_config, entry) => {
111
+ entries.push(entry);
112
+ };
113
+ onTestFinished(log.addProcessor(capture));
114
+
115
+ // Stands in for a pasted credential; any field may hold one, so no field's value may be logged.
116
+ const secret = 'placeholder-secret-0123456789';
117
+ const { result } = renderHook(() => useFormHandler<Values>({ schema, values: { name: 'Alice', city: 'NYC' } }));
118
+ act(() => result.current.onValueChange(['city'], stringAst, secret));
119
+ act(() => result.current.onValueChange(['name'], stringAst, ''));
120
+ expect(result.current.getValue(['city'])).toBe(secret);
121
+
122
+ const messages = entries.map((entry) => entry.message);
123
+ expect(messages).toContain('onValueChange');
124
+ expect(messages).toContain('validate');
125
+ for (const entry of entries) {
126
+ expect(JSON.stringify({ message: entry.message, context: entry.computedContext })).not.toContain(secret);
127
+ }
128
+ });
129
+ });
@@ -170,7 +170,6 @@ export const useFormHandler = <T extends AnyProperties>({
170
170
  }
171
171
 
172
172
  let errors: ValidationError[] = validateSchema(schema, values) ?? [];
173
- log('validate', { values, errors });
174
173
  if (errors.length === 0 && onValidate) {
175
174
  const validatedValues = values as T;
176
175
  errors = onValidate(validatedValues) ?? [];
@@ -178,6 +177,8 @@ export const useFormHandler = <T extends AnyProperties>({
178
177
 
179
178
  setErrors(flatMap(errors));
180
179
  const valid = errors.length === 0;
180
+ // Never log values or error messages: fields may hold secrets (passwords, API keys) and messages can echo them.
181
+ log('validate', { valid, errorPaths: errors.map((error) => error.path) });
181
182
  return valid;
182
183
  },
183
184
  [schema, onValidate],
@@ -303,7 +304,7 @@ export const useFormHandler = <T extends AnyProperties>({
303
304
 
304
305
  const onValueChange = useCallback<FormHandler<T>['onValueChange']>(
305
306
  (path, type, value) => {
306
- log('onValueChange', { path, value });
307
+ log('onValueChange', { path });
307
308
 
308
309
  const jsonPath = SchemaEx.createJsonPath(path);
309
310
  const pathArray = path;