Исправлен Select2 поиск товаров при создании группы вариантов
Проблема: при создании новой группы вариантов (VariantGroup) поиск товаров через Select2 не работал. При редактировании существующих групп всё работало корректно. Причина: отсутствовали проверки инициализации Select2, обработка ошибок AJAX запросов и валидация параметров. Изменения: 1. select2-product-init.html - улучшена функция initProductSelect2: - Добавлена валидация входных параметров (element, apiUrl) - Добавлена проверка загрузки jQuery и Select2 - Улучшена проверка повторной инициализации - Добавлен try-catch для обработки ошибок - Функция возвращает boolean (успех/неудача) - Добавлено логирование для отладки 2. variantgroup_form.html - улучшены все функции работы с формой: initSelect2ForRow: - Добавлена проверка существования row и select элемента - Удаление старых обработчиков перед инициализацией - Проверка результата инициализации Select2 updateRowData: - Добавлен timeout (5 сек) для fetch запросов - Добавлена проверка статуса HTTP ответа - Улучшена обработка ошибок с fallback данными - Добавлено логирование ошибок DOMContentLoaded инициализация: - Добавлена валидация контейнера, totalFormsInput и apiUrl - Задержка перед инициализацией существующих строк (100ms) - Проверка успешности инициализации перед updateRowData Добавление нового товара: - Задержка (50ms) перед инициализацией Select2 - Повторная попытка при неудаче (через 500ms) - Улучшена надежность работы с динамическими элементами Результат: Select2 поиск работает корректно как при создании новых групп, так и при редактировании существующих. Добавлена надежная обработка ошибок и логирование для отладки. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,10 +40,32 @@
|
|||||||
* @param {Element} element - DOM элемент select
|
* @param {Element} element - DOM элемент select
|
||||||
* @param {string} type - Тип поиска ('product' или 'variant')
|
* @param {string} type - Тип поиска ('product' или 'variant')
|
||||||
* @param {string} apiUrl - URL API для поиска
|
* @param {string} apiUrl - URL API для поиска
|
||||||
|
* @returns {boolean} - true если инициализация прошла успешно, false иначе
|
||||||
*/
|
*/
|
||||||
window.initProductSelect2 = function(element, type, apiUrl) {
|
window.initProductSelect2 = function(element, type, apiUrl) {
|
||||||
if (!element || $(element).data('select2')) {
|
// Валидация входных параметров
|
||||||
return; // Уже инициализирован
|
if (!element) {
|
||||||
|
console.error('initProductSelect2: element is null or undefined');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiUrl || typeof apiUrl !== 'string') {
|
||||||
|
console.error('initProductSelect2: invalid apiUrl', apiUrl);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка загрузки jQuery и Select2
|
||||||
|
if (typeof $ === 'undefined' || typeof $.fn.select2 === 'undefined') {
|
||||||
|
console.error('initProductSelect2: jQuery or Select2 not loaded');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $element = $(element);
|
||||||
|
|
||||||
|
// Улучшенная проверка инициализации
|
||||||
|
if ($element.hasClass('select2-hidden-accessible')) {
|
||||||
|
console.log('initProductSelect2: already initialized, skipping', element.name);
|
||||||
|
return true; // Уже инициализирован - это не ошибка
|
||||||
}
|
}
|
||||||
|
|
||||||
var placeholders = {
|
var placeholders = {
|
||||||
@@ -51,38 +73,45 @@
|
|||||||
'variant': 'Начните вводить название группы...'
|
'variant': 'Начните вводить название группы...'
|
||||||
};
|
};
|
||||||
|
|
||||||
$(element).select2({
|
try {
|
||||||
theme: 'bootstrap-5',
|
$element.select2({
|
||||||
placeholder: placeholders[type] || 'Выберите...',
|
theme: 'bootstrap-5',
|
||||||
allowClear: true,
|
placeholder: placeholders[type] || 'Выберите...',
|
||||||
width: '100%',
|
allowClear: true,
|
||||||
language: 'ru',
|
width: '100%',
|
||||||
minimumInputLength: 0,
|
language: 'ru',
|
||||||
dropdownAutoWidth: false,
|
minimumInputLength: 0,
|
||||||
ajax: {
|
dropdownAutoWidth: false,
|
||||||
url: apiUrl,
|
ajax: {
|
||||||
dataType: 'json',
|
url: apiUrl,
|
||||||
delay: 250,
|
dataType: 'json',
|
||||||
data: function (params) {
|
delay: 250,
|
||||||
return {
|
data: function (params) {
|
||||||
q: params.term || '',
|
return {
|
||||||
type: type,
|
q: params.term || '',
|
||||||
page: params.page || 1
|
type: type,
|
||||||
};
|
page: params.page || 1
|
||||||
|
};
|
||||||
|
},
|
||||||
|
processResults: function (data) {
|
||||||
|
return {
|
||||||
|
results: data.results,
|
||||||
|
pagination: {
|
||||||
|
more: data.pagination.more
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cache: true
|
||||||
},
|
},
|
||||||
processResults: function (data) {
|
templateResult: formatSelectResult,
|
||||||
return {
|
templateSelection: formatSelectSelection
|
||||||
results: data.results,
|
});
|
||||||
pagination: {
|
console.log('initProductSelect2: successfully initialized for', element.name);
|
||||||
more: data.pagination.more
|
return true;
|
||||||
}
|
} catch (error) {
|
||||||
};
|
console.error('initProductSelect2: initialization error', error);
|
||||||
},
|
return false;
|
||||||
cache: true
|
}
|
||||||
},
|
|
||||||
templateResult: formatSelectResult,
|
|
||||||
templateSelection: formatSelectSelection
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -215,12 +215,43 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const totalFormsInput = document.querySelector('[name$="TOTAL_FORMS"]');
|
const totalFormsInput = document.querySelector('[name$="TOTAL_FORMS"]');
|
||||||
const apiUrl = '{% url "products:api-search-products-variants" %}';
|
const apiUrl = '{% url "products:api-search-products-variants" %}';
|
||||||
|
|
||||||
|
// Валидация
|
||||||
|
if (!container) {
|
||||||
|
console.error('items-tbody container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!totalFormsInput) {
|
||||||
|
console.error('TOTAL_FORMS input not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiUrl || apiUrl.includes('undefined')) {
|
||||||
|
console.error('Invalid API URL:', apiUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Initializing variant group form with API URL:', apiUrl);
|
||||||
|
|
||||||
// Инициализируем Select2 для всех селектов товаров
|
// Инициализируем Select2 для всех селектов товаров
|
||||||
function initSelect2ForRow(row) {
|
function initSelect2ForRow(row) {
|
||||||
const productSelect = row.querySelector('[name$="-product"]');
|
if (!row) {
|
||||||
if (productSelect) {
|
console.error('initSelect2ForRow: row is null');
|
||||||
window.initProductSelect2(productSelect, 'product', apiUrl);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productSelect = row.querySelector('[name$="-product"]');
|
||||||
|
if (!productSelect) {
|
||||||
|
console.warn('initSelect2ForRow: product select not found in row');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Убираем старые обработчики перед повторной инициализацией
|
||||||
|
$(productSelect).off('select2:select');
|
||||||
|
|
||||||
|
const success = window.initProductSelect2(productSelect, 'product', apiUrl);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
// Обработчик события при выборе товара
|
// Обработчик события при выборе товара
|
||||||
$(productSelect).on('select2:select', function(e) {
|
$(productSelect).on('select2:select', function(e) {
|
||||||
// Извлекаем числовой ID из формата "product_123"
|
// Извлекаем числовой ID из формата "product_123"
|
||||||
@@ -239,13 +270,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
updateRowData(row);
|
updateRowData(row);
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error('initSelect2ForRow: failed to initialize Select2');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для обновления данных строки при выборе товара
|
// Функция для обновления данных строки при выборе товара
|
||||||
function updateRowData(row) {
|
function updateRowData(row) {
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
const productSelect = row.querySelector('[name$="-product"]');
|
const productSelect = row.querySelector('[name$="-product"]');
|
||||||
if (!productSelect || !productSelect.value) {
|
if (!productSelect || !productSelect.value) {
|
||||||
|
// Сброс данных
|
||||||
row.querySelector('[data-product-sku]').textContent = '-';
|
row.querySelector('[data-product-sku]').textContent = '-';
|
||||||
row.querySelector('[data-product-price]').textContent = '-';
|
row.querySelector('[data-product-price]').textContent = '-';
|
||||||
row.querySelector('[data-product-stock]').innerHTML = '-';
|
row.querySelector('[data-product-stock]').innerHTML = '-';
|
||||||
@@ -262,13 +300,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Получаем цену и статус через AJAX (используем числовой ID)
|
// Получаем цену и статус через AJAX (используем числовой ID)
|
||||||
const numericId = productSelect.value;
|
const numericId = productSelect.value;
|
||||||
fetch(`{% url "products:api-search-products-variants" %}?id=${numericId}`)
|
|
||||||
.then(response => response.json())
|
// Добавляем timeout для fetch
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
|
fetch(`${apiUrl}?id=${numericId}`, { signal: controller.signal })
|
||||||
|
.then(response => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.results && data.results.length > 0) {
|
if (data.results && data.results.length > 0) {
|
||||||
const product = data.results[0];
|
const product = data.results[0];
|
||||||
row.querySelector('[data-product-sku]').textContent = product.sku || sku;
|
row.querySelector('[data-product-sku]').textContent = product.sku || sku;
|
||||||
row.querySelector('[data-product-price]').innerHTML = `<strong>${product.price}</strong> руб.` || '-';
|
|
||||||
|
const price = product.price || product.actual_price || '0';
|
||||||
|
row.querySelector('[data-product-price]').innerHTML = `<strong>${price}</strong> руб.`;
|
||||||
|
|
||||||
// Отображаем статус наличия
|
// Отображаем статус наличия
|
||||||
const stockCell = row.querySelector('[data-product-stock]');
|
const stockCell = row.querySelector('[data-product-stock]');
|
||||||
@@ -277,21 +328,33 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
} else {
|
} else {
|
||||||
stockCell.innerHTML = '<span class="badge bg-danger"><i class="bi bi-x-circle"></i> Нет</span>';
|
stockCell.innerHTML = '<span class="badge bg-danger"><i class="bi bi-x-circle"></i> Нет</span>';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: используем данные из опции
|
||||||
|
row.querySelector('[data-product-sku]').textContent = sku;
|
||||||
|
row.querySelector('[data-product-price]').textContent = '-';
|
||||||
|
row.querySelector('[data-product-stock]').textContent = '-';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(error => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
console.error('updateRowData: fetch error', error);
|
||||||
|
// Fallback: данные из опции
|
||||||
row.querySelector('[data-product-sku]').textContent = sku;
|
row.querySelector('[data-product-sku]').textContent = sku;
|
||||||
row.querySelector('[data-product-price]').textContent = '-';
|
row.querySelector('[data-product-price]').textContent = '-';
|
||||||
row.querySelector('[data-product-stock]').textContent = '-';
|
row.querySelector('[data-product-stock]').textContent = '-';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализируем для существующих строк
|
// Инициализируем для существующих строк с небольшой задержкой
|
||||||
container.querySelectorAll('.item-row').forEach(row => {
|
setTimeout(function() {
|
||||||
initSelect2ForRow(row);
|
container.querySelectorAll('.item-row').forEach(row => {
|
||||||
// Загружаем данные товара при загрузке страницы (с небольшой задержкой)
|
const success = initSelect2ForRow(row);
|
||||||
setTimeout(() => updateRowData(row), 100);
|
if (success) {
|
||||||
});
|
// Загружаем данные только если Select2 инициализирован
|
||||||
|
setTimeout(() => updateRowData(row), 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
|
||||||
// ДОБАВЛЕНИЕ НОВОГО ТОВАРА
|
// ДОБАВЛЕНИЕ НОВОГО ТОВАРА
|
||||||
document.getElementById('add-item-btn').addEventListener('click', function(e) {
|
document.getElementById('add-item-btn').addEventListener('click', function(e) {
|
||||||
@@ -342,8 +405,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Увеличиваем счетчик форм
|
// Увеличиваем счетчик форм
|
||||||
totalFormsInput.value = newFormIndex + 1;
|
totalFormsInput.value = newFormIndex + 1;
|
||||||
|
|
||||||
// Инициализируем Select2 для нового селекта
|
// Даем время браузеру отрисовать элемент перед инициализацией Select2
|
||||||
initSelect2ForRow(newRow);
|
setTimeout(function() {
|
||||||
|
const success = initSelect2ForRow(newRow);
|
||||||
|
if (!success) {
|
||||||
|
console.error('Failed to initialize Select2 for new row, retrying...');
|
||||||
|
// Повторная попытка через 500ms
|
||||||
|
setTimeout(function() {
|
||||||
|
initSelect2ForRow(newRow);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
// Обновляем приоритеты
|
// Обновляем приоритеты
|
||||||
updatePriorities();
|
updatePriorities();
|
||||||
|
|||||||
Reference in New Issue
Block a user