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
11 changes: 11 additions & 0 deletions .changeset/palette-canonical-discussion-5495.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@object-ui/app-shell': minor
---

**The page-editor palette now offers the canonical `record:discussion` instead of the `record:chatter` alias.**

The two spellings are one renderer under two names — `@object-ui/plugin-detail` registers both against `RecordChatterRenderer` — and the palette deliberately carries one entry per renderer. It carried the wrong one. `record:discussion` is the canonical spelling: `@object-ui/core`'s `public-blocks.ts` records `record:chatter` as that same block "under a Salesforce-familiar name, kept for schemas", app-shell's own page synthesiser (`buildDefaultPageSchema`) has emitted the canonical name all along, and the console's AI block vocabulary already leaves the alias uncurated for exactly this reason. The palette was the last surface still pointing at the alias, and it did so only because the `@objectstack/spec` build pinned when the entry was written had no canonical member to offer; the currently pinned build declares both. The author-facing label moves **"Chatter feed" → "Discussion"**. Maintainer ruling 2026-08-22 on objectui#5495.

**The alias keeps rendering — this changes what Studio advertises, not what works.** `record:chatter` moves from `BLOCK_TYPE_META` into `PALETTE_EXCLUSIONS`, which is the palette's ledger of "renders, but is not offered". Every page schema in the wild that spells the block the Salesforce way renders exactly as before, and both halves are now pinned by a test (`palette-discussion-alias.test.tsx`) that asserts the registry resolves the alias to the *same renderer function* as the canonical name and that an alias schema still produces the discussion panel — so "no longer offered" cannot quietly drift into "no longer rendered".

One knock-on worth knowing when you open an existing page: the block-editor canvas draws a node's icon and colour tone from the palette entry, so an existing `record:chatter` node now shows the generic unknown-block icon in the outline instead of the message icon. This is the pre-existing behaviour for any renderable-but-unoffered type — until this change it was the *canonical* `record:discussion` that read this way, including in every page the platform generates itself, so the affected spelling is now the rarer one.
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* objectui#5495 — the page palette advertises the CANONICAL `record:discussion`,
* and the legacy `record:chatter` stays RENDERABLE while no longer advertised.
*
* ## What this pins, and why it is a separate file
*
* `block-config.test.ts` derives palette coverage from `PageComponentType`, so
* it already fails when a spec block type has no decision at all. What it
* cannot see is WHICH of two interchangeable spellings the decision picked:
* offering `record:chatter` and excluding `record:discussion` satisfies every
* derived assertion in that file exactly as well as the reverse. The direction
* was a maintainer ruling (2026-08-22, Option A — the palette offers the
* canonical name), and a ruling that nothing asserts is a comment.
*
* ## The second half is the one that matters
*
* "Excluded from the palette" and "removed" are one careless edit apart, and
* the careless edit is invisible: `record:chatter` no longer appears in
* `BLOCK_TYPE_META`, so a later reader who deletes its registration in
* `plugin-detail/src/index.tsx` breaks every page schema in the wild that spells
* the block the Salesforce way — and nothing in the palette suite objects,
* because the palette is exactly where the type is supposed to be absent now.
*
* So the renderability half is asserted here, next to the exclusion that could
* be mistaken for a retirement:
*
* - the registry still resolves the alias, to the SAME renderer function the
* canonical name resolves to (identity, not "both are defined" — two
* independent renderers that happened to diverge would pass the latter);
* - and the alias schema still renders the discussion panel, asserted on the
* DOM rather than on the registration, because a registration pointing at a
* renderer that throws is still a registration.
*
* Each negative pin carries its positive half, for the reason `block-config.
* test.ts` states throughout: `expect(BLOCK_TYPE_META['record:chatter'])
* .toBeUndefined()` passes just as happily over a palette that lost every entry.
*/

import * as React from 'react';
import { describe, it, expect, afterEach } from 'vitest';
import { render, screen, cleanup } from '@testing-library/react';
import { ComponentRegistry } from '@object-ui/core';
import { PageComponentType } from '@objectstack/spec/ui';
// Side-effect import: registers `record:chatter` and `record:discussion`. The
// app-shell test setup does not pull plugin-detail in, and relying on another
// file having imported it first would make this suite order-dependent.
import '@object-ui/plugin-detail';
import { BLOCK_TYPE_META, PALETTE_EXCLUSIONS } from '../block-types';

