This commit is contained in:
Roman Hrynevych 2024-10-11 08:10:37 +03:00 committed by GitHub
commit 847b872d8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 928 additions and 2 deletions

View File

@ -28,6 +28,11 @@ const examples = [
href: '/examples/tasks',
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/tasks',
},
{
name: 'Tasks Virtualized',
href: '/examples/big-data',
code: 'https://github.com/radix-vue/shadcn-vue/tree/dev/apps/www/src/examples/big-data',
},
{
name: 'Playground',
href: '/examples/playground',

View File

@ -365,6 +365,16 @@ export const docsConfig: DocsConfig = {
},
],
},
{
title: 'Guides',
items: [
{
title: 'Render Large Data',
href: '/docs/guides/rendering-large-data',
items: [],
},
],
},
],
}

View File

@ -0,0 +1,37 @@
---
title: Rendering Large Data
description: In some cases, you may need to render a large amount of data. This can be done using the useVirtualList Component from VueUse library to render only the visible items. This can help to improve performance.
sidebar: false
---
## Introduction
For Table logic we use [TanStack Table](https://tanstack.com/table/latest/) library. It's a powerful and flexible library for building tables in Vue, React and other frameworks. It's designed to be easy to use and flexible, while still providing the features you need.
## How to render large data
When you have a large amount of data to render, you can use the [useVirtualList](https://vueuse.org/core/useVirtualList/#usevirtuallist) Component from [VueUse](https://vueuse.org/) library to render only the visible items. This can help to improve performance.
`useVirtualList` waiting for a `MaybeRef<T[]>` so firstly you need to wrap your data with `ref` or `computed` function. So we will convert our `table.getRowModel().rows` to `computed` function.
<<< @/examples/big-data/components/DataTable.vue#tableRows{ts}
Next step is to create a `useVirtualList` instance and pass our `tableRows` to it. For better experience list elements must be same height, which you define inside `itemHeight` property. `overscan` property is used to define how many items should be rendered outside of the visible area.
<<< @/examples/big-data/components/DataTable.vue#useVirtualList{2-3 ts}
As you see `useVirtualList` function returns `list` object which contains `list` property with only visible items and `containerProps`, `wrapperProps` which should be passed to the container and wrapper element.
<<< @/examples/big-data/components/DataTable.vue#template{4,5,14-24 vue:line-numbers}
## Example
Here is an example of how to use `useVirtualList` to render a large amount of data. For test purposes, we will use 2000 rows for our table, you can change statuses, priorities, and view to see speed difference.
<script setup>
import BigDataExample from "@/examples/big-data/Example.vue"
</script>
<Suspense>
<BigDataExample />
</Suspense>

View File

@ -0,0 +1,7 @@
<script setup>
import BigDataExample from "@/examples/big-data/Example.vue"
</script>
<Suspense>
<BigDataExample />
</Suspense>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import DataTable from './components/DataTable.vue'
import { columns } from './components/columns'
const response = await fetch('https://api.json-generator.com/templates/NttPDOzDwSsS/data?access_token=xi78iyv3ez1qwmm9pgou7r5mbqxfxrfmdzcps0hm')
const tasks = await response.json()
</script>
<template>
<DataTable :data="tasks" :columns="columns" />
</template>

View File

@ -0,0 +1,126 @@
<script setup lang="ts">
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from '@tanstack/vue-table'
import {
FlexRender,
getCoreRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
import { useVirtualList } from '@vueuse/core'
import { computed, ref } from 'vue'
import type { Task } from '../data/schema'
import DataTablePagination from './DataTablePagination.vue'
import DataTableToolbar from './DataTableToolbar.vue'
import { valueUpdater } from '@/lib/utils'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/lib/registry/new-york/ui/table'
interface DataTableProps {
columns: ColumnDef<Task, any>[]
data: Task[]
}
const props = defineProps<DataTableProps>()
const sorting = ref<SortingState>([])
const columnFilters = ref<ColumnFiltersState>([])
const columnVisibility = ref<VisibilityState>({})
const rowSelection = ref({})
const table = useVueTable({
get data() { return props.data },
get columns() { return props.columns },
state: {
get sorting() { return sorting.value },
get columnFilters() { return columnFilters.value },
get columnVisibility() { return columnVisibility.value },
get rowSelection() { return rowSelection.value },
},
enableRowSelection: true,
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
initialState: {
pagination: {
pageSize: 500,
},
},
})
// #region tableRows
const tableRows = computed(() => table.getRowModel().rows)
// #endregion tableRows
// #region useVirtualList
const { list, containerProps, wrapperProps } = useVirtualList(tableRows, {
itemHeight: 49,
overscan: 15,
})
// #endregion useVirtualList
</script>
<!-- #region template -->
<template>
<div class="space-y-4">
<DataTableToolbar :table="table" />
<div class="rounded-md border max-h-[75dvh]" v-bind="containerProps">
<Table v-bind="wrapperProps">
<TableHeader style="height: 49px">
<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-black z-10 border-b">
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="tableRows?.length">
<TableRow
v-for="row in list"
:key="row.data.id"
:data-state="row.data.getIsSelected() && 'selected'"
>
<TableCell v-for="cell in row.data.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</TableCell>
</TableRow>
</template>
<TableRow v-else>
<TableCell
:colspan="columns.length"
class="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<DataTablePagination :table="table" />
</div>
</template>
<!-- #endregion template -->

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import type { Column } from '@tanstack/vue-table'
import { type Task } from '../data/schema'
import ArrowDownIcon from '~icons/radix-icons/arrow-down'
import ArrowUpIcon from '~icons/radix-icons/arrow-up'
import CaretSortIcon from '~icons/radix-icons/caret-sort'
import EyeNoneIcon from '~icons/radix-icons/eye-none'
import ResetIcon from '~icons/radix-icons/reset'
import { cn } from '@/lib/utils'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
interface DataTableColumnHeaderProps {
column: Column<Task, any>
title: string
}
defineProps<DataTableColumnHeaderProps>()
</script>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<template>
<div v-if="column.getCanSort()" :class="cn('flex items-center space-x-2', $attrs.class ?? '')">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
size="sm"
class="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{{ title }}</span>
<ArrowDownIcon v-if="column.getIsSorted() === 'desc'" class="ml-2 h-4 w-4" />
<ArrowUpIcon v-else-if=" column.getIsSorted() === 'asc'" class="ml-2 h-4 w-4" />
<CaretSortIcon v-else class="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem @click="column.toggleSorting(false)">
<ArrowUpIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem @click="column.toggleSorting(true)">
<ArrowDownIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="column.toggleVisibility(false)">
<EyeNoneIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
<DropdownMenuItem v-if="column.getIsSorted()" @click="column.clearSorting()">
<ResetIcon class="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Reset
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div v-else :class="$attrs.class">
{{ title }}
</div>
</template>

View File

@ -0,0 +1,149 @@
<script setup lang="ts">
import type { Column } from '@tanstack/vue-table'
import type { Component } from 'vue'
import { computed } from 'vue'
import { useVirtualList } from '@vueuse/core'
import type { Task } from '../data/schema'
import PlusCircledIcon from '~icons/radix-icons/plus-circled'
import CheckIcon from '~icons/radix-icons/check'
import { Badge } from '@/lib/registry/new-york/ui/badge'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '@/lib/registry/new-york/ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/lib/registry/new-york/ui/popover'
import { Separator } from '@/lib/registry/new-york/ui/separator'
import { cn } from '@/lib/utils'
interface DataTableFacetedFilter {
column?: Column<Task, any>
title?: string
options: {
label: string
value: string
icon?: Component
}[]
}
const props = defineProps<DataTableFacetedFilter>()
function repeat<T>(array: T[], times: number) {
return Array.from({ length: times }, () => array).flat()
}
const facets = computed(() => props.column?.getFacetedUniqueValues())
const selectedValues = computed(() => new Set(props.column?.getFilterValue() as string[]))
const options = computed(() => repeat(props.options, 100))
const { list, containerProps, wrapperProps } = useVirtualList(options, {
itemHeight: 32,
})
</script>
<template>
<Popover>
<PopoverTrigger as-child>
<Button variant="outline" size="sm" class="h-8 border-dashed">
<PlusCircledIcon class="mr-2 h-4 w-4" />
{{ title }}
<template v-if="selectedValues.size > 0">
<Separator orientation="vertical" class="mx-2 h-4" />
<Badge
variant="secondary"
class="rounded-sm px-1 font-normal lg:hidden"
>
{{ selectedValues.size }}
</Badge>
<div class="hidden space-x-1 lg:flex">
<Badge
v-if="selectedValues.size > 2"
variant="secondary"
class="rounded-sm px-1 font-normal"
>
{{ selectedValues.size }} selected
</Badge>
<template v-else>
<Badge
v-for="option in options
.filter((option) => selectedValues.has(option.value))"
:key="option.value"
variant="secondary"
class="rounded-sm px-1 font-normal"
>
{{ option.label }}
</Badge>
</template>
</div>
</template>
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0" align="start">
<Command>
<CommandInput :placeholder="title" />
<CommandList>
<CommandEmpty>
No results found.
</CommandEmpty>
<div v-bind="containerProps" class="max-h-52">
<div v-bind="wrapperProps">
<CommandGroup>
<CommandItem
v-for="option in list"
:key="option.index"
style="height: 32px"
:value="option.data"
@select="() => {
const isSelected = selectedValues.has(option.data.value)
if (isSelected) {
selectedValues.delete(option.data.value)
}
else {
selectedValues.add(option.data.value)
}
const filterValues = Array.from(selectedValues)
column?.setFilterValue(
filterValues.length ? filterValues : undefined,
)
}"
>
<div
:class="cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
selectedValues.has(option.data.value)
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible',
)"
>
<CheckIcon :class="cn('h-4 w-4')" />
</div>
<component :is="option.data.icon" v-if="option.data.icon" class="mr-2 h-4 w-4 text-muted-foreground" />
<span>{{ option.data.label }}</span>
<span v-if="facets?.get(option.data.value)" class="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
{{ facets.get(option.data.value) }}
</span>
</CommandItem>
</CommandGroup>
</div>
</div>
<template v-if="selectedValues.size > 0">
<CommandSeparator />
<CommandGroup>
<CommandItem
:value="{ label: 'Clear filters' }"
class="justify-center text-center"
@select="column?.setFilterValue(undefined)"
>
Clear filters
</CommandItem>
</CommandGroup>
</template>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</template>

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import type { Task } from '../data/schema'
import ChevronLeftIcon from '~icons/radix-icons/chevron-left'
import ChevronRightIcon from '~icons/radix-icons/chevron-right'
import DoubleArrowLeftIcon from '~icons/radix-icons/double-arrow-left'
import DoubleArrowRightIcon from '~icons/radix-icons/double-arrow-right'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/lib/registry/new-york/ui/select'
interface DataTablePaginationProps {
table: Table<Task>
}
defineProps<DataTablePaginationProps>()
</script>
<template>
<div class="flex items-center justify-between px-2">
<div class="flex-1 text-sm text-muted-foreground">
{{ table.getFilteredSelectedRowModel().rows.length }} of
{{ table.getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="flex items-center space-x-6 lg:space-x-8">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">
Rows per page
</p>
<Select
:model-value="`${table.getState().pagination.pageSize}`"
@update:model-value="table.setPageSize"
>
<SelectTrigger class="h-8 w-[80px]">
<SelectValue :placeholder="`${table.getState().pagination.pageSize}`" />
</SelectTrigger>
<SelectContent side="top">
<SelectItem v-for="pageSize in [500, 1000, 2000]" :key="pageSize" :value="`${pageSize}`">
{{ pageSize }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
Page {{ table.getState().pagination.pageIndex + 1 }} of
{{ table.getPageCount() }}
</div>
<div class="flex items-center space-x-2">
<Button
variant="outline"
class="hidden h-8 w-8 p-0 lg:flex"
:disabled="!table.getCanPreviousPage()"
@click="table.setPageIndex(0)"
>
<span class="sr-only">Go to first page</span>
<DoubleArrowLeftIcon class="h-4 w-4" />
</Button>
<Button
variant="outline"
class="h-8 w-8 p-0"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
<span class="sr-only">Go to previous page</span>
<ChevronLeftIcon class="h-4 w-4" />
</Button>
<Button
variant="outline"
class="h-8 w-8 p-0"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
>
<span class="sr-only">Go to next page</span>
<ChevronRightIcon class="h-4 w-4" />
</Button>
<Button
variant="outline"
class="hidden h-8 w-8 p-0 lg:flex"
:disabled="!table.getCanNextPage()"
@click="table.setPageIndex(table.getPageCount() - 1)"
>
<span class="sr-only">Go to last page</span>
<DoubleArrowRightIcon class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,65 @@
<script setup lang="ts">
import type { Row } from '@tanstack/vue-table'
import { computed } from 'vue'
import { labels } from '../data/data'
import { taskSchema } from '../data/schema'
import { type Task } from '../data/schema'
import DotsHorizontalIcon from '~icons/radix-icons/dots-horizontal'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
interface DataTableRowActionsProps {
row: Row<Task>
}
const props = defineProps<DataTableRowActionsProps>()
const task = computed(() => taskSchema.parse(props.row.original))
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="ghost"
class="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
>
<DotsHorizontalIcon class="h-4 w-4" />
<span class="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[160px]">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Make a copy</DropdownMenuItem>
<DropdownMenuItem>Favorite</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup :value="task.label">
<DropdownMenuRadioItem v-for="label in labels" :key="label.value" :value="label.value">
{{ label.label }}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>
Delete
<DropdownMenuShortcut></DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue'
import type { Task } from '../data/schema'
import { priorities, statuses } from '../data/data'
import DataTableFacetedFilter from './DataTableFacetedFilter.vue'
import DataTableViewOptions from './DataTableViewOptions.vue'
import Cross2Icon from '~icons/radix-icons/cross-2'
import { Button } from '@/lib/registry/new-york/ui/button'
import { Input } from '@/lib/registry/new-york/ui/input'
interface DataTableToolbarProps {
table: Table<Task>
}
const props = defineProps<DataTableToolbarProps>()
const isFiltered = computed(() => props.table.getState().columnFilters.length > 0)
</script>
<template>
<div class="flex items-start justify-between flex-wrap">
<div class="flex flex-1 items-center gap-2 flex-wrap">
<Input
placeholder="Filter tasks..."
:model-value="(table.getColumn('title')?.getFilterValue() as string) ?? ''"
class="h-8 w-[150px] lg:w-[250px]"
@input="table.getColumn('title')?.setFilterValue($event.target.value)"
/>
<DataTableFacetedFilter
v-if="table.getColumn('status')"
:column="table.getColumn('status')"
title="Status"
:options="statuses"
/>
<DataTableFacetedFilter
v-if="table.getColumn('priority')"
:column="table.getColumn('priority')"
title="Priority"
:options="priorities"
/>
<Button
v-if="isFiltered"
variant="ghost"
class="h-8 px-2 lg:px-3"
@click="table.resetColumnFilters()"
>
Reset
<Cross2Icon class="ml-2 h-4 w-4" />
</Button>
</div>
<DataTableViewOptions :table="table" />
</div>
</template>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import type { Table } from '@tanstack/vue-table'
import { computed } from 'vue'
import { type Task } from '../data/schema'
import MixerHorizontalIcon from '~icons/radix-icons/mixer-horizontal'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
interface DataTableViewOptionsProps {
table: Table<Task>
}
const props = defineProps<DataTableViewOptionsProps>()
const columns = computed(() => props.table.getAllColumns()
.filter(
column =>
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
))
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button
variant="outline"
size="sm"
class="ml-auto hidden h-8 lg:flex"
>
<MixerHorizontalIcon class="mr-2 h-4 w-4" />
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-[150px]">
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
v-for="column in columns"
:key="column.id"
class="capitalize"
:checked="column.getIsVisible()"
@update:checked="(value) => column.toggleVisibility(!!value)"
>
{{ column.id }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/lib/registry/new-york/ui/avatar'
import { Button } from '@/lib/registry/new-york/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/lib/registry/new-york/ui/dropdown-menu'
</script>
<template>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="relative h-8 w-8 rounded-full">
<Avatar class="h-9 w-9">
<AvatarImage src="/avatars/03.png" alt="@shadcn" />
<AvatarFallback>SC</AvatarFallback>
</Avatar>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent class="w-56" align="end">
<DropdownMenuLabel class="font-normal flex">
<div class="flex flex-col space-y-1">
<p class="text-sm font-medium leading-none">
shadcn
</p>
<p class="text-xs leading-none text-muted-foreground">
m@example.com
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
Log out
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>

View File

@ -0,0 +1,86 @@
import type { ColumnDef } from '@tanstack/vue-table'
import { h } from 'vue'
import { labels, priorities, statuses } from '../data/data'
import type { Task } from '../data/schema'
import DataTableColumnHeader from './DataTableColumnHeader.vue'
import DataTableRowActions from './DataTableRowActions.vue'
import { Checkbox } from '@/lib/registry/new-york/ui/checkbox'
import { Badge } from '@/lib/registry/new-york/ui/badge'
export const columns: ColumnDef<Task>[] = [
{
id: 'select',
header: ({ table }) => h(Checkbox,
{ 'checked': table.getIsAllPageRowsSelected(), 'onUpdate:checked': value => table.toggleAllPageRowsSelected(!!value), 'ariaLabel': 'Select all', 'class': 'translate-y-0.5' }),
cell: ({ row }) => h(Checkbox,
{ 'checked': row.getIsSelected(), 'onUpdate:checked': value => row.toggleSelected(!!value), 'ariaLabel': 'Select row', 'class': 'translate-y-0.5' }),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'id',
header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Task' }),
cell: ({ row }) => h('div', { class: 'w-20' }, row.getValue('id')),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: 'title',
header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Title' }),
cell: ({ row }) => {
const label = labels.find(label => label.value === row.original.label)
return h('div', { class: 'flex space-x-2' }, [
label && h(Badge, { variant: 'outline' }, label.label),
h('span', { class: 'max-w-[180px] truncate font-medium' }, row.getValue('title')),
])
},
},
{
accessorKey: 'status',
header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Status' }),
cell: ({ row }) => {
const status = statuses.find(
status => status.value === row.getValue('status'),
)
if (!status)
return null
return h('div', { class: 'flex w-[100px] items-center' }, [
status.icon && h(status.icon, { class: 'mr-2 h-4 w-4 text-muted-foreground' }),
h('span', status.label),
])
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id))
},
},
{
accessorKey: 'priority',
header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Priority' }),
cell: ({ row }) => {
const priority = priorities.find(
priority => priority.value === row.getValue('priority'),
)
if (!priority)
return null
return h('div', { class: 'flex items-center' }, [
priority.icon && h(priority.icon, { class: 'mr-2 h-4 w-4 text-muted-foreground' }),
h('span', priority.label),
])
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id))
},
},
{
id: 'actions',
cell: ({ row }) => h(DataTableRowActions, { row }),
},
]

View File

@ -0,0 +1,70 @@
import { h } from 'vue'
import ArrowDownIcon from '~icons/radix-icons/arrow-down'
import ArrowRightIcon from '~icons/radix-icons/arrow-right'
import ArrowUpIcon from '~icons/radix-icons/arrow-up'
import CheckCircledIcon from '~icons/radix-icons/check-circled'
import CircleIcon from '~icons/radix-icons/circle'
import CrossCircledIcon from '~icons/radix-icons/cross-circled'
import QuestionMarkCircledIcon from '~icons/radix-icons/question-mark-circled'
import StopwatchIcon from '~icons/radix-icons/stopwatch'
export const labels = [
{
value: 'bug',
label: 'Bug',
},
{
value: 'feature',
label: 'Feature',
},
{
value: 'documentation',
label: 'Documentation',
},
]
export const statuses = [
{
value: 'backlog',
label: 'Backlog',
icon: h(QuestionMarkCircledIcon),
},
{
value: 'todo',
label: 'Todo',
icon: h(CircleIcon),
},
{
value: 'in progress',
label: 'In Progress',
icon: h(StopwatchIcon),
},
{
value: 'done',
label: 'Done',
icon: h(CheckCircledIcon),
},
{
value: 'canceled',
label: 'Canceled',
icon: h(CrossCircledIcon),
},
]
export const priorities = [
{
label: 'Low',
value: 'low',
icon: h(ArrowDownIcon),
},
{
label: 'Medium',
value: 'medium',
icon: h(ArrowRightIcon),
},
{
label: 'High',
value: 'high',
icon: h(ArrowUpIcon),
},
]

View File

@ -0,0 +1,13 @@
import { z } from 'zod'
// We're keeping a simple non-relational schema here.
// IRL, you will have a schema for your data models.
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string(),
})
export type Task = z.infer<typeof taskSchema>

View File

@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div class="relative w-full overflow-auto">
<div class="relative w-full">
<table :class="cn('w-full caption-bottom text-sm', props.class)">
<slot />
</table>

View File

@ -8,7 +8,7 @@ const props = defineProps<{
</script>
<template>
<div class="relative w-full overflow-auto">
<div class="relative w-full">
<table :class="cn('w-full caption-bottom text-sm', props.class)">
<slot />
</table>

View File

@ -2907,6 +2907,7 @@ packages:
acorn-walk@8.3.2:
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
engines: {node: '>=0.4.0'}
dev: true
acorn@8.12.1:
resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==}
@ -3540,6 +3541,7 @@ packages:
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
dev: true
croner@8.0.2:
resolution: {integrity: sha512-HgSdlSUX8mIgDTTiQpWUP4qY4IFRMsduPCYdca34Pelt8MVdxdaDOzreFtCscA6R+cRZd7UbD1CD3uyx6J3X1A==}
@ -3961,6 +3963,7 @@ packages:
diff@5.2.0:
resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==}
engines: {node: '>=0.3.1'}
dev: true
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}