refactor(frontend): migrate pagination to Pagination component (3 files)

validation-tasks/+page, DatasetList, dashboards/+page — replaced hand-rolled
pagination controls (prev/next buttons, page numbers, 'Showing X-Y of Z',
page size selector) with shared <Pagination> from $lib/ui.
This commit is contained in:
2026-06-17 14:58:07 +03:00
parent 87cbff9bd8
commit c73246db85
3 changed files with 71 additions and 106 deletions

View File

@@ -25,9 +25,9 @@
import DashboardMaintenanceBadge from "$lib/components/DashboardMaintenanceBadge.svelte";
import { environmentContextStore, initializeEnvironmentContext, setSelectedEnvironment } from "$lib/stores/environmentContext.svelte.js";
import { Button, Skeleton } from "$lib/ui";
import { Button, Pagination, Skeleton } from "$lib/ui";
import ColumnFilterPopover from "./ColumnFilterPopover.svelte";
import { formatDate, getPaginationRange, getSortIndicator } from "./dashboard-helpers.js";
import { formatDate, getSortIndicator } from "./dashboard-helpers.js";
import { DashboardHubModel } from "$lib/models/DashboardHubModel.svelte.ts";
import MigrateDashboardModal from "./MigrateDashboardModal.svelte";
import BackupDashboardModal from "./BackupDashboardModal.svelte";
@@ -52,6 +52,26 @@
setSelectedEnvironment(envId);
}
// ── Pagination bridge (0-indexed ↔ 1-indexed) ──
let pageZero = $state(m.currentPage - 1);
$effect(() => {
if (pageZero + 1 !== m.currentPage) {
m.setPage(pageZero + 1);
}
});
$effect(() => {
if (m.currentPage - 1 !== pageZero) {
pageZero = m.currentPage - 1;
}
});
function handlePageSizeChange(size: number) {
m.pageSize = size;
m.currentPage = 1;
}
// Store-derived values (must stay in page — legacy store not reactive in Model)
let environments = $derived($environmentContextStore?.environments || []);
let shouldShowEnvironmentWizard = $derived(
@@ -656,81 +676,14 @@ class="inline-flex h-8 w-8 items-center justify-center rounded-md border border-
<!-- Pagination -->
{#if m.totalPages > 1}
<div
class="flex items-center justify-between px-4 py-3 bg-surface-muted border-t border-border"
>
<div class="text-sm text-text-muted">
{($t.dashboard?.showing ?? '')
.replace("{start}", String((m.currentPage - 1) * m.pageSize + 1))
.replace("{end}", String(Math.min(m.currentPage * m.pageSize, m.total)))
.replace("{total}", String(m.total))}
</div>
<div class="flex items-center gap-1">
<button
class="px-3 py-1 border border-border-strong rounded hover:bg-surface-muted disabled:opacity-50 disabled:cursor-not-allowed min-w-[32px]"
onclick={() => m.setPage(1)}
disabled={m.currentPage === 1}
>
{$t.common?.first}
</button>
<button
class="px-3 py-1 border border-border-strong rounded hover:bg-surface-muted disabled:opacity-50 disabled:cursor-not-allowed min-w-[32px]"
onclick={() => m.setPage(m.currentPage - 1)}
disabled={m.currentPage === 1}
>
{$t.dashboard?.previous}
</button>
{#each getPaginationRange(m.currentPage, m.totalPages) as pageNum}
{#if pageNum === "..."}
<span class="px-2 py-1 text-text-muted">...</span>
{:else}
<button
class="page-btn {pageNum === m.currentPage ? 'active' : ''}"
onclick={() => m.setPage(pageNum)}
>
{pageNum}
</button>
{/if}
{/each}
<button
class="px-3 py-1 border border-border-strong rounded hover:bg-surface-muted disabled:opacity-50 disabled:cursor-not-allowed min-w-[32px]"
onclick={() => m.setPage(m.currentPage + 1)}
disabled={m.currentPage === m.totalPages}
>
{$t.dashboard?.next}
</button>
<button
class="px-3 py-1 border border-border-strong rounded hover:bg-surface-muted disabled:opacity-50 disabled:cursor-not-allowed min-w-[32px]"
onclick={() => m.setPage(m.totalPages)}
disabled={m.currentPage === m.totalPages}
>
{$t.common?.last}
</button>
</div>
<div>
<select
class="px-4 py-2 border border-border-strong rounded-lg bg-surface-card focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-ring"
value={m.pageSize}
onchange={(e) => m.setPageSize(e)}
>
<option value={5}>
{($t.dashboard?.per_page_option ?? '{count} per page').replace("{count}", "5")}
</option>
<option value={10}>
{($t.dashboard?.per_page_option ?? '{count} per page').replace("{count}", "10")}
</option>
<option value={25}>
{($t.dashboard?.per_page_option ?? '{count} per page').replace("{count}", "25")}
</option>
<option value={50}>
{($t.dashboard?.per_page_option ?? '{count} per page').replace("{count}", "50")}
</option>
<option value={100}>
{($t.dashboard?.per_page_option ?? '{count} per page').replace("{count}", "100")}
</option>
</select>
</div>
</div>
<Pagination
bind:page={pageZero}
pageSize={m.pageSize}
total={m.total}
pageSizeOptions={[5, 10, 25, 50, 100]}
onPageSizeChange={handlePageSizeChange}
class="px-4 py-3 bg-surface-muted border-t border-border"
/>
{/if}
</div>

View File

@@ -10,7 +10,7 @@
<!-- @UX_REACTIVITY Props -> datasets, selectedIds, isLoading, error, total, page, totalPages, pageSize, onselect, onaction, onpagechange, onsearch, oncheckbox -->
<script lang="ts">
import { t } from "$lib/i18n/index.svelte.js";
import { Button } from "$lib/ui";
import { Button, Pagination } from "$lib/ui";
let {
datasets = [],
@@ -53,6 +53,21 @@
localSearch = e.target.value;
onsearch?.(localSearch);
}
// ── Pagination bridge (0-indexed ↔ 1-indexed) ──
let pageZero = $state(page - 1);
$effect(() => {
if (pageZero + 1 !== page) {
onpagechange?.(pageZero + 1);
}
});
$effect(() => {
if (page - 1 !== pageZero) {
pageZero = page - 1;
}
});
</script>
<!-- Search -->
@@ -167,18 +182,11 @@
<!-- Pagination -->
{#if totalPages > 1}
<div class="flex items-center justify-between mt-3 pt-3 border-t border-border">
<span class="text-sm text-text-muted">
{($t.datasets?.showing ?? 'Showing {start}-{end} of {total}').replace('{start}', String((page - 1) * pageSize + 1)).replace('{end}', String(Math.min(page * pageSize, total))).replace('{total}', String(total))}
</span>
<div class="flex gap-1">
<Button variant="secondary" size="sm" class="px-2 py-1" title={$t.datasets?.pagination_first_tip} onclick={() => onpagechange?.(1)} disabled={page <= 1}>«</Button>
<Button variant="secondary" size="sm" class="px-2 py-1" title={$t.datasets?.pagination_prev_tip} onclick={() => onpagechange?.(page - 1)} disabled={page <= 1}></Button>
<span class="px-2 py-1 text-xs">{page} / {totalPages}</span>
<Button variant="secondary" size="sm" class="px-2 py-1" title={$t.datasets?.pagination_next_tip} onclick={() => onpagechange?.(page + 1)} disabled={page >= totalPages}></Button>
<Button variant="secondary" size="sm" class="px-2 py-1" title={$t.datasets?.pagination_last_tip} onclick={() => onpagechange?.(totalPages)} disabled={page >= totalPages}>»</Button>
</div>
</div>
<Pagination
bind:page={pageZero}
{pageSize}
{total}
/>
{/if}
{/if}
<!-- #endregion DatasetList -->

View File

@@ -10,7 +10,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { t } from '$lib/i18n/index.svelte.js';
import { EmptyState, Skeleton } from '$lib/ui';
import { EmptyState, Pagination, Skeleton } from '$lib/ui';
import { ValidationTasksListModel } from '$lib/models/ValidationTasksListModel.svelte';
let { data } = $props();
@@ -54,11 +54,20 @@
return String(task.environment_name || task.environment_id || '\u2014');
}
function pageBtnClass(p: number): string {
const base = 'px-3 py-1.5 text-sm rounded-lg border transition-colors';
if (p === m.currentPage) return `${base} bg-primary-light border-primary-ring text-primary`;
return `${base} border-border text-text-muted hover:bg-surface-muted`;
}
// ── Pagination bridge (0-indexed ↔ 1-indexed) ──
let pageZero = $state(m.currentPage - 1);
$effect(() => {
if (pageZero + 1 !== m.currentPage) {
m.goToPage(pageZero + 1);
}
});
$effect(() => {
if (m.currentPage - 1 !== pageZero) {
pageZero = m.currentPage - 1;
}
});
</script>
<div class="max-w-7xl mx-auto px-4 py-6">
@@ -178,16 +187,11 @@
<!-- Pagination -->
{#if m.totalPages > 1}
<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(m.tasks.length)).replace('{total}', String(m.total))}</p>
<nav class="flex items-center gap-1" aria-label="Pagination">
<button onclick={() => m.goToPage(m.currentPage - 1)} disabled={m.currentPage <= 1} class="px-3 py-1.5 text-sm border border-border rounded-lg disabled:opacity-40 disabled:cursor-not-allowed hover:bg-surface-muted text-text-muted transition-colors">{$t.validation?.previous}</button>
{#each Array.from({ length: Math.min(m.totalPages, 7) }, (_, i) => { if (m.totalPages <= 7) return i + 1; const half = 3; const start = Math.max(1, m.currentPage - half); const end = Math.min(m.totalPages, start + 6); const adj = Math.max(1, end - 6); return adj + i; }) as p (p)}
<button onclick={() => m.goToPage(p)} class={pageBtnClass(p)}>{p}</button>
{/each}
<button onclick={() => m.goToPage(m.currentPage + 1)} disabled={m.currentPage >= m.totalPages} class="px-3 py-1.5 text-sm border border-border rounded-lg disabled:opacity-40 disabled:cursor-not-allowed hover:bg-surface-muted text-text-muted transition-colors">{$t.validation?.next}</button>
</nav>
</div>
<Pagination
bind:page={pageZero}
pageSize={m.pageSize}
total={m.total}
/>
{/if}
{/if}
</div>