@firecms/core 3.0.0-canary.168 → 3.0.0-canary.169

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 (96) hide show
  1. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
  2. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
  3. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +15 -1
  4. package/dist/components/EntityCollectionTable/index.d.ts +1 -1
  5. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +4 -0
  6. package/dist/components/SelectableTable/SelectableTable.d.ts +12 -2
  7. package/dist/components/UnsavedChangesDialog.d.ts +8 -0
  8. package/dist/components/VirtualTable/VirtualTableProps.d.ts +13 -0
  9. package/dist/components/common/index.d.ts +1 -1
  10. package/dist/components/common/{useDataSourceEntityCollectionTableController.d.ts → useDataSourceTableController.d.ts} +10 -2
  11. package/dist/components/common/useDebouncedCallback.d.ts +1 -0
  12. package/dist/components/common/useScrollRestoration.d.ts +14 -0
  13. package/dist/contexts/BreacrumbsContext.d.ts +8 -0
  14. package/dist/core/EntityEditView.d.ts +25 -7
  15. package/dist/core/FireCMSRouter.d.ts +4 -0
  16. package/dist/core/NavigationRoutes.d.ts +0 -1
  17. package/dist/core/index.d.ts +2 -1
  18. package/dist/hooks/data/useEntityFetch.d.ts +2 -1
  19. package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
  20. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  21. package/dist/index.es.js +2344 -1305
  22. package/dist/index.es.js.map +1 -1
  23. package/dist/index.umd.js +2344 -1306
  24. package/dist/index.umd.js.map +1 -1
  25. package/dist/internal/useUnsavedChangesDialog.d.ts +7 -9
  26. package/dist/routes/CustomCMSRoute.d.ts +4 -0
  27. package/dist/routes/FireCMSRoute.d.ts +1 -0
  28. package/dist/routes/HomePageRoute.d.ts +3 -0
  29. package/dist/types/collections.d.ts +18 -6
  30. package/dist/types/datasource.d.ts +2 -2
  31. package/dist/types/dialogs_controller.d.ts +3 -7
  32. package/dist/types/entities.d.ts +1 -1
  33. package/dist/types/entity_actions.d.ts +1 -6
  34. package/dist/types/navigation.d.ts +28 -12
  35. package/dist/types/plugins.d.ts +1 -0
  36. package/dist/types/side_entity_controller.d.ts +1 -1
  37. package/dist/util/entity_cache.d.ts +23 -0
  38. package/dist/util/navigation_from_path.d.ts +5 -1
  39. package/dist/util/navigation_utils.d.ts +12 -1
  40. package/package.json +5 -5
  41. package/src/components/CircularProgressCenter.tsx +1 -1
  42. package/src/components/DeleteEntityDialog.tsx +1 -1
  43. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +40 -19
  44. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -4
  45. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +18 -1
  46. package/src/components/EntityCollectionTable/index.tsx +1 -1
  47. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
  48. package/src/components/EntityCollectionView/EntityCollectionView.tsx +79 -38
  49. package/src/components/NotFoundPage.tsx +2 -2
  50. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -5
  51. package/src/components/SelectableTable/SelectableTable.tsx +18 -3
  52. package/src/components/UnsavedChangesDialog.tsx +42 -0
  53. package/src/components/VirtualTable/VirtualTable.tsx +19 -2
  54. package/src/components/VirtualTable/VirtualTableProps.tsx +15 -0
  55. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +1 -1
  56. package/src/components/common/default_entity_actions.tsx +24 -10
  57. package/src/components/common/index.ts +1 -1
  58. package/src/components/common/{useDataSourceEntityCollectionTableController.tsx → useDataSourceTableController.tsx} +122 -13
  59. package/src/components/common/useDebouncedCallback.tsx +20 -0
  60. package/src/components/common/useScrollRestoration.tsx +68 -0
  61. package/src/contexts/BreacrumbsContext.tsx +38 -0
  62. package/src/contexts/DialogsProvider.tsx +2 -3
  63. package/src/core/DefaultAppBar.tsx +28 -4
  64. package/src/core/EntityEditView.tsx +457 -296
  65. package/src/core/EntitySidePanel.tsx +72 -6
  66. package/src/core/FireCMS.tsx +7 -4
  67. package/src/core/FireCMSRouter.tsx +17 -0
  68. package/src/core/NavigationRoutes.tsx +15 -28
  69. package/src/core/SideDialogs.tsx +10 -5
  70. package/src/core/index.tsx +4 -2
  71. package/src/hooks/data/save.ts +1 -1
  72. package/src/hooks/data/useCollectionFetch.tsx +1 -1
  73. package/src/hooks/data/useEntityFetch.tsx +5 -1
  74. package/src/hooks/useBreadcrumbsController.tsx +31 -0
  75. package/src/hooks/useBuildNavigationController.tsx +59 -21
  76. package/src/hooks/useLargeLayout.tsx +0 -35
  77. package/src/hooks/useResolvedNavigationFrom.tsx +4 -6
  78. package/src/internal/useBuildSideDialogsController.tsx +4 -2
  79. package/src/internal/useBuildSideEntityController.tsx +157 -77
  80. package/src/internal/useUnsavedChangesDialog.tsx +126 -92
  81. package/src/routes/CustomCMSRoute.tsx +21 -0
  82. package/src/routes/FireCMSRoute.tsx +230 -0
  83. package/src/routes/HomePageRoute.tsx +17 -0
  84. package/src/types/collections.ts +19 -6
  85. package/src/types/datasource.ts +2 -1
  86. package/src/types/dialogs_controller.tsx +3 -7
  87. package/src/types/entities.ts +1 -1
  88. package/src/types/entity_actions.tsx +1 -7
  89. package/src/types/navigation.ts +35 -16
  90. package/src/types/plugins.tsx +1 -0
  91. package/src/types/side_entity_controller.tsx +1 -1
  92. package/src/util/entity_cache.ts +196 -0
  93. package/src/util/navigation_from_path.ts +10 -2
  94. package/src/util/navigation_utils.ts +53 -1
  95. package/src/util/parent_references_from_path.ts +3 -3
  96. /package/src/util/{common.tsx → common.ts} +0 -0
