Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
380bc60
feat(index-fields): allow defining own field schema for index files
demshy Jan 29, 2025
618b6ed
feat(index-file): add index file config options
demshy Jan 31, 2025
fc355a0
fix(index-file): check if indexFileConfix exists
demshy Feb 4, 2025
4c99ad1
feat(index-file): replace index file label with home icon
demshy Feb 4, 2025
db6b1b0
feat(index-fields): differentiate nested/non-nested collections
demshy Feb 4, 2025
a9e5828
Merge branch 'main' into feat/index-fields
demshy Feb 21, 2025
2552d93
feat(index-files): update decap-server to allow moving single entries…
demshy Mar 5, 2025
4df5976
feat(index-file): allow updating single entries in github backend
demshy Mar 5, 2025
b307868
feat(index-file): choose file type when adding entry, path handling f…
demshy Mar 5, 2025
991c208
feat(index-file): lint
demshy Mar 5, 2025
532d30d
feat(index-file): lint
demshy Mar 5, 2025
08c19a8
feat(index-file): remove logging
demshy Mar 5, 2025
01f6db2
fix: new path url
martinjagodic Mar 6, 2025
25d15f4
fix: function call
martinjagodic Mar 6, 2025
362ac96
fix: meta path preparing
martinjagodic Mar 6, 2025
3f1b3a3
Merge branch 'main' into feat/index-fields
demshy Mar 7, 2025
38a80b5
style: lint
demshy Mar 7, 2025
f32a871
feat(index-file): keep fileBaseName when editing leaf entries
demshy Mar 12, 2025
05b15c4
style: lint
demshy Mar 12, 2025
0a8af08
feat(index-files): editorBackLink only one level up in nested collect…
demshy Mar 12, 2025
b5dc663
feat(index-file): enable/disable editor preview for index file
demshy Mar 14, 2025
9f216b4
feat(index-file): add some types and a bit of logging
demshy Mar 14, 2025
72df729
feat(index-file): remove logging
demshy Mar 14, 2025
3b497a5
feat(index-page): path type names in dropdown
demshy Mar 14, 2025
573204a
feat(index-files): fix editorial workflow with the new nested collect…
demshy Apr 16, 2025
6d08159
style: lint code
demshy Apr 16, 2025
578dc4b
fix(index-fields): traverse index_file fields for i18n transformation…
demshy Jun 10, 2025
ec04105
feat(index-fields): paths
demshy Jun 13, 2025
a0a219d
feat(index-files): refactor customPAth
demshy Jun 13, 2025
b3b3c55
feat(indexFields): meta paths
demshy Jun 20, 2025
45de5cf
fix(indexFields): revert to original config.yml
demshy Jun 20, 2025
51ae2df
feat(index-fields): fix duplicating entries
demshy Jun 24, 2025
58a68ec
feat(indexFields): check both path types when validating metaPath
demshy Jun 24, 2025
edf033c
Merge branch 'main' into feat/index-fields
martinjagodic Jun 24, 2025
5338816
Merge branch 'main' into feat/index-fields
martinjagodic Jun 24, 2025
5f1ac72
chore: format
martinjagodic Jun 24, 2025
a6875d0
fix: remove duplicate param (merge error)
martinjagodic Jun 24, 2025
fc42467
Merge branch 'main' into feat/index-fields
martinjagodic Jun 26, 2025
c413866
Merge branch 'main' into feat/index-fields
martinjagodic Nov 14, 2025
af9309b
fix: remove meta fields from frontmatter, fix tests, refactor
martinjagodic Nov 25, 2025
02caeaa
feat: add index field to dev-test
martinjagodic Dec 4, 2025
3d8fe14
feat: put index file on top of collection view
martinjagodic Dec 4, 2025
9308a58
feat: translate path types
martinjagodic Dec 4, 2025
4efba64
feat: add schema validation for index fields
martinjagodic Dec 4, 2025
c7b4ad2
feat: add tests for index fields
martinjagodic Dec 4, 2025
1801c14
fix: add path_type
martinjagodic Dec 4, 2025
8fc9cf3
Merge branch 'main' into feat/index-fields
martinjagodic Dec 4, 2025
80195fc
fix: don't try to delete nonexistent files
martinjagodic Dec 4, 2025
cc7dc84
Merge branch 'main' into feat/index-fields
martinjagodic Feb 25, 2026
adbc990
fix: use existing isIndexFileEntry util
yanthomasdev Mar 26, 2026
2f37703
fix: add missing `path_type`
yanthomasdev Mar 26, 2026
77ed71b
fix: add missing type property
yanthomasdev Mar 26, 2026
1392a39
fix: default pathType to content
yanthomasdev Mar 26, 2026
5116383
feat: improve test coverage
yanthomasdev Mar 26, 2026
5da4ff6
fix: stop entries from being shown at the wrong nesting level
yanthomasdev Mar 26, 2026
da27e1e
fix: editor back link issue
yanthomasdev Mar 27, 2026
7c74ed4
feat: add support for other backends
yanthomasdev Mar 27, 2026
4a30832
Merge branch 'main' into feat/index-fields
yanthomasdev Mar 27, 2026
ba6e47b
fix: use path-browserify in gitea backend
yanthomasdev Mar 27, 2026
d16f311
feat: exclude meta fields from preview
yanthomasdev Apr 6, 2026
e3c08f4
Merge branch 'main' into pr/7382
yanthomasdev Jul 9, 2026
95c399b
Merge branch 'main' into feat/index-fields
martinjagodic Sep 3, 2026
b588f91
fix(index-fields): address review feedback
martinjagodic Sep 3, 2026
54848d4
fix(index-fields): keep nested collections without index files unchanged
martinjagodic Sep 3, 2026
5f31d79
test(index-fields): fix e2e specs and cover the nested flow
martinjagodic Sep 3, 2026
24e67e7
fix(i18n): delete only the locale files an entry actually has
martinjagodic Sep 3, 2026
6528e46
test(index-fields): page the collection in before asserting on the in…
martinjagodic Sep 3, 2026
f5adf49
fix(i18n): do not treat an unreadable locale file as an empty transla…
martinjagodic Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions cypress/e2e/index_fields_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import '../utils/dismiss-local-backup';
import { login, exitEditor, publishEntry, assertNotification } from '../utils/steps';
import { notifications } from '../utils/constants';

