Skip to content

fix: include viewName in navigation href and preserve listViews through defineStack#894

Merged
hotlong merged 4 commits intomainfrom
copilot/fix-calendar-view-activation
Feb 27, 2026
Merged

fix: include viewName in navigation href and preserve listViews through defineStack#894
hotlong merged 4 commits intomainfrom
copilot/fix-calendar-view-activation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 27, 2026

NavigationItem of type 'object' with a viewName (e.g. calendar, pipeline) generated hrefs that dropped the view segment, always falling back to the default grid view. Additionally, defineStack() from @objectstack/spec stripped listViews from object definitions via Zod validation, so even with the correct URL the console's ObjectView never saw the calendar/kanban view definitions and always fell back to grid.

// CRM nav config already specifies viewName
{ id: 'nav_events', type: 'object', objectName: 'event', viewName: 'calendar', ... }

// Before: /apps/crm_app/event        → grid (wrong, and listViews missing from objectDef)
// After:  /apps/crm_app/event/view/calendar  → calendar (correct, listViews preserved)

Changes

Part 1: href generation — append /view/${viewName} when present

  • packages/types/src/app.ts — Add optional viewName to NavigationItem
  • packages/types/src/zod/app.zod.ts — Add viewName to Zod schema
  • packages/layout/src/NavigationRenderer.tsxresolveHref appends /view/${viewName} when present
  • packages/layout/src/AppSchemaRenderer.tsx — Same fix in MobileBottomNav inline href logic
  • apps/console/src/App.tsxfindFirstRoute() includes viewName for home redirect
  • apps/console/src/components/AppSidebar.tsx — Console mobile bottom nav includes viewName
  • apps/console/src/components/SearchResultsPage.tsx — Search results include viewName
  • packages/layout/src/__tests__/NavigationRenderer.test.tsx — Test for viewName href generation

Part 2: view data pipeline — preserve listViews through defineStack()

  • apps/console/objectstack.shared.ts — Re-merge listViews into objects after defineStack() validation strips them (MSW/browser mode)
  • objectstack.config.ts — Same fix plus add missing views array to the merged config (server mode)

defineStack() validates config through Zod schemas which strip non-standard properties like listViews from objects. The existing mergeViewsIntoObjects() call was being undone by the subsequent defineStack(). The fix applies defineStack() first for validation, then re-merges views from the raw configs into the validated objects. Without this, ObjectView received objectDef.listViews = undefined and always fell back to a default grid view regardless of the URL.

Original prompt

This section details on the original issue you should resolve

<issue_title>导航到 Calendar 视图时未激活正确视图类型(viewName 被忽略)</issue_title>
<issue_description>### 问题复现

  • 在 CRM 应用(或类似场景)侧边栏点击 "Calendar" 菜单。
  • 实际展示的是表格列表视图(grid),而不是 calendar。

代码根因

  • CRM app 的 navigation 配置项如下:
    {
      id: 'nav_events',
      type: 'object',
      objectName: 'event',
      viewName: 'calendar', // 已正确配置,期望打开 calendar 视图
      label: { key: 'crm.navigation.calendar', defaultValue: 'Calendar' },
      icon: 'calendar'
    }
  • Event 对象的 listViews 也已正确定义 calendar 类型:
    listViews: {
      calendar: { type: 'calendar', ...calendarConfig }, ... }
    }
  • 但 platform 的 NavigationRenderer/resolveHref 函数忽略了 viewName,只生成为 /apps/crm_app/event,未带上 /view/calendar
    case 'object':
      return { href: `${basePath}/${item.objectName ?? ''}`, external: false };
  • 导致 ObjectView fallback 到 grid 视图。

影响

  • 不能通过导航直接打开 calendar/kanban 等特定视图,只会显示默认 grid 视图。
  • "Calendar"、"Pipeline"等菜单都会丢失视图参数,严重影响多视图体验。

修复建议

  1. NavigationItem 类型添加 viewName 属性。
  2. NavigationRenderer.resolveHref 中,如果 item.viewName 存在,URL 应拼接 /view/${item.viewName},如 /apps/crm_app/event/view/calendar

参考案例

  • examples/crm/src/apps/crm.app.ts
  • examples/crm/src/views/event.view.ts
  • packages/layout/src/NavigationRenderer.tsx

截图

复现截图

本问题属于平台 bug,请尽快修复。</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectui Ready Ready Preview, Comment Feb 27, 2026 10:32am
objectui-demo Ready Ready Preview, Comment Feb 27, 2026 10:32am
objectui-storybook Ready Ready Preview, Comment Feb 27, 2026 10:32am

Request Review

When a NavigationItem of type 'object' has a viewName (e.g. 'calendar',
'pipeline'), append /view/{viewName} to the generated href so the
correct list view is activated on navigation.

Changes:
- Add viewName property to NavigationItem interface
- Add viewName to Zod validation schema
- Update resolveHref in NavigationRenderer to append /view/{viewName}
- Update MobileBottomNav href generation in AppSchemaRenderer
- Add test for viewName href generation

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@hotlong
Copy link
Copy Markdown
Collaborator