@@ -8,18 +8,19 @@ import {
8
8
  SideDialogsController,
9
9
  SideEntityController
10
10
  } from "../types";
11
- import { getNavigationEntriesFromPathInternal, NavigationViewInternal } from "../util/navigation_from_path";
11
+ import { getNavigationEntriesFromPath, NavigationViewInternal } from "../util/navigation_from_path";
12
12
  import { useLocation } from "react-router-dom";
13
13
  import { removeInitialAndTrailingSlashes, resolveCollection, resolveDefaultSelectedView } from "../util";
14
14
  import { ADDITIONAL_TAB_WIDTH, CONTAINER_FULL_WIDTH, FORM_CONTAINER_WIDTH } from "./common";
15
15
  import { useLargeLayout } from "../hooks";
16
16
  import { EntitySidePanel } from "../core/EntitySidePanel";
17
17
 
18
- const NEW_URL_HASH = "new";
18
+ const NEW_URL_HASH = "new_side";
19
+ const SIDE_URL_HASH = "side";
19
20
 
20
21
  export function getEntityViewWidth(props: EntitySidePanelProps<any>, small: boolean): string {
21
22
  if (small) return CONTAINER_FULL_WIDTH;
22
- const mainViewSelected = !props.selectedSubPath;
23
+ const mainViewSelected = !props.selectedTab;
23
24
  let resolvedWidth: string | undefined;
24
25
  if (props.width) {
25
26
  resolvedWidth = typeof props.width === "number" ? `${props.width}px` : props.width;
@@ -93,32 +94,55 @@ export const useBuildSideEntityController = (navigation: NavigationController,
93
94
 
94
95
  // only on initialisation, create panels from URL
95
96
  useEffect(() => {
96
- if (!navigation.loading && !initialised.current) {
97
- console.debug("Initialising side entity controller");
98
- if (navigation.isUrlCollectionPath(location.pathname)) {
99
- const newFlag = location.hash === `#${NEW_URL_HASH}`;
97
+
98
+ const newFlag = location.hash === `#${NEW_URL_HASH}`;
99
+ const sideFlag = location.hash === `#${SIDE_URL_HASH}`;
100
+
101
+ if (!navigation.loading) {
102
+ if ((newFlag || sideFlag) && navigation.isUrlCollectionPath(location.pathname)) {
100
103
  const entityOrCollectionPath = navigation.urlPathToDataPath(location.pathname);
101
104
  const panelsFromUrl = buildSidePanelsFromUrl(entityOrCollectionPath, navigation.collections ?? [], newFlag);
102
105
  for (let i = 0; i < panelsFromUrl.length; i++) {
103
106
  const props = panelsFromUrl[i];
104
- setTimeout(() => {
105
- if (i === 0)
106
- sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveAliasesFrom, smallLayout));
107
- else
108
- sideDialogsController.open(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveAliasesFrom, smallLayout))
109
- }, 1);
107
+ // setTimeout(() => {
108
+ if (i === 0)
109
+ sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout));
110
+ else
111
+ sideDialogsController.open(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout))
112
+ // }, 1);
110
113
  }
111
- } else {
112
- // console.warn("Location path is not a collection path");
113
114
  }
114
115
  initialised.current = true;
115
116
  }
116
- }, [location, navigation.loading, navigation.isUrlCollectionPath, navigation.buildUrlCollectionPath, navigation.resolveAliasesFrom, sideDialogsController, smallLayout, navigation]);
117
+ }, [navigation.loading]);
117
118
 
