@object-ui/app-shell 3.3.0 → 3.3.2

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