@dxos/react-ui-form 0.7.1 → 0.7.2-main.f1adc9f

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 (39) hide show
  1. package/dist/lib/browser/index.mjs +137 -111
  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 +130 -104
  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 +137 -111
  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 +2 -2
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Custom.d.ts +2 -1
  13. package/dist/types/src/components/Form/Custom.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Defaults.d.ts +11 -4
  15. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +6 -5
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +1 -4
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/Input.d.ts +3 -8
  21. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/factory.d.ts +2 -2
  23. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  24. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  25. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  26. package/dist/types/src/hooks/useForm.d.ts +8 -7
  27. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  28. package/package.json +22 -24
  29. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  30. package/src/components/FieldEditor/FieldEditor.tsx +8 -8
  31. package/src/components/Form/Custom.tsx +9 -9
  32. package/src/components/Form/Defaults.tsx +10 -5
  33. package/src/components/Form/Form.stories.tsx +6 -6
  34. package/src/components/Form/Form.tsx +52 -28
  35. package/src/components/Form/Input.tsx +3 -7
  36. package/src/components/Form/factory.tsx +4 -4
  37. package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -2
  38. package/src/components/ViewEditor/ViewEditor.tsx +8 -7
  39. package/src/hooks/useForm.ts +18 -17
@@ -2,16 +2,19 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { pipe } from 'effect';
6
+ import { capitalize } from 'effect/String';
5
7
  import React, { useEffect, useMemo } from 'react';
6
8
 
7
- import { AST, S } from '@dxos/echo-schema';
9
+ import { AST, type BaseObject, S, type PropertyKey } from '@dxos/echo-schema';
8
10
  import { findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
9
11
  import { log } from '@dxos/log';
10
12
  import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
11
13
  import { mx } from '@dxos/react-ui-theme';
12
- import { getSchemaProperties, type PropertyKey, type SchemaProperty } from '@dxos/schema';
14
+ import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
13
15
  import { isNotFalsy } from '@dxos/util';
14
16
 
17
+ import { SelectInput } from './Defaults';
15
18
  import { type InputComponent } from './Input';
16
19
  import { getInputComponent } from './factory';
17
20
  import { type FormOptions, useForm } from '../../hooks';
@@ -21,9 +24,9 @@ import { translationKey } from '../../translations';
21
24
 
22
25
  const padding = 'px-2';
23
26
 
24
- export type PropsFilter<T extends Object> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
27
+ export type PropsFilter<T extends BaseObject<T>> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
25
28
 
26
- export type FormProps<T extends object> = ThemedClassName<
29
+ export type FormProps<T extends BaseObject<T>> = ThemedClassName<
27
30
  {
28
31
  values: T;
29
32
  /** Path to the current object from the root. Used with nested forms. */
@@ -42,13 +45,13 @@ export type FormProps<T extends object> = ThemedClassName<
42
45
  * Map of custom renderers for specific properties.
43
46
  */
44
47
  Custom?: Partial<Record<string, InputComponent<T>>>;
45
- } & Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSubmit'>
48
+ } & Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'>
46
49
  >;
47
50
 
48
51
  /**
49
52
  * General purpose form component that displays field controls based on the given schema.
50
53
  */
