@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +5436 -0
  2. package/README.md +108 -8
  3. package/dist/FieldEditWidget.d.ts +54 -6
  4. package/dist/coerceToSafeValue.d.ts +54 -0
  5. package/dist/currency.d.ts +1 -1
  6. package/dist/index.cjs +5 -5
  7. package/dist/index.css +26 -2
  8. package/dist/index.d.ts +229 -57
  9. package/dist/index.js +3811 -3009
  10. package/dist/widgets/AvatarField.d.ts +19 -3
  11. package/dist/widgets/BooleanField.d.ts +1 -1
  12. package/dist/widgets/DateField.d.ts +1 -1
  13. package/dist/widgets/DateTimeField.d.ts +1 -1
  14. package/dist/widgets/FilterConditionField.d.ts +21 -7
  15. package/dist/widgets/GeolocationField.d.ts +9 -3
  16. package/dist/widgets/GridField.d.ts +63 -99
  17. package/dist/widgets/LocationField.d.ts +30 -3
  18. package/dist/widgets/LookupField.d.ts +8 -0
  19. package/dist/widgets/NumberField.d.ts +1 -1
  20. package/dist/widgets/ObjectField.d.ts +9 -2
  21. package/dist/widgets/OptionsEmptyState.d.ts +29 -5
  22. package/dist/widgets/PasswordField.d.ts +14 -0
  23. package/dist/widgets/PeoplePicker.d.ts +17 -2
  24. package/dist/widgets/PercentField.d.ts +2 -1
  25. package/dist/widgets/PersonRow.d.ts +13 -2
  26. package/dist/widgets/RecipientPickerField.d.ts +43 -0
  27. package/dist/widgets/RecordPickerDialog.d.ts +31 -12
  28. package/dist/widgets/RichTextField.d.ts +1 -1
  29. package/dist/widgets/SelectionTray.d.ts +13 -2
  30. package/dist/widgets/TextField.d.ts +1 -1
  31. package/dist/widgets/TimeField.d.ts +1 -1
  32. package/dist/widgets/file-affordance.d.ts +33 -0
  33. package/dist/widgets/file-size-guard.d.ts +5 -23
  34. package/dist/widgets/file-value.d.ts +38 -9
  35. package/dist/widgets/lookupColumnDisplay.d.ts +24 -7
  36. package/dist/widgets/nativeDateValue.d.ts +9 -27
  37. package/dist/widgets/number-format.d.ts +137 -0
  38. package/dist/widgets/numberBadInput.d.ts +135 -0
  39. package/dist/widgets/personDisplay.d.ts +22 -4
  40. package/dist/widgets/richTextDisplay.d.ts +36 -0
  41. package/dist/widgets/toDomProps.d.ts +10 -1
  42. package/dist/widgets/toHostGroupProps.d.ts +53 -0
  43. package/dist/widgets/toHostProps.d.ts +105 -0
  44. package/dist/widgets/types.d.ts +11 -3
  45. package/dist/widgets/upload-error-message.d.ts +6 -0
  46. package/dist/widgets/uploadingScope.d.ts +142 -0
  47. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  48. package/dist/widgets/useRecordQuery.d.ts +4 -2
  49. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  50. 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
- /** Best display name for a person record, tolerating expanded / raw shapes. */
17
- export declare function getPersonName(record: any, nameField?: string): string;
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
- /** Avatar image URL (defaults to `sys_user.image`); undefined when absent. */
27
- export declare function getPersonAvatarUrl(record: any, avatarField?: string): string | undefined;
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
- type DomPassThroughKey = (typeof DOM_PASS_THROUGH_KEYS)[number];
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 {};
@@ -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; widgets fall back to `SchemaRendererContext`.
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 `depends_on` list of API names. Without this map the gate
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,6 @@
1
+ import { TranslateFn } from '@object-ui/i18n';
2
+ /**
3
+ * The translated error row for a pick named `name` whose upload ended in
4
+ * `err`, either kind.
5
+ */
6
+ export declare function uploadErrorMessage(t: TranslateFn, name: string, err: unknown): string;
@@ -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 that sources `record` (the live form values) and the
13
- * predicate scope from context; the actual resolution is the pure
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` goes false (e.g. a dialog closing).
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 `lookup_filters`, dependent
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.6.0",
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.0.0",
27
- "clsx": "^2.1.1",
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
- "tailwind-merge": "^3.6.0",
33
- "@object-ui/components": "17.6.0",
34
- "@object-ui/core": "17.6.0",
35
- "@object-ui/i18n": "17.6.0",
36
- "@object-ui/providers": "17.6.0",
37
- "@object-ui/react": "17.6.0",
38
- "@object-ui/types": "17.6.0"
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
  }