@rebasepro/app 0.0.1-canary.4829d6e
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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/auth/api.d.ts +66 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/types.d.ts +127 -0
- package/dist/auth/useRebaseAuthController.d.ts +13 -0
- package/dist/components/AIIcon.d.ts +17 -0
- package/dist/components/AuthSimulationMenu.d.ts +0 -0
- package/dist/components/ConfirmationDialog.d.ts +9 -0
- package/dist/components/Debug/UIReferenceView.d.ts +13 -0
- package/dist/components/Debug/UIStyleGuide.d.ts +2 -0
- package/dist/components/Debug/collection-views/CardViewDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/CollectionTableDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/KanbanBoardDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/index.d.ts +3 -0
- package/dist/components/Debug/collection-views/sample_data.d.ts +23 -0
- package/dist/components/Debug/crm-dashboard/CalendarWidget.d.ts +22 -0
- package/dist/components/Debug/crm-dashboard/CrmDashboardDemo.d.ts +2 -0
- package/dist/components/Debug/crm-dashboard/DashboardMetrics.d.ts +15 -0
- package/dist/components/Debug/crm-dashboard/PipelineOverview.d.ts +34 -0
- package/dist/components/Debug/crm-dashboard/RecentActivity.d.ts +19 -0
- package/dist/components/Debug/crm-dashboard/TaskFilters.d.ts +30 -0
- package/dist/components/Debug/crm-dashboard/TaskTable.d.ts +50 -0
- package/dist/components/Debug/crm-dashboard/TasksView.d.ts +41 -0
- package/dist/components/ErrorTooltip.d.ts +3 -0
- package/dist/components/ErrorView.d.ts +21 -0
- package/dist/components/LanguageToggle.d.ts +2 -0
- package/dist/components/LoginView/LoginView.d.ts +113 -0
- package/dist/components/LoginView/index.d.ts +2 -0
- package/dist/components/NotFoundPage.d.ts +2 -0
- package/dist/components/RebaseAuth.d.ts +10 -0
- package/dist/components/RebaseLogo.d.ts +7 -0
- package/dist/components/SchemaDriftBanner.d.ts +27 -0
- package/dist/components/UnsavedChangesDialog.d.ts +9 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/UserSelectPopover.d.ts +63 -0
- package/dist/components/UserSettingsView.d.ts +2 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/table_height.d.ts +5 -0
- package/dist/components/common/types.d.ts +66 -0
- package/dist/components/common/useColumnsIds.d.ts +9 -0
- package/dist/components/common/useDataTableController.d.ts +45 -0
- package/dist/components/common/useDebouncedData.d.ts +9 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/contexts/AdminModeController.d.ts +4 -0
- package/dist/contexts/AnalyticsContext.d.ts +3 -0
- package/dist/contexts/AuthControllerContext.d.ts +3 -0
- package/dist/contexts/CollectionScopeContext.d.ts +50 -0
- package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
- package/dist/contexts/CustomizationControllerContext.d.ts +3 -0
- package/dist/contexts/DataDriverContext.d.ts +3 -0
- package/dist/contexts/DataSourcesContext.d.ts +31 -0
- package/dist/contexts/DatabaseAdminContext.d.ts +3 -0
- package/dist/contexts/DialogsProvider.d.ts +4 -0
- package/dist/contexts/EffectiveRoleController.d.ts +4 -0
- package/dist/contexts/ModeController.d.ts +4 -0
- package/dist/contexts/RebaseClientInstanceContext.d.ts +6 -0
- package/dist/contexts/RebaseDataContext.d.ts +3 -0
- package/dist/contexts/SnackbarProvider.d.ts +2 -0
- package/dist/contexts/StorageSourceContext.d.ts +3 -0
- package/dist/contexts/StorageSourcesContext.d.ts +25 -0
- package/dist/contexts/UserConfigurationPersistenceContext.d.ts +3 -0
- package/dist/contexts/index.d.ts +17 -0
- package/dist/core/PluginLifecycleManager.d.ts +17 -0
- package/dist/core/PluginProviderStack.d.ts +21 -0
- package/dist/core/Rebase.d.ts +15 -0
- package/dist/core/RebaseProps.d.ts +291 -0
- package/dist/core/RebaseRouter.d.ts +4 -0
- package/dist/core/RebaseRoutes.d.ts +17 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/hooks/ApiConfigContext.d.ts +24 -0
- package/dist/hooks/data/delete.d.ts +33 -0
- package/dist/hooks/data/save.d.ts +40 -0
- package/dist/hooks/data/useCollection.d.ts +62 -0
- package/dist/hooks/data/useData.d.ts +13 -0
- package/dist/hooks/data/useDataOrder.d.ts +12 -0
- package/dist/hooks/data/useFetch.d.ts +43 -0
- package/dist/hooks/data/useRelationSelector.d.ts +52 -0
- package/dist/hooks/index.d.ts +36 -0
- package/dist/hooks/useAdminModeController.d.ts +19 -0
- package/dist/hooks/useAnalyticsController.d.ts +5 -0
- package/dist/hooks/useAuthController.d.ts +11 -0
- package/dist/hooks/useAuthSubscription.d.ts +2 -0
- package/dist/hooks/useBackendStorageSource.d.ts +30 -0
- package/dist/hooks/useBridgeRegistration.d.ts +18 -0
- package/dist/hooks/useBrowserTitleAndIcon.d.ts +6 -0
- package/dist/hooks/useBuildAdminModeController.d.ts +6 -0
- package/dist/hooks/useBuildEffectiveRoleController.d.ts +8 -0
- package/dist/hooks/useBuildLocalConfigurationPersistence.d.ts +2 -0
- package/dist/hooks/useBuildModeController.d.ts +6 -0
- package/dist/hooks/useClipboard.d.ts +57 -0
- package/dist/hooks/useCollapsedGroups.d.ts +27 -0
- package/dist/hooks/useComponentOverride.d.ts +32 -0
- package/dist/hooks/useCustomizationController.d.ts +11 -0
- package/dist/hooks/useDialogsController.d.ts +11 -0
- package/dist/hooks/useEffectiveRoleController.d.ts +7 -0
- package/dist/hooks/useLargeLayout.d.ts +1 -0
- package/dist/hooks/useModeController.d.ts +19 -0
- package/dist/hooks/usePermissions.d.ts +12 -0
- package/dist/hooks/useRebaseClient.d.ts +5 -0
- package/dist/hooks/useRebaseContext.d.ts +11 -0
- package/dist/hooks/useRebaseRegistry.d.ts +34 -0
- package/dist/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/hooks/useSlot.d.ts +18 -0
- package/dist/hooks/useSnackbarController.d.ts +20 -0
- package/dist/hooks/useStorageSource.d.ts +6 -0
- package/dist/hooks/useStudioBridge.d.ts +79 -0
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/hooks/useUnsavedChangesDialog.d.ts +12 -0
- package/dist/hooks/useUserConfigurationPersistence.d.ts +8 -0
- package/dist/i18n/RebaseI18nProvider.d.ts +33 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.es.js +16843 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +17021 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/internal/common.d.ts +14 -0
- package/dist/internal/useRestoreScroll.d.ts +15 -0
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/util/constants.d.ts +1 -0
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +22 -0
- package/dist/util/enums.d.ts +5 -0
- package/dist/util/icon_list.d.ts +5 -0
- package/dist/util/icons.d.ts +20 -0
- package/dist/util/index.d.ts +8 -0
- package/dist/util/previews.d.ts +4 -0
- package/dist/util/useStorageUploadController.d.ts +44 -0
- package/dist/vitePlugin.d.ts +60 -0
- package/dist/vitePlugin.js +113 -0
- package/package.json +145 -0
- package/src/auth/api.ts +148 -0
- package/src/auth/index.ts +25 -0
- package/src/auth/types.ts +115 -0
- package/src/auth/useRebaseAuthController.ts +328 -0
- package/src/components/AIIcon.tsx +47 -0
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +48 -0
- package/src/components/Debug/UIReferenceView.tsx +958 -0
- package/src/components/Debug/UIStyleGuide.tsx +156 -0
- package/src/components/Debug/collection-views/CardViewDemo.tsx +85 -0
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +107 -0
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +90 -0
- package/src/components/Debug/collection-views/index.ts +3 -0
- package/src/components/Debug/collection-views/sample_data.ts +42 -0
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +389 -0
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +1102 -0
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +133 -0
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +290 -0
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +215 -0
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +184 -0
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +427 -0
- package/src/components/Debug/crm-dashboard/TasksView.tsx +234 -0
- package/src/components/ErrorTooltip.tsx +12 -0
- package/src/components/ErrorView.tsx +94 -0
- package/src/components/LanguageToggle.tsx +59 -0
- package/src/components/LoginView/LoginView.tsx +932 -0
- package/src/components/LoginView/index.ts +2 -0
- package/src/components/NotFoundPage.tsx +27 -0
- package/src/components/RebaseAuth.tsx +27 -0
- package/src/components/RebaseLogo.tsx +29 -0
- package/src/components/SchemaDriftBanner.tsx +102 -0
- package/src/components/UnsavedChangesDialog.tsx +46 -0
- package/src/components/UserDisplay.tsx +54 -0
- package/src/components/UserSelectPopover.tsx +399 -0
- package/src/components/UserSettingsView.tsx +318 -0
- package/src/components/common/index.ts +6 -0
- package/src/components/common/table_height.tsx +21 -0
- package/src/components/common/types.tsx +66 -0
- package/src/components/common/useColumnsIds.tsx +222 -0
- package/src/components/common/useDataTableController.tsx +514 -0
- package/src/components/common/useDebouncedData.ts +49 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/components/index.tsx +26 -0
- package/src/contexts/AdminModeController.tsx +11 -0
- package/src/contexts/AnalyticsContext.tsx +4 -0
- package/src/contexts/AuthControllerContext.tsx +4 -0
- package/src/contexts/CollectionScopeContext.tsx +67 -0
- package/src/contexts/ComponentOverrideContext.tsx +81 -0
- package/src/contexts/CustomizationControllerContext.tsx +4 -0
- package/src/contexts/DataDriverContext.tsx +4 -0
- package/src/contexts/DataSourcesContext.tsx +36 -0
- package/src/contexts/DatabaseAdminContext.tsx +4 -0
- package/src/contexts/DialogsProvider.tsx +56 -0
- package/src/contexts/EffectiveRoleController.tsx +12 -0
- package/src/contexts/ModeController.tsx +11 -0
- package/src/contexts/RebaseClientInstanceContext.tsx +7 -0
- package/src/contexts/RebaseDataContext.tsx +4 -0
- package/src/contexts/SnackbarProvider.tsx +15 -0
- package/src/contexts/StorageSourceContext.tsx +4 -0
- package/src/contexts/StorageSourcesContext.tsx +30 -0
- package/src/contexts/UserConfigurationPersistenceContext.tsx +4 -0
- package/src/contexts/index.ts +17 -0
- package/src/core/PluginLifecycleManager.tsx +95 -0
- package/src/core/PluginProviderStack.tsx +40 -0
- package/src/core/Rebase.tsx +439 -0
- package/src/core/RebaseProps.tsx +320 -0
- package/src/core/RebaseRouter.tsx +17 -0
- package/src/core/RebaseRoutes.tsx +24 -0
- package/src/core/index.tsx +5 -0
- package/src/hooks/ApiConfigContext.tsx +41 -0
- package/src/hooks/data/delete.ts +55 -0
- package/src/hooks/data/save.ts +77 -0
- package/src/hooks/data/useCollection.tsx +176 -0
- package/src/hooks/data/useData.tsx +18 -0
- package/src/hooks/data/useDataOrder.ts +26 -0
- package/src/hooks/data/useFetch.tsx +140 -0
- package/src/hooks/data/useRelationSelector.tsx +234 -0
- package/src/hooks/index.tsx +45 -0
- package/src/hooks/useAdminModeController.tsx +23 -0
- package/src/hooks/useAnalyticsController.tsx +8 -0
- package/src/hooks/useAuthController.tsx +14 -0
- package/src/hooks/useAuthSubscription.ts +87 -0
- package/src/hooks/useBackendStorageSource.ts +288 -0
- package/src/hooks/useBridgeRegistration.tsx +32 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +23 -0
- package/src/hooks/useBuildAdminModeController.tsx +24 -0
- package/src/hooks/useBuildEffectiveRoleController.tsx +31 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +75 -0
- package/src/hooks/useBuildModeController.tsx +71 -0
- package/src/hooks/useClipboard.tsx +158 -0
- package/src/hooks/useCollapsedGroups.ts +116 -0
- package/src/hooks/useComponentOverride.tsx +59 -0
- package/src/hooks/useCustomizationController.tsx +14 -0
- package/src/hooks/useDialogsController.tsx +15 -0
- package/src/hooks/useEffectiveRoleController.tsx +10 -0
- package/src/hooks/useLargeLayout.tsx +65 -0
- package/src/hooks/useModeController.tsx +23 -0
- package/src/hooks/usePermissions.ts +44 -0
- package/src/hooks/useRebaseClient.tsx +10 -0
- package/src/hooks/useRebaseContext.tsx +83 -0
- package/src/hooks/useRebaseRegistry.tsx +91 -0
- package/src/hooks/useResolvedComponent.tsx +159 -0
- package/src/hooks/useSlot.tsx +64 -0
- package/src/hooks/useSnackbarController.tsx +52 -0
- package/src/hooks/useStorageSource.tsx +11 -0
- package/src/hooks/useStudioBridge.tsx +209 -0
- package/src/hooks/useTranslation.ts +33 -0
- package/src/hooks/useUnsavedChangesDialog.tsx +64 -0
- package/src/hooks/useUserConfigurationPersistence.tsx +11 -0
- package/src/i18n/RebaseI18nProvider.tsx +160 -0
- package/src/index.ts +25 -0
- package/src/internal/common.tsx +16 -0
- package/src/internal/useRestoreScroll.tsx +69 -0
- package/src/locales/de.ts +902 -0
- package/src/locales/en.ts +980 -0
- package/src/locales/es.ts +922 -0
- package/src/locales/fr.ts +901 -0
- package/src/locales/hi.ts +901 -0
- package/src/locales/it.ts +901 -0
- package/src/locales/pt.ts +909 -0
- package/src/util/constants.ts +7 -0
- package/src/util/createFormexStub.tsx +66 -0
- package/src/util/entity_cache.ts +239 -0
- package/src/util/enums.ts +32 -0
- package/src/util/icon_list.ts +22 -0
- package/src/util/icons.tsx +149 -0
- package/src/util/index.ts +12 -0
- package/src/util/previews.ts +105 -0
- package/src/util/useStorageUploadController.tsx +350 -0
- package/src/vitePlugin.ts +196 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback, useMemo, useRef } from "react";
|
|
2
|
+
import type { RebaseRegistryController, RebaseAdminConfig, RebaseStudioConfig, RebaseAuthConfig } from "@rebasepro/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Split into two contexts to prevent infinite re-render loops:
|
|
6
|
+
* - DispatchContext: stable register/unregister functions (never changes identity)
|
|
7
|
+
* - StateContext: the current config values (changes when modules register)
|
|
8
|
+
*
|
|
9
|
+
* Feature components (RebaseAuth, RebaseAdmin, RebaseStudio) only consume
|
|
10
|
+
* the dispatch context, so registering does NOT cause them to re-render.
|
|
11
|
+
* RebaseShell consumes state context to read the collected configs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface RegistryDispatch {
|
|
15
|
+
registerCMS: (config: RebaseAdminConfig) => void;
|
|
16
|
+
unregisterCMS: () => void;
|
|
17
|
+
registerStudio: (config: RebaseStudioConfig) => void;
|
|
18
|
+
unregisterStudio: () => void;
|
|
19
|
+
registerAuth: (config: RebaseAuthConfig) => void;
|
|
20
|
+
unregisterAuth: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RegistryState {
|
|
24
|
+
cmsConfig: RebaseAdminConfig | null;
|
|
25
|
+
studioConfig: RebaseStudioConfig | null;
|
|
26
|
+
authConfig: RebaseAuthConfig | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const RegistryDispatchContext = createContext<RegistryDispatch | undefined>(undefined);
|
|
30
|
+
const RegistryStateContext = createContext<RegistryState>({
|
|
31
|
+
cmsConfig: null,
|
|
32
|
+
studioConfig: null,
|
|
33
|
+
authConfig: null
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function RebaseRegistryProvider({ children }: { children: React.ReactNode }) {
|
|
37
|
+
const [cmsConfig, setCmsConfig] = useState<RebaseAdminConfig | null>(null);
|
|
38
|
+
const [studioConfig, setStudioConfig] = useState<RebaseStudioConfig | null>(null);
|
|
39
|
+
const [authConfig, setAuthConfig] = useState<RebaseAuthConfig | null>(null);
|
|
40
|
+
|
|
41
|
+
// Dispatch functions are stable — never change identity
|
|
42
|
+
const dispatch = useMemo<RegistryDispatch>(() => ({
|
|
43
|
+
registerCMS: (config: RebaseAdminConfig) => setCmsConfig(config),
|
|
44
|
+
unregisterCMS: () => setCmsConfig(null),
|
|
45
|
+
registerStudio: (config: RebaseStudioConfig) => setStudioConfig(config),
|
|
46
|
+
unregisterStudio: () => setStudioConfig(null),
|
|
47
|
+
registerAuth: (config: RebaseAuthConfig) => setAuthConfig(config),
|
|
48
|
+
unregisterAuth: () => setAuthConfig(null)
|
|
49
|
+
}), []);
|
|
50
|
+
|
|
51
|
+
const state = useMemo<RegistryState>(() => ({
|
|
52
|
+
cmsConfig,
|
|
53
|
+
studioConfig,
|
|
54
|
+
authConfig
|
|
55
|
+
}), [cmsConfig, studioConfig, authConfig]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<RegistryDispatchContext.Provider value={dispatch}>
|
|
59
|
+
<RegistryStateContext.Provider value={state}>
|
|
60
|
+
{children}
|
|
61
|
+
</RegistryStateContext.Provider>
|
|
62
|
+
</RegistryDispatchContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the full registry (state + dispatch).
|
|
68
|
+
* Use this in RebaseShell where you need to read configs.
|
|
69
|
+
*/
|
|
70
|
+
export function useRebaseRegistry(): RebaseRegistryController {
|
|
71
|
+
const dispatch = useContext(RegistryDispatchContext);
|
|
72
|
+
const state = useContext(RegistryStateContext);
|
|
73
|
+
if (!dispatch) {
|
|
74
|
+
throw new Error("useRebaseRegistry must be used within RebaseRegistryProvider");
|
|
75
|
+
}
|
|
76
|
+
return useMemo(() => ({ ...state,
|
|
77
|
+
...dispatch }), [state, dispatch]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns only the stable dispatch functions.
|
|
82
|
+
* Use this in feature components (RebaseAuth, RebaseAdmin, RebaseStudio)
|
|
83
|
+
* to avoid re-render loops.
|
|
84
|
+
*/
|
|
85
|
+
export function useRebaseRegistryDispatch(): RegistryDispatch {
|
|
86
|
+
const dispatch = useContext(RegistryDispatchContext);
|
|
87
|
+
if (!dispatch) {
|
|
88
|
+
throw new Error("useRebaseRegistryDispatch must be used within RebaseRegistryProvider");
|
|
89
|
+
}
|
|
90
|
+
return dispatch;
|
|
91
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React, { lazy, useMemo } from "react";
|
|
2
|
+
import type { ComponentRef, LazyComponentRef } from "@rebasepro/types";
|
|
3
|
+
import { isLazyComponentRef } from "@rebasepro/types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal cache for resolved lazy components.
|
|
7
|
+
*
|
|
8
|
+
* `React.lazy()` must return the SAME wrapper across renders — if we call
|
|
9
|
+
* `lazy(loader)` on every render, React will treat each result as a new
|
|
10
|
+
* component type and unmount/remount on each render cycle.
|
|
11
|
+
*
|
|
12
|
+
* This WeakMap keeps a stable mapping from a `ComponentRef` (object/function)
|
|
13
|
+
* to the `React.lazy()` wrapper it produced. Strings are keyed by a separate
|
|
14
|
+
* plain Map since they can't be WeakMap keys.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const lazyCache = new WeakMap<object | ((...args: any[]) => any), React.ComponentType<any>>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a `ComponentRef` into a renderable `React.ComponentType`.
|
|
21
|
+
*
|
|
22
|
+
* This hook handles all three forms of `ComponentRef`:
|
|
23
|
+
*
|
|
24
|
+
* 1. **`LazyComponentRef`** (produced by the Vite plugin from string paths):
|
|
25
|
+
* Wraps the lazy loader with `React.lazy()` for automatic code-splitting.
|
|
26
|
+
*
|
|
27
|
+
* 2. **`() => Promise<{ default: ComponentType }>`** (manual lazy import):
|
|
28
|
+
* Also wraps with `React.lazy()`. Distinguished from regular components
|
|
29
|
+
* by checking that the function has no parameters and is not a known
|
|
30
|
+
* React internal (no `$$typeof`).
|
|
31
|
+
*
|
|
32
|
+
* 3. **Direct `React.ComponentType`**:
|
|
33
|
+
* Returned as-is.
|
|
34
|
+
*
|
|
35
|
+
* If the ref is `undefined` or a raw string (which should never happen at
|
|
36
|
+
* runtime in the browser since the Vite plugin transforms strings), returns `undefined`.
|
|
37
|
+
*
|
|
38
|
+
* The returned component is stable across re-renders as long as the `ref`
|
|
39
|
+
* is referentially stable.
|
|
40
|
+
*
|
|
41
|
+
* **Usage:** Wrap the rendered component in `<Suspense>` to handle the
|
|
42
|
+
* loading state of lazy components.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* const ResolvedField = useResolvedComponent(property.ui?.Field);
|
|
47
|
+
* if (!ResolvedField) return null;
|
|
48
|
+
* return (
|
|
49
|
+
* <Suspense fallback={<CircularProgress />}>
|
|
50
|
+
* <ResolvedField {...fieldProps} />
|
|
51
|
+
* </Suspense>
|
|
52
|
+
* );
|
|
53
|
+
* ```
|
|
54
|
+
* */
|
|
55
|
+
export function useResolvedComponent<P = unknown>(
|
|
56
|
+
ref: ComponentRef<P> | undefined
|
|
57
|
+
): React.ComponentType<P> | undefined {
|
|
58
|
+
return useMemo(() => resolveComponentRef(ref), [ref]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wraps a loader function with `React.lazy()`, caching the result so the
|
|
63
|
+
* same loader always returns the same lazy component identity.
|
|
64
|
+
*/
|
|
65
|
+
function getOrCreateLazy<P>(
|
|
66
|
+
key: object | ((...args: any[]) => any),
|
|
67
|
+
loader: () => Promise<{ default: React.ComponentType<P> }>
|
|
68
|
+
): React.ComponentType<P> {
|
|
69
|
+
const cached = lazyCache.get(key);
|
|
70
|
+
if (cached) return cached as React.ComponentType<P>;
|
|
71
|
+
|
|
72
|
+
// SAFETY: React.lazy returns LazyExoticComponent which is structurally compatible with ComponentType<P>
|
|
73
|
+
const LazyComponent = lazy(loader) as React.ComponentType<P>;
|
|
74
|
+
lazyCache.set(key, LazyComponent);
|
|
75
|
+
return LazyComponent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Pure function version of the resolver, for use outside React components.
|
|
80
|
+
* Same resolution logic as `useResolvedComponent`.
|
|
81
|
+
*
|
|
82
|
+
* Results are cached per reference identity — calling this multiple times
|
|
83
|
+
* with the same `ref` object returns the same `React.ComponentType`.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveComponentRef<P = unknown>(
|
|
86
|
+
ref: ComponentRef<P> | undefined
|
|
87
|
+
): React.ComponentType<P> | undefined {
|
|
88
|
+
if (ref == null) return undefined;
|
|
89
|
+
|
|
90
|
+
// 1. String — should not happen at runtime (Vite transforms them).
|
|
91
|
+
// Log a warning and bail out.
|
|
92
|
+
if (typeof ref === "string") {
|
|
93
|
+
console.warn(
|
|
94
|
+
`[Rebase] Encountered a raw string ComponentRef ("${ref}") at runtime. ` +
|
|
95
|
+
"This usually means the Vite transform plugin did not process this file. " +
|
|
96
|
+
"Ensure the file is inside the configured collectionsDir."
|
|
97
|
+
);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. LazyComponentRef — produced by the Vite plugin from string paths.
|
|
102
|
+
// The object has { __rebaseLazy: true, load: () => import(...) }.
|
|
103
|
+
if (isLazyComponentRef(ref)) {
|
|
104
|
+
return getOrCreateLazy<P>(
|
|
105
|
+
ref as object,
|
|
106
|
+
() => (ref as LazyComponentRef<P>).load()
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 3. Function — either a React component or a lazy import loader.
|
|
111
|
+
if (typeof ref === "function") {
|
|
112
|
+
const fn = ref as (...args: any[]) => any;
|
|
113
|
+
|
|
114
|
+
// Class components (React.Component / PureComponent) have this flag
|
|
115
|
+
if (fn.prototype?.isReactComponent) {
|
|
116
|
+
return ref as React.ComponentType<P>;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// React internals (forwardRef, memo, etc.) carry $$typeof
|
|
120
|
+
if ("$$typeof" in fn) {
|
|
121
|
+
return ref as React.ComponentType<P>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// A function with declared parameters (fn.length > 0) is a regular
|
|
125
|
+
// component that accepts props — return as-is.
|
|
126
|
+
if (fn.length > 0) {
|
|
127
|
+
return ref as React.ComponentType<P>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Zero-parameter function — could be a React component with no props
|
|
131
|
+
// OR a lazy loader `() => import(...)`. We distinguish by checking
|
|
132
|
+
// if `fn.name` looks like a component (starts with uppercase) or is
|
|
133
|
+
// an anonymous arrow.
|
|
134
|
+
//
|
|
135
|
+
// Convention: named function components always start with an
|
|
136
|
+
// uppercase letter. Dynamic import wrappers are typically anonymous
|
|
137
|
+
// arrows or have lowercase names.
|
|
138
|
+
const name = fn.name;
|
|
139
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
140
|
+
// Named component (e.g. `function MyField() { ... }`) — return as-is
|
|
141
|
+
return ref as React.ComponentType<P>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Treat as a lazy loader — wrap with React.lazy, cached by identity.
|
|
145
|
+
return getOrCreateLazy<P>(
|
|
146
|
+
fn,
|
|
147
|
+
ref as () => Promise<{ default: React.ComponentType<P> }>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 4. React wrapper objects (React.forwardRef, React.memo) —
|
|
152
|
+
// These are objects with $$typeof but are not functions.
|
|
153
|
+
if (typeof ref === "object" && "$$typeof" in (ref as object)) {
|
|
154
|
+
return ref as unknown as React.ComponentType<P>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 5. Unknown shape — return undefined to be safe
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { SlotName, SlotRegistry } from "@rebasepro/types";
|
|
2
|
+
import React, { useMemo } from "react";
|
|
3
|
+
;
|
|
4
|
+
import { useCustomizationController } from "./useCustomizationController";
|
|
5
|
+
import { ErrorBoundary } from "@rebasepro/ui";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook that retrieves and renders all slot contributions for a given slot name.
|
|
9
|
+
*
|
|
10
|
+
* @param slot - The slot name to render contributions for.
|
|
11
|
+
* @param props - Props passed to each slot component.
|
|
12
|
+
* @returns An array of rendered React nodes, each wrapped in an ErrorBoundary.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const actions = useSlot("home.actions", { context });
|
|
17
|
+
* return <div>{actions}</div>;
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @group Hooks
|
|
21
|
+
*/
|
|
22
|
+
export function useSlot<K extends SlotName>(
|
|
23
|
+
slot: K,
|
|
24
|
+
props: SlotRegistry[K]
|
|
25
|
+
): React.ReactNode[] {
|
|
26
|
+
const { resolvedSlots } = useCustomizationController();
|
|
27
|
+
|
|
28
|
+
const propsRef = React.useRef(props);
|
|
29
|
+
const currentProps = props as unknown as Record<string, unknown>;
|
|
30
|
+
const prevProps = propsRef.current as unknown as Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
let changed = false;
|
|
33
|
+
if (currentProps !== prevProps) {
|
|
34
|
+
const keys = Object.keys(currentProps);
|
|
35
|
+
const prevKeys = Object.keys(prevProps);
|
|
36
|
+
if (keys.length !== prevKeys.length) {
|
|
37
|
+
changed = true;
|
|
38
|
+
} else {
|
|
39
|
+
for (let i = 0; i < keys.length; i++) {
|
|
40
|
+
if (currentProps[keys[i]] !== prevProps[keys[i]]) {
|
|
41
|
+
changed = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (changed) {
|
|
49
|
+
propsRef.current = props;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stableProps = propsRef.current;
|
|
53
|
+
|
|
54
|
+
return useMemo(() => {
|
|
55
|
+
return resolvedSlots
|
|
56
|
+
.filter(s => s.slot === slot)
|
|
57
|
+
.sort((a, b) => (a.order ?? 50) - (b.order ?? 50))
|
|
58
|
+
.map((s, i) => (
|
|
59
|
+
<ErrorBoundary key={`${slot}_${i}`}>
|
|
60
|
+
<s.Component {...(stableProps as unknown as Record<string, unknown>)} {...(s.props ?? {})}/>
|
|
61
|
+
</ErrorBoundary>
|
|
62
|
+
));
|
|
63
|
+
}, [resolvedSlots, slot, stableProps]);
|
|
64
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { SnackbarMessageType } from "@rebasepro/types";
|
|
2
|
+
import React, { useCallback, useMemo } from "react";
|
|
3
|
+
import { useSnackbar } from "notistack";
|
|
4
|
+
;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to retrieve the SnackbarContext.
|
|
8
|
+
*
|
|
9
|
+
* Consider that in order to use this hook you need to have a parent
|
|
10
|
+
* `Rebase`
|
|
11
|
+
*
|
|
12
|
+
* @see SnackbarController
|
|
13
|
+
* @group Hooks and utilities
|
|
14
|
+
*/
|
|
15
|
+
export const useSnackbarController = () => {
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
enqueueSnackbar,
|
|
19
|
+
closeSnackbar
|
|
20
|
+
} = useSnackbar();
|
|
21
|
+
|
|
22
|
+
const open = useCallback((props: {
|
|
23
|
+
type: SnackbarMessageType;
|
|
24
|
+
title?: string;
|
|
25
|
+
message: React.ReactNode;
|
|
26
|
+
autoHideDuration?: number;
|
|
27
|
+
}) => {
|
|
28
|
+
const {
|
|
29
|
+
type,
|
|
30
|
+
message,
|
|
31
|
+
autoHideDuration
|
|
32
|
+
} = props;
|
|
33
|
+
enqueueSnackbar({
|
|
34
|
+
message: props.title ? <div className={"flex flex-col"}>
|
|
35
|
+
<strong>{props.title}</strong>
|
|
36
|
+
{message}
|
|
37
|
+
</div> : message,
|
|
38
|
+
variant: type,
|
|
39
|
+
autoHideDuration
|
|
40
|
+
})
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const close = useCallback(() => {
|
|
44
|
+
closeSnackbar();
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
return useMemo(() => ({
|
|
48
|
+
open,
|
|
49
|
+
close
|
|
50
|
+
}), [open, close]);
|
|
51
|
+
|
|
52
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StorageSource } from "@rebasepro/types";
|
|
2
|
+
import { StorageSourceContext } from "../contexts/StorageSourceContext";
|
|
3
|
+
import { useContext } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Use this hook to get the storage source being used
|
|
7
|
+
* @group Hooks and utilities
|
|
8
|
+
*/
|
|
9
|
+
export const useStorageSource = (): StorageSource => {
|
|
10
|
+
return useContext(StorageSourceContext);
|
|
11
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
CollectionRegistryController,
|
|
4
|
+
SidePanelController,
|
|
5
|
+
UrlController,
|
|
6
|
+
NavigationStateController,
|
|
7
|
+
BreadcrumbEntry,
|
|
8
|
+
BreadcrumbsController
|
|
9
|
+
} from "@rebasepro/types";
|
|
10
|
+
|
|
11
|
+
export type { BreadcrumbEntry, BreadcrumbsController };
|
|
12
|
+
|
|
13
|
+
// ─── Breadcrumbs ──────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// ─── Bridge interface ───────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* StudioBridge provides optional CMS capabilities to Studio components.
|
|
20
|
+
* When CMS is present, a bridge provider injects real implementations.
|
|
21
|
+
* When CMS is absent, noop defaults ensure Studio works standalone.
|
|
22
|
+
*/
|
|
23
|
+
export interface StudioBridge {
|
|
24
|
+
collectionRegistry: CollectionRegistryController;
|
|
25
|
+
sidePanelController: SidePanelController;
|
|
26
|
+
urlController: UrlController;
|
|
27
|
+
navigationState: NavigationStateController;
|
|
28
|
+
breadcrumbs: BreadcrumbsController;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Noop defaults ──────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const NOOP_COLLECTION_REGISTRY: CollectionRegistryController = {
|
|
34
|
+
getCollection: () => undefined,
|
|
35
|
+
getRawCollection: () => undefined,
|
|
36
|
+
getParentReferencesFromPath: () => [],
|
|
37
|
+
getParentCollectionSlugs: () => [],
|
|
38
|
+
getParentEntityIds: () => [],
|
|
39
|
+
convertIdsToPaths: () => [],
|
|
40
|
+
initialised: false
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const NOOP_SIDE_PANEL: SidePanelController = {
|
|
44
|
+
open: () => {},
|
|
45
|
+
replace: () => {},
|
|
46
|
+
close: () => {}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const NOOP_URL_CONTROLLER: UrlController = {
|
|
50
|
+
basePath: "/",
|
|
51
|
+
baseCollectionPath: "/c",
|
|
52
|
+
urlPathToDataPath: () => "",
|
|
53
|
+
homeUrl: "/",
|
|
54
|
+
isUrlCollectionPath: () => false,
|
|
55
|
+
buildUrlCollectionPath: () => "",
|
|
56
|
+
buildAppUrlPath: () => "",
|
|
57
|
+
resolveDatabasePathsFrom: () => "",
|
|
58
|
+
navigate: () => {}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const NOOP_NAVIGATION_STATE: NavigationStateController = {
|
|
62
|
+
loading: false,
|
|
63
|
+
refreshNavigation: () => {}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const NOOP_BREADCRUMBS: BreadcrumbsController = {
|
|
67
|
+
breadcrumbs: [],
|
|
68
|
+
set: () => {},
|
|
69
|
+
updateCount: () => {}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const NOOP_BRIDGE: StudioBridge = {
|
|
73
|
+
collectionRegistry: NOOP_COLLECTION_REGISTRY,
|
|
74
|
+
sidePanelController: NOOP_SIDE_PANEL,
|
|
75
|
+
urlController: NOOP_URL_CONTROLLER,
|
|
76
|
+
navigationState: NOOP_NAVIGATION_STATE,
|
|
77
|
+
breadcrumbs: NOOP_BREADCRUMBS
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// ─── Context & Provider ─────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export const StudioBridgeContext = createContext<StudioBridge>(NOOP_BRIDGE);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Provider that injects CMS capabilities into Studio.
|
|
86
|
+
* Accepts partial overrides — any field not provided falls back to noop.
|
|
87
|
+
*
|
|
88
|
+
* Usage (in app wiring, when CMS is present):
|
|
89
|
+
* ```tsx
|
|
90
|
+
* <StudioBridgeProvider value={{
|
|
91
|
+
* collectionRegistry: useCollectionRegistryController(),
|
|
92
|
+
* sidePanelController: useSidePanel(),
|
|
93
|
+
* urlController: useUrlController(),
|
|
94
|
+
* navigationState: useNavigationStateController(),
|
|
95
|
+
* breadcrumbs: useBreadcrumbsController(),
|
|
96
|
+
* }}>
|
|
97
|
+
* <RebaseStudio ... />
|
|
98
|
+
* </StudioBridgeProvider>
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function StudioBridgeProvider({
|
|
102
|
+
value,
|
|
103
|
+
children
|
|
104
|
+
}: {
|
|
105
|
+
value: Partial<StudioBridge>;
|
|
106
|
+
children: React.ReactNode;
|
|
107
|
+
}) {
|
|
108
|
+
const merged = React.useMemo(
|
|
109
|
+
() => ({ ...NOOP_BRIDGE,
|
|
110
|
+
...value }),
|
|
111
|
+
[value]
|
|
112
|
+
);
|
|
113
|
+
return (
|
|
114
|
+
<StudioBridgeContext.Provider value={merged}>
|
|
115
|
+
{children}
|
|
116
|
+
</StudioBridgeContext.Provider>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Convenience hooks ──────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/** Collection registry — returns noop if CMS is not present. */
|
|
123
|
+
export function useStudioCollectionRegistry(): CollectionRegistryController {
|
|
124
|
+
return useContext(StudioBridgeContext).collectionRegistry;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Side panel controller — returns noop if CMS is not present. */
|
|
128
|
+
export function useStudioSidePanelController(): SidePanelController {
|
|
129
|
+
return useContext(StudioBridgeContext).sidePanelController;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** URL controller — returns noop if CMS is not present. */
|
|
133
|
+
export function useStudioUrlController(): UrlController {
|
|
134
|
+
return useContext(StudioBridgeContext).urlController;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Navigation state — returns noop if CMS is not present. */
|
|
138
|
+
export function useStudioNavigationState(): NavigationStateController {
|
|
139
|
+
return useContext(StudioBridgeContext).navigationState;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Breadcrumbs controller — returns noop if CMS is not present. */
|
|
143
|
+
export function useStudioBreadcrumbs(): BreadcrumbsController {
|
|
144
|
+
return useContext(StudioBridgeContext).breadcrumbs;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Self-Assembling Bridge Registry ────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Registry that controllers use to self-register their implementations
|
|
151
|
+
* into the Studio bridge. Each controller calls `register(key, value)`
|
|
152
|
+
* on mount and `unregister(key)` on unmount.
|
|
153
|
+
*/
|
|
154
|
+
export interface StudioBridgeRegistry {
|
|
155
|
+
register: <K extends keyof StudioBridge>(key: K, value: StudioBridge[K]) => void;
|
|
156
|
+
unregister: (key: keyof StudioBridge) => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const StudioBridgeRegistryContext = createContext<StudioBridgeRegistry | null>(null);
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Provider that creates a self-assembling bridge.
|
|
163
|
+
*
|
|
164
|
+
* Mount this above the controller providers. Each controller calls
|
|
165
|
+
* `useBridgeRegistration(key, value)` to inject its implementation.
|
|
166
|
+
* The bridge context value is automatically kept in sync.
|
|
167
|
+
*
|
|
168
|
+
* ```tsx
|
|
169
|
+
* <StudioBridgeRegistryProvider>
|
|
170
|
+
* <CollectionRegistryProvider> // auto-registers
|
|
171
|
+
* <SidePanelProvider> // auto-registers
|
|
172
|
+
* <UrlProvider> // auto-registers
|
|
173
|
+
* <RebaseStudio /> // consumes bridge
|
|
174
|
+
* </UrlProvider>
|
|
175
|
+
* </SidePanelProvider>
|
|
176
|
+
* </CollectionRegistryProvider>
|
|
177
|
+
* </StudioBridgeRegistryProvider>
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export function StudioBridgeRegistryProvider({ children }: { children: React.ReactNode }) {
|
|
181
|
+
const [version, setVersion] = useState(0);
|
|
182
|
+
const slicesRef = useRef<Partial<StudioBridge>>({});
|
|
183
|
+
|
|
184
|
+
const register = useCallback(<K extends keyof StudioBridge>(key: K, value: StudioBridge[K]) => {
|
|
185
|
+
slicesRef.current[key] = value;
|
|
186
|
+
setVersion(v => v + 1);
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
const unregister = useCallback((key: keyof StudioBridge) => {
|
|
190
|
+
delete slicesRef.current[key];
|
|
191
|
+
setVersion(v => v + 1);
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
const registry = useMemo<StudioBridgeRegistry>(() => ({ register,
|
|
195
|
+
unregister }), [register, unregister]);
|
|
196
|
+
|
|
197
|
+
const bridgeValue = useMemo<StudioBridge>(() => ({
|
|
198
|
+
...NOOP_BRIDGE,
|
|
199
|
+
...slicesRef.current
|
|
200
|
+
}), [version]);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<StudioBridgeRegistryContext.Provider value={registry}>
|
|
204
|
+
<StudioBridgeContext.Provider value={bridgeValue}>
|
|
205
|
+
{children}
|
|
206
|
+
</StudioBridgeContext.Provider>
|
|
207
|
+
</StudioBridgeRegistryContext.Provider>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useTranslation as useI18nTranslation } from "react-i18next";
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
3
|
+
|
|
4
|
+
const REBASE_NS = "rebase_core";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal hook for translating Rebase UI strings.
|
|
8
|
+
*
|
|
9
|
+
* Uses the `rebase_core` i18next namespace that is initialised by
|
|
10
|
+
* `RebaseI18nProvider`. Do NOT use `react-i18next` directly in internal
|
|
11
|
+
* components — always go through this hook so the namespace is consistent.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { t } = useTranslation();
|
|
15
|
+
* <Button>{t("save")}</Button>
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function useTranslation() {
|
|
20
|
+
const { t, i18n } = useI18nTranslation(REBASE_NS);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Typed translation function scoped to RebaseTranslations keys.
|
|
24
|
+
* Also supports i18next interpolation variables, e.g.
|
|
25
|
+
* t("add_to_field", { fieldName: "Tags" })
|
|
26
|
+
* t("error_deleting", { message: err.message })
|
|
27
|
+
*/
|
|
28
|
+
const typedT = useCallback((key: string, vars?: Record<string, string>): string =>
|
|
29
|
+
t(key, vars) as string, [t]);
|
|
30
|
+
|
|
31
|
+
return useMemo(() => ({ t: typedT,
|
|
32
|
+
i18n }), [typedT, i18n]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useBlocker } from "react-router-dom";
|
|
3
|
+
import { UnsavedChangesDialogProps } from "../components/UnsavedChangesDialog";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A single, unified hook to prevent navigation when there are unsaved changes.
|
|
7
|
+
*
|
|
8
|
+
* It automatically handles:
|
|
9
|
+
* 1. Internal React Router navigation using `useBlocker`.
|
|
10
|
+
* 2. External browser navigation (page refresh, tab close) using `beforeunload`.
|
|
11
|
+
*/
|
|
12
|
+
export function useUnsavedChangesDialog(
|
|
13
|
+
when: boolean,
|
|
14
|
+
onOk: () => void
|
|
15
|
+
): {
|
|
16
|
+
dialogProps: UnsavedChangesDialogProps;
|
|
17
|
+
triggerDialog: () => void;
|
|
18
|
+
} {
|
|
19
|
+
const [manualDialogOpen, setManualDialogOpen] = useState(false);
|
|
20
|
+
|
|
21
|
+
const blocker = useBlocker(
|
|
22
|
+
({ currentLocation, nextLocation }) =>
|
|
23
|
+
when && currentLocation.pathname !== nextLocation.pathname
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!when) return;
|
|
28
|
+
|
|
29
|
+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
e.returnValue = "";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
35
|
+
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
36
|
+
}, [when]);
|
|
37
|
+
|
|
38
|
+
const handleOk = useCallback(() => {
|
|
39
|
+
onOk();
|
|
40
|
+
if (blocker.state === "blocked") {
|
|
41
|
+
blocker.proceed();
|
|
42
|
+
}
|
|
43
|
+
setManualDialogOpen(false);
|
|
44
|
+
}, [blocker, onOk]);
|
|
45
|
+
|
|
46
|
+
const handleCancel = useCallback(() => {
|
|
47
|
+
if (blocker.state === "blocked") {
|
|
48
|
+
blocker.reset();
|
|
49
|
+
}
|
|
50
|
+
setManualDialogOpen(false);
|
|
51
|
+
}, [blocker]);
|
|
52
|
+
|
|
53
|
+
const triggerDialog = useCallback(() => setManualDialogOpen(true), []);
|
|
54
|
+
|
|
55
|
+
return useMemo(() => ({
|
|
56
|
+
dialogProps: {
|
|
57
|
+
open: manualDialogOpen || blocker.state === "blocked",
|
|
58
|
+
handleOk,
|
|
59
|
+
handleCancel,
|
|
60
|
+
body: "There are unsaved changes in this collection"
|
|
61
|
+
},
|
|
62
|
+
triggerDialog
|
|
63
|
+
}), [manualDialogOpen, blocker.state, handleOk, handleCancel, triggerDialog]);
|
|
64
|
+
}
|