@firecms/core 3.0.0-canary.167 → 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.
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +2 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
- package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +15 -1
- package/dist/components/EntityCollectionTable/index.d.ts +1 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +4 -0
- package/dist/components/SelectableTable/SelectableTable.d.ts +12 -2
- package/dist/components/UnsavedChangesDialog.d.ts +8 -0
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +13 -0
- package/dist/components/common/index.d.ts +1 -1
- package/dist/components/common/{useDataSourceEntityCollectionTableController.d.ts → useDataSourceTableController.d.ts} +10 -2
- package/dist/components/common/useDebouncedCallback.d.ts +1 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/contexts/BreacrumbsContext.d.ts +8 -0
- package/dist/core/EntityEditView.d.ts +25 -7
- package/dist/core/FireCMSRouter.d.ts +4 -0
- package/dist/core/NavigationRoutes.d.ts +0 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/hooks/data/useEntityFetch.d.ts +2 -1
- package/dist/hooks/useBreadcrumbsController.d.ts +26 -0
- package/dist/hooks/useBuildNavigationController.d.ts +0 -1
- package/dist/index.es.js +2344 -1305
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2344 -1306
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useUnsavedChangesDialog.d.ts +7 -9
- package/dist/routes/CustomCMSRoute.d.ts +4 -0
- package/dist/routes/FireCMSRoute.d.ts +1 -0
- package/dist/routes/HomePageRoute.d.ts +3 -0
- package/dist/types/collections.d.ts +18 -6
- package/dist/types/datasource.d.ts +2 -2
- package/dist/types/dialogs_controller.d.ts +2 -6
- package/dist/types/entities.d.ts +1 -1
- package/dist/types/entity_actions.d.ts +1 -6
- package/dist/types/navigation.d.ts +28 -12
- package/dist/types/plugins.d.ts +1 -0
- package/dist/types/side_entity_controller.d.ts +1 -1
- package/dist/util/entity_cache.d.ts +23 -0
- package/dist/util/navigation_from_path.d.ts +5 -1
- package/dist/util/navigation_utils.d.ts +12 -1
- package/package.json +5 -5
- package/src/components/CircularProgressCenter.tsx +1 -1
- package/src/components/DeleteEntityDialog.tsx +1 -1
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +40 -19
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +10 -4
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +18 -1
- package/src/components/EntityCollectionTable/index.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -3
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +79 -38
- package/src/components/NotFoundPage.tsx +2 -2
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -5
- package/src/components/SelectableTable/SelectableTable.tsx +18 -3
- package/src/components/UnsavedChangesDialog.tsx +42 -0
- package/src/components/VirtualTable/VirtualTable.tsx +19 -2
- package/src/components/VirtualTable/VirtualTableProps.tsx +15 -0
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +1 -1
- package/src/components/common/default_entity_actions.tsx +24 -10
- package/src/components/common/index.ts +1 -1
- package/src/components/common/{useDataSourceEntityCollectionTableController.tsx → useDataSourceTableController.tsx} +122 -13
- package/src/components/common/useDebouncedCallback.tsx +20 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/contexts/BreacrumbsContext.tsx +38 -0
- package/src/contexts/DialogsProvider.tsx +0 -1
- package/src/core/DefaultAppBar.tsx +28 -4
- package/src/core/EntityEditView.tsx +457 -296
- package/src/core/EntitySidePanel.tsx +72 -6
- package/src/core/FireCMS.tsx +7 -4
- package/src/core/FireCMSRouter.tsx +17 -0
- package/src/core/NavigationRoutes.tsx +15 -28
- package/src/core/SideDialogs.tsx +10 -5
- package/src/core/index.tsx +4 -2
- package/src/hooks/data/save.ts +1 -1
- package/src/hooks/data/useCollectionFetch.tsx +1 -1
- package/src/hooks/data/useEntityFetch.tsx +5 -1
- package/src/hooks/useBreadcrumbsController.tsx +31 -0
- package/src/hooks/useBuildNavigationController.tsx +59 -21
- package/src/hooks/useLargeLayout.tsx +0 -35
- package/src/hooks/useResolvedNavigationFrom.tsx +4 -6
- package/src/internal/useBuildSideDialogsController.tsx +4 -2
- package/src/internal/useBuildSideEntityController.tsx +157 -77
- package/src/internal/useUnsavedChangesDialog.tsx +126 -92
- package/src/routes/CustomCMSRoute.tsx +21 -0
- package/src/routes/FireCMSRoute.tsx +230 -0
- package/src/routes/HomePageRoute.tsx +17 -0
- package/src/types/collections.ts +19 -6
- package/src/types/datasource.ts +2 -1
- package/src/types/dialogs_controller.tsx +2 -6
- package/src/types/entities.ts +1 -1
- package/src/types/entity_actions.tsx +1 -7
- package/src/types/navigation.ts +35 -16
- package/src/types/plugins.tsx +1 -0
- package/src/types/side_entity_controller.tsx +1 -1
- package/src/util/entity_cache.ts +196 -0
- package/src/util/navigation_from_path.ts +10 -2
- package/src/util/navigation_utils.ts +53 -1
- package/src/util/parent_references_from_path.ts +3 -3
- /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 {
|
|
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 = "
|
|
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.
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
}, [
|
|
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.
|
|
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(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
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.
|
|
188
|
+
sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout));
|
|
160
189
|
|
|
161
|
-
}, [navigation.buildUrlCollectionPath, navigation.
|
|
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>[] =
|
|
201
|
+
const navigationViewsForPath: NavigationViewInternal<any>[] = getNavigationEntriesFromPath({
|
|
173
202
|
path,
|
|
174
203
|
collections
|
|
175
204
|
});
|
|
176
205
|
|
|
177
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
238
|
+
sidePanel = {
|
|
215
239
|
path: lastCollectionPath,
|
|
216
240
|
copy: false
|
|
217
|
-
}
|
|
241
|
+
}
|
|
218
242
|
}
|
|
219
243
|
|
|
220
|
-
return
|
|
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
|
-
|
|
302
|
+
resolveIdsFrom: (pathWithAliases: string) => string,
|
|
226
303
|
smallLayout: boolean): SideDialogPanelProps => {
|
|
227
304
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Handler to cancel the navigation attempt.
|
|
38
|
+
*/
|
|
39
|
+
const handleCancel = useCallback(() => {
|
|
40
|
+
setIsDialogOpen(false);
|
|
41
|
+
setPendingNavigation(null);
|
|
42
|
+
}, []);
|
|
21
43
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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:
|
|
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
|
+
}
|