@object-ui/plugin-detail 17.3.0 → 17.5.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 +640 -0
- package/README.md +20 -11
- package/dist/DetailSection.d.ts.map +1 -1
- package/dist/DetailView.d.ts.map +1 -1
- package/dist/HeaderHighlight.d.ts.map +1 -1
- package/dist/InlineFieldInput.d.ts +78 -5
- package/dist/InlineFieldInput.d.ts.map +1 -1
- package/dist/RelatedList.d.ts +31 -1
- package/dist/RelatedList.d.ts.map +1 -1
- package/dist/autoLayout.d.ts.map +1 -1
- package/dist/fieldEnrichment.d.ts +61 -0
- package/dist/fieldEnrichment.d.ts.map +1 -1
- package/dist/index.js +1972 -1863
- package/dist/index.umd.cjs +3 -3
- package/dist/renderers/record-details.d.ts.map +1 -1
- package/dist/renderers/record-quick-actions.d.ts.map +1 -1
- package/dist/renderers/record-related-list.d.ts +29 -1
- package/dist/renderers/record-related-list.d.ts.map +1 -1
- package/dist/synth/buildDefaultPageSchema.d.ts +21 -9
- package/dist/synth/buildDefaultPageSchema.d.ts.map +1 -1
- package/dist/useDetailTranslation.d.ts +7 -0
- package/dist/useDetailTranslation.d.ts.map +1 -1
- package/package.json +19 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,612 @@
|
|
|
1
1
|
# @object-ui/plugin-detail
|
|
2
2
|
|
|
3
|
+
## 17.5.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ceccdcf: Action confirm dialogs and success toasts now honour the bundle's translated
|
|
8
|
+
`confirmText` / `successMessage`, not just `label` (objectui#4265).
|
|
9
|
+
|
|
10
|
+
A TranslationBundle entry for an action carries three keys under one
|
|
11
|
+
`_actions.<name>` node — `label`, `confirmText`, `successMessage` — and
|
|
12
|
+
`useObjectLabel()` has always exposed a resolver for each. What had drifted was
|
|
13
|
+
the call sites: `page:header` (authored record pages), `record:quick_actions`
|
|
14
|
+
and the related-list row menu resolved the button `label` only and dispatched
|
|
15
|
+
the authored `confirmText` / `successMessage` untouched. One bundle entry met
|
|
16
|
+
two fates: the button rendered the translation, the confirm dialog rendered the
|
|
17
|
+
authored English.
|
|
18
|
+
|
|
19
|
+
All action-rendering surfaces now go through one resolver,
|
|
20
|
+
`useActionTextLocalizer()` (new, exported from `@object-ui/react`), which
|
|
21
|
+
applies the existing `actionLabel` / `actionConfirm` / `actionSuccess`
|
|
22
|
+
resolvers over the three keys together. Fallback is unchanged: with no bundle
|
|
23
|
+
entry — or an entry lacking a key — the authored text renders. A bundle cannot
|
|
24
|
+
introduce a `confirmText` or `successMessage` the metadata never declared.
|
|
25
|
+
|
|
26
|
+
- 6d01319: Inline edit no longer offers a record picker for a spec-spelled `autonumber` field that carries a `reference_to`
|
|
27
|
+
|
|
28
|
+
`TEXTUAL_REF_FALLBACK_TYPES` — the detail page's one definition of "machine-computed" — spelled the auto-number type `auto_number` only. `@objectstack/spec`, the designer and the metadata importer all spell it `autonumber`, and the set is matched by RAW spelling, so it carried half the type.
|
|
29
|
+
|
|
30
|
+
The reader that had no gate in front of it is `InlineFieldInput`'s reference fallback, `!!field.reference_to && !TEXTUAL_REF_FALLBACK_TYPES.has(type)`, on exported public API. A field typed `autonumber` keeps a `reference_to` for relational metadata — which is the entire reason this set exists — so it took the lookup branch and rendered the RECORD PICKER: a searchable list of records offered as replacements for a machine-generated identity. The `auto_number` spelling of the identical field rendered the textual fallback, as intended. Both spellings are now members, matching how `plugin-form` carries both in each of its non-input sets.
|
|
31
|
+
|
|
32
|
+
The editability half of the same report (objectui#4219) was already closed from another direction by #4228, whose shared exclusion resolves aliases before matching — a field typed `autonumber` offers no inline affordance in either host. The two gates are a union, so this fix also removes the union's dependence on which spelling the metadata happens to use: previously `autonumber` was held by the exclusion gate alone and `auto_number` by both, and losing either gate would have re-opened a different half of the defect depending on how the field was authored.
|
|
33
|
+
|
|
34
|
+
Pins land with it: the reference fallback for `autonumber` (red before this change — the picker really did render), `auto_number` and a real `lookup` as controls in both directions, and set membership asserted directly so the union statement is checked rather than described.
|
|
35
|
+
|
|
36
|
+
- 63fe8fd: `record:related_list` and the detail synthesizer now declare two shapes they already accepted at runtime.
|
|
37
|
+
|
|
38
|
+
`RecordRelatedListRenderer`'s `schema` prop made `objectName` required, which rejected the exact authoring shape the per-element `dataSource` binding exists to support (`{ relationshipField, dataSource: { object, view } }`) — the gate maps the binding onto `objectName` before the body reads it, so the key is supplied, not missing. It is optional on the wrapper's input now, and required everywhere else.
|
|
39
|
+
|
|
40
|
+
`ObjectDefLike.fieldGroups` is derived from the spec's authorable field group instead of restating it. The hand-written list had drifted: it omitted `icon` and `description`, both of which the synthesizer passes through to detail section descriptors, so an object definition declaring the group icon the code honours did not type-check against it.
|
|
41
|
+
|
|
42
|
+
- 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
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
**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.
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
**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.
|
|
53
|
+
|
|
54
|
+
**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.
|
|
55
|
+
|
|
56
|
+
No behavior, no keys added or removed, no placeholder changed.
|
|
57
|
+
|
|
58
|
+
- 6314e87: Inline-editing an `address` on the record detail page now edits it as real sub-fields, instead of collapsing it to one text box reading `[Object]` and saving a string over the structured value.
|
|
59
|
+
|
|
60
|
+
`InlineFieldInput`'s type switch routed the scalar and relational families to their dedicated widgets; every structured-object type matched nothing and fell through to the terminal raw text input at the end of the component. That fallback stringifies an object value through `coerceToSafeValue`, whose general-object case extracts `name || label || externalId || id || _id` and otherwise returns the literal `[Object]`. A stored address carries none of those keys, so the edit box read `[Object]`.
|
|
61
|
+
|
|
62
|
+
The display half was cosmetic; the write half was not. The fallback is a plain input wired to `onChange(v)`, so whatever the user typed was emitted as a **string** that replaced the whole `{ street, city, state, postalCode, country }` object on save — and `[Object]` was what the user saw as the current value they were correcting, which makes typing over it the natural gesture. An ordinary double-click inline edit therefore destroyed the sub-field structure. This is the input path only: objectui#4037 fixed the display registry, and read mode (including the inline-edit read state before editing starts) already rendered a formatted address.
|
|
63
|
+
|
|
64
|
+
`location` and `geolocation` are fixed with it. Both store objects too (`{ latitude, longitude }`), both reached the same terminal input, and both produced the identical `[Object]`-then-overwrite pair — one defect in three spellings, not three defects.
|
|
65
|
+
|
|
66
|
+
No new editor was written and no consumer-side tolerance was added. All three route to the widgets the create/edit dialog already uses (`AddressField` / `LocationField` / `GeolocationField`, the form's own structured-value editors), so the two entry points cannot diverge on the value shape they write back, and `coerceToSafeValue` is left untouched — the routing is what stops an address from ever reaching it. `autoFocus` follows the numeric branches' convention and lands on each widget's first sub-input (street / the coordinate box / latitude).
|
|
67
|
+
|
|
68
|
+
String-valued types are unchanged: `text`, `textarea`, `email`, `phone`, `url`, `color`, `code`, `time`, `qrcode` and the rest keep the terminal text input, where stringification is the identity and nothing is lost.
|
|
69
|
+
|
|
70
|
+
- 5e2e9fa: A `password` or `secret` field on the record detail page is no longer inline-editable: it renders no pencil / double-click affordance and produces no editor, on both the details body and the highlights strip.
|
|
71
|
+
|
|
72
|
+
Both types are **masked on read** — `getCellRenderer` returns a fixed bullet run for either — so the value the row could hand an editor was never the credential. `InlineFieldInput` had no branch for either type, so both reached the terminal raw text input at the end of the component, and the row's payload value was seeded into a `type="text"` box: rendered in clear, selectable and copyable, in a control the user reads as holding their credential. Committing the row then wrote that placeholder back verbatim over the field. For `secret` the overwritten value is an opaque reference into an encrypted store (ADR-0100), so the write destroyed the pointer, not just the display. Nothing in the flow said so; the failure surfaced later, wherever that credential was used.
|
|
73
|
+
|
|
74
|
+
The decision already existed one package over. `INLINE_EXCLUDED_FIELD_TYPES` in `@object-ui/fields` excludes both types with exactly this reasoning, and the grid honours it through `isInlineExcludedFieldType()`. The detail hosts gated on readonly / computed / system only and never consulted it, so the detail page reproduced the precise failure the set exists to prevent. Both hosts now consult that same alias-aware contract (`isInlineExcludedDetailFieldType`, a narrow-only union of the authored and the object type, matching the computed gate under objectui#3355) rather than growing a second hand-maintained list — so the rule cannot drift between the grid and the detail page again.
|
|
75
|
+
|
|
76
|
+
Consulting the shared set closes the container family on the detail page with it: `object`, `composite`, `record`, `grid`, `repeater` and `vector` rode the same plain-text fallback with an object-shaped value, and are now excluded too. The spec spelling `autonumber` is likewise excluded, where the detail computed gate only knew the `auto_number` spelling. The heavy-editor family (`markdown`, `html`, `richtext`) loses its one-line text box on the detail page — those are authored in the record form, which has the real editors.
|
|
77
|
+
|
|
78
|
+
The binary/attachment family is deliberately exempt and keeps its detail editor. It is in the shared set for a grid-cell reason — a cell cannot host an upload dropzone — while `InlineFieldInput` routes `image` / `avatar` / `signature` / `file` (and the `video` / `audio` spellings) to the same upload widgets the record form uses. That exemption is pinned against the routing it claims, so it cannot outlive it.
|
|
79
|
+
|
|
80
|
+
Re-authoring a credential is unchanged and still belongs in the record form, which has the widget for it (`PasswordField`).
|
|
81
|
+
|
|
82
|
+
- 297534b: Align 43 inline `defaultValue` strings with the `en` pack, and make the call-site gate enforce it (objectui#3810)
|
|
83
|
+
|
|
84
|
+
`t(key, { defaultValue: 'English text' })` only renders that text when i18next
|
|
85
|
+
**misses** the key. Where the key exists in `packages/i18n/src/locales/en.ts` the
|
|
86
|
+
pack value always wins, so the inline string is dead code — and 43 of those dead
|
|
87
|
+
strings said something different from the sentence users actually read.
|
|
88
|
+
|
|
89
|
+
`scripts/check-i18n-call-site-keys.mjs` (objectui#3530) now compares the two
|
|
90
|
+
whenever a call site carries a literal `defaultValue` for a key `en` defines, and
|
|
91
|
+
fails on any byte of difference. It is a hard rule with **no baseline**: the
|
|
92
|
+
repo-wide census measured 43 sites in 19 files out of 851 literal inline defaults,
|
|
93
|
+
and all 43 are aligned here, so there is no debt for a ratchet to hold. A
|
|
94
|
+
`defaultValue` on a key that is _not_ yet in `en` stays legal — that transition
|
|
95
|
+
runs for months (objectui#3546) and belongs to the existing `missing-key` rule,
|
|
96
|
+
which keeps reporting it alone.
|
|
97
|
+
|
|
98
|
+
Every fix moved the CALL SITE to the pack's wording. `en.ts` is untouched: its
|
|
99
|
+
values are what users read today, and changing one would oblige the same change in
|
|
100
|
+
the nine other packs (`scripts/check-i18n-en-drift.mjs`, objectui#3650). Six of the
|
|
101
|
+
43 differed only in an ellipsis (`...` against U+2026) — invisible in review, which
|
|
102
|
+
is how they survived three i18n gates that are each blind to this class by
|
|
103
|
+
construction.
|
|
104
|
+
|
|
105
|
+
The visible effect is confined to hosts that render these components with **no**
|
|
106
|
+
`I18nProvider` and no initialised i18next instance. There, react-i18next's
|
|
107
|
+
not-ready `t` returns the `defaultValue`, so the inline string was the rendered
|
|
108
|
+
one; it now matches what a provider-backed app has always shown. Inside the
|
|
109
|
+
console — provider mounted — nothing users see changes. The clearest converging
|
|
110
|
+
examples: the workspaces screen was written as "Organizations" at nine call sites
|
|
111
|
+
while every user has been reading "Workspaces"; the forgot-password success line
|
|
112
|
+
was written as "If an account exists, a reset link has been sent." while the pack
|
|
113
|
+
asserts "We've sent a password reset link to {{email}}."
|
|
114
|
+
|
|
115
|
+
- e7663f2: fix(detail): inline edit no longer destroys array values or flattens types on the record page
|
|
116
|
+
|
|
117
|
+
`InlineFieldInput`'s type switch ended in a raw text input, and every type it had
|
|
118
|
+
no branch for landed there: the value was displayed through `coerceToSafeValue`
|
|
119
|
+
and written back as whatever the user typed — a bare string.
|
|
120
|
+
|
|
121
|
+
Two damage classes survived the earlier passes. Array-valued fields (`tags`,
|
|
122
|
+
`checkboxes`, an options-less multi picklist) were offered for editing as
|
|
123
|
+
`"a, b"` — `coerceToSafeValue` joins arrays — and saved back as that string, so
|
|
124
|
+
the array was gone. Type-lossy scalars (`toggle`, `slider`, `progress`,
|
|
125
|
+
`rating`, `radio`) round-tripped through `String()`, so a boolean column
|
|
126
|
+
received `"true"`, a numeric one `"42"`, and `radio` accepted any free-typed
|
|
127
|
+
value its option list never offered.
|
|
128
|
+
|
|
129
|
+
Types the switch already routes keep their editors. Everything else that the
|
|
130
|
+
fields package can edit inline now falls back to `FieldEditWidget` — the same
|
|
131
|
+
control the form renders, `json` → the code editor included — and only genuinely
|
|
132
|
+
string-valued types (`text`, `textarea`, `email`, `phone`, `url`) keep the plain
|
|
133
|
+
input. A drift guard asserts every field type is exactly one of routed /
|
|
134
|
+
excluded / delegated / benign, so a new type can no longer inherit the
|
|
135
|
+
value-destroying default in silence.
|
|
136
|
+
|
|
137
|
+
`@object-ui/fields`: the four fixed-option widgets no longer clear the stored
|
|
138
|
+
value when the field declares no `options` at all. An empty offered set had two
|
|
139
|
+
opposite causes — a list that cascaded to zero (clear) and a list that was never
|
|
140
|
+
authored (nothing to decide) — and the second deleted the value on mount, which
|
|
141
|
+
the grid's inline cell editor has always been able to trigger. `FieldEditWidget`
|
|
142
|
+
also forwards `autoFocus` to the widget it renders.
|
|
143
|
+
|
|
144
|
+
- e076fd5: Inline-edit toggle reads "Edit fields" without an I18nProvider, matching every locale pack
|
|
145
|
+
|
|
146
|
+
`DETAIL_DEFAULT_TRANSLATIONS` said `Edit fields inline` where all ten packs say
|
|
147
|
+
`Edit fields`, so `InlineEditSaveBar`'s toggle announced two different names for one
|
|
148
|
+
control — the map's on provider-less hosts (standalone embeds, the preview gallery),
|
|
149
|
+
the pack's in the console. The pack wins; the map row now mirrors it byte for byte.
|
|
150
|
+
|
|
151
|
+
The three ungated defaults maps (`plugin-detail`, `plugin-list`, `plugin-designer`) are
|
|
152
|
+
now compared key-by-key against the `en` pack by a new gate, generalizing the
|
|
153
|
+
collaboration-only precedent from objectui#3440. `LIST_DEFAULT_TRANSLATIONS` and
|
|
154
|
+
`DESIGNER_DEFAULT_TRANSLATIONS` are exported for it, as `DETAIL_DEFAULT_TRANSLATIONS`
|
|
155
|
+
and `COLLAB_DEFAULT_TRANSLATIONS` already were.
|
|
156
|
+
|
|
157
|
+
- 456aac8: `@object-ui/plugin-detail` now declares `react-router-dom` as a peer dependency (`^6.0.0 || ^7.0.0`), the range its three siblings already use.
|
|
158
|
+
|
|
159
|
+
It has been importing the router all along — `PermissionFacetLink.tsx` and `record-reference-rail.tsx` both take `Link` and `useParams` from it — while its manifest named it in no field at all. That resolved locally for a reason that does not travel: the workspace root declares `react-router-dom` in its own `devDependencies`, so a `node_modules/react-router-dom` symlink exists at the root of this repository and Node's upward directory walk reaches it from every package directory. A consumer's install has no such root, and this package's rollup config externalises every bare specifier, so the published `dist/index.js` carried an import of a package the manifest never asked for.
|
|
160
|
+
|
|
161
|
+
Consumers already installing `@object-ui/app-shell`, `@object-ui/layout` or `@object-ui/plugin-designer` were unaffected — all three declare the same peer — so this closes the case of a consumer that pulls `plugin-detail` on its own.
|
|
162
|
+
|
|
163
|
+
A new repository gate, `pnpm check:phantom-deps`, now asserts that every bare specifier a released package imports under `src/` is declared by that package rather than merely resolvable from it, so the next one of these fails on the pull request that introduces it (objectui#4394).
|
|
164
|
+
|
|
165
|
+
- 7d04b0e: `record:details` stops publishing a `layout` key the spec removed and the renderer never honoured
|
|
166
|
+
|
|
167
|
+
`record:details` declared `layout: enum ['auto','custom']` with `defaultValue: 'auto'` and the description "auto uses the object highlightFields; custom uses explicit sections". None of that was ever implemented. The renderer's only `schema.layout` read tested `'inline'` | `'compact'` — two values the schema never permitted — so both legal values fell through the same ternary and the key selected nothing. `auto` and `custom` have behaved identically for as long as both have existed.
|
|
168
|
+
|
|
169
|
+
Two directions were wrong with zero diagnostics: `layout: 'auto'` plus explicit `sections` still rendered the sections, and `layout: 'custom'` with no sections silently fell back to the flat body rather than reporting the missing groups. Because the input carried a `defaultValue`, this was not stale documentation — it was the manifest, the generated `sdui-intrinsics.d.ts` and the designer panel actively offering the key. An AI author writing `layout: 'custom'` believed it took effect.
|
|
170
|
+
|
|
171
|
+
`@objectstack/spec` 17.0.0 removed the property (objectstack#6946, ADR-0087 D2); `17.0.0-rc.6` is pinned here, so the key is already rejected on parse with a named migration message pointing at `os migrate meta --from 16`. This release completes the objectui half of that retirement: the input declaration is gone, and so is the dead `inline`/`compact` branch — the synthesized layout is now the constant it always resolved to.
|
|
172
|
+
|
|
173
|
+
Nothing that worked stops working. The body-source contract is unchanged and is now the only one declared: **`sections` renders the explicit groups; omitting it falls back to the flat body derived from the object's fields.** That is pinned in both directions, plus the empty-array boundary between them, in `recordDetailsBodySource.test.tsx`.
|
|
174
|
+
|
|
175
|
+
One gate got sharper on the way through. The parity test's "declares no top-level input the spec does not accept" check read raw `.shape` keys — but an ADR-0087 D2 tombstone stays _in_ the shape as a `z.never()`, so a retired key still answers "is this declared?" with yes. That is precisely why this input survived the rc.6 pin bump with every derived gate green. The check now filters tombstoned members out, so it catches the next D2 retirement instead of waving it through.
|
|
176
|
+
|
|
177
|
+
- c32a8a1: `richtext` fields are placed like the long-form fields they are — four layout sets stopped spelling the type three ways the spec rejects
|
|
178
|
+
|
|
179
|
+
`@objectstack/spec` spells the WYSIWYG type `richtext`, one word, and **rejects** `rich_text` and `rich-text`: both exist only as typo keys in the spec's own `suggestFieldType` table, so `FieldSchema` refuses a field declared with either. Four sets that place fields by matching the RAW type string carried nothing else — `SKIP_TYPES` in the related list spelled it `rich_text`, both `WIDE_FIELD_TYPES` and `SECONDARY_FIELD_TYPES` spelled it `rich-text` — so each set was inert for the only spelling a producer can emit, and every one of them named the type it was failing to handle.
|
|
180
|
+
|
|
181
|
+
For a real `richtext` field that meant: it was auto-derived into a related-list column, it never spanned the full row in a multi-column detail section or form (unlike `markdown` and `html` sitting right beside it in the same sets), and it stayed in the dense primary section of the record page instead of dropping into "More details". All four move together — half of them would have left the detail page and the form disagreeing about the same field, which is worse than the uniform gap.
|
|
182
|
+
|
|
183
|
+
The dead spellings are dropped rather than kept alongside the live one: the alias table is the single place aliases belong, and a set that carries both invites the next drift. The pins are derived from the spec's own `FieldType` vocabulary instead of enumerated, so a member that stops being a real type name fails by name — replacing an assertion that was green only because the set contained the string it asked about.
|
|
184
|
+
|
|
185
|
+
`markdown` joins `richtext` and `html` in the related list's `SKIP_TYPES`, on a measurement rather than on the assumption that it renders raw. It does not: markdown and richtext both render through `MarkdownCellRenderer`, formatted and sanitized. The reason none of the three works in a table is that the formatted output is block-level — a heading, paragraphs, a list — inside a single-line truncating cell, so a document shows as one clipped heading with the rest invisible. `textarea` stays derived for the same reason read the other way: it renders as plain truncated text, which is a useful column. Author-declared columns are untouched — this set only filters the zero-config auto-derive walk.
|
|
186
|
+
|
|
187
|
+
- 3f5f87c: `SchemaRenderer` states its real contract — a typed, required `schema` and a deliberate forwarding surface
|
|
188
|
+
|
|
189
|
+
`SchemaRenderer` is the renderer loop: every registered SDUI component is rendered through it. It handed `forwardRef` a props type of `{ schema: SchemaNode } & Record<string, any>`, which puts `string` into `keyof Props`, so `'ref' extends keyof Props` was always true, React's `PropsWithoutRef` took its `Omit` branch, and `Omit` over a type carrying a string index signature keeps only the index signature. Every declared prop was erased. Measured on the pre-fix source: `keyof ComponentProps<typeof SchemaRenderer>` was `string` and `ComponentProps<typeof SchemaRenderer>['schema']` was `any`, while the type argument went on declaring `SchemaNode`. The other half is the same defect seen from the call site — `<SchemaRenderer />` with no schema at all, `<SchemaRenderer schema={12345} />`, and an arbitrary misspelled prop each type-checked in silence. This is objectui#4422 / PR #4438's trap in the most central component in the repo, spelled `Record<string, any>` rather than `[key: string]: any`, which is why every previous sweep's grep and both shipped guards' detector reported the site as clean.
|
|
190
|
+
|
|
191
|
+
Graded **minor, not major**, on objectui#4528's reasoning: the type argument has always DECLARED `schema`; the index signature erased it from the resolved type, and restoring what the declaration documents is a fix to the published contract rather than a contract break.
|
|
192
|
+
|
|
193
|
+
**The forwarding surface is kept, deliberately.** This component forwards every prop it does not read to the component the schema names, resolved at runtime from a plugin-extensible registry — `packages/react/README.md` documents exactly that, and `@object-ui/components`' form renderer consumes the `onSubmit` it shows being forwarded. Closing that surface would state a false contract and would force every leaf plugin's props into this package. So the two halves are separated: the `forwardRef` type argument is the honest `SchemaRendererProps`, with no index signature for `PropsWithoutRef` to collapse, and the open surface is stated once in an explicit export annotation, which nothing routes through `Omit`. The published `.d.ts` shows the erasure disappearing: `ForwardRefExoticComponent<Omit<{ schema: SchemaNode } & Record<string, any>, "ref"> & RefAttributes<any>>` becomes `ForwardRefExoticComponent<SchemaRendererProps & Record<string, any> & RefAttributes<any>>`.
|
|
194
|
+
|
|
195
|
+
`SchemaRendererProps.schema` is declared as `BaseSchema | string | null | undefined` — what this component actually handles. It previously declared `@object-ui/core`'s `SchemaNode` interface, which requires `type: string` and so contradicted the component's own early returns for strings and nullish, while every caller held `@object-ui/types`' wider union. The erasure hid that mismatch completely.
|
|
196
|
+
|
|
197
|
+
**One declared behaviour change.** A non-object, non-string primitive schema now renders as its own text. It previously fell through to the shallow copy `{ ...schema }`, which spreads a primitive to an empty object, lost the `type` the renderer then looked up, and surfaced the red "Unknown component type: undefined" box — an accident of the spread rather than a decision. The declared props type excludes `number` / `boolean` so no author is invited to pass them; the runtime handling is defence-in-depth for untyped callers and stored metadata. Strings, `null`, `undefined`, `0` and `false` render exactly as before, and an object naming an unregistered type still gets the error box; all four are pinned.
|
|
198
|
+
|
|
199
|
+
Latent defects the erasure had been hiding, each surfaced by the repo-wide type-check and fixed at its call site: `DashboardRenderer` cast its widget schema to `Record<string, any>`, dropping the `type` every branch of `getComponentSchema` sets; `DashboardGridLayout`'s equivalent now states its return type instead of inferring a union that admitted a shape with no `type`; and `ReportViewer` handed a section's `content` array to the renderer whole, so a multi-node section rendered the unknown-component box instead of its content — arrays are mapped rather than widened into the renderer's declared input.
|
|
200
|
+
|
|
201
|
+
A repo-wide structural guard replaces the two per-package siblings' blocked direction: it judges every `forwardRef` in `packages/*/src` (219 sites) and its detector resolves `Record<string, …>` and `string`-keyed mapped types in addition to literal index signatures — the spelling the previous detector went blind on. It judges the type argument only, where an index signature is an accidental eraser, and never an export annotation, where one is a stated contract.
|
|
202
|
+
|
|
203
|
+
- 2fea4d2: `detail.showEmptyRelated` renders Russian and Arabic again — the "+N empty" button no longer falls through to English at the counts it takes most often
|
|
204
|
+
|
|
205
|
+
This was the repo's only pre-existing i18next plural family, and all ten packs defined exactly two slots: `_one` and `_other`. i18next asks `Intl.PluralRules` for the one suffix a language needs for that number, and when the pack has no such slot it walks `fallbackLng` to `en`. Russian has four plural categories and Arabic six, so `ru` at counts 2-4 (`few`) and 5-20, 25-30, … (`many`), and `ar` at 0, 2, 3-10 and 11-99, resolved nothing locally and rendered the English string. The call site is the collapsed-empties button in the record detail's reference rail, whose count is the number of empty related lists — 2 to 4 are the most common values it ever takes, so a Russian user essentially always read English.
|
|
206
|
+
|
|
207
|
+
The fix is a base key (no suffix) beside the two existing slots, in all ten packs. The base key is always in i18next's lookup chain, so every category a pack did not enumerate resolves to it, in that pack's own language — and, unlike adding `_few`/`_many` to `ru` alone, it keeps the ten packs' key sets identical, which full key parity requires. Same shape objectui#3546 slice six established for `perm.facet.*`. Where the base key is genuinely reachable it carries a count-invariant phrasing: `ru` uses the «Существительное: {{count}}» form the pack already writes 22 times, `ar` the «{{count}} مفرد(جمع)» marker it uses throughout. For `en`/`de`/`zh`/`ja`/`ko` the base key cannot be reached at all (their categories are covered by the two existing slots) and repeats `_other` for parity; `fr`/`es`/`pt` reach it only from a million up, where the plural form is already correct. No English copy moves.
|
|
208
|
+
|
|
209
|
+
The provider-less path needed the same row for a different reason: `createSafeTranslation`'s fallback resolves `defaults[key]` literally and never appends a plural suffix, so the two suffixed rows in plugin-detail's defaults table were unreachable through it and that path answered with the raw key. It now carries the base key too.
|
|
210
|
+
|
|
211
|
+
Parity across packs turned out to be necessary and not sufficient — ten identical key sets were green throughout, because the defect is one level below key names: the slot the language needs is not in the set. So the invariant "a plural family must carry a base key" is now asserted over all ten packs in `all-locales-key-parity.test.ts`, where it is pack-intrinsic and fails at PR time without needing a call site to exist. It went red on all ten packs before this change and names the family that is missing its base.
|
|
212
|
+
|
|
213
|
+
- dad805d: Six i18n keys no longer render as raw key strings on hosts with no `I18nProvider` (objectui#4396)
|
|
214
|
+
|
|
215
|
+
`detail.saving`, `list.resetSortToDefault`, `appDesigner.widgetProperties`, `appDesigner.addWidget`, `appDesigner.modeEdit` and `common.delete` were read through `createSafeTranslation` without a row in their hook's defaults table and without an inline `defaultValue` at the call site — the only two fallbacks that path has. On a provider-less host (standalone embedding, the preview gallery, host apps that never mount a provider) `fallbackT` therefore returned the key itself, so users saw `detail.saving` in the inline-edit save button, `list.resetSortToDefault` on the sort popover's reset control, `appDesigner.widgetProperties` as the dashboard inspector heading, `appDesigner.addWidget` as its toolbar label, `appDesigner.modeEdit` as a button's accessible name, and `common.delete` on the designer's destructive confirm.
|
|
216
|
+
|
|
217
|
+
Each key now has a row in its consumer hook's defaults table, byte-identical to the `en` pack value. No pack was edited, no key added, no call site changed.
|
|
218
|
+
|
|
219
|
+
- 35997ce: fix(plugin-detail): synthesize page components in the spec's `properties` carrier so Studio page-create can persist
|
|
220
|
+
|
|
221
|
+
Creating a page in Studio never completed. The create path seeds a record
|
|
222
|
+
page's `regions` from `buildDefaultPageSchema(objectDef)` and PUTs the result,
|
|
223
|
+
and every node that synthesizer emitted carried its widget props at the TOP
|
|
224
|
+
level of the component — `{ type: 'page:header', recordChrome: true }`,
|
|
225
|
+
`{ type: 'page:tabs', items: [...] }`, and the same for `record:highlights`,
|
|
226
|
+
`record:path`, `record:details`, `record:related_list`, `record:history` and
|
|
227
|
+
`record:reference_rail`. ADR-0089 D3a closed `PageComponentSchema` with
|
|
228
|
+
`.strict()`, so those keys are not stripped, they are a parse error
|
|
229
|
+
(`Unrecognized key(s) on this view/page schema: 'recordChrome', 'actions'`).
|
|
230
|
+
The server refused the body and no page row was ever stored.
|
|
231
|
+
|
|
232
|
+
The props now go where the spec declares them — the node's `properties` bag,
|
|
233
|
+
which is where `ComponentPropsMap` defines `page:header.recordChrome` and
|
|
234
|
+
`page:tabs.items` in the first place. Nothing is dropped and nothing changes on
|
|
235
|
+
screen: a header still defaults to record chrome ON, an author's
|
|
236
|
+
`recordChrome: false` is still carried (and now actually persists), the tabs
|
|
237
|
+
keep their items, and `SchemaRenderer` hoists `properties` back onto the node
|
|
238
|
+
before dispatch, so every renderer receives exactly the props it did before.
|
|
239
|
+
|
|
240
|
+
One code path does the wrapping for every node the synthesizer builds, so there
|
|
241
|
+
is a single answer to "what may go in a page write". Slot overrides are
|
|
242
|
+
untouched — a node handed in by a caller is still placed verbatim.
|
|
243
|
+
|
|
244
|
+
- b388950: fix(plugin-detail): the record detail header honors `userActions` predicates
|
|
245
|
+
|
|
246
|
+
`userActions.delete` reached the record detail header in its **boolean** form
|
|
247
|
+
but not in its **predicate** form. `userActions: { delete: false }` removed
|
|
248
|
+
Delete from the row kebab, the selection bar and the detail header, because the
|
|
249
|
+
host lowers the boolean into `schema.showDelete`. `userActions: { delete: {
|
|
250
|
+
visibleWhen: … } }` reached only the row kebab: the header ANDed
|
|
251
|
+
`schema.showDelete ∧ objectAllowsDelete ∧ canDeleteRecord` and never evaluated
|
|
252
|
+
the predicate, so an author upgrading from "nobody may delete this object" to
|
|
253
|
+
"these records may not be deleted" silently lost the surface they were most
|
|
254
|
+
likely to have tested on.
|
|
255
|
+
|
|
256
|
+
The header now folds the per-record predicates in as a fourth conjunct,
|
|
257
|
+
evaluated against the open record through the same helper family the row
|
|
258
|
+
surfaces use — `userActionPredicates` from `@object-ui/core` for the parse (the
|
|
259
|
+
import `RelatedList` already makes) and `useRowPredicate` from
|
|
260
|
+
`@object-ui/react` for the evaluation:
|
|
261
|
+
|
|
262
|
+
- `delete.visibleWhen` / `edit.visibleWhen` false → the header affordance is
|
|
263
|
+
hidden (fails closed; `visibleWhen: false` counts as a declared gate).
|
|
264
|
+
- `delete.disabledWhen` / `edit.disabledWhen` true → the header affordance
|
|
265
|
+
renders disabled rather than disappearing, matching the row kebab.
|
|
266
|
+
|
|
267
|
+
The existing permission and record-writability gates are unchanged, so a
|
|
268
|
+
predicate that holds can never resurrect a button the user may not press. The
|
|
269
|
+
boolean form stays the host's channel — a bare boolean yields no predicate.
|
|
270
|
+
|
|
271
|
+
- Updated dependencies [0e67b53]
|
|
272
|
+
- Updated dependencies [932cbcd]
|
|
273
|
+
- Updated dependencies [734d186]
|
|
274
|
+
- Updated dependencies [3fc2971]
|
|
275
|
+
- Updated dependencies [f7c6430]
|
|
276
|
+
- Updated dependencies [ae10a01]
|
|
277
|
+
- Updated dependencies [92876f0]
|
|
278
|
+
- Updated dependencies [828549a]
|
|
279
|
+
- Updated dependencies [e1ade8f]
|
|
280
|
+
- Updated dependencies [3e19fe7]
|
|
281
|
+
- Updated dependencies [bb58d1d]
|
|
282
|
+
- Updated dependencies [5cc847c]
|
|
283
|
+
- Updated dependencies [fa21254]
|
|
284
|
+
- Updated dependencies [33c32bf]
|
|
285
|
+
- Updated dependencies [66fb4fa]
|
|
286
|
+
- Updated dependencies [6d641c9]
|
|
287
|
+
- Updated dependencies [45e1949]
|
|
288
|
+
- Updated dependencies [92250d6]
|
|
289
|
+
- Updated dependencies [58bebf6]
|
|
290
|
+
- Updated dependencies [405e808]
|
|
291
|
+
- Updated dependencies [c0f9a4b]
|
|
292
|
+
- Updated dependencies [ac853ce]
|
|
293
|
+
- Updated dependencies [fa51109]
|
|
294
|
+
- Updated dependencies [d46f9b8]
|
|
295
|
+
- Updated dependencies [2fea4d2]
|
|
296
|
+
- Updated dependencies [7f1cb33]
|
|
297
|
+
- Updated dependencies [2e3b0c0]
|
|
298
|
+
- Updated dependencies [78fa331]
|
|
299
|
+
- Updated dependencies [31ab1ac]
|
|
300
|
+
- Updated dependencies [0082db8]
|
|
301
|
+
- Updated dependencies [06915b0]
|
|
302
|
+
- Updated dependencies [ff84b05]
|
|
303
|
+
- @object-ui/i18n@17.5.0
|
|
304
|
+
|
|
305
|
+
## 17.4.0
|
|
306
|
+
|
|
307
|
+
### Minor Changes
|
|
308
|
+
|
|
309
|
+
- 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)
|
|
310
|
+
|
|
311
|
+
`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.
|
|
312
|
+
|
|
313
|
+
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.
|
|
314
|
+
|
|
315
|
+
Removed, with a tombstone at each site:
|
|
316
|
+
|
|
317
|
+
- `@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.
|
|
318
|
+
- `@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.
|
|
319
|
+
- `@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.
|
|
320
|
+
- `@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.
|
|
321
|
+
|
|
322
|
+
`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.
|
|
323
|
+
|
|
324
|
+
`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.
|
|
325
|
+
|
|
326
|
+
### Patch Changes
|
|
327
|
+
|
|
328
|
+
- f0a625a: `disabled: ''` no longer greys out the remaining five action surfaces (objectui#3849)
|
|
329
|
+
|
|
330
|
+
objectui#3842 / PR #3851 fixed the "is a `disabled` gate DECLARED?" test on
|
|
331
|
+
`action:button` and app-shell's `DeclaredActionsBar`. Five same-shaped sites were
|
|
332
|
+
outside that PR's scope and stayed on `!= null`, so within one component the
|
|
333
|
+
`visible` gate asked `hasDeclaredVisibilityGate` while the `disabled` gate on the
|
|
334
|
+
next line asked `!= null` — two spellings of one question:
|
|
335
|
+
|
|
336
|
+
- `@object-ui/components` — `action:icon`, `action:group`'s inline button
|
|
337
|
+
(`InlineActionButton`) and dropdown item (`DropdownActionItem`), and
|
|
338
|
+
`action:menu`'s item (`ActionMenuItem`).
|
|
339
|
+
- `@object-ui/plugin-detail` — `record:quick_actions`' `QuickActionButton`.
|
|
340
|
+
|
|
341
|
+
Why the missing `!== ''` half is a defect on this key and not on `visible`:
|
|
342
|
+
`toPredicateInput('')` is `undefined` and `evaluateCondition(undefined)` is
|
|
343
|
+
`true`. On `visible` that `true` means SHOW, so an over-broad "declared" test and
|
|
344
|
+
a permissive empty predicate cancel out. On `disabled` it means DISABLE, so they
|
|
345
|
+
compound — `disabled: ''` (an empty predicate: nothing declared) rendered a
|
|
346
|
+
permanently greyed-out control, with nothing the author could write to un-grey
|
|
347
|
+
it. Unlike #3842's approvals inbox, these five are the general action face
|
|
348
|
+
(toolbars, dropdowns, record quick actions), so the reach is wider even though no
|
|
349
|
+
single high-value host owns them.
|
|
350
|
+
|
|
351
|
+
**Behaviour change surface, deliberately narrow.** Only `disabled: ''` changes —
|
|
352
|
+
from disabled to clickable, which is what "no predicate" asked for. `disabled:
|
|
353
|
+
true` still disables, `disabled: false` and an absent `disabled` still do not, and
|
|
354
|
+
no expression-valued `disabled` changes verdict. On the four sites that also carry
|
|
355
|
+
the legacy non-spec `enabled` fallback, one consequence follows: an empty
|
|
356
|
+
`disabled` now falls THROUGH to that leg instead of short-circuiting on the empty
|
|
357
|
+
predicate, so an action spelling both (`disabled: ''` + `enabled: true`) becomes
|
|
358
|
+
clickable. `record:quick_actions` has no `enabled` leg, so its chain is the single
|
|
359
|
+
gate.
|
|
360
|
+
|
|
361
|
+
Routing those legacy `enabled` legs through the same definition is
|
|
362
|
+
behaviour-preserving by derivation rather than a fix: the leg is negated
|
|
363
|
+
(`disabled = !isEnabled`), so an empty predicate's `true` already arrived as "not
|
|
364
|
+
disabled" — the verdict "no gate declared" produces. #3842's four-shape derivation
|
|
365
|
+
table is reproduced next to the new pins, together with the statement that no
|
|
366
|
+
`enabled` case can go red by reverting that leg.
|
|
367
|
+
|
|
368
|
+
`hasDeclaredVisibilityGate` keeps its historic name (the objectui#3842 ruling): the
|
|
369
|
+
predicate is key-neutral, and one implementation behind two names is how a repo
|
|
370
|
+
grows dialects. The three `@object-ui/components` sites import it relatively;
|
|
371
|
+
`record:quick_actions` takes it from the package barrel, the cross-package route
|
|
372
|
+
objectui#3835 opened. Every call site says so in a comment.
|
|
373
|
+
|
|
374
|
+
- 5bfaabd: `PageComponentSchema.dataSource` now reaches every object-bound block, not just
|
|
375
|
+
`list-view` — and `element:record_picker` stops discarding `view`
|
|
376
|
+
(objectstack#6953).
|
|
377
|
+
|
|
378
|
+
objectstack#5576 wired the spec's per-element data binding
|
|
379
|
+
(`dataSource: { object, view?, filter?, sort?, limit? }`) to `list-view` and left
|
|
380
|
+
the same declaration inert on every other page component. Two gaps remained, and
|
|
381
|
+
both were silent:
|
|
382
|
+
|
|
383
|
+
- **`element:record_picker` read four of the five keys and dropped `view`.** So
|
|
384
|
+
`dataSource: { object: 'account', view: 'hot' }` — the spec's own example —
|
|
385
|
+
built a picker over EVERY account instead of the rows the saved view selects.
|
|
386
|
+
Nothing threw and nothing rendered an error; the option list was simply wider
|
|
387
|
+
than what was authored, which also means a user could select a record the page
|
|
388
|
+
said was out of scope.
|
|
389
|
+
- **`object-grid` / `object-form` / `object-kanban` / `object-calendar` /
|
|
390
|
+
`object-chart` / `object-metric` / `record:related_list` read none of it.**
|
|
391
|
+
Each gates its fetch on its own `objectName`, and nothing mapped
|
|
392
|
+
`dataSource.object` onto it, so a page written the way the spec documents
|
|
393
|
+
rendered an empty grid / a field-less form / a board with no cards / an empty
|
|
394
|
+
month / an empty chart / a static metric number — with no request and no
|
|
395
|
+
diagnostic anywhere. Spec-valid metadata rendering nothing is the
|
|
396
|
+
objectstack#4413 shape.
|
|
397
|
+
|
|
398
|
+
Composition follows objectstack#5576's landed semantics unchanged on every block:
|
|
399
|
+
a named saved view supplies the baseline, a key written on the component itself
|
|
400
|
+
overrides it, an explicit binding key overrides both, `filter` AND-combines
|
|
401
|
+
("additional filter criteria" — a binding can narrow a view, never widen it), and
|
|
402
|
+
a `view` name that does not resolve renders a configuration error instead of
|
|
403
|
+
degrading to the object's full scope.
|
|
404
|
+
|
|
405
|
+
- `@object-ui/react` — new `useElementDataSourceSchema(schema, mapping, dataSource?)`
|
|
406
|
+
and `ElementDataSourceGate` apply a resolved binding to the schema keys a given
|
|
407
|
+
block reads, plus `ElementDataSourceErrorPanel` / `ElementDataSourceLoadingPanel`
|
|
408
|
+
for the two non-final states. One precedence table for all blocks rather than
|
|
409
|
+
one copy per block — that copy is how "additional filter criteria" would have
|
|
410
|
+
become two dialects.
|
|
411
|
+
- A mapping names **only** keys its block genuinely reads. A composed value
|
|
412
|
+
written onto a key the block ignores would be accepted and dropped, which is
|
|
413
|
+
the defect being removed, one layer deeper — so a kanban's swimlane `columns`
|
|
414
|
+
never receive a view's field list, and a block with no row cap leaves `limit`
|
|
415
|
+
unmapped. The per-block coverage table, including two residual gaps that are
|
|
416
|
+
named rather than papered over, is in `content/docs/guide/data-source.md`.
|
|
417
|
+
|
|
418
|
+
No behaviour changes for a block that carries no `dataSource`: the binding-free
|
|
419
|
+
path returns the schema by reference, so nothing remounts and nothing refetches.
|
|
420
|
+
|
|
421
|
+
- 7b3e048: `record:highlights` publishes the `readonly` entry key, so an AI author can discover it from the manifest
|
|
422
|
+
|
|
423
|
+
`readonly` on a `fields[]` entry has been enforced for a while — the renderer copies it
|
|
424
|
+
through normalization and `HeaderHighlight`'s editability gate refuses inline editing on a
|
|
425
|
+
chip carrying it (objectstack#5077) — and `@objectstack/spec` declares it on
|
|
426
|
+
`RecordHighlightsField` (objectstack#5176 / PR #5607). The block's own published authoring
|
|
427
|
+
surface never mentioned it: the `fields` input still spelled the entry shape
|
|
428
|
+
`{name,label?,icon?,type?}`, and since the registry `inputs` are what
|
|
429
|
+
`gen-manifest.ts` serializes into `sdui.manifest.json`, an author reading the manifest was
|
|
430
|
+
told the key did not exist. The `fields` description now states the full entry shape and
|
|
431
|
+
what `readonly` does, which is the discoverability the manifest is for.
|
|
432
|
+
|
|
433
|
+
`readonly` is documented **inside** the `fields` description rather than declared as an
|
|
434
|
+
input of its own, because that is where the contract puts it. The spec's
|
|
435
|
+
`RecordHighlightsProps` has exactly three top-level keys (`fields`, `layout`, `aria`) and
|
|
436
|
+
carries `readonly` per ENTRY. A top-level `{ name: 'readonly', type: 'boolean' }` input
|
|
437
|
+
would publish a key the platform silently discards: the generated `sdui.manifest.json` and
|
|
438
|
+
`sdui-intrinsics.d.ts` would advertise a `readonly` prop, the manifest gate validates
|
|
439
|
+
top-level props only and would raise no diagnostic, `RecordHighlightsProps` is a plain
|
|
440
|
+
`z.object` so the unknown key is stripped on parse without error, and the renderer — which
|
|
441
|
+
reads `field.readonly` per entry — would never see it. An author who trusted that surface
|
|
442
|
+
would be left with the machine-owned column still hand-editable and no diagnostic anywhere
|
|
443
|
+
explaining why. `ComponentInput` is flat by design, so an array-of-objects input publishes
|
|
444
|
+
its member keys in prose, as `record:path.stages` and `record:alert.action` already do.
|
|
445
|
+
|
|
446
|
+
A new spec-parity test derives both directions from `@objectstack/spec` at runtime instead
|
|
447
|
+
of restating today's key list: every key of `RecordHighlightsField`'s object arm must be
|
|
448
|
+
named in the `fields` description, and the block must declare no top-level input that
|
|
449
|
+
`RecordHighlightsProps` does not accept. Nothing previously cross-checked the registry
|
|
450
|
+
`inputs` against the spec, so both drift directions were silent. No runtime behaviour
|
|
451
|
+
changes.
|
|
452
|
+
|
|
453
|
+
- aca561a: Four spec keys the renderers already honoured are now discoverable from the published `inputs`
|
|
454
|
+
|
|
455
|
+
`record:details.hideFields`, `record:related_list.relationshipValueField`,
|
|
456
|
+
`record:related_list.add` and `element:text_input.defaultValue` were declared by
|
|
457
|
+
`@objectstack/spec` and read by their renderers, while the registry `inputs` —
|
|
458
|
+
the surface `gen-manifest.ts` serializes into `sdui.manifest.json` and
|
|
459
|
+
`sdui-intrinsics.d.ts` — never mentioned them. Nothing anywhere reported the
|
|
460
|
+
mismatch, and every layer that reads a manifest said the opposite of the
|
|
461
|
+
runtime: the keys were in no designer panel and no generated `.d.ts`,
|
|
462
|
+
`sdui-parser`'s prop walk returned `unknown-prop` for an author who wrote one,
|
|
463
|
+
and the renderer honoured it regardless. That is objectui#3407's original
|
|
464
|
+
complaint (`readonly` was enforced and honoured, the description just never said
|
|
465
|
+
so) on four more keys.
|
|
466
|
+
|
|
467
|
+
Each description is derived from what the renderer actually does, not from
|
|
468
|
+
restating the spec's one-liner, because the two can differ and the published
|
|
469
|
+
text is what an AI author reads:
|
|
470
|
+
|
|
471
|
+
- `hideFields` documents bare field names only — the renderer tolerates
|
|
472
|
+
`{name}` / `{field}` entries but the spec is `z.array(z.string())` and rejects
|
|
473
|
+
them, so teaching that spelling would publish a dialect the contract refuses;
|
|
474
|
+
- `relationshipValueField` publishes the renderer's `'id'` default and says that
|
|
475
|
+
the resolved value drives the list filter, the Add-picker link value and the
|
|
476
|
+
pre-filled create form together;
|
|
477
|
+
- `add` publishes its member shape in prose (`ComponentInput` is flat and has no
|
|
478
|
+
member-shape slot) with each default taken from the renderer — including
|
|
479
|
+
`picker.labelField`, where the renderer defaults to `name` while the spec's
|
|
480
|
+
own wording says "the object title field". It also names `picker.filter` as a
|
|
481
|
+
KNOWN GAP rather than documenting it as a restriction: the spec declares it
|
|
482
|
+
and nothing reads it, so an author would otherwise believe their picker is
|
|
483
|
+
scoped when it offers every record (objectui#3831);
|
|
484
|
+
- `defaultValue` distinguishes the two behaviours an author can get — seeding a
|
|
485
|
+
bound page variable once while it is still empty, versus the native
|
|
486
|
+
uncontrolled initial value with no variable bound.
|
|
487
|
+
|
|
488
|
+
`element:text_input` is not in the public tier, so its gap was not in
|
|
489
|
+
`sdui.manifest.json` at all — it was in the JSX-page compiler's prop whitelist,
|
|
490
|
+
which `renderers/layout/page.tsx` builds from `getKnownTypes()` plus these same
|
|
491
|
+
`inputs`, making the undeclared `defaultValue` a live `unknown-prop` warning.
|
|
492
|
+
|
|
493
|
+
The repo-wide parity gate now runs in both directions over one covered set and
|
|
494
|
+
one exemption discipline, so neither direction can be forgotten again the way
|
|
495
|
+
the reverse half was after PR #3806. Nine spec keys stay deliberately
|
|
496
|
+
unpublished, each with a written reason and a tracking issue: two the renderers
|
|
497
|
+
do not read at all (objectui#3829), three retired upstream by ADR-0087
|
|
498
|
+
tombstones, `page:tabs.type` (a carrier collision, objectstack#6776), two
|
|
499
|
+
`targetVariable` declarative hints (objectui#3834), and
|
|
500
|
+
`element:record_picker.filter` (objectui#3830).
|
|
501
|
+
|
|
502
|
+
- 6d762da: The five locale keys behind #3546's eight no-fallback `t()` call sites are now defined in all ten packs, so the built-in-view toasts, the activity-timeline source link, the wizard's required-field toast and the Gantt refresh button's accessible name are translated instead of falling back to English — or, on two surfaces, to the key itself (part of #3546).
|
|
503
|
+
|
|
504
|
+
`scripts/check-i18n-call-site-keys.mjs` measured 258 keys that a `t()` call site asks for and no pack defines. These five were the subset with no working inline default: `console.objectView.cannotEditMetaView`, `console.objectView.cannotDeleteMetaView`, `detail.viewSource`, `gantt.toolbar.refresh` and `wizard.missingRequired`. Adding a `defaultValue` is deliberately not the fix — that mechanism is what kept all 258 invisible for months.
|
|
505
|
+
|
|
506
|
+
**Two of the eight sites really did render the raw key**, and both go through a binding with nothing in front of i18next. `ObjectView.tsx` calls `useObjectTranslation()` directly, so five toasts read `console.objectView.cannotEditMetaView` / `cannotDeleteMetaView` on screen; the `|| 'Built-in views cannot be renamed.'` guards next to them were dead on every path, because i18next answers a miss with the key itself and a non-empty string never falls through `||`. Those four unreachable English strings are removed rather than repaired: one key served four call sites (rename / pin / set-as-default / configure), so the pack copy covers any change to a built-in view instead of naming one operation. `RecordActivityTimeline.tsx` fails the same way for a subtler reason — `useDetailTranslation` is `createSafeTranslation(..., 'detail.back')`, and because `detail.back` does resolve, the probe hands back i18next's `t` for every key and bypasses the defaults map wholesale, so `detail.viewSource` reached the user verbatim.
|
|
507
|
+
|
|
508
|
+
**The other two sites were not rendering a raw key**, contrary to the issue's description, and are fixed here as the milder "English in all ten languages" class. `wizard.missingRequired` is its own hook's probe key, so the probe failed and `createSafeTranslation` correctly served its English default. `gantt.toolbar.refresh` goes through `useGanttTranslation`, which deliberately does not use `createSafeTranslation` and falls back per key — so the refresh button's `aria-label` was "Refresh", in English, never the key. Screen-reader users heard an English word rather than an identifier; a `zh` session now hears 刷新.
|
|
509
|
+
|
|
510
|
+
Regression cover is provider-mounted on purpose: with no `I18nProvider` the defaults maps answer every one of these keys and the assertions pass while the console is broken, which is precisely the false-green the issue documents. For the two sites whose English output was already correct, `en` cannot discriminate before from after — the `zh` assertions are the ones that pin the fix.
|
|
511
|
+
|
|
512
|
+
- 4178d5a: `record:details` 的 `sections` 输入说明改为从 spec 形状派生的对象形,不再教已被退役的「Section IDs」
|
|
513
|
+
|
|
514
|
+
`inputs` 不是文档,而是发布出去的编写契约:`gen-manifest.ts` 把它序列化进
|
|
515
|
+
`sdui.manifest.json`(保存门 + parser 白名单)和 `sdui-intrinsics.d.ts`。而
|
|
516
|
+
`record:details.sections` 的说明写的是 `Section IDs to show (required when layout
|
|
517
|
+
is "custom")` —— 那是 17.x 以前的形状。pin 版 `@objectstack/spec@17.0.0-rc.5` 的
|
|
518
|
+
`RecordDetailsProps.sections` 是对象数组 `{ name?, label?, columns?, fields }`,
|
|
519
|
+
objectstack#5611 把 `z.array(z.string())` 那条拼法**删掉**而不是 union 进来(既无
|
|
520
|
+
producer 也无 consumer,一种形状而不是两套事实契约)。
|
|
521
|
+
|
|
522
|
+
照旧说明写 `sections: ['contact_info', 'address']` 的作者,在四层之间拿不到任何
|
|
523
|
+
诊断:`['a','b']` 对 manifest 门是合法 `array`(门只看顶层键名 + 粗类型),上游
|
|
524
|
+
`validateComponentProps` 是 advisory 级,spec 只在真的走 parse 的路径上才拒,而
|
|
525
|
+
`RecordDetailsRenderer` 对每个条目读 `s.name` / `s.label` / `s.fields` —— 字符串上
|
|
526
|
+
三者全 `undefined`,该 section 一个字段都不渲染。`layout: 'custom'` 时 sections 是
|
|
527
|
+
详情页正文的唯一来源,所以结果是一张没有报错的空白详情页。
|
|
528
|
+
|
|
529
|
+
新说明逐键派生自 spec 各成员的 `.describe()` 与渲染器实读:`fields` 必填、按序渲染;
|
|
530
|
+
`label` 是标题(省略即无标题、无边框);`name` 是 snake_case 稳定标识与 i18n 锚点
|
|
531
|
+
(标题走 `objects.{object}._sections.{name}.label`);`columns`(1-4)是本 section 的
|
|
532
|
+
字段栅格宽度,省略则由渲染器推导;并明确写出字符串条目不被接受。渲染器另外还认的
|
|
533
|
+
`title` / `showBorder` / `hideEmpty` **故意不写进说明** —— spec 的 section 对象没有
|
|
534
|
+
声明它们,parse 时会被静默剥掉,发布它们等于教作者写契约丢弃的键。
|
|
535
|
+
|
|
536
|
+
同时新增 `recordDetailsInputs.spec-parity.test.ts`:两个方向的断言都在运行时从 spec
|
|
537
|
+
schema 派生(每个 spec 成员键都能从说明里发现;本 block 不声明 spec 不接受的顶层
|
|
538
|
+
input),所以下一次 spec 变形会先让测试红,而不是又一次静默张开。仅说明文本变化,无
|
|
539
|
+
运行时行为改动。
|
|
540
|
+
|
|
541
|
+
- 54233b1: Record detail pages: a header ⟳ that refreshes the record, its related lists and its tab counts in place — no browser reload
|
|
542
|
+
|
|
543
|
+
Concurrent-editing scenario from the shop floor (MES work orders): operator A sits on a record's detail page while operator B starts or reports the same order. A had no way to see the new state except F5, which throws away the open tab, the scroll position and any in-progress inline edit along with the stale data.
|
|
544
|
+
|
|
545
|
+
The pipeline for this already existed — the objectui#2269 invalidation bus refetches every mounted reader in place, and `RecordContext.refresh` had been declared for it — but nothing produced that field and no UI reached for it. Three changes give it a trigger:
|
|
546
|
+
|
|
547
|
+
- **`RecordDetailView` produces `RecordContext.refresh`**, publishing `notifyDataChanged({ objectName: '*' })`. The wildcard is deliberate: a user reaches for refresh because of a write made by SOMEONE ELSE, which this client never saw and therefore cannot attribute to particular objects. `'*'` marks everything mounted as stale, so the main record, every related child list and the tab-count badges all refetch — no remount, so tab / scroll / draft state survive. First phase covers the standalone record route; embedded hosts (list drawer, split-pane preview) keep their existing chrome unchanged.
|
|
548
|
+
- **`page:header` renders the ⟳** at the far end of the header row when — and only when — the host provides `refresh`. It is page chrome rather than a header action, so its position is the same on every record page regardless of which business actions the object declares, and it can never be collapsed into the `⋯` overflow. Styled as that `⋯` trigger's twin so the row reads as one button family. Its accessible name and tooltip come from the existing `common.refresh` key, so the icon-only button is not English-only in the other nine locales. The icon spins for a short floor after a click, because the bus is fire-and-forget and a warm backend would otherwise finish before the click looked like it landed.
|
|
549
|
+
- **`RelatedList` accepts the `'*'` wildcard** on the legacy `objectui:related-changed` event, matching what `dataChangeMatches` already does for the bus's own readers. This listener compared the payload's object name to its own, so a wildcard invalidation reached everything on the page except the related lists — a concrete foreign object name is still ignored.
|
|
550
|
+
|
|
551
|
+
Hosts that provide no `refresh` render exactly as before.
|
|
552
|
+
|
|
553
|
+
- c2ecbae: 相关列表 Add 选择器兑现 `add.picker.filter`:作者限定的候选范围现在真的生效
|
|
554
|
+
|
|
555
|
+
`record:related_list.add.picker.filter` 被 spec 声明为「Restrict which records the picker offers」,但渲染器从未读过它 —— 挂 `RecordPickerDialog` 时不传任何 filter,对话框照样提供 `picker.object` 的全部记录,选中即建链接行或改父,`os validate` / `os build` 全绿、运行时零诊断。作者写下「只允许指派 active 的岗位」「只允许挂未过期的许可」,得到的是完整候选列表。
|
|
556
|
+
|
|
557
|
+
现在它按原样传给 `RecordPickerDialog` 的 `baseFilter` —— 不是 `lookupFilters`,后者会把条件渲染成用户可编辑的筛选栏行,等于把作者的硬性限制降级成建议。
|
|
558
|
+
|
|
559
|
+
`baseFilter` 因此接受两种形状,按结构判别(`Array.isArray`):
|
|
560
|
+
|
|
561
|
+
- **`QueryParams.$filter` 记录形式**(依赖型 lookup 链)保持原有的键覆盖语义逐字节不变 —— 级联父值必须**替换**同字段上过期的 `lookupFilters` 条目,而不是与之求交。
|
|
562
|
+
- **spec 的 `ViewFilterRule[]`** 经 `mergeFilterNodes`(仓内唯一的 filter 下沉口)下沉,19 个 operator 全部无损到达服务端,包括记录形式没有 `$op` 可用的 `before` / `after` / `is_empty` / `is_not_empty`。此处**不新增**第二份 operator 词汇表。
|
|
563
|
+
|
|
564
|
+
槽位类型同时从 `Record<string, any>` 收紧为 `unknown`:前者会接受规则数组(数组满足 `any` 的字符串索引),旧的对象展开再把它压成 `{"0": {...}}`,于是查询去过滤名为 `0` 的列 —— 类型全绿、查询错误、无任何诊断。
|
|
565
|
+
|
|
566
|
+
- acc34c5: `record:related_list`: an `add` without `add.picker` no longer takes the whole related list down.
|
|
567
|
+
|
|
568
|
+
The Add-picker gate compared only `add` for truthiness and then read `add.picker.object` bare, so page metadata declaring `add` but omitting the (spec-required) `picker` threw during render and `SchemaRenderer` replaced the entire related list with a "Component failed to render" card whose message never mentioned `picker`. Both the Add button and the picker dialog now gate on the resolved `add.picker.object` — the list body renders as usual, only the unconfigured Add affordance is withheld, and a console hint names the missing key. Off-spec `add` still does nothing, so no lenient second dialect is introduced; producing-side validation of page metadata is tracked separately.
|
|
569
|
+
|
|
570
|
+
- c4768a7: `record:related_list` — the declared `filter` reaches the query, and the Add button answers to the same gate as its dialog
|
|
571
|
+
|
|
572
|
+
- **`filter` is consumed** (objectstack#7118). The spec declares
|
|
573
|
+
`RecordRelatedListProps.filter` ("additional filter criteria") and this repo
|
|
574
|
+
published it as a registry input, but nothing read it: `RelatedList` built its
|
|
575
|
+
query from `{ [relationshipField]: parentId }` alone, so an authored filter was
|
|
576
|
+
accepted by every gate and silently dropped — the list answered with every child
|
|
577
|
+
of the parent. It is now AND-combined with the parent condition (never
|
|
578
|
+
substituted for it, so an additional criterion can only narrow), lowered through
|
|
579
|
+
the repo's single filter sink so the spec's `[{ field, operator, value }]`
|
|
580
|
+
vocabulary and a composed `dataSource` binding both work. With nothing authored
|
|
581
|
+
the query is unchanged. As a consequence a saved view named through
|
|
582
|
+
`dataSource: { object, view }` no longer contributes its columns/sort/limit while
|
|
583
|
+
its filter is discarded — the list can no longer be wider than the view it names.
|
|
584
|
+
On the legacy raw-URL fallback path, which cannot express an operator, a declared
|
|
585
|
+
filter is refused with a console explanation instead of dropped.
|
|
586
|
+
- **The Add button now requires `dataSource`** (objectui#3895), matching the picker
|
|
587
|
+
dialog and the add callback. In hosts that supply no `RecordContext` — Studio
|
|
588
|
+
designer previews, context-free embeds — the button rendered and did nothing at
|
|
589
|
+
all when clicked; the affordance is now withheld where the capability behind it
|
|
590
|
+
is absent.
|
|
591
|
+
|
|
592
|
+
- Updated dependencies [7864f03]
|
|
593
|
+
- Updated dependencies [f5f8744]
|
|
594
|
+
- Updated dependencies [69becd2]
|
|
595
|
+
- Updated dependencies [5e52495]
|
|
596
|
+
- Updated dependencies [b750823]
|
|
597
|
+
- Updated dependencies [ac2139c]
|
|
598
|
+
- Updated dependencies [b14ab3a]
|
|
599
|
+
- Updated dependencies [8c60819]
|
|
600
|
+
- Updated dependencies [e64a52e]
|
|
601
|
+
- Updated dependencies [844d17f]
|
|
602
|
+
- Updated dependencies [4dcd52a]
|
|
603
|
+
- Updated dependencies [42ae5c6]
|
|
604
|
+
- Updated dependencies [6d762da]
|
|
605
|
+
- Updated dependencies [f9faa7d]
|
|
606
|
+
- Updated dependencies [33526fd]
|
|
607
|
+
- Updated dependencies [32413ec]
|
|
608
|
+
- @object-ui/i18n@17.4.0
|
|
609
|
+
|
|
3
610
|
## 17.3.0
|
|
4
611
|
|
|
5
612
|
### Minor Changes
|
|
@@ -1544,6 +2151,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
1544
2151
|
objects+fields were editable in Studio; this reworks both surfaces.
|
|
1545
2152
|
|
|
1546
2153
|
**Setup (assign + read-only):**
|
|
2154
|
+
|
|
1547
2155
|
- The six facets (`object_permissions`, `field_permissions`, `system_permissions`,
|
|
1548
2156
|
`row_level_security`, `tab_permissions`, `admin_scope`) now render read-only on
|
|
1549
2157
|
the `sys_permission_set` record page as a compact summary (counts, or capability
|
|
@@ -1558,6 +2166,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
1558
2166
|
|
|
1559
2167
|
**Studio (design every facet):** the permission matrix editor gains structured
|
|
1560
2168
|
editors for the facets that were JSON-only —
|
|
2169
|
+
|
|
1561
2170
|
- **System Capabilities**: a multi-select over the live `sys_capability` registry
|
|
1562
2171
|
(scope-grouped, labelled chips).
|
|
1563
2172
|
- **Row-Level Security**: per-policy rows (object · operation · enabled) with CEL
|
|
@@ -1992,6 +2601,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
1992
2601
|
unknown @-tokens. 9 unit tests.
|
|
1993
2602
|
|
|
1994
2603
|
`@object-ui/app-shell` `RecordDetailView` now:
|
|
2604
|
+
|
|
1995
2605
|
1. Serializes the resolved mention ids into `sys_comment.mentions`
|
|
1996
2606
|
(previously hard-coded `'[]'`, so servers had no idea who was being
|
|
1997
2607
|
pinged).
|
|
@@ -2012,6 +2622,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2012
2622
|
and the renderer also falls back to a value/label heuristic (matches
|
|
2013
2623
|
`closed_lost`, `lost`, `failed`, `cancelled`, `失败`, `流失`, `丢单`, etc.)
|
|
2014
2624
|
so existing CRM-style picklists get the treatment without migration.
|
|
2625
|
+
|
|
2015
2626
|
- **Lost** stages render in a visually separated group with a left
|
|
2016
2627
|
border, destructive (red) tint, pill shape, and `✗` glyph — mirroring
|
|
2017
2628
|
the Salesforce / HubSpot alt-terminus pattern that signals "this
|
|
@@ -2041,6 +2652,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2041
2652
|
- Stage chevron (`record:path`): bump completed-stage contrast (emerald-800 text on emerald-500/15, was 700 on /10) and future-stage text from `foreground/70` to `foreground/85` for legibility.
|
|
2042
2653
|
- i18n: add `notifications.emptyUnread`, `notifications.filterUnread`, `notifications.filterAll` (en + zh) so the InboxPopover Unread/All sub-filter renders in the active locale.
|
|
2043
2654
|
- 5425608: CRM UX polish pass — calmer enterprise look across detail + kanban.
|
|
2655
|
+
|
|
2044
2656
|
- **plugin-kanban**: column headers now use a 2px muted accent stripe with
|
|
2045
2657
|
neutral foreground titles + a quiet grey count pill instead of full
|
|
2046
2658
|
rainbow gradient + colored title + colored count. Pipeline boards
|
|
@@ -2065,12 +2677,14 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2065
2677
|
- 5633edd: feat(detail,grid): tab + selection motion polish
|
|
2066
2678
|
|
|
2067
2679
|
**plugin-detail**
|
|
2680
|
+
|
|
2068
2681
|
- `DetailTabs` and the auto-tabs path in `DetailView` (5 inline
|
|
2069
2682
|
`<TabsContent>` instances: details, related, activity, discussion,
|
|
2070
2683
|
history) now fade in when their tab becomes active, eliminating
|
|
2071
2684
|
the harsh flash when switching tabs.
|
|
2072
2685
|
|
|
2073
2686
|
**plugin-grid**
|
|
2687
|
+
|
|
2074
2688
|
- `BulkActionBar` slides in from the bottom + fades in when a
|
|
2075
2689
|
selection is made, instead of popping into existence.
|
|
2076
2690
|
- The "N items selected" counter re-animates on every count change
|
|
@@ -2099,6 +2713,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2099
2713
|
`defaultCurrency` is set on the field/column, formatting is unchanged.
|
|
2100
2714
|
|
|
2101
2715
|
Fixed call sites:
|
|
2716
|
+
|
|
2102
2717
|
- `@object-ui/fields`: `formatCurrency`, `formatCompactCurrency`, and
|
|
2103
2718
|
`CurrencyCellRenderer` no longer default-param `'USD'`.
|
|
2104
2719
|
- `@object-ui/i18n`: `formatCurrency()` falls back to `formatNumber`
|
|
@@ -2120,6 +2735,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2120
2735
|
- d1ec6a2: Fold inline-edit into the page-header overflow menu (HubSpot/Lightning
|
|
2121
2736
|
pattern) and remove the orphan "Edit fields" toolbar row that previously
|
|
2122
2737
|
floated between the tab strip and the first detail section.
|
|
2738
|
+
|
|
2123
2739
|
- `@object-ui/app-shell` `RecordDetailView`: injects a new `sys_inline_edit`
|
|
2124
2740
|
system action that appears in the ⋯ overflow menu and dispatches a
|
|
2125
2741
|
`objectui:record:inline-edit-toggle` window CustomEvent (filtered by
|
|
@@ -2136,6 +2752,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2136
2752
|
in `page:accordion` / `page:tabs` items.
|
|
2137
2753
|
|
|
2138
2754
|
- cf30cc2: Polish Lightning record detail page layout.
|
|
2755
|
+
|
|
2139
2756
|
- `record:details` sections now render with Card chrome by default when a `title` is present, restoring visual grouping that was missing on pages like the opportunity detail page.
|
|
2140
2757
|
- Section labels can be translated via the `{ns}.objects.{objectName}._sections.{name}.label` convention. Author each section with a stable `name` (e.g. `info`, `forecast`) and the renderer picks up the locale-specific label automatically. Falls back to the literal `label` when no translation exists.
|
|
2141
2758
|
- The `page:header` action toolbar now collapses into a `⋯` overflow menu when more than two actions are present. The first business action stays inline; secondary system actions (Edit / Share / Delete) move into the menu, with destructive styling applied to Delete.
|
|
@@ -2150,6 +2767,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2150
2767
|
`CONCURRENT_UPDATE`). `RecordDetailsRenderer` catches it and opens a
|
|
2151
2768
|
new `<ConcurrentUpdateDialog>` showing the user's pending value next
|
|
2152
2769
|
to the server's current value, with three resolution paths:
|
|
2770
|
+
|
|
2153
2771
|
- **Reload latest** — discard the pending edit and refetch.
|
|
2154
2772
|
- **Overwrite anyway** — retry against the server's freshest version
|
|
2155
2773
|
(still OCC-checked, but acknowledges "I've seen the newer version").
|
|
@@ -2165,6 +2783,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2165
2783
|
backend support in `@objectstack/rest@>=4.2.0`.
|
|
2166
2784
|
|
|
2167
2785
|
- 49b1760: Polish the ConcurrentUpdateDialog and add i18n.
|
|
2786
|
+
|
|
2168
2787
|
- Internationalise all dialog strings (title, body, button labels, "your edit" / "current value" headings, audit-trail line) through `useDetailTranslation`. Locale strings added to `@object-ui/i18n` for English and Chinese.
|
|
2169
2788
|
- Replace the plain dialog header with an amber warning badge + `AlertTriangle` icon to communicate that this is a conflict, not a routine confirmation.
|
|
2170
2789
|
- Visually differentiate the two value blocks: amber tint for the user's pending edit, sky tint for the server's current value. Both wrap long values cleanly.
|
|
@@ -2180,17 +2799,20 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2180
2799
|
- bd8447d: Three platform-wide detail polish items.
|
|
2181
2800
|
|
|
2182
2801
|
**Tighter page rhythm**
|
|
2802
|
+
|
|
2183
2803
|
- Outer `PageRenderer` padding `p-4 md:p-6 lg:p-8` → `p-3 md:p-4 lg:p-6`
|
|
2184
2804
|
and outer body wrap `space-y-8` → `space-y-6` so list / detail / home
|
|
2185
2805
|
pages share the same edge rhythm. Cuts ~16px of edge slack on lg.
|
|
2186
2806
|
|
|
2187
2807
|
**Highlights KPI treatment**
|
|
2808
|
+
|
|
2188
2809
|
- `HeaderHighlight` now renders numeric / currency / percent / decimal
|
|
2189
2810
|
values as KPI numbers (`text-xl md:text-2xl font-semibold tabular-nums`)
|
|
2190
2811
|
instead of the uniform `text-sm font-semibold`, so amount / probability
|
|
2191
2812
|
/ count fields read as headline stats — Salesforce-style key facts.
|
|
2192
2813
|
|
|
2193
2814
|
**Discussion footer upgrade**
|
|
2815
|
+
|
|
2194
2816
|
- `RecordActivityTimeline` now uses `RichTextCommentInput` (bold / italic /
|
|
2195
2817
|
list / code, `@`-mention autocomplete, preview toggle, Send) instead of
|
|
2196
2818
|
a bare `<textarea>`.
|
|
@@ -2205,6 +2827,7 @@ pi-TgoJ4_DM55Fqz` — untranslated, and leaking the object's machine name and th
|
|
|
2205
2827
|
accordion in flush mode dropped all per-item borders so the collapsed
|
|
2206
2828
|
"Quotes / Products / Open Tasks" triggers stacked with zero visual
|
|
2207
2829
|
separation.
|
|
2830
|
+
|
|
2208
2831
|
- `@object-ui/plugin-detail` `DetailSection`: override the `CardTitle`
|
|
2209
2832
|
className to `text-base font-semibold tracking-tight`, slim down
|
|
2210
2833
|
`CardHeader` padding (`py-3 px-4 sm:py-4 sm:px-6`) and `CardContent`
|
|
@@ -2219,6 +2842,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
2219
2842
|
CRM-style related-list lists).
|
|
2220
2843
|
|
|
2221
2844
|
- d51a577: feat(platform): Discussion attachments + @mention directory + Reference Rail aside
|
|
2845
|
+
|
|
2222
2846
|
- **Discussion attachments** — `RichTextCommentInput` now accepts an `extraSlot`
|
|
2223
2847
|
and a `canSubmitEmpty` flag so hosts can mount the existing
|
|
2224
2848
|
`CommentAttachment` composer beneath the editor without forking the toolbar.
|
|
@@ -2242,6 +2866,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
2242
2866
|
custom `Page`. Catalog-style objects (Product, Task) ship with the rail
|
|
2243
2867
|
off by default; hub objects (Account, Opportunity, Contact, Case) keep it
|
|
2244
2868
|
on.
|
|
2869
|
+
|
|
2245
2870
|
- `RecordDetailView` now reads `(objectDef as any)?.detail?.hideReferenceRail`
|
|
2246
2871
|
and `…?.hideRelatedTab` and threads them to `buildDefaultPageSchema`.
|
|
2247
2872
|
- The Reference Rail renderer also accepts entries authored as either a
|
|
@@ -2292,6 +2917,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
2292
2917
|
not only contacts of that account).
|
|
2293
2918
|
|
|
2294
2919
|
Two coupled fixes:
|
|
2920
|
+
|
|
2295
2921
|
1. `RelatedList` now requires `parentId` + `referenceField` to auto-
|
|
2296
2922
|
fetch. When both are present it calls `dataSource.find(api,
|
|
2297
2923
|
{ $filter: { [referenceField]: parentId } })`. When either is
|
|
@@ -2337,6 +2963,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
2337
2963
|
default detail view. They were missing cross-cutting affordances and
|
|
2338
2964
|
shipped with English-only tab labels and heavy bordered section cards
|
|
2339
2965
|
even when the host locale was Chinese. Track 1 closes the visible gap:
|
|
2966
|
+
|
|
2340
2967
|
- **app-shell `RecordDetailView`**: the `assignedPage` branch now wears
|
|
2341
2968
|
the same chrome as the default branch — lifecycle managed-by badge
|
|
2342
2969
|
and presence avatars in the top-right, `MetadataPanel` debug panel,
|
|
@@ -2439,6 +3066,7 @@ hover:no-underline` (Shadcn's hover-underline default looks busy on
|
|
|
2439
3066
|
layout — only the auto-inference default changed.
|
|
2440
3067
|
|
|
2441
3068
|
- b14fe09: Phase P.0 + P.5: tighten record-detail header chrome.
|
|
3069
|
+
|
|
2442
3070
|
- `RecordTitleChip` collapses the title row to a single baseline-aligned line — H1, eyebrow object label, copy-id, favorite star — instead of the previous two-row title + subtitle layout.
|
|
2443
3071
|
- `record:details` extends the highlight-field dedup set to also exclude the title field resolved from `objectSchema.primaryField` (or the standard `name`/`full_name`/`title`/`subject`/`display_name`/`label` fallbacks). Removes the duplicate row that previously echoed the H1 (e.g. "客户名称: Acme Corporation") inside the field grid.
|
|
2444
3072
|
|
|
@@ -2481,6 +3109,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2481
3109
|
result was visually noisy ("有的下划线,有的有边框,有的没边框").
|
|
2482
3110
|
|
|
2483
3111
|
This change commits to a single design language:
|
|
3112
|
+
|
|
2484
3113
|
- **Highlights** (`HeaderHighlight`): drop the `Card`/`CardContent`
|
|
2485
3114
|
wrapper. Render as a borderless `<section>` of stat cells with a
|
|
2486
3115
|
subtle `border-b` separator. The tab strip below now carries the
|
|
@@ -2501,6 +3130,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2501
3130
|
by their tabular content. No internal package APIs changed.
|
|
2502
3131
|
|
|
2503
3132
|
- 74962b0: feat(detail): record:discussion schema component + flush accordion variant
|
|
3133
|
+
|
|
2504
3134
|
- New `record:discussion` schema type lets authors place the record
|
|
2505
3135
|
chatter feed anywhere in a custom Page schema. Wired through a
|
|
2506
3136
|
shared `DiscussionContext` provider on the `assignedPage` branch
|
|
@@ -2514,6 +3144,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2514
3144
|
or `和` (e.g. `Notes & Attachments` → `备注与附件`).
|
|
2515
3145
|
|
|
2516
3146
|
- 8b850b5: feat(detail): record:path chevron stepper + record:highlights surface refresh (Phase E)
|
|
3147
|
+
|
|
2517
3148
|
- `record:path` now renders Salesforce Lightning-style chevron segments
|
|
2518
3149
|
(clip-path arrows + overlap) with a primary glow on the current step
|
|
2519
3150
|
and a check mark on completed steps. On mobile (`<sm`) it falls back
|
|
@@ -2538,6 +3169,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2538
3169
|
accordion, discussion slot) that custom Lightning pages already enjoy.
|
|
2539
3170
|
|
|
2540
3171
|
Changes:
|
|
3172
|
+
|
|
2541
3173
|
- `buildDefaultPageSchema` now emits `page:tabs.items` (correct shape
|
|
2542
3174
|
for the renderer) rather than `tabs`.
|
|
2543
3175
|
- `PageHeaderRenderer.resolvedTitle` honors `objectSchema.primaryField`
|
|
@@ -2561,6 +3193,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2561
3193
|
customize the header or one tab.
|
|
2562
3194
|
|
|
2563
3195
|
**Slot menu (v1):**
|
|
3196
|
+
|
|
2564
3197
|
- `header` — replaces `page:header`
|
|
2565
3198
|
- `actions` — replaces the `record:quick_actions` action bar
|
|
2566
3199
|
- `highlights` — replaces the chips + chevron path strip
|
|
@@ -2588,6 +3221,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2588
3221
|
```
|
|
2589
3222
|
|
|
2590
3223
|
**API changes:**
|
|
3224
|
+
|
|
2591
3225
|
- `PageSchema` (in `@object-ui/types`): adds `kind?: 'full' | 'slotted'`
|
|
2592
3226
|
(default `'full'`) and `slots?: PageSlotMap`.
|
|
2593
3227
|
- `usePageAssignment` (in `@object-ui/react`): result now exposes a
|
|
@@ -2597,6 +3231,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2597
3231
|
`options.slots` map that overrides individual regions at synthesis time.
|
|
2598
3232
|
|
|
2599
3233
|
- 34b66bf: feat(detail): synthesize Related / Activity / History tabs + record:quick_actions header (Track 3 Phase G slice 4)
|
|
3234
|
+
|
|
2600
3235
|
- `buildDefaultPageSchema` now accepts `headerActions`, `related`,
|
|
2601
3236
|
`showActivity`, and `history` options. When provided, the synthesizer
|
|
2602
3237
|
emits a `record:quick_actions` node after `page:header` and appends
|
|
@@ -2621,6 +3256,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2621
3256
|
|
|
2622
3257
|
Two regression fixes surfaced by the Phase J browser canary across CRM
|
|
2623
3258
|
record detail pages:
|
|
3259
|
+
|
|
2624
3260
|
1. **`record:path` now localizes stage labels.** The renderer threads
|
|
2625
3261
|
`useSafeFieldLabel().translateOptions` against the record-context's
|
|
2626
3262
|
`objectName` + the schema's `statusField`, so picklist labels match the
|
|
@@ -2697,6 +3333,7 @@ populated` disclosure that expands on click. The expanded view shows
|
|
|
2697
3333
|
- d714e85: Lookup display-name resolution now falls back through a Salesforce-style chain
|
|
2698
3334
|
when an `$expand`'d reference object lacks a top-level `name`/`label`/
|
|
2699
3335
|
`display_name`/`title` field:
|
|
3336
|
+
|
|
2700
3337
|
1. Standard display fields (existing behaviour)
|
|
2701
3338
|
2. `salutation first_name last_name` composite — handles person records that
|
|
2702
3339
|
only carry first/last name parts
|
|
@@ -2732,6 +3369,7 @@ Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
|
2732
3369
|
### Patch Changes
|
|
2733
3370
|
|
|
2734
3371
|
- 0d8eb98: feat(detail): Salesforce-style record header + section field grid
|
|
3372
|
+
|
|
2735
3373
|
- `page:header` now renders an icon chip (resolves Lucide names via
|
|
2736
3374
|
`LazyIcon`) plus subtitle, so detail pages can show
|
|
2737
3375
|
"Name / Company" without an extra component.
|
|
@@ -2780,6 +3418,7 @@ Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
|
2780
3418
|
detail view (`TypeError: titleFormat.replace is not a function`) and printed
|
|
2781
3419
|
`Failed to evaluate expression: ${[object Object]}` for every action visibility
|
|
2782
3420
|
predicate.
|
|
3421
|
+
|
|
2783
3422
|
- `@object-ui/core`: `ExpressionEvaluator.evaluate` / `evaluateCondition` now
|
|
2784
3423
|
unwrap Expression envelopes transparently.
|
|
2785
3424
|
- `@object-ui/react`: new `toPredicateInput()` helper to safely normalize
|
|
@@ -2831,6 +3470,7 @@ Lin` and no `name` field now renders as `Bob Lin` everywhere — instead of
|
|
|
2831
3470
|
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.
|
|
2832
3471
|
|
|
2833
3472
|
Specifically fixes:
|
|
3473
|
+
|
|
2834
3474
|
- `@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.
|
|
2835
3475
|
- `@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.
|
|
2836
3476
|
- All plugin packages now use a unified `external: (id) => !/^[./]/.test(id) && !id.startsWith(__dirname)` rule, ensuring future additions of CJS deps are automatically externalized.
|