@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.
Files changed (27) hide show
  1. package/dist/components/EntityCollectionView/CollectionCountBadge.d.ts +8 -0
  2. package/dist/components/EntityCollectionView/CollectionPathTitle.d.ts +24 -0
  3. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +5 -0
  4. package/dist/components/EntityCollectionView/EntityIdHeaderWidget.d.ts +12 -0
  5. package/dist/components/EntityCollectionView/utils.d.ts +4 -3
  6. package/dist/index.es.js +592 -567
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +592 -567
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/internal/useLeavePageConfirmation.d.ts +11 -0
  11. package/dist/types/side_dialogs_controller.d.ts +4 -2
  12. package/package.json +3 -3
  13. package/src/components/EntityCollectionView/CollectionCountBadge.tsx +17 -0
  14. package/src/components/EntityCollectionView/CollectionPathTitle.tsx +68 -0
  15. package/src/components/EntityCollectionView/EntityCollectionView.tsx +12 -111
  16. package/src/components/EntityCollectionView/EntityIdHeaderWidget.tsx +166 -0
  17. package/src/components/EntityCollectionView/utils.ts +13 -7
  18. package/src/components/common/default_entity_actions.tsx +2 -1
  19. package/src/core/DefaultAppBar.tsx +12 -9
  20. package/src/core/EntityEditView.tsx +26 -1
  21. package/src/core/SideDialogs.tsx +6 -13
  22. package/src/hooks/useReferenceDialog.tsx +2 -2
  23. package/src/internal/useBuildSideDialogsController.tsx +44 -2
  24. package/src/internal/useLeavePageConfirmation.ts +25 -0
  25. package/src/types/side_dialogs_controller.tsx +4 -2
  26. package/dist/internal/useUnsavedChangesDialog.d.ts +0 -12
  27. package/src/internal/useUnsavedChangesDialog.tsx +0 -142
@@ -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 { useNavigationUnsavedChangesDialog } from "../internal/useUnsavedChangesDialog";
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
- const {
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={drawerCloseRequested ? handleDrawerCloseOk : handleNavigationOk}
216
- handleCancel={drawerCloseRequested ? handleDrawerCloseCancel : handleNavigationCancel}
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 newPanels = panelKeys
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
- }