Skip to content

Commit 2e385e0

Browse files
Copilothotlong
andcommitted
feat: integrate API for system pages, collaboration, reports, and view designer
- Issue 1 (P0): System admin pages now use useAdapter() + dataSource.find() for real CRUD - Issue 2 (P1): Replace hardcoded mock data in AppHeader and RecordDetailView with API calls - Issue 3 (P2): ReportBuilder fields now derived from object schema via useMetadata() - Issue 4 (P3): ViewDesigner save now persists view config via dataSource.create/update Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent cb62e1d commit 2e385e0

8 files changed

Lines changed: 428 additions & 96 deletions

File tree

apps/console/src/components/AppHeader.tsx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ import {
2626
} from '@object-ui/components';
2727
import { Search, HelpCircle, ChevronDown } from 'lucide-react';
2828

29+
import { useState, useEffect, useCallback } from 'react';
2930
import { useOffline } from '@object-ui/react';
3031
import { PresenceAvatars, type PresenceUser } from '@object-ui/collaboration';
3132
import { ModeToggle } from './mode-toggle';
3233
import { LocaleSwitcher } from './LocaleSwitcher';
3334
import { ConnectionStatus } from './ConnectionStatus';
3435
import { ActivityFeed, type ActivityItem } from './ActivityFeed';
3536
import type { ConnectionState } from '../dataSource';
37+
import { useAdapter } from '../context/AdapterProvider';
3638