afterEach(cleanup);

const specNames: string[] = (() => {
const raw = (PageComponentType as unknown as { options?: readonly string[] }).options;
return Array.isArray(raw) ? [...raw] : [];
})();

const meta = BLOCK_TYPE_META as Record<string, { label: string; category: string } | undefined>;

describe('page palette — the discussion/chatter pair points at the canonical name (#5495)', () => {
it('offers `record:discussion`, labelled for authors as "Discussion"', () => {
expect(meta['record:discussion']).toBeDefined();
expect(meta['record:discussion']!.label).toBe('Discussion');
// Same drawer as the rest of the record blocks — a canonical rename that
// moved the entry to another category would change what an author has to
// scroll past to find it.
expect(meta['record:discussion']!.category).toBe('record');
expect(PALETTE_EXCLUSIONS['record:discussion']).toBeUndefined();
});

it('does NOT offer the `record:chatter` alias, and records the reason', () => {
// POSITIVE half first: a palette that lost every entry would satisfy the
// two negatives below without offering anything at all.
expect(Object.keys(BLOCK_TYPE_META).length, 'palette is empty — the pins would be vacuous')
.toBeGreaterThan(10);
expect(meta['record:chatter']).toBeUndefined();
expect(PALETTE_EXCLUSIONS['record:chatter']).toBeTruthy();
});

it('both spellings are real members of the pinned spec enum', () => {
// `block-config.test.ts` checks that every EXCLUDED name is a real spec
// type; the offered side has no such check, and the flip is only legitimate
// because the pinned spec declares the canonical name (it did not, at the
// pin the palette was written against — objectui#5328).
expect(specNames).toContain('record:discussion');
expect(specNames).toContain('record:chatter');
// Counter-probes, so a mis-read enum cannot report membership for
// everything: one known-present neighbour, one genuinely retired member,
// one that never existed.
expect(specNames).toContain('record:details');
expect(specNames).not.toContain('element:filter');
expect(specNames).not.toContain('zzNotARealPageComponentType');
});
});

