@firecms/core 3.0.0-canary.40 → 3.0.0-canary.42

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.
@@ -47,7 +47,7 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
47
47
  * Get the collection configuration for a given path.
48
48
  * The collection is resolved from the given path or alias.
49
49
  */
50
- getCollection: (pathOrAlias: string, entityId?: string, includeUserOverride?: boolean) => EC | undefined;
50
+ getCollection: (pathOrId: string, entityId?: string, includeUserOverride?: boolean) => EC | undefined;
51
51
  /**
52
52
  * Get the collection configuration from its parent path segments.
53
53
  */
@@ -8,10 +8,10 @@ export declare function resolveCollectionPathIds(path: string, allCollections: E
8
8
  /**
9
9
  * Find the corresponding view at any depth for a given path.
10
10
  * Note that path or segments of the paths can be collection aliases.
11
- * @param pathOrAlias
11
+ * @param pathOrId
12
12
  * @param collections
13
13
  */
14
- export declare function getCollectionByPathOrId(pathOrAlias: string, collections: EntityCollection[]): EntityCollection | undefined;
14
+ export declare function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[]): EntityCollection | undefined;
15
15
  /**
16
16
  * Get the subcollection combinations from a path:
17
17
  * "sites/es/locales" => ["sites/es/locales", "sites"]
@@ -2,11 +2,11 @@ import { ArrayProperty, CMSType, EntityCollection, EntityCustomView, EntityValue
2
2
  export declare const resolveCollection: <M extends Record<string, any>>({ collection, path, entityId, values, previousValues, userConfigPersistence, fields }: {
3
3
  collection: EntityCollection<M> | ResolvedEntityCollection<M>;
4
4
  path: string;
5
- entityId?: string | undefined;
6
- values?: Partial<M> | undefined;
7
- previousValues?: Partial<M> | undefined;
8
- userConfigPersistence?: UserConfigurationPersistence | undefined;
9
- fields?: Record<string, PropertyConfig> | undefined;
5
+ entityId?: string;
6
+ values?: Partial<EntityValues<M>>;
7
+ previousValues?: Partial<EntityValues<M>>;
8
+ userConfigPersistence?: UserConfigurationPersistence;
9
+ fields?: Record<string, PropertyConfig>;
10
10
  }) => ResolvedEntityCollection<M>;
11
11
  /**
12
12
  * Resolve property builders, enums and arrays.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.0.0-canary.40",
4
+ "version": "3.0.0-canary.42",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -46,17 +46,20 @@
46
46
  "./package.json": "./package.json"
47
47
  },
48
48
  "dependencies": {
49
- "@firecms/formex": "^3.0.0-canary.40",
50
- "@firecms/ui": "^3.0.0-canary.40",
49
+ "@firecms/formex": "^3.0.0-canary.42",
50
+ "@firecms/ui": "^3.0.0-canary.42",
51
51
  "@fontsource/jetbrains-mono": "^5.0.19",
52
- "@fontsource/roboto": "^5.0.12",
53
52
  "@hello-pangea/dnd": "^16.5.0",
53
+ "@radix-ui/react-portal": "^1.0.4",
54
+ "clsx": "^2.1.0",
54
55
  "date-fns": "^3.6.0",
55
56
  "history": "^5.3.0",
56
57
  "js-search": "^2.0.1",
57
58
  "markdown-it": "^14.1.0",
58
59
  "notistack": "^3.0.1",
59
60
  "object-hash": "^3.0.0",
61
+ "react-dropzone": "^14.2.3",
62
+ "react-fast-compare": "^3.2.2",
60
63
  "react-image-file-resizer": "^0.4.8",
61
64
  "react-markdown-editor-lite": "^1.3.4",
62
65
  "react-transition-group": "^4.4.5",
@@ -115,7 +118,7 @@
115
118
  "dist",
116
119
  "src"
117
120
  ],
118
- "gitHead": "40b01721ac260b6cce7f5d8a3a1d768410e7f20d",
121
+ "gitHead": "d6a2f28e93d3c532dd6efacfccffb91383d5330e",
119
122
  "publishConfig": {
120
123
  "access": "public"
121
124
  }
@@ -74,7 +74,6 @@ import { DeleteEntityDialog } from "../DeleteEntityDialog";
74
74
  import { useAnalyticsController } from "../../hooks/useAnalyticsController";
75
75
  import { useSelectionController } from "./useSelectionController";
76
76
  import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
77
- import { ClearFilterSortButton } from "../ClearFilterSortButton";
78
77
 
79
78
  const COLLECTION_GROUP_PARENT_ID = "collectionGroupParent";
80
79
 
@@ -82,8 +81,18 @@ const COLLECTION_GROUP_PARENT_ID = "collectionGroupParent";
82
81
  * @group Components
83
82
  */
