Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ <h1 jhiTranslate="dependencies.title">Software Dependencies</h1>
[totalRecords]="filteredDependencyCount()"
[page]="currentPage()"
[rows]="pageSize()"
[rowsPerPageOptions]="rowsPerPageOptions"
[loading]="isLoading()"
[selectable]="false"
(lazyLoad)="onPageChange($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export class AdminDependenciesComponent {
/** Number of rows displayed per table page. */
readonly pageSize = signal(25);

/** The dependency list runs to hundreds of entries, so it offers larger pages than the shared default. */
readonly rowsPerPageOptions: number[] = [25, 50, 100];

/** Current search query text entered in the search bar. */
readonly searchQuery = signal('');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ <h1>
[totalRecords]="totalRecords()"
[page]="page()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[loading]="loading()"
[selectable]="false"
(lazyLoad)="loadOnTableEmit($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ <h1 jhiTranslate="entity.application_overview.applicant.header"></h1>
[data]="pageData()"
[totalRecords]="total()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[loading]="loading()"
(lazyLoad)="loadPage($event)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ <h1>
[totalRecords]="total()"
[page]="page()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[loading]="loading()"
[selectable]="false"
(lazyLoad)="loadOnTableEmit($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
[totalRecords]="totalApplicants()"
[page]="pageNumber()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[selectable]="false"
(lazyLoad)="loadOnTableEmit($event)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ <h3 class="text-base font-semibold text-text-primary">{{ slotDate() }} | {{ slot
[rows]="availableInterviewees().length"
[totalRecords]="availableInterviewees().length"
[hideHeader]="true"
[paginator]="false"
/>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ <h1>
[totalRecords]="totalRecords()"
[page]="page()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[loading]="loading()"
[selectable]="false"
(lazyLoad)="loadOnTableEmit($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@
} @else {
<p class="text-center p-8" jhiTranslate="jobOverviewPage.noJobsFound"></p>
}
<p-table
[value]="jobs()"
[paginator]="true"
<jhi-dynamic-table
[data]="[]"
[columns]="[]"
[hideHeader]="true"
[rows]="pageSize()"
[page]="page()"
[totalRecords]="totalRecords()"
[lazy]="true"
(onLazyLoad)="loadOnTableEmit($event)"
[tableStyle]="{ width: '100%' }"
[storageKey]="jobsPerPageStorageKey"
[alwaysUseMobileRows]="true"
(lazyLoad)="loadOnTableEmit($event)"
(rowsHydrated)="onPageSizeHydrated($event)"
/>
@if (canManageSubjectAreaSubscriptions()) {
<p class="mx-auto mt-5 mb-8 max-w-3xl text-center text-sm text-text-secondary">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component, computed, inject, signal } from '@angular/core';
import { TableLazyLoadEvent, TableModule } from 'primeng/table';
import { PaginatorModule } from 'primeng/paginator';
import { TableLazyLoadEvent } from 'primeng/table';
import { firstValueFrom, map } from 'rxjs';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { toSignal } from '@angular/core/rxjs-interop';
Expand All @@ -14,16 +13,19 @@ import { TranslateDirective } from 'app/shared/language';
import { AccountService } from 'app/core/auth/account.service';
import { JobFormDTOLocationEnum, JobFormDTOSubjectAreaEnum } from 'app/generated/model/job-form-dto';
import { UserShortDTORolesEnum } from 'app/generated/model/user-short-dto';
import { DynamicTableComponent } from 'app/shared/components/organisms/dynamic-table/dynamic-table.component';

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

export const JOBS_PER_PAGE_STORAGE_KEY = 'jobsPerPage';

