Skip to content

Commit 2baa13f

Browse files
os-zhuangclaude
andauthored
fix(record): register the record:* blocks under one key, prefixed once (#3023)
Eleven blocks in plugin-detail were registered as register('record:x', …, { namespace: 'record' }) — an already-prefixed name handed to a registry that prefixes it again. Each landed at record:record:x, and the key authors actually resolved, record:x, was the un-namespaced FALLBACK rather than the intended registration. The registry carried 23 keys for 12 components. Nothing failed, which is why it survived: getPublicConfigs() rewrites `type` to the curated tag, so the doubled name never reached the contract, the manifest, or the JSX surface. It was visible only when enumerating the registry directly — which is what #3013's reverse check does. Registering the bare name is what makes `namespace` correct, and skipFallback: true is what keeps the fallback from claiming that bare name globally. Without it these would take over `details`, `path`, `history`, `alert` … as top-level tags; `alert` is the live case, owned by ui:. Every block stays reachable exactly as record:<name>, and 23 keys become 12. record:line_items needed no change — it was the one already registered this way, which is what made #3006's near-miss possible in the first place. Two console assertions hold the shape: no key carries a doubled prefix, and no record:* block owns the bare spelling of its own name. Both were verified against the old form: restoring one registration fails them with the phantom key named. Claude-Session: https://claude.ai/code/session_01N4mrr1ihhwnfEHFSWmGoMp Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8eee174 commit 2baa13f

3 files changed

Lines changed: 90 additions & 33 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@object-ui/plugin-detail": patch
3+
"@object-ui/console": patch
4+
---
5+
6+
fix(record): register the record:\* blocks under one key, prefixed once
7+
8+
Eleven blocks in plugin-detail were registered as
9+
`register('record:x', …, { namespace: 'record' })` — an already-prefixed name
10+
handed to a registry that prefixes it again. Each landed at
11+
`record:record:x`, and the key authors actually resolved, `record:x`, was the
12+
un-namespaced *fallback* rather than the intended registration. The registry
13+
carried 23 keys for 12 components.
14+
15+
Nothing failed, which is why it survived: `getPublicConfigs()` rewrites `type`
16+
to the curated tag, so the doubled name never reached the contract, the
17+
manifest, or the JSX surface. It was visible only when enumerating the registry
18+
directly — which is what objectui#3013's reverse check does.
19+
20+
Registering the bare name is what makes `namespace` correct, and
21+
`skipFallback: true` is what keeps the fallback from claiming that bare name
22+
globally. Without it these would take over `details`, `path`, `history`,
23+
`alert` … as top-level tags; `alert` is the live case, owned by `ui:`. Every
24+
block stays reachable exactly as `record:<name>`, and 23 keys become 12.
25+
26+
`record:line_items` needed no change — it was the one already registered this
27+
way, which is what made objectui#3006's near-miss possible in the first place.
28+
29+
Two console assertions hold the shape: no key carries a doubled prefix, and no
30+
`record:*` block owns the bare spelling of its own name.

apps/console/src/__tests__/public-contract.test.ts

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -185,28 +185,10 @@ const DELIBERATELY_UNCURATED: Record<string, string> = {
185185
describe('PUBLIC_BLOCKS ↔ console coverage (reverse direction)', () => {
186186
const NS = 'record';
187187

188-
/**
189-
* Eleven of these blocks are registered as
190-
* `register('record:x', …, { namespace: 'record' })` — an already-prefixed
191-
* name that gets prefixed again — so the registry holds both `record:x` and a
192-
* doubled `record:record:x` for the same component. The doubling never
193-
* reaches the contract (`getPublicConfigs()` rewrites `type` to the curated
194-
* tag), but it is real in the registry, and counting raw keys would report
195-
* those components twice. Collapsing the repeated prefix is what makes the
196-
* set below one entry per block.
197-
*/
198-
const undouble = (key: string): string => {
199-
let k = key;
200-
while (k.startsWith(`${NS}:${NS}:`)) k = k.slice(NS.length + 1);
201-
return k;
202-
};
203-
204-
const shippedRecordBlocks = (): string[] => {
205-
const keys = ComponentRegistry.getKnownTypes().filter(
206-
(k) => ComponentRegistry.getMeta(k)?.namespace === NS,
207-
);
208-
return [...new Set(keys.map(undouble))].sort();
209-
};
188+
const shippedRecordBlocks = (): string[] =>
189+
ComponentRegistry.getKnownTypes()
190+
.filter((k) => ComponentRegistry.getMeta(k)?.namespace === NS)
191+
.sort();
210192

211193
it('curates every shipped record:* block, or records why not', () => {
212194
const curated = new Set<string>(PUBLIC_BLOCKS);
@@ -215,6 +197,29 @@ describe('PUBLIC_BLOCKS ↔ console coverage (reverse direction)', () => {
215197
expect(uncurated).toEqual(Object.keys(DELIBERATELY_UNCURATED).sort());
216198
});
217199

200+
it('registers each record:* block under one key, prefixed once', () => {
201+
// `register('record:x', …, { namespace: 'record' })` prefixes an
202+
// already-prefixed name: the block lands at `record:record:x` and stays
203+
// reachable only through the un-namespaced fallback, which happens to spell
204+
// `record:x`. Eleven blocks were registered that way. The contract hid it —
205+
// `getPublicConfigs()` rewrites `type` to the curated tag — so nothing
206+
// failed while the registry carried a phantom key per block.
207+
const keys = shippedRecordBlocks();
208+
expect(keys.filter((k) => k.startsWith(`${NS}:${NS}:`))).toEqual([]);
209+
expect(keys.every((k) => k.startsWith(`${NS}:`))).toBe(true);
210+
});
211+
212+
it('leaves the bare names to whoever owns them', () => {
213+
// The corollary of registering bare + `namespace`: without
214+
// `skipFallback: true` these also claim `details`, `path`, `alert` … as
215+
// top-level tags. `alert` is the live example — it belongs to `ui:`, and a
216+
// missing skipFallback here would silently take it over.
217+
for (const tag of shippedRecordBlocks()) {
218+
const bare = tag.slice(NS.length + 1);
219+
expect(ComponentRegistry.getMeta(bare)?.namespace ?? null).not.toBe(NS);
220+
}
221+
});
222+
218223
it('keeps the deliberately-uncurated blocks unconfigurable', () => {
219224
// The stated reason for every exclusion is "nothing to author". If one of
220225
// these grows `inputs`, it became authorable and the exclusion needs

packages/plugin-detail/src/index.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -221,10 +221,22 @@ ComponentRegistry.register('detail', DetailView, {
221221
// These renderers consume RecordContext (provided by app-shell's
222222
// RecordDetailView) and adapt the spec's `RecordXxxComponentProps` onto the
223223
// legacy plugin-detail components above.
224+
//
225+
// Pass the BARE name and let `namespace` do the prefixing — `register('details',
226+
// …, { namespace: 'record' })`, not `register('record:details', …)`. The
227+
// registry prepends the namespace itself, so a pre-prefixed name is prefixed
228+
// again and lands under `record:record:details`; the key that actually worked
229+
// was the un-namespaced fallback, which happened to spell `record:details`.
230+
//
231+
// `skipFallback: true` is what keeps that fallback from claiming the bare name
232+
// globally: without it these would register `details`, `path`, `history`,
233+
// `alert` … as top-level tags, colliding with the `ui:` primitives that own
234+
// them. Every block here is reachable as `record:<name>` and only that.
224235
// ---------------------------------------------------------------------------
225236

226-
ComponentRegistry.register('record:details', RecordDetailsRenderer, {
237+
ComponentRegistry.register('details', RecordDetailsRenderer, {
227238
namespace: 'record',
239+
skipFallback: true,
228240
category: 'record',
229241
label: 'Record Details',
230242
icon: 'FileText',
@@ -237,8 +249,9 @@ ComponentRegistry.register('record:details', RecordDetailsRenderer, {
237249
],
238250
});
239251

240-
ComponentRegistry.register('record:related_list', RecordRelatedListRenderer, {
252+
ComponentRegistry.register('related_list', RecordRelatedListRenderer, {
241253
namespace: 'record',
254+
skipFallback: true,
242255
category: 'record',
243256
label: 'Related List',
244257
icon: 'List',
@@ -256,8 +269,9 @@ ComponentRegistry.register('record:related_list', RecordRelatedListRenderer, {
256269
],
257270
});
258271

259-
ComponentRegistry.register('record:highlights', RecordHighlightsRenderer, {
272+
ComponentRegistry.register('highlights', RecordHighlightsRenderer, {
260273
namespace: 'record',
274+
skipFallback: true,
261275
category: 'record',
262276
label: 'Highlights Panel',
263277
icon: 'Star',
@@ -268,15 +282,17 @@ ComponentRegistry.register('record:highlights', RecordHighlightsRenderer, {
268282
],
269283
});
270284

271-
ComponentRegistry.register('record:activity', RecordActivityRenderer, {
285+
ComponentRegistry.register('activity', RecordActivityRenderer, {
272286
namespace: 'record',
287+
skipFallback: true,
273288
category: 'record',
274289
label: 'Activity Timeline',
275290
icon: 'Activity',
276291
});
277292

278-
ComponentRegistry.register('record:chatter', RecordChatterRenderer, {
293+
ComponentRegistry.register('chatter', RecordChatterRenderer, {
279294
namespace: 'record',
295+
skipFallback: true,
280296
category: 'record',
281297
label: 'Chatter Feed',
282298
icon: 'MessageSquare',
@@ -287,15 +303,17 @@ ComponentRegistry.register('record:chatter', RecordChatterRenderer, {
287303
// share the same DiscussionContext wiring; we keep `record:chatter`
288304
// for Salesforce-familiar authors and for backward compatibility with
289305
// schemas already in the wild.
290-
ComponentRegistry.register('record:discussion', RecordChatterRenderer, {
306+
ComponentRegistry.register('discussion', RecordChatterRenderer, {
291307
namespace: 'record',
308+
skipFallback: true,
292309
category: 'record',
293310
label: 'Discussion',
294311
icon: 'MessageSquare',
295312
});
296313

297-
ComponentRegistry.register('record:path', RecordPathRenderer, {
314+
ComponentRegistry.register('path', RecordPathRenderer, {
298315
namespace: 'record',
316+
skipFallback: true,
299317
category: 'record',
300318
label: 'Path / Stepper',
301319
icon: 'GitBranch',
@@ -306,29 +324,33 @@ ComponentRegistry.register('record:path', RecordPathRenderer, {
306324
],
307325
});
308326

309-
ComponentRegistry.register('record:quick_actions', RecordQuickActionsRenderer, {
327+
ComponentRegistry.register('quick_actions', RecordQuickActionsRenderer, {
310328
namespace: 'record',
329+
skipFallback: true,
311330
category: 'record',
312331
label: 'Quick Actions',
313332
icon: 'Zap',
314333
});
315334

316-
ComponentRegistry.register('record:history', RecordHistoryRenderer, {
335+
ComponentRegistry.register('history', RecordHistoryRenderer, {
317336
namespace: 'record',
337+
skipFallback: true,
318338
category: 'record',
319339
label: 'History Timeline',
320340
icon: 'Clock',
321341
});
322342

323-
ComponentRegistry.register('record:reference_rail', RecordReferenceRailRenderer, {
343+
ComponentRegistry.register('reference_rail', RecordReferenceRailRenderer, {
324344
namespace: 'record',
345+
skipFallback: true,
325346
category: 'record',
326347
label: 'Reference Rail',
327348
icon: 'PanelRight',
328349
});
329350

330-
ComponentRegistry.register('record:alert', RecordAlertRenderer, {
351+
ComponentRegistry.register('alert', RecordAlertRenderer, {
331352
namespace: 'record',
353+
skipFallback: true,
332354
category: 'record',
333355
label: 'Alert Banner',
334356
icon: 'AlertTriangle',

0 commit comments

Comments
 (0)