@object-ui/fields 16.1.0 → 17.2.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 (129) hide show
  1. package/CHANGELOG.md +626 -0
  2. package/dist/FieldEditWidget.d.ts +9 -2
  3. package/dist/__tests__/spec-symbol-batch7.test.d.ts +8 -0
  4. package/dist/__tests__/widget-props-contract.test.d.ts +8 -0
  5. package/dist/field-type-alias.d.ts +16 -0
  6. package/dist/index.cjs +5 -5
  7. package/dist/index.d.ts +14 -11
  8. package/dist/index.js +2426 -2100
  9. package/dist/widgets/AddressField.d.ts +2 -2
  10. package/dist/widgets/AutoNumberField.d.ts +2 -2
  11. package/dist/widgets/AvatarField.d.ts +2 -2
  12. package/dist/widgets/BooleanField.d.ts +2 -2
  13. package/dist/widgets/CapabilityMultiSelectField.d.ts +2 -2
  14. package/dist/widgets/CheckboxesField.d.ts +2 -2
  15. package/dist/widgets/CodeField.d.ts +2 -2
  16. package/dist/widgets/ColorField.d.ts +2 -2
  17. package/dist/widgets/CurrencyField.d.ts +2 -2
  18. package/dist/widgets/DateField.d.ts +2 -2
  19. package/dist/widgets/DateTimeField.d.ts +2 -2
  20. package/dist/widgets/EmailField.d.ts +2 -2
  21. package/dist/widgets/FileField.d.ts +2 -2
  22. package/dist/widgets/FilterConditionField.d.ts +34 -2
  23. package/dist/widgets/FormulaField.d.ts +2 -2
  24. package/dist/widgets/GeolocationField.d.ts +2 -2
  25. package/dist/widgets/GridField.d.ts +2 -2
  26. package/dist/widgets/ImageField.d.ts +2 -2
  27. package/dist/widgets/ImageLightbox.d.ts +30 -0
  28. package/dist/widgets/ImageLightbox.test.d.ts +1 -0
  29. package/dist/widgets/LocationField.d.ts +2 -2
  30. package/dist/widgets/LookupField.d.ts +2 -2
  31. package/dist/widgets/LookupField.hydration.test.d.ts +1 -0
  32. package/dist/widgets/LookupField.idField.test.d.ts +1 -0
  33. package/dist/widgets/MasterDetailField.d.ts +2 -2
  34. package/dist/widgets/MultiSelectField.d.ts +2 -2
  35. package/dist/widgets/NumberField.d.ts +2 -2
  36. package/dist/widgets/ObjectField.d.ts +2 -2
  37. package/dist/widgets/ObjectRefField.d.ts +2 -2
  38. package/dist/widgets/PasswordField.d.ts +2 -2
  39. package/dist/widgets/PercentField.d.ts +2 -2
  40. package/dist/widgets/PhoneField.d.ts +2 -2
  41. package/dist/widgets/QRCodeField.d.ts +2 -2
  42. package/dist/widgets/RadioField.d.ts +2 -2
  43. package/dist/widgets/RatingField.d.ts +2 -2
  44. package/dist/widgets/RecipientPickerField.d.ts +2 -2
  45. package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
  46. package/dist/widgets/RichTextField.d.ts +2 -2
  47. package/dist/widgets/SelectField.d.ts +2 -2
  48. package/dist/widgets/SignatureField.d.ts +2 -2
  49. package/dist/widgets/SliderField.d.ts +2 -2
  50. package/dist/widgets/SummaryField.d.ts +2 -2
  51. package/dist/widgets/TagsField.d.ts +2 -2
  52. package/dist/widgets/TextAreaField.d.ts +2 -2
  53. package/dist/widgets/TextField.d.ts +2 -2
  54. package/dist/widgets/TimeField.d.ts +2 -2
  55. package/dist/widgets/UrlField.d.ts +2 -2
  56. package/dist/widgets/UserField.d.ts +2 -2
  57. package/dist/widgets/VectorField.d.ts +2 -2
  58. package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +8 -0
  59. package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +8 -0
  60. package/dist/widgets/file-value.d.ts +99 -0
  61. package/dist/widgets/file-value.test.d.ts +1 -0
  62. package/dist/widgets/types.d.ts +119 -3
  63. package/dist/widgets/useFieldTranslation.d.ts +2 -0
  64. package/package.json +9 -8
  65. package/src/FieldEditWidget.test.ts +74 -2
  66. package/src/FieldEditWidget.tsx +50 -8
  67. package/src/__tests__/spec-symbol-batch7.test.ts +112 -0
  68. package/src/__tests__/widget-props-contract.test.tsx +129 -0
  69. package/src/complex-widgets.test.tsx +3 -3
  70. package/src/datetime-widgets.test.tsx +2 -2
  71. package/src/field-type-alias.ts +100 -0
  72. package/src/index.tsx +97 -134
  73. package/src/widgets/AddressField.tsx +2 -2
  74. package/src/widgets/AutoNumberField.tsx +2 -2
  75. package/src/widgets/AvatarField.tsx +2 -2
  76. package/src/widgets/BooleanField.tsx +2 -2
  77. package/src/widgets/CapabilityMultiSelectField.tsx +4 -4
  78. package/src/widgets/CheckboxesField.tsx +12 -9
  79. package/src/widgets/CodeField.tsx +2 -2
  80. package/src/widgets/ColorField.tsx +2 -2
  81. package/src/widgets/CurrencyField.tsx +2 -2
  82. package/src/widgets/DateField.tsx +2 -2
  83. package/src/widgets/DateTimeField.tsx +2 -2
  84. package/src/widgets/EmailField.tsx +2 -2
  85. package/src/widgets/FileField.tsx +90 -59
  86. package/src/widgets/FilterConditionField.tsx +112 -12
  87. package/src/widgets/FormulaField.tsx +2 -2
  88. package/src/widgets/GeolocationField.tsx +2 -2
  89. package/src/widgets/GridField.tsx +3 -3
  90. package/src/widgets/ImageField.tsx +73 -35
  91. package/src/widgets/ImageLightbox.test.tsx +52 -0
  92. package/src/widgets/ImageLightbox.tsx +108 -0
  93. package/src/widgets/LocationField.tsx +2 -2
  94. package/src/widgets/LookupField.hydration.test.tsx +136 -0
  95. package/src/widgets/LookupField.idField.test.tsx +61 -0
  96. package/src/widgets/LookupField.tsx +116 -19
  97. package/src/widgets/MasterDetailField.tsx +2 -2
  98. package/src/widgets/MultiSelectField.tsx +10 -7
  99. package/src/widgets/NumberField.tsx +2 -2
  100. package/src/widgets/ObjectField.tsx +2 -2
  101. package/src/widgets/ObjectRefField.tsx +13 -8
  102. package/src/widgets/PasswordField.tsx +2 -2
  103. package/src/widgets/PeoplePicker.test.tsx +20 -0
  104. package/src/widgets/PeoplePicker.tsx +15 -3
  105. package/src/widgets/PercentField.tsx +2 -2
  106. package/src/widgets/PhoneField.tsx +2 -2
  107. package/src/widgets/QRCodeField.tsx +2 -2
  108. package/src/widgets/RadioField.tsx +10 -7
  109. package/src/widgets/RatingField.tsx +2 -2
  110. package/src/widgets/RecipientPickerField.test.tsx +157 -0
  111. package/src/widgets/RecipientPickerField.tsx +40 -20
  112. package/src/widgets/RichTextField.tsx +2 -2
  113. package/src/widgets/SelectField.tsx +5 -5
  114. package/src/widgets/SignatureField.tsx +2 -2
  115. package/src/widgets/SliderField.tsx +2 -2
  116. package/src/widgets/SummaryField.tsx +2 -2
  117. package/src/widgets/TagsField.tsx +3 -3
  118. package/src/widgets/TextAreaField.tsx +6 -3
  119. package/src/widgets/TextField.tsx +2 -2
  120. package/src/widgets/TimeField.tsx +2 -2
  121. package/src/widgets/UrlField.tsx +2 -2
  122. package/src/widgets/UserField.tsx +2 -2
  123. package/src/widgets/VectorField.tsx +2 -2
  124. package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +66 -0
  125. package/src/widgets/__tests__/FilterConditionField.operators.test.ts +142 -0
  126. package/src/widgets/file-value.test.ts +230 -0
  127. package/src/widgets/file-value.ts +246 -0
  128. package/src/widgets/types.ts +129 -6
  129. package/src/widgets/useFieldTranslation.ts +30 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,631 @@
