@firecms/core 3.4.0-canary.f6a889a → 3.4.0

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 (123) hide show
  1. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  2. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  4. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  5. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  8. package/dist/components/ErrorBoundary.d.ts +1 -3
  9. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  10. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  11. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  12. package/dist/components/common/table_url_params.d.ts +24 -0
  13. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  14. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  15. package/dist/core/EntityEditView.d.ts +9 -1
  16. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  17. package/dist/form/EntityForm.d.ts +8 -1
  18. package/dist/form/EntityFormActions.d.ts +2 -0
  19. package/dist/hooks/data/delete.d.ts +1 -1
  20. package/dist/hooks/data/save.d.ts +1 -1
  21. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  22. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  23. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  24. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  25. package/dist/index.es.js +1753 -1170
  26. package/dist/index.es.js.map +1 -1
  27. package/dist/index.umd.js +1751 -1168
  28. package/dist/index.umd.js.map +1 -1
  29. package/dist/types/collections.d.ts +14 -0
  30. package/dist/types/datasource.d.ts +67 -4
  31. package/dist/types/entities.d.ts +21 -1
  32. package/dist/types/entity_actions.d.ts +6 -0
  33. package/dist/types/entity_callbacks.d.ts +21 -0
  34. package/dist/types/navigation.d.ts +34 -5
  35. package/dist/types/plugins.d.ts +13 -0
  36. package/dist/types/properties.d.ts +17 -1
  37. package/dist/types/side_entity_controller.d.ts +7 -0
  38. package/dist/types/translations.d.ts +6 -0
  39. package/dist/util/__tests__/collections.test.d.ts +1 -0
  40. package/dist/util/__tests__/urls.test.d.ts +1 -0
  41. package/dist/util/collections.d.ts +1 -1
  42. package/dist/util/entity_cache.d.ts +11 -0
  43. package/dist/util/index.d.ts +1 -0
  44. package/dist/util/navigation_from_path.d.ts +7 -0
  45. package/dist/util/navigation_utils.d.ts +87 -3
  46. package/dist/util/parent_references_from_path.d.ts +16 -0
  47. package/dist/util/paths.d.ts +28 -0
  48. package/dist/util/permissions.d.ts +10 -4
  49. package/dist/util/urls.d.ts +22 -0
  50. package/package.json +4 -4
  51. package/src/components/DeleteEntityDialog.tsx +2 -0
  52. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  53. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -0
  54. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  55. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  56. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  57. package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
  58. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  59. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  60. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  61. package/src/components/EntityPreview.tsx +4 -1
  62. package/src/components/ErrorBoundary.tsx +12 -18
  63. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  64. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  65. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  66. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  67. package/src/components/common/default_entity_actions.tsx +22 -2
  68. package/src/components/common/table_url_params.ts +172 -0
  69. package/src/components/common/useDataSourceTableController.tsx +86 -179
  70. package/src/components/common/useTableSearchHelper.ts +26 -2
  71. package/src/contexts/SnackbarProvider.tsx +14 -1
  72. package/src/core/EntityEditView.tsx +20 -3
  73. package/src/core/EntityEditViewFormActions.tsx +3 -2
  74. package/src/core/EntitySidePanel.tsx +8 -4
  75. package/src/form/EntityForm.tsx +31 -10
  76. package/src/form/EntityFormActions.tsx +2 -0
  77. package/src/hooks/data/delete.ts +8 -0
  78. package/src/hooks/data/save.ts +6 -1
  79. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  80. package/src/hooks/data/useEntityFetch.tsx +35 -4
  81. package/src/hooks/useBuildNavigationController.tsx +23 -8
  82. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  83. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  84. package/src/i18n/FireCMSi18nProvider.tsx +48 -4
  85. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  86. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  87. package/src/internal/useBuildDataSource.ts +37 -15
  88. package/src/internal/useBuildSideEntityController.tsx +19 -5
  89. package/src/locales/de.ts +5 -0
  90. package/src/locales/en.ts +5 -0
  91. package/src/locales/es.ts +5 -0
  92. package/src/locales/fr.ts +5 -0
  93. package/src/locales/hi.ts +5 -0
  94. package/src/locales/it.ts +5 -0
  95. package/src/locales/pl.ts +5 -0
  96. package/src/locales/pt.ts +5 -0
  97. package/src/preview/components/ReferencePreview.tsx +7 -3
  98. package/src/preview/components/StorageThumbnail.tsx +24 -2
  99. package/src/routes/FireCMSRoute.tsx +11 -3
  100. package/src/types/collections.ts +16 -0
  101. package/src/types/datasource.ts +67 -4
  102. package/src/types/entities.ts +24 -1
  103. package/src/types/entity_actions.tsx +6 -0
  104. package/src/types/entity_callbacks.ts +24 -0
  105. package/src/types/navigation.ts +35 -5
  106. package/src/types/plugins.tsx +13 -0
  107. package/src/types/properties.ts +17 -0
  108. package/src/types/side_entity_controller.tsx +7 -0
  109. package/src/types/translations.ts +6 -0
  110. package/src/util/__tests__/collections.test.ts +32 -0
  111. package/src/util/__tests__/urls.test.ts +131 -0
  112. package/src/util/collections.ts +1 -1
  113. package/src/util/entities.ts +3 -1
  114. package/src/util/entity_cache.ts +18 -0
  115. package/src/util/index.ts +1 -0
  116. package/src/util/navigation_from_path.ts +25 -2
  117. package/src/util/navigation_utils.ts +208 -2
  118. package/src/util/parent_references_from_path.ts +42 -3
  119. package/src/util/paths.ts +40 -3
  120. package/src/util/permissions.ts +22 -10
  121. package/src/util/previews.ts +8 -1
  122. package/src/util/resolutions.ts +8 -0
  123. package/src/util/urls.ts +52 -0
