@dxos/react-ui-form 0.7.5-feature-compute.4d9d99a → 0.7.5-labs.401163d

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.
Files changed (70) hide show
  1. package/dist/lib/browser/index.mjs +881 -556
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +910 -596
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +881 -556
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  11. package/dist/types/src/components/Form/ArrayField.d.ts +16 -0
  12. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -0
  13. package/dist/types/src/components/Form/Defaults.d.ts +6 -6
  14. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.d.ts +12 -21
  16. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.stories.d.ts +2 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormActions.d.ts +7 -0
  20. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -0
  21. package/dist/types/src/components/Form/FormContent.d.ts +26 -0
  22. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormContext.d.ts +23 -0
  24. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/Input.d.ts +5 -6
  26. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -2
  28. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +4 -0
  30. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +7 -0
  32. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -2
  34. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  35. package/dist/types/src/components/Form/custom/index.d.ts +1 -0
  36. package/dist/types/src/components/Form/custom/index.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/factory.d.ts +2 -2
  38. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/index.d.ts +2 -0
  40. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  41. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/deprecated/FormInput.d.ts +5 -0
  43. package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useForm.d.ts +10 -10
  45. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  46. package/dist/types/src/translations.d.ts +6 -0
  47. package/dist/types/src/translations.d.ts.map +1 -1
  48. package/dist/types/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +25 -24
  50. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  51. package/src/components/FieldEditor/FieldEditor.tsx +7 -6
  52. package/src/components/Form/ArrayField.tsx +111 -0
  53. package/src/components/Form/Defaults.tsx +78 -36
  54. package/src/components/Form/Form.stories.tsx +15 -5
  55. package/src/components/Form/Form.tsx +50 -355
  56. package/src/components/Form/FormActions.tsx +41 -0
  57. package/src/components/Form/FormContent.tsx +170 -0
  58. package/src/components/Form/FormContext.tsx +92 -0
  59. package/src/components/Form/Input.tsx +5 -6
  60. package/src/components/Form/custom/GeoPointInput.tsx +6 -15
  61. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +58 -0
  62. package/src/components/Form/custom/SelectOptionsInput.tsx +190 -0
  63. package/src/components/Form/custom/TupleInput.tsx +5 -9
  64. package/src/components/Form/custom/index.ts +1 -0
  65. package/src/components/Form/factory.tsx +5 -6
  66. package/src/components/Form/index.ts +3 -0
  67. package/src/components/ViewEditor/ViewEditor.tsx +7 -4
  68. package/src/components/deprecated/FormInput.tsx +20 -7
  69. package/src/hooks/useForm.ts +60 -47
  70. package/src/translations.ts +8 -0
