@firecms/core 3.3.0-canary.d3242eb → 3.4.0-canary.00f9be5
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/app/useApp.d.ts +1 -0
- package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
- 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/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/EntityPreview.d.ts +3 -1
- package/dist/components/ErrorBoundary.d.ts +1 -3
- package/dist/components/LicenseBanner.d.ts +23 -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/DefaultDrawer.d.ts +10 -3
- package/dist/core/EntityEditView.d.ts +9 -1
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/field_configs.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-BF5vcQPY.js +703 -0
- package/dist/de-BF5vcQPY.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/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/fr-DH9_wyFI.js +703 -0
- package/dist/fr-DH9_wyFI.js.map +1 -0
- package/dist/hi-BM8dm1Lu.js +703 -0
- package/dist/hi-BM8dm1Lu.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 +4429 -10479
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9313 -6582
- package/dist/index.umd.js.map +1 -1
- package/dist/it-D3K1gfe4.js +703 -0
- package/dist/it-D3K1gfe4.js.map +1 -0
- package/dist/locales/pl.d.ts +2 -0
- package/dist/pl-CP1zeLzn.js +705 -0
- package/dist/pl-CP1zeLzn.js.map +1 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
- package/dist/pt-Cus7eKQR.js +705 -0
- package/dist/pt-Cus7eKQR.js.map +1 -0
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +31 -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 +55 -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 +54 -5
- 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/entities.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/geopoint.d.ts +22 -0
- package/dist/util/index.d.ts +3 -0
- package/dist/util/lazy_eager.d.ts +3 -3
- package/dist/util/navigation_blocking.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +17 -0
- package/dist/util/navigation_utils.d.ts +107 -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 +17 -11
- package/src/app/Scaffold.tsx +54 -45
- package/src/app/useApp.tsx +1 -0
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
- package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
- 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 +71 -17
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityPreview.tsx +45 -41
- package/src/components/ErrorBoundary.tsx +12 -18
- package/src/components/HomePage/NavigationCardBinding.tsx +6 -3
- package/src/components/LicenseBanner.tsx +207 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -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/VirtualTable/VirtualTable.tsx +19 -8
- 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 +126 -163
- 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/DefaultDrawer.tsx +151 -65
- package/src/core/DrawerNavigationGroup.tsx +27 -29
- package/src/core/DrawerNavigationItem.tsx +10 -12
- package/src/core/EntityEditView.tsx +77 -35
- 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/field_configs.tsx +15 -0
- 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/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
- package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
- package/src/form/index.tsx +1 -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/useBuildNavigationController.tsx +75 -16
- 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 +199 -23
- 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 +20 -5
- package/src/locales/de.ts +37 -7
- package/src/locales/en.ts +41 -9
- package/src/locales/es.ts +37 -7
- package/src/locales/fr.ts +37 -7
- package/src/locales/hi.ts +37 -7
- package/src/locales/it.ts +37 -7
- package/src/locales/pl.ts +752 -0
- package/src/locales/pt.ts +37 -7
- package/src/preview/PropertyPreview.tsx +12 -0
- 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/preview/index.ts +1 -0
- package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
- package/src/routes/FireCMSRoute.tsx +54 -27
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +34 -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 +63 -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 +54 -5
- 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 +16 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/geopoint.ts +81 -0
- package/src/util/index.ts +3 -0
- package/src/util/lazy_eager.tsx +30 -8
- package/src/util/navigation_blocking.ts +45 -0
- package/src/util/navigation_from_path.ts +47 -7
- package/src/util/navigation_utils.ts +250 -5
- package/src/util/parent_references_from_path.ts +45 -4
- 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 +11 -0
- package/src/util/urls.ts +52 -0
- package/src/util/useStorageUploadController.tsx +6 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getLocalChangesBackup } from "../collections";
|
|
2
|
+
import { EntityCollection } from "../../types";
|
|
3
|
+
|
|
4
|
+
function collectionWith(localChangesBackup?: "manual_apply" | "auto_apply" | false): EntityCollection {
|
|
5
|
+
return {
|
|
6
|
+
id: "products",
|
|
7
|
+
name: "Products",
|
|
8
|
+
path: "products",
|
|
9
|
+
properties: {},
|
|
10
|
+
localChangesBackup
|
|
11
|
+
} as EntityCollection;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("getLocalChangesBackup", () => {
|
|
15
|
+
|
|
16
|
+
it("should default to manual_apply when not set", () => {
|
|
17
|
+
expect(getLocalChangesBackup(collectionWith(undefined))).toEqual("manual_apply");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should return manual_apply when explicitly set", () => {
|
|
21
|
+
expect(getLocalChangesBackup(collectionWith("manual_apply"))).toEqual("manual_apply");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should return auto_apply when explicitly set", () => {
|
|
25
|
+
expect(getLocalChangesBackup(collectionWith("auto_apply"))).toEqual("auto_apply");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return false when explicitly disabled, and not fall back to the default", () => {
|
|
29
|
+
expect(getLocalChangesBackup(collectionWith(false))).toEqual(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getFileExtensionFromUrl, getPreviewTypeFromUrl, isAbsoluteHttpUrl } from "../urls";
|
|
2
|
+
|
|
3
|
+
describe("url utilities", () => {
|
|
4
|
+
|
|
5
|
+
describe("isAbsoluteHttpUrl", () => {
|
|
6
|
+
|
|
7
|
+
it("should detect absolute http and https URLs", () => {
|
|
8
|
+
expect(isAbsoluteHttpUrl("https://abc.com/abc.png")).toBe(true);
|
|
9
|
+
expect(isAbsoluteHttpUrl("http://abc.com/abc.png")).toBe(true);
|
|
10
|
+
expect(isAbsoluteHttpUrl("HTTPS://ABC.COM/ABC.PNG")).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should detect Firebase Storage download URLs as absolute URLs", () => {
|
|
14
|
+
expect(isAbsoluteHttpUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.png?alt=media&token=abc")).toBe(true);
|
|
15
|
+
expect(isAbsoluteHttpUrl("https://storage.googleapis.com/demo.appspot.com/images/cat.png")).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should not consider storage paths absolute URLs", () => {
|
|
19
|
+
expect(isAbsoluteHttpUrl("images/cat.png")).toBe(false);
|
|
20
|
+
expect(isAbsoluteHttpUrl("/images/cat.png")).toBe(false);
|
|
21
|
+
expect(isAbsoluteHttpUrl("cat.png")).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should not consider other protocols absolute http URLs", () => {
|
|
25
|
+
expect(isAbsoluteHttpUrl("gs://demo.appspot.com/images/cat.png")).toBe(false);
|
|
26
|
+
expect(isAbsoluteHttpUrl("data:image/png;base64,AAAA")).toBe(false);
|
|
27
|
+
expect(isAbsoluteHttpUrl("//abc.com/abc.png")).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should handle empty values", () => {
|
|
31
|
+
expect(isAbsoluteHttpUrl("")).toBe(false);
|
|
32
|
+
expect(isAbsoluteHttpUrl(undefined)).toBe(false);
|
|
33
|
+
expect(isAbsoluteHttpUrl(null)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should not match http appearing later in the value", () => {
|
|
37
|
+
expect(isAbsoluteHttpUrl("images/https://abc.com/abc.png")).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("getFileExtensionFromUrl", () => {
|
|
43
|
+
|
|
44
|
+
it("should extract the extension of a plain URL", () => {
|
|
45
|
+
expect(getFileExtensionFromUrl("https://abc.com/abc.png")).toBe("png");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should lowercase the extension", () => {
|
|
49
|
+
expect(getFileExtensionFromUrl("https://abc.com/ABC.PNG")).toBe("png");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should ignore query strings and hash fragments", () => {
|
|
53
|
+
expect(getFileExtensionFromUrl("https://abc.com/abc.png?width=200")).toBe("png");
|
|
54
|
+
expect(getFileExtensionFromUrl("https://abc.com/abc.png#anchor")).toBe("png");
|
|
55
|
+
expect(getFileExtensionFromUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.jpeg?alt=media&token=abc")).toBe("jpeg");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should work with storage paths", () => {
|
|
59
|
+
expect(getFileExtensionFromUrl("images/cat.webp")).toBe("webp");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should use the last dot of the file name", () => {
|
|
63
|
+
expect(getFileExtensionFromUrl("https://abc.com/archive.tar.gz")).toBe("gz");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return undefined when there is no extension", () => {
|
|
67
|
+
expect(getFileExtensionFromUrl("https://abc.com/abc")).toBeUndefined();
|
|
68
|
+
expect(getFileExtensionFromUrl("https://abc.com/")).toBeUndefined();
|
|
69
|
+
expect(getFileExtensionFromUrl("images/cat")).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should not take dots of the host or the path as extensions", () => {
|
|
73
|
+
expect(getFileExtensionFromUrl("https://abc.com/images/cat")).toBeUndefined();
|
|
74
|
+
expect(getFileExtensionFromUrl("https://abc.com/abc.png/download")).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should return undefined for dotfiles and trailing dots", () => {
|
|
78
|
+
expect(getFileExtensionFromUrl("https://abc.com/.gitignore")).toBeUndefined();
|
|
79
|
+
expect(getFileExtensionFromUrl("https://abc.com/abc.")).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should handle empty values", () => {
|
|
83
|
+
expect(getFileExtensionFromUrl("")).toBeUndefined();
|
|
84
|
+
expect(getFileExtensionFromUrl(undefined)).toBeUndefined();
|
|
85
|
+
expect(getFileExtensionFromUrl(null)).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("getPreviewTypeFromUrl", () => {
|
|
91
|
+
|
|
92
|
+
it("should infer image previews", () => {
|
|
93
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.png")).toBe("image");
|
|
94
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.jpg")).toBe("image");
|
|
95
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.jpeg")).toBe("image");
|
|
96
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.gif")).toBe("image");
|
|
97
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.webp")).toBe("image");
|
|
98
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.svg")).toBe("image");
|
|
99
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.avif")).toBe("image");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should infer video previews", () => {
|
|
103
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.mp4")).toBe("video");
|
|
104
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.webm")).toBe("video");
|
|
105
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.mov")).toBe("video");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should infer audio previews", () => {
|
|
109
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.mp3")).toBe("audio");
|
|
110
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.wav")).toBe("audio");
|
|
111
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.m4a")).toBe("audio");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should infer the type of Firebase Storage download URLs", () => {
|
|
115
|
+
expect(getPreviewTypeFromUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.png?alt=media&token=abc")).toBe("image");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should be case insensitive", () => {
|
|
119
|
+
expect(getPreviewTypeFromUrl("https://abc.com/ABC.PNG")).toBe("image");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return undefined for unknown or missing extensions", () => {
|
|
123
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.pdf")).toBeUndefined();
|
|
124
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc.zip")).toBeUndefined();
|
|
125
|
+
expect(getPreviewTypeFromUrl("https://abc.com/abc")).toBeUndefined();
|
|
126
|
+
expect(getPreviewTypeFromUrl(undefined)).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
});
|
package/src/util/collections.ts
CHANGED
|
@@ -117,7 +117,7 @@ export const applyPermissionsFunctionIfEmpty = (collections: EntityCollection[],
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export function getLocalChangesBackup(collection: EntityCollection) {
|
|
120
|
-
if (
|
|
120
|
+
if (collection.localChangesBackup === undefined) {
|
|
121
121
|
return "manual_apply";
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useEffect, useReducer } from "react";
|
|
2
|
+
import type { Locale as DateFnsLocale } from "date-fns";
|
|
3
|
+
import { Locale } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* On-demand loading of date-fns locales.
|
|
7
|
+
*
|
|
8
|
+
* `import * as locales from "date-fns/locale"` pulls in every locale date-fns
|
|
9
|
+
* ships — 625KB of parsed JavaScript, loaded on every single page view, to
|
|
10
|
+
* support a `locale` setting that is unset for almost every project. Worse, it
|
|
11
|
+
* sat on the critical path: `@firecms/core` imported it statically, so the
|
|
12
|
+
* browser could not render anything until it had come down.
|
|
13
|
+
*
|
|
14
|
+
* Each locale now has its own dynamic import, so exactly one small chunk is
|
|
15
|
+
* fetched, and only when a project actually configures a locale. Projects that
|
|
16
|
+
* do not configure one (the default) pay nothing at all.
|
|
17
|
+
*
|
|
18
|
+
* The map is written out rather than built from a template literal because Vite
|
|
19
|
+
* and webpack both need the specifier to be statically analysable to emit the
|
|
20
|
+
* chunks.
|
|
21
|
+
*/
|
|
22
|
+
const loaders: Partial<Record<Locale, () => Promise<DateFnsLocale>>> = {
|
|
23
|
+
af: () => import("date-fns/locale/af").then(m => m.af),
|
|
24
|
+
ar: () => import("date-fns/locale/ar").then(m => m.ar),
|
|
25
|
+
arDZ: () => import("date-fns/locale/ar-DZ").then(m => m.arDZ),
|
|
26
|
+
arMA: () => import("date-fns/locale/ar-MA").then(m => m.arMA),
|
|
27
|
+
arSA: () => import("date-fns/locale/ar-SA").then(m => m.arSA),
|
|
28
|
+
az: () => import("date-fns/locale/az").then(m => m.az),
|
|
29
|
+
be: () => import("date-fns/locale/be").then(m => m.be),
|
|
30
|
+
bg: () => import("date-fns/locale/bg").then(m => m.bg),
|
|
31
|
+
bn: () => import("date-fns/locale/bn").then(m => m.bn),
|
|
32
|
+
ca: () => import("date-fns/locale/ca").then(m => m.ca),
|
|
33
|
+
cs: () => import("date-fns/locale/cs").then(m => m.cs),
|
|
34
|
+
cy: () => import("date-fns/locale/cy").then(m => m.cy),
|
|
35
|
+
da: () => import("date-fns/locale/da").then(m => m.da),
|
|
36
|
+
de: () => import("date-fns/locale/de").then(m => m.de),
|
|
37
|
+
el: () => import("date-fns/locale/el").then(m => m.el),
|
|
38
|
+
enAU: () => import("date-fns/locale/en-AU").then(m => m.enAU),
|
|
39
|
+
enCA: () => import("date-fns/locale/en-CA").then(m => m.enCA),
|
|
40
|
+
enGB: () => import("date-fns/locale/en-GB").then(m => m.enGB),
|
|
41
|
+
enIN: () => import("date-fns/locale/en-IN").then(m => m.enIN),
|
|
42
|
+
enNZ: () => import("date-fns/locale/en-NZ").then(m => m.enNZ),
|
|
43
|
+
enUS: () => import("date-fns/locale/en-US").then(m => m.enUS),
|
|
44
|
+
eo: () => import("date-fns/locale/eo").then(m => m.eo),
|
|
45
|
+
es: () => import("date-fns/locale/es").then(m => m.es),
|
|
46
|
+
et: () => import("date-fns/locale/et").then(m => m.et),
|
|
47
|
+
eu: () => import("date-fns/locale/eu").then(m => m.eu),
|
|
48
|
+
faIR: () => import("date-fns/locale/fa-IR").then(m => m.faIR),
|
|
49
|
+
fi: () => import("date-fns/locale/fi").then(m => m.fi),
|
|
50
|
+
fr: () => import("date-fns/locale/fr").then(m => m.fr),
|
|
51
|
+
frCA: () => import("date-fns/locale/fr-CA").then(m => m.frCA),
|
|
52
|
+
frCH: () => import("date-fns/locale/fr-CH").then(m => m.frCH),
|
|
53
|
+
gd: () => import("date-fns/locale/gd").then(m => m.gd),
|
|
54
|
+
gl: () => import("date-fns/locale/gl").then(m => m.gl),
|
|
55
|
+
gu: () => import("date-fns/locale/gu").then(m => m.gu),
|
|
56
|
+
he: () => import("date-fns/locale/he").then(m => m.he),
|
|
57
|
+
hi: () => import("date-fns/locale/hi").then(m => m.hi),
|
|
58
|
+
hr: () => import("date-fns/locale/hr").then(m => m.hr),
|
|
59
|
+
hu: () => import("date-fns/locale/hu").then(m => m.hu),
|
|
60
|
+
hy: () => import("date-fns/locale/hy").then(m => m.hy),
|
|
61
|
+
id: () => import("date-fns/locale/id").then(m => m.id),
|
|
62
|
+
is: () => import("date-fns/locale/is").then(m => m.is),
|
|
63
|
+
it: () => import("date-fns/locale/it").then(m => m.it),
|
|
64
|
+
ja: () => import("date-fns/locale/ja").then(m => m.ja),
|
|
65
|
+
ka: () => import("date-fns/locale/ka").then(m => m.ka),
|
|
66
|
+
kk: () => import("date-fns/locale/kk").then(m => m.kk),
|
|
67
|
+
kn: () => import("date-fns/locale/kn").then(m => m.kn),
|
|
68
|
+
ko: () => import("date-fns/locale/ko").then(m => m.ko),
|
|
69
|
+
lb: () => import("date-fns/locale/lb").then(m => m.lb),
|
|
70
|
+
lt: () => import("date-fns/locale/lt").then(m => m.lt),
|
|
71
|
+
lv: () => import("date-fns/locale/lv").then(m => m.lv),
|
|
72
|
+
mk: () => import("date-fns/locale/mk").then(m => m.mk),
|
|
73
|
+
ms: () => import("date-fns/locale/ms").then(m => m.ms),
|
|
74
|
+
mt: () => import("date-fns/locale/mt").then(m => m.mt),
|
|
75
|
+
nb: () => import("date-fns/locale/nb").then(m => m.nb),
|
|
76
|
+
nl: () => import("date-fns/locale/nl").then(m => m.nl),
|
|
77
|
+
nlBE: () => import("date-fns/locale/nl-BE").then(m => m.nlBE),
|
|
78
|
+
nn: () => import("date-fns/locale/nn").then(m => m.nn),
|
|
79
|
+
pl: () => import("date-fns/locale/pl").then(m => m.pl),
|
|
80
|
+
pt: () => import("date-fns/locale/pt").then(m => m.pt),
|
|
81
|
+
ptBR: () => import("date-fns/locale/pt-BR").then(m => m.ptBR),
|
|
82
|
+
ro: () => import("date-fns/locale/ro").then(m => m.ro),
|
|
83
|
+
ru: () => import("date-fns/locale/ru").then(m => m.ru),
|
|
84
|
+
sk: () => import("date-fns/locale/sk").then(m => m.sk),
|
|
85
|
+
sl: () => import("date-fns/locale/sl").then(m => m.sl),
|
|
86
|
+
sr: () => import("date-fns/locale/sr").then(m => m.sr),
|
|
87
|
+
srLatn: () => import("date-fns/locale/sr-Latn").then(m => m.srLatn),
|
|
88
|
+
sv: () => import("date-fns/locale/sv").then(m => m.sv),
|
|
89
|
+
ta: () => import("date-fns/locale/ta").then(m => m.ta),
|
|
90
|
+
te: () => import("date-fns/locale/te").then(m => m.te),
|
|
91
|
+
th: () => import("date-fns/locale/th").then(m => m.th),
|
|
92
|
+
tr: () => import("date-fns/locale/tr").then(m => m.tr),
|
|
93
|
+
ug: () => import("date-fns/locale/ug").then(m => m.ug),
|
|
94
|
+
uk: () => import("date-fns/locale/uk").then(m => m.uk),
|
|
95
|
+
uz: () => import("date-fns/locale/uz").then(m => m.uz),
|
|
96
|
+
vi: () => import("date-fns/locale/vi").then(m => m.vi),
|
|
97
|
+
zhCN: () => import("date-fns/locale/zh-CN").then(m => m.zhCN),
|
|
98
|
+
zhTW: () => import("date-fns/locale/zh-TW").then(m => m.zhTW),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const cache = new Map<Locale, DateFnsLocale>();
|
|
102
|
+
const inFlight = new Map<Locale, Promise<DateFnsLocale | undefined>>();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Load a date-fns locale, resolving from cache when it is already in memory.
|
|
106
|
+
* Concurrent callers for the same locale share one request.
|
|
107
|
+
*/
|
|
108
|
+
export function loadDateFnsLocale(locale: Locale): Promise<DateFnsLocale | undefined> {
|
|
109
|
+
const cached = cache.get(locale);
|
|
110
|
+
if (cached) {
|
|
111
|
+
return Promise.resolve(cached);
|
|
112
|
+
}
|
|
113
|
+
const pending = inFlight.get(locale);
|
|
114
|
+
if (pending) {
|
|
115
|
+
return pending;
|
|
116
|
+
}
|
|
117
|
+
const loader = loaders[locale];
|
|
118
|
+
if (!loader) {
|
|
119
|
+
// An unknown key behaves as it did when the whole barrel was imported:
|
|
120
|
+
// the lookup missed and date-fns fell back to its built-in English.
|
|
121
|
+
return Promise.resolve(undefined);
|
|
122
|
+
}
|
|
123
|
+
const promise = loader()
|
|
124
|
+
.then((loaded) => {
|
|
125
|
+
cache.set(locale, loaded);
|
|
126
|
+
return loaded;
|
|
127
|
+
})
|
|
128
|
+
.catch(() => undefined)
|
|
129
|
+
.finally(() => {
|
|
130
|
+
inFlight.delete(locale);
|
|
131
|
+
});
|
|
132
|
+
inFlight.set(locale, promise);
|
|
133
|
+
return promise;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The locale if it is already loaded, otherwise undefined. Never suspends. */
|
|
137
|
+
export function getLoadedDateFnsLocale(locale?: Locale): DateFnsLocale | undefined {
|
|
138
|
+
return locale ? cache.get(locale) : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The date-fns locale for `locale`, loading it in the background on first use.
|
|
143
|
+
*
|
|
144
|
+
* Returns undefined until the chunk arrives, which formats the first paint with
|
|
145
|
+
* date-fns' built-in English and then re-renders. That is a change only for
|
|
146
|
+
* projects that set a locale, and only for the few hundred milliseconds before
|
|
147
|
+
* the chunk lands — in exchange for every project no longer waiting on 625KB.
|
|
148
|
+
*/
|
|
149
|
+
export function useDateFnsLocale(locale?: Locale): DateFnsLocale | undefined {
|
|
150
|
+
const [, forceUpdate] = useReducer((count: number) => count + 1, 0);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (!locale || cache.has(locale)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let active = true;
|
|
157
|
+
loadDateFnsLocale(locale).then((loaded) => {
|
|
158
|
+
if (active && loaded) {
|
|
159
|
+
forceUpdate();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return () => {
|
|
163
|
+
active = false;
|
|
164
|
+
};
|
|
165
|
+
}, [locale]);
|
|
166
|
+
|
|
167
|
+
return locale ? cache.get(locale) : undefined;
|
|
168
|
+
}
|
package/src/util/entities.ts
CHANGED
|
@@ -75,6 +75,8 @@ export function getDefaultValueForDataType(dataType: DataType) {
|
|
|
75
75
|
return [];
|
|
76
76
|
} else if (dataType === "map") {
|
|
77
77
|
return {};
|
|
78
|
+
} else if (dataType === "geopoint") {
|
|
79
|
+
return null;
|
|
78
80
|
} else {
|
|
79
81
|
return null;
|
|
80
82
|
}
|
|
@@ -137,7 +139,12 @@ export function sanitizeData<M extends Record<string, any>>
|
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
export function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M>): EntityReference {
|
|
140
|
-
|
|
142
|
+
if (!entity) {
|
|
143
|
+
throw new Error("getReferenceFrom: entity is null or undefined");
|
|
144
|
+
}
|
|
145
|
+
// Carry the segments the entity was loaded with, so a reference to an entity under a
|
|
146
|
+
// slash-bearing parent stays resolvable. Undefined when the entity does not have them.
|
|
147
|
+
return new EntityReference(entity.id, entity.path, entity.databaseId, entity.pathSegments);
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
export function traverseValuesProperties<M extends Record<string, any>>(
|
|
@@ -196,3 +203,11 @@ export function traverseValueProperty(inputValue: any,
|
|
|
196
203
|
|
|
197
204
|
return value;
|
|
198
205
|
}
|
|
206
|
+
|
|
207
|
+
export function isDataTypeFilterable(dataType: DataType, isPartOfArray = false) {
|
|
208
|
+
if (isPartOfArray) {
|
|
209
|
+
return ["string", "number", "date", "reference"].includes(dataType);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return ["string", "number", "boolean", "date", "reference", "array"].includes(dataType);
|
|
213
|
+
}
|
package/src/util/entity_cache.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { EntityReference, GeoPoint, Vector } from "../types";
|
|
2
2
|
import { isObject, isPlainObject } from "./objects";
|
|
3
|
+
import { encodeEntityId } from "./navigation_utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Key identifying one entity in a cache.
|
|
7
|
+
*
|
|
8
|
+
* The id is escaped so that a `(path, entityId)` pair maps to exactly one key. Without it
|
|
9
|
+
* `("a", "b/c")` and `("a/b", "c")` both flatten to `"a/b/c"`, and one entity's cached
|
|
10
|
+
* values are served for the other.
|
|
11
|
+
*
|
|
12
|
+
* `encodeEntityId` is the identity for any id without "/", "?", "#" or "%", so this changes
|
|
13
|
+
* no key in an app whose ids cannot contain them.
|
|
14
|
+
*/
|
|
15
|
+
export function entityCacheKey(path: string | undefined, entityId: string | undefined): string {
|
|
16
|
+
// Either part may be undefined at a call site that used to build the key by string
|
|
17
|
+
// concatenation; both are stringified exactly as `path + "/" + entityId` did, so such a
|
|
18
|
+
// caller keeps the key it had.
|
|
19
|
+
return `${path}/${entityId === undefined ? entityId : encodeEntityId(entityId)}`;
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
// Define a unique prefix for entity keys in localStorage to avoid key collisions
|
|
5
23
|
const LOCAL_STORAGE_PREFIX = "entity_cache::";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { GeoPoint } from "../types";
|
|
2
|
+
|
|
3
|
+
export type GeoPointLike = GeoPoint | {
|
|
4
|
+
latitude?: number;
|
|
5
|
+
longitude?: number;
|
|
6
|
+
lat?: number;
|
|
7
|
+
lng?: number;
|
|
8
|
+
_lat?: number;
|
|
9
|
+
_long?: number;
|
|
10
|
+
} | null | undefined;
|
|
11
|
+
|
|
12
|
+
export interface GeoPointCoordinates {
|
|
13
|
+
latitude: number;
|
|
14
|
+
longitude: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toNumber(value: any): number | undefined {
|
|
18
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getGeoPointCoordinates(value: GeoPointLike): GeoPointCoordinates | undefined {
|
|
22
|
+
if (!value) return undefined;
|
|
23
|
+
if (value instanceof GeoPoint) {
|
|
24
|
+
return { latitude: value.latitude, longitude: value.longitude };
|
|
25
|
+
}
|
|
26
|
+
if (typeof value !== "object") return undefined;
|
|
27
|
+
|
|
28
|
+
const latitude = toNumber((value as any).latitude ?? (value as any).lat ?? (value as any)._lat);
|
|
29
|
+
const longitude = toNumber((value as any).longitude ?? (value as any).lng ?? (value as any)._long);
|
|
30
|
+
|
|
31
|
+
if (latitude === undefined || longitude === undefined) return undefined;
|
|
32
|
+
|
|
33
|
+
return { latitude, longitude };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeGeoPoint(value: GeoPointLike): GeoPoint | undefined {
|
|
37
|
+
const coordinates = getGeoPointCoordinates(value);
|
|
38
|
+
if (!coordinates) return undefined;
|
|
39
|
+
if (value instanceof GeoPoint) return value;
|
|
40
|
+
return new GeoPoint(coordinates.latitude, coordinates.longitude);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatGeoPoint(value: GeoPointLike, options?: { maximumFractionDigits?: number }): string {
|
|
44
|
+
const coordinates = getGeoPointCoordinates(value);
|
|
45
|
+
if (!coordinates) return "";
|
|
46
|
+
|
|
47
|
+
const maximumFractionDigits = options?.maximumFractionDigits ?? 6;
|
|
48
|
+
// Coordinates are always formatted with a dot as decimal separator, independently of the
|
|
49
|
+
// user locale. Locale-aware formatting would use a comma in locales such as de or fr,
|
|
50
|
+
// making the "lat, lng" output ambiguous and impossible to parse back.
|
|
51
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
52
|
+
maximumFractionDigits,
|
|
53
|
+
minimumFractionDigits: Math.min(2, maximumFractionDigits),
|
|
54
|
+
useGrouping: false
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return `${formatter.format(coordinates.latitude)}, ${formatter.format(coordinates.longitude)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function parseGeoPoint(input: string): { point: GeoPoint | null; error?: string } {
|
|
61
|
+
const trimmed = input.trim();
|
|
62
|
+
if (!trimmed) return { point: null };
|
|
63
|
+
|
|
64
|
+
const parts = trimmed.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
65
|
+
if (parts.length !== 2) return { point: null, error: "Use \"lat, lng\" format" };
|
|
66
|
+
|
|
67
|
+
const latitude = parseFloat(parts[0]);
|
|
68
|
+
const longitude = parseFloat(parts[1]);
|
|
69
|
+
|
|
70
|
+
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
|
|
71
|
+
return { point: null, error: "Latitude and longitude must be numbers" };
|
|
72
|
+
}
|
|
73
|
+
if (latitude < -90 || latitude > 90) {
|
|
74
|
+
return { point: null, error: "Latitude must be between -90 and 90" };
|
|
75
|
+
}
|
|
76
|
+
if (longitude < -180 || longitude > 180) {
|
|
77
|
+
return { point: null, error: "Longitude must be between -180 and 180" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { point: new GeoPoint(latitude, longitude) };
|
|
81
|
+
}
|
package/src/util/index.ts
CHANGED
|
@@ -6,12 +6,14 @@ export * from "./enums";
|
|
|
6
6
|
export * from "./objects";
|
|
7
7
|
export * from "./paths";
|
|
8
8
|
export * from "./regexp";
|
|
9
|
+
export * from "./urls";
|
|
9
10
|
export * from "./navigation_utils";
|
|
10
11
|
export * from "./entity_actions";
|
|
11
12
|
export * from "./useDebouncedCallback";
|
|
12
13
|
export * from "./property_utils";
|
|
13
14
|
export * from "./resolutions";
|
|
14
15
|
export * from "./permissions";
|
|
16
|
+
export * from "./geopoint";
|
|
15
17
|
export * from "./icon_list";
|
|
16
18
|
export * from "./icon_synonyms";
|
|
17
19
|
export * from "./icons";
|
|
@@ -26,3 +28,4 @@ export * from "./storage";
|
|
|
26
28
|
export * from "./callbacks";
|
|
27
29
|
export * from "./conditions";
|
|
28
30
|
export * from "./lazy_eager";
|
|
31
|
+
export * from "./date_fns_locales";
|
package/src/util/lazy_eager.tsx
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Prefetch after the page has loaded, never during it.
|
|
5
|
+
*
|
|
6
|
+
* An idle callback scheduled at module evaluation time fires in the gaps between
|
|
7
|
+
* the tasks of the initial load — which is exactly when the critical chunks are
|
|
8
|
+
* still in flight. With a dozen route components each queueing one, the
|
|
9
|
+
* background prefetch was competing with the render path for connections and
|
|
10
|
+
* bandwidth. Waiting for the load event costs the prefetch nothing that matters:
|
|
11
|
+
* it exists to be ready before a navigation, not before first paint.
|
|
12
|
+
*/
|
|
13
|
+
function schedulePrefetch(load: () => void) {
|
|
14
|
+
const whenIdle = () => {
|
|
15
|
+
if ("requestIdleCallback" in window) {
|
|
16
|
+
(window as any).requestIdleCallback(load);
|
|
17
|
+
} else {
|
|
18
|
+
setTimeout(load, 500);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (document.readyState === "complete") {
|
|
23
|
+
whenIdle();
|
|
24
|
+
} else {
|
|
25
|
+
window.addEventListener("load", whenIdle, { once: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns a React.lazy component that is also preloaded in the background, once
|
|
31
|
+
* the page has finished loading.
|
|
32
|
+
* This ensures that chunks are split, but fetched before they are actually needed.
|
|
7
33
|
*/
|
|
8
34
|
export function lazyEager<T extends React.ComponentType<any>>(
|
|
9
35
|
factory: () => Promise<any>,
|
|
@@ -22,11 +48,7 @@ export function lazyEager<T extends React.ComponentType<any>>(
|
|
|
22
48
|
};
|
|
23
49
|
|
|
24
50
|
if (typeof window !== "undefined") {
|
|
25
|
-
|
|
26
|
-
(window as any).requestIdleCallback(load);
|
|
27
|
-
} else {
|
|
28
|
-
setTimeout(load, 500);
|
|
29
|
-
}
|
|
51
|
+
schedulePrefetch(load);
|
|
30
52
|
}
|
|
31
53
|
|
|
32
54
|
return React.lazy(load);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure predicate deciding whether an in-progress navigation away from an open
|
|
3
|
+
* entity form should be blocked (to warn about unsaved changes).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from FireCMSRoute so the branching can be unit-tested without
|
|
6
|
+
* mounting the router. This is an internal helper — it is intentionally not
|
|
7
|
+
* re-exported from the package index.
|
|
8
|
+
*/
|
|
9
|
+
export interface NavigationBlockLocation {
|
|
10
|
+
pathname: string;
|
|
11
|
+
hash: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SIDE_PANEL_HASHES = ["#side", "#new_side"];
|
|
15
|
+
|
|
16
|
+
export function shouldBlockEntityNavigation(params: {
|
|
17
|
+
currentLocation: NavigationBlockLocation;
|
|
18
|
+
nextLocation: NavigationBlockLocation;
|
|
19
|
+
/** Path of the currently open entity (basePath + "/" + entityId). */
|
|
20
|
+
entityPath: string;
|
|
21
|
+
/** Collection path the entity form lives under. */
|
|
22
|
+
basePath: string;
|
|
23
|
+
/** Whether the form currently has unsaved modifications. */
|
|
24
|
+
blocked: boolean;
|
|
25
|
+
}): boolean {
|
|
26
|
+
const { currentLocation, nextLocation, entityPath, basePath, blocked } = params;
|
|
27
|
+
|
|
28
|
+
// Navigating deeper within the same entity — never block.
|
|
29
|
+
if (nextLocation.pathname.startsWith(entityPath))
|
|
30
|
+
return false;
|
|
31
|
+
|
|
32
|
+
// Side panel overlay navigations preserve the underlying form via
|
|
33
|
+
// base_location in router state — no data is lost in either direction.
|
|
34
|
+
|
|
35
|
+
// Opening a side panel (e.g. clicking a relation/reference arrow).
|
|
36
|
+
if (SIDE_PANEL_HASHES.includes(nextLocation.hash))
|
|
37
|
+
return false;
|
|
38
|
+
|
|
39
|
+
// Closing a side panel (navigate(-1) back to the form's own path).
|
|
40
|
+
if (SIDE_PANEL_HASHES.includes(currentLocation.hash) &&
|
|
41
|
+
nextLocation.pathname === basePath)
|
|
42
|
+
return false;
|
|
43
|
+
|
|
44
|
+
return blocked;
|
|
45
|
+
}
|