describe('the `record:chatter` alias stays RENDERABLE — not offered is not removed (#5495)', () => {
const config = (type: string) =>
ComponentRegistry.getConfig(type) as { component?: React.ComponentType<any> } | undefined;

it('the registry resolves the alias to the very same renderer as the canonical name', () => {
const alias = config('record:chatter');
const canonical = config('record:discussion');
expect(alias, '`record:chatter` no longer resolves — existing schemas would break').toBeDefined();
expect(canonical).toBeDefined();
// Identity, not merely "both defined": two renderers that drifted apart
// would pass a defined/defined check while authors of the two spellings
// silently got different blocks.
expect(alias!.component).toBe(canonical!.component);
// Non-vacuity for the lookup itself — a `getConfig` that returned a truthy
// object for anything would make the assertions above meaningless.
expect(config('record:zzNotARegisteredType')).toBeUndefined();
});

it('a `record:chatter` schema still renders the discussion panel', () => {
const Alias = config('record:chatter')!.component!;
const view = render(<Alias schema={{ type: 'record:chatter', position: 'bottom' }} />);
// Standalone (no DiscussionContext host) the panel renders an empty feed —
// that copy is the proof the renderer ran, not just that it was resolved.
expect(screen.getByText('No comments yet')).toBeTruthy();
const aliasHtml = view.container.innerHTML;
expect(aliasHtml.length, 'alias rendered nothing').toBeGreaterThan(0);
cleanup();

const Canonical = config('record:discussion')!.component!;
const canonicalView = render(
<Canonical schema={{ type: 'record:discussion', position: 'bottom' }} />,
);
// Same markup from the same props: the pair is one block under two names,
// and this is the assertion that fails the day that stops being true.
expect(canonicalView.container.innerHTML).toBe(aliasHtml);
});
});
37 changes: 23 additions & 14 deletions packages/app-shell/src/views/metadata-admin/previews/block-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export type BlockTypeId =
| 'page:accordion' | 'page:card' | 'page:section'
// record:*
| 'record:details' | 'record:highlights' | 'record:related_list'
| 'record:activity' | 'record:chatter' | 'record:path' | 'record:alert'
| 'record:activity' | 'record:discussion' | 'record:path' | 'record:alert'
| 'record:quick_actions' | 'record:reference_rail' | 'record:history'
// nav:* — page-content navigation (shell singletons like app:launcher /
// global:notifications / user:profile are intentionally NOT page blocks)
Expand Down Expand Up @@ -100,7 +100,7 @@ export const BLOCK_TYPE_META: Record<BlockTypeId, Omit<BlockTypeMeta, 'id'>> = {
'record:highlights': { label: 'Highlights', category: 'record', Icon: Tag },
'record:related_list': { label: 'Related list', category: 'record', Icon: ListChecks },
'record:activity': { label: 'Activity timeline', category: 'record', Icon: Activity },
'record:chatter': { label: 'Chatter feed', category: 'record', Icon: MessageSquare },
'record:discussion': { label: 'Discussion', category: 'record', Icon: MessageSquare },
'record:path': { label: 'Stage path', category: 'record', Icon: Compass },
'record:alert': { label: 'Alert banner', category: 'record', Icon: AlertTriangle },
'record:quick_actions': { label: 'Quick actions', category: 'record', Icon: Zap },
Expand Down Expand Up @@ -158,19 +158,28 @@ export const PALETTE_EXCLUSIONS: Record<string, string> = {
'element:text_input': 'no renderer — bare inputs belong to a form, not a page block',
// Renders fine — excluded to keep ONE palette entry per renderer, not because
// it is unauthorable. `record:chatter` and `record:discussion` are the same
// renderer under two names (plugin-detail/src/index.tsx:589), and the palette
// already offers it above as 'Chatter feed'. Entered when `@objectstack/spec`
// 17.1.0 added `record:discussion` to `PageComponentType` (objectui#5328);
// objectui itself has emitted the type all along
// (synth/buildDefaultPageSchema.ts:731), so nothing new became authorable —
// only the spec's enum caught up.
// renderer under two names: `plugin-detail/src/index.tsx` registers both
// against `RecordChatterRenderer`, and the palette offers the CANONICAL one
// above as 'Discussion'.
//
// ⚠️ The pair is offered under the LEGACY name: `public-blocks.ts` records
// `record:chatter` as "`record:discussion` under a Salesforce-familiar name",
// i.e. this exclusion hides the canonical spelling and offers the alias.
// Flipping which one the palette offers changes what authors drag, so it is a
// decision rather than a pin-bump edit — filed as objectui#5495.
'record:discussion': 'same renderer as the offered `record:chatter` — one palette entry per renderer',
// ⛔ Excluded from the palette is NOT removed. `record:chatter` stays fully
// renderable for schemas already in the wild — this entry changes what Studio
// ADVERTISES, not what works. Deleting the registration is a different (and
// breaking) change; `palette-discussion-alias.test.tsx` pins both halves so
// "not offered" cannot quietly become "not rendered".
//
// Which of the two the palette advertises was a decision, not a pin-bump edit
// (maintainer ruling 2026-08-22, objectui#5495 — Option A, the canonical name).
// Everything else already pointed that way: `core/src/registry/public-blocks.ts`
// records `record:chatter` as "`record:discussion` under a Salesforce-familiar
// name, kept for schemas", objectui's own generator has emitted the canonical
// spelling all along (`synth/buildDefaultPageSchema.ts`), and the console's AI
// vocabulary leaves the alias uncurated for the same reason
// (`apps/console/src/__tests__/public-contract.test.ts`, DELIBERATELY_UNCURATED).
// The palette was the last surface still pointing at the alias — it offered it
// only because the then-pinned `@objectstack/spec` did not declare the
// canonical name yet (objectui#5328); the pinned spec now declares both.
'record:chatter': 'compatibility alias — same renderer as the offered canonical `record:discussion`; still renders, just no longer advertised',
};

export const CATEGORY_LABEL_EN: Record<BlockCategory, string> = {
Expand Down
Loading