@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.
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 +2 -6
  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 +0 -1
  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 +2 -6
  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
@@ -0,0 +1,230 @@
1
+ import { Blocker, useBlocker, useLocation } from "react-router";
2
+ import { EntityEditView } from "../core/EntityEditView";
3
+ import { useNavigationController } from "../hooks";
4
+ import { useEffect, useRef, useState } from "react";
5
+ import { useNavigate } from "react-router-dom";
6
+ import {
7
+ getNavigationEntriesFromPath,
8
+ NavigationViewCollectionInternal,
9
+ NavigationViewEntityCustomInternal,
10
+ NavigationViewInternal
11
+ } from "../util/navigation_from_path";
12
+ import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
13
+ import { toArray } from "../util/arrays";
14
+ import { EntityCollectionView } from "../components";
15
+ import { UnsavedChangesDialog } from "../components/UnsavedChangesDialog";
16
+
17
+ export function FireCMSRoute() {
18
+
19
+ const location = useLocation();
20
+ const navigation = useNavigationController();
21
+ const breadcrumbs = useBreadcrumbsController();
22
+
23
+ const hash = location.hash;
24
+ const isSidePanel = hash.includes("#side");
25
+ const isNew = hash.includes("#new") || hash.includes("#new_side");
26
+
27
+ const pathname = location.pathname;
28
+ const navigationPath = navigation.urlPathToDataPath(pathname);
29
+
30
+ const navigationEntries = getNavigationEntriesFromPath({
31
+ path: navigationPath,
32
+ collections: navigation.collections ?? []
33
+ });
34
+
35
+ useEffect(() => {
36
+ breadcrumbs.set({
37
+ breadcrumbs: navigationEntries.map(entry => {
38
+ if (entry.type === "entity") {
39
+ return ({
40
+ title: entry.entityId,
41
+ url: navigation.buildUrlCollectionPath(entry.fullPath)
42
+ });
43
+ } else if (entry.type === "custom_view") {
44
+ return ({
45
+ title: entry.view.name,
46
+ url: navigation.buildUrlCollectionPath(entry.fullPath)
47
+ });
48
+ } else if (entry.type === "collection") {
49
+ return ({
50
+ title: entry.collection.name,
51
+ url: navigation.buildUrlCollectionPath(entry.fullPath)
52
+ });
53
+ } else {
54
+ throw new Error("Unexpected navigation entry type");
55
+ }
56
+ })
57
+ });
58
+ }, [navigationEntries.map(entry => entry.path).join(",")]);
59
+
60
+ if (isNew) {
61
+ return <EntityFullScreenRoute
62
+ pathname={pathname}
63
+ navigationEntries={navigationEntries}
64
+ isNew={true}
65
+ />;
66
+ }
67
+
68
+ if (navigationEntries.length === 1 && navigationEntries[0].type === "collection") {
69
+ const collection = navigation.getCollection(navigationEntries[0].path);
70
+ if (!collection)
71
+ return null;
72
+ return <EntityCollectionView
73
+ key={`collection_view_${collection.id ?? collection.path}`}
74
+ isSubCollection={false}
75
+ parentCollectionIds={[]}
76
+ fullPath={collection.id}
77
+ updateUrl={true}
78
+ {...collection}
79
+ Actions={toArray(collection.Actions)}/>
80
+ }
81
+
82
+ if (isSidePanel) {
83
+ const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
84
+ if (lastCollectionEntry) {
85
+ const collection = navigation.getCollection(lastCollectionEntry.path);
86
+ if (!collection)
87
+ return null;
88
+ return <EntityCollectionView
89
+ key={`collection_view_${collection.id ?? collection.path}`}
90
+ isSubCollection={false}
91
+ parentCollectionIds={[]}
92
+ fullPath={collection.id}
93
+ updateUrl={true}
94
+ {...collection}
95
+ Actions={toArray(collection.Actions)}/>;
96
+ }
97
+ }
98
+
99
+ return <EntityFullScreenRoute
100
+ pathname={pathname}
101
+ navigationEntries={navigationEntries}
102
+ isNew={isNew}
103
+ />;
104
+
105
+ }
106
+
107
+ function EntityFullScreenRoute({
108
+ pathname,
109
+ navigationEntries,
110
+ isNew
111
+ }: {
112
+ pathname: string;
113
+ navigationEntries: NavigationViewInternal[],
114
+ isNew: boolean
115
+ }) {
116
+
117
+ const navigation = useNavigationController();
118
+ const navigate = useNavigate();
119
+
120
+ const navigationPath = navigation.urlPathToDataPath(pathname);
121
+
122
+ // is navigating away blocked
123
+ const blocked = useRef(false);
124
+
125
+ const lastEntityEntry = navigationEntries.findLast((entry) => entry.type === "entity");
126
+ const navigationEntriesAfterEntity = lastEntityEntry ? navigationEntries.slice(navigationEntries.indexOf(lastEntityEntry) + 1) : [];
127
+
128
+ const lastCustomView = navigationEntriesAfterEntity.findLast(
129
+ (entry) => entry.type === "custom_view" || entry.type === "collection"
130
+ ) as NavigationViewCollectionInternal<any> | NavigationViewEntityCustomInternal<any> | undefined;
131
+
132
+ const entityId = lastEntityEntry?.entityId;
133
+
134
+ const urlTab = isNew ? undefined : (lastCustomView && "id" in lastCustomView ? lastCustomView?.id : undefined) ?? lastCustomView?.path;
135
+ const [selectedTab, setSelectedTab] = useState<string | undefined>(urlTab);
136
+
137
+ const parentCollectionIds = navigation.getParentCollectionIds(navigationPath);
138
+ useEffect(() => {
139
+ if (urlTab !== selectedTab) {
140
+ setSelectedTab(urlTab);
141
+ }
142
+ }, [urlTab]);
143
+
144
+ const basePath = !entityId || isNew
145
+ ? pathname
146
+ : pathname.substring(0, pathname.lastIndexOf(`/${entityId}`));
147
+
148
+ const entityPath = basePath + `/${entityId}`;
149
+
150
+ let blocker: Blocker | undefined = undefined;
151
+ try {
152
+ blocker = useBlocker(({
153
+ nextLocation
154
+ }) => {
155
+ if (nextLocation.pathname.startsWith(entityPath))
156
+ return false;
157
+ return blocked.current;
158
+ });
159
+ } catch (e) {
160
+ console.warn("Blocker not available, navigation will not be blocked");
161
+ }
162
+
163
+ function updateUrl(entityId: string | undefined, newSelectedTab: string | undefined, replace: boolean, path: string, isNew: boolean) {
164
+
165
+ console.log("Updating url", {
166
+ entityId,
167
+ newSelectedTab,
168
+ replace,
169
+ basePath,
170
+ path,
171
+ isNew
172
+ });
173
+
174
+ if (!isNew && (newSelectedTab ?? null) === (selectedTab ?? null)) {
175
+ return;
176
+ }
177
+
178
+ if (isNew) {
179
+ navigate(`${basePath}/${entityId}`, { replace: replace });
180
+ return;
181
+ }
182
+
183
+ if (newSelectedTab) {
184
+ navigate(`${basePath}/${entityId}/${newSelectedTab}`, { replace: replace });
185
+ } else {
186
+ navigate(`${basePath}/${entityId}`, { replace: replace });
187
+ }
188
+
189
+ }
190
+
191
+ const lastCollectionEntry = navigationEntries.findLast((entry) => entry.type === "collection");
192
+
193
+ if (isNew && !lastCollectionEntry) {
194
+ throw new Error("No collection found in the navigation");
195
+ }
196
+
197
+ if (!isNew && !lastEntityEntry) {
198
+ throw new Error("No entity found in the navigation");
199
+ }
200
+
201
+ const collection = isNew ? lastCollectionEntry!.collection : lastEntityEntry!.parentCollection;
202
+ const collectionPath = navigation.resolveIdsFrom(isNew ? lastCollectionEntry!.path : lastEntityEntry!.path);
203
+
204
+ return <>
205
+ <EntityEditView
206
+ key={collection.id + "_" + (isNew ? "new" : entityId)}
207
+ entityId={isNew ? undefined : entityId}
208
+ collection={collection}
209
+ layout={"full_screen"}
210
+ path={collectionPath}
211
+ selectedTab={selectedTab ?? undefined}
212
+ onValuesModified={(modified) => blocked.current = modified}
213
+ onSaved={(params) => {
214
+ updateUrl(params.entityId, params.selectedTab, true, params.path, isNew);
215
+ }}
216
+ onTabChange={(params) => {
217
+ updateUrl(params.entityId, params.selectedTab, !isNew, params.path, isNew);
218
+ setSelectedTab(params.selectedTab);
219
+ }}
220
+ parentCollectionIds={parentCollectionIds}
221
+ />
222
+
223
+ <UnsavedChangesDialog
224
+ open={blocker?.state === "blocked"}
225
+ handleOk={() => blocker?.proceed?.()}
226
+ handleCancel={() => blocker?.reset?.()}
227
+ body={"You have unsaved changes in this entity."}/>
228
+
229
+ </>;
230
+ }
@@ -0,0 +1,17 @@
1
+ import { useBreadcrumbsController } from "../hooks/useBreadcrumbsController";
2
+ import { useEffect } from "react";
3
+
4
+ export function HomePageRoute({ children }: {
5
+ children: React.ReactNode;
6
+ }) {
7
+
8
+ const breadcrumbs = useBreadcrumbsController();
9
+
10
+ useEffect(() => {
11
+ breadcrumbs.set({
12
+ breadcrumbs: []
13
+ });
14
+ }, []);
15
+
16
+ return children;
17
+ }
@@ -84,6 +84,11 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
84
84
  */
