@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
|
@@ -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
|
@@ -142,7 +142,9 @@ export function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M
|
|
|
142
142
|
if (!entity) {
|
|
143
143
|
throw new Error("getReferenceFrom: entity is null or undefined");
|
|
144
144
|
}
|
|
145
|
-
|
|
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);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
export function traverseValuesProperties<M extends Record<string, any>>(
|
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::";
|
|
@@ -7,8 +25,16 @@ const LOCAL_STORAGE_PREFIX = "entity_cache::";
|
|
|
7
25
|
// In-memory cache to store entities for quick access
|
|
8
26
|
const entityCache: Map<string, object> = new Map();
|
|
9
27
|
|
|
10
|
-
// Check `localStorage` availability once during initialization
|
|
11
|
-
|
|
28
|
+
// Check `localStorage` availability once during initialization. Reading it
|
|
29
|
+
// throws where the browser blocks site data, which would fail this module's
|
|
30
|
+
// import and take the whole CMS down with it.
|
|
31
|
+
const isLocalStorageAvailable = (() => {
|
|
32
|
+
try {
|
|
33
|
+
return typeof localStorage !== "undefined";
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
12
38
|
|
|
13
39
|
// Define custom replacer for JSON.stringify
|
|
14
40
|
function customReplacer(key: string): any {
|
package/src/util/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ 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";
|
|
@@ -27,3 +28,4 @@ export * from "./storage";
|
|
|
27
28
|
export * from "./callbacks";
|
|
28
29
|
export * from "./conditions";
|
|
29
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,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `localStorage` access that never throws.
|
|
3
|
+
*
|
|
4
|
+
* Merely reading `window.localStorage` throws a SecurityError when the browser
|
|
5
|
+
* blocks site data (all cookies blocked, some sandboxed iframes), and `setItem`
|
|
6
|
+
* throws when the quota is full. Nothing the CMS keeps there is worth failing a
|
|
7
|
+
* render over, so a failed read counts as "nothing stored" and a failed write
|
|
8
|
+
* is dropped.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export function getLocalStorageItem(key: string): string | null {
|
|
13
|
+
try {
|
|
14
|
+
return typeof window !== "undefined" ? window.localStorage.getItem(key) : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The stored JSON value for `key`, or `fallback` when there is none, it is not
|
|
22
|
+
* valid JSON, or storage cannot be read.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export function getLocalStorageJSON<T>(key: string, fallback: T): T {
|
|
27
|
+
const item = getLocalStorageItem(key);
|
|
28
|
+
if (!item) return fallback;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(item);
|
|
31
|
+
} catch {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @internal */
|
|
37
|
+
export function setLocalStorageItem(key: string, value: string): void {
|
|
38
|
+
try {
|
|
39
|
+
if (typeof window !== "undefined") window.localStorage.setItem(key, value);
|
|
40
|
+
} catch {
|
|
41
|
+
// Blocked or full: the value lives only as long as the page.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @internal */
|
|
46
|
+
export function removeLocalStorageItem(key: string): void {
|
|
47
|
+
try {
|
|
48
|
+
if (typeof window !== "undefined") window.localStorage.removeItem(key);
|
|
49
|
+
} catch {
|
|
50
|
+
// Blocked: nothing was stored to remove.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -13,6 +13,8 @@ export interface NavigationViewEntityInternal<M extends Record<string, any>> {
|
|
|
13
13
|
path: string;
|
|
14
14
|
fullIdPath: string;
|
|
15
15
|
fullPath: string;
|
|
16
|
+
/** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
|
|
17
|
+
pathSegments: string[];
|
|
16
18
|
parentCollection: EntityCollection<M>;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -22,6 +24,8 @@ export interface NavigationViewCollectionInternal<M extends Record<string, any>>
|
|
|
22
24
|
path: string;
|
|
23
25
|
fullIdPath: string;
|
|
24
26
|
fullPath: string;
|
|
27
|
+
/** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
|
|
28
|
+
pathSegments: string[];
|
|
25
29
|
collection: EntityCollection<M>;
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -30,6 +34,8 @@ export interface NavigationViewEntityCustomInternal<M extends Record<string, any
|
|
|
30
34
|
path: string;
|
|
31
35
|
fullIdPath: string;
|
|
32
36
|
fullPath: string;
|
|
37
|
+
/** `path` split at its real boundaries — see `pathSegments` on the datasource props. */
|
|
38
|
+
pathSegments: string[];
|
|
33
39
|
entityId: string;
|
|
34
40
|
view: EntityCustomView<M>;
|
|
35
41
|
}
|
|
@@ -49,6 +55,7 @@ export function getNavigationEntriesFromPath(props: {
|
|
|
49
55
|
currentFullPath?: string,
|
|
50
56
|
currentFullIdPath?: string,
|
|
51
57
|
currentFullUrlPath?: string,
|
|
58
|
+
currentPathSegments?: string[],
|
|
52
59
|
contextEntityViews?: EntityCustomView<any>[]
|
|
53
60
|
}): NavigationViewInternal [] {
|
|
54
61
|
|
|
@@ -57,7 +64,8 @@ export function getNavigationEntriesFromPath(props: {
|
|
|
57
64
|
collections = [],
|
|
58
65
|
currentFullPath,
|
|
59
66
|
currentFullIdPath,
|
|
60
|
-
currentFullUrlPath
|
|
67
|
+
currentFullUrlPath,
|
|
68
|
+
currentPathSegments = []
|
|
61
69
|
} = props;
|
|
62
70
|
|
|
63
71
|
const subpaths = removeInitialAndTrailingSlashes(path).split("/");
|
|
@@ -83,12 +91,16 @@ export function getNavigationEntriesFromPath(props: {
|
|
|
83
91
|
const fullIdPath = currentFullIdPath && currentFullIdPath.length > 0
|
|
84
92
|
? (currentFullIdPath + "/" + collection.id)
|
|
85
93
|
: collection.id;
|
|
94
|
+
// A collection `path` may itself span several segments (e.g. "users/uid/experiences"),
|
|
95
|
+
// and those are unambiguous, so they are spread rather than kept whole.
|
|
96
|
+
const collectionSegments = [...currentPathSegments, ...collection.path.split("/")];
|
|
86
97
|
result.push({
|
|
87
98
|
type: "collection",
|
|
88
99
|
id: collection.id,
|
|
89
100
|
path: collectionPath,
|
|
90
101
|
fullPath: collectionUrlPath,
|
|
91
102
|
fullIdPath,
|
|
103
|
+
pathSegments: collectionSegments,
|
|
92
104
|
collection
|
|
93
105
|
});
|
|
94
106
|
const restOfThePath = removeInitialAndTrailingSlashes(removeInitialAndTrailingSlashes(path).replace(subpathCombination, ""));
|
|
@@ -98,12 +110,16 @@ export function getNavigationEntriesFromPath(props: {
|
|
|
98
110
|
const entityId = decodeEntityId(encodedEntityId);
|
|
99
111
|
const fullPath = collectionPath + "/" + entityId;
|
|
100
112
|
const fullUrlPath = collectionUrlPath + "/" + encodedEntityId;
|
|
113
|
+
// The id is ONE segment however many slashes it contains — that is the
|
|
114
|
+
// whole point of this array.
|
|
115
|
+
const entitySegments = [...collectionSegments, entityId];
|
|
101
116
|
result.push({
|
|
102
117
|
type: "entity",
|
|
103
118
|
entityId,
|
|
104
119
|
path: collectionPath,
|
|
105
120
|
fullIdPath,
|
|
106
121
|
fullPath: fullUrlPath,
|
|
122
|
+
pathSegments: collectionSegments,
|
|
107
123
|
parentCollection: collection
|
|
108
124
|
});
|
|
109
125
|
if (nextSegments.length > 1) {
|
|
@@ -123,6 +139,7 @@ export function getNavigationEntriesFromPath(props: {
|
|
|
123
139
|
entityId: entityId,
|
|
124
140
|
fullIdPath,
|
|
125
141
|
fullPath: fullUrlPath + "/" + customView.key,
|
|
142
|
+
pathSegments: collectionSegments,
|
|
126
143
|
view: customView
|
|
127
144
|
});
|
|
128
145
|
} else if (collection.subcollections) {
|
|
@@ -130,8 +147,14 @@ export function getNavigationEntriesFromPath(props: {
|
|
|
130
147
|
path: newPath,
|
|
131
148
|
collections: collection.subcollections,
|
|
132
149
|
currentFullPath: fullPath,
|
|
133
|
-
|
|
150
|
+
// The entity id is a hop in the id chain exactly as it is in the
|
|
151
|
+
// other two. Without it a nested `fullIdPath` was
|
|
152
|
+
// "products/locales" rather than "products/pid/locales", so any
|
|
153
|
+
// URL built from it pointed at a collection that does not exist.
|
|
154
|
+
// Escaped, because `fullIdPath` is URL-facing.
|
|
155
|
+
currentFullIdPath: fullIdPath + "/" + encodedEntityId,
|
|
134
156
|
currentFullUrlPath: fullUrlPath,
|
|
157
|
+
currentPathSegments: entitySegments,
|
|
135
158
|
contextEntityViews: props.contextEntityViews
|
|
136
159
|
}));
|
|
137
160
|
}
|