1
1
  # @object-ui/fields
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 785b8a5: `FieldWidgetComponentProps` stops claiming to have every key (objectui#3221).
8
+
9
+ **Breaking for widget authors**: the exported `FieldWidgetComponentProps` no
10
+ longer ends in `[key: string]: any`. FROM: any prop name at all type-checked and
11
+ read as `any`. TO: the type declares a closed set — the controlled-input
12
+ contract (`value` / `onChange` / `field` / `readonly` / `disabled` /
13
+ `className` / `errorMessage` / `onUploadingChange`), the host plumbing a
14
+ renderer forwards (`schema`, `dataSource`, `dependentValues`, `dependsOn`,
15
+ `emptyHint`, `compact`, `onSelectRecord`, `onCreateNew`), and DOM pass-through
16
+ (`id`, `name`, `autoFocus`, `tabIndex`, `onBlur`/`onFocus`/`onClick`, every
17
+ `aria-*`, and `data-*` via a template-literal key). A custom widget reading
18
+ anything else now fails `tsc`; the fix is to read it off `field` (the metadata)
19
+ or to add the key here with its producer named.
20
+
21
+ Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
22
+ tracks `@objectstack`, so breaking changes of our own ship as minor with the
23
+ semantics spelled out (see AGENTS.md §版本号策略). The practical blast radius is
24
+ small: every call site in this monorepo — `plugin-detail`'s inline editor,
25
+ `plugin-grid`'s cell editor, `app-shell`'s metadata inspectors, the form
26
+ renderer — compiles unchanged, because the closed set was derived from them.
27
+
28
+ Why it mattered: an index signature is the objectstack#4075 mechanism — **a type
29
+ that claims to have every key can never be reported as missing one**. Three
30
+ things followed, and all three are now fixed:
31
+
32
+ - `props.required` and `props.error`, both declared by the spec's
33
+ `FieldWidgetPropsSchema` and both absent here, were legal reads typed `any`
34
+ and `undefined` at runtime forever. They are compile errors now, which is what
35
+ makes the `error` / `errorMessage` divergence (objectui#3222) decidable by the
36
+ compiler instead of by a symbol guard. This change deliberately does **not**
37
+ resolve that divergence — only make it visible.
38
+ - A misspelled prop (`readOnly` for `readonly`, `onchange` for `onChange`)
39
+ compiled and silently did nothing.
40
+ - Any structural / parity comparison against the type was useless _in
41
+ principle_, which is why objectui#3161's batch-7 symbol guard was the only
42
+ detector that could see the collision at all.
43
+
44
+ Also cleaned up inside the package: ~20 `(props as any).x` reads of keys the
45
+ type now declares (`compact`, `dataSource`, `disabled`, `name`, `id`,
46
+ `onCreateNew`, `onSelectRecord`, `contextRecord`, `dependentValues`) read
47
+ through the type instead — leaving them would have kept the "a typo compiles"
48
+ half of the defect alive at exactly the sites that matter. The three batch-7
49
+ tripwires that existed to go red on this change
50
+ (`_IndexSignatureStillThere` / `_RequiredSilentlyReadsAsAny` /
51
+ `_ErrorSilentlyReadsAsAny`) are replaced by their inverse, so re-widening the
52
+ type fails a test rather than passing one.
53
+
54
+ - 4a51e77: Stop declaring 14 symbols across ten packages under names `@objectstack/spec`
55
+ owns (objectui#3161, objectstack#4115 batch 7 — the long tail, one or two
56
+ entries per package). All ten packages leave the ledger, which drops from 17
57
+ collisions across 11 packages to 3 across 1.
58
+
59
+ **Renamed exports** — in every case the spec exports the same name for a
60
+ _different_ thing, so the old name was a mis-description rather than a dialect:
61
+
62
+ | package | was | now | what the spec's same-named export is |
63
+ | :------------------------- | :--------------------------------- | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------- |
64
+ | `@object-ui/fields` | `FieldWidgetProps` | `FieldWidgetComponentProps` | the DECLARED field-widget plugin props contract (a zod object; `field.type` is the `FieldType` enum, `readonly`/`required` carry defaults) |
65
+ | `@object-ui/layout` | `PageHeaderProps` | `PageHeaderComponentProps` | the authored `page:header` node — a zod schema of `title`, `subtitle`, an icon NAME, `breadcrumb`, `actions: string[]` |
66
+ | `@object-ui/layout` | `Page` | `PageNodeRenderer` | the authored page metadata DOCUMENT (`name`, `label`, `type`, `regions`) |
67
+ | `@object-ui/plugin-detail` | `ObjectFieldLike` | `ObjectDefFieldLike` | the i18n duck type `translateObject` walks (`help`/`description`, plus `[key: string]: any`) |
68
+ | `@object-ui/plugin-grid` | `ColumnSummaryConfig` | `ColumnSummarySetting` | the OBJECT form of `ListColumn.summary` **only** — the local one was the whole union, shorthand included |
69
+ | `@object-ui/plugin-grid` | `isMultiValueField` | `hasMultiValueShape` | the spec's classifier, which requires a def with a `type`; the local one is called with `undefined` |
70
+ | `@object-ui/collaboration` | `RealtimeConfig` | `RealtimeSubscriptionConfig` | the app's realtime DECLARATION (`enabled`, `transport`, `subscriptions[]`) |
71
+ | `@object-ui/plugin-charts` | `ChartConfig` | `ChartContainerConfig` | the authored chart document (`type`, `xAxis`, `series`, `showLegend`, …) |
72
+ | `@object-ui/plugin-form` | `FormSection` / `FormSectionProps` | `FormSectionContainer` / `FormSectionContainerProps` | the authored form-section metadata (`name`, `pane`, `visibleWhen`, `fields`) |
73
+ | `@object-ui/providers` | `Theme` | `ThemePreference` | a whole theme DOCUMENT (`name`, `label`, `colors`, `typography`) |
74
+ | `@object-ui/runner` | `App` (default export) | `RunnerApp` | the authored application metadata type **and** the `App.create()` builder |
75
+ | `@object-ui/sdui-parser` | `ValidationResult` | `ManifestValidationResult` | plugin-manifest validation (`{ valid, errors?, warnings? }`), exported from both `kernel` and `contracts` |
76
+
77
+ `ManifestValidationResult` follows the `<what was validated>Validation<Error|Result>`
78
+ convention registered on objectstack#4115 (`@object-ui/core` took
79
+ `SchemaNodeValidationResult` in batch 4). `PageHeaderComponentProps` deliberately
80
+ reuses the name `@object-ui/app-shell` already chose for its own header props in
81
+ batch 3, so one concept does not acquire two dialect names one package apart.
82
+
83
+ **Now derived from the spec instead of hand-written:**
84
+
85
+ - `@object-ui/fields` — `isFileIdToken` is re-exported from
86
+ `@objectstack/spec/data`. The local copy was character-for-character identical
87
+ to the spec's function while its comment said it "mirrors" it, so every
88
+ behaviour test passed and only reference identity could tell the two apart.
89
+ The regex is a wire decision: widening it server-side while a copy here kept
90
+ the old bound would make every new id read as "not a reference", and the
91
+ widget would submit the legacy inline blob to a backend expecting a reference.
92
+ - `@object-ui/plugin-detail` — `FeedFilterMode` is re-exported from
93
+ `@objectstack/spec/data`, in a file that already imported the sibling
94
+ `FeedItemType` from the spec.
95
+ - `@object-ui/plugin-grid` — the eleven-member aggregation union is now the
96
+ spec's `ColumnSummary` enum, so the total `Record<ColumnSummaryType, string>`
97
+ label map turns a member the spec adds into a compile error instead of a
98
+ blank footer cell. `ColumnSummarySetting` is `NonNullable<ListColumn['summary']>`,
99
+ i.e. whatever forms the spec itself accepts. `hasMultiValueShape` delegates to
100
+ the spec's `isMultiValueField` rather than re-deriving it from
101
+ `MULTI_OPTION_TYPES` / `MULTI_CAPABLE_TYPES`.
102
+ - `@object-ui/providers` — `ThemePreference` is the spec's `ThemeMode` union
103
+ plus the one legacy `'system'` spelling this provider still honours for stored
104
+ preferences, read off the schema's own `_zod` carrier so the package takes no
105
+ zod dependency.
106
+
107
+ `@objectstack/spec` moves from `devDependencies` to `dependencies` in
108
+ `@object-ui/fields` (it re-exports a runtime function) and `@object-ui/providers`
109
+ (its public `.d.ts` now references the spec).
110
+
111
+ Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
112
+ tracks `@objectstack`, so breaking changes of our own ship as minor with the
113
+ semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
114
+ all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
115
+
116
+ ### Patch Changes
117
+
118
+ - a8ad6c0: A required boolean must be savable in its UNCHECKED state — `false` and `0` are values.
119
+
120
+ Reported against an AI-built task tracker whose 任务 object has a required
121
+ `是否完成` boolean: the create form showed the switch OFF, answered "是否完成不能
122
+ 为空", and saved instantly once the switch was turned ON. The app could only ever
123
+ create ALREADY-DONE tasks — the one state the control shows by default was the
124
+ one value it refused to save (cloud#972).
125
+
126
+ Two defects stacked, and either alone is enough to break it:
127
+
128
+ **The `required` verdict read truthiness, not presence.** `@objectstack/spec`
129
+ FieldSchema.required (ADR-0113) is "an insert must provide a NON-NULL value",
130
+ and objectql's record validator implements exactly that. react-hook-form's
131
+ built-in rule instead fails whenever `isBoolean(value) && !value` — its
132
+ accept-the-terms checkbox heritage — silently redefining every required boolean
133
+ as "must be TRUE", including a select whose chosen option value is `false`. It
134
+ also disagreed the other way, letting a whitespace-only string through for the
135
+ server to reject with a 400. The form renderer no longer hands RHF its own
136
+ `required`: the check is now a `validate` entry keyed `required` (so the error
137
+ still surfaces as `type: 'required'`, which the conditional-required cleanup
138
+ keys on) backed by a new shared `isMissingForRequired` in `@object-ui/core`, a
139
+ deliberate mirror of objectql `record-validator.isMissing` — `undefined`,
140
+ `null`, blank-after-trim string, empty array. Deleting the inherited rule also
141
+ stops a `required` that rode in on `validation` from outliving a `requiredWhen`
142
+ that resolved to FALSE.
143
+
144
+ **A boolean field held `undefined` while displaying "off".** A two-state control
145
+ has no third state, but a field with no entry in `defaultValues` rendered an OFF
146
+ switch backed by nothing: the create payload omitted the column (it lands null,
147
+ which reads as unchecked but isn't) and the presence check above would still
148
+ refuse it. The form renderer now folds `false` into `defaultValues` for every
149
+ boolean-widget field the caller left unset — in `defaultValues` itself, not
150
+ per-Controller, because that object is also the dirty-check baseline and what
151
+ the defaults-reset window replays. Every surface gets it, including the
152
+ modal/drawer create dialogs that start from a bare `{}`. An authored default
153
+ (or a loaded record, `null` included) still wins.
154
+
155
+ `WizardForm`'s cross-step gate had its own copy of the empty-value predicate; it
156
+ now imports the shared one so it cannot drift from the per-field verdict. And
157
+ the field-demo renderer read `schema.defaultValue || schema.value`, throwing
158
+ away an authored default of `false` / `0` / `''` — same falsy-as-empty class,
159
+ now `??`.
160
+
161
+ Verified end to end on a local stack against the exact metadata shape
162
+ `apply_blueprint` materializes (`{ type: 'boolean', required: true }`, no
163
+ default): a 是否完成 = 否 task with 工时 = 0 now creates and persists as
164
+ `{ hours: 0, is_done: false }`, turning the switch on still stores `true`, and a
165
+ blank required text is still refused.
166
+
167
+ - Updated dependencies [4ae0ac4]
168
+ - Updated dependencies [696e3c1]
169
+ - Updated dependencies [bca45cc]
170
+ - Updated dependencies [a889e31]
171
+ - Updated dependencies [09d30a4]
172
+ - Updated dependencies [4bf612c]
173
+ - Updated dependencies [335041c]
174
+ - Updated dependencies [b414983]
175
+ - Updated dependencies [256f8cc]
176
+ - Updated dependencies [d9668a7]
177
+ - Updated dependencies [4b470b9]
178
+ - Updated dependencies [cb82705]
179
+ - Updated dependencies [f572849]
180
+ - Updated dependencies [4a51e77]
181
+ - Updated dependencies [f6e8d78]
182
+ - Updated dependencies [ea96284]
183
+ - Updated dependencies [d3584c6]
184
+ - Updated dependencies [a8ad6c0]
185
+ - Updated dependencies [444457c]
186
+ - Updated dependencies [850033c]
187
+ - Updated dependencies [022e4c3]
188
+ - Updated dependencies [009e25d]
189
+ - Updated dependencies [726b89c]
190
+ - @object-ui/types@17.2.0
191
+ - @object-ui/components@17.2.0
192
+ - @object-ui/core@17.2.0
193
+ - @object-ui/react@17.2.0
194
+ - @object-ui/i18n@17.2.0
195
+ - @object-ui/providers@17.2.0
196
+
197
+ ## 17.1.0
198
+
199
+ ### Minor Changes
200
+
201
+ - f8a95e5: fix(fields): the sharing-criteria builder stops calling an empty criteria "All records" (objectstack#3896)
202
+
203
+ `FilterConditionField` renders `sys_sharing_rule.criteria_json`. With no
204
+ criteria it displayed **"All records"**, and `filterGroupToMongo` carried a
205
+ matching `// empty = match all` comment. That was describing a bug as a
206
+ feature: a sharing rule with no predicate was stored as `criteria_json: null`
207
+ and evaluated as `find(object, { filter: {} })` under the system context —
208
+ every record of the object, granted to the recipient. `SharingRuleSchema` had
209
+ always forbidden the shape ("never seeded as a permissive match-all",
210
+ ADR-0049); the REST and data-API entries just never checked.
211
+
212
+ objectstack#3896 closes those entries: the server now refuses to save a rule
213
+ whose criteria would match everything, and one already stored shares nothing.
214
+ This is the renderer catching up.
215
+
216
+ - **The empty read-only state now says the rule shares nothing**, in
217
+ `destructive` styling — key renamed `fields.filterCondition.allRecords` →
218
+ `fields.filterCondition.noCriteria`, retranslated across all ten locales.
219
+ Nothing else read the old key.
220
+ - **A new `fields.filterCondition.criteriaRequired` hint** renders under the
221
+ builder (and the JSON editor) while the criteria is empty. The server's
222
+ rejection is precise but only arrives as a toast _after_ Save; this says it
223
+ while the admin is still looking at the empty builder.
224
+ - **`isMatchAllCriteria` is exported** — a client-side mirror of the server
225
+ predicate covering `{}`, `[]`, and the vacuous combinators (`{ $and: [] }`,
226
+ `{ $or: [{}] }`), conservative in the same direction. The server stays
227
+ authoritative; this only decides whether to show the hint.
228
+
229
+ Unparsable JSON keeps its own `invalidJson` message and does **not** also
230
+ collect the empty-criteria hint.
231
+
232
+ Note for anyone wiring this end-to-end: the Criteria field is not marked
233
+ `required` in the object metadata, deliberately — `sys_sharing_rule.criteria_json`
234
+ is nullable in deployed tenants, so `required: true` would only produce a
235
+ destructive `NOT NULL` migration that those nulls block. The invariant lives in
236
+ the server's write guards; this change makes the UI stop contradicting it.
237
+
238
+ ### Patch Changes
239
+
240
+ - aecc934: fix(fields): PeoplePicker's keyboard cursor can no longer be eaten by a late reset
241
+
242
+ The cursor reset on new results lived in a `useEffect`. Effects flush
243
+ asynchronously after the render that delivered the records — so a reset queued
244
+ by their arrival could land AFTER a subsequent ArrowDown and wipe the just-set
245
+ cursor. That was the residual ArrowDown→Enter flake in
246
+ `PeoplePicker.test.tsx` (the earlier signature-keyed fix closed the
247
+ too-often resets, not the too-late one), and a real fast-fingers UX bug: rows
248
+ appear, the user presses ArrowDown, the highlight vanishes.
249
+
250
+ The reset now runs in the render phase (the "adjusting state during render"
251
+ pattern), in the same render that shows the new rows — by the time a row is
252
+ visible, the reset has already happened, so it can never race a keypress.
253
+ Semantics unchanged and now pinned by a test: a replaced result set does not
254
+ inherit the previous set's cursor.
255
+
256
+ - 7f23cd0: fix(form): a numeric/boolean select option survives selection with its type intact — #3090
257
+
258
+ `SelectOptionSchema.value` has accepted `string | number | boolean` for as
259
+ long as it has existed, but the Radix controls underneath speak strings:
260
+ picking `{ value: 2 }` silently submitted `"2"` — a wrong-typed write into a
261
+ number field that nothing on the client ever reported. (Display half-worked:
262
+ a numeric default matched its numeric item; only SELECTION morphed the type.)
263
+
264
+ The renderers now stringify on the way into the control and map the selection
265
+ back to the AUTHORED option value on the way out (`matchOptionValue`), across
266
+ the in-form select, the standalone `type: 'select'` component, and the
267
+ standalone `type: 'radio-group'` component. The TS types stop lying to match:
268
+ `SelectOption.value` / `RadioOption.value` and the corresponding
269
+ `value`/`defaultValue`/`onChange` channels widen to what the zod schemas
270
+ always accepted — a call site treating `option.value` as `string` is now a
271
+ compile error pointing at a real latent crash, not a false comfort.
272
+
273
+ The ripple the widening named, handled at each boundary: `@object-ui/core`'s
274
+ `OptionLike.value` widens (the option engines compare by identity, so values
275
+ flow opaquely; the option-lint's CEL-literal domain stringifies at its
276
+ boundary), and the multi-value field widgets (checkboxes / multiselect /
277
+ radio) stringify at theirs — multi-value fields store string arrays.
278
+
279
+ Round-trip pinned by real Radix interactions in jsdom: the in-form select
280
+ submits `2` (number), the standalone select hands its handler `false`
281
+ (boolean).
282
+
283
+ - Updated dependencies [62311b6]
284
+ - Updated dependencies [fc0272a]
285
+ - Updated dependencies [9e7349e]
286
+ - Updated dependencies [8864971]
287
+ - Updated dependencies [1cf0de7]
288
+ - Updated dependencies [752e18f]
289
+ - Updated dependencies [c785740]
290
+ - Updated dependencies [b41f401]
291
+ - Updated dependencies [19e9fa0]
292
+ - Updated dependencies [d61efd1]
293
+ - Updated dependencies [95b7214]
294
+ - Updated dependencies [7d9734d]
295
+ - Updated dependencies [6ae818e]
296
+ - Updated dependencies [9eb932b]
297
+ - Updated dependencies [746dd00]
298
+ - Updated dependencies [aebfa4f]
299
+ - Updated dependencies [38ca8be]
300
+ - Updated dependencies [3cb9646]
301
+ - Updated dependencies [68ef584]
302
+ - Updated dependencies [4952edf]
303
+ - Updated dependencies [7f0252e]
304
+ - Updated dependencies [c4d7b20]
305
+ - Updated dependencies [c769d3d]
306
+ - Updated dependencies [7639a61]
307
+ - Updated dependencies [94e63ef]
308
+ - Updated dependencies [c735bf7]
309
+ - Updated dependencies [02aef0c]
310
+ - Updated dependencies [6f29aa5]
311
+ - Updated dependencies [d21794c]
312
+ - Updated dependencies [c4db402]
313
+ - Updated dependencies [5319bf1]
314
+ - Updated dependencies [49e5671]
315
+ - Updated dependencies [9a04d25]
316
+ - Updated dependencies [b5b97e2]
317
+ - Updated dependencies [f59f2c1]
318
+ - Updated dependencies [07de839]
319
+ - Updated dependencies [2a40b5e]
320
+ - Updated dependencies [df613fa]
321
+ - Updated dependencies [4874117]
322
+ - Updated dependencies [ad0183a]
323
+ - Updated dependencies [ce08d55]
324
+ - Updated dependencies [eb4b740]
325
+ - Updated dependencies [5b084eb]
326
+ - Updated dependencies [aa1240a]
327
+ - Updated dependencies [2374a49]
328
+ - Updated dependencies [390c071]
329
+ - Updated dependencies [d10f526]
330
+ - Updated dependencies [2d5d594]
331
+ - Updated dependencies [ea7f477]
332
+ - Updated dependencies [379728f]
333
+ - Updated dependencies [7f23cd0]
334
+ - Updated dependencies [0ded602]
335
+ - Updated dependencies [24e0e0a]
336
+ - Updated dependencies [f8a95e5]
337
+ - Updated dependencies [3a6cf24]
338
+ - Updated dependencies [aa35561]
339
+ - Updated dependencies [03bd53b]
340
+ - Updated dependencies [3c1f321]
341
+ - Updated dependencies [a045a32]
342
+ - Updated dependencies [912496d]
343
+ - Updated dependencies [80edbd4]
344
+ - Updated dependencies [9867281]
345
+ - @object-ui/core@17.1.0
346
+ - @object-ui/components@17.1.0
347
+ - @object-ui/react@17.1.0
348
+ - @object-ui/types@17.1.0
349
+ - @object-ui/i18n@17.1.0
350
+ - @object-ui/providers@17.1.0
351
+
352
+ ## 17.0.0
353
+
354
+ ### Minor Changes
355
+
356
+ - 697cda4: feat(fields): adopt the file-as-reference value shape (ObjectStack ADR-0104 D3 wave 2)
357
+
358
+ A `file`/`image` field value now reaches the UI in one of three forms, and the
359
+ rules for reading them live in one place — `@object-ui/fields`' new
360
+ `file-value` module — instead of being re-derived in each widget:
361
+
362
+ 1. **Reference** — a bare `sys_file` id string, what the backend stores once
363
+ file-as-reference is adopted.
364
+ 2. **Expanded** — `{ id, name, size, mimeType, url }`, what the read path
365
+ returns after resolving a reference.
366
+ 3. **Legacy inline blob** — `{ file_id?, name, original_name, size, mime_type,
367
+ url }`, the pre-reference shape this package used to build itself.
368
+
369
+ **The casing split is the bug this fixes.** The expanded form carries
370
+ `mimeType`; the legacy blob carries `mime_type`. `FileField`, `FileCell` and
371
+ `ImageField` all read only `mime_type`, so the moment a backend starts returning
372
+ the expanded form they stop recognising images — thumbnails silently degrade to
373
+ a generic file icon, with nothing pointing at a value shape as the cause.
374
+ `readFileValue()` accepts both.
375
+
376
+ **Uploads now submit the reference form** — the bare `sys_file` id — when the
377
+ upload adapter surfaced one, falling back to the legacy blob when it did not
378
+ (the object-URL fallback adapter, or a backend predating file-as-reference). The
379
+ same build therefore works against both. Action params already POSTed a bare
380
+ fileId; record field values now use the same contract, and
381
+ `serializeParamValues` shares the `fileIdOf()` extractor so the two surfaces
382
+ cannot drift on what counts as an id.
383
+
384
+ Because a bare id carries no name or URL, each widget remembers the display
385
+ details of files it just uploaded, keyed by id, so an upload renders immediately
386
+ rather than showing a bare token until the next read enriches it.
387
+
388
+ ### Patch Changes
389
+
390
+ - 4b1ed7d: feat(app-shell): approval approver values become record lookups (framework #3508)
391
+
392
+ - The flow designer's approver `Value` cell now sources directory kinds from DATA
393
+ records instead of the metadata registry: `user` / `team` / `department` / `position`
394
+ render a single-select record lookup (`LookupField` over `sys_user` / `sys_team` /
395
+ `sys_business_unit` / `sys_position` via the DataSource adapter), with a manual-entry
396
+ escape hatch and a plain free-text fallback when no adapter is available (offline
397
+ preview). `position` commits the machine name; the others commit the record id —
398
+ matching the approval engine's resolution semantics.
399
+ - `org-membership-level` is now a strict select (owner/admin/member); a stored
400
+ out-of-enum value renders flagged instead of being blanked.
401
+ - `manager` renders as an auto-resolved (disabled) cell; `queue` is no longer offered
402
+ for new approver rows and stored queue rows carry a "not supported by the runtime"
403
+ warning.
404
+ - `@object-ui/fields`: `LookupField` hydrates the selected label through `id_field`
405
+ when it is not the primary id (e.g. `id_field: 'name'`), instead of always calling
406
+ `findOne` with the primary id.
407
+
408
+ - 3e886eb: fix(i18n): localize FileField upload widget + approvals snapshot field labels
409
+
410
+ - `FileField` (the shared upload widget) hard-coded every visible string
411
+ ("Drag & drop files here", "or click to browse", "Take photo", "Uploading…",
412
+ size/upload validation messages, …). They now route through
413
+ `useObjectTranslation` with new `fields.file.*` keys, translated across all
414
+ 10 locale bundles. This is why the approvals Approve/Reject dialog's
415
+ attachment dropzone was English in a Chinese console.
416
+ - The approvals inbox record-snapshot summary title-cased raw machine keys
417
+ instead of the target object's field labels. It now consumes the
418
+ server-sent `payload_labels` in `payloadSummary`/`decisionAmountEntry`,
419
+ falling back to the prettified key when absent; `approvalsApi`'s row type
420
+ gains `payload_labels`.
421
+
422
+ - faad45e: fix(fields): render `image` fields consistently and add click-to-zoom (#2836)
423
+
424
+ An `image` field rendered differently — and wrongly — on three surfaces:
425
+
426
+ - **Edit form showed broken thumbnails.** A record read back its `image` value
427
+ as a bare `sys_file` id (the reference form), but `readFileValue` returned an
428
+ id with no URL — the comment assumed the read path expands it, which the
429
+ edit-form data path does not. The result was `<img src="">`. `file-value` now
430
+ derives the stable download URL (`/api/v1/storage/files/:id`, which
431
+ 302-redirects to a signed URL and works directly as `<img src>`) for a bare
432
+ id or an id-only object, so every widget and cell renderer resolves one.
433
+ - **Inline edit leaked the raw storage URL.** `InlineFieldInput` had no branch
434
+ for file-backed types and fell through to a plain text input showing
435
+ `/api/v1/storage/files/…`. It now renders the same upload widgets the form
436
+ uses (`image`/`avatar`/`signature`/`file`/`video`/`audio`).
437
+ - **Hard-coded English.** `ImageField`'s upload/crop/remove/alt strings now go
438
+ through `t('fields.image.*')` (en + zh added).
439
+
440
+ Also adds an `ImageLightbox` — click a read-only thumbnail (detail or list cell)
441
+ to open a full-screen preview; multiple images get prev/next navigation, a
442
+ position counter and arrow-key support, a single image just the image. In a
443
+ grid cell the click is `stopPropagation`-guarded so enlarging doesn't also open
444
+ the row.
445
+
446
+ - 341bfb5: fix: read spec-canonical keys for dashboard header title and field length rules
447
+
448
+ Two naming-drift closeouts (framework#1878 / framework#1891):
449
+
450
+ - `DashboardRenderer` header now falls back to the spec-canonical `label` when
451
+ the legacy `title` is absent (mirrors the `DashboardGridLayout` fallback from
452
+ #2666) — a spec-compliant dashboard gets its header title.
453
+ - Field validation rules now read the spec-canonical camelCase
454
+ `minLength`/`maxLength` (what the server record-validator enforces) with the
455
+ legacy snake_case `min_length`/`max_length` kept as fallback — authored
456
+ length constraints reach the client form.
457
+
458
+ - 2f947e4: fix(page,field): consume the spec's `type`/`label`/`maxLength` keys (framework#1878 §3 naming-drift recheck)
459
+
460
+ Three forward-drifts where objectui read a different key than the spec
461
+ declares, so authoring the documented key silently no-oped:
462
+
463
+ - **page `type` → `pageType`** (app-shell + components): `PageSchema` declares
464
+ the page KIND as `type`, but `PageRenderer` reads `schema.pageType` and fell
465
+ back to `'record'` — and nothing mapped between them. Every non-record page
466
+ (`home`/`app`/`list`/`utility`) rendered with the record max-width, a wrong
467
+ `data-page-type` attribute, and a suppressed header. `PageView` now passes
468
+ `pageType` alongside the SchemaNode discriminator `type`.
469
+ - **page `label` → `title`** (components): `PageSchema.label` is required but the
470
+ region renderer read only `title`. Now dual-reads `title ?? label`, mirroring
471
+ the fallback `DashboardRenderer` already uses. Coupled with the above — the
472
+ header is gated on `pageType !== 'record'`, so both were needed for a title to
473
+ appear.
474
+ - **field `maxLength`/`minLength`** (plugin-form + fields): validation already
475
+ dual-read these, but `ObjectForm`'s HTML-attribute pass and `TextAreaField`
476
+ read `max_length` only, so a spec-authored `maxLength` gave no browser cap and
477
+ no character counter. Both now dual-read, matching `buildValidationRules`.
478
+
479
+ Verified in the browser against the showcase: `capability_map` (`type: 'home'`)
480
+ now renders `data-page-type="home"`, the `home` max-width and its page title;
481
+ record pages are unchanged.
482
+
483
+ - 53642d4: fix(core,fields): a string `$orderby` is a clause, not a character array — and localize the sharing-rule widgets (objectstack#3821)
484
+
485
+ **The recipient picker listed nothing, ever.** `QueryParams['$orderby']` was
486
+ typed as `Record | string[] | SortObject[]`, so `queryParamsToRecord` sent any
487
+ non-array value through `Object.entries`. Handed the clause string `'name asc'`
488
+ — which callers do build by hand — it walked the string index by index and
489
+ emitted `$orderby=0 n,1 a,2 m,3 e,4 ,5 a,6 s,7 c`. The server sorted by columns
490
+ that don't exist and every row was filtered out, so
491
+ `sys_sharing_rule.recipient_id` rendered "No matches" for every recipient type
492
+ and no sharing rule could be created from the Console. `ObjectGrid` builds the
493
+ same shape from a schema-level `sort` in three places, so grids with a string
494
+ sort silently showed an empty table.
495
+
496
+ A string `$orderby` is now passed through verbatim (the server's OData
497
+ normalizer has always parsed `'name asc'`), and the type admits `string`.
498
+ `RecipientPickerField` additionally switched to the structured
499
+ `{ name: 'asc' }` form so it can't regress this way against any data source.
500
+
501
+ **The three sharing-rule authoring widgets never had translations.**
502
+ `ObjectRefField`, `RecipientPickerField` and `FilterConditionField` hardcoded
503
+ their English copy — a Chinese Console showed "Select an object", "Select a
504
+ user", "Search…", "No matches", "Edit as JSON". They now go through
505
+ `useFieldTranslation` like every other widget, with keys added under `fields.*`
506
+ in all ten locales.
507
+
508
+ The recipient placeholder was the interesting one: it read
509
+ `` `Select a ${recipientType.replace(/_/g,' ')}` ``, interpolating the enum
510
+ value into an English sentence — a shape no locale can translate. It is now a
511
+ per-type key (`fields.recipient.selectUser`, `…selectBusinessUnit`, …), so
512
+ "选择业务单元" and "Select a business unit" no longer have to share a structure.
513
+
514
+ **Editing a rule silently dropped its recipient.** The picker resets the stored
515
+ id when `recipient_type` changes, because an id valid for a user is meaningless
516
+ for a team. It treated the edit form's `'' → 'user'` hydration as such a change:
517
+ opening any saved rule blanked the recipient, and saving persisted the blank.
518
+ Only a non-empty predecessor now counts as a type switch.
519
+
520
+ **Building a filter submitted the surrounding form.** None of `FilterBuilder`'s
521
+ controls declared `type="button"`, and a bare `<button>` inside a `<form>`
522
+ defaults to `type="submit"`. Adding, removing or clearing a condition therefore
523
+ submitted the sharing-rule dialog — firing validation mid-edit, and on an
524
+ already-valid form saving the record before the admin was done.
525
+
526
+ **A rejected write showed the user raw server diagnostics.** The form rendered
527
+ `error.message` verbatim, so a sharing / RLS denial reached the dialog and the
528
+ toast as `FORBIDDEN: insufficient privileges to update showcase_private_note
529
+ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and the
530
+ record id to whoever hit it. Permission failures now render localized copy
531
+ (`form.noPermissionToSave`, added in all ten locales), with the server text kept
532
+ on the console for debugging; other failures still show the server's message,
533
+ which is the useful part, and fall back to `form.submitFailed` when there is
534
+ none — replacing the previously hardcoded English "An error occurred during
535
+ submission".
536
+
537
+ **The detail header offered "Edit" on records the user may only read.** Object
538
+ permissions can't express "this one record is read-only" — a read-only sharing
539
+ grant sits inside an object the user may otherwise edit — so the header showed
540
+ the primary Edit CTA, opened the form, and let the user retype a field before
541
+ the server rejected the save. `DetailView` now gates Edit / Delete on the
542
+ object-level check AND on the explain engine's record-grained verdict
543
+ (`POST /api/v1/security/explain` with a `recordId`, ADR-0090 D6 / ADR-0095 C2 —
544
+ the same pipeline the enforcement middleware runs, so button and server cannot
545
+ disagree). Explaining oneself needs no special permission. The probe is one
546
+ cached request per record, skipped entirely when the object-level check already
547
+ says no, and **fails open** on every uncertainty — an unanswered hint must never
548
+ be the reason a permitted user cannot act; the server stays the authority
549
+ (ADR-0057 D10).
550
+
551
+ **A long option rendered straight past the combobox border.** `Combobox`'s
552
+ trigger pinned itself to the component's `w-[200px]` default while the fields
553
+ around it ran the full form column, and the selected label was a bare text child
554
+ of a flex button — flex items need `truncate` AND `min-w-0` to clip, and it had
555
+ neither. So "成员 (showcase_project_membership)" in the object picker overflowed
556
+ the control and collided with the field beside it. The label now truncates, the
557
+ trigger can shrink, the dropdown matches the trigger's width instead of a
558
+ hardcoded 200px (a widened combobox used to clip its own options), and the two
559
+ sharing-rule pickers ask for `w-full` so they line up with every other input.
560
+
561
+ Hardens `evaluatePermission` while there: a role config carrying only
562
+ `fieldPermissions` (no `actions`) made `check()` throw a TypeError that
563
+ propagated out of the render. A permission check must not be able to crash a
564
+ view.
565
+
566
+ Browser-verified against the framework showcase Console in Chinese: object /
567
+ criteria / recipient copy is fully localized, the recipient dropdown lists real
568
+ users, business units and positions, a saved rule reopens with its recipient and
569
+ criteria intact, editing the filter no longer submits, and a rule created
570
+ end-to-end stores a real record id rather than free text. The criteria authored
571
+ in the builder is honored by the evaluator: `{"pinned":true}` on an owner-private
572
+ object granted the recipient exactly the matching records and nothing else.
573
+
574
+ - Updated dependencies [7b21891]
575
+ - Updated dependencies [0b3be01]
576
+ - Updated dependencies [3c4d935]
577
+ - Updated dependencies [4b60d2d]
578
+ - Updated dependencies [952b978]
579
+ - Updated dependencies [de5e40c]
580
+ - Updated dependencies [1a03af6]
581
+ - Updated dependencies [3e886eb]
582
+ - Updated dependencies [cfc675e]
583
+ - Updated dependencies [20df08c]
584
+ - Updated dependencies [1767124]
585
+ - Updated dependencies [8ecf5a6]
586
+ - Updated dependencies [af705b9]
587
+ - Updated dependencies [0502a7c]
588
+ - Updated dependencies [7b35e4b]
589
+ - Updated dependencies [8fb1295]
590
+ - Updated dependencies [e16ed2d]
591
+ - Updated dependencies [c6fd752]
592
+ - Updated dependencies [f9bbddb]
593
+ - Updated dependencies [dfd3705]
594
+ - Updated dependencies [c77108c]
595
+ - Updated dependencies [2735de6]
596
+ - Updated dependencies [c19ac11]
597
+ - Updated dependencies [6dee2cb]
598
+ - Updated dependencies [e05f052]
599
+ - Updated dependencies [0502a7c]
600
+ - Updated dependencies [faad45e]
601
+ - Updated dependencies [09c6a17]
602
+ - Updated dependencies [c7cff19]
603
+ - Updated dependencies [ba73a02]
604
+ - Updated dependencies [cd09a7b]
605
+ - Updated dependencies [f1abf0e]
606
+ - Updated dependencies [f05b84e]
607
+ - Updated dependencies [9b4b952]
608
+ - Updated dependencies [2f947e4]
609
+ - Updated dependencies [7d46648]
610
+ - Updated dependencies [9b53d72]
611
+ - Updated dependencies [bb4aa25]
612
+ - Updated dependencies [75f1cdf]
613
+ - Updated dependencies [662bdf9]
614
+ - Updated dependencies [059a052]
615
+ - Updated dependencies [53642d4]
616
+ - Updated dependencies [8aae006]
617
+ - Updated dependencies [c6cfdf1]
618
+ - Updated dependencies [d147a13]
619
+ - Updated dependencies [c6aaed8]
620
+ - Updated dependencies [263f885]
621
+ - Updated dependencies [dc334da]
622
+ - @object-ui/components@17.0.0
623
+ - @object-ui/i18n@17.0.0
624
+ - @object-ui/react@17.0.0
625
+ - @object-ui/types@17.0.0
626
+ - @object-ui/core@17.0.0
627
+ - @object-ui/providers@17.0.0
628
+
3
629
  ## 16.1.0
4
630
 
5
631
  ### Minor Changes
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { FieldWidgetProps } from './widgets/types';
2
+ import { FieldWidgetComponentProps } from './widgets/types';
3
3
  /**
4
4
  * Form field types that are deliberately NOT given an inline editor — every
5
5
  * other form widget type must be in {@link EDIT_WIDGETS}. This pairing is
@@ -12,10 +12,17 @@ export declare const INLINE_EXCLUDED_FIELD_TYPES: Set<string>;
12
12
  export declare const DISCRETE_EDIT_TYPES: Set<string>;
13
13
  /** True when a field type has a dedicated in-place edit widget. */
14
14
  export declare function hasFieldEditWidget(type: string | undefined): boolean;
15
+ /**
16
+ * True when a field type is deliberately NOT inline-editable (alias-aware:
17
+ * `composite` resolves to the excluded `object`, `video` to `file`, …).
18
+ * Grid hosts must treat these as read-only cells — falling back to a plain
19
+ * text editor is exactly the corruption path the exclusion exists to close.
20
+ */
21
+ export declare function isInlineExcludedFieldType(type: string | undefined): boolean;
15
22
  /**
16
23
  * Render the dedicated edit widget for a field's type — the SAME control the
17
24
  * form uses — as a controlled `{ value, onChange, field }` input. Returns
18
25
  * `null` for types without a registered widget so the caller can fall back to
19
26
  * a plain editor.
20
27
  */
21
- export declare function FieldEditWidget({ field, value, onChange, readonly, }: FieldWidgetProps<any>): React.ReactElement | null;
28
+ export declare function FieldEditWidget({ field, value, onChange, readonly, }: FieldWidgetComponentProps<any>): React.ReactElement | null;
@@ -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 {};