85
85
  properties: PropertiesOrBuilders<M>;
86
86
 
87
+ /**
88
+ * Default preview properties displayed when this collection is referenced to.
89
+ */
90
+ previewProperties?: string[];
91
+
87
92
  /**
88
93
  * Title property of the entity. This is the property that will be used
89
94
  * as the title in entity related views and references.
@@ -92,11 +97,10 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
92
97
  titleProperty?: keyof M;
93
98
 
94
99
  /**
95
- * Can this collection be edited by the end user.
96
- * Defaults to `true`.
97
- * Keep in mind that you can also set this prop to individual properties.
100
+ * When editing an entity, you can choose to open the entity in a side dialog
101
+ * or in a full screen dialog. Defaults to `full_screen`.
98
102
  */
99
- editable?: boolean;
103
+ openEntityMode?: "side_panel" | "full_screen";
100
104
 
101
105
  /**
102
106
  * Order in which the properties are displayed.
@@ -313,9 +317,12 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
313
317
  sideDialogWidth?: number | string;
314
318
 
315
319
  /**
316
- * Default preview properties displayed when this collection is referenced to.
320
+ * Can this collection configuration be edited by the end user.
321
+ * Defaults to `true`.
322
+ * Keep in mind that you can also set this prop to individual properties.
323
+ * This prop has only effect if you are using the collection editor.
317
324
  */
