@dxos/react-ui-form 0.7.3-main.2dd075e → 0.7.3-staging.0905f03

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.
@@ -4,23 +4,24 @@
4
4
 
5
5
  import React, { type FC, type PropsWithChildren } from 'react';
6
6
 
7
- import { type BaseObject, type FormatEnum, type PropertyKey } from '@dxos/echo-schema';
7
+ import { type BaseObject, type FormatEnum } from '@dxos/echo-schema';
8
8
  import { type SimpleType } from '@dxos/effect';
9
9
  import { Icon, Tooltip } from '@dxos/react-ui';
10
10
  import { errorText } from '@dxos/react-ui-theme';
11
11
 
12
- import { type FormHandler } from '../../hooks';
12
+ import { type FormPath, type FormHandler } from '../../hooks';
13
13
 
14
14
  /**
15
15
  * Props passed to input components.
16
16
  */
17
17
  export type InputProps<T extends BaseObject> = {
18
- property: PropertyKey<T>;
18
+ property: FormPath<T>;
19
19
  type: SimpleType;
20
20
  format?: FormatEnum;
21
- label: string;
21
+ label?: string;
22
22
  disabled?: boolean;
23
23
  placeholder?: string;
24
+ inputOnly?: boolean;
24
25
  } & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
25
26
 
26
27
  /**
@@ -60,7 +60,7 @@ export const ViewEditor = ({
60
60
  name: view.name,
61
61
  // TODO(burdon): Need to warn user of possible consequences of editing.
62
62
  // TODO(burdon): Settings should have domain name owned by user.
63
- typename: view.query.typename,
63
+ typename: view.query.type,
64
64
  };
65
65
  }, [view]);
66
66
 
@@ -68,7 +68,7 @@ export const ViewEditor = ({
68
68
  ({ name, typename }: ViewMetaType) => {
69
69
  requestAnimationFrame(() => {
70
70
  view.name = name;
71
- view.query.typename = typename;
71
+ view.query.type = typename;
72
72
  schema.updateTypename(typename);
73
73
  });
74
74
  },
@@ -9,6 +9,10 @@ import { type SimpleType, type S } 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';
12
+ import { type MaybePromise } from '@dxos/util';
13
+
14
+ // TODO(ZaymonFC): Where should this live?
15
+ export type FormPath<T extends object = {}> = PropertyKey<T> | `${PropertyKey<T>}.${number}`;
12
16
 
13
17
  /**
14
18
  * Return type from `useForm` hook.
@@ -19,9 +23,9 @@ export type FormHandler<T extends BaseObject> = {
19
23
  //
20
24
 
21
25
  values: T;
22
- errors: Record<PropertyKey<T>, string>;
23
- touched: Record<PropertyKey<T>, boolean>;
24
- changed: Record<PropertyKey<T>, boolean>;
26
+ errors: Record<FormPath<T>, string>;
27
+ touched: Record<FormPath<T>, boolean>;
28
+ changed: Record<FormPath<T>, boolean>;
25
29
  canSave: boolean;
26
30
  handleSave: () => void;
27
31
 
@@ -29,9 +33,9 @@ export type FormHandler<T extends BaseObject> = {
29
33
  // Form input component helpers.
30
34
  //
31
35
 
32
- getStatus: (property: PropertyKey<T>) => { status?: 'error'; error?: string };
33
- getValue: <V>(property: PropertyKey<T>) => V;
34
- onValueChange: <V>(property: PropertyKey<T>, type: SimpleType, value: V) => void;
36
+ getStatus: (property: FormPath<T>) => { status?: 'error'; error?: string };
37
+ getValue: <V>(property: FormPath<T>) => V;
38
+ onValueChange: <V>(property: FormPath<T>, type: SimpleType, value: V) => void;
35
39
  onBlur: (event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
36
40
  };
37
41
 
@@ -74,7 +78,7 @@ export interface FormOptions<T extends BaseObject> {
74
78
  /**
75
79
  * Called when the form is submitted and passes validation.
76
80
  */
77
- onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => void;
81
+ onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => MaybePromise<void>;
78
82
  }
79
83
 
