@object-ui/fields 17.6.0 → 17.7.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 +5436 -0
- package/README.md +108 -8
- package/dist/FieldEditWidget.d.ts +54 -6
- package/dist/coerceToSafeValue.d.ts +54 -0
- package/dist/currency.d.ts +1 -1
- package/dist/index.cjs +5 -5
- package/dist/index.css +26 -2
- package/dist/index.d.ts +229 -57
- package/dist/index.js +3811 -3009
- package/dist/widgets/AvatarField.d.ts +19 -3
- package/dist/widgets/BooleanField.d.ts +1 -1
- package/dist/widgets/DateField.d.ts +1 -1
- package/dist/widgets/DateTimeField.d.ts +1 -1
- package/dist/widgets/FilterConditionField.d.ts +21 -7
- package/dist/widgets/GeolocationField.d.ts +9 -3
- package/dist/widgets/GridField.d.ts +63 -99
- package/dist/widgets/LocationField.d.ts +30 -3
- package/dist/widgets/LookupField.d.ts +8 -0
- package/dist/widgets/NumberField.d.ts +1 -1
- package/dist/widgets/ObjectField.d.ts +9 -2
- package/dist/widgets/OptionsEmptyState.d.ts +29 -5
- package/dist/widgets/PasswordField.d.ts +14 -0
- package/dist/widgets/PeoplePicker.d.ts +17 -2
- package/dist/widgets/PercentField.d.ts +2 -1
- package/dist/widgets/PersonRow.d.ts +13 -2
- package/dist/widgets/RecipientPickerField.d.ts +43 -0
- package/dist/widgets/RecordPickerDialog.d.ts +31 -12
- package/dist/widgets/RichTextField.d.ts +1 -1
- package/dist/widgets/SelectionTray.d.ts +13 -2
- package/dist/widgets/TextField.d.ts +1 -1
- package/dist/widgets/TimeField.d.ts +1 -1
- package/dist/widgets/file-affordance.d.ts +33 -0
- package/dist/widgets/file-size-guard.d.ts +5 -23
- package/dist/widgets/file-value.d.ts +38 -9
- package/dist/widgets/lookupColumnDisplay.d.ts +24 -7
- package/dist/widgets/nativeDateValue.d.ts +9 -27
- package/dist/widgets/number-format.d.ts +137 -0
- package/dist/widgets/numberBadInput.d.ts +135 -0
- package/dist/widgets/personDisplay.d.ts +22 -4
- package/dist/widgets/richTextDisplay.d.ts +36 -0
- package/dist/widgets/toDomProps.d.ts +10 -1
- package/dist/widgets/toHostGroupProps.d.ts +53 -0
- package/dist/widgets/toHostProps.d.ts +105 -0
- package/dist/widgets/types.d.ts +11 -3
- package/dist/widgets/upload-error-message.d.ts +6 -0
- package/dist/widgets/uploadingScope.d.ts +142 -0
- package/dist/widgets/useCascadingOptions.d.ts +3 -2
- package/dist/widgets/useRecordQuery.d.ts +4 -2
- package/dist/widgets/useUploadingSignal.d.ts +8 -0
- package/package.json +11 -12
|
@@ -13,8 +13,22 @@
|
|
|
13
13
|
*/
|
|
14
14
|
/** Resolve a possibly-dotted path (e.g. `primary_business_unit_id.name`) against a record. */
|
|
15
15
|
export declare function resolvePath(record: any, path: string): any;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The fields a person's display name is read from, in order: the configured
|
|
18
|
+
* display field, then `name`, `username`, `label`. A caller that gates the name
|
|
19
|
+
* by field-level security filters THIS list and hands the rest to
|
|
20
|
+
* {@link getPersonName} (objectui#10535): a denied rung is skipped, so the name
|
|
21
|
+
* falls through to the next readable one — the row a stripping backend serves
|
|
22
|
+
* reads the same.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getPersonNameFields(nameField?: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Best display name for a person record, tolerating expanded / raw shapes: the
|
|
27
|
+
* first field of the name ladder the record holds a value for. `nameField` is
|
|
28
|
+
* the configured display field, whose ladder is {@link getPersonNameFields}, or
|
|
29
|
+
* the ladder itself, already filtered to the fields the viewer may read.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getPersonName(record: any, nameField?: string | readonly string[]): string;
|
|
18
32
|
/**
|
|
19
33
|
* Up-to-2-char initials for the avatar fallback.
|
|
20
34
|
* - "John Doe" → "JD"; "John" → "JO"
|
|
@@ -23,8 +37,12 @@ export declare function getPersonName(record: any, nameField?: string): string;
|
|
|
23
37
|
export declare function getPersonInitials(name: string): string;
|
|
24
38
|
/** Secondary line: resolve each field path, drop empties, join with " · ". */
|
|
25
39
|
export declare function getPersonSubtitle(record: any, fields?: string[]): string;
|
|
26
|
-
/**
|
|
27
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Avatar image URL (defaults to `sys_user.image`); undefined when absent, and
|
|
42
|
+
* when `avatarField` is `null` — no avatar field to read, e.g. one field-level
|
|
43
|
+
* security withholds (objectui#10433).
|
|
44
|
+
*/
|
|
45
|
+
export declare function getPersonAvatarUrl(record: any, avatarField?: string | null): string | undefined;
|
|
28
46
|
/** Record id, tolerating `id` / `_id` / a custom id field. */
|
|
29
47
|
export declare function getPersonId(record: any, idField?: string): any;
|
|
30
48
|
/**
|
|
@@ -12,6 +12,10 @@ import { CellRendererProps } from '../index.js';
|
|
|
12
12
|
* pipeline drops all of it; see {@link HtmlCellRenderer}, which is where that
|
|
13
13
|
* type belongs. Loosening this pipeline to pass raw HTML through would have
|
|
14
14
|
* "fixed" one type by moving every `markdown` cell's trust boundary.
|
|
15
|
+
*
|
|
16
|
+
* What the cell holds is decided by {@link richTextSource} (objectui#8580):
|
|
17
|
+
* `[]` is the No-value affordance, never a childless container, and an
|
|
18
|
+
* object is the text the string class prints for it, never `[object Object]`.
|
|
15
19
|
*/
|
|
16
20
|
export declare function MarkdownCellRenderer({ value }: CellRendererProps): React.ReactElement;
|
|
17
21
|
/**
|
|
@@ -27,6 +31,14 @@ export declare function MarkdownCellRenderer({ value }: CellRendererProps): Reac
|
|
|
27
31
|
* emits — headings, paragraphs, emphasis, lists, links, quotes all survive, so
|
|
28
32
|
* routing `richtext` here restores the content rather than trading a blank cell
|
|
29
33
|
* for a mangled one.
|
|
34
|
+
*
|
|
35
|
+
* What the cell holds is decided by {@link richTextSource} (objectui#8580):
|
|
36
|
+
* `[]` is the No-value affordance, never a childless `prose` container, and
|
|
37
|
+
* an object is the text the string class prints for it. The populated branch
|
|
38
|
+
* stays on {@link sanitizeHtml}: a stored string comes back from
|
|
39
|
+
* `richTextSource` verbatim, so the bytes that reach the sanitiser are the
|
|
40
|
+
* bytes that always did — pinned byte-for-byte in
|
|
41
|
+
* `__tests__/cellRenderers.childlessContainer-8580.test.tsx`.
|
|
30
42
|
*/
|
|
31
43
|
export declare function HtmlCellRenderer({ value }: CellRendererProps): React.ReactElement;
|
|
32
44
|
/**
|
|
@@ -50,6 +62,30 @@ export declare const RICH_TEXT_CELL_RENDERERS: {
|
|
|
50
62
|
};
|
|
51
63
|
/** The rich-content field types, i.e. the keys of {@link RICH_TEXT_CELL_RENDERERS}. */
|
|
52
64
|
export type RichTextFieldType = keyof typeof RICH_TEXT_CELL_RENDERERS;
|
|
65
|
+
/**
|
|
66
|
+
* The same three types as a RUNTIME list, DERIVED from THE table rather than
|
|
67
|
+
* spelled a second time — the type-level {@link RichTextFieldType} cannot be
|
|
68
|
+
* enumerated at runtime, and a hand-written array beside it would be exactly
|
|
69
|
+
* the shape this export exists to retire.
|
|
70
|
+
*
|
|
71
|
+
* ## Why this export exists
|
|
72
|
+
*
|
|
73
|
+
* objectui#4831 asked, in its own body, whether the hand-written type lists
|
|
74
|
+
* that keep omitting the third of this widget's three registry keys should
|
|
75
|
+
* become "every type that resolves to the long-text widget". It was answered
|
|
76
|
+
* by adding one literal, and the omission recurred (objectui#4250,
|
|
77
|
+
* objectui#8438). This is that question answered in the other direction: a
|
|
78
|
+
* consumer that spreads this list cannot omit a key, because it never names
|
|
79
|
+
* one. Adding a fourth key to {@link RICH_TEXT_CELL_RENDERERS} extends every
|
|
80
|
+
* such consumer in the same commit that adds the key.
|
|
81
|
+
*
|
|
82
|
+
* ⚠️ It is NOT a general "long text" list and must not be used as one:
|
|
83
|
+
* `textarea` renders a `<Textarea>` too and is deliberately absent, because it
|
|
84
|
+
* is a different widget with its own registry key and its own metadata face.
|
|
85
|
+
* The invariant this list states is narrower and exact — *these are the keys
|
|
86
|
+
* `RichTextField` serves*.
|
|
87
|
+
*/
|
|
88
|
+
export declare const RICH_TEXT_FIELD_TYPES: readonly RichTextFieldType[];
|
|
53
89
|
/**
|
|
54
90
|
* The SYNTAX a rich-content type stores, derived from the renderer that type
|
|
55
91
|
* resolves to rather than declared a second time.
|
|
@@ -83,7 +83,16 @@ import { DomProps as CoreDomProps } from '@object-ui/core';
|
|
|
83
83
|
* the contract it executes. Not two judges.
|
|
84
84
|
*/
|
|
85
85
|
declare const DOM_PASS_THROUGH_KEYS: readonly ["id", "name", "autoFocus", "tabIndex", "onBlur", "onFocus", "onClick", "className", "disabled"];
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The keys this helper forwards.
|
|
88
|
+
*
|
|
89
|
+
* Exported so the SIBLING executor — `toHostProps`, which carries the declared
|
|
90
|
+
* NON-DOM keys (`f08bcd9af`) — can subtract this set from the contract and
|
|
91
|
+
* assert that the two together cover every declared key exactly once. Without
|
|
92
|
+
* that subtraction there is no way to state "these keys are handled elsewhere"
|
|
93
|
+
* as a compile-time fact rather than as a comment.
|
|
94
|
+
*/
|
|
95
|
+
export type DomPassThroughKey = (typeof DOM_PASS_THROUGH_KEYS)[number];
|
|
87
96
|
/** The subset of `P` this helper forwards, with each key's declared type. */
|
|
88
97
|
export type DomProps<P> = CoreDomProps<P, DomPassThroughKey>;
|
|
89
98
|
/**
|
|
@@ -187,3 +187,56 @@ export declare function toHostGroupProps(props: {
|
|
|
187
187
|
'aria-labelledby'?: string;
|
|
188
188
|
'aria-describedby'?: string;
|
|
189
189
|
}, surface: HostGroupSurface): HostGroupProps;
|
|
190
|
+
/**
|
|
191
|
+
* The host keys a LABELABLE surface may consume — the `labelling: 'control'`
|
|
192
|
+
* counterpart of {@link HostGroupProps} (objectui#8803).
|
|
193
|
+
*
|
|
194
|
+
* ## Why a second, smaller bag instead of reusing the group one
|
|
195
|
+
*
|
|
196
|
+
* A widget declared `'control'` is told nothing by the host: its label emits a
|
|
197
|
+
* plain `for` and expects to reach a labelable element on its own. Every
|
|
198
|
+
* editable branch answers that by spreading `toDomProps` onto the control it
|
|
199
|
+
* renders. The branches that return EARLY answer it with nothing — and for the
|
|
200
|
+
* single `SelectField` that is the zero-option box, where the `…-form-item` id
|
|
201
|
+
* reached no element at all and the label's `for` DANGLED.
|
|
202
|
+
*
|
|
203
|
+
* What such a surface needs is therefore the exact complement of the group
|
|
204
|
+
* bag:
|
|
205
|
+
*
|
|
206
|
+
* - `id` — yes. It is the id the `for` points at, and landing it is the whole
|
|
207
|
+
* repair;
|
|
208
|
+
* - `aria-describedby` — yes, for the objectui#4005 reason, unchanged by the
|
|
209
|
+
* surface being labelable: the field renders no input in this state, so
|
|
210
|
+
* this box is the only element the visible help text can be announced on;
|
|
211
|
+
* - `aria-labelledby` — ⛔ NO. The `for` already names this element. Adding an
|
|
212
|
+
* IDREF beside it is the second naming channel objectui#3978 removed;
|
|
213
|
+
* - `role` — ⛔ NO. A labelable element's own semantics are what make the name
|
|
214
|
+
* land; a synthetic role on top is the category error objectui#3991 named.
|
|
215
|
+
*
|
|
216
|
+
* `aria-invalid` / `aria-required` / `name` stay off for the reasons spelled
|
|
217
|
+
* out at length on {@link HostGroupProps}: control-channel state on a surface
|
|
218
|
+
* nobody can edit, and the DOM leak objectui#3291 sweeps for. A closed type,
|
|
219
|
+
* not an open props tail, for that same reason.
|
|
220
|
+
*/
|
|
221
|
+
export interface HostControlProps {
|
|
222
|
+
/** The host's control id — the id its label's `for` points at. */
|
|
223
|
+
id?: string;
|
|
224
|
+
/** IDREF of the host's visible help text. */
|
|
225
|
+
'aria-describedby'?: string;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Read the two whole-field keys a labelable replacement surface may carry. See
|
|
229
|
+
* {@link HostControlProps}.
|
|
230
|
+
*
|
|
231
|
+
* Routed through {@link toDomProps} for the same reason {@link toHostGroupProps}
|
|
232
|
+
* is: the keys keep ONE gate, and its compile-time assertions bind that
|
|
233
|
+
* whitelist to the props contract in both directions.
|
|
234
|
+
*
|
|
235
|
+
* Standalone rendering (the grid's inline cell editor, an action param dialog,
|
|
236
|
+
* a bare SDUI node) hands down neither key, so both values are `undefined`,
|
|
237
|
+
* React emits no attribute, and that markup stays byte-identical.
|
|
238
|
+
*/
|
|
239
|
+
export declare function toHostControlProps(props: {
|
|
240
|
+
id?: string;
|
|
241
|
+
'aria-describedby'?: string;
|
|
242
|
+
}): HostControlProps;
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
* The RUNTIME EXECUTOR of the NON-DOM half of {@link FieldWidgetComponentProps}
|
|
10
|
+
* (`f08bcd9af`) — the sibling of `toDomProps`, and deliberately a SEPARATE
|
|
11
|
+
* function rather than more entries in that whitelist.
|
|
12
|
+
*
|
|
13
|
+
* ## The defect
|
|
14
|
+
*
|
|
15
|
+
* objectui#6909 / #7009 closed the DOM half at `FieldEditWidget`: the factory
|
|
16
|
+
* hands the widget `toDomProps(props)`, so `id` / `name` / `tabIndex` /
|
|
17
|
+
* `aria-*` / `data-*` and friends finally arrive. The contract also declares a
|
|
18
|
+
* "Host plumbing" block plus two controlled-input keys the factory neither owns
|
|
19
|
+
* nor routes to the DOM, and NOTHING carried those. A host could pass any of
|
|
20
|
+
* them with no type error and the widget never received it — this package's own
|
|
21
|
+
* first-class defect class, named in `toDomProps.ts`: "a key that type-checks,
|
|
22
|
+
* reads as supported, and silently never reaches the element" (objectui#3290's
|
|
23
|
+
* `aria-required`, objectui#3222's validation slot).
|
|
24
|
+
*
|
|
25
|
+
* Measured on `main` at `71d83a6b1`, the live victim was `error`:
|
|
26
|
+
* `InlineFieldInput` (`@object-ui/plugin-detail`, since PR #7109) already
|
|
27
|
+
* passes `error={error}` to `FieldEditWidget`, and the factory dropped it on
|
|
28
|
+
* the floor — so a control that had failed validation never reported
|
|
29
|
+
* `aria-invalid` and a screen-reader user was never told. The kanban
|
|
30
|
+
* `RequiredFieldsDialog` had the same hole from the other side: it computes the
|
|
31
|
+
* validation state, renders it in red text, and had no way to hand it over.
|
|
32
|
+
*
|
|
33
|
+
* ## Why NOT more keys in `DOM_PASS_THROUGH_KEYS`
|
|
34
|
+
*
|
|
35
|
+
* Because none of them is DOM-legal. That whitelist is deliberately closed and
|
|
36
|
+
* exists precisely to stop renderer plumbing reaching an element — its own
|
|
37
|
+
* header names `error`, `emptyHint`, `dataSource`, `dependentValues` and
|
|
38
|
+
* `dependsOn` as examples of what a blacklist would have failed to catch. Put
|
|
39
|
+
* them there and a `dataSource` adapter becomes `dataSource="[object Object]"`
|
|
40
|
+
* on an `<input>`. They travel as COMPONENT props, which is what they are.
|
|
41
|
+
*
|
|
42
|
+
* ## Declared = enforced, in both directions
|
|
43
|
+
*
|
|
44
|
+
* The three assertions below make the two executors PARTITION the contract:
|
|
45
|
+
* every declared key is DOM-handled, forwarded here, or named as one the
|
|
46
|
+
* factory itself owns. A key added to `FieldWidgetComponentProps` that is none
|
|
47
|
+
* of those is a compile error, so the next non-DOM key cannot repeat this bug.
|
|
48
|
+
*
|
|
49
|
+
* ## Precedence: this is a CONDUIT, and never a second authority
|
|
50
|
+
*
|
|
51
|
+
* Several of these keys have a context-based fallback inside the widget that
|
|
52
|
+
* reads them, so DELIVERING one can change behaviour where before it could not
|
|
53
|
+
* arrive at all. The rule is stated once, here, and this function implements it
|
|
54
|
+
* by doing nothing: it forwards what the host passed and resolves NOTHING.
|
|
55
|
+
* Each key's precedence stays where its single reader already implements and
|
|
56
|
+
* documents it —
|
|
57
|
+
*
|
|
58
|
+
* - `dataSource`: **the explicit prop WINS** over `SchemaRendererContext`.
|
|
59
|
+
* `LookupField` already resolves exactly that order and says so on the line
|
|
60
|
+
* that does it: "explicit prop > field-level > wrapper field >
|
|
61
|
+
* SchemaRendererContext > none". Hosts that pass nothing keep reading the
|
|
62
|
+
* context, so the grid's inline editor is unaffected.
|
|
63
|
+
* - `dependentValues`: the explicit prop is the ONLY channel that can carry a
|
|
64
|
+
* record — the host supplies it, and there is NO context fallback. Both
|
|
65
|
+
* readers (`useCascadingOptions`, and `LookupField`'s own resolver) used to
|
|
66
|
+
* spell a `?? ctx.formValues ?? ctx.data` tail after it, but
|
|
67
|
+
* `SchemaRendererContextType` declares exactly `dataSource` / `debug` /
|
|
68
|
+
* `debugFlags` / `apiFetch` — neither member exists, so that tail was
|
|
69
|
+
* unconditionally `{}` and no host could change it. It was retired under
|
|
70
|
+
* ADR-0049 enforce-or-remove (objectui#7206). Unlike `dataSource` above,
|
|
71
|
+
* DELIVERING this key cannot displace a context value, because there has
|
|
72
|
+
* never been one to displace.
|
|
73
|
+
* - `dependsOn`: the FIELD METADATA wins over the prop — the one documented
|
|
74
|
+
* inversion, stated on the key's own doc comment and implemented as
|
|
75
|
+
* `field?.dependsOn ?? dependsOnProp` in all four option widgets.
|
|
76
|
+
* - `emptyHint`: the host's value wins when supplied (objectui#3231).
|
|
77
|
+
* - `onCreateNew`: the prop wins over `field.onCreateNew`; `onSelectRecord`
|
|
78
|
+
* has no metadata carrier at all, so the prop is its ONLY one.
|
|
79
|
+
*
|
|
80
|
+
* Resolving any of that HERE would give each key a second author — the
|
|
81
|
+
* `field || schema` shape objectui#3233 spent a release removing. One key, one
|
|
82
|
+
* resolver, in the widget that reads it.
|
|
83
|
+
*/
|
|
84
|
+
declare const HOST_PLUMBING_KEYS: readonly ["error", "onUploadingChange", "dataSource", "dependentValues", "dependsOn", "dependsOnLabels", "emptyHint", "onSelectRecord", "onCreateNew"];
|
|
85
|
+
type HostPlumbingKey = (typeof HOST_PLUMBING_KEYS)[number];
|
|
86
|
+
/** The subset of `P` this helper forwards, with each key's declared type. */
|
|
87
|
+
export type HostProps<P> = Pick<P, Extract<keyof P, HostPlumbingKey>>;
|
|
88
|
+
/**
|
|
89
|
+
* Keep only the declared NON-DOM keys — the host plumbing a widget interprets
|
|
90
|
+
* as component props — and drop everything else.
|
|
91
|
+
*
|
|
92
|
+
* The companion to `toDomProps`, and used the same way. A host that passes
|
|
93
|
+
* nothing gets an empty object, so the widget's prop set is unchanged:
|
|
94
|
+
*
|
|
95
|
+
* ```tsx
|
|
96
|
+
* <Widget {...toDomProps(props)} {...toHostProps(props)} field={field} … />
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* Iterating the props (rather than the key list) mirrors `pickDomProps`, so a
|
|
100
|
+
* key the host did not mention stays ABSENT rather than arriving as
|
|
101
|
+
* `undefined` — which keeps the factory-boundary prop set readable as "what the
|
|
102
|
+
* host actually supplied".
|
|
103
|
+
*/
|
|
104
|
+
export declare function toHostProps<P extends object>(props: P): HostProps<P>;
|
|
105
|
+
export {};
|
package/dist/widgets/types.d.ts
CHANGED
|
@@ -150,7 +150,7 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
150
150
|
*/
|
|
151
151
|
error?: string;
|
|
152
152
|
/**
|
|
153
|
-
* Upload widgets (`file`/`image`) fire this when their in-progress state
|
|
153
|
+
* Upload widgets (`file`/`image`/`avatar`) fire this when their in-progress state
|
|
154
154
|
* flips, so a host can block submit until a presigned upload settles. Other
|
|
155
155
|
* widgets ignore it.
|
|
156
156
|
*/
|
|
@@ -173,7 +173,15 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
173
173
|
/**
|
|
174
174
|
* Live sibling-field values driving cascading / role-gated options and
|
|
175
175
|
* dependent lookups (ADR-0058, #2215/#2284). The form renderer passes the
|
|
176
|
-
* in-progress record
|
|
176
|
+
* in-progress record.
|
|
177
|
+
*
|
|
178
|
+
* ⚠️ This used to add "widgets fall back to `SchemaRendererContext`". They
|
|
179
|
+
* never could: they spelled such a fallback (`?? ctx.formValues ?? ctx.data`)
|
|
180
|
+
* while `SchemaRendererContextType` declares exactly `dataSource` / `debug` /
|
|
181
|
+
* `debugFlags` / `apiFetch`, so neither member could be supplied by any host
|
|
182
|
+
* and the fallback was unconditionally empty. Those reads were retired under
|
|
183
|
+
* ADR-0049 enforce-or-remove (objectui#7206) — this prop is the one channel
|
|
184
|
+
* that carries a record, and a widget reached without it gates.
|
|
177
185
|
*/
|
|
178
186
|
dependentValues?: Record<string, unknown>;
|
|
179
187
|
/**
|
|
@@ -187,7 +195,7 @@ export type FieldWidgetComponentProps<T = any> = {
|
|
|
187
195
|
* sibling field in user-visible copy (the dependent-lookup gate hint).
|
|
188
196
|
*
|
|
189
197
|
* Only the form renderer knows a field's label — the widget sees its own
|
|
190
|
-
* metadata and a `
|
|
198
|
+
* metadata and a `dependsOn` list of API names. Without this map the gate
|
|
191
199
|
* sentence interpolated the raw API name into every locale, `en` included
|
|
192
200
|
* ("Select crm_account first"), which is an internal identifier leaking into
|
|
193
201
|
* the UI, not merely an untranslated word (objectstack#5407). This is the
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* An ambient "is any upload still in flight?" scope for a whole form subtree.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists beside `onUploadingChange`, and why it is not a second contract
|
|
6
|
+
*
|
|
7
|
+
* `onUploadingChange` (ADR-0059) is a PER-WIDGET prop: a host that renders one
|
|
8
|
+
* upload control and knows its name can pass a callback and keep the answer
|
|
9
|
+
* itself. That is `ActionParamDialog`, which holds a `Record<paramName, boolean>`
|
|
10
|
+
* because it lays out its own params one by one.
|
|
11
|
+
*
|
|
12
|
+
* A record form cannot do that. Its fields are rendered by the `form` node
|
|
13
|
+
* renderer from a `fields` array — the host hands over a list of field configs
|
|
14
|
+
* and never touches the widget — and its upload widgets may sit an arbitrary
|
|
15
|
+
* number of levels down (a section, a tab, a line-items subform). There is no
|
|
16
|
+
* place in that chain where the form host can attach a per-widget callback, so
|
|
17
|
+
* a record form was left with no notion of upload state at all and could submit
|
|
18
|
+
* mid-upload: the record was written WITHOUT the attachment and reported
|
|
19
|
+
* success (objectui#10166).
|
|
20
|
+
*
|
|
21
|
+
* The scope is the aggregation, not a second producer. `useUploadingSignal` is
|
|
22
|
+
* still the ONE place a widget's in-flight state is published; it drives the
|
|
23
|
+
* host prop and this scope from that same call, so the two can never disagree.
|
|
24
|
+
* A widget reports nothing extra and knows nothing about the scope, and a host
|
|
25
|
+
* that mounts no provider (every host on `main` today) is unaffected — the
|
|
26
|
+
* context read answers `null` and the reporting hook is inert.
|
|
27
|
+
*
|
|
28
|
+
* ## The one thing a mount-bound report cannot see (objectui#10180)
|
|
29
|
+
*
|
|
30
|
+
* A widget's in-flight state is React state, so it ends when the widget
|
|
31
|
+
* unmounts — and an upload does not. `FileField` does not abort its upload
|
|
32
|
+
* when it unmounts; the promise settles later and hands the value to the
|
|
33
|
+
* `onChange` it captured, which the form still accepts. So "the widget
|
|
34
|
+
* unmounted" is not "the upload ended". A wizard step the user leaves is the
|
|
35
|
+
* everyday case: leaving the step drops the widget's report, the wizard's final
|
|
36
|
+
* gate reads `false` while the upload is still running, and a Create pressed in
|
|
37
|
+
* that window writes the record WITHOUT the attachment and reports success.
|
|
38
|
+
*
|
|
39
|
+
* The in-repo upload widgets therefore also hold the scope for the lifetime of
|
|
40
|
+
* the upload ITSELF, through {@link useUploadingScopeHold}: taken when the
|
|
41
|
+
* upload starts, released when its promise settles, whether or not the widget
|
|
42
|
+
* is still mounted by then. While the widget is mounted the two entries start
|
|
43
|
+
* and stop together, so they cannot disagree; after it unmounts the hold is the
|
|
44
|
+
* only entry left, and it is the true one. The hold is internal to this package
|
|
45
|
+
* — it is not exported from the package entry.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* The write half of a scope. Kept separate from the boolean so a widget that
|
|
49
|
+
* only REPORTS does not re-render when some other widget's upload flips —
|
|
50
|
+
* reporters read this context, the host owns the boolean.
|
|
51
|
+
*/
|
|
52
|
+
interface UploadingScopeSink {
|
|
53
|
+
/**
|
|
54
|
+
* Record whether the widget registered under `id` is currently uploading.
|
|
55
|
+
* Reporting `false` for an unknown id is a no-op, which is what makes the
|
|
56
|
+
* unmount release below safe to call unconditionally.
|
|
57
|
+
*/
|
|
58
|
+
report: (id: string, uploading: boolean) => void;
|
|
59
|
+
}
|
|
60
|
+
/** What {@link useUploadingScope} hands a host. */
|
|
61
|
+
export interface UploadingScope {
|
|
62
|
+
/** True while at least one upload widget inside the scope is in flight. */
|
|
63
|
+
anyUploading: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Passed to {@link UploadingScopeProvider}. Opaque to the host: it is the
|
|
66
|
+
* channel the widgets report on, not state the host is meant to read.
|
|
67
|
+
*/
|
|
68
|
+
sink: UploadingScopeSink;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Own an uploading scope. The host reads `anyUploading` to gate its own submit
|
|
72
|
+
* and to say WHY that control is disabled, and renders its form subtree inside
|
|
73
|
+
* {@link UploadingScopeProvider}.
|
|
74
|
+
*
|
|
75
|
+
* The host owns the state rather than reading it back out of the context on
|
|
76
|
+
* purpose: a provider cannot consume its own value, and splitting it would give
|
|
77
|
+
* "is anything uploading" two authors.
|
|
78
|
+
*/
|
|
79
|
+
export declare function useUploadingScope(): UploadingScope;
|
|
80
|
+
/**
|
|
81
|
+
* Publish a host's {@link useUploadingScope} to every upload widget rendered
|
|
82
|
+
* below it. Nesting is legal and CHAINS: an inner scope gates its own Save on
|
|
83
|
+
* its own uploads AND reports itself to the scope above, so an outer Save whose
|
|
84
|
+
* write encompasses the inner content is never blind to it. See
|
|
85
|
+
* {@link useUploadingScope} for why the direction is not a preference.
|
|
86
|
+
*/
|
|
87
|
+
export declare function UploadingScopeProvider({ scope, children, }: {
|
|
88
|
+
scope: UploadingScope;
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
}): React.ReactElement;
|
|
91
|
+
/**
|
|
92
|
+
* Report this widget's in-flight state to the enclosing scope, if there is one.
|
|
93
|
+
* Called by `useUploadingSignal` so every widget that already publishes to a
|
|
94
|
+
* host prop publishes here too, from the same state.
|
|
95
|
+
*
|
|
96
|
+
* ⚠️ The unmount release is load-bearing, not tidiness. A widget removed while
|
|
97
|
+
* its upload is in flight — a section collapsing, a tab switching, a subform
|
|
98
|
+
* row deleted — would otherwise leave its id in the set with nothing left to
|
|
99
|
+
* ever clear it, and the form's Save would stay disabled for the rest of the
|
|
100
|
+
* session. That failure mode is worse than the defect this scope fixes,
|
|
101
|
+
* because it has no user-visible cause at all.
|
|
102
|
+
*
|
|
103
|
+
* This report covers the widget's MOUNTED lifetime only. The upload's own
|
|
104
|
+
* lifetime, which can outlast the widget, is covered by
|
|
105
|
+
* {@link useUploadingScopeHold} — that is what keeps the release above from
|
|
106
|
+
* also releasing an upload that is still running (objectui#10180).
|
|
107
|
+
*/
|
|
108
|
+
export declare function useUploadingScopeReport(uploading: boolean): void;
|
|
109
|
+
/**
|
|
110
|
+
* Hold the enclosing scope "uploading" for the lifetime of ONE upload
|
|
111
|
+
* (objectui#10180). The returned `hold()` is called when an upload starts and
|
|
112
|
+
* answers that upload's `release()`, to be called once its promise settles —
|
|
113
|
+
* in a `finally`, so a failed upload releases too.
|
|
114
|
+
*
|
|
115
|
+
* ## Why this is separate from the mount-bound report
|
|
116
|
+
*
|
|
117
|
+
* {@link useUploadingScopeReport} has to release on unmount (see there), and
|
|
118
|
+
* an upload keeps running after the widget that started it has unmounted —
|
|
119
|
+
* `FileField` does not abort it, and its settle still reaches the form's
|
|
120
|
+
* `onChange`. Leaving a wizard step mid-upload is exactly that: the step's
|
|
121
|
+
* widgets unmount, their reports are released, and without this hold the
|
|
122
|
+
* wizard's final gate read `false` while the file was still on its way, so a
|
|
123
|
+
* Create pressed in that window wrote the record without it.
|
|
124
|
+
*
|
|
125
|
+
* ## Why this cannot wedge the form
|
|
126
|
+
*
|
|
127
|
+
* The entry is released by the upload's own settle, not by a render, so it is
|
|
128
|
+
* cleared exactly when the upload ends, mounted widget or not. It can stay
|
|
129
|
+
* held only as long as the upload itself stays unsettled — the same bound a
|
|
130
|
+
* mounted widget's report already has — and while it is held the host shows
|
|
131
|
+
* the reason it gives for any upload in flight.
|
|
132
|
+
*
|
|
133
|
+
* The sink is captured when the upload STARTS: the scope the user picked the
|
|
134
|
+
* file under is the one whose save the file belongs to. A release that reaches
|
|
135
|
+
* a scope whose host has since unmounted is a no-op, and that host already
|
|
136
|
+
* released itself from any scope above it on the way out.
|
|
137
|
+
*
|
|
138
|
+
* Inert without a provider, like the report. Not exported from the package
|
|
139
|
+
* entry: it is how this package's own upload widgets report, not a contract.
|
|
140
|
+
*/
|
|
141
|
+
export declare function useUploadingScopeHold(): () => () => void;
|
|
142
|
+
export {};
|
|
@@ -9,8 +9,9 @@ import { CascadingOptions, OptionLike, DependsOnInput } from '@object-ui/core';
|
|
|
9
9
|
* empty the list is *gated* — callers surface a "select the parent first" hint
|
|
10
10
|
* rather than an unfiltered set, mirroring the dependent-lookup UX.
|
|
11
11
|
*
|
|
12
|
-
* This is the React wrapper
|
|
13
|
-
*
|
|
12
|
+
* This is the React wrapper: `record` (the live form values) arrives on the
|
|
13
|
+
* `dependentValues` argument its HOST passes, the predicate scope comes from
|
|
14
|
+
* context, and the actual resolution is the pure
|
|
14
15
|
* {@link resolveCascadingOptions} in `@object-ui/core`, shared with the form
|
|
15
16
|
* renderer so gating/filtering can never drift between them (#2715).
|
|
16
17
|
*/
|
|
@@ -12,7 +12,9 @@ import { DataSource } from '@object-ui/types';
|
|
|
12
12
|
* (`{ data, total }`, tolerating a bare array),
|
|
13
13
|
* - owns `records`/`loading`/`error`/`total` plus the `page`/`search`/`sort`
|
|
14
14
|
* controls, with a debounced search path,
|
|
15
|
-
* - clears itself when `enabled`
|
|
15
|
+
* - clears itself when it can no longer query — `enabled` false (e.g. a dialog
|
|
16
|
+
* closing), or no data source or object name — superseding any read still in
|
|
17
|
+
* flight, so that read's answer commits nothing (objectui#10712).
|
|
16
18
|
*
|
|
17
19
|
* The caller keeps ownership of *selection* state and of record→option mapping;
|
|
18
20
|
* this hook only answers "what records match the current query". It is the
|
|
@@ -43,7 +45,7 @@ export interface UseRecordQueryOptions {
|
|
|
43
45
|
*/
|
|
44
46
|
paginate?: boolean;
|
|
45
47
|
/**
|
|
46
|
-
* `$filter` — already merged by the caller (base `
|
|
48
|
+
* `$filter` — already merged by the caller (base `lookupFilters`, dependent
|
|
47
49
|
* lookup chain, candidate hygiene like `banned != true`, …). Compared by
|
|
48
50
|
* value, so a referentially-new-but-equal object each render will not loop.
|
|
49
51
|
*
|
|
@@ -5,5 +5,13 @@
|
|
|
5
5
|
* submitted mid-upload (the value only becomes the fileId once the upload
|
|
6
6
|
* settles). Fires with the latest callback and only when `uploading` actually
|
|
7
7
|
* changes, so an inline arrow prop doesn't thrash the parent every render.
|
|
8
|
+
*
|
|
9
|
+
* It ALSO reports into the ambient `UploadingScope` when one is mounted above
|
|
10
|
+
* the widget (objectui#10166). The prop suits a host that renders one named
|
|
11
|
+
* control; a record form hands its fields to the `form` node renderer and never
|
|
12
|
+
* touches the widget, so it has no place to attach a callback — the scope is how
|
|
13
|
+
* that host learns an upload is in flight. Both sinks are driven from this one
|
|
14
|
+
* call so they cannot disagree, and a host that mounts no provider is
|
|
15
|
+
* unaffected: `useUploadingScopeReport` is inert without one.
|
|
8
16
|
*/
|
|
9
17
|
export declare function useUploadingSignal(uploading: boolean, onUploadingChange?: (uploading: boolean) => void): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/fields",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.7.0",
|
|
4
4
|
"description": "Field renderers and registry for Object UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,19 +23,18 @@
|
|
|
23
23
|
"LICENSE"
|
|
24
24
|
],
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@objectstack/spec": "^17.
|
|
27
|
-
"
|
|
28
|
-
"lucide-react": "^1.31.0",
|
|
26
|
+
"@objectstack/spec": "^17.6.0",
|
|
27
|
+
"lucide-react": "^1.43.0",
|
|
29
28
|
"react-markdown": "^10.1.0",
|
|
30
29
|
"rehype-sanitize": "^6.0.0",
|
|
31
30
|
"remark-gfm": "^4.0.1",
|
|
32
|
-
"
|
|
33
|
-
"@object-ui/
|
|
34
|
-
"@object-ui/
|
|
35
|
-
"@object-ui/i18n": "17.
|
|
36
|
-
"@object-ui/providers": "17.
|
|
37
|
-
"@object-ui/react": "17.
|
|
38
|
-
"@object-ui/types": "17.
|
|
31
|
+
"@object-ui/components": "17.7.0",
|
|
32
|
+
"@object-ui/core": "17.7.0",
|
|
33
|
+
"@object-ui/permissions": "17.7.0",
|
|
34
|
+
"@object-ui/i18n": "17.7.0",
|
|
35
|
+
"@object-ui/providers": "17.7.0",
|
|
36
|
+
"@object-ui/react": "17.7.0",
|
|
37
|
+
"@object-ui/types": "17.7.0"
|
|
39
38
|
},
|
|
40
39
|
"peerDependencies": {
|
|
41
40
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -83,7 +82,7 @@
|
|
|
83
82
|
"build": "tsc && vite build && node scripts/build-css.mjs",
|
|
84
83
|
"clean": "rm -rf dist",
|
|
85
84
|
"type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
86
|
-
"test": "vitest run",
|
|
85
|
+
"test": "vitest run --root ../.. packages/fields/",
|
|
87
86
|
"lint": "eslint ."
|
|
88
87
|
}
|
|
89
88
|
}
|