import {useState, useEffect, useMemo} from '@wordpress/element'; import {Spinner} from '@wordpress/components'; import {__} from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import Checkbox from '../components/Checkbox'; import Select from '../components/Select'; import Button from '../components/Button'; import TextInput from '../components/TextInput'; import CloseIcon from '../icons/CloseIcon'; import './GroupAdsAssignmentField.scss'; import PlusIcon from '../icons/PlusIcon'; import AdFallbackIcon from '../icons/AdFallbackIcon'; /** * Every ad, page by page: per_page=-1 is not supported by the endpoint (absint turned it into 1, so only one ad loaded). * * @return {Promise} */ export async function fetchAllAds() { const all = []; for ( let page = 1, totalPages = 1; page <= totalPages; page++ ) { const response = await apiFetch( {path: `/adpresso/v1/ads?per_page=100&page=${page}`, parse: false} ); totalPages = parseInt( response.headers.get( 'X-WP-TotalPages' ), 10 ) || 1; all.push( ...await response.json() ); } return all; } const GroupAdsAssignmentField = ( {field, value, onChange, allMetaData} ) => { const [assignedAds, setAssignedAds] = useState( () => { if ( Array.isArray( value ) && value.length > 0 && value[0].title ) { return value; } return []; } ); const [isAssigning, setIsAssigning] = useState( false ); const [availableAds, setAvailableAds] = useState( [] ); const [isLoadingAds, setIsLoadingAds] = useState( false ); const [searchQuery, setSearchQuery] = useState( '' ); const [selection, setSelection] = useState( {} ); const [sortConfig, setSortConfig] = useState( {key: 'weight', direction: 'desc'} ); const isRandomMode = allMetaData.random === true || allMetaData.random === 1 || allMetaData.random === '1'; const weightLabel = isRandomMode ? __( 'Weight', 'adpresso' ) : __( 'Priority', 'adpresso' ); // Check if we already have hydrated data from the server. useEffect( () => { if ( Array.isArray( value ) && value.length > 0 ) { const isHydratedData = typeof value[0].title === 'object' || typeof value[0].image === 'string'; if ( isHydratedData ) { setAssignedAds( value ); } } }, [value] ); // Load all available Ads for the assign mode useEffect( () => { if ( isAssigning && availableAds.length === 0 ) { setIsLoadingAds( true ); fetchAllAds() .then( ads => { setAvailableAds( ads ); setIsLoadingAds( false ); } ) .catch( err => { console.error( err ); setIsLoadingAds( false ); } ); } }, [isAssigning] ); // Prepare weight options as objects for the select component // Format: { value: 20, label: "20" } const weightOptions = useMemo( () => { return Array.from( {length: 20}, ( _, i ) => { const val = 20 - i; return {value: val, label: String( val )}; } ); }, [] ); const updateParent = ( newAdsList ) => { const minified = newAdsList.map( ad => ({ ad_id: ad.id, weight: ad.weight }) ); onChange( field.id, minified ); }; const handleRemove = ( adId ) => { const newList = assignedAds.filter( ad => ad.id !== adId ); setAssignedAds( newList ); updateParent( newList ); }; const handleWeightChangeAssigned = ( adId, newWeight ) => { const newList = assignedAds.map( ad => ad.id === adId ? {...ad, weight: parseInt( newWeight )} : ad ); // Optional: Neu sortieren nach Gewicht newList.sort( ( a, b ) => b.weight - a.weight ); setAssignedAds( newList ); updateParent( newList ); }; // Handle the assign ads button const handleAssignAds = () => { const newAssignments = Object.keys( selection ).map( adId => { const id = parseInt( adId ); const originalAd = availableAds.find( a => a.id === id ); return { ...originalAd, weight: parseInt( selection[adId] ) }; } ); const merged = [...assignedAds, ...newAssignments]; merged.sort( ( a, b ) => b.weight - a.weight ); setAssignedAds( merged ); updateParent( merged ); // Reset and change mode setSelection( {} ); setSearchQuery( '' ); setIsAssigning( false ); }; const toggleSelection = ( adId, weight = 10 ) => { setSelection( prev => { const next = {...prev}; if ( next[adId] ) { delete next[adId]; } else { next[adId] = weight; } return next; } ); }; // Updates the weight of an already selected ad const updateSelectionWeight = ( adId, newWeight ) => { setSelection( prev => ({ ...prev, [adId]: newWeight }) ); }; // filter ads for selection (search + already selected are excluded) const filteredAvailableAds = useMemo( () => { return availableAds.filter( ad => { // don't show if already assigned. if ( assignedAds.some( assigned => assigned.id === ad.id ) ) { return false; } const titleRaw = ad.title?.raw || ad.title || ''; const q = searchQuery.toLowerCase(); if ( searchQuery && !String( titleRaw ).toLowerCase().includes( q ) ) { return false; } return true; } ); }, [availableAds, assignedAds, searchQuery] ); const renderTypeIcon = ( ad ) => { if ( !ad.image ) { return ( {__( 'Type not found', 'adpresso' )} ); } return ( {ad.type_label || ad.type} ); }; const requestSort = ( key ) => { let direction = 'asc'; if ( sortConfig.key === key && sortConfig.direction === 'asc' ) { direction = 'desc'; } setSortConfig( {key, direction} ); }; const getSortedItems = ( items ) => { if ( !items ) { return []; } const sorted = [...items]; sorted.sort( ( a, b ) => { let valA, valB; switch ( sortConfig.key ) { case 'type': valA = (a.type_label || a.type || '').toLowerCase(); valB = (b.type_label || b.type || '').toLowerCase(); break; case 'name': valA = (a.title?.raw || a.title || '').toLowerCase(); valB = (b.title?.raw || b.title || '').toLowerCase(); break; case 'status': valA = (a.status || '').toLowerCase(); valB = (b.status || '').toLowerCase(); break; case 'conditions': // Simple logic: number of conditions or string compare valA = (a.conditions || []).length; valB = (b.conditions || []).length; break; case 'weight': default: valA = a.weight || 0; valB = b.weight || 0; break; } if ( valA < valB ) { return sortConfig.direction === 'asc' ? - 1 : 1; } if ( valA > valB ) { return sortConfig.direction === 'asc' ? 1 : - 1; } return 0; } ); return sorted; }; const sortedAssignedAds = useMemo( () => getSortedItems( assignedAds ), [assignedAds, sortConfig] ); const sortedFilteredAds = useMemo( () => getSortedItems( filteredAvailableAds ), [filteredAvailableAds, sortConfig] ); // BULK SELECTION --- const isAllSelected = filteredAvailableAds.length > 0 && filteredAvailableAds.every( ad => selection[ad.id] ); const isIndeterminate = filteredAvailableAds.some( ad => selection[ad.id] ) && !isAllSelected; const handleSelectAll = () => { if ( isAllSelected ) { setSelection( {} ); } else { // Select everything (default value 10) const newSelection = {}; filteredAvailableAds.forEach( ad => { newSelection[ad.id] = 10; } ); // merge to keep individual weights setSelection( prev => ({...newSelection, ...prev}) ); } }; const SortableHeader = ( {label, sortKey, className} ) => { const isSorted = sortConfig.key === sortKey; const sortOrder = isSorted ? sortConfig.direction : ''; return (
{label}
); }; const formatConditionLabel = ( type ) => { if ( !type ) { return ''; } return type .replace( /_/g, ' ' ) .replace( /\b\w/g, l => l.toUpperCase() ); }; const formatConditionValue = ( value ) => { if ( Array.isArray( value ) ) { return value.join( ', ' ); } return String( value || '' ); }; const getAllConditions = ( jsonString ) => { if ( !jsonString ) { return []; } try { const tree = typeof jsonString === 'string' ? JSON.parse( jsonString ) : jsonString; if ( !tree || !tree.groups || !Array.isArray( tree.groups ) ) { return []; } const conditions = []; tree.groups.forEach( group => { if ( group.conditions && Array.isArray( group.conditions ) ) { group.conditions.forEach( cond => { if ( cond.type ) { conditions.push( cond ); } } ); } } ); return conditions; } catch ( error ) { return []; } }; const renderConditions = ( conditionsTree ) => { const conditions = getAllConditions( conditionsTree ); if ( conditions.length === 0 ) { return null; } return (
{conditions.map( ( cond, index ) => { const label = formatConditionLabel( cond.type ); const operator = cond.operator || 'is'; // Fallback const value = formatConditionValue( cond.value ); const tooltipText = `${operator}: ${value}`; return ( {label} {tooltipText} ); } )}
); }; return (
{/* VIEW MODE: List of assigned ads */} {!isAssigning && ( <>
{assignedAds.length === 0 && ( )} {sortedAssignedAds.map( ad => { const conditionsSource = ad.conditions_tree || ad.conditions || (ad.meta_data && ad.meta_data.conditions_tree); const currentWeightObj = weightOptions.find( o => o.value === ad.weight ) || weightOptions[0]; return ( ); } )}
{__( 'No ads assigned to this group yet.', 'adpresso' )}
{renderTypeIcon( ad )} {ad.title?.raw || ad.title} {ad.status} {renderConditions( conditionsSource )}
)} {/* ASSIGN MODE: Select */} {isAssigning && (
{isLoadingAds ? (
) : ( {sortedFilteredAds.length === 0 && ( )} {sortedFilteredAds.map( ad => { const isSelected = !!selection[ad.id]; const currentVal = selection[ad.id] || 10; const currentWeightObj = weightOptions.find( o => o.value === currentVal ); return ( ); } )}
{weightLabel}
{__( 'No ads found.', 'adpresso' )}
toggleSelection( ad.id )} /> {renderTypeIcon( ad )} {ad.title?.raw || ad.title} {isSelected && (
)}
)}
); }; export default GroupAdsAssignmentField;