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
20 changes: 20 additions & 0 deletions .changeset/record-alert-cta-label-i18n-4998.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@object-ui/plugin-detail': patch
---

`record:alert`'s renderer-local `RecordAlertProps` CTA slot (`action.label`) is
widened to `string | I18nLabel` in both copies (`properties.*` and the flat
compat mirror) in `packages/plugin-detail/src/renderers/record-alert.tsx`.

The renderer already resolves `action.label` through the same inline-locale-map
`pickLocalized` call as `title` / `body` (`const ctaLabel =
pickLocalized(props.action?.label, language)`), so a bare `string` declaration
was narrower than the renderer's own runtime behavior — the same
declaration-narrower-than-the-renderer contradiction objectui#4970 fixed for
`title` / `body` one level up in the same interface (objectui#4998).

Type-only: the block's published authoring surface still declares `action` as
a bare `object` with the member shape in prose only
(`plugin-detail/src/index.tsx`), so there is no manifest arm to align yet —
that half stays parked on the `ComponentInput` member-shape question (PR
#3795) and is out of scope here.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ import { RecordAlertRenderer } from '../record-alert';
// the ambient predicate scope pinned at the bottom of this file is the shipped
// one, not a stand-in for it.
import { PredicateScopeProvider } from '@object-ui/react';
// Real I18nProvider (not mocked) — pins the active language so the locale-map
// case below (objectui#4998) resolves deterministically instead of riding
// whatever language another test file left on the global i18next singleton.
import { I18nProvider } from '@object-ui/i18n';

const RECORD_DEFAULTS = {
recordCtx: {
Expand Down Expand Up @@ -193,6 +197,35 @@ describe('RecordAlertRenderer', () => {
expect(screen.getByTestId('alert-cta').textContent).toBe('Send again');
});

// objectui#4998 — `action.label` accepts the inline locale map, not just a
// plain string. The renderer already resolved one before this pin existed
// (`const ctaLabel = pickLocalized(props.action?.label, language)`), so a
// plain-string CTA label is green whether or not the local `RecordAlertProps`
// type admits a map — that's the `label: 'Send again'` case just above, and
// it cannot tell the widened declaration apart from the narrow one it
// replaced. This case can: a bare `label?: string` refused a locale-map
// value at the TYPE level even though `pickLocalized` (an `unknown` input)
// already resolved it correctly at runtime, exactly the `title` / `body`
// contradiction objectui#4970 fixed one level up in the same interface.
it('resolves an inline locale-map action.label to the active-language string (objectui#4998)', () => {
render(
<I18nProvider config={{ defaultLanguage: 'en', detectBrowserLanguage: false }}>
<RecordAlertRenderer
schema={{
properties: {
title: 'X',
action: {
actionName: 'resend_verification_email',
label: { en: 'Send again', 'zh-CN': '再次发送' },
},
},
}}
/>
</I18nProvider>,
);
expect(screen.getByTestId('alert-cta').textContent).toBe('Send again');
});

it('does NOT render a CTA when the action name fails to resolve in metadata', () => {
render(
<RecordAlertRenderer
Expand Down
17 changes: 11 additions & 6 deletions packages/plugin-detail/src/renderers/record-alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,15 @@ type Severity = 'info' | 'warning' | 'error' | 'success';
* the renderer and this block's own published surface — the
* declaration-narrower-than-the-renderer family of objectui#4581.
*
* The CTA's `action.label` below is the same slot one level down and is
* deliberately NOT widened here (objectui#4998): its published surface declares
* `action` as a bare `object` with the member shape in prose only, so the arms it
* would be aligned against do not exist yet.
* The CTA's `action.label` below is the same slot one level down, widened here
* (objectui#4998) to match: it is read through the same `pickLocalized` call as
* `title` / `body` (`const ctaLabel = pickLocalized(props.action?.label, language)`
* further down), so a declaration of bare `string` was narrower than the
* renderer's own runtime behaviour, exactly as `title` / `body` were before
* objectui#4970. This widening is TYPE-only: the block's published surface still
* declares `action` as a bare `object` with the member shape in prose
* (`plugin-detail/src/index.tsx`), so there is no manifest arm to align — that
* half stays parked on the `ComponentInput` member-shape question (PR #3795).
*/
interface RecordAlertProps {
schema?: {
Expand All @@ -90,7 +95,7 @@ interface RecordAlertProps {
body?: string | I18nLabel;
visible?: any;
icon?: string;
action?: { actionName: string; label?: string; variant?: string };
action?: { actionName: string; label?: string | I18nLabel; variant?: string };
dismissible?: boolean;
dismissKey?: string;
};
Expand All @@ -100,7 +105,7 @@ interface RecordAlertProps {
body?: string | I18nLabel;
visible?: any;
icon?: string;
action?: { actionName: string; label?: string; variant?: string };
action?: { actionName: string; label?: string | I18nLabel; variant?: string };
dismissible?: boolean;
dismissKey?: string;
className?: string;
Expand Down
Loading