Skip to content

Commit 4c8dbfe

Browse files
zJaaalclaude
andauthored
fix(content-drive): fix totalItems calculation and type paginate event (#34890)
## Summary - **Fix off-by-one in `$totalItems`**: `pagination.page` is already 1-indexed — removed the erroneous `+ 1` (and the trailing `- 1`) that inflated the total by a full page on every page - **Correct `hasMoreContent = true` formula**: changed from `items.length * currentPage` to `limit * (currentPage + 1)` so PrimeNG's next-page button is reliably enabled regardless of partial page sizes - **Extract `DotContentDrivePaginateEvent` type**: moved `LazyLoadEvent & { page: number }` into `@dotcms/dotcms-models` and used it across the `paginate` output in `DotFolderListViewComponent` and the `onPaginate` handler in the shell component - **Add ES2022 lib to portlet tsconfig**: scoped to `dot-content-drive` portlet only to enable `Array.prototype.at()` without a workspace-wide change ## Test plan - [ ] Navigate to Content Drive, verify pagination numbers are correct on each page - [ ] Verify "next page" button is enabled when `hasMoreContent = true` - [ ] Verify "next page" button is disabled on the last page (`hasMoreContent = false`) - [ ] Verify page resets to 1 when changing path, filters, or rows-per-page - [ ] Verify no TypeScript errors on `paginate` output / `onPaginate` handler 🤖 Generated with [Claude Code](https://claude.com/claude-code) This PR fixes: #34354 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f19482d commit 4c8dbfe

12 files changed

Lines changed: 275 additions & 87 deletions

core-web/libs/dotcms-models/src/lib/dot-content-drive.model.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { LazyLoadEvent } from 'primeng/api';
2+
13
import { DotCMSContentlet } from './dot-contentlet.model';
24

35
export interface DotContentDriveFolder {
@@ -28,6 +30,12 @@ export interface DotContentDriveFolder {
2830
// but for now we will just use the DotCMSContentlet until we have folders on the request response
2931
export type DotContentDriveItem = DotCMSContentlet | DotContentDriveFolder;
3032

33+
/**
34+
* Pagination event emitted by the folder list view,
35+
* extending PrimeNG's LazyLoadEvent with a resolved 1-indexed page number.
36+
*/
37+
export type DotContentDrivePaginateEvent = LazyLoadEvent & { page: number };
38+
3139
/**
3240
* Interface representing data needed for context menu interactions
3341
* @interface ContextMenuData
@@ -125,10 +133,16 @@ export interface DotContentDriveSearchRequest {
125133
filters?: DotContentDriveQueryFilters;
126134

127135
/**
128-
* Number of results to skip for pagination.
136+
* Number of content items to skip for pagination.
137+
* @default 0
138+
*/
139+
contentCursor?: number;
140+
141+
/**
142+
* Number of folder items to skip for pagination.
129143
* @default 0
130144
*/
131-
offset?: number;
145+
folderCursor?: number;
132146

133147
/**
134148
* Maximum number of results to return.
@@ -174,8 +188,11 @@ export interface DotContentDriveSearchRequest {
174188
* @property {DotContentDriveItem[]} list - The list of content items
175189
*/
176190
export interface DotContentDriveSearchResponse {
177-
contentTotalCount: number;
178191
folderCount: number;
179192
contentCount: number;
180193
list: DotContentDriveItem[];
194+
hasMoreContent: boolean;
195+
hasMoreFolders: boolean;
196+
nextContentCursor: number;
197+
nextFolderCursor: number;
181198
}

core-web/libs/portlets/dot-content-drive/portlet/src/lib/dot-content-drive-shell/dot-content-drive-shell.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
class="overflow-auto col-start-2 row-start-3">
4949
<dot-folder-list-view
5050
[items]="$items()"
51-
[totalItems]="$totalItems()"
5251
[loading]="$loading()"
52+
[totalItems]="$totalItems()"
5353
[offset]="$offset()"
5454
(paginate)="onPaginate($event)"
5555
(sort)="onSort($event)"

core-web/libs/portlets/dot-content-drive/portlet/src/lib/dot-content-drive-shell/dot-content-drive-shell.component.spec.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { CoreWebServiceMock } from '@dotcms/utils-testing';
4646
import { DotContentDriveShellComponent } from './dot-content-drive-shell.component';
4747

4848
import {
49+
DEFAULT_PAGE,
4950
DEFAULT_PAGINATION,
5051
DIALOG_TYPE,
5152
HIDE_MESSAGE_BANNER_LOCALSTORAGE_KEY,
@@ -147,7 +148,7 @@ describe('DotContentDriveShellComponent', () => {
147148
sort: jest
148149
.fn()
149150
.mockReturnValue({ field: 'modDate', order: DotContentDriveSortOrder.ASC }),
150-
totalItems: jest.fn().mockReturnValue(MOCK_ITEMS.length),
151+
pages: jest.fn().mockReturnValue([DEFAULT_PAGE]),
151152
setItems: jest.fn(),
152153
setStatus: jest.fn(),
153154
setPagination: jest.fn(),
@@ -368,15 +369,52 @@ describe('DotContentDriveShellComponent', () => {
368369
});
369370
});
370371

372+
describe('$totalItems', () => {
373+
it('should return limit * (currentPage + 1) when hasMoreContent is true', () => {
374+
// DEFAULT_PAGINATION: { page: 1, limit: 20 }, DEFAULT_PAGE: { hasMoreContent: true }
375+
store.pagination.mockReturnValue({ page: 1, limit: 20, offset: 0 });
376+
store.pages.mockReturnValue([DEFAULT_PAGE]);
377+
store.items.mockReturnValue(MOCK_ITEMS);
378+
spectator.detectChanges();
379+
380+
// hasMoreContent = true, page=1, limit=20 → 20 * (1+1) = 40
381+
expect(spectator.component.$totalItems()).toBe(40);
382+
});
383+
384+
it('should return exact total when hasMoreContent is false', () => {
385+
store.pagination.mockReturnValue({ page: 1, limit: 20, offset: 0 });
386+
store.pages.mockReturnValue([{ ...DEFAULT_PAGE, hasMoreContent: false }]);
387+
store.items.mockReturnValue(MOCK_ITEMS);
388+
spectator.detectChanges();
389+
390+
// hasMoreContent = false, page=1, limit=20, items=MOCK_ITEMS.length → 20*(1-1) + MOCK_ITEMS.length
391+
expect(spectator.component.$totalItems()).toBe(MOCK_ITEMS.length);
392+
});
393+
394+
it('should account for previous pages when hasMoreContent is false on page 2', () => {
395+
store.pagination.mockReturnValue({ page: 2, limit: 20, offset: 20 });
396+
store.pages.mockReturnValue([{ ...DEFAULT_PAGE, hasMoreContent: false }]);
397+
store.items.mockReturnValue(MOCK_ITEMS);
398+
spectator.detectChanges();
399+
400+
// hasMoreContent = false, page=2, limit=20, items=MOCK_ITEMS.length → 20*(2-1) + MOCK_ITEMS.length
401+
expect(spectator.component.$totalItems()).toBe(20 + MOCK_ITEMS.length);
402+
});
403+
});
404+
371405
describe('onPaginate', () => {
372406
it('should set pagination with provided values', () => {
373407
const folderListView = spectator.debugElement.query(
374408
By.directive(DotFolderListViewComponent)
375409
);
376410

377-
spectator.triggerEventHandler(folderListView, 'paginate', { rows: 10, first: 0 });
411+
spectator.triggerEventHandler(folderListView, 'paginate', {
412+
rows: 10,
413+
first: 0,
414+
page: 1
415+
});
378416

379-
expect(store.setPagination).toHaveBeenCalledWith({ limit: 10, offset: 0 });
417+
expect(store.setPagination).toHaveBeenCalledWith({ limit: 10, page: 1, offset: 0 });
380418
});
381419

382420
it('should not set pagination if rows are not provided', () => {

core-web/libs/portlets/dot-content-drive/portlet/src/lib/dot-content-drive-shell/dot-content-drive-shell.component.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from '@angular/core';
1515
import { Router } from '@angular/router';
1616

17-
import { LazyLoadEvent, MessageService, SortEvent } from 'primeng/api';
17+
import { MessageService, SortEvent } from 'primeng/api';
1818
import { ButtonModule } from 'primeng/button';
1919
import { DialogModule } from 'primeng/dialog';
2020
import { MessageModule } from 'primeng/message';
@@ -30,7 +30,11 @@ import {
3030
DotMessageService,
3131
DotWorkflowActionsFireService
3232
} from '@dotcms/data-access';
33-
import { ContextMenuData, DotContentDriveItem } from '@dotcms/dotcms-models';
33+
import {
34+
ContextMenuData,
35+
DotContentDriveItem,
36+
DotContentDrivePaginateEvent
37+
} from '@dotcms/dotcms-models';
3438
import {
3539
DotFolderListViewComponent,
3640
DotContentDriveUploadFiles,
@@ -97,7 +101,6 @@ export class DotContentDriveShellComponent {
97101
readonly #localStorageService = inject(DotLocalstorageService);
98102

99103
readonly $items = this.#store.items;
100-
readonly $totalItems = this.#store.totalItems;
101104
readonly $status = this.#store.status;
102105
readonly $treeExpanded = this.#store.isTreeExpanded;
103106

@@ -116,6 +119,22 @@ export class DotContentDriveShellComponent {
116119

117120
readonly $fileInput = viewChild<ElementRef>('fileInput');
118121

122+
readonly $totalItems = computed(() => {
123+
const pagination = untracked(() => this.#store.pagination());
124+
const currentPage = pagination.page; // 1-indexed
125+
const limit = pagination.limit;
126+
const page = this.#store.pages().at(-1);
127+
128+
const items = untracked(() => this.#store.items());
129+
130+
// The API uses cursor-based pagination and does not return a total count.
131+
// When hasMoreContent is true, we return one page beyond current so PrimeNG enables the next-page button.
132+
// When hasMoreContent is false, we can calculate the exact total.
133+
return page.hasMoreContent
134+
? limit * (currentPage + 1)
135+
: limit * (currentPage - 1) + items.length;
136+
});
137+
119138
readonly updateQueryParamsEffect = effect(() => {
120139
const isTreeExpanded = this.#store.isTreeExpanded();
121140
const path = this.#store.path();
@@ -167,15 +186,16 @@ export class DotContentDriveShellComponent {
167186
);
168187
}
169188

170-
protected onPaginate(event: LazyLoadEvent) {
189+
protected onPaginate(event: DotContentDrivePaginateEvent) {
171190
// Explicit check because it can potentially be 0
172191
if (event.rows === undefined || event.first === undefined) {
173192
return;
174193
}
175194

176195
this.#store.setPagination({
177196
limit: event.rows,
178-
offset: event.first
197+
page: event.page ?? 1,
198+
offset: event.first ?? 0
179199
});
180200
}
181201

core-web/libs/portlets/dot-content-drive/portlet/src/lib/shared/constants.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DotCMSBaseTypesContentTypes, DotSite } from '@dotcms/dotcms-models';
22

3-
import { DotContentDrivePagination, DotContentDriveSortOrder } from './models';
3+
import { DotContentDrivePage, DotContentDrivePagination, DotContentDriveSortOrder } from './models';
44

55
export const HIDE_MESSAGE_BANNER_LOCALSTORAGE_KEY = 'content-drive-hide-message-banner';
66

@@ -18,6 +18,7 @@ export const BASE_QUERY = '+systemType:false -contentType:forms -contentType:Hos
1818
// Default pagination
1919
export const DEFAULT_PAGINATION: DotContentDrivePagination = {
2020
limit: 20,
21+
page: 1,
2122
offset: 0
2223
};
2324

@@ -38,6 +39,14 @@ export const DEFAULT_TREE_EXPANDED = true;
3839
// Default path, it needs to be undefined to show the root folder
3940
export const DEFAULT_PATH = undefined;
4041

42+
export const DEFAULT_PAGE: DotContentDrivePage = {
43+
hasMoreContent: true,
44+
hasMoreFolders: true,
45+
folderCursor: 0,
46+
contentCursor: 0,
47+
offset: 0
48+
};
49+
4150
// Map numbers to base types, ticket: https://github.com/dotCMS/core/issues/32991
4251
export const MAP_NUMBERS_TO_BASE_TYPES = {
4352
1: DotCMSBaseTypesContentTypes.CONTENT,

core-web/libs/portlets/dot-content-drive/portlet/src/lib/shared/models.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export enum DotContentDriveSortOrder {
3939
*/
4040
export interface DotContentDrivePagination {
4141
limit: number;
42+
page: number;
4243
offset: number;
4344
}
4445

@@ -84,6 +85,14 @@ export interface DotContentDriveDialog {
8485
payload?: DotContentDriveFolder;
8586
}
8687

88+
export interface DotContentDrivePage {
89+
hasMoreContent: boolean;
90+
hasMoreFolders: boolean;
91+
folderCursor: number;
92+
contentCursor: number;
93+
offset: number;
94+
}
95+
8796
/**
8897
* The state of the content drive.
8998
*
@@ -94,10 +103,10 @@ export interface DotContentDriveState extends DotContentDriveInit {
94103
items: DotContentDriveItem[];
95104
selectedItems: DotContentDriveItem[];
96105
status: DotContentDriveStatus;
97-
totalItems: number;
98106
pagination: DotContentDrivePagination;
99107
sort: DotContentDriveSort;
100108
contextMenu?: DotContentDriveContextMenu;
109+
pages: DotContentDrivePage[];
101110
}
102111

103112
/**

0 commit comments

Comments
 (0)