@firecms/core 3.4.0-canary.0ef7442 → 3.4.0-canary.1faa2a9
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/README.md +4 -0
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- package/dist/components/ErrorBoundary.d.ts +1 -3
- package/dist/components/LicenseBanner.d.ts +23 -0
- package/dist/components/common/useDataSourceTableController.d.ts +4 -2
- package/dist/components/common/useTableSearchHelper.d.ts +3 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/contexts/LicenseStatusContext.d.ts +16 -0
- package/dist/core/EntityEditView.d.ts +2 -0
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/plugin_translations.d.ts +38 -0
- package/dist/core/pro_plugins.d.ts +40 -0
- package/dist/de-KtBROUvS.js +700 -0
- package/dist/de-KtBROUvS.js.map +1 -0
- package/dist/editor/extensions/Image.d.ts +11 -0
- package/dist/editor/index.d.ts +2 -1
- package/dist/editor/lazy_editor.d.ts +6 -0
- package/dist/editor-BSo_aS1I.js +4563 -0
- package/dist/editor-BSo_aS1I.js.map +1 -0
- package/dist/form/EntityForm.d.ts +2 -0
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/fr-BRFSBaVh.js +700 -0
- package/dist/fr-BRFSBaVh.js.map +1 -0
- package/dist/hi-DwCkga5X.js +700 -0
- package/dist/hi-DwCkga5X.js.map +1 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +1 -1
- package/dist/hooks/data/useEntityFetch.d.ts +4 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useLicenseStatus.d.ts +16 -0
- package/dist/hooks/useProjectLog.d.ts +11 -6
- package/dist/hooks/useTranslation.d.ts +1 -1
- package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
- package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
- package/dist/index.es.js +3503 -11244
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +8028 -7006
- package/dist/index.umd.js.map +1 -1
- package/dist/it-B2YGgDsx.js +700 -0
- package/dist/it-B2YGgDsx.js.map +1 -0
- package/dist/pl-BxHkywK8.js +702 -0
- package/dist/pl-BxHkywK8.js.map +1 -0
- package/dist/pt-q6UV4diT.js +702 -0
- package/dist/pt-q6UV4diT.js.map +1 -0
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +7 -0
- package/dist/types/datasource.d.ts +8 -0
- package/dist/types/entity_actions.d.ts +6 -0
- package/dist/types/entity_callbacks.d.ts +21 -0
- package/dist/types/firecms.d.ts +18 -1
- package/dist/types/firecms_context.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/license.d.ts +54 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +22 -1
- package/dist/types/side_entity_controller.d.ts +3 -2
- package/dist/types/translations.d.ts +31 -2
- package/dist/util/date_fns_locales.d.ts +18 -0
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/lazy_eager.d.ts +3 -3
- package/dist/util/navigation_utils.d.ts +84 -2
- package/dist/util/parent_references_from_path.d.ts +15 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- package/package.json +3 -3
- package/src/app/Scaffold.tsx +7 -0
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +8 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +40 -15
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +7 -2
- package/src/components/EntityPreview.tsx +4 -1
- package/src/components/ErrorBoundary.tsx +12 -18
- package/src/components/LicenseBanner.tsx +209 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +8 -2
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/useDataSourceTableController.tsx +26 -8
- package/src/components/common/useTableSearchHelper.ts +5 -0
- package/src/components/index.tsx +1 -0
- package/src/contexts/LicenseStatusContext.tsx +50 -0
- package/src/core/DefaultAppBar.tsx +28 -16
- package/src/core/EntityEditView.tsx +8 -12
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +6 -4
- package/src/core/FireCMS.tsx +73 -47
- package/src/core/SideDialogs.tsx +44 -1
- package/src/core/index.tsx +1 -0
- package/src/core/plugin_translations.ts +87 -0
- package/src/core/pro_plugins.ts +79 -0
- package/src/editor/components/SlashCommandMenu.tsx +4 -1
- package/src/editor/extensions/Image.ts +17 -1
- package/src/editor/hooks/useProseMirror.ts +5 -0
- package/src/editor/index.ts +11 -1
- package/src/editor/lazy_editor.tsx +33 -0
- package/src/form/EntityForm.tsx +22 -7
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/hooks/data/delete.ts +8 -2
- package/src/hooks/data/save.ts +4 -1
- package/src/hooks/data/useCollectionFetch.tsx +9 -2
- package/src/hooks/data/useEntityFetch.tsx +19 -6
- package/src/hooks/index.tsx +2 -0
- package/src/hooks/useBuildNavigationController.tsx +70 -15
- package/src/hooks/useFireCMSContext.tsx +7 -3
- package/src/hooks/useLicenseStatus.tsx +32 -0
- package/src/hooks/useProjectLog.tsx +27 -9
- package/src/hooks/useResolvedNavigationFrom.tsx +1 -1
- package/src/hooks/useTranslation.ts +3 -1
- package/src/i18n/FireCMSi18nProvider.tsx +199 -25
- package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
- package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
- package/src/internal/useBuildDataSource.ts +13 -12
- package/src/internal/useBuildSideEntityController.tsx +13 -5
- package/src/locales/de.ts +16 -2
- package/src/locales/en.ts +18 -2
- package/src/locales/es.ts +16 -2
- package/src/locales/fr.ts +16 -2
- package/src/locales/hi.ts +16 -2
- package/src/locales/it.ts +16 -2
- package/src/locales/pl.ts +16 -2
- package/src/locales/pt.ts +16 -2
- package/src/preview/components/DatePreview.tsx +2 -4
- package/src/preview/components/ReferencePreview.tsx +7 -3
- package/src/routes/FireCMSRoute.tsx +9 -4
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +8 -0
- package/src/types/datasource.ts +8 -0
- package/src/types/entity_actions.tsx +6 -0
- package/src/types/entity_callbacks.ts +24 -0
- package/src/types/firecms.tsx +19 -1
- package/src/types/firecms_context.tsx +9 -0
- package/src/types/index.ts +1 -0
- package/src/types/license.ts +62 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +23 -1
- package/src/types/side_entity_controller.tsx +3 -2
- package/src/types/translations.ts +31 -2
- package/src/util/date_fns_locales.ts +168 -0
- package/src/util/entity_cache.ts +18 -0
- package/src/util/index.ts +1 -0
- package/src/util/lazy_eager.tsx +30 -8
- package/src/util/navigation_from_path.ts +6 -1
- package/src/util/navigation_utils.ts +204 -2
- package/src/util/parent_references_from_path.ts +32 -1
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
- package/src/util/useStorageUploadController.tsx +6 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
import { CircularProgressCenter } from "../components/CircularProgressCenter";
|
|
6
|
+
import type { FireCMSEditorProps } from "./editor";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `FireCMSEditor`, loaded on demand.
|
|
10
|
+
*
|
|
11
|
+
* The editor is built on ProseMirror — roughly 250KB across a dozen packages —
|
|
12
|
+
* and is only reachable from a markdown field, which most views never render.
|
|
13
|
+
* `MarkdownEditorFieldBinding` already loaded it lazily, but that split was
|
|
14
|
+
* defeated by `src/index.ts` re-exporting the editor from the package barrel:
|
|
15
|
+
* a static re-export puts the module back in the entry graph regardless of who
|
|
16
|
+
* imports it dynamically. So ProseMirror sat in `@firecms/core` for everyone.
|
|
17
|
+
*
|
|
18
|
+
* The name and props are unchanged, and this component carries its own Suspense
|
|
19
|
+
* boundary, so it stays a drop-in for the synchronous component it replaces —
|
|
20
|
+
* a consumer rendering it without a boundary of their own still works.
|
|
21
|
+
*/
|
|
22
|
+
const LazyFireCMSEditor = React.lazy(() =>
|
|
23
|
+
import("./editor").then((module) => ({ default: module.FireCMSEditor })));
|
|
24
|
+
|
|
25
|
+
export function FireCMSEditor(props: FireCMSEditorProps) {
|
|
26
|
+
return (
|
|
27
|
+
<React.Suspense fallback={<CircularProgressCenter/>}>
|
|
28
|
+
<LazyFireCMSEditor {...props}/>
|
|
29
|
+
</React.Suspense>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
FireCMSEditor.displayName = "FireCMSEditor";
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -48,6 +48,7 @@ import { useAnalyticsController } from "../hooks/useAnalyticsController";
|
|
|
48
48
|
import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
|
|
49
49
|
import { ValidationError } from "yup";
|
|
50
50
|
import {
|
|
51
|
+
entityCacheKey,
|
|
51
52
|
flattenKeys,
|
|
52
53
|
getEntityFromCache,
|
|
53
54
|
removeEntityFromCache,
|
|
@@ -64,6 +65,8 @@ export type OnUpdateParams = {
|
|
|
64
65
|
entity: Entity<any>,
|
|
65
66
|
status: EntityStatus,
|
|
66
67
|
path: string,
|
|
68
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
69
|
+
pathSegments?: string[],
|
|
67
70
|
entityId?: string;
|
|
68
71
|
selectedTab?: string;
|
|
69
72
|
collection: EntityCollection<any>
|
|
@@ -276,6 +279,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
276
279
|
entityId: entityIdProp,
|
|
277
280
|
collection,
|
|
278
281
|
path,
|
|
282
|
+
pathSegments,
|
|
279
283
|
values: valuesToBeSaved
|
|
280
284
|
});
|
|
281
285
|
}, false, 2000);
|
|
@@ -314,7 +318,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
314
318
|
const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [authController, collection, path, status, entity, customizationController.propertyConfigs]);
|
|
315
319
|
|
|
316
320
|
const localChangesDataRaw = useMemo(() => entityId
|
|
317
|
-
? getEntityFromCache(path
|
|
321
|
+
? getEntityFromCache(entityCacheKey(path, entityId))
|
|
318
322
|
: getEntityFromCache(path + "#new"), [entityId, path]);
|
|
319
323
|
|
|
320
324
|
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
@@ -393,7 +397,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
393
397
|
onValuesModified?.(false, initialValues as M);
|
|
394
398
|
},
|
|
395
399
|
onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
|
|
396
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path
|
|
400
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
397
401
|
if (controller.dirty && localChangesBackup !== false) {
|
|
398
402
|
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
399
403
|
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
@@ -474,8 +478,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
474
478
|
removeEntityFromMemoryCache(path + "#new");
|
|
475
479
|
removeEntityFromCache(path + "#new");
|
|
476
480
|
} else {
|
|
477
|
-
removeEntityFromMemoryCache(path
|
|
478
|
-
removeEntityFromCache(path
|
|
481
|
+
removeEntityFromMemoryCache(entityCacheKey(path, entityId));
|
|
482
|
+
removeEntityFromCache(entityCacheKey(path, entityId));
|
|
479
483
|
}
|
|
480
484
|
}
|
|
481
485
|
|
|
@@ -497,6 +501,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
497
501
|
entity: updatedEntity,
|
|
498
502
|
status,
|
|
499
503
|
path,
|
|
504
|
+
pathSegments,
|
|
500
505
|
entityId: updatedEntity.id,
|
|
501
506
|
collection
|
|
502
507
|
});
|
|
@@ -517,16 +522,20 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
517
522
|
previousValues,
|
|
518
523
|
entityId,
|
|
519
524
|
collection,
|
|
520
|
-
path
|
|
525
|
+
path,
|
|
526
|
+
pathSegments
|
|
521
527
|
}: {
|
|
522
528
|
collection: EntityCollection<M>,
|
|
523
529
|
path: string,
|
|
530
|
+
/** `path` split at its real segment boundaries, travelling with the `path` above. */
|
|
531
|
+
pathSegments?: string[],
|
|
524
532
|
entityId: string | undefined,
|
|
525
533
|
values: M,
|
|
526
534
|
previousValues?: M,
|
|
527
535
|
}) => {
|
|
528
536
|
return saveEntityWithCallbacks({
|
|
529
537
|
path,
|
|
538
|
+
pathSegments,
|
|
530
539
|
entityId,
|
|
531
540
|
values,
|
|
532
541
|
previousValues,
|
|
@@ -544,6 +553,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
544
553
|
type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
545
554
|
collection: ResolvedEntityCollection<M>,
|
|
546
555
|
path: string,
|
|
556
|
+
pathSegments?: string[],
|
|
547
557
|
entityId: string | undefined,
|
|
548
558
|
values: EntityValues<M>,
|
|
549
559
|
previousValues?: EntityValues<M>,
|
|
@@ -553,6 +563,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
553
563
|
const onSaveEntityRequest = async ({
|
|
554
564
|
collection,
|
|
555
565
|
path,
|
|
566
|
+
pathSegments,
|
|
556
567
|
entityId,
|
|
557
568
|
values,
|
|
558
569
|
previousValues,
|
|
@@ -566,6 +577,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
566
577
|
return saveEntity({
|
|
567
578
|
collection,
|
|
568
579
|
path,
|
|
580
|
+
pathSegments,
|
|
569
581
|
entityId,
|
|
570
582
|
values,
|
|
571
583
|
previousValues
|
|
@@ -579,6 +591,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
579
591
|
return onSaveEntityRequest({
|
|
580
592
|
collection: resolvedCollection,
|
|
581
593
|
path,
|
|
594
|
+
pathSegments,
|
|
582
595
|
entityId,
|
|
583
596
|
values,
|
|
584
597
|
previousValues: entity?.values,
|
|
@@ -629,13 +642,14 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
629
642
|
const plugins = customizationController.plugins;
|
|
630
643
|
|
|
631
644
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
632
|
-
const parentCollectionIds = navigationController.getParentCollectionIds(path);
|
|
645
|
+
const parentCollectionIds = navigationController.getParentCollectionIds(path, pathSegments);
|
|
633
646
|
|
|
634
647
|
if (plugins && collection) {
|
|
635
648
|
const actionProps: PluginFormActionProps = {
|
|
636
649
|
entityId,
|
|
637
650
|
parentCollectionIds,
|
|
638
651
|
path,
|
|
652
|
+
pathSegments,
|
|
639
653
|
status,
|
|
640
654
|
collection,
|
|
641
655
|
context,
|
|
@@ -896,6 +910,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
896
910
|
const dialogActions = <EntityFormActionsComponent
|
|
897
911
|
collection={resolvedCollection}
|
|
898
912
|
path={path}
|
|
913
|
+
pathSegments={pathSegments}
|
|
899
914
|
fullPath={path}
|
|
900
915
|
fullIdPath={fullIdPath}
|
|
901
916
|
entity={entity}
|
|
@@ -930,7 +945,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
930
945
|
|
|
931
946
|
{manualApplyLocalChanges && hasLocalChanges &&
|
|
932
947
|
<LocalChangesMenu
|
|
933
|
-
cacheKey={status === "new" || status === "copy" ? path + "#new" : path
|
|
948
|
+
cacheKey={status === "new" || status === "copy" ? path + "#new" : entityCacheKey(path, entityId)}
|
|
934
949
|
properties={resolvedCollection.properties}
|
|
935
950
|
cachedData={localChangesDataRaw as Partial<M>}
|
|
936
951
|
formex={formex}
|
|
@@ -26,6 +26,8 @@ export interface EntityFormActionsProps {
|
|
|
26
26
|
fullIdPath?: string;
|
|
27
27
|
collection: ResolvedEntityCollection;
|
|
28
28
|
path: string;
|
|
29
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
30
|
+
pathSegments?: string[];
|
|
29
31
|
entity?: Entity;
|
|
30
32
|
layout: "bottom" | "side";
|
|
31
33
|
savingError?: Error;
|
package/src/hooks/data/delete.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type DeleteEntityWithCallbacksProps<M extends Record<string, any>, USER e
|
|
|
46
46
|
export async function deleteEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
|
|
47
47
|
dataSource,
|
|
48
48
|
entity,
|
|
49
|
+
pathSegments,
|
|
49
50
|
collection,
|
|
50
51
|
callbacks,
|
|
51
52
|
onDeleteSuccess,
|
|
@@ -62,11 +63,17 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
62
63
|
|
|
63
64
|
console.debug("Deleting entity", entity.path, entity.id);
|
|
64
65
|
|
|
66
|
+
// An explicit `pathSegments` wins, then the ones the entity was loaded with. The prop
|
|
67
|
+
// used to be accepted and silently ignored, which is the same class of bug this field
|
|
68
|
+
// exists to prevent.
|
|
69
|
+
const resolvedPathSegments = pathSegments ?? entity.pathSegments;
|
|
70
|
+
|
|
65
71
|
const entityDeleteProps: EntityOnDeleteProps<M, any> = {
|
|
66
72
|
entity,
|
|
67
73
|
collection,
|
|
68
74
|
entityId: entity.id,
|
|
69
75
|
path: entity.path,
|
|
76
|
+
pathSegments: resolvedPathSegments,
|
|
70
77
|
context
|
|
71
78
|
};
|
|
72
79
|
|
|
@@ -81,8 +88,7 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
return dataSource.deleteEntity({
|
|
84
|
-
|
|
85
|
-
pathSegments: entity.pathSegments,
|
|
91
|
+
pathSegments: resolvedPathSegments,
|
|
86
92
|
entity,
|
|
87
93
|
collection
|
|
88
94
|
}).then(() => {
|
package/src/hooks/data/save.ts
CHANGED
|
@@ -68,7 +68,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
68
68
|
|
|
69
69
|
const customizationController = context.customizationController;
|
|
70
70
|
|
|
71
|
-
const resolvedPath = context.navigation.resolveIdsFrom(path);
|
|
71
|
+
const resolvedPath = context.navigation.resolveIdsFrom(path, pathSegments);
|
|
72
72
|
|
|
73
73
|
const callbacks = collection.callbacks;
|
|
74
74
|
if (callbacks?.onPreSave) {
|
|
@@ -84,6 +84,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
84
84
|
updatedValues = await callbacks.onPreSave({
|
|
85
85
|
collection: resolvedCollection,
|
|
86
86
|
path,
|
|
87
|
+
pathSegments,
|
|
87
88
|
resolvedPath,
|
|
88
89
|
entityId,
|
|
89
90
|
values,
|
|
@@ -123,6 +124,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
123
124
|
callbacks.onSaveSuccess({
|
|
124
125
|
collection: resolvedCollection,
|
|
125
126
|
path,
|
|
127
|
+
pathSegments,
|
|
126
128
|
resolvedPath,
|
|
127
129
|
entityId: entity.id,
|
|
128
130
|
values: updatedValues,
|
|
@@ -152,6 +154,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
152
154
|
callbacks.onSaveFailure({
|
|
153
155
|
collection: resolvedCollection,
|
|
154
156
|
path,
|
|
157
|
+
pathSegments,
|
|
155
158
|
resolvedPath,
|
|
156
159
|
entityId,
|
|
157
160
|
values: updatedValues,
|
|
@@ -69,7 +69,7 @@ export interface CollectionFetchResult<M extends Record<string, any>> {
|
|
|
69
69
|
export function useCollectionFetch<M extends Record<string, any>, USER extends User>(
|
|
70
70
|
{
|
|
71
71
|
path: inputPath,
|
|
72
|
-
pathSegments,
|
|
72
|
+
pathSegments: inputPathSegments,
|
|
73
73
|
collection,
|
|
74
74
|
filterValues,
|
|
75
75
|
sortBy,
|
|
@@ -80,7 +80,13 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
80
80
|
const dataSource = useDataSource(collection);
|
|
81
81
|
const navigationController = useNavigationController();
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
// Resolved together so the two representations always describe the same chain.
|
|
84
|
+
// Segments are resolved, never derived: only collection ids are swapped for their
|
|
85
|
+
// real paths, and entity ids are carried through whole (see useEntityFetch).
|
|
86
|
+
const pathSegments = inputPathSegments
|
|
87
|
+
? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
|
|
88
|
+
: undefined;
|
|
89
|
+
const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
|
|
84
90
|
|
|
85
91
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
86
92
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
@@ -109,6 +115,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
109
115
|
collection.callbacks!.onFetch!({
|
|
110
116
|
collection,
|
|
111
117
|
path,
|
|
118
|
+
pathSegments,
|
|
112
119
|
entity,
|
|
113
120
|
context
|
|
114
121
|
})));
|
|
@@ -3,6 +3,7 @@ import { Entity, EntityCollection, FireCMSContext, User } from "../../types";
|
|
|
3
3
|
import { useDataSource } from "./useDataSource";
|
|
4
4
|
import { useNavigationController } from "../useNavigationController";
|
|
5
5
|
import { useFireCMSContext } from "../useFireCMSContext";
|
|
6
|
+
import { entityCacheKey } from "../../util/entity_cache";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @group Hooks and utilities
|
|
@@ -11,7 +12,10 @@ export interface EntityFetchProps<M extends Record<string, any>, USER extends Us
|
|
|
11
12
|
path: string;
|
|
12
13
|
/**
|
|
13
14
|
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
14
|
-
* entity id containing "/" stays unambiguous.
|
|
15
|
+
* entity id containing "/" stays unambiguous.
|
|
16
|
+
*
|
|
17
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
18
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
15
19
|
*/
|
|
16
20
|
pathSegments?: string[];
|
|
17
21
|
entityId?: string;
|
|
@@ -54,11 +58,19 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
54
58
|
const dataSource = useDataSource(collection);
|
|
55
59
|
const navigationController = useNavigationController();
|
|
56
60
|
|
|
57
|
-
const path = navigationController.resolveIdsFrom(inputPath);
|
|
58
61
|
// Never fabricate segments. If the caller did not thread them, pass undefined so a
|
|
59
62
|
// delegate can tell "not provided" from a real answer — splitting `path` here would
|
|
60
63
|
// produce a confidently wrong array for any id containing "/".
|
|
61
|
-
|
|
64
|
+
//
|
|
65
|
+
// When they were threaded, both representations are resolved from them together, so
|
|
66
|
+
// `path` cannot end up describing a different chain than `pathSegments`.
|
|
67
|
+
// `resolveSegmentsFrom` is optional on the controller: a host app may be supplying one
|
|
68
|
+
// built against an earlier version. Falling back to the segments as given is what
|
|
69
|
+
// happened before it existed — they are used unresolved, never invented.
|
|
70
|
+
const pathSegments = inputPathSegments
|
|
71
|
+
? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
|
|
72
|
+
: undefined;
|
|
73
|
+
const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
|
|
62
74
|
|
|
63
75
|
const context: FireCMSContext<USER> = useFireCMSContext();
|
|
64
76
|
|
|
@@ -82,6 +94,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
82
94
|
updatedEntity = await collection.callbacks.onFetch({
|
|
83
95
|
collection,
|
|
84
96
|
path,
|
|
97
|
+
pathSegments,
|
|
85
98
|
entity: updatedEntity,
|
|
86
99
|
context
|
|
87
100
|
});
|
|
@@ -89,7 +102,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
89
102
|
console.error(e);
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
|
-
CACHE[
|
|
105
|
+
CACHE[entityCacheKey(path, entityId)] = updatedEntity;
|
|
93
106
|
setEntity(updatedEntity);
|
|
94
107
|
setDataLoading(false);
|
|
95
108
|
setDataLoadingError(undefined);
|
|
@@ -102,8 +115,8 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
102
115
|
setDataLoadingError(error);
|
|
103
116
|
};
|
|
104
117
|
|
|
105
|
-
if (entityId && useCache && CACHE[
|
|
106
|
-
setEntity(CACHE[
|
|
118
|
+
if (entityId && useCache && CACHE[entityCacheKey(path, entityId)]) {
|
|
119
|
+
setEntity(CACHE[entityCacheKey(path, entityId)]);
|
|
107
120
|
setDataLoading(false);
|
|
108
121
|
setDataLoadingError(undefined);
|
|
109
122
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
package/src/hooks/index.tsx
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./useDialogsController";
|
|
|
14
14
|
export * from "./useSideDialogsController";
|
|
15
15
|
export * from "./useSideEntityController";
|
|
16
16
|
export * from "./useFireCMSContext";
|
|
17
|
+
export * from "./useAnalyticsController";
|
|
17
18
|
export * from "./useSnackbarController";
|
|
18
19
|
export * from "./useModeController";
|
|
19
20
|
export * from "./useClipboard";
|
|
@@ -32,3 +33,4 @@ export * from "./useBuildModeController";
|
|
|
32
33
|
|
|
33
34
|
export * from "./useValidateAuthenticator";
|
|
34
35
|
export * from "./useTranslation";
|
|
36
|
+
export * from "./useLicenseStatus";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import equal from "react-fast-compare"
|
|
3
3
|
import { useBlocker, useNavigate } from "react-router-dom";
|
|
4
|
+
import { filterPausedPlugins, isProPaused } from "../core/pro_plugins";
|
|
5
|
+
import { useLicenseStatus } from "./useLicenseStatus";
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
AuthController,
|
|
@@ -22,11 +24,14 @@ import {
|
|
|
22
24
|
} from "../types";
|
|
23
25
|
import {
|
|
24
26
|
applyPermissionsFunctionIfEmpty,
|
|
27
|
+
collectionSegmentsFrom,
|
|
28
|
+
fullPathToCollectionSegments,
|
|
25
29
|
getCollectionByPathOrId,
|
|
26
30
|
mergeDeep,
|
|
27
31
|
removeFunctions,
|
|
28
32
|
removeInitialAndTrailingSlashes,
|
|
29
33
|
resolveCollectionPathIds,
|
|
34
|
+
resolveCollectionPathSegments,
|
|
30
35
|
resolvePermissions
|
|
31
36
|
} from "../util";
|
|
32
37
|
import { getParentReferencesFromPath } from "../util/parent_references_from_path";
|
|
@@ -112,7 +117,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
112
117
|
views: viewsProp,
|
|
113
118
|
adminViews: adminViewsProp,
|
|
114
119
|
viewsOrder,
|
|
115
|
-
plugins,
|
|
120
|
+
plugins: allPlugins,
|
|
116
121
|
userConfigPersistence,
|
|
117
122
|
dataSourceDelegate,
|
|
118
123
|
disabled,
|
|
@@ -121,6 +126,27 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
121
126
|
|
|
122
127
|
const navigate = useNavigate();
|
|
123
128
|
|
|
129
|
+
// While PRO is paused, PRO plugins contribute no collections, collection
|
|
130
|
+
// modifications, views or navigation entries. This hook runs above
|
|
131
|
+
// `FireCMS`, so the status arrives through the store `FireCMS` publishes to,
|
|
132
|
+
// and its arrival re-renders this hook, which resolves navigation again.
|
|
133
|
+
const licenseStatus = useLicenseStatus();
|
|
134
|
+
const proPaused = isProPaused(licenseStatus);
|
|
135
|
+
const plugins = useMemo(
|
|
136
|
+
() => filterPausedPlugins(allPlugins, licenseStatus),
|
|
137
|
+
[allPlugins, licenseStatus]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Navigation resolves asynchronously, so a resolution started before the
|
|
141
|
+
// pause changed can finish after one started since. This ref holds the
|
|
142
|
+
// current pause so the older one is dropped instead of restoring what the
|
|
143
|
+
// paused plugins contributed. Declared before the effect that refreshes.
|
|
144
|
+
const proPausedRef = useRef(proPaused);
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
proPausedRef.current = proPaused;
|
|
147
|
+
}, [proPaused]);
|
|
148
|
+
const resolvedProPausedRef = useRef<boolean | undefined>(undefined);
|
|
149
|
+
|
|
124
150
|
const collectionsRef = useRef<EntityCollection[] | undefined>(undefined);
|
|
125
151
|
const viewsRef = useRef<CMSView[] | undefined>(undefined);
|
|
126
152
|
const adminViewsRef = useRef<CMSView[] | undefined>(undefined);
|
|
@@ -299,7 +325,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
299
325
|
groups: uniqueGroups,
|
|
300
326
|
onNavigationEntriesUpdate: onNavigationEntriesUpdateCallback!,
|
|
301
327
|
};
|
|
302
|
-
}, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups]);
|
|
328
|
+
}, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups, plugins]);
|
|
303
329
|
|
|
304
330
|
const onNavigationEntriesOrderUpdate = useCallback((entries: NavigationGroupMapping[]) => {
|
|
305
331
|
if (!plugins) {
|
|
@@ -339,6 +365,8 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
339
365
|
|
|
340
366
|
console.debug("Refreshing navigation");
|
|
341
367
|
|
|
368
|
+
const resolvingWhilePaused = proPaused;
|
|
369
|
+
|
|
342
370
|
try {
|
|
343
371
|
|
|
344
372
|
const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
|
|
@@ -348,10 +376,21 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
348
376
|
]
|
|
349
377
|
);
|
|
350
378
|
|
|
379
|
+
// Superseded: a newer resolution runs with the current pause.
|
|
380
|
+
if (resolvingWhilePaused !== proPausedRef.current)
|
|
381
|
+
return;
|
|
382
|
+
|
|
351
383
|
const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder, undefined, onNavigationEntriesOrderUpdate);
|
|
352
384
|
|
|
353
|
-
|
|
354
|
-
|
|
385
|
+
// When the pause changes, everything below is replaced, including
|
|
386
|
+
// what the comparisons ignore: they skip functions, and a plugin may
|
|
387
|
+
// have added only collection callbacks, or only regrouped entries.
|
|
388
|
+
const pauseChanged = resolvedProPausedRef.current !== undefined
|
|
389
|
+
&& resolvedProPausedRef.current !== resolvingWhilePaused;
|
|
390
|
+
resolvedProPausedRef.current = resolvingWhilePaused;
|
|
391
|
+
|
|
392
|
+
let shouldUpdateTopLevelNav = pauseChanged;
|
|
393
|
+
if (pauseChanged || !areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
|
|
355
394
|
collectionsRef.current = resolvedCollections;
|
|
356
395
|
console.debug("Collections have changed", resolvedCollections);
|
|
357
396
|
shouldUpdateTopLevelNav = true;
|
|
@@ -360,7 +399,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
360
399
|
collectionsRef.current = resolvedCollections;
|
|
361
400
|
shouldUpdateTopLevelNav = true;
|
|
362
401
|
}
|
|
363
|
-
if (!equal(viewsRef.current, resolvedViews)) {
|
|
402
|
+
if (pauseChanged || !equal(viewsRef.current, resolvedViews)) {
|
|
364
403
|
viewsRef.current = resolvedViews;
|
|
365
404
|
shouldUpdateTopLevelNav = true;
|
|
366
405
|
}
|
|
@@ -397,6 +436,8 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
397
436
|
viewsProp,
|
|
398
437
|
adminViewsProp,
|
|
399
438
|
computeTopNavigation,
|
|
439
|
+
plugins,
|
|
440
|
+
proPaused
|
|
400
441
|
]);
|
|
401
442
|
|
|
402
443
|
useEffect(() => {
|
|
@@ -405,13 +446,14 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
405
446
|
|
|
406
447
|
const getCollection = useCallback((
|
|
407
448
|
idOrPath: string,
|
|
408
|
-
includeUserOverride = false
|
|
449
|
+
includeUserOverride = false,
|
|
450
|
+
pathSegments?: string[]
|
|
409
451
|
): EC | undefined => {
|
|
410
452
|
const collections = collectionsRef.current;
|
|
411
453
|
if (!collections)
|
|
412
454
|
return undefined;
|
|
413
455
|
|
|
414
|
-
const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections);
|
|
456
|
+
const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections, pathSegments);
|
|
415
457
|
|
|
416
458
|
const userOverride = includeUserOverride ? userConfigPersistence?.getCollectionConfig(idOrPath) : undefined;
|
|
417
459
|
let overriddenCollection = baseCollection;
|
|
@@ -505,23 +547,33 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
505
547
|
throw Error("Expected path starting with " + fullCollectionPath);
|
|
506
548
|
}, [fullCollectionPath]);
|
|
507
549
|
|
|
508
|
-
const resolveIdsFrom = useCallback((path: string): string => {
|
|
550
|
+
const resolveIdsFrom = useCallback((path: string, pathSegments?: string[]): string => {
|
|
551
|
+
const collections = collectionsRef.current ?? [];
|
|
552
|
+
return resolveCollectionPathIds(path, collections, pathSegments);
|
|
553
|
+
}, []);
|
|
554
|
+
|
|
555
|
+
const resolveSegmentsFrom = useCallback((pathSegments: string[]): string[] => {
|
|
509
556
|
const collections = collectionsRef.current ?? [];
|
|
510
|
-
return
|
|
557
|
+
return resolveCollectionPathSegments(pathSegments, collections);
|
|
511
558
|
}, []);
|
|
512
559
|
|
|
513
|
-
const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
|
|
560
|
+
const getAllParentReferencesForPath = useCallback((path: string, pathSegments?: string[]): EntityReference[] => {
|
|
514
561
|
const collections = collectionsRef.current ?? [];
|
|
515
562
|
return getParentReferencesFromPath({
|
|
516
563
|
path,
|
|
564
|
+
pathSegments,
|
|
517
565
|
collections
|
|
518
566
|
});
|
|
519
567
|
}, []);
|
|
520
568
|
|
|
521
|
-
const getParentCollectionIds = useCallback((path: string): string[] => {
|
|
569
|
+
const getParentCollectionIds = useCallback((path: string, pathSegments?: string[]): string[] => {
|
|
522
570
|
|
|
523
|
-
|
|
524
|
-
|
|
571
|
+
// Positional filtering only picks out the collection segments while no entity id
|
|
572
|
+
// contains "/"; with one, every following segment shifts. Given the real boundaries
|
|
573
|
+
// the same rule applies correctly; without them this is the historical reading.
|
|
574
|
+
const oddPathSegments = pathSegments
|
|
575
|
+
? collectionSegmentsFrom(pathSegments)
|
|
576
|
+
: fullPathToCollectionSegments(path);
|
|
525
577
|
oddPathSegments.pop();
|
|
526
578
|
|
|
527
579
|
const result: string[][] = [];
|
|
@@ -567,13 +619,16 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
567
619
|
urlPathToDataPath,
|
|
568
620
|
buildUrlCollectionPath,
|
|
569
621
|
resolveIdsFrom,
|
|
622
|
+
resolveSegmentsFrom,
|
|
570
623
|
topLevelNavigation,
|
|
571
624
|
refreshNavigation,
|
|
572
625
|
getParentReferencesFromPath: getAllParentReferencesForPath,
|
|
573
626
|
getParentCollectionIds,
|
|
574
627
|
convertIdsToPaths,
|
|
575
628
|
navigate,
|
|
576
|
-
|
|
629
|
+
// All of them, paused or not: `FireCMS` sends every key to the license
|
|
630
|
+
// check and applies the same pause to what it renders.
|
|
631
|
+
plugins: allPlugins
|
|
577
632
|
};
|
|
578
633
|
}
|
|
579
634
|
|
|
@@ -12,6 +12,7 @@ import { useCustomizationController } from "./useCustomizationController";
|
|
|
12
12
|
import { useAnalyticsController } from "./useAnalyticsController";
|
|
13
13
|
import React, { useEffect } from "react";
|
|
14
14
|
import { useInternalUserManagementController } from "./useInternalUserManagementController";
|
|
15
|
+
import { useLicenseStatus } from "./useLicenseStatus";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Hook to retrieve the {@link FireCMSContext}.
|
|
@@ -36,6 +37,7 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
|
|
|
36
37
|
const customizationController = useCustomizationController();
|
|
37
38
|
const analyticsController = useAnalyticsController();
|
|
38
39
|
const userManagement = useInternalUserManagementController<USER>();
|
|
40
|
+
const licenseStatus = useLicenseStatus();
|
|
39
41
|
|
|
40
42
|
const fireCMSContextRef = React.useRef<FireCMSContext<USER, AuthControllerType>>({
|
|
41
43
|
authController,
|
|
@@ -49,7 +51,8 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
|
|
|
49
51
|
dialogsController,
|
|
50
52
|
customizationController,
|
|
51
53
|
analyticsController,
|
|
52
|
-
userManagement
|
|
54
|
+
userManagement,
|
|
55
|
+
licenseStatus
|
|
53
56
|
});
|
|
54
57
|
|
|
55
58
|
useEffect(() => {
|
|
@@ -65,9 +68,10 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
|
|
|
65
68
|
dialogsController,
|
|
66
69
|
customizationController,
|
|
67
70
|
analyticsController,
|
|
68
|
-
userManagement
|
|
71
|
+
userManagement,
|
|
72
|
+
licenseStatus
|
|
69
73
|
};
|
|
70
|
-
}, [authController, dialogsController, navigation, sideDialogsController]);
|
|
74
|
+
}, [authController, dialogsController, navigation, sideDialogsController, licenseStatus]);
|
|
71
75
|
|
|
72
76
|
return fireCMSContextRef.current;
|
|
73
77
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useContext, useSyncExternalStore } from "react";
|
|
2
|
+
import { AccessResponse } from "../types";
|
|
3
|
+
import {
|
|
4
|
+
getLicenseStatusSnapshot,
|
|
5
|
+
getServerLicenseStatusSnapshot,
|
|
6
|
+
LicenseStatusContext,
|
|
7
|
+
subscribeLicenseStatus
|
|
8
|
+
} from "../contexts/LicenseStatusContext";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The FireCMS license status of this project: whether PRO runs on a trial, is
|
|
12
|
+
* licensed, or is paused, plus the link to get a license.
|
|
13
|
+
*
|
|
14
|
+
* `null` until the license check answers, and when it is not sent at all
|
|
15
|
+
* (`telemetry={false}` without an `apiKey`). A response from an older server
|
|
16
|
+
* may lack `licenseState`; treat that as unknown. PRO features are paused when
|
|
17
|
+
* `blocked` is true; use `isProPaused(status)` for that check.
|
|
18
|
+
*
|
|
19
|
+
* Works inside `FireCMS` and also above it, e.g. in the component that calls
|
|
20
|
+
* `useBuildNavigationController`.
|
|
21
|
+
*
|
|
22
|
+
* @group Hooks and utilities
|
|
23
|
+
*/
|
|
24
|
+
export function useLicenseStatus(): AccessResponse | null {
|
|
25
|
+
const fromContext = useContext(LicenseStatusContext);
|
|
26
|
+
const published = useSyncExternalStore(
|
|
27
|
+
subscribeLicenseStatus,
|
|
28
|
+
getLicenseStatusSnapshot,
|
|
29
|
+
getServerLicenseStatusSnapshot
|
|
30
|
+
);
|
|
31
|
+
return fromContext !== undefined ? fromContext : published;
|
|
32
|
+
}
|