119
+ // sync panels if URL changes with #side
120
+ const currentPanelKeys = sideDialogsController.sidePanels.map(p => p.key);
121
+ useEffect(() => {
122
+ if (initialised.current) {
123
+ const sideFlag = location.hash === `#${SIDE_URL_HASH}`;
124
+ if (sideFlag) {
125
+ const entityOrCollectionPath = navigation.urlPathToDataPath(location.pathname);
126
+ const panelsFromUrl = buildSidePanelsFromUrl(entityOrCollectionPath, navigation.collections ?? [], false);
127
+ // if we have more panels than determined by the url, we ignore the url. We might have references open
128
+ if (panelsFromUrl.length <= currentPanelKeys.length) {
129
+ return;
130
+ }
131
+ const lastPanel = panelsFromUrl[panelsFromUrl.length - 1];
132
+ const panelProps = propsToSidePanel(lastPanel, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout);
133
+ const lastCurrentPanel = currentPanelKeys.length > 0 ? currentPanelKeys[currentPanelKeys.length - 1] : undefined;
134
+ if (!lastCurrentPanel || lastCurrentPanel !== panelProps.key) {
135
+ sideDialogsController.replace(panelProps);
136
+ }
137
+ }
138
+ }
139
+ }, [location.pathname, location.hash, currentPanelKeys]);
140
+
141
+ // update side panels to match browser size
118
142
  useEffect(() => {
119
143
  const updatedSidePanels = sideDialogsController.sidePanels.map(sidePanelProps => {
120
144
  if (sidePanelProps.additional) {
121
- return propsToSidePanel(sidePanelProps.additional, navigation.buildUrlCollectionPath, navigation.resolveAliasesFrom, smallLayout);
145
+ return propsToSidePanel(sidePanelProps.additional, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout);
122
146
  }
123
147
  return sidePanelProps;
124
148
  });
@@ -143,12 +167,17 @@ export const useBuildSideEntityController = (navigation: NavigationController,
143
167
  }
144
168
  );
145
169
 
146
- sideDialogsController.open(propsToSidePanel({
147
- selectedSubPath: defaultSelectedView,
148
- ...props,
149
- }, navigation.buildUrlCollectionPath, navigation.resolveAliasesFrom, smallLayout));
170
+ sideDialogsController.open(
171
+ propsToSidePanel({
172
+ selectedTab: defaultSelectedView,
173
+ ...props
174
+ },
175
+ navigation.buildUrlCollectionPath,
176
+ navigation.resolveIdsFrom,
177
+ smallLayout
178
+ ));
150
179
 
151
- }, [sideDialogsController, navigation.buildUrlCollectionPath, navigation.resolveAliasesFrom, smallLayout]);
180
+ }, [sideDialogsController, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout]);
152
181
 
153
182
  const replace = useCallback((props: EntitySidePanelProps<any>) => {
154
183
 
@@ -156,9 +185,9 @@ export const useBuildSideEntityController = (navigation: NavigationController,
156
185
  throw Error("If you want to copy an entity you need to provide an entityId");
157
186
  }
158
187
 
159
- sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveAliasesFrom, smallLayout));
188
+ sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout));
160
189
 
