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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user