@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.
- package/dist/components/ArrayContainer.d.ts +7 -12
- package/dist/components/EntityPreview.d.ts +4 -2
- package/dist/components/SelectableTable/SelectableTable.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +0 -4
- package/dist/components/index.d.ts +1 -0
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +3 -9
- package/dist/hooks/useBuildNavigationController.d.ts +2 -9
- package/dist/index.es.js +1183 -705
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1191 -716
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +20 -2
- package/dist/types/firecms.d.ts +2 -1
- package/dist/types/navigation.d.ts +5 -0
- package/dist/types/plugins.d.ts +12 -0
- package/dist/types/side_entity_controller.d.ts +4 -0
- package/dist/util/callbacks.d.ts +2 -0
- package/dist/util/index.d.ts +1 -0
- package/package.json +8 -6
- package/src/components/ArrayContainer.tsx +409 -294
- package/src/components/ClearFilterSortButton.tsx +1 -1
- package/src/components/ConfirmationDialog.tsx +9 -9
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +13 -2
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +53 -27
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +9 -9
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +4 -0
- package/src/components/EntityPreview.tsx +18 -14
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/HomePage/DefaultHomePage.tsx +2 -1
- package/src/components/HomePage/NavigationCardBinding.tsx +3 -1
- package/src/components/SelectableTable/SelectableTable.tsx +140 -143
- package/src/components/VirtualTable/VirtualTable.tsx +8 -30
- package/src/components/VirtualTable/VirtualTableProps.tsx +0 -5
- package/src/components/VirtualTable/fields/VirtualTableInput.tsx +0 -1
- package/src/components/index.tsx +2 -0
- package/src/core/EntityEditView.tsx +26 -14
- package/src/core/EntitySidePanel.tsx +15 -20
- package/src/core/FireCMS.tsx +7 -1
- package/src/form/EntityForm.tsx +1 -1
- package/src/form/PropertyFieldBinding.tsx +0 -1
- package/src/form/components/CustomIdField.tsx +3 -1
- package/src/form/components/LabelWithIcon.tsx +1 -1
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +5 -4
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +1 -1
- package/src/form/field_bindings/RepeatFieldBinding.tsx +1 -0
- package/src/form/field_bindings/SelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +222 -154
- package/src/hooks/data/save.ts +0 -6
- package/src/hooks/useBuildNavigationController.tsx +30 -16
- package/src/internal/useBuildSideEntityController.tsx +1 -1
- package/src/preview/components/ImagePreview.tsx +2 -2
- package/src/preview/components/ReferencePreview.tsx +1 -1
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +1 -1
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +1 -1
- package/src/preview/property_previews/StringPropertyPreview.tsx +1 -1
- package/src/types/collections.ts +24 -2
- package/src/types/firecms.tsx +2 -2
- package/src/types/navigation.ts +6 -0
- package/src/types/plugins.tsx +16 -0
- package/src/types/side_entity_controller.tsx +5 -5
- package/src/util/callbacks.ts +119 -0
- package/src/util/index.ts +1 -0
- package/src/util/navigation_utils.ts +70 -55
- package/src/util/objects.ts +53 -20
- package/dist/components/EntityCollectionTable/internal/popup_field/ElementResizeListener.d.ts +0 -5
- 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,
|
|
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
|
-
|
|
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 (
|
|
467
|
-
|
|
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={"
|
|
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={"
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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>
|
package/src/types/collections.ts
CHANGED
|
@@ -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
|
|
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;
|
package/src/types/firecms.tsx
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
package/src/types/navigation.ts
CHANGED
|
@@ -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 {
|
package/src/types/plugins.tsx
CHANGED
|
@@ -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
|
@@ -32,75 +32,90 @@ export function getLastSegment(path: string) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function resolveCollectionPathIds(path: string, allCollections: EntityCollection[]): string {
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
95
|
+
resolvedPathParts.push(entityId); // Append entity ID
|
|
96
|
+
currentCollections = foundCollection.subcollections; // Move to subcollections
|
|
97
|
+
foundMatch = true;
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
return `${matchingCollection.path}/${entityId}/${remainingPath.join("/")}`;
|
|
118
|
+
return resolvedPathParts.join("/");
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
/**
|