@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.
Files changed (173) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +22 -2
  3. package/dist/chrome/CommandPalette.d.ts +16 -0
  4. package/dist/chrome/CommandPalette.js +88 -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 +217 -0
  101. package/dist/layout/AppSwitcher.d.ts +12 -0
  102. package/dist/layout/AppSwitcher.js +39 -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/ConsoleLayout.d.ts +21 -0
  108. package/dist/layout/ConsoleLayout.js +71 -0
  109. package/dist/layout/LocaleSwitcher.d.ts +7 -0
  110. package/dist/layout/LocaleSwitcher.js +27 -0
  111. package/dist/layout/ModeToggle.d.ts +1 -0
  112. package/dist/layout/ModeToggle.js +10 -0
  113. package/dist/layout/UnifiedSidebar.d.ts +22 -0
  114. package/dist/layout/UnifiedSidebar.js +210 -0
  115. package/dist/layout/index.d.ts +11 -0
  116. package/dist/layout/index.js +10 -0
  117. package/dist/providers/AdapterProvider.d.ts +21 -0
  118. package/dist/providers/AdapterProvider.js +52 -0
  119. package/dist/providers/ExpressionProvider.d.ts +50 -0
  120. package/dist/providers/ExpressionProvider.js +67 -0
  121. package/dist/providers/MetadataProvider.d.ts +18 -0
  122. package/dist/providers/MetadataProvider.js +324 -0
  123. package/dist/services/MetadataService.d.ts +119 -0
  124. package/dist/services/MetadataService.js +211 -0
  125. package/dist/skeletons/SkeletonDashboard.d.ts +13 -0
  126. package/dist/skeletons/SkeletonDashboard.js +12 -0
  127. package/dist/skeletons/SkeletonDetail.d.ts +15 -0
  128. package/dist/skeletons/SkeletonDetail.js +12 -0
  129. package/dist/skeletons/SkeletonGrid.d.ts +15 -0
  130. package/dist/skeletons/SkeletonGrid.js +12 -0
  131. package/dist/skeletons/index.d.ts +3 -0
  132. package/dist/skeletons/index.js +3 -0
  133. package/dist/utils/getIcon.d.ts +10 -0
  134. package/dist/utils/getIcon.js +24 -0
  135. package/dist/utils/index.d.ts +33 -0
  136. package/dist/utils/index.js +62 -0
  137. package/dist/utils/metadataConverters.d.ts +78 -0
  138. package/dist/utils/metadataConverters.js +74 -0
  139. package/dist/views/ActionConfirmDialog.d.ts +22 -0
  140. package/dist/views/ActionConfirmDialog.js +22 -0
  141. package/dist/views/ActionParamDialog.d.ts +23 -0
  142. package/dist/views/ActionParamDialog.js +67 -0
  143. package/dist/views/DashboardView.d.ts +9 -0
  144. package/dist/views/DashboardView.js +357 -0
  145. package/dist/views/DesignDrawer.d.ts +28 -0
  146. package/dist/views/DesignDrawer.js +51 -0
  147. package/dist/views/MetadataInspector.d.ts +40 -0
  148. package/dist/views/MetadataInspector.js +69 -0
  149. package/dist/views/ObjectView.d.ts +14 -0
  150. package/dist/views/ObjectView.js +770 -0
  151. package/dist/views/PageView.d.ts +6 -0
  152. package/dist/views/PageView.js +49 -0
  153. package/dist/views/RecordDetailView.d.ts +14 -0
  154. package/dist/views/RecordDetailView.js +508 -0
  155. package/dist/views/ReportView.d.ts +4 -0
  156. package/dist/views/ReportView.js +284 -0
  157. package/dist/views/SearchResultsPage.d.ts +9 -0
  158. package/dist/views/SearchResultsPage.js +107 -0
  159. package/dist/views/ViewConfigPanel.d.ts +56 -0
  160. package/dist/views/ViewConfigPanel.js +91 -0
  161. package/dist/views/index.d.ts +11 -0
  162. package/dist/views/index.js +11 -0
  163. package/package.json +59 -11
  164. package/.turbo/turbo-build.log +0 -4
  165. package/src/components/AppShell.tsx +0 -31
  166. package/src/components/DashboardRenderer.tsx +0 -36
  167. package/src/components/FormRenderer.tsx +0 -71
  168. package/src/components/ObjectRenderer.tsx +0 -122
  169. package/src/components/PageRenderer.tsx +0 -28
  170. package/src/index.ts +0 -20
  171. package/src/types.ts +0 -78
  172. package/tsconfig.json +0 -20
  173. 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
+ }