@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.cb27f16

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 (82) hide show
  1. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  2. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
  4. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  5. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  7. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  8. package/dist/components/common/useDataSourceTableController.d.ts +4 -2
  9. package/dist/components/common/useTableSearchHelper.d.ts +3 -1
  10. package/dist/core/EntityEditView.d.ts +2 -0
  11. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  12. package/dist/form/EntityForm.d.ts +8 -1
  13. package/dist/form/EntityFormActions.d.ts +2 -0
  14. package/dist/hooks/data/delete.d.ts +1 -1
  15. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  16. package/dist/hooks/data/useEntityFetch.d.ts +4 -1
  17. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  18. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  19. package/dist/index.es.js +970 -652
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/index.umd.js +969 -651
  22. package/dist/index.umd.js.map +1 -1
  23. package/dist/types/collections.d.ts +7 -0
  24. package/dist/types/datasource.d.ts +25 -4
  25. package/dist/types/entities.d.ts +21 -1
  26. package/dist/types/entity_actions.d.ts +6 -0
  27. package/dist/types/entity_callbacks.d.ts +21 -0
  28. package/dist/types/navigation.d.ts +34 -5
  29. package/dist/types/plugins.d.ts +13 -0
  30. package/dist/types/side_entity_controller.d.ts +3 -2
  31. package/dist/util/entity_cache.d.ts +11 -0
  32. package/dist/util/navigation_utils.d.ts +84 -2
  33. package/dist/util/parent_references_from_path.d.ts +16 -0
  34. package/dist/util/paths.d.ts +28 -0
  35. package/dist/util/permissions.d.ts +10 -4
  36. package/package.json +4 -4
  37. package/src/components/DeleteEntityDialog.tsx +2 -0
  38. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  39. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  40. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  41. package/src/components/EntityCollectionView/EntityCollectionView.tsx +49 -16
  42. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  43. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  44. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  45. package/src/components/EntityPreview.tsx +4 -1
  46. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  47. package/src/components/common/default_entity_actions.tsx +22 -2
  48. package/src/components/common/useDataSourceTableController.tsx +32 -8
  49. package/src/components/common/useTableSearchHelper.ts +5 -0
  50. package/src/core/EntityEditView.tsx +11 -12
  51. package/src/core/EntityEditViewFormActions.tsx +3 -2
  52. package/src/core/EntitySidePanel.tsx +6 -4
  53. package/src/form/EntityForm.tsx +30 -9
  54. package/src/form/EntityFormActions.tsx +2 -0
  55. package/src/hooks/data/delete.ts +8 -0
  56. package/src/hooks/data/save.ts +5 -2
  57. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  58. package/src/hooks/data/useEntityFetch.tsx +28 -6
  59. package/src/hooks/useBuildNavigationController.tsx +23 -8
  60. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  61. package/src/i18n/FireCMSi18nProvider.tsx +48 -4
  62. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  63. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  64. package/src/internal/useBuildDataSource.ts +26 -15
  65. package/src/internal/useBuildSideEntityController.tsx +13 -5
  66. package/src/preview/components/ReferencePreview.tsx +7 -3
  67. package/src/routes/FireCMSRoute.tsx +9 -4
  68. package/src/types/collections.ts +8 -0
  69. package/src/types/datasource.ts +25 -4
  70. package/src/types/entities.ts +24 -1
  71. package/src/types/entity_actions.tsx +6 -0
  72. package/src/types/entity_callbacks.ts +24 -0
  73. package/src/types/navigation.ts +35 -5
  74. package/src/types/plugins.tsx +13 -0
  75. package/src/types/side_entity_controller.tsx +3 -2
  76. package/src/util/entities.ts +3 -1
  77. package/src/util/entity_cache.ts +18 -0
  78. package/src/util/navigation_from_path.ts +6 -1
  79. package/src/util/navigation_utils.ts +204 -2
  80. package/src/util/parent_references_from_path.ts +42 -3
  81. package/src/util/paths.ts +40 -3
  82. package/src/util/permissions.ts +22 -10
@@ -1,18 +1,40 @@
1
1
  import { EntityCollection, EntityReference } from "../types";
2
- import { decodeEntityId, getCollectionPathsCombinations, removeInitialAndTrailingSlashes } from "./navigation_utils";
2
+ import {
3
+ decodeEntityId,
4
+ getCollectionPathsCombinations,
5
+ removeInitialAndTrailingSlashes,
6
+ walkPathSegments
7
+ } from "./navigation_utils";
3
8
 
