Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ collections: # A list of collections the CMS should be able to edit
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
create: true # Allow users to create new documents in this collection
pagination:
enabled: true
per_page: 10
editor:
visualEditing: true
view_filters:
Expand Down
13 changes: 11 additions & 2 deletions packages/decap-cms-backend-git-gateway/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,17 @@ export default class GitGateway implements Implementation {
return this.tokenPromise!();
}

async entriesByFolder(folder: string, extension: string, depth: number) {
return this.backend!.entriesByFolder(folder, extension, depth);
async entriesByFolder(
folder: string,
extension: string,
depth: number,
options?: {
page?: number;
pageSize?: number;
pagination?: boolean;
},
) {
return this.backend!.entriesByFolder(folder, extension, depth, options);
}
allEntriesByFolder(folder: string, extension: string, depth: number, pathRegex?: RegExp) {
return this.backend!.allEntriesByFolder(folder, extension, depth, pathRegex);
Expand Down
43 changes: 32 additions & 11 deletions packages/decap-cms-backend-github/src/implementation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,7 @@ export default class GitHub implements Implementation {
return Promise.resolve(this.token);
}

getCursorAndFiles = (files: ApiFile[], page: number) => {
const pageSize = 20;
getCursorAndFiles = (files: ApiFile[], page: number, pageSize = 20) => {
const count = files.length;
const pageCount = Math.ceil(files.length / pageSize);

Expand All @@ -413,8 +412,20 @@ export default class GitHub implements Implementation {
return { cursor, files: pageFiles };
};

async entriesByFolder(folder: string, extension: string, depth: number) {
async entriesByFolder(
folder: string,
extension: string,
depth: number,
options?: {
page?: number;
pageSize?: number;
pagination?: boolean;
},
) {
const repoURL = this.api!.originRepoURL;
const page = options?.page ?? 1;
const pageSize = options?.pageSize ?? 20;
const usePagination = options?.pagination ?? true;

let cursor: Cursor;

Expand All @@ -424,9 +435,18 @@ export default class GitHub implements Implementation {
depth,
}).then(files => {
const filtered = files.filter(file => filterByExtension(file, extension));
const result = this.getCursorAndFiles(filtered, 1);
cursor = result.cursor;
return result.files;

if (usePagination) {
// Paginated: return only the requested page
const result = this.getCursorAndFiles(filtered, page, pageSize);
cursor = result.cursor;
return result.files;
} else {
// Non-paginated: return all files (no slicing)
const result = this.getCursorAndFiles(filtered, 1, pageSize);
cursor = result.cursor;
return filtered;
}
});

const readFile = (path: string, id: string | null | undefined) =>
Expand Down Expand Up @@ -562,27 +582,28 @@ export default class GitHub implements Implementation {
async traverseCursor(cursor: Cursor, action: string) {
const meta = cursor.meta!;
const files = cursor.data!.get('files')!.toJS() as ApiFile[];
const pageSize = meta.get('pageSize') || 20;

let result: { cursor: Cursor; files: ApiFile[] };
switch (action) {
case 'first': {
result = this.getCursorAndFiles(files, 1);
result = this.getCursorAndFiles(files, 1, pageSize);
break;
}
case 'last': {
result = this.getCursorAndFiles(files, meta.get('pageCount'));
result = this.getCursorAndFiles(files, meta.get('pageCount'), pageSize);
break;
}
case 'next': {
result = this.getCursorAndFiles(files, meta.get('page') + 1);
result = this.getCursorAndFiles(files, meta.get('page') + 1, pageSize);
break;
}
case 'prev': {
result = this.getCursorAndFiles(files, meta.get('page') - 1);
result = this.getCursorAndFiles(files, meta.get('page') - 1, pageSize);
break;
}
default: {
result = this.getCursorAndFiles(files, 1);
result = this.getCursorAndFiles(files, 1, pageSize);
break;
}
}
Expand Down
13 changes: 11 additions & 2 deletions packages/decap-cms-backend-proxy/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,19 @@ export default class ProxyBackend implements Implementation {
}
}

entriesByFolder(folder: string, extension: string, depth: number) {
entriesByFolder(
folder: string,
extension: string,
depth: number,
options?: {
page?: number;
pageSize?: number;
pagination?: boolean;
},
) {
return this.request({
action: 'entriesByFolder',
params: { branch: this.branch, folder, extension, depth },
params: { branch: this.branch, folder, extension, depth, options },
});
}

Expand Down
39 changes: 31 additions & 8 deletions packages/decap-cms-backend-test/src/implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,24 @@ function deleteFile(path: string, tree: RepoTree) {
unset(tree, path.split('/'));
}

const pageSize = 10;
const DEFAULT_PAGE_SIZE = 10;

function getCursor(
folder: string,
extension: string,
entries: ImplementationEntry[],
index: number,
depth: number,
pageSize = DEFAULT_PAGE_SIZE,
) {
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Division by zero is possible if pageSize is 0. Although the default is 10, the function accepts a pageSize parameter that could be 0 or negative. Add validation: if (pageSize <= 0) throw new Error('pageSize must be positive'); at the start of the getCursor function.

Suggested change
) {
) {
if (pageSize <= 0) {
throw new Error('pageSize must be positive');
}

Copilot uses AI. Check for mistakes.
const count = entries.length;
const pageCount = Math.floor(count / pageSize);
const pageCount = Math.ceil(count / pageSize);
return Cursor.create({
actions: [
...(index < pageCount ? ['next', 'last'] : []),
...(index > 0 ? ['prev', 'first'] : []),
],
meta: { index, count, pageSize, pageCount },
meta: { index, page: index + 1, count, pageSize, pageCount },
data: { folder, extension, index, pageCount, depth },
});
}
Expand Down Expand Up @@ -173,6 +174,9 @@ export default class TestBackend implements Implementation {
pageCount: number;
depth: number;
};
const meta = cursor.meta!;
const currentPageSize = meta.get('pageSize', DEFAULT_PAGE_SIZE);

const newIndex = (() => {
if (action === 'next') {
return (index as number) + 1;
Expand All @@ -194,19 +198,38 @@ export default class TestBackend implements Implementation {
data: f.content as string,
file: { path: f.path, id: f.path },
}));
const entries = allEntries.slice(newIndex * pageSize, newIndex * pageSize + pageSize);
const newCursor = getCursor(folder, extension, allEntries, newIndex, depth);
const entries = allEntries.slice(
newIndex * currentPageSize,
newIndex * currentPageSize + currentPageSize,
);
const newCursor = getCursor(folder, extension, allEntries, newIndex, depth, currentPageSize);
return Promise.resolve({ entries, cursor: newCursor });
}

entriesByFolder(folder: string, extension: string, depth: number) {
entriesByFolder(
folder: string,
extension: string,
depth: number,
options?: {
page?: number;
pageSize?: number;
pagination?: boolean;
},
) {
const files = folder ? getFolderFiles(window.repoFiles, folder, extension, depth) : [];
const entries = files.map(f => ({
data: f.content as string,
file: { path: f.path, id: f.path },
}));
const cursor = getCursor(folder, extension, entries, 0, depth);
const ret = take(entries, pageSize);
const pageSize = options?.pageSize ?? DEFAULT_PAGE_SIZE;
const page = options?.page ?? 1;
const usePagination = options?.pagination ?? true;

const cursor = getCursor(folder, extension, entries, page - 1, depth, pageSize);

// If pagination is enabled, return only the requested page
// Otherwise, return all entries (for backward compatibility)
const ret = usePagination ? take(entries.slice((page - 1) * pageSize), pageSize) : entries;
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic is incorrect. entries.slice((page - 1) * pageSize) already slices to the start of the desired page. Calling take(..., pageSize) on the result will work, but it's redundant. The correct implementation should be: entries.slice((page - 1) * pageSize, page * pageSize) to get exactly one page of results.

Suggested change
const ret = usePagination ? take(entries.slice((page - 1) * pageSize), pageSize) : entries;
const ret = usePagination ? entries.slice((page - 1) * pageSize, page * pageSize) : entries;

Copilot uses AI. Check for mistakes.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ret[CURSOR_COMPATIBILITY_SYMBOL] = cursor;
Expand Down
Loading
Loading