Skip to content

Commit 790f362

Browse files
az108claude
andcommitted
Add jobs-per-page selector to job overview pagination
Default is now 15 jobs and the chosen value is stored in localStorage so it survives navigation and reloads. The PrimeNG paginator exposes a dropdown with options 15, 30 and 50. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bffa799 commit 790f362

5 files changed

Lines changed: 97 additions & 3 deletions

File tree

src/main/webapp/app/job/job-overview/job-card-list/job-card-list.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
[value]="jobs()"
6060
[paginator]="true"
6161
[rows]="pageSize()"
62+
[rowsPerPageOptions]="pageSizeOptions"
6263
[totalRecords]="totalRecords()"
6364
[lazy]="true"
6465
(onLazyLoad)="loadOnTableEmit($event)"

src/main/webapp/app/job/job-overview/job-card-list/job-card-list.component.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ import { TranslateDirective } from 'app/shared/language';
1414
import { AccountService } from 'app/core/auth/account.service';
1515
import { JobFormDTOLocationEnum, JobFormDTOSubjectAreaEnum } from 'app/generated/model/job-form-dto';
1616
import { UserShortDTORolesEnum } from 'app/generated/model/user-short-dto';
17+
import { LocalStorageService } from 'app/service/localStorage.service';
1718

1819
import { ApplicationStatusExtended, JobCardComponent } from '../job-card/job-card.component';
1920
import { JobCardDTO } from '../../../generated/model/job-card-dto';
2021
import { JobResourceApi } from '../../../generated/api/job-resource-api';
2122
import * as DropdownOptions from '../.././dropdown-options';
2223