161
- }, [navigation.buildUrlCollectionPath, navigation.resolveAliasesFrom, sideDialogsController, smallLayout]);
190
+ }, [navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, sideDialogsController, smallLayout]);
162
191
 
163
192
  return {
164
193
  close,
@@ -169,12 +198,12 @@ export const useBuildSideEntityController = (navigation: NavigationController,
169
198
 
170
199
  export function buildSidePanelsFromUrl(path: string, collections: EntityCollection[], newFlag: boolean): EntitySidePanelProps<any>[] {
171
200
 
172
- const navigationViewsForPath: NavigationViewInternal<any>[] = getNavigationEntriesFromPathInternal({
201
+ const navigationViewsForPath: NavigationViewInternal<any>[] = getNavigationEntriesFromPath({
173
202
  path,
174
203
  collections
175
204
  });
176
205
 
177
- const sidePanels: EntitySidePanelProps<any>[] = [];
206
+ let sidePanel: EntitySidePanelProps<any> | undefined = undefined;
178
207
  let lastCollectionPath = "";
179
208
  for (let i = 0; i < navigationViewsForPath.length; i++) {
180
209
  const navigationEntry = navigationViewsForPath[i];
@@ -183,70 +212,121 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
183
212
  lastCollectionPath = navigationEntry.path;
184
213
  }
185
214
 
186
- if (i > 0) { // the first collection is handled by the main navigation
187
- const previousEntry = navigationViewsForPath[i - 1];
188
- if (navigationEntry.type === "entity") {
189
- sidePanels.push({
190
- path: navigationEntry.path,
191
- entityId: navigationEntry.entityId,
192
- copy: false,
193
- width: navigationEntry.parentCollection?.sideDialogWidth
194
- }
195
- );
196
- } else if (navigationEntry.type === "custom_view") {
197
- if (previousEntry.type === "entity") {
198
- const lastSidePanel: EntitySidePanelProps<any> = sidePanels[sidePanels.length - 1];
199
- if (lastSidePanel)
200
- lastSidePanel.selectedSubPath = navigationEntry.view.key;
201
- }
202
- } else if (navigationEntry.type === "collection") {
203
- if (previousEntry.type === "entity") {
204
- const lastSidePanel: EntitySidePanelProps<any> = sidePanels[sidePanels.length - 1];
205
- if (lastSidePanel)
206
- lastSidePanel.selectedSubPath = navigationEntry.collection.id ?? navigationEntry.collection.path;
207
- }
215
+ const previousEntry = navigationViewsForPath[i - 1];
216
+ if (navigationEntry.type === "entity") {
217
+ sidePanel = {
218
+ path: navigationEntry.path,
219
+ entityId: navigationEntry.entityId,
220
+ copy: false,
221
+ width: navigationEntry.parentCollection?.sideDialogWidth
222
+ };
223
+ } else if (navigationEntry.type === "custom_view") {
224
+ if (previousEntry?.type === "entity") {
225
+ if (sidePanel)
226
+ sidePanel.selectedTab = navigationEntry.view.key;
227
+ }
228
+ } else if (navigationEntry.type === "collection") {
229
+ if (previousEntry?.type === "entity") {
230
+ if (sidePanel)
231
+ sidePanel.selectedTab = navigationEntry.collection.id ?? navigationEntry.collection.path;
208
232
  }
209
233
  }
210
234
 
211
235
  }
212
236
 
213
237
  if (newFlag) {
214
- sidePanels.push({
238
+ sidePanel = {
215
239
  path: lastCollectionPath,
216
240
  copy: false
217
- });
241
+ }
218
242
  }
219
243
 
220
- return sidePanels;
244
+ return sidePanel ? [sidePanel] : [];
221
245
  }
222
246
 
247
+ // export function buildSidePanelsFromUrl(path: string, collections: EntityCollection[], newFlag: boolean): EntitySidePanelProps<any>[] {
248
+ //
249
+ // const navigationViewsForPath: NavigationViewInternal<any>[] = getNavigationEntriesFromPathInternal({
250
+ // path,
251
+ // collections
252
+ // });
253
+ //
254
+ // const sidePanels: EntitySidePanelProps<any>[] = [];
255
+ // let lastCollectionPath = "";
256
+ // for (let i = 0; i < navigationViewsForPath.length; i++) {
257
+ // const navigationEntry = navigationViewsForPath[i];
258
+ //
259
+ // if (navigationEntry.type === "collection") {
260
+ // lastCollectionPath = navigationEntry.path;
261
+ // }
262
+ //
263
+ // if (i > 0) { // the first collection is handled by the main navigation
264
+ // const previousEntry = navigationViewsForPath[i - 1];
265
+ // if (navigationEntry.type === "entity") {
266
+ // sidePanels.push({
267
+ // path: navigationEntry.path,
268
+ // entityId: navigationEntry.entityId,
269
+ // copy: false,
270
+ // width: navigationEntry.parentCollection?.sideDialogWidth
271
+ // }
272
+ // );
273
+ // } else if (navigationEntry.type === "custom_view") {
274
+ // if (previousEntry.type === "entity") {
275
+ // const lastSidePanel: EntitySidePanelProps<any> = sidePanels[sidePanels.length - 1];
276
+ // if (lastSidePanel)
277
+ // lastSidePanel.selectedTab = navigationEntry.view.key;
278
+ // }
279
+ // } else if (navigationEntry.type === "collection") {
280
+ // if (previousEntry.type === "entity") {
281
+ // const lastSidePanel: EntitySidePanelProps<any> = sidePanels[sidePanels.length - 1];
282
+ // if (lastSidePanel)
283
+ // lastSidePanel.selectedTab = navigationEntry.collection.id ?? navigationEntry.collection.path;
284
+ // }
285
+ // }
286
+ // }
287
+ //
288
+ // }
289
+ //
290
+ // if (newFlag) {
291
+ // sidePanels.push({
292
+ // path: lastCollectionPath,
293
+ // copy: false
294
+ // });
295
+ // }
296
+ //
297
+ // return sidePanels;
298
+ // }
299
+
223
300
  const propsToSidePanel = (props: EntitySidePanelProps,
224
301
  buildUrlCollectionPath: (path: string) => string,
225
- resolveAliasesFrom: (pathWithAliases: string) => string,
302
+ resolveIdsFrom: (pathWithAliases: string) => string,
226
303
  smallLayout: boolean): SideDialogPanelProps => {
227
304
 
228
- const collectionPath = removeInitialAndTrailingSlashes(props.path);
229
-
230
- const newPath = props.entityId
231
- ? buildUrlCollectionPath(`${collectionPath}/${props.entityId}/${props.selectedSubPath || ""}`)
232
- : buildUrlCollectionPath(`${collectionPath}#${NEW_URL_HASH}`);
233
- const resolvedPath = resolveAliasesFrom(props.path);
234
-
235
- const resolvedPanelProps: EntitySidePanelProps<any> = {
236
- ...props,
237
- path: resolvedPath,
238
- };
239
-
240
- const entityViewWidth = getEntityViewWidth(props, smallLayout);
241
-
242
- return {
243
- key: `${props.path}/${props.entityId}`,
244
- component: <EntitySidePanel {...resolvedPanelProps}/>,
245
- urlPath: newPath,
246
- parentUrlPath: buildUrlCollectionPath(collectionPath),
247
- width: entityViewWidth,
248
- onClose: props.onClose,
249
- additional: props
250
- };
251
- }
252
- ;
305
+ const collectionPath = removeInitialAndTrailingSlashes(props.path);
306
+
307
+ const newPath = props.entityId
308
+ ? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}#${SIDE_URL_HASH}`)
309
+ : buildUrlCollectionPath(`${collectionPath}#${NEW_URL_HASH}`);
310
+ const resolvedPath = resolveIdsFrom(props.path);
311
+
312
+ const resolvedPanelProps: EntitySidePanelProps<any> = {
313
+ ...props,
314
+ path: resolvedPath,
315
+ };
316
+
317
+ const entityViewWidth = getEntityViewWidth(props, smallLayout);
318
+
319
+ return {
320
+ key: `${props.path}/${props.entityId}`,
321
+ component: <EntitySidePanel {...resolvedPanelProps}/>,
322
+ urlPath: newPath,
323
+ parentUrlPath: buildUrlCollectionPath(collectionPath),
324
+ width: entityViewWidth,
325
+ onClose: props.onClose,
326
+ additional: props
327
+ };
328
+ }
329
+
330
+ function isSideUrl(url: string): boolean {
331
+ return url.endsWith("#" + SIDE_URL_HASH) || url.endsWith("#" + NEW_URL_HASH);
332
+ }
@@ -1,108 +1,142 @@
1
- import React, { useCallback } from "react";
2
- import { Blocker, Transition } from "history";
3
- import { UNSAFE_NavigationContext, useNavigate } from "react-router-dom";
1
+ import React, { useCallback, useEffect, useState } from "react";
4
2
  import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@firecms/ui";
5
3
 
6
- export function useNavigationUnsavedChangesDialog(when: boolean, onSuccess: () => void):
7
- {
8
- navigationWasBlocked: boolean,
9
- handleCancel: () => void,
10
- handleOk: () => void
11
- } {
12
-
13
- const [nextLocation, setNextLocation] = React.useState<any | undefined>();
14
- const { navigator } = React.useContext(UNSAFE_NavigationContext);
4
+ /**
5
+ * Type representing a pending navigation action.
6
+ */
7
+ type PendingNavigation =
8
+ | {
9
+ type: "popstate";
10
+ delta: number;
11
+ }
12
+ | {
13
+ type: "link";
14
+ href: string;
15
+ }
16
+ | null;
15
17
 
16
- const navigate = useNavigate();
18
+ /**
19
+ * Custom hook to handle navigation blocking when there are unsaved changes.
20
+ *
21
+ * @param when - Indicates whether to block navigation.
22
+ * @param onSuccess - Callback invoked when navigation is confirmed.
23
+ * @returns An object containing the state of navigation blocking and handlers.
24
+ */
25
+ export function useNavigationUnsavedChangesDialog(
26
+ when: boolean,
27
+ onSuccess: () => void
28
+ ): {
29
+ navigationWasBlocked: boolean;
30
+ handleCancel: () => void;
31
+ handleOk: () => void;
32
+ } {
33
+ const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
34
+ const [pendingNavigation, setPendingNavigation] = useState<PendingNavigation>(null);
17
35
 
18
- const handleCancel = () => {
19
- setNextLocation(undefined);
20
- };
36
+ /**
37
+ * Handler to cancel the navigation attempt.
38
+ */
39
+ const handleCancel = useCallback(() => {
40
+ setIsDialogOpen(false);
41
+ setPendingNavigation(null);
42
+ }, []);
21
43
 
22
- const handleOk = () => {
23
- onSuccess();
24
- setNextLocation(undefined);
25
- navigate(-1);
26
- };
44
+ /**
45
+ * Handler to confirm and proceed with the navigation.
46
+ */
47
+ const handleOk = useCallback(() => {
48
+ setIsDialogOpen(false);
49
+ if (pendingNavigation) {
50
+ onSuccess();
51
+ if (pendingNavigation.type === "popstate") {
52
+ window.history.go(pendingNavigation.delta);
53
+ } else if (pendingNavigation.type === "link") {
54
+ window.location.href = pendingNavigation.href;
55
+ }
56
+ setPendingNavigation(null);
57
+ }
58
+ }, [onSuccess, pendingNavigation]);
27
59
 
28
- const blocker: Blocker = useCallback(({
29
- action,
30
- location: nextLocation,
31
- retry
32
- }) => {
33
- switch (action) {
34
- case "REPLACE": {
35
- retry();
36
- return;
60
+ /**
61
+ * Event handler for beforeunload to handle page refresh or tab close.
62
+ */
63
+ const handleBeforeUnload = useCallback(
64
+ (e: BeforeUnloadEvent) => {
65
+ if (when) {
66
+ e.preventDefault();
67
+ e.returnValue = "";
37
68
  }
38
- case "POP": {
39
- setNextLocation(nextLocation);
69
+ },
70
+ [when]
71
+ );
72
+
73
+ /**
74
+ * Event handler for popstate to handle back and forward browser buttons.
75
+ */
76
+ const handlePopState = useCallback(
77
+ (e: PopStateEvent) => {
78
+ if (when) {
79
+ e.preventDefault();
80
+ // Assuming backward navigation; adjust delta as needed
81
+ setPendingNavigation({
82
+ type: "popstate",
83
+ delta: -1
84
+ });
85
+ setIsDialogOpen(true);
40
86
  }
41
- }
42
- }, []);
87
+ },
88
+ [when]
89
+ );
90
+
91
+ /**
92
+ * Event handler to intercept link clicks within the application.
93
+ */
94
+ const handleLinkClick = useCallback(
95
+ (e: MouseEvent) => {
96
+ if (!when) return;
43
97
 
44
- React.useEffect(() => {
45
- if (!when) return;
46
- if (nextLocation) return;
47
- if (!("block" in navigator)) return;
48
- const unblock = (navigator as any).block((tx: Transition) => {
49
- const autoUnblockingTx = {
50
- ...tx,
51
- retry() {
52
- unblock();
53
- tx.retry();
98
+ const target = e.target as HTMLElement;
99
+ const anchor = target.closest("a[href]") as HTMLAnchorElement | null;
100
+ if (anchor && anchor.host === window.location.host) {
101
+ e.preventDefault();
102
+ const href = anchor.getAttribute("href");
103
+ if (href) {
104
+ setPendingNavigation({
105
+ type: "link",
106
+ href
107
+ });
108
+ setIsDialogOpen(true);
54
109
  }
55
- };
56
- blocker(autoUnblockingTx);
57
- });
110
+ }
111
+ },
112
+ [when]
113
+ );
114
+
115
+ /**
116
+ * Effect hook to add and clean up event listeners based on the `when` condition.
117
+ */
118
+ useEffect(() => {
119
+ if (when) {
120
+ window.addEventListener("beforeunload", handleBeforeUnload);
121
+ window.addEventListener("popstate", handlePopState);
122
+ document.addEventListener("click", handleLinkClick);
123
+ } else {
124
+ window.removeEventListener("beforeunload", handleBeforeUnload);
125
+ window.removeEventListener("popstate", handlePopState);
126
+ document.removeEventListener("click", handleLinkClick);
127
+ }
58
128
 
59
- return unblock;
60
- }, [navigator, blocker, when, nextLocation]);
129
+ // Cleanup on unmount or when `when` changes
130
+ return () => {
131
+ window.removeEventListener("beforeunload", handleBeforeUnload);
132
+ window.removeEventListener("popstate", handlePopState);
133
+ document.removeEventListener("click", handleLinkClick);
134
+ };
135
+ }, [when, handleBeforeUnload, handlePopState, handleLinkClick]);
61
136
 
62
137
  return {
63
- navigationWasBlocked: Boolean(nextLocation),
138
+ navigationWasBlocked: isDialogOpen,
64
139
  handleCancel,
65
- handleOk
140
+ handleOk,
66
141
  };
67
142
  }
68
-
69
- export interface UnsavedChangesDialogProps {
70
- open: boolean;
71
- body?: React.ReactNode;
72
- title?: string;
73
- handleOk: () => void;
74
- handleCancel: () => void;
75
- }
76
-
77
- export function UnsavedChangesDialog({
78
- open,
79
- handleOk,
80
- handleCancel,
81
- body,
82
- title
83
- }: UnsavedChangesDialogProps) {
84
-
85
- return (
86
- <Dialog
87
- onEscapeKeyDown={() => {
88
- handleCancel();
89
- }}
90
- open={open}
91
- >
92
- <DialogTitle variant={"h6"}>{title}</DialogTitle>
93
- <DialogContent>
94
-
95
- {body}
96
-
97
- <Typography>
98
- Are you sure you want to leave this page?
99
- </Typography>
100
-
101
- </DialogContent>
102
- <DialogActions>
103
- <Button variant="text" onClick={handleCancel} autoFocus> Cancel </Button>
104
- <Button onClick={handleOk}> Ok </Button>
105
- </DialogActions>
106
- </Dialog>
107
- );
108
- }
@@ -0,0 +1,21 @@
1
+ import { CMSView } from "../types";
2
+ import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
3
+ import { useEffect } from "react";
4
+
5
+ export function CustomCMSRoute({ cmsView }: {
6
+ cmsView: CMSView
7
+ }) {
8
+
9
+ const breadcrumbs = useBreadcrumbsController();
10
+
11
+ useEffect(() => {
12
+ breadcrumbs.set({
13
+ breadcrumbs: [{
14
+ title: cmsView.name,
15
+ url: cmsView.path
16
+ }]
17
+ });
18
+ }, [cmsView.path]);
19
+
20
+ return cmsView.view;
21
+ }