@object-ui/fields 17.3.0 → 17.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1655 -0
- package/README.md +1 -12
- package/dist/FieldEditWidget.d.ts +23 -3
- package/dist/currency.d.ts +46 -0
- package/dist/field-type-alias.d.ts +35 -1
- package/dist/index.cjs +4 -4
- package/dist/index.css +651 -0
- package/dist/index.d.ts +474 -85
- package/dist/index.js +5418 -5023
- package/dist/widgets/AddressField.d.ts +4 -12
- package/dist/widgets/AutoNumberField.d.ts +1 -1
- package/dist/widgets/AvatarField.d.ts +1 -1
- package/dist/widgets/BooleanField.d.ts +1 -1
- package/dist/widgets/CapabilityMultiSelectField.d.ts +1 -1
- package/dist/widgets/CheckboxesField.d.ts +1 -1
- package/dist/widgets/CodeField.d.ts +1 -1
- package/dist/widgets/ColorField.d.ts +1 -1
- package/dist/widgets/CurrencyField.d.ts +1 -1
- package/dist/widgets/DateField.d.ts +1 -1
- package/dist/widgets/DateTimeField.d.ts +1 -1
- package/dist/widgets/EmailField.d.ts +1 -1
- package/dist/widgets/FileField.d.ts +18 -2
- package/dist/widgets/FilterConditionField.d.ts +28 -1
- package/dist/widgets/FormulaField.d.ts +1 -1
- package/dist/widgets/FullscreenFieldEditor.d.ts +49 -152
- package/dist/widgets/GeolocationField.d.ts +1 -1
- package/dist/widgets/GridField.d.ts +68 -6
- package/dist/widgets/ImageField.d.ts +1 -1
- package/dist/widgets/LocationField.d.ts +1 -1
- package/dist/widgets/LookupField.d.ts +1 -1
- package/dist/widgets/MasterDetailField.d.ts +1 -1
- package/dist/widgets/MultiSelectField.d.ts +1 -1
- package/dist/widgets/NumberField.d.ts +1 -1
- package/dist/widgets/ObjectField.d.ts +1 -1
- package/dist/widgets/ObjectRefField.d.ts +1 -1
- package/dist/widgets/OptionsEmptyState.d.ts +21 -1
- package/dist/widgets/PasswordField.d.ts +1 -1
- package/dist/widgets/PercentField.d.ts +1 -1
- package/dist/widgets/PhoneField.d.ts +1 -1
- package/dist/widgets/QRCodeField.d.ts +1 -1
- package/dist/widgets/RadioField.d.ts +1 -1
- package/dist/widgets/RatingField.d.ts +1 -1
- package/dist/widgets/RecipientPickerField.d.ts +1 -1
- package/dist/widgets/RecordPickerDialog.d.ts +34 -10
- package/dist/widgets/RichTextField.d.ts +37 -1
- package/dist/widgets/SelectField.d.ts +26 -6
- package/dist/widgets/SignatureField.d.ts +32 -2
- package/dist/widgets/SliderField.d.ts +37 -2
- package/dist/widgets/SummaryField.d.ts +1 -1
- package/dist/widgets/TagsField.d.ts +1 -1
- package/dist/widgets/TextAreaField.d.ts +6 -2
- package/dist/widgets/TextField.d.ts +1 -1
- package/dist/widgets/TimeField.d.ts +1 -1
- package/dist/widgets/UrlField.d.ts +1 -1
- package/dist/widgets/UserField.d.ts +1 -1
- package/dist/widgets/VectorField.d.ts +1 -1
- package/dist/widgets/_cell-renderer-bridge.d.ts +1 -1
- package/dist/widgets/address-format.d.ts +88 -0
- package/dist/widgets/file-size-guard.d.ts +41 -0
- package/dist/widgets/lookupColumnDisplay.d.ts +93 -0
- package/dist/widgets/nativeDateValue.d.ts +37 -0
- package/dist/widgets/richTextDisplay.d.ts +80 -0
- package/dist/widgets/toDomProps.d.ts +30 -10
- package/dist/widgets/toHostGroupProps.d.ts +189 -0
- package/dist/widgets/types.d.ts +8 -3
- package/dist/widgets/useRecordQuery.d.ts +7 -1
- package/package.json +17 -14
- package/dist/FieldEditWidget.test.d.ts +0 -8
- package/dist/__tests__/DateCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/NumberCellRenderer.test.d.ts +0 -0
- package/dist/__tests__/cell-truncation.test.d.ts +0 -0
- package/dist/__tests__/field-carrier-sdui.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayField.test.d.ts +0 -0
- package/dist/__tests__/lookupCellDisplayName.test.d.ts +0 -0
- package/dist/__tests__/lookupCellReferenceKey.test.d.ts +0 -0
- package/dist/__tests__/registry-collision.test.d.ts +0 -0
- package/dist/__tests__/resolveFieldCurrency.test.d.ts +0 -1
- package/dist/__tests__/spec-symbol-batch7.test.d.ts +0 -8
- package/dist/__tests__/validation-feedback.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-aria-invalid-registry-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-dom-leak-e2e.test.d.ts +0 -0
- package/dist/__tests__/widget-props-contract.test.d.ts +0 -8
- package/dist/coerce-safe-value.test.d.ts +0 -1
- package/dist/complex-widgets.test.d.ts +0 -1
- package/dist/datetime-widgets.test.d.ts +0 -1
- package/dist/field-type-coverage.test.d.ts +0 -1
- package/dist/standard-widgets.test.d.ts +0 -0
- package/dist/widgets/AddressField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/CapabilityMultiSelectField.test.d.ts +0 -0
- package/dist/widgets/CharacterCount.d.ts +0 -56
- package/dist/widgets/CheckboxesField.cascade.test.d.ts +0 -0
- package/dist/widgets/GeolocationField.uniqueIds.test.d.ts +0 -0
- package/dist/widgets/GridField.test.d.ts +0 -1
- package/dist/widgets/ImageLightbox.test.d.ts +0 -1
- package/dist/widgets/LookupField.dependsOn.test.d.ts +0 -8
- package/dist/widgets/LookupField.gateHintLabel.test.d.ts +0 -8
- package/dist/widgets/LookupField.hydration.test.d.ts +0 -1
- package/dist/widgets/LookupField.idField.test.d.ts +0 -1
- package/dist/widgets/MultiSelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.no-provider.test.d.ts +0 -0
- package/dist/widgets/OptionsEmptyState.test.d.ts +0 -0
- package/dist/widgets/PeoplePicker.test.d.ts +0 -8
- package/dist/widgets/PeoplePickerComponents.test.d.ts +0 -8
- package/dist/widgets/RadioField.cascade.test.d.ts +0 -0
- package/dist/widgets/RecipientPickerField.test.d.ts +0 -0
- package/dist/widgets/RecordPickerDialog.filterOptions.test.d.ts +0 -8
- package/dist/widgets/RecordPickerDialog.selectLabel.test.d.ts +0 -8
- package/dist/widgets/SelectField.cascade.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.no-provider.test.d.ts +0 -0
- package/dist/widgets/TagsField.placeholder.test.d.ts +0 -0
- package/dist/widgets/UserFieldPicker.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.matchAll.test.d.ts +0 -8
- package/dist/widgets/__tests__/FilterConditionField.operators.test.d.ts +0 -8
- package/dist/widgets/__tests__/FullscreenFieldEditor.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/FullscreenFieldEditor.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/RichTextField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.announcements.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.characterCount.no-provider.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.fullscreenDisabled.test.d.ts +0 -0
- package/dist/widgets/__tests__/TextAreaField.mobileFullscreen.test.d.ts +0 -0
- package/dist/widgets/deriveLookupColumns.test.d.ts +0 -1
- package/dist/widgets/file-value.test.d.ts +0 -1
- package/dist/widgets/personDisplay.test.d.ts +0 -8
- package/dist/widgets/recentLookups.test.d.ts +0 -1
- package/dist/widgets/useRecordQuery.test.d.ts +0 -8
- package/dist/widgets/useUploadingSignal.test.d.ts +0 -1
- package/src/FieldEditWidget.test.ts +0 -133
- package/src/FieldEditWidget.tsx +0 -191
- package/src/__tests__/DateCellRenderer.test.tsx +0 -73
- package/src/__tests__/NumberCellRenderer.test.tsx +0 -60
- package/src/__tests__/cell-truncation.test.tsx +0 -96
- package/src/__tests__/field-carrier-sdui.test.tsx +0 -169
- package/src/__tests__/lookupCellDisplayField.test.tsx +0 -107
- package/src/__tests__/lookupCellDisplayName.test.tsx +0 -119
- package/src/__tests__/lookupCellReferenceKey.test.tsx +0 -57
- package/src/__tests__/registry-collision.test.ts +0 -91
- package/src/__tests__/resolveFieldCurrency.test.ts +0 -20
- package/src/__tests__/spec-symbol-batch7.test.ts +0 -126
- package/src/__tests__/validation-feedback.test.tsx +0 -361
- package/src/__tests__/widget-aria-invalid-e2e.test.tsx +0 -265
- package/src/__tests__/widget-aria-invalid-registry-e2e.test.tsx +0 -338
- package/src/__tests__/widget-dom-leak-e2e.test.tsx +0 -598
- package/src/__tests__/widget-props-contract.test.tsx +0 -142
- package/src/coerce-safe-value.test.ts +0 -34
- package/src/complex-widgets.test.tsx +0 -908
- package/src/currency.ts +0 -14
- package/src/datetime-widgets.test.tsx +0 -93
- package/src/field-type-alias.ts +0 -100
- package/src/field-type-coverage.test.ts +0 -67
- package/src/index.tsx +0 -2503
- package/src/standard-widgets.test.tsx +0 -357
- package/src/widgets/AddressField.tsx +0 -132
- package/src/widgets/AddressField.uniqueIds.test.tsx +0 -92
- package/src/widgets/AutoNumberField.tsx +0 -18
- package/src/widgets/AvatarField.tsx +0 -117
- package/src/widgets/BooleanField.tsx +0 -52
- package/src/widgets/CapabilityMultiSelectField.test.tsx +0 -168
- package/src/widgets/CapabilityMultiSelectField.tsx +0 -229
- package/src/widgets/CharacterCount.tsx +0 -263
- package/src/widgets/CheckboxesField.cascade.test.tsx +0 -143
- package/src/widgets/CheckboxesField.tsx +0 -136
- package/src/widgets/CodeField.tsx +0 -41
- package/src/widgets/ColorField.tsx +0 -53
- package/src/widgets/CurrencyField.tsx +0 -95
- package/src/widgets/DateField.tsx +0 -31
- package/src/widgets/DateTimeField.tsx +0 -37
- package/src/widgets/EmailField.tsx +0 -37
- package/src/widgets/FileField.tsx +0 -494
- package/src/widgets/FilterConditionField.tsx +0 -454
- package/src/widgets/FormulaField.tsx +0 -33
- package/src/widgets/FullscreenFieldEditor.tsx +0 -271
- package/src/widgets/GeolocationField.tsx +0 -167
- package/src/widgets/GeolocationField.uniqueIds.test.tsx +0 -88
- package/src/widgets/GridField.test.tsx +0 -406
- package/src/widgets/GridField.tsx +0 -966
- package/src/widgets/ImageCropperDialog.tsx +0 -245
- package/src/widgets/ImageField.tsx +0 -255
- package/src/widgets/ImageLightbox.test.tsx +0 -52
- package/src/widgets/ImageLightbox.tsx +0 -108
- package/src/widgets/LocationField.tsx +0 -57
- package/src/widgets/LookupField.dependsOn.test.tsx +0 -233
- package/src/widgets/LookupField.gateHintLabel.test.tsx +0 -135
- package/src/widgets/LookupField.hydration.test.tsx +0 -136
- package/src/widgets/LookupField.idField.test.tsx +0 -61
- package/src/widgets/LookupField.tsx +0 -1291
- package/src/widgets/MarkdownContent.tsx +0 -19
- package/src/widgets/MasterDetailField.tsx +0 -131
- package/src/widgets/MultiSelectField.cascade.test.tsx +0 -143
- package/src/widgets/MultiSelectField.tsx +0 -142
- package/src/widgets/NumberField.tsx +0 -49
- package/src/widgets/ObjectField.tsx +0 -94
- package/src/widgets/ObjectRefField.tsx +0 -121
- package/src/widgets/OptionsEmptyState.no-provider.test.tsx +0 -88
- package/src/widgets/OptionsEmptyState.test.tsx +0 -169
- package/src/widgets/OptionsEmptyState.tsx +0 -83
- package/src/widgets/PasswordField.tsx +0 -49
- package/src/widgets/PeoplePicker.test.tsx +0 -360
- package/src/widgets/PeoplePicker.tsx +0 -579
- package/src/widgets/PeoplePickerComponents.test.tsx +0 -104
- package/src/widgets/PercentField.tsx +0 -95
- package/src/widgets/PersonRow.tsx +0 -117
- package/src/widgets/PhoneField.tsx +0 -37
- package/src/widgets/QRCodeField.tsx +0 -103
- package/src/widgets/RadioField.cascade.test.tsx +0 -141
- package/src/widgets/RadioField.tsx +0 -111
- package/src/widgets/RatingField.tsx +0 -81
- package/src/widgets/RecipientPickerField.test.tsx +0 -157
- package/src/widgets/RecipientPickerField.tsx +0 -188
- package/src/widgets/RecordPickerDialog.filterOptions.test.tsx +0 -611
- package/src/widgets/RecordPickerDialog.selectLabel.test.tsx +0 -112
- package/src/widgets/RecordPickerDialog.tsx +0 -1316
- package/src/widgets/RichTextField.tsx +0 -176
- package/src/widgets/SelectField.cascade.test.tsx +0 -138
- package/src/widgets/SelectField.tsx +0 -162
- package/src/widgets/SelectionTray.tsx +0 -110
- package/src/widgets/SignatureField.tsx +0 -140
- package/src/widgets/SliderField.tsx +0 -39
- package/src/widgets/SummaryField.tsx +0 -31
- package/src/widgets/TagsField.placeholder.no-provider.test.tsx +0 -68
- package/src/widgets/TagsField.placeholder.test.tsx +0 -95
- package/src/widgets/TagsField.tsx +0 -88
- package/src/widgets/TextAreaField.tsx +0 -213
- package/src/widgets/TextField.tsx +0 -45
- package/src/widgets/TimeField.tsx +0 -31
- package/src/widgets/UrlField.tsx +0 -46
- package/src/widgets/UserField.tsx +0 -58
- package/src/widgets/UserFieldPicker.test.tsx +0 -78
- package/src/widgets/VectorField.tsx +0 -29
- package/src/widgets/__tests__/FilterConditionField.matchAll.test.ts +0 -66
- package/src/widgets/__tests__/FilterConditionField.operators.test.ts +0 -142
- package/src/widgets/__tests__/FullscreenFieldEditor.i18n.test.tsx +0 -248
- package/src/widgets/__tests__/FullscreenFieldEditor.no-provider.test.tsx +0 -116
- package/src/widgets/__tests__/RichTextField.fullscreenDisabled.test.tsx +0 -244
- package/src/widgets/__tests__/RichTextField.mobileFullscreen.test.tsx +0 -195
- package/src/widgets/__tests__/TextAreaField.characterCount.announcements.test.tsx +0 -417
- package/src/widgets/__tests__/TextAreaField.characterCount.i18n.test.tsx +0 -287
- package/src/widgets/__tests__/TextAreaField.characterCount.no-provider.test.tsx +0 -160
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.i18n.test.tsx +0 -161
- package/src/widgets/__tests__/TextAreaField.fullscreenCharacterCount.test.tsx +0 -346
- package/src/widgets/__tests__/TextAreaField.fullscreenDisabled.test.tsx +0 -246
- package/src/widgets/__tests__/TextAreaField.mobileFullscreen.test.tsx +0 -175
- package/src/widgets/_cell-renderer-bridge.ts +0 -19
- package/src/widgets/deriveLookupColumns.test.ts +0 -112
- package/src/widgets/deriveLookupColumns.ts +0 -108
- package/src/widgets/file-value.test.ts +0 -230
- package/src/widgets/file-value.ts +0 -246
- package/src/widgets/openNativePicker.ts +0 -18
- package/src/widgets/personDisplay.test.ts +0 -113
- package/src/widgets/personDisplay.ts +0 -92
- package/src/widgets/recentLookups.test.ts +0 -45
- package/src/widgets/recentLookups.ts +0 -47
- package/src/widgets/toDomProps.ts +0 -175
- package/src/widgets/types.ts +0 -242
- package/src/widgets/useCascadingOptions.ts +0 -46
- package/src/widgets/useFieldTranslation.ts +0 -150
- package/src/widgets/useRecordQuery.test.ts +0 -174
- package/src/widgets/useRecordQuery.ts +0 -264
- package/src/widgets/useUploadingSignal.test.tsx +0 -62
- package/src/widgets/useUploadingSignal.ts +0 -20
- package/src/withFieldCarrier.tsx +0 -75
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,1643 @@
|
|
|
1
1
|
# @object-ui/fields
|
|
2
2
|
|
|
3
|
+
## 17.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a09bc33: fix(fields): render the option colour an author declared as an explicit hex, instead of quantizing it to nine palette families (objectui#5141)
|
|
8
|
+
|
|
9
|
+
`options[].color` accepts any hex, but the badge renderer answered a lossy
|
|
10
|
+
question with it: `hexToPaletteName` bucketed the value by hue into nine
|
|
11
|
+
families (`red`/`orange`/`yellow`/`green`/`blue`/`indigo`/`purple`/`pink`, plus
|
|
12
|
+
`gray` below 22% saturation), and `BADGE_COLOR_MAP` held exactly one class set
|
|
13
|
+
per family. Two tiers an author declared as distinct therefore rendered
|
|
14
|
+
byte-identical: `#2ecc71` ("in progress") and `#1e8449` ("completed") differ by
|
|
15
|
+
0.1 degree of hue, both landed in `green`, and both emitted
|
|
16
|
+
`bg-green-50 text-green-700 border-green-200`. Pressing the second colour darker
|
|
17
|
+
still changed nothing. End users could not tell the two states apart in a list.
|
|
18
|
+
|
|
19
|
+
`plugin-gantt` had already settled this class of conflict the other way —
|
|
20
|
+
*explicit colorField value (hex or semantic name) — metadata wins* — and
|
|
21
|
+
Studio's own option editor paints the author's swatch straight from the raw hex.
|
|
22
|
+
Badges were the odd surface out.
|
|
23
|
+
|
|
24
|
+
Now an explicitly declared hex is rendered as declared: the soft-pill surface,
|
|
25
|
+
label and border are derived from that hex rather than snapped to a family, for
|
|
26
|
+
both `appearance: 'badge'` and `appearance: 'dot'`.
|
|
27
|
+
|
|
28
|
+
Two properties the family maps gave us for free are kept deliberately:
|
|
29
|
+
|
|
30
|
+
- **The design system keeps control of theming.** The derived colours are
|
|
31
|
+
published as CSS custom properties and consumed by *static* Tailwind
|
|
32
|
+
utilities, so light and dark remain ordinary `dark:` variants
|
|
33
|
+
(`.dark\:bg-...:where(.dark, .dark *)` in the built sheet) rather than a
|
|
34
|
+
hard-coded inline background that would render identically in dark mode.
|
|
35
|
+
Tailwind cannot generate a class for a runtime value, so the custom property —
|
|
36
|
+
not the colour — has to be the dynamic part.
|
|
37
|
+
- **Contrast is pinned, not just colour identity.** The label is the lightness
|
|
38
|
+
along the declared hue nearest the declared one that still clears WCAG AA
|
|
39
|
+
(4.5:1) against the surface actually rendered. Authors can and do declare
|
|
40
|
+
colours that are unreadable under a label; honoring the declaration must not
|
|
41
|
+
turn legibility loose across every list view. Dots are held to 1.9:1 against
|
|
42
|
+
the row, the measured floor of the `-500` shades shipped today.
|
|
43
|
+
|
|
44
|
+
**What changes for an author relying on the current look:** every select/status
|
|
45
|
+
badge whose option colour is declared as a hex — which the renderer's own notes
|
|
46
|
+
describe as almost all of them — will render in that declared colour rather than
|
|
47
|
+
its palette family's fixed pill. Colours near a family's canonical shade look
|
|
48
|
+
much as before; a colour the author picked deliberately *away* from it (a deep
|
|
49
|
+
green, a muted red) now looks like what was written, and the pill's depth tracks
|
|
50
|
+
the declared lightness. Declarations that are not an explicit hex are untouched:
|
|
51
|
+
family names, the semantic value map and the deterministic hash fallback all
|
|
52
|
+
resolve exactly as they did, and `getSemanticColorName` still returns family
|
|
53
|
+
names, so the Gantt path is unaffected.
|
|
54
|
+
|
|
55
|
+
The badge classes exported by `getBadgeColorClasses` are unchanged, so callers
|
|
56
|
+
that consume only a class string (the grid's compact card view and group-header
|
|
57
|
+
pills, Kanban) keep today's quantized rendering until they adopt the new
|
|
58
|
+
`getBadgeHexAppearance` / `getDotHexAppearance` helpers.
|
|
59
|
+
- 167ec42: `ComponentMeta.labelling` grows a third value: `'control' | 'group' | 'display'`
|
|
60
|
+
(objectui#4857, ruled jointly with objectui#4871 as the single repo-wide vocabulary for
|
|
61
|
+
"how does a host learn what a widget will render"). `'display'` declares a widget whose
|
|
62
|
+
whole surface is a pure display in EVERY state — no focusable control, nothing a
|
|
63
|
+
`<label for>` could ever reach.
|
|
64
|
+
|
|
65
|
+
The form renderer answers the declaration with the objectui#4788 host container (field
|
|
66
|
+
id + `aria-labelledby` + `aria-describedby` + `role="group"`) in the editable state too;
|
|
67
|
+
the `readonly === true` arm keeps its exact #4788 semantics for undeclared widgets. The
|
|
68
|
+
display-only four (`formula` / `summary` / `auto_number` / `vector`) declare `'display'`
|
|
69
|
+
— on the real object-form path they arrive `disabled`, never `readonly` (a deliberate
|
|
70
|
+
distinction this change does not touch), so their visible labels pointed `for` at an id
|
|
71
|
+
no element carried and their help text had zero consumers in every editable form.
|
|
72
|
+
|
|
73
|
+
`grid` was re-measured before being classified: its only bare-config focusable is the
|
|
74
|
+
auxiliary "Add line" button (routing `for` there would have label clicks insert rows),
|
|
75
|
+
and every realistic config is a table of per-cell inputs — a composite. It declares
|
|
76
|
+
`labelling: 'group'` and its root container now consumes the host id, name and
|
|
77
|
+
description, exactly like `address` / `checkboxes`.
|
|
78
|
+
|
|
79
|
+
Companion registry gate: `FIELD_WIDGET_LABELLING` (exported) is a `Record` keyed by the
|
|
80
|
+
field-widget map's own literal key union, so registering a widget without deciding its
|
|
81
|
+
labelling is a compile error rather than a silent fall-through to the dangling-`for`
|
|
82
|
+
path, and the declaration test asserts the registered meta agrees with it key by key.
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- 69251bf: `AddressField` is translatable, shows no US example placeholders, and formats its readonly line in the reader's address order.
|
|
87
|
+
|
|
88
|
+
The five sub-labels ("Street Address", "City", "State / Province", "ZIP /
|
|
89
|
+
Postal Code", "Country") were English string literals with no i18n key. On a
|
|
90
|
+
non-English console every address field showed five English words in the middle
|
|
91
|
+
of an otherwise fully translated form, and an app had no way to reach them: the
|
|
92
|
+
parts are not fields on the object (`billing_address` is a single `address`
|
|
93
|
+
column), so a translation bundle had nothing to key on, there is no `subLabels`
|
|
94
|
+
property to declare, and the widget cannot be replaced from metadata. They now
|
|
95
|
+
resolve through `fields.address.street` / `.city` / `.state` / `.postalCode` /
|
|
96
|
+
`.country`, added to all ten locale packs. The `en` values are byte-identical to
|
|
97
|
+
the literals they replace, and `FIELD_DEFAULTS` carries the same five, so
|
|
98
|
+
English and provider-less rendering are unchanged.
|
|
99
|
+
|
|
100
|
+
The five input placeholders (`123 Main St`, `San Francisco`, `CA`, `94102`,
|
|
101
|
+
`United States`) are **removed** rather than keyed. They were untranslated and
|
|
102
|
+
US-specific — a zh/ja/ar user was shown an American address as the example of
|
|
103
|
+
what to type — and the right example is a function of the address's country,
|
|
104
|
+
not the reader's language, which no channel in the stored value can supply
|
|
105
|
+
today. Each box keeps the visible label that names it.
|
|
106
|
+
|
|
107
|
+
The readonly line's part order now follows the reader's display locale
|
|
108
|
+
(`useDisplayLocale()`): `zh`, `ja` and `ko` read largest-first (`Country, ZIP
|
|
109
|
+
State, City, Street`), every other locale keeps the unchanged small-to-large
|
|
110
|
+
order (`Street, City, State ZIP, Country`). The display cell renderer takes the
|
|
111
|
+
same locale through the same shared `formatAddress`, so a stored address reads
|
|
112
|
+
identically in a readonly form and in a grid cell.
|
|
113
|
+
- 0ae27f7: The form renderer's built-in `textarea` branch now honours a declared character cap the same way the registered `field:textarea` widget does.
|
|
114
|
+
|
|
115
|
+
One `maxLength` declaration produced two experiences. The registered path has
|
|
116
|
+
shipped four things since objectui#3406/#3408/#3417 — the native cap, visible
|
|
117
|
+
`{n}/{max}` digits, a description reached through `aria-describedby` so the
|
|
118
|
+
limit is announced on focus, and a threshold-gated debounced near-limit notice.
|
|
119
|
+
The built-in branch — the path standalone and embedded hosts take, the ones that
|
|
120
|
+
call no `registerAllFields()` — shipped a subset of one of them.
|
|
121
|
+
|
|
122
|
+
The accessibility half is the half that mattered: a screen-reader user on this
|
|
123
|
+
path learned the field's limit only as a validation error AFTER submitting. All
|
|
124
|
+
four affordances now render on both of the branch's surfaces (the inline control
|
|
125
|
+
and the fullscreen dialog), from the SAME `CharacterCount` component the widget
|
|
126
|
+
renders rather than a second copy of it.
|
|
127
|
+
|
|
128
|
+
Also fixed, and wider than the visible gap: the branch never READ the cap, it
|
|
129
|
+
only spread its leftover field props onto the element. A camelCase `maxLength`
|
|
130
|
+
therefore worked by coincidence — it names a real DOM attribute — while the
|
|
131
|
+
legacy `max_length` spelling, which the registered widget and all three
|
|
132
|
+
producers of a form field have dual-read since framework#1878 §3, landed as a
|
|
133
|
+
stray inert `max_length="…"` attribute and capped nothing at all. The branch now
|
|
134
|
+
resolves both spellings and keeps the non-attribute spelling off the DOM.
|
|
135
|
+
|
|
136
|
+
`CharacterCount` moved from `@object-ui/fields` to `@object-ui/components`, the
|
|
137
|
+
package both render paths may import, in the direction and for the reason
|
|
138
|
+
objectui#3398 measured for `FullscreenEditor`. It was internal to `fields` (never
|
|
139
|
+
exported from that package's barrel), so no published export changed; it is a
|
|
140
|
+
new export of `@object-ui/components`. Its copy moved with it onto the same
|
|
141
|
+
`fields.textarea.*` keys with byte-identical English defaults, so the ten locale
|
|
142
|
+
packs need no edit and provider-less rendering is unchanged.
|
|
143
|
+
- bbe8b86: The allow-list of option widgets that are fed the live record is now one exported constant, `CASCADE_OPTION_WIDGET_TYPES`, instead of three private copies.
|
|
144
|
+
|
|
145
|
+
`select` / `multiselect` / `radio` / `checkboxes` are the widgets whose OFFERED
|
|
146
|
+
option set is re-resolved against a record (per-option `visibleWhen`, plus the
|
|
147
|
+
`dependsOn` gate), so they are the widgets a surface must thread its live record
|
|
148
|
+
to. Three surfaces feed that one evaluator — the object form, the single-record
|
|
149
|
+
action dialog and the bulk action dialog — and until now each carried its own
|
|
150
|
+
private `new Set([...])` of the same four keys, with a comment in each asking the
|
|
151
|
+
next person to change all three together. Nothing could have reported them
|
|
152
|
+
drifting: every copy passed its own behavioural tests, and a divergence would
|
|
153
|
+
have shown up only as one surface silently disagreeing with another about what
|
|
154
|
+
"the record" is.
|
|
155
|
+
|
|
156
|
+
The set now lives in `@object-ui/core`, next to `resolveCascadingOptions` — the
|
|
157
|
+
evaluator that reads that record — because core is the one package all three
|
|
158
|
+
surfaces already depend on, and it is re-exported from `@object-ui/fields` next
|
|
159
|
+
to `resolveFormWidgetType`, whose output is the vocabulary the keys are written
|
|
160
|
+
in. Both are the same object, pinned by test; each consumer keeps its own
|
|
161
|
+
normalization (`normalizeFieldType` in the form, `resolveFormWidgetType` in the
|
|
162
|
+
dialogs), which agree on these four members.
|
|
163
|
+
|
|
164
|
+
No behaviour changes: the members are identical on all three surfaces, and the
|
|
165
|
+
existing pins for each surface still assert the same records reaching the same
|
|
166
|
+
widgets. The rationale that was repeated in the three copies — including the
|
|
167
|
+
note that the widget-hint picker family (`filter-condition`, `recipient-picker`,
|
|
168
|
+
the lookup family) reads a different sibling key off the same channel and is
|
|
169
|
+
deliberately NOT in this set — is now stated once, in the constant's own
|
|
170
|
+
documentation. Whether the action and bulk dialogs should ever feed those
|
|
171
|
+
pickers stays an open question (objectui#4771), unchanged by this convergence.
|
|
172
|
+
- 65e88e6: `@object-ui/fields` stops publishing its `src/` tree
|
|
173
|
+
|
|
174
|
+
The manifest's `files` array listed `src` alongside `dist`, so every published tarball carried all 173 source files — 97 of them `*.test.tsx` / `*.test.ts`. It has been that way since the file's first commit (`780a1b993`), where `files` was already `["dist", "src", "README.md"]` while `exports` named only `dist`, so the entry was never added for a consumer. objectui#4006 recorded this exact shape and did not act on it: its scope was the `*.test.d.ts` half the build program emitted into `dist`, and it noted in passing that the test sources were already in the tarball by this other route.
|
|
175
|
+
|
|
176
|
+
Nothing in the published surface reached those files, which is why no consumer changes in either direction. Measured on a cleanly rebuilt `dist`, all four ways in are closed: the `exports` map has two entries and both target `dist` (`.` resolves `types` / `import` / `require` to `./dist/index.d.ts` / `./dist/index.js` / `./dist/index.cjs`, and `./style.css` to `./dist/index.css`); `main` / `module` / `types` are `./dist/index.cjs`, `./dist/index.js`, `./dist/index.d.ts`; no deep import into this package exists anywhere in the repo or the docs — the one that used to, `@object-ui/fields/widgets/MarkdownContent`, was ruled out by objectui#4325 precisely because a package's surface is its index, and the `../fields/src` paths in sibling `vite.config.ts` files are workspace aliases resolved through `resolve()` against the source tree, which no `files` array shapes; and the tarball holds no sourcemap that could point back at `src`. That last one is the check that had to be measured rather than assumed, because this package emits its declarations through `vite-plugin-dts` rather than the `tsup` of objectui#4847 or the bare `tsc` of `@object-ui/types`: a clean rebuild writes 78 files into `dist`, of which zero are `.d.ts.map`, zero are `.js.map`, zero contain a `sourceMappingURL` comment and zero mention `../src`. `src/index.css` is an input to `scripts/build-css.mjs`, not an output anyone resolves; the sheet the `./style.css` export names is the built `dist/index.css`, which still ships.
|
|
177
|
+
|
|
178
|
+
`npm pack --dry-run` across the change, on the same `dist`:
|
|
179
|
+
|
|
180
|
+
| | before | after |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| entries | 255 | 82 |
|
|
183
|
+
| unpacked | 2265557 B | 841772 B |
|
|
184
|
+
| tarball | 629843 B | 252364 B |
|
|
185
|
+
|
|
186
|
+
173 files leave, none arrives, nothing outside `src/` moves, and every surviving entry is byte-identical apart from the edited `package.json` itself. The 173 are the 97 tests plus 76 implementation modules, whose published form remains the bundled `dist/index.js` / `dist/index.cjs` and the 75 declaration files beside them.
|
|
187
|
+
|
|
188
|
+
`@object-ui/types` keeps its `src` entry for now, and that is a different judgement rather than an omission: it builds with a bare `tsc` under a `declarationMap` / `sourceMap` config with no `inlineSources`, so its shipped `dist/*.d.ts.map` name `../src/*.ts` with no embedded content and dropping `src` there would leave published maps pointing at files the tarball no longer carries. That trade-off is filed as objectui#4851.
|
|
189
|
+
- 37f6844: FilterConditionField can author the spec's `$icontains` — case-insensitive contains is reachable from the filter UI.
|
|
190
|
+
|
|
191
|
+
`@objectstack/spec`'s `FieldOperatorsSchema` gained `$icontains` between
|
|
192
|
+
`17.0.0-rc.2` and `rc.5`, and every driver and evaluation face the platform
|
|
193
|
+
ships now executes it. `FilterConditionField` had no builder operator that could
|
|
194
|
+
author it, so the capability was unreachable from the sharing-rule criteria
|
|
195
|
+
builder and sat in that widget's parity test as an explicit `KNOWN_UNREACHABLE`
|
|
196
|
+
entry.
|
|
197
|
+
|
|
198
|
+
The FilterBuilder gains a `containsCaseInsensitive` operator ("Contains (ignore
|
|
199
|
+
case)", translated in all ten locale packs). `condToMongo` emits
|
|
200
|
+
`{ field: { $icontains: value } }` and `kvToCondition` reads it back, so a saved
|
|
201
|
+
criteria reopens in the visual builder instead of falling into the raw-JSON
|
|
202
|
+
editor. Today's `contains` is unchanged and still emits the case-SENSITIVE
|
|
203
|
+
`$contains`; whether it should have been case-insensitive all along is a product
|
|
204
|
+
question that stays open, and stored filter views keep meaning what they meant.
|
|
205
|
+
|
|
206
|
+
The fold is ASCII-only by contract — `café` does not match `CAFÉ`.
|
|
207
|
+
|
|
208
|
+
The new operator is **opt-in per consumer**: `FilterBuilder` takes an
|
|
209
|
+
`extraOperators` prop, and only `FilterConditionField` passes it. The one
|
|
210
|
+
dropdown feeds three at-rest dialects and only the MongoDB-style criteria this
|
|
211
|
+
widget writes can carry the operator — the spec's `VIEW_FILTER_OPERATORS` (saved
|
|
212
|
+
views) and `VALID_AST_OPERATORS` (the live grid's filter AST) have no
|
|
213
|
+
case-insensitive contains, so offering it there would author a filter those
|
|
214
|
+
paths cannot execute. Every other FilterBuilder is unchanged.
|
|
215
|
+
- 911ceaa: The fullscreen long-text dialog announces the field's validation state and carries the field's name
|
|
216
|
+
|
|
217
|
+
objectui#4824, objectui#4832.
|
|
218
|
+
|
|
219
|
+
`mobile.fullscreenLongText` is a shipped opt-in, and with it on the phone user
|
|
220
|
+
edits long text in this dialog and nowhere else. Measured on all three surfaces
|
|
221
|
+
that render the dialog — `TextAreaField`, `RichTextField`, and the form
|
|
222
|
+
renderer's built-in `textarea` branch — with the field genuinely invalid at that
|
|
223
|
+
moment:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
INLINE richtext aria-invalid= true
|
|
227
|
+
DIALOG richtext aria-invalid= false aria-describedby= null
|
|
228
|
+
INLINE textarea aria-invalid= true
|
|
229
|
+
DIALOG textarea aria-invalid= null aria-describedby= null
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
and the accessible name of every dialog control empty, against `F` on every
|
|
233
|
+
inline one. The rich-text row is the sharp half: the dialog was not silent about
|
|
234
|
+
the failure, it was announcing the OPPOSITE of the inline control for the same
|
|
235
|
+
field at the same moment, because `RichTextEditorSurface` computed
|
|
236
|
+
`aria-invalid={!!error}` from an `error` prop the dialog rendering never
|
|
237
|
+
received. 3 surfaces, 3 broken, one cause: the dialog's control is built from
|
|
238
|
+
scratch by the host, so none of the wiring the inline control gets from the form
|
|
239
|
+
renderer reaches it.
|
|
240
|
+
|
|
241
|
+
**Answered once, in the primitive.** `FullscreenEditor` now takes the field's
|
|
242
|
+
`error` and owns what the dialog does with it: it renders the message in a
|
|
243
|
+
dialog-local node, and hands `children` a required fourth argument — a
|
|
244
|
+
spreadable set of DOM attributes — carrying `aria-labelledby` (the dialog
|
|
245
|
+
title's text, i.e. the field label #3393 already put there), `aria-invalid`, and
|
|
246
|
+
`aria-errormessage` naming that node. The host spreads it; the host never learns
|
|
247
|
+
an id, so it cannot name the wrong node, cannot compose the attributes subtly
|
|
248
|
+
wrong, and cannot compute its own `aria-invalid` from a prop it forgot to plumb.
|
|
249
|
+
Three hosts hand-answering this is the shape that produced three identical
|
|
250
|
+
holes.
|
|
251
|
+
|
|
252
|
+
**On objectui#3222's "the text belongs to `FormMessage`".** The maintainer's
|
|
253
|
+
ruling of 2026-08-16 restates that rule as what it was always protecting —
|
|
254
|
+
only one copy of the error text is in the accessibility tree at any moment —
|
|
255
|
+
which the dialog-local node satisfies: it exists only while the dialog is open,
|
|
256
|
+
and for exactly that window Radix `aria-hidden`s everything outside the modal,
|
|
257
|
+
`FormMessage` included. The shortcut of pointing the dialog control's
|
|
258
|
+
`aria-describedby` / `aria-errormessage` at the host's `FormMessage` id is
|
|
259
|
+
forbidden rather than merely unused: it resolves to a node that is `aria-hidden`
|
|
260
|
+
for the whole time the reference is live (an ARIA MUST violation), and neither
|
|
261
|
+
happy-dom nor jsdom can see the difference — which is why every new pin asserts
|
|
262
|
+
that the named node is inside THIS dialog, not merely that it exists.
|
|
263
|
+
|
|
264
|
+
`aria-errormessage` carries a single IDREF and is emitted only alongside
|
|
265
|
+
`aria-invalid="true"`. It is deliberately not folded into the host's
|
|
266
|
+
`aria-describedby` chain, which on the textarea surface already carries the
|
|
267
|
+
fullscreen character counter's sentence.
|
|
268
|
+
|
|
269
|
+
**The name reuses the visible title rather than minting a second author for it**
|
|
270
|
+
(#3978): `aria-labelledby` points at a span inside `DialogTitle`, not at
|
|
271
|
+
`DialogTitle` itself — Radix renders the title as `h2` with the id its own
|
|
272
|
+
`DialogContent` `aria-labelledby` names, so putting an id on it would buy the
|
|
273
|
+
control a name at the cost of the dialog's.
|
|
274
|
+
|
|
275
|
+
**Breaking (shipped as `minor`, see below), `@object-ui/components` only.**
|
|
276
|
+
`FullscreenEditorProps.error` is REQUIRED, not optional, and
|
|
277
|
+
`FullscreenEditorProps['children']` takes a fourth argument.
|
|
278
|
+
|
|
279
|
+
FROM → TO for an out-of-repo host:
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
<FullscreenEditor value={v} onCommit={c} label={l} testIdPrefix="x">
|
|
283
|
+
{(draft, setDraft, disabled) => <textarea … />}
|
|
284
|
+
</FullscreenEditor>
|
|
285
|
+
|
|
286
|
+
<FullscreenEditor value={v} onCommit={c} label={l} testIdPrefix="x" error={err}>
|
|
287
|
+
{(draft, setDraft, disabled, aria) => <textarea {...aria} … />}
|
|
288
|
+
</FullscreenEditor>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
A render prop may still declare fewer parameters, so only `error` fails to
|
|
292
|
+
compile — which is the point of making it required. Every consumer already has
|
|
293
|
+
the value at hand (registered widgets take `error` off the widget props
|
|
294
|
+
contract, objectui#3222; the built-in branch reads `fieldState.error?.message`),
|
|
295
|
+
and an omitted `error` reproduces this defect exactly: a dialog announcing
|
|
296
|
+
`aria-invalid="false"` for a field its own form has already failed. An optional
|
|
297
|
+
key was forgotten by three surfaces in a row; a required one cannot be.
|
|
298
|
+
|
|
299
|
+
`@object-ui/fields` is `patch`: `FullscreenFieldEditor` is internal to that
|
|
300
|
+
package (not re-exported from its entry), so nothing in its public surface
|
|
301
|
+
changes — only the behaviour of the two long-text widgets' dialogs.
|
|
302
|
+
|
|
303
|
+
`minor` rather than `major` follows the repo's standing retirement precedent
|
|
304
|
+
(AGENTS.md §版本号策略, enforced by `scripts/check-changeset-no-major.mjs`): all
|
|
305
|
+
publishable packages sit in one `fixed` group, so a `major` here would carry the
|
|
306
|
+
whole family up against an `@objectstack` that has not moved.
|
|
307
|
+
- 0bffb18: A readonly group-labelled field is now DESCRIBED by its own help text, not just named by its label.
|
|
308
|
+
|
|
309
|
+
The seven `labelling: 'group'` widgets (`address`, `geolocation`, `checkboxes`, `radio`, `rating`, `file`, `multiselect`) render a `<FormDescription>` in their readonly and zero-option states, and the form renderer publishes its `id` — but nothing in the document referenced that id, so the visible help text had no programmatic association with the field it describes. Measured before the change, one field per row: `consumers=0` on every readonly surface of all seven, against `consumers=1` on the same widget's editable one.
|
|
310
|
+
|
|
311
|
+
`toHostGroupProps` now carries `aria-describedby` alongside the host `id` and `aria-labelledby`, onto the `role="group"` surface those states already render (a group is a description carrier under ARIA 1.2 — no focusable control required, which is why this became possible only once objectui#3990 gave those surfaces a role).
|
|
312
|
+
|
|
313
|
+
Control-channel state deliberately does NOT come along: `aria-invalid` and `aria-required` report what a user's editing may do wrong, and a readonly display cannot be edited. The boundary is pinned from both sides, including with a live failed validation on a readonly required field. Editable surfaces are unchanged — a composite's editable container still leaves the description on the sub-input the user focuses, so the help text is never announced twice.
|
|
314
|
+
- 800f455: fix(fields): grid columns are keyed by the declared `name`, so spec-compliant grid metadata renders populated cells
|
|
315
|
+
|
|
316
|
+
`GridField` declared its own column interface keyed by `field` and read
|
|
317
|
+
`c.field` at every site (`key=`, `row[…]`, the blank row, cell writes, the
|
|
318
|
+
column chooser, the running-total lookup), while the published
|
|
319
|
+
`GridColumnDefinition` in `@object-ui/types` — and the grid documentation, and
|
|
320
|
+
the `fields-grid` catalog examples — declare the key as `name`. Metadata
|
|
321
|
+
authored against the published type therefore rendered a grid with the correct
|
|
322
|
+
row count and every cell empty, plus a React "unique key" warning per column.
|
|
323
|
+
|
|
324
|
+
The renderer now reads the declared `name`, and the master-detail derivation
|
|
325
|
+
(`deriveColumns` / `hydrateColumns` / `pickAmountField`) produces and consumes
|
|
326
|
+
the same key. There is deliberately **no** `col.field ?? col.name` alias: one
|
|
327
|
+
spelling at the producer (AGENTS.md #0.1).
|
|
328
|
+
|
|
329
|
+
**Breaking for `field`-keyed columns.** Grid / line-item / master-detail
|
|
330
|
+
subform columns spelled `{ field: 'amount' }` must be re-spelled
|
|
331
|
+
`{ name: 'amount' }`. This affects author-supplied `columns` on the `grid`
|
|
332
|
+
field, `record:line_items`, `object-master-detail-form` details and a
|
|
333
|
+
relationship field's `inlineColumns`. Auto-derived columns (no explicit
|
|
334
|
+
`columns` block) need no change. List-view and `object-grid` columns are a
|
|
335
|
+
different contract (`ListColumn`) and keep their own `field` key.
|
|
336
|
+
- 5458414: Publish relative import specifiers with explicit `.js` extensions so these six packages load under plain Node ESM.
|
|
337
|
+
|
|
338
|
+
Node's ESM resolver does not extension-search relative specifiers and `tsc` never rewrites them, so an extensionless `./Foo` in the source shipped as an extensionless `./Foo` in `dist` and importing the package entry outside a bundler failed with `ERR_MODULE_NOT_FOUND`. Bundled consumers were unaffected. Unbundled consumers — plain Node ESM, an SSR host importing the package directly, anyone running the published tarball without a build step — can now import these entries, and so can the downstream `@object-ui/plugin-*` packages that evaluate through `mobile`, `permissions` and `providers`.
|
|
339
|
+
- 3241559: `object-kanban` now caps its fetch with a real top-level `$top`, and honours `limit`.
|
|
340
|
+
|
|
341
|
+
The board's row cap was written `dataSource.find(objectName, { options: { $top: 100 }, $filter: … })`
|
|
342
|
+
— `$filter` at the top level, where the adapters read it, and the cap one level
|
|
343
|
+
down under `options`, which is not a `QueryParams` key. Nothing in this repo
|
|
344
|
+
reads `params.options` (`convertQueryParams` in `@object-ui/data-objectstack` and
|
|
345
|
+
`ApiDataSource` both read `params.$top`), so the intended cap never reached the
|
|
346
|
+
wire: a board over a large object fetched every row the server would return and
|
|
347
|
+
grouped all of it into lanes client-side. The author's 100 was never live.
|
|
348
|
+
|
|
349
|
+
The cap is now `$top: schema.limit ?? 100`, the shape `object-timeline` took for
|
|
350
|
+
the identical defect. Two consequences worth reading before upgrading:
|
|
351
|
+
|
|
352
|
+
- **A board that used to fetch unbounded now fetches 100 rows by default.** That
|
|
353
|
+
is the previously-declared intent taking effect, not a new limit — but a board
|
|
354
|
+
over an object with more than 100 matching records will show fewer cards than
|
|
355
|
+
it did. Authors who want the old behaviour should declare the window they
|
|
356
|
+
actually want (`limit: 500`), not rely on the absence of one.
|
|
357
|
+
- **`limit` is now mapped from the `dataSource` binding** (`object-kanban`'s
|
|
358
|
+
`ElementDataSourceMapping` gains `limit: 'limit'`). A board bound to a saved
|
|
359
|
+
view is capped by that view's `pagination.pageSize`, and the binding's own
|
|
360
|
+
`limit` overrides it. The flag comes with the read site: it stayed unmapped
|
|
361
|
+
until now on the rationale that the board had a "fixed window", which is the
|
|
362
|
+
claim this change falsifies. `sort` remains unmapped — the board still has no
|
|
363
|
+
`$orderby` read site — and `columns` remains unmapped because a board's columns
|
|
364
|
+
are its swimlanes, not a field projection.
|
|
365
|
+
|
|
366
|
+
`KanbanSchema` gains `limit?: number` (the board's row cap; distinct from a
|
|
367
|
+
column's `limit`, which is that lane's WIP limit and never reaches the query),
|
|
368
|
+
and the guide's per-block table in `content/docs/guide/data-source.md` no longer
|
|
369
|
+
claims a fixed window for `object-kanban`.
|
|
370
|
+
|
|
371
|
+
`@object-ui/fields` carried the same nesting in the lookup-name fallback
|
|
372
|
+
(`find(referenceTo, { $filter: { id }, options: { $top: 1 } })`) where it was
|
|
373
|
+
harmless — a primary-key equality returns at most one row with or without a cap.
|
|
374
|
+
It is spelled `$top: 1` now, so no live instance of the dead nesting is left in
|
|
375
|
+
the repo to read as precedent. No behaviour change there.
|
|
376
|
+
- 616a2a5: The list filter builder no longer offers `Is set` / `Is not set`, which its query dialects cannot express.
|
|
377
|
+
|
|
378
|
+
**User-visible before/after.** The operator dropdown in the list toolbar's
|
|
379
|
+
filter popover — and in the Studio view/tab/page filter inspectors, the dataset
|
|
380
|
+
inspector and the generic `filter` config field — loses two rows: **"Is set"**
|
|
381
|
+
and **"Is not set"**. The sharing-rule criteria builder (`FilterConditionField`)
|
|
382
|
+
keeps both, unchanged. `Is null` / `Is not null` and `Is empty` / `Is not empty`
|
|
383
|
+
are untouched everywhere and remain the way to filter on a missing value from
|
|
384
|
+
the list.
|
|
385
|
+
|
|
386
|
+
Nothing that worked stops working. Every save path behind those two rows was
|
|
387
|
+
already broken, in three different ways depending on the surface:
|
|
388
|
+
|
|
389
|
+
- **Live grid** — `ListView.mapOperator` had no row for either id, so its
|
|
390
|
+
`default:` arm returned the id verbatim and the query went out as
|
|
391
|
+
`['name', 'exists', 'x']`. `exists` is not a member of the spec's
|
|
392
|
+
`VALID_AST_OPERATORS`, so `isFilterAST()` rejects the shape: an unfiltered
|
|
393
|
+
read or a 400, never the filter the user asked for.
|
|
394
|
+
- **Save as view** — `foldFilterGroupToSpecRules` normalizes through the spec's
|
|
395
|
+
`normalizeFilterOperator`, which does not know the pair, and
|
|
396
|
+
`ViewFilterRuleSchema`'s enum then refuses the rule.
|
|
397
|
+
- **Dataset inspector** — `groupToCondition` has no row either and drops the
|
|
398
|
+
condition silently, so the filter simply never applied.
|
|
399
|
+
|
|
400
|
+
**Why withheld rather than mapped.** Measured on `@objectstack/spec`
|
|
401
|
+
17.0.0-rc.6: neither `VIEW_FILTER_OPERATORS` nor `VALID_AST_OPERATORS` contains
|
|
402
|
+
an existence operator, under any spelling — both sets have zero members matching
|
|
403
|
+
`/exist/`. Only the MongoDB-style `FieldOperatorsSchema` criteria carries
|
|
404
|
+
`$exists`, and that is precisely the dialect `FilterConditionField` writes, so
|
|
405
|
+
the pair moves behind the existing `OPT_IN_OPERATORS` gate and that widget opts
|
|
406
|
+
in. Collapsing them onto `isNotNull` / `isNull` was rejected: the builder
|
|
407
|
+
already draws those as their own rows, the round trip is lossy (a saved
|
|
408
|
+
`exists` reads back as `isNotNull`), and the spec's own note records `$exists` =
|
|
409
|
+
has-value as still unsettled across drivers — `driver-memory`'s live mingo path
|
|
410
|
+
and `driver-mongodb` read key-presence.
|
|
411
|
+
|
|
412
|
+
**The class is now closed by an assertion, not by discipline.** objectui's three
|
|
413
|
+
existing operator-parity guards all sweep spec vocabulary → objectui; none asked
|
|
414
|
+
whether an id the dropdown draws is an id the consumer can persist, which is the
|
|
415
|
+
direction that broke. `plugin-list`'s new
|
|
416
|
+
`list-offered-operator-expressible-parity.test.ts` forces the set the list
|
|
417
|
+
toolbar offers to **equal** the set its two dialects can express, in both
|
|
418
|
+
directions — so an unexpressible operator cannot be offered, and an operator
|
|
419
|
+
that becomes expressible upstream cannot stay needlessly withheld.
|
|
420
|
+
- 6c68b13: Lookup "recently used" now obeys the field's declared filters. The recents rail
|
|
421
|
+
re-fetched its rows without merging either `lookupFilters` or the `dependsOn`
|
|
422
|
+
cascade, so a record the author's metadata excludes stayed visible and
|
|
423
|
+
selectable — pick a product under project A, switch the form to project B, and
|
|
424
|
+
project A's product was still offered. Both re-fetch channels are fixed: the
|
|
425
|
+
inline dropdown (which re-read each remembered id with an unfiltered `findOne`)
|
|
426
|
+
and the search-first picker (which batched recents into an unfiltered `$in`
|
|
427
|
+
query). The currently-selected value still resolves unfiltered, so an existing
|
|
428
|
+
value keeps its label and a selection tray is never silently emptied.
|
|
429
|
+
- 5607092: objectui#4029 — the repo root now lints `no-console` (`error`, allowing
|
|
430
|
+
`warn`/`error`) so a stray module- or function-scope `console.log`/`info`/
|
|
431
|
+
`debug` fails CI instead of shipping silently (as `console.log('Registering
|
|
432
|
+
object-map...')` did in #7139, caught only by hand). Landing the rule meant
|
|
433
|
+
individually judging every real hit outside the tooling exemptions
|
|
434
|
+
(`scripts/**`, `**/examples/**`, test files, `packages/cli/src/**`,
|
|
435
|
+
`packages/create-plugin/src/**`) — this changeset covers the published
|
|
436
|
+
packages whose call sites changed:
|
|
437
|
+
|
|
438
|
+
- `@object-ui/app-shell`: `ObjectDataPage`'s dropped-URL-filter message is a
|
|
439
|
+
real diagnostic (data silently discarded), so it moves from `console.debug`
|
|
440
|
+
to `console.warn` to match the house convention.
|
|
441
|
+
- `@object-ui/plugin-detail`: `DetailView`'s Web Share API failure now reports
|
|
442
|
+
via `console.error` (it is a real failure, not debug noise); a redundant
|
|
443
|
+
"Link copied to clipboard" success log is removed.
|
|
444
|
+
- `@object-ui/fields`: `MasterDetailField`'s `handleView` stub no longer logs
|
|
445
|
+
the item it does nothing with.
|
|
446
|
+
- `@object-ui/runner`: `App`'s loader-selection debug prints, `LayoutRenderer`'s
|
|
447
|
+
unused click-handler stub log, and `MockDataSource`'s per-call narration
|
|
448
|
+
(`find`/`create`/`getObjectSchema`) are removed — none diagnosed a problem,
|
|
449
|
+
they only echoed the happy path.
|
|
450
|
+
- `object-ui` (VS Code extension): the "extension is now active!" activation
|
|
451
|
+
log is removed.
|
|
452
|
+
|
|
453
|
+
No behavior changes beyond console output. `@object-ui/core` and
|
|
454
|
+
`@object-ui/data-objectstack` also touch `no-console`-adjacent lines
|
|
455
|
+
(`debugLog`/`debugTime`/`debugTimeEnd`, `createQuietHttpLogger`) but only to
|
|
456
|
+
add `eslint-disable-next-line` documentation — those ARE the repo's
|
|
457
|
+
deliberate debug/logger infrastructure, not leaked residue, so their own
|
|
458
|
+
changeset carries empty frontmatter.
|
|
459
|
+
- e7747f1: fix(fields): retire the `owner` field-type alias with a loud tombstone
|
|
460
|
+
|
|
461
|
+
`owner` was a synonym for `user` with zero behavioral delta — both resolved to
|
|
462
|
+
the same `UserField` widget — and it is not a member of `@objectstack/spec`'s
|
|
463
|
+
closed `FieldType`, so no object schema could ever declare it. It was reachable
|
|
464
|
+
only through hand-written SDUI, and the three code faces that read it had
|
|
465
|
+
already drifted apart on the word: the form's data-source rule excluded it,
|
|
466
|
+
while plugin-grid's bulk-action dialog and app-shell's `paramToField` included
|
|
467
|
+
it.
|
|
468
|
+
|
|
469
|
+
The retired spelling now fails **loudly**. Deleting the alias on its own would
|
|
470
|
+
have been absorbed by two silent tails (`mapFieldTypeToFormType`'s
|
|
471
|
+
`|| 'field:text'` and `resolveFormWidgetType`'s `: 'text'`), each handing back a
|
|
472
|
+
working plain text input with no check turning red — so anyone who had written
|
|
473
|
+
`type: 'owner'`, including an AI author copying it out of a doc, would have
|
|
474
|
+
shipped a text box believing they shipped a person picker. Instead:
|
|
475
|
+
|
|
476
|
+
- `type: 'owner'` and `widget: 'field:owner'` both resolve to a registered
|
|
477
|
+
tombstone widget that renders a visible refusal naming the migration;
|
|
478
|
+
- the same prescription is written to the console once per spelling;
|
|
479
|
+
- the read/cell path degrades to the text cell deliberately and says so.
|
|
480
|
+
|
|
481
|
+
Migration: write the record-owner field as `{ type: 'user', name: 'owner' }` —
|
|
482
|
+
the field NAME carries the ownership meaning, the type carries the widget.
|
|
483
|
+
`UserField` and `UserCellRenderer` are unchanged; only the synonym is gone.
|
|
484
|
+
|
|
485
|
+
Also corrects the `dataSource` TSDoc in `@object-ui/fields`, which listed `grid`
|
|
486
|
+
among the widgets the form renderer wires a DataSource to. `GridField` never
|
|
487
|
+
read `dataSource` and no data-source table ever contained the key.
|
|
488
|
+
- ac2f332: Retired field-type spellings can no longer reach an inline editor by delegation — the grid's inline cell editor stops offering a working person picker for `owner`.
|
|
489
|
+
|
|
490
|
+
objectui#4814 retired the `owner` field type with a loud tombstone, and the
|
|
491
|
+
record form has answered `type: 'owner'` with a visible refusal ever since.
|
|
492
|
+
`FieldEditWidget` did not participate, and the reason it was missed is that no
|
|
493
|
+
routing table had to list the spelling for it to stay alive:
|
|
494
|
+
`hasFieldEditWidget` answers `resolveInlineEditType(type) in EDIT_WIDGETS`, and
|
|
495
|
+
`resolveInlineEditType` returns a type **unchanged** when it is already a key of
|
|
496
|
+
`EDIT_WIDGETS`. So while `owner: UserField` sat in that map, the alias table —
|
|
497
|
+
where the retirement lives — was never consulted, and the retirement never
|
|
498
|
+
applied. Measured: `hasFieldEditWidget('owner') === true`, and a direct
|
|
499
|
+
`FieldEditWidget` call rendered a working person picker. Every host built on
|
|
500
|
+
this seam, the data grid's inline cell editor being the user-facing one, still
|
|
501
|
+
let a person be picked and saved for a field the record form refuses.
|
|
502
|
+
|
|
503
|
+
The gate now consults the retirement table itself, so this closes the **class**
|
|
504
|
+
rather than the spelling — the next retirement covers this seam the day it lands
|
|
505
|
+
in `RETIRED_FIELD_TYPES`, with no edit here:
|
|
506
|
+
|
|
507
|
+
- `hasFieldEditWidget()` answers `false` for any retired spelling, ahead of
|
|
508
|
+
every table lookup (so it holds even if a retired spelling is still a key of
|
|
509
|
+
the widget map);
|
|
510
|
+
- `isInlineExcludedFieldType()` answers `true` for any retired spelling, which
|
|
511
|
+
is what routes a host to its read-only path;
|
|
512
|
+
- `FieldEditWidget` itself renders no control for a retired spelling and writes
|
|
513
|
+
the once-per-spelling console prescription naming the migration;
|
|
514
|
+
- `owner` is dropped from `EDIT_WIDGETS` and from the cosmetic
|
|
515
|
+
`COMPACT_EDIT_TYPES` sizing set.
|
|
516
|
+
|
|
517
|
+
The second bullet is load-bearing and not cosmetic. `ObjectGrid` marks a column
|
|
518
|
+
`editable: false` exactly when `isFieldInlineEditable` is false, and that
|
|
519
|
+
predicate consults `isInlineExcludedFieldType`. Without it, closing the first
|
|
520
|
+
gate alone would have left the column editable, handed the cell editor a `null`
|
|
521
|
+
widget, and let DataTable fall back to its built-in **plain text input** — which
|
|
522
|
+
for a stored person reference displays a coerced value and writes a bare string
|
|
523
|
+
straight back over it. Measured before/after on the grid consumer: a stored
|
|
524
|
+
`owner` field went from an editable cell rendering the person picker to a
|
|
525
|
+
read-only cell (`isFieldInlineEditable('owner')` `true` → `false`), which is the
|
|
526
|
+
same disposition `password`, `formula` and `composite` already get, for the same
|
|
527
|
+
reason. The read path is unchanged from #4814: the text cell plus the console
|
|
528
|
+
prescription.
|
|
529
|
+
|
|
530
|
+
`user` is untouched — it keeps the person picker, its compact sizing and its
|
|
531
|
+
expanded-object value resolution. The record-owner idiom survives verbatim as
|
|
532
|
+
`{ type: 'user', name: 'owner' }`: the field NAME carries the ownership meaning,
|
|
533
|
+
the type carries the widget.
|
|
534
|
+
- a777058: Editable `markdown` / `html` / `richtext` fields now carry the host's `id` and `aria-describedby` on the editor.
|
|
535
|
+
|
|
536
|
+
`RichTextField` — the one widget all three registry keys resolve to — read only
|
|
537
|
+
`className` and `disabled` off its props and had no `toDomProps` anywhere in the
|
|
538
|
+
file, so everything `<FormControl>`'s Radix `Slot` hands a field widget landed on
|
|
539
|
+
no element at all. Measured on a real form with a field carrying a description:
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
markdown editable descEl=SET consumers=0 hostIdEl=NONE for=DANGLING
|
|
543
|
+
text editable descEl=SET consumers=1 hostIdEl=input for=RESOLVES
|
|
544
|
+
textarea editable descEl=SET consumers=1 hostIdEl=textarea for=RESOLVES
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Two user-visible consequences: the form's visible label pointed at an id nothing
|
|
548
|
+
carried, so clicking it did not reach the editor and the control had no
|
|
549
|
+
accessible name; and the `<FormDescription>` the form rendered below the field
|
|
550
|
+
had zero consumers, so a screen reader never announced the help text or — on a
|
|
551
|
+
failed submit — the error message id that rides in the same `aria-describedby`.
|
|
552
|
+
|
|
553
|
+
The fix is objectui#3318's standing recipe: `toDomProps(props)` spread onto the
|
|
554
|
+
REAL focusable control, which here is the `<Textarea>` inside the shared
|
|
555
|
+
`RichTextEditorSurface`. The pass-through is given to the inline surface only —
|
|
556
|
+
the fullscreen dialog's copy of the same surface must not carry a duplicate of
|
|
557
|
+
the host id, and the description ids it names sit outside the modal Radix
|
|
558
|
+
`aria-hidden`s. `aria-invalid` still comes from the widget's own `error` read,
|
|
559
|
+
kept after the spread so objectui#3318's PASS entry for this widget is unchanged.
|
|
560
|
+
|
|
561
|
+
The readonly branch is untouched: the same reading there belongs to
|
|
562
|
+
objectui#4788, whose mechanism is still open.
|
|
563
|
+
- 75444e3: The dependency-gate hint now enumerates its controlling fields with the locale's
|
|
564
|
+
own list separator, and reads identically whichever caller produced it.
|
|
565
|
+
|
|
566
|
+
`lookup.selectFirst` and `fields.options.selectFirst` are deliberately one
|
|
567
|
+
wording, so a field gated on two or more parents says the same thing whether the
|
|
568
|
+
lookup widget or the form renderer rendered it. The sentence was shared but its
|
|
569
|
+
`{{fields}}` slot was not: each call site joined the controlling-field names
|
|
570
|
+
with its own hardcoded separator, and not even the same one — `', '` in
|
|
571
|
+
`LookupField`, `' / '` in the form renderer's `gatedHint` and in
|
|
572
|
+
`OptionsEmptyState`. A field gated on Account and Lead Source read
|
|
573
|
+
`Select Account, Lead Source first` from one side and
|
|
574
|
+
`Select Account / Lead Source first` from the other.
|
|
575
|
+
|
|
576
|
+
A list separator is a property of the locale rather than of the code, so both
|
|
577
|
+
spellings were also wrong for the script under zh/ja (which enumerate with
|
|
578
|
+
U+3001) and under ar (U+060C). All three call sites now read
|
|
579
|
+
`validation.formInvalidJoiner` — the key already shipped in all ten packs for
|
|
580
|
+
the invalid-submit toast's field list, which is the same class of truncated-name
|
|
581
|
+
list. One key, every caller: a second, gate-specific key would have recreated
|
|
582
|
+
the divergence the shared sentence exists to prevent.
|
|
583
|
+
|
|
584
|
+
No locale pack changes, and no change to what a provider-less render produces in
|
|
585
|
+
English: the `@object-ui/fields` defaults table declares the joiner as `', '`,
|
|
586
|
+
the `en` pack's value and the literal `LookupField` previously hardcoded.
|
|
587
|
+
- dad51e5: fix(fields): deliver the host's a11y channels to `slider` and name `signature`
|
|
588
|
+
|
|
589
|
+
`SliderField` and `SignatureField` forwarded nothing a form host handed them —
|
|
590
|
+
neither spread `toDomProps(props)` at all — so `<FormControl>`'s whole payload
|
|
591
|
+
landed on nothing. Measured on a real form, one required field per row, freshly
|
|
592
|
+
failed validation:
|
|
593
|
+
|
|
594
|
+
```
|
|
595
|
+
slider ariaInvalidTrue=[] labelFor=…-form-item -> DANGLING descConsumers=0 ids=[]
|
|
596
|
+
signature ariaInvalidTrue=[] labelFor=…-form-item -> DANGLING descConsumers=0 ids=[]
|
|
597
|
+
text ariaInvalidTrue=[input] labelFor -> input descConsumers=1
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
`ids=[]` is the tell: no element in either row carried an id at all, so the
|
|
601
|
+
visible label pointed `for` at nothing, the rendered help text had zero
|
|
602
|
+
consumers, and a failed slider announced no error state.
|
|
603
|
+
|
|
604
|
+
**`slider`** now delivers all three. Its focusable control is Radix's
|
|
605
|
+
`span[role="slider"]` thumb, which the synced `ui/slider.tsx` renders internally
|
|
606
|
+
and does not export, so the primitive grew a declared `thumbProps` — routed
|
|
607
|
+
through a new `lib/slider-thumb` and applied to the no-touch file as a declared
|
|
608
|
+
sync patch, so it survives regeneration. The split of which keys stay on Root
|
|
609
|
+
(`name`, `disabled`) is the one the `select` fix already settled.
|
|
610
|
+
|
|
611
|
+
**`signature`** gets the name and the description on a `role="group"` container.
|
|
612
|
+
Its control state deliberately does not follow: the drawing surface is a
|
|
613
|
+
`<canvas>` with no keyboard path, and its only other element is disabled while
|
|
614
|
+
the pad is empty, so there is no element a control state could be read from.
|
|
615
|
+
|
|
616
|
+
Both are now declared `labelling: 'group'` — a `<span>` and a `<canvas>` are not
|
|
617
|
+
labelable elements, so a host `for` could only dangle at them.
|
|
618
|
+
- Updated dependencies [88085e3]
|
|
619
|
+
- Updated dependencies [69251bf]
|
|
620
|
+
- Updated dependencies [57e668f]
|
|
621
|
+
- Updated dependencies [516663d]
|
|
622
|
+
- Updated dependencies [41ac1b7]
|
|
623
|
+
- Updated dependencies [1eaf0a1]
|
|
624
|
+
- Updated dependencies [460c4d0]
|
|
625
|
+
- Updated dependencies [0ae27f7]
|
|
626
|
+
- Updated dependencies [2533ec5]
|
|
627
|
+
- Updated dependencies [78c0f9a]
|
|
628
|
+
- Updated dependencies [bbe8b86]
|
|
629
|
+
- Updated dependencies [8477be5]
|
|
630
|
+
- Updated dependencies [279fb13]
|
|
631
|
+
- Updated dependencies [2e82ab2]
|
|
632
|
+
- Updated dependencies [ad07b65]
|
|
633
|
+
- Updated dependencies [41f498b]
|
|
634
|
+
- Updated dependencies [ef0d150]
|
|
635
|
+
- Updated dependencies [f34226e]
|
|
636
|
+
- Updated dependencies [564b605]
|
|
637
|
+
- Updated dependencies [e1d4251]
|
|
638
|
+
- Updated dependencies [40d3a33]
|
|
639
|
+
- Updated dependencies [8b9dc62]
|
|
640
|
+
- Updated dependencies [1184192]
|
|
641
|
+
- Updated dependencies [a2a9747]
|
|
642
|
+
- Updated dependencies [a1609a6]
|
|
643
|
+
- Updated dependencies [53f23bc]
|
|
644
|
+
- Updated dependencies [c4533dc]
|
|
645
|
+
- Updated dependencies [be60815]
|
|
646
|
+
- Updated dependencies [37f6844]
|
|
647
|
+
- Updated dependencies [93de4f6]
|
|
648
|
+
- Updated dependencies [2b50261]
|
|
649
|
+
- Updated dependencies [384f30d]
|
|
650
|
+
- Updated dependencies [ac600e5]
|
|
651
|
+
- Updated dependencies [97fba31]
|
|
652
|
+
- Updated dependencies [232f61a]
|
|
653
|
+
- Updated dependencies [d374caf]
|
|
654
|
+
- Updated dependencies [5673576]
|
|
655
|
+
- Updated dependencies [c1ef923]
|
|
656
|
+
- Updated dependencies [911ceaa]
|
|
657
|
+
- Updated dependencies [98eab36]
|
|
658
|
+
- Updated dependencies [af5e292]
|
|
659
|
+
- Updated dependencies [3fbbea1]
|
|
660
|
+
- Updated dependencies [5458414]
|
|
661
|
+
- Updated dependencies [7f96b10]
|
|
662
|
+
- Updated dependencies [167ec42]
|
|
663
|
+
- Updated dependencies [616a2a5]
|
|
664
|
+
- Updated dependencies [0046d8f]
|
|
665
|
+
- Updated dependencies [f1d4748]
|
|
666
|
+
- Updated dependencies [bea374e]
|
|
667
|
+
- Updated dependencies [b1119ec]
|
|
668
|
+
- Updated dependencies [9f23d2b]
|
|
669
|
+
- Updated dependencies [578e025]
|
|
670
|
+
- Updated dependencies [af025ee]
|
|
671
|
+
- Updated dependencies [d109a4d]
|
|
672
|
+
- Updated dependencies [598c89a]
|
|
673
|
+
- Updated dependencies [4a0bd17]
|
|
674
|
+
- Updated dependencies [b8b9af4]
|
|
675
|
+
- Updated dependencies [31676be]
|
|
676
|
+
- Updated dependencies [8c0d52e]
|
|
677
|
+
- Updated dependencies [aff10e2]
|
|
678
|
+
- Updated dependencies [70a774b]
|
|
679
|
+
- Updated dependencies [9ce096f]
|
|
680
|
+
- Updated dependencies [e05db88]
|
|
681
|
+
- Updated dependencies [7458a41]
|
|
682
|
+
- Updated dependencies [ad13d63]
|
|
683
|
+
- Updated dependencies [5ffcc14]
|
|
684
|
+
- Updated dependencies [d971e51]
|
|
685
|
+
- Updated dependencies [97abb24]
|
|
686
|
+
- Updated dependencies [deb157a]
|
|
687
|
+
- Updated dependencies [9c60144]
|
|
688
|
+
- Updated dependencies [d2ce342]
|
|
689
|
+
- Updated dependencies [9695da7]
|
|
690
|
+
- Updated dependencies [75444e3]
|
|
691
|
+
- Updated dependencies [58b8346]
|
|
692
|
+
- Updated dependencies [2d0bd16]
|
|
693
|
+
- Updated dependencies [a9e17b4]
|
|
694
|
+
- Updated dependencies [b8ce7dc]
|
|
695
|
+
- Updated dependencies [dad51e5]
|
|
696
|
+
- Updated dependencies [1c9c342]
|
|
697
|
+
- Updated dependencies [787c738]
|
|
698
|
+
- Updated dependencies [8396656]
|
|
699
|
+
- Updated dependencies [dbbd38a]
|
|
700
|
+
- Updated dependencies [8871c14]
|
|
701
|
+
- Updated dependencies [93fe362]
|
|
702
|
+
- Updated dependencies [dfc6975]
|
|
703
|
+
- Updated dependencies [3cf4de0]
|
|
704
|
+
- Updated dependencies [c9dc811]
|
|
705
|
+
- Updated dependencies [144ef9b]
|
|
706
|
+
- Updated dependencies [138ab04]
|
|
707
|
+
- Updated dependencies [a0b9e91]
|
|
708
|
+
- Updated dependencies [99bd015]
|
|
709
|
+
- Updated dependencies [21e4585]
|
|
710
|
+
- @object-ui/types@17.6.0
|
|
711
|
+
- @object-ui/i18n@17.6.0
|
|
712
|
+
- @object-ui/react@17.6.0
|
|
713
|
+
- @object-ui/components@17.6.0
|
|
714
|
+
- @object-ui/core@17.6.0
|
|
715
|
+
- @object-ui/providers@17.6.0
|
|
716
|
+
|
|
717
|
+
## 17.5.0
|
|
718
|
+
|
|
719
|
+
### Minor Changes
|
|
720
|
+
|
|
721
|
+
- ebb4e0e: The date formatter's last three en-US channels now follow the display locale
|
|
722
|
+
(objectui#4272).
|
|
723
|
+
|
|
724
|
+
objectui#4468 (PR #4512) pointed every date _renderer_ at `useDisplayLocale()`.
|
|
725
|
+
Three channels were out of its reach because they are properties of the
|
|
726
|
+
formatter's signature and of its callers rather than of any renderer, so a `zh`
|
|
727
|
+
console still met English dates in three places:
|
|
728
|
+
|
|
729
|
+
- **`formatDate`'s `'short'` branch** hardcoded
|
|
730
|
+
`toLocaleDateString('en-US', { month: 'short' })`, so it rendered an English
|
|
731
|
+
month even when the caller had threaded `options.locale` into that very call.
|
|
732
|
+
Its only consumers are ObjectGrid's two mobile-card date cells, which threaded
|
|
733
|
+
no locale — fixing either half alone moves nothing, so both land here.
|
|
734
|
+
- **`formatDateTime` took no options parameter at all**, so no caller could
|
|
735
|
+
localize it however hard it tried; it always handed `Intl` an `undefined` tag,
|
|
736
|
+
which means the MACHINE's locale — neither of the repo's two locale channels.
|
|
737
|
+
The parameter is optional and lands together with its consumers, plugin-gantt's
|
|
738
|
+
four tooltip call sites.
|
|
739
|
+
- **The lookup picker's MongoDB `$date` fallback** called a bare
|
|
740
|
+
`toLocaleDateString()` with no tag.
|
|
741
|
+
|
|
742
|
+
One resolver everywhere, as before: `useDisplayLocale()` (tenant regional
|
|
743
|
+
default → active UI language → `'en'`). `Intl` accepts `'zh'` verbatim, so there
|
|
744
|
+
is still no mapping table anywhere.
|
|
745
|
+
|
|
746
|
+
English output is byte-identical at every touched site — `en` and `en-US` agree
|
|
747
|
+
on all twelve short month names — and the `'short'` layout itself is unchanged:
|
|
748
|
+
only the month token is localized, the compact `"Jan 15, '24"` shape around it
|
|
749
|
+
is a deliberate fixed layout for narrow cards.
|
|
750
|
+
|
|
751
|
+
`@object-ui/fields` is `minor` because `formatDateTime`'s new optional parameter
|
|
752
|
+
is visible in the package's entry `.d.ts`; the plugin packages' own `.d.ts` files
|
|
753
|
+
are byte-identical, so their change is module-local.
|
|
754
|
+
|
|
755
|
+
- 36310dc: `formatPercent` groups its output and follows the display locale — the last
|
|
756
|
+
tooltip/cell channel (objectui#4553).
|
|
757
|
+
|
|
758
|
+
PR #4557 threaded the gantt tooltip's number and currency rows and measured that
|
|
759
|
+
the percent row could not follow: `formatPercent(value, precision)` took no
|
|
760
|
+
locale parameter, and its whole body was
|
|
761
|
+
`${percentDisplayValue(value).toFixed(precision)}%`. It built no
|
|
762
|
+
`Intl.NumberFormat` and never reached `formatDisplayNumber` — so unlike its
|
|
763
|
+
siblings it did not render in the MACHINE's locale, it rendered in **no** locale:
|
|
764
|
+
an ASCII decimal mark, never a grouping separator, byte-identical on every
|
|
765
|
+
machine.
|
|
766
|
+
|
|
767
|
+
**English output MOVES, and that is the fix.** Because the function never
|
|
768
|
+
grouped, `1235%` was wrong in en-US too, not only in German. Grouping and locale
|
|
769
|
+
therefore land together:
|
|
770
|
+
|
|
771
|
+
| | before | after |
|
|
772
|
+
| ---------- | ------- | -------------- |
|
|
773
|
+
| en, 1234.5 | `1235%` | `1,235%` |
|
|
774
|
+
| de, 1234.5 | `1235%` | `1.235\u00a0%` |
|
|
775
|
+
| de, 80 | `80%` | `80\u00a0%` |
|
|
776
|
+
|
|
777
|
+
Values below the grouping threshold are unchanged in English (`80%`, `12.5%`,
|
|
778
|
+
`33.33%`), so the move is confined to four digits and up. German changes at every
|
|
779
|
+
magnitude, because the no-break space before the sign is part of the locale's
|
|
780
|
+
percent convention — which is what routing through `Intl` buys over appending a
|
|
781
|
+
literal `%`.
|
|
782
|
+
|
|
783
|
+
The scaling contract is untouched: `percentDisplayValue` still disambiguates a
|
|
784
|
+
fraction-stored percent (`0.8` → 80%) from a whole one, so the list cell and the
|
|
785
|
+
dashboard measure formatter still agree.
|
|
786
|
+
|
|
787
|
+
Consumers are threaded in the same change, the parameter never landing
|
|
788
|
+
speculatively:
|
|
789
|
+
|
|
790
|
+
- **fields** — `PercentCellRenderer`, on BOTH of its paths. Its whole-percent
|
|
791
|
+
branch (`progress` / `completion` fields, which store 0-100 and must skip the
|
|
792
|
+
fraction scaling) was a second bare `toFixed` call; leaving it behind would
|
|
793
|
+
have made one grid internally inconsistent, so both branches now share one
|
|
794
|
+
locale-aware body and differ only in the scaling policy.
|
|
795
|
+
- **plugin-gantt** — the tooltip percent row, completing objectui#4553's switch.
|
|
796
|
+
- **plugin-grid** — the mobile card's percent cell, which sits in the same
|
|
797
|
+
density row as a date cell objectui#4272 had already localized.
|
|
798
|
+
- **plugin-dashboard** — `renderFieldValue`'s percent branch. It is a plain
|
|
799
|
+
function rather than a component, so it takes the locale as an optional fourth
|
|
800
|
+
parameter beside the `tenantCurrency` already threaded that way, and both of
|
|
801
|
+
its callers pass it and declare it in their memo dependency arrays.
|
|
802
|
+
|
|
803
|
+
Bumps follow each package's own `.d.ts` diff, measured in both directions.
|
|
804
|
+
`@object-ui/fields` and `@object-ui/plugin-dashboard` are `minor` on the
|
|
805
|
+
objectui#4272 / PR #4544 precedent — quoted from that changeset: "`@object-ui/fields`
|
|
806
|
+
is `minor` because `formatDateTime`'s new optional parameter is visible in the
|
|
807
|
+
package's entry `.d.ts`; the plugin packages' own `.d.ts` files are
|
|
808
|
+
byte-identical, so their change is module-local." Here `formatPercent` and
|
|
809
|
+
`renderFieldValue` each gain an entry-visible optional parameter, while
|
|
810
|
+
plugin-gantt's and plugin-grid's `.d.ts` files are byte-identical and stay
|
|
811
|
+
`patch`.
|
|
812
|
+
|
|
813
|
+
- 52d878a: fix(fields): `formatPercent` renders percentage points directly — ties round half-up and extremes keep every digit
|
|
814
|
+
|
|
815
|
+
`formatPercent` rendered a value that is already in percentage POINTS through
|
|
816
|
+
`Intl`'s `style: 'percent'`, which expects a FRACTION, so the body divided by
|
|
817
|
+
100 for `Intl` to multiply straight back. That round trip is not
|
|
818
|
+
value-preserving: `Intl` formats from the shortest decimal representation of the
|
|
819
|
+
double it is handed, and the quotient's is not the authored one. A stored
|
|
820
|
+
`1.005` at 2 decimals rendered `1.00%` where half-up on the authored decimal is
|
|
821
|
+
`1.01%`; `1.45` at 1 decimal rendered `1.4%` for `1.5%`. Every case was a
|
|
822
|
+
last-digit off-by-one — the failure mode least likely to be noticed and most
|
|
823
|
+
likely to be trusted.
|
|
824
|
+
|
|
825
|
+
The body now renders through `style: 'percentPoints'` with no scaling round
|
|
826
|
+
trip. Measured on this repo's runner (node v22.22.2 / ICU 78.2), 27,577 of
|
|
827
|
+
1,200,003 ordinary en-US forms move (0.005-step grid to 2,000, precisions
|
|
828
|
+
0/1/2), and the same artefact at the top of the double range is gone too:
|
|
829
|
+
`Number.MAX_SAFE_INTEGER` percentage points rendered `9,007,199,254,740,990%`
|
|
830
|
+
and now render `9,007,199,254,740,991%`.
|
|
831
|
+
|
|
832
|
+
The locale percent CONVENTION is unchanged — this is a numeral move only.
|
|
833
|
+
`'percentPoints'` is `Intl`'s `style: 'unit'` / `unit: 'percent'`, re-measured on
|
|
834
|
+
this call shape across 720 combinations (10 locales x 18 values x 4 precisions):
|
|
835
|
+
0 convention differences, 130 numeral differences. The no-break space in
|
|
836
|
+
de/fr/ru/sv, Turkish's prefixed sign, Arabic's own percent sign and Bengali's
|
|
837
|
+
digits all render exactly as before. Percent SCALING (a stored fraction below 1
|
|
838
|
+
scaling by 100) is upstream of the render and untouched.
|
|
839
|
+
|
|
840
|
+
A percentage point now reads identically in a list cell and in a dashboard
|
|
841
|
+
measure, which `formatMeasure` already rendered this way.
|
|
842
|
+
|
|
843
|
+
- bb68488: Stop declaring 14 symbols under names `@objectstack/spec` owns at `17.0.0-rc.6`
|
|
844
|
+
(objectui#4167, objectstack#4115).
|
|
845
|
+
|
|
846
|
+
The rc.6 bump published nine names this repo already declared locally, on top of
|
|
847
|
+
four that predate it — `check:spec-symbols` reported all thirteen at once, and a
|
|
848
|
+
fourteenth (`GlobalFilterSchema`) appeared during the bump itself. Each was
|
|
849
|
+
triaged on its own rather than blanket-renamed, because the right answer differs
|
|
850
|
+
per symbol: five bind to the spec, three are renamed because the spec's
|
|
851
|
+
same-named export means something else, five arrive by derivation, and one is a
|
|
852
|
+
declared dialect with a written reason.
|
|
853
|
+
|
|
854
|
+
**Breaking for importers of `@object-ui/react`, `@object-ui/app-shell` and
|
|
855
|
+
`@object-ui/types`** — three exported names changed, because the spec exports the
|
|
856
|
+
same name for a _different_ thing:
|
|
857
|
+
|
|
858
|
+
| package | was | now | what the spec's same-named export actually is |
|
|
859
|
+
| :-------------------- | :----------------- | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
860
|
+
| `react` / `app-shell` | `MetadataState` | `MetadataCacheState` | a metadata item's LIFECYCLE state — `'draft' \| 'active' \| 'deprecated' \| 'archived'` (`MetadataStateSchema`, `@objectstack/spec/system`) |
|
|
861
|
+
| `react` / `app-shell` | `resolveI18nLabel` | `resolveKeyedI18nLabel` | a resolver for the INLINE per-locale map (`{ en: 'Owner', 'zh-CN': '负责人' }`) against a BCP-47 locale |
|
|
862
|
+
| `types` | `DateRangePreset` | `FilterBuilderDateRangePreset` | the thirteen HISTORICAL dashboard filter-bar presets; this one is the filter-builder set, which adds eight FUTURE windows the dashboard schema rejects |
|
|
863
|
+
|
|
864
|
+
`resolveI18nLabel` is the one where the collision had already started costing
|
|
865
|
+
something. rc.6 widened `I18nLabel` from `string` to
|
|
866
|
+
`string | Record< string, string >`, so the same authored value now reaches
|
|
867
|
+
either resolver — and each answers wrongly, silently, for the other's input: the
|
|
868
|
+
keyed one returns `undefined` for `{ en: 'Owner' }` (no `key`, no
|
|
869
|
+
`defaultValue`), and the spec's reads `key` / `defaultValue` / `params` as locale
|
|
870
|
+
tags. The rc.6 bump PR met this and aliased the spec's import as
|
|
871
|
+
`resolveInlineI18nLabel` in five files, with hand-written comments at two of
|
|
872
|
+
them. That is a review convention, which is what objectstack#4115 exists to
|
|
873
|
+
replace with a rule — so `Keyed` is now the counterpart of that `Inline`, and the
|
|
874
|
+
name says which vocabulary it resolves at every call site.
|
|
875
|
+
|
|
876
|
+
**Eleven keep their names and are now imported or derived from the spec** instead
|
|
877
|
+
of re-declared: `DATE_RANGE_PRESETS`, `NavigationMode`, `AddressValue`,
|
|
878
|
+
`BreakpointColumnMap`, `BreakpointOrderMap`, `KanbanConfig`, `CalendarConfig`,
|
|
879
|
+
`GanttConfig`, plus the three renamed above at their new names.
|
|
880
|
+
|
|
881
|
+
**Four of the copies were losing information, not just duplicating it.**
|
|
882
|
+
|
|
883
|
+
- **`GanttConfig` declared six keys and called itself canonical; rc.6's
|
|
884
|
+
`GanttConfigSchema` declares seventeen.** The eleven it never mentioned —
|
|
885
|
+
`parentField`, `typeField`, `baselineStartField`, `baselineEndField`,
|
|
886
|
+
`groupByField`, `resourceView`, `assigneeField`, `effortField`, `capacity`,
|
|
887
|
+
`quickFilters`, `autoZoomToFilter` — are all read by
|
|
888
|
+
`plugin-gantt/src/ObjectGantt.tsx`, through a local `GanttConfigEx`
|
|
889
|
+
intersection that existed only because this type did not carry them. It now
|
|
890
|
+
derives from the spec, with `timeSegments` (shift segmentation) as the one
|
|
891
|
+
genuinely local extension; the schema is `$loose` upstream, so that key is
|
|
892
|
+
legal metadata rather than a second dialect.
|
|
893
|
+
- **`GanttConfig.tooltipFields` carried the comment "not part of the upstream
|
|
894
|
+
GanttConfigSchema".** It is, as of rc.6, so the key now arrives from the spec.
|
|
895
|
+
- **`AddressValue` declared five of the spec's seven parts** — `countryCode` and
|
|
896
|
+
`formatted` were missing, under a comment already claiming to be "the part
|
|
897
|
+
names of `AddressSchema`". The widget still renders five inputs; binding the
|
|
898
|
+
type stops it from asserting the platform cannot store the other two, and makes
|
|
899
|
+
the `{ ...address }` write-through say so.
|
|
900
|
+
- **`DATE_RANGE_PRESETS` was `Object.keys(PRESET_RANGES)`,** a third copy of a
|
|
901
|
+
vocabulary the spec extracted in objectstack#4614 precisely to collapse — its
|
|
902
|
+
own doc comment names this module as one of the three. It is now the spec's
|
|
903
|
+
array by reference, and the local date-macro bounds table is pinned complete
|
|
904
|
+
against it with `satisfies`, so a preset the schema gains without bounds here
|
|
905
|
+
is a compile error rather than a filter that validates clean and then selects
|
|
906
|
+
nothing.
|
|
907
|
+
|
|
908
|
+
`NavigationMode` was one hop from the spec already (`NavigationConfig['mode']`);
|
|
909
|
+
it is bound directly, with a both-directions type pin that it stays the same type
|
|
910
|
+
as the config's own `mode`. `KanbanConfig` / `CalendarConfig` /
|
|
911
|
+
`BreakpointColumnMap` / `BreakpointOrderMap` were exact hand copies of `$strict`
|
|
912
|
+
schemas and are now re-exports — "still exact" is the argument for binding them,
|
|
913
|
+
since a copy with nothing to protect can only drift.
|
|
914
|
+
|
|
915
|
+
`GlobalFilterSchema` is the one ALLOW entry. It is the same spread-composition
|
|
916
|
+
dialect as `SelectOptionSchema` next to it, and it collided only because rc.6's
|
|
917
|
+
new refinement forced `.extend()` to be respelled as a `.shape` spread — which
|
|
918
|
+
moved a derivation the guard could see into an object literal it deliberately
|
|
919
|
+
does not descend into. The dialect is unchanged and its three divergences are
|
|
920
|
+
pinned; which side moves on the refinement itself is objectui#4165.
|
|
921
|
+
|
|
922
|
+
`@objectstack/spec` moves from `devDependencies` to `dependencies` in
|
|
923
|
+
`@object-ui/layout`: its public type surface now references the spec.
|
|
924
|
+
|
|
925
|
+
### Patch Changes
|
|
926
|
+
|
|
927
|
+
- e2e6360: A `Field.address` value now reads as a formatted postal address on the record detail page, instead of stringified JSON.
|
|
928
|
+
|
|
929
|
+
The display (read) registry mapped `address` straight to `JsonCellRenderer`, so a populated address rendered as `{"street":"中策路 1 号","city":"杭州",…}` — while a `location` field sitting next to it in the same field group rendered formatted, and the create/edit dialog rendered the very same value as proper Street / City / State / ZIP / Country inputs. The gap was display-side only: the input registry has always carried `address`. Both read surfaces the detail page exposes are affected and both are fixed, because they share one `displayValue` path — read mode, and the inline-edit read state (the row carrying the pencil affordance, before a field is actually being edited).
|
|
930
|
+
|
|
931
|
+
Layout is not invented for the read side. `AddressField`'s readonly branch already collapsed a stored address to a single line, and that rule — `Street, City, State ZIP, Country`, with `state` and the postal code sharing one comma group — is now the _only_ implementation, moved into a pure `address-format` module that both surfaces call. A readonly form and a detail page therefore cannot spell one stored address two ways; a second copy next to the renderer would have been a rule that drifts. The module is deliberately React-free, so the eagerly-loaded barrel can format a cell without pulling `AddressField` and its inputs out of the lazy widget chunk.
|
|
932
|
+
|
|
933
|
+
Partial values degrade the way the readonly line already did: absent, non-string and whitespace-only parts are dropped rather than spaced over, so a street-only address renders as `中策路 1 号` and never as `, , ,` or as the string `undefined`. Legacy records whose postal code was written under `zipCode` (objectstack#5143) still render it, matching what the input widget reads.
|
|
934
|
+
|
|
935
|
+
Nothing is silently swallowed by the change: a value the formatter cannot recognize — an object carrying no known part — keeps today's compact-JSON rendering rather than disappearing, and `{}` or a null value shows the usual empty placeholder. A plain string address passes straight through. `location`, `geolocation`, and the genuinely structural `json` / `object` types are untouched.
|
|
936
|
+
|
|
937
|
+
The address _input_ is unchanged on every surface, including the create/edit dialog.
|
|
938
|
+
|
|
939
|
+
- dde7283: `chatbot` and `chatbot-enhanced` now pass only whitelisted DOM props to their host element (objectui#4431)
|
|
940
|
+
|
|
941
|
+
Both registrations destructured `schema` and `className` and forwarded everything else. `SchemaRenderer` hands a registered component the authored node's own keys, the contents of its `props` container, the ARIA it resolved and the host's trailing props — so all of it became attributes on the chat root `div`, because React passes unknown lowercase attributes through in silence and stringifies object values. Measured through the real SDUI path with a data-source adapter attached: **14 non-DOM attributes on each widget**, including `datasource="[object Object]"` (the injected adapter, which only appears on a deployment that really loads data) and a camelCase `arialabel` sitting next to the resolved `aria-label`, so the element carried each ARIA value twice under two spellings — one of them meaningless to assistive technology.
|
|
942
|
+
|
|
943
|
+
Both are now consume-or-whitelist: configuration is read off `schema` as before, the evaluated `disabled` verdict is consumed by name, and only `toDomProps`' output reaches the element. The resolved `aria-label` / `aria-describedby`, `role`, `id`, `tabIndex` and the `data-*` family still arrive — dropping them would have been an accessibility regression dressed as a leak fix, so the pin asserts the delivered set exactly, not just the absent one. `chatbot-floating` is untouched: its content mounts through a portal and its root never spread.
|
|
944
|
+
|
|
945
|
+
`@object-ui/core` gains the shared executor this migration needs (`utils/dom-props.ts`): `toDomProps` for the SDUI widget contract, plus `pickDomProps` — the mechanism — for a package whose own contract declares a different key set. That is the objectui#4409 dependency direction: plugin packages declare `@object-ui/core` and must not grow a dependency on `@object-ui/fields` to reach a whitelist.
|
|
946
|
+
|
|
947
|
+
`@object-ui/fields` keeps its own key list and its compile-time bindings, and now executes them through core's mechanism. Its behaviour is unchanged and its exported `DomProps<P>` is the same structural type. The two lists differ for measured reasons and no longer can drift silently: `name` and `disabled` are legal only on form controls, which is what every field widget renders and what `FieldWidgetComponentProps` declares, while `role` is resolved by `SchemaRenderer` for every SDUI node and is not part of the field contract. A new assertion binds every shared key in both directions, with `role` named as the single deliberate exception.
|
|
948
|
+
|
|
949
|
+
- 0f21348: Currency amounts now follow each currency's own ISO 4217 fraction-digit
|
|
950
|
+
convention instead of a hardcoded 2 (objectui#4361).
|
|
951
|
+
|
|
952
|
+
Both currency formatting paths in `@object-ui/fields` picked a fraction-digit
|
|
953
|
+
width and handed it to `Intl.NumberFormat`, which OVERRIDES the digit count
|
|
954
|
+
`Intl` already knows for the currency being rendered. `formatCurrency` derived
|
|
955
|
+
its width from the VALUE's wholeness alone (`isWhole ? 0 : 2` — a literal 2 for
|
|
956
|
+
every currency on earth), and `CurrencyField` defaulted an undeclared
|
|
957
|
+
`precision` to the same literal. So a yen amount was printed with cents the
|
|
958
|
+
currency does not have and a dinar amount with one digit fewer than it does:
|
|
959
|
+
|
|
960
|
+
| | before | after |
|
|
961
|
+
| ------------ | -------------- | ----------- |
|
|
962
|
+
| JPY `1234.5` | `¥1,234.50` | `¥1,235` |
|
|
963
|
+
| KWD `1.5` | `KWD 1.50` | `KWD 1.500` |
|
|
964
|
+
| CLP `1234.5` | `CLP 1,234.50` | `CLP 1,235` |
|
|
965
|
+
| BHD `2.5` | `BHD 2.50` | `BHD 2.500` |
|
|
966
|
+
| USD `1234.5` | `$1,234.50` | `$1,234.50` |
|
|
967
|
+
| USD `1234` | `$1,234` | `$1,234` |
|
|
968
|
+
|
|
969
|
+
Both call sites now derive the width from the currency itself
|
|
970
|
+
(`Intl.NumberFormat(undefined, { style: 'currency', currency })
|
|
971
|
+
.resolvedOptions().maximumFractionDigits`, memoized per code) and switch
|
|
972
|
+
wholeness against THAT.
|
|
973
|
+
|
|
974
|
+
**The whole-number convention is extended, not retired.** Simply dropping both
|
|
975
|
+
bounds and letting `Intl` decide would have fixed the digit count while turning
|
|
976
|
+
`$1,234` back into `$1,234.00` — the Salesforce convention `formatCurrency`
|
|
977
|
+
documents and objectui#4033 pinned. A whole amount still drops the fraction, now
|
|
978
|
+
for every currency: `KWD 1` renders `KWD 1`, not `KWD 1.000`. Two-decimal
|
|
979
|
+
currencies are byte-identical to before, which is why the objectui#4033 and
|
|
980
|
+
objectui#4332 pins pass unchanged.
|
|
981
|
+
|
|
982
|
+
**On `CurrencyField`, an explicitly authored `precision` still wins** — it is
|
|
983
|
+
authored metadata and authored metadata keeps priority, so a JPY field declaring
|
|
984
|
+
`precision: 2` still renders `¥1,234.50`. Only an ABSENT `precision` derives from
|
|
985
|
+
the currency; because that derivation is the widget's one precision, it also
|
|
986
|
+
reaches the spinner `step` and the blur rounding, so a JPY field no longer offers
|
|
987
|
+
a `0.01` step for a currency with no minor unit. Whether a declared `precision`
|
|
988
|
+
that contradicts the currency's ISO 4217 digits should be REJECTED at publish
|
|
989
|
+
time is a contract question, filed upstream in `@objectstack/spec` rather than
|
|
990
|
+
answered here by overriding the author.
|
|
991
|
+
|
|
992
|
+
Reachable wherever the resolved currency is not a 2-decimal one — the field's
|
|
993
|
+
`currency`, `currencyConfig.defaultCurrency`, or the tenant default (ADR-0053).
|
|
994
|
+
|
|
995
|
+
- d2e2caf: fix(fields): `formatCurrency` keeps both cents digits on a fractional amount
|
|
996
|
+
|
|
997
|
+
The symbol branch passed `minimumFractionDigits: 0` against a
|
|
998
|
+
wholeness-switched `maximumFractionDigits`, which handed `Intl` the range
|
|
999
|
+
`[0, 2]` — and `Intl` emits the shortest representation in range, so a real
|
|
1000
|
+
cents value of `.50` was printed as `.5`. Any price ending in a zero cent digit
|
|
1001
|
+
rendered one digit short: `$1,234.50` as `$1,234.5`, `$19.90` as `$19.9`,
|
|
1002
|
+
`$0.50` as `$0.5` — money on a record page and in grid cells reading as a data
|
|
1003
|
+
error rather than a formatting one.
|
|
1004
|
+
|
|
1005
|
+
Both bounds now take the same wholeness-switched width, so the function
|
|
1006
|
+
delivers the contract its own doc comment states: a fractional amount shows
|
|
1007
|
+
exactly two digits, a whole amount still drops `.00` (`$1,234`). The
|
|
1008
|
+
no-currency branch and the bad-currency fallback already behaved this way; only
|
|
1009
|
+
the symbol branch disagreed.
|
|
1010
|
+
|
|
1011
|
+
Reaches every consumer of the shared helper: `CurrencyCellRenderer`,
|
|
1012
|
+
`ObjectGrid`, the dashboard `recordFields` and `ObjectGantt`.
|
|
1013
|
+
|
|
1014
|
+
- 3a9021e: fields: the currency adornment has one symbol channel
|
|
1015
|
+
|
|
1016
|
+
`CurrencyField` carried the same one-entry fact twice — a dead `CURRENCY_SYMBOLS`
|
|
1017
|
+
map that nothing read, and a live `currency === 'USD' ? '$' : currency` ternary
|
|
1018
|
+
two lines below it. Both were hand copies of knowledge `Intl` already carries,
|
|
1019
|
+
and both are gone: a new `currencySymbol(currency, locale)` beside
|
|
1020
|
+
`currencyFractionDigits()` reads the `currency` part of the very format the
|
|
1021
|
+
widget's readonly branch already renders amounts with.
|
|
1022
|
+
|
|
1023
|
+
USD is unchanged at the display-locale default. Other currencies now show their
|
|
1024
|
+
real symbol instead of the bare ISO code — `€` for EUR, `¥` for JPY, `£` for
|
|
1025
|
+
GBP — which is what the same widget's readonly mode has always displayed; the
|
|
1026
|
+
edit adornment simply stopped disagreeing with it. Currencies CLDR has no symbol
|
|
1027
|
+
for (KWD, BHD, CHF, ISK, CLP) still render their code, exactly as before.
|
|
1028
|
+
|
|
1029
|
+
- 8f60d73: `@object-ui/fields` and `@object-ui/plugin-editor` stop publishing their test declarations
|
|
1030
|
+
|
|
1031
|
+
Both packages' build tsconfigs set `include: ["src"]` with no test exclude, so every test file entered the declaration program and its `.d.ts` was written into `dist/`. Both are published (`private` is false, `files` contains `dist`), so those declarations shipped: 85 from `@object-ui/fields` and one from `@object-ui/plugin-editor`. Adding the test exclude the other twenty-odd packages already use removes them.
|
|
1032
|
+
|
|
1033
|
+
Nothing else about either artifact moves. Measured by building each package both ways from a cleared `dist/`, then diffing the file lists: `@object-ui/fields` goes from 163 files to 78 and `@object-ui/plugin-editor` from 6 to 5, every one of the 86 disappearances is a `*.test.d.ts`, no file appears, and all 83 surviving files are byte-identical by sha256 — including each package's entry `dist/index.d.ts`. The entry type surface is therefore unchanged and no import can break; this is the tarball shedding files nothing resolved.
|
|
1034
|
+
|
|
1035
|
+
The type coverage those files were a side effect of did not go with them. Because the build program read the tests, these two packages counted as "tests type-checked" in `scripts/check-type-check-coverage.mjs` — a correct verdict reached through an emit nobody wanted. Excluding the tests alone would have silently dropped 86 test files out of every `tsc` program, so the same change adds a `tsconfig.test.json` per package, chained from each package's `type-check` script, and the coverage gate stays at 41 of 41 packages compiling their tests with zero declared debt on both sides of the change.
|
|
1036
|
+
|
|
1037
|
+
- cb13400: One fullscreen long-text editor, hoisted to the package both render paths may import
|
|
1038
|
+
|
|
1039
|
+
The "expand to a full-height dialog" interaction had two independent implementations. `FullscreenTextarea` lived inside the form renderer's built-in (unregistered) `textarea` branch in `@object-ui/components`; `FullscreenFieldEditor` lived in `@object-ui/fields` and served the registered `TextAreaField` / `RichTextField` widgets. They exist because ONE form-level promise — `ObjectFormSchema.mobile.fullscreenLongText`, projected onto every long-text field as `mobile_fullscreen` — is honoured on two render paths, and each path grew its own answer.
|
|
1040
|
+
|
|
1041
|
+
Two copies of a state machine drift, and these did, in both directions: objectui#3400 measured a read-only long-text field that was fully editable through the built-in branch's dialog (and "Done" wrote the edit into form state), objectui#3402 measured the same write-back hole for `disabled` on the registered path, and objectui#3393 (the dialog title needs the field label) and objectui#3272 (the copy needs i18n) each landed on one side before the other. Every repair was correct and none of them scaled.
|
|
1042
|
+
|
|
1043
|
+
`@object-ui/components` now exports `FullscreenEditor`, a single primitive owning the affordance, the dialog, the draft/commit state machine and the copy. The direction follows the measured import graph rather than fighting it: `@object-ui/fields` depends on `@object-ui/components`, and `components` declares no dependency on `fields` in either `dependencies` or `peerDependencies`, so the shared code can only live in `components`. `FullscreenFieldEditor` becomes a thin wrapper over it and keeps its name, its props and its test-id namespaces, so both hosts and their pins are unchanged.
|
|
1044
|
+
|
|
1045
|
+
The load-bearing part of the merge is that the primitive DEFINES `readOnly` and `disabled` instead of inheriting them by accident. Neither copy defined both: the built-in one grew them under objectui#3400, while the fields one declared only `disabled` and was shielded from `readonly` by its hosts' early return — a single implementation cannot be shielded by one caller's control flow. So both are answered once, and both call paths inherit the same answers: `readOnly` renders no affordance at all (it means "shown plainly", so advertising an expand button the user cannot use is worse than showing none), `disabled` leaves an inert one (it means "not interactive, muted"). Neither relies on the toggle alone, because `disabled` also carries the form's `isSubmitting` and can flip to true while the dialog is already open — so opening refuses independently of the attribute, the injected editor is told, "Done" is disabled, and `onCommit` is gated as the single point where a value leaves for host state.
|
|
1046
|
+
|
|
1047
|
+
No copy changed and no locale pack needed an edit: the primitive consumes the same `form.fullscreen.*` / `common.cancel` keys both copies already read, through `createSafeTranslation` with English defaults byte-identical to the literals, so provider-less hosts render exactly what they did. The now-unread `form.fullscreen.*` defaults are dropped from `useFieldTranslation`, where they would have re-created in the defaults map precisely the duplication this change removes from the components.
|
|
1048
|
+
|
|
1049
|
+
`toggleClassName` is not carried into the new primitive. It was declared on `FullscreenFieldEditorProps` and written by nobody — zero producers repo-wide — and `FullscreenFieldEditor` is not exported from the `@object-ui/fields` barrel, so no consumer outside the package could ever have set it. Minting it as part of a NEW public export in `@object-ui/components` would have published a prop with no producer, the shape objectui#3232/#3233 keeps deleting.
|
|
1050
|
+
|
|
1051
|
+
- bc64bfe: A dependency-gated option list no longer deletes the field's stored value on mount
|
|
1052
|
+
|
|
1053
|
+
The four fixed-option widgets (`SelectField`, `MultiSelectField`, `CheckboxesField`, `RadioField`) end their cascade resolution with a "drop what is no longer offered" effect, and the form renderer runs an equivalent clear of its own over every option field. Both read `resolveCascadingOptions`, which returns an **empty** offered set whenever the list is _gated_ — a declared `dependsOn` parent is still empty. Nothing the field held could be "still offered" against an empty set, so both paths wrote the field empty **on mount, with no interaction**, while the control rendered "Select Country first" beside it: it told the user it could not offer anything, and deleted what they had.
|
|
1054
|
+
|
|
1055
|
+
Gated means **unknown**, not invalid. The cascade clear exists (ADR-0058) so a user-driven parent change prunes a now-invalid child; a withheld list on mount is missing information — the record simply arrived with its controlling field empty (a later-cleared parent, an import, a partially-migrated row) — and that is not a reason to destroy stored data. Both clears now skip while gated, reading the resolver's own `gated` flag rather than re-deriving it from an empty offered set, which would collide with the distinct never-configured case guarded separately in objectui#4220.
|
|
1056
|
+
|
|
1057
|
+
Convergence stays exactly where it belongs: once the parent **is** chosen and the resolved set genuinely excludes the stored value, the prune applies unchanged — including at the moment the gate lifts, so picking a parent whose list does not contain the old value still clears it on that transition. The three states are pinned apart (never-configured / gated / resolved-and-excludes) across all four widgets and the form host, so a future edit cannot collapse them back into one empty-set test.
|
|
1058
|
+
|
|
1059
|
+
Reachable on every host that mounts these widgets with a live record: the form renderer, the grid's inline cell editor, and the detail page's inline editor — where each `onChange` went straight into the record draft the save bar commits.
|
|
1060
|
+
|
|
1061
|
+
- 3e19fe7: i18n copy: one ellipsis glyph across the ten packs, `usted` in the es draft-preview empty state, and a pt sentence that stops contracting `de` onto its own hole
|
|
1062
|
+
|
|
1063
|
+
Three locale-copy defects that no gate could see, because all three are _value_ defects on keys whose names, placeholders and key sets were already correct.
|
|
1064
|
+
|
|
1065
|
+
**One ellipsis (objectui#3878).** `en` ended 33 values with three ASCII full stops (`Loading...`, `Ask anything...`) and 110 with the typographic ellipsis `…`, and the nine translation packs had copied `en` value by value — so a user could read both glyphs on one screen: `common.loading` beside `dashboard.loading`, `console.ai.askAnything` beside its own panel's siblings. All ten packs now spell it `…` (U+2026), per the maintainer-authorized consistency pass registered on objectstack#6015. 312 pack values changed: 34 in `en` (the 33 trailing plus the one mid-sentence `collaboration.commentPlaceholder`) and 278 across the nine. Eleven inline `defaultValue` call sites were re-synchronised with the new `en` text, which `scripts/check-i18n-call-site-keys.mjs` requires byte-for-byte.
|
|
1066
|
+
|
|
1067
|
+
The convention is now pinned so the split cannot regrow: `packages/i18n/src/__tests__/ellipsis-glyph-3878.test.ts` fails, by key name, on any value in any of the ten packs that holds three ASCII full stops. It is deliberately wider than "a trailing `...` in `en`", because the census showed the narrow rule would have shipped with two holes in it — `collaboration.commentPlaceholder` puts the ellipsis mid-sentence, and `list.loading` had the packs wrong while `en` was already right, which no `en`-only rule can see.
|
|
1068
|
+
|
|
1069
|
+
Fifteen module-local **no-provider fallback** entries were moved with the packs, across `useCollaborationTranslation`, `useFieldTranslation`, `useDetailTranslation`, `ObjectGrid`, `KanbanImpl`, `data-table` and `ConnectionStatus`. Those maps exist to render when no `LocalizationProvider` is mounted, and each one's own docblock requires it to stay byte-identical to the `en` pack — a requirement objectui#3440 already enforces mechanically for the collaboration map. Leaving them behind would have made the provider-less path disagree with the provider path on ten keys.
|
|
1070
|
+
|
|
1071
|
+
**es `usted` (objectui#3875).** `preview.empty.notReadyDescription` said `Revisa la conversación` — the tú imperative — in a namespace that is otherwise 23:1 usted, and it renders _underneath the usted draft-preview banner at the same moment_, not before or after it. `Revisa` → `Revise`; nothing else in the sentence carries a register. The neighbouring `approvalsInbox` namespace is legitimately tú and was left alone.
|
|
1072
|
+
|
|
1073
|
+
**pt contraction (objectui#3877).** `ConcurrentUpdateDialog` splits `detail.concurrentUpdateDescription` on `{{field}}` and renders a bolded label in the gap, and pt left a bare `de` in front of that gap. When the multi-field conflict branch passes the record label (`este registro`), Portuguese users read `de este registro` — a contraction error every native speaker sees, and one that no spelling of the leaf value could fix (`deste registro` renders `de deste registro`). The pt sentence is rewritten so the hole is preceded by the verb `afeta` instead of any preposition, which closes the whole class rather than trading `de` for an `em` or `a` that contract just as hard. pt only; `en` is unchanged.
|
|
1074
|
+
|
|
1075
|
+
No behavior, no keys added or removed, no placeholder changed.
|
|
1076
|
+
|
|
1077
|
+
- bb58d1d: i18n: the two search placeholders become pack values, and four values the packs served in English get translated
|
|
1078
|
+
|
|
1079
|
+
**objectui#4375** — `ListView` and `LookupField` built their search placeholder as
|
|
1080
|
+
`t(key) + '...'`, so the ellipsis was a literal concatenated in code: it stayed ASCII
|
|
1081
|
+
in all ten locales on screens where objectui#3878 had converged everything else on
|
|
1082
|
+
U+2026, and no pack could opt out of it (sharpest in `ar`, where a left-to-right run
|
|
1083
|
+
was appended to right-to-left text). Both now read `table.search`, which is already
|
|
1084
|
+
the repo's search-input placeholder key — `data-table`, `RecordPickerDialog` and
|
|
1085
|
+
`PeoplePicker` render it too — and is translated with the right ellipsis in all ten
|
|
1086
|
+
packs. No new keys.
|
|
1087
|
+
|
|
1088
|
+
**objectui#4376** — `list.loading` served the English `Loading records…` in eight of
|
|
1089
|
+
the nine translation packs (`zh` alone had translated it); `designer.undo` and
|
|
1090
|
+
`designer.redo` were English in all nine; `appDesigner.snakeCaseHint` in `ko`, `pt`,
|
|
1091
|
+
`ru` and `ar`. All translated, reusing each pack's own established vocabulary. A new
|
|
1092
|
+
pin (`untranslated-identity-4376.test.ts`) fails on any value byte-identical to `en`
|
|
1093
|
+
inside a non-Latin pack unless the key is on an explicit 22-entry allowlist.
|
|
1094
|
+
|
|
1095
|
+
- 433ff9f: An image field's declared `maxSize` is enforced before the upload starts, not after it finishes
|
|
1096
|
+
|
|
1097
|
+
`ImageField` received a `maxSize` and ignored it. `paramToField` copies `maxSize` onto the field config for every action param regardless of type, so an image param declared with a 5 MB limit handed the widget its constraint and the widget uploaded anyway: a 6.3 MB PNG fired the full `presigned → PUT → complete` chain and rendered a thumbnail, with no rejection anywhere. The sibling file param, declared the same way, refused the identical pick without a single request. Reported from a QA run driving the two side by side (objectui#4141).
|
|
1098
|
+
|
|
1099
|
+
Both of the widget's upload doors now check the limit first. The native picker rejects oversize picks before any request, keeping FileField's partial-acceptance rule — the in-limit members of a multi-select still upload, and only the oversize ones are reported. The crop dialog is the second door and needed the check in its own right: the cropper re-encodes to PNG, so cropping an in-limit JPEG can produce a blob _over_ the limit, and it is the crop's size that is uploaded. A rejected pick or crop now surfaces the same message the file widget has always shown, in a new error row — this widget had no surface for rejections before, because it never rejected anything.
|
|
1100
|
+
|
|
1101
|
+
The guard itself moved into one shared `maxSizeError` helper that both widgets call, rather than a second copy living in the image widget. The check is the only thing between a declared limit and a real upload, and a per-widget copy is what let these two drift apart unnoticed in the first place. Both widgets also share the existing `fields.file.exceedsMaxSize` message: it names a file and a limit, says nothing file-specific, and is already translated in all built-in locales, so no new key was added and no translation is pending. FileField's own behavior is unchanged — same threshold, same message, same partial acceptance.
|
|
1102
|
+
|
|
1103
|
+
An undeclared `maxSize` still means unrestricted; the falsy check is preserved deliberately, so a missing limit can never be read as a zero-byte one.
|
|
1104
|
+
|
|
1105
|
+
- e7663f2: fix(detail): inline edit no longer destroys array values or flattens types on the record page
|
|
1106
|
+
|
|
1107
|
+
`InlineFieldInput`'s type switch ended in a raw text input, and every type it had
|
|
1108
|
+
no branch for landed there: the value was displayed through `coerceToSafeValue`
|
|
1109
|
+
and written back as whatever the user typed — a bare string.
|
|
1110
|
+
|
|
1111
|
+
Two damage classes survived the earlier passes. Array-valued fields (`tags`,
|
|
1112
|
+
`checkboxes`, an options-less multi picklist) were offered for editing as
|
|
1113
|
+
`"a, b"` — `coerceToSafeValue` joins arrays — and saved back as that string, so
|
|
1114
|
+
the array was gone. Type-lossy scalars (`toggle`, `slider`, `progress`,
|
|
1115
|
+
`rating`, `radio`) round-tripped through `String()`, so a boolean column
|
|
1116
|
+
received `"true"`, a numeric one `"42"`, and `radio` accepted any free-typed
|
|
1117
|
+
value its option list never offered.
|
|
1118
|
+
|
|
1119
|
+
Types the switch already routes keep their editors. Everything else that the
|
|
1120
|
+
fields package can edit inline now falls back to `FieldEditWidget` — the same
|
|
1121
|
+
control the form renders, `json` → the code editor included — and only genuinely
|
|
1122
|
+
string-valued types (`text`, `textarea`, `email`, `phone`, `url`) keep the plain
|
|
1123
|
+
input. A drift guard asserts every field type is exactly one of routed /
|
|
1124
|
+
excluded / delegated / benign, so a new type can no longer inherit the
|
|
1125
|
+
value-destroying default in silence.
|
|
1126
|
+
|
|
1127
|
+
`@object-ui/fields`: the four fixed-option widgets no longer clear the stored
|
|
1128
|
+
value when the field declares no `options` at all. An empty offered set had two
|
|
1129
|
+
opposite causes — a list that cascaded to zero (clear) and a list that was never
|
|
1130
|
+
authored (nothing to decide) — and the second deleted the value on mount, which
|
|
1131
|
+
the grid's inline cell editor has always been able to trigger. `FieldEditWidget`
|
|
1132
|
+
also forwards `autoFocus` to the widget it renders.
|
|
1133
|
+
|
|
1134
|
+
- b953a97: fix(detail): lookup field values link to the referenced record
|
|
1135
|
+
|
|
1136
|
+
A valued lookup on a record detail page rendered as plain text plus a copy
|
|
1137
|
+
button — the referenced document's name was visible but unreachable, so users
|
|
1138
|
+
copied the number and searched for it from the list page instead. Lookup cells
|
|
1139
|
+
inside a related list that pointed at a third object were dead the same way.
|
|
1140
|
+
|
|
1141
|
+
`LookupCellRenderer` — the one cell renderer both surfaces resolve through —
|
|
1142
|
+
now renders the display value as a link to the referenced record. The display
|
|
1143
|
+
name resolution, the copy affordance and every non-lookup field are unchanged,
|
|
1144
|
+
and a lookup with no value still renders its placeholder rather than an empty
|
|
1145
|
+
link.
|
|
1146
|
+
|
|
1147
|
+
The URL is not assembled in the renderer. `RelatedRecordActionsContext` gains
|
|
1148
|
+
an optional `recordHref` / `openRecord` pair, published by the console's
|
|
1149
|
+
`RelatedRecordActionsBridge` from the SAME builder its related-list row
|
|
1150
|
+
navigation already used, so there is one record-route shape rather than a
|
|
1151
|
+
second one. A host that does not provide it (Studio designer, embedded
|
|
1152
|
+
renderers, standalone grids) renders exactly what it rendered before.
|
|
1153
|
+
|
|
1154
|
+
- 45e1949: Numbers render in the user's locale, and a `Field.number` year is no longer `2,026`
|
|
1155
|
+
|
|
1156
|
+
Every numeric field the console rendered went through an `Intl.NumberFormat` built with the locale hardcoded to `en-US` and `useGrouping` never set. Two defects rode in that one construction: a `zh-CN` or `de-DE` console still grouped and pointed decimals the US way, and a four-digit **year** stored as `Field.number({ scale: 0 })` rendered as `2,026` — in every locale, with no field property able to turn it off. Apps had been converting year columns to `Field.text` to escape it, permanently trading numeric comparison, range filters and dataset dimension types for a display detail.
|
|
1157
|
+
|
|
1158
|
+
The construction had been copied into five places — the number cell renderer, the currency cell renderer, the `CurrencyField` widget, the compact `formatNumber` helper, and the dashboard `MetricWidget` — so fixing any one surface never changed the answer. They now share one formatter, `formatDisplayNumber` in `@object-ui/i18n`, which owns the locale and the grouping policy together, plus one locale resolver, `useDisplayLocale`.
|
|
1159
|
+
|
|
1160
|
+
`useDisplayLocale` composes the two locale channels this repo already had rather than adding a third: the tenant's regional default (`useLocalization().locale`, ADR-0053) when an org has configured one, otherwise the active UI language (`useObjectTranslation().language`) so grouping and decimal marks follow a language switch. That second step is what covers the case the report was measured in — a fresh database, where the tenant localization endpoint has no locale to give.
|
|
1161
|
+
|
|
1162
|
+
Grouping is now suppressed when a field declares `scale: 0` and carries no currency, which is what makes years, fiscal periods and other ordinals render plainly. This is an **interim default** with an accepted cost: a large scale-0 _count_ loses its separators too. It holds only until the spec gains an authorable presentation hint, which is being specified separately, contract-first; when that lands it overrides this heuristic.
|
|
1163
|
+
|
|
1164
|
+
Three surfaces deliberately keep their separators, because a zero-decimal display there does not come from a field declaration: the dashboard `MetricWidget` (its decimals are parsed from a numeral.js format pattern, and its own contract calls the separators load-bearing — "`1,930,000` not `1930000`"), the `element:number` aggregate renderer, and every currency path including amounts whose currency code could not be resolved. An **undeclared** `scale` also keeps grouping — absent means "decimals unknown", not "integer".
|
|
1165
|
+
|
|
1166
|
+
`formatCurrency`, `formatCompactCurrency` and `formatNumber` each take a new optional trailing `locale` argument. Existing calls are unaffected; omitting it now follows the runtime default rather than forcing US conventions.
|
|
1167
|
+
|
|
1168
|
+
- ac853ce: i18n: retire the reader-less `common.search` key from all ten locale packs
|
|
1169
|
+
|
|
1170
|
+
`common.search` (`Search`, no ellipsis) had exactly one consumer: `LookupField`
|
|
1171
|
+
built its dialog placeholder by concatenating the key with three ASCII full
|
|
1172
|
+
stops. objectui#4375 / PR #4391 retired that concatenation — the placeholder is
|
|
1173
|
+
the reused `table.search` pack value (`Search…`, one U+2026 glyph), which is what
|
|
1174
|
+
brought it under objectui#3878's glyph pin. That left `common.search` with zero
|
|
1175
|
+
readers repo-wide while it still existed in all ten packs.
|
|
1176
|
+
|
|
1177
|
+
Re-verified before deleting, repo-wide: no `t()` call site in any package or app,
|
|
1178
|
+
no MDX or JSON reference, and the one dynamic template-literal reader of the
|
|
1179
|
+
`common` namespace takes a two-member union parameter (`'openChat' |
|
|
1180
|
+
'closeChat'`) that cannot resolve to it. No user-visible string changes — this key never rendered.
|
|
1181
|
+
|
|
1182
|
+
The dormant copy in `@object-ui/fields`' no-provider fallback table
|
|
1183
|
+
(`useFieldTranslation.ts`'s `FIELD_DEFAULTS`) goes with it. That table is a
|
|
1184
|
+
module-local `Record<string, string>` read only when no `LocalizationProvider`
|
|
1185
|
+
is mounted; it is not exported, so removing an entry no reader asks for changes
|
|
1186
|
+
no rendered output and narrows no public type. Hence patch for that package,
|
|
1187
|
+
while the pack change is a minor: deleting a key from `en` narrows the exported
|
|
1188
|
+
`TranslationKeys` type (`typeof en`), so code indexing `TranslationKeys` at
|
|
1189
|
+
`common.search` stops type-checking. Same grading, for the same reason, as
|
|
1190
|
+
objectui#4145's `report.editor.*` retirement. No runtime consumer existed to
|
|
1191
|
+
break.
|
|
1192
|
+
|
|
1193
|
+
Retiring a key from `common` was the ruled decision on objectui#4392 rather than
|
|
1194
|
+
keeping it as vocabulary: nothing pins a dormant key's meaning, so its next
|
|
1195
|
+
reader inherits an unreviewed contract, and a dormant key beside a live
|
|
1196
|
+
`table.search` is where a second dialect gets started. The objectui#4328
|
|
1197
|
+
dead-surface family has consistently chosen removal for zero-consumer surfaces.
|
|
1198
|
+
|
|
1199
|
+
The neighbouring `common.select` (minted one commit earlier by objectui#4386 /
|
|
1200
|
+
PR #4397) is a different key and is untouched.
|
|
1201
|
+
|
|
1202
|
+
A negative pin (`packages/i18n/src/__tests__/common-search-retired-4392.test.ts`)
|
|
1203
|
+
fails if the key returns to any pack, if any package reads or re-declares it, or
|
|
1204
|
+
if a dynamic `common.*` reader grows a `search` member — every existing i18n gate
|
|
1205
|
+
runs call site to key, and none of them can see a key with no call site.
|
|
1206
|
+
|
|
1207
|
+
- 06915b0: fix(i18n): every date branch threads the active locale, so a `zh` session no longer renders half its dates in English
|
|
1208
|
+
|
|
1209
|
+
Date rendering had two locale channels and only one followed the user's
|
|
1210
|
+
language, so the same row could read `逾期 6 天` in one column and `In 3 days`
|
|
1211
|
+
in the next, with a datetime column showing `8/11/2026 12:00 am`
|
|
1212
|
+
(objectui#4468).
|
|
1213
|
+
|
|
1214
|
+
The overdue phrase resolves through the translate fn (the active UI language),
|
|
1215
|
+
while every `Intl` branch took its tag from the raw tenant locale
|
|
1216
|
+
(`useLocalization().locale`) — which is `undefined` on any workspace that never
|
|
1217
|
+
configured one, and `undefined` makes `Intl` use the _machine's_ locale.
|
|
1218
|
+
`DateTimeCellRenderer` passed no tag at all.
|
|
1219
|
+
|
|
1220
|
+
Every date-formatting site in `@object-ui/fields` now resolves through the one
|
|
1221
|
+
existing channel, `useDisplayLocale()` (tenant regional default → active UI
|
|
1222
|
+
language → `en`): `DateCellRenderer` (relative past, relative future, near-today
|
|
1223
|
+
and the beyond-±7-days absolute fallback), `DateTimeCellRenderer`, the read-only
|
|
1224
|
+
`DateField` / `DateTimeField` / `FormulaField` faces, and the sub-grid's
|
|
1225
|
+
temporal cells. English output is unchanged, and the already-localized overdue
|
|
1226
|
+
wording is untouched.
|
|
1227
|
+
|
|
1228
|
+
No public signature changed. `@object-ui/i18n` carries a documentation
|
|
1229
|
+
correction only: `useDisplayLocale`'s docstring claimed `DateCellRenderer`
|
|
1230
|
+
already formatted from this channel, which was the very thing that was not true.
|
|
1231
|
+
|
|
1232
|
+
- Updated dependencies [0e67b53]
|
|
1233
|
+
- Updated dependencies [ceccdcf]
|
|
1234
|
+
- Updated dependencies [d6e5124]
|
|
1235
|
+
- Updated dependencies [debad27]
|
|
1236
|
+
- Updated dependencies [dc2aa3e]
|
|
1237
|
+
- Updated dependencies [ee66e2e]
|
|
1238
|
+
- Updated dependencies [ee26e65]
|
|
1239
|
+
- Updated dependencies [5900ac5]
|
|
1240
|
+
- Updated dependencies [932cbcd]
|
|
1241
|
+
- Updated dependencies [734d186]
|
|
1242
|
+
- Updated dependencies [f650253]
|
|
1243
|
+
- Updated dependencies [3d9769a]
|
|
1244
|
+
- Updated dependencies [8f85f8b]
|
|
1245
|
+
- Updated dependencies [d0c3b26]
|
|
1246
|
+
- Updated dependencies [3fc2971]
|
|
1247
|
+
- Updated dependencies [aca27fa]
|
|
1248
|
+
- Updated dependencies [dde7283]
|
|
1249
|
+
- Updated dependencies [f7c6430]
|
|
1250
|
+
- Updated dependencies [4dadf0d]
|
|
1251
|
+
- Updated dependencies [ae10a01]
|
|
1252
|
+
- Updated dependencies [92876f0]
|
|
1253
|
+
- Updated dependencies [f279deb]
|
|
1254
|
+
- Updated dependencies [4b70d28]
|
|
1255
|
+
- Updated dependencies [eb7f586]
|
|
1256
|
+
- Updated dependencies [e901131]
|
|
1257
|
+
- Updated dependencies [d9d3463]
|
|
1258
|
+
- Updated dependencies [2a40f69]
|
|
1259
|
+
- Updated dependencies [bec3e14]
|
|
1260
|
+
- Updated dependencies [613b167]
|
|
1261
|
+
- Updated dependencies [b4d3c22]
|
|
1262
|
+
- Updated dependencies [1f9b905]
|
|
1263
|
+
- Updated dependencies [cb13400]
|
|
1264
|
+
- Updated dependencies [828549a]
|
|
1265
|
+
- Updated dependencies [e1ade8f]
|
|
1266
|
+
- Updated dependencies [bc64bfe]
|
|
1267
|
+
- Updated dependencies [abb0f81]
|
|
1268
|
+
- Updated dependencies [38ab505]
|
|
1269
|
+
- Updated dependencies [3e19fe7]
|
|
1270
|
+
- Updated dependencies [bb58d1d]
|
|
1271
|
+
- Updated dependencies [5cc847c]
|
|
1272
|
+
- Updated dependencies [fa21254]
|
|
1273
|
+
- Updated dependencies [33c32bf]
|
|
1274
|
+
- Updated dependencies [66fb4fa]
|
|
1275
|
+
- Updated dependencies [b953a97]
|
|
1276
|
+
- Updated dependencies [d7f3e30]
|
|
1277
|
+
- Updated dependencies [6d641c9]
|
|
1278
|
+
- Updated dependencies [7e4f0e5]
|
|
1279
|
+
- Updated dependencies [a84385b]
|
|
1280
|
+
- Updated dependencies [45e1949]
|
|
1281
|
+
- Updated dependencies [92250d6]
|
|
1282
|
+
- Updated dependencies [c1d939f]
|
|
1283
|
+
- Updated dependencies [58bebf6]
|
|
1284
|
+
- Updated dependencies [405e808]
|
|
1285
|
+
- Updated dependencies [49ae9f4]
|
|
1286
|
+
- Updated dependencies [a3ae404]
|
|
1287
|
+
- Updated dependencies [bfdf3d4]
|
|
1288
|
+
- Updated dependencies [bb68488]
|
|
1289
|
+
- Updated dependencies [c0f9a4b]
|
|
1290
|
+
- Updated dependencies [b1e42d0]
|
|
1291
|
+
- Updated dependencies [2459a3e]
|
|
1292
|
+
- Updated dependencies [ac853ce]
|
|
1293
|
+
- Updated dependencies [fa51109]
|
|
1294
|
+
- Updated dependencies [d6aa172]
|
|
1295
|
+
- Updated dependencies [fe52a04]
|
|
1296
|
+
- Updated dependencies [d46f9b8]
|
|
1297
|
+
- Updated dependencies [3f5f87c]
|
|
1298
|
+
- Updated dependencies [2fea4d2]
|
|
1299
|
+
- Updated dependencies [f5e1143]
|
|
1300
|
+
- Updated dependencies [7f1cb33]
|
|
1301
|
+
- Updated dependencies [f148a64]
|
|
1302
|
+
- Updated dependencies [bb68488]
|
|
1303
|
+
- Updated dependencies [2e3b0c0]
|
|
1304
|
+
- Updated dependencies [9461dd3]
|
|
1305
|
+
- Updated dependencies [78fa331]
|
|
1306
|
+
- Updated dependencies [47f551b]
|
|
1307
|
+
- Updated dependencies [31ab1ac]
|
|
1308
|
+
- Updated dependencies [0082db8]
|
|
1309
|
+
- Updated dependencies [ab04728]
|
|
1310
|
+
- Updated dependencies [5bf09fd]
|
|
1311
|
+
- Updated dependencies [06915b0]
|
|
1312
|
+
- Updated dependencies [ff84b05]
|
|
1313
|
+
- @object-ui/i18n@17.5.0
|
|
1314
|
+
- @object-ui/react@17.5.0
|
|
1315
|
+
- @object-ui/components@17.5.0
|
|
1316
|
+
- @object-ui/core@17.5.0
|
|
1317
|
+
- @object-ui/types@17.5.0
|
|
1318
|
+
- @object-ui/providers@17.5.0
|
|
1319
|
+
|
|
1320
|
+
## 17.4.0
|
|
1321
|
+
|
|
1322
|
+
### Minor Changes
|
|
1323
|
+
|
|
1324
|
+
- ecae400: Retire the `capability-multiselect` field widget name, which existed only on the docs-site registration path and which nothing ever stamped (objectui#3308, ADR-0049 enforce-or-remove)
|
|
1325
|
+
|
|
1326
|
+
`field:capability-multiselect` was registered by `registerFields()` and only there. That function's sole caller is the docs site, so the key never existed on the live path (`registerAllFields()`, run at module import, iterates `fieldWidgetMap` — which never listed it). A field authored with `widget: 'capability-multiselect'` therefore resolved to nothing in every real application, while the comment above the registration described it as usable from a record form: a code comment promising a capability that does not exist, which is the worst direction for a metadata renderer AI-authored apps read as authority.
|
|
1327
|
+
|
|
1328
|
+
Nothing stamped the hint either. ADR-0056 P1 stamps `permission-facet-link` on all six `sys_permission_set` facets — `system_permissions` included — through the single `ObjectStackAdapter.getObjectSchema` choke point, and P2 put the capability editor in Studio. The widget name was a leftover from an intermediate iteration of that rollout.
|
|
1329
|
+
|
|
1330
|
+
Removed, with a tombstone at each site:
|
|
1331
|
+
|
|
1332
|
+
- `@object-ui/fields` — the `field:capability-multiselect` registration and the comment that advertised it. **Breaking in name only**: the key was unreachable outside the docs site, so no application could have resolved it. A field still carrying the hint now degrades to its declared `type` renderer, the defined behavior for an unregistered widget.
|
|
1333
|
+
- `@object-ui/plugin-detail` — `InlineFieldInput`'s `widget === 'capability-multiselect'` branch, the hint's last honoring surface. Leaving one consumer for a name no producer emits and no form resolves is the same declared-vs-enforced split, inverted. The sibling `permission-facet-link` branch is untouched and pinned.
|
|
1334
|
+
- `@object-ui/components` — the dead `capability-multiselect` entry in the form renderer's `DATA_SOURCE_FIELD_TYPES` set, which could never match a resolvable widget.
|
|
1335
|
+
- `@object-ui/plugin-form` — a comment naming `capability-multiselect` as the widget stamped onto `sys_permission_set.system_permissions`; it names `permission-facet-link` now, which is what is actually stamped.
|
|
1336
|
+
|
|
1337
|
+
`CapabilityMultiSelectField` itself is **unchanged and still exported**: Studio's `PermissionMatrixEditor` imports and renders it directly, which is ADR-0056 P2's design. Only the widget name is retired — the component is not a registry field widget and its doc comment now says so.
|
|
1338
|
+
|
|
1339
|
+
`registerFields()` is also **kept**, with its `@deprecated Use registerAllFields() instead` note corrected. The two are not interchangeable: it registers `createFieldRenderer(widget)`, which synthesizes the label, description and the local `value`/`onChange` state that lets a bare field node (`{ type: 'currency', label: 'Amount' }`) render standalone in the docs demos. Retiring it needs a decision about where that demo chrome goes; the note now records that instead of implying a drop-in replacement.
|
|
1340
|
+
|
|
1341
|
+
- 65bb513: fields: remove the docs-demo registration path (`registerFields` + `createFieldRenderer`), and host the docs field examples in a real form
|
|
1342
|
+
|
|
1343
|
+
**Breaking (shipped as `minor` per AGENTS.md §版本号策略 — objectui's major tracks `@objectstack`, so the repo's own breaking changes are `minor` with the break spelled out here):** two public exports are removed from `@object-ui/fields`:
|
|
1344
|
+
|
|
1345
|
+
- `registerFields()` — registered every widget under the same `field:<type>` keys as `registerAllFields()`, but wrapped in a demo-only renderer. Two paths writing the same registry keys meant whoever ran LAST won it for every consumer sharing the registry.
|
|
1346
|
+
- `createFieldRenderer()` — that wrapper. It synthesized a `label`, a description, and a local `useState`/`onChange` around a widget.
|
|
1347
|
+
|
|
1348
|
+
**Migration:** there is nothing to migrate for an application. `registerAllFields()` runs on import of `@object-ui/fields` and is the one registration seam (`registerField(type)` for a single type); no shipped application ever called the removed pair — the docs site was the only caller. Code that rendered a **bare field node** standalone (`{ type: 'currency', label: 'Amount' }`) and relied on the wrapper for label and value state must host the field in a form instead: `{ type: 'form', fields: [{ name: 'amount', label: 'Amount', type: 'currency' }] }`. Seed values through the form's `defaultValues`, not a field-level `value`.
|
|
1349
|
+
|
|
1350
|
+
**Why the removal rather than a relocation** (ruling B of objectui#3798, confirmed by the maintainer; objectui#3308 is the origin, PR #3793 the safety net that first corrected the misleading `@deprecated` note): the wrapper existed only so the documentation could render a bare field node as a labelled, editable input. No application produces that rendering — on the live path a bare field node has no host for its label or its value. So the field docs, which are a first-hand transcription source for AI authors, were teaching a shape that does not work, and an author copying it got a node with neither label nor `onChange`. Relocating the wrapper into the docs site would have preserved that divergence under a new owner. Hosting the examples in a real form removes the reason for the wrapper to exist: the form renderer already owns label and value state, so the docs can only show what an application actually renders.
|
|
1351
|
+
|
|
1352
|
+
The 74 bare-node examples under `examples/schema-catalog/src/schemas/fields-*` are now form-hosted (the other 2 already were). A field's `value` moved to the form's `defaultValues`, because the form renderer spreads react-hook-form's state after the schema props and a field-level `value` is therefore ignored — a catalog guard now pins that so a dead `value` cannot come back.
|
|
1353
|
+
|
|
1354
|
+
- b19162d: Build and publish `@object-ui/fields/style.css` — the subpath the package has always declared and never shipped
|
|
1355
|
+
|
|
1356
|
+
`packages/fields/package.json` has declared `"./style.css": "./dist/index.css"` for the package's entire life, while its build was `tsc && vite build` and the package contained no `.css` file for Vite to extract. **No published version up to and including 17.3.0 contains a stylesheet** — the `@object-ui/fields@17.3.0` tarball has zero `.css` files in it. The subpath did not merely render badly, it failed to resolve: a consumer writing the `@import '@object-ui/fields/style.css'` that the quick-start guide taught got a build error. This release is the first one where that import works, so it is a new capability rather than a repair of a working one, and no existing consumer can be relying on the old behaviour — an import that never resolved has no working callers.
|
|
1357
|
+
|
|
1358
|
+
Removing the export was the cheaper option and was rejected on a measurement: fields' class surface is not a subset of what `@object-ui/components` publishes. 155 classes exist only here, and 17 of them (`hover:bg-accent/30`, `ring-destructive/50`, `bg-primary/20`, …) resolve `@theme` tokens declared in unpublished package source, so no consumer-side Tailwind configuration can generate them. Dropping the export would have made the field widgets permanently under-styled with no supported remedy.
|
|
1359
|
+
|
|
1360
|
+
The new sheet is a **supplement, not a replacement** — it is compiled against the components theme and then has every rule that package's sheet already ships subtracted from it, so it is ~22 kB rather than another ~180 kB of near-duplicate CSS. Import it after the components sheet:
|
|
1361
|
+
|
|
1362
|
+
```css
|
|
1363
|
+
@import "tailwindcss";
|
|
1364
|
+
@import "@object-ui/components/style.css";
|
|
1365
|
+
@import "@object-ui/fields/style.css";
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
Also adds a workspace-wide guard (`scripts/__tests__/package-files-exist.test.ts`) that fails when any package exports a subpath its published tarball cannot contain, so a stylesheet export with nothing building it cannot recur silently.
|
|
1369
|
+
|
|
1370
|
+
- 1bd6faa: fix(fields,plugin-form): stop the inline child grid from collapsing `datetime`/`time` columns onto the `date` control
|
|
1371
|
+
|
|
1372
|
+
`deriveMasterDetail`'s `fieldTypeToColumnType` mapped `date`, `datetime` and `time` onto the single `date` grid column type, and `GridField` renders that as `<input type="date">`. The consequence was not cosmetic: that control emits a bare `YYYY-MM-DD` on change, so a user who merely re-picked the **day** of a `datetime` cell silently wrote the record's time component out of existence — a `14:30` became midnight with no warning and no undo.
|
|
1373
|
+
|
|
1374
|
+
`GridColumn['type']` now carries `'datetime'` and `'time'` alongside `'date'`, and each renders its own control with its own read/write adapter:
|
|
1375
|
+
|
|
1376
|
+
- `datetime` → `<input type="datetime-local">`, read through `toDateTimeInputValue` and written back through `fromDateTimeInputValue`, so the stored shape stays ISO-8601 and read and write share one basis (the contract `DateTimeField` already follows, objectui#3127).
|
|
1377
|
+
- `time` → `<input type="time">`, round-tripping the stored zone-less `HH:mm[:ss]` verbatim.
|
|
1378
|
+
- `date` → unchanged.
|
|
1379
|
+
|
|
1380
|
+
The read-only surfaces are fixed with it. `displayText()` and the read-only table both fell through to `String(value)` and printed the raw stored ISO on screen (`2026-06-17T00:00:00.000Z`); each temporal type now formats as itself — a day for `date`, day + local time for `datetime`, the wall clock for `time`. That could not be fixed before the type collapse was undone, because with one column type the renderer had no way to know which of the two to show.
|
|
1381
|
+
|
|
1382
|
+
Authors writing explicit grid `columns` can now declare `type: 'datetime'` / `type: 'time'`; previously those spellings were not part of the exported union.
|
|
1383
|
+
|
|
1384
|
+
### Patch Changes
|
|
1385
|
+
|
|
1386
|
+
- 0186cdc: `address` widget: the ZIP box now reads and writes `postalCode`, the part name the platform stores
|
|
1387
|
+
|
|
1388
|
+
`AddressField` bound its ZIP input to `zipCode` — a part name that appears
|
|
1389
|
+
nowhere in `@objectstack/spec`. The stored value uses `postalCode`, which
|
|
1390
|
+
`AddressValueSchema` declares and enforces with `$strip` semantics, so the two
|
|
1391
|
+
sides never met:
|
|
1392
|
+
|
|
1393
|
+
- opening a stored address showed an **empty** ZIP box (no input read
|
|
1394
|
+
`postalCode`), while street / city / state / country all populated — four of
|
|
1395
|
+
five parts working is what let this survive review;
|
|
1396
|
+
- anything the user then typed into that box was written back as `zipCode` and
|
|
1397
|
+
**stripped at the contract boundary**. On a new record the postal code was
|
|
1398
|
+
lost outright; on an existing one the correction was silently discarded and
|
|
1399
|
+
the stale stored code remained, with no error anywhere.
|
|
1400
|
+
|
|
1401
|
+
The widget now uses `postalCode` throughout — state key, sub-input id, the
|
|
1402
|
+
`onChange` part name and the read-only formatter. Data written by the previous
|
|
1403
|
+
builds is still **read** through `zipCode` as a compatibility limb, and the
|
|
1404
|
+
first edit of any part normalizes such a record onto `postalCode` rather than
|
|
1405
|
+
writing the split shape back out. Nothing writes `zipCode` any more, and it is
|
|
1406
|
+
deliberately not part of the exported `AddressValue` type, so authoring code
|
|
1407
|
+
cannot spell it.
|
|
1408
|
+
|
|
1409
|
+
`AddressValue` is otherwise unchanged in shape; consumers that referenced
|
|
1410
|
+
`AddressValue['zipCode']` must read `postalCode`.
|
|
1411
|
+
|
|
1412
|
+
Fixes objectstack-ai/objectstack#5143.
|
|
1413
|
+
|
|
1414
|
+
- ea41a59: fix(fields): `BooleanField` uses the control id its host hands down, so a boolean field's visible label is really associated with the switch
|
|
1415
|
+
|
|
1416
|
+
A `boolean` / `checkbox` field inside a form emitted **two** labels with the same text, and the visible one pointed at nothing. `<FormControl>` (a Radix `Slot`) hands the control the id its `<FormLabel htmlFor>` already references, and the widget replaced it with the field name — so `label for="_r_3_-form-item"` had no target while the switch carried `id="notifications"`. Clicking the visible label, the normal affordance for a switch/checkbox row, toggled nothing on every generated form in every app; the accessible name survived only through the widget's own `sr-only` label.
|
|
1417
|
+
|
|
1418
|
+
The widget now honours the id it was handed (`id` is a declared key of the widget contract, forwarded by `toDomProps`) and stops emitting its own label when a host supplied one. Standalone rendering is unchanged: with no host id the id still falls back to the field name and then to `useId()`, and the `sr-only` label — the only accessible name the inline grid editor and the console's action-param dialog have — is still emitted.
|
|
1419
|
+
|
|
1420
|
+
- c3b01a7: Give composite and grouped field widgets a real accessible name: the form renderer now associates its label by IDREF for widgets that declare `labelling: 'group'`, instead of emitting a `<label for>` that nothing labelable answers (objectui#3961).
|
|
1421
|
+
|
|
1422
|
+
Six widgets rendered a visible group label that named **nothing** in the accessibility tree. Measured in a real form, one field per row, reading each label's `for` against the DOM:
|
|
1423
|
+
|
|
1424
|
+
```
|
|
1425
|
+
address for=…-form-item -> MISSING byLabelText=0 role+name=0
|
|
1426
|
+
geolocation for=…-form-item -> MISSING byLabelText=0 role+name=0
|
|
1427
|
+
checkboxes for=…-form-item -> div byLabelText=0 role+name=0
|
|
1428
|
+
radio for=…-form-item -> div[radiogroup] byLabelText=0 role+name=0
|
|
1429
|
+
rating for=…-form-item -> div byLabelText=0 role+name=0
|
|
1430
|
+
file for=…-form-item -> div[role=button] byLabelText=0 role+name=0
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
Two shapes, one outcome. `address` / `geolocation` spread the host's id onto their first sub-input and then replaced it with that input's own unique id (objectui#3343, correct in itself), so the `for` named an id no element carried — clicking "Shipping Address" did nothing and the group label was absent from the accessibility tree entirely. `checkboxes` / `radio` / `rating` / `file` kept the id, but on a `div`: a `<label for>` on a non-labelable element is inert HTML — `HTMLLabelElement.control` is `null`, so it activates nothing and contributes no name. A screen reader heard "Street Address", "City", "Alpha", "Beta" — never which group they belonged to.
|
|
1434
|
+
|
|
1435
|
+
The fix is the WAI-ARIA group pattern, driven by a DECLARATION rather than by the host guessing at widget DOM:
|
|
1436
|
+
|
|
1437
|
+
- `@object-ui/core` — `ComponentMeta` gains `labelling?: 'control' | 'group'`. Additive and optional; absent means `'control'`, which is every existing component's behaviour.
|
|
1438
|
+
- `@object-ui/components` — the form renderer reads it. For a `'group'` field the `<FormLabel>` publishes an `id` and drops its `for`, and the widget receives `aria-labelledby`. The single-control path is unchanged down to the attribute: no id on the label, no `aria-labelledby` key on the widget, so no field acquires a second naming channel. `ui/form.tsx` is untouched (Shadcn no-touch) — both halves travel as ordinary props, since `<FormLabel>` spreads props after its own `htmlFor`.
|
|
1439
|
+
- `@object-ui/fields` — the six audited widgets declare `labelling: 'group'`. `address` / `geolocation` move the host id (and only the id) from their first sub-input to the group container; `checkboxes` / `rating` answer a host-supplied `aria-labelledby` with `role="group"`; `radio` keeps Radix's more specific `radiogroup`; `file` takes the name on its dropzone with no invented group layer, because it has exactly one control that merely happens not to be labelable.
|
|
1440
|
+
|
|
1441
|
+
No new key in the widget props contract: `aria-*` is already declared on it and forwarded by `toDomProps`, the same channel `aria-required` (objectui#3290) travels.
|
|
1442
|
+
|
|
1443
|
+
Deliberately unchanged: sub-labels keep naming their own inputs (`aria-labelledby` overrides `<label for>`, so putting the group name on the first sub-input would have replaced "Street Address" with the field name — the concatenated-name outcome this issue rejected), `aria-describedby` stays on the first focusable sub-input where focus can reach it (objectui#3318), the sub-input ids of objectui#3343 do not move, and standalone rendering — the inline grid editor, a bare SDUI node, where nobody hands down an id and there is no host label to point at — emits no role and no IDREF at all.
|
|
1444
|
+
|
|
1445
|
+
A widget that does not declare itself keeps the old `for`, which the label-association tests report as an association resolving to a non-labelable element. Silence was the failure mode being fixed; the default path stays loud.
|
|
1446
|
+
|
|
1447
|
+
- c97a45e: Group-labelled field widgets now consume the host label's IDREF in their readonly and zero-option states, so the visible label names something there too
|
|
1448
|
+
|
|
1449
|
+
A field declared `labelling: 'group'` (objectui#3961) is named by IDREF: the form
|
|
1450
|
+
renderer publishes its `<FormLabel>`'s own `id`, drops the inert `for`, and hands
|
|
1451
|
+
the widget `aria-labelledby` plus the host control `id`. All seven such widgets
|
|
1452
|
+
consumed that pair on their editable surface only. A field-level
|
|
1453
|
+
`readonly: true`, and an option list that resolved to zero offered options, take
|
|
1454
|
+
early returns that render before the container doing the consuming — so the label
|
|
1455
|
+
published an id that no element in the document referenced.
|
|
1456
|
+
|
|
1457
|
+
Measured on the previous `main`, one field per row in a real form, counting the
|
|
1458
|
+
elements that reference the host label's published id:
|
|
1459
|
+
|
|
1460
|
+
```
|
|
1461
|
+
consumers byLabelText named group
|
|
1462
|
+
multiselect readonly+value 0 0 0
|
|
1463
|
+
multiselect readonly+empty 0 0 0
|
|
1464
|
+
multiselect zeroOptions 0 0 0
|
|
1465
|
+
multiselect editable 1 1 1
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
All seven measured identically in every readonly state; each is 1 now. The
|
|
1469
|
+
user-visible effect is the one objectui#3961 fixed for editable fields: the
|
|
1470
|
+
visible label was the accessible name of NOTHING, so a readonly "Tags" or
|
|
1471
|
+
"Shipping Address" was announced as loose text next to unattributed values. It is
|
|
1472
|
+
not a regression of #3961 or objectui#3975 — before them these same states
|
|
1473
|
+
emitted a `for` pointing at an id no element carried, which named nothing either.
|
|
1474
|
+
The shape changed from a dangling `for` to an unconsumed IDREF; the defect did
|
|
1475
|
+
not.
|
|
1476
|
+
|
|
1477
|
+
Each readonly surface now carries exactly two keys — the host `id` and
|
|
1478
|
+
`aria-labelledby` — plus the `role="group"` that makes them meaningful, in one
|
|
1479
|
+
shared spelling (`toHostGroupProps`). The narrow pair is deliberate: a readonly
|
|
1480
|
+
display has no focusable control, so `aria-describedby` / `aria-required` /
|
|
1481
|
+
`disabled` / the focus handlers have nothing to announce on, and `name` on a
|
|
1482
|
+
`div` is exactly the DOM leak objectui#3291 sweeps for.
|
|
1483
|
+
|
|
1484
|
+
Two widgets answer with a different role than they do while editable, because
|
|
1485
|
+
they render a different surface: `radio` (editable `radiogroup`) shows the chosen
|
|
1486
|
+
option's label as text with no radios in it, and `file` (editable `button`, the
|
|
1487
|
+
dropzone) shows file names with no dropzone. `role="group"` is also what the
|
|
1488
|
+
shared "no options available" box now answers with for `checkboxes` / `radio` /
|
|
1489
|
+
`multiselect`; the single `select` is not group-labelled, keeps its working
|
|
1490
|
+
`for`, and that box emits nothing new for it.
|
|
1491
|
+
|
|
1492
|
+
Standalone rendering is untouched. The grid's inline cell editor and bare SDUI
|
|
1493
|
+
nodes hand down neither key, so nothing is emitted and the markup stays
|
|
1494
|
+
byte-identical — including the `EmptyValue` placeholder, which keeps its own
|
|
1495
|
+
`aria-label` and gains no role. Hosted-and-empty, that placeholder is the whole
|
|
1496
|
+
readonly surface, so it carries the group props: its `aria-label` ("No value") is
|
|
1497
|
+
then outranked by `aria-labelledby` per accname, which is the intended outcome —
|
|
1498
|
+
on the `generic` role that span carries, an author name is prohibited and was
|
|
1499
|
+
never exposed, so the choice was between the field's name and no name.
|
|
1500
|
+
|
|
1501
|
+
- d8a0be4: A form-hosted `multiselect` field is now NAMED by its visible label. It was the
|
|
1502
|
+
residual of objectui#3961: that issue's probe audited six widgets and fixed
|
|
1503
|
+
them, and re-running the same probe over the whole widget map afterwards put
|
|
1504
|
+
`multiselect` on the byte-identical failure shape as `checkboxes` — the host's
|
|
1505
|
+
`id` kept, but on the chip row's wrapper `div`, where a `<label for>` is inert
|
|
1506
|
+
HTML (`HTMLLabelElement.control` is `null`, so it activates nothing and
|
|
1507
|
+
contributes no accessible name). Measured on the tree that already carried
|
|
1508
|
+
#3961's fix, one field per row:
|
|
1509
|
+
|
|
1510
|
+
```
|
|
1511
|
+
checkboxes for=(none) ownId=…-group-label byLabelText=1(div[role=group])
|
|
1512
|
+
multiselect for=…-form-item ownId=(none) byLabelText=0
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
Visually the field had a "Tags" label; a screen reader heard only "Alpha" /
|
|
1516
|
+
"Beta" and nothing about what the set of chips was for.
|
|
1517
|
+
|
|
1518
|
+
No new mechanism — #3961's declaration, applied to one more widget:
|
|
1519
|
+
`multiselect` declares `labelling: 'group'` at the registration boundary, so
|
|
1520
|
+
the form renderer publishes its label's `id` and drops the dead `for`, and the
|
|
1521
|
+
chip row answers with `role="group"` + the handed-down `aria-labelledby`.
|
|
1522
|
+
|
|
1523
|
+
Unchanged on purpose: each chip keeps its own accessible name from its text
|
|
1524
|
+
content (the group name sits one level up and does not override it), and
|
|
1525
|
+
STANDALONE rendering — the grid's inline cell editor, a bare SDUI node, where
|
|
1526
|
+
nobody hands an id and there is no host label to point at — emits no role and
|
|
1527
|
+
no IDREF, exactly as before.
|
|
1528
|
+
|
|
1529
|
+
- f4b97c8: `percent` / `progress` cells now give the NUMBER shrink priority over the decorative bar (objectstack#5066)
|
|
1530
|
+
|
|
1531
|
+
In a narrow clipping container the percent display renderer sacrificed the wrong
|
|
1532
|
+
half of itself. It emitted a `w-16 shrink-0` bar in front of a shrinkable value
|
|
1533
|
+
span, so in a `record:highlights` chip — `basis-[9rem]`, shrinking toward
|
|
1534
|
+
`min-w-[7rem]` as chips are added, clipping with `truncate` — the 64px bar plus
|
|
1535
|
+
the 8px gap consumed the content box and `truncate` removed what was left of the
|
|
1536
|
+
value. A stored `33.33` carried `33%` in the DOM and read as `3` on screen
|
|
1537
|
+
(measured: 79px clip box, 32px text node, 25px overflow), with no ellipsis and
|
|
1538
|
+
nothing in the accessible name to signal the loss: a silently smaller, plausible
|
|
1539
|
+
number rather than a cosmetic glitch. Downstream the app had to override the
|
|
1540
|
+
highlight's render type to `number` to get its digits back, losing the `%` glyph
|
|
1541
|
+
and the bar entirely.
|
|
1542
|
+
|
|
1543
|
+
The priority is now inverted, on the principle that the number is the CONTENT and
|
|
1544
|
+
the bar is DECORATION: the value span is `shrink-0` and the bar carries
|
|
1545
|
+
`min-w-0 shrink`, keeping `w-16` only as its PREFERRED width, so a squeezed
|
|
1546
|
+
container eats the bar and the digits survive intact.
|
|
1547
|
+
|
|
1548
|
+
The bar is deliberately NOT `flex-1`. `flex-1` would let it GROW as well as
|
|
1549
|
+
shrink, stretching the bar across every wide grid cell and changing a surface
|
|
1550
|
+
that has no bug; `w-16` stays the upper bound, so wide containers render exactly
|
|
1551
|
+
as before and only the shrink direction changed. Below roughly 40px of content
|
|
1552
|
+
the bar has collapsed to nothing and the value clips like any other single-line
|
|
1553
|
+
cell — same floor as the plain number renderer, which is as far as a
|
|
1554
|
+
self-contained renderer fix reaches (the chip's own width is a function of how
|
|
1555
|
+
many chips the record has, so the strip's `@container` cannot see it).
|
|
1556
|
+
|
|
1557
|
+
The neighbouring `number` renderer is untouched.
|
|
1558
|
+
|
|
1559
|
+
- c2ecbae: 相关列表 Add 选择器兑现 `add.picker.filter`:作者限定的候选范围现在真的生效
|
|
1560
|
+
|
|
1561
|
+
`record:related_list.add.picker.filter` 被 spec 声明为「Restrict which records the picker offers」,但渲染器从未读过它 —— 挂 `RecordPickerDialog` 时不传任何 filter,对话框照样提供 `picker.object` 的全部记录,选中即建链接行或改父,`os validate` / `os build` 全绿、运行时零诊断。作者写下「只允许指派 active 的岗位」「只允许挂未过期的许可」,得到的是完整候选列表。
|
|
1562
|
+
|
|
1563
|
+
现在它按原样传给 `RecordPickerDialog` 的 `baseFilter` —— 不是 `lookupFilters`,后者会把条件渲染成用户可编辑的筛选栏行,等于把作者的硬性限制降级成建议。
|
|
1564
|
+
|
|
1565
|
+
`baseFilter` 因此接受两种形状,按结构判别(`Array.isArray`):
|
|
1566
|
+
|
|
1567
|
+
- **`QueryParams.$filter` 记录形式**(依赖型 lookup 链)保持原有的键覆盖语义逐字节不变 —— 级联父值必须**替换**同字段上过期的 `lookupFilters` 条目,而不是与之求交。
|
|
1568
|
+
- **spec 的 `ViewFilterRule[]`** 经 `mergeFilterNodes`(仓内唯一的 filter 下沉口)下沉,19 个 operator 全部无损到达服务端,包括记录形式没有 `$op` 可用的 `before` / `after` / `is_empty` / `is_not_empty`。此处**不新增**第二份 operator 词汇表。
|
|
1569
|
+
|
|
1570
|
+
槽位类型同时从 `Record<string, any>` 收紧为 `unknown`:前者会接受规则数组(数组满足 `any` 的字符串索引),旧的对象展开再把它压成 `{"0": {...}}`,于是查询去过滤名为 `0` 的列 —— 类型全绿、查询错误、无任何诊断。
|
|
1571
|
+
|
|
1572
|
+
- 11c1e71: Resolve a `select` field declared `multiple: true` to the `field:multiselect` widget, so the object form's visible label actually names the chip picker it renders (objectui#3986).
|
|
1573
|
+
|
|
1574
|
+
`mapFieldTypeToFormType` keyed the widget id on the field's `type` string alone, so an object-schema `{ type: 'select', multiple: true }` picklist — a spec-legal, entirely ordinary shape — became `field:select`. `SelectField` then delegated to `MultiSelectField` on `config.multiple`, so the component that RENDERED was the chip picker while everything keyed on the widget id still answered for the single-value combobox. Above all the label-association declaration (`ComponentMeta.labelling`, objectui#3961), which the form renderer resolves per widget id: the host emitted `<label for>` at the chip row's wrapper `div`, where a `for` is inert — `HTMLLabelElement.control` returns `null`. Visually the field had a label; in the accessibility tree that label named nothing. Measured on the object-form path, `role=group` + accessible name went from 1 for a `multiselect`-typed field (fixed in objectui#3975) to 0 for this one.
|
|
1575
|
+
|
|
1576
|
+
Declaring `select` itself `labelling: 'group'` was not available: a single-value select's trigger is a labelable `button[role=combobox]` whose `for` association works, and a bare `select` is a builtin the renderer resolves without consulting the registry at all. The fix is therefore at the producer — the widget id now carries the arity, so one place decides which widget renders and the declaration can no longer be addressed to a widget that is not rendering.
|
|
1577
|
+
|
|
1578
|
+
- `mapFieldTypeToFormType(fieldType, config?)` takes an optional second argument — the rest of the field definition, of which only `multiple` is read. Existing single-argument calls are unchanged, and so is every type outside the new table: `select` is the only one whose `multiple` form is a different WIDGET. The spec's multi-capable set is larger (select / lookup / file / image, with `radio` on the select branch and `user` storing like `lookup`), but `LookupField`, `FileField` and `ImageField` each render both arities themselves, so their id — and their labelling declaration — is already right for either.
|
|
1579
|
+
- The four object-form producers pass the pair: `ObjectForm`, `DrawerForm`, `ModalForm`, and `sectionFields` (Tabbed / Wizard / Split / Drawer / Modal). In `sectionFields` the id is now computed once from the EFFECTIVE pair, after view-level overrides have merged, because `multiple` is itself a spec `FormField` key: a view restating only `multiple: true` over a single-value object field moves the widget too, and `multiple: false` moves it back.
|
|
1580
|
+
- `SelectField`'s `multiple` delegation is KEPT, not retired. Measured, it stays reachable from three entrances that never consult the alias map: the inline grid editor (`FieldEditWidget` finds `select` in its own table first), `ActionParamDialog` (`resolveFormWidgetType` returns `select` from `fieldWidgetMap` first), and hand-written SDUI addressing `field:select` by name with `multiple` on its metadata.
|
|
1581
|
+
|
|
1582
|
+
Read-only rendering of these widgets is untouched (objectui#4005), as is the built-in `Select` branch (objectui#3976).
|
|
1583
|
+
|
|
1584
|
+
- Updated dependencies [794c497]
|
|
1585
|
+
- Updated dependencies [993336f]
|
|
1586
|
+
- Updated dependencies [f0a625a]
|
|
1587
|
+
- Updated dependencies [b5980f4]
|
|
1588
|
+
- Updated dependencies [8aad9fd]
|
|
1589
|
+
- Updated dependencies [6719877]
|
|
1590
|
+
- Updated dependencies [56ff091]
|
|
1591
|
+
- Updated dependencies [7864f03]
|
|
1592
|
+
- Updated dependencies [0cbdca8]
|
|
1593
|
+
- Updated dependencies [d229dfa]
|
|
1594
|
+
- Updated dependencies [ecae400]
|
|
1595
|
+
- Updated dependencies [4bc6c23]
|
|
1596
|
+
- Updated dependencies [d3e738a]
|
|
1597
|
+
- Updated dependencies [c3b01a7]
|
|
1598
|
+
- Updated dependencies [f5f8744]
|
|
1599
|
+
- Updated dependencies [7ed3360]
|
|
1600
|
+
- Updated dependencies [69becd2]
|
|
1601
|
+
- Updated dependencies [5e52495]
|
|
1602
|
+
- Updated dependencies [0fa5e4d]
|
|
1603
|
+
- Updated dependencies [b750823]
|
|
1604
|
+
- Updated dependencies [5bfaabd]
|
|
1605
|
+
- Updated dependencies [e06810e]
|
|
1606
|
+
- Updated dependencies [ab3ad4f]
|
|
1607
|
+
- Updated dependencies [c2fd122]
|
|
1608
|
+
- Updated dependencies [ac2139c]
|
|
1609
|
+
- Updated dependencies [b14ab3a]
|
|
1610
|
+
- Updated dependencies [e24d767]
|
|
1611
|
+
- Updated dependencies [8c60819]
|
|
1612
|
+
- Updated dependencies [aca561a]
|
|
1613
|
+
- Updated dependencies [e64a52e]
|
|
1614
|
+
- Updated dependencies [844d17f]
|
|
1615
|
+
- Updated dependencies [48132f7]
|
|
1616
|
+
- Updated dependencies [4dcd52a]
|
|
1617
|
+
- Updated dependencies [42ae5c6]
|
|
1618
|
+
- Updated dependencies [0ef9dfd]
|
|
1619
|
+
- Updated dependencies [1d723e3]
|
|
1620
|
+
- Updated dependencies [0109f54]
|
|
1621
|
+
- Updated dependencies [7e5bb5d]
|
|
1622
|
+
- Updated dependencies [fbc23e0]
|
|
1623
|
+
- Updated dependencies [6d762da]
|
|
1624
|
+
- Updated dependencies [e6fdbdc]
|
|
1625
|
+
- Updated dependencies [54233b1]
|
|
1626
|
+
- Updated dependencies [f9faa7d]
|
|
1627
|
+
- Updated dependencies [97b63d7]
|
|
1628
|
+
- Updated dependencies [6bb454a]
|
|
1629
|
+
- Updated dependencies [523be48]
|
|
1630
|
+
- Updated dependencies [7e2b7e9]
|
|
1631
|
+
- Updated dependencies [33526fd]
|
|
1632
|
+
- Updated dependencies [32413ec]
|
|
1633
|
+
- Updated dependencies [c1e1e6b]
|
|
1634
|
+
- @object-ui/components@17.4.0
|
|
1635
|
+
- @object-ui/react@17.4.0
|
|
1636
|
+
- @object-ui/core@17.4.0
|
|
1637
|
+
- @object-ui/i18n@17.4.0
|
|
1638
|
+
- @object-ui/types@17.4.0
|
|
1639
|
+
- @object-ui/providers@17.4.0
|
|
1640
|
+
|
|
3
1641
|
## 17.3.0
|
|
4
1642
|
|
|
5
1643
|
### Minor Changes
|
|
@@ -60,6 +1698,7 @@ first`. The form renderer passes a new `dependsOnLabels` widget prop (the
|
|
|
60
1698
|
so that line filtered nothing — the comment actively misled. It is gone.
|
|
61
1699
|
|
|
62
1700
|
## What changed
|
|
1701
|
+
|
|
63
1702
|
- New `toDomProps(props)`, exported from `@object-ui/fields`. It keeps only
|
|
64
1703
|
what may legitimately become a DOM attribute and drops the rest.
|
|
65
1704
|
- The 14 field widgets that spread onto a host element now go through it:
|
|
@@ -245,6 +1884,7 @@ props}` _alongside_ `field`. The predictable result was ~30 widgets resolving
|
|
|
245
1884
|
compile time rather than work under one host and not another.
|
|
246
1885
|
|
|
247
1886
|
## What did NOT change
|
|
1887
|
+
|
|
248
1888
|
- **Host metadata (SDUI JSON) is untouched.** No authored schema changes; this is
|
|
249
1889
|
a change to how widgets are _written_, not to what apps declare.
|
|
250
1890
|
- **`schema` is still the universal SDUI prop** every registered component
|
|
@@ -1666,6 +3306,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
1666
3306
|
objects+fields were editable in Studio; this reworks both surfaces.
|
|
1667
3307
|
|
|
1668
3308
|
**Setup (assign + read-only):**
|
|
3309
|
+
|
|
1669
3310
|
- The six facets (`object_permissions`, `field_permissions`, `system_permissions`,
|
|
1670
3311
|
`row_level_security`, `tab_permissions`, `admin_scope`) now render read-only on
|
|
1671
3312
|
the `sys_permission_set` record page as a compact summary (counts, or capability
|
|
@@ -1680,6 +3321,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
1680
3321
|
|
|
1681
3322
|
**Studio (design every facet):** the permission matrix editor gains structured
|
|
1682
3323
|
editors for the facets that were JSON-only —
|
|
3324
|
+
|
|
1683
3325
|
- **System Capabilities**: a multi-select over the live `sys_capability` registry
|
|
1684
3326
|
(scope-grouped, labelled chips).
|
|
1685
3327
|
- **Row-Level Security**: per-policy rows (object · operation · enabled) with CEL
|
|
@@ -2455,6 +4097,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2455
4097
|
assuming USD.
|
|
2456
4098
|
|
|
2457
4099
|
- d912a60: CRM polish — denser kanban cards, smarter currency, calmer dates.
|
|
4100
|
+
|
|
2458
4101
|
- **plugin-kanban card body**: drop the verbose `Label: value` two-column
|
|
2459
4102
|
grid in favor of a single-column dense list (values only, with the
|
|
2460
4103
|
field label preserved as a hover `title` for accessibility). Pipeline
|
|
@@ -2483,6 +4126,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2483
4126
|
`defaultCurrency` is set on the field/column, formatting is unchanged.
|
|
2484
4127
|
|
|
2485
4128
|
Fixed call sites:
|
|
4129
|
+
|
|
2486
4130
|
- `@object-ui/fields`: `formatCurrency`, `formatCompactCurrency`, and
|
|
2487
4131
|
`CurrencyCellRenderer` no longer default-param `'USD'`.
|
|
2488
4132
|
- `@object-ui/i18n`: `formatCurrency()` falls back to `formatNumber`
|
|
@@ -2643,6 +4287,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2643
4287
|
- d714e85: Lookup display-name resolution now falls back through a Salesforce-style chain
|
|
2644
4288
|
when an `$expand`'d reference object lacks a top-level `name`/`label`/
|
|
2645
4289
|
`display_name`/`title` field:
|
|
4290
|
+
|
|
2646
4291
|
1. Standard display fields (existing behaviour)
|
|
2647
4292
|
2. `salutation first_name last_name` composite — handles person records that
|
|
2648
4293
|
only carry first/last name parts
|
|
@@ -2832,6 +4477,7 @@ Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
|
2832
4477
|
### Patch Changes
|
|
2833
4478
|
|
|
2834
4479
|
- 89ae109: Fix click navigation and required-FK form rendering
|
|
4480
|
+
|
|
2835
4481
|
- **plugin-grid**: ObjectGrid's `getSelectFields()` now always includes `id` in
|
|
2836
4482
|
the SELECT projection. Previously, when a view configured `columns` without
|
|
2837
4483
|
`id`, the SQL driver stripped it from results, and row-click handlers silently
|
|
@@ -2845,6 +4491,7 @@ Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
|
2845
4491
|
to create child records.
|
|
2846
4492
|
|
|
2847
4493
|
- 1b6dc64: fix: complete Tailwind v3→v4 migration cleanup
|
|
4494
|
+
|
|
2848
4495
|
- Rename deprecated `flex-shrink-0` → `shrink-0` and `flex-grow-N` →
|
|
2849
4496
|
`grow-N` (Tailwind v4 dropped the long-form aliases). Affects
|
|
2850
4497
|
data-table, fields/index, FileField, ChatbotEnhanced,
|
|
@@ -2909,6 +4556,7 @@ Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
|
2909
4556
|
Library builds (vite lib mode) now externalize every non-relative import instead of bundling third-party CJS dependencies into the published dist. This avoids inlined `require("react")` / `require("react-dom")` calls that cause `Calling \`require\` for "react" in an environment that doesn't expose the \`require\` function` runtime errors when consumer apps re-bundle the published dist.
|
|
2910
4557
|
|
|
2911
4558
|
Specifically fixes:
|
|
4559
|
+
|
|
2912
4560
|
- `@object-ui/plugin-dashboard` no longer inlines `react-grid-layout` (and its transitive `react-draggable` / `react-resizable` CJS bundles). `react-grid-layout` is now declared as a peer dependency so consumers install a single ESM-friendly copy.
|
|
2913
4561
|
- `@object-ui/components`, `@object-ui/plugin-calendar`, `@object-ui/plugin-charts`, `@object-ui/plugin-designer` no longer inline `react-i18next` / `i18next` / `use-sync-external-store` CJS shims.
|
|
2914
4562
|
- All plugin packages now use a unified `external: (id) => !/^[./]/.test(id) && !id.startsWith(__dirname)` rule, ensuring future additions of CJS deps are automatically externalized.
|
|
@@ -2971,24 +4619,29 @@ Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
|
2971
4619
|
- a2d7023: End-user feature batch — forms, designer history, import/export, and PWA offline sync.
|
|
2972
4620
|
|
|
2973
4621
|
**Forms (`@object-ui/fields`, `@object-ui/providers`)**
|
|
4622
|
+
|
|
2974
4623
|
- `FileField`: native `<input capture="environment">` camera capture for mobile devices, plus a uploading-progress indicator driven by `UploadProvider`.
|
|
2975
4624
|
- `ImageField`: per-image inline crop/rotate via the lazy-loaded `ImageCropperDialog` (canvas-based, zero new deps).
|
|
2976
4625
|
- New `UploadProvider` in `@object-ui/providers` with pluggable adapters for S3 and Azure Blob (plus the default object-URL adapter for local previews). XHR-based with progress, abort, and retry.
|
|
2977
4626
|
- `LookupField`: `lookup.dependsOn: string | string[]` to chain dependent lookups (e.g. State depends on Country); the trigger is gated until parent values are present and the OData `$filter` is built automatically.
|
|
2978
4627
|
|
|
2979
4628
|
**Container-aware widget widths (`@object-ui/components`)**
|
|
4629
|
+
|
|
2980
4630
|
- New `useResizeObserver(ref)` hook exposing `{ width, height }` of any element. SSR-safe; reads the initial size via `getBoundingClientRect`.
|
|
2981
4631
|
- `plugin-gantt` and `plugin-kanban` now react to their container size instead of `window.innerWidth`, so they behave correctly inside split panels and dashboards.
|
|
2982
4632
|
|
|
2983
4633
|
**Designer history (`@object-ui/plugin-designer`)**
|
|
4634
|
+
|
|
2984
4635
|
- `useUndoRedo` (and therefore `useDesignerHistory`) gains `persistKey` + `storage` options to round-trip the undo/redo stack through `sessionStorage`, plus a `clearPersisted()` cleanup helper. Drafts now survive accidental tab refreshes.
|
|
2985
4636
|
- New `<HistoryPanel>` component renders the timeline visually with one-click jump-to-checkpoint via the new `jumpTo(index)` API.
|
|
2986
4637
|
|
|
2987
4638
|
**Import wizard (`@object-ui/plugin-grid`)**
|
|
4639
|
+
|
|
2988
4640
|
- Saved column-mapping templates: name, save, re-apply, and delete via a new template bar in the mapping step. Persisted under `objectui:import-templates:${objectName}` (override via `templateStorageKey` / `templateStorage`).
|
|
2989
4641
|
- Inline validation correction: cells with errors in the preview step are now editable; corrections feed straight into the import without requiring a re-upload, with green-bar status indicators for fixed rows.
|
|
2990
4642
|
|
|
2991
4643
|
**PWA offline sync (`@object-ui/mobile`)**
|
|
4644
|
+
|
|
2992
4645
|
- New `MemoryOfflineQueue` / `IndexedDbOfflineQueue` (`createOfflineQueue()` picks the best backend) backed by IndexedDB.
|
|
2993
4646
|
- `createOfflineDataSource(inner, { queue })` wraps any DataSource so mutations issued while offline (or that fail with a network-style error) are queued and replayed in order on reconnect. Includes `replay()`, `drop()`, `clear()`, `pending()`, an `onChange` notifier, and an opt-in `resolveConflict` hook for stale-write conflicts.
|
|
2994
4647
|
- New `useOfflineSync(source)` hook exposes `{ isOnline, pending, isReplaying, replay, drop, clear }` and auto-replays on the browser's `online` event.
|
|
@@ -3016,11 +4669,13 @@ Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
|
3016
4669
|
`FormSchema.mobileStickyActions` (new) is the lower-level escape hatch — applied automatically when `mobile.stickyActions` is set on `ObjectFormSchema`.
|
|
3017
4670
|
|
|
3018
4671
|
**`@object-ui/plugin-form`** — `ObjectForm` now:
|
|
4672
|
+
|
|
3019
4673
|
- propagates `mobile.fullscreenLongText` to every textarea/markdown/html field as `mobile_fullscreen: true`,
|
|
3020
4674
|
- sets `mobileStickyActions` on the inner form schema and adds `pb-20` padding so content isn't covered by the fixed bar,
|
|
3021
4675
|
- when `mobile.stepper === true` (or `'auto'` + `useIsMobile()` + > `stepperMinFields` fields), routes the flat field list through the existing `WizardForm` with synthetic single-field "steps" — keeping per-step validation and the existing `Next`/`Back`/`Submit` flow.
|
|
3022
4676
|
|
|
3023
4677
|
**`@object-ui/components`** — the registered `form` renderer adds:
|
|
4678
|
+
|
|
3024
4679
|
- a `mobileStickyActions` opt-in that turns the action row into a `position: sticky; bottom: 0` bar on small viewports, and
|
|
3025
4680
|
- an inline `FullscreenTextarea` wrapper used when no field-package widget is registered, providing the same expand-button + edit-dialog UX so the feature works even in lighter setups.
|
|
3026
4681
|
|