@object-ui/app-shell 3.3.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +22 -2
- package/dist/chrome/CommandPalette.d.ts +16 -0
- package/dist/chrome/CommandPalette.js +88 -0
- package/dist/chrome/ConditionalAuthWrapper.d.ts +23 -0
- package/dist/chrome/ConditionalAuthWrapper.js +135 -0
- package/dist/chrome/ConsoleToaster.d.ts +11 -0
- package/dist/chrome/ConsoleToaster.js +28 -0
- package/dist/chrome/ErrorBoundary.d.ts +39 -0
- package/dist/chrome/ErrorBoundary.js +64 -0
- package/dist/chrome/KeyboardShortcutsDialog.d.ts +7 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +63 -0
- package/dist/chrome/LoadingScreen.d.ts +12 -0
- package/dist/chrome/LoadingScreen.js +44 -0
- package/dist/chrome/OnboardingWalkthrough.d.ts +9 -0
- package/dist/chrome/OnboardingWalkthrough.js +11 -0
- package/dist/chrome/ThemeProvider.d.ts +13 -0
- package/dist/chrome/ThemeProvider.js +37 -0
- package/dist/chrome/index.d.ts +8 -0
- package/dist/chrome/index.js +8 -0
- package/dist/console/AppContent.d.ts +26 -0
- package/dist/console/AppContent.js +266 -0
- package/dist/console/ConsoleShell.d.ts +66 -0
- package/dist/console/ConsoleShell.js +97 -0
- package/dist/console/auth/AuthPageLayout.d.ts +9 -0
- package/dist/console/auth/AuthPageLayout.js +4 -0
- package/dist/console/auth/ForgotPasswordPage.d.ts +4 -0
- package/dist/console/auth/ForgotPasswordPage.js +23 -0
- package/dist/console/auth/LoginPage.d.ts +4 -0
- package/dist/console/auth/LoginPage.js +24 -0
- package/dist/console/auth/RegisterPage.d.ts +4 -0
- package/dist/console/auth/RegisterPage.js +29 -0
- package/dist/console/auth/index.d.ts +4 -0
- package/dist/console/auth/index.js +4 -0
- package/dist/console/home/AppCard.d.ts +14 -0
- package/dist/console/home/AppCard.js +36 -0
- package/dist/console/home/HomeLayout.d.ts +15 -0
- package/dist/console/home/HomeLayout.js +20 -0
- package/dist/console/home/HomePage.d.ts +18 -0
- package/dist/console/home/HomePage.js +54 -0
- package/dist/console/home/QuickActions.d.ts +9 -0
- package/dist/console/home/QuickActions.js +53 -0
- package/dist/console/home/RecentApps.d.ts +13 -0
- package/dist/console/home/RecentApps.js +32 -0
- package/dist/console/home/StarredApps.d.ts +13 -0
- package/dist/console/home/StarredApps.js +29 -0
- package/dist/console/home/index.d.ts +11 -0
- package/dist/console/home/index.js +11 -0
- package/dist/console/organizations/CreateWorkspaceDialog.d.ts +16 -0
- package/dist/console/organizations/CreateWorkspaceDialog.js +71 -0
- package/dist/console/organizations/OrganizationsLayout.d.ts +15 -0
- package/dist/console/organizations/OrganizationsLayout.js +20 -0
- package/dist/console/organizations/OrganizationsPage.d.ts +9 -0
- package/dist/console/organizations/OrganizationsPage.js +76 -0
- package/dist/console/organizations/index.d.ts +8 -0
- package/dist/console/organizations/index.js +8 -0
- package/dist/console/organizations/manage/AcceptInvitationPage.d.ts +7 -0
- package/dist/console/organizations/manage/AcceptInvitationPage.js +100 -0
- package/dist/console/organizations/manage/InvitationsPage.d.ts +6 -0
- package/dist/console/organizations/manage/InvitationsPage.js +107 -0
- package/dist/console/organizations/manage/InviteMemberDialog.d.ts +16 -0
- package/dist/console/organizations/manage/InviteMemberDialog.js +83 -0
- package/dist/console/organizations/manage/MembersPage.d.ts +6 -0
- package/dist/console/organizations/manage/MembersPage.js +89 -0
- package/dist/console/organizations/manage/OrganizationLayout.d.ts +8 -0
- package/dist/console/organizations/manage/OrganizationLayout.js +51 -0
- package/dist/console/organizations/manage/SettingsPage.d.ts +6 -0
- package/dist/console/organizations/manage/SettingsPage.js +141 -0
- package/dist/console/organizations/manage/orgContext.d.ts +5 -0
- package/dist/console/organizations/manage/orgContext.js +4 -0
- package/dist/context/FavoritesProvider.d.ts +50 -0
- package/dist/context/FavoritesProvider.js +129 -0
- package/dist/context/NavigationContext.d.ts +46 -0
- package/dist/context/NavigationContext.js +59 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.js +2 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/useFavorites.d.ts +13 -0
- package/dist/hooks/useFavorites.js +13 -0
- package/dist/hooks/useMetadataService.d.ts +13 -0
- package/dist/hooks/useMetadataService.js +18 -0
- package/dist/hooks/useNavPins.d.ts +16 -0
- package/dist/hooks/useNavPins.js +72 -0
- package/dist/hooks/useNavigationSync.d.ts +82 -0
- package/dist/hooks/useNavigationSync.js +495 -0
- package/dist/hooks/useObjectActions.d.ts +38 -0
- package/dist/hooks/useObjectActions.js +92 -0
- package/dist/hooks/useRecentItems.d.ts +21 -0
- package/dist/hooks/useRecentItems.js +50 -0
- package/dist/hooks/useResponsiveSidebar.d.ts +8 -0
- package/dist/hooks/useResponsiveSidebar.js +28 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +35 -0
- package/dist/layout/ActivityFeed.d.ts +22 -0
- package/dist/layout/ActivityFeed.js +57 -0
- package/dist/layout/AppHeader.d.ts +33 -0
- package/dist/layout/AppHeader.js +182 -0
- package/dist/layout/AppSidebar.d.ts +14 -0
- package/dist/layout/AppSidebar.js +217 -0
- package/dist/layout/AppSwitcher.d.ts +12 -0
- package/dist/layout/AppSwitcher.js +39 -0
- package/dist/layout/AuthPageLayout.d.ts +9 -0
- package/dist/layout/AuthPageLayout.js +4 -0
- package/dist/layout/ConnectionStatus.d.ts +14 -0
- package/dist/layout/ConnectionStatus.js +36 -0
- package/dist/layout/ConsoleLayout.d.ts +21 -0
- package/dist/layout/ConsoleLayout.js +71 -0
- package/dist/layout/LocaleSwitcher.d.ts +7 -0
- package/dist/layout/LocaleSwitcher.js +27 -0
- package/dist/layout/ModeToggle.d.ts +1 -0
- package/dist/layout/ModeToggle.js +10 -0
- package/dist/layout/UnifiedSidebar.d.ts +22 -0
- package/dist/layout/UnifiedSidebar.js +210 -0
- package/dist/layout/index.d.ts +11 -0
- package/dist/layout/index.js +10 -0
- package/dist/providers/AdapterProvider.d.ts +21 -0
- package/dist/providers/AdapterProvider.js +52 -0
- package/dist/providers/ExpressionProvider.d.ts +50 -0
- package/dist/providers/ExpressionProvider.js +67 -0
- package/dist/providers/MetadataProvider.d.ts +18 -0
- package/dist/providers/MetadataProvider.js +324 -0
- package/dist/services/MetadataService.d.ts +119 -0
- package/dist/services/MetadataService.js +211 -0
- package/dist/skeletons/SkeletonDashboard.d.ts +13 -0
- package/dist/skeletons/SkeletonDashboard.js +12 -0
- package/dist/skeletons/SkeletonDetail.d.ts +15 -0
- package/dist/skeletons/SkeletonDetail.js +12 -0
- package/dist/skeletons/SkeletonGrid.d.ts +15 -0
- package/dist/skeletons/SkeletonGrid.js +12 -0
- package/dist/skeletons/index.d.ts +3 -0
- package/dist/skeletons/index.js +3 -0
- package/dist/utils/getIcon.d.ts +10 -0
- package/dist/utils/getIcon.js +24 -0
- package/dist/utils/index.d.ts +33 -0
- package/dist/utils/index.js +62 -0
- package/dist/utils/metadataConverters.d.ts +78 -0
- package/dist/utils/metadataConverters.js +74 -0
- package/dist/views/ActionConfirmDialog.d.ts +22 -0
- package/dist/views/ActionConfirmDialog.js +22 -0
- package/dist/views/ActionParamDialog.d.ts +23 -0
- package/dist/views/ActionParamDialog.js +67 -0
- package/dist/views/DashboardView.d.ts +9 -0
- package/dist/views/DashboardView.js +357 -0
- package/dist/views/DesignDrawer.d.ts +28 -0
- package/dist/views/DesignDrawer.js +51 -0
- package/dist/views/MetadataInspector.d.ts +40 -0
- package/dist/views/MetadataInspector.js +69 -0
- package/dist/views/ObjectView.d.ts +14 -0
- package/dist/views/ObjectView.js +770 -0
- package/dist/views/PageView.d.ts +6 -0
- package/dist/views/PageView.js +49 -0
- package/dist/views/RecordDetailView.d.ts +14 -0
- package/dist/views/RecordDetailView.js +508 -0
- package/dist/views/ReportView.d.ts +4 -0
- package/dist/views/ReportView.js +284 -0
- package/dist/views/SearchResultsPage.d.ts +9 -0
- package/dist/views/SearchResultsPage.js +107 -0
- package/dist/views/ViewConfigPanel.d.ts +56 -0
- package/dist/views/ViewConfigPanel.js +91 -0
- package/dist/views/index.d.ts +11 -0
- package/dist/views/index.js +11 -0
- package/package.json +59 -11
- package/.turbo/turbo-build.log +0 -4
- package/src/components/AppShell.tsx +0 -31
- package/src/components/DashboardRenderer.tsx +0 -36
- package/src/components/FormRenderer.tsx +0 -71
- package/src/components/ObjectRenderer.tsx +0 -122
- package/src/components/PageRenderer.tsx +0 -28
- package/src/index.ts +0 -20
- package/src/types.ts +0 -78
- package/tsconfig.json +0 -20
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Console ObjectView
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper around the plugin-view ObjectView that adds:
|
|
6
|
+
* - Multi-view resolution from objectDef.list_views
|
|
7
|
+
* - MetadataInspector toggle
|
|
8
|
+
* - Drawer for record detail preview
|
|
9
|
+
* - useObjectActions for toolbar create button
|
|
10
|
+
* - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
|
|
11
|
+
*/
|
|
12
|
+
import { useMemo, useState, useCallback, useEffect } from 'react';
|
|
13
|
+
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
|
14
|
+
import { ObjectChart } from '@object-ui/plugin-charts';
|
|
15
|
+
import { ListView } from '@object-ui/plugin-list';
|
|
16
|
+
import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
|
|
17
|
+
import { ObjectView as PluginObjectView, ViewTabBar } from '@object-ui/plugin-view';
|
|
18
|
+
// Import plugins for side-effects (registration)
|
|
19
|
+
import '@object-ui/plugin-grid';
|
|
20
|
+
import '@object-ui/plugin-kanban';
|
|
21
|
+
import '@object-ui/plugin-calendar';
|
|
22
|
+
import { Button, Empty, EmptyTitle, EmptyDescription, NavigationOverlay, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from '@object-ui/components';
|
|
23
|
+
import { Plus, Table as TableIcon, Settings2, Wrench, KanbanSquare, Calendar, LayoutGrid, Activity, GanttChart, MapPin, BarChart3 } from 'lucide-react';
|
|
24
|
+
import { getIcon } from '../utils/getIcon';
|
|
25
|
+
import { MetadataToggle, MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
26
|
+
import { ViewConfigPanel } from './ViewConfigPanel';
|
|
27
|
+
import { useObjectActions } from '../hooks/useObjectActions';
|
|
28
|
+
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
|
|
29
|
+
import { usePermissions } from '@object-ui/permissions';
|
|
30
|
+
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
31
|
+
import { useRealtimeSubscription, useConflictResolution } from '@object-ui/collaboration';
|
|
32
|
+
import { ActionProvider, useNavigationOverlay, SchemaRenderer } from '@object-ui/react';
|
|
33
|
+
import { toast } from 'sonner';
|
|
34
|
+
import { ActionConfirmDialog } from './ActionConfirmDialog';
|
|
35
|
+
import { ActionParamDialog } from './ActionParamDialog';
|
|
36
|
+
/** Map view types to Lucide icons (Airtable-style) */
|
|
37
|
+
const VIEW_TYPE_ICONS = {
|
|
38
|
+
grid: TableIcon,
|
|
39
|
+
kanban: KanbanSquare,
|
|
40
|
+
calendar: Calendar,
|
|
41
|
+
gallery: LayoutGrid,
|
|
42
|
+
timeline: Activity,
|
|
43
|
+
gantt: GanttChart,
|
|
44
|
+
map: MapPin,
|
|
45
|
+
chart: BarChart3,
|
|
46
|
+
};
|
|
47
|
+
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
48
|
+
/**
|
|
49
|
+
* DrawerDetailContent — extracted component for NavigationOverlay content.
|
|
50
|
+
* Needs to be a proper component (not a render prop) so it can use hooks
|
|
51
|
+
* for data fetching, comment handling, etc.
|
|
52
|
+
*/
|
|
53
|
+
function DrawerDetailContent({ objectDef, recordId, dataSource, onEdit }) {
|
|
54
|
+
const { user } = useAuth();
|
|
55
|
+
const currentUser = user
|
|
56
|
+
? { id: user.id, name: user.name, avatar: user.image }
|
|
57
|
+
: FALLBACK_USER;
|
|
58
|
+
const [feedItems, setFeedItems] = useState([]);
|
|
59
|
+
// Fetch persisted comments from API
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!dataSource || !objectDef?.name || !recordId)
|
|
62
|
+
return;
|
|
63
|
+
const threadId = `${objectDef.name}:${recordId}`;
|
|
64
|
+
dataSource.find('sys_comment', { $filter: { threadId }, $orderby: { createdAt: 'asc' } })
|
|
65
|
+
.then((res) => {
|
|
66
|
+
if (res.data?.length) {
|
|
67
|
+
setFeedItems(res.data.map((c) => ({
|
|
68
|
+
id: c.id,
|
|
69
|
+
type: 'comment',
|
|
70
|
+
actor: c.author?.name ?? 'Unknown',
|
|
71
|
+
actorAvatarUrl: c.author?.avatar,
|
|
72
|
+
body: c.content,
|
|
73
|
+
createdAt: c.createdAt,
|
|
74
|
+
updatedAt: c.updatedAt,
|
|
75
|
+
parentId: c.parentId,
|
|
76
|
+
reactions: c.reactions
|
|
77
|
+
? Object.entries(c.reactions).map(([emoji, userIds]) => ({
|
|
78
|
+
emoji,
|
|
79
|
+
count: userIds.length,
|
|
80
|
+
reacted: userIds.includes(currentUser.id),
|
|
81
|
+
}))
|
|
82
|
+
: undefined,
|
|
83
|
+
})));
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
.catch(() => { });
|
|
87
|
+
}, [dataSource, objectDef?.name, recordId, currentUser.id]);
|
|
88
|
+
const handleAddComment = useCallback(async (text) => {
|
|
89
|
+
const newItem = {
|
|
90
|
+
id: crypto.randomUUID(),
|
|
91
|
+
type: 'comment',
|
|
92
|
+
actor: currentUser.name,
|
|
93
|
+
actorAvatarUrl: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
94
|
+
body: text,
|
|
95
|
+
createdAt: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
setFeedItems(prev => [...prev, newItem]);
|
|
98
|
+
if (dataSource) {
|
|
99
|
+
const threadId = `${objectDef.name}:${recordId}`;
|
|
100
|
+
dataSource.create('sys_comment', {
|
|
101
|
+
id: newItem.id,
|
|
102
|
+
threadId,
|
|
103
|
+
author: currentUser,
|
|
104
|
+
content: text,
|
|
105
|
+
mentions: [],
|
|
106
|
+
createdAt: newItem.createdAt,
|
|
107
|
+
}).catch(() => { });
|
|
108
|
+
}
|
|
109
|
+
}, [currentUser, dataSource, objectDef?.name, recordId]);
|
|
110
|
+
const handleAddReply = useCallback(async (parentId, text) => {
|
|
111
|
+
const newItem = {
|
|
112
|
+
id: crypto.randomUUID(),
|
|
113
|
+
type: 'comment',
|
|
114
|
+
actor: currentUser.name,
|
|
115
|
+
actorAvatarUrl: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
116
|
+
body: text,
|
|
117
|
+
createdAt: new Date().toISOString(),
|
|
118
|
+
parentId,
|
|
119
|
+
};
|
|
120
|
+
setFeedItems(prev => {
|
|
121
|
+
const updated = [...prev, newItem];
|
|
122
|
+
return updated.map(item => item.id === parentId
|
|
123
|
+
? { ...item, replyCount: (item.replyCount ?? 0) + 1 }
|
|
124
|
+
: item);
|
|
125
|
+
});
|
|
126
|
+
if (dataSource) {
|
|
127
|
+
const threadId = `${objectDef.name}:${recordId}`;
|
|
128
|
+
dataSource.create('sys_comment', {
|
|
129
|
+
id: newItem.id,
|
|
130
|
+
threadId,
|
|
131
|
+
author: currentUser,
|
|
132
|
+
content: text,
|
|
133
|
+
mentions: [],
|
|
134
|
+
createdAt: newItem.createdAt,
|
|
135
|
+
parentId,
|
|
136
|
+
}).catch(() => { });
|
|
137
|
+
}
|
|
138
|
+
}, [currentUser, dataSource, objectDef?.name, recordId]);
|
|
139
|
+
const handleToggleReaction = useCallback((itemId, emoji) => {
|
|
140
|
+
setFeedItems(prev => prev.map(item => {
|
|
141
|
+
if (item.id !== itemId)
|
|
142
|
+
return item;
|
|
143
|
+
const reactions = [...(item.reactions ?? [])];
|
|
144
|
+
const idx = reactions.findIndex(r => r.emoji === emoji);
|
|
145
|
+
if (idx >= 0) {
|
|
146
|
+
const r = reactions[idx];
|
|
147
|
+
if (r.reacted) {
|
|
148
|
+
if (r.count <= 1) {
|
|
149
|
+
reactions.splice(idx, 1);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
reactions[idx] = { ...r, count: r.count - 1, reacted: false };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
reactions[idx] = { ...r, count: r.count + 1, reacted: true };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
reactions.push({ emoji, count: 1, reacted: true });
|
|
161
|
+
}
|
|
162
|
+
const updated = { ...item, reactions };
|
|
163
|
+
if (dataSource) {
|
|
164
|
+
dataSource.update('sys_comment', String(itemId), {
|
|
165
|
+
$toggleReaction: { emoji, userId: currentUser.id },
|
|
166
|
+
}).catch(() => { });
|
|
167
|
+
}
|
|
168
|
+
return updated;
|
|
169
|
+
}));
|
|
170
|
+
}, [currentUser.id, dataSource]);
|
|
171
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-auto p-3 sm:p-4 lg:p-6", children: [_jsx(DetailView, { schema: {
|
|
172
|
+
type: 'detail-view',
|
|
173
|
+
objectName: objectDef.name,
|
|
174
|
+
resourceId: recordId,
|
|
175
|
+
showBack: false,
|
|
176
|
+
showEdit: true,
|
|
177
|
+
title: objectDef.label,
|
|
178
|
+
sections: [
|
|
179
|
+
{
|
|
180
|
+
title: 'Details',
|
|
181
|
+
fields: Object.keys(objectDef.fields || {}).map((key) => ({
|
|
182
|
+
name: key,
|
|
183
|
+
label: objectDef.fields[key].label || key,
|
|
184
|
+
type: objectDef.fields[key].type || 'text'
|
|
185
|
+
})),
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}, dataSource: dataSource, onEdit: () => onEdit({ id: recordId }) }), _jsx("div", { className: "mt-6 border-t pt-6", children: _jsx(RecordChatterPanel, { config: {
|
|
189
|
+
position: 'bottom',
|
|
190
|
+
collapsible: true,
|
|
191
|
+
defaultCollapsed: true,
|
|
192
|
+
feed: {
|
|
193
|
+
enableReactions: true,
|
|
194
|
+
enableThreading: true,
|
|
195
|
+
showCommentInput: true,
|
|
196
|
+
},
|
|
197
|
+
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) })] }));
|
|
198
|
+
}
|
|
199
|
+
export function ObjectView({ dataSource, objects, onEdit }) {
|
|
200
|
+
const navigate = useNavigate();
|
|
201
|
+
const { objectName, viewId } = useParams();
|
|
202
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
203
|
+
const { showDebug, toggleDebug } = useMetadataInspector();
|
|
204
|
+
const { t } = useObjectTranslation();
|
|
205
|
+
const { objectLabel, objectDescription: objectDesc } = useObjectLabel();
|
|
206
|
+
// Inline view config panel state (Airtable-style right sidebar)
|
|
207
|
+
const [showViewConfigPanel, setShowViewConfigPanel] = useState(false);
|
|
208
|
+
const [viewConfigPanelMode, setViewConfigPanelMode] = useState('edit');
|
|
209
|
+
// Draft state for view config edits — cached locally, saved on demand
|
|
210
|
+
const [viewDraft, setViewDraft] = useState(null);
|
|
211
|
+
const handleViewConfigSave = useCallback((draft) => {
|
|
212
|
+
setViewDraft(draft);
|
|
213
|
+
setRefreshKey(k => k + 1);
|
|
214
|
+
// Persist to backend if dataSource supports it
|
|
215
|
+
if (dataSource?.updateViewConfig) {
|
|
216
|
+
const objName = objectName;
|
|
217
|
+
const vid = draft.id;
|
|
218
|
+
if (objName && vid) {
|
|
219
|
+
dataSource.updateViewConfig(objName, vid, draft).catch((err) => {
|
|
220
|
+
console.error('[ViewConfigPanel] Failed to persist view config:', err);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.warn('[ViewConfigPanel] Cannot persist view config: missing objectName or viewId.');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.warn('[ViewConfigPanel] dataSource.updateViewConfig is not available. View config saved locally only.');
|
|
229
|
+
}
|
|
230
|
+
}, [dataSource, objectName]);
|
|
231
|
+
/** Create a new view via the config panel */
|
|
232
|
+
const handleViewCreate = useCallback(async (config) => {
|
|
233
|
+
try {
|
|
234
|
+
if (dataSource?.create) {
|
|
235
|
+
const payload = { objectName, ...config };
|
|
236
|
+
await dataSource.create('sys_view', payload);
|
|
237
|
+
}
|
|
238
|
+
setShowViewConfigPanel(false);
|
|
239
|
+
setViewConfigPanelMode('edit');
|
|
240
|
+
setRefreshKey(k => k + 1);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
console.error('[ViewConfigPanel] Failed to create view:', err);
|
|
244
|
+
}
|
|
245
|
+
}, [dataSource, objectName]);
|
|
246
|
+
// Record count tracking for footer
|
|
247
|
+
const [recordCount, setRecordCount] = useState(undefined);
|
|
248
|
+
// Admin users automatically get design tools (no toggle needed)
|
|
249
|
+
const { user } = useAuth();
|
|
250
|
+
const isAdmin = user?.role === 'admin';
|
|
251
|
+
const { can } = usePermissions();
|
|
252
|
+
// Get Object Definition
|
|
253
|
+
const objectDef = objects.find((o) => o.name === objectName);
|
|
254
|
+
if (!objectDef) {
|
|
255
|
+
return (_jsx("div", { className: "h-full p-4 flex items-center justify-center", children: _jsxs(Empty, { children: [_jsx("div", { className: "mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted", children: _jsx(TableIcon, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('console.objectView.objectNotFound') }), _jsxs(EmptyDescription, { children: [t('console.objectView.objectNotFoundDescription', { objectName }), ' ', t('console.objectView.objectNotFoundHint')] })] }) }));
|
|
256
|
+
}
|
|
257
|
+
// Resolve Views from objectDef.listViews (camelCase per @objectstack/spec)
|
|
258
|
+
const views = useMemo(() => {
|
|
259
|
+
const definedViews = objectDef.listViews || objectDef.list_views || {};
|
|
260
|
+
const viewList = Object.entries(definedViews).map(([key, value]) => ({
|
|
261
|
+
id: key,
|
|
262
|
+
...value,
|
|
263
|
+
type: value.type || 'grid'
|
|
264
|
+
}));
|
|
265
|
+
if (viewList.length === 0) {
|
|
266
|
+
// Default column resolution priority:
|
|
267
|
+
// 1. `compactLayout` (curated primary business fields).
|
|
268
|
+
// 2. Business fields only — exclude system-managed identifiers/audit
|
|
269
|
+
// columns (id, created_at, updated_at, …) and fields explicitly
|
|
270
|
+
// marked hidden/readonly on the schema. First 5 kept for compactness.
|
|
271
|
+
const SYSTEM_FIELDS = new Set([
|
|
272
|
+
'id', 'created_at', 'createdAt', 'updated_at', 'updatedAt',
|
|
273
|
+
'deleted_at', 'deletedAt', 'created_by', 'createdBy',
|
|
274
|
+
'updated_by', 'updatedBy', '_version', '_rev',
|
|
275
|
+
]);
|
|
276
|
+
let defaultColumns = [];
|
|
277
|
+
if (Array.isArray(objectDef.compactLayout) && objectDef.compactLayout.length > 0) {
|
|
278
|
+
defaultColumns = objectDef.compactLayout.filter((n) => objectDef.fields?.[n]);
|
|
279
|
+
}
|
|
280
|
+
else if (objectDef.fields) {
|
|
281
|
+
defaultColumns = Object.entries(objectDef.fields)
|
|
282
|
+
.filter(([name, f]) => {
|
|
283
|
+
if (!f)
|
|
284
|
+
return false;
|
|
285
|
+
if (f.hidden)
|
|
286
|
+
return false;
|
|
287
|
+
if (SYSTEM_FIELDS.has(name))
|
|
288
|
+
return false;
|
|
289
|
+
return true;
|
|
290
|
+
})
|
|
291
|
+
.map(([name]) => name)
|
|
292
|
+
.slice(0, 5);
|
|
293
|
+
}
|
|
294
|
+
viewList.push({
|
|
295
|
+
id: 'all',
|
|
296
|
+
label: t('console.objectView.allRecords'),
|
|
297
|
+
type: 'grid',
|
|
298
|
+
columns: defaultColumns,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return viewList;
|
|
302
|
+
}, [objectDef]);
|
|
303
|
+
// Active View State — merge saved draft if available for this view
|
|
304
|
+
const activeViewId = viewId || searchParams.get('view') || views[0]?.id;
|
|
305
|
+
const baseView = views.find((v) => v.id === activeViewId) || views[0];
|
|
306
|
+
const activeView = viewDraft && viewDraft.id === baseView?.id
|
|
307
|
+
? { ...baseView, ...viewDraft }
|
|
308
|
+
: baseView;
|
|
309
|
+
/** Real-time draft field update — propagates each toggle/input change immediately */
|
|
310
|
+
const handleViewUpdate = useCallback((field, value) => {
|
|
311
|
+
setViewDraft(prev => ({
|
|
312
|
+
...(prev || {}),
|
|
313
|
+
id: baseView?.id,
|
|
314
|
+
[field]: value,
|
|
315
|
+
}));
|
|
316
|
+
}, [baseView?.id]);
|
|
317
|
+
const handleViewChange = (newViewId) => {
|
|
318
|
+
// The plugin ObjectView returns the view ID directly via onViewChange
|
|
319
|
+
const matchedView = views.find((v) => v.id === newViewId);
|
|
320
|
+
if (!matchedView)
|
|
321
|
+
return;
|
|
322
|
+
// Auto-close the config panel when switching views
|
|
323
|
+
setShowViewConfigPanel(false);
|
|
324
|
+
if (viewId) {
|
|
325
|
+
navigate(`../${matchedView.id}`, { relative: "path" });
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
navigate(`view/${matchedView.id}`);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
// ViewSwitcher callbacks — wired to both PluginObjectView instances
|
|
332
|
+
const handleCreateView = useCallback(() => {
|
|
333
|
+
setViewConfigPanelMode('create');
|
|
334
|
+
setShowViewConfigPanel(true);
|
|
335
|
+
}, []);
|
|
336
|
+
const handleViewAction = useCallback((actionType, viewType) => {
|
|
337
|
+
if (actionType === 'settings') {
|
|
338
|
+
const matchedView = views.find((v) => v.type === viewType);
|
|
339
|
+
if (matchedView)
|
|
340
|
+
handleViewChange(matchedView.id);
|
|
341
|
+
setViewConfigPanelMode('edit');
|
|
342
|
+
setShowViewConfigPanel(true);
|
|
343
|
+
}
|
|
344
|
+
}, [views, handleViewChange]);
|
|
345
|
+
// Action system for toolbar operations
|
|
346
|
+
const [refreshKey, setRefreshKey] = useState(0);
|
|
347
|
+
const actions = useObjectActions({
|
|
348
|
+
objectName: objectDef.name,
|
|
349
|
+
objectLabel: objectDef.label,
|
|
350
|
+
dataSource,
|
|
351
|
+
onEdit,
|
|
352
|
+
onRefresh: () => setRefreshKey(k => k + 1),
|
|
353
|
+
});
|
|
354
|
+
// ─── ActionProvider handlers for schema-driven toolbar actions ──────
|
|
355
|
+
const currentUser = user
|
|
356
|
+
? { id: user.id, name: user.name, avatar: user.image }
|
|
357
|
+
: FALLBACK_USER;
|
|
358
|
+
const [confirmState, setConfirmState] = useState({ open: false, message: '' });
|
|
359
|
+
const [paramState, setParamState] = useState({ open: false, params: [] });
|
|
360
|
+
const confirmHandler = useCallback((message, options) => {
|
|
361
|
+
return new Promise((resolve) => {
|
|
362
|
+
setConfirmState({ open: true, message, options, resolve });
|
|
363
|
+
});
|
|
364
|
+
}, []);
|
|
365
|
+
const paramCollectionHandler = useCallback((params) => {
|
|
366
|
+
return new Promise((resolve) => {
|
|
367
|
+
setParamState({ open: true, params, resolve });
|
|
368
|
+
});
|
|
369
|
+
}, []);
|
|
370
|
+
const toastHandler = useCallback((message, options) => {
|
|
371
|
+
if (options?.type === 'error')
|
|
372
|
+
toast.error(message);
|
|
373
|
+
else
|
|
374
|
+
toast.success(message);
|
|
375
|
+
}, []);
|
|
376
|
+
const navigateHandler = useCallback((url, options) => {
|
|
377
|
+
if (options?.external || options?.newTab) {
|
|
378
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
navigate(url);
|
|
382
|
+
}
|
|
383
|
+
}, [navigate]);
|
|
384
|
+
const apiHandler = useCallback(async (action) => {
|
|
385
|
+
try {
|
|
386
|
+
const target = action.target || action.name;
|
|
387
|
+
const params = action.params || {};
|
|
388
|
+
// Generic list-level API handler: update/execute via dataSource
|
|
389
|
+
if (typeof dataSource.execute === 'function') {
|
|
390
|
+
await dataSource.execute(objectDef.name, target, params);
|
|
391
|
+
}
|
|
392
|
+
else if (params.recordId && Object.keys(params).length > 1 && typeof dataSource.update === 'function') {
|
|
393
|
+
await dataSource.update(objectDef.name, params.recordId, params);
|
|
394
|
+
}
|
|
395
|
+
const shouldRefresh = action.refreshAfter !== false;
|
|
396
|
+
if (shouldRefresh) {
|
|
397
|
+
setRefreshKey(k => k + 1);
|
|
398
|
+
}
|
|
399
|
+
return { success: true, reload: shouldRefresh };
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
return { success: false, error: error.message };
|
|
403
|
+
}
|
|
404
|
+
}, [dataSource, objectDef.name]);
|
|
405
|
+
// Authenticated fetch for direct backend calls (e.g. flow trigger).
|
|
406
|
+
const authFetch = useMemo(() => createAuthenticatedFetch(), []);
|
|
407
|
+
// Flow action handler — POST to /api/v1/automation/{name}/trigger.
|
|
408
|
+
// Triggered when an Action with `type: 'flow'` is invoked from list-level
|
|
409
|
+
// locations (list_toolbar, list_item). For list_item the row's recordId is
|
|
410
|
+
// expected in `action.params.recordId`.
|
|
411
|
+
const flowHandler = useCallback(async (action) => {
|
|
412
|
+
const flowName = action.target || action.name;
|
|
413
|
+
if (!flowName) {
|
|
414
|
+
return { success: false, error: 'No flow target provided for flow action' };
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
const baseUrl = import.meta.env.VITE_SERVER_URL || '';
|
|
418
|
+
const params = action.params || {};
|
|
419
|
+
const res = await authFetch(`${baseUrl}/api/v1/automation/${encodeURIComponent(flowName)}/trigger`, {
|
|
420
|
+
method: 'POST',
|
|
421
|
+
headers: { 'Content-Type': 'application/json' },
|
|
422
|
+
body: JSON.stringify({
|
|
423
|
+
recordId: params.recordId,
|
|
424
|
+
objectName: objectDef.name,
|
|
425
|
+
params,
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
const json = await res.json().catch(() => null);
|
|
429
|
+
if (!res.ok || (json && json.success === false)) {
|
|
430
|
+
const errMsg = json?.error || `Flow "${flowName}" failed (HTTP ${res.status})`;
|
|
431
|
+
return { success: false, error: errMsg };
|
|
432
|
+
}
|
|
433
|
+
const shouldRefresh = action.refreshAfter !== false;
|
|
434
|
+
if (shouldRefresh) {
|
|
435
|
+
setRefreshKey(k => k + 1);
|
|
436
|
+
}
|
|
437
|
+
return { success: true, data: json?.data, reload: shouldRefresh };
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
return { success: false, error: error.message };
|
|
441
|
+
}
|
|
442
|
+
}, [authFetch, objectDef.name]);
|
|
443
|
+
// Server-side action handler — POST to /api/v1/actions/{object}/{action}.
|
|
444
|
+
// For list-toolbar/list-item `script` and `modal` actions whose `target`
|
|
445
|
+
// matches a server-registered handler. selectedIds (from action.params)
|
|
446
|
+
// is forwarded so bulk handlers like massUpdateStage / addToCampaign work.
|
|
447
|
+
const serverActionHandler = useCallback(async (action) => {
|
|
448
|
+
const targetName = action.target || action.name;
|
|
449
|
+
if (!targetName) {
|
|
450
|
+
return { success: false, error: 'No action target provided' };
|
|
451
|
+
}
|
|
452
|
+
const params = (action.params && !Array.isArray(action.params))
|
|
453
|
+
? action.params
|
|
454
|
+
: {};
|
|
455
|
+
try {
|
|
456
|
+
const baseUrl = import.meta.env.VITE_SERVER_URL || '';
|
|
457
|
+
const obj = action.objectName || objectDef.name || 'global';
|
|
458
|
+
const res = await authFetch(`${baseUrl}/api/v1/actions/${encodeURIComponent(obj)}/${encodeURIComponent(targetName)}`, {
|
|
459
|
+
method: 'POST',
|
|
460
|
+
headers: { 'Content-Type': 'application/json' },
|
|
461
|
+
body: JSON.stringify({ recordId: params.recordId, params }),
|
|
462
|
+
});
|
|
463
|
+
const json = await res.json().catch(() => null);
|
|
464
|
+
if (!res.ok || (json && json.success === false)) {
|
|
465
|
+
const errMsg = json?.error || `Action "${targetName}" failed (HTTP ${res.status})`;
|
|
466
|
+
return { success: false, error: errMsg };
|
|
467
|
+
}
|
|
468
|
+
const shouldRefresh = action.refreshAfter !== false;
|
|
469
|
+
if (shouldRefresh)
|
|
470
|
+
setRefreshKey(k => k + 1);
|
|
471
|
+
return { success: true, data: json?.data, reload: shouldRefresh };
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
return { success: false, error: error.message };
|
|
475
|
+
}
|
|
476
|
+
}, [authFetch, objectDef.name]);
|
|
477
|
+
// Real-time: auto-refresh when server reports data changes
|
|
478
|
+
const { lastMessage: realtimeMessage } = useRealtimeSubscription({
|
|
479
|
+
channel: `object:${objectDef.name}`,
|
|
480
|
+
});
|
|
481
|
+
// Conflict resolution: detect and queue conflicts on reconnection
|
|
482
|
+
const conflictUserId = objectDef.name ? `user-${objectDef.name}` : 'current-user';
|
|
483
|
+
const { hasConflicts, resolveAllConflicts } = useConflictResolution(conflictUserId);
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
if (realtimeMessage) {
|
|
486
|
+
// On reconnection data change, auto-resolve with server-wins strategy
|
|
487
|
+
if (hasConflicts) {
|
|
488
|
+
resolveAllConflicts('remote');
|
|
489
|
+
}
|
|
490
|
+
setRefreshKey(k => k + 1);
|
|
491
|
+
}
|
|
492
|
+
}, [realtimeMessage, hasConflicts, resolveAllConflicts]);
|
|
493
|
+
// Fetch record count for footer display
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
if (dataSource?.find && objectDef.name) {
|
|
496
|
+
dataSource.find(objectDef.name, { limit: 0 }).then((result) => {
|
|
497
|
+
if (typeof result?.total === 'number') {
|
|
498
|
+
setRecordCount(result.total);
|
|
499
|
+
}
|
|
500
|
+
else if (Array.isArray(result?.data)) {
|
|
501
|
+
setRecordCount(result.data.length);
|
|
502
|
+
}
|
|
503
|
+
else if (Array.isArray(result)) {
|
|
504
|
+
setRecordCount(result.length);
|
|
505
|
+
}
|
|
506
|
+
}).catch(() => {
|
|
507
|
+
// Silently ignore — record count is non-critical
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}, [dataSource, objectDef.name, refreshKey]);
|
|
511
|
+
// Navigation overlay for record detail (supports drawer/modal/split/popover via config)
|
|
512
|
+
// Priority: activeView.navigation > objectDef.navigation > default page
|
|
513
|
+
// Memoize to avoid unstable object identity on every render (stale closure prevention)
|
|
514
|
+
const detailNavigation = useMemo(() => activeView?.navigation ?? objectDef.navigation ?? { mode: 'page' }, [activeView?.navigation, objectDef.navigation]);
|
|
515
|
+
const drawerRecordId = searchParams.get('recordId');
|
|
516
|
+
// Memoize onNavigate to prevent stale closure in useNavigationOverlay's handleClick
|
|
517
|
+
const handleNavOverlayNavigate = useCallback((recordId, action) => {
|
|
518
|
+
if (action === 'new_window') {
|
|
519
|
+
// Open record detail in a new browser tab with Console-correct URL
|
|
520
|
+
const basePath = window.location.pathname.replace(/\/view\/.*$/, '');
|
|
521
|
+
window.open(`${basePath}/record/${encodeURIComponent(String(recordId))}`, '_blank');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
// page / view mode — navigate to record detail page
|
|
525
|
+
// Handles action === 'view' (from useNavigationOverlay page mode) and
|
|
526
|
+
// default fallthrough for any unrecognised action
|
|
527
|
+
if (action === 'view' || !action || action === 'page') {
|
|
528
|
+
if (viewId) {
|
|
529
|
+
navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
navigate(`record/${encodeURIComponent(String(recordId))}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}, [navigate, viewId]);
|
|
536
|
+
const navOverlay = useNavigationOverlay({
|
|
537
|
+
navigation: detailNavigation,
|
|
538
|
+
objectName: objectDef.name,
|
|
539
|
+
onNavigate: handleNavOverlayNavigate,
|
|
540
|
+
});
|
|
541
|
+
const handleDrawerClose = () => {
|
|
542
|
+
navOverlay.close();
|
|
543
|
+
const newParams = new URLSearchParams(searchParams);
|
|
544
|
+
newParams.delete('recordId');
|
|
545
|
+
setSearchParams(newParams);
|
|
546
|
+
};
|
|
547
|
+
// Sync URL-based recordId to overlay state
|
|
548
|
+
useEffect(() => {
|
|
549
|
+
if (drawerRecordId && !navOverlay.isOpen) {
|
|
550
|
+
navOverlay.open({ id: drawerRecordId });
|
|
551
|
+
}
|
|
552
|
+
else if (!drawerRecordId && navOverlay.isOpen) {
|
|
553
|
+
navOverlay.close();
|
|
554
|
+
}
|
|
555
|
+
}, [drawerRecordId]);
|
|
556
|
+
// Render multi-view content via ListView plugin (for kanban, calendar, etc.)
|
|
557
|
+
const renderListView = useCallback(({ schema: listSchema, dataSource: ds, onEdit: editHandler, className, refreshKey: pluginRefreshKey }) => {
|
|
558
|
+
// Combine local refreshKey with the plugin ObjectView's refreshKey for full propagation
|
|
559
|
+
const combinedRefreshKey = refreshKey + (pluginRefreshKey || 0);
|
|
560
|
+
const key = `${objectName}-${activeView.id}-${combinedRefreshKey}`;
|
|
561
|
+
const viewDef = activeView;
|
|
562
|
+
// Warn in dev mode if flat properties are used instead of nested spec format
|
|
563
|
+
if (process.env.NODE_ENV === 'development') {
|
|
564
|
+
const flatKeys = ['startDateField', 'endDateField', 'dateField', 'groupBy', 'groupField',
|
|
565
|
+
'locationField', 'imageField', 'chartType', 'xAxisField', 'dependenciesField',
|
|
566
|
+
'progressField', 'colorField', 'allDayField', 'subjectField', 'endField',
|
|
567
|
+
'latitudeField', 'longitudeField', 'zoom', 'center', 'cardFields', 'subtitleField',
|
|
568
|
+
'descriptionField', 'yAxisFields', 'aggregation', 'series'];
|
|
569
|
+
const nestedConfig = viewDef[viewDef.type] || {};
|
|
570
|
+
const found = flatKeys.filter(k => k in viewDef && !(k in nestedConfig));
|
|
571
|
+
if (found.length > 0) {
|
|
572
|
+
console.warn(`[Spec Compliance] View "${viewDef.id}" uses flat properties ${JSON.stringify(found)}. ` +
|
|
573
|
+
`Move them under viewDef.${viewDef.type || '<type>'} per @objectstack/spec protocol.`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (viewDef.type === 'chart') {
|
|
577
|
+
const chartConfig = viewDef.chart || {};
|
|
578
|
+
return (_jsx(ObjectChart, { dataSource: ds, schema: {
|
|
579
|
+
type: 'object-chart',
|
|
580
|
+
objectName: objectDef.name,
|
|
581
|
+
chartType: chartConfig.chartType,
|
|
582
|
+
xAxisField: chartConfig.xAxisField,
|
|
583
|
+
yAxisFields: chartConfig.yAxisFields,
|
|
584
|
+
aggregation: chartConfig.aggregation,
|
|
585
|
+
series: chartConfig.series,
|
|
586
|
+
config: chartConfig.config,
|
|
587
|
+
filter: chartConfig.filter,
|
|
588
|
+
} }, key));
|
|
589
|
+
}
|
|
590
|
+
const fullSchema = {
|
|
591
|
+
...listSchema,
|
|
592
|
+
// Propagate appearance/view-config properties for live preview
|
|
593
|
+
rowHeight: viewDef.rowHeight ?? listSchema.rowHeight,
|
|
594
|
+
densityMode: viewDef.densityMode ?? listSchema.densityMode,
|
|
595
|
+
inlineEdit: viewDef.inlineEdit ?? viewDef.editRecordsInline ?? listSchema.inlineEdit,
|
|
596
|
+
appearance: viewDef.showDescription != null
|
|
597
|
+
? { showDescription: viewDef.showDescription }
|
|
598
|
+
: listSchema.appearance,
|
|
599
|
+
// Propagate toolbar/display flags for all view types
|
|
600
|
+
showSearch: viewDef.showSearch ?? listSchema.showSearch,
|
|
601
|
+
showSort: viewDef.showSort ?? listSchema.showSort,
|
|
602
|
+
showFilters: viewDef.showFilters ?? listSchema.showFilters,
|
|
603
|
+
showHideFields: viewDef.showHideFields ?? listSchema.showHideFields,
|
|
604
|
+
showGroup: viewDef.showGroup ?? listSchema.showGroup,
|
|
605
|
+
showColor: viewDef.showColor ?? listSchema.showColor,
|
|
606
|
+
showDensity: viewDef.showDensity ?? listSchema.showDensity,
|
|
607
|
+
allowExport: viewDef.allowExport ?? listSchema.allowExport,
|
|
608
|
+
exportOptions: viewDef.allowExport === false ? undefined : (viewDef.exportOptions ?? listSchema.exportOptions),
|
|
609
|
+
striped: viewDef.striped ?? listSchema.striped,
|
|
610
|
+
bordered: viewDef.bordered ?? listSchema.bordered,
|
|
611
|
+
color: viewDef.color ?? listSchema.color,
|
|
612
|
+
// Propagate view-config properties (Bug 4 / items 14-22)
|
|
613
|
+
wrapHeaders: viewDef.wrapHeaders ?? listSchema.wrapHeaders,
|
|
614
|
+
clickIntoRecordDetails: viewDef.clickIntoRecordDetails ?? listSchema.clickIntoRecordDetails,
|
|
615
|
+
addRecordViaForm: viewDef.addRecordViaForm ?? listSchema.addRecordViaForm,
|
|
616
|
+
addDeleteRecordsInline: viewDef.addDeleteRecordsInline ?? listSchema.addDeleteRecordsInline,
|
|
617
|
+
collapseAllByDefault: viewDef.collapseAllByDefault ?? listSchema.collapseAllByDefault,
|
|
618
|
+
fieldTextColor: viewDef.fieldTextColor ?? listSchema.fieldTextColor,
|
|
619
|
+
prefixField: viewDef.prefixField ?? listSchema.prefixField,
|
|
620
|
+
showDescription: viewDef.showDescription ?? listSchema.showDescription,
|
|
621
|
+
// Propagate new spec properties (P0/P1/P2)
|
|
622
|
+
navigation: viewDef.navigation ?? listSchema.navigation,
|
|
623
|
+
selection: viewDef.selection ?? listSchema.selection,
|
|
624
|
+
pagination: viewDef.pagination ?? listSchema.pagination,
|
|
625
|
+
searchableFields: viewDef.searchableFields ?? listSchema.searchableFields,
|
|
626
|
+
filterableFields: viewDef.filterableFields ?? listSchema.filterableFields,
|
|
627
|
+
resizable: viewDef.resizable ?? listSchema.resizable,
|
|
628
|
+
hiddenFields: viewDef.hiddenFields ?? listSchema.hiddenFields,
|
|
629
|
+
rowActions: viewDef.rowActions ?? listSchema.rowActions,
|
|
630
|
+
bulkActions: viewDef.bulkActions ?? listSchema.bulkActions,
|
|
631
|
+
sharing: viewDef.sharing ?? listSchema.sharing,
|
|
632
|
+
addRecord: viewDef.addRecord ?? listSchema.addRecord,
|
|
633
|
+
conditionalFormatting: viewDef.conditionalFormatting ?? listSchema.conditionalFormatting,
|
|
634
|
+
quickFilters: viewDef.quickFilters ?? listSchema.quickFilters,
|
|
635
|
+
userFilters: viewDef.userFilters ?? listSchema.userFilters,
|
|
636
|
+
showRecordCount: viewDef.showRecordCount ?? listSchema.showRecordCount,
|
|
637
|
+
allowPrinting: viewDef.allowPrinting ?? listSchema.allowPrinting,
|
|
638
|
+
virtualScroll: viewDef.virtualScroll ?? listSchema.virtualScroll,
|
|
639
|
+
emptyState: viewDef.emptyState ?? listSchema.emptyState,
|
|
640
|
+
aria: viewDef.aria ?? listSchema.aria,
|
|
641
|
+
tabs: listSchema.tabs,
|
|
642
|
+
// Propagate filter/sort as default filters/sort for data flow
|
|
643
|
+
...(viewDef.filter?.length ? { filters: viewDef.filter } : {}),
|
|
644
|
+
...(viewDef.sort?.length ? { sort: viewDef.sort } : {}),
|
|
645
|
+
options: {
|
|
646
|
+
kanban: {
|
|
647
|
+
groupBy: viewDef.kanban?.groupByField || viewDef.kanban?.groupField || 'status',
|
|
648
|
+
groupField: viewDef.kanban?.groupByField || viewDef.kanban?.groupField || 'status',
|
|
649
|
+
titleField: viewDef.kanban?.titleField || objectDef.titleField || 'name',
|
|
650
|
+
cardFields: viewDef.kanban?.columns,
|
|
651
|
+
},
|
|
652
|
+
calendar: {
|
|
653
|
+
startDateField: viewDef.calendar?.startDateField || 'due_date',
|
|
654
|
+
endDateField: viewDef.calendar?.endDateField,
|
|
655
|
+
titleField: viewDef.calendar?.titleField || 'name',
|
|
656
|
+
colorField: viewDef.calendar?.colorField,
|
|
657
|
+
allDayField: viewDef.calendar?.allDayField,
|
|
658
|
+
defaultView: viewDef.calendar?.defaultView,
|
|
659
|
+
},
|
|
660
|
+
timeline: {
|
|
661
|
+
dateField: viewDef.timeline?.dateField || 'due_date',
|
|
662
|
+
titleField: viewDef.timeline?.titleField || objectDef.titleField || 'name',
|
|
663
|
+
descriptionField: viewDef.timeline?.descriptionField,
|
|
664
|
+
},
|
|
665
|
+
map: {
|
|
666
|
+
locationField: viewDef.map?.locationField,
|
|
667
|
+
titleField: viewDef.map?.titleField || objectDef.titleField || 'name',
|
|
668
|
+
latitudeField: viewDef.map?.latitudeField,
|
|
669
|
+
longitudeField: viewDef.map?.longitudeField,
|
|
670
|
+
zoom: viewDef.map?.zoom,
|
|
671
|
+
center: viewDef.map?.center,
|
|
672
|
+
},
|
|
673
|
+
gallery: {
|
|
674
|
+
imageField: viewDef.gallery?.imageField || 'image',
|
|
675
|
+
titleField: viewDef.gallery?.titleField || objectDef.titleField || 'name',
|
|
676
|
+
subtitleField: viewDef.gallery?.subtitleField,
|
|
677
|
+
},
|
|
678
|
+
gantt: {
|
|
679
|
+
startDateField: viewDef.gantt?.startDateField || 'start_date',
|
|
680
|
+
endDateField: viewDef.gantt?.endDateField || 'end_date',
|
|
681
|
+
titleField: viewDef.gantt?.titleField || 'name',
|
|
682
|
+
progressField: viewDef.gantt?.progressField,
|
|
683
|
+
dependenciesField: viewDef.gantt?.dependenciesField,
|
|
684
|
+
colorField: viewDef.gantt?.colorField,
|
|
685
|
+
},
|
|
686
|
+
chart: {
|
|
687
|
+
chartType: viewDef.chart?.chartType,
|
|
688
|
+
xAxisField: viewDef.chart?.xAxisField,
|
|
689
|
+
yAxisFields: viewDef.chart?.yAxisFields,
|
|
690
|
+
aggregation: viewDef.chart?.aggregation,
|
|
691
|
+
series: viewDef.chart?.series,
|
|
692
|
+
config: viewDef.chart?.config,
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
return (_jsx(ListView, { schema: fullSchema, className: className, onEdit: editHandler, onRowClick: (record) => {
|
|
697
|
+
navOverlay.handleClick(record);
|
|
698
|
+
}, dataSource: ds }, key));
|
|
699
|
+
}, [activeView, objectDef, objectName, refreshKey, navOverlay]);
|
|
700
|
+
// Memoize the merged views array so PluginObjectView doesn't get a new
|
|
701
|
+
// reference on every render (which would trigger unnecessary data refetches).
|
|
702
|
+
const mergedViews = useMemo(() => views.map((v) => v.id === activeViewId && viewDraft && viewDraft.id === v.id
|
|
703
|
+
? { ...v, ...viewDraft }
|
|
704
|
+
: v), [views, activeViewId, viewDraft]);
|
|
705
|
+
// Build the ObjectViewSchema for the plugin — reads from activeView (which merges draft)
|
|
706
|
+
const objectViewSchema = useMemo(() => ({
|
|
707
|
+
type: 'object-view',
|
|
708
|
+
objectName: objectDef.name,
|
|
709
|
+
layout: 'page',
|
|
710
|
+
showSearch: activeView?.showSearch !== false,
|
|
711
|
+
showFilters: activeView?.showFilters !== false,
|
|
712
|
+
showSort: activeView?.showSort !== false,
|
|
713
|
+
showCreate: false, // We render our own create button in the header
|
|
714
|
+
showRefresh: true,
|
|
715
|
+
allowCreateView: isAdmin,
|
|
716
|
+
viewActions: isAdmin ? [
|
|
717
|
+
{ type: 'settings' },
|
|
718
|
+
{ type: 'share' },
|
|
719
|
+
{ type: 'duplicate' },
|
|
720
|
+
{ type: 'delete' },
|
|
721
|
+
] : [],
|
|
722
|
+
onNavigate: (recordId, mode) => {
|
|
723
|
+
if (mode === 'edit') {
|
|
724
|
+
onEdit?.({ id: recordId });
|
|
725
|
+
}
|
|
726
|
+
else if (mode === 'view') {
|
|
727
|
+
if (viewId) {
|
|
728
|
+
navigate(`../../record/${encodeURIComponent(String(recordId))}`, { relative: 'path' });
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
navigate(`record/${encodeURIComponent(String(recordId))}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
}), [objectDef.name, onEdit, activeView?.showSearch, activeView?.showFilters, activeView?.showSort, navigate, viewId, isAdmin]);
|
|
736
|
+
return (_jsxs(ActionProvider, { context: { objectName: objectDef.name, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler }, children: [_jsxs("div", { className: "h-full flex flex-col bg-background min-w-0 overflow-hidden", children: [_jsxs("div", { className: "flex justify-between items-center py-2.5 sm:py-3 px-3 sm:px-4 border-b shrink-0 bg-background z-10", children: [_jsxs("div", { className: "flex items-center gap-2 sm:gap-3 min-w-0 flex-1", children: [_jsx("div", { className: "bg-primary/10 p-1.5 sm:p-2 rounded-md shrink-0", children: (() => { const I = getIcon(objectDef?.icon); return _jsx(I, { className: "h-4 w-4 text-primary" }); })() }), _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-base sm:text-lg font-semibold tracking-tight text-foreground truncate", children: objectLabel(objectDef) }), objectDef.description && (_jsx("p", { className: "text-xs text-muted-foreground truncate hidden sm:block max-w-md", children: objectDesc(objectDef) }))] })] }), _jsxs("div", { className: "flex items-center gap-1.5 sm:gap-2 shrink-0", children: [can(objectDef.name, 'create') && (_jsxs(Button, { size: "sm", onClick: actions.create, className: "shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { className: "hidden sm:inline", children: t('console.objectView.new') })] })), objectDef.actions?.some((a) => a.locations?.includes('list_toolbar')) && (_jsx(SchemaRenderer, { schema: {
|
|
737
|
+
type: 'action:bar',
|
|
738
|
+
location: 'list_toolbar',
|
|
739
|
+
actions: objectDef.actions,
|
|
740
|
+
size: 'sm',
|
|
741
|
+
variant: 'outline',
|
|
742
|
+
} })), isAdmin && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { size: "sm", variant: "ghost", className: "shadow-none h-8 sm:h-9 px-2", title: t('console.objectView.designTools'), children: _jsx(Wrench, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-48", children: [_jsxs(DropdownMenuItem, { onClick: toggleDebug, children: [_jsx(MetadataToggle, { open: showDebug, onToggle: toggleDebug, className: "hidden" }), t('console.objectView.metadataInspector')] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: () => { setViewConfigPanelMode('edit'); setShowViewConfigPanel(prev => !prev); }, children: [_jsx(Settings2, { className: "h-4 w-4 mr-2" }), t('console.objectView.editView')] }), _jsxs(DropdownMenuItem, { onClick: () => { setViewConfigPanelMode('create'); setShowViewConfigPanel(true); }, children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), t('console.objectView.addView')] })] })] }))] })] }), views.length > 1 && (_jsx("div", { className: "border-b px-3 sm:px-4 bg-background overflow-x-auto shrink-0", children: _jsx(ViewTabBar, { views: views.map((view) => ({
|
|
743
|
+
id: view.id,
|
|
744
|
+
label: view.label,
|
|
745
|
+
type: view.type,
|
|
746
|
+
hasActiveFilters: Array.isArray(view.filter) && view.filter.length > 0,
|
|
747
|
+
hasActiveSort: Array.isArray(view.sort) && view.sort.length > 0,
|
|
748
|
+
})), activeViewId: activeViewId, onViewChange: handleViewChange, viewTypeIcons: VIEW_TYPE_ICONS, config: { reorderable: false } }) })), _jsxs("div", { className: "flex-1 overflow-hidden relative flex flex-row", children: [navOverlay.mode === 'split' && navOverlay.isOpen ? (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
|
|
749
|
+
handleDrawerClose(); }, title: objectLabel(objectDef), mainContent: _jsxs("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: [_jsx("div", { className: "flex-1 relative overflow-auto p-3 sm:p-4", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record) => {
|
|
750
|
+
navOverlay.handleClick(record);
|
|
751
|
+
}, renderListView: renderListView, onCreateView: handleCreateView, onViewAction: handleViewAction }) }), typeof recordCount === 'number' && (_jsx("div", { "data-testid": "record-count-footer", className: "border-t px-3 sm:px-4 py-1.5 text-xs text-muted-foreground bg-muted/5 shrink-0", children: t('console.objectView.recordCount', { count: recordCount }) }))] }), children: (record) => {
|
|
752
|
+
const recordId = (record.id || record._id);
|
|
753
|
+
return (_jsx(DrawerDetailContent, { objectDef: objectDef, recordId: recordId, dataSource: dataSource, onEdit: onEdit }));
|
|
754
|
+
} })) : (_jsx("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: _jsx("div", { className: "flex-1 relative overflow-auto p-3 sm:p-4", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record) => {
|
|
755
|
+
navOverlay.handleClick(record);
|
|
756
|
+
}, renderListView: renderListView, onCreateView: handleCreateView, onViewAction: handleViewAction }) }) })), _jsx(MetadataPanel, { open: showDebug && isAdmin, sections: [
|
|
757
|
+
{ title: 'View Configuration', data: activeView },
|
|
758
|
+
{ title: 'Object Definition', data: objectDef },
|
|
759
|
+
] }), _jsx("div", { "data-testid": "view-config-panel-wrapper", className: `transition-[max-width,opacity] duration-300 ease-in-out overflow-hidden ${showViewConfigPanel && isAdmin ? 'max-w-[280px] opacity-100' : 'max-w-0 opacity-0'}`, children: _jsx(ViewConfigPanel, { open: showViewConfigPanel && isAdmin, onClose: () => { setShowViewConfigPanel(false); setViewConfigPanelMode('edit'); }, mode: viewConfigPanelMode, activeView: activeView, objectDef: objectDef, recordCount: recordCount, onSave: handleViewConfigSave, onViewUpdate: handleViewUpdate, onCreate: handleViewCreate }) })] }), navOverlay.mode !== 'split' && (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
|
|
760
|
+
handleDrawerClose(); }, title: objectDef.label, className: navOverlay.mode === 'drawer' ? 'w-[90vw] sm:max-w-2xl p-0 overflow-hidden' : undefined, children: (record) => {
|
|
761
|
+
const recordId = (record.id || record._id);
|
|
762
|
+
return (_jsx(DrawerDetailContent, { objectDef: objectDef, recordId: recordId, dataSource: dataSource, onEdit: onEdit }));
|
|
763
|
+
} }))] }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
764
|
+
if (!open)
|
|
765
|
+
setConfirmState({ open: false, message: '' });
|
|
766
|
+
} }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
|
|
767
|
+
if (!open)
|
|
768
|
+
setParamState({ open: false, params: [] });
|
|
769
|
+
} })] }));
|
|
770
|
+
}
|