refactor(frontend): migrate remaining pagination to Pagination component (4 files)

Replaced hand-rolled prev/next buttons and 'Showing X-Y of Z' with
<Pagination> from $lib/ui:
- translate/+page, translate/history, validation-tasks/history,
  validation-tasks/[policyId]

Fixed latent bug in validation-tasks/history where prev/next always
called page=1 instead of the actual page number.
This commit is contained in:
2026-06-17 15:22:39 +03:00
parent 286133957b
commit fba46a5a42
4 changed files with 92 additions and 68 deletions

View File

@@ -6,7 +6,7 @@
import { ROUTES } from '$lib/routes';
import { t, _ } from '$lib/i18n/index.svelte.js';
import { addToast } from '$lib/toasts.svelte.js';
import { Badge, Button, EmptyState } from '$lib/ui';
import { Badge, Button, EmptyState, Pagination } from '$lib/ui';
import { fetchJobs, deleteJob, duplicateJob } from '$lib/api/translate.js';
/** @type {string} idle | loading | empty | populated | error */
@@ -18,6 +18,7 @@
let statusFilter = $state('');
let currentPage = $state(1);
let pageSize = $state(20);
let total = $state(0);
// Count jobs by status for filter pills
let statusCounts = $derived.by(() => {
@@ -28,6 +29,16 @@
return counts;
});
let page0 = $state(currentPage - 1);
$effect(() => {
const newPage = page0 + 1;
if (newPage !== currentPage) {
currentPage = newPage;
loadJobs();
}
});
let statusPills = $derived([
{ label: 'All', value: '', count: jobs.length },
{ label: $t.translate?.jobs?.status_draft, value: 'DRAFT', count: statusCounts.DRAFT },
@@ -49,6 +60,7 @@
try {
const result = await fetchJobs({ page: currentPage, page_size: pageSize, status: statusFilter || undefined });
jobs = Array.isArray(result) ? result : (result?.results || result?.items || []);
total = result?.total ?? jobs.length;
uxState = jobs.length === 0 ? 'empty' : 'populated';
} catch (err) {
error = err?.message || $t.translate?.jobs?.load_failed;
@@ -62,6 +74,7 @@
function filterByStatus(status) {
statusFilter = status;
currentPage = 1;
page0 = 0;
loadJobs();
}
@@ -268,6 +281,16 @@
</div>
{/each}
</div>
<!-- Pagination -->
<Pagination
bind:page={page0}
pageSize={pageSize}
total={total}
pageSizeOptions={[10, 20, 50, 100]}
onPageSizeChange={(size) => { pageSize = size; page0 = 0; loadJobs(); }}
class="mt-4"
/>
{/if}
</div>

View File

@@ -6,11 +6,23 @@
<script lang="ts">
import { onMount } from 'svelte';
import { t, _ } from '$lib/i18n/index.svelte.js';
import { EmptyState } from '$lib/ui';
import { EmptyState, Pagination } from '$lib/ui';
import { TranslateHistoryModel } from '$lib/models/TranslateHistoryModel.svelte';
const m = new TranslateHistoryModel();
// 0-indexed page for Pagination component (model uses 1-indexed)
let page0 = $state(m.currentPage - 1);
// Sync Pagination page changes → model
$effect(() => {
const newPage = page0 + 1;
if (newPage !== m.currentPage) {
m.currentPage = newPage;
m.loadRuns();
}
});
onMount(() => {
m.loadRuns();
m.loadMetrics();
@@ -24,7 +36,7 @@
<h1 class="text-2xl font-bold text-text">{$t.translate?.history?.title}</h1>
<p class="text-sm text-text-muted mt-1">{$t.translate?.history?.subtitle}</p>
</div>
<button onclick={() => { m.currentPage = 1; m.applyFilters(); }} class="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary-hover">{$t.translate?.history?.refresh}</button>
<button onclick={() => { page0 = 0; m.currentPage = 1; m.applyFilters(); }} class="px-4 py-2 text-sm text-white bg-primary rounded-lg hover:bg-primary-hover">{$t.translate?.history?.refresh}</button>
</div>
<!-- Metrics -->
@@ -59,7 +71,7 @@
<option value="baseline_expired">{$t.translate?.history?.trigger_baseline_expired}</option>
</select>
<input type="date" bind:value={m.filterDateFrom} class="px-3 py-2 border border-border-strong rounded-lg text-sm" placeholder={$t.translate?.common?.from} />
<button onclick={() => m.applyFilters()} class="px-4 py-2 text-sm bg-primary text-white rounded-lg hover:bg-primary-hover">{$t.translate?.history?.filter}</button>
<button onclick={() => { page0 = 0; m.applyFilters(); }} class="px-4 py-2 text-sm bg-primary text-white rounded-lg hover:bg-primary-hover">{$t.translate?.history?.filter}</button>
</div>
</div>
@@ -125,13 +137,14 @@
</div>
<!-- Pagination -->
<div class="flex items-center justify-between mt-4">
<p class="text-sm text-text-muted">{_('translate.history.showing').replace('{count}', String(m.runs.length)).replace('{total}', String(m.total))}</p>
<div class="flex gap-2">
<button onclick={() => { m.currentPage--; m.loadRuns(); }} disabled={m.currentPage <= 1} class="px-3 py-1.5 text-sm border border-border rounded-lg disabled:opacity-50">{$t.translate?.history?.previous}</button>
<button onclick={() => { m.currentPage++; m.loadRuns(); }} disabled={m.currentPage * m.pageSize >= m.total} class="px-3 py-1.5 text-sm border border-border rounded-lg disabled:opacity-50">{$t.translate?.history?.next}</button>
</div>
</div>
<Pagination
bind:page={page0}
pageSize={m.pageSize}
total={m.total}
pageSizeOptions={[10, 20, 50, 100]}
onPageSizeChange={(size) => { m.pageSize = size; page0 = 0; m.loadRuns(); }}
class="mt-4"
/>
{/if}
<!-- Detail panel -->

View File

@@ -14,7 +14,7 @@
import { goto } from '$app/navigation';
import { ROUTES } from '$lib/routes';
import { t } from '$lib/i18n/index.svelte.js';
import { Badge, EmptyState } from '$lib/ui';
import { Badge, EmptyState, Pagination } from '$lib/ui';
import { addToast } from '$lib/toasts.svelte.js';
import { triggerValidationRun as triggerRun, deleteValidationTask as deleteTask, getValidationRuns as fetchRuns } from '$lib/api.js';
import { page } from '$app/state';
@@ -36,6 +36,18 @@
let currentPage = $state(1);
let pageSize = $state(20);
// 0-indexed page for Pagination component (currentPage is 1-indexed)
let page0 = $state(currentPage - 1);
// Sync Pagination page changes → reload run history
$effect(() => {
const newPage = page0 + 1;
if (newPage !== currentPage) {
currentPage = newPage;
loadRunHistory();
}
});
// Action states
let isRunning = $state(false);
let showDeleteConfirm = $state(false);
@@ -112,11 +124,6 @@
}
}
function handlePageChange(newPage) {
currentPage = newPage;
loadRunHistory();
}
function handleViewRun(runId) {
// eslint-disable-next-line svelte/no-navigation-without-resolve
goto(ROUTES.validationTasks.runReport(policyId, runId));
@@ -342,31 +349,14 @@
</div>
<!-- Pagination -->
{#if total > pageSize}
<div class="flex items-center justify-between mt-4">
<p class="text-sm text-text-muted">
{($t.validation?.showing || 'Showing {count} of {total}')
.replace('{count}', String(runs.length))
.replace('{total}', String(total))}
</p>
<div class="flex gap-2">
<button
onclick={() => handlePageChange(currentPage - 1)}
disabled={currentPage <= 1}
class="px-3 py-1.5 text-sm border border-border rounded-lg disabled:opacity-50 hover:bg-surface-muted"
>
{$t.validation?.previous || 'Previous'}
</button>
<button
onclick={() => handlePageChange(currentPage + 1)}
disabled={currentPage * pageSize >= total}
class="px-3 py-1.5 text-sm border border-border rounded-lg disabled:opacity-50 hover:bg-surface-muted"
>
{$t.validation?.next || 'Next'}
</button>
</div>
</div>
{/if}
<Pagination
bind:page={page0}
pageSize={pageSize}
total={total}
pageSizeOptions={[10, 20, 50, 100]}
onPageSizeChange={(size) => { pageSize = size; page0 = 0; currentPage = 1; loadRunHistory(); }}
class="mt-4"
/>
{/if}
</div>
<!-- #endregion ValidationTaskDetailPage -->

View File

@@ -7,6 +7,7 @@ import { SvelteURLSearchParams } from "svelte/reactivity";
import { goto } from '$app/navigation';
import { t } from '$lib/i18n/index.svelte.js';
import { formatDateTime } from "$lib/utils/dateFormat";
import { Pagination } from '$lib/ui';
/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();
@@ -21,6 +22,17 @@ import { SvelteURLSearchParams } from "svelte/reactivity";
const totalPages = $derived(Math.ceil(total / pageSize) || 1);
// 0-indexed page for Pagination component (currentPage is 1-indexed)
let page0 = $state(currentPage - 1);
// Sync Pagination page changes → load data
$effect(() => {
const newPage = page0 + 1;
if (newPage !== currentPage) {
applyFilter(newPage);
}
});
function getStatusBadge(status: string): string {
const map: Record<string, string> = {
PASS: 'bg-success-light text-success',
@@ -58,12 +70,12 @@ import { SvelteURLSearchParams } from "svelte/reactivity";
if (link) goto(link);
}
async function applyFilter() {
async function applyFilter(page: number = 1) {
loading = true;
error = null;
try {
const qs = new SvelteURLSearchParams();
qs.append('page', '1');
qs.append('page', String(page));
qs.append('page_size', String(pageSize));
if (statusFilter) qs.append('status', statusFilter);
@@ -72,7 +84,7 @@ import { SvelteURLSearchParams } from "svelte/reactivity";
runs = result?.items ?? result?.results ?? [];
total = result?.total ?? 0;
currentPage = 1;
currentPage = page;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load runs';
} finally {
@@ -90,7 +102,7 @@ import { SvelteURLSearchParams } from "svelte/reactivity";
<h1 class="text-2xl font-bold text-text">{$t.validation?.history_page || 'Validation History'}</h1>
<select
value={statusFilter}
onchange={(e) => { statusFilter = (e.target as HTMLSelectElement).value; applyFilter(); }}
onchange={(e) => { statusFilter = (e.target as HTMLSelectElement).value; page0 = 0; currentPage = 1; applyFilter(); }}
class="px-3 py-2 border border-border-strong rounded-lg text-sm"
>
<option value="">{$t.validation?.all_statuses || 'All statuses'}</option>
@@ -152,28 +164,14 @@ import { SvelteURLSearchParams } from "svelte/reactivity";
</table>
<!-- Pagination -->
{#if totalPages > 1}
<div class="flex items-center justify-between px-4 py-3 bg-surface-muted border-t border-border">
<p class="text-sm text-text-muted">
{($t.validation?.showing || 'Showing {from}-{to} of {total}')
.replace('{from}', String((currentPage - 1) * pageSize + 1))
.replace('{to}', String(Math.min(currentPage * pageSize, total)))
.replace('{total}', String(total))}
</p>
<nav class="flex gap-1">
<button
disabled={currentPage <= 1}
onclick={() => { currentPage--; applyFilter(); }}
class="px-3 py-1 text-sm border border-border-strong rounded-md disabled:opacity-50 hover:bg-surface-muted"
></button>
<button
disabled={currentPage >= totalPages}
onclick={() => { currentPage++; applyFilter(); }}
class="px-3 py-1 text-sm border border-border-strong rounded-md disabled:opacity-50 hover:bg-surface-muted"
></button>
</nav>
</div>
{/if}
<Pagination
bind:page={page0}
pageSize={pageSize}
total={total}
pageSizeOptions={[10, 20, 50, 100]}
onPageSizeChange={(size) => { pageSize = size; page0 = 0; currentPage = 1; applyFilter(); }}
class="px-4 py-3 bg-surface-muted border-t border-border"
/>
{/if}
</div>
</div>