hotlong commented Feb 27, 2026

@copilot 仔细评估是否已经完成了任务中的所有要求,目前我测试了日历识图还是显示成表格

…rch)

Three additional places in the console app built object navigation hrefs
without considering viewName:
- findFirstRoute() in App.tsx (home redirect)
- Mobile bottom nav in AppSidebar.tsx
- SearchResultsPage.tsx search results

All now append /view/{viewName} when present, matching the layout fix.

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot 仔细评估是否已经完成了任务中的所有要求,目前我测试了日历识图还是显示成表格

发现了 3 处遗漏 —— 除了 packages/layoutresolveHrefMobileBottomNav 之外,console 应用中还有以下位置生成 object 导航 href 时未拼接 viewName

  1. apps/console/src/App.tsxfindFirstRoute()(首页重定向)
  2. apps/console/src/components/AppSidebar.tsx — console 自己的移动端底部导航
  3. apps/console/src/components/SearchResultsPage.tsx — 搜索结果页

已在 a436d40 中全部修复,所有 6 处 href 生成逻辑现在都会在 viewName 存在时拼接 /view/${viewName}

@hotlong
Copy link
Copy Markdown
Collaborator

hotlong commented Feb 27, 2026

@copilot 还是不行,看到的还是表格视图从头开始排查每一个细节,看问题到底出在哪里

Root cause: defineStack() validates config through Zod schemas which strip
non-standard properties like listViews from objects. The mergeViewsIntoObjects()
call in objectstack.shared.ts was being undone by the subsequent defineStack().

Fix: Apply defineStack() first for validation, then re-merge views from the
raw config into the validated objects. Applied to both:
- apps/console/objectstack.shared.ts (MSW/browser mode)
- objectstack.config.ts (server mode)

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot 还是不行,看到的还是表格视图从头开始排查每一个细节,看问题到底出在哪里

找到了根本原因 —— 问题不在 href 生成,而是 视图数据管道

objectstack.shared.ts 中的 defineStack() 使用 Zod schema 验证配置时,会剥离对象上的 listViews 属性(因为 Zod ObjectSchema 不包含 listViews)。虽然 mergeViewsIntoObjects() 正确地将视图合并到了对象中,但后续的 defineStack() 调用把它们全部清除了。

结果:ObjectView 组件获取到的 objectDef.listViewsundefined,只能回退到默认的 grid 视图。即使 URL 正确包含 /view/calendar,也找不到对应的 calendar 视图定义。

修复方案(15ff2e6):先执行 defineStack() 验证,然后从原始配置重新合并 listViews 到验证后的对象中。同时修复了两个配置文件:

  • apps/console/objectstack.shared.ts(MSW/浏览器模式)
  • objectstack.config.ts(服务器模式,之前还缺少 views 数组的合并)

Copilot AI changed the title fix: include viewName in navigation href for object items fix: include viewName in navigation href and preserve listViews through defineStack Feb 27, 2026
@hotlong hotlong marked this pull request as ready for review February 27, 2026 10:38
Copilot AI review requested due to automatic review settings February 27, 2026 10:38
@hotlong hotlong merged commit 1f8e03c into main Feb 27, 2026
6 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes navigation to named object views (e.g. calendar/kanban) by (1) preserving viewName in generated hrefs and (2) ensuring listViews survives defineStack() validation so the console can resolve the correct view type instead of falling back to the default grid.

Changes:

  • Add viewName?: string to NavigationItem and its Zod schema.
  • Append /view/${viewName} to object navigation hrefs across layout + console entry points, plus a layout unit test.
  • Re-merge stack-level views[].listViews into validated objects after defineStack() (console MSW mode + server/dev config).

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/types/src/app.ts Adds viewName to NavigationItem type so apps can declare a target named view.
packages/types/src/zod/app.zod.ts Updates Zod validation to preserve viewName in navigation configs.
packages/layout/src/NavigationRenderer.tsx Updates resolveHref() to include /view/<viewName> for object nav items.
packages/layout/src/AppSchemaRenderer.tsx Updates mobile bottom nav href generation to include /view/<viewName>.
packages/layout/src/tests/NavigationRenderer.test.tsx Adds a unit test covering viewName href generation in NavigationRenderer.
apps/console/src/App.tsx Updates initial home redirect route selection to include viewName when present.
apps/console/src/components/AppSidebar.tsx Updates console mobile bottom nav links to include /view/<viewName>.
apps/console/src/components/SearchResultsPage.tsx Updates search result hrefs for object items to include /view/<viewName>.
apps/console/objectstack.shared.ts Applies defineStack() first, then re-merges listViews back into validated objects (MSW/browser).
objectstack.config.ts Adds merged views to the validated stack and re-merges listViews into objects after defineStack() (server/dev).

