()
| 1597 | * auto-maps headers by sanitized name. |
| 1598 | */ |
| 1599 | export function useImportCsvIntoTable() { |
| 1600 | const queryClient = useQueryClient() |
| 1601 | |
| 1602 | return useMutation({ |
| 1603 | mutationFn: async ({ |
| 1604 | workspaceId, |
| 1605 | tableId, |
| 1606 | file, |
| 1607 | mode, |
| 1608 | mapping, |
| 1609 | createColumns, |
| 1610 | }: ImportCsvIntoTableParams): Promise<ImportCsvIntoTableResponse> => { |
| 1611 | // Text fields must precede the file part: the server parses the body as a |
| 1612 | // stream and needs these fields before it reaches the (large) file. |
| 1613 | const formData = new FormData() |
| 1614 | formData.append('workspaceId', workspaceId) |
| 1615 | formData.append('mode', mode) |
| 1616 | if (mapping) { |
| 1617 | formData.append('mapping', JSON.stringify(mapping)) |
| 1618 | } |
| 1619 | if (createColumns && createColumns.length > 0) { |
| 1620 | formData.append('createColumns', JSON.stringify(createColumns)) |
| 1621 | } |
| 1622 | formData.append('file', file) |
| 1623 | |
| 1624 | // boundary-raw-fetch: multipart/form-data CSV upload, requestJson only supports JSON bodies |
| 1625 | const response = await fetch(`/api/table/${tableId}/import`, { |
| 1626 | method: 'POST', |
| 1627 | body: formData, |
| 1628 | }) |
| 1629 | |
| 1630 | if (!response.ok) { |
| 1631 | const data = await response.json().catch(() => ({})) |
| 1632 | throw new Error(data.error || 'CSV import failed') |
| 1633 | } |
| 1634 | |
| 1635 | return response.json() |
| 1636 | }, |
| 1637 | onError: (error) => { |
| 1638 | logger.error('Failed to import CSV into table:', error) |
| 1639 | toast.error(error.message, { duration: 5000 }) |
| 1640 | }, |
| 1641 | onSettled: (_data, _error, variables) => { |
| 1642 | invalidateRowCount(queryClient, variables.tableId) |
| 1643 | }, |
| 1644 | }) |
| 1645 | } |
| 1646 | |
| 1647 | /** |
| 1648 | * Cancels an in-flight async table job (import or delete). Plain function (not a hook) because the |
no test coverage detected