Skip to content

Commit 1f8e03c

Browse files
authored
Merge pull request #894 from objectstack-ai/copilot/fix-calendar-view-activation
2 parents c993ca6 + 15ff2e6 commit 1f8e03c

File tree

10 files changed

+85
-8
lines changed

10 files changed

+85
-8
lines changed

apps/console/objectstack.shared.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,11 @@ export const sharedConfig = {
142142
]
143143
};
144144

145-
export default defineStack(sharedConfig as Parameters<typeof defineStack>[0]);
145+
// defineStack() validates the config but strips non-standard properties like
146+
// listViews from objects. Re-merge listViews after validation so the runtime
147+
// protocol serves objects with their view definitions (calendar, kanban, etc.).
148+
const validated = defineStack(sharedConfig as Parameters<typeof defineStack>[0]);
149+
export default {
150+
...validated,
151+
objects: mergeViewsIntoObjects(validated.objects || [], allConfigs),
152+
};

apps/console/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ export function AppContent() {
427427
function findFirstRoute(items: any[]): string {
428428
if (!items || items.length === 0) return '';
429429
for (const item of items) {
430-
if (item.type === 'object') return `${item.objectName}`;
430+
if (item.type === 'object') return item.viewName ? `${item.objectName}/view/${item.viewName}` : `${item.objectName}`;
431431
if (item.type === 'page') return item.pageName ? `page/${item.pageName}` : '';
432432
if (item.type === 'dashboard') return item.dashboardName ? `dashboard/${item.dashboardName}` : '';
433433
if (item.type === 'url') continue; // Skip external URLs

apps/console/src/components/AppSidebar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,10 @@ export function AppSidebar({ activeAppName, onAppChange }: { activeAppName: stri
524524
const NavIcon = getIcon(item.icon);
525525
const baseUrl = `/apps/${activeAppName}`;
526526
let href = '#';
527-
if (item.type === 'object') href = `${baseUrl}/${item.objectName}`;
527+
if (item.type === 'object') {
528+
href = `${baseUrl}/${item.objectName}`;
529+
if (item.viewName) href += `/view/${item.viewName}`;
530+
}
528531
else if (item.type === 'dashboard') href = item.dashboardName ? `${baseUrl}/dashboard/${item.dashboardName}` : '#';
529532
else if (item.type === 'page') href = item.pageName ? `${baseUrl}/page/${item.pageName}` : '#';
530533
return (

apps/console/src/components/SearchResultsPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ export function SearchResultsPage() {
7777
const navItems = flattenNavigation(activeApp.navigation || []);
7878
return navItems.map((item: any) => {
7979
let href = '#';
80-
if (item.type === 'object') href = `${baseUrl}/${item.objectName}`;
80+
if (item.type === 'object') {
81+
href = `${baseUrl}/${item.objectName}`;
82+
if (item.viewName) href += `/view/${item.viewName}`;
83+
}
8184
else if (item.type === 'dashboard') href = `${baseUrl}/dashboard/${item.dashboardName}`;
8285
else if (item.type === 'page') href = `${baseUrl}/page/${item.pageName}`;
8386
else if (item.type === 'report') href = `${baseUrl}/report/${item.reportName}`;

objectstack.config.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,36 @@ const baseObjects = [
3434
...(kitchenSink.objects || []),
3535
];
3636

37+
// Collect all example configs for view merging
38+
const allConfigs = [crm, todo, kitchenSink];
39+
40+
// ---------------------------------------------------------------------------
41+
// Merge stack-level views into object definitions.
42+
// defineStack() strips non-standard properties like listViews from objects.
43+
// Re-merge listViews after validation so the runtime protocol serves objects
44+
// with their view definitions (calendar, kanban, etc.).
45+
// ---------------------------------------------------------------------------
46+
function mergeViewsIntoObjects(objects: any[], configs: any[]): any[] {
47+
const viewsByObject: Record<string, Record<string, any>> = {};
48+
for (const config of configs) {
49+
if (!Array.isArray(config.views)) continue;
50+
for (const view of config.views) {
51+
if (!view.listViews) continue;
52+
for (const [viewName, listView] of Object.entries(view.listViews as Record<string, any>)) {
53+
const objectName = listView?.data?.object;
54+
if (!objectName) continue;
55+
if (!viewsByObject[objectName]) viewsByObject[objectName] = {};
56+
viewsByObject[objectName][viewName] = listView;
57+
}
58+
}
59+
}
60+
return objects.map((obj: any) => {
61+
const views = viewsByObject[obj.name];
62+
if (!views) return obj;
63+
return { ...obj, listViews: { ...(obj.listViews || {}), ...views } };
64+
});
65+
}
66+
3767
// Merge all example configs into a single app bundle for AppPlugin
3868
const mergedApp = defineStack({
3969
manifest: {
@@ -49,6 +79,11 @@ const mergedApp = defineStack({
4979
],
5080
},
5181
objects: baseObjects,
82+
views: [
83+
...(crm.views || []),
84+
...(todo.views || []),
85+
...(kitchenSink.views || []),
86+
],
5287
apps: [
5388
...(crm.apps || []),
5489
...(todo.apps || []),
@@ -69,14 +104,20 @@ const mergedApp = defineStack({
69104
],
70105
} as any);
71106

107+
// Re-merge listViews that defineStack stripped from objects
108+
const mergedAppWithViews = {
109+
...mergedApp,
110+
objects: mergeViewsIntoObjects(mergedApp.objects || [], allConfigs),
111+
};
112+
72113
// Export only plugins — no top-level objects/manifest/apps.
73114
// The CLI auto-creates an AppPlugin from the config if it detects objects/manifest/apps,
74115
// which would conflict with our explicit AppPlugin and skip seed data loading.
75116
export default {
76117
plugins: [
77118
new ObjectQLPlugin(),
78119
new DriverPlugin(new InMemoryDriver()),
79-
new AppPlugin(mergedApp),
120+
new AppPlugin(mergedAppWithViews),
80121
new HonoServerPlugin({ port: 3000 }),
81122
new ConsolePlugin(),
82123
],

packages/layout/src/AppSchemaRenderer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ function MobileBottomNav({
195195
{leaves.map((item) => {
196196
const NavIcon = resolveIcon(item.icon);
197197
let href = '#';
198-
if (item.type === 'object') href = `${basePath}/${item.objectName}`;
198+
if (item.type === 'object') {
199+
href = `${basePath}/${item.objectName}`;
200+
if (item.viewName) href += `/view/${item.viewName}`;
201+
}
199202
else if (item.type === 'dashboard') href = item.dashboardName ? `${basePath}/dashboard/${item.dashboardName}` : '#';
200203
else if (item.type === 'page') href = item.pageName ? `${basePath}/page/${item.pageName}` : '#';
201204
else if (item.type === 'report') href = item.reportName ? `${basePath}/report/${item.reportName}` : '#';

packages/layout/src/NavigationRenderer.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,10 @@ const defaultPermission: PermissionChecker = () => true;
184184

185185
function resolveHref(item: NavigationItem, basePath: string): { href: string; external: boolean } {
186186
switch (item.type) {
187-
case 'object':
188-
return { href: `${basePath}/${item.objectName ?? ''}`, external: false };
187+
case 'object': {
188+
const objectPath = `${basePath}/${item.objectName ?? ''}`;
189+
return { href: item.viewName ? `${objectPath}/view/${item.viewName}` : objectPath, external: false };
190+
}
189191
case 'dashboard':
190192
return { href: item.dashboardName ? `${basePath}/dashboard/${item.dashboardName}` : '#', external: false };
191193
case 'page':

packages/layout/src/__tests__/NavigationRenderer.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,20 @@ describe('NavigationRenderer', () => {
110110
expect(link?.getAttribute('href')).toBe('/apps/test/account');
111111
});
112112

113+
it('renders object navigation item with viewName in href', () => {
114+
const calendarItem: NavigationItem = {
115+
id: 'nav-calendar',
116+
type: 'object',
117+
label: 'Calendar',
118+
icon: 'Calendar',
119+
objectName: 'event',
120+
viewName: 'calendar',
121+
};
122+
renderNav([calendarItem]);
123+
const link = screen.getByText('Calendar').closest('a');
124+
expect(link?.getAttribute('href')).toBe('/apps/test/event/view/calendar');
125+
});
126+
113127
it('renders dashboard navigation item', () => {
114128
renderNav([dashboardItem]);
115129
expect(screen.getByText('Sales Dashboard')).toBeTruthy();

packages/types/src/app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export interface NavigationItem {
6666
/** Target object name (for type: 'object') */
6767
objectName?: string;
6868

69+
/** Target view name (for type: 'object') — opens a specific named list view e.g. 'calendar', 'pipeline' */
70+
viewName?: string;
71+
6972
/** Target dashboard name (for type: 'dashboard') */
7073
dashboardName?: string;
7174

packages/types/src/zod/app.zod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const NavigationItemSchema: z.ZodType<any> = z.lazy(() => z.object({
4141

4242
// Type-specific target fields
4343
objectName: z.string().optional().describe('Target object name (type: object)'),
44+
viewName: z.string().optional().describe('Target view name (type: object) — named list view e.g. calendar, pipeline'),
4445
dashboardName: z.string().optional().describe('Target dashboard name (type: dashboard)'),
4546
pageName: z.string().optional().describe('Target page name (type: page)'),
4647
reportName: z.string().optional().describe('Target report name (type: report)'),

0 commit comments

Comments
 (0)