@object-ui/app-shell 3.3.0 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +22 -2
- package/dist/chrome/CommandPalette.d.ts +16 -0
- package/dist/chrome/CommandPalette.js +88 -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 +217 -0
- package/dist/layout/AppSwitcher.d.ts +12 -0
- package/dist/layout/AppSwitcher.js +39 -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/ConsoleLayout.d.ts +21 -0
- package/dist/layout/ConsoleLayout.js +71 -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 +22 -0
- package/dist/layout/UnifiedSidebar.js +210 -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 +10 -0
- package/dist/utils/getIcon.js +24 -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 +14 -0
- package/dist/views/ObjectView.js +770 -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 +284 -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,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNavigationSync
|
|
3
|
+
*
|
|
4
|
+
* Synchronizes the App navigation tree when Pages or Dashboards are
|
|
5
|
+
* created, deleted, or renamed. Pure utility helpers are exported for
|
|
6
|
+
* unit-testing; the React hook wires them to the adapter / metadata
|
|
7
|
+
* context and shows sonner toasts with an undo action.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { NavigationItem } from '@object-ui/types';
|
|
12
|
+
/** Generate a simple unique id for a new navigation item. */
|
|
13
|
+
export declare function generateNavId(prefix?: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Add a navigation item to the end of a navigation array (immutable).
|
|
16
|
+
*/
|
|
17
|
+
export declare function addNavigationItem(navigation: NavigationItem[], item: NavigationItem): NavigationItem[];
|
|
18
|
+
/**
|
|
19
|
+
* Recursively remove all navigation items that match a given type and name.
|
|
20
|
+
* Returns a new array (immutable).
|
|
21
|
+
*/
|
|
22
|
+
export declare function removeNavigationItems(navigation: NavigationItem[], type: 'page' | 'dashboard', name: string): NavigationItem[];
|
|
23
|
+
/**
|
|
24
|
+
* Recursively rename all navigation items that reference the old name.
|
|
25
|
+
* Returns a new array (immutable).
|
|
26
|
+
*/
|
|
27
|
+
export declare function renameNavigationItems(navigation: NavigationItem[], type: 'page' | 'dashboard', oldName: string, newName: string): NavigationItem[];
|
|
28
|
+
/**
|
|
29
|
+
* Shallow-structural equality check for two NavigationItem arrays.
|
|
30
|
+
* Works reliably because NavigationItem objects are plain serializable JSON
|
|
31
|
+
* whose key ordering is deterministic (we control object creation).
|
|
32
|
+
*/
|
|
33
|
+
export declare function navigationEqual(a: NavigationItem[], b: NavigationItem[]): boolean;
|
|
34
|
+
export interface UseNavigationSyncReturn {
|
|
35
|
+
/** Call after a new page has been created / saved for the first time. */
|
|
36
|
+
syncPageCreated: (appName: string, pageName: string, label?: string) => Promise<void>;
|
|
37
|
+
/** Call after a new dashboard has been created / saved for the first time. */
|
|
38
|
+
syncDashboardCreated: (appName: string, dashboardName: string, label?: string) => Promise<void>;
|
|
39
|
+
/** Call after a page has been deleted. */
|
|
40
|
+
syncPageDeleted: (appName: string, pageName: string) => Promise<void>;
|
|
41
|
+
/** Call after a dashboard has been deleted. */
|
|
42
|
+
syncDashboardDeleted: (appName: string, dashboardName: string) => Promise<void>;
|
|
43
|
+
/** Call after a page has been renamed. */
|
|
44
|
+
syncPageRenamed: (appName: string, oldName: string, newName: string) => Promise<void>;
|
|
45
|
+
/** Call after a dashboard has been renamed. */
|
|
46
|
+
syncDashboardRenamed: (appName: string, oldName: string, newName: string) => Promise<void>;
|
|
47
|
+
/** Convenience: add page to navigation of ALL apps. */
|
|
48
|
+
syncPageCreatedAllApps: (pageName: string, label?: string) => Promise<void>;
|
|
49
|
+
/** Convenience: add dashboard to navigation of ALL apps. */
|
|
50
|
+
syncDashboardCreatedAllApps: (dashboardName: string, label?: string) => Promise<void>;
|
|
51
|
+
/** Convenience: remove page from navigation of ALL apps. */
|
|
52
|
+
syncPageDeletedAllApps: (pageName: string) => Promise<void>;
|
|
53
|
+
/** Convenience: remove dashboard from navigation of ALL apps. */
|
|
54
|
+
syncDashboardDeletedAllApps: (dashboardName: string) => Promise<void>;
|
|
55
|
+
/** Convenience: rename page references across ALL apps. */
|
|
56
|
+
syncPageRenamedAllApps: (oldName: string, newName: string) => Promise<void>;
|
|
57
|
+
/** Convenience: rename dashboard references across ALL apps. */
|
|
58
|
+
syncDashboardRenamedAllApps: (oldName: string, newName: string) => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Hook that provides methods to keep the App navigation tree in sync with
|
|
62
|
+
* Page / Dashboard CRUD operations. Each method:
|
|
63
|
+
*
|
|
64
|
+
* 1. Finds the target app from metadata
|
|
65
|
+
* 2. Mutates the `navigation` array (immutably)
|
|
66
|
+
* 3. Persists via `client.meta.saveItem`
|
|
67
|
+
* 4. Refreshes metadata cache
|
|
68
|
+
* 5. Shows a toast with an **Undo** action
|
|
69
|
+
*/
|
|
70
|
+
export declare function useNavigationSync(): UseNavigationSyncReturn;
|
|
71
|
+
/**
|
|
72
|
+
* Headless component that watches the `pages` and `dashboards` metadata
|
|
73
|
+
* arrays. When items are added or removed it automatically calls the
|
|
74
|
+
* matching navigation-sync methods for every app.
|
|
75
|
+
*
|
|
76
|
+
* Mount once inside the component tree (e.g. inside `AppContent`) where
|
|
77
|
+
* both `useMetadata` and `useAdapter` are available.
|
|
78
|
+
*
|
|
79
|
+
* > **Rename detection** is not possible with a simple diff — callers
|
|
80
|
+
* > should invoke `syncPageRenamed` / `syncDashboardRenamed` explicitly.
|
|
81
|
+
*/
|
|
82
|
+
export declare function NavigationSyncEffect(): null;
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useNavigationSync
|
|
3
|
+
*
|
|
4
|
+
* Synchronizes the App navigation tree when Pages or Dashboards are
|
|
5
|
+
* created, deleted, or renamed. Pure utility helpers are exported for
|
|
6
|
+
* unit-testing; the React hook wires them to the adapter / metadata
|
|
7
|
+
* context and shows sonner toasts with an undo action.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
import { useAdapter } from '../providers/AdapterProvider';
|
|
14
|
+
import { useMetadata } from '../providers/MetadataProvider';
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Pure utility helpers (exported for testing)
|
|
17
|
+
// ============================================================================
|
|
18
|
+
let _idCounter = 0;
|
|
19
|
+
/** Generate a simple unique id for a new navigation item. */
|
|
20
|
+
export function generateNavId(prefix = 'nav') {
|
|
21
|
+
return `${prefix}_${Date.now()}_${++_idCounter}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Add a navigation item to the end of a navigation array (immutable).
|
|
25
|
+
*/
|
|
26
|
+
export function addNavigationItem(navigation, item) {
|
|
27
|
+
return [...navigation, item];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Recursively remove all navigation items that match a given type and name.
|
|
31
|
+
* Returns a new array (immutable).
|
|
32
|
+
*/
|
|
33
|
+
export function removeNavigationItems(navigation, type, name) {
|
|
34
|
+
return navigation
|
|
35
|
+
.filter((item) => {
|
|
36
|
+
if (item.type === type) {
|
|
37
|
+
if (type === 'page' && item.pageName === name)
|
|
38
|
+
return false;
|
|
39
|
+
if (type === 'dashboard' && item.dashboardName === name)
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
})
|
|
44
|
+
.map((item) => {
|
|
45
|
+
if (item.children && item.children.length > 0) {
|
|
46
|
+
return {
|
|
47
|
+
...item,
|
|
48
|
+
children: removeNavigationItems(item.children, type, name),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return item;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Recursively rename all navigation items that reference the old name.
|
|
56
|
+
* Returns a new array (immutable).
|
|
57
|
+
*/
|
|
58
|
+
export function renameNavigationItems(navigation, type, oldName, newName) {
|
|
59
|
+
return navigation.map((item) => {
|
|
60
|
+
let updated = item;
|
|
61
|
+
if (type === 'page' && item.type === 'page' && item.pageName === oldName) {
|
|
62
|
+
updated = { ...item, pageName: newName };
|
|
63
|
+
}
|
|
64
|
+
else if (type === 'dashboard' && item.type === 'dashboard' && item.dashboardName === oldName) {
|
|
65
|
+
updated = { ...item, dashboardName: newName };
|
|
66
|
+
}
|
|
67
|
+
if (updated.children && updated.children.length > 0) {
|
|
68
|
+
return {
|
|
69
|
+
...updated,
|
|
70
|
+
children: renameNavigationItems(updated.children, type, oldName, newName),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return updated;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Shallow-structural equality check for two NavigationItem arrays.
|
|
78
|
+
* Works reliably because NavigationItem objects are plain serializable JSON
|
|
79
|
+
* whose key ordering is deterministic (we control object creation).
|
|
80
|
+
*/
|
|
81
|
+
export function navigationEqual(a, b) {
|
|
82
|
+
if (a.length !== b.length)
|
|
83
|
+
return false;
|
|
84
|
+
// Fast-path: same reference
|
|
85
|
+
if (a === b)
|
|
86
|
+
return true;
|
|
87
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Hook that provides methods to keep the App navigation tree in sync with
|
|
91
|
+
* Page / Dashboard CRUD operations. Each method:
|
|
92
|
+
*
|
|
93
|
+
* 1. Finds the target app from metadata
|
|
94
|
+
* 2. Mutates the `navigation` array (immutably)
|
|
95
|
+
* 3. Persists via `client.meta.saveItem`
|
|
96
|
+
* 4. Refreshes metadata cache
|
|
97
|
+
* 5. Shows a toast with an **Undo** action
|
|
98
|
+
*/
|
|
99
|
+
export function useNavigationSync() {
|
|
100
|
+
const adapter = useAdapter();
|
|
101
|
+
const { apps, refresh } = useMetadata();
|
|
102
|
+
// Keep a ref so the undo closure always reads the latest adapter
|
|
103
|
+
const adapterRef = useRef(adapter);
|
|
104
|
+
adapterRef.current = adapter;
|
|
105
|
+
const refreshRef = useRef(refresh);
|
|
106
|
+
refreshRef.current = refresh;
|
|
107
|
+
/** Persist an updated app schema and refresh metadata. */
|
|
108
|
+
const saveApp = useCallback(async (appName, schema) => {
|
|
109
|
+
const client = adapterRef.current?.getClient();
|
|
110
|
+
if (client) {
|
|
111
|
+
await client.meta.saveItem('app', appName, schema);
|
|
112
|
+
}
|
|
113
|
+
await refreshRef.current?.();
|
|
114
|
+
}, []);
|
|
115
|
+
/** Find the current app schema from metadata by name. */
|
|
116
|
+
const findApp = useCallback((appName) => apps.find((a) => a.name === appName), [apps]);
|
|
117
|
+
// ------------------------------------------------------------------
|
|
118
|
+
// Created
|
|
119
|
+
// ------------------------------------------------------------------
|
|
120
|
+
const syncPageCreated = useCallback(async (appName, pageName, label) => {
|
|
121
|
+
const app = findApp(appName);
|
|
122
|
+
if (!app)
|
|
123
|
+
return;
|
|
124
|
+
const prev = app.navigation ?? [];
|
|
125
|
+
const newItem = {
|
|
126
|
+
id: generateNavId('nav_page'),
|
|
127
|
+
type: 'page',
|
|
128
|
+
label: label || pageName,
|
|
129
|
+
pageName,
|
|
130
|
+
icon: 'FileText',
|
|
131
|
+
};
|
|
132
|
+
const updated = addNavigationItem(prev, newItem);
|
|
133
|
+
const updatedApp = { ...app, navigation: updated };
|
|
134
|
+
try {
|
|
135
|
+
await saveApp(appName, updatedApp);
|
|
136
|
+
toast.success(`Navigation updated: added page "${label || pageName}"`, {
|
|
137
|
+
action: {
|
|
138
|
+
label: 'Undo',
|
|
139
|
+
onClick: async () => {
|
|
140
|
+
try {
|
|
141
|
+
await saveApp(appName, { ...app, navigation: prev });
|
|
142
|
+
toast.info('Navigation change undone');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
toast.error('Failed to undo navigation change');
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
toast.error('Failed to update navigation');
|
|
153
|
+
}
|
|
154
|
+
}, [findApp, saveApp]);
|
|
155
|
+
const syncDashboardCreated = useCallback(async (appName, dashboardName, label) => {
|
|
156
|
+
const app = findApp(appName);
|
|
157
|
+
if (!app)
|
|
158
|
+
return;
|
|
159
|
+
const prev = app.navigation ?? [];
|
|
160
|
+
const newItem = {
|
|
161
|
+
id: generateNavId('nav_dash'),
|
|
162
|
+
type: 'dashboard',
|
|
163
|
+
label: label || dashboardName,
|
|
164
|
+
dashboardName,
|
|
165
|
+
icon: 'LayoutDashboard',
|
|
166
|
+
};
|
|
167
|
+
const updated = addNavigationItem(prev, newItem);
|
|
168
|
+
const updatedApp = { ...app, navigation: updated };
|
|
169
|
+
try {
|
|
170
|
+
await saveApp(appName, updatedApp);
|
|
171
|
+
toast.success(`Navigation updated: added dashboard "${label || dashboardName}"`, {
|
|
172
|
+
action: {
|
|
173
|
+
label: 'Undo',
|
|
174
|
+
onClick: async () => {
|
|
175
|
+
try {
|
|
176
|
+
await saveApp(appName, { ...app, navigation: prev });
|
|
177
|
+
toast.info('Navigation change undone');
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
toast.error('Failed to undo navigation change');
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
toast.error('Failed to update navigation');
|
|
188
|
+
}
|
|
189
|
+
}, [findApp, saveApp]);
|
|
190
|
+
// ------------------------------------------------------------------
|
|
191
|
+
// Deleted
|
|
192
|
+
// ------------------------------------------------------------------
|
|
193
|
+
const syncPageDeleted = useCallback(async (appName, pageName) => {
|
|
194
|
+
const app = findApp(appName);
|
|
195
|
+
if (!app)
|
|
196
|
+
return;
|
|
197
|
+
const prev = app.navigation ?? [];
|
|
198
|
+
const updated = removeNavigationItems(prev, 'page', pageName);
|
|
199
|
+
if (navigationEqual(updated, prev))
|
|
200
|
+
return; // nothing changed
|
|
201
|
+
const updatedApp = { ...app, navigation: updated };
|
|
202
|
+
try {
|
|
203
|
+
await saveApp(appName, updatedApp);
|
|
204
|
+
toast.success(`Navigation updated: removed page "${pageName}"`, {
|
|
205
|
+
action: {
|
|
206
|
+
label: 'Undo',
|
|
207
|
+
onClick: async () => {
|
|
208
|
+
try {
|
|
209
|
+
await saveApp(appName, { ...app, navigation: prev });
|
|
210
|
+
toast.info('Navigation change undone');
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
toast.error('Failed to undo navigation change');
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
toast.error('Failed to update navigation');
|
|
221
|
+
}
|
|
222
|
+
}, [findApp, saveApp]);
|
|
223
|
+
const syncDashboardDeleted = useCallback(async (appName, dashboardName) => {
|
|
224
|
+
const app = findApp(appName);
|
|
225
|
+
if (!app)
|
|
226
|
+
return;
|
|
227
|
+
const prev = app.navigation ?? [];
|
|
228
|
+
const updated = removeNavigationItems(prev, 'dashboard', dashboardName);
|
|
229
|
+
if (navigationEqual(updated, prev))
|
|
230
|
+
return;
|
|
231
|
+
const updatedApp = { ...app, navigation: updated };
|
|
232
|
+
try {
|
|
233
|
+
await saveApp(appName, updatedApp);
|
|
234
|
+
toast.success(`Navigation updated: removed dashboard "${dashboardName}"`, {
|
|
235
|
+
action: {
|
|
236
|
+
label: 'Undo',
|
|
237
|
+
onClick: async () => {
|
|
238
|
+
try {
|
|
239
|
+
await saveApp(appName, { ...app, navigation: prev });
|
|
240
|
+
toast.info('Navigation change undone');
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
toast.error('Failed to undo navigation change');
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
toast.error('Failed to update navigation');
|
|
251
|
+
}
|
|
252
|
+
}, [findApp, saveApp]);
|
|
253
|
+
// ------------------------------------------------------------------
|
|
254
|
+
// Renamed
|
|
255
|
+
// ------------------------------------------------------------------
|
|
256
|
+
const syncPageRenamed = useCallback(async (appName, oldName, newName) => {
|
|
257
|
+
const app = findApp(appName);
|
|
258
|
+
if (!app)
|
|
259
|
+
return;
|
|
260
|
+
const prev = app.navigation ?? [];
|
|
261
|
+
const updated = renameNavigationItems(prev, 'page', oldName, newName);
|
|
262
|
+
if (navigationEqual(updated, prev))
|
|
263
|
+
return;
|
|
264
|
+
const updatedApp = { ...app, navigation: updated };
|
|
265
|
+
try {
|
|
266
|
+
await saveApp(appName, updatedApp);
|
|
267
|
+
toast.success(`Navigation updated: renamed page "${oldName}" → "${newName}"`, {
|
|
268
|
+
action: {
|
|
269
|
+
label: 'Undo',
|
|
270
|
+
onClick: async () => {
|
|
271
|
+
try {
|
|
272
|
+
await saveApp(appName, { ...app, navigation: prev });
|
|
273
|
+
toast.info('Navigation change undone');
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
toast.error('Failed to undo navigation change');
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
toast.error('Failed to update navigation');
|
|
284
|
+
}
|
|
285
|
+
}, [findApp, saveApp]);
|
|
286
|
+
const syncDashboardRenamed = useCallback(async (appName, oldName, newName) => {
|
|
287
|
+
const app = findApp(appName);
|
|
288
|
+
if (!app)
|
|
289
|
+
return;
|
|
290
|
+
const prev = app.navigation ?? [];
|
|
291
|
+
const updated = renameNavigationItems(prev, 'dashboard', oldName, newName);
|
|
292
|
+
if (navigationEqual(updated, prev))
|
|
293
|
+
return;
|
|
294
|
+
const updatedApp = { ...app, navigation: updated };
|
|
295
|
+
try {
|
|
296
|
+
await saveApp(appName, updatedApp);
|
|
297
|
+
toast.success(`Navigation updated: renamed dashboard "${oldName}" → "${newName}"`, {
|
|
298
|
+
action: {
|
|
299
|
+
label: 'Undo',
|
|
300
|
+
onClick: async () => {
|
|
301
|
+
try {
|
|
302
|
+
await saveApp(appName, { ...app, navigation: prev });
|
|
303
|
+
toast.info('Navigation change undone');
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
toast.error('Failed to undo navigation change');
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
toast.error('Failed to update navigation');
|
|
314
|
+
}
|
|
315
|
+
}, [findApp, saveApp]);
|
|
316
|
+
// ------------------------------------------------------------------
|
|
317
|
+
// All-Apps convenience methods
|
|
318
|
+
// ------------------------------------------------------------------
|
|
319
|
+
/** Safely extract the app name from a metadata entry. */
|
|
320
|
+
const getAppName = (app) => app && typeof app === 'object' && 'name' in app ? app.name : undefined;
|
|
321
|
+
/** Add a page nav item to ALL apps that don't already reference it. */
|
|
322
|
+
const syncPageCreatedAllApps = useCallback(async (pageName, label) => {
|
|
323
|
+
for (const app of apps) {
|
|
324
|
+
const name = getAppName(app);
|
|
325
|
+
if (!name)
|
|
326
|
+
continue;
|
|
327
|
+
await syncPageCreated(name, pageName, label);
|
|
328
|
+
}
|
|
329
|
+
}, [apps, syncPageCreated]);
|
|
330
|
+
/** Add a dashboard nav item to ALL apps that don't already reference it. */
|
|
331
|
+
const syncDashboardCreatedAllApps = useCallback(async (dashboardName, label) => {
|
|
332
|
+
for (const app of apps) {
|
|
333
|
+
const name = getAppName(app);
|
|
334
|
+
if (!name)
|
|
335
|
+
continue;
|
|
336
|
+
await syncDashboardCreated(name, dashboardName, label);
|
|
337
|
+
}
|
|
338
|
+
}, [apps, syncDashboardCreated]);
|
|
339
|
+
/** Remove a page from navigation across ALL apps. */
|
|
340
|
+
const syncPageDeletedAllApps = useCallback(async (pageName) => {
|
|
341
|
+
for (const app of apps) {
|
|
342
|
+
const name = getAppName(app);
|
|
343
|
+
if (!name)
|
|
344
|
+
continue;
|
|
345
|
+
await syncPageDeleted(name, pageName);
|
|
346
|
+
}
|
|
347
|
+
}, [apps, syncPageDeleted]);
|
|
348
|
+
/** Remove a dashboard from navigation across ALL apps. */
|
|
349
|
+
const syncDashboardDeletedAllApps = useCallback(async (dashboardName) => {
|
|
350
|
+
for (const app of apps) {
|
|
351
|
+
const name = getAppName(app);
|
|
352
|
+
if (!name)
|
|
353
|
+
continue;
|
|
354
|
+
await syncDashboardDeleted(name, dashboardName);
|
|
355
|
+
}
|
|
356
|
+
}, [apps, syncDashboardDeleted]);
|
|
357
|
+
/** Rename page references across ALL apps. */
|
|
358
|
+
const syncPageRenamedAllApps = useCallback(async (oldName, newName) => {
|
|
359
|
+
for (const app of apps) {
|
|
360
|
+
const name = getAppName(app);
|
|
361
|
+
if (!name)
|
|
362
|
+
continue;
|
|
363
|
+
await syncPageRenamed(name, oldName, newName);
|
|
364
|
+
}
|
|
365
|
+
}, [apps, syncPageRenamed]);
|
|
366
|
+
/** Rename dashboard references across ALL apps. */
|
|
367
|
+
const syncDashboardRenamedAllApps = useCallback(async (oldName, newName) => {
|
|
368
|
+
for (const app of apps) {
|
|
369
|
+
const name = getAppName(app);
|
|
370
|
+
if (!name)
|
|
371
|
+
continue;
|
|
372
|
+
await syncDashboardRenamed(name, oldName, newName);
|
|
373
|
+
}
|
|
374
|
+
}, [apps, syncDashboardRenamed]);
|
|
375
|
+
return {
|
|
376
|
+
syncPageCreated,
|
|
377
|
+
syncDashboardCreated,
|
|
378
|
+
syncPageDeleted,
|
|
379
|
+
syncDashboardDeleted,
|
|
380
|
+
syncPageRenamed,
|
|
381
|
+
syncDashboardRenamed,
|
|
382
|
+
syncPageCreatedAllApps,
|
|
383
|
+
syncDashboardCreatedAllApps,
|
|
384
|
+
syncPageDeletedAllApps,
|
|
385
|
+
syncDashboardDeletedAllApps,
|
|
386
|
+
syncPageRenamedAllApps,
|
|
387
|
+
syncDashboardRenamedAllApps,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// ============================================================================
|
|
391
|
+
// NavigationSyncEffect — auto-detect page/dashboard metadata changes
|
|
392
|
+
// ============================================================================
|
|
393
|
+
/**
|
|
394
|
+
* Headless component that watches the `pages` and `dashboards` metadata
|
|
395
|
+
* arrays. When items are added or removed it automatically calls the
|
|
396
|
+
* matching navigation-sync methods for every app.
|
|
397
|
+
*
|
|
398
|
+
* Mount once inside the component tree (e.g. inside `AppContent`) where
|
|
399
|
+
* both `useMetadata` and `useAdapter` are available.
|
|
400
|
+
*
|
|
401
|
+
* > **Rename detection** is not possible with a simple diff — callers
|
|
402
|
+
* > should invoke `syncPageRenamed` / `syncDashboardRenamed` explicitly.
|
|
403
|
+
*/
|
|
404
|
+
export function NavigationSyncEffect() {
|
|
405
|
+
const { pages, dashboards, apps } = useMetadata();
|
|
406
|
+
const adapter = useAdapter();
|
|
407
|
+
const adapterRef = useRef(adapter);
|
|
408
|
+
adapterRef.current = adapter;
|
|
409
|
+
const { syncPageCreated, syncDashboardCreated, syncPageDeleted, syncDashboardDeleted, } = useNavigationSync();
|
|
410
|
+
// Track previous page/dashboard name sets
|
|
411
|
+
const prevPageNamesRef = useRef(null);
|
|
412
|
+
const prevDashNamesRef = useRef(null);
|
|
413
|
+
// Guard against circular refreshes and concurrent sync operations.
|
|
414
|
+
// Incremented on each effect invocation; the async closure captures
|
|
415
|
+
// its own version and bails out if a newer run has started.
|
|
416
|
+
const syncVersionRef = useRef(0);
|
|
417
|
+
const syncingRef = useRef(false);
|
|
418
|
+
/** Safely extract the app name from a metadata entry. */
|
|
419
|
+
const getAppName = (app) => app && typeof app === 'object' && 'name' in app ? app.name : undefined;
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
if (syncingRef.current)
|
|
422
|
+
return;
|
|
423
|
+
const currentPageNames = new Set((pages ?? []).map((p) => p.name).filter(Boolean));
|
|
424
|
+
const currentDashNames = new Set((dashboards ?? []).map((d) => d.name).filter(Boolean));
|
|
425
|
+
const prevPages = prevPageNamesRef.current;
|
|
426
|
+
const prevDash = prevDashNamesRef.current;
|
|
427
|
+
// First render — seed refs and exit without syncing
|
|
428
|
+
if (prevPages === null || prevDash === null) {
|
|
429
|
+
prevPageNamesRef.current = currentPageNames;
|
|
430
|
+
prevDashNamesRef.current = currentDashNames;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
// Compute diff
|
|
434
|
+
const addedPages = [...currentPageNames].filter((n) => !prevPages.has(n));
|
|
435
|
+
const removedPages = [...prevPages].filter((n) => !currentPageNames.has(n));
|
|
436
|
+
const addedDash = [...currentDashNames].filter((n) => !prevDash.has(n));
|
|
437
|
+
const removedDash = [...prevDash].filter((n) => !currentDashNames.has(n));
|
|
438
|
+
if (addedPages.length === 0 &&
|
|
439
|
+
removedPages.length === 0 &&
|
|
440
|
+
addedDash.length === 0 &&
|
|
441
|
+
removedDash.length === 0) {
|
|
442
|
+
// Nothing changed — update refs and exit
|
|
443
|
+
prevPageNamesRef.current = currentPageNames;
|
|
444
|
+
prevDashNamesRef.current = currentDashNames;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
// Sync navigation across all apps
|
|
448
|
+
const version = ++syncVersionRef.current;
|
|
449
|
+
syncingRef.current = true;
|
|
450
|
+
let cancelled = false;
|
|
451
|
+
(async () => {
|
|
452
|
+
try {
|
|
453
|
+
for (const app of apps) {
|
|
454
|
+
if (cancelled || syncVersionRef.current !== version)
|
|
455
|
+
break;
|
|
456
|
+
const appName = getAppName(app);
|
|
457
|
+
if (!appName)
|
|
458
|
+
continue;
|
|
459
|
+
for (const pageName of addedPages) {
|
|
460
|
+
if (cancelled)
|
|
461
|
+
break;
|
|
462
|
+
await syncPageCreated(appName, pageName);
|
|
463
|
+
}
|
|
464
|
+
for (const pageName of removedPages) {
|
|
465
|
+
if (cancelled)
|
|
466
|
+
break;
|
|
467
|
+
await syncPageDeleted(appName, pageName);
|
|
468
|
+
}
|
|
469
|
+
for (const dashName of addedDash) {
|
|
470
|
+
if (cancelled)
|
|
471
|
+
break;
|
|
472
|
+
await syncDashboardCreated(appName, dashName);
|
|
473
|
+
}
|
|
474
|
+
for (const dashName of removedDash) {
|
|
475
|
+
if (cancelled)
|
|
476
|
+
break;
|
|
477
|
+
await syncDashboardDeleted(appName, dashName);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
// Only update refs if this is still the latest sync version
|
|
483
|
+
if (syncVersionRef.current === version) {
|
|
484
|
+
prevPageNamesRef.current = currentPageNames;
|
|
485
|
+
prevDashNamesRef.current = currentDashNames;
|
|
486
|
+
syncingRef.current = false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
})();
|
|
490
|
+
return () => {
|
|
491
|
+
cancelled = true;
|
|
492
|
+
};
|
|
493
|
+
}, [pages, dashboards, apps, syncPageCreated, syncDashboardCreated, syncPageDeleted, syncDashboardDeleted]);
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useObjectActions Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides action handlers for CRUD operations on an object, backed by
|
|
5
|
+
* the ActionRunner from @object-ui/core via the useActionRunner hook.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - create: Open create dialog
|
|
9
|
+
* - delete: Delete a record with confirmation
|
|
10
|
+
* - navigate: Route to a specific view or record
|
|
11
|
+
* - refresh: Trigger a data refresh
|
|
12
|
+
*/
|
|
13
|
+
import type { ActionDef, ActionResult } from '@object-ui/core';
|
|
14
|
+
interface ObjectActionConfig {
|
|
15
|
+
objectName: string;
|
|
16
|
+
objectLabel?: string;
|
|
17
|
+
dataSource: any;
|
|
18
|
+
onEdit?: (record: any) => void;
|
|
19
|
+
onRefresh?: () => void;
|
|
20
|
+
}
|
|
21
|
+
interface ObjectActions {
|
|
22
|
+
/** Run an action by schema or type string */
|
|
23
|
+
execute: (action: ActionDef) => Promise<ActionResult>;
|
|
24
|
+
/** Create new record — opens the create dialog */
|
|
25
|
+
create: () => void;
|
|
26
|
+
/** Delete a record by id */
|
|
27
|
+
deleteRecord: (recordId: string) => Promise<ActionResult>;
|
|
28
|
+
/** Navigate to a view */
|
|
29
|
+
navigateToView: (viewId: string) => void;
|
|
30
|
+
/** Navigate to a record detail */
|
|
31
|
+
navigateToRecord: (recordId: string) => void;
|
|
32
|
+
/** Whether an action is currently executing */
|
|
33
|
+
loading: boolean;
|
|
34
|
+
/** Last error message */
|
|
35
|
+
error: string | null;
|
|
36
|
+
}
|
|
37
|
+
export declare function useObjectActions({ objectName, objectLabel, dataSource, onEdit, onRefresh, }: ObjectActionConfig): ObjectActions;
|
|
38
|
+
export {};
|