import {useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle, useRef} from '@wordpress/element'; import {Spinner} from '@wordpress/components'; import {__} from '@wordpress/i18n'; import SourceColumn from '../components/AssignmentSourceColumn'; import TargetColumn from '../components/AssignmentTargetColumn'; import './AssignmentField.scss'; import '../scss/_assignment.scss'; const {apiNonce} = window.adpressoAdminData || {}; const assignmentCache = {}; export const clearAssignmentCacheForItem = ( itemId ) => { if ( itemId && assignmentCache[itemId] ) { delete assignmentCache[itemId]; console.log( `Cache cleared for item #${itemId}` ); } }; const AssignmentField = forwardRef( ( {field, value, onChange, entityData, onSetDirty}, ref ) => { const currentAdId = entityData.id; const restUrl = 'adpresso/v1/'; // State Management --- const [groups, setGroups] = useState( [] ); const [placements, setPlacements] = useState( [] ); const [activeContext, setActiveContext] = useState( null ); const [assignments, setAssignments] = useState( {groupIds: [], placementIds: []} ); const [isLoading, setIsLoading] = useState( true ); const [isTargetLoading, setIsTargetLoading] = useState( false ); const [localChanges, setLocalChanges] = useState( [] ); const [groupSearch, setGroupSearch] = useState( '' ); const [placementSearch, setPlacementSearch] = useState( '' ); const hasBecomeDirty = useRef( false ); // Hook for initial loading of the groups. useEffect( () => { setIsLoading( true ); const endpoint = `${restUrl}assignments/ad/${currentAdId}`; if ( assignmentCache[currentAdId] ) { const data = assignmentCache[currentAdId]; console.log( `Loading assignments for Ad #${currentAdId} from cache.` ); setGroups( data.groups || [] ); setPlacements( data.placements || [] ); const assignedGroupIds = (data.groups || []).filter( g => g.is_assigned_to_ad ).map( g => g.id ); setAssignments( { groupIds: assignedGroupIds, placementIds: data.singular_ad_assignments?.placement_ids || [] } ); setIsLoading( false ); return; } wp.apiFetch( {path: endpoint, headers: {'X-WP-Nonce': apiNonce}} ) .then( data => { assignmentCache[currentAdId] = data; // The data is already sorted from PHP setGroups( data.groups || [] ); setPlacements( data.placements || [] ); // Derive the initially assigned group IDs from the pre-processed data const assignedGroupIds = (data.groups || []) .filter( group => group.is_assigned_to_ad ) .map( group => group.id ); // Set the initial assignments for both contexts setAssignments( { groupIds: assignedGroupIds, placementIds: data.singular_ad_assignments?.placement_ids || [] } ); } ) .catch( error => console.error( 'Failed to load initial assignment data:', error ) ) .finally( () => setIsLoading( false ) ); }, [currentAdId] ); const filteredGroups = useMemo( () => { if ( !groupSearch ) { return groups; } const lowerCaseQuery = groupSearch.toLowerCase(); return groups.filter( group => group.title.toLowerCase().includes( lowerCaseQuery ) ); }, [groups, groupSearch] ); const filteredPlacements = useMemo( () => { if ( !placementSearch ) { return placements; } const lowerCaseQuery = placementSearch.toLowerCase(); return placements.filter( placement => placement.title.toLowerCase().includes( lowerCaseQuery ) || placement.type_label.toLowerCase().includes( lowerCaseQuery ) ); }, [placements, placementSearch] ); const handleContextChange = useCallback( ( newContext ) => { if ( activeContext && newContext.type === activeContext.type && newContext.id === activeContext.id ) { return; } setIsTargetLoading( true ); setActiveContext( newContext ); const isSingular = newContext.type === 'singular'; const endpoint = isSingular ? `${restUrl}assignments/ad/${newContext.id}` : `${restUrl}assignments/group/${newContext.id}`; wp.apiFetch( {path: endpoint, headers: {'X-WP-Nonce': apiNonce}} ) .then( data => { if ( data.placements ) { setPlacements( data.placements ); } // Use the right key based on the context. const newPlacementIds = isSingular ? data.singular_ad_assignments?.placement_ids || [] : data.assigned_placement_ids || []; setAssignments( prev => ({ ...prev, placementIds: newPlacementIds }) ); } ) .catch( error => { console.error( `Failed to load assignment data for context:`, newContext, error ); setPlacements( [] ); setAssignments( prev => ({...prev, placementIds: []}) ); } ) .finally( () => { setIsTargetLoading( false ); } ); }, [activeContext] ); // The changes handed out by the last getChanges(), to revert the ones the server skipped. const sentChanges = useRef( [] ); useImperativeHandle( ref, () => ({ getChanges: () => { // Return the gathered changes and empty the list for the next save progress. const changes = [...localChanges]; sentChanges.current = changes; setLocalChanges( [] ); return changes; }, // Undoes the optimistic state of changes the server did not apply (skipped_assignments). revertChanges: ( skipped = [] ) => { const sameContext = ( a, b ) => (a?.type ?? null) === (b?.type ?? null) && (a?.id ?? null) === (b?.id ?? null); const groupIds = new Set( assignments.groupIds ); const placementIds = new Set( assignments.placementIds ); [...skipped].reverse().forEach( skip => { if ( skip.action === 'add_to_group' ) { groupIds.delete( skip.groupId ); } else if ( skip.action === 'remove_from_group' ) { groupIds.add( skip.groupId ); } else { // Placement checkboxes show the active context only. const sent = sentChanges.current.find( c => c.action === skip.action && c.placementId === skip.placementId ); if ( !sent || !sameContext( sent.context, activeContext ) ) { return; } if ( skip.action === 'assign_to_placement' ) { placementIds.delete( skip.placementId ); } else if ( skip.action === 'unassign_from_placement' ) { placementIds.add( skip.placementId ); } } } ); setAssignments( {groupIds: Array.from( groupIds ), placementIds: Array.from( placementIds )} ); } }) ); const handleAssignmentChange = useCallback( ( change ) => { if ( change.action.includes( 'group' ) ) { setAssignments( prev => { const newGroupIds = new Set( prev.groupIds ); if ( change.action === 'add_to_group' ) { newGroupIds.add( change.groupId ); } else { newGroupIds.delete( change.groupId ); } return {...prev, groupIds: Array.from( newGroupIds )}; } ); } else if ( change.action.includes( 'placement' ) ) { setAssignments( prev => { const newPlacementIds = new Set( prev.placementIds ); if ( change.action === 'assign_to_placement' ) { newPlacementIds.add( change.placementId ); } else { newPlacementIds.delete( change.placementId ); } return {...prev, placementIds: Array.from( newPlacementIds )}; } ); } if ( !hasBecomeDirty.current ) { onSetDirty(); hasBecomeDirty.current = true; } setLocalChanges( prevChanges => { return [...prevChanges, {...change, context: activeContext}]; } ); }, [activeContext, onSetDirty] ); if ( isLoading ) { return ; } return (
{__( 'Assign without grouping', 'adpresso' )} {__( 'Assign the selected item to a placement', 'adpresso' )}
); } ); export default AssignmentField;