Skip to content

Console renders nav:menu / global:search / global:notifications / app:launcher as "Component Placeholder" — four spec-declared PageComponentType members with no renderer #12183

Closed
3 of 3 issues completed

Description

@huangyiirene

Four page-component types that @objectstack/spec declares as first-class, no-props members of PageComponentType have no renderer in the console: each one draws the literal scaffold text Component Placeholder with its own type name above it, on the page where an author placed it.

Measured in a real browser against a running app — not read off source. Downstream tracking card: objectstack-ai/hotcrm#734 (half 1).

Environment

  • @objectstack/* 17.1.0 (every package in the app pinned to it; nothing bumped for the measurement)
  • App: HotCRM at objectstack-ai/hotcrm@f685136, served by objectstack dev -p 4001, seed data loaded on first boot
  • Browser: Chromium 1194 via Playwright, admin-authenticated as the seeded dev admin (admin@objectos.ai), viewport 1600×1200
  • Node 22.22.2, pnpm 10.33.0

Reproduction

Sign in to the console as an admin and open, in the published app:

/_console/apps/app.objectstack.hotcrm/page/sales_home_page
/_console/apps/app.objectstack.hotcrm/page/app_launcher_page
/_console/apps/app.objectstack.hotcrm/page/utility_bar_page

What renders

All five component instances across the three pages draw the placeholder. Each is a dashed-outline box containing a document-with-question-mark glyph, the component type in monospace, and the caption Component Placeholder:

Page Component Renders
sales_home_page nav:menu (inside the Quick Create card) ⛔ nav:menu / Component Placeholder
app_launcher_page global:search ⛔ global:search / Component Placeholder
app_launcher_page app:launcher ⛔ app:launcher / Component Placeholder
utility_bar_page global:notifications ⛔ global:notifications / Component Placeholder
utility_bar_page global:search ⛔ global:search / Component Placeholder

Occurrences of the literal string Component Placeholder in document.body.innerText: 1 on sales_home_page, 2 on app_launcher_page, 2 on utility_bar_page.

Verbatim innerText from the app_launcher_page main region:

Central hub for accessing all applications
global:search
Component Placeholder
App Launcher
Select an app to get started
app:launcher
Component Placeholder

and from utility_bar_page:

Utility Bar
Quick access utility bar with floating tools
global:notifications
Component Placeholder
Quick Notes
global:search
Component Placeholder

This is not a page-framework failure — the same pages render everything else correctly. On sales_home_page the four object-metric KPI cards pull real figures (Revenue (Won) 1,290,000.00 / Deals Won 8 / Pipeline Value 3,775,000.00 / Open Leads 21), the embedded list views return 21 / 10 / 6 real rows, and page:header draws. Only these four component types fall through to the placeholder.

Why this is a platform gap and not app metadata using a wrong spelling

All four are named members of the PageComponentType enum shipped in @objectstack/spec@17.1.0, and each has a deliberate no-props row in ComponentPropsMap:

dist/ui/index.d.ts:

readonly 'app:launcher':         z.ZodObject<{}, z.core.$strict>;
readonly 'nav:menu':             z.ZodObject<{}, z.core.$strict>;
readonly 'global:search':        z.ZodObject<{}, z.core.$strict>;
readonly 'global:notifications': z.ZodObject<{}, z.core.$strict>;

dist/ui/index.js:

"app:launcher":         emptyProps("app:launcher"),
"nav:menu":             emptyProps("nav:menu"),
"global:search":        emptyProps("global:search"),
"global:notifications": emptyProps("global:notifications"),

where emptyProps builds a strict, intentionally empty props object whose own history string is explicit that the emptiness is the point:

var emptyProps = (type) => strictObject(
  {
    surface: `this \`${type}\` component`,
    history: `\`${type}\` declares no props at all. ${PROPS_HISTORY}`,
    guidanceSets: COMPONENT_LEVEL_GUIDANCE
  },
  {}
);

So the app is using the spec's sanctioned spelling, with the exact props shape the spec prescribes (properties: {}), and objectstack validate passes clean on all three pages. The metadata is right; the renderer is missing.

Impact

An end user who reaches any of these pages sees developer scaffolding. app_launcher_page and utility_bar_page are, between them, entirely placeholder plus one empty card — there is no non-scaffold content on either page. Any app that composes a launcher, a global search box, a notifications panel or a nav menu from the spec's own vocabulary gets the same result.

Ask

Either implement renderers for the four types, or — if they are not intended to be author-placeable on a page — make that explicit in the spec and in validate so the failure lands at author time rather than in front of a user. Shipping them as first-class enum members with prescribed props and no renderer is the state that produces this report.

Notes for whoever picks this up

  • Screenshots of all five placeholder instances were captured during the measurement and are archived on the downstream card, [17.0-rc2验收] sales_home_page / app_launcher_page / utility_bar_page 多个组件渲染为「Component Placeholder」,首页欢迎语用户名插值为空 hotcrm#734.
  • ⚠️ The downstream app metadata is deliberately being left untouched: these three pages are currently the only reproduction of this gap, so "fixing" them app-side would delete the evidence rather than the defect.
  • Adjacent but separate, and not part of this report: page:card components declaring a title and no body render as titled empty boxes (Quick Notes on utility_bar_page, Recent Items / Today's Schedule on sales_home_page). That is a container with nothing to draw, not a missing renderer.
  • Small spec-accuracy note while looking: PageComponentSchema.type is a z.ZodUnion<[z.ZodEnum<{…}>, z.ZodString]>, so despite being an enumerated vocabulary it accepts arbitrary strings at parse time. That is why an unimplemented type reaches the renderer as a plain string and lands on the placeholder branch rather than being rejected. It does not weaken anything above — the four names are declared members with declared props — but it is likely relevant to any fix that wants author-time rejection.

Activity

  1. os-steve commented on Aug 26, 2026

    @os-steve
    Collaborator

    Maintainer ruling recorded — A, phased; B for any member not planned within the horizon

    Provenance: maintainer, 2026-08-26, live PM chat (decision-inbox batch 3, session session_01JANH3y7qe3MD8aLaLXci8N), verbatim: 「其他接受你的建议」 — accepting the presented recommendation.

    • Phase 1: implement renderers for app:launcher and nav:menu — the two purely metadata-driven members with no external data-source dependency. Landing is objectui's console; the spec seat decomposes contract-first (parent card + per-repo sub-issues with Blocked-by:) per the standing cross-repo split.
    • Phase 2: global:search / global:notifications — read platform data-source readiness FIRST; a member with no data-source plan inside the horizon is retired per B (ADR-0087 channel, author-time loud rejection) rather than left as a declared-unrendered trap.
    • Rides with the ruling: the spec-accuracy note from the card's tail — PageComponentSchema.type's union accepts arbitrary strings at parse time, which is why an unimplemented type reaches the renderer as a plain string; the author-time rejection half gets handled alongside whichever phase touches the enum.
    • The reproduction pages in hotcrm stay untouched until a renderer lands (they are the only evidence).

    State: needs-user-decision → pm:queue (domain:spec stands; spec seat owns the decomposition).


    Generated by Claude Code

  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 26, 2026
  3. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 30, 2026
  4. claude commented on Aug 30, 2026

    @claude
    Contributor

    State: pm:queue → pm:blocked (domain:spec seat, session session_01KX8wnyjStaZcuMyAMNsy3N).

    Blocked-by: objectstack-ai/objectui#6661

    This card is the COORDINATION PARENT per the 2026-08-28 decomposition — never dispatched itself. Status of the two dispatch units: #12950 (Phase 2) is CLOSED ✅; objectui#6661 (Phase 1 renderers) remains the only open dependency. A queue label on a never-dispatchable parent misleads the dispatch scan; the machine-readable edge above is what the unlock scan should watch. Parent closes when #6661 closes (keep-decisions, if any, recorded here per the decomposition note).


    Generated by Claude Code

  5. os-zhuang commented on Sep 2, 2026

    @os-zhuang
    Contributor

    Closing as completed — unlock scan by the domain:spec seat (session_01GDA48PuRFrHyRfdkBz8m21), release double-check per the state-machine rules: ① the most recent pm:blocked transition (comment 5466679802) named exactly one condition — objectui#6661 closes — and it closed 2026-08-31T20:38Z as completed via merged objectui PR #7091 (app:launcher + nav:menu console renderers); ② no PR merged against this card after that comment (closed-by list empty). All three sub-issues are closed completed: objectui#6661 (Phase 1 renderers), #12950 (Phase 2 readiness read + the component-type-unknown author-time gate, PR #13117), objectui#6757 (Phase 2 renderers for global:search / global:notifications, filed after the readiness read showed both data sources shipped).

    Keep-decisions recorded here per the 2026-08-28 decomposition note: the Phase-2 readiness read (PR #13117) evidenced both global:search and global:notifications data sources SHIPPED, so both members STAY declared — no B-retirement was exercised; the keep verdict is pinned spec-side (component-type-vocabulary.test.ts) and the arbitrary-string parse acceptance on PageComponentSchema.type was closed by the same PR. The hotCRM repro pages (hotcrm#734) were kept untouched until the renderers landed, as the ruling required; updating them is that repo's follow-up, not this card's.

    pm:blocked stripped in the same stroke; domain:spec stays as attribution. Related but separate: #14159 (user:profile, the one sibling the 2026-08-26 ruling never covered) remains in the lane queue.


    Generated by Claude Code

  6. removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specDomain lane — seat card indexed by label:pm:seat

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions