Add AJAX filtering for preview popover with width lock
- Convert preview filter from client-side JS to server-side AJAX - Add debounce utility function for 300ms delayed AJAX calls - Add filter handlers for all 4 preview types (tab, condition, group, filter-group) - Add filterProductIdsByQuery() method to EntityPreviewHandler - Update all AJAX handlers to support filter parameter - Lock popover width when filtering to prevent resize - Add loading overlay CSS for filter state Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -332,6 +332,7 @@ trait EntitySelector
|
||||
$blockType = Tools::getValue('block_type', 'products');
|
||||
$limit = (int) Tools::getValue('limit', 10);
|
||||
$offset = (int) Tools::getValue('offset', 0);
|
||||
$filter = Tools::getValue('filter', '');
|
||||
|
||||
$values = json_decode($valuesJson, true);
|
||||
if (!is_array($values)) {
|
||||
@@ -344,12 +345,19 @@ trait EntitySelector
|
||||
try {
|
||||
if ($blockType === 'products') {
|
||||
$matchingIds = $this->getProductConditionResolver()->getIdsByMethod($method, $values);
|
||||
|
||||
// Apply filter if provided
|
||||
$previewHandler = $this->getEntityPreviewHandler();
|
||||
if (!empty($filter)) {
|
||||
$matchingIds = $previewHandler->filterProductIdsByQuery($matchingIds, $filter, $idLang, $idShop);
|
||||
}
|
||||
|
||||
$totalCount = count($matchingIds);
|
||||
|
||||
$previewItems = [];
|
||||
if ($totalCount > 0 && $limit > 0) {
|
||||
$previewIds = array_slice($matchingIds, $offset, $limit);
|
||||
$previewItems = $this->getEntityPreviewHandler()->getProductPreviewData($previewIds, $idLang, $idShop);
|
||||
$previewItems = $previewHandler->getProductPreviewData($previewIds, $idLang, $idShop);
|
||||
}
|
||||
|
||||
$this->ajaxDie(json_encode([
|
||||
@@ -386,6 +394,7 @@ trait EntitySelector
|
||||
$blockType = Tools::getValue('block_type', 'products');
|
||||
$limit = (int) Tools::getValue('limit', 10);
|
||||
$offset = (int) Tools::getValue('offset', 0);
|
||||
$filter = Tools::getValue('filter', '');
|
||||
|
||||
$groupData = json_decode($groupDataJson, true);
|
||||
if (!is_array($groupData) || !isset($groupData['include'])) {
|
||||
@@ -482,6 +491,11 @@ trait EntitySelector
|
||||
$matchingIds = $resolver->applyModifiers($matchingIds, $modifiers);
|
||||
}
|
||||
|
||||
// Apply filter if provided
|
||||
if (!empty($filter)) {
|
||||
$matchingIds = $previewHandler->filterProductIdsByQuery($matchingIds, $filter, $idLang, $idShop);
|
||||
}
|
||||
|
||||
$totalCount = count($matchingIds);
|
||||
$previewItems = [];
|
||||
if ($totalCount > 0 && $limit > 0) {
|
||||
@@ -595,6 +609,7 @@ trait EntitySelector
|
||||
$groupId = (int) Tools::getValue('group_id');
|
||||
$groupType = Tools::getValue('group_type'); // 'attribute' or 'feature'
|
||||
$limit = (int) Tools::getValue('limit', 10);
|
||||
$filter = Tools::getValue('filter', '');
|
||||
|
||||
if (!$groupId || !in_array($groupType, ['attribute', 'feature'])) {
|
||||
die(json_encode([
|
||||
@@ -630,30 +645,35 @@ trait EntitySelector
|
||||
$sql->where('ps.active = 1');
|
||||
}
|
||||
|
||||
// Get total count first
|
||||
$countSql = clone $sql;
|
||||
$countResult = $db->executeS($countSql);
|
||||
$totalCount = count($countResult);
|
||||
|
||||
// Get limited results for preview
|
||||
$sql->limit($limit);
|
||||
// Get all matching product IDs
|
||||
$results = $db->executeS($sql);
|
||||
$productIds = array_column($results, 'id_product');
|
||||
|
||||
// Apply filter if provided
|
||||
if (!empty($filter) && !empty($productIds)) {
|
||||
$productIds = $this->getEntityPreviewHandler()->filterProductIdsByQuery($productIds, $filter, $idLang, $idShop);
|
||||
}
|
||||
|
||||
$totalCount = count($productIds);
|
||||
|
||||
// Get limited results for preview
|
||||
$previewIds = array_slice($productIds, 0, $limit);
|
||||
|
||||
// Get product details for preview
|
||||
$items = [];
|
||||
if (!empty($productIds)) {
|
||||
if (!empty($previewIds)) {
|
||||
$productSql = new DbQuery();
|
||||
$productSql->select('p.id_product, pl.name, p.reference, i.id_image, m.name as manufacturer');
|
||||
$productSql->from('product', 'p');
|
||||
$productSql->innerJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . $idLang . ' AND pl.id_shop = ' . $idShop);
|
||||
$productSql->leftJoin('image', 'i', 'i.id_product = p.id_product AND i.cover = 1');
|
||||
$productSql->leftJoin('manufacturer', 'm', 'm.id_manufacturer = p.id_manufacturer');
|
||||
$productSql->where('p.id_product IN (' . implode(',', array_map('intval', $productIds)) . ')');
|
||||
$productSql->limit($limit);
|
||||
$productSql->where('p.id_product IN (' . implode(',', array_map('intval', $previewIds)) . ')');
|
||||
|
||||
$products = $db->executeS($productSql);
|
||||
|
||||
// Map products by ID for ordering
|
||||
$productsById = [];
|
||||
foreach ($products as $product) {
|
||||
$imageUrl = null;
|
||||
if ($product['id_image']) {
|
||||
@@ -664,7 +684,7 @@ trait EntitySelector
|
||||
);
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
$productsById[(int) $product['id_product']] = [
|
||||
'id' => (int) $product['id_product'],
|
||||
'name' => $product['name'],
|
||||
'reference' => $product['reference'],
|
||||
@@ -672,6 +692,13 @@ trait EntitySelector
|
||||
'image' => $imageUrl
|
||||
];
|
||||
}
|
||||
|
||||
// Preserve order from previewIds
|
||||
foreach ($previewIds as $id) {
|
||||
if (isset($productsById[(int) $id])) {
|
||||
$items[] = $productsById[(int) $id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
die(json_encode([
|
||||
|
||||
@@ -211,6 +211,63 @@ class EntityPreviewHandler
|
||||
return $this->buildPreviewResponse($allIds, $items, $limit, $offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter product IDs by search query (name, reference)
|
||||
*
|
||||
* @param array $productIds Product IDs to filter
|
||||
* @param string $query Search query
|
||||
* @param int $idLang Language ID
|
||||
* @param int $idShop Shop ID
|
||||
* @return array Filtered product IDs (preserves order)
|
||||
*/
|
||||
public function filterProductIdsByQuery(array $productIds, $query, $idLang, $idShop)
|
||||
{
|
||||
if (empty($productIds) || empty($query)) {
|
||||
return $productIds;
|
||||
}
|
||||
|
||||
$query = trim($query);
|
||||
if (strlen($query) < 1) {
|
||||
return $productIds;
|
||||
}
|
||||
|
||||
$escapedQuery = '%' . pSQL($query) . '%';
|
||||
|
||||
$sql = new DbQuery();
|
||||
$sql->select('DISTINCT p.id_product');
|
||||
$sql->from('product', 'p');
|
||||
$sql->innerJoin('product_shop', 'ps', 'ps.id_product = p.id_product AND ps.id_shop = ' . (int) $idShop);
|
||||
$sql->leftJoin('product_lang', 'pl', 'pl.id_product = p.id_product AND pl.id_lang = ' . (int) $idLang . ' AND pl.id_shop = ' . (int) $idShop);
|
||||
$sql->leftJoin('manufacturer', 'm', 'm.id_manufacturer = p.id_manufacturer');
|
||||
$sql->where('p.id_product IN (' . implode(',', array_map('intval', $productIds)) . ')');
|
||||
$sql->where('(
|
||||
pl.name LIKE \'' . $escapedQuery . '\'
|
||||
OR p.reference LIKE \'' . $escapedQuery . '\'
|
||||
OR m.name LIKE \'' . $escapedQuery . '\'
|
||||
)');
|
||||
|
||||
$results = Db::getInstance()->executeS($sql);
|
||||
|
||||
if (!$results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$matchingIds = [];
|
||||
foreach ($results as $row) {
|
||||
$matchingIds[(int) $row['id_product']] = true;
|
||||
}
|
||||
|
||||
// Preserve original order
|
||||
$filtered = [];
|
||||
foreach ($productIds as $id) {
|
||||
if (isset($matchingIds[(int) $id])) {
|
||||
$filtered[] = (int) $id;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product preview data
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user