# Dashboard Tables

* **Component**: \<TableComponent/>
* **Path**: /components/table/table.tsx
* **Import**:

```typescript
import { TableComponent } from "@/components/table/table";
```

<figure><img src="/files/Vw5y7G8IZ08ZCyIBAcH4" alt=""><figcaption><p>Dashboard Table</p></figcaption></figure>

*Table components can be tedious to set up...*

DirectoryFast attempts to solve this problem by using and simplifying the already excellent [Shadcn/ui Table](https://ui.shadcn.com/docs/components/data-table) components, and adding its own logic for maximum reusability.

To add a new table to your dashboard (or wherever you need one), simply follow these 3 steps:

1. Add the Table component with your data to your page:

```typescript
import { TableComponent } from "@/components/table/table";
import { [YOUR_FOLDER]_columns } from "data/dashboard/[YOUR_FOLDER]/columns";
...
<TableComponent
    data={data}
    columns={[YOUR_FOLDER]_columns}
    isLoading={isLoading}
    error={error}
    sort={{ id: "title", desc: true }}
/>
```

2. Define the columns in `/data/config/[YOUR_FOLDER]/columns.tsx`:

   Check [TanStack-Table documentation](https://tanstack.com/table/latest/docs/api/core/column-def) for more informations.

{% code title="Suggestion Table Columns example" overflow="wrap" lineNumbers="true" %}

```typescript
"use client";

import { Suggestion } from "@prisma/client";
import { ColumnDef } from "@tanstack/react-table";
import { ExternalLink } from "lucide-react";

import { DataTableColumnHeader } from "@/components/table/table-column-header";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { statuses } from "@/lib/tables/data";

import { DataTableRowActions } from "./actions";

interface SuggestionWithCategory extends Suggestion {
  category: {
    name: string;
  };
}

export const suggestions_columns: ColumnDef<SuggestionWithCategory>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={
          table.getIsAllPageRowsSelected() ||
          (table.getIsSomePageRowsSelected() && "indeterminate")
        }
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
        aria-label="Select all"
        className="translate-y-[2px]"
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
        aria-label="Select row"
        className="translate-y-[2px]"
      />
    ),
    enableSorting: false,
    enableHiding: false,
  },
  {
    accessorKey: "id",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Suggestion" />
    ),
    cell: ({ row }) => (
      <TooltipProvider delayDuration={100}>
        <Tooltip>
          <TooltipTrigger asChild>
            <div className="w-16 truncate">{row.getValue("id")}</div>
          </TooltipTrigger>
          <TooltipContent>
            <p>{row.getValue("id")}</p>
          </TooltipContent>
        </Tooltip>
      </TooltipProvider>
    ),
    enableSorting: false,
    enableHiding: false,
  },
  {
    accessorKey: "title",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Title" />
    ),
    cell: ({ row }) => <div className="w-48">{row.getValue("title")}</div>,
    enableHiding: false,
  },
  {
    accessorKey: "owner",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Owner" />
    ),
    cell: ({ row }) => <div className="w-32">{row.getValue("owner")}</div>,
    enableHiding: false,
  },
  {
    accessorKey: "category",
    accessorFn: (row) => row.category?.name,
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Category" />
    ),
    cell: ({ row }) => (
      <div className="w-32">
        <Badge>{row.getValue("category")}</Badge>
      </div>
    ),
    enableHiding: false,
  },
  {
    accessorKey: "link",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Link" />
    ),
    cell: ({ row }) => (
      <TooltipProvider delayDuration={100}>
        <Tooltip>
          <TooltipTrigger asChild>
            <a
              href={row.getValue("link")}
              target="_blank"
              rel="noreferrer"
              className="w-[80px]"
            >
              <Button variant="outline" size="icon">
                <ExternalLink className="h-4 w-4" />
              </Button>
            </a>
          </TooltipTrigger>
          <TooltipContent>
            <p>{row.getValue("link")}</p>
          </TooltipContent>
        </Tooltip>
      </TooltipProvider>
    ),
    enableSorting: false,
  },
  {
    accessorKey: "comment",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Comment" />
    ),
    cell: ({ row }) => (
      <div className="line-clamp-3 w-64">{row.getValue("comment")}</div>
    ),
    enableSorting: false,
  },
  {
    accessorKey: "status",
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title="Status" />
    ),
    cell: ({ row }) => {
      const status = statuses.find(
        (status) => status.value === row.getValue("status"),
      );

      if (!status) {
        return null;
      }

      return (
        <Badge variant="secondary">
          {status.icon && (
            <status.icon className="mr-2 h-4 w-4 text-muted-foreground" />
          )}
          <span>{status.label}</span>
        </Badge>
      );
    },
    filterFn: (row, id, value) => {
      return value.includes(row.getValue(id));
    },
  },
  // {
  //   accessorKey: "priority",
  //   header: ({ column }) => (
  //     <DataTableColumnHeader column={column} title="Priority" />
  //   ),
  //   cell: ({ row }) => {
  //     const priority = priorities.find(
  //       (priority) => priority.value === row.getValue("priority"),
  //     );

  //     if (!priority) {
  //       return null;
  //     }

  //     return (
  //       <div className="flex items-center">
  //         {priority.icon && (
  //           <priority.icon className="mr-2 h-4 w-4 text-muted-foreground" />
  //         )}
  //         <span>{priority.label}</span>
  //       </div>
  //     );
  //   },
  //   filterFn: (row, id, value) => {
  //     return value.includes(row.getValue(id));
  //   },
  // },
  {
    id: "actions",
    cell: ({ row }) => <DataTableRowActions row={row} />,
  },
];

```

{% endcode %}

3. Create your Actions page (delete/edit...) in the same folder `/data/config/[YOUR_FOLDER]/actions.tsx`

   <pre class="language-typescript" data-title="Suggestion Table Actions example" data-overflow="wrap" data-line-numbers><code class="lang-typescript">"use client";

   import { useState } from "react";

   import { zodResolver } from "@hookform/resolvers/zod";
   import { Category } from "@prisma/client";
   import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
   import { Row } from "@tanstack/react-table";
   import { MoreHorizontal } from "lucide-react";
   import { useSession } from "next-auth/react";
   import { useForm } from "react-hook-form";
   import * as z from "zod";

   import { LoadingSpinner } from "@/components/shared/icons";
   import { Button } from "@/components/ui/button";
   import { Dialog, DialogTrigger } from "@/components/ui/dialog";
   import {
     DialogContent,
     DialogDescription,
     DialogFooter,
     DialogHeader,
     DialogTitle,
   } from "@/components/ui/dialog";
   import {
     DropdownMenu,
     DropdownMenuContent,
     DropdownMenuItem,
     DropdownMenuSeparator,
     DropdownMenuTrigger,
   } from "@/components/ui/dropdown-menu";
   import {
     Form,
     FormControl,
     FormDescription,
     FormField,
     FormItem,
     FormLabel,
     FormMessage,
   } from "@/components/ui/form";
   import { Input } from "@/components/ui/input";
   import {
     Select,
     SelectContent,
     SelectItem,
     SelectTrigger,
     SelectValue,
   } from "@/components/ui/select";
   import { Textarea } from "@/components/ui/textarea";
   import { toast, useToast } from "@/components/ui/use-toast";
   import { metadata } from "@/data/config/metadata";

   interface DataTableRowActionsProps&#x3C;TData> {
     row: Row&#x3C;TData>;
   }

   export function DataTableRowActions&#x3C;TData>({
     row,
   }: DataTableRowActionsProps&#x3C;TData>) {
     const [open, setOpen] = useState(false);

     return (
       &#x3C;Dialog open={open} onOpenChange={setOpen}>
         &#x3C;DropdownMenu>
           &#x3C;DropdownMenuTrigger asChild>
             &#x3C;Button
               variant="ghost"
               className="flex h-8 w-8 p-0 data-[state=open]:bg-muted"
             >
               &#x3C;MoreHorizontal className="h-4 w-4" />
               &#x3C;span className="sr-only">Open menu&#x3C;/span>
             &#x3C;/Button>
           &#x3C;/DropdownMenuTrigger>
           &#x3C;DropdownMenuContent align="end" className="w-[160px]">
             &#x3C;DialogTrigger asChild>
               &#x3C;DropdownMenuItem>Edit&#x3C;/DropdownMenuItem>
             &#x3C;/DialogTrigger>
             &#x3C;DropdownMenuSeparator />
             &#x3C;TableActionDelete row={row} />
           &#x3C;/DropdownMenuContent>
         &#x3C;/DropdownMenu>
         &#x3C;TableActionEdit setOpen={setOpen} row={row} />
       &#x3C;/Dialog>
     );
   }

   //EDIT
   const formSchema = z.object({
     title: z.string().min(2).max(150),
     owner: z.string().min(2).max(150),
     category: z.union([z.string().min(2).max(100).optional(), z.literal("")]),
     link: z.union([z.string().min(5).max(255).optional(), z.literal("")]),
     comment: z.union([z.string().min(3).max(255).optional(), z.literal("")]),
   });

   export const TableActionEdit = ({
     setOpen,
     row,
   }: {
     setOpen: React.Dispatch&#x3C;React.SetStateAction&#x3C;boolean>>;
     row: any;
   }) => {
     const queryClient: any = useQueryClient();

     const session = useSession();
     const userId = session?.data?.user?.id;

     const {
       isLoading,
       error,
       data: categories,
     } = useQuery({
       queryKey: ["categories"],
       queryFn: async () => {
         const res = await fetch("/api/categories");
         return res.json();
       },
     });

     const { toast } = useToast();

     const form = useForm&#x3C;z.infer&#x3C;typeof formSchema>>({
       resolver: zodResolver(formSchema),
       defaultValues: {
         title: row.original.title || undefined,
         owner: row.original.owner || undefined,
         category: row.original.category.slug || undefined,
         link: row.original.link || undefined,
         comment: row.original.comment || undefined,
       },
     });

     const mutation = useMutation({
       mutationFn: async (values: z.infer&#x3C;typeof formSchema>) => {
         const res = await fetch(`/api/suggestions/${row.original.id}`, {
           method: "PATCH",
           body: JSON.stringify(values),
           headers: {
             "Content-Type": "application/json",
           },
         });
         return res.json();
       },
       onSuccess: () => {
         queryClient.invalidateQueries({ queryKey: [`product-sub-${userId}`] });
         toast({
           title: `Updating ${metadata.productLabel} suggestion`,
           description: `Your ${metadata.productLabel} suggestion has been updated before review.`,
         });
         setOpen(false);
         form.reset();
       },
       onError: () => {
         toast({
           title: "Error",
           description: `An error occurred while submitting your ${metadata.productLabel}.`,
           variant: "destructive",
         });
       },
     });

     function onSubmit(values: z.infer&#x3C;typeof formSchema>) {
       mutation.mutate(values);
     }

     return (
       &#x3C;>
         &#x3C;DialogContent className="sm:max-w-[425px]">
           &#x3C;DialogHeader>
             &#x3C;DialogTitle>Edit a {metadata.productLabel} suggestion&#x3C;/DialogTitle>
             &#x3C;DialogDescription>
               You can edit your {metadata.productLabel} suggestion before it is
               reviewed.
             &#x3C;/DialogDescription>
           &#x3C;/DialogHeader>
           &#x3C;Form {...form}>
             &#x3C;form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
               &#x3C;div className="grid gap-4 py-4">
                 &#x3C;FormField
                   control={form.control}
                   name="title"
                   render={({ field }) => (
                     &#x3C;FormItem>
                       &#x3C;FormLabel className="capitalize">
                         {metadata.productLabel} Title
                       &#x3C;/FormLabel>
                       &#x3C;FormControl>
                         &#x3C;Input
                           placeholder={`An awesome ${metadata.productLabel}`}
                           defaultValue={field.value}
                           {...field}
                         />
                       &#x3C;/FormControl>
                       &#x3C;FormMessage />
                     &#x3C;/FormItem>
                   )}
                 />
                 &#x3C;FormField
                   control={form.control}
                   name="owner"
                   render={({ field }) => (
                     &#x3C;FormItem>
                       &#x3C;FormLabel>Owner&#x3C;/FormLabel>
                       &#x3C;FormControl>
                         &#x3C;Input
                           placeholder="An awesome guy"
                           defaultValue={field.value}
                           {...field}
                         />
                       &#x3C;/FormControl>
                       &#x3C;FormMessage />
                     &#x3C;/FormItem>
                   )}
                 />
                 &#x3C;FormField
                   control={form.control}
                   name="category"
                   render={({ field }) => (
                     &#x3C;FormItem>
                       &#x3C;FormLabel>Category&#x3C;/FormLabel>
                       &#x3C;Select
                         onValueChange={field.onChange}
                         defaultValue={field.value}
                       >
                         &#x3C;FormControl>
                           &#x3C;SelectTrigger>
                             &#x3C;SelectValue placeholder="Select a category" />
                           &#x3C;/SelectTrigger>
                         &#x3C;/FormControl>
                         &#x3C;SelectContent>
                           {isLoading &#x26;&#x26; &#x3C;LoadingSpinner />}
                           {error &#x26;&#x26; "An error has occurred, please try again."}
                           {categories?.map((category: Category) => (
                             &#x3C;SelectItem key={category.id} value={category.slug}>
                               {category.name}
                             &#x3C;/SelectItem>
                           ))}
                         &#x3C;/SelectContent>
                       &#x3C;/Select>
                       &#x3C;FormMessage />
                     &#x3C;/FormItem>
                   )}
                 />
                 &#x3C;FormField
                   control={form.control}
                   name="link"
                   render={({ field }) => (
                     &#x3C;FormItem>
                       &#x3C;FormLabel>URL&#x3C;/FormLabel>
                       &#x3C;FormControl>
                         &#x3C;Input
                           placeholder="https://awesomeproduct.com"
                           defaultValue={field.value}
                           {...field}
                         />
                       &#x3C;/FormControl>
                       &#x3C;FormMessage />
                     &#x3C;/FormItem>
                   )}
                 />
                 &#x3C;FormField
                   control={form.control}
                   name="comment"
                   render={({ field }) => (
                     &#x3C;FormItem>
                       &#x3C;FormLabel>Comment&#x3C;/FormLabel>
                       &#x3C;FormControl>
                         &#x3C;Textarea
                           maxLength={255}
                           placeholder={`This ${metadata.productLabel} is a good addition because...`}
                           defaultValue={field.value}
                           {...field}
                           className="resize-none"
                         />
                       &#x3C;/FormControl>
                       &#x3C;FormDescription>
                         This is optional, but it will help us review your{" "}
                         {metadata.productLabel}
                         faster.
                       &#x3C;/FormDescription>
                       &#x3C;FormMessage />
                     &#x3C;/FormItem>
                   )}
                 />
               &#x3C;/div>
               &#x3C;DialogFooter>
                 &#x3C;Button type="submit">Suggest {metadata.productLabel}&#x3C;/Button>
               &#x3C;/DialogFooter>
             &#x3C;/form>
           &#x3C;/Form>
         &#x3C;/DialogContent>
       &#x3C;/>
     );
   };

   // DELETE
   function TableActionDelete&#x3C;TData>({ row }: DataTableRowActionsProps&#x3C;TData>) {
     const queryClient = useQueryClient();

     const session = useSession();
     const userId = session?.data?.user?.id;

     const suggestion: any = row.original;
     const deleteMutation = useMutation({
       mutationKey: [`suggestion-${suggestion.id}`],
       mutationFn: async () => {
         const res = await fetch(`/api/suggestions/${suggestion.id}`, {
           method: "DELETE",
         });
         return res.json();
       },
       onSuccess: () => {
         toast({
           title: "Suggestion deleted",
           description: "The suggestion has been deleted.",
         });
         queryClient.invalidateQueries({ queryKey: [`product-sub-${userId}`] });
       },
       onError: () => {
         toast({
           title: "Error",
           description: "Something went wrong. Please try again.",
           variant: "destructive",
         });
       },
     });
     return (
       &#x3C;DropdownMenuItem onClick={() => deleteMutation.mutate()}>
         Delete
       &#x3C;/DropdownMenuItem>
     );
   }

   </code></pre>

Your Table is now functional!

{% hint style="info" %}
If you want to build a more specific table, follow the comprehensive [Shadcn/ui guide](https://ui.shadcn.com/docs/components/data-table).
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.directoryfa.st/directory-components/dashboard-tables.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