51
- export const Form = <T extends object = {}>({
54
+ export const Form = <T extends BaseObject<T>>({
52
55
  classNames,
53
56
  schema,
54
57
  values: initialValues,
@@ -59,24 +62,24 @@ export const Form = <T extends object = {}>({
59
62
  autoSave,
60
63
  onValuesChanged,
61
64
  onValidate,
62
- onSubmit,
65
+ onSave,
63
66
  onCancel,
64
67
  Custom,
65
68
  }: FormProps<T>) => {
66
69
  const { t } = useTranslation(translationKey);
67
- const onValid = useMemo(() => (autoSave ? onSubmit : undefined), [autoSave, onSubmit]);
68
- const { canSubmit, values, errors, handleSubmit, ...inputProps } = useForm<T>({
70
+ const onValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
71
+ const { canSave, values, errors, handleSave, ...inputProps } = useForm<T>({
69
72
  schema,
70
73
  initialValues,
71
74
  onValuesChanged,
72
75
  onValidate,
73
76
  onValid,
74
- onSubmit,
77
+ onSave,
75
78
  });
76
79
 
77
80
  // Filter and sort props.
78
81
  // TODO(burdon): Move into useForm?
79
- const props = useMemo(() => {
82
+ const properties = useMemo(() => {
80
83
  const props = getSchemaProperties<T>(schema.ast, values);
81
84
  const filtered = filter ? filter(props) : props;
82
85
  const findIndex = (props: PropertyKey<T>[], prop: PropertyKey<T>) => {
@@ -96,25 +99,50 @@ export const Form = <T extends object = {}>({
96
99
 
97
100
  return (
98
101
  <div role='form' className={mx('flex flex-col w-full gap-2 py-2', classNames)}>
99
- {props
100
- .map(({ prop, name, type, format, title, description, examples }) => {
101
- // Custom property allows for sub forms.
102
- // TODO(burdon): Use Select control if options are present in annotation?
102
+ {properties
103
+ .map((property) => {
104
+ const { ast, name, type, format, title, description, examples, options } = property;
103
105
  const key = [...path, name];
104
- const InputComponent = Custom?.[key.join('.')] ?? getInputComponent<T>(type, format);
106
+ const label = title ?? pipe(name, capitalize);
107
+ const placeholder = examples?.length ? `Example: "${examples[0]}"` : description;
108
+
109
+ // Get generic input.
110
+ let InputComponent = Custom?.[key.join('.')];
111
+ if (!InputComponent) {
112
+ // Select.
113
+ if (options) {
114
+ return (
115
+ <div key={name} role='none' className={padding}>
116
+ <SelectInput
117
+ type={type}
118
+ format={format}
119
+ property={name}
120
+ disabled={readonly}
121
+ label={label}
122
+ options={options.map((option) => ({ value: option, label: String(option) }))}
123
+ placeholder={placeholder}
124
+ {...inputProps}
125
+ />
126
+ </div>
127
+ );
128
+ }
129
+
130
+ InputComponent = getInputComponent<T>(type, format);
131
+ }
132
+
105
133
  if (!InputComponent) {
106
134
  // Recursively render form.
107
135
  if (type === 'object') {
108
- const baseNode = findNode(prop.type, isDiscriminatedUnion);
136
+ const baseNode = findNode(ast, isDiscriminatedUnion);
109
137
  const typeLiteral = baseNode
110
138
  ? getDiscriminatedType(baseNode, values[name] as any)
111
- : findNode(prop.type, AST.isTypeLiteral);
139
+ : findNode(ast, AST.isTypeLiteral);
112
140
 
113
141
  if (typeLiteral) {
114
142
  const schema = S.make(typeLiteral);
115
143
  return (
116
144
  <div key={name} role='none'>
117
- <div className={padding}>{title ?? name}</div>
145
+ <div className={padding}>{label}</div>
118
146
  <Form<any>
119
147
  schema={schema}
120
148
  path={key}
@@ -133,8 +161,6 @@ export const Form = <T extends object = {}>({
133
161
  return null;
134
162
  }
135
163
 
136
- const placeholder = examples?.length ? `Example: ${examples[0]}` : description;
137
-
138
164
  return (
139
165
  <div key={name} role='none' className={padding}>
140
166
  <InputComponent
@@ -142,7 +168,7 @@ export const Form = <T extends object = {}>({
142
168
  format={format}
143
169
  property={name}
144
170
  disabled={readonly}
145
- label={title ?? name}
171
+ label={label}
146
172
  placeholder={placeholder}
147
173
  {...inputProps}
148
174
  />
@@ -151,21 +177,19 @@ export const Form = <T extends object = {}>({
151
177
  })
152
178
  .filter(isNotFalsy)}
153
179
 
154
- {/* {errors && <div className='overflow-hidden text-sm'>{JSON.stringify(errors)}</div>} */}
155
-
156
- {(onCancel || onSubmit) && !autoSave && (
180
+ {(onCancel || onSave) && !autoSave && (
157
181
  <div role='none' className='flex justify-center'>
158
182
  <div role='none' className={mx(onCancel && !readonly && 'grid grid-cols-2 gap-2')}>
159
183
  {onCancel && !readonly && (
160
184
  <IconButton icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
161
185
  )}
162
- {onSubmit && (
186
+ {onSave && (
163
187
  <IconButton
164
188
  type='submit'
165
- disabled={!canSubmit}
189
+ disabled={!canSave}
166
190
  icon='ph--check--regular'
167
191
  label={t('button save')}
168
- onClick={handleSubmit}
192
+ onClick={handleSave}
169
193
  />
170
194
  )}
171
195
  </div>
@@ -4,33 +4,29 @@
4
4
 
5
5
  import React, { type FC, type PropsWithChildren } from 'react';
6
6
 
7
- import { type FormatEnum } from '@dxos/echo-schema';
7
+ import { type BaseObject, type FormatEnum, type PropertyKey } 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
- import { type PropertyKey } from '@dxos/schema';
12
11
 
13
12
  import { type FormHandler } from '../../hooks';
14
13
 
15
14
  /**
16
15
  * Props passed to input components.
17
16
  */
18
- export type InputProps<T extends object = {}> = {
17
+ export type InputProps<T extends BaseObject<T>> = {
19
18
  property: PropertyKey<T>;
20
19
  type: SimpleType;
21
20
  format?: FormatEnum;
22
21
  label: string;
23
22
  disabled?: boolean;
24
23
  placeholder?: string;
25
- // TODO(burdon): Provide from annotations? E.g., enum.
26
- options?: Array<{ value: string | number; label?: string }>;
27
24
  } & Pick<FormHandler<T>, 'getStatus' | 'getValue' | 'onValueChange' | 'onBlur'>;
28
25
 
29
26
  /**
30
27
  * Form input component.
31
28
  */
32
- // TODO(burdon): Remove generic from inputs.
33
- export type InputComponent<T extends object = {}> = FC<InputProps<T>>;
29
+ export type InputComponent<T extends BaseObject<T>> = FC<InputProps<T>>;
34
30
 
35
31
  export type InputHeaderProps = PropsWithChildren<{
36
32
  error?: string;
@@ -2,23 +2,23 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { FormatEnum } from '@dxos/echo-schema';
5
+ import { type BaseObject, FormatEnum } from '@dxos/echo-schema';
6
6
  import { type SimpleType } from '@dxos/effect';
7
7
 
8
- import { LatLngInput } from './Custom';
8
+ import { GeiPointInput } from './Custom';
9
9
  import { BooleanInput, NumberInput, TextInput } from './Defaults';
10
10
  import { type InputComponent } from './Input';
11
11
 
12
12
  /**
13
13
  * Get property input component.
14
14
  */
15
- export const getInputComponent = <T extends object>(
15
+ export const getInputComponent = <T extends BaseObject<T>>(
16
16
  type: SimpleType,
17
17
  format?: FormatEnum,
18
18
  ): InputComponent<T> | undefined => {
19
19
  switch (format) {
20
20
  case FormatEnum.LatLng:
21
- return LatLngInput;
21
+ return GeiPointInput;
22
22
  }
23
23
 
24
24
  switch (type) {
@@ -10,8 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react';
10
10
  import { Format, type MutableSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
11
11
  import { useSpace } from '@dxos/react-client/echo';
12
12
  import { withClientProvider } from '@dxos/react-client/testing';
13
- import { ViewProjection, createView } from '@dxos/schema';
14
- import { type ViewType } from '@dxos/schema';
13
+ import { type ViewType, ViewProjection, createView } from '@dxos/schema';
15
14
  import { withTheme, withLayout } from '@dxos/storybook-utils';
16
15
 
17
16
  import { ViewEditor } from './ViewEditor';
@@ -22,7 +22,7 @@ const ViewMetaSchema = S.Struct({
22
22
  name: S.String.annotations({
23
23
  [AST.TitleAnnotationId]: 'View',
24
24
  }),
25
- type: Format.DXN.annotations({
25
+ typename: Format.DXN.annotations({
26
26
  [AST.TitleAnnotationId]: 'Typename',
27
27
  }),
28
28
  }).pipe(S.mutable);
@@ -60,16 +60,16 @@ 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
- type: view.query.type,
63
+ typename: view.query.typename,
64
64
  };
65
65
  }, [view]);
66
66
 
67
67
  const handleViewUpdate = useCallback(
68
- ({ name, type }: ViewMetaType) => {
68
+ ({ name, typename }: ViewMetaType) => {
69
69
  requestAnimationFrame(() => {
70
70
  view.name = name;
71
- view.query.type = type;
72
- schema.updateTypename(type);
71
+ view.query.typename = typename;
72
+ schema.updateTypename(typename);
73
73
  });
74
74
  },
75
75
  [view, schema],
@@ -95,7 +95,7 @@ export const ViewEditor = ({
95
95
 
96
96
  return (
97
97
  <div role='none' className={mx('flex flex-col w-full divide-y divide-separator', classNames)}>
98
- <Form<ViewMetaType> autoFocus autoSave schema={ViewMetaSchema} values={viewValues} onSubmit={handleViewUpdate} />
98
+ <Form<ViewMetaType> autoFocus autoSave schema={ViewMetaSchema} values={viewValues} onSave={handleViewUpdate} />
99
99
 
100
100
  <div>
101
101
  {/* TODO(burdon): Clean up common form ux. */}
@@ -139,9 +139,10 @@ export const ViewEditor = ({
139
139
  projection={projection}
140
140
  field={field}
141
141
  registry={registry}
142
- onClose={handleClose}
142
+ onSave={handleClose}
143
143
  />
144
144
  )}
145
+
145
146
  {!readonly && !field && (
146
147
  <div className='flex p-2 justify-center'>
147
148
  <IconButton
@@ -4,15 +4,16 @@
4
4
 
5
5
  import { type FocusEvent, useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
+ import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
7
8
  import { type SimpleType, type S } from '@dxos/effect';
8
9
  import { invariant } from '@dxos/invariant';
9
10
  import { log } from '@dxos/log';
10
- import { validateSchema, type PropertyKey, type ValidationError } from '@dxos/schema';
11
+ import { validateSchema, type ValidationError } from '@dxos/schema';
11
12
 
12
13
  /**
13
14
  * Return type from `useForm` hook.
14
15
  */
15
- export type FormHandler<T extends object = {}> = {
16
+ export type FormHandler<T extends BaseObject<T>> = {
16
17
  //
17
18
  // Form state management.
18
19
  //
@@ -21,8 +22,8 @@ export type FormHandler<T extends object = {}> = {
21
22
  errors: Record<PropertyKey<T>, string>;
22
23
  touched: Record<PropertyKey<T>, boolean>;
23
24
  changed: Record<PropertyKey<T>, boolean>;
24
- canSubmit: boolean;
25
- handleSubmit: () => void;
25
+ canSave: boolean;
26
+ handleSave: () => void;
26
27
 
27
28
  //
28
29
  // Form input component helpers.
@@ -37,7 +38,7 @@ export type FormHandler<T extends object = {}> = {
37
38
  /**
38
39
  * Hook options.
39
40
  */
40
- export interface FormOptions<T extends object> {
41
+ export interface FormOptions<T extends BaseObject<T>> {
41
42
  schema: S.Schema<T>;
42
43
 
43
44
  // TODO(burdon): Are these reactive?
@@ -66,20 +67,20 @@ export interface FormOptions<T extends object> {
66
67
  /**
67
68
  * Called when the form is submitted and passes validation.
68
69
  */
69
- onSubmit?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => void;
70
+ onSave?: (values: T, meta: { changed: FormHandler<T>['changed'] }) => void;
70
71
  }
71
72
 
72
73
  /**
73
74
  * Creates a hook for managing form state, including values, validation, and submission.
74
75
  * Deeply integrated with `@dxos/schema` for schema-based validation.
75
76
  */
76
- export const useForm = <T extends object>({
77
+ export const useForm = <T extends BaseObject<T>>({
77
78
  schema,
78
79
  initialValues,
79
80
  onValuesChanged,
80
81
  onValidate,
81
82
  onValid,
82
- onSubmit,
83
+ onSave,
83
84
  }: FormOptions<T>): FormHandler<T> => {
84
85
  const [values, setValues] = useState<T>(initialValues);
85
86
  useEffect(() => {
@@ -122,7 +123,7 @@ export const useForm = <T extends object>({
122
123
  * NOTE: We can submit if there is no touched field that has an error.
123
124
  * Basically, if there's a validation message visible in the form, submit should be disabled.
124
125
  */
125
- const canSubmit = useMemo(
126
+ const canSave = useMemo(
126
127
  () =>
127
128
  Object.keys(values).every(
128
129
  (property) => touched[property as PropertyKey<T>] === false || !errors[property as PropertyKey<T>],
@@ -130,11 +131,11 @@ export const useForm = <T extends object>({
130
131
  [values, touched, errors],
131
132
  );
132
133
 
133
- const handleSubmit = useCallback(() => {
134
+ const handleSave = useCallback(() => {
134
135
  if (validate(values)) {
135
- onSubmit?.(values, { changed });
136
+ onSave?.(values, { changed });
136
137
  }
137
- }, [values, validate, onSubmit]);
138
+ }, [values, validate, onSave]);
138
139
 
139
140
  //
140
141
  // Fields.
@@ -184,7 +185,7 @@ export const useForm = <T extends object>({
184
185
 
185
186
  // TODO(Zan): This should be configurable behavior.
186
187
  if (event.relatedTarget?.getAttribute('type') === 'submit') {
187
- // NOTE: We do this here instead of onSubmit because the blur event is triggered before the submit event
188
+ // NOTE: We do this here instead of onSave because the blur event is triggered before the submit event
188
189
  // and results in the submit button being disabled when the form is invalid.
189
190
  setTouched(createKeySet(values, true));
190
191
  }
@@ -200,8 +201,8 @@ export const useForm = <T extends object>({
200
201
  errors,
201
202
  touched,
202
203
  changed,
203
- canSubmit,
204
- handleSubmit,
204
+ canSave,
205
+ handleSave,
205
206
 
206
207
  // Field utils.
207
208
  getStatus,
@@ -211,12 +212,12 @@ export const useForm = <T extends object>({
211
212
  } satisfies FormHandler<T>;
212
213
  };
213
214
 
214
- const createKeySet = <T extends object, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
215
+ const createKeySet = <T extends BaseObject<T>, V>(obj: T, value: V): Record<PropertyKey<T>, V> => {
215
216
  invariant(obj);
216
217
  return Object.keys(obj).reduce((acc, key) => ({ ...acc, [key]: value }), {} as Record<PropertyKey<T>, V>);
217
218
  };
218
219
 
219
- const flatMap = <T extends object>(errors: ValidationError[]) => {
220
+ const flatMap = <T extends BaseObject<T>>(errors: ValidationError[]) => {
220
221
  return errors.reduce(
221
222
  (result, { path, message }) => {
222
223
  if (!(path in result)) {