Добавлено быстрое создание тегов на странице списка

API функционал:
- Создан endpoint /api/tags/create/ для AJAX создания тегов
- Валидация: пустое имя, длина, уникальность (регистронезависимо)
- Автоматическая генерация slug через модель
- Возврат JSON с данными созданного тега

UI функционал на странице списка тегов:
- Панель быстрого создания с крупным полем ввода
- Автофокус на поле при загрузке страницы
- Создание тега по нажатию Enter или клику на кнопку
- Индикатор загрузки (спиннер) во время создания
- Блокировка поля/кнопки во время запроса
- Автоматическая перезагрузка страницы после создания
- Возврат фокуса в поле при ошибке
- Красивые alert-сообщения об успехе/ошибке
- CSRF защита

Workflow: ввёл название → Enter → тег создан → фокус снова в поле → можно вводить следующий

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-11 23:26:40 +03:00
parent 1a0360f8c0
commit 1daee88cbd
4 changed files with 193 additions and 1 deletions

View File

@@ -11,6 +11,24 @@
</a>
</div>
<!-- Панель быстрого создания тега -->
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title mb-3">
<i class="bi bi-lightning-charge"></i> Быстрое создание тега
</h5>
<div class="input-group">
<input type="text" id="quick-tag-input" class="form-control form-control-lg"
placeholder="Введите название тега и нажмите Enter..."
autocomplete="off">
<button class="btn btn-success" id="quick-tag-btn" type="button">
<i class="bi bi-plus-circle"></i> Создать
</button>
</div>
<div id="quick-tag-message" class="mt-2"></div>
</div>
</div>
<!-- Панель поиска и фильтров -->
<div class="card mb-4">
<div class="card-body">
@@ -144,4 +162,85 @@
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const input = document.getElementById('quick-tag-input');
const btn = document.getElementById('quick-tag-btn');
const messageDiv = document.getElementById('quick-tag-message');
// Автофокус на поле при загрузке страницы
input.focus();
function createTag() {
const name = input.value.trim();
if (!name) {
showMessage('Введите название тега', 'warning');
return;
}
// Блокируем кнопку и поле во время создания
btn.disabled = true;
input.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Создание...';
fetch('{% url "products:api-tag-create" %}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({name: name})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage(`Тег "${data.tag.name}" успешно создан!`, 'success');
input.value = '';
// Перезагружаем страницу через 1 секунду для обновления списка
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showMessage('Ошибка: ' + data.error, 'danger');
// Разблокируем поле и кнопку при ошибке
btn.disabled = false;
input.disabled = false;
btn.innerHTML = '<i class="bi bi-plus-circle"></i> Создать';
input.focus();
}
})
.catch(error => {
console.error('Error:', error);
showMessage('Ошибка сети при создании тега', 'danger');
// Разблокируем поле и кнопку при ошибке
btn.disabled = false;
input.disabled = false;
btn.innerHTML = '<i class="bi bi-plus-circle"></i> Создать';
input.focus();
});
}
function showMessage(text, type) {
messageDiv.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show mb-0" role="alert">
${text}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>`;
}
// Обработчик клика по кнопке
btn.addEventListener('click', createTag);
// Обработчик нажатия Enter
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
createTag();
}
});
});
</script>
{% endblock %}

View File

@@ -37,6 +37,7 @@ urlpatterns = [
# API endpoints
path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'),
path('api/kits/temporary/create/', views.create_temporary_kit_api, name='api-temporary-kit-create'),
path('api/tags/create/', views.create_tag_api, name='api-tag-create'),
# CRUD URLs for ProductVariantGroup (Варианты товаров)
path('variant-groups/', views.ProductVariantGroupListView.as_view(), name='variantgroup-list'),

View File

@@ -80,7 +80,7 @@ from .tag_views import (
)
# API представления
from .api_views import search_products_and_variants, validate_kit_cost, create_temporary_kit_api
from .api_views import search_products_and_variants, validate_kit_cost, create_temporary_kit_api, create_tag_api
__all__ = [
@@ -149,4 +149,5 @@ __all__ = [
'search_products_and_variants',
'validate_kit_cost',
'create_temporary_kit_api',
'create_tag_api',
]

View File

@@ -620,3 +620,94 @@ def create_temporary_kit_api(request):
'success': False,
'error': f'Ошибка при создании комплекта: {str(e)}'
}, status=500)
def create_tag_api(request):
"""
AJAX endpoint для быстрого создания тега из списка тегов.
Принимает JSON:
{
"name": "Новый тег"
}
Возвращает JSON:
{
"success": true,
"tag": {
"id": 1,
"name": "Новый тег",
"slug": "novyj-teg",
"is_active": true,
"products_count": 0,
"kits_count": 0
}
}
Или при ошибке:
{
"success": false,
"error": "Описание ошибки"
}
"""
if request.method != 'POST':
return JsonResponse({
'success': False,
'error': 'Метод не поддерживается'
}, status=405)
try:
import json
from ..models import ProductTag
data = json.loads(request.body)
name = data.get('name', '').strip()
# Валидация
if not name:
return JsonResponse({
'success': False,
'error': 'Название тега не может быть пустым'
}, status=400)
if len(name) > 100:
return JsonResponse({
'success': False,
'error': 'Название тега слишком длинное (максимум 100 символов)'
}, status=400)
# Проверка уникальности (регистронезависимо)
if ProductTag.objects.filter(name__iexact=name).exists():
return JsonResponse({
'success': False,
'error': f'Тег "{name}" уже существует'
}, status=400)
# Создание тега (slug будет сгенерирован автоматически в модели)
tag = ProductTag.objects.create(
name=name,
is_active=True
)
return JsonResponse({
'success': True,
'tag': {
'id': tag.id,
'name': tag.name,
'slug': tag.slug,
'is_active': tag.is_active,
'products_count': 0,
'kits_count': 0
}
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Некорректный JSON'
}, status=400)
except Exception as e:
return JsonResponse({
'success': False,
'error': f'Ошибка при создании тега: {str(e)}'
}, status=500)