Skip to content

Commit 47a2f70

Browse files
fgnassadunkmaneric-gadedemshy
authored
feat(nested collections): allow non-index files (#7359)
* feat(nested collections): allow non-index files This commit fixes #4972 to allow nested folders with additional content beyond an index file. Side effect: To keep the feature simple, this will now show index files as pages within a folder in NetlifyCMS. This enables creating additional files alongside the given index, but is a change in behavior from the current implementation. Co-authored-by: Eric Gade <[email protected]> * test(e2e): adapt to new way nested collections work We use regexps as otherwise .contains("Sub Directory") would also match "Another Sub Directory" --------- Co-authored-by: Andrew Dunkman <[email protected]> Co-authored-by: Eric Gade <[email protected]> Co-authored-by: Anze Demsar <[email protected]>
1 parent 2ffe3f8 commit 47a2f70

File tree

6 files changed

+108
-47
lines changed

6 files changed

+108
-47
lines changed

cypress/e2e/editorial_workflow_spec_test_backend.js

+20-24
Original file line numberDiff line numberDiff line change
@@ -207,17 +207,16 @@ describe('Test Backend Editorial Workflow', () => {
207207
login();
208208

209209
inSidebar(() => cy.contains('a', 'Pages').click());
210-
inSidebar(() => cy.contains('a', 'Directory'));
210+
inSidebar(() => cy.contains('a', /^Directory$/));
211211
inGrid(() => cy.contains('a', 'Root Page'));
212-
inGrid(() => cy.contains('a', 'Directory'));
213212

214-
inSidebar(() => cy.contains('a', 'Directory').click());
213+
inSidebar(() => cy.contains('a', /^Directory$/).click());
215214

216-
inGrid(() => cy.contains('a', 'Sub Directory'));
217-
inGrid(() => cy.contains('a', 'Another Sub Directory'));
215+
inSidebar(() => cy.contains('a', /^Sub Directory$/));
216+
inSidebar(() => cy.contains('a', 'Another Sub Directory'));
218217

219-
inSidebar(() => cy.contains('a', 'Sub Directory').click());
220-
inGrid(() => cy.contains('a', 'Nested Directory'));
218+
inSidebar(() => cy.contains('a', /^Sub Directory$/).click());
219+
inSidebar(() => cy.contains('a', 'Nested Directory'));
221220
cy.url().should(
222221
'eq',
223222
'http://localhost:8080/#/collections/pages/filter/directory/sub-directory',
@@ -233,21 +232,17 @@ describe('Test Backend Editorial Workflow', () => {
233232
login();
234233

235234
inSidebar(() => cy.contains('a', 'Pages').click());
236-
inSidebar(() => cy.contains('a', 'Directory').click());
237-
inGrid(() => cy.contains('a', 'Another Sub Directory').click());
238-
239-
cy.url().should(
240-
'eq',
241-
'http://localhost:8080/#/collections/pages/entries/directory/another-sub-directory/index',
242-
);
235+
inSidebar(() => cy.contains('a', /^Directory$/).click());
236+
inSidebar(() => cy.contains('a', 'Another Sub Directory').click());
237+
inGrid(() => cy.contains('a', 'Another Sub Directory'));
243238
});
244239

245240
it(`can create a new entry with custom path`, () => {
246241
login();
247242

248243
inSidebar(() => cy.contains('a', 'Pages').click());
249-
inSidebar(() => cy.contains('a', 'Directory').click());
250-
inSidebar(() => cy.contains('a', 'Sub Directory').click());
244+
inSidebar(() => cy.contains('a', /^Directory$/).click());
245+
inSidebar(() => cy.contains('a', /^Sub Directory$/).click());
251246
cy.contains('a', 'New Page').click();
252247

253248
cy.get('[id^="path-field"]').should('have.value', 'directory/sub-directory');
@@ -262,18 +257,18 @@ describe('Test Backend Editorial Workflow', () => {
262257
publishEntryInEditor(publishTypes.publishNow);
263258
exitEditor();
264259

265-
inGrid(() => cy.contains('a', 'New Path Title'));
266-
inSidebar(() => cy.contains('a', 'Directory').click());
267-
inSidebar(() => cy.contains('a', 'Directory').click());
260+
inSidebar(() => cy.contains('a', 'New Path Title'));
261+
inSidebar(() => cy.contains('a', /^Directory$/).click());
262+
inSidebar(() => cy.contains('a', /^Directory$/).click());
268263
inGrid(() => cy.contains('a', 'New Path Title').should('not.exist'));
269264
});
270265

271266
it(`can't create an entry with an existing path`, () => {
272267
login();
273268

274269
inSidebar(() => cy.contains('a', 'Pages').click());
275-
inSidebar(() => cy.contains('a', 'Directory').click());
276-
inSidebar(() => cy.contains('a', 'Sub Directory').click());
270+
inSidebar(() => cy.contains('a', /^Directory$/).click());
271+
inSidebar(() => cy.contains('a', /^Sub Directory$/).click());
277272

278273
cy.contains('a', 'New Page').click();
279274
cy.get('[id^="title-field"]').type('New Path Title');
@@ -292,7 +287,8 @@ describe('Test Backend Editorial Workflow', () => {
292287
login();
293288

294289
inSidebar(() => cy.contains('a', 'Pages').click());
295-
inGrid(() => cy.contains('a', 'Directory').click());
290+
inSidebar(() => cy.contains('a', /^Directory$/).click());
291+
inGrid(() => cy.contains('a', /^Directory$/).click());
296292

297293
cy.get('[id^="path-field"]').should('have.value', 'directory');
298294
cy.get('[id^="path-field"]').clear();
@@ -310,7 +306,7 @@ describe('Test Backend Editorial Workflow', () => {
310306

311307
inSidebar(() => cy.contains('a', 'New Directory').click());
312308

313-
inGrid(() => cy.contains('a', 'Sub Directory'));
314-
inGrid(() => cy.contains('a', 'Another Sub Directory'));
309+
inSidebar(() => cy.contains('a', /^Sub Directory$/));
310+
inSidebar(() => cy.contains('a', 'Another Sub Directory'));
315311
});
316312
});

packages/decap-cms-core/src/components/Collection/Entries/EntriesCollection.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -119,20 +119,19 @@ export class EntriesCollection extends React.Component {
119119

120120
export function filterNestedEntries(path, collectionFolder, entries) {
121121
const filtered = entries.filter(e => {
122-
const entryPath = e.get('path').slice(collectionFolder.length + 1);
122+
let entryPath = e.get('path').slice(collectionFolder.length + 1);
123123
if (!entryPath.startsWith(path)) {
124124
return false;
125125
}
126126

127-
// only show immediate children
127+
// for subdirectories, trim off the parent folder corresponding to
128+
// this nested collection entry
128129
if (path) {
129-
// non root path
130-
const trimmed = entryPath.slice(path.length + 1);
131-
return trimmed.split('/').length === 2;
132-
} else {
133-
// root path
134-
return entryPath.split('/').length <= 2;
130+
entryPath = entryPath.slice(path.length + 1);
135131
}
132+
133+
// only show immediate children
134+
return !entryPath.includes('/');
136135
});
137136
return filtered;
138137
}

packages/decap-cms-core/src/components/Collection/Entries/__tests__/EntriesCollection.spec.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,11 @@ describe('filterNestedEntries', () => {
4545
];
4646
const entries = fromJS(entriesArray);
4747
expect(filterNestedEntries('dir3', 'src/pages', entries).toJS()).toEqual([
48-
{ slug: 'dir3/dir4/index', path: 'src/pages/dir3/dir4/index.md', data: { title: 'File 4' } },
48+
{ slug: 'dir3/index', path: 'src/pages/dir3/index.md', data: { title: 'File 3' } },
4949
]);
5050
});
5151

52-
it('should return immediate children and root for root path', () => {
52+
it('should return only immediate children for root path', () => {
5353
const entriesArray = [
5454
{ slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } },
5555
{ slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } },
@@ -60,8 +60,6 @@ describe('filterNestedEntries', () => {
6060
const entries = fromJS(entriesArray);
6161
expect(filterNestedEntries('', 'src/pages', entries).toJS()).toEqual([
6262
{ slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } },
63-
{ slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } },
64-
{ slug: 'dir3/index', path: 'src/pages/dir3/index.md', data: { title: 'File 3' } },
6563
]);
6664
});
6765
});
@@ -126,7 +124,7 @@ describe('EntriesCollection', () => {
126124
expect(asFragment()).toMatchSnapshot();
127125
});
128126

129-
it('should render apply filter term for nested collections', () => {
127+
it('should render with applied filter term for nested collections', () => {
130128
const entriesArray = [
131129
{ slug: 'index', path: 'src/pages/index.md', data: { title: 'Root' } },
132130
{ slug: 'dir1/index', path: 'src/pages/dir1/index.md', data: { title: 'File 1' } },

packages/decap-cms-core/src/components/Collection/Entries/__tests__/__snapshots__/EntriesCollection.spec.js.snap

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,36 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`EntriesCollection should render apply filter term for nested collections 1`] = `
3+
exports[`EntriesCollection should render connected component 1`] = `
44
<DocumentFragment>
55
<mock-entries
66
collectionname="Pages"
7-
collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10 } }"
7+
collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\" }"
88
cursor="[object Object]"
9-
entries="List []"
9+
entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } }, Map { \\"slug\\": \\"dir1/index\\", \\"path\\": \\"src/pages/dir1/index.md\\", \\"data\\": Map { \\"title\\": \\"File 1\\" } }, Map { \\"slug\\": \\"dir2/index\\", \\"path\\": \\"src/pages/dir2/index.md\\", \\"data\\": Map { \\"title\\": \\"File 2\\" } } ]"
1010
isfetching="false"
1111
/>
1212
</DocumentFragment>
1313
`;
1414

15-
exports[`EntriesCollection should render connected component 1`] = `
15+
exports[`EntriesCollection should render show only immediate children for nested collection 1`] = `
1616
<DocumentFragment>
1717
<mock-entries
1818
collectionname="Pages"
19-
collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\" }"
19+
collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10 } }"
2020
cursor="[object Object]"
21-
entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } }, Map { \\"slug\\": \\"dir1/index\\", \\"path\\": \\"src/pages/dir1/index.md\\", \\"data\\": Map { \\"title\\": \\"File 1\\" } }, Map { \\"slug\\": \\"dir2/index\\", \\"path\\": \\"src/pages/dir2/index.md\\", \\"data\\": Map { \\"title\\": \\"File 2\\" } } ]"
21+
entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } } ]"
2222
isfetching="false"
2323
/>
2424
</DocumentFragment>
2525
`;
2626

27-
exports[`EntriesCollection should render show only immediate children for nested collection 1`] = `
27+
exports[`EntriesCollection should render with applied filter term for nested collections 1`] = `
2828
<DocumentFragment>
2929
<mock-entries
3030
collectionname="Pages"
3131
collections="Map { \\"name\\": \\"pages\\", \\"label\\": \\"Pages\\", \\"folder\\": \\"src/pages\\", \\"nested\\": Map { \\"depth\\": 10 } }"
3232
cursor="[object Object]"
33-
entries="List [ Map { \\"slug\\": \\"index\\", \\"path\\": \\"src/pages/index.md\\", \\"data\\": Map { \\"title\\": \\"Root\\" } }, Map { \\"slug\\": \\"dir1/index\\", \\"path\\": \\"src/pages/dir1/index.md\\", \\"data\\": Map { \\"title\\": \\"File 1\\" } }, Map { \\"slug\\": \\"dir3/index\\", \\"path\\": \\"src/pages/dir3/index.md\\", \\"data\\": Map { \\"title\\": \\"File 3\\" } } ]"
33+
entries="List [ Map { \\"slug\\": \\"dir3/dir4/index\\", \\"path\\": \\"src/pages/dir3/dir4/index.md\\", \\"data\\": Map { \\"title\\": \\"File 4\\" } } ]"
3434
isfetching="false"
3535
/>
3636
</DocumentFragment>

packages/decap-cms-core/src/components/Collection/NestedCollection.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function TreeNode(props) {
8080

8181
const sortedData = sortBy(treeData, getNodeTitle);
8282
return sortedData.map(node => {
83-
const leaf = node.children.length <= 1 && !node.children[0]?.isDir && depth > 0;
83+
const leaf = node.children.length === 0 && depth > 0;
8484
if (leaf) {
8585
return null;
8686
}
@@ -90,7 +90,7 @@ function TreeNode(props) {
9090
}
9191
const title = getNodeTitle(node);
9292

93-
const hasChildren = depth === 0 || node.children.some(c => c.children.some(c => c.isDir));
93+
const hasChildren = depth === 0 || node.children.some(c => c.isDir);
9494

9595
return (
9696
<React.Fragment key={node.path}>

packages/decap-cms-core/src/components/Collection/__tests__/__snapshots__/NestedCollection.spec.js.snap

+68
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,20 @@ exports[`NestedCollection should render connected component 1`] = `
138138
margin-right: 4px;
139139
}
140140
141+
.emotion-6 {
142+
position: relative;
143+
top: 2px;
144+
color: #fff;
145+
width: 0;
146+
height: 0;
147+
border: 5px solid transparent;
148+
border-radius: 2px;
149+
border-left: 6px solid currentColor;
150+
border-right: 0;
151+
color: currentColor;
152+
left: 2px;
153+
}
154+
141155
<a
142156
class="emotion-0 emotion-1"
143157
data-testid="/a"
@@ -155,6 +169,9 @@ exports[`NestedCollection should render connected component 1`] = `
155169
>
156170
File 1
157171
</div>
172+
<div
173+
class="emotion-6 emotion-7"
174+
/>
158175
</div>
159176
</a>
160177
.emotion-0 {
@@ -207,6 +224,20 @@ exports[`NestedCollection should render connected component 1`] = `
207224
margin-right: 4px;
208225
}
209226
227+
.emotion-6 {
228+
position: relative;
229+
top: 2px;
230+
color: #fff;
231+
width: 0;
232+
height: 0;
233+
border: 5px solid transparent;
234+
border-radius: 2px;
235+
border-left: 6px solid currentColor;
236+
border-right: 0;
237+
color: currentColor;
238+
left: 2px;
239+
}
240+
210241
<a
211242
class="emotion-0 emotion-1"
212243
data-testid="/b"
@@ -224,6 +255,9 @@ exports[`NestedCollection should render connected component 1`] = `
224255
>
225256
File 2
226257
</div>
258+
<div
259+
class="emotion-6 emotion-7"
260+
/>
227261
</div>
228262
</a>
229263
</DocumentFragment>
@@ -367,6 +401,20 @@ exports[`NestedCollection should render correctly with nested entries 1`] = `
367401
margin-right: 4px;
368402
}
369403
404+
.emotion-6 {
405+
position: relative;
406+
top: 2px;
407+
color: #fff;
408+
width: 0;
409+
height: 0;
410+
border: 5px solid transparent;
411+
border-radius: 2px;
412+
border-left: 6px solid currentColor;
413+
border-right: 0;
414+
color: currentColor;
415+
left: 2px;
416+
}
417+
370418
<a
371419
class="emotion-0 emotion-1"
372420
data-testid="/a"
@@ -384,6 +432,9 @@ exports[`NestedCollection should render correctly with nested entries 1`] = `
384432
>
385433
File 1
386434
</div>
435+
<div
436+
class="emotion-6 emotion-7"
437+
/>
387438
</div>
388439
</a>
389440
.emotion-0 {
@@ -436,6 +487,20 @@ exports[`NestedCollection should render correctly with nested entries 1`] = `
436487
margin-right: 4px;
437488
}
438489
490+
.emotion-6 {
491+
position: relative;
492+
top: 2px;
493+
color: #fff;
494+
width: 0;
495+
height: 0;
496+
border: 5px solid transparent;
497+
border-radius: 2px;
498+
border-left: 6px solid currentColor;
499+
border-right: 0;
500+
color: currentColor;
501+
left: 2px;
502+
}
503+
439504
<a
440505
class="emotion-0 emotion-1"
441506
data-testid="/b"
@@ -453,6 +518,9 @@ exports[`NestedCollection should render correctly with nested entries 1`] = `
453518
>
454519
File 2
455520
</div>
521+
<div
522+
class="emotion-6 emotion-7"
523+
/>
456524
</div>
457525
</a>
458526
</DocumentFragment>

0 commit comments

Comments
 (0)