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