Skip to content

Commit f16b4bf

Browse files
authored
Merge pull request #569 from objectstack-ai/copilot/fix-mobile-crm-dashboard-issues
2 parents ae180d4 + c242406 commit f16b4bf

4 files changed

Lines changed: 240 additions & 14 deletions

File tree

apps/console/src/components/AppHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,12 @@ export function AppHeader({ appName, objects, connectionState, presenceUsers }:
236236
</Button>
237237

238238
{/* Theme toggle */}
239-
<div className="shrink-0">
239+
<div className="hidden sm:flex shrink-0">
240240
<ModeToggle />
241241
</div>
242242

243243
{/* Language switcher */}
244-
<div className="shrink-0">
244+
<div className="hidden sm:flex shrink-0">
245245
<LocaleSwitcher />
246246
</div>
247247
</div>

apps/console/src/components/DashboardView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function DashboardView({ dataSource }: { dataSource?: any }) {
6565
</div>
6666

6767
<div className="flex-1 overflow-hidden flex flex-col sm:flex-row relative">
68-
<div className="flex-1 overflow-auto p-4 sm:p-6">
68+
<div className="flex-1 overflow-auto p-0 sm:p-6">
6969
<DashboardRenderer schema={dashboard} dataSource={dataSource} />
7070
</div>
7171

packages/plugin-dashboard/src/DashboardRenderer.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
3838
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
3939

4040
useEffect(() => {
41-
const checkMobile = () => setIsMobile(window.innerWidth < 640);
41+
const checkMobile = () => setIsMobile(window.innerWidth < 768);
4242
checkMobile();
4343
window.addEventListener('resize', checkMobile);
4444
return () => window.removeEventListener('resize', checkMobile);
@@ -61,7 +61,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
6161
};
6262
}, [schema.refreshInterval, onRefresh, handleRefresh]);
6363

64-
const renderWidget = (widget: DashboardWidgetSchema, index: number) => {
64+
const renderWidget = (widget: DashboardWidgetSchema, index: number, forceMobileFullWidth?: boolean) => {
6565
const getComponentSchema = () => {
6666
if (widget.component) return widget.component;
6767

@@ -113,7 +113,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
113113
return (
114114
<div
115115
key={widgetKey}
116-
className={cn("h-full w-full", isMobile && "w-[85vw] shrink-0 snap-center")}
116+
className={cn("h-full w-full")}
117117
style={!isMobile && widget.layout ? {
118118
gridColumn: `span ${widget.layout.w}`,
119119
gridRow: `span ${widget.layout.h}`
@@ -130,7 +130,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
130130
className={cn(
131131
"overflow-hidden border-border/50 shadow-sm transition-all hover:shadow-md",
132132
"bg-card/50 backdrop-blur-sm",
133-
isMobile && "w-[85vw] shrink-0 snap-center"
133+
forceMobileFullWidth && "w-full"
134134
)}
135135
style={!isMobile && widget.layout ? {
136136
gridColumn: `span ${widget.layout.w}`,
@@ -169,15 +169,27 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
169169
);
170170

171171
if (isMobile) {
172+
// Separate metric widgets from other widgets for better mobile layout
173+
const metricWidgets = schema.widgets?.filter(w => w.type === 'metric') || [];
174+
const otherWidgets = schema.widgets?.filter(w => w.type !== 'metric') || [];
175+
172176
return (
173-
<div ref={ref} className={cn("flex flex-col", className)} {...props}>
177+
<div ref={ref} className={cn("flex flex-col gap-4 px-4", className)} {...props}>
174178
{refreshButton}
175-
<div
176-
className="flex overflow-x-auto snap-x snap-mandatory gap-3 pb-4 [-webkit-overflow-scrolling:touch]"
177-
style={{ scrollPaddingLeft: '0.75rem' }}
178-
>
179-
{schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index))}
180-
</div>
179+
180+
{/* Metric cards: 2-column grid */}
181+
{metricWidgets.length > 0 && (
182+
<div className="grid grid-cols-2 gap-3">
183+
{metricWidgets.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index))}
184+
</div>
185+
)}
186+
187+
{/* Other widgets (charts, tables): full-width vertical stack */}
188+
{otherWidgets.length > 0 && (
189+
<div className="flex flex-col gap-4">
190+
{otherWidgets.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index, true))}
191+
</div>
192+
)}
181193
</div>
182194
);
183195
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
10+
import { render } from '@testing-library/react';
11+
import { DashboardRenderer } from '../DashboardRenderer';
12+
import type { DashboardSchema } from '@object-ui/types';
13+
14+
describe('DashboardRenderer mobile layout', () => {
15+
let originalInnerWidth: number;
16+
17+
beforeEach(() => {
18+
originalInnerWidth = window.innerWidth;
19+
});
20+
21+
afterEach(() => {
22+
// Restore original window size
23+
Object.defineProperty(window, 'innerWidth', {
24+
writable: true,
25+
configurable: true,
26+
value: originalInnerWidth,
27+
});
28+
});
29+
30+
const setWindowWidth = (width: number) => {
31+
Object.defineProperty(window, 'innerWidth', {
32+
writable: true,
33+
configurable: true,
34+
value: width,
35+
});
36+
window.dispatchEvent(new Event('resize'));
37+
};
38+
39+
it('should use mobile layout when window width is less than 768px', () => {
40+
const schema: DashboardSchema = {
41+
type: 'dashboard',
42+
name: 'test',
43+
title: 'Test Dashboard',
44+
widgets: [
45+
{
46+
type: 'metric',
47+
title: 'Revenue',
48+
options: { value: '$100k', label: 'Total Revenue' },
49+
},
50+
{
51+
type: 'metric',
52+
title: 'Users',
53+
options: { value: '1,234', label: 'Active Users' },
54+
},
55+
],
56+
};
57+
58+
// Set mobile viewport width
59+
setWindowWidth(375);
60+
61+
const { container } = render(<DashboardRenderer schema={schema} />);
62+
63+
// Mobile layout should have px-4 class and grid grid-cols-2 for metrics
64+
const mobileContainer = container.querySelector('.px-4');
65+
expect(mobileContainer).toBeTruthy();
66+
67+
const metricGrid = container.querySelector('.grid.grid-cols-2');
68+
expect(metricGrid).toBeTruthy();
69+
});
70+
71+
it('should use desktop layout when window width is 768px or more', () => {
72+
const schema: DashboardSchema = {
73+
type: 'dashboard',
74+
name: 'test',
75+
title: 'Test Dashboard',
76+
widgets: [
77+
{
78+
type: 'metric',
79+
title: 'Revenue',
80+
options: { value: '$100k', label: 'Total Revenue' },
81+
},
82+
],
83+
};
84+
85+
// Set desktop viewport width
86+
setWindowWidth(1024);
87+
88+
const { container } = render(<DashboardRenderer schema={schema} />);
89+
90+
// Desktop layout should have grid class but not px-4 or grid-cols-2
91+
const desktopGrid = container.querySelector('.grid.auto-rows-min');
92+
expect(desktopGrid).toBeTruthy();
93+
94+
const mobileContainer = container.querySelector('.px-4');
95+
expect(mobileContainer).toBeFalsy();
96+
});
97+
98+
it('should separate metric and non-metric widgets in mobile layout', () => {
99+
const schema: DashboardSchema = {
100+
type: 'dashboard',
101+
name: 'test',
102+
title: 'Test Dashboard',
103+
widgets: [
104+
{
105+
type: 'metric',
106+
title: 'Metric 1',
107+
options: { value: '100', label: 'Count' },
108+
},
109+
{
110+
type: 'metric',
111+
title: 'Metric 2',
112+
options: { value: '200', label: 'Total' },
113+
},
114+
{
115+
type: 'bar',
116+
title: 'Chart',
117+
options: {
118+
data: { items: [{ name: 'A', value: 10 }] },
119+
xField: 'name',
120+
yField: 'value',
121+
},
122+
},
123+
],
124+
};
125+
126+
// Set mobile viewport width
127+
setWindowWidth(375);
128+
129+
const { container } = render(<DashboardRenderer schema={schema} />);
130+
131+
// Should have a 2-column grid for metrics
132+
const metricGrid = container.querySelector('.grid.grid-cols-2');
133+
expect(metricGrid).toBeTruthy();
134+
135+
// Should have metric widgets in the grid
136+
const metricWidgets = metricGrid?.children;
137+
expect(metricWidgets?.length).toBe(2);
138+
139+
// Should have a flex-col container for other widgets
140+
const otherWidgetsContainer = container.querySelector('.flex.flex-col.gap-4');
141+
expect(otherWidgetsContainer).toBeTruthy();
142+
});
143+
144+
it('should handle mobile layout with only metric widgets', () => {
145+
const schema: DashboardSchema = {
146+
type: 'dashboard',
147+
name: 'test',
148+
title: 'Test Dashboard',
149+
widgets: [
150+
{
151+
type: 'metric',
152+
title: 'Metric 1',
153+
options: { value: '100' },
154+
},
155+
{
156+
type: 'metric',
157+
title: 'Metric 2',
158+
options: { value: '200' },
159+
},
160+
],
161+
};
162+
163+
// Set mobile viewport width
164+
setWindowWidth(375);
165+
166+
const { container } = render(<DashboardRenderer schema={schema} />);
167+
168+
// Should have a 2-column grid for metrics
169+
const metricGrid = container.querySelector('.grid.grid-cols-2');
170+
expect(metricGrid).toBeTruthy();
171+
expect(metricGrid?.children.length).toBe(2);
172+
});
173+
174+
it('should handle mobile layout with only non-metric widgets', () => {
175+
const schema: DashboardSchema = {
176+
type: 'dashboard',
177+
name: 'test',
178+
title: 'Test Dashboard',
179+
widgets: [
180+
{
181+
type: 'bar',
182+
title: 'Chart 1',
183+
options: {
184+
data: { items: [{ name: 'A', value: 10 }] },
185+
xField: 'name',
186+
yField: 'value',
187+
},
188+
},
189+
{
190+
type: 'line',
191+
title: 'Chart 2',
192+
options: {
193+
data: { items: [{ name: 'B', value: 20 }] },
194+
xField: 'name',
195+
yField: 'value',
196+
},
197+
},
198+
],
199+
};
200+
201+
// Set mobile viewport width
202+
setWindowWidth(375);
203+
204+
const { container } = render(<DashboardRenderer schema={schema} />);
205+
206+
// Should not have a metric grid
207+
const metricGrid = container.querySelector('.grid.grid-cols-2');
208+
expect(metricGrid).toBeFalsy();
209+
210+
// Should have a flex-col container for other widgets
211+
const otherWidgetsContainer = container.querySelector('.flex.flex-col.gap-4');
212+
expect(otherWidgetsContainer).toBeTruthy();
213+
});
214+
});

0 commit comments

Comments
 (0)