110 lines
4.0 KiB
Vue
110 lines
4.0 KiB
Vue
<script setup lang="ts" generic="TData, TValue">
|
|
import { defineProps } from 'vue'
|
|
import type { DefineComponent } from 'vue'
|
|
import type { ColumnDef } from '@tanstack/vue-table'
|
|
import {
|
|
useVueTable,
|
|
getCoreRowModel,
|
|
FlexRender,
|
|
} from '@tanstack/vue-table'
|
|
|
|
import {
|
|
Table, TableHeader, TableRow, TableHead, TableBody, TableCell,
|
|
} from '@/components/ui/table'
|
|
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' // <-- add ScrollBar
|
|
|
|
const props = defineProps<{
|
|
columns: ColumnDef<TData, TValue>[]
|
|
data: TData[]
|
|
dropdownComponent?: DefineComponent<{ row: TData }, any, any>
|
|
dropdownProps?: Record<string, any>
|
|
/** Optional tailwind class to control min table width for horizontal scrolling */
|
|
minTableWidth?: string
|
|
}>()
|
|
|
|
const table = useVueTable({
|
|
get data() { return props.data },
|
|
get columns() { return props.columns },
|
|
getCoreRowModel: getCoreRowModel(),
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'row-updated', row: TData, payload: any): void
|
|
(e: 'row-deleted', row: TData): void
|
|
}>()
|
|
|
|
const minWidthClass = props.minTableWidth ?? 'min-w-[1100px]' // tweak as needed
|
|
</script>
|
|
|
|
<template>
|
|
<!-- Parent must not rely on h-full; use overflow-hidden to contain scrollbars -->
|
|
<div class="w-full max-h-full border rounded-md flex flex-col overflow-hidden">
|
|
<!-- This element grows and can scroll internally -->
|
|
<ScrollArea class="flex-1 min-h-0 w-full">
|
|
<!-- min-width keeps horizontal scroll available when needed -->
|
|
<div :class="['w-full', minWidthClass]">
|
|
<!-- separate borders help sticky headers render above rows -->
|
|
<Table class="w-full border-separate border-spacing-0">
|
|
<TableHeader>
|
|
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
|
<TableHead
|
|
v-for="header in headerGroup.headers"
|
|
:key="header.id"
|
|
class="sticky top-0 bg-background z-10 whitespace-nowrap"
|
|
>
|
|
<FlexRender
|
|
v-if="!header.isPlaceholder"
|
|
:render="header.column.columnDef.header"
|
|
:props="header.getContext()"
|
|
/>
|
|
</TableHead>
|
|
<TableHead
|
|
v-if="props.dropdownComponent"
|
|
class="sticky top-0 bg-background z-10 w-12"
|
|
/>
|
|
</TableRow>
|
|
</TableHeader>
|
|
|
|
<TableBody>
|
|
<template v-if="table.getRowModel().rows.length">
|
|
<TableRow
|
|
v-for="row in table.getRowModel().rows"
|
|
:key="row.id"
|
|
class="whitespace-nowrap"
|
|
>
|
|
<TableCell v-for="cell in row.getVisibleCells()" :key="cell.id">
|
|
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
|
</TableCell>
|
|
|
|
<TableCell v-if="props.dropdownComponent" class="text-right">
|
|
<component
|
|
:is="props.dropdownComponent"
|
|
:row="row.original"
|
|
v-bind="props.dropdownProps"
|
|
@updated="(payload: any) => emit('row-updated', row.original, payload)"
|
|
@deleted="() => emit('row-deleted', row.original)"
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<TableRow>
|
|
<TableCell
|
|
:colspan="props.columns.length + (props.dropdownComponent ? 1 : 0)"
|
|
class="h-24 text-center"
|
|
>
|
|
No data.
|
|
</TableCell>
|
|
</TableRow>
|
|
</template>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<!-- Always show both scrollbars when needed -->
|
|
<ScrollBar orientation="horizontal" />
|
|
<ScrollBar orientation="vertical" />
|
|
</ScrollArea>
|
|
</div>
|
|
</template> |