@firecms/core 3.4.0-canary.f6a889a → 3.4.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/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/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/core/EntityEditView.d.ts +9 -1
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +8 -1
- package/dist/form/EntityFormActions.d.ts +2 -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/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 +1753 -1170
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1751 -1168
- package/dist/index.umd.js.map +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/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +13 -0
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +7 -0
- package/dist/types/translations.d.ts +6 -0
- 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/entity_cache.d.ts +11 -0
- package/dist/util/index.d.ts +1 -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 +4 -4
- 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/EntityPreview.tsx +4 -1
- package/src/components/ErrorBoundary.tsx +12 -18
- 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/contexts/SnackbarProvider.tsx +14 -1
- package/src/core/EntityEditView.tsx +20 -3
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +8 -4
- 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/useBuildNavigationController.tsx +23 -8
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +48 -4
- 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 +5 -0
- package/src/locales/en.ts +5 -0
- package/src/locales/es.ts +5 -0
- package/src/locales/fr.ts +5 -0
- package/src/locales/hi.ts +5 -0
- package/src/locales/it.ts +5 -0
- package/src/locales/pl.ts +5 -0
- package/src/locales/pt.ts +5 -0
- 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/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/navigation.ts +35 -5
- package/src/types/plugins.tsx +13 -0
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +7 -0
- package/src/types/translations.ts +6 -0
- 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/entities.ts +3 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/index.ts +1 -0
- package/src/util/navigation_from_path.ts +25 -2
- package/src/util/navigation_utils.ts +208 -2
- 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/types/plugins.tsx
CHANGED
|
@@ -146,6 +146,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
146
146
|
blockSearch?: (props: {
|
|
147
147
|
context: FireCMSContext,
|
|
148
148
|
path: string,
|
|
149
|
+
/** `path` split at its real segment boundaries, when known. */
|
|
150
|
+
pathSegments?: string[],
|
|
149
151
|
collection: EC,
|
|
150
152
|
parentCollectionIds?: string[]
|
|
151
153
|
}) => boolean;
|
|
@@ -153,6 +155,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
153
155
|
showTextSearchBar?: (props: {
|
|
154
156
|
context: FireCMSContext,
|
|
155
157
|
path: string,
|
|
158
|
+
/** `path` split at its real segment boundaries, when known. */
|
|
159
|
+
pathSegments?: string[],
|
|
156
160
|
collection: EC,
|
|
157
161
|
parentCollectionIds?: string[]
|
|
158
162
|
}) => boolean;
|
|
@@ -160,6 +164,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
160
164
|
onTextSearchClick?: (props: {
|
|
161
165
|
context: FireCMSContext,
|
|
162
166
|
path: string,
|
|
167
|
+
/** `path` split at its real segment boundaries, when known. */
|
|
168
|
+
pathSegments?: string[],
|
|
163
169
|
collection: EC,
|
|
164
170
|
parentCollectionIds?: string[]
|
|
165
171
|
}) => Promise<boolean>;
|
|
@@ -308,6 +314,13 @@ export interface PluginHomePageActionsProps<EP extends object = object, M extend
|
|
|
308
314
|
export interface PluginFormActionProps<USER extends User = User, EC extends EntityCollection = EntityCollection> {
|
|
309
315
|
entityId?: string;
|
|
310
316
|
path: string;
|
|
317
|
+
/**
|
|
318
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
319
|
+
*
|
|
320
|
+
* Optional, and never derived by splitting `path` — a parent entity id may itself
|
|
321
|
+
* contain "/". Absent means "not known here", not "no slashes".
|
|
322
|
+
*/
|
|
323
|
+
pathSegments?: string[];
|
|
311
324
|
parentCollectionIds: string[];
|
|
312
325
|
status: EntityStatus;
|
|
313
326
|
collection: EC;
|
package/src/types/properties.ts
CHANGED
|
@@ -562,6 +562,22 @@ export type PropertyBuilderProps<M extends Record<string, any> = any> =
|
|
|
562
562
|
* Current value of this property
|
|
563
563
|
*/
|
|
564
564
|
propertyValue?: any;
|
|
565
|
+
/**
|
|
566
|
+
* Path of this property within the entity, in dot notation, e.g.
|
|
567
|
+
* `my_map.my_field` or `my_array.0.my_field`.
|
|
568
|
+
* Useful to look up sibling values in `values` with `getValueInPath`.
|
|
569
|
+
*
|
|
570
|
+
* When a property builder is used as the `of` prop of an array property,
|
|
571
|
+
* it is resolved twice: once per existing element, with an index
|
|
572
|
+
* qualified key (`my_array.0`), and once as the template for the element
|
|
573
|
+
* shape, with the array's own key (`my_array`). In the latter case
|
|
574
|
+
* `propertyValue` is the whole array, since the template is not bound to
|
|
575
|
+
* any element.
|
|
576
|
+
*
|
|
577
|
+
* It may be undefined if the property is resolved outside the context of
|
|
578
|
+
* an entity.
|
|
579
|
+
*/
|
|
580
|
+
propertyKey?: string;
|
|
565
581
|
/**
|
|
566
582
|
* Index of this property (only for arrays)
|
|
567
583
|
*/
|
|
@@ -592,6 +608,7 @@ export type PropertyBuilder<T extends CMSType = any, M extends Record<string, an
|
|
|
592
608
|
values,
|
|
593
609
|
previousValues,
|
|
594
610
|
propertyValue,
|
|
611
|
+
propertyKey,
|
|
595
612
|
index,
|
|
596
613
|
path,
|
|
597
614
|
entityId,
|
|
@@ -19,6 +19,13 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
|
|
|
19
19
|
* Full CMS path of the entity, including the collection and sub-collections.
|
|
20
20
|
*/
|
|
21
21
|
fullIdPath?: string;
|
|
22
|
+
/**
|
|
23
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
24
|
+
*
|
|
25
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
26
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
27
|
+
*/
|
|
28
|
+
pathSegments?: string[];
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
* ID of the entity, if not set, it means we are creating a new entity
|
|
@@ -419,6 +419,8 @@ export interface FireCMSTranslations {
|
|
|
419
419
|
download: string;
|
|
420
420
|
large_number_of_documents: string;
|
|
421
421
|
include_undefined_values: string;
|
|
422
|
+
/** Toggle in the export dialog, only shown when a filter or sort is active */
|
|
423
|
+
export_apply_filter_sort: string;
|
|
422
424
|
submit: string;
|
|
423
425
|
|
|
424
426
|
no_filterable_properties: string;
|
|
@@ -620,6 +622,10 @@ export interface FireCMSTranslations {
|
|
|
620
622
|
order_property_not_found: string;
|
|
621
623
|
no_number_properties: string;
|
|
622
624
|
order_property_description: string;
|
|
625
|
+
image_property: string;
|
|
626
|
+
image_property_not_found: string;
|
|
627
|
+
no_image_properties: string;
|
|
628
|
+
image_property_description: string;
|
|
623
629
|
display_settings: string;
|
|
624
630
|
default_row_size: string;
|
|
625
631
|
side_dialog_width: string;
|
|
@@ -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
|
|
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
|
@@ -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
|
}
|