@dxos/react-ui-form 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc
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/LICENSE +102 -5
- package/README.md +0 -1
- package/dist/lib/browser/index.mjs +2734 -2312
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +82 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +2734 -2312
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +84 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +68 -60
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +109 -26
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +114 -137
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +43 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +79 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts +15 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts +22 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +84 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/index.d.ts +12 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/index.d.ts +2 -4
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +37 -10
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +64 -59
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +51 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +84 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +4 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +12 -15
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +70 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +3 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -6
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -3
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts +7 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +65 -60
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field-type.d.ts +19 -0
- package/dist/types/src/util/field-type.d.ts.map +1 -0
- package/dist/types/src/util/field.d.ts +3 -3
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +2 -2
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -0
- package/dist/types/src/util/properties.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +2 -2
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +57 -51
- package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +76 -79
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +139 -251
- package/src/components/Form/Form.tsx +375 -107
- package/src/components/Form/FormField.tsx +306 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +174 -0
- package/src/components/Form/FormFieldSet.tsx +144 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +155 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/GeoPointField.tsx +121 -0
- package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
- package/src/components/Form/fields/MarkdownField.tsx +168 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +229 -0
- package/src/components/Form/fields/SelectField.tsx +65 -0
- package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +23 -18
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +30 -24
- package/src/components/Form/fields/TextAreaField.tsx +38 -0
- package/src/components/Form/fields/TextField.tsx +38 -0
- package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectForm/ObjectForm.tsx +114 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +30 -25
- package/src/components/ObjectPicker/ObjectPicker.tsx +122 -81
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/AUDIT.md +52 -0
- package/src/components/Settings/Settings.stories.tsx +104 -0
- package/src/components/Settings/Settings.tsx +80 -0
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +58 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +7 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +53 -50
- package/src/components/ViewEditor/ViewEditor.test.tsx +6 -6
- package/src/components/ViewEditor/ViewEditor.tsx +291 -208
- package/src/components/index.ts +3 -1
- package/src/components/testing.tsx +24 -11
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +380 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/index.ts +0 -4
- package/src/translations.ts +70 -65
- package/src/util/field-type.ts +60 -0
- package/src/util/field.ts +25 -25
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +2 -1
- package/src/util/properties.ts +18 -0
- package/src/util/refs.ts +16 -18
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -84
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
- package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
- package/dist/types/src/components/Form/Defaults.d.ts +0 -14
- package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormActions.d.ts +0 -9
- package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContent.d.ts +0 -63
- package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContext.d.ts +0 -23
- package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
- package/dist/types/src/components/Form/Input.d.ts +0 -29
- package/dist/types/src/components/Form/Input.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -22
- package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -79
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/index.d.ts +0 -4
- package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/factory.d.ts +0 -8
- package/dist/types/src/components/Form/factory.d.ts.map +0 -1
- package/dist/types/src/hooks/useForm.d.ts +0 -67
- package/dist/types/src/hooks/useForm.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
- package/dist/types/src/util/schema.d.ts +0 -3
- package/dist/types/src/util/schema.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/components/Form/ArrayField.tsx +0 -125
- package/src/components/Form/Defaults.tsx +0 -205
- package/src/components/Form/FormActions.tsx +0 -54
- package/src/components/Form/FormContent.tsx +0 -322
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -54
- package/src/components/Form/RefField.tsx +0 -283
- package/src/components/Form/custom/GeoPointInput.tsx +0 -80
- package/src/components/Form/custom/index.ts +0 -7
- package/src/components/Form/factory.tsx +0 -31
- package/src/hooks/useForm.ts +0 -296
- package/src/hooks/useQueryRefOptions.ts +0 -62
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -12
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Match from 'effect/Match';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
|
+
import * as Schema from 'effect/Schema';
|
|
8
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
9
|
+
import * as String from 'effect/String';
|
|
10
|
+
import React, { useMemo } from 'react';
|
|
11
|
+
|
|
12
|
+
import { Format } from '@dxos/echo';
|
|
13
|
+
import {
|
|
14
|
+
createJsonPath,
|
|
15
|
+
findNode,
|
|
16
|
+
getAnnotation,
|
|
17
|
+
getDiscriminatedType,
|
|
18
|
+
isArrayType,
|
|
19
|
+
isDiscriminatedUnion,
|
|
20
|
+
isLiteralUnion,
|
|
21
|
+
isNestedType,
|
|
22
|
+
} from '@dxos/effect';
|
|
23
|
+
import { useTranslation } from '@dxos/react-ui';
|
|
24
|
+
import { type ProjectionModel } from '@dxos/schema';
|
|
25
|
+
|
|
26
|
+
import { translationKey } from '#translations';
|
|
27
|
+
|
|
28
|
+
import { getRefProps } from '../../util';
|
|
29
|
+
import {
|
|
30
|
+
ArrayField,
|
|
31
|
+
BooleanField,
|
|
32
|
+
GeoPointField,
|
|
33
|
+
MarkdownField,
|
|
34
|
+
NumberField,
|
|
35
|
+
RefField,
|
|
36
|
+
type RefFieldProps,
|
|
37
|
+
SelectField,
|
|
38
|
+
TextAreaField,
|
|
39
|
+
TextField,
|
|
40
|
+
} from './fields';
|
|
41
|
+
import { useFormFieldState } from './Form';
|
|
42
|
+
import {
|
|
43
|
+
type FormFieldComponent,
|
|
44
|
+
type FormFieldComponentProps,
|
|
45
|
+
type FormFieldMap,
|
|
46
|
+
type FormFieldProvider,
|
|
47
|
+
} from './FormFieldComponent';
|
|
48
|
+
import { FormFieldSet } from './FormFieldSet';
|
|
49
|
+
|
|
50
|
+
export type FormFieldProps = {
|
|
51
|
+
/**
|
|
52
|
+
* AST of the property to render.
|
|
53
|
+
*/
|
|
54
|
+
type: SchemaAST.AST;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Name of the property.
|
|
58
|
+
*/
|
|
59
|
+
name: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Path to the current object from the root. Used with nested forms.
|
|
63
|
+
*/
|
|
64
|
+
path?: (string | number)[];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Optional projection for projection-based field management.
|
|
68
|
+
*/
|
|
69
|
+
projection?: ProjectionModel;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Map of custom renderers for specific properties.
|
|
73
|
+
* Prefer fieldProvider for plugin specific input surfaces.
|
|
74
|
+
*/
|
|
75
|
+
fieldMap?: FormFieldMap;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Function to lookup custom renderers for specific properties.
|
|
79
|
+
*/
|
|
80
|
+
fieldProvider?: FormFieldProvider;
|
|
81
|
+
/**
|
|
82
|
+
* Typename of the ref type that the create props apply to.
|
|
83
|
+
* When set, createOptionLabel/createOptionIcon/createInitialValuePath/createFieldMap/onCreate
|
|
84
|
+
* are only passed to ref fields whose typename matches.
|
|
85
|
+
*/
|
|
86
|
+
createTypename?: string;
|
|
87
|
+
} & Pick<FormFieldComponentProps, 'autoFocus' | 'readonly' | 'layout'> &
|
|
88
|
+
Pick<
|
|
89
|
+
RefFieldProps,
|
|
90
|
+
| 'createOptionLabel'
|
|
91
|
+
| 'createOptionIcon'
|
|
92
|
+
| 'createInitialValuePath'
|
|
93
|
+
| 'createFieldMap'
|
|
94
|
+
| 'db'
|
|
95
|
+
| 'useSchema'
|
|
96
|
+
| 'getOptions'
|
|
97
|
+
| 'onCreate'
|
|
98
|
+
>;
|
|
99
|
+
|
|
100
|
+
export const FormField = (props: FormFieldProps) => {
|
|
101
|
+
const {
|
|
102
|
+
type,
|
|
103
|
+
name,
|
|
104
|
+
path,
|
|
105
|
+
projection,
|
|
106
|
+
fieldMap,
|
|
107
|
+
fieldProvider,
|
|
108
|
+
readonly,
|
|
109
|
+
layout,
|
|
110
|
+
|
|
111
|
+
// RefFieldProps
|
|
112
|
+
createTypename,
|
|
113
|
+
createOptionLabel,
|
|
114
|
+
createOptionIcon,
|
|
115
|
+
createInitialValuePath,
|
|
116
|
+
createFieldMap,
|
|
117
|
+
db,
|
|
118
|
+
useSchema: schemaHook,
|
|
119
|
+
getOptions,
|
|
120
|
+
onCreate,
|
|
121
|
+
} = props;
|
|
122
|
+
const { t } = useTranslation(translationKey);
|
|
123
|
+
const title = getAnnotation<string>(SchemaAST.TitleAnnotationId)(type);
|
|
124
|
+
const description = getAnnotation<string>(SchemaAST.DescriptionAnnotationId)(type);
|
|
125
|
+
const examples = getAnnotation<string[]>(SchemaAST.ExamplesAnnotationId)(type);
|
|
126
|
+
|
|
127
|
+
const label = useMemo(() => title ?? String.capitalize(name), [title, name]);
|
|
128
|
+
const placeholder = useMemo(
|
|
129
|
+
() => (examples?.length ? `${t('example.placeholder')}: ${examples[0]}` : (description ?? label)),
|
|
130
|
+
[examples, description, label],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const fieldState = useFormFieldState(FormField.displayName, path);
|
|
134
|
+
const fieldProps: FormFieldComponentProps = {
|
|
135
|
+
type,
|
|
136
|
+
format: Format.FormatAnnotation.getFromAst(type).pipe((annotation) => Option.getOrUndefined(annotation)),
|
|
137
|
+
readonly,
|
|
138
|
+
label,
|
|
139
|
+
placeholder,
|
|
140
|
+
layout,
|
|
141
|
+
db,
|
|
142
|
+
...fieldState,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
//
|
|
146
|
+
// Custom field.
|
|
147
|
+
//
|
|
148
|
+
|
|
149
|
+
const jsonPath = createJsonPath(path ?? []);
|
|
150
|
+
const CustomField = fieldMap?.[jsonPath];
|
|
151
|
+
if (CustomField) {
|
|
152
|
+
return <CustomField {...fieldProps} />;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// TODO(burdon): Expensive to create schema each time; pass AST?
|
|
156
|
+
const component = fieldProvider?.({ schema: Schema.make(type), prop: name, fieldProps });
|
|
157
|
+
if (component) {
|
|
158
|
+
return component;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//
|
|
162
|
+
// Array field.
|
|
163
|
+
//
|
|
164
|
+
|
|
165
|
+
if (isArrayType(type)) {
|
|
166
|
+
return <ArrayField fieldProps={fieldState} label={label} {...props} />;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
//
|
|
170
|
+
// Regular field.
|
|
171
|
+
//
|
|
172
|
+
|
|
173
|
+
const Field = getFormField(fieldProps);
|
|
174
|
+
if (Field) {
|
|
175
|
+
return <Field {...fieldProps} />;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
//
|
|
179
|
+
// Select field.
|
|
180
|
+
//
|
|
181
|
+
|
|
182
|
+
const options = getSelectOptions(type);
|
|
183
|
+
if (options) {
|
|
184
|
+
// Resolve labels from projection metadata when available.
|
|
185
|
+
const fieldProjections = projection?.getFieldProjections();
|
|
186
|
+
const fieldProjection = fieldProjections?.find((fp) => fp.field.path === name);
|
|
187
|
+
const selectOptions = fieldProjection?.props.options;
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<SelectField
|
|
191
|
+
{...fieldProps}
|
|
192
|
+
options={options.map((option) => {
|
|
193
|
+
const selectOption = selectOptions?.find((so) => so.id === globalThis.String(option));
|
|
194
|
+
return {
|
|
195
|
+
value: option,
|
|
196
|
+
label: selectOption?.title ?? option.toString(),
|
|
197
|
+
};
|
|
198
|
+
})}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//
|
|
204
|
+
// Ref field.
|
|
205
|
+
//
|
|
206
|
+
|
|
207
|
+
const refProps = getRefProps(type);
|
|
208
|
+
if (refProps) {
|
|
209
|
+
const isCreateTarget = !createTypename || refProps.typename === createTypename;
|
|
210
|
+
return (
|
|
211
|
+
<RefField
|
|
212
|
+
{...fieldProps}
|
|
213
|
+
{...refProps}
|
|
214
|
+
createOptionLabel={isCreateTarget ? createOptionLabel : undefined}
|
|
215
|
+
createOptionIcon={isCreateTarget ? createOptionIcon : undefined}
|
|
216
|
+
createInitialValuePath={isCreateTarget ? createInitialValuePath : undefined}
|
|
217
|
+
createFieldMap={isCreateTarget ? createFieldMap : undefined}
|
|
218
|
+
db={db}
|
|
219
|
+
useSchema={schemaHook}
|
|
220
|
+
getOptions={getOptions}
|
|
221
|
+
onCreate={onCreate}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
//
|
|
227
|
+
// Nested Object field.
|
|
228
|
+
//
|
|
229
|
+
|
|
230
|
+
if (isNestedType(type)) {
|
|
231
|
+
const baseNode = findNode(type, isDiscriminatedUnion);
|
|
232
|
+
const typeLiteral = baseNode
|
|
233
|
+
? getDiscriminatedType(baseNode, fieldState.getValue() as any)
|
|
234
|
+
: findNode(type, SchemaAST.isTypeLiteral);
|
|
235
|
+
|
|
236
|
+
if (typeLiteral) {
|
|
237
|
+
const schema = Schema.make(typeLiteral);
|
|
238
|
+
return (
|
|
239
|
+
<FormFieldSet
|
|
240
|
+
schema={schema}
|
|
241
|
+
path={path}
|
|
242
|
+
readonly={readonly}
|
|
243
|
+
layout={layout}
|
|
244
|
+
label={label}
|
|
245
|
+
projection={projection}
|
|
246
|
+
fieldMap={fieldMap}
|
|
247
|
+
fieldProvider={fieldProvider}
|
|
248
|
+
createOptionLabel={createOptionLabel}
|
|
249
|
+
createOptionIcon={createOptionIcon}
|
|
250
|
+
createInitialValuePath={createInitialValuePath}
|
|
251
|
+
db={db}
|
|
252
|
+
useSchema={schemaHook}
|
|
253
|
+
getOptions={getOptions}
|
|
254
|
+
onCreate={onCreate}
|
|
255
|
+
/>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
FormField.displayName = 'Form.FormField';
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get property input component.
|
|
267
|
+
*/
|
|
268
|
+
const getFormField = ({ type, format }: FormFieldComponentProps): FormFieldComponent | undefined => {
|
|
269
|
+
//
|
|
270
|
+
// Standard formats.
|
|
271
|
+
//
|
|
272
|
+
|
|
273
|
+
const formatField = Match.value(format).pipe(
|
|
274
|
+
Match.withReturnType<FormFieldComponent | undefined>(),
|
|
275
|
+
Match.when(Format.TypeFormat.GeoPoint, () => GeoPointField),
|
|
276
|
+
Match.when(Format.TypeFormat.Markdown, () => MarkdownField),
|
|
277
|
+
Match.when(Format.TypeFormat.Text, () => TextAreaField),
|
|
278
|
+
Match.orElse(() => undefined),
|
|
279
|
+
);
|
|
280
|
+
if (formatField) {
|
|
281
|
+
return formatField;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//
|
|
285
|
+
// Standard types.
|
|
286
|
+
//
|
|
287
|
+
|
|
288
|
+
switch (type._tag) {
|
|
289
|
+
// TODO(wittjosiah): Schema.Any is currently used to represent template inputs.
|
|
290
|
+
case 'AnyKeyword':
|
|
291
|
+
case 'StringKeyword':
|
|
292
|
+
return TextField;
|
|
293
|
+
case 'NumberKeyword':
|
|
294
|
+
return NumberField;
|
|
295
|
+
case 'BooleanKeyword':
|
|
296
|
+
return BooleanField;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const getSelectOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
|
|
301
|
+
if (isLiteralUnion(ast)) {
|
|
302
|
+
return ast.types.map((type) => type.literal).filter((v): v is string | number => v !== null);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return Format.OptionsAnnotation.getFromAst(ast).pipe((annotation) => Option.getOrUndefined(annotation));
|
|
306
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { type ChangeEvent, useCallback } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Input } from '@dxos/react-ui';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
|
|
11
|
+
import { type FormFieldComponentProps, FormFieldWrapper } from './FormFieldComponent';
|
|
12
|
+
|
|
13
|
+
const DefaultStory = ({
|
|
14
|
+
type,
|
|
15
|
+
placeholder,
|
|
16
|
+
readonly,
|
|
17
|
+
onBlur,
|
|
18
|
+
onValueChange,
|
|
19
|
+
...props
|
|
20
|
+
}: FormFieldComponentProps<string>) => {
|
|
21
|
+
const handleChange = useCallback(
|
|
22
|
+
(event: ChangeEvent<HTMLInputElement>) => onValueChange(type, event.target.value),
|
|
23
|
+
[type, onValueChange],
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className='p-form-padding'>
|
|
28
|
+
<FormFieldWrapper<string> {...props}>
|
|
29
|
+
{({ value = '' }) => (
|
|
30
|
+
<Input.TextInput
|
|
31
|
+
disabled={!!readonly}
|
|
32
|
+
placeholder={placeholder}
|
|
33
|
+
noAutoFill
|
|
34
|
+
value={value}
|
|
35
|
+
onChange={handleChange}
|
|
36
|
+
onBlur={onBlur}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
</FormFieldWrapper>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const meta: Meta<typeof DefaultStory> = {
|
|
45
|
+
title: 'ui/react-ui-form/FormFieldComponent',
|
|
46
|
+
component: DefaultStory,
|
|
47
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
48
|
+
parameters: {
|
|
49
|
+
layout: 'fullscreen',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
|
|
55
|
+
type Story = StoryObj<typeof meta>;
|
|
56
|
+
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
label: 'Label',
|
|
60
|
+
getStatus: () => ({ status: 'error', error: 'Required field' }),
|
|
61
|
+
getValue: () => 'DXOS',
|
|
62
|
+
onBlur: () => {},
|
|
63
|
+
onValueChange: () => {},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
import type * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
+
import React, {
|
|
8
|
+
Component,
|
|
9
|
+
type FC,
|
|
10
|
+
type FocusEvent,
|
|
11
|
+
type PropsWithChildren,
|
|
12
|
+
type ReactElement,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from 'react';
|
|
15
|
+
|
|
16
|
+
import { type Database } from '@dxos/echo';
|
|
17
|
+
import { type Format } from '@dxos/echo/internal';
|
|
18
|
+
import { Icon, Input, Tooltip } from '@dxos/react-ui';
|
|
19
|
+
import { inputTextLabel } from '@dxos/ui-theme';
|
|
20
|
+
|
|
21
|
+
import { type FormFieldStatus } from '../../hooks';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Dynamic props passed to input components.
|
|
25
|
+
*/
|
|
26
|
+
export type FormFieldStateProps<T = any> = {
|
|
27
|
+
getStatus: () => FormFieldStatus;
|
|
28
|
+
getValue: () => T | undefined;
|
|
29
|
+
onBlur: (event: FocusEvent<HTMLElement>) => void;
|
|
30
|
+
onValueChange: (type: SchemaAST.AST, value: T) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Presentation mode.
|
|
35
|
+
* - full: Show label, control, and status.
|
|
36
|
+
* - compact: Show label and control.
|
|
37
|
+
* - inline: Control only.
|
|
38
|
+
* - static: Plain DOM; omit all undefined values.
|
|
39
|
+
*/
|
|
40
|
+
// TODO(burdon): 'inline' should be orthogonal to the layout (e.g., array of inline static).
|
|
41
|
+
export type Presentation = 'full' | 'compact' | 'inline' | 'static';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Props passed to input components.
|
|
45
|
+
*/
|
|
46
|
+
export type FormFieldComponentProps<T = any> = {
|
|
47
|
+
type: SchemaAST.AST;
|
|
48
|
+
format?: Format.TypeFormat;
|
|
49
|
+
readonly?: boolean;
|
|
50
|
+
label: string;
|
|
51
|
+
placeholder?: string;
|
|
52
|
+
autoFocus?: boolean;
|
|
53
|
+
layout?: Presentation;
|
|
54
|
+
/** Database the form is editing against; populated for fields whose value is an ECHO object/Ref. */
|
|
55
|
+
db?: Database.Database;
|
|
56
|
+
} & FormFieldStateProps<T>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Form field component.
|
|
60
|
+
*/
|
|
61
|
+
export type FormFieldComponent = FC<FormFieldComponentProps>;
|
|
62
|
+
|
|
63
|
+
export type FormFieldMap = Record<string, FormFieldComponent>;
|
|
64
|
+
|
|
65
|
+
export type FormFieldProvider = (props: {
|
|
66
|
+
prop: string; // TODO(burdon): Path?
|
|
67
|
+
schema: Schema.Schema<any>;
|
|
68
|
+
fieldProps: FormFieldComponentProps;
|
|
69
|
+
}) => ReactElement | undefined;
|
|
70
|
+
|
|
71
|
+
//
|
|
72
|
+
// FormFieldLabel
|
|
73
|
+
//
|
|
74
|
+
|
|
75
|
+
export type FormFieldLabelProps = {
|
|
76
|
+
asChild?: boolean;
|
|
77
|
+
error?: string;
|
|
78
|
+
} & Pick<FormFieldComponentProps, 'label' | 'readonly'>;
|
|
79
|
+
|
|
80
|
+
export const FormFieldLabel = ({ label, error, readonly, asChild }: FormFieldLabelProps) => {
|
|
81
|
+
const Label = readonly || asChild ? 'span' : Input.Label;
|
|
82
|
+
return (
|
|
83
|
+
<div className='flex items-center justify-between'>
|
|
84
|
+
<Label className={inputTextLabel}>{label}</Label>
|
|
85
|
+
{error && (
|
|
86
|
+
<Tooltip.Trigger asChild content={error} side='bottom'>
|
|
87
|
+
<Icon icon='ph--warning--regular' size={4} classNames='text-error-text' />
|
|
88
|
+
</Tooltip.Trigger>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
FormFieldLabel.displayName = 'Form.FieldLabel';
|
|
95
|
+
|
|
96
|
+
//
|
|
97
|
+
// FormFieldWrapper
|
|
98
|
+
//
|
|
99
|
+
|
|
100
|
+
export type FormFieldWrapperProps<T = any> = Pick<
|
|
101
|
+
FormFieldComponentProps,
|
|
102
|
+
'readonly' | 'label' | 'layout' | 'getStatus' | 'getValue'
|
|
103
|
+
> & {
|
|
104
|
+
children?: (props: { value: T }) => ReactNode;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
|
|
108
|
+
const { children, readonly, layout, label, getStatus, getValue } = props;
|
|
109
|
+
const { status, error } = getStatus();
|
|
110
|
+
|
|
111
|
+
const value = getValue();
|
|
112
|
+
if (layout === 'static' && value == null) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const str = String(value ?? '');
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className='contents'>
|
|
120
|
+
<Input.Root validationValence={status}>
|
|
121
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
122
|
+
{layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
|
|
123
|
+
{layout === 'full' && (
|
|
124
|
+
<Input.DescriptionAndValidation>
|
|
125
|
+
<Input.Validation>{error}</Input.Validation>
|
|
126
|
+
</Input.DescriptionAndValidation>
|
|
127
|
+
)}
|
|
128
|
+
</Input.Root>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
//
|
|
134
|
+
// FormFieldErrorBoundary
|
|
135
|
+
//
|
|
136
|
+
|
|
137
|
+
type FormFieldErrorState = {
|
|
138
|
+
error: Error | undefined;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
type FormFieldErrorBoundaryProps = PropsWithChildren<{
|
|
142
|
+
path?: (string | number)[];
|
|
143
|
+
}>;
|
|
144
|
+
|
|
145
|
+
export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProps, FormFieldErrorState> {
|
|
146
|
+
static getDerivedStateFromError(error: Error): { error: Error } {
|
|
147
|
+
return { error };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override state = { error: undefined };
|
|
151
|
+
|
|
152
|
+
override componentDidUpdate(prevProps: FormFieldErrorBoundaryProps) {
|
|
153
|
+
if (prevProps.path !== this.props.path) {
|
|
154
|
+
this.resetError();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
override render() {
|
|
159
|
+
if (this.state.error) {
|
|
160
|
+
return (
|
|
161
|
+
<div className='flex gap-2 border border-rose-fill font-mono text-sm'>
|
|
162
|
+
<span className='bg-rose-fill text-base-foreground px-1 font-thin'>ERROR</span>
|
|
163
|
+
{String(this.props.path?.join('.'))}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return this.props.children;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private resetError() {
|
|
172
|
+
this.setState({ error: undefined });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type AnyProperties } from '@dxos/echo/internal';
|
|
8
|
+
import { type SchemaProperty } from '@dxos/effect';
|
|
9
|
+
|
|
10
|
+
import { type FormHandlerProps } from '../../hooks';
|
|
11
|
+
import { getFormProperties } from '../../util';
|
|
12
|
+
import { useFormValues } from './Form';
|
|
13
|
+
import { FormField, type FormFieldProps } from './FormField';
|
|
14
|
+
import { FormFieldErrorBoundary, FormFieldLabel } from './FormFieldComponent';
|
|
15
|
+
|
|
16
|
+
const FORM_FIELDSET_NAME = 'Form.FieldSet';
|
|
17
|
+
|
|
18
|
+
export type FormFieldSetProps<T extends AnyProperties> = {
|
|
19
|
+
label?: string;
|
|
20
|
+
sort?: string[];
|
|
21
|
+
exclude?: (props: SchemaProperty[]) => SchemaProperty[];
|
|
22
|
+
} & Pick<FormHandlerProps<T>, 'schema'> &
|
|
23
|
+
Pick<
|
|
24
|
+
FormFieldProps,
|
|
25
|
+
| 'path'
|
|
26
|
+
| 'autoFocus'
|
|
27
|
+
| 'readonly'
|
|
28
|
+
| 'layout'
|
|
29
|
+
| 'projection'
|
|
30
|
+
| 'fieldMap'
|
|
31
|
+
| 'fieldProvider'
|
|
32
|
+
| 'createTypename'
|
|
33
|
+
| 'createOptionLabel'
|
|
34
|
+
| 'createOptionIcon'
|
|
35
|
+
| 'createInitialValuePath'
|
|
36
|
+
| 'createFieldMap'
|
|
37
|
+
| 'db'
|
|
38
|
+
| 'useSchema'
|
|
39
|
+
| 'getOptions'
|
|
40
|
+
| 'onCreate'
|
|
41
|
+
>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Renders a set of form fields derived from a schema object.
|
|
45
|
+
*/
|
|
46
|
+
export const FormFieldSet = ({
|
|
47
|
+
label,
|
|
48
|
+
schema,
|
|
49
|
+
readonly,
|
|
50
|
+
path,
|
|
51
|
+
sort,
|
|
52
|
+
exclude,
|
|
53
|
+
projection,
|
|
54
|
+
layout,
|
|
55
|
+
...props
|
|
56
|
+
}: FormFieldSetProps<any>) => {
|
|
57
|
+
const values = useFormValues(FORM_FIELDSET_NAME, path);
|
|
58
|
+
const properties = useFormFieldSetProperties({ schema, values, exclude, sort, projection });
|
|
59
|
+
if ((readonly || layout === 'static') && values == null) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
{layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
|
|
66
|
+
{properties.map((property) => {
|
|
67
|
+
const name = property.name.toString();
|
|
68
|
+
return (
|
|
69
|
+
<FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
|
|
70
|
+
<FormField
|
|
71
|
+
type={property.type}
|
|
72
|
+
name={name}
|
|
73
|
+
path={[...(path ?? []), name]}
|
|
74
|
+
readonly={readonly}
|
|
75
|
+
layout={layout}
|
|
76
|
+
projection={projection}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
</FormFieldErrorBoundary>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
FormFieldSet.displayName = FORM_FIELDSET_NAME;
|
|
87
|
+
|
|
88
|
+
type UseFormFieldSetPropertiesParams = Pick<FormFieldSetProps<any>, 'schema' | 'exclude' | 'projection' | 'sort'> & {
|
|
89
|
+
values: AnyProperties | undefined;
|
|
90
|
+
sort?: string[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolves ordered schema properties for a field set (projection order, exclude, or sort).
|
|
95
|
+
*/
|
|
96
|
+
const useFormFieldSetProperties = ({
|
|
97
|
+
schema,
|
|
98
|
+
exclude,
|
|
99
|
+
projection,
|
|
100
|
+
values,
|
|
101
|
+
sort,
|
|
102
|
+
}: UseFormFieldSetPropertiesParams): SchemaProperty[] => {
|
|
103
|
+
// TODO(burdon): Updates on every value change.
|
|
104
|
+
// Remove values dep if can remove from getSchemaProperties.
|
|
105
|
+
return useMemo(() => {
|
|
106
|
+
if (!schema) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
|
|
111
|
+
const schemaProps = getFormProperties(schema.ast);
|
|
112
|
+
const filteredProps = exclude ? exclude(schemaProps) : schemaProps;
|
|
113
|
+
|
|
114
|
+
// Use projection-based field management when view and projection are available.
|
|
115
|
+
if (projection) {
|
|
116
|
+
const fieldProjections = projection.getFieldProjections();
|
|
117
|
+
const hiddenProperties = new Set(projection.getHiddenProperties());
|
|
118
|
+
|
|
119
|
+
// Filter properties to only include visible ones and order by projection.
|
|
120
|
+
const visibleProps = filteredProps.filter((prop) => !hiddenProperties.has(prop.name.toString()));
|
|
121
|
+
const orderedProps: SchemaProperty[] = [];
|
|
122
|
+
|
|
123
|
+
// Add properties in projection field order.
|
|
124
|
+
for (const fieldProjection of fieldProjections) {
|
|
125
|
+
const fieldPath = String(fieldProjection.field.path);
|
|
126
|
+
const prop = visibleProps.find((prop) => prop.name === fieldPath);
|
|
127
|
+
if (prop) {
|
|
128
|
+
orderedProps.push(prop);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Add any remaining properties not in projection.
|
|
133
|
+
const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
|
|
134
|
+
const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
|
|
135
|
+
orderedProps.push(...remainingProps);
|
|
136
|
+
return orderedProps;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fallback to legacy filter/sort behavior.
|
|
140
|
+
return sort
|
|
141
|
+
? [...filteredProps].sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
|
|
142
|
+
: filteredProps;
|
|
143
|
+
}, [schema, values, exclude, sort, projection]);
|
|
144
|
+
};
|