@@ -146,6 +146,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
146
146
  blockSearch?: (props: {
147
147
  context: FireCMSContext,
148
148
  path: string,
149
+ /** `path` split at its real segment boundaries, when known. */
150
+ pathSegments?: string[],
149
151
  collection: EC,
150
152
  parentCollectionIds?: string[]
151
153
  }) => boolean;
@@ -153,6 +155,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
153
155
  showTextSearchBar?: (props: {
154
156
  context: FireCMSContext,
155
157
  path: string,
158
+ /** `path` split at its real segment boundaries, when known. */
159
+ pathSegments?: string[],
156
160
  collection: EC,
157
161
  parentCollectionIds?: string[]
158
162
  }) => boolean;
@@ -160,6 +164,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
160
164
  onTextSearchClick?: (props: {
161
165
  context: FireCMSContext,
162
166
  path: string,
167
+ /** `path` split at its real segment boundaries, when known. */
168
+ pathSegments?: string[],
163
169
  collection: EC,
164
170
  parentCollectionIds?: string[]
165
171
  }) => Promise<boolean>;
@@ -308,6 +314,13 @@ export interface PluginHomePageActionsProps<EP extends object = object, M extend
308
314
  export interface PluginFormActionProps<USER extends User = User, EC extends EntityCollection = EntityCollection> {
309
315
  entityId?: string;
310
316
  path: string;
317
+ /**
318
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
319
+ *
320
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
321
+ * contain "/". Absent means "not known here", not "no slashes".
322
+ */
323
+ pathSegments?: string[];
311
324
  parentCollectionIds: string[];
312
325
  status: EntityStatus;
313
326
  collection: EC;
@@ -562,6 +562,22 @@ export type PropertyBuilderProps<M extends Record<string, any> = any> =
562
562
  * Current value of this property
563
563
  */
564
564
  propertyValue?: any;
565
+ /**
566
+ * Path of this property within the entity, in dot notation, e.g.
567
+ * `my_map.my_field` or `my_array.0.my_field`.
568
+ * Useful to look up sibling values in `values` with `getValueInPath`.
569
+ *
570
+ * When a property builder is used as the `of` prop of an array property,
571
+ * it is resolved twice: once per existing element, with an index
572
+ * qualified key (`my_array.0`), and once as the template for the element
573
+ * shape, with the array's own key (`my_array`). In the latter case
574
+ * `propertyValue` is the whole array, since the template is not bound to
575
+ * any element.
576
+ *
577
+ * It may be undefined if the property is resolved outside the context of
578
+ * an entity.
579
+ */
580
+ propertyKey?: string;
565
581
  /**
566
582
  * Index of this property (only for arrays)
567
583
  */
@@ -592,6 +608,7 @@ export type PropertyBuilder<T extends CMSType = any, M extends Record<string, an
592
608
  values,
593
609
  previousValues,
594
610
  propertyValue,
611
+ propertyKey,
595
612
  index,
596
613
  path,
597
614
  entityId,
@@ -19,6 +19,13 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
19
19
  * Full CMS path of the entity, including the collection and sub-collections.
20
20
  */
21
21
  fullIdPath?: string;
22
+ /**
23
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
24
+ *
25
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
26
+ * the case the field exists for. Absent means "not known here", not "no slashes".
27
+ */
28
+ pathSegments?: string[];
22
29
 
23
30
  /**
24
31
  * ID of the entity, if not set, it means we are creating a new entity
@@ -419,6 +419,8 @@ export interface FireCMSTranslations {
419
419
  download: string;
420
420
  large_number_of_documents: string;
421
421
  include_undefined_values: string;
422
+ /** Toggle in the export dialog, only shown when a filter or sort is active */
423
+ export_apply_filter_sort: string;
422
424
  submit: string;
423
425
 
424
426
  no_filterable_properties: string;
@@ -620,6 +622,10 @@ export interface FireCMSTranslations {
620
622
  order_property_not_found: string;
621
623
  no_number_properties: string;
622
624
  order_property_description: string;
625
+ image_property: string;
626
+ image_property_not_found: string;
627
+ no_image_properties: string;
628
+ image_property_description: string;
623
629
  display_settings: string;
624
630
  default_row_size: string;
625
631
  side_dialog_width: string;
@@ -0,0 +1,32 @@
1
+ import { getLocalChangesBackup } from "../collections";
2
+ import { EntityCollection } from "../../types";
3
+
4
+ function collectionWith(localChangesBackup?: "manual_apply" | "auto_apply" | false): EntityCollection {
5
+ return {
6
+ id: "products",
7
+ name: "Products",
8
+ path: "products",
9
+ properties: {},
10
+ localChangesBackup
11
+ } as EntityCollection;
12
+ }
13
+
14
+ describe("getLocalChangesBackup", () => {
15
+
16
+ it("should default to manual_apply when not set", () => {
17
+ expect(getLocalChangesBackup(collectionWith(undefined))).toEqual("manual_apply");
18
+ });
19
+
20
+ it("should return manual_apply when explicitly set", () => {
21
+ expect(getLocalChangesBackup(collectionWith("manual_apply"))).toEqual("manual_apply");
22
+ });
23
+
24
+ it("should return auto_apply when explicitly set", () => {
25
+ expect(getLocalChangesBackup(collectionWith("auto_apply"))).toEqual("auto_apply");
26
+ });
27
+
28
+ it("should return false when explicitly disabled, and not fall back to the default", () => {
29
+ expect(getLocalChangesBackup(collectionWith(false))).toEqual(false);
30
+ });
31
+
32
+ });
@@ -0,0 +1,131 @@
1
+ import { getFileExtensionFromUrl, getPreviewTypeFromUrl, isAbsoluteHttpUrl } from "../urls";
2
+
3
+ describe("url utilities", () => {
4
+
5
+ describe("isAbsoluteHttpUrl", () => {
6
+
7
+ it("should detect absolute http and https URLs", () => {
8
+ expect(isAbsoluteHttpUrl("https://abc.com/abc.png")).toBe(true);
9
+ expect(isAbsoluteHttpUrl("http://abc.com/abc.png")).toBe(true);
10
+ expect(isAbsoluteHttpUrl("HTTPS://ABC.COM/ABC.PNG")).toBe(true);
11
+ });
12
+
13
+ it("should detect Firebase Storage download URLs as absolute URLs", () => {
14
+ expect(isAbsoluteHttpUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.png?alt=media&token=abc")).toBe(true);
15
+ expect(isAbsoluteHttpUrl("https://storage.googleapis.com/demo.appspot.com/images/cat.png")).toBe(true);
16
+ });
17
+
18
+ it("should not consider storage paths absolute URLs", () => {
19
+ expect(isAbsoluteHttpUrl("images/cat.png")).toBe(false);
20
+ expect(isAbsoluteHttpUrl("/images/cat.png")).toBe(false);
21
+ expect(isAbsoluteHttpUrl("cat.png")).toBe(false);
22
+ });
23
+
24
+ it("should not consider other protocols absolute http URLs", () => {
25
+ expect(isAbsoluteHttpUrl("gs://demo.appspot.com/images/cat.png")).toBe(false);
26
+ expect(isAbsoluteHttpUrl("data:image/png;base64,AAAA")).toBe(false);
27
+ expect(isAbsoluteHttpUrl("//abc.com/abc.png")).toBe(false);
28
+ });
29
+
30
+ it("should handle empty values", () => {
31
+ expect(isAbsoluteHttpUrl("")).toBe(false);
32
+ expect(isAbsoluteHttpUrl(undefined)).toBe(false);
33
+ expect(isAbsoluteHttpUrl(null)).toBe(false);
34
+ });
35
+
36
+ it("should not match http appearing later in the value", () => {
37
+ expect(isAbsoluteHttpUrl("images/https://abc.com/abc.png")).toBe(false);
38
+ });
39
+
40
+ });
41
+
42
+ describe("getFileExtensionFromUrl", () => {
43
+
44
+ it("should extract the extension of a plain URL", () => {
45
+ expect(getFileExtensionFromUrl("https://abc.com/abc.png")).toBe("png");
46
+ });
47
+
48
+ it("should lowercase the extension", () => {
49
+ expect(getFileExtensionFromUrl("https://abc.com/ABC.PNG")).toBe("png");
50
+ });
51
+
52
+ it("should ignore query strings and hash fragments", () => {
53
+ expect(getFileExtensionFromUrl("https://abc.com/abc.png?width=200")).toBe("png");
54
+ expect(getFileExtensionFromUrl("https://abc.com/abc.png#anchor")).toBe("png");
55
+ expect(getFileExtensionFromUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.jpeg?alt=media&token=abc")).toBe("jpeg");
56
+ });
57
+
58
+ it("should work with storage paths", () => {
59
+ expect(getFileExtensionFromUrl("images/cat.webp")).toBe("webp");
60
+ });
61
+
62
+ it("should use the last dot of the file name", () => {
63
+ expect(getFileExtensionFromUrl("https://abc.com/archive.tar.gz")).toBe("gz");
64
+ });
65
+
66
+ it("should return undefined when there is no extension", () => {
67
+ expect(getFileExtensionFromUrl("https://abc.com/abc")).toBeUndefined();
68
+ expect(getFileExtensionFromUrl("https://abc.com/")).toBeUndefined();
69
+ expect(getFileExtensionFromUrl("images/cat")).toBeUndefined();
70
+ });
71
+
72
+ it("should not take dots of the host or the path as extensions", () => {
73
+ expect(getFileExtensionFromUrl("https://abc.com/images/cat")).toBeUndefined();
74
+ expect(getFileExtensionFromUrl("https://abc.com/abc.png/download")).toBeUndefined();
75
+ });
76
+
77
+ it("should return undefined for dotfiles and trailing dots", () => {
78
+ expect(getFileExtensionFromUrl("https://abc.com/.gitignore")).toBeUndefined();
79
+ expect(getFileExtensionFromUrl("https://abc.com/abc.")).toBeUndefined();
80
+ });
81
+
82
+ it("should handle empty values", () => {
83
+ expect(getFileExtensionFromUrl("")).toBeUndefined();
84
+ expect(getFileExtensionFromUrl(undefined)).toBeUndefined();
85
+ expect(getFileExtensionFromUrl(null)).toBeUndefined();
86
+ });
87
+
88
+ });
89
+
90
+ describe("getPreviewTypeFromUrl", () => {
91
+
92
+ it("should infer image previews", () => {
93
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.png")).toBe("image");
94
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.jpg")).toBe("image");
95
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.jpeg")).toBe("image");
96
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.gif")).toBe("image");
97
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.webp")).toBe("image");
98
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.svg")).toBe("image");
99
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.avif")).toBe("image");
100
+ });
101
+
102
+ it("should infer video previews", () => {
103
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.mp4")).toBe("video");
104
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.webm")).toBe("video");
105
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.mov")).toBe("video");
106
+ });
107
+
108
+ it("should infer audio previews", () => {
109
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.mp3")).toBe("audio");
110
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.wav")).toBe("audio");
111
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.m4a")).toBe("audio");
112
+ });
113
+
114
+ it("should infer the type of Firebase Storage download URLs", () => {
115
+ expect(getPreviewTypeFromUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.png?alt=media&token=abc")).toBe("image");
116
+ });
117
+
118
+ it("should be case insensitive", () => {
119
+ expect(getPreviewTypeFromUrl("https://abc.com/ABC.PNG")).toBe("image");
120
+ });
121
+
122
+ it("should return undefined for unknown or missing extensions", () => {
123
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.pdf")).toBeUndefined();
124
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.zip")).toBeUndefined();
125
+ expect(getPreviewTypeFromUrl("https://abc.com/abc")).toBeUndefined();
126
+ expect(getPreviewTypeFromUrl(undefined)).toBeUndefined();
127
+ });
128
+
129
+ });
130
+
131
+ });
@@ -117,7 +117,7 @@ export const applyPermissionsFunctionIfEmpty = (collections: EntityCollection[],
117
117
  }
118
118
 
119
119
  export function getLocalChangesBackup(collection: EntityCollection) {
120
- if (!collection.localChangesBackup) {
120
+ if (collection.localChangesBackup === undefined) {
121
121
  return "manual_apply";
122
122
  }
123
123
 
@@ -142,7 +142,9 @@ export function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M
142
142
  if (!entity) {
143
143
  throw new Error("getReferenceFrom: entity is null or undefined");
144
144
  }
145
- return new EntityReference(entity.id, entity.path, entity.databaseId);
145
+ // Carry the segments the entity was loaded with, so a reference to an entity under a
146
+ // slash-bearing parent stays resolvable. Undefined when the entity does not have them.
147
+ return new EntityReference(entity.id, entity.path, entity.databaseId, entity.pathSegments);
146
148
  }
147
149
 
148
150
  export function traverseValuesProperties<M extends Record<string, any>>(
@@ -1,5 +1,23 @@
1
1
  import { EntityReference, GeoPoint, Vector } from "../types";
2
2
  import { isObject, isPlainObject } from "./objects";
3
+ import { encodeEntityId } from "./navigation_utils";
4
+
5
+ /**
6
+ * Key identifying one entity in a cache.
7
+ *
8
+ * The id is escaped so that a `(path, entityId)` pair maps to exactly one key. Without it
9
+ * `("a", "b/c")` and `("a/b", "c")` both flatten to `"a/b/c"`, and one entity's cached
10
+ * values are served for the other.
11
+ *
12
+ * `encodeEntityId` is the identity for any id without "/", "?", "#" or "%", so this changes
13
+ * no key in an app whose ids cannot contain them.
14
+ */
15
+ export function entityCacheKey(path: string | undefined, entityId: string | undefined): string {
16
+ // Either part may be undefined at a call site that used to build the key by string
17
+ // concatenation; both are stringified exactly as `path + "/" + entityId` did, so such a
18
+ // caller keeps the key it had.
19
+ return `${path}/${entityId === undefined ? entityId : encodeEntityId(entityId)}`;
20
+ }
3
21
 
4
22
  // Define a unique prefix for entity keys in localStorage to avoid key collisions
5
23
  const LOCAL_STORAGE_PREFIX = "entity_cache::";
package/src/util/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./enums";
6
6
  export * from "./objects";
7
7
  export * from "./paths";
8
8
  export * from "./regexp";
9
+ export * from "./urls";
9
10
  export * from "./navigation_utils";
10
11
  export * from "./entity_actions";
11
12
  export * from "./useDebouncedCallback";
@@ -13,6 +13,8 @@ export interface NavigationViewEntityInternal<M extends Record<string, any>> {
13
13
  path: string;
14
14
  fullIdPath: string;
15
15
  fullPath: string;
16
+ /** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
17
+ pathSegments: string[];
16
18
  parentCollection: EntityCollection<M>;
17
19
  }
18
20
 
@@ -22,6 +24,8 @@ export interface NavigationViewCollectionInternal<M extends Record<string, any>>
22
24
  path: string;
23
25
  fullIdPath: string;
24
26
  fullPath: string;
27
+ /** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
28
+ pathSegments: string[];
25
29
  collection: EntityCollection<M>;
26
30
  }
27
31
 
@@ -30,6 +34,8 @@ export interface NavigationViewEntityCustomInternal<M extends Record<string, any
30
34
  path: string;
31
35
  fullIdPath: string;
32
36
  fullPath: string;
37
+ /** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
38
+ pathSegments: string[];
33
39
  entityId: string;
34
40
  view: EntityCustomView<M>;
35
41
  }
@@ -49,6 +55,7 @@ export function getNavigationEntriesFromPath(props: {
49
55
  currentFullPath?: string,
50
56
  currentFullIdPath?: string,
51
57
  currentFullUrlPath?: string,
58
+ currentPathSegments?: string[],
52
59
  contextEntityViews?: EntityCustomView<any>[]
53
60
  }): NavigationViewInternal [] {
54
61
 
@@ -57,7 +64,8 @@ export function getNavigationEntriesFromPath(props: {
57
64
  collections = [],
58
65
  currentFullPath,
59
66
  currentFullIdPath,
60
- currentFullUrlPath
67
+ currentFullUrlPath,
68
+ currentPathSegments = []
61
69
  } = props;
62
70
 
63
71
  const subpaths = removeInitialAndTrailingSlashes(path).split("/");
@@ -83,12 +91,16 @@ export function getNavigationEntriesFromPath(props: {
83
91
  const fullIdPath = currentFullIdPath && currentFullIdPath.length > 0
84
92
  ? (currentFullIdPath + "/" + collection.id)
85
93
  : collection.id;
94
+ // A collection `path` may itself span several segments (e.g. "users/uid/experiences"),
95
+ // and those are unambiguous, so they are spread rather than kept whole.
96
+ const collectionSegments = [...currentPathSegments, ...collection.path.split("/")];
86
97
  result.push({
87
98
  type: "collection",
88
99
  id: collection.id,
89
100
  path: collectionPath,
90
101
  fullPath: collectionUrlPath,
91
102
  fullIdPath,
103
+ pathSegments: collectionSegments,
92
104
  collection
93
105
  });
94
106
  const restOfThePath = removeInitialAndTrailingSlashes(removeInitialAndTrailingSlashes(path).replace(subpathCombination, ""));
@@ -98,12 +110,16 @@ export function getNavigationEntriesFromPath(props: {
98
110
  const entityId = decodeEntityId(encodedEntityId);
99
111
  const fullPath = collectionPath + "/" + entityId;
100
112
  const fullUrlPath = collectionUrlPath + "/" + encodedEntityId;
113
+ // The id is ONE segment however many slashes it contains — that is the
114
+ // whole point of this array.
115
+ const entitySegments = [...collectionSegments, entityId];
101
116
  result.push({
102
117
  type: "entity",
103
118
  entityId,
104
119
  path: collectionPath,
105
120
  fullIdPath,
106
121
  fullPath: fullUrlPath,
122
+ pathSegments: collectionSegments,
107
123
  parentCollection: collection
108
124
  });
109
125
  if (nextSegments.length > 1) {
@@ -123,6 +139,7 @@ export function getNavigationEntriesFromPath(props: {
123
139
  entityId: entityId,
124
140
  fullIdPath,
125
141
  fullPath: fullUrlPath + "/" + customView.key,
142
+ pathSegments: collectionSegments,
126
143
  view: customView
127
144
  });
128
145
  } else if (collection.subcollections) {
@@ -130,8 +147,14 @@ export function getNavigationEntriesFromPath(props: {
130
147
  path: newPath,
131
148
  collections: collection.subcollections,
132
149
  currentFullPath: fullPath,
133
- currentFullIdPath: fullIdPath,
150
+ // The entity id is a hop in the id chain exactly as it is in the
151
+ // other two. Without it a nested `fullIdPath` was
152
+ // "products/locales" rather than "products/pid/locales", so any
153
+ // URL built from it pointed at a collection that does not exist.
154
+ // Escaped, because `fullIdPath` is URL-facing.
155
+ currentFullIdPath: fullIdPath + "/" + encodedEntityId,
134
156
  currentFullUrlPath: fullUrlPath,
157
+ currentPathSegments: entitySegments,
135
158
  contextEntityViews: props.contextEntityViews
136
159
  }));
137
160
  }