@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.
- package/CHANGELOG.md +73 -0
- package/README.md +22 -2
- package/dist/chrome/CommandPalette.d.ts +16 -0
- package/dist/chrome/CommandPalette.js +74 -0
- package/dist/chrome/ConditionalAuthWrapper.d.ts +23 -0
- package/dist/chrome/ConditionalAuthWrapper.js +135 -0
- package/dist/chrome/ConsoleToaster.d.ts +11 -0
- package/dist/chrome/ConsoleToaster.js +28 -0
- package/dist/chrome/ErrorBoundary.d.ts +39 -0
- package/dist/chrome/ErrorBoundary.js +64 -0
- package/dist/chrome/KeyboardShortcutsDialog.d.ts +7 -0
- package/dist/chrome/KeyboardShortcutsDialog.js +63 -0
- package/dist/chrome/LoadingScreen.d.ts +12 -0
- package/dist/chrome/LoadingScreen.js +44 -0
- package/dist/chrome/OnboardingWalkthrough.d.ts +9 -0
- package/dist/chrome/OnboardingWalkthrough.js +11 -0
- package/dist/chrome/ThemeProvider.d.ts +13 -0
- package/dist/chrome/ThemeProvider.js +37 -0
- package/dist/chrome/index.d.ts +8 -0
- package/dist/chrome/index.js +8 -0
- package/dist/console/AppContent.d.ts +26 -0
- package/dist/console/AppContent.js +266 -0
- package/dist/console/ConsoleShell.d.ts +66 -0
- package/dist/console/ConsoleShell.js +97 -0
- package/dist/console/auth/AuthPageLayout.d.ts +9 -0
- package/dist/console/auth/AuthPageLayout.js +4 -0
- package/dist/console/auth/ForgotPasswordPage.d.ts +4 -0
- package/dist/console/auth/ForgotPasswordPage.js +23 -0
- package/dist/console/auth/LoginPage.d.ts +4 -0
- package/dist/console/auth/LoginPage.js +24 -0
- package/dist/console/auth/RegisterPage.d.ts +4 -0
- package/dist/console/auth/RegisterPage.js +29 -0
- package/dist/console/auth/index.d.ts +4 -0
- package/dist/console/auth/index.js +4 -0
- package/dist/console/home/AppCard.d.ts +14 -0
- package/dist/console/home/AppCard.js +36 -0
- package/dist/console/home/HomeLayout.d.ts +15 -0
- package/dist/console/home/HomeLayout.js +20 -0
- package/dist/console/home/HomePage.d.ts +18 -0
- package/dist/console/home/HomePage.js +54 -0
- package/dist/console/home/QuickActions.d.ts +9 -0
- package/dist/console/home/QuickActions.js +53 -0
- package/dist/console/home/RecentApps.d.ts +13 -0
- package/dist/console/home/RecentApps.js +32 -0
- package/dist/console/home/StarredApps.d.ts +13 -0
- package/dist/console/home/StarredApps.js +29 -0
- package/dist/console/home/index.d.ts +11 -0
- package/dist/console/home/index.js +11 -0
- package/dist/console/organizations/CreateWorkspaceDialog.d.ts +16 -0
- package/dist/console/organizations/CreateWorkspaceDialog.js +71 -0
- package/dist/console/organizations/OrganizationsLayout.d.ts +15 -0
- package/dist/console/organizations/OrganizationsLayout.js +20 -0
- package/dist/console/organizations/OrganizationsPage.d.ts +9 -0
- package/dist/console/organizations/OrganizationsPage.js +76 -0
- package/dist/console/organizations/index.d.ts +8 -0
- package/dist/console/organizations/index.js +8 -0
- package/dist/console/organizations/manage/AcceptInvitationPage.d.ts +7 -0
- package/dist/console/organizations/manage/AcceptInvitationPage.js +100 -0
- package/dist/console/organizations/manage/InvitationsPage.d.ts +6 -0
- package/dist/console/organizations/manage/InvitationsPage.js +107 -0
- package/dist/console/organizations/manage/InviteMemberDialog.d.ts +16 -0
- package/dist/console/organizations/manage/InviteMemberDialog.js +83 -0
- package/dist/console/organizations/manage/MembersPage.d.ts +6 -0
- package/dist/console/organizations/manage/MembersPage.js +89 -0
- package/dist/console/organizations/manage/OrganizationLayout.d.ts +8 -0
- package/dist/console/organizations/manage/OrganizationLayout.js +51 -0
- package/dist/console/organizations/manage/SettingsPage.d.ts +6 -0
- package/dist/console/organizations/manage/SettingsPage.js +141 -0
- package/dist/console/organizations/manage/orgContext.d.ts +5 -0
- package/dist/console/organizations/manage/orgContext.js +4 -0
- package/dist/context/FavoritesProvider.d.ts +50 -0
- package/dist/context/FavoritesProvider.js +129 -0
- package/dist/context/NavigationContext.d.ts +46 -0
- package/dist/context/NavigationContext.js +59 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.js +2 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/useFavorites.d.ts +13 -0
- package/dist/hooks/useFavorites.js +13 -0
- package/dist/hooks/useMetadataService.d.ts +13 -0
- package/dist/hooks/useMetadataService.js +18 -0
- package/dist/hooks/useNavPins.d.ts +16 -0
- package/dist/hooks/useNavPins.js +72 -0
- package/dist/hooks/useNavigationSync.d.ts +82 -0
- package/dist/hooks/useNavigationSync.js +495 -0
- package/dist/hooks/useObjectActions.d.ts +38 -0
- package/dist/hooks/useObjectActions.js +92 -0
- package/dist/hooks/useRecentItems.d.ts +21 -0
- package/dist/hooks/useRecentItems.js +50 -0
- package/dist/hooks/useResponsiveSidebar.d.ts +8 -0
- package/dist/hooks/useResponsiveSidebar.js +28 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +35 -0
- package/dist/layout/ActivityFeed.d.ts +22 -0
- package/dist/layout/ActivityFeed.js +57 -0
- package/dist/layout/AppHeader.d.ts +33 -0
- package/dist/layout/AppHeader.js +182 -0
- package/dist/layout/AppSidebar.d.ts +14 -0
- package/dist/layout/AppSidebar.js +202 -0
- package/dist/layout/AppSwitcher.d.ts +12 -0
- package/dist/layout/AppSwitcher.js +24 -0
- package/dist/layout/AuthPageLayout.d.ts +9 -0
- package/dist/layout/AuthPageLayout.js +4 -0
- package/dist/layout/ConnectionStatus.d.ts +14 -0
- package/dist/layout/ConnectionStatus.js +36 -0
- package/dist/layout/ConsoleFloatingChatbot.d.ts +10 -0
- package/dist/layout/ConsoleFloatingChatbot.js +27 -0
- package/dist/layout/ConsoleLayout.d.ts +22 -0
- package/dist/layout/ConsoleLayout.js +49 -0
- package/dist/layout/LocaleSwitcher.d.ts +7 -0
- package/dist/layout/LocaleSwitcher.js +27 -0
- package/dist/layout/ModeToggle.d.ts +1 -0
- package/dist/layout/ModeToggle.js +10 -0
- package/dist/layout/UnifiedSidebar.d.ts +27 -0
- package/dist/layout/UnifiedSidebar.js +186 -0
- package/dist/layout/index.d.ts +11 -0
- package/dist/layout/index.js +10 -0
- package/dist/providers/AdapterProvider.d.ts +21 -0
- package/dist/providers/AdapterProvider.js +52 -0
- package/dist/providers/ExpressionProvider.d.ts +50 -0
- package/dist/providers/ExpressionProvider.js +67 -0
- package/dist/providers/MetadataProvider.d.ts +18 -0
- package/dist/providers/MetadataProvider.js +324 -0
- package/dist/services/MetadataService.d.ts +119 -0
- package/dist/services/MetadataService.js +211 -0
- package/dist/skeletons/SkeletonDashboard.d.ts +13 -0
- package/dist/skeletons/SkeletonDashboard.js +12 -0
- package/dist/skeletons/SkeletonDetail.d.ts +15 -0
- package/dist/skeletons/SkeletonDetail.js +12 -0
- package/dist/skeletons/SkeletonGrid.d.ts +15 -0
- package/dist/skeletons/SkeletonGrid.js +12 -0
- package/dist/skeletons/index.d.ts +3 -0
- package/dist/skeletons/index.js +3 -0
- package/dist/utils/getIcon.d.ts +24 -0
- package/dist/utils/getIcon.js +87 -0
- package/dist/utils/index.d.ts +33 -0
- package/dist/utils/index.js +62 -0
- package/dist/utils/metadataConverters.d.ts +78 -0
- package/dist/utils/metadataConverters.js +74 -0
- package/dist/views/ActionConfirmDialog.d.ts +22 -0
- package/dist/views/ActionConfirmDialog.js +22 -0
- package/dist/views/ActionParamDialog.d.ts +23 -0
- package/dist/views/ActionParamDialog.js +67 -0
- package/dist/views/DashboardView.d.ts +9 -0
- package/dist/views/DashboardView.js +357 -0
- package/dist/views/DesignDrawer.d.ts +28 -0
- package/dist/views/DesignDrawer.js +51 -0
- package/dist/views/MetadataInspector.d.ts +40 -0
- package/dist/views/MetadataInspector.js +69 -0
- package/dist/views/ObjectView.d.ts +11 -0
- package/dist/views/ObjectView.js +769 -0
- package/dist/views/PageView.d.ts +6 -0
- package/dist/views/PageView.js +49 -0
- package/dist/views/RecordDetailView.d.ts +14 -0
- package/dist/views/RecordDetailView.js +508 -0
- package/dist/views/ReportView.d.ts +4 -0
- package/dist/views/ReportView.js +285 -0
- package/dist/views/SearchResultsPage.d.ts +9 -0
- package/dist/views/SearchResultsPage.js +107 -0
- package/dist/views/ViewConfigPanel.d.ts +56 -0
- package/dist/views/ViewConfigPanel.js +91 -0
- package/dist/views/index.d.ts +11 -0
- package/dist/views/index.js +11 -0
- package/package.json +59 -11
- package/.turbo/turbo-build.log +0 -4
- package/src/components/AppShell.tsx +0 -31
- package/src/components/DashboardRenderer.tsx +0 -36
- package/src/components/FormRenderer.tsx +0 -71
- package/src/components/ObjectRenderer.tsx +0 -122
- package/src/components/PageRenderer.tsx +0 -28
- package/src/index.ts +0 -20
- package/src/types.ts +0 -78
- package/tsconfig.json +0 -20
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Page View Component
|
|
4
|
+
* Renders a custom page based on the pageName parameter.
|
|
5
|
+
* Edit opens a right-side drawer with PageCanvasEditor for real-time preview.
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useCallback, lazy, Suspense } from 'react';
|
|
8
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
9
|
+
import { SchemaRenderer } from '@object-ui/react';
|
|
10
|
+
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
11
|
+
import { FileText, Pencil } from 'lucide-react';
|
|
12
|
+
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
13
|
+
import { useMetadata } from '../providers/MetadataProvider';
|
|
14
|
+
import { DesignDrawer } from './DesignDrawer';
|
|
15
|
+
const PageCanvasEditor = lazy(() => import('@object-ui/plugin-designer').then((m) => ({ default: m.PageCanvasEditor })));
|
|
16
|
+
export function PageView() {
|
|
17
|
+
const { pageName } = useParams();
|
|
18
|
+
const [searchParams] = useSearchParams();
|
|
19
|
+
const { showDebug } = useMetadataInspector();
|
|
20
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
21
|
+
// Find page definition from API-driven metadata
|
|
22
|
+
const { pages } = useMetadata();
|
|
23
|
+
const page = pages?.find((p) => p.name === pageName);
|
|
24
|
+
// Local schema state for live preview — initialized from metadata
|
|
25
|
+
const [editSchema, setEditSchema] = useState(null);
|
|
26
|
+
const handleOpenDrawer = useCallback(() => {
|
|
27
|
+
// Snapshot the current page definition as starting schema
|
|
28
|
+
setEditSchema(page);
|
|
29
|
+
setDrawerOpen(true);
|
|
30
|
+
}, [page]);
|
|
31
|
+
const handleCloseDrawer = useCallback((open) => {
|
|
32
|
+
setDrawerOpen(open);
|
|
33
|
+
if (!open) {
|
|
34
|
+
// Drawer closed — keep the latest saved schema (already persisted via auto-save)
|
|
35
|
+
}
|
|
36
|
+
}, []);
|
|
37
|
+
if (!page) {
|
|
38
|
+
return (_jsx("div", { className: "h-full flex items-center justify-center p-8", 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(FileText, { className: "h-6 w-6 text-muted-foreground" }) }), _jsx(EmptyTitle, { children: "Page Not Found" }), _jsxs(EmptyDescription, { children: ["The page \"", pageName, "\" could not be found. It may have been removed or renamed."] })] }) }));
|
|
39
|
+
}
|
|
40
|
+
// Convert URL search params to an object for the page context
|
|
41
|
+
const params = Object.fromEntries(searchParams.entries());
|
|
42
|
+
// Use the live-edited schema for preview when the drawer is open
|
|
43
|
+
const previewSchema = drawerOpen && editSchema ? editSchema : page;
|
|
44
|
+
return (_jsxs("div", { className: "flex flex-row h-full w-full overflow-hidden relative", children: [_jsxs("div", { className: "flex-1 overflow-auto h-full relative group", children: [_jsx("div", { className: `absolute top-1.5 sm:top-2 right-1.5 sm:right-2 z-50 flex items-center gap-1.5 transition-opacity ${showDebug ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`, children: _jsxs("button", { type: "button", onClick: handleOpenDrawer, className: "inline-flex items-center gap-1.5 rounded-md border border-input bg-background px-2.5 py-1.5 text-xs font-medium text-muted-foreground shadow-sm hover:bg-accent hover:text-accent-foreground", "data-testid": "page-edit-button", children: [_jsx(Pencil, { className: "h-3.5 w-3.5" }), "Edit"] }) }), _jsx(SchemaRenderer, { schema: {
|
|
45
|
+
...previewSchema,
|
|
46
|
+
type: previewSchema.type || 'page',
|
|
47
|
+
context: { ...previewSchema.context, params }
|
|
48
|
+
} })] }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'Page Configuration', data: previewSchema }] }), _jsx(DesignDrawer, { open: drawerOpen, onOpenChange: handleCloseDrawer, title: `Edit Page: ${page.label || page.title || pageName}`, schema: editSchema || page, onSchemaChange: setEditSchema, collection: "sys_page", recordName: pageName, children: (schema, onChange) => (_jsx(Suspense, { fallback: _jsx("div", { className: "p-4 text-muted-foreground", children: "Loading editor\u2026" }), children: _jsx(PageCanvasEditor, { schema: schema, onChange: onChange }) })) })] }));
|
|
49
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecordDetailView Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a detail view for a single record, resolved by URL params.
|
|
5
|
+
* Uses the DetailView plugin component with auto-generated sections from
|
|
6
|
+
* the object field definitions.
|
|
7
|
+
*/
|
|
8
|
+
interface RecordDetailViewProps {
|
|
9
|
+
dataSource: any;
|
|
10
|
+
objects: any[];
|
|
11
|
+
onEdit: (record: any) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function RecordDetailView({ dataSource, objects, onEdit }: RecordDetailViewProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* RecordDetailView Component
|
|
4
|
+
*
|
|
5
|
+
* Renders a detail view for a single record, resolved by URL params.
|
|
6
|
+
* Uses the DetailView plugin component with auto-generated sections from
|
|
7
|
+
* the object field definitions.
|
|
8
|
+
*/
|
|
9
|
+
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
10
|
+
import { useParams, useNavigate } from 'react-router-dom';
|
|
11
|
+
import { DetailView, RecordChatterPanel } from '@object-ui/plugin-detail';
|
|
12
|
+
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
|
|
13
|
+
import { PresenceAvatars } from '@object-ui/collaboration';
|
|
14
|
+
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
15
|
+
import { ActionProvider, useObjectTranslation } from '@object-ui/react';
|
|
16
|
+
import { toast } from 'sonner';
|
|
17
|
+
import { Database, Users } from 'lucide-react';
|
|
18
|
+
import { MetadataPanel, useMetadataInspector } from './MetadataInspector';
|
|
19
|
+
import { SkeletonDetail } from '../skeletons';
|
|
20
|
+
import { ActionConfirmDialog } from './ActionConfirmDialog';
|
|
21
|
+
import { ActionParamDialog } from './ActionParamDialog';
|
|
22
|
+
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
|
|
23
|
+
const FALLBACK_USER = { id: 'current-user', name: 'Demo User' };
|
|
24
|
+
export function RecordDetailView({ dataSource, objects, onEdit }) {
|
|
25
|
+
const { objectName, recordId } = useParams();
|
|
26
|
+
const { showDebug } = useMetadataInspector();
|
|
27
|
+
const { user } = useAuth();
|
|
28
|
+
const navigate = useNavigate();
|
|
29
|
+
const { t } = useObjectTranslation();
|
|
30
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
31
|
+
const [feedItems, setFeedItems] = useState([]);
|
|
32
|
+
const [recordViewers, setRecordViewers] = useState([]);
|
|
33
|
+
const [actionRefreshKey, setActionRefreshKey] = useState(0);
|
|
34
|
+
const [childRelatedData, setChildRelatedData] = useState({});
|
|
35
|
+
const [recordTitle, setRecordTitle] = useState();
|
|
36
|
+
const objectDef = objects.find((o) => o.name === objectName);
|
|
37
|
+
// Publish record title to the navigation context so the top-bar breadcrumb
|
|
38
|
+
// can display "Acme Platform Upgrade" instead of "#9U1_MmmxjiGR…".
|
|
39
|
+
useRecordBreadcrumbTitle(recordTitle);
|
|
40
|
+
// Use the URL recordId as-is — it contains the actual record id.
|
|
41
|
+
// Navigation code passes `record.id || record._id` directly into the URL
|
|
42
|
+
// without adding any prefix, so no stripping is needed.
|
|
43
|
+
const pureRecordId = recordId;
|
|
44
|
+
// ─── Action Provider Handlers ───────────────────────────────────────
|
|
45
|
+
// Confirm dialog state (promise-based)
|
|
46
|
+
const [confirmState, setConfirmState] = useState({ open: false, message: '' });
|
|
47
|
+
// Param collection dialog state (promise-based)
|
|
48
|
+
const [paramState, setParamState] = useState({ open: false, params: [] });
|
|
49
|
+
const confirmHandler = useCallback((message, options) => {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
setConfirmState({ open: true, message, options, resolve });
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
54
|
+
const paramCollectionHandler = useCallback((params) => {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
setParamState({ open: true, params, resolve });
|
|
57
|
+
});
|
|
58
|
+
}, []);
|
|
59
|
+
const toastHandler = useCallback((message, options) => {
|
|
60
|
+
if (options?.type === 'error')
|
|
61
|
+
toast.error(message);
|
|
62
|
+
else
|
|
63
|
+
toast.success(message);
|
|
64
|
+
}, []);
|
|
65
|
+
const navigateHandler = useCallback((url, options) => {
|
|
66
|
+
if (options?.external || options?.newTab) {
|
|
67
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
navigate(url);
|
|
71
|
+
}
|
|
72
|
+
}, [navigate]);
|
|
73
|
+
// API action handler — maps logical action targets to dataSource operations
|
|
74
|
+
const apiHandler = useCallback(async (action) => {
|
|
75
|
+
try {
|
|
76
|
+
const target = action.target || action.name;
|
|
77
|
+
const params = action.params || {};
|
|
78
|
+
switch (target) {
|
|
79
|
+
case 'opportunity_change_stage':
|
|
80
|
+
await dataSource.update(objectName, pureRecordId, { stage: params.new_stage });
|
|
81
|
+
break;
|
|
82
|
+
case 'opportunity_mark_won':
|
|
83
|
+
await dataSource.update(objectName, pureRecordId, { stage: 'closed_won' });
|
|
84
|
+
break;
|
|
85
|
+
case 'opportunity_mark_lost':
|
|
86
|
+
await dataSource.update(objectName, pureRecordId, { stage: 'closed_lost', loss_reason: params.loss_reason });
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
// Generic: update record with collected params
|
|
90
|
+
if (Object.keys(params).length > 0) {
|
|
91
|
+
await dataSource.update(objectName, pureRecordId, params);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
const shouldRefresh = action.refreshAfter === true;
|
|
96
|
+
if (shouldRefresh) {
|
|
97
|
+
setActionRefreshKey(k => k + 1);
|
|
98
|
+
}
|
|
99
|
+
return { success: true, reload: shouldRefresh };
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return { success: false, error: error.message };
|
|
103
|
+
}
|
|
104
|
+
}, [dataSource, objectName, pureRecordId]);
|
|
105
|
+
// Authenticated fetch for direct backend calls (e.g. flow trigger).
|
|
106
|
+
const authFetch = useMemo(() => createAuthenticatedFetch(), []);
|
|
107
|
+
// Flow action handler — POST to /api/v1/automation/{name}/trigger.
|
|
108
|
+
// Triggered when an Action with `type: 'flow'` is invoked from a record-level
|
|
109
|
+
// location (record_header, record_more, …). The server-side automation
|
|
110
|
+
// engine resolves `{name}` against the registered flow definitions and
|
|
111
|
+
// returns `{success, output, durationMs}`.
|
|
112
|
+
const flowHandler = useCallback(async (action) => {
|
|
113
|
+
const flowName = action.target || action.name;
|
|
114
|
+
if (!flowName) {
|
|
115
|
+
return { success: false, error: 'No flow target provided for flow action' };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const baseUrl = import.meta.env.VITE_SERVER_URL || '';
|
|
119
|
+
const res = await authFetch(`${baseUrl}/api/v1/automation/${encodeURIComponent(flowName)}/trigger`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
recordId: pureRecordId,
|
|
124
|
+
objectName,
|
|
125
|
+
params: action.params ?? {},
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
const json = await res.json().catch(() => null);
|
|
129
|
+
if (!res.ok || (json && json.success === false)) {
|
|
130
|
+
const errMsg = json?.error || `Flow "${flowName}" failed (HTTP ${res.status})`;
|
|
131
|
+
return { success: false, error: errMsg };
|
|
132
|
+
}
|
|
133
|
+
const shouldRefresh = action.refreshAfter !== false;
|
|
134
|
+
if (shouldRefresh) {
|
|
135
|
+
setActionRefreshKey(k => k + 1);
|
|
136
|
+
}
|
|
137
|
+
return { success: true, data: json?.data, reload: shouldRefresh };
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
return { success: false, error: error.message };
|
|
141
|
+
}
|
|
142
|
+
}, [authFetch, pureRecordId, objectName]);
|
|
143
|
+
// Server-side action handler — POST to /api/v1/actions/{object}/{action}.
|
|
144
|
+
// Used for `script` and `modal` actions where `action.target` matches a
|
|
145
|
+
// server-registered handler name (engine.registerAction). Sends the
|
|
146
|
+
// current recordId, objectName, and any collected/static params, and the
|
|
147
|
+
// server resolves the handler (with wildcard '*' fallback) and runs it.
|
|
148
|
+
const serverActionHandler = useCallback(async (action) => {
|
|
149
|
+
const targetName = action.target || action.name;
|
|
150
|
+
if (!targetName) {
|
|
151
|
+
return { success: false, error: 'No action target provided' };
|
|
152
|
+
}
|
|
153
|
+
const params = (action.params && !Array.isArray(action.params))
|
|
154
|
+
? action.params
|
|
155
|
+
: {};
|
|
156
|
+
try {
|
|
157
|
+
const baseUrl = import.meta.env.VITE_SERVER_URL || '';
|
|
158
|
+
const obj = action.objectName || objectName || 'global';
|
|
159
|
+
const res = await authFetch(`${baseUrl}/api/v1/actions/${encodeURIComponent(obj)}/${encodeURIComponent(targetName)}`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify({ recordId: pureRecordId, params }),
|
|
163
|
+
});
|
|
164
|
+
const json = await res.json().catch(() => null);
|
|
165
|
+
if (!res.ok || (json && json.success === false)) {
|
|
166
|
+
const errMsg = json?.error || `Action "${targetName}" failed (HTTP ${res.status})`;
|
|
167
|
+
return { success: false, error: errMsg };
|
|
168
|
+
}
|
|
169
|
+
const shouldRefresh = action.refreshAfter !== false;
|
|
170
|
+
if (shouldRefresh)
|
|
171
|
+
setActionRefreshKey(k => k + 1);
|
|
172
|
+
return { success: true, data: json?.data, reload: shouldRefresh };
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return { success: false, error: error.message };
|
|
176
|
+
}
|
|
177
|
+
}, [authFetch, pureRecordId, objectName]);
|
|
178
|
+
// Discover reverse references: other objects with lookup/master_detail fields
|
|
179
|
+
// pointing to the current object (e.g., order_item.order → order).
|
|
180
|
+
const childRelations = useMemo(() => {
|
|
181
|
+
if (!objectDef || !objects)
|
|
182
|
+
return [];
|
|
183
|
+
const relations = [];
|
|
184
|
+
for (const obj of objects) {
|
|
185
|
+
if (obj.name === objectDef.name)
|
|
186
|
+
continue;
|
|
187
|
+
for (const [fieldName, fieldDef] of Object.entries(obj.fields || {})) {
|
|
188
|
+
if (fieldDef &&
|
|
189
|
+
(fieldDef.type === 'lookup' || fieldDef.type === 'master_detail') &&
|
|
190
|
+
(fieldDef.reference_to || fieldDef.reference) === objectDef.name) {
|
|
191
|
+
relations.push({
|
|
192
|
+
childObject: obj.name,
|
|
193
|
+
childLabel: obj.label || obj.name,
|
|
194
|
+
referenceField: fieldName,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return relations;
|
|
200
|
+
}, [objectDef, objects]);
|
|
201
|
+
// Fetch related child records for each reverse reference
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!dataSource || !pureRecordId || childRelations.length === 0)
|
|
204
|
+
return;
|
|
205
|
+
let cancelled = false;
|
|
206
|
+
Promise.all(childRelations.map(({ childObject, referenceField }) => dataSource.find(childObject, {
|
|
207
|
+
$filter: { [referenceField]: pureRecordId },
|
|
208
|
+
})
|
|
209
|
+
.then((res) => {
|
|
210
|
+
const items = Array.isArray(res) ? res : res?.data || [];
|
|
211
|
+
return { childObject, items };
|
|
212
|
+
})
|
|
213
|
+
.catch((err) => {
|
|
214
|
+
console.warn(`[RecordDetailView] Failed to fetch related ${childObject}:`, err);
|
|
215
|
+
return { childObject, items: [] };
|
|
216
|
+
}))).then((results) => {
|
|
217
|
+
if (cancelled)
|
|
218
|
+
return;
|
|
219
|
+
const data = {};
|
|
220
|
+
for (const { childObject, items } of results) {
|
|
221
|
+
data[childObject] = items;
|
|
222
|
+
}
|
|
223
|
+
setChildRelatedData(data);
|
|
224
|
+
});
|
|
225
|
+
return () => { cancelled = true; };
|
|
226
|
+
}, [dataSource, pureRecordId, childRelations]);
|
|
227
|
+
const currentUser = user
|
|
228
|
+
? { id: user.id, name: user.name, avatar: user.image }
|
|
229
|
+
: FALLBACK_USER;
|
|
230
|
+
// Fetch presence and comments from API
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (!dataSource || !objectName || !pureRecordId)
|
|
233
|
+
return;
|
|
234
|
+
const threadId = `${objectName}:${pureRecordId}`;
|
|
235
|
+
// Fetch record viewers
|
|
236
|
+
dataSource.find('sys_presence', { $filter: { recordId: pureRecordId } })
|
|
237
|
+
.then((res) => { if (res.data?.length)
|
|
238
|
+
setRecordViewers(res.data); })
|
|
239
|
+
.catch(() => { });
|
|
240
|
+
// Fetch persisted comments and map to FeedItem[]
|
|
241
|
+
dataSource.find('sys_comment', { $filter: { threadId }, $orderby: { createdAt: 'asc' } })
|
|
242
|
+
.then((res) => {
|
|
243
|
+
if (res.data?.length) {
|
|
244
|
+
setFeedItems(res.data.map((c) => ({
|
|
245
|
+
id: c.id,
|
|
246
|
+
type: 'comment',
|
|
247
|
+
actor: c.author?.name ?? 'Unknown',
|
|
248
|
+
actorAvatarUrl: c.author?.avatar,
|
|
249
|
+
body: c.content,
|
|
250
|
+
createdAt: c.createdAt,
|
|
251
|
+
updatedAt: c.updatedAt,
|
|
252
|
+
parentId: c.parentId,
|
|
253
|
+
reactions: c.reactions
|
|
254
|
+
? Object.entries(c.reactions).map(([emoji, userIds]) => ({
|
|
255
|
+
emoji,
|
|
256
|
+
count: userIds.length,
|
|
257
|
+
reacted: userIds.includes(currentUser.id),
|
|
258
|
+
}))
|
|
259
|
+
: undefined,
|
|
260
|
+
})));
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
.catch(() => { });
|
|
264
|
+
}, [dataSource, objectName, pureRecordId, currentUser]);
|
|
265
|
+
const handleAddComment = useCallback(async (text) => {
|
|
266
|
+
const newItem = {
|
|
267
|
+
id: crypto.randomUUID(),
|
|
268
|
+
type: 'comment',
|
|
269
|
+
actor: currentUser.name,
|
|
270
|
+
actorAvatarUrl: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
271
|
+
body: text,
|
|
272
|
+
createdAt: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
setFeedItems(prev => [...prev, newItem]);
|
|
275
|
+
// Persist to backend
|
|
276
|
+
if (dataSource) {
|
|
277
|
+
const threadId = `${objectName}:${pureRecordId}`;
|
|
278
|
+
dataSource.create('sys_comment', {
|
|
279
|
+
id: newItem.id,
|
|
280
|
+
threadId,
|
|
281
|
+
author: currentUser,
|
|
282
|
+
content: text,
|
|
283
|
+
mentions: [],
|
|
284
|
+
createdAt: newItem.createdAt,
|
|
285
|
+
}).catch(() => { });
|
|
286
|
+
}
|
|
287
|
+
}, [currentUser, dataSource, objectName, pureRecordId]);
|
|
288
|
+
const handleAddReply = useCallback(async (parentId, text) => {
|
|
289
|
+
const newItem = {
|
|
290
|
+
id: crypto.randomUUID(),
|
|
291
|
+
type: 'comment',
|
|
292
|
+
actor: currentUser.name,
|
|
293
|
+
actorAvatarUrl: 'avatar' in currentUser ? currentUser.avatar : undefined,
|
|
294
|
+
body: text,
|
|
295
|
+
createdAt: new Date().toISOString(),
|
|
296
|
+
parentId,
|
|
297
|
+
};
|
|
298
|
+
setFeedItems(prev => {
|
|
299
|
+
const updated = [...prev, newItem];
|
|
300
|
+
// Increment replyCount on parent
|
|
301
|
+
return updated.map(item => item.id === parentId
|
|
302
|
+
? { ...item, replyCount: (item.replyCount ?? 0) + 1 }
|
|
303
|
+
: item);
|
|
304
|
+
});
|
|
305
|
+
if (dataSource) {
|
|
306
|
+
const threadId = `${objectName}:${pureRecordId}`;
|
|
307
|
+
dataSource.create('sys_comment', {
|
|
308
|
+
id: newItem.id,
|
|
309
|
+
threadId,
|
|
310
|
+
author: currentUser,
|
|
311
|
+
content: text,
|
|
312
|
+
mentions: [],
|
|
313
|
+
createdAt: newItem.createdAt,
|
|
314
|
+
parentId,
|
|
315
|
+
}).catch(() => { });
|
|
316
|
+
}
|
|
317
|
+
}, [currentUser, dataSource, objectName, pureRecordId]);
|
|
318
|
+
const handleToggleReaction = useCallback((itemId, emoji) => {
|
|
319
|
+
setFeedItems(prev => prev.map(item => {
|
|
320
|
+
if (item.id !== itemId)
|
|
321
|
+
return item;
|
|
322
|
+
const reactions = [...(item.reactions ?? [])];
|
|
323
|
+
const idx = reactions.findIndex(r => r.emoji === emoji);
|
|
324
|
+
if (idx >= 0) {
|
|
325
|
+
const r = reactions[idx];
|
|
326
|
+
if (r.reacted) {
|
|
327
|
+
// Remove user's reaction
|
|
328
|
+
if (r.count <= 1) {
|
|
329
|
+
reactions.splice(idx, 1);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
reactions[idx] = { ...r, count: r.count - 1, reacted: false };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
reactions[idx] = { ...r, count: r.count + 1, reacted: true };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
reactions.push({ emoji, count: 1, reacted: true });
|
|
341
|
+
}
|
|
342
|
+
const updated = { ...item, reactions };
|
|
343
|
+
// Persist reaction toggle to backend
|
|
344
|
+
if (dataSource) {
|
|
345
|
+
dataSource.update('sys_comment', String(itemId), {
|
|
346
|
+
$toggleReaction: { emoji, userId: currentUser.id },
|
|
347
|
+
}).catch(() => { });
|
|
348
|
+
}
|
|
349
|
+
return updated;
|
|
350
|
+
}));
|
|
351
|
+
}, [currentUser.id, dataSource]);
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
// Reset loading on navigation; the actual DetailView handles data fetching
|
|
354
|
+
setIsLoading(true);
|
|
355
|
+
queueMicrotask(() => setIsLoading(false));
|
|
356
|
+
}, [objectName, recordId]);
|
|
357
|
+
// Build detail schema — must be before early returns to keep hook count
|
|
358
|
+
// consistent across renders and avoid React error #310.
|
|
359
|
+
const detailSchema = useMemo(() => {
|
|
360
|
+
if (!objectDef) {
|
|
361
|
+
return { type: 'detail-view' };
|
|
362
|
+
}
|
|
363
|
+
// Auto-detect primary field: prefer objectDef metadata, then 'name' or 'title' heuristic
|
|
364
|
+
const primaryField = objectDef.primaryField
|
|
365
|
+
|| Object.keys(objectDef.fields || {}).find((key) => key === 'name' || key === 'title');
|
|
366
|
+
// Build sections: prefer form sections from objectDef, fallback to flat field list
|
|
367
|
+
const formSections = objectDef.views?.form?.sections;
|
|
368
|
+
const sections = formSections && formSections.length > 0
|
|
369
|
+
? formSections.map((sec) => ({
|
|
370
|
+
title: sec.title,
|
|
371
|
+
collapsible: sec.collapsible,
|
|
372
|
+
defaultCollapsed: sec.defaultCollapsed,
|
|
373
|
+
fields: (sec.fields || []).map((f) => {
|
|
374
|
+
const fieldName = typeof f === 'string' ? f : f.name;
|
|
375
|
+
const fieldDef = objectDef.fields[fieldName];
|
|
376
|
+
if (!fieldDef) {
|
|
377
|
+
console.warn(`[RecordDetailView] Field "${fieldName}" not found in ${objectDef.name} definition`);
|
|
378
|
+
return { name: fieldName, label: fieldName };
|
|
379
|
+
}
|
|
380
|
+
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
381
|
+
return {
|
|
382
|
+
name: fieldName,
|
|
383
|
+
label: fieldDef.label || fieldName,
|
|
384
|
+
type: fieldDef.type || 'text',
|
|
385
|
+
...(fieldDef.options && { options: fieldDef.options }),
|
|
386
|
+
...(refTarget && { reference_to: refTarget }),
|
|
387
|
+
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
388
|
+
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
389
|
+
};
|
|
390
|
+
}),
|
|
391
|
+
}))
|
|
392
|
+
: [
|
|
393
|
+
{
|
|
394
|
+
// Intentionally untitled: when there's only one auto-generated
|
|
395
|
+
// section, DetailSection flattens it (no Card chrome, no
|
|
396
|
+
// redundant "Details" heading).
|
|
397
|
+
showBorder: false,
|
|
398
|
+
fields: Object.keys(objectDef.fields || {}).map(key => {
|
|
399
|
+
const fieldDef = objectDef.fields[key];
|
|
400
|
+
const refTarget = fieldDef.reference_to || fieldDef.reference;
|
|
401
|
+
return {
|
|
402
|
+
name: key,
|
|
403
|
+
label: fieldDef.label || key,
|
|
404
|
+
type: fieldDef.type || 'text',
|
|
405
|
+
...(fieldDef.options && { options: fieldDef.options }),
|
|
406
|
+
...(refTarget && { reference_to: refTarget }),
|
|
407
|
+
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
|
|
408
|
+
...(fieldDef.currency && { currency: fieldDef.currency }),
|
|
409
|
+
};
|
|
410
|
+
}),
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
// Filter actions for record_header location and deduplicate by name
|
|
414
|
+
const recordHeaderActions = (() => {
|
|
415
|
+
const seen = new Set();
|
|
416
|
+
return (objectDef.actions || []).filter((a) => {
|
|
417
|
+
if (!a.locations?.includes('record_header'))
|
|
418
|
+
return false;
|
|
419
|
+
if (!a.name)
|
|
420
|
+
return true;
|
|
421
|
+
if (seen.has(a.name))
|
|
422
|
+
return false;
|
|
423
|
+
seen.add(a.name);
|
|
424
|
+
return true;
|
|
425
|
+
});
|
|
426
|
+
})();
|
|
427
|
+
// Build highlightFields: exclusively from objectDef metadata (no hardcoded fallback)
|
|
428
|
+
const highlightFields = objectDef.views?.detail?.highlightFields ?? [];
|
|
429
|
+
// Build sectionGroups from objectDef detail/form config if available
|
|
430
|
+
const sectionGroups = objectDef.views?.detail?.sectionGroups ?? objectDef.views?.form?.sectionGroups;
|
|
431
|
+
// Build related entries from reverse-reference child objects.
|
|
432
|
+
// `referenceField` is the FK field on the child pointing back to this
|
|
433
|
+
// record — passed so the related-list renderer can hide the redundant
|
|
434
|
+
// parent-ID column.
|
|
435
|
+
const related = childRelations.map(({ childObject, childLabel, referenceField }) => {
|
|
436
|
+
const childObjectDef = objects.find((o) => o.name === childObject);
|
|
437
|
+
return {
|
|
438
|
+
title: childLabel,
|
|
439
|
+
type: 'table',
|
|
440
|
+
api: childObject,
|
|
441
|
+
data: childRelatedData[childObject] || [],
|
|
442
|
+
referenceField,
|
|
443
|
+
icon: childObjectDef?.icon,
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
return {
|
|
447
|
+
type: 'detail-view',
|
|
448
|
+
objectName: objectDef.name,
|
|
449
|
+
resourceId: pureRecordId,
|
|
450
|
+
showBack: true,
|
|
451
|
+
onBack: 'history',
|
|
452
|
+
showEdit: true,
|
|
453
|
+
title: objectDef.label,
|
|
454
|
+
primaryField,
|
|
455
|
+
sections,
|
|
456
|
+
autoTabs: true,
|
|
457
|
+
autoDiscoverRelated: true,
|
|
458
|
+
...(related.length > 0 && { related }),
|
|
459
|
+
...(highlightFields.length > 0 && { highlightFields }),
|
|
460
|
+
...(sectionGroups && sectionGroups.length > 0 && { sectionGroups }),
|
|
461
|
+
...(recordHeaderActions.length > 0 && {
|
|
462
|
+
actions: [{
|
|
463
|
+
type: 'action:bar',
|
|
464
|
+
location: 'record_header',
|
|
465
|
+
actions: recordHeaderActions,
|
|
466
|
+
}],
|
|
467
|
+
}),
|
|
468
|
+
};
|
|
469
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
470
|
+
}, [objectDef?.name, pureRecordId, childRelatedData, actionRefreshKey]);
|
|
471
|
+
if (isLoading) {
|
|
472
|
+
return _jsx(SkeletonDetail, {});
|
|
473
|
+
}
|
|
474
|
+
if (!objectDef) {
|
|
475
|
+
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: "Object Not Found" }), _jsxs(EmptyDescription, { children: ["Object \"", objectName, "\" definition missing. Check your configuration or navigate back to select a valid object."] })] }) }));
|
|
476
|
+
}
|
|
477
|
+
return (_jsxs("div", { className: "h-full bg-background overflow-hidden flex flex-col relative", children: [_jsx("div", { className: "absolute top-2 sm:top-4 right-2 sm:right-4 z-50 flex items-center gap-2", children: recordViewers.length > 0 && (_jsxs("div", { className: "flex items-center gap-1.5", title: "Users viewing this record", 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 }, children: _jsx(DetailView, { schema: detailSchema, dataSource: dataSource, objectLabel: objectDef.label, onDataLoaded: (record) => {
|
|
478
|
+
if (!record || typeof record !== 'object')
|
|
479
|
+
return;
|
|
480
|
+
const primary = detailSchema.primaryField;
|
|
481
|
+
const candidates = [
|
|
482
|
+
primary && record[primary],
|
|
483
|
+
record.name,
|
|
484
|
+
record.title,
|
|
485
|
+
record.label,
|
|
486
|
+
record.subject,
|
|
487
|
+
];
|
|
488
|
+
const next = candidates.find((v) => typeof v === 'string' && v.trim().length > 0);
|
|
489
|
+
if (next && next !== recordTitle)
|
|
490
|
+
setRecordTitle(next);
|
|
491
|
+
}, onEdit: () => {
|
|
492
|
+
onEdit({ id: pureRecordId });
|
|
493
|
+
}, discussionSlot: _jsx(RecordChatterPanel, { config: {
|
|
494
|
+
position: 'bottom',
|
|
495
|
+
collapsible: false,
|
|
496
|
+
feed: {
|
|
497
|
+
enableReactions: true,
|
|
498
|
+
enableThreading: true,
|
|
499
|
+
showCommentInput: true,
|
|
500
|
+
},
|
|
501
|
+
}, items: feedItems, onAddComment: handleAddComment, onAddReply: handleAddReply, onToggleReaction: handleToggleReaction }) }, actionRefreshKey) }) }), _jsx(MetadataPanel, { open: showDebug, sections: [{ title: 'View Schema', data: detailSchema }] })] }), _jsx(ActionConfirmDialog, { state: confirmState, onOpenChange: (open) => {
|
|
502
|
+
if (!open)
|
|
503
|
+
setConfirmState(s => ({ ...s, open: false }));
|
|
504
|
+
} }), _jsx(ActionParamDialog, { state: paramState, onOpenChange: (open) => {
|
|
505
|
+
if (!open)
|
|
506
|
+
setParamState(s => ({ ...s, open: false }));
|
|
507
|
+
} })] }));
|
|
508
|
+
}
|