@object-ui/app-shell 4.5.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +21 -0
- package/dist/console/AppContent.js +6 -39
- package/dist/console/ConsoleShell.js +45 -4
- package/dist/console/home/AppCard.d.ts +2 -1
- package/dist/console/home/AppCard.js +24 -5
- package/dist/console/home/HomePage.js +29 -7
- package/dist/console/home/QuickActions.js +13 -7
- package/dist/console/home/RecentApps.js +12 -5
- package/dist/console/home/StarredApps.js +12 -5
- package/dist/context/FavoritesProvider.d.ts +9 -12
- package/dist/context/FavoritesProvider.js +83 -37
- package/dist/context/RecentItemsProvider.d.ts +45 -0
- package/dist/context/RecentItemsProvider.js +139 -0
- package/dist/context/UserStateAdapters.d.ts +61 -0
- package/dist/context/UserStateAdapters.js +141 -0
- package/dist/context/index.d.ts +4 -0
- package/dist/context/index.js +2 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useRecentItems.d.ts +9 -17
- package/dist/hooks/useRecentItems.js +9 -46
- package/dist/hooks/useTrackRouteAsRecent.d.ts +18 -0
- package/dist/hooks/useTrackRouteAsRecent.js +91 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/layout/PageHeader.d.ts +29 -2
- package/dist/layout/PageHeader.js +20 -2
- package/dist/utils/resolveActionParams.d.ts +11 -0
- package/dist/utils/resolveActionParams.js +19 -0
- package/dist/views/ActionParamDialog.js +31 -2
- package/dist/views/ObjectView.js +63 -164
- package/dist/views/RecordDetailView.d.ts +20 -1
- package/dist/views/RecordDetailView.js +23 -9
- package/package.json +24 -24
package/dist/views/ObjectView.js
CHANGED
|
@@ -14,7 +14,6 @@ import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
|
|
14
14
|
const ObjectChart = lazy(() => import('@object-ui/plugin-charts').then((m) => ({ default: m.ObjectChart })));
|
|
15
15
|
const ImportWizard = lazy(() => import('@object-ui/plugin-grid').then((m) => ({ default: m.ImportWizard })));
|
|
16
16
|
import { ListView } from '@object-ui/plugin-list';
|
|
17
|
-
import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
|
|
18
17
|
import { ObjectView as PluginObjectView, ViewTabBar, ManageViewsDialog } from '@object-ui/plugin-view';
|
|
19
18
|
// Plugin registration is handled by the host app (e.g. apps/console/src/main.tsx
|
|
20
19
|
// uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
|
|
@@ -27,6 +26,7 @@ import { ViewConfigPanel } from './ViewConfigPanel';
|
|
|
27
26
|
import { CreateViewDialog } from './CreateViewDialog';
|
|
28
27
|
import { PageHeader } from '../layout/PageHeader';
|
|
29
28
|
import { ManagedByBadge } from '../components/ManagedByBadge';
|
|
29
|
+
import { RecordDetailView } from './RecordDetailView';
|
|
30
30
|
import { resolveCrudAffordances } from '../utils/crudAffordances';
|
|
31
31
|
import { resolveManagedByEmptyState } from '../utils/managedByEmptyState';
|
|
32
32
|
import { useObjectActions } from '../hooks/useObjectActions';
|
|
@@ -191,157 +191,6 @@ function fromSysViewRecord(sv) {
|
|
|
191
191
|
pageSize: sv.page_size ?? sv.pageSize,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
|
-
/**
|
|
195
|
-
* DrawerDetailContent — extracted component for NavigationOverlay content.
|
|
196
|
-
* Needs to be a proper component (not a render prop) so it can use hooks
|
|
197
|
-
* for data fetching, comment handling, etc.
|
|
198
|
-
*/
|
|
199
|
-
function DrawerDetailContent({ objectDef, recordId, dataSource, onEdit }) {
|
|
200
|
-
const { user } = useAuth();
|
|
201
|
-
const currentUser = user
|
|
202
|
-
? { id: user.id, name: user.name, avatar: user.image }
|
|
203
|
-
: FALLBACK_USER;
|
|
204
|
-
const [feedItems, setFeedItems] = useState([]);
|
|
205
|
-
// Fetch persisted comments from API
|
|
206
|
-
useEffect(() => {
|
|
207
|
-
if (!dataSource || !objectDef?.name || !recordId)
|
|
208
|
-
return;
|
|
209
|
-
const threadId = `${objectDef.name}:${recordId}`;
|
|
210
|
-
dataSource.find('sys_comment', { $filter: { threadId }, $orderby: { createdAt: 'asc' } })
|
|
211
|
-
.then((res) => {
|
|
212
|
-
if (res.data?.length) {
|
|
213
|
-
setFeedItems(res.data.map((c) => ({
|
|
214
|
-
id: c.id,
|
|
215
|
-
type: 'comment',
|
|
216
|
-
actor: c.author?.name ?? 'Unknown',
|
|
217
|
-
actorAvatarUrl: c.author?.avatar,
|
|
218
|
-
body: c.content,
|
|
219
|
-
createdAt: c.createdAt,
|
|
220
|
-
updatedAt: c.updatedAt,
|
|
221
|
-
parentId: c.parentId,
|
|
222
|
-
reactions: c.reactions
|
|
223
|
-
? Object.entries(c.reactions).map(([emoji, userIds]) => ({
|
|
224
|
-
emoji,
|
|
225
|
-
count: userIds.length,
|
|
226
|
-
reacted: userIds.includes(currentUser.id),
|
|
227
|
-
}))
|
|
228
|
-
: undefined,
|
|
229
|
-
})));
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
.catch(() => { });
|
|
233
|
-
}, [dataSource, objectDef?.name, recordId, currentUser.id]);
|
|
234
|
-
const handleAddComment = useCallback(async (text) => {
|
|
235
|
-
const newItem = {
|
|
236
|
-
id: crypto.randomUUID(),
|
|
237
|
-
type: 'comment',
|
|
238
|
-
actor: currentUser.name,
|
|
239
|
-
actorAvatarUrl: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
240
|
-
body: text,
|
|
241
|
-
createdAt: new Date().toISOString(),
|
|
242
|
-
};
|
|
243
|
-
setFeedItems(prev => [...prev, newItem]);
|
|
244
|
-
if (dataSource) {
|
|
245
|
-
const threadId = `${objectDef.name}:${recordId}`;
|
|
246
|
-
dataSource.create('sys_comment', {
|
|
247
|
-
id: newItem.id,
|
|
248
|
-
threadId,
|
|
249
|
-
author: currentUser,
|
|
250
|
-
content: text,
|
|
251
|
-
mentions: [],
|
|
252
|
-
createdAt: newItem.createdAt,
|
|
253
|
-
}).catch(() => { });
|
|
254
|
-
}
|
|
255
|
-
}, [currentUser, dataSource, objectDef?.name, recordId]);
|
|
256
|
-
const handleAddReply = useCallback(async (parentId, text) => {
|
|
257
|
-
const newItem = {
|
|
258
|
-
id: crypto.randomUUID(),
|
|
259
|
-
type: 'comment',
|
|
260
|
-
actor: currentUser.name,
|
|
261
|
-
actorAvatarUrl: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
262
|
-
body: text,
|
|
263
|
-
createdAt: new Date().toISOString(),
|
|
264
|
-
parentId,
|
|
265
|
-
};
|
|
266
|
-
setFeedItems(prev => {
|
|
267
|
-
const updated = [...prev, newItem];
|
|
268
|
-
return updated.map(item => item.id === parentId
|
|
269
|
-
? { ...item, replyCount: (item.replyCount ?? 0) + 1 }
|
|
270
|
-
: item);
|
|
271
|
-
});
|
|
272
|
-
if (dataSource) {
|
|
273
|
-
const threadId = `${objectDef.name}:${recordId}`;
|
|
274
|
-
dataSource.create('sys_comment', {
|
|
275
|
-
id: newItem.id,
|
|
276
|
-
threadId,
|
|
277
|
-
author: currentUser,
|
|
278
|
-
content: text,
|
|
279
|
-
mentions: [],
|
|
280
|
-
createdAt: newItem.createdAt,
|
|
281
|
-
parentId,
|
|
282
|
-
}).catch(() => { });
|
|
283
|
-
}
|
|
284
|
-
}, [currentUser, dataSource, objectDef?.name, recordId]);
|
|
285
|
-
const handleToggleReaction = useCallback((itemId, emoji) => {
|
|
286
|
-
setFeedItems(prev => prev.map(item => {
|
|
287
|
-
if (item.id !== itemId)
|
|
288
|
-
return item;
|
|
289
|
-
const reactions = [...(item.reactions ?? [])];
|
|
290
|
-
const idx = reactions.findIndex(r => r.emoji === emoji);
|
|
291
|
-
if (idx >= 0) {
|
|
292
|
-
const r = reactions[idx];
|
|
293
|
-
if (r.reacted) {
|
|
294
|
-
if (r.count <= 1) {
|
|
295
|
-
reactions.splice(idx, 1);
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
reactions[idx] = { ...r, count: r.count - 1, reacted: false };
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
reactions[idx] = { ...r, count: r.count + 1, reacted: true };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
reactions.push({ emoji, count: 1, reacted: true });
|
|
307
|
-
}
|
|
308
|
-
const updated = { ...item, reactions };
|
|
309
|
-
if (dataSource) {
|
|
310
|
-
dataSource.update('sys_comment', String(itemId), {
|
|
311
|
-
$toggleReaction: { emoji, userId: currentUser.id },
|
|
312
|
-
}).catch(() => { });
|
|
313
|
-
}
|
|
314
|
-
return updated;
|
|
315
|
-
}));
|
|
316
|
-
}, [currentUser.id, dataSource]);
|
|
317
|
-
return (_jsxs("div", { className: "h-full bg-background overflow-auto p-3 sm:p-4 lg:p-6", children: [_jsx(DetailView, { schema: {
|
|
318
|
-
type: 'detail-view',
|
|
319
|
-
objectName: objectDef.name,
|
|
320
|
-
resourceId: recordId,
|
|
321
|
-
showBack: false,
|
|
322
|
-
showEdit: true,
|
|
323
|
-
title: objectDef.label,
|
|
324
|
-
sections: [
|
|
325
|
-
{
|
|
326
|
-
title: 'Details',
|
|
327
|
-
fields: Object.keys(objectDef.fields || {}).map((key) => ({
|
|
328
|
-
name: key,
|
|
329
|
-
label: objectDef.fields[key].label || key,
|
|
330
|
-
type: objectDef.fields[key].type || 'text'
|
|
331
|
-
})),
|
|
332
|
-
}
|
|
333
|
-
]
|
|
334
|
-
}, dataSource: dataSource, onEdit: () => onEdit({ id: recordId }) }), _jsx("div", { className: "mt-6 border-t pt-6", children: _jsx(RecordChatterPanel, { config: {
|
|
335
|
-
position: 'bottom',
|
|
336
|
-
collapsible: true,
|
|
337
|
-
defaultCollapsed: true,
|
|
338
|
-
feed: {
|
|
339
|
-
enableReactions: true,
|
|
340
|
-
enableThreading: true,
|
|
341
|
-
showCommentInput: true,
|
|
342
|
-
},
|
|
343
|
-
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) })] }));
|
|
344
|
-
}
|
|
345
194
|
export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey }) {
|
|
346
195
|
const navigate = useNavigate();
|
|
347
196
|
const { objectName, viewId } = useParams();
|
|
@@ -1326,9 +1175,16 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1326
1175
|
}
|
|
1327
1176
|
}, [dataSource, objectDef.name, refreshKey]);
|
|
1328
1177
|
// Navigation overlay for record detail (supports drawer/modal/split/popover via config)
|
|
1329
|
-
// Priority: activeView.navigation > objectDef.navigation > default
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1178
|
+
// Priority: activeView.navigation > objectDef.navigation > default drawer.
|
|
1179
|
+
//
|
|
1180
|
+
// Default mode = 'drawer'. Mirrors Linear / Notion / Airtable / Jira where
|
|
1181
|
+
// record peek is the primary interaction and full-page is the upgrade.
|
|
1182
|
+
// Direct URL access (`/record/:id`) still opens as a full page because
|
|
1183
|
+
// RecordDetailView owns its own route — only same-page click navigation
|
|
1184
|
+
// is drawer-by-default. Per-view config can still override (e.g. a heavy
|
|
1185
|
+
// detail object can set `navigation.mode = 'page'`).
|
|
1186
|
+
const detailNavigation = useMemo(() => activeView?.navigation ??
|
|
1187
|
+
objectDef.navigation ?? { mode: 'drawer', width: 'min(92vw, 1280px)' }, [activeView?.navigation, objectDef.navigation]);
|
|
1332
1188
|
const drawerRecordId = searchParams.get('recordId');
|
|
1333
1189
|
/**
|
|
1334
1190
|
* URL-derived equality filters in the form `?filter[<field>]=<value>`.
|
|
@@ -1379,6 +1235,49 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1379
1235
|
newParams.delete('recordId');
|
|
1380
1236
|
setSearchParams(newParams);
|
|
1381
1237
|
};
|
|
1238
|
+
/**
|
|
1239
|
+
* Row-click handler used by all list/grid/gallery/kanban surfaces inside
|
|
1240
|
+
* this object view. Wraps `navOverlay.handleClick` so that drawer-mode
|
|
1241
|
+
* opens are URL-driven (writes `?recordId=…`) — making the drawer state
|
|
1242
|
+
* shareable, refresh-safe, and respected by browser back/forward. The
|
|
1243
|
+
* URL→state sync effect below handles actually opening the drawer.
|
|
1244
|
+
*/
|
|
1245
|
+
const handleRowClick = useCallback((record, event) => {
|
|
1246
|
+
// Cmd/Ctrl/middle-click — let the hook open in a new tab (full page)
|
|
1247
|
+
// regardless of configured mode. Matches browser link convention.
|
|
1248
|
+
const isModifier = !!(event && (event.metaKey || event.ctrlKey || event.button === 1));
|
|
1249
|
+
if (isModifier) {
|
|
1250
|
+
navOverlay.handleClick(record, event);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
// Drawer mode → URL is the source of truth. Push `?recordId=…`
|
|
1254
|
+
// and let the existing URL-sync effect open the overlay.
|
|
1255
|
+
if (navOverlay.mode === 'drawer') {
|
|
1256
|
+
const id = (record?.id ?? record?._id);
|
|
1257
|
+
if (id != null) {
|
|
1258
|
+
const next = new URLSearchParams(searchParams);
|
|
1259
|
+
next.set('recordId', String(id));
|
|
1260
|
+
setSearchParams(next);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
// All other modes (page / modal / split / popover / new_window / none)
|
|
1265
|
+
// — delegate to the hook.
|
|
1266
|
+
navOverlay.handleClick(record, event);
|
|
1267
|
+
}, [navOverlay, searchParams, setSearchParams]);
|
|
1268
|
+
/**
|
|
1269
|
+
* "Expand to full page" — invoked from the drawer header chevron. Closes
|
|
1270
|
+
* the drawer (which clears `?recordId=…`) and router-pushes to the
|
|
1271
|
+
* dedicated `/record/:id` route. Mirrors Linear/Notion peek-to-page.
|
|
1272
|
+
*/
|
|
1273
|
+
const handleExpandDrawer = useCallback(() => {
|
|
1274
|
+
const rec = navOverlay.selectedRecord;
|
|
1275
|
+
const id = rec && (rec.id ?? rec._id);
|
|
1276
|
+
if (id == null)
|
|
1277
|
+
return;
|
|
1278
|
+
handleDrawerClose();
|
|
1279
|
+
handleNavOverlayNavigate(id);
|
|
1280
|
+
}, [navOverlay.selectedRecord, handleNavOverlayNavigate]);
|
|
1382
1281
|
// Sync URL-based recordId to overlay state
|
|
1383
1282
|
useEffect(() => {
|
|
1384
1283
|
if (drawerRecordId && !navOverlay.isOpen) {
|
|
@@ -1606,8 +1505,8 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1606
1505
|
}),
|
|
1607
1506
|
params: { records: valid },
|
|
1608
1507
|
});
|
|
1609
|
-
}, onRowClick: (record) => {
|
|
1610
|
-
|
|
1508
|
+
}, onRowClick: (record, event) => {
|
|
1509
|
+
handleRowClick(record, event);
|
|
1611
1510
|
}, onSortChange: (sort) => {
|
|
1612
1511
|
persistViewPatch(viewDef.id, viewDef, { sort });
|
|
1613
1512
|
}, onFilterChange: (filter) => {
|
|
@@ -1727,20 +1626,20 @@ export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey })
|
|
|
1727
1626
|
showVisibilityGroups: true,
|
|
1728
1627
|
}, onAddView: isAdmin ? handleAddView : undefined, onRenameView: isAdmin ? handleRenameView : undefined, onDeleteView: isAdmin ? handleDeleteView : undefined, onDuplicateView: isAdmin ? handleDuplicateView : undefined, onPinView: isAdmin ? handlePinView : undefined, onSetDefaultView: isAdmin ? handleSetDefaultView : undefined, onConfigView: isAdmin ? handleConfigView : undefined, onManageViews: isAdmin ? () => setManageViewsOpen(true) : undefined }), isAdmin && (_jsx(ManageViewsDialog, { open: manageViewsOpen, onOpenChange: setManageViewsOpen, views: viewTabItems, activeViewId: activeViewId, viewTypeIcons: VIEW_TYPE_ICONS, onRename: handleRenameView, onDelete: handleDeleteView, onDuplicate: handleDuplicateView, onSetDefault: handleSetDefaultView, onSetPinned: handlePinView, onReorder: handleReorderViews, onAddView: handleAddView, onConfigView: handleConfigView }))] }));
|
|
1729
1628
|
})(), _jsxs("div", { className: "flex-1 overflow-hidden relative flex flex-row", children: [navOverlay.mode === 'split' && navOverlay.isOpen ? (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
|
|
1730
|
-
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-hidden", children: _jsx("div", { className: "h-full overflow-auto", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record) => {
|
|
1731
|
-
|
|
1629
|
+
handleDrawerClose(); }, title: objectLabel(objectDef), onExpand: handleExpandDrawer, expandLabel: t('console.objectView.expandToPage', { defaultValue: 'Open as full page' }), storageKey: `drawer-width:${objectDef.name}`, mainContent: _jsxs("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: [_jsx("div", { className: "flex-1 relative overflow-hidden", children: _jsx("div", { className: "h-full overflow-auto", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record, event) => {
|
|
1630
|
+
handleRowClick(record, event);
|
|
1732
1631
|
}, 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) => {
|
|
1733
1632
|
const recordId = (record.id || record._id);
|
|
1734
|
-
return (_jsx(
|
|
1735
|
-
} })) : (_jsx("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: _jsx("div", { className: "flex-1 relative overflow-hidden", children: _jsx("div", { className: "h-full overflow-auto", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record) => {
|
|
1736
|
-
|
|
1633
|
+
return (_jsx(RecordDetailView, { dataSource: dataSource, objects: objects, onEdit: onEdit, objectNameOverride: objectDef.name, recordIdOverride: recordId, embedded: true }));
|
|
1634
|
+
} })) : (_jsx("div", { className: "flex-1 min-w-0 relative h-full flex flex-col", children: _jsx("div", { className: "flex-1 relative overflow-hidden", children: _jsx("div", { className: "h-full overflow-auto", children: _jsx(PluginObjectView, { schema: objectViewSchema, dataSource: dataSource, views: mergedViews, activeViewId: activeViewId, onViewChange: handleViewChange, onEdit: (record) => onEdit?.(record), onRowClick: (record, event) => {
|
|
1635
|
+
handleRowClick(record, event);
|
|
1737
1636
|
}, renderListView: renderListView, onCreateView: handleCreateView, onViewAction: handleViewAction }) }) }) })), _jsx(MetadataPanel, { open: showDebug && isAdmin, sections: [
|
|
1738
1637
|
{ title: 'View Configuration', data: activeView },
|
|
1739
1638
|
{ title: 'Object Definition', data: objectDef },
|
|
1740
1639
|
] }), _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 }) }), _jsx(CreateViewDialog, { open: showCreateViewDialog && isAdmin, onOpenChange: setShowCreateViewDialog, existingLabels: views.map((v) => v.label).filter(Boolean), objectDef: objectDef, onCreate: (cfg) => handleViewCreate(cfg) })] }), navOverlay.mode !== 'split' && (_jsx(NavigationOverlay, { ...navOverlay, setIsOpen: (open) => { if (!open)
|
|
1741
|
-
handleDrawerClose(); }, title: objectDef.label,
|
|
1640
|
+
handleDrawerClose(); }, title: objectDef.label, onExpand: handleExpandDrawer, expandLabel: t('console.objectView.expandToPage', { defaultValue: 'Open as full page' }), storageKey: `drawer-width:${objectDef.name}`, children: (record) => {
|
|
1742
1641
|
const recordId = (record.id || record._id);
|
|
1743
|
-
return (_jsx(
|
|
1642
|
+
return (_jsx(RecordDetailView, { dataSource: dataSource, objects: objects, onEdit: onEdit, objectNameOverride: objectDef.name, recordIdOverride: recordId, embedded: true }));
|
|
1744
1643
|
} }))] }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
1745
1644
|
if (!open)
|
|
1746
1645
|
setConfirmState({ open: false, message: '' });
|
|
@@ -9,6 +9,25 @@ interface RecordDetailViewProps {
|
|
|
9
9
|
dataSource: any;
|
|
10
10
|
objects: any[];
|
|
11
11
|
onEdit: (record: any) => void;
|
|
12
|
+
/**
|
|
13
|
+
* When provided, this object name overrides the value derived from the
|
|
14
|
+
* current URL (`useParams()`). Used when rendering the detail inside a
|
|
15
|
+
* navigation drawer or split-pane — the parent route owns the URL while
|
|
16
|
+
* the embedded detail is keyed by an in-memory record selection.
|
|
17
|
+
*/
|
|
18
|
+
objectNameOverride?: string;
|
|
19
|
+
/**
|
|
20
|
+
* When provided, this record id overrides the value derived from the
|
|
21
|
+
* current URL. See {@link objectNameOverride}.
|
|
22
|
+
*/
|
|
23
|
+
recordIdOverride?: string;
|
|
24
|
+
/**
|
|
25
|
+
* `true` when this view is embedded (drawer / split-pane). Suppresses
|
|
26
|
+
* side effects that would conflict with the host route — namely the
|
|
27
|
+
* breadcrumb title publish (the parent route already owns the
|
|
28
|
+
* breadcrumb).
|
|
29
|
+
*/
|
|
30
|
+
embedded?: boolean;
|
|
12
31
|
}
|
|
13
|
-
export declare function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailViewProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
export declare function RecordDetailView({ dataSource, objects, onEdit, objectNameOverride, recordIdOverride, embedded }: RecordDetailViewProps): import("react/jsx-runtime").JSX.Element;
|
|
14
33
|
export {};
|
|
@@ -47,8 +47,11 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
|
|
|
47
47
|
const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
|
|
48
48
|
'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
|
|
49
49
|
]);
|
|
50
|
-
export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
51
|
-
const
|
|
50
|
+
export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverride, recordIdOverride, embedded }) {
|
|
51
|
+
const params = useParams();
|
|
52
|
+
const appName = params.appName;
|
|
53
|
+
const objectName = objectNameOverride ?? params.objectName;
|
|
54
|
+
const recordId = recordIdOverride ?? params.recordId;
|
|
52
55
|
const { showDebug } = useMetadataInspector();
|
|
53
56
|
const { user } = useAuth();
|
|
54
57
|
const navigate = useNavigate();
|
|
@@ -64,8 +67,9 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
64
67
|
const [recordTitle, setRecordTitle] = useState();
|
|
65
68
|
const objectDef = objects.find((o) => o.name === objectName);
|
|
66
69
|
// Publish record title to the navigation context so the top-bar breadcrumb
|
|
67
|
-
// can display "Acme Platform Upgrade" instead of "#9U1_MmmxjiGR…".
|
|
68
|
-
|
|
70
|
+
// can display "Acme Platform Upgrade" instead of "#9U1_MmmxjiGR…". Skip
|
|
71
|
+
// when embedded (drawer/split): the parent list route owns the breadcrumb.
|
|
72
|
+
useRecordBreadcrumbTitle(embedded ? undefined : recordTitle);
|
|
69
73
|
// Use the URL recordId as-is — it contains the actual record id.
|
|
70
74
|
// Navigation code passes `record.id || record._id` directly into the URL
|
|
71
75
|
// without adding any prefix, so no stripping is needed.
|
|
@@ -734,7 +738,17 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
734
738
|
title: sec.name ? sectionLabel(objectDef.name, sec.name, sec.title || sec.name) : sec.title,
|
|
735
739
|
collapsible: sec.collapsible,
|
|
736
740
|
defaultCollapsed: sec.defaultCollapsed,
|
|
737
|
-
fields: (sec.fields || [])
|
|
741
|
+
fields: (sec.fields || [])
|
|
742
|
+
.filter((f) => {
|
|
743
|
+
// Honor `hidden: true` on a field def even when the form
|
|
744
|
+
// section explicitly lists it. Hidden fields are typically
|
|
745
|
+
// internal artifacts (e.g. database URL, environment id)
|
|
746
|
+
// that platform actions read but end-users shouldn't see.
|
|
747
|
+
const fieldName = typeof f === 'string' ? f : f.name;
|
|
748
|
+
const fieldDef = objectDef.fields?.[fieldName];
|
|
749
|
+
return !fieldDef?.hidden;
|
|
750
|
+
})
|
|
751
|
+
.map((f) => {
|
|
738
752
|
const fieldName = typeof f === 'string' ? f : f.name;
|
|
739
753
|
const fieldDef = objectDef.fields[fieldName];
|
|
740
754
|
if (!fieldDef) {
|
|
@@ -760,7 +774,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
760
774
|
// redundant "Details" heading).
|
|
761
775
|
showBorder: false,
|
|
762
776
|
fields: Object.keys(objectDef.fields || {})
|
|
763
|
-
.filter(key => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key))
|
|
777
|
+
.filter(key => !AUDIT_FIELD_NAMES.has(key) && !HIDDEN_SYSTEM_FIELD_NAMES.has(key) && !objectDef.fields[key]?.hidden)
|
|
764
778
|
.map(key => {
|
|
765
779
|
const fieldDef = objectDef.fields[key];
|
|
766
780
|
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
@@ -954,7 +968,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
954
968
|
type: 'detail-view',
|
|
955
969
|
objectName: objectDef.name,
|
|
956
970
|
resourceId: pureRecordId,
|
|
957
|
-
showBack:
|
|
971
|
+
showBack: !embedded,
|
|
958
972
|
onBack: 'history',
|
|
959
973
|
// Hide the Edit button for objects whose lifecycle isn't user-managed
|
|
960
974
|
// (approval requests, audit logs, better-auth tables, …). The
|
|
@@ -985,7 +999,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
985
999
|
}),
|
|
986
1000
|
};
|
|
987
1001
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
988
|
-
}, [objectDef?.name, pureRecordId, childRelatedData, actionRefreshKey, appName, navigate, dataSource, t, objectLabel, objects, historyEnabled, historyEntries, historyLoading, approvals.available, approvals.processes, approvals.canSubmit, approvals.canRecall, approvals.pendingRequest, approvals.latestRequest]);
|
|
1002
|
+
}, [objectDef?.name, pureRecordId, childRelatedData, actionRefreshKey, appName, navigate, dataSource, t, objectLabel, objects, historyEnabled, historyEntries, historyLoading, approvals.available, approvals.processes, approvals.canSubmit, approvals.canRecall, approvals.pendingRequest, approvals.latestRequest, embedded]);
|
|
989
1003
|
if (isLoading) {
|
|
990
1004
|
return _jsx(SkeletonDetail, {});
|
|
991
1005
|
}
|
|
@@ -993,7 +1007,7 @@ export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
|
993
1007
|
return (_jsx("div", { className: "flex h-full items-center justify-center p-4", 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(Database, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: t('empty.objectNotFound') }), _jsx(EmptyDescription, { children: t('empty.objectNotFoundDescription', { name: objectName }) })] }) }));
|
|
994
1008
|
}
|
|
995
1009
|
if (assignedPage) {
|
|
996
|
-
return (_jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx("div", { className: "bg-background p-3 sm:p-4 lg:p-6", children: _jsx(SchemaRenderer, { schema: assignedPage }) }) }) }));
|
|
1010
|
+
return (_jsx(RecordContextProvider, { objectName: objectName, recordId: pureRecordId, data: pageRecord, objectSchema: objectDef, dataSource: dataSource, embedded: embedded, children: _jsx(ActionProvider, { context: { record: pageRecord || {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx("div", { className: "bg-background p-3 sm:p-4 lg:p-6", children: _jsx(SchemaRenderer, { schema: assignedPage }) }) }) }));
|
|
997
1011
|
}
|
|
998
1012
|
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsxs("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: [_jsx(ManagedByBadge, { managedBy: objectDef?.managedBy }), recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: t('recordDetail.viewersTooltip'), children: [_jsx(Users, { className: "h-3.5 w-3.5 text-muted-foreground" }), _jsx(PresenceAvatars, { users: recordViewers, size: "sm", maxVisible: 4, showStatus: true })] }))] }), _jsxs("div", { className: "flex-1 overflow-hidden flex flex-row", children: [_jsx("div", { className: "flex-1 overflow-auto p-3 sm:p-4 lg:p-6 scroll-pb-48", children: _jsx(ActionProvider, { context: { record: {}, objectName, user: currentUser }, onConfirm: confirmHandler, onToast: toastHandler, onNavigate: navigateHandler, onParamCollection: paramCollectionHandler, handlers: { api: apiHandler, flow: flowHandler, script: serverActionHandler, modal: serverActionHandler, approval: approvalHandler }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectLabel({ name: objectDef.name, label: objectDef.label }), onDataLoaded: (record) => {
|
|
999
1013
|
if (!record || typeof record !== 'object')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -27,34 +27,34 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"lucide-react": "^1.16.0",
|
|
29
29
|
"sonner": "^2.0.7",
|
|
30
|
-
"@object-ui/auth": "4.
|
|
31
|
-
"@object-ui/collaboration": "4.
|
|
32
|
-
"@object-ui/components": "4.
|
|
33
|
-
"@object-ui/core": "4.
|
|
34
|
-
"@object-ui/data-objectstack": "4.
|
|
35
|
-
"@object-ui/fields": "4.
|
|
36
|
-
"@object-ui/i18n": "4.
|
|
37
|
-
"@object-ui/layout": "4.
|
|
38
|
-
"@object-ui/permissions": "4.
|
|
39
|
-
"@object-ui/react": "4.
|
|
40
|
-
"@object-ui/types": "4.
|
|
30
|
+
"@object-ui/auth": "4.7.0",
|
|
31
|
+
"@object-ui/collaboration": "4.7.0",
|
|
32
|
+
"@object-ui/components": "4.7.0",
|
|
33
|
+
"@object-ui/core": "4.7.0",
|
|
34
|
+
"@object-ui/data-objectstack": "4.7.0",
|
|
35
|
+
"@object-ui/fields": "4.7.0",
|
|
36
|
+
"@object-ui/i18n": "4.7.0",
|
|
37
|
+
"@object-ui/layout": "4.7.0",
|
|
38
|
+
"@object-ui/permissions": "4.7.0",
|
|
39
|
+
"@object-ui/react": "4.7.0",
|
|
40
|
+
"@object-ui/types": "4.7.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
45
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
46
|
-
"@object-ui/plugin-calendar": "^4.
|
|
47
|
-
"@object-ui/plugin-charts": "^4.
|
|
48
|
-
"@object-ui/plugin-chatbot": "^4.
|
|
49
|
-
"@object-ui/plugin-dashboard": "^4.
|
|
50
|
-
"@object-ui/plugin-designer": "^4.
|
|
51
|
-
"@object-ui/plugin-detail": "^4.
|
|
52
|
-
"@object-ui/plugin-form": "^4.
|
|
53
|
-
"@object-ui/plugin-grid": "^4.
|
|
54
|
-
"@object-ui/plugin-kanban": "^4.
|
|
55
|
-
"@object-ui/plugin-list": "^4.
|
|
56
|
-
"@object-ui/plugin-report": "^4.
|
|
57
|
-
"@object-ui/plugin-view": "^4.
|
|
46
|
+
"@object-ui/plugin-calendar": "^4.7.0",
|
|
47
|
+
"@object-ui/plugin-charts": "^4.7.0",
|
|
48
|
+
"@object-ui/plugin-chatbot": "^4.7.0",
|
|
49
|
+
"@object-ui/plugin-dashboard": "^4.7.0",
|
|
50
|
+
"@object-ui/plugin-designer": "^4.7.0",
|
|
51
|
+
"@object-ui/plugin-detail": "^4.7.0",
|
|
52
|
+
"@object-ui/plugin-form": "^4.7.0",
|
|
53
|
+
"@object-ui/plugin-grid": "^4.7.0",
|
|
54
|
+
"@object-ui/plugin-kanban": "^4.7.0",
|
|
55
|
+
"@object-ui/plugin-list": "^4.7.0",
|
|
56
|
+
"@object-ui/plugin-report": "^4.7.0",
|
|
57
|
+
"@object-ui/plugin-view": "^4.7.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^25.9.0",
|