@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.9408c41
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/popup_field/PopupFormField.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/useBoardDataController.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/common/useDataSourceTableController.d.ts +4 -2
- package/dist/components/common/useTableSearchHelper.d.ts +3 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/contexts/LicenseStatusContext.d.ts +16 -0
- package/dist/core/EntityEditView.d.ts +2 -0
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/plugin_translations.d.ts +38 -0
- package/dist/core/pro_plugins.d.ts +40 -0
- package/dist/de-DMp6cIY1.js +702 -0
- package/dist/de-DMp6cIY1.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-Cckz4IkR.js +702 -0
- package/dist/fr-Cckz4IkR.js.map +1 -0
- package/dist/hi-CDCoIqbu.js +702 -0
- package/dist/hi-CDCoIqbu.js.map +1 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/hooks/data/useEntityFetch.d.ts +4 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useLicenseStatus.d.ts +16 -0
- package/dist/hooks/useProjectLog.d.ts +11 -6
- package/dist/hooks/useTranslation.d.ts +1 -1
- package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
- package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
- package/dist/index.es.js +3113 -10785
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +7939 -6836
- package/dist/index.umd.js.map +1 -1
- package/dist/it-BJCUqixj.js +702 -0
- package/dist/it-BJCUqixj.js.map +1 -0
- package/dist/pl-DgT_joFf.js +704 -0
- package/dist/pl-DgT_joFf.js.map +1 -0
- package/dist/pt-B3dN3x0X.js +704 -0
- package/dist/pt-B3dN3x0X.js.map +1 -0
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +7 -0
- package/dist/types/datasource.d.ts +25 -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 +54 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +22 -1
- package/dist/types/side_entity_controller.d.ts +3 -2
- package/dist/types/translations.d.ts +35 -2
- package/dist/util/date_fns_locales.d.ts +18 -0
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/lazy_eager.d.ts +3 -3
- package/dist/util/navigation_utils.d.ts +84 -2
- package/dist/util/parent_references_from_path.d.ts +16 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- 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/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +49 -16
- 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/EntityPreview.tsx +4 -1
- package/src/components/ErrorBoundary.tsx +12 -18
- package/src/components/LicenseBanner.tsx +209 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/useDataSourceTableController.tsx +32 -8
- package/src/components/common/useTableSearchHelper.ts +5 -0
- package/src/components/index.tsx +1 -0
- package/src/contexts/LicenseStatusContext.tsx +50 -0
- package/src/core/DefaultAppBar.tsx +28 -16
- package/src/core/EntityEditView.tsx +11 -12
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +6 -4
- package/src/core/FireCMS.tsx +73 -47
- package/src/core/SideDialogs.tsx +44 -1
- package/src/core/index.tsx +1 -0
- package/src/core/plugin_translations.ts +87 -0
- package/src/core/pro_plugins.ts +79 -0
- package/src/editor/components/SlashCommandMenu.tsx +4 -1
- package/src/editor/extensions/Image.ts +17 -1
- package/src/editor/hooks/useProseMirror.ts +5 -0
- package/src/editor/index.ts +11 -1
- package/src/editor/lazy_editor.tsx +33 -0
- package/src/form/EntityForm.tsx +30 -9
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/hooks/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +5 -2
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +28 -6
- package/src/hooks/index.tsx +2 -0
- package/src/hooks/useBuildNavigationController.tsx +70 -15
- package/src/hooks/useFireCMSContext.tsx +7 -3
- package/src/hooks/useLicenseStatus.tsx +32 -0
- package/src/hooks/useProjectLog.tsx +27 -9
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
- package/src/hooks/useTranslation.ts +3 -1
- package/src/i18n/FireCMSi18nProvider.tsx +199 -25
- package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
- package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
- package/src/internal/useBuildDataSource.ts +26 -15
- package/src/internal/useBuildSideEntityController.tsx +13 -5
- package/src/locales/de.ts +18 -2
- package/src/locales/en.ts +21 -3
- package/src/locales/es.ts +18 -2
- package/src/locales/fr.ts +18 -2
- package/src/locales/hi.ts +18 -2
- package/src/locales/it.ts +18 -2
- package/src/locales/pl.ts +18 -2
- package/src/locales/pt.ts +18 -2
- package/src/preview/components/DatePreview.tsx +2 -4
- package/src/preview/components/ReferencePreview.tsx +7 -3
- package/src/routes/FireCMSRoute.tsx +9 -4
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +8 -0
- package/src/types/datasource.ts +25 -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 +62 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +23 -1
- package/src/types/side_entity_controller.tsx +3 -2
- package/src/types/translations.ts +35 -2
- package/src/util/date_fns_locales.ts +168 -0
- package/src/util/entities.ts +3 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/index.ts +1 -0
- package/src/util/lazy_eager.tsx +30 -8
- package/src/util/navigation_from_path.ts +6 -1
- package/src/util/navigation_utils.ts +210 -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/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::";
|
package/src/util/index.ts
CHANGED
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);
|
|
@@ -147,7 +147,12 @@ export function getNavigationEntriesFromPath(props: {
|
|
|
147
147
|
path: newPath,
|
|
148
148
|
collections: collection.subcollections,
|
|
149
149
|
currentFullPath: fullPath,
|
|
150
|
-
|
|
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,
|
|
151
156
|
currentFullUrlPath: fullUrlPath,
|
|
152
157
|
currentPathSegments: entitySegments,
|
|
153
158
|
contextEntityViews: props.contextEntityViews
|
|
@@ -56,6 +56,30 @@ export function decodeEntityId(encodedEntityId: string): string {
|
|
|
56
56
|
.replaceAll("%25", "%");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Segments of a subcollection sitting under the entity `entityId` of `parentPathSegments`.
|
|
61
|
+
*
|
|
62
|
+
* The parent entity id is kept whole however many slashes it contains, while the
|
|
63
|
+
* subcollection's own configured path is spread — a collection path never contains an
|
|
64
|
+
* entity id, so splitting that one is unambiguous.
|
|
65
|
+
*
|
|
66
|
+
* So `["test"] + "test/test" + "accommodation"` yields three segments,
|
|
67
|
+
* `["test", "test/test", "accommodation"]`, where splitting the flattened path
|
|
68
|
+
* `"test/test/test/accommodation"` would wrongly yield four.
|
|
69
|
+
*
|
|
70
|
+
* Returns undefined when the parent segments are unknown: the parent chain cannot be
|
|
71
|
+
* recovered from a flattened path, so there is nothing honest to build on.
|
|
72
|
+
*
|
|
73
|
+
* @group Hooks and utilities
|
|
74
|
+
*/
|
|
75
|
+
export function buildSubcollectionPathSegments(parentPathSegments: string[] | undefined,
|
|
76
|
+
entityId: string | undefined,
|
|
77
|
+
subcollectionPath: string): string[] | undefined {
|
|
78
|
+
if (!parentPathSegments || !entityId) return undefined;
|
|
79
|
+
const ownSegments = removeInitialAndTrailingSlashes(subcollectionPath).split("/");
|
|
80
|
+
return [...parentPathSegments, entityId, ...ownSegments];
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
export function getLastSegment(path: string) {
|
|
60
84
|
const cleanPath = removeInitialAndTrailingSlashes(path);
|
|
61
85
|
if (cleanPath.includes("/")) {
|
|
@@ -65,7 +89,171 @@ export function getLastSegment(path: string) {
|
|
|
65
89
|
return cleanPath;
|
|
66
90
|
}
|
|
67
91
|
|
|
68
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The outcome of walking a segment chain against a collection tree.
|
|
94
|
+
* @group Hooks and utilities
|
|
95
|
+
*/
|
|
96
|
+
export type PathSegmentsWalkStep = {
|
|
97
|
+
collection: EntityCollection;
|
|
98
|
+
/** Resolved path of this collection, e.g. `"products/pid/locales"`. */
|
|
99
|
+
collectionPath: string;
|
|
100
|
+
/** `collectionPath` split at its real segment boundaries. */
|
|
101
|
+
collectionSegments: string[];
|
|
102
|
+
/** The entity addressed inside it, when the chain continues past the collection. */
|
|
103
|
+
entityId?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type PathSegmentsWalk = {
|
|
107
|
+
/** Segments with every collection id replaced by the collection's real `path`. */
|
|
108
|
+
resolved: string[];
|
|
109
|
+
/** The collection the chain ends in, if the whole chain matched. */
|
|
110
|
+
collection?: EntityCollection;
|
|
111
|
+
/** One entry per collection level traversed, outermost first. */
|
|
112
|
+
steps: PathSegmentsWalkStep[];
|
|
113
|
+
/** The first segment that could not be matched, if the walk stopped early. */
|
|
114
|
+
unmatched?: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Walk a segment chain against a collection tree, matching a collection by either its
|
|
119
|
+
* `path` or its `id` and treating exactly one segment as each entity id.
|
|
120
|
+
*
|
|
121
|
+
* This is the primitive the string-based helpers cannot have: working on the flattened
|
|
122
|
+
* path they must *guess* where an entity id ends by reading up to the next "/", which is
|
|
123
|
+
* wrong for any id that contains one. Here the boundaries are given.
|
|
124
|
+
*
|
|
125
|
+
* Stops at the first segment it cannot match and reports it, rather than throwing.
|
|
126
|
+
*
|
|
127
|
+
* @group Hooks and utilities
|
|
128
|
+
*/
|
|
129
|
+
export function walkPathSegments(pathSegments: string[], allCollections: EntityCollection[]): PathSegmentsWalk {
|
|
130
|
+
|
|
131
|
+
const resolved: string[] = [];
|
|
132
|
+
const steps: PathSegmentsWalkStep[] = [];
|
|
133
|
+
let currentCollections: EntityCollection[] | undefined = allCollections;
|
|
134
|
+
let index = 0;
|
|
135
|
+
|
|
136
|
+
while (index < pathSegments.length) {
|
|
137
|
+
|
|
138
|
+
const remaining = pathSegments.slice(index);
|
|
139
|
+
|
|
140
|
+
if (!currentCollections || currentCollections.length === 0) {
|
|
141
|
+
resolved.push(...remaining);
|
|
142
|
+
return {
|
|
143
|
+
resolved,
|
|
144
|
+
steps,
|
|
145
|
+
unmatched: remaining[0]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A collection's own `path` may span several segments ("users/uid/experiences"), so
|
|
150
|
+
// candidates are matched segment by segment and the longest match wins.
|
|
151
|
+
let match: { collection: EntityCollection, length: number } | undefined;
|
|
152
|
+
for (const collection of currentCollections) {
|
|
153
|
+
for (const candidate of [collection.path, collection.id]) {
|
|
154
|
+
if (!candidate) continue;
|
|
155
|
+
const parts = removeInitialAndTrailingSlashes(candidate).split("/");
|
|
156
|
+
if (parts.length > remaining.length) continue;
|
|
157
|
+
if (parts.some((part, i) => part !== remaining[i])) continue;
|
|
158
|
+
if (!match || parts.length > match.length) match = { collection, length: parts.length };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!match) {
|
|
163
|
+
resolved.push(...remaining);
|
|
164
|
+
return {
|
|
165
|
+
resolved,
|
|
166
|
+
steps,
|
|
167
|
+
unmatched: remaining[0]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
resolved.push(...removeInitialAndTrailingSlashes(match.collection.path).split("/"));
|
|
172
|
+
index += match.length;
|
|
173
|
+
|
|
174
|
+
const step: PathSegmentsWalkStep = {
|
|
175
|
+
collection: match.collection,
|
|
176
|
+
collectionPath: resolved.join("/"),
|
|
177
|
+
collectionSegments: [...resolved]
|
|
178
|
+
};
|
|
179
|
+
steps.push(step);
|
|
180
|
+
|
|
181
|
+
// The chain ends on a collection, which is the one being addressed.
|
|
182
|
+
if (index >= pathSegments.length) {
|
|
183
|
+
return {
|
|
184
|
+
resolved,
|
|
185
|
+
steps,
|
|
186
|
+
collection: match.collection
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Exactly one segment is the entity id, however many slashes it contains.
|
|
191
|
+
step.entityId = pathSegments[index];
|
|
192
|
+
resolved.push(pathSegments[index]);
|
|
193
|
+
index += 1;
|
|
194
|
+
|
|
195
|
+
// The chain ends on an entity, which lives in the collection just matched.
|
|
196
|
+
if (index >= pathSegments.length) {
|
|
197
|
+
return {
|
|
198
|
+
resolved,
|
|
199
|
+
steps,
|
|
200
|
+
collection: match.collection
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
currentCollections = match.collection.subcollections;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { resolved, steps };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve collection aliases in a segment array: every collection id is replaced by the
|
|
212
|
+
* collection's real `path`, and every entity id is carried through untouched.
|
|
213
|
+
*
|
|
214
|
+
* The segment-wise counterpart of {@link resolveCollectionPathIds}, and the reason it
|
|
215
|
+
* exists: that one works on the flattened string, so an entity id containing "/" shifts
|
|
216
|
+
* the rest of the chain by a segment and resolution fails with
|
|
217
|
+
* "Collection definition not found for segment starting with …".
|
|
218
|
+
*
|
|
219
|
+
* Matching accepts either a collection's `path` or its `id`, so already-resolved segments
|
|
220
|
+
* pass through unchanged — the function is idempotent.
|
|
221
|
+
*
|
|
222
|
+
* Mirrors the string version when a chain cannot be matched: it warns and carries the
|
|
223
|
+
* remainder through unresolved, rather than throwing.
|
|
224
|
+
*
|
|
225
|
+
* @group Hooks and utilities
|
|
226
|
+
*/
|
|
227
|
+
export function resolveCollectionPathSegments(pathSegments: string[], allCollections: EntityCollection[]): string[] {
|
|
228
|
+
const walk = walkPathSegments(pathSegments, allCollections);
|
|
229
|
+
if (walk.unmatched !== undefined) {
|
|
230
|
+
console.warn(`resolveCollectionPathSegments: Collection definition not found for segment "${walk.unmatched}" in [${pathSegments.join(", ")}]. Carrying the remaining segments through unresolved.`);
|
|
231
|
+
}
|
|
232
|
+
return walk.resolved;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Find the collection a segment chain addresses, whether it ends on the collection itself
|
|
237
|
+
* or on one of its entities.
|
|
238
|
+
*
|
|
239
|
+
* The segment-wise counterpart of {@link getCollectionByPathOrId}, which asserts an odd
|
|
240
|
+
* number of "/"-separated parts — an assertion a slash-bearing entity id fails, throwing
|
|
241
|
+
* where it should simply have resolved.
|
|
242
|
+
*
|
|
243
|
+
* @group Hooks and utilities
|
|
244
|
+
*/
|
|
245
|
+
export function getCollectionByPathSegments(pathSegments: string[], collections: EntityCollection[]): EntityCollection | undefined {
|
|
246
|
+
return walkPathSegments(pathSegments, collections).collection;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function resolveCollectionPathIds(path: string, allCollections: EntityCollection[], pathSegments?: string[]): string {
|
|
250
|
+
|
|
251
|
+
// When the caller knows the real segment boundaries there is nothing to parse: the
|
|
252
|
+
// ambiguity the string walk below has to guess its way through simply is not present.
|
|
253
|
+
if (pathSegments) {
|
|
254
|
+
return resolveCollectionPathSegments(pathSegments, allCollections).join("/");
|
|
255
|
+
}
|
|
256
|
+
|
|
69
257
|
let remainingPath = removeInitialAndTrailingSlashes(path);
|
|
70
258
|
if (!remainingPath) {
|
|
71
259
|
return "";
|
|
@@ -93,7 +281,12 @@ export function resolveCollectionPathIds(path: string, allCollections: EntityCol
|
|
|
93
281
|
col,
|
|
94
282
|
match: col.id
|
|
95
283
|
}])
|
|
96
|
-
|
|
284
|
+
// A match has to end where a path segment ends. Matching any prefix made
|
|
285
|
+
// "products-test" resolve to the "products" collection with an entity called
|
|
286
|
+
// "-test" (FirebaseError: Invalid document reference), and any collection whose
|
|
287
|
+
// path merely starts with another's — "products_archive", which the collection
|
|
288
|
+
// editor generates from "Products archive" — resolve to the wrong one.
|
|
289
|
+
.filter(p => p.match && (remainingPath === p.match || remainingPath.startsWith(p.match + "/")))
|
|
97
290
|
.sort((a, b) => b.match.length - a.match.length);
|
|
98
291
|
|
|
99
292
|
if (potentialMatches.length > 0) {
|
|
@@ -158,7 +351,14 @@ export function resolveCollectionPathIds(path: string, allCollections: EntityCol
|
|
|
158
351
|
* @param pathOrId
|
|
159
352
|
* @param collections
|
|
160
353
|
*/
|
|
161
|
-
export function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[]): EntityCollection | undefined {
|
|
354
|
+
export function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[], pathSegments?: string[]): EntityCollection | undefined {
|
|
355
|
+
|
|
356
|
+
// Given the real boundaries there is nothing to parse, and no parity to assert: a chain
|
|
357
|
+
// whose entity id contains "/" splits into an even number of parts below and would be
|
|
358
|
+
// rejected outright, even though it is perfectly valid.
|
|
359
|
+
if (pathSegments) {
|
|
360
|
+
return getCollectionByPathSegments(pathSegments, collections);
|
|
361
|
+
}
|
|
162
362
|
|
|
163
363
|
const subpaths = removeInitialAndTrailingSlashes(pathOrId).split("/");
|
|
164
364
|
if (subpaths.length % 2 === 0) {
|
|
@@ -249,6 +449,13 @@ export function navigateToEntity({
|
|
|
249
449
|
});
|
|
250
450
|
|
|
251
451
|
} else {
|
|
452
|
+
// A URL must be built from the ESCAPED chain. `fullIdPath` is that chain; `path` is
|
|
453
|
+
// the raw datasource one, and falling back to it is only safe while no id in it
|
|
454
|
+
// contains "/". When the segments show that it does, the resulting URL would address
|
|
455
|
+
// a different entity, so say so instead of navigating somewhere wrong in silence.
|
|
456
|
+
if (!fullIdPath && pathSegments?.some(segment => segment.includes("/"))) {
|
|
457
|
+
console.warn(`navigateToEntity: no "fullIdPath" was given and "${path}" contains an entity id with a "/", so no unambiguous URL can be built for it. The link will point at a different location. Pass "fullIdPath" — the escaped chain — alongside "path".`);
|
|
458
|
+
}
|
|
252
459
|
let to = navigation.buildUrlCollectionPath(entityId ? `${fullIdPath ?? path}/${encodeEntityId(entityId)}` : fullIdPath ?? path);
|
|
253
460
|
if (entityId && selectedTab) {
|
|
254
461
|
to += `/${selectedTab}`;
|
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
import { EntityCollection, EntityReference } from "../types";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
decodeEntityId,
|
|
4
|
+
getCollectionPathsCombinations,
|
|
5
|
+
removeInitialAndTrailingSlashes,
|
|
6
|
+
walkPathSegments
|
|
7
|
+
} from "./navigation_utils";
|
|
3
8
|
|
|
9
|
+
/**
|
|
10
|
+
* References to every parent entity in a path.
|
|
11
|
+
*
|
|
12
|
+
* NOTE on encoding: `path` is expected ESCAPED, because it comes from the URL — the entity
|
|
13
|
+
* ids in it are decoded on the way out. Pass `pathSegments` instead whenever the path at
|
|
14
|
+
* hand is a raw datasource path (`entity.path`), which is not escaped and whose ids may
|
|
15
|
+
* therefore contain a bare "/": splitting that would silently produce the wrong references.
|
|
16
|
+
*/
|
|
4
17
|
export function getParentReferencesFromPath(props: {
|
|
5
18
|
path: string,
|
|
6
19
|
collections: EntityCollection[] | undefined,
|
|
7
20
|
currentFullPath?: string,
|
|
21
|
+
currentPathSegments?: string[],
|
|
22
|
+
/** `path` split at its real segment boundaries, with RAW ids. Takes precedence. */
|
|
23
|
+
pathSegments?: string[],
|
|
8
24
|
}): EntityReference [] {
|
|
9
25
|
|
|
10
26
|
const {
|
|
11
27
|
path,
|
|
12
28
|
collections = [],
|
|
13
29
|
currentFullPath,
|
|
30
|
+
currentPathSegments = [],
|
|
31
|
+
pathSegments,
|
|
14
32
|
} = props;
|
|
15
33
|
|
|
34
|
+
if (pathSegments) {
|
|
35
|
+
return getParentReferencesFromPathSegments(pathSegments, collections);
|
|
36
|
+
}
|
|
37
|
+
|
|
16
38
|
const subpaths = removeInitialAndTrailingSlashes(path).split("/");
|
|
17
39
|
const subpathCombinations = getCollectionPathsCombinations(subpaths);
|
|
18
40
|
|
|
@@ -27,6 +49,9 @@ export function getParentReferencesFromPath(props: {
|
|
|
27
49
|
const collectionPath = currentFullPath && currentFullPath.length > 0
|
|
28
50
|
? (currentFullPath + "/" + collection.path)
|
|
29
51
|
: collection.path;
|
|
52
|
+
// A collection's own path may span several segments, and those are
|
|
53
|
+
// unambiguous, so they are spread rather than kept whole.
|
|
54
|
+
const collectionSegments = [...currentPathSegments, ...collection.path.split("/")];
|
|
30
55
|
|
|
31
56
|
const restOfThePath = removeInitialAndTrailingSlashes(removeInitialAndTrailingSlashes(path).replace(subpathCombination, ""));
|
|
32
57
|
const nextSegments = restOfThePath.length > 0 ? restOfThePath.split("/") : [];
|
|
@@ -35,7 +60,9 @@ export function getParentReferencesFromPath(props: {
|
|
|
35
60
|
// datasource-facing, so they carry the raw id.
|
|
36
61
|
const entityId = decodeEntityId(nextSegments[0]);
|
|
37
62
|
const fullPath = collectionPath + "/" + entityId;
|
|
38
|
-
|
|
63
|
+
// The id is ONE segment however many slashes it contains.
|
|
64
|
+
const entitySegments = [...collectionSegments, entityId];
|
|
65
|
+
result.push(new EntityReference(entityId, collectionPath, undefined, collectionSegments));
|
|
39
66
|
if (nextSegments.length > 1) {
|
|
40
67
|
const newPath = nextSegments.slice(1).join("/");
|
|
41
68
|
if (!collection) {
|
|
@@ -45,7 +72,8 @@ export function getParentReferencesFromPath(props: {
|
|
|
45
72
|
result.push(...getParentReferencesFromPath({
|
|
46
73
|
path: newPath,
|
|
47
74
|
collections: collection.subcollections,
|
|
48
|
-
currentFullPath: fullPath
|
|
75
|
+
currentFullPath: fullPath,
|
|
76
|
+
currentPathSegments: entitySegments
|
|
49
77
|
}));
|
|
50
78
|
}
|
|
51
79
|
}
|
|
@@ -56,3 +84,14 @@ export function getParentReferencesFromPath(props: {
|
|
|
56
84
|
}
|
|
57
85
|
return result;
|
|
58
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The segment-wise counterpart: the boundaries are given, so an entity id keeps its
|
|
90
|
+
* slashes and nothing has to be decoded.
|
|
91
|
+
*/
|
|
92
|
+
export function getParentReferencesFromPathSegments(pathSegments: string[],
|
|
93
|
+
collections: EntityCollection[]): EntityReference[] {
|
|
94
|
+
return walkPathSegments(pathSegments, collections).steps
|
|
95
|
+
.filter(step => step.entityId !== undefined)
|
|
96
|
+
.map(step => new EntityReference(step.entityId!, step.collectionPath, undefined, step.collectionSegments));
|
|
97
|
+
}
|
package/src/util/paths.ts
CHANGED
|
@@ -18,10 +18,47 @@ export function segmentsToStrippedPath(paths: string[]) {
|
|
|
18
18
|
/**
|
|
19
19
|
* Extract the collection path routes
|
|
20
20
|
* `products/B44RG6APH/locales` => [`products`, `locales`]
|
|
21
|
+
*
|
|
22
|
+
* Finds the entity ids by position, which only works while no id contains "/" — with one,
|
|
23
|
+
* every following segment shifts and the wrong elements are kept. Use
|
|
24
|
+
* {@link collectionSegmentsFrom} when the real boundaries are known.
|
|
25
|
+
*
|
|
21
26
|
* @param path
|
|
22
27
|
*/
|
|
23
28
|
export function fullPathToCollectionSegments(path: string): string[] {
|
|
24
|
-
return path
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
return positionalCollectionSegments(path);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The collection chain of an already-segmented path:
|
|
34
|
+
* `['products', 'B44RG6APH', 'locales']` => [`products`, `locales`]
|
|
35
|
+
*
|
|
36
|
+
* The counterpart of {@link fullPathToCollectionSegments} for callers that know the real
|
|
37
|
+
* segment boundaries. Kept as a separate function rather than an extra parameter on that
|
|
38
|
+
* one: these helpers are small enough to be passed point-free, and `paths.map(fn)` would
|
|
39
|
+
* then hand the array index in as the new argument.
|
|
40
|
+
*/
|
|
41
|
+
export function collectionSegmentsFrom(pathSegments: string[]): string[] {
|
|
42
|
+
return keepCollectionPositions(pathSegments);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The historical, positional reading of a flattened path:
|
|
47
|
+
* `products/B44RG6APH/locales` => [`products`, `locales`]
|
|
48
|
+
*
|
|
49
|
+
* This produces a **collection chain**, not a datasource `pathSegments` value, and must
|
|
50
|
+
* never be used as one — see `path_segments_no_fabrication.test.ts`. It is only correct
|
|
51
|
+
* while no entity id contains "/", which is why every caller prefers real segments when
|
|
52
|
+
* they are available, and why it is named for what it assumes.
|
|
53
|
+
*
|
|
54
|
+
* It is still the right reading for a path that came from a URL, where entity ids arrive
|
|
55
|
+
* escaped and so cannot contain a bare "/".
|
|
56
|
+
*/
|
|
57
|
+
export function positionalCollectionSegments(path: string): string[] {
|
|
58
|
+
return keepCollectionPositions(path.split("/"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Keep the even positions of an alternating collection/entity-id chain. */
|
|
62
|
+
function keepCollectionPositions(segments: string[]): string[] {
|
|
63
|
+
return segments.filter((e, i) => i % 2 === 0);
|
|
27
64
|
}
|