@object-ui/fields 17.6.0 → 18.0.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 (64) hide show
  1. package/CHANGELOG.md +6228 -0
  2. package/README.md +251 -9
  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/fieldHelpText.d.ts +41 -0
  7. package/dist/index.cjs +5 -5
  8. package/dist/index.css +68 -5
  9. package/dist/index.d.ts +446 -69
  10. package/dist/index.js +5326 -3443
  11. package/dist/resolveActionParams.d.ts +259 -0
  12. package/dist/widgets/AvatarField.d.ts +19 -3
  13. package/dist/widgets/BooleanField.d.ts +1 -1
  14. package/dist/widgets/DateField.d.ts +1 -1
  15. package/dist/widgets/DateTimeField.d.ts +1 -1
  16. package/dist/widgets/FilterConditionField.d.ts +21 -7
  17. package/dist/widgets/FormulaField.d.ts +41 -0
  18. package/dist/widgets/GeolocationField.d.ts +9 -3
  19. package/dist/widgets/GridField.d.ts +73 -101
  20. package/dist/widgets/LocationField.d.ts +30 -3
  21. package/dist/widgets/LookupField.d.ts +8 -0
  22. package/dist/widgets/NumberField.d.ts +1 -1
  23. package/dist/widgets/ObjectField.d.ts +9 -2
  24. package/dist/widgets/OptionsEmptyState.d.ts +29 -5
  25. package/dist/widgets/PasswordField.d.ts +14 -0
  26. package/dist/widgets/PeoplePicker.d.ts +17 -2
  27. package/dist/widgets/PercentField.d.ts +2 -1
  28. package/dist/widgets/PersonRow.d.ts +13 -2
  29. package/dist/widgets/RecipientPickerField.d.ts +75 -0
  30. package/dist/widgets/RecordPickerDialog.d.ts +31 -12
  31. package/dist/widgets/RegistryRefField.d.ts +77 -0
  32. package/dist/widgets/RichTextField.d.ts +1 -1
  33. package/dist/widgets/SelectionTray.d.ts +13 -2
  34. package/dist/widgets/SummaryField.d.ts +30 -0
  35. package/dist/widgets/TextField.d.ts +1 -1
  36. package/dist/widgets/TimeField.d.ts +1 -1
  37. package/dist/widgets/booleanValueLabel.d.ts +18 -0
  38. package/dist/widgets/date-picker/DatePickerField.d.ts +40 -0
  39. package/dist/widgets/date-picker/localeDateText.d.ts +39 -0
  40. package/dist/widgets/deriveLookupColumns.d.ts +1 -1
  41. package/dist/widgets/file-affordance.d.ts +63 -0
  42. package/dist/widgets/file-refused.d.ts +17 -0
  43. package/dist/widgets/file-size-guard.d.ts +5 -23
  44. package/dist/widgets/file-value.d.ts +49 -9
  45. package/dist/widgets/longTextCarrierHandoff.d.ts +35 -0
  46. package/dist/widgets/lookupColumnDisplay.d.ts +26 -9
  47. package/dist/widgets/nativeDateValue.d.ts +9 -27
  48. package/dist/widgets/number-format.d.ts +137 -0
  49. package/dist/widgets/numberBadInput.d.ts +135 -0
  50. package/dist/widgets/objectRefLabel.d.ts +16 -0
  51. package/dist/widgets/personDisplay.d.ts +22 -4
  52. package/dist/widgets/recipientKinds.d.ts +85 -0
  53. package/dist/widgets/richTextDisplay.d.ts +32 -13
  54. package/dist/widgets/searchOffer.d.ts +8 -0
  55. package/dist/widgets/toDomProps.d.ts +26 -4
  56. package/dist/widgets/toHostGroupProps.d.ts +53 -0
  57. package/dist/widgets/toHostProps.d.ts +105 -0
  58. package/dist/widgets/types.d.ts +11 -3
  59. package/dist/widgets/upload-error-message.d.ts +6 -0
  60. package/dist/widgets/uploadingScope.d.ts +142 -0
  61. package/dist/widgets/useCascadingOptions.d.ts +3 -2
  62. package/dist/widgets/useRecordQuery.d.ts +17 -2
  63. package/dist/widgets/useUploadingSignal.d.ts +8 -0
  64. package/package.json +11 -12
