@firecms/core 3.0.0-beta.13 → 3.0.0-beta.14

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 (71) hide show
  1. package/dist/components/ArrayContainer.d.ts +7 -12
  2. package/dist/components/EntityPreview.d.ts +4 -2
  3. package/dist/components/SelectableTable/SelectableTable.d.ts +1 -1
  4. package/dist/components/VirtualTable/VirtualTableProps.d.ts +0 -4
  5. package/dist/components/index.d.ts +1 -0
  6. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  7. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -9
  8. package/dist/hooks/useBuildNavigationController.d.ts +2 -9
  9. package/dist/index.es.js +1183 -705
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/index.umd.js +1191 -716
  12. package/dist/index.umd.js.map +1 -1
  13. package/dist/types/collections.d.ts +20 -2
  14. package/dist/types/firecms.d.ts +2 -1
  15. package/dist/types/navigation.d.ts +5 -0
  16. package/dist/types/plugins.d.ts +12 -0
  17. package/dist/types/side_entity_controller.d.ts +4 -0
  18. package/dist/util/callbacks.d.ts +2 -0
  19. package/dist/util/index.d.ts +1 -0
  20. package/package.json +8 -6
  21. package/src/components/ArrayContainer.tsx +409 -294
  22. package/src/components/ClearFilterSortButton.tsx +1 -1
  23. package/src/components/ConfirmationDialog.tsx +9 -9
  24. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +1 -1
  25. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +1 -1
  26. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +13 -2
  27. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +53 -27
  28. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +9 -9
  29. package/src/components/EntityCollectionView/EntityCollectionView.tsx +4 -0
  30. package/src/components/EntityPreview.tsx +18 -14
  31. package/src/components/ErrorView.tsx +1 -1
  32. package/src/components/HomePage/DefaultHomePage.tsx +2 -1
  33. package/src/components/HomePage/NavigationCardBinding.tsx +3 -1
  34. package/src/components/SelectableTable/SelectableTable.tsx +140 -143
  35. package/src/components/VirtualTable/VirtualTable.tsx +8 -30
  36. package/src/components/VirtualTable/VirtualTableProps.tsx +0 -5
  37. package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -1
  38. package/src/components/index.tsx +2 -0
  39. package/src/core/EntityEditView.tsx +26 -14
  40. package/src/core/EntitySidePanel.tsx +15 -20
  41. package/src/core/FireCMS.tsx +7 -1
  42. package/src/form/EntityForm.tsx +1 -1
  43. package/src/form/PropertyFieldBinding.tsx +0 -1
  44. package/src/form/components/CustomIdField.tsx +3 -1
  45. package/src/form/components/LabelWithIcon.tsx +1 -1
  46. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
  47. package/src/form/field_bindings/MapFieldBinding.tsx +5 -4
  48. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -1
  49. package/src/form/field_bindings/RepeatFieldBinding.tsx +1 -0
  50. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
  51. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +222 -154
  52. package/src/hooks/data/save.ts +0 -6
  53. package/src/hooks/useBuildNavigationController.tsx +30 -16
  54. package/src/internal/useBuildSideEntityController.tsx +1 -1
  55. package/src/preview/components/ImagePreview.tsx +2 -2
  56. package/src/preview/components/ReferencePreview.tsx +1 -1
  57. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
  58. package/src/preview/property_previews/MapPropertyPreview.tsx +1 -1
  59. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +1 -1
  60. package/src/preview/property_previews/StringPropertyPreview.tsx +1 -1
  61. package/src/types/collections.ts +24 -2
  62. package/src/types/firecms.tsx +2 -2
  63. package/src/types/navigation.ts +6 -0
  64. package/src/types/plugins.tsx +16 -0
  65. package/src/types/side_entity_controller.tsx +5 -5
  66. package/src/util/callbacks.ts +119 -0
  67. package/src/util/index.ts +1 -0
  68. package/src/util/navigation_utils.ts +70 -55
  69. package/src/util/objects.ts +53 -20
  70. package/dist/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.d.ts +0 -5
  71. package/src/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.tsx +0 -59
