192 lines
5.8 KiB
Svelte
192 lines
5.8 KiB
Svelte
<script lang="ts" generics="TData, TValue">
|
|
import {
|
|
type ColumnDef,
|
|
getCoreRowModel
|
|
} from "@tanstack/table-core";
|
|
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
|
import * as Table from "$lib/components/ui/table/index.js";
|
|
|
|
type DataTableProps<TData, TValue> = {
|
|
columns: ColumnDef<TData, TValue>[];
|
|
data: TData[];
|
|
loading: boolean;
|
|
selectedIds?: number[];
|
|
onSelectedIdsChange?: (selectedIds: number[]) => void;
|
|
onRowDoubleClick?: (row: TData) => void;
|
|
sorting?: import("@tanstack/table-core").SortingState;
|
|
onSortingChange?: (sorting: import("@tanstack/table-core").SortingState) => void;
|
|
};
|
|
|
|
let {
|
|
data,
|
|
columns,
|
|
loading,
|
|
selectedIds = [],
|
|
onSelectedIdsChange,
|
|
onRowDoubleClick,
|
|
sorting = [],
|
|
onSortingChange
|
|
}: DataTableProps<TData, TValue> = $props();
|
|
|
|
function getRowIdValue(row: TData): number | null {
|
|
const candidate = (row as { id?: unknown })?.id;
|
|
return typeof candidate === 'number' ? candidate : null;
|
|
}
|
|
|
|
function toggleSelectedId(rowId: number, forceSelected?: boolean) {
|
|
const isSelected = selectedIds.includes(rowId);
|
|
const shouldSelect = forceSelected ?? !isSelected;
|
|
|
|
if (shouldSelect && !isSelected) {
|
|
onSelectedIdsChange?.([...selectedIds, rowId]);
|
|
return;
|
|
}
|
|
|
|
if (!shouldSelect && isSelected) {
|
|
onSelectedIdsChange?.(selectedIds.filter((id) => id !== rowId));
|
|
}
|
|
}
|
|
|
|
const table = createSvelteTable({
|
|
get data() {
|
|
return data;
|
|
},
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getRowId: (row: any) => row.id?.toString(),
|
|
state: {
|
|
get rowSelection() {
|
|
return Object.fromEntries(selectedIds.map((id) => [id.toString(), true]));
|
|
},
|
|
get sorting() {
|
|
return sorting;
|
|
}
|
|
},
|
|
meta: {
|
|
toggleSelectedId
|
|
},
|
|
onSortingChange: (updater) => {
|
|
if (onSortingChange) {
|
|
const nextSorting = typeof updater === 'function' ? updater(sorting) : updater;
|
|
onSortingChange(nextSorting);
|
|
}
|
|
},
|
|
manualSorting: true,
|
|
enableRowSelection: true,
|
|
enableMultiRowSelection: true
|
|
});
|
|
</script>
|
|
|
|
<div class="w-full h-full flex flex-col overflow-hidden">
|
|
<div class="rounded-md border flex-1 overflow-auto bg-white dark:bg-black">
|
|
<Table.Root class="w-full">
|
|
<Table.Header class="bg-background sticky top-0 z-10">
|
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
|
<Table.Row>
|
|
{#each headerGroup.headers as header (header.id)}
|
|
<Table.Head class="whitespace-nowrap p-0">
|
|
{#if !header.isPlaceholder}
|
|
<button
|
|
class="group flex h-full w-full items-center gap-2 px-2 py-2 text-left hover:bg-muted/50"
|
|
onclick={header.column.getToggleSortingHandler()}
|
|
disabled={!header.column.getCanSort()}
|
|
>
|
|
<FlexRender
|
|
content={header.column.columnDef.header}
|
|
context={header.getContext()}
|
|
/>
|
|
{#if header.column.getCanSort()}
|
|
<div class="flex h-4 w-4 shrink-0 items-center justify-center">
|
|
{#if header.column.getIsSorted() === 'asc'}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="lucide lucide-chevron-up text-primary"
|
|
><path d="m18 15-6-6-6 6" /></svg
|
|
>
|
|
{:else if header.column.getIsSorted() === 'desc'}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="lucide lucide-chevron-down text-primary"
|
|
><path d="m6 9 6 6 6-6" /></svg
|
|
>
|
|
{:else}
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="lucide lucide-chevrons-up-down opacity-30 group-hover:opacity-100"
|
|
><path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" /></svg
|
|
>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</button>
|
|
{/if}
|
|
</Table.Head>
|
|
{/each}
|
|
</Table.Row>
|
|
{/each}
|
|
</Table.Header>
|
|
<Table.Body>
|
|
{#each table.getRowModel().rows as row (row.id)}
|
|
<Table.Row
|
|
data-state={row.getIsSelected() && "selected"}
|
|
onclick={() => {
|
|
const rowId = getRowIdValue(row.original);
|
|
if (rowId !== null) {
|
|
toggleSelectedId(rowId);
|
|
}
|
|
}}
|
|
ondblclick={() => onRowDoubleClick?.(row.original)}
|
|
class="cursor-pointer hover:bg-muted/50 transition-colors {row.getIsSelected() ? 'bg-primary/10' : ''}"
|
|
>
|
|
{#each row.getVisibleCells() as cell (cell.id)}
|
|
<Table.Cell class="whitespace-nowrap px-2 py-1">
|
|
<FlexRender
|
|
content={cell.column.columnDef.cell}
|
|
context={cell.getContext()}
|
|
/>
|
|
</Table.Cell>
|
|
{/each}
|
|
</Table.Row>
|
|
{:else}
|
|
<Table.Row>
|
|
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
|
{#if loading}
|
|
<div class="flex items-center justify-center gap-2">
|
|
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
|
<p class="text-sm text-muted-foreground">Cargando...</p>
|
|
</div>
|
|
{:else}
|
|
No hay resultados.
|
|
{/if}
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
{/each}
|
|
</Table.Body>
|
|
</Table.Root>
|
|
</div>
|
|
</div>
|