@@ -0,0 +1,135 @@
1
+ import { default as React } from 'react';
2
+ import { TranslateFn } from './file-size-guard.js';
3
+ /**
4
+ * ONE reading of "this box is showing text the browser cannot read", shared by
5
+ * every `type="number"` widget in this package (objectui#6780).
6
+ *
7
+ * ## The defect this closes
8
+ *
9
+ * A `type="number"` box can DISPLAY one thing and hold another, with nothing
10
+ * said. Measured on Chromium 141.0.7390.37 (Playwright 1.62.1), typing `1e`
11
+ * into an empty number input:
12
+ *
13
+ * ```
14
+ * .value = "" <- what every widget here reads
15
+ * validity.badInput = true <- what the browser actually thinks
16
+ * pixels vs an untouched box: DIFFERENT <- the box IS still showing `1e`
17
+ * ```
18
+ *
19
+ * So the widget emitted `null`, `aria-invalid` stayed `"false"`, and no
20
+ * diagnostic was drawn: objectui#6716's silent-refusal class, on a money field.
21
+ *
22
+ * ## Why `validity.badInput` and not a regex of our own
23
+ *
24
+ * It is the PLATFORM's own predicate — the browser stating, in its own words,
25
+ * whether it can read the text — rather than a second renderer-side dialect of
26
+ * "what a number is" (AGENTS.md #0.1).
27
+ *
28
+ * ⚠️ objectui#6765 recorded `badInput` as "the ONE signal happy-dom and Chromium
29
+ * agree on". objectui#6780 measured that and NARROWED it, because the
30
+ * difference decides which strings a unit test may drive: in Chromium
31
+ * `badInput` is never true for a programmatic `.value` write (it reports that
32
+ * the UA cannot read the USER's input), so on the only route a happy-dom test
33
+ * has, the two engines agree on nothing. The agreement this guard rests on is
34
+ * between happy-dom's programmatic verdict and Chromium's TYPED verdict, and it
35
+ * holds for a measured subset — four strings agree on "bad", seven agree on
36
+ * "fine", and eight disagree. The full matrix, and the lists the tests are
37
+ * allowed to use, are in `__tests__/numberInputBrowserReadings.ts`.
38
+ *
39
+ * ⭐ And it is NOT objectui#6715's anchored guard wearing a different hat.
40
+ * That one was measured to be a provable no-op here — it rejects only strings
41
+ * this test environment fabricates. This one fires on keyboard-reachable
42
+ * states, MEASURED in Chromium by typing each key by key into an empty box:
43
+ *
44
+ * ```
45
+ * typed "1e" "1e-" "1e+" "-" "." "+" "-." "e" "5e" -> badInput TRUE
46
+ * typed "1." "1e5" "1.2.3" "0x10" "12abc" -> badInput false
47
+ * ```
48
+ *
49
+ * and never on anything the browser can actually emit: `12`, `1.23`, `010`,
50
+ * `15`, `1`, `12.345` all read `badInput === false`.
51
+ *
52
+ * ## ⛔ What this deliberately does NOT do: change what is emitted
53
+ *
54
+ * objectui#6716's shape REFUSES (its `onChange` never fires and the prior value
55
+ * stands). Here that would destroy the very text the diagnostic points at, so
56
+ * these widgets keep emitting exactly what they emitted before and only stop
57
+ * being silent about it. MEASURED, in both halves:
58
+ *
59
+ * - Chromium: after typing `1e`, a script write of `.value` clears the raw
60
+ * display and flips `badInput` back to `false` — even writing `""`.
61
+ * - React 19.2.8's own `updateInput` (`react-dom/cjs/react-dom-client.
62
+ * development.js`) restores a number input with
63
+ * `if ((0 === value && "" === element.value) || element.value != value)
64
+ * element.value = "" + getToStringValue(value);`
65
+ *
66
+ * So for a box that already held `5`, refusing leaves `props.value` at `5`,
67
+ * `element.value` at `""`, and React writes `"5"` back — wiping the `5e` the
68
+ * user is looking at and the message is about. Emitting keeps `props.value` at
69
+ * `""`, the write is skipped, and the raw text survives, which is what
70
+ * objectui#6716 requires of a refusal ("keeps the refused text in the box, so
71
+ * the message has something to point at").
72
+ *
73
+ * ⚠️ The text itself is NOT quotable. The browser displays it but never exposes
74
+ * it — `.value` is `""` and there is no other channel — so unlike
75
+ * objectui#6715's residue message this one cannot name what was typed. It
76
+ * points at the box instead.
77
+ */
78
+ /**
79
+ * The one sentence, in objectui#6716's `Not saved: …` shape, read from the
80
+ * package's locale channel (objectui#8148).
81
+ *
82
+ * objectui#6755 ruled that a widget's OWN refusal sentence goes through
83
+ * `useFieldTranslation` + `FIELD_DEFAULTS`; `320374d2a` applied it to
84
+ * `LocationField`'s residue arm. This was the FIFTH such sentence and the only
85
+ * SHARED one — the four already keyed are each one widget's, while this literal
86
+ * served `NumberField`, `CurrencyField`, `PercentField` and both of
87
+ * `GeolocationField`'s boxes through {@link useBadInputRefusal}. Its sharpest
88
+ * consequence: `GeolocationField` sits beside `LocationField`, whose refusals
89
+ * are all keyed, so two adjacent coordinate widgets refused bad input in two
90
+ * different languages on one form.
91
+ *
92
+ * ⭐ `example` stays a HOLE rather than being keyed per widget, and that is
93
+ * what keeps the decimal numerals out of every pack. Five different values
94
+ * reach this one sentence — `1234`, `1234.56`, `12.5`, `30.2741`, `120.1551` —
95
+ * so a per-widget key would need five keys times ten packs, and each pack would
96
+ * then hold a decimal numeral it could legitimately re-punctuate. A pack that
97
+ * wrote `1234,56` would read as the `latitude, longitude` PAIR the adjacent
98
+ * widget asks for; the hole is filled by the widget in ASCII and no pack ever
99
+ * spells a digit.
100
+ *
101
+ * The English value is byte-identical to the literal it replaces, so English
102
+ * and provider-less rendering are unchanged.
103
+ */
104
+ export declare function badInputMessage(t: TranslateFn, example: string): string;
105
+ /**
106
+ * The widget's OWN refusal state, and the reader that fills it.
107
+ *
108
+ * Named `refusal`, never `error`: `error` is the published validation slot on
109
+ * the widget contract (objectui#3222) with exactly one author, the form
110
+ * renderer. This is the widget's own reading, which no host can produce — the
111
+ * same two-name split `LocationField` uses (objectui#6716).
112
+ *
113
+ * `readBadInput` is called from BOTH arms, because neither alone sees every
114
+ * route (MEASURED in Chromium):
115
+ *
116
+ * - TYPED `1e` into an empty box fires 2 `input` events — the `e` keystroke
117
+ * moves `.value` from `"1"` to `""`, so React does deliver an `onChange`.
118
+ * - PASTED `1e` into an empty box fires 1 `input` event, but `.value` never
119
+ * leaves `""`; React's own input-value tracking suppresses the synthetic
120
+ * `onChange`, so the CHANGE arm never runs. Blur fires, and `badInput` is
121
+ * still `true` at blur time — which is the whole reason for the blur arm.
122
+ */
123
+ export declare function useBadInputRefusal(example: string): {
124
+ refusal: string | null;
125
+ readBadInput: (target: HTMLInputElement | null | undefined) => boolean;
126
+ };
127
+ /**
128
+ * The drawn diagnostic — objectui#6716's exact markup, in one spelling so the
129
+ * four widgets of this class cannot drift apart.
130
+ */
131
+ export declare function BadInputMessage({ refusal }: {
132
+ refusal: string | null;
133
+ }): React.JSX.Element | null;
134
+ /** The class that marks the refused control, in one spelling. */
135
+ export declare const BAD_INPUT_BORDER = "border-red-500 focus-visible:ring-red-500";
@@ -0,0 +1,16 @@
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
+ * An object's name as the `object-ref` picker spells it, `Label (name)`, or the
10
+ * bare name when the object has no label (objectui#12613). `ObjectRefField`
11
+ * labels each option with it, and the console's read face for the field
12
+ * (`@object-ui/app-shell`'s widget reference cells) uses it for its tooltip, so
13
+ * the two faces spell an object one way. Published from the package barrel for
14
+ * that reader. No React here.
15
+ */
16
+ export declare function objectRefOptionLabel(name: string, label: string | undefined): string;
@@ -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
  /**
@@ -0,0 +1,85 @@
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 recipient kinds a `recipient-picker` field stores, and where each kind's
10
+ * candidates and labels come from (objectui#12613).
11
+ *
12
+ * ONE table, two readers: `RecipientPickerField` (the edit widget) lists a
13
+ * kind's candidates from it, and the console's read face for the field
14
+ * (`@object-ui/app-shell`'s widget reference cells) names a stored recipient
15
+ * from it. They lived inside the widget until the face needed the same answer;
16
+ * a second copy beside the face is the drift this module exists to prevent, so
17
+ * a kind added here reaches both faces at once. Published from the package
18
+ * barrel for that reader. The picker's candidate query is not shared: it stops
19
+ * at 500 rows by name, and the face reads stored ids by exact id instead.
20
+ *
21
+ * No React here.
22
+ */
23
+ export interface RecipientMapping {
24
+ object: string;
25
+ /** Which record field to persist into recipient_id. */
26
+ storeField: 'id' | 'name';
27
+ /** Candidate display-label fields, in preference order. */
28
+ labelFields: string[];
29
+ /**
30
+ * i18n key for the "choose one" placeholder. Keyed per type rather than
31
+ * interpolating the enum value into an English sentence — "Select a
32
+ * business unit" and "选择业务单元" share no structure (objectstack#3821).
33
+ */
34
+ placeholderKey: string;
35
+ }
36
+ /**
37
+ * The recipient kinds whose candidates are RECORDS of one directory object,
38
+ * stored by the record field the evaluator matches on:
39
+ *
40
+ * user → sys_user, store `id`
41
+ * team → sys_team, store `id`
42
+ * business_unit → sys_business_unit, store `id`
43
+ * unit_and_subordinates → sys_business_unit, store `id`
44
+ */
45
+ export declare const RECIPIENT_RECORD_KINDS: Readonly<Record<string, RecipientMapping>>;
46
+ /** A recipient kind whose candidates are catalog items of one metadata type. */
47
+ export interface RegistryRecipientMapping {
48
+ /** The metadata type listed — the registry's `GET /api/v1/meta/<type>`. */
49
+ type: string;
50
+ /** Catalog items are referenced by machine name (ADR-0131 D4). */
51
+ storeField: 'name';
52
+ /** Candidate display-label keys, in preference order. */
53
+ labelFields: string[];
54
+ /** i18n key for the "choose one" placeholder (see {@link RecipientMapping}). */
55
+ placeholderKey: string;
56
+ }
57
+ /**
58
+ * The recipient kinds whose candidates come from the registry (objectui#7611).
59
+ *
60
+ * ADR-0131 D3 gives positions one home, the environment registry, and D7 says
61
+ * a picker LISTS that registry: one source, with no second list of
62
+ * `sys_position` rows merged in. The read goes through the console's metadata
63
+ * store (`useMetadata().ensureType`), the same reader that serves apps and
64
+ * objects, so the list is fetched once per session and shared with every
65
+ * other registry consumer.
66
+ */
67
+ export declare const RECIPIENT_REGISTRY_KINDS: Readonly<Record<string, RegistryRecipientMapping>>;
68
+ /**
69
+ * The RECORD-RELATIVE recipient kind (maintainer ruling objectstack#14103,
70
+ * executor objectstack#15072). It is deliberately absent from
71
+ * `RECIPIENT_RECORD_KINDS`: there is no target object to query and no record id to
72
+ * store. `recipient_id` holds the NAME of a user-valued column on the SHARED
73
+ * object, and the recipients are whoever that column names on each matched
74
+ * record — so this mode reads the object chosen in the sibling `object_name`
75
+ * field (the same dependency the `filter-condition` widget already reads) and
76
+ * offers that object's user-valued fields.
77
+ */
78
+ export declare const FIELD_RECIPIENT_TYPE = "field";
79
+ /**
80
+ * The display label of one candidate: the first non-empty member of the
81
+ * kind's `labelFields`, then the record id. The picker labels its options with
82
+ * it and the read cell names a stored recipient with it, so a recipient reads
83
+ * the same in the list and in the open picker.
84
+ */
85
+ export declare function recipientCandidateLabel(labelFields: readonly string[] | undefined, row: unknown): string;
@@ -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
  /**
@@ -51,22 +63,29 @@ export declare const RICH_TEXT_CELL_RENDERERS: {
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;
53
65
  /**
54
- * The SYNTAX a rich-content type stores, derived from the renderer that type
55
- * resolves to rather than declared a second time.
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
56
72
  *
57
- * This is what `RichTextField`'s editor header names ("Format: markdown"), and
58
- * deriving it from {@link RICH_TEXT_CELL_RENDERERS} is the point: the header
59
- * used to be computed as `field.format || 'markdown'`, and since `format` is
60
- * declared on no rich-content field type (only on `date`/`datetime`/`time`/
61
- * `phone`/`auto_number`), it read `undefined` for every real field and labelled
62
- * an `html` field "Format: markdown" — a label pointing at a pipeline the value
63
- * does not go through. Derived, the label cannot disagree with the renderer.
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.
64
81
  *
65
- * `undefined` for a type with no pipeline, rather than a guessed default: the
66
- * caller has to say what it does with "not a rich-content type" instead of
67
- * silently getting the markdown answer, which is the bug this replaces.
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*.
68
87
  */
69
- export declare function richTextSyntax(fieldType: string): 'markdown' | 'html' | undefined;
88
+ export declare const RICH_TEXT_FIELD_TYPES: readonly RichTextFieldType[];
70
89
  /**
71
90
  * The display pipeline a field type reads through, or `undefined` when the
72
91
  * type is not one of the three rich-content types.
@@ -0,0 +1,8 @@
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
+ export declare function objectResolvesSearch(objectSchema: unknown): boolean | undefined;
@@ -83,9 +83,31 @@ 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];
87
- /** The subset of `P` this helper forwards, with each key's declared type. */
88
- export type DomProps<P> = CoreDomProps<P, DomPassThroughKey>;
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];
96
+ /**
97
+ * The subset of `P` this helper forwards, with each key's declared type.
98
+ *
99
+ * Not {@link FieldWidgetDomProps}: that is the contract's DOM block, the
100
+ * non-generic set of keys a widget may receive; this is the generic RESULT
101
+ * type of {@link toDomProps}, whatever `P` it is handed narrowed to this
102
+ * helper's key set.
103
+ *
104
+ * Not `DomProps`: `@object-ui/core` publishes that name for the general
105
+ * mechanism, whose default key set forwards `role` where this one forwards
106
+ * `name` and `disabled`. One exported name stood for both key sets until this
107
+ * declaration was renamed, the way `@object-ui/components` names its own
108
+ * specialisation `FormControlDomProps` (objectui#6349, batch 11).
109
+ */
110
+ export type FieldDomProps<P> = CoreDomProps<P, DomPassThroughKey>;
89
111
  /**
90
112
  * Keep only what may legitimately become a DOM attribute, and drop everything
91
113
  * else — renderer plumbing, and any extra key an author put on the field
@@ -107,5 +129,5 @@ export type DomProps<P> = CoreDomProps<P, DomPassThroughKey>;
107
129
  * `readonly`, `error`, …) are read from `props` as before; this function only
108
130
  * governs what gets spread.
109
131
  */
110
- export declare function toDomProps<P extends object>(props: P): DomProps<P>;
132
+ export declare function toDomProps<P extends object>(props: P): FieldDomProps<P>;
111
133
  export {};
@@ -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;