@@ -9,6 +9,7 @@ import {
9
9
  EntityCollection,
10
10
  EntityCollectionsBuilder,
11
11
  EntityReference,
12
+ FireCMSPlugin,
12
13
  NavigationBlocker,
13
14
  NavigationController,
14
15
  PermissionsBuilder,
@@ -43,15 +44,7 @@ export type BuildNavigationContextProps<EC extends EntityCollection, USER extend
43
44
  viewsOrder?: string[];
44
45
  userConfigPersistence?: UserConfigurationPersistence;
45
46
  dataSourceDelegate: DataSourceDelegate;
46
- /**
47
- * Use this method to inject collections to the CMS.
48
- * You receive the current collections as a parameter, and you can return
49
- * a new list of collections.
50
- * @see {@link joinCollectionLists}
51
- * @param collections
52
- */
53
- injectCollections?: (collections: EntityCollection[]) => EntityCollection[];
54
-
47
+ plugins?: FireCMSPlugin[];
55
48
  /**
56
49
  * If true, the navigation logic will not be updated until this flag is false
57
50
  */
@@ -68,9 +61,9 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
68
61
  views: viewsProp,
69
62
  adminViews: adminViewsProp,
70
63
  viewsOrder,
64
+ plugins,
71
65
  userConfigPersistence,
72
66
  dataSourceDelegate,
73
- injectCollections,
74
67
  disabled
75
68
  } = props;
76
69
 
@@ -203,7 +196,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
203
196
  try {
204
197
 
205
198
  const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
206
- resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, injectCollections),
199
+ resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, plugins),
207
200
  resolveCMSViews(viewsProp, authController, dataSourceDelegate),
208
201
  resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
209
202
  ]
@@ -250,8 +243,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
250
243
  disabled,
251
244
  viewsProp,
252
245
  adminViewsProp,
253
- computeTopNavigation,
254
- injectCollections
246
+ computeTopNavigation
255
247
  ]);
256
248
 
257
249
  useEffect(() => {
@@ -421,6 +413,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
421
413
  getParentCollectionIds,
422
414
  convertIdsToPaths,
423
415
  navigate,
416
+ plugins
424
417
  };
425
418
  }
426
419
 
@@ -447,11 +440,24 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
447
440
  });
448
441
  }
449
442
 
443
+ function applyPluginModifyCollection(resolvedCollections: EntityCollection[], modifyCollection: (collection: EntityCollection) => EntityCollection) {
444
+ return resolvedCollections.map((collection: EntityCollection): EntityCollection => {
445
+ const modifiedCollection = modifyCollection(collection);
446
+ if (modifiedCollection.subcollections) {
447
+ return {
448
+ ...modifiedCollection,
449
+ subcollections: applyPluginModifyCollection(modifiedCollection.subcollections, modifyCollection)
450
+ } satisfies EntityCollection;
451
+ }
452
+ return modifiedCollection;
453
+ });
454
+ }
455
+
450
456
  async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
451
457
  collectionPermissions: PermissionsBuilder | undefined,
452
458
  authController: AuthController,
453
459
  dataSource: DataSourceDelegate,