@Component({
selector: 'jhi-job-card-list',
standalone: true,
imports: [TableModule, JobCardComponent, PaginatorModule, SearchFilterSortBar, TranslateModule, TranslateDirective, RouterLink],
imports: [DynamicTableComponent, JobCardComponent, SearchFilterSortBar, TranslateModule, TranslateDirective, RouterLink],
templateUrl: './job-card-list.component.html',
})
export class JobCardListComponent {
Expand All @@ -32,9 +34,11 @@ export class JobCardListComponent {
jobs = signal<JobCardDTO[]>([]);
totalRecords = signal<number>(0);
page = signal<number>(0);
pageSize = signal<number>(12);
pageSize = signal<number>(10);
Comment thread
Cathy0123456789 marked this conversation as resolved.
Outdated
searchQuery = signal<string>('');

readonly jobsPerPageStorageKey = JOBS_PER_PAGE_STORAGE_KEY;

sortBy = signal<string>('startDate');
sortDirection = signal<'ASC' | 'DESC'>('DESC');

Expand Down Expand Up @@ -85,6 +89,10 @@ export class JobCardListComponent {
void this.loadJobs();
}

onPageSizeHydrated(size: number): void {
this.pageSize.set(size);
}

onSearchEmit(searchQuery: string): void {
const normalizedQuery = searchQuery.trim().replace(/\s+/g, ' ');
const currentQuery = this.searchQuery().trim().replace(/\s+/g, ' ');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ <h1>
[totalRecords]="totalRecords()"
[page]="page()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[loading]="loading()"
[selectable]="false"
(lazyLoad)="loadOnTableEmit($event)"
Expand Down
31 changes: 31 additions & 0 deletions src/main/webapp/app/service/localStorage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,37 @@ export class LocalStorageService {
localStorage.setItem(this.SIDEBAR_STATE_KEY, String(this.sidebarCollapsed()));
}

// =======================================================
// PAGE SIZE PREFERENCE
// =======================================================

/**
* Returns the user's stored page-size preference for a given key.
*
* @param key storage key identifying the paginated view
* @param fallback value returned when nothing is stored or the value cannot be parsed
* @param allowed optional whitelist; values outside it are treated as missing
* @returns the stored page size if valid, otherwise the fallback
*/
loadPageSize(key: string, fallback: number, allowed?: readonly number[]): number {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
if (allowed && !allowed.includes(parsed)) return fallback;
return parsed;
}

/**
* Persists a page-size preference under the given key so it survives navigation and reloads.
*
* @param key storage key identifying the paginated view
* @param pageSize the page size to remember
*/
savePageSize(key: string, pageSize: number): void {
localStorage.setItem(key, String(pageSize));
}

private getApplicationKey(applicationId?: string, jobId?: string): string {
if (applicationId) return `application_draft_${applicationId}`;
if (jobId) return `application_draft_job_${jobId}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
[scrollable]="true"
[first]="page() * rows()"
[rows]="rows()"
[rowsPerPageOptions]="rowsPerPageOptions()"
[lazy]="lazy()"
[lazyLoadOnInit]="false"
(onLazyLoad)="emitLazy($event)"
[tableStyle]="{ width: '100%' }"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Component, TemplateRef, input, output } from '@angular/core';
import { Component, TemplateRef, afterNextRender, inject, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TableLazyLoadEvent, TableModule } from 'primeng/table';
import { ButtonModule } from 'primeng/button';
import { TranslateDirective } from 'app/shared/language';
import { ProgressSpinnerComponent } from 'app/shared/components/atoms/progress-spinner/progress-spinner.component';
import { LocalStorageService } from 'app/service/localStorage.service';
import { BREAKPOINT_QUERIES } from 'app/shared/constants/breakpoints';

export class DynamicTableColumn {
field!: string;
Expand All @@ -14,6 +16,14 @@ export class DynamicTableColumn {
template?: TemplateRef<unknown>;
}

export const DEFAULT_ROWS_PER_PAGE_OPTIONS: number[] = [5, 10, 15, 20];

/** Page size small screens start on, where a full page of rows is a long scroll. */
export const MOBILE_ROWS_PER_PAGE = 5;

/** Stands in for "the user has not picked a page size yet", since any real size is positive. */
const NO_STORED_SIZE = -1;

@Component({
selector: 'jhi-dynamic-table',
standalone: true,
Expand All @@ -32,10 +42,84 @@ export class DynamicTableComponent {
hideHeader = input<boolean>(false);
paginator = input<boolean>(true);
lazy = input<boolean>(true);
rowsPerPageOptions = input<number[]>(DEFAULT_ROWS_PER_PAGE_OPTIONS);
storageKey = input<string | undefined>(undefined);
/**
* Whether small screens always open on the short page, even when the reader picked a size before.
* Off by default, so a remembered size normally wins.
*/
alwaysUseMobileRows = input<boolean>(false);

lazyLoad = output<TableLazyLoadEvent>();
rowsHydrated = output<number>();

private readonly localStorageService = inject(LocalStorageService);

constructor() {
// The table owns the first load rather than PrimeNG, which would fire it before the page size below
// is known. Changing the size afterwards only relabels the paginator, since PrimeNG does not reload
// when the rows input changes, which would leave a page of the old size on screen under the new label.
afterNextRender(() => {
const initial = this.resolveInitialRows();
if (initial !== this.rows()) {
this.rowsHydrated.emit(initial);
}
if (this.lazy()) {
this.lazyLoad.emit({ first: 0, rows: initial });
}
});
}

emitLazy(event: TableLazyLoadEvent): void {
const key = this.storageKey();
if (key !== undefined && event.rows !== undefined && event.rows !== null && event.rows !== this.rows()) {
this.localStorageService.savePageSize(key, event.rows);
}
this.lazyLoad.emit(event);
}

/**
* Works out which page size to start on.
*
* A full page of rows is a long scroll on a phone, so small screens start on a shorter page than
* the view asked for. Views that set {@link alwaysUseMobileRows} keep that short page on every
* visit; everywhere else a size the reader picked before wins.
*
* @returns the page size to start on
*/
private resolveInitialRows(): number {
const mobileRows = this.mobileRows();
if (mobileRows !== undefined && this.alwaysUseMobileRows()) {
return mobileRows;
}

const key = this.storageKey();
if (key !== undefined) {
const stored = this.localStorageService.loadPageSize(key, NO_STORED_SIZE, this.rowsPerPageOptions());
if (stored !== NO_STORED_SIZE) {
return stored;
}
}
return mobileRows ?? this.rows();
}

/**
* @returns the short page size on a phone-sized viewport that offers it, otherwise {@code undefined}
*/
private mobileRows(): number | undefined {
if (!this.isMobileViewport() || !this.rowsPerPageOptions().includes(MOBILE_ROWS_PER_PAGE)) {
return undefined;
}
return MOBILE_ROWS_PER_PAGE;
}

/**
* @returns {@code true} if the viewport is phone-sized, {@code false} where it cannot be determined
*/
private isMobileViewport(): boolean {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return false;
}
return window.matchMedia(BREAKPOINT_QUERIES.onlyMobile).matches;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ <h1 jhiTranslate="researchGroup.adminView.title"></h1>
[totalRecords]="totalRecords()"
[page]="page()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
(lazyLoad)="loadOnTableEmit($event)"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ <h1 jhiTranslate="researchGroup.departments.header">Departments</h1>
[totalRecords]="total()"
[page]="pageNumber()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[selectable]="false"
(lazyLoad)="loadOnTableEmit($event)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ <h1>
[totalRecords]="total()"
[page]="pageNumber()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[selectable]="false"
(lazyLoad)="loadOnTableEmit($event)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ <h1 jhiTranslate="researchGroup.schools.header">Schools</h1>
[totalRecords]="total()"
[page]="pageNumber()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[selectable]="false"
(lazyLoad)="loadOnTableEmit($event)"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1>
[columns]="columns()"
[data]="tableData()"
[rows]="pageSize()"
(rowsHydrated)="pageSize.set($event)"
[totalRecords]="total()"
[page]="pageNumber()"
(lazyLoad)="onTableEmit($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('JobCardListComponent', () => {
let mockToastService = createToastServiceMock();

beforeEach(async () => {
localStorage.clear();
jobApi = {
getAllFilters: vi.fn().mockReturnValue(
of({
Expand Down Expand Up @@ -185,6 +186,12 @@ describe('JobCardListComponent', () => {
expect(spy).toHaveBeenCalledOnce();
});

it('should update pageSize when dynamic-table reports a hydrated value', () => {
component.onPageSizeHydrated(30);

expect(component.pageSize()).toBe(30);
});

it('should set empty jobs and totalRecords when API returns no content', async () => {
jobApi.getAvailableJobs.mockReturnValueOnce(of({ content: undefined, totalElements: undefined }));

Expand Down
19 changes: 19 additions & 0 deletions src/test/webapp/app/service/localStorage.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ describe('LocalStorageService', () => {
expect(() => getApplicationKey.call(service, undefined, undefined)).toThrowError();
});

it('should return the fallback when no page-size preference is stored', () => {
expect(service.loadPageSize('jobsPerPage', 10)).toBe(10);
});

it('should return the stored page-size preference', () => {
service.savePageSize('jobsPerPage', 20);
expect(service.loadPageSize('jobsPerPage', 10)).toBe(20);
});

it('should fall back when the stored page-size value is not in the allowed set', () => {
service.savePageSize('jobsPerPage', 7);
expect(service.loadPageSize('jobsPerPage', 10, [5, 10, 15, 20])).toBe(10);
});

it('should fall back when the stored page-size value cannot be parsed', () => {
localStorage.setItem('jobsPerPage', 'not-a-number');
expect(service.loadPageSize('jobsPerPage', 10)).toBe(10);
});

it('rethrows error when JSON.stringify fails (circular data)', () => {
const circularPersonal: ApplicationDraftData['personalInfoData'] & { self?: any } = {
firstName: emptyPersonalInfo.firstName,
Expand Down
Loading
Loading