import { ReactElement, useEffect, useRef, useState } from "react"; import { format } from "date-fns"; import { isEmpty } from "lodash"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useStore } from "../../store"; import { useShallow } from "zustand/react/shallow"; import axios from "axios"; import { useGetJobResultStatisticsQuery } from "../../graphql/generated"; import { RealTimeImportStats } from "./RealTimeImportStats"; import { useImportSessionStatus } from "../../hooks/useImportSessionStatus"; export const Import = (): ReactElement => { const [importError, setImportError] = useState(null); const queryClient = useQueryClient(); const { importJobQueue, getSocket, disconnectSocket } = useStore( useShallow((state) => ({ importJobQueue: state.importJobQueue, getSocket: state.getSocket, disconnectSocket: state.disconnectSocket, })), ); // Force re-import mutation - re-imports all files regardless of import status const { mutate: forceReImport, isPending: isForceReImporting } = useMutation({ mutationFn: async () => { const sessionId = localStorage.getItem("sessionId") || ""; return await axios.request({ url: `http://localhost:3000/api/library/forceReImport`, method: "POST", data: { sessionId }, }); }, onSuccess: (response) => { console.log("Force re-import initiated:", response.data); importJobQueue.setStatus("running"); setImportError(null); }, onError: (error: any) => { console.error("Failed to start force re-import:", error); setImportError(error?.response?.data?.message || error?.message || "Failed to start force re-import. Please try again."); }, }); const { data, isLoading, refetch } = useGetJobResultStatisticsQuery(); const importSession = useImportSessionStatus(); const hasActiveSession = importSession.isActive; const wasComplete = useRef(false); // React to importSession.isComplete rather than socket events — more reliable // since it's derived from the actual GraphQL state, not a raw socket event. useEffect(() => { if (importSession.isComplete && !wasComplete.current) { wasComplete.current = true; // Small delay so the backend has time to commit job result stats setTimeout(() => { // Invalidate the cache to force a fresh fetch of job result statistics queryClient.invalidateQueries({ queryKey: ["GetJobResultStatistics"] }); refetch(); }, 1500); importJobQueue.setStatus("drained"); } else if (!importSession.isComplete) { wasComplete.current = false; } }, [importSession.isComplete, refetch, importJobQueue, queryClient]); // Listen to socket events to update Past Imports table in real-time useEffect(() => { const socket = getSocket("/"); const handleImportCompleted = () => { console.log("[Import] IMPORT_SESSION_COMPLETED event - refreshing Past Imports"); // Small delay to ensure backend has committed the job results setTimeout(() => { queryClient.invalidateQueries({ queryKey: ["GetJobResultStatistics"] }); }, 1500); }; const handleQueueDrained = () => { console.log("[Import] LS_IMPORT_QUEUE_DRAINED event - refreshing Past Imports"); // Small delay to ensure backend has committed the job results setTimeout(() => { queryClient.invalidateQueries({ queryKey: ["GetJobResultStatistics"] }); }, 1500); }; socket.on("IMPORT_SESSION_COMPLETED", handleImportCompleted); socket.on("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained); return () => { socket.off("IMPORT_SESSION_COMPLETED", handleImportCompleted); socket.off("LS_IMPORT_QUEUE_DRAINED", handleQueueDrained); }; }, [getSocket, queryClient]); /** * Handles force re-import - re-imports all files to fix indexing issues */ const handleForceReImport = async () => { setImportError(null); // Check for active session before starting using definitive status if (hasActiveSession) { setImportError( `Cannot start import: An import session "${importSession.sessionId}" is already active. Please wait for it to complete.` ); return; } if (window.confirm( "This will re-import ALL files in your library folder, even those already imported. " + "This can help fix Elasticsearch indexing issues. Continue?" )) { if (importJobQueue.status === "drained") { localStorage.removeItem("sessionId"); disconnectSocket("/"); setTimeout(() => { getSocket("/"); setTimeout(() => { forceReImport(); }, 500); }, 100); } else { forceReImport(); } } }; return (

Import

Import comics into the ThreeTwo library.

Importing will add comics identified from the mapped folder into ThreeTwo's database.

Metadata from ComicInfo.xml, if present, will also be extracted.

This process could take a while, if you have a lot of comics, or are importing over a network connection.

{/* Import Statistics */}
{/* Error Message */} {importError && (

Import Error

{importError}

)} {/* Force Re-Import Button - always shown when no import is running */} {!hasActiveSession && (importJobQueue.status === "drained" || importJobQueue.status === undefined) && (
)} {/* Import activity is now shown in the RealTimeImportStats component above */} {!isLoading && !isEmpty(data?.getJobResultStatistics) && (
Past Imports
{data?.getJobResultStatistics.map((jobResult: any, index: number) => { return ( ); })}
# Time Started Session Id Imported Failed
{index + 1} {jobResult.earliestTimestamp && !isNaN(parseInt(jobResult.earliestTimestamp)) ? format( new Date(parseInt(jobResult.earliestTimestamp)), "EEEE, hh:mma, do LLLL y", ) : "N/A"} {jobResult.sessionId}

{jobResult.completedJobs}

{jobResult.failedJobs}

)}
); }; export default Import;