Comment thread apps/console/src/App.tsx
if (!items || items.length === 0) return '';
for (const item of items) {
if (item.type === 'object') return `${item.objectName}`;
if (item.type === 'object') return item.viewName ? `${item.objectName}/view/${item.viewName}` : `${item.objectName}`;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findFirstRoute() now includes viewName, but it still assumes item.objectName is present for type: 'object'. If the first visible object nav item is missing objectName, this will return undefined (or undefined/view/...) and break the app’s root redirect. Consider skipping object items without objectName and continuing the search.

Suggested change
if (item.type === 'object') return item.viewName ? `${item.objectName}/view/${item.viewName}` : `${item.objectName}`;
if (item.type === 'object') {
// Skip object items that do not have a valid objectName
if (!item.objectName) continue;
return item.viewName ? `${item.objectName}/view/${item.viewName}` : `${item.objectName}`;
}

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +152
const validated = defineStack(sharedConfig as Parameters<typeof defineStack>[0]);
export default {
...validated,
objects: mergeViewsIntoObjects(validated.objects || [], allConfigs),
};
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change re-merges listViews after defineStack() (objects: mergeViewsIntoObjects(validated.objects || [], allConfigs)), but sharedConfig.objects already ran mergeViewsIntoObjects() before validation. Since defineStack() strips listViews, the pre-merge is now redundant work and makes the pipeline harder to follow. Consider constructing sharedConfig.objects without listViews and applying mergeViewsIntoObjects() only once after validation.

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +189
const objectPath = `${basePath}/${item.objectName ?? ''}`;
return { href: item.viewName ? `${objectPath}/view/${item.viewName}` : objectPath, external: false };
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveHref() builds an object href even when item.objectName is missing (it becomes ${basePath}/), and with viewName present it can produce a double-slash path like ${basePath}//view/<name>. There are existing configs/tests that create NavigationItem objects without objectName (e.g. designer hidden items), so this can cause navigation to unexpectedly route to the app root. Consider returning '#' (non-navigable) when objectName is absent, consistent with the dashboard/page/report cases.

Suggested change
const objectPath = `${basePath}/${item.objectName ?? ''}`;
return { href: item.viewName ? `${objectPath}/view/${item.viewName}` : objectPath, external: false };
if (!item.objectName) {
return { href: '#', external: false };
}
const objectPath = `${basePath}/${item.objectName}`;
return {
href: item.viewName ? `${objectPath}/view/${item.viewName}` : objectPath,
external: false,
};

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +200
href = `${basePath}/${item.objectName}`;
if (item.viewName) href += `/view/${item.viewName}`;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MobileBottomNav constructs object hrefs with ${basePath}/${item.objectName} without guarding for a missing objectName. This can yield routes like /apps/foo/undefined and will now also append /view/${item.viewName} onto that. Since other code paths allow NavigationItem objects without objectName (e.g. designer-created hidden items), it would be safer to keep href = '#' unless objectName is present.

Suggested change
href = `${basePath}/${item.objectName}`;
if (item.viewName) href += `/view/${item.viewName}`;
if (item.objectName) {
href = `${basePath}/${item.objectName}`;
if (item.viewName) href += `/view/${item.viewName}`;
} else {
href = '#';
}

Copilot uses AI. Check for mistakes.
Comment on lines 195 to +199
{leaves.map((item) => {
const NavIcon = resolveIcon(item.icon);
let href = '#';
if (item.type === 'object') href = `${basePath}/${item.objectName}`;
if (item.type === 'object') {
href = `${basePath}/${item.objectName}`;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s test coverage for href generation in packages/layout/src/__tests__/AppSchemaRenderer.test.tsx, but no test asserting the new viewName/view/<name> behavior for the mobile bottom nav. Adding a focused test would help prevent regressions since href logic is duplicated across multiple components.

Copilot uses AI. Check for mistakes.
return navItems.map((item: any) => {
let href = '#';
if (item.type === 'object') href = `${baseUrl}/${item.objectName}`;
if (item.type === 'object') {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SearchResultsPage builds object hrefs using ${baseUrl}/${item.objectName} without checking that objectName exists. Navigation configs produced by the designer can include type: 'object' items without objectName, which would cause these results to show up with /apps/<app>/undefined (and possibly /view/... appended). Consider only creating an href when objectName is present (otherwise keep '#' so it gets filtered out).

Suggested change
if (item.type === 'object') {
if (item.type === 'object' && item.objectName) {

Copilot uses AI. Check for mistakes.
Comment on lines +528 to +529
href = `${baseUrl}/${item.objectName}`;
if (item.viewName) href += `/view/${item.viewName}`;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console mobile bottom nav constructs object hrefs with ${baseUrl}/${item.objectName} without guarding for missing objectName. If a type: 'object' navigation item lacks objectName, this produces /apps/<app>/undefined (and now may append /view/...). Consider leaving href as '#' unless objectName is defined, consistent with other target types.

Suggested change
href = `${baseUrl}/${item.objectName}`;
if (item.viewName) href += `/view/${item.viewName}`;
if (item.objectName) {
href = `${baseUrl}/${item.objectName}`;
if (item.viewName) href += `/view/${item.viewName}`;
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

导航到 Calendar 视图时未激活正确视图类型(viewName 被忽略)

3 participants