80
84
  /**
@@ -94,9 +98,10 @@ export const useForm = <T extends BaseObject>({
94
98
  setValues(initialValues);
95
99
  }, [initialValues]);
96
100
 
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>);
101
+ const [touched, setTouched] = useState<Record<FormPath<T>, boolean>>(createKeySet(initialValues, false));
102
+ const [changed, setChanged] = useState<Record<FormPath<T>, boolean>>(createKeySet(initialValues, false));
103
+ const [errors, setErrors] = useState<Record<FormPath<T>, string>>({} as Record<FormPath<T>, string>);
104
+ const [saving, setSaving] = useState(false);
100
105
 
101
106
  //
102
107
  // Validation.
@@ -132,15 +137,21 @@ export const useForm = <T extends BaseObject>({
132
137
  */
133
138
  const canSave = useMemo(
134
139
  () =>
140
+ !saving &&
135
141
  Object.keys(values).every(
136
142
  (property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
137
143
  ),
138
- [values, touched, errors],
144
+ [values, touched, errors, saving],
139
145
  );
140
146
 
141
- const handleSave = useCallback(() => {
147
+ const handleSave = useCallback(async () => {
142
148
  if (validate(values)) {
143
- onSave?.(values, { changed });
149
+ setSaving(true);
150
+ try {
151
+ await onSave?.(values, { changed });
152
+ } finally {
153
+ setSaving(false);
154
+ }
144
155
  }
145
156
  }, [values, validate, onSave]);
146
157
 
@@ -149,7 +160,7 @@ export const useForm = <T extends BaseObject>({
149
160
  //
150
161
 
151
162
  const getStatus = useCallback<FormHandler<T>['getStatus']>(
152
- (property: PropertyKey<T>) => ({
163
+ (property: FormPath<T>) => ({
153
164
  status: errors[property] ? 'error' : undefined,
154
165
  error: errors[property] ? errors[property] : undefined,
155
166
  }),
@@ -157,12 +168,12 @@ export const useForm = <T extends BaseObject>({
157
168
  );
158
169
 
159
170
  // TODO(burdon): Use path to extract hierarchical value.
160
- const getValue = <V>(property: PropertyKey<T>): V => {
161
- return values[property] as V;
171
+ const getValue = <V>(property: FormPath<T>): V => {
172
+ return getByPath(values, property);
162
173
  };
163
174
 
164
175
  // TODO(burdon): Use path to set hierarchical value.
165
- const onValueChange = (property: PropertyKey<T>, type: SimpleType, value: any) => {
176
+ const onValueChange = (property: FormPath<T>, type: SimpleType, value: any) => {
166
177
  let parsedValue = value;
167
178
  try {
168
179
  if (type === 'number') {
@@ -173,7 +184,7 @@ export const useForm = <T extends BaseObject>({
173
184
  parsedValue = undefined;
174
185
  }
175
186
 
176
- const newValues = { ...values, [property]: parsedValue };
187
+ const newValues = setByPath(values, property, parsedValue);
177
188
  setValues(newValues);
178
189
  setChanged((prev) => ({ ...prev, [property]: true }));
179
190
  onValuesChanged?.(newValues);
@@ -219,19 +230,34 @@ export const useForm = <T extends BaseObject>({
219
230
  } satisfies FormHandler<T>;
220
231
  };
221
232
 
222
- 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<FormPath<T>, V> => {
223
234
  invariant(obj);
224
- 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<FormPath<T>, V>);
225
236
  };
226
237
 
227
238
  const flatMap = <T extends BaseObject>(errors: ValidationError[]) => {
228
239
  return errors.reduce(
229
240
  (result, { path, message }) => {
230
241
  if (!(path in result)) {
231
- result[path as PropertyKey<T>] = message;
242
+ result[path as FormPath<T>] = message;
232
243
  }
233
244
  return result;
234
245
  },
235
- {} as Record<PropertyKey<T>, string>,
246
+ {} as Record<FormPath<T>, string>,
236
247
  );
237
248
  };
249
+
250
+ // TODO(ZaymonFC): Move to util?
251
+ const getByPath = <T extends object, V>(obj: T, path: FormPath<T>): V => {
252
+ const [key, index] = path.split('.');
253
+ return index === undefined ? (obj[key as keyof T] as V) : ((obj[key as keyof T] as any[])[parseInt(index)] as V);
254
+ };
255
+
256
+ const setByPath = <T extends object>(obj: T, path: FormPath<T>, value: any): T => {
257
+ const [key, index] = path.split('.');
258
+ const newValue =
259
+ index === undefined
260
+ ? value
261
+ : [...(obj[key as keyof T] as any[])].map((v, i) => (i === parseInt(index) ? value : v));
262
+ return { ...obj, [key]: newValue };
263
+ };