Skip to content

Commit 1b44be5

Browse files
committed
Fix for complex subcollection with alias navigation bug
1 parent 22d1652 commit 1b44be5

File tree

4 files changed

+247
-83
lines changed

4 files changed

+247
-83
lines changed

packages/firecms_core/src/components/EntityCollectionView/EntityCollectionView.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,13 @@ export const EntityCollectionView = React.memo(
147147
) {
148148

149149
const context = useFireCMSContext();
150-
const fullPath = fullPathProp ?? collectionProp.path;
151-
const dataSource = useDataSource(collectionProp);
152150
const navigation = useNavigationController();
151+
const fullPath = (fullPathProp ? navigation.resolveIdsFrom(fullPathProp) : undefined) ?? collectionProp.path;
152+
console.log("aaa", {
153+
fullPathProp,
154+
fullPath,
155+
})
156+
const dataSource = useDataSource(collectionProp);
153157
const sideEntityController = useSideEntityController();
154158
const authController = useAuthController();
155159
const userConfigPersistence = useUserConfigurationPersistence();

packages/firecms_core/src/util/navigation_utils.ts

+59-14
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,76 @@ export function getLastSegment(path: string) {
3232
}
3333

3434
export function resolveCollectionPathIds(path: string, allCollections: EntityCollection[]): string {
35-
3635
const cleanPath = removeInitialAndTrailingSlashes(path);
3736
const subpaths = cleanPath.split("/");
37+
3838
if (subpaths.length % 2 === 0) {
39-
throw Error(`resolveCollectionPathAliases: Collection paths must have an odd number of segments: ${path}`);
39+
throw Error(`resolveCollectionPathIds: Collection paths must have an odd number of segments: ${path}`);
40+
}
41+
42+
// Check if the path exactly matches a collection path
43+
const exactMatch = allCollections.find(col => col.path === cleanPath);
44+
if (exactMatch) {
45+
return exactMatch.path;
46+
}
47+
48+
if (subpaths.length === 1) {
49+
// Find collection by ID and return its path
50+
const aliasedCollection = allCollections.find((col) => col.id === subpaths[0]);
51+
return aliasedCollection?.path ?? subpaths[0];
4052
}
4153

42-
const aliasedCollection = allCollections.find((col) => col.id === subpaths[0]);
43-
let resolvedAliased;
44-
if (aliasedCollection) {
45-
resolvedAliased = aliasedCollection.path;
54+
// Try to match a multi-segment collection path
55+
let matchingCollection: EntityCollection | undefined;
56+
let entityIndex = 1;
57+
58+
// Check if the path starts with a multi-segment collection path
59+
for (const collection of allCollections) {
60+
const pathSegments = collection.path.split("/");
61+
if (pathSegments.length > 1 &&
62+
subpaths.slice(0, pathSegments.length).join("/") === collection.path) {
63+
matchingCollection = collection;
64+
entityIndex = pathSegments.length;
65+
break;
66+
}
4667
}
4768

48-
if (subpaths.length > 1) {
49-
const segmentCollection = getCollectionByPathOrId(resolvedAliased ?? subpaths[0], allCollections);
50-
if (!segmentCollection?.subcollections) {
69+
// If no multi-segment match, fall back to single segment matching
70+
if (!matchingCollection) {
71+
const matchingCollections = allCollections.filter(col =>
72+
col.id === subpaths[0] || col.path === subpaths[0]
73+
);
74+
75+
if (!matchingCollections.length) {
5176
return cleanPath;
5277
}
53-
const restOfThePath = cleanPath.split("/").slice(2).join("/");
54-
return (resolvedAliased ?? subpaths[0]) + "/" + subpaths[1] + "/" + resolveCollectionPathIds(restOfThePath, segmentCollection.subcollections);
55-
} else {
56-
return resolvedAliased ?? cleanPath;
78+
79+
matchingCollection = matchingCollections[0];
5780
}
58-
}
5981

82+
const entityId = subpaths[entityIndex];
83+
const remainingPath = subpaths.slice(entityIndex + 1);
84+
85+
// If we have a subcollection ID, try to resolve it
86+
if (remainingPath.length > 0) {
87+
const subcollectionId = remainingPath[0];
88+
const subcollection = matchingCollection.subcollections?.find(
89+
subcol => subcol.id === subcollectionId
90+
);
91+
92+
if (subcollection) {
93+
return `${matchingCollection.path}/${entityId}/${subcollection.path}`;
94+
}
95+
}
96+
97+
// If there are no remaining path segments, just return the collection path with entity ID
98+
if (remainingPath.length === 0) {
99+
return `${matchingCollection.path}/${entityId}`;
100+
}
101+
102+
// Default case - couldn't match subcollection
103+
return `${matchingCollection.path}/${entityId}/${remainingPath.join("/")}`;
104+
}
60105
/**
61106
* Find the corresponding view at any depth for a given path.
62107
* Note that path or segments of the paths can be collection aliases.
+169-66
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,200 @@
1+
jest.mock("@tiptap/extension-placeholder", () => ({
2+
configure: jest.fn(() => ({}))
3+
}));
4+
jest.mock("@tiptap/extension-horizontal-rule", () => ({
5+
configure: jest.fn(() => ({})),
6+
extend: jest.fn(() => ({ configure: jest.fn(() => ({})) }))
7+
}));
8+
jest.mock("@tiptap/extension-link", () => ({
9+
configure: jest.fn(() => ({})),
10+
extend: jest.fn(() => ({}))
11+
}));
12+
13+
jest.mock("@tiptap/starter-kit", () => ({
14+
configure: jest.fn(() => ({}))
15+
}));
16+
17+
jest.mock("@tiptap/extension-ordered-list", () => ({
18+
configure: jest.fn(() => ({}))
19+
}));
20+
21+
jest.mock("@tiptap/extension-bullet-list", () => ({
22+
configure: jest.fn(() => ({}))
23+
}));
24+
25+
jest.mock("@tiptap/extension-list-item", () => ({
26+
configure: jest.fn(() => ({}))
27+
}));
28+
29+
jest.mock("@tiptap/extension-code-block", () => ({
30+
configure: jest.fn(() => ({}))
31+
}));
32+
33+
jest.mock("@tiptap/extension-blockquote", () => ({
34+
configure: jest.fn(() => ({}))
35+
}));
36+
37+
jest.mock("@tiptap/extension-code", () => ({
38+
configure: jest.fn(() => ({}))
39+
}));
40+
141
import { expect, it } from "@jest/globals";
242
import { siteConfig } from "./test_site_config";
343
import { EntityCollection } from "../src/types";
4-
import { getCollectionByPathOrId, resolveCollectionPathIds } from "../src";
44+
import { getCollectionByPathOrId, resolveCollectionPathIds, buildCollection, buildProperty } from "../src";
545
import { getNavigationEntriesFromPath } from "../src/util/navigation_from_path";
646

747
const collections = siteConfig.collections as EntityCollection[];
848

9-
it("collection view matches ok", () => {
49+
describe("Resolving paths test", () => {
50+
it("collection view matches ok", () => {
1051

11-
const collectionViewFromPath = getCollectionByPathOrId("products", collections);
12-
expect(
13-
collectionViewFromPath && collectionViewFromPath.path
14-
).toEqual("products");
52+
const collectionViewFromPath = getCollectionByPathOrId("products", collections);
53+
expect(
54+
collectionViewFromPath && collectionViewFromPath.path
55+
).toEqual("products");
1556

16-
const collectionViewFromPath1 = getCollectionByPathOrId("products/pid/locales", collections);
17-
expect(
18-
collectionViewFromPath1 && collectionViewFromPath1.path
19-
).toEqual("locales");
57+
const collectionViewFromPath1 = getCollectionByPathOrId("products/pid/locales", collections);
58+
expect(
59+
collectionViewFromPath1 && collectionViewFromPath1.path
60+
).toEqual("locales");
2061

21-
const collectionViewFromPath2 = getCollectionByPathOrId("sites/es/products", collections);
22-
expect(
23-
collectionViewFromPath2 && collectionViewFromPath2.path
24-
).toEqual("sites/es/products");
62+
const collectionViewFromPath2 = getCollectionByPathOrId("sites/es/products", collections);
63+
expect(
64+
collectionViewFromPath2 && collectionViewFromPath2.path
65+
).toEqual("sites/es/products");
2566

26-
const collectionViewFromPath3 = getCollectionByPathOrId("sites/es/products/pid/locales", collections);
27-
expect(
28-
collectionViewFromPath3 && collectionViewFromPath3.path
29-
).toEqual("locales");
67+
const collectionViewFromPath3 = getCollectionByPathOrId("sites/es/products/pid/locales", collections);
68+
expect(
69+
collectionViewFromPath3 && collectionViewFromPath3.path
70+
).toEqual("locales");
3071

31-
expect(
32-
() => getCollectionByPathOrId("products/pid", collections)
33-
).toThrow(
34-
"Collection paths must have an odd number of segments: products/pid"
35-
);
72+
expect(
73+
() => getCollectionByPathOrId("products/pid", collections)
74+
).toThrow(
75+
"Collection paths must have an odd number of segments: products/pid"
76+
);
3677

37-
expect(
38-
getCollectionByPathOrId("products", [])
39-
).toEqual(undefined);
78+
expect(
79+
getCollectionByPathOrId("products", [])
80+
).toEqual(undefined);
4081

41-
const collectionViewFromPath10 = getCollectionByPathOrId("products/id/subcollection_inline", collections);
42-
expect(
43-
collectionViewFromPath10 && collectionViewFromPath10.path
44-
).toEqual("products/id/subcollection_inline");
82+
const collectionViewFromPath10 = getCollectionByPathOrId("products/id/subcollection_inline", collections);
83+
expect(
84+
collectionViewFromPath10 && collectionViewFromPath10.path
85+
).toEqual("products/id/subcollection_inline");
4586

46-
});
87+
});
4788

48-
it("build entity collection array", () => {
89+
it("build entity collection array", () => {
4990

50-
const navigationEntries = getNavigationEntriesFromPath({
51-
path: "products/pid",
52-
collections: collections,
91+
const navigationEntries = getNavigationEntriesFromPath({
92+
path: "products/pid",
93+
collections: collections
94+
});
95+
console.log(navigationEntries);
96+
// expect(
97+
// navigationEntries.map((collection) => collection.relativePath)
98+
// ).toEqual(["products", "locales"]);
5399
});
54-
console.log(navigationEntries);
55-
// expect(
56-
// navigationEntries.map((collection) => collection.relativePath)
57-
// ).toEqual(["products", "locales"]);
58-
});
59-
60100

61-
it("Custom view internal", () => {
101+
it("Custom view internal", () => {
62102

63-
const navigationEntries = getNavigationEntriesFromPath({
64-
path: "products/pid/custom_view",
65-
collections: collections,
103+
const navigationEntries = getNavigationEntriesFromPath({
104+
path: "products/pid/custom_view",
105+
collections: collections
106+
});
107+
console.log(navigationEntries);
108+
expect(navigationEntries.length).toEqual(3);
66109
});
67-
console.log(navigationEntries);
68-
expect(navigationEntries.length).toEqual(3);
69-
});
70110

71-
it("build entity collection array 2", () => {
111+
it("build entity collection array 2", () => {
72112

73-
const navigationEntries = getNavigationEntriesFromPath({
74-
path: "products/pid/locales/yep",
75-
collections: collections,
113+
const navigationEntries = getNavigationEntriesFromPath({
114+
path: "products/pid/locales/yep",
115+
collections: collections
116+
});
117+
console.log(navigationEntries);
118+
expect(navigationEntries.length).toEqual(4);
76119
});
77-
console.log(navigationEntries);
78-
expect(navigationEntries.length).toEqual(4);
79-
});
80120

81-
it("Test aliases", () => {
121+
it("Test aliases", () => {
122+
123+
const resolvedPath = resolveCollectionPathIds("u", collections);
124+
expect(resolvedPath).toEqual("users");
82125

83-
const resolvedPath = resolveCollectionPathIds("u", collections);
84-
expect(resolvedPath).toEqual("users");
126+
const resolvedPath2 = resolveCollectionPathIds("u/123/products", collections);
127+
expect(resolvedPath2).toEqual("users/123/products");
85128

86-
const resolvedPath2 = resolveCollectionPathIds("u/123/products", collections);
87-
expect(resolvedPath2).toEqual("users/123/products");
129+
const resolvedPath3 = resolveCollectionPathIds("u/123/p", collections);
130+
expect(resolvedPath3).toEqual("users/123/products");
88131

89-
const resolvedPath3 = resolveCollectionPathIds("u/123/p", collections);
90-
expect(resolvedPath3).toEqual("users/123/products");
132+
const resolvedPath4 = resolveCollectionPathIds("users/123/p", collections);
133+
expect(resolvedPath4).toEqual("users/123/products");
91134

92-
const resolvedPath4 = resolveCollectionPathIds("users/123/p", collections);
93-
expect(resolvedPath4).toEqual("users/123/products");
135+
const resolvedPath5 = resolveCollectionPathIds("products/id/subcollection_inline", collections);
136+
expect(resolvedPath5).toEqual("products/id/subcollection_inline");
137+
});
94138

95-
const resolvedPath5 = resolveCollectionPathIds("products/id/subcollection_inline", collections);
96-
expect(resolvedPath5).toEqual("products/id/subcollection_inline");
139+
140+
it("should correctly resolve subcollection with different id and path", () => {
141+
// Simplified locale collection
142+
const jointLocaleCollection = buildCollection({
143+
id: "medico_v2_0_0_joint_locales",
144+
path: "locales",
145+
name: "Translations",
146+
properties: {
147+
name: buildProperty({
148+
dataType: "string",
149+
name: "Name"
150+
})
151+
}
152+
});
153+
154+
// Simplified joint movements collection
155+
const jointMovementsCollection = buildCollection({
156+
id: "medico_v2_0_0_joint_movements",
157+
path: "movements",
158+
name: "Joint movements",
159+
properties: {
160+
reference_value_min: buildProperty({
161+
name: "Reference value min",
162+
dataType: "number"
163+
})
164+
},
165+
subcollections: [jointLocaleCollection]
166+
});
167+
168+
// Simplified joints collection
169+
const jointsCollection = buildCollection({
170+
id: "medico_v2_0_0_joints",
171+
path: "medico/v2.0.0/joints",
172+
name: "Joint",
173+
properties: {
174+
latin_name: buildProperty({
175+
name: "Latin name",
176+
dataType: "string"
177+
})
178+
},
179+
subcollections: [jointMovementsCollection, jointLocaleCollection]
180+
});
181+
182+
const collections = [jointsCollection];
183+
184+
// Test path resolution for joint movements subcollection
185+
const result = resolveCollectionPathIds(
186+
"medico_v2_0_0_joints/cervical_spine/medico_v2_0_0_joint_movements",
187+
collections
188+
);
189+
190+
expect(result).toEqual("medico/v2.0.0/joints/cervical_spine/movements");
191+
192+
// Alternative test using path instead of ID for the parent collection
193+
const result2 = resolveCollectionPathIds(
194+
"medico/v2.0.0/joints/cervical_spine/medico_v2_0_0_joint_movements",
195+
collections
196+
);
197+
198+
expect(result2).toEqual("medico/v2.0.0/joints/cervical_spine/movements");
199+
});
97200
});

0 commit comments

Comments
 (0)