GoLiveKit
Features

Import / Export

Generic CSV and JSON import/export toolkit for any Prisma-backed table.

Generate with AI

Use the prompt below in your AI assistant to add import/export to any existing admin table. The full spec is in .ai-specs/feature-import-export.md.

Open the target table component, then run in the chat panel:

Add import and export to this table using the import/export feature.

@.ai-specs/feature-import-export.md

Follow the checklist at the bottom of the spec.
The Prisma model unique key for deduplication is: [slug / id / name — pick one].
Required CSV columns are: [list your columns].

Open the target table component, then use inline chat (Ctrl+I / ⌘I):

Add import and export to this table using the import/export feature.

#file:.ai-specs/feature-import-export.md

Follow the checklist at the bottom of the spec.
The Prisma model unique key for deduplication is: [slug / id / name — pick one].
Required CSV columns are: [list your columns].

Attach .ai-specs/feature-import-export.md and the target table component file to the conversation, then send:

Add import and export to the attached table component.

Use the import/export spec (also attached) as the implementation guide.
Follow the checklist at the bottom of the spec.
The Prisma model unique key for deduplication is: [slug / id / name — pick one].
Required CSV columns are: [list your columns].

Overview

The @/features/common/import-export module provides a reusable, hook-driven import/export system that works with any Prisma model. It handles file parsing, validation, preview, deduplication, and client-side download with minimal wiring effort.

Stack: react-dropzone, papaparse, sonner (toasts), shadcn/ui.

Folder structure

src/features/common/import-export/
├── model/import-export.schema.ts   # ImportRow type, ImportResult schema
├── service/import-export.service.ts # importRows() server-side helper
├── utils/parse.ts                   # parseCsv(), parseJson()
├── utils/export.ts                  # exportToCSV(), exportToJSON()
├── hooks/use-import.ts              # useImport() hook
├── hooks/use-export.ts              # useExport() hook
├── components/import-modal.tsx      # Dialog UI driven by useImport
├── components/preview-table.tsx    # Row preview table before commit
└── index.ts                         # Barrel — import everything from here

useImport

Manages the import flow state machine: idle → parsing → preview → importing → done.

const imp = useImport({
  requiredHeaders: ['title', 'slug', 'shortDescription', 'content', 'published', 'authorName'],
  onImport: myServerAction, // (rows: ImportRow[]) => Promise<ImportResult>
});

// Trigger
<Button onClick={() => imp.setOpen(true)}>Import</Button>

// Dialog
<ImportModal {...imp} onOpenChange={imp.setOpen} />

Options

| Prop | Type | Description | |||| | requiredHeaders | string[] | CSV columns that must be present. Validated on file parse. | | onImport | (rows: ImportRow[]) => Promise<ImportResult> | Your 'use server' action |

Return values

| Field | Type | Description | |||| | open | boolean | Dialog open state | | setOpen | (v: boolean) => void | Open/close; closing resets everything | | phase | ImportPhase | 'idle' \| 'parsing' \| 'preview' \| 'importing' \| 'done' | | rows | ImportRow[] | Parsed rows (available from preview phase onwards) | | result | ImportResult \| null | { imported: number, skipped: number } after completion | | onFileAccepted | (file: File) => Promise<void> | Called by dropzone on file drop | | onConfirmImport | () => Promise<void> | Called when user clicks "Import N rows" | | reset | () => void | Reset to idle, clear rows and result |

useExport

Thin wrapper over exportToCSV / exportToJSON for when data is already in memory.

const exp = useExport({ data: rows, filename: 'blog-posts' });

<Button onClick={exp.exportAsCSV}>Export CSV</Button>
<Button onClick={exp.exportAsJSON}>Export JSON</Button>

For async export (fetch all records)

When export data must be fetched first (e.g. all pages):

const exportQuery = useQuery({ ...orpc.blog.export.queryOptions({}), enabled: false });
const [isExporting, setIsExporting] = useState(false);

const handleExport = async (format: 'csv' | 'json') => {
  setIsExporting(true);
  try {
    const { data } = await exportQuery.refetch();
    if (!data) return;
    const rows: ImportRow[] = data.map((post) => ({
      title: post.title,
      slug: post.slug,
      // ... map all exportable fields to plain string/primitive values
    }));
    if (format === 'csv') exportToCSV(rows, 'blog-posts');
    else exportToJSON(rows, 'blog-posts');
  } finally {
    setIsExporting(false);
  }
};

importRows (server-side)

Use inside a 'use server' action. Handles deduplication: calls findDuplicate for each row and skips it if it returns true.

'use server';

import { importRows, type ImportRow, type ImportResult } from '@/features/common/import-export';

export async function importMyRecords(rows: ImportRow[]): Promise<ImportResult> {
  return importRows({
    rows,
    findDuplicate: async (row) => {
      const existing = await prisma.myModel.findUnique({
        where: { slug: String(row.slug) },
      });
      return !!existing;
    },
    create: async (row) => {
      await prisma.myModel.create({
        data: {
          title: String(row.title ?? '').trim(),
          slug: String(row.slug ?? '').trim(),
        },
      });
    },
  });
}

ImportModal

Stateless dialog component driven entirely by useImport return values.

<ImportModal
  open={imp.open}
  onOpenChange={imp.setOpen}
  phase={imp.phase}
  rows={imp.rows}
  result={imp.result}
  onFileAccepted={imp.onFileAccepted}
  onConfirmImport={imp.onConfirmImport}
  reset={imp.reset}
/>

// Or with spread (equivalent):
<ImportModal {...imp} onOpenChange={imp.setOpen} />

UX flow

  1. idle — dropzone visible. User drags or clicks to select a .csv or .json file.
  2. parsing — file is being parsed. Dropzone shows "Parsing file…".
  3. previewPreviewTable shows up to 50 rows. User reviews and clicks "Import N rows".
  4. importing — button shows "Importing…".
  5. doneAlert shows imported / skipped counts. toast.success() also fires.

DataTable integration

Pass import/export controls via the toolbarActions prop — they render right-aligned after the filter inputs:

<DataTable
  ...
  toolbarActions={
    <>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button variant="outline" size="sm">Export</Button>
        </DropdownMenuTrigger>
        <DropdownMenuContent>
          <DropdownMenuItem onClick={() => void handleExport('csv')}>Export as CSV</DropdownMenuItem>
          <DropdownMenuItem onClick={() => void handleExport('json')}>Export as JSON</DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <Button variant="outline" size="sm" onClick={() => imp.setOpen(true)}>Import</Button>
    </>
  }
/>
<ImportModal {...imp} onOpenChange={imp.setOpen} />

Adding to a new table (checklist)

  1. Server actionsrc/features/{workspace}/{domain}/actions/import-{domain}.ts
  2. Export oRPC procedure → add {domain}.export to the feature router (returns all records, no pagination)
  3. Hook callsuseImport + handle export in the table component
  4. toolbarActions prop → pass the buttons to <DataTable>
  5. <ImportModal /> → render next to the table

On this page