@firecms/core 3.5.2-canary.16bf7bb → 3.5.2-canary.a36570e
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/EntityCollectionView/CollectionCountBadge.d.ts +8 -0
- package/dist/components/EntityCollectionView/CollectionPathTitle.d.ts +24 -0
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +5 -0
- package/dist/components/EntityCollectionView/EntityIdHeaderWidget.d.ts +12 -0
- package/dist/components/EntityCollectionView/utils.d.ts +4 -3
- package/dist/index.es.js +592 -567
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +592 -567
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useLeavePageConfirmation.d.ts +11 -0
- package/dist/types/side_dialogs_controller.d.ts +4 -2
- package/package.json +3 -3
- package/src/components/EntityCollectionView/CollectionCountBadge.tsx +17 -0
- package/src/components/EntityCollectionView/CollectionPathTitle.tsx +68 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +12 -111
- package/src/components/EntityCollectionView/EntityIdHeaderWidget.tsx +166 -0
- package/src/components/EntityCollectionView/utils.ts +13 -7
- package/src/components/common/default_entity_actions.tsx +2 -1
- package/src/core/DefaultAppBar.tsx +12 -9
- package/src/core/EntityEditView.tsx +26 -1
- package/src/core/SideDialogs.tsx +6 -13
- package/src/hooks/useReferenceDialog.tsx +2 -2
- package/src/internal/useBuildSideDialogsController.tsx +44 -2
- package/src/internal/useLeavePageConfirmation.ts +25 -0
- package/src/types/side_dialogs_controller.tsx +4 -2
- package/dist/internal/useUnsavedChangesDialog.d.ts +0 -12
- package/src/internal/useUnsavedChangesDialog.tsx +0 -142
package/src/core/SideDialogs.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { useSideDialogsController } from "../hooks";
|
|
3
3
|
import { EntitySidePanelProps, SideDialogPanelProps } from "../types";
|
|
4
4
|
import { Sheet } from "@firecms/ui";
|
|
5
|
-
import {
|
|
5
|
+
import { useLeavePageConfirmation } from "../internal/useLeavePageConfirmation";
|
|
6
6
|
import { ErrorBoundary } from "../components";
|
|
7
7
|
import { UnsavedChangesDialog } from "../components/UnsavedChangesDialog";
|
|
8
8
|
import { EntitySidePanel } from "./EntitySidePanel";
|
|
@@ -123,14 +123,7 @@ function SideDialogView({
|
|
|
123
123
|
|
|
124
124
|
const sideDialogsController = useSideDialogsController();
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
navigationWasBlocked,
|
|
128
|
-
handleOk: handleNavigationOk,
|
|
129
|
-
handleCancel: handleNavigationCancel
|
|
130
|
-
} = useNavigationUnsavedChangesDialog(
|
|
131
|
-
blocked && !drawerCloseRequested,
|
|
132
|
-
() => setBlocked(false)
|
|
133
|
-
);
|
|
126
|
+
useLeavePageConfirmation(blocked && !drawerCloseRequested);
|
|
134
127
|
|
|
135
128
|
useEffect(() => {
|
|
136
129
|
if (panel)
|
|
@@ -140,7 +133,7 @@ function SideDialogView({
|
|
|
140
133
|
const handleDrawerCloseOk = () => {
|
|
141
134
|
setBlocked(false);
|
|
142
135
|
setDrawerCloseRequested(false);
|
|
143
|
-
sideDialogsController.close();
|
|
136
|
+
sideDialogsController.close(panel?.key);
|
|
144
137
|
panel?.onClose?.();
|
|
145
138
|
};
|
|
146
139
|
|
|
@@ -152,7 +145,7 @@ function SideDialogView({
|
|
|
152
145
|
if (blocked && !force) {
|
|
153
146
|
setDrawerCloseRequested(true);
|
|
154
147
|
} else {
|
|
155
|
-
sideDialogsController.close();
|
|
148
|
+
sideDialogsController.close(panel?.key);
|
|
156
149
|
panel?.onClose?.();
|
|
157
150
|
}
|
|
158
151
|
};
|
|
@@ -212,8 +205,8 @@ function SideDialogView({
|
|
|
212
205
|
|
|
213
206
|
<UnsavedChangesDialog
|
|
214
207
|
open={drawerCloseRequested}
|
|
215
|
-
handleOk={
|
|
216
|
-
handleCancel={
|
|
208
|
+
handleOk={handleDrawerCloseOk}
|
|
209
|
+
handleCancel={handleDrawerCloseCancel}
|
|
217
210
|
body={blockedNavigationMessage} />
|
|
218
211
|
|
|
219
212
|
</SideDialogContext.Provider>
|
|
@@ -45,8 +45,8 @@ export function useReferenceDialog<M extends Record<string, any>>(referenceDialo
|
|
|
45
45
|
}, [navigation, referenceDialogProps, sideDialogsController]);
|
|
46
46
|
|
|
47
47
|
const close = useCallback(() => {
|
|
48
|
-
sideDialogsController.close();
|
|
49
|
-
}, [sideDialogsController]);
|
|
48
|
+
sideDialogsController.close(`reference_${referenceDialogProps.path}`);
|
|
49
|
+
}, [sideDialogsController, referenceDialogProps.path]);
|
|
50
50
|
|
|
51
51
|
return {
|
|
52
52
|
open,
|
|
@@ -14,6 +14,9 @@ export function useBuildSideDialogsController(): SideDialogsController {
|
|
|
14
14
|
const routesStore = useRef<Record<string, SideDialogPanelProps>>({});
|
|
15
15
|
const routesCount = useRef<number>(0);
|
|
16
16
|
|
|
17
|
+
// The history entry each panel without a URL was opened on, by panel key.
|
|
18
|
+
const entryKeysOfPanelsWithoutUrl = useRef<Record<string, string>>({});
|
|
19
|
+
|
|
17
20
|
const updateSidePanels = (newPanels: SideDialogPanelProps[]) => {
|
|
18
21
|
sidePanelsRef.current = newPanels;
|
|
19
22
|
setSidePanels(newPanels);
|
|
@@ -22,20 +25,55 @@ export function useBuildSideDialogsController(): SideDialogsController {
|
|
|
22
25
|
useEffect(() => {
|
|
23
26
|
const state = location.state as any;
|
|
24
27
|
const panelKeys: string[] = state?.panels ?? [];
|
|
25
|
-
const
|
|
28
|
+
const panelsInHistory = panelKeys
|
|
26
29
|
.map(key => routesStore.current[key])
|
|
27
30
|
.filter(p => Boolean(p)) as SideDialogPanelProps[];
|
|
31
|
+
|
|
32
|
+
// A panel without a URL, like a reference dialog, pushes no history entry, so the
|
|
33
|
+
// entry under it does not list it, and going back to that entry, as closing a
|
|
34
|
+
// panel opened on top of it does, would drop it. It stays open for as long as
|
|
35
|
+
// what it was opened over does: the panel below it or, with none, the entry itself.
|
|
36
|
+
const keptPanels: SideDialogPanelProps[] = [];
|
|
37
|
+
let matched = 0;
|
|
38
|
+
for (const panel of sidePanelsRef.current) {
|
|
39
|
+
if (panelsInHistory[matched]?.key === panel.key) {
|
|
40
|
+
keptPanels.push(panel);
|
|
41
|
+
matched++;
|
|
42
|
+
} else if (!panel.urlPath &&
|
|
43
|
+
(keptPanels.length > 0 || entryKeysOfPanelsWithoutUrl.current[panel.key] === location.key)) {
|
|
44
|
+
keptPanels.push(panel);
|
|
45
|
+
} else {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const newPanels = [...keptPanels, ...panelsInHistory.slice(matched)];
|
|
50
|
+
|
|
28
51
|
if (!equal(sidePanelsRef.current.map(p => p.key), newPanels.map(p => p.key))) {
|
|
29
52
|
updateSidePanels(newPanels);
|
|
30
53
|
}
|
|
31
54
|
}, [location]);
|
|
32
55
|
|
|
33
|
-
const close = useCallback(() => {
|
|
56
|
+
const close = useCallback((key?: string) => {
|
|
34
57
|
|
|
35
58
|
const currentPanels = sidePanelsRef.current;
|
|
36
59
|
if (currentPanels.length === 0)
|
|
37
60
|
return;
|
|
38
61
|
|
|
62
|
+
// A panel can ask to close itself while another one is open on top of it: the
|
|
63
|
+
// reference dialog does when an entity created from it is saved. Closing the top
|
|
64
|
+
// one instead would close that entity, and then the entity's own close would take
|
|
65
|
+
// the panel under the dialog with it. Only a panel without a URL can be taken out
|
|
66
|
+
// of the middle, since it has no history entry of its own. Should two panels share
|
|
67
|
+
// a key (a reference dialog opened from an entity created in another one for the
|
|
68
|
+
// same collection), it is the upper one.
|
|
69
|
+
const index = key !== undefined ? currentPanels.findLastIndex(p => p.key === key) : -1;
|
|
70
|
+
if (index !== -1 && index < currentPanels.length - 1 && !currentPanels[index].urlPath) {
|
|
71
|
+
updateSidePanels(currentPanels.filter((_, i) => i !== index));
|
|
72
|
+
if (routesCount.current > 0)
|
|
73
|
+
routesCount.current--;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
39
77
|
const lastSidePanel = currentPanels[currentPanels.length - 1];
|
|
40
78
|
const updatedPanels = [...currentPanels.slice(0, -1)];
|
|
41
79
|
updateSidePanels(updatedPanels);
|
|
@@ -65,6 +103,8 @@ export function useBuildSideDialogsController(): SideDialogsController {
|
|
|
65
103
|
|
|
66
104
|
newPanels.forEach((panel) => {
|
|
67
105
|
routesStore.current[panel.key] = panel;
|
|
106
|
+
if (!panel.urlPath)
|
|
107
|
+
entryKeysOfPanelsWithoutUrl.current[panel.key] = location.key;
|
|
68
108
|
});
|
|
69
109
|
routesCount.current = routesCount.current + newPanels.length;
|
|
70
110
|
|
|
@@ -95,6 +135,8 @@ export function useBuildSideDialogsController(): SideDialogsController {
|
|
|
95
135
|
const newPanels: SideDialogPanelProps[] = Array.isArray(panelProps) ? panelProps : [panelProps];
|
|
96
136
|
newPanels.forEach((panel) => {
|
|
97
137
|
routesStore.current[panel.key] = panel;
|
|
138
|
+
if (!panel.urlPath)
|
|
139
|
+
entryKeysOfPanelsWithoutUrl.current[panel.key] = location.key;
|
|
98
140
|
});
|
|
99
141
|
|
|
100
142
|
const baseLocation = (location.state as any)?.base_location ?? location;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Asks the browser to confirm leaving the page (reload, tab close, a link that
|
|
5
|
+
* loads another document) while `when` is true.
|
|
6
|
+
*
|
|
7
|
+
* Navigation inside the app is not intercepted: unsaved entity values are kept
|
|
8
|
+
* in the dirty entity cache, so they survive it. Cancelling link clicks here
|
|
9
|
+
* used to leave plain and new-tab links doing nothing, with no dialog shown.
|
|
10
|
+
*
|
|
11
|
+
* @param when - Whether leaving the page should be confirmed.
|
|
12
|
+
*/
|
|
13
|
+
export function useLeavePageConfirmation(when: boolean): void {
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!when) return;
|
|
16
|
+
|
|
17
|
+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
e.returnValue = "";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
23
|
+
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
24
|
+
}, [when]);
|
|
25
|
+
}
|
|
@@ -8,9 +8,11 @@ import { EntityFormProps } from "../form";
|
|
|
8
8
|
export interface SideDialogsController {
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Close the last panel
|
|
11
|
+
* Close the last panel.
|
|
12
|
+
* When the `key` of a panel without a URL is given, that panel is closed
|
|
13
|
+
* instead, even if other panels are open on top of it.
|
|
12
14
|
*/
|
|
13
|
-
close: () => void;
|
|
15
|
+
close: (key?: string) => void;
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* List of side panels currently open
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom hook to handle navigation blocking when there are unsaved changes.
|
|
3
|
-
*
|
|
4
|
-
* @param when - Indicates whether to block navigation.
|
|
5
|
-
* @param onSuccess - Callback invoked when navigation is confirmed.
|
|
6
|
-
* @returns An object containing the state of navigation blocking and handlers.
|
|
7
|
-
*/
|
|
8
|
-
export declare function useNavigationUnsavedChangesDialog(when: boolean, onSuccess: () => void): {
|
|
9
|
-
navigationWasBlocked: boolean;
|
|
10
|
-
handleCancel: () => void;
|
|
11
|
-
handleOk: () => void;
|
|
12
|
-
};
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useState } from "react";
|
|
2
|
-
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@firecms/ui";
|
|
3
|
-
|
|
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;
|
|
17
|
-
|
|
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);
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Handler to cancel the navigation attempt.
|
|
38
|
-
*/
|
|
39
|
-
const handleCancel = useCallback(() => {
|
|
40
|
-
setIsDialogOpen(false);
|
|
41
|
-
setPendingNavigation(null);
|
|
42
|
-
}, []);
|
|
43
|
-
|
|
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]);
|
|
59
|
-
|
|
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 = "";
|
|
68
|
-
}
|
|
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);
|
|
86
|
-
}
|
|
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;
|
|
97
|
-
|
|
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);
|
|
109
|
-
}
|
|
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
|
-
}
|
|
128
|
-
|
|
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]);
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
navigationWasBlocked: isDialogOpen,
|
|
139
|
-
handleCancel,
|
|
140
|
-
handleOk,
|
|
141
|
-
};
|
|
142
|
-
}
|