Spaces:
Running
Running
| <script lang="ts" generics="TData"> | |
| import { | |
| type ColumnDef, | |
| type ColumnFiltersState, | |
| type PaginationState, | |
| type SortingState, | |
| type VisibilityState, | |
| getCoreRowModel, | |
| getFilteredRowModel, | |
| getPaginationRowModel, | |
| getSortedRowModel | |
| } from '@tanstack/table-core'; | |
| import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table'; | |
| import * as Table from '$lib/components/ui/table'; | |
| import DataTableToolbar from './data-table-toolbar.svelte'; | |
| import DataTablePagination from './data-table-pagination.svelte'; | |
| import { goto } from '$app/navigation'; | |
| import { cn } from '$lib/utils'; | |
| import { untrack } from 'svelte'; | |
| interface Props { | |
| data: TData[]; | |
| columns: ColumnDef<TData>[]; | |
| searchPlaceholder?: string; | |
| mapOptions?: string[]; | |
| getRowHref?: (row: TData) => string; | |
| emptyMessage?: string; | |
| // Bindable state so the parent route can persist it across navigations | |
| // via SvelteKit's snapshot API. | |
| pagination?: PaginationState; | |
| sorting?: SortingState; | |
| columnFilters?: ColumnFiltersState; | |
| columnVisibility?: VisibilityState; | |
| globalFilter?: string; | |
| } | |
| let { | |
| data, | |
| columns, | |
| searchPlaceholder, | |
| mapOptions = [], | |
| getRowHref, | |
| emptyMessage = 'No results.', | |
| pagination = $bindable<PaginationState>({ pageIndex: 0, pageSize: 25 }), | |
| sorting = $bindable<SortingState>([]), | |
| columnFilters = $bindable<ColumnFiltersState>([]), | |
| columnVisibility = $bindable<VisibilityState>({}), | |
| globalFilter = $bindable<string>('') | |
| }: Props = $props(); | |
| // Reset to first page whenever filters change so users don't end up on an | |
| // empty page after narrowing the result set. | |
| $effect(() => { | |
| void globalFilter; | |
| void columnFilters; | |
| untrack(() => { | |
| if (pagination.pageIndex !== 0) { | |
| pagination = { ...pagination, pageIndex: 0 }; | |
| } | |
| }); | |
| }); | |
| const table = createSvelteTable<TData>({ | |
| get data() { | |
| return data; | |
| }, | |
| get columns() { | |
| return columns; | |
| }, | |
| state: { | |
| get pagination() { | |
| return pagination; | |
| }, | |
| get sorting() { | |
| return sorting; | |
| }, | |
| get columnFilters() { | |
| return columnFilters; | |
| }, | |
| get columnVisibility() { | |
| return columnVisibility; | |
| }, | |
| get globalFilter() { | |
| return globalFilter; | |
| } | |
| }, | |
| globalFilterFn: 'includesString', | |
| getCoreRowModel: getCoreRowModel(), | |
| getSortedRowModel: getSortedRowModel(), | |
| getFilteredRowModel: getFilteredRowModel(), | |
| getPaginationRowModel: getPaginationRowModel(), | |
| onPaginationChange: (u) => { | |
| pagination = typeof u === 'function' ? u(pagination) : u; | |
| }, | |
| onSortingChange: (u) => { | |
| sorting = typeof u === 'function' ? u(sorting) : u; | |
| }, | |
| onColumnFiltersChange: (u) => { | |
| columnFilters = typeof u === 'function' ? u(columnFilters) : u; | |
| }, | |
| onColumnVisibilityChange: (u) => { | |
| columnVisibility = typeof u === 'function' ? u(columnVisibility) : u; | |
| }, | |
| onGlobalFilterChange: (u) => { | |
| globalFilter = typeof u === 'function' ? u(globalFilter) : u; | |
| } | |
| }); | |
| function handleRowClick(row: TData, e: MouseEvent | KeyboardEvent) { | |
| if (!getRowHref) return; | |
| const href = getRowHref(row); | |
| if (e instanceof MouseEvent && (e.metaKey || e.ctrlKey || e.button === 1)) { | |
| window.open(href, '_blank', 'noopener,noreferrer'); | |
| return; | |
| } | |
| goto(href); | |
| } | |
| </script> | |
| <div> | |
| <DataTableToolbar {table} {searchPlaceholder} {mapOptions} /> | |
| <div class="rounded-md border"> | |
| <Table.Root> | |
| <Table.Header> | |
| {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} | |
| <Table.Row class="bg-muted/40 hover:bg-muted/40"> | |
| {#each headerGroup.headers as header (header.id)} | |
| <Table.Head class={cn('h-9 text-xs', header.column.columnDef.meta?.headClass)}> | |
| {#if !header.isPlaceholder} | |
| <FlexRender | |
| content={header.column.columnDef.header} | |
| context={header.getContext()} | |
| /> | |
| {/if} | |
| </Table.Head> | |
| {/each} | |
| </Table.Row> | |
| {/each} | |
| </Table.Header> | |
| <Table.Body> | |
| {#each table.getRowModel().rows as row (row.id)} | |
| <Table.Row | |
| class={cn(getRowHref && 'cursor-pointer')} | |
| onclick={(e: MouseEvent) => handleRowClick(row.original, e)} | |
| onkeydown={(e: KeyboardEvent) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| handleRowClick(row.original, e); | |
| } | |
| }} | |
| tabindex={getRowHref ? 0 : undefined} | |
| role={getRowHref ? 'link' : undefined} | |
| > | |
| {#each row.getVisibleCells() as cell (cell.id)} | |
| <Table.Cell class={cn('py-2 align-middle', cell.column.columnDef.meta?.cellClass)}> | |
| <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 text-sm text-muted-foreground" | |
| > | |
| {emptyMessage} | |
| </Table.Cell> | |
| </Table.Row> | |
| {/each} | |
| </Table.Body> | |
| </Table.Root> | |
| </div> | |
| <DataTablePagination {table} /> | |
| </div> | |