@object-ui/fields 17.1.0 → 17.3.0
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/CHANGELOG.md +786 -0
- package/dist/FieldEditWidget.d.ts +2 -2
- package/dist/__tests__/cell-truncation.test.d.ts +0 -0
- package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
- package/dist/__tests__/spec-symbol-batch7.test.d.ts +8 -0
- package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-props-contract.test.d.ts +8 -0
- package/dist/index.cjs +5 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2882 -2569
- package/dist/widgets/AddressField.d.ts +2 -2
- package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/AutoNumberField.d.ts +2 -2
- package/dist/widgets/AvatarField.d.ts +2 -2
- package/dist/widgets/BooleanField.d.ts +2 -2
- package/dist/widgets/CapabilityMultiSelectField.d.ts +2 -2
- package/dist/widgets/CharacterCount.d.ts +56 -0
- package/dist/widgets/CheckboxesField.d.ts +2 -2
- package/dist/widgets/CodeField.d.ts +2 -2
- package/dist/widgets/ColorField.d.ts +2 -2
- package/dist/widgets/CurrencyField.d.ts +2 -2
- package/dist/widgets/DateField.d.ts +2 -2
- package/dist/widgets/DateTimeField.d.ts +2 -2
- package/dist/widgets/EmailField.d.ts +2 -2
- package/dist/widgets/FileField.d.ts +2 -2
- package/dist/widgets/FilterConditionField.d.ts +2 -2
- package/dist/widgets/FormulaField.d.ts +2 -2
- package/dist/widgets/FullscreenFieldEditor.d.ts +155 -0
- package/dist/widgets/GeolocationField.d.ts +2 -2
- package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/GridField.d.ts +2 -2
- package/dist/widgets/ImageField.d.ts +2 -2
- package/dist/widgets/LocationField.d.ts +2 -2
- package/dist/widgets/LookupField.d.ts +2 -2
- package/dist/widgets/LookupField.gateHintLabel.test.d.ts +8 -0
- package/dist/widgets/MasterDetailField.d.ts +2 -2
- package/dist/widgets/MultiSelectField.d.ts +2 -2
- package/dist/widgets/NumberField.d.ts +2 -2
- package/dist/widgets/ObjectField.d.ts +2 -2
- package/dist/widgets/ObjectRefField.d.ts +2 -2
- package/dist/widgets/OptionsEmptyState.d.ts +45 -0
- package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
- package/dist/widgets/PasswordField.d.ts +2 -2
- package/dist/widgets/PercentField.d.ts +2 -2
- package/dist/widgets/PhoneField.d.ts +2 -2
- package/dist/widgets/QRCodeField.d.ts +2 -2
- package/dist/widgets/RadioField.d.ts +2 -2
- package/dist/widgets/RatingField.d.ts +2 -2
- package/dist/widgets/RecipientPickerField.d.ts +2 -2
- package/dist/widgets/RecordPickerDialog.d.ts +17 -1
- package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +8 -0
- package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +8 -0
- package/dist/widgets/RichTextField.d.ts +20 -2
- package/dist/widgets/SelectField.d.ts +2 -2
- package/dist/widgets/SignatureField.d.ts +2 -2
- package/dist/widgets/SliderField.d.ts +2 -2
- package/dist/widgets/SummaryField.d.ts +2 -2
- package/dist/widgets/TagsField.d.ts +2 -2
- package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
- package/dist/widgets/TextAreaField.d.ts +45 -6
- package/dist/widgets/TextField.d.ts +2 -2
- package/dist/widgets/TimeField.d.ts +2 -2
- package/dist/widgets/UrlField.d.ts +2 -2
- package/dist/widgets/UserField.d.ts +2 -2
- package/dist/widgets/VectorField.d.ts +2 -2
- package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/file-value.d.ts +20 -33
- package/dist/widgets/toDomProps.d.ts +91 -0
- package/dist/widgets/types.d.ts +215 -4
- package/dist/withFieldCarrier.d.ts +56 -0
- package/package.json +13 -13
- package/src/FieldEditWidget.tsx +6 -4
- package/src/__tests__/cell-truncation.test.tsx +96 -0
- package/src/__tests__/field-carrier-sdui.test.tsx +169 -0
- package/src/__tests__/spec-symbol-batch7.test.ts +126 -0
- package/src/__tests__/validation-feedback.test.tsx +22 -13
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +265 -0
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +338 -0
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +598 -0
- package/src/__tests__/widget-props-contract.test.tsx +142 -0
- package/src/complex-widgets.test.tsx +3 -3
- package/src/datetime-widgets.test.tsx +2 -2
- package/src/index.tsx +77 -27
- package/src/widgets/AddressField.tsx +32 -13
- package/src/widgets/AddressField.uniqueIds.test.tsx +92 -0
- package/src/widgets/AutoNumberField.tsx +2 -2
- package/src/widgets/AvatarField.tsx +10 -3
- package/src/widgets/BooleanField.tsx +5 -5
- package/src/widgets/CapabilityMultiSelectField.tsx +4 -4
- package/src/widgets/CharacterCount.tsx +263 -0
- package/src/widgets/CheckboxesField.tsx +35 -18
- package/src/widgets/CodeField.tsx +10 -3
- package/src/widgets/ColorField.tsx +12 -3
- package/src/widgets/CurrencyField.tsx +6 -5
- package/src/widgets/DateField.tsx +4 -4
- package/src/widgets/DateTimeField.tsx +4 -4
- package/src/widgets/EmailField.tsx +6 -6
- package/src/widgets/FileField.tsx +14 -4
- package/src/widgets/FilterConditionField.tsx +14 -3
- package/src/widgets/FormulaField.tsx +3 -3
- package/src/widgets/FullscreenFieldEditor.tsx +271 -0
- package/src/widgets/GeolocationField.tsx +22 -7
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +88 -0
- package/src/widgets/GridField.tsx +4 -4
- package/src/widgets/ImageField.tsx +10 -3
- package/src/widgets/LocationField.tsx +10 -3
- package/src/widgets/LookupField.gateHintLabel.test.tsx +135 -0
- package/src/widgets/LookupField.tsx +50 -15
- package/src/widgets/MasterDetailField.tsx +3 -3
- package/src/widgets/MultiSelectField.tsx +34 -18
- package/src/widgets/NumberField.tsx +5 -5
- package/src/widgets/ObjectField.tsx +18 -9
- package/src/widgets/ObjectRefField.tsx +14 -4
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +88 -0
- package/src/widgets/OptionsEmptyState.test.tsx +169 -0
- package/src/widgets/OptionsEmptyState.tsx +83 -0
- package/src/widgets/PasswordField.tsx +5 -5
- package/src/widgets/PeoplePicker.tsx +7 -1
- package/src/widgets/PercentField.tsx +6 -5
- package/src/widgets/PhoneField.tsx +6 -6
- package/src/widgets/QRCodeField.tsx +10 -3
- package/src/widgets/RadioField.tsx +28 -18
- package/src/widgets/RatingField.tsx +19 -4
- package/src/widgets/RecipientPickerField.tsx +22 -4
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +611 -0
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +112 -0
- package/src/widgets/RecordPickerDialog.tsx +173 -8
- package/src/widgets/RichTextField.tsx +152 -25
- package/src/widgets/SelectField.tsx +53 -22
- package/src/widgets/SignatureField.tsx +2 -2
- package/src/widgets/SliderField.tsx +3 -3
- package/src/widgets/SummaryField.tsx +3 -3
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +68 -0
- package/src/widgets/TagsField.placeholder.test.tsx +95 -0
- package/src/widgets/TagsField.tsx +25 -4
- package/src/widgets/TextAreaField.tsx +172 -94
- package/src/widgets/TextField.tsx +5 -5
- package/src/widgets/TimeField.tsx +4 -4
- package/src/widgets/UrlField.tsx +6 -6
- package/src/widgets/UserField.tsx +3 -3
- package/src/widgets/VectorField.tsx +3 -3
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +248 -0
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +116 -0
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +244 -0
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +195 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +417 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +287 -0
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +160 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +161 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +346 -0
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +246 -0
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +175 -0
- package/src/widgets/file-value.ts +21 -5
- package/src/widgets/toDomProps.ts +175 -0
- package/src/widgets/types.ts +230 -7
- package/src/widgets/useFieldTranslation.ts +60 -0
- package/src/withFieldCarrier.tsx +75 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `TextAreaField`'s fullscreen affordance has ONE source (objectui#3232).
|
|
11
|
+
*
|
|
12
|
+
* It used to be read from four places with a `??` chain — a `mobileFullscreen`
|
|
13
|
+
* (camelCase) prop, `field.mobile_fullscreen`, a `mobile_fullscreen` prop, and
|
|
14
|
+
* `schema.mobile_fullscreen`. Only the field-metadata flag was ever produced:
|
|
15
|
+
* `ObjectForm` stamps it on long-text fields from
|
|
16
|
+
* `ObjectFormSchema.mobile.fullscreenLongText`. The camelCase prop had no
|
|
17
|
+
* producer anywhere in the repo, and the snake_case prop could not arrive
|
|
18
|
+
* because `stripRegisteredFieldProps` (components/src/renderers/form/form.tsx)
|
|
19
|
+
* removes it from what registered widgets are forwarded.
|
|
20
|
+
*
|
|
21
|
+
* So this file pins both halves of that convergence:
|
|
22
|
+
*
|
|
23
|
+
* 1. **The live path still works.** The metadata flag drives the affordance
|
|
24
|
+
* end to end — button, dialog, and the committed edit. Deleting the dead
|
|
25
|
+
* reads must not have cost the working behaviour, and this is the test
|
|
26
|
+
* that fails if a future refactor drops the real read too.
|
|
27
|
+
* 2. **The retired prop spellings are gone**, at compile time (they are not
|
|
28
|
+
* on the closed `FieldWidgetComponentProps`, objectui#3221) and at runtime
|
|
29
|
+
* (a host that passes one anyway gets no affordance). Convergence means a
|
|
30
|
+
* misspelled flag is now inert and loud, not silently absorbed.
|
|
31
|
+
*
|
|
32
|
+
* Note the built-in (unregistered) `textarea` branch in `form.tsx` reads
|
|
33
|
+
* `mobile_fullscreen || fullscreen` off the form-field props and renders its
|
|
34
|
+
* own `FullscreenTextarea`. That is a separate, live path and is untouched
|
|
35
|
+
* here — this file is about the registered `field:textarea` widget only.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
39
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
40
|
+
import '@testing-library/jest-dom';
|
|
41
|
+
|
|
42
|
+
import { TextAreaField } from '../TextAreaField';
|
|
43
|
+
import type { FieldWidgetComponentProps } from '../types';
|
|
44
|
+
import type { FieldMetadata } from '@object-ui/types';
|
|
45
|
+
|
|
46
|
+
const fieldMeta = (extra: Record<string, unknown> = {}) =>
|
|
47
|
+
({
|
|
48
|
+
name: 'description',
|
|
49
|
+
label: 'Description',
|
|
50
|
+
type: 'textarea',
|
|
51
|
+
...extra,
|
|
52
|
+
}) as unknown as FieldMetadata;
|
|
53
|
+
|
|
54
|
+
describe('TextAreaField mobile fullscreen — the metadata flag is the only source', () => {
|
|
55
|
+
it('renders the expand affordance when the field metadata sets mobile_fullscreen', () => {
|
|
56
|
+
render(
|
|
57
|
+
<TextAreaField
|
|
58
|
+
value="hello"
|
|
59
|
+
onChange={() => {}}
|
|
60
|
+
field={fieldMeta({ mobile_fullscreen: true })}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(screen.getByTestId('textarea-fullscreen-toggle')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('opens the fullscreen dialog and commits the edited draft', () => {
|
|
68
|
+
const onChange = vi.fn();
|
|
69
|
+
render(
|
|
70
|
+
<TextAreaField
|
|
71
|
+
value="hello"
|
|
72
|
+
onChange={onChange}
|
|
73
|
+
field={fieldMeta({ mobile_fullscreen: true })}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Closed until the affordance is used — the dialog is not just mounted.
|
|
78
|
+
expect(screen.queryByTestId('textarea-fullscreen-dialog')).not.toBeInTheDocument();
|
|
79
|
+
|
|
80
|
+
fireEvent.click(screen.getByTestId('textarea-fullscreen-toggle'));
|
|
81
|
+
expect(screen.getByTestId('textarea-fullscreen-dialog')).toBeInTheDocument();
|
|
82
|
+
|
|
83
|
+
const dialogInput = screen.getByTestId('textarea-fullscreen-input');
|
|
84
|
+
expect(dialogInput).toHaveValue('hello');
|
|
85
|
+
|
|
86
|
+
fireEvent.change(dialogInput, { target: { value: 'hello from fullscreen' } });
|
|
87
|
+
// The draft is local until "Done" — the host is not notified per keystroke.
|
|
88
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
89
|
+
|
|
90
|
+
fireEvent.click(screen.getByTestId('textarea-fullscreen-save'));
|
|
91
|
+
expect(onChange).toHaveBeenCalledWith('hello from fullscreen');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('no longer reads the flag off a `schema` prop (objectui#3233)', () => {
|
|
95
|
+
// This used to be the second carrier: `SchemaRenderer` passed the authored
|
|
96
|
+
// node as `schema` and the widget resolved `field || schema`. v17 converged
|
|
97
|
+
// that at the producer — the registry adapter (`withFieldCarrier`) maps the
|
|
98
|
+
// SDUI node onto `field` before any widget sees it, so the widget has one
|
|
99
|
+
// read. A host that still passes `schema` is now inert AND loud: a compile
|
|
100
|
+
// error on the closed props type, and no affordance at runtime.
|
|
101
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
102
|
+
try {
|
|
103
|
+
const legacyHostProps = {
|
|
104
|
+
schema: fieldMeta({ mobile_fullscreen: true }),
|
|
105
|
+
} as unknown as Partial<FieldWidgetComponentProps<string>>;
|
|
106
|
+
|
|
107
|
+
render(
|
|
108
|
+
<TextAreaField
|
|
109
|
+
value=""
|
|
110
|
+
onChange={() => {}}
|
|
111
|
+
field={fieldMeta()}
|
|
112
|
+
{...legacyHostProps}
|
|
113
|
+
/>,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(screen.queryByTestId('textarea-fullscreen-toggle')).not.toBeInTheDocument();
|
|
117
|
+
} finally {
|
|
118
|
+
consoleError.mockRestore();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('renders no affordance when the metadata does not opt in', () => {
|
|
123
|
+
render(<TextAreaField value="hello" onChange={() => {}} field={fieldMeta()} />);
|
|
124
|
+
|
|
125
|
+
expect(screen.queryByTestId('textarea-fullscreen-toggle')).not.toBeInTheDocument();
|
|
126
|
+
expect(screen.queryByTestId('textarea-fullscreen-dialog')).not.toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('TextAreaField mobile fullscreen — the retired prop spellings', () => {
|
|
131
|
+
it('does not declare them on the widget contract', () => {
|
|
132
|
+
const props = {} as FieldWidgetComponentProps<string>;
|
|
133
|
+
|
|
134
|
+
// Both were read by the widget and written by no host. With the closed
|
|
135
|
+
// props type (objectui#3221) they are now compile errors, so a future
|
|
136
|
+
// author reaches for the metadata flag instead of a silently ignored prop.
|
|
137
|
+
// @ts-expect-error `mobileFullscreen` is not part of this contract
|
|
138
|
+
void props.mobileFullscreen;
|
|
139
|
+
// @ts-expect-error `mobile_fullscreen` is a metadata key, not a widget prop
|
|
140
|
+
void props.mobile_fullscreen;
|
|
141
|
+
// …and the retired second METADATA carrier (objectui#3233). The flag has
|
|
142
|
+
// one source and that source now has one carrier.
|
|
143
|
+
// @ts-expect-error `schema` was retired from the widget contract in v17
|
|
144
|
+
void props.schema;
|
|
145
|
+
|
|
146
|
+
expect(true).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('ignores them at runtime when a host passes them anyway', () => {
|
|
150
|
+
// Untyped hosts (plain JS, `as any` spreads) can still get these onto the
|
|
151
|
+
// element. They must not resurrect the affordance: the metadata flag is
|
|
152
|
+
// the contract. Unknown props land on the DOM spread, so React's unknown
|
|
153
|
+
// -attribute warnings are expected noise here and are suppressed.
|
|
154
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
155
|
+
try {
|
|
156
|
+
const hostProps = {
|
|
157
|
+
mobileFullscreen: true,
|
|
158
|
+
mobile_fullscreen: true,
|
|
159
|
+
} as unknown as Partial<FieldWidgetComponentProps<string>>;
|
|
160
|
+
|
|
161
|
+
render(
|
|
162
|
+
<TextAreaField
|
|
163
|
+
value="hello"
|
|
164
|
+
onChange={() => {}}
|
|
165
|
+
field={fieldMeta()}
|
|
166
|
+
{...hostProps}
|
|
167
|
+
/>,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(screen.queryByTestId('textarea-fullscreen-toggle')).not.toBeInTheDocument();
|
|
171
|
+
} finally {
|
|
172
|
+
consoleError.mockRestore();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
* onto the same contract.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
import { isFileIdToken } from '@objectstack/spec/data';
|
|
33
|
+
|
|
32
34
|
/** A file value normalised for rendering, whatever form it arrived in. */
|
|
33
35
|
export interface FileValueView {
|
|
34
36
|
/** `sys_file` id, when the value carries one. */
|
|
@@ -45,12 +47,26 @@ export interface FileValueView {
|
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
49
|
* A minted file id: uuid/nanoid-shaped, and crucially not a URL — a URL always
|
|
48
|
-
* carries `:`, `/` or `.`, so it can never match.
|
|
49
|
-
*
|
|
50
|
+
* carries `:`, `/` or `.`, so it can never match.
|
|
51
|
+
*
|
|
52
|
+
* This IS the platform's arbiter now, not a mirror of it (objectui#3161,
|
|
53
|
+
* objectstack#4115 ledger batch 7). The declaration that stood here said it
|
|
54
|
+
* "mirrors the platform's `isFileIdToken`" while being a character-for-character
|
|
55
|
+
* copy of that function's body under that function's own name — the shape
|
|
56
|
+
* objectui#3003 argued about and objectui#3169 caught again in
|
|
57
|
+
* `isAggregatedViewContainer`: every value test and every behaviour test passes
|
|
58
|
+
* against a faithful copy, so reference identity is the only check that can tell
|
|
59
|
+
* a re-export from a fork. It is asserted in
|
|
60
|
+
* `__tests__/spec-symbol-batch7.test.ts`.
|
|
61
|
+
*
|
|
62
|
+
* Why it matters that this one is shared rather than duplicated: the regex is a
|
|
63
|
+
* WIRE decision. Widening it server-side (say, ids grow past 64 chars) while a
|
|
64
|
+
* copy here keeps the old bound turns every new id into "not a reference", and
|
|
65
|
+
* the widget then submits the legacy inline blob against a backend that expects
|
|
66
|
+
* a reference. That failure surfaces as a broken thumbnail, nowhere near a
|
|
67
|
+
* regex.
|
|
50
68
|
*/
|
|
51
|
-
export
|
|
52
|
-
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(value);
|
|
53
|
-
}
|
|
69
|
+
export { isFileIdToken };
|
|
54
70
|
|
|
55
71
|
/**
|
|
56
72
|
* The `sys_file` id a value refers to, or `undefined`.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FieldWidgetComponentProps, FieldWidgetDomProps } from './types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The RUNTIME EXECUTOR of the "DOM pass-through" section of
|
|
13
|
+
* {@link FieldWidgetComponentProps} (objectui#3291).
|
|
14
|
+
*
|
|
15
|
+
* ## The defect
|
|
16
|
+
*
|
|
17
|
+
* Field widgets forwarded their leftover props to a control with a bare
|
|
18
|
+
* spread — `const { inputType, ...domProps } = props as any; <Input
|
|
19
|
+
* {...domProps} />`. Whatever a host handed the widget therefore became a DOM
|
|
20
|
+
* attribute. Measured on `origin/main`, a real form + a real widget:
|
|
21
|
+
*
|
|
22
|
+
* ```
|
|
23
|
+
* <input placeholder="PH-f" zzcanary="CANARY-STR" zzcanaryobj="[object Object]"
|
|
24
|
+
* zzcanarynum="42" id="…" type="text" value="" name="f">
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The `[object Object]` this issue is named for is an authored field-config
|
|
28
|
+
* key being `String()`-ed onto an attribute. React 19 does not warn: an
|
|
29
|
+
* all-lowercase unknown attribute is passed through in complete silence, which
|
|
30
|
+
* is why this survived so long.
|
|
31
|
+
*
|
|
32
|
+
* ## Why a whitelist, and not a list of keys to drop
|
|
33
|
+
*
|
|
34
|
+
* The biggest leak source is not any NAMED renderer prop — it is the open tail
|
|
35
|
+
* of author-supplied keys. The form renderer destructures a fixed set of known
|
|
36
|
+
* keys and forwards `...fieldProps` verbatim
|
|
37
|
+
* (`components/src/renderers/form/form.tsx`), and `SchemaRenderer` is wider
|
|
38
|
+
* still: it spreads the whole authored node as props and has no strip layer at
|
|
39
|
+
* all, so on the SDUI path a widget's own spread is the ONLY line of defence.
|
|
40
|
+
*
|
|
41
|
+
* A blacklist enumerating today's renderer-only keys (`error`, `emptyHint`,
|
|
42
|
+
* `dataSource`, `dependentValues`, `dependsOn`, `options`, `inputType`, …)
|
|
43
|
+
* would pass every one of the canaries above and would not stop the next
|
|
44
|
+
* authored key either. Only "keep the known-safe set, drop the rest" closes
|
|
45
|
+
* it, which is what this function does.
|
|
46
|
+
*
|
|
47
|
+
* ## Declared = enforced
|
|
48
|
+
*
|
|
49
|
+
* {@link FieldWidgetComponentProps} already DECLARES the closed set of keys a
|
|
50
|
+
* widget may receive, and {@link FieldWidgetDomProps} names the subset that may
|
|
51
|
+
* legitimately reach a DOM element (objectui#3221). Until now that was a
|
|
52
|
+
* type-level claim a widget could violate at runtime simply by spreading. This
|
|
53
|
+
* function is that declaration's executable form, and TWO compile-time
|
|
54
|
+
* assertions below bind them in both directions — forwarding an undeclared key
|
|
55
|
+
* and declaring an unforwarded DOM key are each a compile error. Neither
|
|
56
|
+
* direction can drift silently.
|
|
57
|
+
*
|
|
58
|
+
* ## Deliberately NOT forwarded
|
|
59
|
+
*
|
|
60
|
+
* `role` and other HTML global attributes are absent because the contract does
|
|
61
|
+
* not declare them. They reached the DOM before this change only through the
|
|
62
|
+
* open spread. If a field node should be able to author one, DECLARE it on
|
|
63
|
+
* `FieldWidgetComponentProps` first and add it here — do not reopen the spread
|
|
64
|
+
* (AGENTS.md #0.1: fix the contract, never widen the consumer).
|
|
65
|
+
*/
|
|
66
|
+
const DOM_PASS_THROUGH_KEYS = [
|
|
67
|
+
/* ── The contract's own "DOM pass-through" block, verbatim ─────────────── */
|
|
68
|
+
'id',
|
|
69
|
+
'name',
|
|
70
|
+
'autoFocus',
|
|
71
|
+
'tabIndex',
|
|
72
|
+
'onBlur',
|
|
73
|
+
'onFocus',
|
|
74
|
+
'onClick',
|
|
75
|
+
|
|
76
|
+
/* ── Two more declared keys that ARE valid DOM attributes ──────────────── */
|
|
77
|
+
//
|
|
78
|
+
// Both are declared on `FieldWidgetComponentProps` under the
|
|
79
|
+
// controlled-input contract rather than the DOM block, because widgets also
|
|
80
|
+
// INTERPRET them (`className` gets composed with the widget's own classes;
|
|
81
|
+
// `disabled` is OR-ed with `readonly`). They are still `class` and
|
|
82
|
+
// `disabled` on the element, and every widget here already forwards them.
|
|
83
|
+
//
|
|
84
|
+
// Withholding them would make this helper a silent styling- and
|
|
85
|
+
// interactivity-dropper: `<Input {...toDomProps(props)} />` — the shape this
|
|
86
|
+
// change teaches every future widget author to write — would quietly lose
|
|
87
|
+
// the host's className and disabled state. That is a NEW silent failure of
|
|
88
|
+
// exactly the kind the whitelist exists to prevent, so they are forwarded.
|
|
89
|
+
'className',
|
|
90
|
+
'disabled',
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
93
|
+
type DomPassThroughKey = (typeof DOM_PASS_THROUGH_KEYS)[number];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compile-time link to the declaration, direction 1 of 2: every key forwarded
|
|
97
|
+
* at runtime must exist on {@link FieldWidgetComponentProps}. Deleting a key
|
|
98
|
+
* from the contract without deleting it here is a compile error.
|
|
99
|
+
*
|
|
100
|
+
* Catches: helper forwards something the contract no longer declares.
|
|
101
|
+
*/
|
|
102
|
+
type EveryForwardedKeyIsDeclared =
|
|
103
|
+
DomPassThroughKey extends keyof FieldWidgetComponentProps ? true : never;
|
|
104
|
+
const _everyForwardedKeyIsDeclared: EveryForwardedKeyIsDeclared = true;
|
|
105
|
+
void _everyForwardedKeyIsDeclared;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Direction 2 of 2: every key of {@link FieldWidgetDomProps} — the contract's
|
|
109
|
+
* DOM pass-through block — must be one this helper actually forwards. Adding a
|
|
110
|
+
* DOM key to the contract without adding it to `DOM_PASS_THROUGH_KEYS` is a
|
|
111
|
+
* compile error.
|
|
112
|
+
*
|
|
113
|
+
* Catches: DECLARED BUT NOT DELIVERED — a key that type-checks, reads as
|
|
114
|
+
* supported, and silently never reaches the element. That is the failure this
|
|
115
|
+
* repo treats as first-class (objectui#3290's `aria-required` that never
|
|
116
|
+
* reached a control; objectui#3222's validation slot nobody produced), and it
|
|
117
|
+
* is the one direction the leak test structurally CANNOT see: that test looks
|
|
118
|
+
* for attributes that arrive, not for ones that go missing.
|
|
119
|
+
*
|
|
120
|
+
* `className` / `disabled` are not part of `FieldWidgetDomProps` (they are
|
|
121
|
+
* controlled-input keys this helper also forwards — see above), so they are
|
|
122
|
+
* bound by direction 1 only. That is deliberate, not an oversight.
|
|
123
|
+
*/
|
|
124
|
+
type EveryDeclaredDomKeyIsForwarded =
|
|
125
|
+
keyof FieldWidgetDomProps extends DomPassThroughKey ? true : never;
|
|
126
|
+
const _everyDeclaredDomKeyIsForwarded: EveryDeclaredDomKeyIsForwarded = true;
|
|
127
|
+
void _everyDeclaredDomKeyIsForwarded;
|
|
128
|
+
|
|
129
|
+
const DOM_PASS_THROUGH: ReadonlySet<string> = new Set<string>(DOM_PASS_THROUGH_KEYS);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `aria-*` is declared on the contract as React's `AriaAttributes`; `data-*`
|
|
133
|
+
* is declared as an open template-literal family (open in HTML too, and the
|
|
134
|
+
* only open family the contract has). Both are matched by prefix so the helper
|
|
135
|
+
* needs no per-attribute list.
|
|
136
|
+
*/
|
|
137
|
+
function isOpenDomFamily(key: string): boolean {
|
|
138
|
+
return key.startsWith('data-') || key.startsWith('aria-');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The subset of `P` this helper forwards, with each key's declared type. */
|
|
142
|
+
export type DomProps<P> = Pick<P, Extract<keyof P, DomPassThroughKey>> & {
|
|
143
|
+
[K in Extract<keyof P, `data-${string}` | `aria-${string}`>]: P[K];
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Keep only what may legitimately become a DOM attribute, and drop everything
|
|
148
|
+
* else — renderer plumbing, and any extra key an author put on the field
|
|
149
|
+
* config or SDUI node.
|
|
150
|
+
*
|
|
151
|
+
* Replaces the bare `{...props}` spread in every field widget that renders a
|
|
152
|
+
* host element:
|
|
153
|
+
*
|
|
154
|
+
* ```tsx
|
|
155
|
+
* // before — forwards whatever it was handed
|
|
156
|
+
* const { inputType, ...domProps } = props as any;
|
|
157
|
+
* return <Input {...domProps} … />;
|
|
158
|
+
*
|
|
159
|
+
* // after
|
|
160
|
+
* return <Input {...toDomProps(props)} … />;
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* Semantic props a widget INTERPRETS (`value`, `onChange`, `field`,
|
|
164
|
+
* `readonly`, `error`, …) are read from `props` as before; this function only
|
|
165
|
+
* governs what gets spread.
|
|
166
|
+
*/
|
|
167
|
+
export function toDomProps<P extends object>(props: P): DomProps<P> {
|
|
168
|
+
const domProps: Record<string, unknown> = {};
|
|
169
|
+
for (const key of Object.keys(props)) {
|
|
170
|
+
if (DOM_PASS_THROUGH.has(key) || isOpenDomFamily(key)) {
|
|
171
|
+
domProps[key] = (props as Record<string, unknown>)[key];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return domProps as DomProps<P>;
|
|
175
|
+
}
|
package/src/widgets/types.ts
CHANGED
|
@@ -1,19 +1,242 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AriaAttributes, FocusEventHandler, MouseEventHandler } from 'react';
|
|
2
|
+
import type { DependsOnInput, FieldMetadata } from '@object-ui/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DOM pass-through: what a widget's `...props` spread may legitimately put on
|
|
6
|
+
* the element it renders.
|
|
7
|
+
*
|
|
8
|
+
* A NAMED type rather than an inline block of {@link FieldWidgetComponentProps}
|
|
9
|
+
* so the compiler can bind it to its runtime executor in BOTH directions
|
|
10
|
+
* (objectui#3291). `toDomProps` asserts:
|
|
11
|
+
*
|
|
12
|
+
* - every key it forwards is declared on the widget contract — deleting one
|
|
13
|
+
* here without deleting it there is a compile error;
|
|
14
|
+
* - every key of THIS type is one it forwards — adding one here without
|
|
15
|
+
* adding it to `DOM_PASS_THROUGH_KEYS` is a compile error too.
|
|
16
|
+
*
|
|
17
|
+
* The second direction is the one that matters for the failure mode this repo
|
|
18
|
+
* treats as first-class: DECLARED BUT NOT DELIVERED (objectui#3290's
|
|
19
|
+
* `aria-required` that never reached a control, objectui#3222's validation slot
|
|
20
|
+
* nobody produced). Without it, a key added here would type-check, read as
|
|
21
|
+
* supported, and silently never reach the DOM — and the leak test cannot see
|
|
22
|
+
* that class of bug, because it looks for attributes that ARRIVE, not for ones
|
|
23
|
+
* that go missing.
|
|
24
|
+
*
|
|
25
|
+
* `className` and `disabled` are deliberately NOT here. They are DOM-legal and
|
|
26
|
+
* `toDomProps` does forward them, but they are declared on the controlled-input
|
|
27
|
+
* block of {@link FieldWidgetComponentProps} because widgets also INTERPRET
|
|
28
|
+
* them (className is composed with the widget's own classes; disabled is OR-ed
|
|
29
|
+
* with readonly). They are therefore bound in the forward direction only.
|
|
30
|
+
*
|
|
31
|
+
* `AriaAttributes` and the `data-${string}` family are matched by prefix at
|
|
32
|
+
* runtime rather than key-by-key, so they are intersected in separately.
|
|
33
|
+
*
|
|
34
|
+
* Adding a key here is a contract change: say who produces it and who reads it.
|
|
35
|
+
*/
|
|
36
|
+
export type FieldWidgetDomProps = {
|
|
37
|
+
id?: string;
|
|
38
|
+
/** react-hook-form's field name, spread in by the form renderer. */
|
|
39
|
+
name?: string;
|
|
40
|
+
autoFocus?: boolean;
|
|
41
|
+
tabIndex?: number;
|
|
42
|
+
onBlur?: FocusEventHandler<HTMLElement>;
|
|
43
|
+
onFocus?: FocusEventHandler<HTMLElement>;
|
|
44
|
+
onClick?: MouseEventHandler<HTMLElement>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Props every field widget in this package receives at RUNTIME.
|
|
49
|
+
*
|
|
50
|
+
* Named `FieldWidgetComponentProps`, not `FieldWidgetProps` (objectui#3161,
|
|
51
|
+
* objectstack#4115 ledger batch 7), adopting the name `@object-ui/app-shell`
|
|
52
|
+
* settled on for the same split in objectui#3169: `@objectstack/spec/ui` owns
|
|
53
|
+
* `FieldWidgetProps` for the DECLARED widget-plugin contract — a zod object
|
|
54
|
+
* (`FieldWidgetPropsSchema`) a plugin manifest is validated against, with
|
|
55
|
+
* `field` narrowed to `{ name?, label?, type: <the FieldType enum> }` and
|
|
56
|
+
* `readonly` / `required` carrying `.default()`s. This is the React interface
|
|
57
|
+
* the widgets in this directory actually implement, against
|
|
58
|
+
* `@object-ui/types`'s much richer `FieldMetadata`.
|
|
59
|
+
*
|
|
60
|
+
* The two are NOT interchangeable, but they no longer disagree on the
|
|
61
|
+
* validation slot: this type used to call it `errorMessage` while the spec
|
|
62
|
+
* calls it `error`, so a widget written to the published contract read
|
|
63
|
+
* `undefined` forever. objectui#3222 resolved that in the direction the
|
|
64
|
+
* contract points — the slot below IS the spec's `error`, and the form
|
|
65
|
+
* renderer now produces it (see `error`'s own doc comment). Reading
|
|
66
|
+
* `props.required` off this type is still a compile error rather than a
|
|
67
|
+
* silent `any`; that key is deliberately NOT lowered here (see below).
|
|
68
|
+
*
|
|
69
|
+
* ## Why `required` is still absent, though the spec declares it
|
|
70
|
+
*
|
|
71
|
+
* The required MARKER is drawn exactly once, by the form renderer's
|
|
72
|
+
* `<FormLabel>` (the `*` with `aria-label="required"`, which the label
|
|
73
|
+
* association folds into the control's accessible name). Handing `required`
|
|
74
|
+
* to widgets would give that marker a second possible author, and the very
|
|
75
|
+
* next AI-written widget draws its own asterisk — the same double-display
|
|
76
|
+
* failure that keeps the validation TEXT out of the widget below. The a11y
|
|
77
|
+
* state a widget genuinely could carry is `aria-required` on the input, and
|
|
78
|
+
* that needs no new key at all: `AriaAttributes` is already part of this type
|
|
79
|
+
* and every widget already forwards it to its control. Tracked separately;
|
|
80
|
+
* do not add `required` here to "align" without that decision.
|
|
81
|
+
*
|
|
82
|
+
* ## Why there is no `[key: string]: any` (objectui#3221)
|
|
83
|
+
*
|
|
84
|
+
* This type used to end in an index signature, "load-bearing" for the widgets'
|
|
85
|
+
* `...props` spreads. It was also why every drift above was invisible: **a type
|
|
86
|
+
* that claims to have every key can never be reported as missing one** (the
|
|
87
|
+
* objectstack#4075 mechanism). `props.required` and `props.error` were legal
|
|
88
|
+
* reads typed `any` and always `undefined` at runtime; a misspelled prop
|
|
89
|
+
* (`readOnly` for `readonly`, `onchange` for `onChange`) compiled; and any
|
|
90
|
+
* structural/parity comparison against the type was useless *in principle*,
|
|
91
|
+
* which is why the batch-7 symbol guard was the only detector that could see
|
|
92
|
+
* the collision at all.
|
|
93
|
+
*
|
|
94
|
+
* It is replaced by the CLOSED set below: the controlled-input contract, the
|
|
95
|
+
* host plumbing the form renderer genuinely forwards, and a DOM pass-through
|
|
96
|
+
* allowance. `data-*` is an open family by design (it is open in HTML too) and
|
|
97
|
+
* is expressed as a template-literal index signature, which — unlike
|
|
98
|
+
* `[key: string]` — leaves `keyof` finite, so an undeclared prop still fails.
|
|
99
|
+
*
|
|
100
|
+
* ## Why there is no `schema` (objectui#3233)
|
|
101
|
+
*
|
|
102
|
+
* `schema` used to be declared here as a SECOND carrier for what `field`
|
|
103
|
+
* already means — `SchemaRenderer` handed the SDUI node down as `schema`, the
|
|
104
|
+
* form renderer forwarded `schema={props.field || props.schema || props}`
|
|
105
|
+
* *alongside* `field`, and ~30 widgets resolved their config as
|
|
106
|
+
* `field || schema`. One concept, two keys, two producers: exactly the
|
|
107
|
+
* consumer-side tolerance AGENTS.md #0.1 forbids, and exactly the shape that
|
|
108
|
+
* lets an AI-written widget pick the wrong spelling and still "work" under one
|
|
109
|
+
* host while reading `undefined` under another.
|
|
110
|
+
*
|
|
111
|
+
* v17 converged it at the PRODUCERS. `field` is the only carrier a widget ever
|
|
112
|
+
* sees; the SDUI node → `field` translation happens once, in this package's
|
|
113
|
+
* registry adapter (`withFieldCarrier`), not ~30 times in the widgets. Reading
|
|
114
|
+
* `props.schema` is now a compile error rather than a silent second contract.
|
|
115
|
+
*
|
|
116
|
+
* Adding a key here is a contract change: say who produces it and who reads it.
|
|
117
|
+
*/
|
|
118
|
+
export type FieldWidgetComponentProps<T = any> = {
|
|
119
|
+
/* ── The controlled-input contract every widget implements ─────────────── */
|
|
2
120
|
|
|
3
|
-
export type FieldWidgetProps<T = any> = {
|
|
4
121
|
value: T;
|
|
5
122
|
onChange: (val: T) => void;
|
|
6
|
-
|
|
7
|
-
|
|
123
|
+
/**
|
|
124
|
+
* The field's metadata, and the SINGLE carrier for it (objectui#3233).
|
|
125
|
+
*
|
|
126
|
+
* Deliberately the looser `@object-ui/types` shape rather than the spec's,
|
|
127
|
+
* to avoid a circular dependency for now.
|
|
128
|
+
*
|
|
129
|
+
* Every host produces it: the form renderer's `renderFieldComponent`, the
|
|
130
|
+
* inline-edit hosts (`FieldEditWidget`), `ActionParamDialog` /
|
|
131
|
+
* `BulkActionDialog`, and — since #3233 — the registry adapter that adapts
|
|
132
|
+
* `SchemaRenderer`'s SDUI node onto this contract (`withFieldCarrier` in
|
|
133
|
+
* `packages/fields/src/withFieldCarrier.tsx`). A widget reads `props.field`
|
|
134
|
+
* and nothing else; there is no second key to check.
|
|
135
|
+
*/
|
|
136
|
+
field: FieldMetadata;
|
|
8
137
|
readonly?: boolean;
|
|
9
138
|
disabled?: boolean;
|
|
10
139
|
className?: string;
|
|
11
|
-
|
|
140
|
+
/**
|
|
141
|
+
* The active validation message for this field, named as
|
|
142
|
+
* `@objectstack/spec/ui`'s `FieldWidgetPropsSchema` names it (objectui#3222).
|
|
143
|
+
*
|
|
144
|
+
* **Producer**: the form renderer, from react-hook-form's
|
|
145
|
+
* `fieldState.error?.message` (`packages/components/src/renderers/form/
|
|
146
|
+
* form.tsx`). Before #3222 nothing in the repo produced it under EITHER
|
|
147
|
+
* spelling, so the seven widgets computing `aria-invalid={!!errorMessage}`
|
|
148
|
+
* were computing it from a permanent `undefined` — `aria-invalid` was never
|
|
149
|
+
* once set and a screen reader was never told the field had failed.
|
|
150
|
+
*
|
|
151
|
+
* **Consumer**: a widget reads this ONLY to drive `aria-invalid` on the
|
|
152
|
+
* control it renders. The message TEXT stays with the form renderer's
|
|
153
|
+
* `<FormMessage/>`; a widget that also renders it double-displays it.
|
|
154
|
+
*/
|
|
155
|
+
error?: string;
|
|
12
156
|
/**
|
|
13
157
|
* Upload widgets (`file`/`image`) fire this when their in-progress state
|
|
14
158
|
* flips, so a host can block submit until a presigned upload settles. Other
|
|
15
159
|
* widgets ignore it.
|
|
16
160
|
*/
|
|
17
161
|
onUploadingChange?: (uploading: boolean) => void;
|
|
18
|
-
|
|
19
|
-
|
|
162
|
+
|
|
163
|
+
/* ── Host plumbing: what a rendering host actually forwards ─────────────── */
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* DataSource for widgets that query records (lookup / user / object-ref /
|
|
167
|
+
* recipient-picker / grid). Injected by the form renderer for the field
|
|
168
|
+
* types that need it, and passed directly by inline-edit hosts. Option
|
|
169
|
+
* widgets destructure it purely to keep it off their DOM spread.
|
|
170
|
+
*
|
|
171
|
+
* Left structural (`unknown`): `@object-ui/fields` must not depend on a
|
|
172
|
+
* concrete adapter — every consumer narrows it itself.
|
|
173
|
+
*/
|
|
174
|
+
dataSource?: unknown;
|
|
175
|
+
/**
|
|
176
|
+
* Live sibling-field values driving cascading / role-gated options and
|
|
177
|
+
* dependent lookups (ADR-0058, #2215/#2284). The form renderer passes the
|
|
178
|
+
* in-progress record; widgets fall back to `SchemaRendererContext`.
|
|
179
|
+
*/
|
|
180
|
+
dependentValues?: Record<string, unknown>;
|
|
181
|
+
/**
|
|
182
|
+
* Controlling field(s) that gate this field's option list. Normally declared
|
|
183
|
+
* on the field metadata; accepted as a prop so a host can drive the gate for
|
|
184
|
+
* a field it synthesised. Field metadata wins when both are present.
|
|
185
|
+
*/
|
|
186
|
+
dependsOn?: DependsOnInput;
|
|
187
|
+
/**
|
|
188
|
+
* Controlling-field name → its human LABEL, for widgets that have to NAME a
|
|
189
|
+
* sibling field in user-visible copy (the dependent-lookup gate hint).
|
|
190
|
+
*
|
|
191
|
+
* Only the form renderer knows a field's label — the widget sees its own
|
|
192
|
+
* metadata and a `depends_on` list of API names. Without this map the gate
|
|
193
|
+
* sentence interpolated the raw API name into every locale, `en` included
|
|
194
|
+
* ("Select crm_account first"), which is an internal identifier leaking into
|
|
195
|
+
* the UI, not merely an untranslated word (objectstack#5407). This is the
|
|
196
|
+
* lookup-side counterpart of `emptyHint`, which the form already resolves to
|
|
197
|
+
* labels for the fixed-option widgets. A name with no entry falls back to
|
|
198
|
+
* itself, so a host that passes nothing renders exactly what it did before.
|
|
199
|
+
*/
|
|
200
|
+
dependsOnLabels?: Record<string, string>;
|
|
201
|
+
/**
|
|
202
|
+
* Hint shown when an option list cannot be filled — typically a
|
|
203
|
+
* dependency-gated list still waiting on its controlling field (#2284).
|
|
204
|
+
* Forwarded by the form renderer, which resolves the controlling fields to
|
|
205
|
+
* their human LABELS. **When supplied it wins**; the option widgets fall back
|
|
206
|
+
* to their own translated copy only when a host computed none (objectui#3231
|
|
207
|
+
* — they used to discard this prop and always render their own hardcoded
|
|
208
|
+
* English). See `OptionsEmptyState`, the single consumer.
|
|
209
|
+
*/
|
|
210
|
+
emptyHint?: string;
|
|
211
|
+
/**
|
|
212
|
+
* Render as a single-line, borderless control for a grid cell (the inline
|
|
213
|
+
* editor and the line-item grid set it) instead of the full form layout.
|
|
214
|
+
*/
|
|
215
|
+
compact?: boolean;
|
|
216
|
+
/**
|
|
217
|
+
* Receive the FULL selected record rather than just its id, so a host can
|
|
218
|
+
* auto-fill sibling fields from it (a line-item grid copying a product's
|
|
219
|
+
* price). When provided, the host owns the resulting value change.
|
|
220
|
+
*/
|
|
221
|
+
onSelectRecord?: (record: Record<string, any>) => void;
|
|
222
|
+
/**
|
|
223
|
+
* Offer a "create new" affordance in a record picker, carrying whatever the
|
|
224
|
+
* user had typed. Also declarable on the field metadata.
|
|
225
|
+
*/
|
|
226
|
+
onCreateNew?: (searchQuery: string) => void;
|
|
227
|
+
|
|
228
|
+
/* ── DOM pass-through ───────────────────────────────────────────────────── */
|
|
229
|
+
//
|
|
230
|
+
// Lives in the named {@link FieldWidgetDomProps} above, because that is what
|
|
231
|
+
// lets the compiler bind the declaration to `toDomProps` — its runtime
|
|
232
|
+
// executor — in BOTH directions (objectui#3291). Add a DOM key THERE, not
|
|
233
|
+
// here, and the compiler will make you add it to the whitelist too.
|
|
234
|
+
} & FieldWidgetDomProps & AriaAttributes & {
|
|
235
|
+
/**
|
|
236
|
+
* Arbitrary `data-*` attributes (test ids, analytics hooks). Open by design,
|
|
237
|
+
* but a template-literal key — `keyof` stays finite, so this does NOT
|
|
238
|
+
* reintroduce the index signature objectui#3221 removed: `props.required`
|
|
239
|
+
* is still an error, `props['data-testid']` is still fine.
|
|
240
|
+
*/
|
|
241
|
+
[dataAttribute: `data-${string}`]: string | number | boolean | undefined;
|
|
242
|
+
};
|