454
- injectCollections?: (collections: EntityCollection[]) => EntityCollection[]): Promise<EntityCollection[]> {
460
+ plugins: FireCMSPlugin[] | undefined): Promise<EntityCollection[]> {
455
461
  let resolvedCollections: EntityCollection[] = [];
456
462
  if (typeof collections === "function") {
457
463
  resolvedCollections = await collections({
@@ -463,8 +469,16 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
463
469
  resolvedCollections = collections;
464
470
  }
465
471
 
466
- if (injectCollections) {
467
- resolvedCollections = injectCollections(resolvedCollections ?? []);
472
+ if (plugins) {
473
+ for (const plugin of plugins) {
474
+ if (plugin.collection?.modifyCollection) {
475
+ resolvedCollections = applyPluginModifyCollection(resolvedCollections, plugin.collection.modifyCollection);
476
+ }
477
+
478
+ if (plugin.collection?.injectCollections) {
479
+ resolvedCollections = plugin.collection.injectCollections(resolvedCollections ?? []);
480
+ }
481
+ }
468
482
  }
469
483
 
470
484
  resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
@@ -33,7 +33,7 @@ export function getEntityViewWidth(props: EntitySidePanelProps<any>, small: bool
33
33
  selectedSecondaryForm
34
34
  } = resolvedSelectedEntityView(props.collection?.entityViews, customizationController, props.selectedTab);
35
35
 
36
- const shouldUseSmallLayout = !props.selectedTab || props.selectedTab === JSON_TAB_VALUE || Boolean(selectedSecondaryForm);
36
+ const shouldUseSmallLayout = !props.selectedTab || props.selectedTab === JSON_TAB_VALUE || props.selectedTab === "__history" || Boolean(selectedSecondaryForm);
37
37
 
38
38
  let resolvedWidth: string | undefined;
39
39
  if (props.width) {
@@ -62,7 +62,7 @@ export function ImagePreview({
62
62
  title="Copy url to clipboard" side={"bottom"}>
63
63
  <IconButton
64
64
  variant={"filled"}
65
- size={"small"}
65
+ size={"smallest"}
66
66
  onClick={(e) => {
67
67
  e.stopPropagation();
68
68
  e.preventDefault();
@@ -81,7 +81,7 @@ export function ImagePreview({
81
81
  href={url}
82
82
  rel="noopener noreferrer"
83
83
  target="_blank"
84
- size={"small"}
84
+ size={"smallest"}
85
85
  onClick={(e: any) => e.stopPropagation()}
86
86
  >
87
87
  <OpenInNewIcon className={"text-surface-700 dark:text-surface-300"}
@@ -141,7 +141,7 @@ function ReferencePreviewExisting<M extends Record<string, any> = any>({
141
141
  );
142
142
  }
143
143
  return <EntityPreview size={size}
144
- previewProperties={previewProperties}
144
+ previewKeys={previewProperties}
145
145
  disabled={disabled}
146
146
  entity={usedEntity}
147
147
  collection={collection}
@@ -37,7 +37,7 @@ export function ArrayOfMapsPreview({
37
37
  throw Error(`You need to specify a 'properties' prop (or specify a custom field) in your map property ${propertyKey}`);
38
38
  }
39
39
  const values = value;
40
- const previewProperties: string[] | undefined = mapProperty.previewProperties;
40
+ const previewProperties: string[] | undefined = mapProperty.previewKeys;
41
41
 
42
42
  if (!values) return null;
43
43
 
@@ -128,7 +128,7 @@ export function KeyValuePreview({ value }: { value: any }) {
128
128
  </div>
129
129
  <div
130
130
  className="flex-grow max-w-[75%]">
131
- {childValue && <Typography>
131
+ {childValue && typeof childValue !== "object" && <Typography>
132
132
  <ErrorBoundary>
133
133
  {childValue.toString()}
134
134
  </ErrorBoundary>
@@ -46,7 +46,7 @@ export function SkeletonPropertyComponent({
46
46
  content = <>{arrayProperty.of.map((p, i) => renderGenericArrayCell(p, i))} </>;
47
47
  } else {
48
48
  if (arrayProperty.of.dataType === "map" && arrayProperty.of.properties) {
49
- content = renderArrayOfMaps(arrayProperty.of.properties, size, arrayProperty.of.previewProperties);
49
+ content = renderArrayOfMaps(arrayProperty.of.properties, size, arrayProperty.of.previewKeys);
50
50
  } else if (arrayProperty.of.dataType === "string") {
51
51
  if (arrayProperty.of.enumValues) {
52
52
  content = renderArrayEnumTableCell();
@@ -45,7 +45,7 @@ export function StringPropertyPreview({
45
45
  if (!value) return <></>;
46
46
  const lines = value.split("\n");
47
47
  return value && value.includes("\n")
48
- ? <div className={cls("overflow-x-scroll", size === "small" ? "text-sm" : "")}>
48
+ ? <div className={cls("overflow-x-scroll overflow-hidden", size === "small" ? "text-sm" : "")}>
49
49
  {lines.map((str, index) =>
50
50
  <React.Fragment key={`string_preview_${index}`}>
51
51
  <span>{str}</span>
@@ -112,7 +112,7 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
112
112
  * `hidden` in the property definition, will be ignored.
113
113
  * `propertiesOrder` has precedence over `hidden`.
114
114
  * - For properties use the property key.
115
- * - For additional columns use the column id.
115
+ * - For additional fields use the field key.
116
116
  * - If you have subcollections, you get a column for each subcollection,
117
117
  * with the path (or alias) as the subcollection, prefixed with
118
118
  * `subcollection:`. e.g. `subcollection:orders`.
@@ -127,7 +127,8 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
127
127
 
128
128
  /**
129
129
  * If enabled, content is loaded in batches. If `false` all entities in the
130
- * collection are loaded.
130
+ * collection are loaded. This means that when reaching the end of the
131
+ * collection, the CMS will load more entities.
131
132
  * You can specify a number to specify the pagination size (50 by default)
132
133
  * Defaults to `true`
133
134
  */
@@ -337,6 +338,12 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
337
338
  * If set to true, a tab including the JSON representation of the entity will be included.
338
339
  */
339
340
  includeJsonView?: boolean;
341
+
342
+ /**
343
+ * If set to true, changes to the entity will be saved in a subcollection.
344
+ * This prop has no effect if the history plugin is not enabled
345
+ */
346
+ history?: boolean;
340
347
  }
341
348
 
342
349
  /**
@@ -519,6 +526,11 @@ export type EntityCustomView<M extends Record<string, any> = any> =
519
526
  */
520
527
  name: string;
521
528
 
529
+ /**
530
+ * Render this custom view in the tab of the entity view, instead of the name
531
+ */
532
+ tabComponent?: React.ReactNode;
533
+
522
534
  /**
523
535
  * If set to true, the actions of the entity (save, discard,delete) will be
524
536
  * included in the view. By default the actions are located in the right or bottom,
@@ -531,6 +543,11 @@ export type EntityCustomView<M extends Record<string, any> = any> =
531
543
  * Builder for rendering the custom view
532
544
  */
533
545
  Builder?: React.ComponentType<EntityCustomViewParams<M>>;
546
+
547
+ /**
548
+ * Position of this tab in the entity view. Defaults to `end`.
549
+ */
550
+ position?: "start" | "end";
534
551
  };
535
552
 
536
553
  /**
@@ -561,6 +578,11 @@ export interface EntityCustomViewParams<M extends Record<string, any> = any> {
561
578
  * Use the form context to access the form state and methods
562
579
  */
563
580
  formContext: FormContext;
581
+
582
+ /**
583
+ * If this is a subcollection, this is the path of the parent collections
584
+ */
585
+ parentCollectionIds?: string[];
564
586
  }
565
587
 
566
588
  export type InferCollectionType<S extends EntityCollection> = S extends EntityCollection<infer M> ? M : never;
@@ -92,7 +92,6 @@ export type FireCMSProps<USER extends User> = {
92
92
  *
93
93
  * You can also define an entity view from the UI.
94
94
  */
95
-
96
95
  entityViews?: EntityCustomView[];
97
96
 
98
97
  /**
@@ -129,7 +128,8 @@ export type FireCMSProps<USER extends User> = {
129
128
 
130
129
  /**
131
130
  * Use plugins to modify the behaviour of the CMS.
132
- * Currently, in ALPHA, and likely subject to change.
131
+ * DEPRECATED: use the `plugins` prop in the `useBuildNavigationController` instead.
132
+ * This prop will work as a fallback for the `plugins` prop in the `useBuildNavigationController`.
133
133
  */
134
134
  plugins?: FireCMSPlugin<any, any, any>[];
135
135
 
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { EntityCollection } from "./collections";
3
3
  import { EntityReference } from "./entities";
4
+ import { FireCMSPlugin } from "./plugins";
4
5
 
5
6
  /**
6
7
  * Controller that includes the resolved navigation and utility methods and
@@ -152,6 +153,11 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
152
153
  */
153
154
  navigate: (to: string, options?: NavigateOptions) => void;
154
155
 
156
+ /**
157
+ * Plugin system allowing to extend the CMS functionality.
158
+ */
159
+ plugins?: FireCMSPlugin<any, any, any>[];
160
+
155
161
  }
156
162
 
157
163
  export interface NavigateOptions {
@@ -158,6 +158,22 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
158
158
  fieldBuilderEnabled?: <T extends CMSType = CMSType>(props: PluginFieldBuilderParams<T>) => boolean;
159
159
  }
160
160
 
161
+ collection?: {
162
+
163
+ /**
164
+ * Use this method to modify a single collection before it is rendered.
165
+ * @param collection
166
+ */
167
+ modifyCollection?: (collection: EntityCollection) => EntityCollection;
168
+
169
+ /**
170
+ * Use this method to modify, add or remove collections.
171
+ * @param collections
172
+ */
173
+ injectCollections?: (collections: EntityCollection[]) => EntityCollection[];
174
+
175
+ }
176
+
161
177
  }
162
178
 
163
179
  /**
@@ -20,11 +20,6 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
20
20
  */
21
21
  entityId?: string;
22
22
 
23
- // /**
24
- // * Navigation path with ids of the entity
25
- // */
26
- // fullIdPath: string;
27
-
28
23
  /**
29
24
  * Set this flag to true if you want to make a copy of an existing entity
30
25
  */
@@ -76,6 +71,11 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
76
71
  * Override some form properties
77
72
  */
78
73
  formProps?: Partial<EntityFormProps<M>>;
74
+
75
+ /**
76
+ * Allow the user to open the entity fullscreen
77
+ */
78
+ allowFullScreen?: boolean;
79
79
  }
80
80
 
81
81
  /**
@@ -0,0 +1,119 @@
1
+ import { EntityCallbacks } from "../types";
2
+
3
+ export const mergeCallbacks = (
4
+ baseCallbacks: EntityCallbacks = {},
5
+ pluginCallbacks: EntityCallbacks = {}
6
+ ): EntityCallbacks | undefined => {
7
+
8
+ if (!baseCallbacks && !pluginCallbacks) {
9
+ return undefined;
10
+ }
11
+
12
+ const mergedCallbacks: EntityCallbacks = {};
13
+
14
+ // Handle onFetch - returns Entity<M> or Promise<Entity<M>>
15
+ if (baseCallbacks.onFetch || pluginCallbacks.onFetch) {
16
+ mergedCallbacks.onFetch = async (props) => {
17
+ let entity = props.entity;
18
+ if (baseCallbacks.onFetch) {
19
+ entity = await Promise.resolve(baseCallbacks.onFetch(props));
20
+ }
21
+ if (pluginCallbacks.onFetch) {
22
+ entity = await Promise.resolve(pluginCallbacks.onFetch({
23
+ ...props,
24
+ entity
25
+ }));
26
+ }
27
+ return entity;
28
+ };
29
+ }
30
+
31
+ // Handle onSaveSuccess - returns void or Promise<void>
32
+ if (baseCallbacks.onSaveSuccess || pluginCallbacks.onSaveSuccess) {
33
+ mergedCallbacks.onSaveSuccess = async (props) => {
34
+ if (baseCallbacks.onSaveSuccess) {
35
+ await Promise.resolve(baseCallbacks.onSaveSuccess(props));
36
+ }
37
+ if (pluginCallbacks.onSaveSuccess) {
38
+ await Promise.resolve(pluginCallbacks.onSaveSuccess(props));
39
+ }
40
+ };
41
+ }
42
+
43
+ // Handle onSaveFailure - returns void or Promise<void>
44
+ if (baseCallbacks.onSaveFailure || pluginCallbacks.onSaveFailure) {
45
+ mergedCallbacks.onSaveFailure = async (props) => {
46
+ if (baseCallbacks.onSaveFailure) {
47
+ await Promise.resolve(baseCallbacks.onSaveFailure(props));
48
+ }
49
+ if (pluginCallbacks.onSaveFailure) {
50
+ await Promise.resolve(pluginCallbacks.onSaveFailure(props));
51
+ }
52
+ };
53
+ }
54
+
55
+ // Handle onPreSave - returns Partial<EntityValues<M>> or Promise<Partial<EntityValues<M>>>
56
+ if (baseCallbacks.onPreSave || pluginCallbacks.onPreSave) {
57
+ mergedCallbacks.onPreSave = async (props) => {
58
+ let values = { ...props.values };
59
+ if (baseCallbacks.onPreSave) {
60
+ const baseValues = await Promise.resolve(baseCallbacks.onPreSave(props));
61
+ values = { ...values, ...baseValues };
62
+ }
63
+ if (pluginCallbacks.onPreSave) {
64
+ const pluginValues = await Promise.resolve(pluginCallbacks.onPreSave({
65
+ ...props,
66
+ values
67
+ }));
68
+ values = { ...values, ...pluginValues };
69
+ }
70
+ return values;
71
+ };
72
+ }
73
+
74
+ // Handle onPreDelete - returns void
75
+ if (baseCallbacks.onPreDelete || pluginCallbacks.onPreDelete) {
76
+ mergedCallbacks.onPreDelete = (props) => {
77
+ if (baseCallbacks.onPreDelete) {
78
+ baseCallbacks.onPreDelete(props);
79
+ }
80
+ if (pluginCallbacks.onPreDelete) {
81
+ pluginCallbacks.onPreDelete(props);
82
+ }
83
+ };
84
+ }
85
+
86
+ // Handle onDelete - returns void
87
+ if (baseCallbacks.onDelete || pluginCallbacks.onDelete) {
88
+ mergedCallbacks.onDelete = (props) => {
89
+ if (baseCallbacks.onDelete) {
90
+ baseCallbacks.onDelete(props);
91
+ }
92
+ if (pluginCallbacks.onDelete) {
93
+ pluginCallbacks.onDelete(props);
94
+ }
95
+ };
96
+ }
97
+
98
+ // Handle onIdUpdate - returns string or Promise<string>
99
+ if (baseCallbacks.onIdUpdate || pluginCallbacks.onIdUpdate) {
100
+ mergedCallbacks.onIdUpdate = async (props) => {
101
+ let id = props.entityId || "";
102
+
103
+ if (baseCallbacks.onIdUpdate) {
104
+ id = await Promise.resolve(baseCallbacks.onIdUpdate(props));
105
+ }
106
+
107
+ if (pluginCallbacks.onIdUpdate) {
108
+ id = await Promise.resolve(pluginCallbacks.onIdUpdate({
109
+ ...props,
110
+ entityId: id
111
+ }));
112
+ }
113
+
114
+ return id;
115
+ };
116
+ }
117
+
118
+ return Object.keys(mergedCallbacks).length > 0 ? mergedCallbacks : undefined;
119
+ };
package/src/util/index.ts CHANGED
@@ -23,3 +23,4 @@ export * from "./join_collections";
23
23
  export * from "./builders";
24
24
  export * from "./useTraceUpdate";
25
25
  export * from "./storage";
26
+ export * from "./callbacks";
@@ -32,75 +32,90 @@ export function getLastSegment(path: string) {
32
32
  }
33
33
 
34
34
  export function resolveCollectionPathIds(path: string, allCollections: EntityCollection[]): string {
35
- const cleanPath = removeInitialAndTrailingSlashes(path);
36
- const subpaths = cleanPath.split("/");
37
-
38
- if (subpaths.length % 2 === 0) {
39
- throw Error(`resolveCollectionPathIds: Collection paths must have an odd number of segments: ${path}`);
40
- }
41
-
42
- // Check if the path exactly matches a collection path
43
- const exactMatch = allCollections.find(col => col.path === cleanPath);
44
- if (exactMatch) {
45
- return exactMatch.path;
35
+ let remainingPath = removeInitialAndTrailingSlashes(path);
36
+ if (!remainingPath) {
37
+ return "";
46
38
  }
47
39
 
48
- if (subpaths.length === 1) {
49
- // Find collection by ID and return its path
50
- const aliasedCollection = allCollections.find((col) => col.id === subpaths[0]);
51
- return aliasedCollection?.path ?? subpaths[0];
52
- }
40
+ let currentCollections: EntityCollection[] | undefined = allCollections;
41
+ const resolvedPathParts: string[] = [];
53
42
 
54
- // Try to match a multi-segment collection path
55
- let matchingCollection: EntityCollection | undefined;
56
- let entityIndex = 1;
57
-
58
- // Check if the path starts with a multi-segment collection path
59
- for (const collection of allCollections) {
60
- const pathSegments = collection.path.split("/");
61
- if (pathSegments.length > 1 &&
62
- subpaths.slice(0, pathSegments.length).join("/") === collection.path) {
63
- matchingCollection = collection;
64
- entityIndex = pathSegments.length;
43
+ while (remainingPath.length > 0) {
44
+ if (!currentCollections || currentCollections.length === 0) {
45
+ // We have remaining path segments but no more collections to match against
46
+ console.warn(`resolveCollectionPathIds: Path structure implies subcollections, but none found before segment starting with "${remainingPath}" in original path "${path}". Appending remaining original path.`);
47
+ resolvedPathParts.push(remainingPath);
48
+ remainingPath = ""; // Stop processing
65
49
  break;
66
50
  }
67
- }
68
-
69
- // If no multi-segment match, fall back to single segment matching
70
- if (!matchingCollection) {
71
- const matchingCollections = allCollections.filter(col =>
72
- col.id === subpaths[0] || col.path === subpaths[0]
73
- );
74
51
 
75
- if (!matchingCollections.length) {
76
- return cleanPath;
77
- }
52
+ let foundMatch = false;
53
+ // Sort potential matches by length descending to prioritize longer matches (e.g., "a/b" over "a")
54
+ const potentialMatches: { col: EntityCollection; match: string; }[] = currentCollections
55
+ .flatMap(col => [{
56
+ col,
57
+ match: col.path
58
+ }, {
59
+ col,
60
+ match: col.id
61
+ }])
62
+ .filter(p => p.match && remainingPath.startsWith(p.match))
63
+ .sort((a, b) => b.match.length - a.match.length);
64
+
65
+ if (potentialMatches.length > 0) {
66
+ const {
67
+ col: foundCollection,
68
+ match: matchString
69
+ } = potentialMatches[0];
70
+
71
+ resolvedPathParts.push(foundCollection.path); // Use the defined path
72
+ remainingPath = removeInitialSlash(remainingPath.substring(matchString.length));
73
+
74
+ // Check if we are at the end of the path
75
+ if (remainingPath.length === 0) {
76
+ foundMatch = true;
77
+ break; // Path ends with a collection segment
78
+ }
78
79
 
79
- matchingCollection = matchingCollections[0];
80
- }
80
+ // The next segment must be an entity ID
81
+ const idSeparatorIndex = remainingPath.indexOf("/");
82
+ let entityId: string;
83
+ if (idSeparatorIndex > -1) {
84
+ entityId = remainingPath.substring(0, idSeparatorIndex);
85
+ remainingPath = remainingPath.substring(idSeparatorIndex + 1);
86
+ } else {
87
+ // This should not happen if the original path is valid (odd segments)
88
+ // but handle it defensively: assume the rest is the ID
89
+ entityId = remainingPath;
90
+ remainingPath = "";
91
+ console.warn(`resolveCollectionPathIds: Path seems to end with an entity ID "${entityId}" instead of a collection segment in original path "${path}". This might indicate an invalid input path.`);
92
+ // Even if it ends here, we still need to push the ID
93
+ }
81
94
 
82
- const entityId = subpaths[entityIndex];
83
- const remainingPath = subpaths.slice(entityIndex + 1);
95
+ resolvedPathParts.push(entityId); // Append entity ID
96
+ currentCollections = foundCollection.subcollections; // Move to subcollections
97
+ foundMatch = true;
84
98
 
85
- // If we have a subcollection ID, try to resolve it
86
- if (remainingPath.length > 0) {
87
- const subcollectionId = remainingPath[0];
88
- const subcollection = matchingCollection.subcollections?.find(
89
- subcol => subcol.id === subcollectionId
90
- );
99
+ if (!currentCollections && remainingPath.length > 0) {
100
+ // Warn if the path continues but no subcollections were defined
101
+ console.warn(`resolveCollectionPathIds: Path continues after entity ID "${entityId}", but no subcollections are defined for the preceding collection "${foundCollection.path}" in path "${path}". Appending remaining original path.`);
102
+ resolvedPathParts.push(remainingPath); // Append the rest
103
+ remainingPath = ""; // Stop processing
104
+ break;
105
+ }
91
106
 
92
- if (subcollection) {
93
- return `${matchingCollection.path}/${entityId}/${subcollection.path}`;
94
107
  }
95
- }
96
108
 
97
- // If there are no remaining path segments, just return the collection path with entity ID
98
- if (remainingPath.length === 0) {
99
- return `${matchingCollection.path}/${entityId}`;
109
+ if (!foundMatch) {
110
+ // Collection definition not found for the start of the remaining path
111
+ console.warn(`resolveCollectionPathIds: Collection definition not found for segment starting with "${remainingPath}" in original path "${path}". Appending remaining original path.`);
112
+ resolvedPathParts.push(remainingPath); // Append the rest
113
+ remainingPath = ""; // Stop processing
114
+ break;
115
+ }
100
116
  }
101
117
 
102
- // Default case - couldn't match subcollection
103
- return `${matchingCollection.path}/${entityId}/${remainingPath.join("/")}`;
118
+ return resolvedPathParts.join("/");
104
119
  }
105
120
 
106
121
  /**