9
+ /**
10
+ * References to every parent entity in a path.
11
+ *
12
+ * NOTE on encoding: `path` is expected ESCAPED, because it comes from the URL — the entity
13
+ * ids in it are decoded on the way out. Pass `pathSegments` instead whenever the path at
14
+ * hand is a raw datasource path (`entity.path`), which is not escaped and whose ids may
15
+ * therefore contain a bare "/": splitting that would silently produce the wrong references.
16
+ */
4
17
  export function getParentReferencesFromPath(props: {
5
18
  path: string,
6
19
  collections: EntityCollection[] | undefined,
7
20
  currentFullPath?: string,
21
+ currentPathSegments?: string[],
22
+ /** `path` split at its real segment boundaries, with RAW ids. Takes precedence. */
23
+ pathSegments?: string[],
8
24
  }): EntityReference [] {
9
25
 
10
26
  const {
11
27
  path,
12
28
  collections = [],
13
29
  currentFullPath,
30
+ currentPathSegments = [],
31
+ pathSegments,
14
32
  } = props;
15
33
 
34
+ if (pathSegments) {
35
+ return getParentReferencesFromPathSegments(pathSegments, collections);
36
+ }
37
+
16
38
  const subpaths = removeInitialAndTrailingSlashes(path).split("/");
17
39
  const subpathCombinations = getCollectionPathsCombinations(subpaths);
18
40
 
@@ -27,6 +49,9 @@ export function getParentReferencesFromPath(props: {
27
49
  const collectionPath = currentFullPath && currentFullPath.length > 0
28
50
  ? (currentFullPath + "/" + collection.path)
29
51
  : collection.path;
52
+ // A collection's own path may span several segments, and those are
53
+ // unambiguous, so they are spread rather than kept whole.
54
+ const collectionSegments = [...currentPathSegments, ...collection.path.split("/")];
30
55
 
31
56
  const restOfThePath = removeInitialAndTrailingSlashes(removeInitialAndTrailingSlashes(path).replace(subpathCombination, ""));
32
57
  const nextSegments = restOfThePath.length > 0 ? restOfThePath.split("/") : [];
@@ -35,7 +60,9 @@ export function getParentReferencesFromPath(props: {
35
60
  // datasource-facing, so they carry the raw id.
36
61
  const entityId = decodeEntityId(nextSegments[0]);
37
62
  const fullPath = collectionPath + "/" + entityId;
38
- result.push(new EntityReference(entityId, collectionPath));
63
+ // The id is ONE segment however many slashes it contains.
64
+ const entitySegments = [...collectionSegments, entityId];
65
+ result.push(new EntityReference(entityId, collectionPath, undefined, collectionSegments));
39
66
  if (nextSegments.length > 1) {
40
67
  const newPath = nextSegments.slice(1).join("/");
41
68
  if (!collection) {
@@ -45,7 +72,8 @@ export function getParentReferencesFromPath(props: {
45
72
  result.push(...getParentReferencesFromPath({
46
73
  path: newPath,
47
74
  collections: collection.subcollections,
48
- currentFullPath: fullPath
75
+ currentFullPath: fullPath,
76
+ currentPathSegments: entitySegments
49
77
  }));
50
78
  }
51
79
  }
@@ -56,3 +84,14 @@ export function getParentReferencesFromPath(props: {
56
84
  }
57
85
  return result;
58
86
  }
87
+
88
+ /**
89
+ * The segment-wise counterpart: the boundaries are given, so an entity id keeps its
90
+ * slashes and nothing has to be decoded.
91
+ */
92
+ export function getParentReferencesFromPathSegments(pathSegments: string[],
93
+ collections: EntityCollection[]): EntityReference[] {
94
+ return walkPathSegments(pathSegments, collections).steps
95
+ .filter(step => step.entityId !== undefined)
96
+ .map(step => new EntityReference(step.entityId!, step.collectionPath, undefined, step.collectionSegments));
97
+ }
package/src/util/paths.ts CHANGED
@@ -18,10 +18,47 @@ export function segmentsToStrippedPath(paths: string[]) {
18
18
  /**
19
19
  * Extract the collection path routes
20
20
  * `products/B44RG6APH/locales` => [`products`, `locales`]
21
+ *
22
+ * Finds the entity ids by position, which only works while no id contains "/" — with one,
23
+ * every following segment shifts and the wrong elements are kept. Use
24
+ * {@link collectionSegmentsFrom} when the real boundaries are known.
25
+ *
21
26
  * @param path
22
27
  */
23
28
  export function fullPathToCollectionSegments(path: string): string[] {
24
- return path
25
- .split("/")
26
- .filter((e, i) => i % 2 === 0);
29
+ return positionalCollectionSegments(path);
30
+ }
31
+
32
+ /**
33
+ * The collection chain of an already-segmented path:
34
+ * `['products', 'B44RG6APH', 'locales']` => [`products`, `locales`]
35
+ *
36
+ * The counterpart of {@link fullPathToCollectionSegments} for callers that know the real
37
+ * segment boundaries. Kept as a separate function rather than an extra parameter on that
38
+ * one: these helpers are small enough to be passed point-free, and `paths.map(fn)` would
39
+ * then hand the array index in as the new argument.
40
+ */
41
+ export function collectionSegmentsFrom(pathSegments: string[]): string[] {
42
+ return keepCollectionPositions(pathSegments);
43
+ }
44
+
45
+ /**
46
+ * The historical, positional reading of a flattened path:
47
+ * `products/B44RG6APH/locales` => [`products`, `locales`]
48
+ *
49
+ * This produces a **collection chain**, not a datasource `pathSegments` value, and must
50
+ * never be used as one — see `path_segments_no_fabrication.test.ts`. It is only correct
51
+ * while no entity id contains "/", which is why every caller prefers real segments when
52
+ * they are available, and why it is named for what it assumes.
53
+ *
54
+ * It is still the right reading for a path that came from a URL, where entity ids arrive
55
+ * escaped and so cannot contain a bare "/".
56
+ */
57
+ export function positionalCollectionSegments(path: string): string[] {
58
+ return keepCollectionPositions(path.split("/"));
59
+ }
60
+
61
+ /** Keep the even positions of an alternating collection/entity-id chain. */
62
+ function keepCollectionPositions(segments: string[]): string[] {
63
+ return segments.filter((e, i) => i % 2 === 0);
27
64
  }
@@ -1,5 +1,5 @@
1
1
  import { AuthController, Entity, EntityCollection, Permissions, User } from "../types";
2
- import { fullPathToCollectionSegments } from "./paths";
2
+ import { collectionSegmentsFrom, fullPathToCollectionSegments } from "./paths";
3
3
 
4
4
  const DEFAULT_PERMISSIONS = {
5
5
  read: true,
@@ -8,11 +8,18 @@ const DEFAULT_PERMISSIONS = {
8
8
  delete: true
9
9
  };
10
10
 
11
+ /**
12
+ * `pathSegments` is `path` split at its real segment boundaries. It is forwarded so the
13
+ * collection chain handed to a permissions builder stays correct when an entity id contains
14
+ * "/" — splitting `path` positionally would keep the wrong elements. Optional everywhere:
15
+ * omitted, behaviour is exactly as before.
16
+ */
11
17
  export function resolvePermissions<M extends Record<string, any>, USER extends User>
12
18
  (collection: EntityCollection<M>,
13
19
  authController: AuthController<USER>,
14
20
  path: string,
15
- entity: Entity<M> | null): Permissions | undefined {
21
+ entity: Entity<M> | null,
22
+ pathSegments?: string[]): Permissions | undefined {
16
23
 
17
24
  const permission = collection.permissions;
18
25
  if (permission === undefined) {
@@ -20,14 +27,16 @@ export function resolvePermissions<M extends Record<string, any>, USER extends U
20
27
  } else if (typeof permission === "object") {
21
28
  return permission as Permissions;
22
29
  } else if (typeof permission === "function") {
23
- const pathSegments = fullPathToCollectionSegments(path);
30
+ const collectionSegments = pathSegments
31
+ ? collectionSegmentsFrom(pathSegments)
32
+ : fullPathToCollectionSegments(path);
24
33
  return permission({
25
34
  entity,
26
35
  path,
27
36
  user: authController.user,
28
37
  authController,
29
38
  collection,
30
- pathSegments
39
+ pathSegments: collectionSegments
31
40
  });
32
41
  }
33
42
  console.error("Permissions:", permission);
@@ -39,8 +48,9 @@ export function canEditEntity<M extends Record<string, any>, USER extends User>
39
48
  collection: EntityCollection<M>,
40
49
  authController: AuthController<USER>,
41
50
  path: string,
42
- entity: Entity<M> | null): boolean {
43
- return resolvePermissions(collection, authController, path, entity)?.edit ?? DEFAULT_PERMISSIONS.edit;
51
+ entity: Entity<M> | null,
52
+ pathSegments?: string[]): boolean {
53
+ return resolvePermissions(collection, authController, path, entity, pathSegments)?.edit ?? DEFAULT_PERMISSIONS.edit;
44
54
  }
45
55
 
46
56
  export function canCreateEntity<M extends Record<string, any>, USER extends User>
@@ -48,9 +58,10 @@ export function canCreateEntity<M extends Record<string, any>, USER extends User
48
58
  collection: EntityCollection<M>,
49
59
  authController: AuthController<USER>,
50
60
  path: string,
51
- entity: Entity<M> | null): boolean {
61
+ entity: Entity<M> | null,
62
+ pathSegments?: string[]): boolean {
52
63
  if (collection.collectionGroup) return false;
53
- return resolvePermissions(collection, authController, path, entity)?.create ?? DEFAULT_PERMISSIONS.create;
64
+ return resolvePermissions(collection, authController, path, entity, pathSegments)?.create ?? DEFAULT_PERMISSIONS.create;
54
65
  }
55
66
 
56
67
  export function canDeleteEntity<M extends Record<string, any>, USER extends User>
@@ -58,8 +69,9 @@ export function canDeleteEntity<M extends Record<string, any>, USER extends User
58
69
  collection: EntityCollection<M>,
59
70
  authController: AuthController<USER>,
60
71
  path: string,
61
- entity: Entity<M> | null): boolean {
62
- return resolvePermissions(collection, authController, path, entity)?.delete ?? DEFAULT_PERMISSIONS.delete;
72
+ entity: Entity<M> | null,
73
+ pathSegments?: string[]): boolean {
74
+ return resolvePermissions(collection, authController, path, entity, pathSegments)?.delete ?? DEFAULT_PERMISSIONS.delete;
63
75
  }
64
76
 
65
77
  // export function resolveCollectionsPermissions(roles: Role[]): Record<string, Permissions> {