318
- previewProperties?: string[];
325
+ editable?: boolean;
319
326
  }
320
327
 
321
328
  /**
@@ -570,6 +577,12 @@ export type EntityTableController<M extends Record<string, any> = any> = {
570
577
  clearFilter?: () => void;
571
578
  itemCount?: number;
572
579
  setItemCount?: (itemCount: number) => void;
580
+ initialScroll?: number;
581
+ onScroll?: (props: {
582
+ scrollDirection: "forward" | "backward",
583
+ scrollOffset: number,
584
+ scrollUpdateWasRequested: boolean
585
+ }) => void;
573
586
  paginationEnabled?: boolean;
574
587
  pageSize?: number;
575
588
  checkFilterCombination?: (filterValues: FilterValues<any>,
@@ -9,6 +9,7 @@ import { FireCMSContext } from "./firecms_context";
9
9
  export interface FetchEntityProps<M extends Record<string, any> = any> {
10
10
  path: string;
11
11
  entityId: string;
12
+ databaseId?: string;
12
13
  collection?: EntityCollection<M, any>
13
14
  }
14
15
 
@@ -18,7 +19,6 @@ export interface FetchEntityProps<M extends Record<string, any> = any> {
18
19
  export type ListenEntityProps<M extends Record<string, any> = any> =
19
20
  FetchEntityProps<M>
20
21
  & {
21
- databaseId?: string;
22
22
  onUpdate: (entity: Entity<M>) => void,
23
23
  onError?: (error: Error) => void,
24
24
  }
@@ -139,6 +139,7 @@ export interface DataSource {
139
139
  fetchEntity<M extends Record<string, any> = any>({
140
140
  path,
141
141
  entityId,
142
+ databaseId,
142
143
  collection
143
144
  }: FetchEntityProps<M>
144
145
  ): Promise<Entity<M> | undefined>;
@@ -22,16 +22,12 @@ export interface DialogsController {
22
22
  * Props used to open a side dialog
23
23
  * @group Hooks and utilities
24
24
  */
