mappings+migrate
This commit is contained in:
205
frontend/src/components/DashboardGrid.svelte
Normal file
205
frontend/src/components/DashboardGrid.svelte
Normal file
@@ -0,0 +1,205 @@
|
||||
<!-- [DEF:DashboardGrid:Component] -->
|
||||
<!--
|
||||
@SEMANTICS: dashboard, grid, selection, pagination
|
||||
@PURPOSE: Displays a grid of dashboards with selection and pagination.
|
||||
@LAYER: Component
|
||||
@RELATION: USED_BY -> frontend/src/routes/migration/+page.svelte
|
||||
|
||||
@INVARIANT: Selected IDs must be a subset of available dashboards.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
// [SECTION: IMPORTS]
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { DashboardMetadata } from '../types/dashboard';
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: PROPS]
|
||||
export let dashboards: DashboardMetadata[] = [];
|
||||
export let selectedIds: number[] = [];
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: STATE]
|
||||
let filterText = "";
|
||||
let currentPage = 0;
|
||||
let pageSize = 20;
|
||||
let sortColumn: keyof DashboardMetadata = "title";
|
||||
let sortDirection: "asc" | "desc" = "asc";
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: DERIVED]
|
||||
$: filteredDashboards = dashboards.filter(d =>
|
||||
d.title.toLowerCase().includes(filterText.toLowerCase())
|
||||
);
|
||||
|
||||
$: sortedDashboards = [...filteredDashboards].sort((a, b) => {
|
||||
let aVal = a[sortColumn];
|
||||
let bVal = b[sortColumn];
|
||||
if (sortColumn === "id") {
|
||||
aVal = Number(aVal);
|
||||
bVal = Number(bVal);
|
||||
}
|
||||
if (aVal < bVal) return sortDirection === "asc" ? -1 : 1;
|
||||
if (aVal > bVal) return sortDirection === "asc" ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
$: paginatedDashboards = sortedDashboards.slice(
|
||||
currentPage * pageSize,
|
||||
(currentPage + 1) * pageSize
|
||||
);
|
||||
|
||||
$: totalPages = Math.ceil(sortedDashboards.length / pageSize);
|
||||
|
||||
$: allSelected = paginatedDashboards.length > 0 && paginatedDashboards.every(d => selectedIds.includes(d.id));
|
||||
$: someSelected = paginatedDashboards.some(d => selectedIds.includes(d.id));
|
||||
// [/SECTION]
|
||||
|
||||
// [SECTION: EVENTS]
|
||||
const dispatch = createEventDispatcher<{ selectionChanged: number[] }>();
|
||||
// [/SECTION]
|
||||
|
||||
// [DEF:handleSort:Function]
|
||||
// @PURPOSE: Toggles sort direction or changes sort column.
|
||||
function handleSort(column: keyof DashboardMetadata) {
|
||||
if (sortColumn === column) {
|
||||
sortDirection = sortDirection === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortColumn = column;
|
||||
sortDirection = "asc";
|
||||
}
|
||||
}
|
||||
// [/DEF:handleSort]
|
||||
|
||||
// [DEF:handleSelectionChange:Function]
|
||||
// @PURPOSE: Handles individual checkbox changes.
|
||||
function handleSelectionChange(id: number, checked: boolean) {
|
||||
let newSelected = [...selectedIds];
|
||||
if (checked) {
|
||||
if (!newSelected.includes(id)) newSelected.push(id);
|
||||
} else {
|
||||
newSelected = newSelected.filter(sid => sid !== id);
|
||||
}
|
||||
selectedIds = newSelected;
|
||||
dispatch('selectionChanged', newSelected);
|
||||
}
|
||||
// [/DEF:handleSelectionChange]
|
||||
|
||||
// [DEF:handleSelectAll:Function]
|
||||
// @PURPOSE: Handles select all checkbox.
|
||||
function handleSelectAll(checked: boolean) {
|
||||
let newSelected = [...selectedIds];
|
||||
if (checked) {
|
||||
paginatedDashboards.forEach(d => {
|
||||
if (!newSelected.includes(d.id)) newSelected.push(d.id);
|
||||
});
|
||||
} else {
|
||||
paginatedDashboards.forEach(d => {
|
||||
newSelected = newSelected.filter(sid => sid !== d.id);
|
||||
});
|
||||
}
|
||||
selectedIds = newSelected;
|
||||
dispatch('selectionChanged', newSelected);
|
||||
}
|
||||
// [/DEF:handleSelectAll]
|
||||
|
||||
// [DEF:goToPage:Function]
|
||||
// @PURPOSE: Changes current page.
|
||||
function goToPage(page: number) {
|
||||
if (page >= 0 && page < totalPages) {
|
||||
currentPage = page;
|
||||
}
|
||||
}
|
||||
// [/DEF:goToPage]
|
||||
|
||||
</script>
|
||||
|
||||
<!-- [SECTION: TEMPLATE] -->
|
||||
<div class="dashboard-grid">
|
||||
<!-- Filter Input -->
|
||||
<div class="mb-4">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={filterText}
|
||||
placeholder="Search dashboards..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Grid/Table -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full bg-white border border-gray-300">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 border-b">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
indeterminate={someSelected && !allSelected}
|
||||
on:change={(e) => handleSelectAll((e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
</th>
|
||||
<th class="px-4 py-2 border-b cursor-pointer" on:click={() => handleSort('title')}>
|
||||
Title {sortColumn === 'title' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
<th class="px-4 py-2 border-b cursor-pointer" on:click={() => handleSort('last_modified')}>
|
||||
Last Modified {sortColumn === 'last_modified' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
<th class="px-4 py-2 border-b cursor-pointer" on:click={() => handleSort('status')}>
|
||||
Status {sortColumn === 'status' ? (sortDirection === 'asc' ? '↑' : '↓') : ''}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each paginatedDashboards as dashboard (dashboard.id)}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-2 border-b">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.includes(dashboard.id)}
|
||||
on:change={(e) => handleSelectionChange(dashboard.id, (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
</td>
|
||||
<td class="px-4 py-2 border-b">{dashboard.title}</td>
|
||||
<td class="px-4 py-2 border-b">{new Date(dashboard.last_modified).toLocaleDateString()}</td>
|
||||
<td class="px-4 py-2 border-b">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full {dashboard.status === 'published' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}">
|
||||
{dashboard.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<div class="text-sm text-gray-700">
|
||||
Showing {currentPage * pageSize + 1} to {Math.min((currentPage + 1) * pageSize, sortedDashboards.length)} of {sortedDashboards.length} dashboards
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button
|
||||
class="px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={currentPage === 0}
|
||||
on:click={() => goToPage(currentPage - 1)}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={currentPage >= totalPages - 1}
|
||||
on:click={() => goToPage(currentPage + 1)}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- [/SECTION] -->
|
||||
|
||||
<style>
|
||||
/* Component styles */
|
||||
</style>
|
||||
|
||||
<!-- [/DEF:DashboardGrid] -->
|
||||
Reference in New Issue
Block a user