/******************************************************************************** * Copyright (c) 2026 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License 2.0 which is available at * https://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 ********************************************************************************/ import { FC, createContext, useContext, useReducer, useMemo, useRef } from 'react'; import { initialScanState, ScanResult, FileDecision } from './scan-types'; import { scanReducer } from './scan-reducer'; import { ScanContextValue, ScanProviderProps, DerivedData } from './scan-context-types'; import { useFilterOptionsEffect, useScansEffect, useScanCountsEffect, useFilesEffect, useFileCountsEffect, useAutoRefreshEffect, } from './scan-api-effects'; import { useConfirmAction, useFileAction, useRetryFailedScannerJobsAction } from './scan-api-actions'; import { useScanActions } from './scan-actions'; // ============================================================================ // Context Creation // ============================================================================ const ScanContext = createContext(null); // ============================================================================ // Provider Component // ============================================================================ export const ScanProvider: FC = ({ children, service, handleError }) => { const [state, dispatch] = useReducer(scanReducer, initialScanState); // Use a ref for handleError to avoid re-running effects when parent re-renders // This prevents infinite loops when errors trigger parent state changes const handleErrorRef = useRef(handleError); handleErrorRef.current = handleError; // ======================================================================== // API Effects // ======================================================================== useFilterOptionsEffect(service, state, dispatch, handleErrorRef); useScansEffect(service, state, dispatch, handleErrorRef); useScanCountsEffect(service, state, dispatch, handleErrorRef); useFilesEffect(service, state, dispatch, handleErrorRef); useFileCountsEffect(service, state, dispatch, handleErrorRef); useAutoRefreshEffect(state, dispatch); // ======================================================================== // Async API Actions // ======================================================================== const executeConfirmAction = useConfirmAction(service, state, dispatch, handleErrorRef); const executeFileAction = useFileAction(service, state, dispatch, handleErrorRef); const retryFailedScannerJobs = useRetryFailedScannerJobsAction(service, dispatch, handleErrorRef); // ======================================================================== // Actions // ======================================================================== const actions = useScanActions(dispatch, executeConfirmAction, executeFileAction, retryFailedScannerJobs); // ======================================================================== // Derived Data (memoized) // ======================================================================== const selectedExtensions = useMemo((): ScanResult[] => { return state.scans.filter(scan => state.quarantinedChecked[scan.id]); }, [state.scans, state.quarantinedChecked]); const selectedFiles = useMemo((): FileDecision[] => { return state.files.filter(file => state.filesChecked.has(file.id)); }, [state.files, state.filesChecked]); const totalPages = useMemo(() => { // For tabs 0-2 (scans), use totalScans; for tabs 3-4 (files), use totalFiles const total = state.selectedTab >= 3 ? state.totalFiles : state.totalScans; return Math.ceil(total / state.pageSize); }, [state.selectedTab, state.totalScans, state.totalFiles, state.pageSize]); const derived: DerivedData = useMemo(() => ({ selectedExtensions, selectedFiles, totalPages, }), [selectedExtensions, selectedFiles, totalPages]); // ======================================================================== // Context Value // ======================================================================== const contextValue = useMemo(() => ({ state, dispatch, actions, derived, }), [state, actions, derived]); return ( {children} ); }; // ============================================================================ // Custom Hook // ============================================================================ // eslint-disable-next-line react-refresh/only-export-components export const useScanContext = (): ScanContextValue => { const context = useContext(ScanContext); if (!context) { throw new Error('useScanContext must be used within a ScanProvider'); } return context; };