Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .changeset/namespace-double-prefix-sweep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
"@object-ui/components": patch
"@object-ui/console": patch
---

fix(registry): prefix every namespaced key exactly once, in every namespace

objectui#3023 fixed eleven `record:*` blocks registered as
`register('record:x', …, { namespace: 'record' })` — an already-prefixed name
handed to a registry that prefixes it again, landing the block at
`record:record:x` — and guarded that namespace alone. Twenty-two more were
sitting in `action:` (5), `element:` (10) and `page:` (7), two of them
(`page:header`, `element:divider`) curated public blocks.

Checking one namespace is exactly what let them keep sitting there, so the
guard now asks the whole registry rather than a prefix of it.

Same fix as before: register the bare name and let `namespace` do the
prefixing, with `skipFallback: true` so the fallback does not claim that bare
name globally. It would otherwise take over `header`, `footer`, `sidebar`,
`tabs`, `card`, `accordion`, `section`, `text`, `image`, `button`, `icon` —
every one of which belongs to `ui:`. All 22 stay reachable exactly as
`<namespace>:<name>`; the registry goes 522 keys to 500, and the contract is
unchanged at 42/42.

Found while probing why six curated Tier B primitives report no `inputs`. They
do declare them — `vitest.setup.dom.tsx` registers simplified `text` / `image` /
`html` / `grid` stubs that shadow the real registrations inside the test
environment only. That shadowing is a separate question, left alone here; the
doubled keys it turned up are not test-environment artifacts.
28 changes: 21 additions & 7 deletions apps/console/src/__tests__/public-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,7 @@ describe('PUBLIC_BLOCKS ↔ console coverage (reverse direction)', () => {
});

it('registers each record:* block under one key, prefixed once', () => {
// `register('record:x', …, { namespace: 'record' })` prefixes an
// already-prefixed name: the block lands at `record:record:x` and stays
// reachable only through the un-namespaced fallback, which happens to spell
// `record:x`. Eleven blocks were registered that way. The contract hid it —
// `getPublicConfigs()` rewrites `type` to the curated tag — so nothing
// failed while the registry carried a phantom key per block.
const keys = shippedRecordBlocks();
expect(keys.filter((k) => k.startsWith(`${NS}:${NS}:`))).toEqual([]);
expect(keys.every((k) => k.startsWith(`${NS}:`))).toBe(true);
});

Expand All @@ -224,6 +217,27 @@ describe('PUBLIC_BLOCKS ↔ console coverage (reverse direction)', () => {
}
});

it('prefixes every namespaced key exactly once, in every namespace', () => {
// `register('page:header', …, { namespace: 'page' })` hands an
// already-prefixed name to a registry that prefixes it again: the block
// lands at `page:page:header` and stays reachable only through the
// un-namespaced fallback, which happens to spell `page:header`. Nothing
// fails — `getPublicConfigs()` rewrites `type` to the curated tag — so the
// registry quietly carries a phantom key per block.
//
// objectui#3023 fixed the eleven in `record:` and guarded that namespace
// alone. Twenty-two more were sitting in `action:`, `element:` and `page:`,
// two of them (`page:header`, `element:divider`) curated public blocks.
// Checking one namespace is what let them keep sitting there, so this asks
// the whole registry.
const doubled = ComponentRegistry.getKnownTypes().filter((k) => {
const ns = ComponentRegistry.getMeta(k)?.namespace;
return !!ns && k.startsWith(`${ns}:${ns}:`);
});

expect(doubled).toEqual([]);
});