@@ -2,10 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
5
+ import { useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
- import { type BaseObject, type PropertyKey, getValue, setValue } from '@dxos/echo-schema';
8
- import { type SimpleType, type S, type JsonPath, AST } from '@dxos/effect';
7
+ import { type BaseObject, getValue, setValue } from '@dxos/echo-schema';
8
+ import { AST, type S, type SimpleType, type JsonPath, createJsonPath, fromEffectValidationPath } from '@dxos/effect';
9
9
  import { invariant } from '@dxos/invariant';
10
10
  import { log } from '@dxos/log';
11
11
  import { validateSchema, type ValidationError } from '@dxos/schema';
@@ -20,20 +20,21 @@ export type FormHandler<T extends BaseObject> = {
20
20
  //
21
21
 
22
22
  values: Partial<T>;
23
- errors: Record<PropertyKey<T>, string>;
24
- touched: Record<PropertyKey<T>, boolean>;
25
- changed: Record<PropertyKey<T>, boolean>;
23
+ errors: Record<JsonPath, string>;
24
+ touched: Record<JsonPath, boolean>;
25
+ changed: Record<JsonPath, boolean>;
26
26
  canSave: boolean;
27
+ formIsValid: boolean;
27
28
  handleSave: () => void;
28
29
 
29
30
  //
30
31
  // Form input component helpers.
31
32
  //
32
33
 
33
- getStatus: (property: PropertyKey<T>) => { status?: 'error'; error?: string };
34
- getValue: <V>(property: PropertyKey<T>) => V | undefined;
35
- onValueChange: <V>(property: PropertyKey<T>, type: SimpleType, value: V) => void;
36
- onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
34
+ getStatus: (path: string | (string | number)[]) => { status?: 'error'; error?: string };
35
+ getValue: <V>(path: (string | number)[]) => V | undefined;
36
+ onValueChange: <V>(path: (string | number)[], type: SimpleType, value: V) => void;
37
+ onTouched: (path: (string | number)[]) => void;
37
38
  };
38
39
 
39
40
  /**
@@ -95,16 +96,15 @@ export const useForm = <T extends BaseObject>({
95
96
  setValues(initialValues);
96
97
  }, [initialValues]);
97
98
 
98
- const [touched, setTouched] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
99
- const [changed, setChanged] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
100
- const [errors, setErrors] = useState<Record<PropertyKey<T>, string>>({} as Record<PropertyKey<T>, string>);
99
+ const [touched, setTouched] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
100
+ const [changed, setChanged] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
101
+ const [errors, setErrors] = useState<Record<JsonPath, string>>({});
101
102
  const [saving, setSaving] = useState(false);
102
103
 
103
104
  //
104
105
  // Validation.
105
106
  //
106
107
 
107
- // TODO(burdon): Validate each property separately.
108
108
  const validate = useCallback(
109
109
  (values: Partial<T>): values is T => {
110
110
  let errors: ValidationError[] = validateSchema(schema, values) ?? [];
@@ -130,6 +130,10 @@ export const useForm = <T extends BaseObject>({
130
130
  }
131
131
  }, [values, schemaDirty]);
132
132
 
133
+ const formIsValid = useMemo(() => {
134
+ return Object.keys(errors).length === 0;
135
+ }, [errors]);
136
+
133
137
  /**
134
138
  * NOTE: We can submit if there is no touched field that has an error.
135
139
  * Basically, if there's a validation message visible in the form, submit should be disabled.
@@ -137,8 +141,15 @@ export const useForm = <T extends BaseObject>({
137
141
  const canSave = useMemo(
138
142
  () =>
139
143
  !saving &&
140
- Object.keys(values).every((property) => touched[property as JsonPath] === false || !errors[property as JsonPath]),
141
- [values, touched, errors, saving],
144
+ // Check if any error paths that are touched have errors
145
+ !Object.entries(touched).some(
146
+ ([path, isTouched]) =>
147
+ isTouched &&
148
+ Object.keys(errors).some(
149
+ (errorPath) => errorPath === path || errorPath.startsWith(`${path}.`) || errorPath.startsWith(`${path}[`),
150
+ ),
151
+ ),
152
+ [touched, errors, saving],
142
153
  );
143
154
 
144
155
  const handleSave = useCallback(async () => {
@@ -157,19 +168,27 @@ export const useForm = <T extends BaseObject>({
157
168
  //
158
169
 
159
170
  const getStatus = useCallback<FormHandler<T>['getStatus']>(
160
- (property: PropertyKey<T>) => ({
161
- status: errors[property] ? 'error' : undefined,
162
- error: errors[property] ? errors[property] : undefined,
163
- }),
164
- [touched, errors],
171
+ (path) => {
172
+ const jsonPath = Array.isArray(path) ? createJsonPath(path) : path;
173
+ const matchingError = Object.entries(errors).find(
174
+ ([errorPath]) =>
175
+ errorPath === jsonPath || errorPath.startsWith(`${jsonPath}.`) || errorPath.startsWith(`${jsonPath}[`),
176
+ );
177
+
178
+ return {
179
+ status: matchingError ? 'error' : undefined,
180
+ error: matchingError ? matchingError[1] : undefined,
181
+ };
182
+ },
183
+ [errors],
165
184
  );
166
185
 
167
- // TODO(burdon): Use path to extract hierarchical value.
168
- const getFormValue = <V>(property: PropertyKey<T>): V | undefined => {
169
- return getValue(values, property as any as JsonPath);
186
+ const getFormValue = <V>(path: (string | number)[]): V | undefined => {
187
+ return getValue(values, createJsonPath(path));
170
188
  };
171
189
 
172
- const onValueChange = (property: PropertyKey<T>, type: SimpleType, value: any) => {
190
+ const onValueChange = (path: (string | number)[], type: SimpleType, value: any) => {
191
+ const jsonPath = createJsonPath(path);
173
192
  let parsedValue = value;
174
193
  try {
175
194
  if (type === 'number') {
@@ -180,9 +199,9 @@ export const useForm = <T extends BaseObject>({
180
199
  parsedValue = undefined;
181
200
  }
182
201
 
183
- const newValues = { ...setValue(values, property as any as JsonPath, parsedValue) };
202
+ const newValues = { ...setValue(values, jsonPath, parsedValue) };
184
203
  setValues(newValues);
185
- setChanged((prev) => ({ ...prev, [property]: true }));
204
+ setChanged((prev) => ({ ...prev, [jsonPath]: true }));
186
205
  onValuesChanged?.(newValues);
187
206
 
188
207
  const isValid = validate(newValues);
@@ -191,19 +210,10 @@ export const useForm = <T extends BaseObject>({
191
210
  }
192
211
  };
193
212
 
194
- // TODO(burdon): This is a leaky abstraction: the hook ideally shouldn't get involved in UX.
195
- const onBlur = useCallback(
196
- (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
197
- const { name } = event.target;
198
- setTouched((touched) => ({ ...touched, [name]: true }));
199
-
200
- // TODO(Zan): This should be configurable behavior.
201
- if (event.relatedTarget?.getAttribute('type') === 'submit') {
202
- // NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
203
- // and results in the submit button being disabled when the form is invalid.
204
- setTouched(createKeySet(values, true));
205
- }
206
-
213
+ const onTouched = useCallback(
214
+ (path: (string | number)[]) => {
215
+ const jsonPath = createJsonPath(path);
216
+ setTouched((touched) => ({ ...touched, [jsonPath]: true }));
207
217
  validate(values);
208
218
  },
209
219
  [validate, values],
@@ -217,28 +227,31 @@ export const useForm = <T extends BaseObject>({
217
227
  changed,
218
228
  canSave,
219
229
  handleSave,
230
+ formIsValid,
220
231
 
221
232
  // Field utils.
222
233
  getStatus,
223
234
  getValue: getFormValue,
224
235
  onValueChange,
225
- onBlur,
236
+ onTouched,
226
237
  } satisfies FormHandler<T>;
227
238
  };
228
239
 
229
- const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
240
+ const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<JsonPath, V> => {
230
241
  invariant(obj);
231
- return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<PropertyKey<T>, V>);
242
+ return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
232
243
  };
233
244
 
234
- const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
245
+ const flatMap = (errors: ValidationError[]) => {
235
246
  return errors.reduce(
236
247
  (result, { path, message }) => {
237
- if (!(path in result)) {
238
- result[path as PropertyKey<T>] = message;
248
+ // Convert the validation error path format to our JsonPath format
249
+ const jsonPath = fromEffectValidationPath(path);
250
+ if (!(jsonPath in result)) {
251
+ result[jsonPath] = message;
239
252
  }
240
253
  return result;
241
254
  },
242
- {} as Record<PropertyKey<T>, string>,
255
+ {} as Record<JsonPath, string>,
243
256
  );
244
257
  };
@@ -42,6 +42,7 @@ export default [
42
42
  'format ref': 'Reference',
43
43
  'format regex': 'RegExp',
44
44
  'format string': 'String',
45
+ 'format single-select': 'Select',
45
46
  'format text': 'Long text',
46
47
  'format time': 'Time',
47
48
  'format timestamp': 'Timestamp',
@@ -53,6 +54,13 @@ export default [
53
54
  // Placeholders
54
55
  'placeholder latitude': 'Latitude',
55
56
  'placeholder longitude': 'Longitude',
57
+
58
+ // SelectOptionsInput
59
+ 'select option label': 'Label',
60
+ 'select option label placeholder': 'Option label',
61
+ 'select option color': 'Color',
62
+ 'select option delete': 'Delete',
63
+ 'select option add': 'Add option',
56
64
  },
57
65
  },
58
66
  },