import React from "react"; import { useEffect, useState } from "react"; import { ButtonProps, notification, UploadProps, Progress } from "antd"; import { UploadChangeParam } from "antd/lib/upload"; import { useCreate, useTranslate, useCreateMany, useResourceWithRoute, useRouterContext, } from "@hooks"; import { MapDataFn, BaseRecord, HttpError, ResourceRouterParams, MetaDataQuery, } from "../../interfaces"; import { parse, ParseConfig } from "papaparse"; import { importCSVMapper } from "@definitions"; import chunk from "lodash/chunk"; import { UseCreateReturnType } from "@hooks/data/useCreate"; import { UseCreateManyReturnType } from "@hooks/data/useCreateMany"; export type ImportSuccessResult = { request: TVariables[]; type: "success"; response: TData[]; }; export type ImportErrorResult = { request: TVariables[]; type: "error"; response: HttpError[]; }; export type OnFinishParams = { succeeded: ImportSuccessResult[]; errored: ImportErrorResult[]; }; type ImportOptions< TItem, TVariables = any, TData extends BaseRecord = BaseRecord, > = { resourceName?: string; mapData?: MapDataFn; paparseOptions?: ParseConfig; batchSize?: number; onFinish?: (results: OnFinishParams) => void; metaData?: MetaDataQuery; }; export type CreatedValuesType = | ImportSuccessResult | ImportErrorResult; /** * `useImport` hook allows you to handle your csv import logic easily. * * @see {@link https://refine.dev/docs/api-references/hooks/import-export/useImport} for more details. * * @typeParam TItem - Interface of parsed csv data * @typeParam TData - Result data of the query extends {@link https://refine.dev/docs/api-references/interfaceReferences#baserecord `BaseRecord`} * @typeParam TError - Custom error object that extends {@link https://refine.dev/docs/api-references/interfaceReferences#httperror `HttpError`} * @typeParam TVariables - Values for mutation function * */ export const useImport = < TItem extends unknown = any, TData extends BaseRecord = BaseRecord, TError extends HttpError = HttpError, TVariables = any, >({ resourceName, mapData = (item) => item as unknown as TVariables, paparseOptions, batchSize = Number.MAX_SAFE_INTEGER, onFinish, metaData, }: ImportOptions = {}): { uploadProps: UploadProps; buttonProps: ButtonProps; mutationResult: | UseCreateReturnType | UseCreateManyReturnType; } => { const [processedAmount, setProcessedAmount] = useState(0); const [totalAmount, setTotalAmount] = useState(0); const [isLoading, setIsLoading] = useState(false); const t = useTranslate(); const resourceWithRoute = useResourceWithRoute(); const { useParams } = useRouterContext(); const { resource: routeResourceName } = useParams(); const { name: resource } = resourceWithRoute( resourceName ?? routeResourceName, ); const createMany = useCreateMany(); const create = useCreate(); let mutationResult: | UseCreateReturnType | UseCreateManyReturnType; if (batchSize === 1) { mutationResult = create; } else { mutationResult = createMany; } const handleCleanup = () => { setTotalAmount(0); setProcessedAmount(0); setIsLoading(false); setTimeout(() => { notification.close(`${resource}-import`); }, 4500); }; const handleFinish = ( createdValues: CreatedValuesType[], ) => { const result = { succeeded: createdValues.filter( (item) => item.type === "success", ) as unknown as ImportSuccessResult[], errored: createdValues.filter( (item) => item.type === "error", ) as unknown as ImportErrorResult[], }; handleCleanup(); onFinish?.(result); }; useEffect(() => { if (totalAmount > 0 && processedAmount > 0) { const description = (
{t( "notifications.importProgress", { processed: processedAmount, total: totalAmount, }, `Importing: ${processedAmount}/${totalAmount}`, )}
); notification.open({ description, message: null, key: `${resource}-import`, duration: 0, }); if (processedAmount >= totalAmount) { } } }, [totalAmount, processedAmount]); const handleChange = ({ file, }: UploadChangeParam): Promise[]> => { return new Promise[]>( (resolve) => { setIsLoading(true); parse(file as any, { complete: async ({ data }: { data: unknown[][] }) => { const values = importCSVMapper(data, mapData); setTotalAmount(values.length); if (batchSize === 1) { const createdValues = await Promise.all( values .map((value) => { const response = create.mutateAsync({ resource, values: value, successNotification: false, errorNotification: false, metaData, }); return { response, value }; }) .map(({ response, value }) => response .then(({ data }) => { setProcessedAmount( (currentAmount) => currentAmount + 1, ); return { response: [data], type: "success", request: [value], } as ImportSuccessResult< TVariables, TData >; }) .catch( (error: HttpError) => ({ response: [error], type: "error", request: [value], } as ImportErrorResult), ), ), ); resolve(createdValues); handleFinish(createdValues); } else { const createdValues = await Promise.all( chunk(values, batchSize) .map((batch) => { return { response: createMany.mutateAsync({ resource, values: batch, successNotification: false, errorNotification: false, metaData, }), currentBatchLength: batch.length, value: batch, }; }) .map( ({ response, value, currentBatchLength, }) => response .then((response) => { setProcessedAmount( (currentAmount) => currentAmount + currentBatchLength, ); return { response: response.data, type: "success", request: value, } as ImportSuccessResult< TVariables, TData >; }) .catch( (error: HttpError) => ({ response: [error], type: "error", request: value, } as ImportErrorResult), ), ), ); resolve(createdValues); handleFinish(createdValues); } }, ...paparseOptions, }); }, ).then((createdValues) => { handleFinish(createdValues); return createdValues; }); }; return { uploadProps: { onChange: handleChange, beforeUpload: () => false, showUploadList: false, accept: ".csv", }, buttonProps: { type: "default", loading: isLoading, }, mutationResult, }; };