@dxos/react-ui-form 0.7.5-main.9d26e3a → 0.7.5-main.e9bb01b

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 +743 -466
  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 +777 -515
  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 +743 -466
  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 +5 -6
  14. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.d.ts +13 -16
  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 +20 -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/ViewEditor/ViewEditor.d.ts +2 -1
  40. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/ViewEditor/ViewEditor.stories.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 +9 -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 +58 -40
  52. package/src/components/Form/ArrayField.tsx +111 -0
  53. package/src/components/Form/Defaults.tsx +18 -33
  54. package/src/components/Form/Form.stories.tsx +15 -5
  55. package/src/components/Form/Form.tsx +39 -330
  56. package/src/components/Form/FormActions.tsx +41 -0
  57. package/src/components/Form/FormContent.tsx +173 -0
  58. package/src/components/Form/FormContext.tsx +48 -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 +192 -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 +2 -5
  66. package/src/components/ViewEditor/ViewEditor.stories.tsx +25 -4
  67. package/src/components/ViewEditor/ViewEditor.tsx +14 -6
  68. package/src/components/deprecated/FormInput.tsx +19 -3
  69. package/src/hooks/useForm.ts +54 -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 } from '@dxos/effect';
7
+ import { type BaseObject, getValue, setValue } from '@dxos/echo-schema';
8
+ import { type SimpleType, type S, 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,9 +20,9 @@ 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
27
  handleSave: () => void;
28
28
 
@@ -30,10 +30,10 @@ export type FormHandler<T extends BaseObject> = {
30
30
  // Form input component helpers.
31
31
  //
32
32
 
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;
33
+ getStatus: (path: string | (string | number)[]) => { status?: 'error'; error?: string };
34
+ getValue: <V>(path: (string | number)[]) => V | undefined;
35
+ onValueChange: <V>(path: (string | number)[], type: SimpleType, value: V) => void;
36
+ onTouched: (path: (string | number)[]) => void;
37
37
  };
38
38
 
39
39
  /**
@@ -94,16 +94,15 @@ export const useForm = <T extends BaseObject>({
94
94
  setValues(initialValues);
95
95
  }, [initialValues]);
96
96
 
97
- const [touched, setTouched] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
98
- const [changed, setChanged] = useState<Record<PropertyKey<T>, boolean>>(createKeySet(initialValues, false));
99
- const [errors, setErrors] = useState<Record<PropertyKey<T>, string>>({} as Record<PropertyKey<T>, string>);
97
+ const [touched, setTouched] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
98
+ const [changed, setChanged] = useState<Record<JsonPath, boolean>>(createKeySet(initialValues, false));
99
+ const [errors, setErrors] = useState<Record<JsonPath, string>>({});
100
100
  const [saving, setSaving] = useState(false);
101
101
 
102
102
  //
103
103
  // Validation.
104
104
  //
105
105
 
106
- // TODO(burdon): Validate each property separately.
107
106
  const validate = useCallback(
108
107
  (values: Partial<T>): values is T => {
109
108
  let errors: ValidationError[] = validateSchema(schema, values) ?? [];
@@ -136,8 +135,15 @@ export const useForm = <T extends BaseObject>({
136
135
  const canSave = useMemo(
137
136
  () =>
138
137
  !saving &&
139
- Object.keys(values).every((property) => touched[property as JsonPath] === false || !errors[property as JsonPath]),
140
- [values, touched, errors, saving],
138
+ // Check if any error paths that are touched have errors
139
+ !Object.entries(touched).some(
140
+ ([path, isTouched]) =>
141
+ isTouched &&
142
+ Object.keys(errors).some(
143
+ (errorPath) => errorPath === path || errorPath.startsWith(`${path}.`) || errorPath.startsWith(`${path}[`),
144
+ ),
145
+ ),
146
+ [touched, errors, saving],
141
147
  );
142
148
 
143
149
  const handleSave = useCallback(async () => {
@@ -156,19 +162,27 @@ export const useForm = <T extends BaseObject>({
156
162
  //
157
163
 
158
164
  const getStatus = useCallback<FormHandler<T>['getStatus']>(
159
- (property: PropertyKey<T>) => ({
160
- status: errors[property] ? 'error' : undefined,
161
- error: errors[property] ? errors[property] : undefined,
162
- }),
163
- [touched, errors],
165
+ (path) => {
166
+ const jsonPath = Array.isArray(path) ? createJsonPath(path) : path;
167
+ const matchingError = Object.entries(errors).find(
168
+ ([errorPath]) =>
169
+ errorPath === jsonPath || errorPath.startsWith(`${jsonPath}.`) || errorPath.startsWith(`${jsonPath}[`),
170
+ );
171
+
172
+ return {
173
+ status: matchingError ? 'error' : undefined,
174
+ error: matchingError ? matchingError[1] : undefined,
175
+ };
176
+ },
177
+ [errors],
164
178
  );
165
179
 
166
- // TODO(burdon): Use path to extract hierarchical value.
167
- const getFormValue = <V>(property: PropertyKey<T>): V | undefined => {
168
- return getValue(values, property as any as JsonPath);
180
+ const getFormValue = <V>(path: (string | number)[]): V | undefined => {
181
+ return getValue(values, createJsonPath(path));
169
182
  };
170
183
 
171
- const onValueChange = (property: PropertyKey<T>, type: SimpleType, value: any) => {
184
+ const onValueChange = (path: (string | number)[], type: SimpleType, value: any) => {
185
+ const jsonPath = createJsonPath(path);
172
186
  let parsedValue = value;
173
187
  try {
174
188
  if (type === 'number') {
@@ -179,9 +193,9 @@ export const useForm = <T extends BaseObject>({
179
193
  parsedValue = undefined;
180
194
  }
181
195
 
182
- const newValues = { ...setValue(values, property as any as JsonPath, parsedValue) };
196
+ const newValues = { ...setValue(values, jsonPath, parsedValue) };
183
197
  setValues(newValues);
184
- setChanged((prev) => ({ ...prev, [property]: true }));
198
+ setChanged((prev) => ({ ...prev, [jsonPath]: true }));
185
199
  onValuesChanged?.(newValues);
186
200
 
187
201
  const isValid = validate(newValues);
@@ -190,19 +204,10 @@ export const useForm = <T extends BaseObject>({
190
204
  }
191
205
  };
192
206
 
193
- // TODO(burdon): This is a leaky abstraction: the hook ideally shouldn't get involved in UX.
194
- const onBlur = useCallback(
195
- (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
196
- const { name } = event.target;
197
- setTouched((touched) => ({ ...touched, [name]: true }));
198
-
199
- // TODO(Zan): This should be configurable behavior.
200
- if (event.relatedTarget?.getAttribute('type') === 'submit') {
201
- // NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
202
- // and results in the submit button being disabled when the form is invalid.
203
- setTouched(createKeySet(values, true));
204
- }
205
-
207
+ const onTouched = useCallback(
208
+ (path: (string | number)[]) => {
209
+ const jsonPath = createJsonPath(path);
210
+ setTouched((touched) => ({ ...touched, [jsonPath]: true }));
206
211
  validate(values);
207
212
  },
208
213
  [validate, values],
@@ -221,23 +226,25 @@ export const useForm = <T extends BaseObject>({
221
226
  getStatus,
222
227
  getValue: getFormValue,
223
228
  onValueChange,
224
- onBlur,
229
+ onTouched,
225
230
  } satisfies FormHandler<T>;
226
231
  };
227
232
 
228
- const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
233
+ const createKeySet = <T extends BaseObject, V>(obj: T, value: V): Record<JsonPath, V> => {
229
234
  invariant(obj);
230
- return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<PropertyKey<T>, V>);
235
+ return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<JsonPath, V>);
231
236
  };
232
237
 
233
- const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
238
+ const flatMap = (errors: ValidationError[]) => {
234
239
  return errors.reduce(
235
240
  (result, { path, message }) => {
236
- if (!(path in result)) {
237
- result[path as PropertyKey<T>] = message;
241
+ // Convert the validation error path format to our JsonPath format
242
+ const jsonPath = fromEffectValidationPath(path);
243
+ if (!(jsonPath in result)) {
244
+ result[jsonPath] = message;
238
245
  }
239
246
  return result;
240
247
  },
241
- {} as Record<PropertyKey<T>, string>,
248
+ {} as Record<JsonPath, string>,
242
249
  );
243
250
  };
@@ -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
  },