84
83
  export type EntityCollectionViewProps<M extends Record<string, any>> = {
85
- fullPath: string;
84
+ /**
85
+ * Complete path where this collection is located.
86
+ * It defaults to the collection path if not provided.
87
+ */
88
+ fullPath?: string;
89
+ /**
90
+ * If this is a subcollection, specify the parent collection ids.
91
+ */
86
92
  parentCollectionIds?: string[];
93
+ /**
94
+ * Whether this is a subcollection or not.
95
+ */
87
96
  isSubCollection?: boolean;
88
97
  className?: string;
89
98
  } & EntityCollection<M>;
@@ -114,7 +123,7 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
114
123
  */
115
124
  export const EntityCollectionView = React.memo(
116
125
  function EntityCollectionView<M extends Record<string, any>>({
117
- fullPath,
126
+ fullPath: fullPathProp,
118
127
  parentCollectionIds,
119
128
  isSubCollection,
120
129
  className,
@@ -122,6 +131,7 @@ export const EntityCollectionView = React.memo(
122
131
  }: EntityCollectionViewProps<M>
123
132
  ) {
124
133
 
134
+ const fullPath = fullPathProp ?? collectionProp.path;
125
135
  const dataSource = useDataSource(collectionProp);
126
136
  const navigation = useNavigationController();
127
137
  const sideEntityController = useSideEntityController();
@@ -176,7 +186,6 @@ export const EntityCollectionView = React.memo(
176
186
  const usedSelectionController = collection.selectionController ?? selectionController;
177
187
  const {
178
188
  selectedEntities,
179
- toggleEntitySelection,
180
189
  isEntitySelected,
181
190
  setSelectedEntities
182
191
  } = usedSelectionController;
@@ -58,7 +58,7 @@ export function EntityView<M extends Record<string, any>>(
58
58
  </div>
59
59
  {Object.entries(properties)
60
60
  .map(([key, property]) => {
61
- const value = (entity.values)[key];
61
+ const value = entity.values?.[key];
62
62
  return (
63
63
  <div
64
64
  key={`reference_previews_${key}`}
@@ -191,7 +191,7 @@ export function DefaultHomePage({
191
191
  />
192
192
  </div>
193
193
  ))}
194
- {AdditionalCards &&
194
+ {group?.toLowerCase() !== "admin" && AdditionalCards &&
195
195
  AdditionalCards.map((AdditionalCard, i) => (
196
196
  <div key={`nav_${group}_add_${i}`}>
197
197
  <AdditionalCard {...actionProps} />
@@ -20,7 +20,7 @@ import { VirtualTableContextProps } from "./types";
20
20
  import { VirtualTableHeaderRow } from "./VirtualTableHeaderRow";
21
21
  import { VirtualTableRow } from "./VirtualTableRow";
22
22
  import { VirtualTableCell } from "./VirtualTableCell";
23
- import { AssignmentIcon, cn, Markdown, Typography } from "@firecms/ui";
23
+ import { AssignmentIcon, CenteredView, cn, Typography } from "@firecms/ui";
24
24
 
25
25
  const VirtualListContext = createContext<VirtualTableContextProps<any>>({} as any);
26
26
  VirtualListContext.displayName = "VirtualListContext";
@@ -225,24 +225,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
225
225
  }
226
226
 
227
227
  if (onFilterUpdate) onFilterUpdate(newFilterValue);
228
-
229
- if (column.key !== sortByProperty) {
230
- resetSort();
231
- }
232
- }, [checkFilterCombination, currentSort, onFilterUpdate, resetSort, sortByProperty]);
233
-
234
- const buildErrorView = useCallback(() => (
235
- <div
236
- className="h-full flex flex-col items-center justify-center sticky left-0">
237
-
238
- <Typography variant={"h6"}>
239
- {"Error fetching data from the data source"}
240
- </Typography>
241
-
242
- {error?.message && <Markdown className={"px-4 break-all"} source={error.message}/>}
243
-
244
- </div>
245
- ), [error?.message]);
228
+ }, [checkFilterCombination, currentSort, onFilterUpdate, sortByProperty]);
246
229
 
247
230
  const buildEmptyView = useCallback(() => {
248
231
  if (loading)
@@ -255,7 +238,18 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
255
238
  }, [emptyComponent, loading]);
256
239
 
257
240
  const empty = !loading && (data?.length ?? 0) === 0;
258
- const customView = error ? buildErrorView() : (empty ? buildEmptyView() : undefined);
241
+ const customView = error
242
+ ? <CenteredView maxWidth={"2xl"}
243
+ className="flex flex-col gap-2">
244
+
245
+ <Typography variant={"h6"}>
246
+ {"Error fetching data from the data source"}
247
+ </Typography>
248
+
249
+ {error?.message && <SafeLinkRenderer text={error.message}/>}
250
+
251
+ </CenteredView>
252
+ : (empty ? buildEmptyView() : undefined);
259
253
 
260
254
  const virtualListController = {
261
255
  data,
@@ -403,3 +397,17 @@ function MemoizedList({
403
397
  {Row}
404
398
  </List>;
405
399
  }
400
+
401
+ const SafeLinkRenderer: React.FC<{
402
+ text: string;
403
+ }> = ({ text }) => {
404
+ const urlRegex = /https?:\/\/[^\s]+/g;
405
+ const htmlContent = text.replace(urlRegex, (url) => {
406
+ // For each URL found, replace it with an HTML <a> tag
407
+ return `<a href="${url}" target="_blank">Link</a><br/>`;
408
+ });
409
+
410
+ return (
411
+ <div className={"break-all"} dangerouslySetInnerHTML={{ __html: htmlContent }}/>
412
+ );
413
+ };
@@ -12,8 +12,7 @@ import { DataSourceContext } from "../contexts/DataSourceContext";
12
12
  import { SideEntityControllerContext } from "../contexts/SideEntityControllerContext";
13
13
  import { NavigationContext } from "../contexts/NavigationContext";
14
14
  import { SideDialogsControllerContext } from "../contexts/SideDialogsControllerContext";
15
- import { useLocaleConfig } from "../internal/useLocaleConfig";
16
- import { CenteredView, Typography } from "@firecms/ui";
15
+ import { CenteredView, Typography, useLocaleConfig } from "@firecms/ui";
17
16
  import { DialogsProvider } from "../contexts/DialogsProvider";
18
17
  import { useBuildDataSource } from "../internal/useBuildDataSource";
19
18
  import { useBuildCustomizationController } from "../internal/useBuildCustomizationController";
@@ -276,9 +276,9 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
276
276
  const getCollectionFromPaths = useCallback(<EC extends EntityCollection>(pathSegments: string[]): EC | undefined => {
277
277
 
278
278
  const collections = collectionsRef.current;
279
+ if (collections === undefined)
280
+ throw Error("getCollectionFromPaths: Collections have not been initialised yet");
279
281
  let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
280
- if (!currentCollections)
281
- throw Error("Collections have not been initialised yet");
282
282
 
283
283
  for (let i = 0; i < pathSegments.length; i++) {
284
284
  const pathSegment = pathSegments[i];
@@ -297,9 +297,9 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
297
297
  const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
298
298
 
299
299
  const collections = collectionsRef.current;
300
+ if (collections === undefined)
301
+ throw Error("getCollectionFromIds: Collections have not been initialised yet");
300
302
  let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
301
- if (!currentCollections)
302
- throw Error("Collections have not been initialised yet");
303
303
 
304
304
  for (let i = 0; i < ids.length; i++) {
305
305
  const id = ids[i];
@@ -335,9 +335,7 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
335
335
  []);
336
336
 
337
337
  const resolveAliasesFrom = useCallback((path: string): string => {
338
- const collections = collectionsRef.current;
339
- if (!collections)
340
- throw Error("Collections have not been initialised yet");
338
+ const collections = collectionsRef.current ?? [];
341
339
  return resolveCollectionPathIds(path, collections);
342
340
  }, []);
343
341
 
@@ -440,7 +438,7 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
440
438
  collectionPermissions: PermissionsBuilder | undefined,
441
439
  authController: AuthController,
442
440
  dataSource: DataSourceDelegate,
443
- injectCollections?: (collections: EntityCollection[]) => EntityCollection[]) {
441
+ injectCollections?: (collections: EntityCollection[]) => EntityCollection[]): Promise<EntityCollection[]> {
444
442
  let resolvedCollections: EntityCollection[] = [];
445
443
  if (typeof collections === "function") {
446
444
  resolvedCollections = await collections({
@@ -452,14 +450,14 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
452
450
  resolvedCollections = collections;
453
451
  }
454
452
 
455
- resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
456
-
457
- resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
458
-
459
453
  if (injectCollections) {
460
454
  resolvedCollections = injectCollections(resolvedCollections ?? []);
461
455
  }
462
456
 
457
+ resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
458
+
459
+ resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
460
+
463
461
  return resolvedCollections;
464
462
  }
465
463
 
@@ -489,8 +487,10 @@ function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
489
487
  if (a.length !== b.length) {
490
488
  return false;
491
489
  }
492
- const aSorted = a.sort((a, b) => a.id.localeCompare(b.id));
493
- const bSorted = b.sort((a, b) => a.id.localeCompare(b.id));
490
+ const aCopy = [...a];
491
+ const bCopy = [...b];
492
+ const aSorted = aCopy.sort((x, y) => x.id.localeCompare(y.id));
493
+ const bSorted = bCopy.sort((x, y) => x.id.localeCompare(y.id));
494
494
  return aSorted.every((value, index) => areCollectionsEqual(value, bSorted[index]));
495
495
  }
496
496
 
@@ -9,7 +9,7 @@ export type AccessResponse = {
9
9
  message?: string;
10
10
  }
11
11
 
12
- async function makeRequest(authController: AuthController, pluginKeys: string | undefined) {
12
+ async function makeRequest(authController: AuthController, pluginKeys: string[] | undefined) {
13
13
  const firebaseToken = await authController.getAuthToken();
14
14
  return fetch(DEFAULT_SERVER + "/access_log",
15
15
  {
@@ -30,12 +30,12 @@ export function useProjectLog(authController: AuthController,
30
30
  plugins?: FireCMSPlugin<any, any, any>[]): AccessResponse | null {
31
31
  const [accessResponse, setAccessResponse] = useState<AccessResponse | null>(null);
32
32
  const accessedUserRef = useRef<string | null>(null);
33
- const pluginKeys = plugins?.map(plugin => plugin.key).join(",");
33
+ const pluginKeys = plugins?.map(plugin => plugin.key);
34
34
  useEffect(() => {
35
35
  if (authController.user && authController.user.uid !== accessedUserRef.current && !authController.initialLoading) {
36
36
  makeRequest(authController, pluginKeys).then(setAccessResponse);
37
37
  accessedUserRef.current = authController.user.uid;
38
38
  }
39
- }, [authController]);
39
+ }, [authController, pluginKeys]);
40
40
  return accessResponse;
41
41
  }
@@ -49,7 +49,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
49
49
  }, 1);
50
50
  }
51
51
  } else {
52
- console.warn("Location path is not a collection path");
52
+ // console.warn("Location path is not a collection path");
53
53
  }
54
54
  initialised.current = true;
55
55
  }
@@ -56,7 +56,7 @@ 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: (pathOrAlias: string,
59
+ getCollection: (pathOrId: string,
60
60
  entityId?: string,
61
61
  includeUserOverride?: boolean) => EC | undefined;
62
62
  /**
@@ -60,14 +60,14 @@ export function resolveCollectionPathIds(path: string, allCollections: EntityCol
60
60
  /**
61
61
  * Find the corresponding view at any depth for a given path.
62
62
  * Note that path or segments of the paths can be collection aliases.
63
- * @param pathOrAlias
63
+ * @param pathOrId
64
64
  * @param collections
65
65
  */
66
- export function getCollectionByPathOrId(pathOrAlias: string, collections: EntityCollection[]): EntityCollection | undefined {
66
+ export function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[]): EntityCollection | undefined {
67
67
 
68
- const subpaths = removeInitialAndTrailingSlashes(pathOrAlias).split("/");
68
+ const subpaths = removeInitialAndTrailingSlashes(pathOrId).split("/");
69
69
  if (subpaths.length % 2 === 0) {
70
- throw Error(`getCollectionByPathOrAlias: Collection paths must have an odd number of segments: ${pathOrAlias}`);
70
+ throw Error(`getCollectionByPathOrId: Collection paths must have an odd number of segments: ${pathOrId}`);
71
71
  }
72
72
 
73
73
  const subpathCombinations = getCollectionPathsCombinations(subpaths);
@@ -80,10 +80,10 @@ export function getCollectionByPathOrId(pathOrAlias: string, collections: Entity
80
80
 
81
81
  if (navigationEntry) {
82
82
 
83
- if (subpathCombination === pathOrAlias) {
83
+ if (subpathCombination === pathOrId) {
84
84
  result = navigationEntry;
85
85
  } else if (navigationEntry.subcollections) {
86
- const newPath = pathOrAlias.replace(subpathCombination, "").split("/").slice(2).join("/");
86
+ const newPath = pathOrId.replace(subpathCombination, "").split("/").slice(2).join("/");
87
87
  if (newPath.length > 0)
88
88
  result = getCollectionByPathOrId(newPath, navigationEntry.subcollections);
89
89
  }
@@ -1 +0,0 @@
1
- export declare function useLocaleConfig(locale?: string): void;
@@ -1,18 +0,0 @@
1
- import * as locales from "date-fns/locale";
2
- // @ts-ignore
3
- import { registerLocale, setDefaultLocale } from "react-datepicker";
4
- import { useEffect } from "react";
5
-
6
- export function useLocaleConfig(locale?: string) {
7
- useEffect(() => {
8
- if (!locale) {
9
- return;
10
- }
11
- // @ts-ignore
12
- const dateFnsLocale = locales[locale];
13
- if (dateFnsLocale) {
14
- registerLocale(locale, dateFnsLocale);
15
- setDefaultLocale(locale);
16
- }
17
- }, [locale])
18
- }