@dxos/react-ui-form 0.9.1-main.c7dcc2e112 → 0.9.1-staging.ee54ba693a
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/dist/lib/browser/index.mjs +1883 -1539
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1883 -1539
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/annotations.d.ts +47 -0
- package/dist/types/src/annotations.d.ts.map +1 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +12 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +46 -65
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.theme.d.ts +252 -0
- package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.theme.test.d.ts +2 -0
- package/dist/types/src/components/Form/Form.theme.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormControls.d.ts +17 -8
- package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts +16 -2
- package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts +27 -0
- package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/FormRow.d.ts +85 -0
- package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/{FormFieldWrapper.stories.d.ts → FormRow.stories.d.ts} +4 -1
- package/dist/types/src/components/Form/FormField/FormRow.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +11 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts +4 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -0
- package/dist/types/src/components/{Settings/Settings.stories.d.ts → Form/FormField/fields/PasswordField/PasswordField.stories.d.ts} +1 -2
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/InlineRefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +18 -2
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/index.d.ts +4 -0
- package/dist/types/src/components/Form/FormField/fields/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/index.d.ts +4 -3
- package/dist/types/src/components/Form/FormField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/presentation.d.ts +22 -0
- package/dist/types/src/components/Form/FormField/presentation.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts +8 -5
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +22 -0
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormFieldSet/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +2 -0
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +5 -5
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/meta-tags.d.ts +1 -12
- package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +2 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -2
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAsyncFieldEffect.d.ts +24 -0
- package/dist/types/src/hooks/useAsyncFieldEffect.d.ts.map +1 -0
- package/dist/types/src/hooks/useFormContext.d.ts +8 -3
- package/dist/types/src/hooks/useFormContext.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +8 -2
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/TestLayout.d.ts +1 -1
- package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
- package/dist/types/src/testing/schema.d.ts +13 -16
- package/dist/types/src/testing/schema.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +50 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/omit.d.ts +1 -1
- package/dist/types/src/util/omit.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -40
- package/src/annotations.ts +63 -0
- package/src/components/FieldEditor/FieldEditor.tsx +5 -2
- package/src/components/Form/DESIGN.md +24 -0
- package/src/components/Form/Form.stories.tsx +257 -13
- package/src/components/Form/Form.theme.test.ts +33 -0
- package/src/components/Form/Form.theme.ts +86 -0
- package/src/components/Form/Form.tsx +15 -9
- package/src/components/Form/FormControls.tsx +94 -37
- package/src/components/Form/FormField/FormField.tsx +83 -32
- package/src/components/Form/FormField/FormFieldHeader.tsx +68 -0
- package/src/components/Form/FormField/{FormFieldWrapper.stories.tsx → FormRow.stories.tsx} +34 -9
- package/src/components/Form/FormField/FormRow.tsx +298 -0
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.tsx +25 -9
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +83 -24
- package/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.tsx +59 -0
- package/src/components/Form/FormField/fields/AsyncSelectField/index.ts +5 -0
- package/src/components/Form/FormField/fields/AutofillField/AutofillField.tsx +67 -0
- package/src/components/Form/FormField/fields/AutofillField/index.ts +5 -0
- package/src/components/Form/FormField/fields/BooleanField/BooleanField.tsx +5 -6
- package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +104 -0
- package/src/components/Form/FormField/fields/ComboboxField/index.ts +5 -0
- package/src/components/Form/FormField/fields/DateField/DateField.tsx +9 -9
- package/src/components/Form/FormField/fields/GeoPointField/GeoPointField.tsx +17 -20
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +2 -2
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +39 -40
- package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +29 -0
- package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +60 -21
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.tsx +53 -0
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +41 -0
- package/src/components/Form/FormField/fields/PasswordField/index.ts +5 -0
- package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +16 -10
- package/src/components/Form/FormField/fields/RefField/RefField.test.ts +8 -0
- package/src/components/Form/FormField/fields/RefField/RefField.tsx +50 -23
- package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +104 -47
- package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +3 -3
- package/src/components/Form/FormField/fields/TextAreaField/TextAreaField.tsx +3 -3
- package/src/components/Form/FormField/fields/TextField/TextField.tsx +3 -3
- package/src/components/Form/FormField/fields/TupleField/TupleField.tsx +1 -1
- package/src/components/Form/FormField/fields/index.ts +4 -0
- package/src/components/Form/FormField/index.ts +5 -3
- package/src/components/Form/FormField/presentation.tsx +32 -0
- package/src/components/Form/FormFieldSet/FormFieldSet.tsx +49 -70
- package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +93 -0
- package/src/components/Form/FormFieldSet/index.ts +1 -0
- package/src/components/Form/FormLayout/FormLayout.stories.tsx +7 -5
- package/src/components/Form/FormLayout/FormLayout.tsx +24 -14
- package/src/components/Form/FormLayout/index.ts +1 -1
- package/src/components/Form/meta-tags.test.ts +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +2 -2
- package/src/components/ObjectPicker/ObjectPicker.tsx +1 -0
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
- package/src/components/ObjectProperties/ObjectProperties.tsx +12 -8
- package/src/components/RefEditor/RefEditor.stories.tsx +2 -2
- package/src/components/RefEditor/RefEditor.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +2 -2
- package/src/components/ViewEditor/ViewEditor.test.tsx +8 -2
- package/src/components/ViewEditor/ViewEditor.tsx +3 -3
- package/src/components/index.ts +0 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAsyncFieldEffect.ts +81 -0
- package/src/hooks/useFormContext.ts +6 -3
- package/src/hooks/useFormHandler.ts +11 -6
- package/src/index.ts +1 -0
- package/src/testing/TestLayout.tsx +15 -12
- package/src/testing/schema.ts +43 -43
- package/src/types.ts +53 -1
- package/src/util/index.ts +0 -1
- package/src/util/omit.ts +25 -11
- package/src/util/properties.test.ts +110 -28
- package/src/util/refs.ts +1 -1
- package/dist/types/src/components/Form/FormField/FieldHeader.d.ts +0 -19
- package/dist/types/src/components/Form/FormField/FieldHeader.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts +0 -47
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.stories.d.ts.map +0 -1
- package/dist/types/src/components/Settings/Settings.d.ts +0 -51
- package/dist/types/src/components/Settings/Settings.d.ts.map +0 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +0 -43
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsItem.d.ts +0 -16
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +0 -18
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +0 -1
- package/dist/types/src/components/Settings/index.d.ts +0 -4
- package/dist/types/src/components/Settings/index.d.ts.map +0 -1
- package/dist/types/src/util/field-type.d.ts +0 -19
- package/dist/types/src/util/field-type.d.ts.map +0 -1
- package/src/components/Form/FormField/FieldHeader.tsx +0 -36
- package/src/components/Form/FormField/FormFieldWrapper.tsx +0 -179
- package/src/components/Settings/AUDIT.md +0 -51
- package/src/components/Settings/Settings.stories.tsx +0 -104
- package/src/components/Settings/Settings.tsx +0 -81
- package/src/components/Settings/SettingsFieldSet.tsx +0 -257
- package/src/components/Settings/SettingsItem.tsx +0 -58
- package/src/components/Settings/SettingsSelectField.tsx +0 -59
- package/src/components/Settings/index.ts +0 -7
- package/src/util/field-type.ts +0 -60
|
@@ -15,15 +15,17 @@ import { HuePicker } from '@dxos/react-ui-pickers';
|
|
|
15
15
|
import { FactoryAnnotation } from '@dxos/schema';
|
|
16
16
|
|
|
17
17
|
import { translationKey } from '#translations';
|
|
18
|
-
import { type FormFieldMap } from '#types';
|
|
18
|
+
import { type FormFieldMap, type RefFieldDataProps } from '#types';
|
|
19
19
|
|
|
20
20
|
import { Form, META_TAGS_KEY, withMetaTags } from '../Form';
|
|
21
21
|
|
|
22
|
-
export type ObjectPropertiesProps = PropsWithChildren<
|
|
22
|
+
export type ObjectPropertiesProps = PropsWithChildren<
|
|
23
|
+
{ object: Obj.Unknown } & Pick<RefFieldDataProps, 'getCreateDefaults' | 'resolveCreateEntry'>
|
|
24
|
+
>;
|
|
23
25
|
|
|
24
26
|
// TODO(wittjosiah): Reconcile w/ ObjectForm.
|
|
25
27
|
export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps>(
|
|
26
|
-
({ children, object, ...props }, forwardedRef) => {
|
|
28
|
+
({ children, object, getCreateDefaults, resolveCreateEntry, ...props }, forwardedRef) => {
|
|
27
29
|
const db = Obj.getDatabase(object);
|
|
28
30
|
const meta = Obj.getMeta(object);
|
|
29
31
|
// `meta.tags` already holds `Ref<Tag>`s (materialized by the database handler).
|
|
@@ -111,6 +113,7 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
|
|
|
111
113
|
|
|
112
114
|
return (
|
|
113
115
|
<Form.Root
|
|
116
|
+
db={db}
|
|
114
117
|
schema={formSchema}
|
|
115
118
|
defaultValues={values as any}
|
|
116
119
|
createTypename={Type.getTypename(Tag.Tag)}
|
|
@@ -118,14 +121,15 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
|
|
|
118
121
|
createOptionLabel={['add-tag.label', { ns: translationKey }]}
|
|
119
122
|
createInitialValuePath='label'
|
|
120
123
|
createFieldMap={createFieldMap}
|
|
121
|
-
db={db}
|
|
122
124
|
onValuesChanged={handleChange}
|
|
123
125
|
onCreate={handleCreate}
|
|
126
|
+
getCreateDefaults={getCreateDefaults}
|
|
127
|
+
resolveCreateEntry={resolveCreateEntry}
|
|
124
128
|
>
|
|
125
|
-
<Form.Viewport {...composableProps(props)} ref={forwardedRef}>
|
|
129
|
+
<Form.Viewport {...composableProps(props)} scroll ref={forwardedRef}>
|
|
126
130
|
<Form.Content>
|
|
127
131
|
<Form.FieldSet />
|
|
128
|
-
{children}
|
|
132
|
+
<Form.Section>{children}</Form.Section>
|
|
129
133
|
</Form.Content>
|
|
130
134
|
</Form.Viewport>
|
|
131
135
|
</Form.Root>
|
|
@@ -134,12 +138,12 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
|
|
|
134
138
|
);
|
|
135
139
|
|
|
136
140
|
const createFieldMap: FormFieldMap = {
|
|
137
|
-
hue: ({ type, label,
|
|
141
|
+
hue: ({ type, label, presentation, getValue, onValueChange }) => {
|
|
138
142
|
const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
|
|
139
143
|
const handleReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
|
|
140
144
|
return (
|
|
141
145
|
<>
|
|
142
|
-
{
|
|
146
|
+
{presentation !== 'inline' && <Form.Label label={label} />}
|
|
143
147
|
<HuePicker value={getValue()} onChange={handleChange} onReset={handleReset} />
|
|
144
148
|
</>
|
|
145
149
|
);
|
|
@@ -69,11 +69,11 @@ const PeopleGrid = ({ db }: { db?: Database.Database }) => {
|
|
|
69
69
|
);
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
type
|
|
72
|
+
type StoryArgs = Partial<Pick<RefEditorProps, 'value' | 'mode' | 'activateOnTyping' | 'autoFocus'>>;
|
|
73
73
|
|
|
74
74
|
// NOTE: The Person config (type/match/getValues) is supplied in render, not args — Storybook's
|
|
75
75
|
// arg traversal attempts to mutate the proxied ECHO schema and crashes.
|
|
76
|
-
const DefaultStory = ({ value: initialValue, ...args }:
|
|
76
|
+
const DefaultStory = ({ value: initialValue, ...args }: StoryArgs) => {
|
|
77
77
|
const { space } = useClientStory();
|
|
78
78
|
const [value, setValue] = useState(initialValue ?? '');
|
|
79
79
|
|
|
@@ -246,7 +246,7 @@ export const RefEditor = forwardRef<EditorController, RefEditorProps>(
|
|
|
246
246
|
>
|
|
247
247
|
<Editor.View
|
|
248
248
|
// TOOD(burdon): Use same style as react-ui Input.
|
|
249
|
-
classNames='border border-separator rounded-xs px-2'
|
|
249
|
+
classNames='border border-input-separator rounded-xs px-2'
|
|
250
250
|
{...props}
|
|
251
251
|
initialValue={value}
|
|
252
252
|
selectionEnd
|
|
@@ -35,9 +35,9 @@ export type ViewEditorDebugObjects = {
|
|
|
35
35
|
projection: ProjectionModel;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
type
|
|
38
|
+
type StoryArgs = Pick<ViewEditorProps, 'readonly' | 'mode'>;
|
|
39
39
|
|
|
40
|
-
const DefaultStory = (props:
|
|
40
|
+
const DefaultStory = (props: StoryArgs) => {
|
|
41
41
|
const { space } = useClientStory();
|
|
42
42
|
const [type, setType] = useState<Type.AnyEntity>();
|
|
43
43
|
const [view, setView] = useState<View.View>();
|
|
@@ -138,11 +138,17 @@ describe('ViewEditor', () => {
|
|
|
138
138
|
expect(addedPropertyProjection!.props.property).toBe('added_property');
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
test('delete property', { retry: 2 }, async () => {
|
|
141
|
+
test('delete property', async () => {
|
|
143
142
|
await Default.run();
|
|
144
143
|
await waitForViewEditor();
|
|
145
144
|
|
|
145
|
+
// Wait for the field list to render — useObject(view) inside ViewEditor resolves
|
|
146
|
+
// asynchronously after the outer story's type/view state is set, so the debug
|
|
147
|
+
// element can appear before the list items are in the DOM.
|
|
148
|
+
await waitFor(() => {
|
|
149
|
+
expect(screen.getByText('name')).toBeInTheDocument();
|
|
150
|
+
});
|
|
151
|
+
|
|
146
152
|
// Find the delete button for the 'name' property.
|
|
147
153
|
const nameProperty = screen.getByText('name');
|
|
148
154
|
const propertyRow = nameProperty.closest('[role="listitem"]');
|
|
@@ -39,10 +39,10 @@ import {
|
|
|
39
39
|
import { mx } from '@dxos/ui-theme';
|
|
40
40
|
|
|
41
41
|
import { translationKey } from '#translations';
|
|
42
|
-
import { type
|
|
42
|
+
import { type FormFieldMap, type FormFieldRenderer, type FormFieldRendererProps } from '#types';
|
|
43
43
|
|
|
44
44
|
import { FieldEditor } from '../FieldEditor';
|
|
45
|
-
import { CompactIconButton,
|
|
45
|
+
import { CompactIconButton, Form, FormFieldHeader, FormFieldLabel, type FormRootProps } from '../Form';
|
|
46
46
|
|
|
47
47
|
export type ViewEditorProps = ThemedClassName<
|
|
48
48
|
{
|
|
@@ -352,7 +352,7 @@ const FieldList = ({ type, view, registry, readonly, showHeading = false, onDele
|
|
|
352
352
|
>
|
|
353
353
|
{({ items: fields }) => (
|
|
354
354
|
<>
|
|
355
|
-
<
|
|
355
|
+
<FormFieldHeader
|
|
356
356
|
label={t('fields.label')}
|
|
357
357
|
readonly={readonly}
|
|
358
358
|
add={{
|
package/src/components/index.ts
CHANGED
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Cause from 'effect/Cause';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Exit from 'effect/Exit';
|
|
8
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
|
+
|
|
12
|
+
/** Picks the declared dependency fields from the form values — the subset a dynamic field subscribes to. */
|
|
13
|
+
export const pickValues = (values: AnyProperties | undefined, deps: readonly string[]): Record<string, unknown> => {
|
|
14
|
+
const subset: Record<string, unknown> = {};
|
|
15
|
+
for (const dep of deps) {
|
|
16
|
+
subset[dep] = values?.[dep];
|
|
17
|
+
}
|
|
18
|
+
return subset;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type AsyncFieldEffectState<A> = {
|
|
22
|
+
loading: boolean;
|
|
23
|
+
error: unknown;
|
|
24
|
+
data: A | undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type UseAsyncFieldEffectOptions = {
|
|
28
|
+
/** Debounce applied before running, so per-keystroke value changes coalesce into one run. */
|
|
29
|
+
debounceMs?: number;
|
|
30
|
+
/** When false, the effect is not run and prior data/error is cleared. */
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Runs a self-contained Effect (`R = never`) whenever `key` changes, debounced and abortable. Used by
|
|
36
|
+
* dynamic form fields to load select options, derive values, or validate against an external source from
|
|
37
|
+
* the current form values. The latest `factory` is always used, but a run is only (re)triggered when
|
|
38
|
+
* `key` changes — derive `key` from the inputs the factory actually reads. Re-running interrupts the
|
|
39
|
+
* prior run (cancelling any in-flight request).
|
|
40
|
+
*/
|
|
41
|
+
export const useAsyncFieldEffect = <A>(
|
|
42
|
+
factory: () => Effect.Effect<A, unknown>,
|
|
43
|
+
key: string,
|
|
44
|
+
{ debounceMs = 400, enabled = true }: UseAsyncFieldEffectOptions = {},
|
|
45
|
+
): AsyncFieldEffectState<A> => {
|
|
46
|
+
const factoryRef = useRef(factory);
|
|
47
|
+
factoryRef.current = factory;
|
|
48
|
+
const [state, setState] = useState<AsyncFieldEffectState<A>>({ loading: false, error: undefined, data: undefined });
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!enabled) {
|
|
52
|
+
setState({ loading: false, error: undefined, data: undefined });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let cancel: (() => void) | undefined;
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
setState((prev) => ({ ...prev, loading: true, error: undefined }));
|
|
59
|
+
cancel = Effect.runCallback(factoryRef.current(), {
|
|
60
|
+
onExit: (exit: Exit.Exit<A, unknown>) => {
|
|
61
|
+
Exit.match(exit, {
|
|
62
|
+
onSuccess: (data) => setState({ loading: false, error: undefined, data }),
|
|
63
|
+
onFailure: (cause) => {
|
|
64
|
+
// A superseding run interrupts this one; ignore the resulting interruption.
|
|
65
|
+
if (!Cause.isInterruptedOnly(cause)) {
|
|
66
|
+
setState((prev) => ({ loading: false, error: Cause.squash(cause), data: prev.data }));
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}, debounceMs);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
cancel?.();
|
|
77
|
+
};
|
|
78
|
+
}, [key, enabled, debounceMs]);
|
|
79
|
+
|
|
80
|
+
return state;
|
|
81
|
+
};
|
|
@@ -9,8 +9,9 @@ import { useEffect, useMemo, useRef } from 'react';
|
|
|
9
9
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
10
10
|
import { SchemaEx } from '@dxos/effect';
|
|
11
11
|
|
|
12
|
-
import { type
|
|
12
|
+
import { type FieldContext, type FormFieldStateProps } from '#types';
|
|
13
13
|
|
|
14
|
+
import { type FormVariant } from '../components/Form/Form.theme';
|
|
14
15
|
import { type FormHandler } from './useFormHandler';
|
|
15
16
|
|
|
16
17
|
//
|
|
@@ -27,8 +28,10 @@ export type FormContextValue<T extends AnyProperties = any> = {
|
|
|
27
28
|
* Testing.
|
|
28
29
|
*/
|
|
29
30
|
testId?: string;
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
/** Visual variant applied across the form's parts (see Form.theme). */
|
|
33
|
+
variant?: FormVariant;
|
|
34
|
+
} & FieldContext;
|
|
32
35
|
|
|
33
36
|
export const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
|
|
34
37
|
|
|
@@ -77,17 +77,21 @@ export type FormHandler<T extends AnyProperties> = Pick<FormHandlerProps<T>, 'sc
|
|
|
77
77
|
/** Initial values (which may not pass validation). */
|
|
78
78
|
values: Partial<T>;
|
|
79
79
|
|
|
80
|
-
/**
|
|
80
|
+
/** Fields whose value has been edited (set by `onValueChange`). Gates autosave-on-blur. */
|
|
81
81
|
changed: Record<SchemaEx.JsonPath, boolean>;
|
|
82
|
-
|
|
82
|
+
/** Fields the user has interacted with (set by `onBlur`). Gates error *visibility* — errors only surface once touched. */
|
|
83
83
|
touched: Record<SchemaEx.JsonPath, boolean>;
|
|
84
84
|
|
|
85
85
|
/** Map of error strings. */
|
|
86
86
|
errors: Record<SchemaEx.JsonPath, string>;
|
|
87
87
|
|
|
88
|
-
/** Whether the
|
|
88
|
+
/** Whether the values pass validation (no errors at all). */
|
|
89
89
|
isValid: boolean;
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Whether the submit affordance should be enabled. Stricter than `isValid`: it also requires no save
|
|
92
|
+
* in flight (`saving`) and only blocks on errors for *touched* fields, so a pristine form with
|
|
93
|
+
* not-yet-touched required fields can still show an enabled control.
|
|
94
|
+
*/
|
|
91
95
|
canSave: boolean;
|
|
92
96
|
|
|
93
97
|
onSave: () => void;
|
|
@@ -325,6 +329,7 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
325
329
|
}),
|
|
326
330
|
[
|
|
327
331
|
schema,
|
|
332
|
+
autoSave,
|
|
328
333
|
values,
|
|
329
334
|
errors,
|
|
330
335
|
touched,
|
|
@@ -335,8 +340,8 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
335
340
|
getValue,
|
|
336
341
|
onBlur,
|
|
337
342
|
onValueChange,
|
|
338
|
-
|
|
339
|
-
|
|
343
|
+
handleSave,
|
|
344
|
+
handleCancel,
|
|
340
345
|
],
|
|
341
346
|
);
|
|
342
347
|
};
|
package/src/index.ts
CHANGED
|
@@ -7,23 +7,26 @@ import React, { type PropsWithChildren } from 'react';
|
|
|
7
7
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
8
|
import { composableProps, slottable } from '@dxos/react-ui';
|
|
9
9
|
import { Syntax } from '@dxos/react-ui-syntax-highlighter';
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
10
11
|
|
|
11
|
-
type TestLayoutProps = PropsWithChildren<{ json?:
|
|
12
|
+
type TestLayoutProps = PropsWithChildren<{ json?: unknown }>;
|
|
12
13
|
|
|
13
14
|
export const TestLayout = ({ children, json }: TestLayoutProps) => {
|
|
14
15
|
return (
|
|
15
|
-
<div className='dx-container grid grid-cols-
|
|
16
|
+
<div className={mx('dx-container grid grid-cols-1 p-4 gap-4', !!json && 'grid-cols-[1fr_1fr]')}>
|
|
16
17
|
<TestPanel>{children}</TestPanel>
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
<Syntax.
|
|
20
|
-
<Syntax.
|
|
21
|
-
|
|
22
|
-
<Syntax.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
{!!json && (
|
|
19
|
+
<TestPanel>
|
|
20
|
+
<Syntax.Root data={json}>
|
|
21
|
+
<Syntax.Content>
|
|
22
|
+
<Syntax.Filter />
|
|
23
|
+
<Syntax.Viewport>
|
|
24
|
+
<Syntax.Code testId='debug' classNames='text-sm' />
|
|
25
|
+
</Syntax.Viewport>
|
|
26
|
+
</Syntax.Content>
|
|
27
|
+
</Syntax.Root>
|
|
28
|
+
</TestPanel>
|
|
29
|
+
)}
|
|
27
30
|
</div>
|
|
28
31
|
);
|
|
29
32
|
};
|
package/src/testing/schema.ts
CHANGED
|
@@ -12,48 +12,48 @@ import { Annotation, DXN, Format, Ref, Tag, Type } from '@dxos/echo';
|
|
|
12
12
|
* structs, refs, ref arrays, enums, and the various formats.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
export class Organization extends Type.makeObject<Organization>(DXN.make('com.example.type.organization', '0.1.0'))(
|
|
16
|
+
Schema.Struct({
|
|
17
|
+
name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
|
|
18
|
+
}),
|
|
19
|
+
) {}
|
|
18
20
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
address: Schema.optional(
|
|
26
|
-
Schema.Struct({
|
|
27
|
-
street: Schema.String,
|
|
28
|
-
city: Schema.String,
|
|
29
|
-
// TODO(burdon): Constrain input control.
|
|
30
|
-
state: Schema.String.pipe(Schema.minLength(2), Schema.maxLength(2)).annotations({
|
|
31
|
-
title: 'State',
|
|
32
|
-
description: 'State code',
|
|
33
|
-
}),
|
|
34
|
-
zip: Schema.Number.annotations({ title: 'ZIP Code' }),
|
|
35
|
-
}).annotations({ title: 'Address' }),
|
|
36
|
-
),
|
|
37
|
-
employer: Schema.optional(Ref.Ref(Organization).annotations({ title: 'Employer' })),
|
|
38
|
-
tags: Schema.optional(Schema.Array(Ref.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
|
|
39
|
-
status: Schema.optional(Schema.Literal('active', 'inactive').annotations({ title: 'Status' })),
|
|
40
|
-
notes: Schema.optional(Format.Text.annotations({ title: 'Notes' })),
|
|
41
|
-
location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
|
|
42
|
-
birthday: Schema.optional(Format.DateOnly.annotations({ title: 'Birthday' })),
|
|
43
|
-
meetingAt: Schema.optional(Format.DateTime.annotations({ title: 'Next meeting' })),
|
|
44
|
-
reminderAt: Schema.optional(Format.TimeOnly.annotations({ title: 'Reminder time' })),
|
|
45
|
-
tasks: Schema.optional(Schema.Array(Schema.String).annotations({ title: 'Tasks' })),
|
|
46
|
-
locations: Schema.optional(Schema.Array(Format.GeoPoint).annotations({ title: 'Locations' })),
|
|
47
|
-
identities: Schema.optional(
|
|
48
|
-
Schema.Array(
|
|
21
|
+
export class Person extends Type.makeObject<Person>(DXN.make('org.dxos.type.person', '0.1.0'))(
|
|
22
|
+
Schema.Struct({
|
|
23
|
+
name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
|
|
24
|
+
ignore: Schema.String.pipe(Annotation.FormInputAnnotation.set(false), Schema.optional),
|
|
25
|
+
active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
|
|
26
|
+
address: Schema.optional(
|
|
49
27
|
Schema.Struct({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
28
|
+
street: Schema.String,
|
|
29
|
+
city: Schema.String,
|
|
30
|
+
// TODO(burdon): Constrain input control.
|
|
31
|
+
state: Schema.String.pipe(Schema.minLength(2), Schema.maxLength(2)).annotations({
|
|
32
|
+
title: 'State',
|
|
33
|
+
description: 'State code',
|
|
34
|
+
}),
|
|
35
|
+
zip: Schema.Number.annotations({ title: 'ZIP Code' }),
|
|
36
|
+
}).annotations({ title: 'Address' }),
|
|
37
|
+
),
|
|
38
|
+
employer: Schema.optional(Ref.Ref(Organization).annotations({ title: 'Employer' })),
|
|
39
|
+
tags: Schema.optional(Schema.Array(Ref.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
|
|
40
|
+
status: Schema.optional(Schema.Literal('active', 'inactive').annotations({ title: 'Status' })),
|
|
41
|
+
notes: Schema.optional(Format.Text.annotations({ title: 'Notes' })),
|
|
42
|
+
location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
|
|
43
|
+
birthday: Schema.optional(Format.DateOnly.annotations({ title: 'Birthday' })),
|
|
44
|
+
meetingAt: Schema.optional(Format.DateTime.annotations({ title: 'Next meeting' })),
|
|
45
|
+
reminderAt: Schema.optional(Format.TimeOnly.annotations({ title: 'Reminder time' })),
|
|
46
|
+
tasks: Schema.optional(Schema.Array(Schema.String).annotations({ title: 'Tasks' })),
|
|
47
|
+
locations: Schema.optional(Schema.Array(Format.GeoPoint).annotations({ title: 'Locations' })),
|
|
48
|
+
identities: Schema.optional(
|
|
49
|
+
Schema.Array(
|
|
50
|
+
Schema.Struct({
|
|
51
|
+
type: Schema.String.annotations({ title: 'Type' }),
|
|
52
|
+
value: Schema.String.annotations({ title: 'Value' }),
|
|
53
|
+
}).annotations({ title: 'Identities' }),
|
|
54
|
+
).annotations({
|
|
55
|
+
title: 'Identities',
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
|
+
}),
|
|
59
|
+
) {}
|
package/src/types.ts
CHANGED
|
@@ -52,13 +52,21 @@ export type FormFieldRendererProps<T = any> = {
|
|
|
52
52
|
format?: Format.TypeFormat;
|
|
53
53
|
readonly?: boolean;
|
|
54
54
|
label: string;
|
|
55
|
+
/**
|
|
56
|
+
* Human-readable description (from the schema's description annotation). Rendered as visible text
|
|
57
|
+
* only when the resolved presentation opts in; otherwise it falls back to the input placeholder.
|
|
58
|
+
*/
|
|
59
|
+
description?: string;
|
|
55
60
|
/**
|
|
56
61
|
* Dotted JSON path of this field within the form values (e.g. `runtime.client.storage.persistent`).
|
|
57
62
|
*/
|
|
58
63
|
jsonPath?: string;
|
|
59
64
|
placeholder?: string;
|
|
60
65
|
autoFocus?: boolean;
|
|
61
|
-
|
|
66
|
+
/** Presentation mode for the field (full/compact/inline/static); see {@link FormPresentation}. */
|
|
67
|
+
presentation?: FormPresentation;
|
|
68
|
+
/** Whether the field is required (non-optional in the schema); surfaces a trailing asterisk on the label. */
|
|
69
|
+
required?: boolean;
|
|
62
70
|
} & FormFieldStateProps<T>;
|
|
63
71
|
|
|
64
72
|
export type FormFieldRenderer = FC<FormFieldRendererProps>;
|
|
@@ -75,6 +83,18 @@ export type FormFieldProvider = (props: {
|
|
|
75
83
|
// Ref / create contracts.
|
|
76
84
|
//
|
|
77
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Per-typename override for the ref-field inline create form and its submission handler.
|
|
88
|
+
* The type parameter S links the form schema's decoded type to the createObject values argument,
|
|
89
|
+
* ensuring the schema and the handler agree on the shape of the form data.
|
|
90
|
+
*/
|
|
91
|
+
export type CreateEntryOverride<S extends Schema.Schema.AnyNoContext = Schema.Schema.AnyNoContext> = {
|
|
92
|
+
/** Replaces the raw ECHO type schema for the inline create form. */
|
|
93
|
+
inputSchema?: S;
|
|
94
|
+
/** Runs instead of the default onCreate(schema, values) path; values are typed from inputSchema. */
|
|
95
|
+
createObject?: (values: Schema.Schema.Type<S>, db: Database.Database) => Promise<Obj.Unknown>;
|
|
96
|
+
};
|
|
97
|
+
|
|
78
98
|
/**
|
|
79
99
|
* Configuration for the inline "create a new referenced object" affordance, shared by every
|
|
80
100
|
* surface that can spawn a ref target (the picker, `RefField`, and the `Form*` props that forward
|
|
@@ -91,6 +111,8 @@ export type CreateOptions = {
|
|
|
91
111
|
export type RefOption = {
|
|
92
112
|
id: string;
|
|
93
113
|
label: string;
|
|
114
|
+
/** Optional secondary line shown beneath the label in the picker list. */
|
|
115
|
+
description?: string;
|
|
94
116
|
hue?: Palette;
|
|
95
117
|
};
|
|
96
118
|
|
|
@@ -112,6 +134,20 @@ export type RefFieldDataProps = {
|
|
|
112
134
|
* object is then wired into this slot's form value as a Ref.
|
|
113
135
|
*/
|
|
114
136
|
onCreate?: (schema: Type.AnyEntity, values: any) => Obj.Unknown | Promise<Obj.Unknown> | undefined | void;
|
|
137
|
+
/**
|
|
138
|
+
* Supply default values for a newly-created owned object when the user clicks "add" on an owned-ref
|
|
139
|
+
* array field (see `FormCreateAnnotation`). Keyed by the field's json path so a container can pre-populate
|
|
140
|
+
* the new object (e.g. a back-reference to the parent). The returned values are passed to
|
|
141
|
+
* `onCreate(schema, values)` before the object is persisted.
|
|
142
|
+
*/
|
|
143
|
+
getCreateDefaults?: (props: { jsonPath: string; schema: Type.AnyEntity }) => Record<string, unknown> | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Optional resolver that maps a ref's typename to a {@link CreateEntryOverride}, replacing the
|
|
146
|
+
* raw ECHO type schema and/or the default `onCreate` path with plugin-registered create logic
|
|
147
|
+
* (e.g. `SpaceCapabilities.CreateObjectEntry`). When absent or returning `undefined` for a given
|
|
148
|
+
* typename the existing fallback behaviour is preserved.
|
|
149
|
+
*/
|
|
150
|
+
resolveCreateEntry?: (typename: string) => CreateEntryOverride | undefined;
|
|
115
151
|
};
|
|
116
152
|
|
|
117
153
|
/**
|
|
@@ -132,3 +168,19 @@ export type FormFieldOptions = {
|
|
|
132
168
|
*/
|
|
133
169
|
createTypename?: string;
|
|
134
170
|
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The subset of {@link RefFieldDataProps} that is forwarded down the field recursion. `useResults`
|
|
174
|
+
* is consumed only by `RefField` itself (it defaults internally), so it is not threaded.
|
|
175
|
+
*/
|
|
176
|
+
export type RefThreadedProps = Pick<
|
|
177
|
+
RefFieldDataProps,
|
|
178
|
+
'useType' | 'getOptions' | 'onCreate' | 'getCreateDefaults' | 'resolveCreateEntry'
|
|
179
|
+
>;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Form-wide configuration threaded unchanged down the entire field recursion. This is the single
|
|
183
|
+
* canonical source for the bag that `FormContextValue`, `FormFieldProps`, and `FormFieldSetProps`
|
|
184
|
+
* all carry — so none of them re-Picks fragments of the others.
|
|
185
|
+
*/
|
|
186
|
+
export type FieldContext = FormFieldOptions & CreateOptions & RefThreadedProps;
|
package/src/util/index.ts
CHANGED
package/src/util/omit.ts
CHANGED
|
@@ -9,14 +9,13 @@ import * as SchemaAST from 'effect/SchemaAST';
|
|
|
9
9
|
import { Annotation, Type } from '@dxos/echo';
|
|
10
10
|
|
|
11
11
|
// TODO(burdon): Reconcile with @dxos/echo.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
>;
|
|
12
|
+
// Distributive: applied per union member so `ExcludeId<A | B>` → `ExcludeId<A> | ExcludeId<B>` rather
|
|
13
|
+
// than `Omit<InstanceType<A> | InstanceType<B>, 'id'>` (which would intersect away per-member narrowing).
|
|
14
|
+
export type ExcludeId<S extends Schema.Schema.AnyNoContext | Type.AnyEntity> = S extends Type.AnyEntity
|
|
15
|
+
? Omit<Type.InstanceType<S>, 'id'>
|
|
16
|
+
: S extends Schema.Schema.AnyNoContext
|
|
17
|
+
? Omit<Schema.Schema.Type<S>, 'id'>
|
|
18
|
+
: never;
|
|
20
19
|
|
|
21
20
|
// TODO(burdon): Move to @dxos/schema (re-export here).
|
|
22
21
|
export const omitId = <S extends Schema.Schema.AnyNoContext | Type.AnyEntity>(
|
|
@@ -25,9 +24,24 @@ export const omitId = <S extends Schema.Schema.AnyNoContext | Type.AnyEntity>(
|
|
|
25
24
|
const schema = Type.isType(schemaOrType)
|
|
26
25
|
? Type.getSchema(schemaOrType)
|
|
27
26
|
: (schemaOrType as Schema.Schema.AnyNoContext);
|
|
28
|
-
// Cast: `Schema.omit` cannot statically express the `ExcludeId<S>` result type, so the
|
|
29
|
-
//
|
|
30
|
-
return schema
|
|
27
|
+
// Cast: `Schema.omit` cannot statically express the `ExcludeId<S>` result type, so the result is a
|
|
28
|
+
// widened schema; the runtime shape matches the declared return type.
|
|
29
|
+
return omitIdFromSchema(schema) as any;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Removes the `id` field where present, preserving the schema's shape. Recurses into unions so a
|
|
34
|
+
* discriminated (create) schema keeps its discriminant — `Schema.omit('id')` is a struct operation and
|
|
35
|
+
* would otherwise flatten the union, breaking variant rendering. A no-op when there is no `id` (plain
|
|
36
|
+
* create structs that never carried one).
|
|
37
|
+
*/
|
|
38
|
+
const omitIdFromSchema = (schema: Schema.Schema.AnyNoContext): Schema.Schema.AnyNoContext => {
|
|
39
|
+
const ast = schema.ast;
|
|
40
|
+
if (SchemaAST.isUnion(ast)) {
|
|
41
|
+
return Schema.Union(...ast.types.map((type) => omitIdFromSchema(Schema.make(type))));
|
|
42
|
+
}
|
|
43
|
+
const hasId = SchemaAST.getPropertySignatures(ast).some((prop) => prop.name === 'id');
|
|
44
|
+
return hasId ? schema.pipe(Schema.omit('id')) : schema;
|
|
31
45
|
};
|
|
32
46
|
|
|
33
47
|
/**
|