it('keeps the chatter alias identical to the block it aliases', () => {
// `record:chatter` is excluded because it duplicates `record:discussion`,
// not because it is lesser. The moment the two configuration surfaces
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/renderers/action/action-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,9 @@ const ActionBarRenderer = forwardRef<HTMLDivElement, { schema: ActionBarSchema;

ActionBarRenderer.displayName = 'ActionBarRenderer';

ComponentRegistry.register('action:bar', ActionBarRenderer, {
ComponentRegistry.register('bar', ActionBarRenderer, {
namespace: 'action',
skipFallback: true,
label: 'Action Bar',
inputs: [
{ name: 'actions', type: 'object', label: 'Actions' },
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/renderers/action/action-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ const ActionButtonRenderer = forwardRef<HTMLButtonElement, ActionButtonProps>(

ActionButtonRenderer.displayName = 'ActionButtonRenderer';

ComponentRegistry.register('action:button', ActionButtonRenderer, {
ComponentRegistry.register('button', ActionButtonRenderer, {
namespace: 'action',
skipFallback: true,
label: 'Action Button',
inputs: [
{ name: 'name', type: 'string', label: 'Action Name' },
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/renderers/action/action-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,9 @@ const ActionGroupRenderer = forwardRef<HTMLDivElement, { schema: ActionGroupSche

ActionGroupRenderer.displayName = 'ActionGroupRenderer';

ComponentRegistry.register('action:group', ActionGroupRenderer, {
ComponentRegistry.register('group', ActionGroupRenderer, {
namespace: 'action',
skipFallback: true,
label: 'Action Group',
inputs: [
{ name: 'name', type: 'string', label: 'Group Name' },
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/renderers/action/action-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@ const ActionIconRenderer = forwardRef<HTMLButtonElement, ActionIconProps>(

ActionIconRenderer.displayName = 'ActionIconRenderer';

ComponentRegistry.register('action:icon', ActionIconRenderer, {
ComponentRegistry.register('icon', ActionIconRenderer, {
namespace: 'action',
skipFallback: true,
label: 'Action Icon',
inputs: [
{ name: 'name', type: 'string', label: 'Action Name' },
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/renderers/action/action-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,9 @@ const ActionMenuRenderer = forwardRef<HTMLButtonElement, { schema: ActionMenuSch

ActionMenuRenderer.displayName = 'ActionMenuRenderer';

ComponentRegistry.register('action:menu', ActionMenuRenderer, {
ComponentRegistry.register('menu', ActionMenuRenderer, {
namespace: 'action',
skipFallback: true,
label: 'Action Menu',
inputs: [
{ name: 'label', type: 'string', label: 'Trigger Label' },
Expand Down
6 changes: 4 additions & 2 deletions packages/components/src/renderers/basic/data-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ function DefinitionListRenderer({ schema }: { schema: any }) {
);
}

ComponentRegistry.register('element:definition-list', DefinitionListRenderer, {
ComponentRegistry.register('definition-list', DefinitionListRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Definition List',
category: 'content',
});
Expand Down Expand Up @@ -172,8 +173,9 @@ function RepeaterRenderer({ schema }: { schema: any }) {
);
}

ComponentRegistry.register('element:repeater', RepeaterRenderer, {
ComponentRegistry.register('repeater', RepeaterRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Repeater',
category: 'content',
});
15 changes: 10 additions & 5 deletions packages/components/src/renderers/basic/elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ function ElementTextRenderer({ schema }: { schema: any }) {
);
}

ComponentRegistry.register('element:text', ElementTextRenderer, {
ComponentRegistry.register('text', ElementTextRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Text',
category: 'content',
});
Expand All @@ -106,8 +107,9 @@ function ElementDividerRenderer({ schema }: { schema: any }) {
return <Separator className={cn('my-4', schema?.className)} />;
}

ComponentRegistry.register('element:divider', ElementDividerRenderer, {
ComponentRegistry.register('divider', ElementDividerRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Divider',
category: 'content',
});
Expand Down Expand Up @@ -155,8 +157,9 @@ function ElementImageRenderer({ schema }: { schema: any }) {
);
}

ComponentRegistry.register('element:image', ElementImageRenderer, {
ComponentRegistry.register('image', ElementImageRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Image',
category: 'content',
});
Expand Down Expand Up @@ -260,8 +263,9 @@ function ElementButtonRenderer({ schema }: { schema: any }) {
);
}

ComponentRegistry.register('element:button', ElementButtonRenderer, {
ComponentRegistry.register('button', ElementButtonRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Button',
category: 'action',
});
Expand Down Expand Up @@ -381,8 +385,9 @@ function ElementNumberRenderer({ schema }: { schema: any }) {
);
}

ComponentRegistry.register('element:number', ElementNumberRenderer, {
ComponentRegistry.register('number', ElementNumberRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Number',
category: 'content',
});
3 changes: 2 additions & 1 deletion packages/components/src/renderers/basic/metadata-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,9 @@ export function ElementMetadataViewerRenderer({ schema }: { schema: any }) {
}
}

ComponentRegistry.register('element:metadata_viewer', ElementMetadataViewerRenderer, {
ComponentRegistry.register('metadata_viewer', ElementMetadataViewerRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Metadata Viewer',
category: 'content',
});
3 changes: 2 additions & 1 deletion packages/components/src/renderers/basic/record-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,9 @@ function ElementRecordPickerRenderer({ schema }: { schema: any }) {
);
}

ComponentRegistry.register('element:record_picker', ElementRecordPickerRenderer, {
ComponentRegistry.register('record_picker', ElementRecordPickerRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Record Picker',
category: 'input',
inputs: [
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/renderers/basic/text-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,9 @@ function ElementTextInputRenderer({ schema }: { schema: any }) {
);
}

ComponentRegistry.register('element:text_input', ElementTextInputRenderer, {
ComponentRegistry.register('text_input', ElementTextInputRenderer, {
namespace: 'element',
skipFallback: true,
label: 'Text Input',
category: 'input',
inputs: [
Expand Down
21 changes: 14 additions & 7 deletions packages/components/src/renderers/layout/containers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -553,8 +553,9 @@ const PageTabsRenderer: React.FC<any> = ({ schema, className, ...props }) => {
);
};

ComponentRegistry.register('page:tabs', PageTabsRenderer, {
ComponentRegistry.register('tabs', PageTabsRenderer, {
namespace: 'page',
skipFallback: true,
label: 'Page Tabs',
category: 'layout',
isContainer: true,
Expand Down Expand Up @@ -595,8 +596,9 @@ const PageCardRenderer: React.FC<any> = ({ schema, className, ...props }) => {
);
};

ComponentRegistry.register('page:card', PageCardRenderer, {
ComponentRegistry.register('card', PageCardRenderer, {
namespace: 'page',
skipFallback: true,
label: 'Page Card',
category: 'layout',
isContainer: true,
Expand Down Expand Up @@ -675,8 +677,9 @@ const PageAccordionRenderer: React.FC<any> = ({ schema, className, ...props }) =
);
};

ComponentRegistry.register('page:accordion', PageAccordionRenderer, {
ComponentRegistry.register('accordion', PageAccordionRenderer, {
namespace: 'page',
skipFallback: true,
label: 'Page Accordion',
category: 'layout',
isContainer: true,
Expand All @@ -698,8 +701,9 @@ const PageSectionRenderer: React.FC<any> = ({ schema, className, ...props }) =>
);
};

ComponentRegistry.register('page:section', PageSectionRenderer, {
ComponentRegistry.register('section', PageSectionRenderer, {
namespace: 'page',
skipFallback: true,
label: 'Page Section',
category: 'layout',
isContainer: true,
Expand Down Expand Up @@ -1297,8 +1301,9 @@ const PageHeaderRenderer: React.FC<any> = ({ schema, className, ...props }) => {
);
};

ComponentRegistry.register('page:header', PageHeaderRenderer, {
ComponentRegistry.register('header', PageHeaderRenderer, {
namespace: 'page',
skipFallback: true,
label: 'Page Header',
category: 'layout',
});
Expand All @@ -1322,8 +1327,9 @@ const PageFooterRenderer: React.FC<any> = ({ schema, className, ...props }) => {
);
};

ComponentRegistry.register('page:footer', PageFooterRenderer, {
ComponentRegistry.register('footer', PageFooterRenderer, {
namespace: 'page',
skipFallback: true,
label: 'Page Footer',
category: 'layout',
isContainer: true,
Expand All @@ -1345,8 +1351,9 @@ const PageSidebarRenderer: React.FC<any> = ({ schema, className, ...props }) =>
);
};

ComponentRegistry.register('page:sidebar', PageSidebarRenderer, {
ComponentRegistry.register('sidebar', PageSidebarRenderer, {
namespace: 'page',
skipFallback: true,
label: 'Page Sidebar',
category: 'layout',
isContainer: true,
Expand Down
Loading