25
- export interface DialogControllerEntryProps<T extends object = {}> {
25
+ export interface DialogControllerEntryProps {
26
26
 
27
27
  key: string;
28
28
  /**
29
29
  * The component type that will be rendered
30
30
  */
31
- Component: React.ComponentType<{ open: boolean, closeDialog: () => void } & T>;
32
- /**
33
- * Props to pass to the dialog component
34
- */
35
- props?: T;
31
+ Component: React.ComponentType<{ open: boolean, closeDialog: () => void }>;
36
32
 
37
33
  }
@@ -8,7 +8,7 @@ export type EntityStatus = "new" | "existing" | "copy";
8
8
  * Representation of an entity fetched from the datasource
9
9
  * @group Models
10
10
  */
11
- export interface Entity<M extends object> {
11
+ export interface Entity<M extends object = any> {
12
12
 
13
13
  /**
14
14
  * ID of the entity
@@ -4,8 +4,6 @@ import { Entity } from "./entities";
4
4
  import { EntityCollection, SelectionController } from "./collections";
5
5
  import { User } from "./user";
6
6
  import { SideEntityController } from "./side_entity_controller";
7
- import { FormexController } from "@firecms/formex";
8
- import { FormContext } from "./fields";
9
7
 
10
8
  /**
11
9
  * An entity action is a custom action that can be performed on an entity.
@@ -66,9 +64,5 @@ export type EntityActionClickProps<M extends object, USER extends User = User> =
66
64
  */
67
65
  sideEntityController?: SideEntityController;
68
66
 
69
- /**
70
- * You can use the form context to manage the state of the form.
71
- * This is only available if this action in being triggered from a form context.
72
- */
73
- formContext?: FormContext<M>;
67
+ openEntityMode: "side_panel" | "full_screen";
74
68
  };
@@ -56,8 +56,8 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
56
56
  * Get the collection configuration for a given path.
57
57
  * The collection is resolved from the given path or alias.
58
58
  */
59
- getCollection: (pathOrId: string,
60
- includeUserOverride?: boolean) => EC | undefined;
59
+ getCollection: (pathOrId: string, includeUserOverride?: boolean) => EC | undefined;
60
+
61
61
  /**
62
62
  * Get the collection configuration from its parent ids.
63
63
  */
@@ -69,12 +69,14 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
69
69
  getCollectionFromPaths: (pathSegments: string[]) => EC | undefined;
70
70
 
71
71
  /**
72
- * Default path under the navigation routes of the CMS will be created
72
+ * Default path under the navigation routes of the CMS will be created.
73
+ * Defaults to '/'. You may want to change this `basepath` to 'admin' for example.
73
74
  */
74
75
  basePath: string;
75
76
 
76
77
  /**
77
- * Default path under the collection routes of the CMS will be created
78
+ * Default path under the collection routes of the CMS will be created.
79
+ * It defaults to '/c'
78
80
  */
79
81
  baseCollectionPath: string;
80
82
 
@@ -87,16 +89,6 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
87
89
  */
88
90
  urlPathToDataPath: (cmsPath: string) => string;
89
91
 
90
- /**
91
- * Convert a collection or entity path to a URL path
92
- * @param path
93
- */
94
- buildCMSUrlPath: (path: string) => string;
95
-
96
- buildUrlEditCollectionPath: (props: {
97
- path: string
98
- }) => string;
99
-
100
92
  /**
101
93
  * Base url path for the home screen
102
94
  */
@@ -117,10 +109,11 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
117
109
  buildUrlCollectionPath: (path: string) => string;
118
110
 
119
111
  /**
120
- * Turn a path with aliases into a resolved path
112
+ * Turn a path with collection ids into a resolved path.
113
+ * The ids (typically used in urls) will be replaced with relative paths (typically used in database paths)
121
114
  * @param pathWithAliases
122
115
  */
123
- resolveAliasesFrom: (pathWithAliases: string) => string;
116
+ resolveIdsFrom: (pathWithAliases: string) => string;
124
117
 
125
118
  /**
126
119
  * Call this method to recalculate the navigation
@@ -144,8 +137,34 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
144
137
  * @param ids
145
138
  */
146
139
  convertIdsToPaths: (ids: string[]) => string[];
140
+
141
+ /**
142
+ * A function to navigate to a specified route or URL.
143
+ *
144
+ * @param {string} to - The target route or URL to navigate to.
145
+ * @param {NavigateOptions} [options] - Optional configuration settings for navigation, such as replace behavior or state data.
146
+ */
147
+ navigate: (to: string, options?: NavigateOptions) => void;
148
+
147
149
  }
148
150
 
151
+ export interface NavigateOptions {
152
+ replace?: boolean;
153
+ state?: any;
154
+ preventScrollReset?: boolean;
155
+ relative?: "route" | "path";
156
+ flushSync?: boolean;
157
+ viewTransition?: boolean;
158
+ }
159
+
160
+ // currently not used, in favor of a single blocker in `FireCMSRoute`
161
+ export type NavigationBlocker = {
162
+ updateBlockListener: (path: string, block: boolean, basePath?: string) => () => void;
163
+ isBlocked: (path: string) => boolean;
164
+ proceed?: () => void;
165
+ reset?: () => void;
166
+ };
167
+
149
168
  /**
150
169
  * Custom additional views created by the developer, added to the main
151
170
  * navigation.
@@ -195,6 +195,7 @@ export interface PluginFormActionProps<USER extends User = User, EC extends Enti
195
195
  formContext?: FormContext<any>;
196
196
  context: FireCMSContext<USER>;
197
197
  currentEntityId?: string;
198
+ layout: "side_panel" | "full_screen";
198
199
  }
199
200
 
200
201
  export type PluginFieldBuilderParams<T extends CMSType = CMSType, M extends Record<string, any> = any, EC extends EntityCollection<M> = EntityCollection<M>> = {
@@ -28,7 +28,7 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
28
28
  * Open the entity with a selected sub-collection view. If the panel for this
29
29
  * entity was already open, it is replaced.
30
30
  */
31
- selectedSubPath?: string;
31
+ selectedTab?: string;
32
32
 
33
33
  /**
34
34
  * Use this prop to override the width of the form view.
@@ -0,0 +1,196 @@
1
+ import { EntityReference, GeoPoint, Vector } from "../types";
2
+
3
+ // Define a unique prefix for entity keys in localStorage to avoid key collisions
4
+ const LOCAL_STORAGE_PREFIX = "entity_cache::";
5
+
6
+ // In-memory cache to store entities for quick access
7
+ const entityCache: Map<string, object> = new Map();
8
+
9
+ // Check `localStorage` availability once during initialization
10
+ const isLocalStorageAvailable = typeof localStorage !== "undefined";
11
+
12
+ // Define custom replacer for JSON.stringify
13
+ function customReplacer(key: string, value: any): any {
14
+ // Handle Date objects
15
+ if (value instanceof Date) {
16
+ return { __type: "Date", value: value.toISOString() };
17
+ }
18
+
19
+ // Handle EntityReference
20
+ if (value instanceof EntityReference) {
21
+ return { __type: "EntityReference", id: value.id, path: value.path };
22
+ }
23
+
24
+ // Handle GeoPoint
25
+ if (value instanceof GeoPoint) {
26
+ return { __type: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
27
+ }
28
+
29
+ // Handle Vector
30
+ if (value instanceof Vector) {
31
+ return { __type: "Vector", value: value.value };
32
+ }
33
+
34
+ return value;
35
+ }
36
+
37
+ // Define custom reviver for JSON.parse
38
+ function customReviver(key: string, value: any): any {
39
+ if (value && typeof value === "object" && "__type" in value) {
40
+ switch (value.__type) {
41
+ case "Date":
42
+ return new Date(value.value);
43
+ case "EntityReference":
44
+ return new EntityReference(value.id, value.path);
45
+ case "GeoPoint":
46
+ return new GeoPoint(value.latitude, value.longitude);
47
+ case "Vector":
48
+ return new Vector(value.value);
49
+ default:
50
+ return value;
51
+ }
52
+ }
53
+ return value;
54
+ }
55
+
56
+ // Initialize the in-memory cache by loading entities from `localStorage`
57
+ if (isLocalStorageAvailable) {
58
+ try {
59
+ // Iterate over all keys in localStorage to find those with the specified prefix
60
+ for (let i = 0; i < localStorage.length; i++) {
61
+ const fullKey = localStorage.key(i);
62
+ if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
63
+ const path = fullKey.substring(LOCAL_STORAGE_PREFIX.length);
64
+ const entityString = localStorage.getItem(fullKey);
65
+ if (entityString) {
66
+ try {
67
+ const entity: object = JSON.parse(entityString, customReviver);
68
+ entityCache.set(path, entity);
69
+ } catch (parseError) {
70
+ console.error(
71
+ `Failed to parse entity for path "${path}" from localStorage:`,
72
+ parseError
73
+ );
74
+ }
75
+ }
76
+ }
77
+ }
78
+ } catch (error) {
79
+ console.error("Error accessing localStorage during initialization:", error);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Saves data to the in-memory cache and persists it individually in `localStorage`.
85
+ * @param path - The unique path/key for the data.
86
+ * @param data - The data to cache and persist.
87
+ */
88
+ export function saveEntityToCache(path: string, data: object): void {
89
+ // Update the in-memory cache
90
+ entityCache.set(path, data);
91
+
92
+ // Persist the data individually in localStorage
93
+ if (isLocalStorageAvailable) {
94
+ try {
95
+ const key = LOCAL_STORAGE_PREFIX + path;
96
+ const entityString = JSON.stringify(data, customReplacer);
97
+ localStorage.setItem(key, entityString);
98
+ } catch (error) {
99
+ console.error(
100
+ `Failed to save entity for path "${path}" to localStorage:`,
101
+ error
102
+ );
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Retrieves an entity from the in-memory cache or `localStorage`.
109
+ * If the entity is not in the cache but exists in `localStorage`, it loads it into the cache.
110
+ * @param path - The unique path/key for the entity.
111
+ * @returns The cached entity or `undefined` if not found.
112
+ */
113
+ export function getEntityFromCache(path: string): object | undefined {
114
+
115
+ // Attempt to retrieve the entity from the in-memory cache
116
+ if (entityCache.has(path)) {
117
+ return entityCache.get(path);
118
+ }
119
+
120
+ // If not in the cache, attempt to load it from localStorage
121
+ if (isLocalStorageAvailable) {
122
+ try {
123
+ const key = LOCAL_STORAGE_PREFIX + path;
124
+ const entityString = localStorage.getItem(key);
125
+ if (entityString) {
126
+ const entity: object = JSON.parse(entityString, customReviver);
127
+ entityCache.set(path, entity); // Update the cache
128
+ return entity;
129
+ }
130
+ } catch (error) {
131
+ console.error(
132
+ `Failed to load entity for path "${path}" from localStorage:`,
133
+ error
134
+ );
135
+ }
136
+ }
137
+
138
+ // Entity not found
139
+ return undefined;
140
+ }
141
+
142
+ export function hasEntityInCache(path: string): boolean {
143
+ return entityCache.has(path);
144
+ }
145
+
146
+ /**
147
+ * Removes an entity from both the in-memory cache and `localStorage`.
148
+ * @param path - The unique path/key for the entity to remove.
149
+ */
150
+ export function removeEntityFromCache(path: string): void {
151
+
152
+
153
+ console.log("Removing entity from cache", path);
154
+
155
+ // Remove from the in-memory cache
156
+ entityCache.delete(path);
157
+
158
+ // Remove from localStorage
159
+ if (isLocalStorageAvailable) {
160
+ try {
161
+ const key = LOCAL_STORAGE_PREFIX + path;
162
+ localStorage.removeItem(key);
163
+ } catch (error) {
164
+ console.error(
165
+ `Failed to remove entity for path "${path}" from localStorage:`,
166
+ error
167
+ );
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Clears the entire in-memory cache and removes all related entities from `localStorage`.
174
+ */
175
+ export function clearEntityCache(): void {
176
+ // Clear the in-memory cache
177
+ entityCache.clear();
178
+
179
+ // Remove all entities with the specified prefix from localStorage
180
+ if (isLocalStorageAvailable) {
181
+ try {
182
+ const keysToRemove: string[] = [];
183
+ for (let i = 0; i < localStorage.length; i++) {
184
+ const fullKey = localStorage.key(i);
185
+ if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
186
+ keysToRemove.push(fullKey);
187
+ }
188
+ }
189
+
190
+ // Remove the keys after collecting them to avoid issues while iterating
191
+ keysToRemove.forEach((key) => localStorage.removeItem(key));
192
+ } catch (error) {
193
+ console.error("Failed to clear entity cache from localStorage:", error);
194
+ }
195
+ }
196
+ }