@rebasepro/common 0.0.1-canary.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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/collections/CollectionRegistry.d.ts +48 -0
- package/dist/collections/index.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.es.js +2380 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2379 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/util/arrays.d.ts +1 -0
- package/dist/util/builders.d.ts +64 -0
- package/dist/util/callbacks.d.ts +6 -0
- package/dist/util/collections.d.ts +11 -0
- package/dist/util/common.d.ts +2 -0
- package/dist/util/conditions.d.ts +26 -0
- package/dist/util/dates.d.ts +1 -0
- package/dist/util/entities.d.ts +28 -0
- package/dist/util/entity_actions.d.ts +2 -0
- package/dist/util/enums.d.ts +3 -0
- package/dist/util/fields.d.ts +2 -0
- package/dist/util/flatten_object.d.ts +5 -0
- package/dist/util/hash.d.ts +1 -0
- package/dist/util/index.d.ts +26 -0
- package/dist/util/names.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +29 -0
- package/dist/util/navigation_utils.d.ts +31 -0
- package/dist/util/objects.d.ts +26 -0
- package/dist/util/os.d.ts +2 -0
- package/dist/util/parent_references_from_path.d.ts +6 -0
- package/dist/util/paths.d.ts +14 -0
- package/dist/util/permissions.d.ts +5 -0
- package/dist/util/permissions.test.d.ts +1 -0
- package/dist/util/plurals.d.ts +16 -0
- package/dist/util/references.d.ts +2 -0
- package/dist/util/regexp.d.ts +7 -0
- package/dist/util/relations.d.ts +12 -0
- package/dist/util/resolutions.d.ts +74 -0
- package/dist/util/storage.d.ts +24 -0
- package/dist/util/strings.d.ts +7 -0
- package/package.json +118 -0
- package/src/collections/CollectionRegistry.ts +319 -0
- package/src/collections/index.ts +1 -0
- package/src/index.ts +2 -0
- package/src/util/arrays.ts +3 -0
- package/src/util/builders.ts +138 -0
- package/src/util/callbacks.ts +115 -0
- package/src/util/collections.ts +126 -0
- package/src/util/common.ts +2 -0
- package/src/util/conditions.ts +348 -0
- package/src/util/dates.ts +1 -0
- package/src/util/entities.ts +212 -0
- package/src/util/entity_actions.ts +28 -0
- package/src/util/enums.ts +26 -0
- package/src/util/fields.ts +28 -0
- package/src/util/flatten_object.ts +45 -0
- package/src/util/hash.ts +11 -0
- package/src/util/index.ts +26 -0
- package/src/util/names.ts +30 -0
- package/src/util/navigation_from_path.ts +121 -0
- package/src/util/navigation_utils.ts +222 -0
- package/src/util/objects.ts +376 -0
- package/src/util/os.ts +13 -0
- package/src/util/parent_references_from_path.ts +57 -0
- package/src/util/paths.ts +27 -0
- package/src/util/permissions.test.ts +716 -0
- package/src/util/permissions.ts +235 -0
- package/src/util/plurals.ts +188 -0
- package/src/util/references.ts +34 -0
- package/src/util/regexp.ts +32 -0
- package/src/util/relations.ts +211 -0
- package/src/util/resolutions.ts +383 -0
- package/src/util/storage.ts +144 -0
- package/src/util/strings.ts +84 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { EntityAction } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
const reservedKeys = ["edit", "copy", "delete"];
|
|
4
|
+
|
|
5
|
+
export function mergeEntityActions(currentActions: EntityAction[], newActions: EntityAction[]): EntityAction[] {
|
|
6
|
+
// given the current actions, replace the ones with the same key
|
|
7
|
+
// and append the new ones
|
|
8
|
+
const updatedActions: EntityAction[] = [];
|
|
9
|
+
currentActions.forEach(action => {
|
|
10
|
+
const newAction = newActions.find(a => a.key === action.key);
|
|
11
|
+
if (newAction) {
|
|
12
|
+
const mergedAction = {
|
|
13
|
+
...action,
|
|
14
|
+
...newAction
|
|
15
|
+
}
|
|
16
|
+
updatedActions.push(mergedAction);
|
|
17
|
+
} else {
|
|
18
|
+
updatedActions.push(action);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
newActions.forEach(action => {
|
|
22
|
+
if (!currentActions.find(a => a.key === action.key) && (!action.key || !reservedKeys.includes(action.key))) {
|
|
23
|
+
updatedActions.push(action);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return updatedActions;
|
|
27
|
+
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EnumValueConfig, EnumValues } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
export function enumToObjectEntries(enumValues: EnumValues): EnumValueConfig[] {
|
|
4
|
+
if (Array.isArray(enumValues)) {
|
|
5
|
+
return enumValues;
|
|
6
|
+
} else {
|
|
7
|
+
return Object.entries(enumValues).map(([id, value]) => {
|
|
8
|
+
if (typeof value === "string") {
|
|
9
|
+
return {
|
|
10
|
+
id,
|
|
11
|
+
label: value
|
|
12
|
+
}
|
|
13
|
+
} else {
|
|
14
|
+
return {
|
|
15
|
+
...value,
|
|
16
|
+
id
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getLabelOrConfigFrom(enumValues: EnumValueConfig[], key?: string | number): EnumValueConfig | undefined {
|
|
24
|
+
if (key === null || key === undefined) return undefined;
|
|
25
|
+
return enumValues.find((entry) => String(entry.id) === String(key));
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DefaultFieldConfig } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
export function isDefaultFieldConfigId(id: string): id is DefaultFieldConfig {
|
|
4
|
+
return ["text_field",
|
|
5
|
+
"multiline",
|
|
6
|
+
"markdown",
|
|
7
|
+
"url",
|
|
8
|
+
"email",
|
|
9
|
+
"switch",
|
|
10
|
+
"select",
|
|
11
|
+
"multi_select",
|
|
12
|
+
"number_input",
|
|
13
|
+
"number_select",
|
|
14
|
+
"multi_number_select",
|
|
15
|
+
"file_upload",
|
|
16
|
+
"multi_file_upload",
|
|
17
|
+
"reference_as_string",
|
|
18
|
+
"reference",
|
|
19
|
+
"multi_references",
|
|
20
|
+
"relation",
|
|
21
|
+
"date_time",
|
|
22
|
+
"group",
|
|
23
|
+
"key_value",
|
|
24
|
+
"repeat",
|
|
25
|
+
"custom_array",
|
|
26
|
+
"block"
|
|
27
|
+
].includes(id);
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function flattenObject(obj: Record<string, unknown>, parentKey = "") {
|
|
2
|
+
if (!obj) return obj;
|
|
3
|
+
return Object.keys(obj).reduce((flatObj, key) => {
|
|
4
|
+
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
|
5
|
+
|
|
6
|
+
if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
7
|
+
if (Array.isArray(obj[key])) {
|
|
8
|
+
obj[key].forEach((item: unknown, index: number) => {
|
|
9
|
+
Object.assign(flatObj, flattenObject(item as Record<string, unknown>, `${newKey}[${index}]`));
|
|
10
|
+
});
|
|
11
|
+
} else {
|
|
12
|
+
Object.assign(flatObj, flattenObject(obj[key] as Record<string, unknown>, newKey));
|
|
13
|
+
}
|
|
14
|
+
} else {
|
|
15
|
+
flatObj[newKey] = obj[key];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return flatObj;
|
|
19
|
+
}, {} as { [key: string]: unknown });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// map from nested property key like "a.b.c" to the maximum array count found in a list of objects for that array
|
|
24
|
+
export type ArrayValuesCount = Record<string, number>;
|
|
25
|
+
|
|
26
|
+
export function getArrayValuesCount(array: Record<string, unknown>[]): ArrayValuesCount {
|
|
27
|
+
return array.reduce((acc: ArrayValuesCount, obj: Record<string, unknown>) => {
|
|
28
|
+
Object.entries(obj).forEach(([key, value]) => {
|
|
29
|
+
// proceed only if value is an array
|
|
30
|
+
if (Array.isArray(value)) {
|
|
31
|
+
acc[key] = Math.max(acc[key] || 0, value.length);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// handle nested object
|
|
35
|
+
if (typeof value === "object" && value !== null) {
|
|
36
|
+
const nested = getArrayValuesCount([value as Record<string, unknown>]);
|
|
37
|
+
Object.entries(nested).forEach(([nestedKey, nestedCount]) => {
|
|
38
|
+
const compoundKey = `${key}.${nestedKey}`;
|
|
39
|
+
acc[compoundKey] = Math.max(acc[compoundKey] || 0, nestedCount);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return acc;
|
|
44
|
+
}, {});
|
|
45
|
+
}
|
package/src/util/hash.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function hashString(str: string): number {
|
|
2
|
+
let hash = 0;
|
|
3
|
+
let i;
|
|
4
|
+
let chr;
|
|
5
|
+
for (i = 0; i < str.length; i++) {
|
|
6
|
+
chr = str.charCodeAt(i);
|
|
7
|
+
hash = ((hash << 5) - hash) + chr;
|
|
8
|
+
hash |= 0; // Convert to 32bit integer
|
|
9
|
+
}
|
|
10
|
+
return Math.abs(hash);
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from "./collections";
|
|
2
|
+
export * from "./common";
|
|
3
|
+
export * from "./entities";
|
|
4
|
+
export * from "./strings";
|
|
5
|
+
export * from "./dates";
|
|
6
|
+
export * from "./enums";
|
|
7
|
+
export * from "./objects";
|
|
8
|
+
export * from "./paths";
|
|
9
|
+
export * from "./regexp";
|
|
10
|
+
export * from "./navigation_utils";
|
|
11
|
+
export * from "./entity_actions";
|
|
12
|
+
export * from "./fields";
|
|
13
|
+
export * from "./resolutions";
|
|
14
|
+
export * from "./permissions";
|
|
15
|
+
export * from "./plurals";
|
|
16
|
+
export * from "./references";
|
|
17
|
+
export * from "./flatten_object";
|
|
18
|
+
export * from "./navigation_from_path";
|
|
19
|
+
export * from "./parent_references_from_path";
|
|
20
|
+
export * from "./builders";
|
|
21
|
+
export * from "./storage";
|
|
22
|
+
export * from "./arrays";
|
|
23
|
+
export * from "./callbacks";
|
|
24
|
+
export * from "./hash";
|
|
25
|
+
export * from "./relations";
|
|
26
|
+
export * from "./conditions";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { toSnakeCase } from "./strings";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a foreign key column name from a given string, typically a collection slug or name.
|
|
5
|
+
* It converts the name to snake_case, attempts to singularize it by removing a trailing 's'
|
|
6
|
+
* (a common convention for collection names), and appends '_id'.
|
|
7
|
+
*
|
|
8
|
+
* @param name The base name to convert to a foreign key.
|
|
9
|
+
* @returns A foreign key name in the format 'singular_name_id'.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // returns "user_id"
|
|
13
|
+
* generateForeignKeyName("users")
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // returns "post_id"
|
|
17
|
+
* generateForeignKeyName("posts")
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // returns "product_id"
|
|
21
|
+
* generateForeignKeyName("Product")
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
export function generateForeignKeyName(name: string): string {
|
|
25
|
+
const snakeCaseName = toSnakeCase(name);
|
|
26
|
+
// A simple heuristic to singularize a plural name, which is a common convention.
|
|
27
|
+
const singularName = snakeCaseName.endsWith("s") ? snakeCaseName.slice(0, -1) : snakeCaseName;
|
|
28
|
+
return `${singularName}_id`;
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { EntityCollection, EntityCustomView } from "@rebasepro/types";
|
|
2
|
+
import { getCollectionPathsCombinations, removeInitialAndTrailingSlashes } from "./navigation_utils";
|
|
3
|
+
import { getSubcollections } from "./resolutions";
|
|
4
|
+
|
|
5
|
+
export type NavigationViewInternal<M extends Record<string, any> = any> =
|
|
6
|
+
| NavigationViewEntityInternal<M>
|
|
7
|
+
| NavigationViewCollectionInternal<M>
|
|
8
|
+
| NavigationViewEntityCustomInternal<M>;
|
|
9
|
+
|
|
10
|
+
export interface NavigationViewEntityInternal<M extends Record<string, any>> {
|
|
11
|
+
type: "entity";
|
|
12
|
+
entityId: string | number;
|
|
13
|
+
slug: string;
|
|
14
|
+
path: string;
|
|
15
|
+
parentCollection: EntityCollection<M>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NavigationViewCollectionInternal<M extends Record<string, any>> {
|
|
19
|
+
type: "collection";
|
|
20
|
+
id: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
path: string;
|
|
23
|
+
collection: EntityCollection<M>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NavigationViewEntityCustomInternal<M extends Record<string, any>> {
|
|
27
|
+
type: "custom_view";
|
|
28
|
+
slug: string;
|
|
29
|
+
path: string;
|
|
30
|
+
entityId: string | number;
|
|
31
|
+
view: EntityCustomView<M>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getNavigationEntriesFromPath(props: {
|
|
35
|
+
path: string,
|
|
36
|
+
collections: EntityCollection[] | undefined,
|
|
37
|
+
currentFullPath?: string,
|
|
38
|
+
contextEntityViews?: EntityCustomView[]
|
|
39
|
+
}): NavigationViewInternal[] {
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
path,
|
|
43
|
+
collections = [],
|
|
44
|
+
currentFullPath,
|
|
45
|
+
} = props;
|
|
46
|
+
|
|
47
|
+
const subpaths = removeInitialAndTrailingSlashes(path).split("/");
|
|
48
|
+
const subpathCombinations = getCollectionPathsCombinations(subpaths);
|
|
49
|
+
|
|
50
|
+
const result: NavigationViewInternal[] = [];
|
|
51
|
+
for (let i = 0; i < subpathCombinations.length; i++) {
|
|
52
|
+
const subpathCombination = subpathCombinations[i];
|
|
53
|
+
|
|
54
|
+
const collection = collections && collections.find((entry) => entry.slug === subpathCombination);
|
|
55
|
+
|
|
56
|
+
if (collection) {
|
|
57
|
+
const collectionPath = currentFullPath && currentFullPath.length > 0
|
|
58
|
+
? (currentFullPath + "/" + collection.slug)
|
|
59
|
+
: collection.slug;
|
|
60
|
+
result.push({
|
|
61
|
+
type: "collection",
|
|
62
|
+
id: collection.slug,
|
|
63
|
+
slug: collectionPath,
|
|
64
|
+
path: collectionPath,
|
|
65
|
+
collection
|
|
66
|
+
});
|
|
67
|
+
const restOfThePath = removeInitialAndTrailingSlashes(removeInitialAndTrailingSlashes(path).replace(subpathCombination, ""));
|
|
68
|
+
const nextSegments = restOfThePath.length > 0 ? restOfThePath.split("/") : [];
|
|
69
|
+
if (nextSegments.length > 0) {
|
|
70
|
+
const entityId = nextSegments[0];
|
|
71
|
+
const path = collectionPath + "/" + entityId;
|
|
72
|
+
result.push({
|
|
73
|
+
type: "entity",
|
|
74
|
+
entityId,
|
|
75
|
+
slug: collectionPath,
|
|
76
|
+
path,
|
|
77
|
+
parentCollection: collection
|
|
78
|
+
});
|
|
79
|
+
if (nextSegments.length > 1) {
|
|
80
|
+
const newPath = nextSegments.slice(1).join("/");
|
|
81
|
+
if (!collection) {
|
|
82
|
+
throw Error("collection not found resolving path: " + collection);
|
|
83
|
+
}
|
|
84
|
+
const entityViews = collection.entityViews;
|
|
85
|
+
const customView = entityViews && entityViews
|
|
86
|
+
.map((entry) => resolveEntityView(entry, props.contextEntityViews))
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.find((entry) => entry!.key === newPath);
|
|
89
|
+
const subcollections = getSubcollections(collection);
|
|
90
|
+
if (customView) {
|
|
91
|
+
result.push({
|
|
92
|
+
type: "custom_view",
|
|
93
|
+
slug: collectionPath,
|
|
94
|
+
entityId: entityId,
|
|
95
|
+
path: path + "/" + customView.key,
|
|
96
|
+
view: customView
|
|
97
|
+
});
|
|
98
|
+
} else if (subcollections) {
|
|
99
|
+
result.push(...getNavigationEntriesFromPath({
|
|
100
|
+
path: newPath,
|
|
101
|
+
collections: subcollections,
|
|
102
|
+
currentFullPath: path,
|
|
103
|
+
contextEntityViews: props.contextEntityViews
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveEntityView(entityView: string | EntityCustomView, contextEntityViews?: EntityCustomView[]): EntityCustomView | undefined {
|
|
116
|
+
if (typeof entityView === "string") {
|
|
117
|
+
return contextEntityViews?.find((entry) => entry.key === entityView);
|
|
118
|
+
} else {
|
|
119
|
+
return entityView;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { EntityCollection, CMSUrlController, SideEntityController } from "@rebasepro/types";
|
|
2
|
+
import { getSubcollections } from "./resolutions";
|
|
3
|
+
|
|
4
|
+
export function removeInitialAndTrailingSlashes(s: string): string {
|
|
5
|
+
return removeInitialSlash(removeTrailingSlash(s));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function removeInitialSlash(s: string) {
|
|
9
|
+
if (s.startsWith("/"))
|
|
10
|
+
return s.slice(1);
|
|
11
|
+
else return s;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function removeTrailingSlash(s: string) {
|
|
15
|
+
if (s.endsWith("/"))
|
|
16
|
+
return s.slice(0, -1);
|
|
17
|
+
else return s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function addInitialSlash(s: string) {
|
|
21
|
+
if (s.startsWith("/"))
|
|
22
|
+
return s;
|
|
23
|
+
else return `/${s}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getLastSegment(path: string) {
|
|
27
|
+
const cleanPath = removeInitialAndTrailingSlashes(path);
|
|
28
|
+
if (cleanPath.includes("/")) {
|
|
29
|
+
const segments = cleanPath.split("/");
|
|
30
|
+
return segments[segments.length - 1];
|
|
31
|
+
}
|
|
32
|
+
return cleanPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveCollectionPathIds(path: string, allCollections: EntityCollection[]): string {
|
|
36
|
+
let remainingPath = removeInitialAndTrailingSlashes(path);
|
|
37
|
+
if (!remainingPath) {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let currentCollections: EntityCollection[] | undefined = allCollections;
|
|
42
|
+
const resolvedPathParts: string[] = [];
|
|
43
|
+
|
|
44
|
+
while (remainingPath.length > 0) {
|
|
45
|
+
if (!currentCollections || currentCollections.length === 0) {
|
|
46
|
+
// We have remaining path segments but no more collections to match against
|
|
47
|
+
console.warn(`resolveCollectionPathIds: Path structure implies subcollections, but none found before segment starting with "${remainingPath}" in original path "${path}". Appending remaining original path.`);
|
|
48
|
+
resolvedPathParts.push(remainingPath);
|
|
49
|
+
remainingPath = ""; // Stop processing
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let foundMatch = false;
|
|
54
|
+
// Sort potential matches by length descending to prioritize longer matches (e.g., "a/b" over "a")
|
|
55
|
+
const potentialMatches: { col: EntityCollection; match: string; }[] = currentCollections
|
|
56
|
+
.flatMap(col => [{
|
|
57
|
+
col,
|
|
58
|
+
match: col.slug
|
|
59
|
+
}])
|
|
60
|
+
.filter(p => p.match && remainingPath.startsWith(p.match))
|
|
61
|
+
.sort((a, b) => b.match.length - a.match.length);
|
|
62
|
+
|
|
63
|
+
if (potentialMatches.length > 0) {
|
|
64
|
+
const {
|
|
65
|
+
col: foundCollection,
|
|
66
|
+
match: matchString
|
|
67
|
+
} = potentialMatches[0];
|
|
68
|
+
|
|
69
|
+
resolvedPathParts.push(foundCollection.dbPath); // Use the defined path
|
|
70
|
+
remainingPath = removeInitialSlash(remainingPath.substring(matchString.length));
|
|
71
|
+
|
|
72
|
+
// Check if we are at the end of the path
|
|
73
|
+
if (remainingPath.length === 0) {
|
|
74
|
+
foundMatch = true;
|
|
75
|
+
break; // Path ends with a collection segment
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The next segment must be an entity ID
|
|
79
|
+
const idSeparatorIndex = remainingPath.indexOf("/");
|
|
80
|
+
let entityId: string | number;
|
|
81
|
+
if (idSeparatorIndex > -1) {
|
|
82
|
+
entityId = remainingPath.substring(0, idSeparatorIndex);
|
|
83
|
+
remainingPath = remainingPath.substring(idSeparatorIndex + 1);
|
|
84
|
+
} else {
|
|
85
|
+
// This should not happen if the original path is valid (odd segments)
|
|
86
|
+
// but handle it defensively: assume the rest is the ID
|
|
87
|
+
entityId = remainingPath;
|
|
88
|
+
remainingPath = "";
|
|
89
|
+
console.warn(`resolveCollectionPathIds: Path seems to end with an entity ID "${entityId}" instead of a collection segment in original path "${path}". This might indicate an invalid input path.`);
|
|
90
|
+
// Even if it ends here, we still need to push the ID
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
resolvedPathParts.push(entityId); // Append entity ID
|
|
94
|
+
currentCollections = getSubcollections(foundCollection); // Move to subcollections
|
|
95
|
+
foundMatch = true;
|
|
96
|
+
|
|
97
|
+
if (!currentCollections && remainingPath.length > 0) {
|
|
98
|
+
// Warn if the path continues but no subcollections were defined
|
|
99
|
+
console.warn(`resolveCollectionPathIds: Path continues after entity ID "${entityId}", but no subcollections are defined for the preceding collection "${foundCollection.slug}" in path "${path}". Appending remaining original path.`);
|
|
100
|
+
resolvedPathParts.push(remainingPath); // Append the rest
|
|
101
|
+
remainingPath = ""; // Stop processing
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!foundMatch) {
|
|
108
|
+
// Collection definition not found for the start of the remaining path
|
|
109
|
+
console.warn(`resolveCollectionPathIds: Collection definition not found for segment starting with "${remainingPath}" in original path "${path}". Appending remaining original path.`);
|
|
110
|
+
resolvedPathParts.push(remainingPath); // Append the rest
|
|
111
|
+
remainingPath = ""; // Stop processing
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return resolvedPathParts.join("/");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Find the corresponding view at any depth for a given path.
|
|
121
|
+
* Note that path or segments of the paths can be collection aliases.
|
|
122
|
+
* @param slugOrPath
|
|
123
|
+
* @param collections
|
|
124
|
+
*/
|
|
125
|
+
export function getCollectionBySlugWithin(slugOrPath: string, collections: EntityCollection[]): EntityCollection | undefined {
|
|
126
|
+
|
|
127
|
+
const subpaths = removeInitialAndTrailingSlashes(slugOrPath).split("/");
|
|
128
|
+
if (subpaths.length % 2 === 0) {
|
|
129
|
+
throw Error(`getCollectionBySlug: Collection paths must have an odd number of segments: ${slugOrPath}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const subpathCombinations = getCollectionPathsCombinations(subpaths);
|
|
133
|
+
let result: EntityCollection | undefined;
|
|
134
|
+
for (let i = 0; i < subpathCombinations.length; i++) {
|
|
135
|
+
const subpathCombination = subpathCombinations[i];
|
|
136
|
+
const navigationEntry = collections && collections
|
|
137
|
+
.sort((a, b) => (a.slug ?? "").localeCompare(b.slug ?? ""))
|
|
138
|
+
.find((entry) => entry.slug === subpathCombination);
|
|
139
|
+
|
|
140
|
+
if (navigationEntry) {
|
|
141
|
+
|
|
142
|
+
if (subpathCombination === slugOrPath) {
|
|
143
|
+
result = navigationEntry;
|
|
144
|
+
} else if (navigationEntry.subcollections) {
|
|
145
|
+
const newPath = slugOrPath.replace(subpathCombination, "").split("/").slice(2).join("/");
|
|
146
|
+
if (newPath.length > 0)
|
|
147
|
+
result = getCollectionBySlugWithin(newPath, getSubcollections(navigationEntry));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (result) break;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the subcollection combinations from a path:
|
|
157
|
+
* "sites/es/locales" => ["sites/es/locales", "sites"]
|
|
158
|
+
* @param subpaths
|
|
159
|
+
*/
|
|
160
|
+
export function getCollectionPathsCombinations(subpaths: string[]): string[] {
|
|
161
|
+
const entries = subpaths.length > 0 && subpaths.length % 2 === 0 ? subpaths.splice(0, subpaths.length - 1) : subpaths;
|
|
162
|
+
|
|
163
|
+
const length = entries.length;
|
|
164
|
+
const result: string[] = [];
|
|
165
|
+
for (let i = length; i > 0; i = i - 2) {
|
|
166
|
+
result.push(entries.slice(0, i).join("/"));
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function navigateToEntity({
|
|
173
|
+
openEntityMode,
|
|
174
|
+
collection,
|
|
175
|
+
entityId,
|
|
176
|
+
copy,
|
|
177
|
+
path,
|
|
178
|
+
selectedTab,
|
|
179
|
+
sideEntityController,
|
|
180
|
+
onClose,
|
|
181
|
+
navigation
|
|
182
|
+
}:
|
|
183
|
+
|
|
184
|
+
{
|
|
185
|
+
openEntityMode: "side_panel" | "full_screen";
|
|
186
|
+
collection?: EntityCollection;
|
|
187
|
+
entityId?: string | number;
|
|
188
|
+
selectedTab?: string;
|
|
189
|
+
copy?: boolean;
|
|
190
|
+
path: string;
|
|
191
|
+
sideEntityController: SideEntityController;
|
|
192
|
+
onClose?: () => void;
|
|
193
|
+
navigation: CMSUrlController
|
|
194
|
+
}) {
|
|
195
|
+
|
|
196
|
+
if (openEntityMode === "side_panel") {
|
|
197
|
+
|
|
198
|
+
sideEntityController.open({
|
|
199
|
+
entityId,
|
|
200
|
+
path: path,
|
|
201
|
+
copy,
|
|
202
|
+
selectedTab,
|
|
203
|
+
collection,
|
|
204
|
+
updateUrl: true,
|
|
205
|
+
onClose
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
} else {
|
|
209
|
+
let to = navigation.buildUrlCollectionPath(entityId ? `${path ?? path}/${entityId}` : path ?? path);
|
|
210
|
+
if (entityId && selectedTab) {
|
|
211
|
+
to += `/${selectedTab}`;
|
|
212
|
+
}
|
|
213
|
+
if (!entityId) {
|
|
214
|
+
to += "#new";
|
|
215
|
+
}
|
|
216
|
+
if (copy) {
|
|
217
|
+
to += "#copy";
|
|
218
|
+
}
|
|
219
|
+
navigation.navigate(to);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
}
|