const backend = 'test';

/**
* The dev-test `posts` collection is configured with:
*
* index_file:
* pattern: _index
* editor: { preview: false }
* fields: [title, body]
*
* and ships a matching `_posts/_index.md` fixture. Everything below asserts that an entry
* matching that pattern is treated differently from the rest of the collection.
*/
const indexEntryTitle = 'Posts list page';
const regularEntryTitle = 'This is a YAML front matter post';

const entryCards = () => cy.get('[class*=ListCardLink]');
const indexFileIcons = () => cy.get('[class*=ListCardLink] [class*=TitleIcons] svg');
const previewPane = () => cy.get('[class*=PreviewPaneFrame]');

function openEntry(title) {
cy.contains('[class*=ListCardLink]', title).click();
}

/**
* The collection view pages in 20 entries at a time, and the index file is hoisted to the
* top client side, over the entries that are already loaded. The dev-test posts collection
* has 24 entries and the index file sorts last by date, so it is not on the first page at
* all: everything below has to page the whole collection in before it can see it.
*/
function loadWholeCollection() {
cy.get('[class*=ListCardLink]').should('have.length', 20);
cy.scrollTo('bottom');
cy.get('[class*=ListCardLink]').should('have.length', 24);
}

describe('Index File Feature', () => {
before(() => {
Cypress.config('defaultCommandTimeout', 4000);
cy.task('setupBackend', { backend, options: { publish_mode: 'simple' } });
});

after(() => {
cy.task('teardownBackend', { backend });
});

beforeEach(() => {
login();
});

describe('in the posts collection', () => {
beforeEach(() => {
loadWholeCollection();
});

it('sorts the index file to the top of the collection', () => {
entryCards().first().should('contain.text', indexEntryTitle);
});

it('marks only the index file entry with an icon', () => {
// the icon lives on the index entry's card and on no other
indexFileIcons().should('have.length', 1);
cy.contains('[class*=ListCardLink]', indexEntryTitle).find('[class*=TitleIcons] svg');
});

it('edits the index file entry with the index fields', () => {
openEntry(indexEntryTitle);

cy.contains('label', 'Title');
cy.contains('label', 'Body');
// fields that only exist on the collection's regular entries
cy.contains('label', 'Publish Date').should('not.exist');
cy.contains('label', 'Cover Image').should('not.exist');
});

it('edits a regular entry with the collection fields', () => {
openEntry(regularEntryTitle);

cy.contains('label', 'Title');
cy.contains('label', 'Publish Date');
cy.contains('label', 'Cover Image');
});

it('disables the editor preview for the index file entry only', () => {
openEntry(indexEntryTitle);
previewPane().should('not.exist');

exitEditor();

openEntry(regularEntryTitle);
previewPane().should('exist');
});

it('can edit and republish the index file entry', () => {
const updatedTitle = 'Updated posts list page';

openEntry(indexEntryTitle);
cy.get('[id^="title-field"]').clear();
cy.get('[id^="title-field"]').type(updatedTitle);
publishEntry();
assertNotification(notifications.saved);

// the entry is written back to the same file rather than re-slugged from the new title
cy.url().should('contain', '/collections/posts/entries/_index');

exitEditor();

// still the collection's only index file, still sorted to the top
// (the card summary itself is not re-fetched after a save, which is pre-existing behaviour)
entryCards().first().should('contain.text', indexEntryTitle);
indexFileIcons().should('have.length', 1);
});
});

describe('in a nested collection', () => {
/**
* The dev-test `sections` collection is nested and configured with both
*
* index_file: { pattern: '^index$', ... } // which entries get the index fields
* meta: { path: { index_file: index } } // the filename an index entry is written to
*
* which together let one collection hold index pages and content pages side by side.
*/
const newEntryDropdown = () => cy.contains('[role="button"]', '+ Section');

function chooseNewEntryType(label) {
newEntryDropdown().click();
cy.contains('[role="menuitem"] span', label).click();
}

beforeEach(() => {
cy.contains('a', 'Sections').click();
});

it('offers a path type when creating an entry', () => {
newEntryDropdown().click();
cy.contains('[role="menuitem"] span', 'Index Page');
cy.contains('[role="menuitem"] span', 'Content Page');
});

it('creates an index page as the folder index file', () => {
chooseNewEntryType('Index Page');
cy.url().should('contain', 'path_type=index');

cy.get('[id^="path-field"]').clear();
cy.get('[id^="path-field"]').type('guides/advanced');
cy.get('[id^="title-field"]').type('Advanced guides');
publishEntry();
assertNotification(notifications.saved);

// written to `_sections/guides/advanced/index.md`
cy.url().should('contain', '/collections/sections/entries/guides/advanced/index');
});

it('creates a content page at its own slug', () => {
chooseNewEntryType('Content Page');
cy.url().should('contain', 'path_type=slug');

cy.get('[id^="path-field"]').clear();
cy.get('[id^="path-field"]').type('guides/second-steps');
cy.get('[id^="title-field"]').type('Second steps');
cy.getMarkdownEditor().first().click();
cy.getMarkdownEditor().first().type('A regular content page.');
publishEntry();
assertNotification(notifications.saved);

// written to `_sections/guides/second-steps.md`, not to a folder index file
cy.url().should('contain', '/collections/sections/entries/guides/second-steps');
cy.url().should('not.contain', 'second-steps/index');
});
});
});
4 changes: 2 additions & 2 deletions cypress/e2e/view_filters_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('View Filter', () => {

// disable filter
filter('Posts With Index');
assertEntriesCount(23);
assertEntriesCount(24);
for (let i = 1; i <= 20; i++) {
assertInEntries(`This is post # ${i} --`);
}
Expand Down Expand Up @@ -91,7 +91,7 @@ describe('View Filter', () => {

// disable filter
filter('Relation Test');
assertEntriesCount(23);
assertEntriesCount(24);
for (let i = 1; i <= 20; i++) {
assertInEntries(`This is post # ${i} --`);
}
Expand Down
11 changes: 7 additions & 4 deletions cypress/e2e/view_groups_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,17 @@ describe('View Group', () => {
// enable group
group('Year');

assertGroupsCount(2);
// three groups: the two post years plus the index file, which has no date
assertGroupsCount(3);
const year = new Date().getFullYear();
assertEachGroupCount(`Year${year}`, 20);
assertEachGroupCount('Year2015', 3);
assertEachGroupCount('missing_value', 1);

//disable group
group('Year');

assertEntriesCount(23);
assertEntriesCount(24);
for (let i = 1; i <= 20; i++) {
assertInEntries(`This is post # ${i} --`);
}
Expand All @@ -61,10 +63,11 @@ describe('View Group', () => {
//enable group
group('Relation Test');

assertEntriesCount(23);
assertEntriesCount(24);
assertGroupsCount(3);
assertEachGroupCount('Relation Testtrue', 10);
assertEachGroupCount('Relation Testfalse', 10);
assertEachGroupCount('missing_value', 3);
// the 3 dated posts without a relation_test field, plus the index file
assertEachGroupCount('missing_value', 4);
});
});
27 changes: 27 additions & 0 deletions dev-test/backends/test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ collections: # A list of collections the CMS should be able to edit
pattern: \d{4}
- label: Relation Test
field: relation_test
index_file:
pattern: _index
editor:
preview: false
fields:
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
fields: # The fields each document in this collection have
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
- { label: 'Relation Test', name: 'relation_test', widget: 'hidden', default: true }
Expand Down Expand Up @@ -244,6 +251,26 @@ collections: # A list of collections the CMS should be able to edit
fields:
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
- name: sections # nested collection demonstrating index fields
label: Sections
label_singular: 'Section'
folder: _sections
create: true
nested: { depth: 3, subfolders: false }
index_file:
pattern: '^index$'
editor:
preview: false
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Section Intro', name: 'intro', widget: 'text', required: false }
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Body', name: 'body', widget: 'markdown' }
# `index_file.pattern` decides which entries get the index fields; `meta.path.index_file`
# decides the filename an index entry is written to. Both are needed, and must agree.
meta: { path: { widget: string, label: 'Path', index_file: 'index' } }

- name: pages # a nested collection
label: Pages
label_singular: 'Page'
Expand Down
19 changes: 19 additions & 0 deletions dev-test/backends/test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<script>
window.repoFiles = {
_posts: {
"_index.md": {
content: "---\ntitle: Posts list page\n---\n\nThis is a list of dummy posts\n"
},
"2015-02-14-this-is-a-post.md": {
content: "---\ntitle: This is a YAML front matter post\nimage: /nf-logo.png\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n"
},
Expand Down Expand Up @@ -78,6 +81,22 @@
content: "---\ntitle: \"This FAQ item # " + i + "\"\ndate: " + dateString + "T00:00:00.000Z\n---\n\n# Loren ipsum dolor sit amet"
}
}
window.repoFiles._sections = {
guides: {
'index.md': {
path: '_sections/guides/index.md',
content: '---\ntitle: "Guides"\nintro: "Everything you need to get going."\n---\n',
},
'first-steps.md': {
path: '_sections/guides/first-steps.md',
content: '---\ntitle: "First steps"\nbody: "A regular content page."\n---\n',
},
},
'index.md': {
path: '_sections/index.md',
content: '---\ntitle: "All sections"\nintro: "Top level section index."\n---\n',
},
};
window.repoFiles._pages = {
directory: {
'sub-directory': {
Expand Down
27 changes: 27 additions & 0 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ collections: # A list of collections the CMS should be able to edit
pattern: \d{4}
- label: Relation Test
field: relation_test
index_file:
pattern: _index
editor:
preview: false
fields:
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
fields: # The fields each document in this collection have
- { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' }
- { label: 'Relation Test', name: 'relation_test', widget: 'hidden', default: true }
Expand Down Expand Up @@ -286,6 +293,26 @@ collections: # A list of collections the CMS should be able to edit
fields:
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
- name: sections # nested collection demonstrating index fields
label: Sections
label_singular: 'Section'
folder: _sections
create: true
nested: { depth: 3, subfolders: false }
index_file:
pattern: '^index$'
editor:
preview: false
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Section Intro', name: 'intro', widget: 'text', required: false }
fields:
- { label: 'Title', name: 'title', widget: 'string' }
- { label: 'Body', name: 'body', widget: 'markdown' }
# `index_file.pattern` decides which entries get the index fields; `meta.path.index_file`
# decides the filename an index entry is written to. Both are needed, and must agree.
meta: { path: { widget: string, label: 'Path', index_file: 'index' } }

- name: pages # a nested collection
label: Pages
label_singular: 'Page'
Expand Down
19 changes: 19 additions & 0 deletions dev-test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<script>
window.repoFiles = {
_posts: {
"_index.md": {
content: "---\ntitle: Posts list page\n---\n\nThis is a list of dummy posts\n"
},
"2015-02-14-this-is-a-post.md": {
content: "---\ntitle: This is a YAML front matter post\nimage: /nf-logo.png\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n"
},
Expand Down Expand Up @@ -84,6 +87,22 @@
content: "---\ntitle: \"This FAQ item # " + i + "\"\ndate: " + dateString + "T00:00:00.000Z\n---\n\n# Loren ipsum dolor sit amet"
}
}
window.repoFiles._sections = {
guides: {
'index.md': {
path: '_sections/guides/index.md',
content: '---\ntitle: "Guides"\nintro: "Everything you need to get going."\n---\n',
},
'first-steps.md': {
path: '_sections/guides/first-steps.md',
content: '---\ntitle: "First steps"\nbody: "A regular content page."\n---\n',
},
},
'index.md': {
path: '_sections/index.md',
content: '---\ntitle: "All sections"\nintro: "Top level section index."\n---\n',
},
};
window.repoFiles._pages = {
directory: {
'sub-directory': {
Expand Down
Loading
Loading