3739
/** Convert a slug like "crm_dashboard" or "audit-log" to "Crm Dashboard" / "Audit Log" */
3840
function humanizeSlug(slug: string): string {
@@ -41,27 +43,40 @@ function humanizeSlug(slug: string): string {
4143
.replace(/\b\w/g, (c) => c.toUpperCase());
4244
}
4345

44-
// Demo presence users for local/mock mode
45-
const MOCK_PRESENCE_USERS: PresenceUser[] = [
46+
// Fallback presence users when API is unavailable
47+
const FALLBACK_PRESENCE_USERS: PresenceUser[] = [
4648
{ userId: 'u1', userName: 'Alice Chen', color: '#3498db', status: 'active', lastActivity: new Date().toISOString() },
4749
{ userId: 'u2', userName: 'Bob Smith', color: '#2ecc71', status: 'idle', lastActivity: new Date().toISOString() },
4850
{ userId: 'u3', userName: 'Carol Li', color: '#e74c3c', status: 'active', lastActivity: new Date().toISOString() },
4951
];
5052

51-
// Demo activity items for local/mock mode
52-
const DEMO_ACTIVITIES: ActivityItem[] = [
53-
{ id: 'a1', type: 'create', objectName: 'Contact', recordId: 'c-101', user: 'Alice Chen', description: 'Created new contact "Acme Corp"', timestamp: new Date(Date.now() - 2 * 60 * 1000).toISOString() },
54-
{ id: 'a2', type: 'update', objectName: 'Deal', recordId: 'd-42', user: 'Bob Smith', description: 'Updated deal stage to "Negotiation"', timestamp: new Date(Date.now() - 15 * 60 * 1000).toISOString() },
55-
{ id: 'a3', type: 'comment', objectName: 'Task', recordId: 't-88', user: 'Carol Li', description: 'Commented on task "Q4 Review"', timestamp: new Date(Date.now() - 45 * 60 * 1000).toISOString() },
56-
{ id: 'a4', type: 'delete', objectName: 'Lead', recordId: 'l-7', user: 'Alice Chen', description: 'Deleted duplicate lead "Test Lead"', timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString() },
57-
{ id: 'a5', type: 'update', objectName: 'Contact', recordId: 'c-55', user: 'Bob Smith', description: 'Updated email for "Jane Doe"', timestamp: new Date(Date.now() - 5 * 60 * 60 * 1000).toISOString() },
58-
];
59-
60-
export function AppHeader({ appName, objects, connectionState, presenceUsers }: { appName: string, objects: any[], connectionState?: ConnectionState, presenceUsers?: PresenceUser[] }) {
53+
export function AppHeader({ appName, objects, connectionState, presenceUsers, activities }: { appName: string, objects: any[], connectionState?: ConnectionState, presenceUsers?: PresenceUser[], activities?: ActivityItem[] }) {
6154
const location = useLocation();
6255
const params = useParams();
6356
const { isOnline } = useOffline();
64-
const activeUsers = presenceUsers ?? MOCK_PRESENCE_USERS;
57+
const dataSource = useAdapter();
58+
59+
const [apiPresenceUsers, setApiPresenceUsers] = useState<PresenceUser[] | null>(null);
60+
const [apiActivities, setApiActivities] = useState<ActivityItem[] | null>(null);
61+
62+
const fetchPresenceAndActivities = useCallback(async () => {
63+
if (!dataSource) return;
64+
try {
65+
const [presenceResult, activityResult] = await Promise.all([
66+
dataSource.find('sys_presence').catch(() => ({ data: [] })),
67+
dataSource.find('sys_activity', { $orderby: 'timestamp desc', $top: 20 }).catch(() => ({ data: [] })),
68+
]);
69+
if (presenceResult.data?.length) setApiPresenceUsers(presenceResult.data);
70+
if (activityResult.data?.length) setApiActivities(activityResult.data);
71+
} catch {
72+
// Fallback to defaults handled below
73+
}
74+
}, [dataSource]);
75+
76+
useEffect(() => { fetchPresenceAndActivities(); }, [fetchPresenceAndActivities]);
77+
78+
const activeUsers = presenceUsers ?? apiPresenceUsers ?? FALLBACK_PRESENCE_USERS;
79+
const activeActivities = activities ?? apiActivities ?? [];
6580

6681
// Parse the current route to build breadcrumbs
6782
const pathParts = location.pathname.split('/').filter(Boolean);
@@ -227,7 +242,7 @@ export function AppHeader({ appName, objects, connectionState, presenceUsers }:
227242

228243
{/* Activity Feed */}
229244
<div className="hidden sm:flex shrink-0 relative">
230-
<ActivityFeed activities={DEMO_ACTIVITIES} />
245+
<ActivityFeed activities={activeActivities} />
231246
</div>
232247

233248
{/* Help */}

apps/console/src/components/RecordDetailView.tsx

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,7 @@ interface RecordDetailViewProps {
2222
onEdit: (record: any) => void;
2323
}
2424

25-
const MOCK_USER = { id: 'current-user', name: 'Demo User' };
26-
27-
// Demo presence users viewing the current record
28-
const MOCK_RECORD_VIEWERS: PresenceUser[] = [
29-
{ userId: 'u1', userName: 'Alice Chen', color: '#3498db', status: 'active', lastActivity: new Date().toISOString() },
30-
{ userId: 'u3', userName: 'Carol Li', color: '#e74c3c', status: 'active', lastActivity: new Date().toISOString() },
31-
];
25+
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
3226

3327
export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailViewProps) {
3428
const { objectName, recordId } = useParams();
@@ -37,14 +31,31 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
3731
const [isLoading, setIsLoading] = useState(true);
3832
const [comments, setComments] = useState<Comment[]>([]);
3933
const [threadResolved, setThreadResolved] = useState(false);
34+
const [recordViewers, setRecordViewers] = useState<PresenceUser[]>([]);
4035
const objectDef = objects.find((o: any) => o.name === objectName);
4136

4237
const currentUser = user
4338
? { id: user.id, name: user.name, avatar: user.image }
44-
: MOCK_USER;
39+
: FALLBACK_USER;
40+
41+
// Fetch presence and comments from API
42+
useEffect(() => {
43+
if (!dataSource || !objectName || !recordId) return;
44+
const threadId = `${objectName}:${recordId}`;
45+
46+
// Fetch record viewers
47+
dataSource.find('sys_presence', { $filter: `recordId eq '${recordId}'` })
48+
.then((res: any) => { if (res.data?.length) setRecordViewers(res.data); })
49+
.catch(() => {});
50+
51+
// Fetch persisted comments
52+
dataSource.find('sys_comment', { $filter: `threadId eq '${threadId}'`, $orderby: 'createdAt asc' })
53+
.then((res: any) => { if (res.data?.length) setComments(res.data); })
54+
.catch(() => {});
55+
}, [dataSource, objectName, recordId]);
4556

4657
const handleAddComment = useCallback(
47-
(content: string, mentions: string[], parentId?: string) => {
58+
async (content: string, mentions: string[], parentId?: string) => {
4859
const newComment: Comment = {
4960
id: crypto.randomUUID(),
5061
author: currentUser,
@@ -54,15 +65,23 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
5465
parentId,
5566
};
5667
setComments(prev => [...prev, newComment]);
68+
// Persist to backend
69+
if (dataSource) {
70+
const threadId = `${objectName}:${recordId}`;
71+
dataSource.create('sys_comment', { ...newComment, threadId }).catch(() => {});
72+
}
5773
},
58-
[currentUser],
74+
[currentUser, dataSource, objectName, recordId],
5975
);
6076

6177
const handleDeleteComment = useCallback(
62-
(commentId: string) => {
78+
async (commentId: string) => {
6379
setComments(prev => prev.filter(c => c.id !== commentId));
80+
if (dataSource) {
81+
dataSource.delete('sys_comment', commentId).catch(() => {});
82+
}
6483
},
65-
[],
84+
[dataSource],
6685
);
6786

6887
const handleReaction = useCallback(
@@ -77,10 +96,15 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
7796
} else {
7897
reactions[emoji] = [...userIds, currentUser.id];
7998
}
80-
return { ...c, reactions };
99+
const updated = { ...c, reactions };
100+
// Persist reaction update to backend
101+
if (dataSource) {
102+
dataSource.update('sys_comment', commentId, { reactions }).catch(() => {});
103+
}
104+
return updated;
81105
}));
82106
},
83-
[currentUser.id],
107+
[currentUser.id, dataSource],
84108
);
85109

86110
useEffect(() => {
@@ -135,10 +159,10 @@ export function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailVi
135159
<div className="h-full bg-background overflow-hidden flex flex-col relative">
136160
<div className="absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2">
137161
{/* Presence: who else is viewing this record */}
138-
{MOCK_RECORD_VIEWERS.length > 0 && (
162+
{recordViewers.length > 0 && (
139163
<div className="flex items-center gap-1.5" title="Users viewing this record">
140164
<Users className="h-3.5 w-3.5 text-muted-foreground" />
141-
<PresenceAvatars users={MOCK_RECORD_VIEWERS} size="sm" maxVisible={4} showStatus />
165+
<PresenceAvatars users={recordViewers} size="sm" maxVisible={4} showStatus />
142166
</div>
143167
)}
144168
<MetadataToggle open={showDebug} onToggle={toggleDebug} />

apps/console/src/components/ReportView.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect } from 'react';
1+
import { useState, useEffect, useMemo } from 'react';
22
import { useParams } from 'react-router-dom';
33
import { ReportViewer, ReportBuilder } from '@object-ui/plugin-report';
44
import { Empty, EmptyTitle, EmptyDescription, Button } from '@object-ui/components';
@@ -7,8 +7,8 @@ import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataI
77
import { useMetadata } from '../context/MetadataProvider';
88
import type { DataSource } from '@object-ui/types';
99

10-
// Mock fields for the builder since we don't have a dynamic schema provider here yet
11-
const MOCK_FIELDS = [
10+
// Fallback fields when no schema is available
11+
const FALLBACK_FIELDS = [
1212
{ name: 'month', label: 'Month', type: 'string' },
1313
{ name: 'revenue', label: 'Revenue', type: 'number' },
1414
{ name: 'count', label: 'Count', type: 'number' },
@@ -25,14 +25,38 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
2525
const [isEditing, setIsEditing] = useState(false);
2626

2727
// Find report definition from API-driven metadata
28-
const { reports, loading } = useMetadata();
28+
const { reports, objects, loading } = useMetadata();
2929
const initialReport = reports?.find((r: any) => r.name === reportName);
3030
const [reportData, setReportData] = useState(initialReport);
3131

3232
// State for report runtime data
3333
const [reportRuntimeData, setReportRuntimeData] = useState<any[]>([]);
3434
const [dataLoading, setDataLoading] = useState(false);
3535

36+
// Derive available fields from object schema when report has objectName/dataSource
37+
const availableFields = useMemo(() => {
38+
const objName = reportData?.objectName || reportData?.dataSource?.object || reportData?.dataSource?.resource;
39+
if (objName && objects?.length) {
40+
const objDef = objects.find((o: any) => o.name === objName);
41+
if (objDef?.fields) {
42+
const fields = objDef.fields;
43+
if (Array.isArray(fields)) {
44+
return fields.map((f: any) =>
45+
typeof f === 'string'
46+
? { name: f, label: f, type: 'text' }
47+
: { name: f.name, label: f.label || f.name, type: f.type || 'text' },
48+
);
49+
}
50+
return Object.entries(fields).map(([name, def]: [string, any]) => ({
51+
name,
52+
label: def.label || name,
53+
type: def.type || 'text',
54+
}));
55+
}
56+
}
57+
return FALLBACK_FIELDS;
58+
}, [reportData, objects]);
59+
3660
// Sync reportData when metadata finishes loading or reportName changes
3761
useEffect(() => {
3862
setReportData(initialReport);
@@ -165,7 +189,7 @@ export function ReportView({ dataSource }: { dataSource?: DataSource }) {
165189
schema={{
166190
title: 'Report Builder',
167191
report: reportData,
168-
availableFields: MOCK_FIELDS,
192+
availableFields: availableFields,
169193
onSave: handleSave,
170194
onCancel: () => setIsEditing(false)
171195
}}

apps/console/src/components/ViewDesignerPage.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import { useParams, useNavigate } from 'react-router-dom';
1414
import { ViewDesigner } from '@object-ui/plugin-designer';
1515
import type { ViewDesignerConfig } from '@object-ui/plugin-designer';
1616
import { toast } from 'sonner';
17+
import { useAdapter } from '../context/AdapterProvider';
1718

1819
export function ViewDesignerPage({ objects }: { objects: any[] }) {
1920
const navigate = useNavigate();
2021
const { objectName, viewId } = useParams();
22+
const dataSource = useAdapter();
2123

2224
const objectDef = objects.find((o: any) => o.name === objectName);
2325

@@ -48,18 +50,27 @@ export function ViewDesignerPage({ objects }: { objects: any[] }) {
4850
}, [viewId, objectDef]);
4951

5052
const handleSave = useCallback(
51-
(config: ViewDesignerConfig) => {
52-
// In a real implementation this would persist the view config.
53-
// For now, log and show toast.
54-
console.log('[ViewDesigner] Save view config:', config);
55-
toast.success(
56-
existingView
57-
? `View "${config.viewLabel}" updated`
58-
: `View "${config.viewLabel}" created`,
59-
);
53+
async (config: ViewDesignerConfig) => {
54+
try {
55+
if (dataSource) {
56+
const payload = { objectName: objectDef?.name, viewId: viewId === 'new' ? undefined : viewId, ...config };
57+
if (existingView) {
58+
await dataSource.update('sys_view', viewId!, payload);
59+
} else {
60+
await dataSource.create('sys_view', payload);
61+
}
62+
}
63+
toast.success(
64+
existingView
65+
? `View "${config.viewLabel}" updated`
66+
: `View "${config.viewLabel}" created`,
67+
);
68+
} catch {
69+
toast.error('Failed to save view configuration');
70+
}
6071
navigate(-1);
6172
},
62-
[existingView, navigate],
73+
[existingView, navigate, dataSource, objectDef, viewId],
6374
);
6475

6576
const handleCancel = useCallback(() => {

apps/console/src/pages/system/AuditLogPage.tsx

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,40 @@
22
* Audit Log Page
33
*
44
* Read-only grid displaying system audit logs.
5-
* Shows user actions, resources, timestamps, and details.
5+
* Fetches data via dataSource.find('sys_audit_log').
66
*/
77

8+
import { useState, useEffect, useCallback } from 'react';
89
import { Card, CardContent, Badge } from '@object-ui/components';
9-
import { ScrollText } from 'lucide-react';
10+
import { ScrollText, Loader2 } from 'lucide-react';
11+
import { toast } from 'sonner';
12+
import { useAdapter } from '../../context/AdapterProvider';
1013
import { systemObjects } from './systemObjects';
1114

1215
const auditObject = systemObjects.find((o) => o.name === 'sys_audit_log')!;
16+
const columns = auditObject.views[0].columns;
1317

1418
export function AuditLogPage() {
19+
const dataSource = useAdapter();
20+
21+
const [records, setRecords] = useState<any[]>([]);
22+
const [loading, setLoading] = useState(true);
23+
24+
const fetchData = useCallback(async () => {
25+
if (!dataSource) return;
26+
setLoading(true);
27+
try {
28+
const result = await dataSource.find('sys_audit_log', { $orderby: 'createdAt desc' });
29+
setRecords(result.data || []);
30+
} catch {
31+
toast.error('Failed to load audit logs');
32+
} finally {
33+
setLoading(false);
34+
}
35+
}, [dataSource]);
36+
37+
useEffect(() => { fetchData(); }, [fetchData]);
38+
1539
return (
1640
<div className="flex flex-col gap-4 sm:gap-6 p-4 sm:p-6">
1741
<div className="flex items-center gap-3 min-w-0">
@@ -30,7 +54,7 @@ export function AuditLogPage() {
3054
<table className="w-full text-sm">
3155
<thead>
3256
<tr className="border-b bg-muted/50">
33-
{auditObject.views[0].columns.map((col) => {
57+
{columns.map((col) => {
3458
const field = auditObject.fields.find((f) => f.name === col);
3559
return (
3660
<th key={col} className="h-10 px-3 sm:px-4 text-left font-medium text-muted-foreground whitespace-nowrap">
@@ -41,15 +65,33 @@ export function AuditLogPage() {
4165
</tr>
4266
</thead>
4367
<tbody>
44-
<tr>
45-
<td className="p-4 sm:p-6 text-center text-sm text-muted-foreground" colSpan={auditObject.views[0].columns.length}>
46-
<div className="flex flex-col items-center gap-2 py-4">
47-
<ScrollText className="h-8 w-8 text-muted-foreground/50" />
48-
<p>Connect to ObjectStack server to load audit logs.</p>
49-
<Badge variant="secondary" className="text-xs">Read-only</Badge>
50-
</div>
51-
</td>
52-
</tr>
68+
{loading ? (
69+
<tr>
70+
<td className="p-6 text-center" colSpan={columns.length}>
71+
<Loader2 className="h-6 w-6 animate-spin mx-auto text-muted-foreground" />
72+
</td>
73+
</tr>
74+
) : records.length === 0 ? (
75+
<tr>
76+
<td className="p-4 sm:p-6 text-center text-sm text-muted-foreground" colSpan={columns.length}>
77+
<div className="flex flex-col items-center gap-2 py-4">
78+
<ScrollText className="h-8 w-8 text-muted-foreground/50" />
79+
<p>No audit logs found.</p>
80+
<Badge variant="secondary" className="text-xs">Read-only</Badge>
81+
</div>
82+
</td>
83+
</tr>
84+
) : (
85+
records.map((record) => (
86+
<tr key={record.id || record._id} className="border-b hover:bg-muted/50 transition-colors">
87+
{columns.map((col) => (
88+
<td key={col} className="h-10 px-3 sm:px-4 whitespace-nowrap">
89+
{String(record[col] ?? '')}
90+
</td>
91+
))}
92+
</tr>
93+
))
94+
)}
5395
</tbody>
5496
</table>
5597
</div>

0 commit comments

Comments
 (0)