Skip to content

[console][nav] filterNav never drops a group DECLARED with children: [] — Setup renders an inert 「Approvals」 group on a runtime without plugin-approvals #7380

Description

@baozhoutao

Found while browser-verifying the #7213 epic's converged approvals surface (the epic's close-out lists browser verification as its open gap). Not a regression from that epic — the defect predates it — but it is on the same nav face, so recording it here.

The finding

filterNav (packages/rest/src/rest-server.ts:2724) is documented to "Drop empty groups so the sidebar doesn't render a label with nothing under it". It does not do that for a group declared with children: [] — only for a group whose children were non-empty and were then filtered away:

if (Array.isArray(e.children) && e.children.length > 0) {   // ← gate
    const kids = filterNav(e.children);
    if (e.type === 'group' && kids.length === 0) continue;  // ← the drop rule
    out.push({ ...e, children: kids });
} else {
    out.push(e);                                            // ← declared-empty group lands here
}

A group that starts out empty never enters the branch that owns the drop rule, so it is pushed through untouched. That is precisely the shape every contribution slot in the setup app uses — packages/platform-objects/src/apps/setup.app.ts declares group_approvals (and its siblings) with children: [] and lets plugins fill them via navigationContributions. When the contributing plugin is absent, the slot survives filtering as an empty group.

Reproduction (measured)

Two runtimes, same build (objectstack main 88154bee1, vendored console rebuilt at .objectui-sha = 8aad9fd50b16), both objectstack dev --ui --seed-admin on a fresh DB, signed in as the dev admin:

  • examples/app-showcaserequires: [… 'approvals' …], so ApprovalsServicePlugin loads and contributes four items. GET /api/v1/meta/appgroup_approvals has its four children; the sidebar renders 审批 with 审批中心 / 审批申请 / 审批历史 / 审批委派(外出). Correct.
  • examples/app-crmrequires: ['ui', 'automation'], no approvals plugin. GET /api/v1/meta/app returns:
  - group_approvals [group] "Approvals"      ← no children key content, nothing under it

and the Setup sidebar renders an 审批 group with a chevron that does not expand and reveals nothing on click — an inert label.

Worth noting the same run confirms the item-level gate works exactly as intended one app over: the account app's nav_account_approvals (requiresService: 'approvals') is correctly absent from the same response, so no dead component route is served. It is only the group that leaks.

Scope

Every children: [] contribution slot has this exposure, not just approvals — approvals is simply the one where the contributing plugin is genuinely optional, so it is the one that shows up on a stock example. A runtime that drops any other optional contributor would surface the same empty label.

Suggested shape (not prescriptive)

Move the drop rule out from under the non-empty gate, so a type: 'group' with no surviving children is dropped whether it became empty or started empty. The one thing worth deciding before writing it: whether a childless group is ever legitimately a navigation target on its own (a clickable group header). If it never is, the rule is unconditional; if it can be, the rule needs to spare those.

An alternative fix lives in the console (AppSidebar skipping empty groups), but ADR-0057 D10 makes the server the authoritative visibility gate and the client's filtering a courtesy, so the server side is the one that should not emit it.

Environment

  • objectstack main @ 88154bee1, @objectstack/*@17.0.0-rc.5, vendored console at objectui 8aad9fd50b16, sqlite driver, single tenancy, zh-CN.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions