@firecms/core 3.4.0-canary.f6a889a → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
- package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/ErrorBoundary.d.ts +1 -3
- package/dist/components/LicenseBanner.d.ts +24 -0
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
- package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
- package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
- package/dist/components/common/table_url_params.d.ts +24 -0
- package/dist/components/common/useDataSourceTableController.d.ts +9 -1
- package/dist/components/common/useTableSearchHelper.d.ts +9 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/contexts/LicenseStatusContext.d.ts +16 -0
- package/dist/core/EntityEditView.d.ts +9 -1
- 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-CBOjCvsy.js +707 -0
- package/dist/de-CBOjCvsy.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 +8 -1
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/fr-DRfttcG0.js +707 -0
- package/dist/fr-DRfttcG0.js.map +1 -0
- package/dist/hi-DwieOZNf.js +707 -0
- package/dist/hi-DwieOZNf.js.map +1 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/save.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/hooks/data/useEntityFetch.d.ts +9 -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 +4059 -11384
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9090 -7610
- package/dist/index.umd.js.map +1 -1
- package/dist/it-l2KXtx9n.js +707 -0
- package/dist/it-l2KXtx9n.js.map +1 -0
- package/dist/pl-DPZrdKhi.js +709 -0
- package/dist/pl-DPZrdKhi.js.map +1 -0
- package/dist/pt-BoHpabw1.js +709 -0
- package/dist/pt-BoHpabw1.js.map +1 -0
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +14 -0
- package/dist/types/datasource.d.ts +67 -4
- package/dist/types/entities.d.ts +21 -1
- 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 +81 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +22 -1
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +7 -0
- package/dist/types/translations.d.ts +51 -2
- package/dist/util/__tests__/collections.test.d.ts +1 -0
- package/dist/util/__tests__/urls.test.d.ts +1 -0
- package/dist/util/collections.d.ts +1 -1
- 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 +2 -0
- package/dist/util/lazy_eager.d.ts +3 -3
- package/dist/util/local_storage.d.ts +23 -0
- package/dist/util/navigation_from_path.d.ts +7 -0
- package/dist/util/navigation_utils.d.ts +87 -3
- package/dist/util/parent_references_from_path.d.ts +16 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- package/dist/util/urls.d.ts +22 -0
- package/package.json +5 -6
- 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/EntityCollectionTable/EntityCollectionTable.tsx +1 -0
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityCollectionView/utils.ts +4 -4
- package/src/components/EntityPreview.tsx +4 -1
- package/src/components/ErrorBoundary.tsx +12 -18
- package/src/components/LicenseBanner.tsx +287 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
- package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
- package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/table_url_params.ts +172 -0
- package/src/components/common/useDataSourceTableController.tsx +86 -179
- package/src/components/common/useTableSearchHelper.ts +26 -2
- package/src/components/index.tsx +1 -0
- package/src/contexts/LicenseStatusContext.tsx +50 -0
- package/src/contexts/SnackbarProvider.tsx +14 -1
- package/src/core/DefaultAppBar.tsx +28 -16
- package/src/core/EntityEditView.tsx +20 -3
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +8 -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 +31 -10
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/hooks/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +6 -1
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +35 -4
- package/src/hooks/index.tsx +2 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +9 -9
- package/src/hooks/useBuildModeController.tsx +6 -4
- 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 +3 -1
- package/src/hooks/useTranslation.ts +3 -1
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +204 -33
- 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 +37 -15
- package/src/internal/useBuildSideEntityController.tsx +19 -5
- package/src/locales/de.ts +28 -2
- package/src/locales/en.ts +33 -5
- package/src/locales/es.ts +28 -2
- package/src/locales/fr.ts +28 -2
- package/src/locales/hi.ts +28 -2
- package/src/locales/it.ts +28 -2
- package/src/locales/pl.ts +28 -2
- package/src/locales/pt.ts +28 -2
- package/src/preview/components/DatePreview.tsx +2 -4
- package/src/preview/components/ReferencePreview.tsx +7 -3
- package/src/preview/components/StorageThumbnail.tsx +24 -2
- package/src/routes/FireCMSRoute.tsx +11 -3
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +16 -0
- package/src/types/datasource.ts +67 -4
- package/src/types/entities.ts +24 -1
- 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 +90 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +23 -1
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +7 -0
- package/src/types/translations.ts +51 -2
- package/src/util/__tests__/collections.test.ts +32 -0
- package/src/util/__tests__/urls.test.ts +131 -0
- package/src/util/collections.ts +1 -1
- package/src/util/date_fns_locales.ts +168 -0
- package/src/util/entities.ts +3 -1
- package/src/util/entity_cache.ts +28 -2
- package/src/util/index.ts +2 -0
- package/src/util/lazy_eager.tsx +30 -8
- package/src/util/local_storage.ts +52 -0
- package/src/util/navigation_from_path.ts +25 -2
- package/src/util/navigation_utils.ts +214 -3
- package/src/util/parent_references_from_path.ts +42 -3
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
- package/src/util/previews.ts +8 -1
- package/src/util/resolutions.ts +8 -0
- package/src/util/urls.ts +52 -0
- package/src/util/useStorageUploadController.tsx +6 -1
package/src/editor/index.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// The editor's runtime entry point is the lazy wrapper, not the implementation:
|
|
2
|
+
// a static re-export here would pull ProseMirror into every consumer of
|
|
3
|
+
// `@firecms/core`. The types come straight from the implementation and are
|
|
4
|
+
// erased at build time, so they cost nothing.
|
|
5
|
+
export { FireCMSEditor } from "./lazy_editor";
|
|
6
|
+
export type {
|
|
7
|
+
CustomEditorComponent,
|
|
8
|
+
MarkdownEditorConfig,
|
|
9
|
+
FireCMSEditorTextSize,
|
|
10
|
+
FireCMSEditorProps
|
|
11
|
+
} from "./editor";
|
|
2
12
|
export * from "./types";
|
|
@@ -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>
|
|
@@ -71,6 +74,11 @@ export type OnUpdateParams = {
|
|
|
71
74
|
|
|
72
75
|
export type EntityFormProps<M extends Record<string, any>> = {
|
|
73
76
|
path: string;
|
|
77
|
+
/**
|
|
78
|
+
* `path` split at its real segment boundaries; forwarded to the datasource for
|
|
79
|
+
* generateEntityId and checkUniqueField. Never derived from `path`.
|
|
80
|
+
*/
|
|
81
|
+
pathSegments?: string[];
|
|
74
82
|
fullIdPath?: string;
|
|
75
83
|
collection: EntityCollection<M>;
|
|
76
84
|
entityId?: string;
|
|
@@ -214,6 +222,7 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
|
|
|
214
222
|
|
|
215
223
|
export function EntityForm<M extends Record<string, any>>({
|
|
216
224
|
path,
|
|
225
|
+
pathSegments,
|
|
217
226
|
fullIdPath,
|
|
218
227
|
entityId: entityIdProp,
|
|
219
228
|
collection,
|
|
@@ -270,6 +279,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
270
279
|
entityId: entityIdProp,
|
|
271
280
|
collection,
|
|
272
281
|
path,
|
|
282
|
+
pathSegments,
|
|
273
283
|
values: valuesToBeSaved
|
|
274
284
|
});
|
|
275
285
|
}, false, 2000);
|
|
@@ -292,7 +302,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
292
302
|
if (mustSetCustomId) {
|
|
293
303
|
return undefined;
|
|
294
304
|
} else {
|
|
295
|
-
return dataSource.generateEntityId(path, collection);
|
|
305
|
+
return dataSource.generateEntityId(path, collection, pathSegments);
|
|
296
306
|
}
|
|
297
307
|
} else {
|
|
298
308
|
return entityIdProp;
|
|
@@ -308,7 +318,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
308
318
|
const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [authController, collection, path, status, entity, customizationController.propertyConfigs]);
|
|
309
319
|
|
|
310
320
|
const localChangesDataRaw = useMemo(() => entityId
|
|
311
|
-
? getEntityFromCache(path
|
|
321
|
+
? getEntityFromCache(entityCacheKey(path, entityId))
|
|
312
322
|
: getEntityFromCache(path + "#new"), [entityId, path]);
|
|
313
323
|
|
|
314
324
|
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
@@ -387,8 +397,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
387
397
|
onValuesModified?.(false, initialValues as M);
|
|
388
398
|
},
|
|
389
399
|
onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
|
|
390
|
-
const key = (status === "new" || status === "copy") ? path + "#new" : path
|
|
391
|
-
if (controller.dirty) {
|
|
400
|
+
const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
|
|
401
|
+
if (controller.dirty && localChangesBackup !== false) {
|
|
392
402
|
const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
|
|
393
403
|
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
394
404
|
saveEntityToCache(key, touchedValues);
|
|
@@ -468,8 +478,8 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
468
478
|
removeEntityFromMemoryCache(path + "#new");
|
|
469
479
|
removeEntityFromCache(path + "#new");
|
|
470
480
|
} else {
|
|
471
|
-
removeEntityFromMemoryCache(path
|
|
472
|
-
removeEntityFromCache(path
|
|
481
|
+
removeEntityFromMemoryCache(entityCacheKey(path, entityId));
|
|
482
|
+
removeEntityFromCache(entityCacheKey(path, entityId));
|
|
473
483
|
}
|
|
474
484
|
}
|
|
475
485
|
|
|
@@ -491,6 +501,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
491
501
|
entity: updatedEntity,
|
|
492
502
|
status,
|
|
493
503
|
path,
|
|
504
|
+
pathSegments,
|
|
494
505
|
entityId: updatedEntity.id,
|
|
495
506
|
collection
|
|
496
507
|
});
|
|
@@ -511,16 +522,20 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
511
522
|
previousValues,
|
|
512
523
|
entityId,
|
|
513
524
|
collection,
|
|
514
|
-
path
|
|
525
|
+
path,
|
|
526
|
+
pathSegments
|
|
515
527
|
}: {
|
|
516
528
|
collection: EntityCollection<M>,
|
|
517
529
|
path: string,
|
|
530
|
+
/** `path` split at its real segment boundaries, travelling with the `path` above. */
|
|
531
|
+
pathSegments?: string[],
|
|
518
532
|
entityId: string | undefined,
|
|
519
533
|
values: M,
|
|
520
534
|
previousValues?: M,
|
|
521
535
|
}) => {
|
|
522
536
|
return saveEntityWithCallbacks({
|
|
523
537
|
path,
|
|
538
|
+
pathSegments,
|
|
524
539
|
entityId,
|
|
525
540
|
values,
|
|
526
541
|
previousValues,
|
|
@@ -538,6 +553,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
538
553
|
type EntityFormSaveParams<M extends Record<string, any>> = {
|
|
539
554
|
collection: ResolvedEntityCollection<M>,
|
|
540
555
|
path: string,
|
|
556
|
+
pathSegments?: string[],
|
|
541
557
|
entityId: string | undefined,
|
|
542
558
|
values: EntityValues<M>,
|
|
543
559
|
previousValues?: EntityValues<M>,
|
|
@@ -547,6 +563,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
547
563
|
const onSaveEntityRequest = async ({
|
|
548
564
|
collection,
|
|
549
565
|
path,
|
|
566
|
+
pathSegments,
|
|
550
567
|
entityId,
|
|
551
568
|
values,
|
|
552
569
|
previousValues,
|
|
@@ -560,6 +577,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
560
577
|
return saveEntity({
|
|
561
578
|
collection,
|
|
562
579
|
path,
|
|
580
|
+
pathSegments,
|
|
563
581
|
entityId,
|
|
564
582
|
values,
|
|
565
583
|
previousValues
|
|
@@ -573,6 +591,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
573
591
|
return onSaveEntityRequest({
|
|
574
592
|
collection: resolvedCollection,
|
|
575
593
|
path,
|
|
594
|
+
pathSegments,
|
|
576
595
|
entityId,
|
|
577
596
|
values,
|
|
578
597
|
previousValues: entity?.values,
|
|
@@ -623,13 +642,14 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
623
642
|
const plugins = customizationController.plugins;
|
|
624
643
|
|
|
625
644
|
const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
|
|
626
|
-
const parentCollectionIds = navigationController.getParentCollectionIds(path);
|
|
645
|
+
const parentCollectionIds = navigationController.getParentCollectionIds(path, pathSegments);
|
|
627
646
|
|
|
628
647
|
if (plugins && collection) {
|
|
629
648
|
const actionProps: PluginFormActionProps = {
|
|
630
649
|
entityId,
|
|
631
650
|
parentCollectionIds,
|
|
632
651
|
path,
|
|
652
|
+
pathSegments,
|
|
633
653
|
status,
|
|
634
654
|
collection,
|
|
635
655
|
context,
|
|
@@ -692,7 +712,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
692
712
|
const uniqueFieldValidator: CustomFieldValidator = useCallback(({
|
|
693
713
|
name,
|
|
694
714
|
value
|
|
695
|
-
}) => dataSource.checkUniqueField(path, name, value, entityId, collection),
|
|
715
|
+
}) => dataSource.checkUniqueField(path, name, value, entityId, collection, pathSegments),
|
|
696
716
|
[dataSource, path, entityId]);
|
|
697
717
|
|
|
698
718
|
const validationSchema = useMemo(() => entityId
|
|
@@ -890,6 +910,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
890
910
|
const dialogActions = <EntityFormActionsComponent
|
|
891
911
|
collection={resolvedCollection}
|
|
892
912
|
path={path}
|
|
913
|
+
pathSegments={pathSegments}
|
|
893
914
|
fullPath={path}
|
|
894
915
|
fullIdPath={fullIdPath}
|
|
895
916
|
entity={entity}
|
|
@@ -924,7 +945,7 @@ export function EntityForm<M extends Record<string, any>>({
|
|
|
924
945
|
|
|
925
946
|
{manualApplyLocalChanges && hasLocalChanges &&
|
|
926
947
|
<LocalChangesMenu
|
|
927
|
-
cacheKey={status === "new" || status === "copy" ? path + "#new" : path
|
|
948
|
+
cacheKey={status === "new" || status === "copy" ? path + "#new" : entityCacheKey(path, entityId)}
|
|
928
949
|
properties={resolvedCollection.properties}
|
|
929
950
|
cachedData={localChangesDataRaw as Partial<M>}
|
|
930
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,6 +88,7 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
return dataSource.deleteEntity({
|
|
91
|
+
pathSegments: resolvedPathSegments,
|
|
84
92
|
entity,
|
|
85
93
|
collection
|
|
86
94
|
}).then(() => {
|
package/src/hooks/data/save.ts
CHANGED
|
@@ -43,6 +43,7 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, any>> =
|
|
|
43
43
|
export async function saveEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
|
|
44
44
|
collection,
|
|
45
45
|
path,
|
|
46
|
+
pathSegments,
|
|
46
47
|
entityId,
|
|
47
48
|
values,
|
|
48
49
|
previousValues,
|
|
@@ -67,7 +68,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
67
68
|
|
|
68
69
|
const customizationController = context.customizationController;
|
|
69
70
|
|
|
70
|
-
const resolvedPath = context.navigation.resolveIdsFrom(path);
|
|
71
|
+
const resolvedPath = context.navigation.resolveIdsFrom(path, pathSegments);
|
|
71
72
|
|
|
72
73
|
const callbacks = collection.callbacks;
|
|
73
74
|
if (callbacks?.onPreSave) {
|
|
@@ -83,6 +84,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
83
84
|
updatedValues = await callbacks.onPreSave({
|
|
84
85
|
collection: resolvedCollection,
|
|
85
86
|
path,
|
|
87
|
+
pathSegments,
|
|
86
88
|
resolvedPath,
|
|
87
89
|
entityId,
|
|
88
90
|
values,
|
|
@@ -103,6 +105,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
103
105
|
return dataSource.saveEntity({
|
|
104
106
|
collection,
|
|
105
107
|
path: resolvedPath,
|
|
108
|
+
pathSegments,
|
|
106
109
|
entityId,
|
|
107
110
|
values: updatedValues,
|
|
108
111
|
previousValues,
|
|
@@ -121,6 +124,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
121
124
|
callbacks.onSaveSuccess({
|
|
122
125
|
collection: resolvedCollection,
|
|
123
126
|
path,
|
|
127
|
+
pathSegments,
|
|
124
128
|
resolvedPath,
|
|
125
129
|
entityId: entity.id,
|
|
126
130
|
values: updatedValues,
|
|
@@ -150,6 +154,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
|
|
|
150
154
|
callbacks.onSaveFailure({
|
|
151
155
|
collection: resolvedCollection,
|
|
152
156
|
path,
|
|
157
|
+
pathSegments,
|
|
153
158
|
resolvedPath,
|
|
154
159
|
entityId,
|
|
155
160
|
values: updatedValues,
|
|
@@ -13,6 +13,12 @@ export interface CollectionFetchProps<M extends Record<string, any>> {
|
|
|
13
13
|
* Absolute collection path
|
|
14
14
|
*/
|
|
15
15
|
path: string;
|
|
16
|
+
/**
|
|
17
|
+
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
18
|
+
* entity id containing "/" stays unambiguous. Never derived from `path` — see
|
|
19
|
+
* `pathSegments` on the datasource props.
|
|
20
|
+
*/
|
|
21
|
+
pathSegments?: string[];
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* collection of the entity displayed by this collection
|
|
@@ -63,6 +69,7 @@ export interface CollectionFetchResult<M extends Record<string, any>> {
|
|
|
63
69
|
export function useCollectionFetch<M extends Record<string, any>, USER extends User>(
|
|
64
70
|
{
|
|
65
71
|
path: inputPath,
|
|
72
|
+
pathSegments: inputPathSegments,
|
|
66
73
|
collection,
|
|
67
74
|
filterValues,
|
|
68
75
|
sortBy,
|
|
@@ -73,7 +80,13 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
73
80
|
const dataSource = useDataSource(collection);
|
|
74
81
|
const navigationController = useNavigationController();
|
|
75
82
|
|
|
76
|
-
|
|
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);
|
|
77
90
|
|
|
78
91
|
const sortByProperty = sortBy ? sortBy[0] : undefined;
|
|
79
92
|
const currentSort = sortBy ? sortBy[1] : undefined;
|
|
@@ -91,6 +104,10 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
91
104
|
setDataLoading(true);
|
|
92
105
|
|
|
93
106
|
const onEntitiesUpdate = async (entities: Entity<M>[]) => {
|
|
107
|
+
// See useEntityFetch: entities carry the segments they were loaded with.
|
|
108
|
+
if (pathSegments) {
|
|
109
|
+
entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments });
|
|
110
|
+
}
|
|
94
111
|
if (collection.callbacks?.onFetch) {
|
|
95
112
|
try {
|
|
96
113
|
entities = await Promise.all(
|
|
@@ -98,6 +115,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
98
115
|
collection.callbacks!.onFetch!({
|
|
99
116
|
collection,
|
|
100
117
|
path,
|
|
118
|
+
pathSegments,
|
|
101
119
|
entity,
|
|
102
120
|
context
|
|
103
121
|
})));
|
|
@@ -124,6 +142,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
124
142
|
if (dataSource.listenCollection) {
|
|
125
143
|
return dataSource.listenCollection<M>({
|
|
126
144
|
path,
|
|
145
|
+
pathSegments,
|
|
127
146
|
collection,
|
|
128
147
|
onUpdate: onEntitiesUpdate,
|
|
129
148
|
onError,
|
|
@@ -137,6 +156,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
|
|
|
137
156
|
} else {
|
|
138
157
|
dataSource.fetchCollection<M>({
|
|
139
158
|
path,
|
|
159
|
+
pathSegments,
|
|
140
160
|
collection,
|
|
141
161
|
searchString,
|
|
142
162
|
filter: filterValues,
|
|
@@ -3,12 +3,21 @@ 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
|
|
9
10
|
*/
|
|
10
11
|
export interface EntityFetchProps<M extends Record<string, any>, USER extends User = User> {
|
|
11
12
|
path: string;
|
|
13
|
+
/**
|
|
14
|
+
* `path` split at its real segment boundaries. Forwarded to the datasource so a parent
|
|
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".
|
|
19
|
+
*/
|
|
20
|
+
pathSegments?: string[];
|
|
12
21
|
entityId?: string;
|
|
13
22
|
databaseId?: string;
|
|
14
23
|
collection: EntityCollection<M, USER>;
|
|
@@ -39,6 +48,7 @@ const CACHE: Record<string, Entity<any> | undefined> = {};
|
|
|
39
48
|
export function useEntityFetch<M extends Record<string, any>, USER extends User>(
|
|
40
49
|
{
|
|
41
50
|
path: inputPath,
|
|
51
|
+
pathSegments: inputPathSegments,
|
|
42
52
|
entityId,
|
|
43
53
|
collection,
|
|
44
54
|
databaseId,
|
|
@@ -48,7 +58,19 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
48
58
|
const dataSource = useDataSource(collection);
|
|
49
59
|
const navigationController = useNavigationController();
|
|
50
60
|
|
|
51
|
-
|
|
61
|
+
// Never fabricate segments. If the caller did not thread them, pass undefined so a
|
|
62
|
+
// delegate can tell "not provided" from a real answer — splitting `path` here would
|
|
63
|
+
// produce a confidently wrong array for any id containing "/".
|
|
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);
|
|
52
74
|
|
|
53
75
|
const context: FireCMSContext<USER> = useFireCMSContext();
|
|
54
76
|
|
|
@@ -61,11 +83,18 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
61
83
|
setDataLoading(true);
|
|
62
84
|
|
|
63
85
|
const onEntityUpdate = async (updatedEntity: Entity<M> | undefined) => {
|
|
86
|
+
// Attach the segments this entity was loaded with, so anything derived from it
|
|
87
|
+
// later — a reference, a delete — stays resolvable without re-deriving them
|
|
88
|
+
// from the flattened path. Delegates are not required to set this themselves.
|
|
89
|
+
if (updatedEntity && pathSegments && !updatedEntity.pathSegments) {
|
|
90
|
+
updatedEntity = { ...updatedEntity, pathSegments };
|
|
91
|
+
}
|
|
64
92
|
if (collection.callbacks?.onFetch && updatedEntity) {
|
|
65
93
|
try {
|
|
66
94
|
updatedEntity = await collection.callbacks.onFetch({
|
|
67
95
|
collection,
|
|
68
96
|
path,
|
|
97
|
+
pathSegments,
|
|
69
98
|
entity: updatedEntity,
|
|
70
99
|
context
|
|
71
100
|
});
|
|
@@ -73,7 +102,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
73
102
|
console.error(e);
|
|
74
103
|
}
|
|
75
104
|
}
|
|
76
|
-
CACHE[
|
|
105
|
+
CACHE[entityCacheKey(path, entityId)] = updatedEntity;
|
|
77
106
|
setEntity(updatedEntity);
|
|
78
107
|
setDataLoading(false);
|
|
79
108
|
setDataLoadingError(undefined);
|
|
@@ -86,8 +115,8 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
86
115
|
setDataLoadingError(error);
|
|
87
116
|
};
|
|
88
117
|
|
|
89
|
-
if (entityId && useCache && CACHE[
|
|
90
|
-
setEntity(CACHE[
|
|
118
|
+
if (entityId && useCache && CACHE[entityCacheKey(path, entityId)]) {
|
|
119
|
+
setEntity(CACHE[entityCacheKey(path, entityId)]);
|
|
91
120
|
setDataLoading(false);
|
|
92
121
|
setDataLoadingError(undefined);
|
|
93
122
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
@@ -97,6 +126,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
97
126
|
if (dataSource.listenEntity) {
|
|
98
127
|
return dataSource.listenEntity<M>({
|
|
99
128
|
path,
|
|
129
|
+
pathSegments,
|
|
100
130
|
entityId,
|
|
101
131
|
databaseId,
|
|
102
132
|
collection,
|
|
@@ -106,6 +136,7 @@ export function useEntityFetch<M extends Record<string, any>, USER extends User>
|
|
|
106
136
|
} else {
|
|
107
137
|
dataSource.fetchEntity<M>({
|
|
108
138
|
path,
|
|
139
|
+
pathSegments,
|
|
109
140
|
entityId,
|
|
110
141
|
databaseId,
|
|
111
142
|
collection
|
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,14 +1,14 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import { PartialEntityCollection, UserConfigurationPersistence } from "../types";
|
|
3
3
|
import { mergeDeep, stripCollectionPath } from "../util";
|
|
4
|
+
import { getLocalStorageJSON, setLocalStorageItem } from "../util/local_storage";
|
|
4
5
|
|
|
5
6
|
export function useBuildLocalConfigurationPersistence(): UserConfigurationPersistence {
|
|
6
7
|
|
|
7
8
|
const configCache = useRef<Record<string, PartialEntityCollection>>({});
|
|
8
9
|
|
|
9
10
|
const getCollectionFromStorage = useCallback((storageKey: string) => {
|
|
10
|
-
|
|
11
|
-
return item ? JSON.parse(item) : {};
|
|
11
|
+
return getLocalStorageJSON(storageKey, {});
|
|
12
12
|
}, []);
|
|
13
13
|
|
|
14
14
|
const getCollectionConfig = useCallback(<M extends Record<string, any>>(path: string): PartialEntityCollection<M> => {
|
|
@@ -21,7 +21,7 @@ export function useBuildLocalConfigurationPersistence(): UserConfigurationPersis
|
|
|
21
21
|
|
|
22
22
|
const onCollectionModified = useCallback(<M extends Record<string, any>>(path: string, data: PartialEntityCollection<M>) => {
|
|
23
23
|
const storageKey = `collection_config::${stripCollectionPath(path)}`;
|
|
24
|
-
|
|
24
|
+
setLocalStorageItem(storageKey, JSON.stringify(data));
|
|
25
25
|
const cachedConfig = configCache.current[storageKey];
|
|
26
26
|
const newConfig = mergeDeep(cachedConfig ?? getCollectionFromStorage(path), data);
|
|
27
27
|
configCache.current[storageKey] = mergeDeep(configCache.current[storageKey], newConfig);
|
|
@@ -32,23 +32,23 @@ export function useBuildLocalConfigurationPersistence(): UserConfigurationPersis
|
|
|
32
32
|
const [collapsedGroups, _setCollapsedGroups] = useState<string[]>([]);
|
|
33
33
|
|
|
34
34
|
useEffect(() => {
|
|
35
|
-
_setRecentlyVisitedPaths(
|
|
36
|
-
_setFavouritePaths(
|
|
37
|
-
_setCollapsedGroups(
|
|
35
|
+
_setRecentlyVisitedPaths(getLocalStorageJSON("recently_visited_paths", []));
|
|
36
|
+
_setFavouritePaths(getLocalStorageJSON("favourite_paths", []));
|
|
37
|
+
_setCollapsedGroups(getLocalStorageJSON("collapsed_groups", []));
|
|
38
38
|
}, []);
|
|
39
39
|
|
|
40
40
|
const setRecentlyVisitedPaths = useCallback((paths: string[]) => {
|
|
41
|
-
|
|
41
|
+
setLocalStorageItem("recently_visited_paths", JSON.stringify(paths));
|
|
42
42
|
_setRecentlyVisitedPaths(paths);
|
|
43
43
|
}, []);
|
|
44
44
|
|
|
45
45
|
const setFavouritePaths = useCallback((paths: string[]) => {
|
|
46
|
-
|
|
46
|
+
setLocalStorageItem("favourite_paths", JSON.stringify(paths));
|
|
47
47
|
_setFavouritePaths(paths);
|
|
48
48
|
}, []);
|
|
49
49
|
|
|
50
50
|
const setCollapsedGroups = useCallback((paths: string[]) => {
|
|
51
|
-
|
|
51
|
+
setLocalStorageItem("collapsed_groups", JSON.stringify(paths));
|
|
52
52
|
_setCollapsedGroups(paths);
|
|
53
53
|
}, []);
|
|
54
54
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { ModeController } from "./index";
|
|
4
|
+
import { getLocalStorageItem, removeLocalStorageItem, setLocalStorageItem } from "../util/local_storage";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Use this hook to build a color mode controller that determines
|
|
@@ -15,7 +16,8 @@ export function useBuildModeController(): ModeController {
|
|
|
15
16
|
return mediaQueryList.matches;
|
|
16
17
|
}, []);
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
+
const storedMode = getLocalStorageItem("prefers-dark-mode");
|
|
20
|
+
const prefersDarkModeStorage: boolean | null = storedMode != null ? storedMode === "true" : null;
|
|
19
21
|
const prefersDarkMode = prefersDarkModeStorage ?? prefersDarkModeQuery();
|
|
20
22
|
const [mode, setMode] = useState<"light" | "dark">(prefersDarkMode ? "dark" : "light");
|
|
21
23
|
|
|
@@ -32,16 +34,16 @@ export function useBuildModeController(): ModeController {
|
|
|
32
34
|
const setModeInternal = useCallback((mode: "light" | "dark" | "system") => {
|
|
33
35
|
if (mode === "light") {
|
|
34
36
|
setDocumentMode("light");
|
|
35
|
-
|
|
37
|
+
setLocalStorageItem("prefers-dark-mode", "false");
|
|
36
38
|
setMode("light");
|
|
37
39
|
} else if (mode === "dark") {
|
|
38
40
|
setDocumentMode("dark");
|
|
39
|
-
|
|
41
|
+
setLocalStorageItem("prefers-dark-mode", "true");
|
|
40
42
|
setMode("dark");
|
|
41
43
|
} else {
|
|
42
44
|
const preferredMode = prefersDarkModeQuery() ? "dark" : "light";
|
|
43
45
|
setDocumentMode(preferredMode);
|
|
44
|
-
|
|
46
|
+
removeLocalStorageItem("prefers-dark-mode");
|
|
45
47
|
setMode(preferredMode);
|
|
46
48
|
}
|
|
47
49
|
}, [prefersDarkModeQuery]);
|