@object-ui/app-shell 3.3.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +22 -2
  3. package/dist/chrome/CommandPalette.d.ts +16 -0
  4. package/dist/chrome/CommandPalette.js +74 -0
  5. package/dist/chrome/ConditionalAuthWrapper.d.ts +23 -0
  6. package/dist/chrome/ConditionalAuthWrapper.js +135 -0
  7. package/dist/chrome/ConsoleToaster.d.ts +11 -0
  8. package/dist/chrome/ConsoleToaster.js +28 -0
  9. package/dist/chrome/ErrorBoundary.d.ts +39 -0
  10. package/dist/chrome/ErrorBoundary.js +64 -0
  11. package/dist/chrome/KeyboardShortcutsDialog.d.ts +7 -0
  12. package/dist/chrome/KeyboardShortcutsDialog.js +63 -0
  13. package/dist/chrome/LoadingScreen.d.ts +12 -0
  14. package/dist/chrome/LoadingScreen.js +44 -0
  15. package/dist/chrome/OnboardingWalkthrough.d.ts +9 -0
  16. package/dist/chrome/OnboardingWalkthrough.js +11 -0
  17. package/dist/chrome/ThemeProvider.d.ts +13 -0
  18. package/dist/chrome/ThemeProvider.js +37 -0
  19. package/dist/chrome/index.d.ts +8 -0
  20. package/dist/chrome/index.js +8 -0
  21. package/dist/console/AppContent.d.ts +26 -0
  22. package/dist/console/AppContent.js +266 -0
  23. package/dist/console/ConsoleShell.d.ts +66 -0
  24. package/dist/console/ConsoleShell.js +97 -0
  25. package/dist/console/auth/AuthPageLayout.d.ts +9 -0
  26. package/dist/console/auth/AuthPageLayout.js +4 -0
  27. package/dist/console/auth/ForgotPasswordPage.d.ts +4 -0
  28. package/dist/console/auth/ForgotPasswordPage.js +23 -0
  29. package/dist/console/auth/LoginPage.d.ts +4 -0
  30. package/dist/console/auth/LoginPage.js +24 -0
  31. package/dist/console/auth/RegisterPage.d.ts +4 -0
  32. package/dist/console/auth/RegisterPage.js +29 -0
  33. package/dist/console/auth/index.d.ts +4 -0
  34. package/dist/console/auth/index.js +4 -0
  35. package/dist/console/home/AppCard.d.ts +14 -0
  36. package/dist/console/home/AppCard.js +36 -0
  37. package/dist/console/home/HomeLayout.d.ts +15 -0
  38. package/dist/console/home/HomeLayout.js +20 -0
  39. package/dist/console/home/HomePage.d.ts +18 -0
  40. package/dist/console/home/HomePage.js +54 -0
  41. package/dist/console/home/QuickActions.d.ts +9 -0
  42. package/dist/console/home/QuickActions.js +53 -0
  43. package/dist/console/home/RecentApps.d.ts +13 -0
  44. package/dist/console/home/RecentApps.js +32 -0
  45. package/dist/console/home/StarredApps.d.ts +13 -0
  46. package/dist/console/home/StarredApps.js +29 -0
  47. package/dist/console/home/index.d.ts +11 -0
  48. package/dist/console/home/index.js +11 -0
  49. package/dist/console/organizations/CreateWorkspaceDialog.d.ts +16 -0
  50. package/dist/console/organizations/CreateWorkspaceDialog.js +71 -0
  51. package/dist/console/organizations/OrganizationsLayout.d.ts +15 -0
  52. package/dist/console/organizations/OrganizationsLayout.js +20 -0
  53. package/dist/console/organizations/OrganizationsPage.d.ts +9 -0
  54. package/dist/console/organizations/OrganizationsPage.js +76 -0
  55. package/dist/console/organizations/index.d.ts +8 -0
  56. package/dist/console/organizations/index.js +8 -0
  57. package/dist/console/organizations/manage/AcceptInvitationPage.d.ts +7 -0
  58. package/dist/console/organizations/manage/AcceptInvitationPage.js +100 -0
  59. package/dist/console/organizations/manage/InvitationsPage.d.ts +6 -0
  60. package/dist/console/organizations/manage/InvitationsPage.js +107 -0
  61. package/dist/console/organizations/manage/InviteMemberDialog.d.ts +16 -0
  62. package/dist/console/organizations/manage/InviteMemberDialog.js +83 -0
  63. package/dist/console/organizations/manage/MembersPage.d.ts +6 -0
  64. package/dist/console/organizations/manage/MembersPage.js +89 -0
  65. package/dist/console/organizations/manage/OrganizationLayout.d.ts +8 -0
  66. package/dist/console/organizations/manage/OrganizationLayout.js +51 -0
  67. package/dist/console/organizations/manage/SettingsPage.d.ts +6 -0
  68. package/dist/console/organizations/manage/SettingsPage.js +141 -0
  69. package/dist/console/organizations/manage/orgContext.d.ts +5 -0
  70. package/dist/console/organizations/manage/orgContext.js +4 -0
  71. package/dist/context/FavoritesProvider.d.ts +50 -0
  72. package/dist/context/FavoritesProvider.js +129 -0
  73. package/dist/context/NavigationContext.d.ts +46 -0
  74. package/dist/context/NavigationContext.js +59 -0
  75. package/dist/context/index.d.ts +3 -0
  76. package/dist/context/index.js +2 -0
  77. package/dist/hooks/index.d.ts +7 -0
  78. package/dist/hooks/index.js +7 -0
  79. package/dist/hooks/useFavorites.d.ts +13 -0
  80. package/dist/hooks/useFavorites.js +13 -0
  81. package/dist/hooks/useMetadataService.d.ts +13 -0
  82. package/dist/hooks/useMetadataService.js +18 -0
  83. package/dist/hooks/useNavPins.d.ts +16 -0
  84. package/dist/hooks/useNavPins.js +72 -0
  85. package/dist/hooks/useNavigationSync.d.ts +82 -0
  86. package/dist/hooks/useNavigationSync.js +495 -0
  87. package/dist/hooks/useObjectActions.d.ts +38 -0
  88. package/dist/hooks/useObjectActions.js +92 -0
  89. package/dist/hooks/useRecentItems.d.ts +21 -0
  90. package/dist/hooks/useRecentItems.js +50 -0
  91. package/dist/hooks/useResponsiveSidebar.d.ts +8 -0
  92. package/dist/hooks/useResponsiveSidebar.js +28 -0
  93. package/dist/index.d.ts +29 -0
  94. package/dist/index.js +35 -0
  95. package/dist/layout/ActivityFeed.d.ts +22 -0
  96. package/dist/layout/ActivityFeed.js +57 -0
  97. package/dist/layout/AppHeader.d.ts +33 -0
  98. package/dist/layout/AppHeader.js +182 -0
  99. package/dist/layout/AppSidebar.d.ts +14 -0
  100. package/dist/layout/AppSidebar.js +202 -0
  101. package/dist/layout/AppSwitcher.d.ts +12 -0
  102. package/dist/layout/AppSwitcher.js +24 -0
  103. package/dist/layout/AuthPageLayout.d.ts +9 -0
  104. package/dist/layout/AuthPageLayout.js +4 -0
  105. package/dist/layout/ConnectionStatus.d.ts +14 -0
  106. package/dist/layout/ConnectionStatus.js +36 -0
  107. package/dist/layout/ConsoleFloatingChatbot.d.ts +10 -0
  108. package/dist/layout/ConsoleFloatingChatbot.js +27 -0
  109. package/dist/layout/ConsoleLayout.d.ts +22 -0
  110. package/dist/layout/ConsoleLayout.js +49 -0
  111. package/dist/layout/LocaleSwitcher.d.ts +7 -0
  112. package/dist/layout/LocaleSwitcher.js +27 -0
  113. package/dist/layout/ModeToggle.d.ts +1 -0
  114. package/dist/layout/ModeToggle.js +10 -0
  115. package/dist/layout/UnifiedSidebar.d.ts +27 -0
  116. package/dist/layout/UnifiedSidebar.js +186 -0
  117. package/dist/layout/index.d.ts +11 -0
  118. package/dist/layout/index.js +10 -0
  119. package/dist/providers/AdapterProvider.d.ts +21 -0
  120. package/dist/providers/AdapterProvider.js +52 -0
  121. package/dist/providers/ExpressionProvider.d.ts +50 -0
  122. package/dist/providers/ExpressionProvider.js +67 -0
  123. package/dist/providers/MetadataProvider.d.ts +18 -0
  124. package/dist/providers/MetadataProvider.js +324 -0
  125. package/dist/services/MetadataService.d.ts +119 -0
  126. package/dist/services/MetadataService.js +211 -0
  127. package/dist/skeletons/SkeletonDashboard.d.ts +13 -0
  128. package/dist/skeletons/SkeletonDashboard.js +12 -0
  129. package/dist/skeletons/SkeletonDetail.d.ts +15 -0
  130. package/dist/skeletons/SkeletonDetail.js +12 -0
  131. package/dist/skeletons/SkeletonGrid.d.ts +15 -0
  132. package/dist/skeletons/SkeletonGrid.js +12 -0
  133. package/dist/skeletons/index.d.ts +3 -0
  134. package/dist/skeletons/index.js +3 -0
  135. package/dist/utils/getIcon.d.ts +24 -0
  136. package/dist/utils/getIcon.js +87 -0
  137. package/dist/utils/index.d.ts +33 -0
  138. package/dist/utils/index.js +62 -0
  139. package/dist/utils/metadataConverters.d.ts +78 -0
  140. package/dist/utils/metadataConverters.js +74 -0
  141. package/dist/views/ActionConfirmDialog.d.ts +22 -0
  142. package/dist/views/ActionConfirmDialog.js +22 -0
  143. package/dist/views/ActionParamDialog.d.ts +23 -0
  144. package/dist/views/ActionParamDialog.js +67 -0
  145. package/dist/views/DashboardView.d.ts +9 -0
  146. package/dist/views/DashboardView.js +357 -0
  147. package/dist/views/DesignDrawer.d.ts +28 -0
  148. package/dist/views/DesignDrawer.js +51 -0
  149. package/dist/views/MetadataInspector.d.ts +40 -0
  150. package/dist/views/MetadataInspector.js +69 -0
  151. package/dist/views/ObjectView.d.ts +11 -0
  152. package/dist/views/ObjectView.js +769 -0
  153. package/dist/views/PageView.d.ts +6 -0
  154. package/dist/views/PageView.js +49 -0
  155. package/dist/views/RecordDetailView.d.ts +14 -0
  156. package/dist/views/RecordDetailView.js +508 -0
  157. package/dist/views/ReportView.d.ts +4 -0
  158. package/dist/views/ReportView.js +285 -0
  159. package/dist/views/SearchResultsPage.d.ts +9 -0
  160. package/dist/views/SearchResultsPage.js +107 -0
  161. package/dist/views/ViewConfigPanel.d.ts +56 -0
  162. package/dist/views/ViewConfigPanel.js +91 -0
  163. package/dist/views/index.d.ts +11 -0
  164. package/dist/views/index.js +11 -0
  165. package/package.json +59 -11
  166. package/.turbo/turbo-build.log +0 -4
  167. package/src/components/AppShell.tsx +0 -31
  168. package/src/components/DashboardRenderer.tsx +0 -36
  169. package/src/components/FormRenderer.tsx +0 -71
  170. package/src/components/ObjectRenderer.tsx +0 -122
  171. package/src/components/PageRenderer.tsx +0 -28
  172. package/src/index.ts +0 -20
  173. package/src/types.ts +0 -78
  174. package/tsconfig.json +0 -20
  175. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Page View Component
3
+ * Renders a custom page based on the pageName parameter.
4
+ * Edit opens a right-side drawer with PageCanvasEditor for real-time preview.
5
+ */
6
+ export declare function PageView(): import("react/jsx-runtime").JSX.Element;
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ import type { DataSource } from '@object-ui/types';
2
+ export declare function ReportView({ dataSource }: {
3
+ dataSource?: DataSource;
4
+ }): import("react/jsx-runtime").JSX.Element;