24+
export const JOBS_PER_PAGE_DEFAULT = 15;
25+
export const JOBS_PER_PAGE_OPTIONS: readonly number[] = [15, 30, 50];
26+
2327
@Component({
2428
selector: 'jhi-job-card-list',
2529
standalone: true,
@@ -32,7 +36,8 @@ export class JobCardListComponent {
3236
jobs = signal<JobCardDTO[]>([]);
3337
totalRecords = signal<number>(0);
3438
page = signal<number>(0);
35-
pageSize = signal<number>(12);
39+
pageSize = signal<number>(JOBS_PER_PAGE_DEFAULT);
40+
pageSizeOptions = JOBS_PER_PAGE_OPTIONS;
3641
searchQuery = signal<string>('');
3742

3843
sortBy = signal<string>('startDate');
@@ -68,6 +73,7 @@ export class JobCardListComponent {
6873

6974
private jobApi = inject(JobResourceApi);
7075
private readonly toastService = inject(ToastService);
76+
private readonly localStorageService = inject(LocalStorageService);
7177

7278
private readonly loadJobsEffect = effect(() => {
7379
this.page();
@@ -82,6 +88,7 @@ export class JobCardListComponent {
8288
});
8389

8490
constructor() {
91+
this.pageSize.set(this.localStorageService.loadJobsPerPage(JOBS_PER_PAGE_DEFAULT, JOBS_PER_PAGE_OPTIONS));
8592
void this.loadAllFilter();
8693
}
8794

@@ -90,7 +97,10 @@ export class JobCardListComponent {
9097
const size = event.rows ?? this.pageSize();
9198

9299
this.page.set(page);
93-
this.pageSize.set(size);
100+
if (size !== this.pageSize()) {
101+
this.pageSize.set(size);
102+
this.localStorageService.saveJobsPerPage(size);
103+
}
94104
}
95105

96106
onSearchEmit(searchQuery: string): void {

src/main/webapp/app/service/localStorage.service.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface ApplicationDraftData {
1414
export class LocalStorageService {
1515
readonly APPLICATION_DRAFT_VALIDITY_DURATION_IN_DAYS = 30;
1616
readonly SIDEBAR_STATE_KEY = 'sidebarCollapsed';
17+
readonly JOBS_PER_PAGE_KEY = 'jobsPerPage';
1718
readonly sidebarCollapsed = signal(localStorage.getItem(this.SIDEBAR_STATE_KEY) === 'true');
1819

1920
// =======================================================
@@ -67,6 +68,35 @@ export class LocalStorageService {
6768
localStorage.setItem(this.SIDEBAR_STATE_KEY, String(this.sidebarCollapsed()));
6869
}
6970

71+
// =======================================================
72+
// JOBS PER PAGE PREFERENCE
73+
// =======================================================
74+
75+
/**
76+
* Returns the user's stored jobs-per-page preference.
77+
*
78+
* @param fallback value returned when nothing is stored or the value cannot be parsed
79+
* @param allowed optional whitelist; values outside it are treated as missing
80+
* @returns the stored page size if valid, otherwise the fallback
81+
*/
82+
loadJobsPerPage(fallback: number, allowed?: readonly number[]): number {
83+
const raw = localStorage.getItem(this.JOBS_PER_PAGE_KEY);
84+
if (raw === null) return fallback;
85+
const parsed = Number(raw);
86+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
87+
if (allowed && !allowed.includes(parsed)) return fallback;
88+
return parsed;
89+
}
90+
91+
/**
92+
* Persists the user's jobs-per-page preference so it survives navigation and reloads.
93+
*
94+
* @param pageSize the page size to remember
95+
*/
96+
saveJobsPerPage(pageSize: number): void {
97+
localStorage.setItem(this.JOBS_PER_PAGE_KEY, String(pageSize));
98+
}
99+
70100
private getApplicationKey(applicationId?: string, jobId?: string): string {
71101
if (applicationId) return `application_draft_${applicationId}`;
72102
if (jobId) return `application_draft_job_${jobId}`;

src/test/webapp/app/job/job-overview/job-card-list/job-card-list.component.spec.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
33
import { of, throwError } from 'rxjs';
44
import { provideRouter, Router } from '@angular/router';
55

6-
import { JobCardListComponent } from 'app/job/job-overview/job-card-list/job-card-list.component';
6+
import { JobCardListComponent, JOBS_PER_PAGE_DEFAULT } from 'app/job/job-overview/job-card-list/job-card-list.component';
7+
import { LocalStorageService } from 'app/service/localStorage.service';
78
import { JobResourceApi } from 'app/generated/api/job-resource-api';
89
import { provideTranslateMock } from 'src/test/webapp/util/translate.mock';
910
import { provideFontAwesomeTesting } from 'src/test/webapp/util/fontawesome.testing';
@@ -30,6 +31,7 @@ describe('JobCardListComponent', () => {
3031
let mockToastService = createToastServiceMock();
3132

3233
beforeEach(async () => {
34+
localStorage.clear();
3335
jobApi = {
3436
getAllFilters: vi.fn().mockReturnValue(
3537
of({
@@ -185,6 +187,38 @@ describe('JobCardListComponent', () => {
185187
expect(spy).toHaveBeenCalledOnce();
186188
});
187189

190+
it('should default pageSize to 15 when nothing is stored', () => {
191+
expect(component.pageSize()).toBe(JOBS_PER_PAGE_DEFAULT);
192+
});
193+
194+
it('should hydrate pageSize from localStorage on construction', () => {
195+
TestBed.inject(LocalStorageService).saveJobsPerPage(30);
196+
197+
const hydratedFixture = TestBed.createComponent(JobCardListComponent);
198+
hydratedFixture.detectChanges();
199+
200+
expect(hydratedFixture.componentInstance.pageSize()).toBe(30);
201+
});
202+
203+
it('should persist a new pageSize when lazy-load reports a different value', () => {
204+
vi.spyOn(component, 'loadJobs').mockResolvedValue();
205+
const saveSpy = vi.spyOn(TestBed.inject(LocalStorageService), 'saveJobsPerPage');
206+
207+
component.loadOnTableEmit({ first: 0, rows: 30 });
208+
209+
expect(component.pageSize()).toBe(30);
210+
expect(saveSpy).toHaveBeenCalledExactlyOnceWith(30);
211+
});
212+
213+
it('should not write to localStorage when lazy-load reports the same pageSize', () => {
214+
vi.spyOn(component, 'loadJobs').mockResolvedValue();
215+
const saveSpy = vi.spyOn(TestBed.inject(LocalStorageService), 'saveJobsPerPage');
216+
217+
component.loadOnTableEmit({ first: 0, rows: component.pageSize() });
218+
219+
expect(saveSpy).not.toHaveBeenCalled();
220+
});
221+
188222
it('should set empty jobs and totalRecords when API returns no content', async () => {
189223
jobApi.getAvailableJobs.mockReturnValueOnce(of({ content: undefined, totalElements: undefined }));
190224

src/test/webapp/app/service/localStorage.service.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,25 @@ describe('LocalStorageService', () => {
9090
expect(() => getApplicationKey.call(service, undefined, undefined)).toThrowError();
9191
});
9292

93+
it('should return the fallback when no jobs-per-page preference is stored', () => {
94+
expect(service.loadJobsPerPage(15)).toBe(15);
95+
});
96+
97+
it('should return the stored jobs-per-page preference', () => {
98+
service.saveJobsPerPage(30);
99+
expect(service.loadJobsPerPage(15)).toBe(30);
100+
});
101+
102+
it('should fall back when the stored jobs-per-page value is not in the allowed set', () => {
103+
service.saveJobsPerPage(7);
104+
expect(service.loadJobsPerPage(15, [15, 30, 50])).toBe(15);
105+
});
106+
107+
it('should fall back when the stored jobs-per-page value cannot be parsed', () => {
108+
localStorage.setItem(service.JOBS_PER_PAGE_KEY, 'not-a-number');
109+
expect(service.loadJobsPerPage(15)).toBe(15);
110+
});
111+
93112
it('rethrows error when JSON.stringify fails (circular data)', () => {
94113
const circularPersonal: ApplicationDraftData['personalInfoData'] & { self?: any } = {
95114
firstName: emptyPersonalInfo.firstName,

0 commit comments

Comments
 (0)