Add ability to drag-n-drop sort Products, Variants, and Photos
This commit is contained in:
parent
7673da7e3d
commit
985697f8ee
@ -19,7 +19,7 @@
|
|||||||
<h3>{{ category }}</h3>
|
<h3>{{ category }}</h3>
|
||||||
<a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category →</a>
|
<a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category →</a>
|
||||||
</header>
|
</header>
|
||||||
{% include 'dashboard/product/_table.html' with product_list=category.product_set.all %}
|
{% include 'dashboard/product/_table.html' with product_list=category.product_set.all category=category %}
|
||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
<h3>{{ category }}</h3>
|
<h3>{{ category }}</h3>
|
||||||
<a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category →</a>
|
<a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category →</a>
|
||||||
</header>
|
</header>
|
||||||
{% include 'dashboard/product/_table.html' with product_list=category.product_set.all %}
|
{% include 'dashboard/product/_table.html' with product_list=category.product_set.all category=category %}
|
||||||
</section>
|
</section>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
<th>Visible in listings</th>
|
<th>Visible in listings</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="sortable" data-model="product" data-filter="category" data-fid="{{ category.pk }}">
|
||||||
{% for product in product_list %}
|
{% for product in product_list %}
|
||||||
<tr class="is-link" onclick="window.location='{% url 'dashboard:product-detail' product.pk %}'">
|
<tr class="is-link" data-id="{{ product.pk }}" onclick="window.location='{% url 'dashboard:product-detail' product.pk %}'">
|
||||||
<td>{{ product.sorting }}</td>
|
<td class="handle">☰</td>
|
||||||
<td>
|
<td>
|
||||||
<figure class="product-figure">
|
<figure class="product-figure">
|
||||||
<img class="product-image" src="{{ product.get_first_img.image.url }}" alt="{{ product.get_first_img.image }}">
|
<img class="product-image" src="{{ product.get_first_img.image.url }}" alt="{{ product.get_first_img.image }}">
|
||||||
|
|||||||
@ -80,10 +80,10 @@
|
|||||||
<th colspan="2">Stock</th>
|
<th colspan="2">Stock</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="sortable" data-model="productvariant" data-filter="product" data-fid="{{ product.pk }}">
|
||||||
{% for variant in product.variants.all %}
|
{% for variant in product.variants.all %}
|
||||||
<tr>
|
<tr data-id="{{ variant.pk }}">
|
||||||
<td>{{ variant.sorting }}</td>
|
<td class="handle">☰</td>
|
||||||
<td>
|
<td>
|
||||||
<h3>{{ variant.name }}</h3>
|
<h3>{{ variant.name }}</h3>
|
||||||
</td>
|
</td>
|
||||||
@ -124,18 +124,18 @@
|
|||||||
<h4>Photos</h4>
|
<h4>Photos</h4>
|
||||||
<a href="{% url 'dashboard:prodphoto-create' product.pk %}" class="btn">+ Upload new photo</a>
|
<a href="{% url 'dashboard:prodphoto-create' product.pk %}" class="btn">+ Upload new photo</a>
|
||||||
</header>
|
</header>
|
||||||
<div class="gallery panel-section">
|
<div class="gallery panel-section sortable" data-model="productphoto" data-filter="product" data-fid="{{ product.pk }}">
|
||||||
{% for photo in product.productphoto_set.all %}
|
{% for photo in product.productphoto_set.all %}
|
||||||
<figure class="gallery-item">
|
<figure class="gallery-item handle" data-id="{{ photo.pk }}">
|
||||||
<img src="{{ photo.image.url }}">
|
<img src="{{ photo.image.url }}">
|
||||||
<figcaption>
|
<figcaption>
|
||||||
<form action="{% url 'dashboard:prodphoto-delete' product.pk photo.pk %}" method="post">
|
<form action="{% url 'dashboard:prodphoto-delete' product.pk photo.pk %}" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="submit" class="btn btn-warning" value="Delete photo">
|
<input type="submit" class="btn btn-warning" value="Delete photo">
|
||||||
</form>
|
</form>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -3,6 +3,11 @@
|
|||||||
|
|
||||||
{% block head_title %}Products | {% endblock %}
|
{% block head_title %}Products | {% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||||
|
<script type="module" src="{% static 'scripts/sorting.js' %}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<article>
|
<article>
|
||||||
<header class="object-header">
|
<header class="object-header">
|
||||||
|
|||||||
@ -231,6 +231,12 @@ urlpatterns = [
|
|||||||
])),
|
])),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
path(
|
||||||
|
'update-sorting/',
|
||||||
|
views.update_sorting,
|
||||||
|
name='update-sorting'
|
||||||
|
),
|
||||||
|
|
||||||
path(
|
path(
|
||||||
'customers/',
|
'customers/',
|
||||||
views.CustomerListView.as_view(),
|
views.CustomerListView.as_view(),
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.apps import apps
|
||||||
from django.shortcuts import render, reverse, redirect, get_object_or_404
|
from django.shortcuts import render, reverse, redirect, get_object_or_404
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import JsonResponse, HttpResponseRedirect
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.views.generic.base import RedirectView, TemplateView
|
from django.views.generic.base import RedirectView, TemplateView
|
||||||
from django.views.generic.edit import (
|
from django.views.generic.edit import (
|
||||||
@ -13,6 +15,7 @@ from django.views.generic.edit import (
|
|||||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
from django.contrib.auth.mixins import (
|
from django.contrib.auth.mixins import (
|
||||||
LoginRequiredMixin, PermissionRequiredMixin
|
LoginRequiredMixin, PermissionRequiredMixin
|
||||||
)
|
)
|
||||||
@ -616,6 +619,27 @@ class ProductOptionDeleteView(
|
|||||||
success_url = reverse_lazy('dashboard:catalog')
|
success_url = reverse_lazy('dashboard:catalog')
|
||||||
|
|
||||||
|
|
||||||
|
def sort(objs, order):
|
||||||
|
for i, pk in enumerate(order):
|
||||||
|
m = objs.get(pk=pk)
|
||||||
|
m.sorting = i+1
|
||||||
|
yield m
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def update_sorting(request):
|
||||||
|
data = json.loads(request.body)
|
||||||
|
model = apps.get_model('core', data['model_name'])
|
||||||
|
objs = model.objects.filter(
|
||||||
|
Q((data['filter'], data['filter_id']))
|
||||||
|
).order_by('sorting')
|
||||||
|
|
||||||
|
updated_objs = sort(objs, data['order'])
|
||||||
|
|
||||||
|
model.objects.bulk_update(updated_objs, ['sorting'])
|
||||||
|
return JsonResponse({'message': 'Sorting updated'})
|
||||||
|
|
||||||
|
|
||||||
class CustomerListView(LoginRequiredMixin, ListView):
|
class CustomerListView(LoginRequiredMixin, ListView):
|
||||||
model = User
|
model = User
|
||||||
template_name = 'dashboard/customer/list.html'
|
template_name = 'dashboard/customer/list.html'
|
||||||
|
|||||||
51
static/scripts/sorting.js
Normal file
51
static/scripts/sorting.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { getCookie } from './cookie.js'
|
||||||
|
const sorting_url = JSON.parse(document.querySelector('#sorting-url').textContent);
|
||||||
|
|
||||||
|
document.querySelectorAll('.sortable').forEach(el => {
|
||||||
|
const options = {
|
||||||
|
animation: 150,
|
||||||
|
store: {
|
||||||
|
/**
|
||||||
|
* Save the order of elements. Called onEnd (when the item is dropped).
|
||||||
|
* @param {Sortable} sortable
|
||||||
|
*/
|
||||||
|
set: (sortable) => {
|
||||||
|
const order = sortable.toArray()
|
||||||
|
const csrftoken = getCookie('csrftoken')
|
||||||
|
const data = {
|
||||||
|
model_name: el.dataset.model,
|
||||||
|
filter: el.dataset.filter,
|
||||||
|
filter_id: el.dataset.fid,
|
||||||
|
order: order
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
mode: 'same-origin',
|
||||||
|
};
|
||||||
|
|
||||||
|
// construct a new Request passing in the csrftoken
|
||||||
|
const request = new Request(sorting_url, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': csrftoken
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return fetch(request, options)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
console.log('Success:', data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (el.querySelector('.handle')) {
|
||||||
|
options.handle = '.handle'
|
||||||
|
}
|
||||||
|
new Sortable(el, options)
|
||||||
|
})
|
||||||
@ -643,3 +643,7 @@ main > article > header {
|
|||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|||||||
@ -22,6 +22,8 @@
|
|||||||
{% endcompress %}
|
{% endcompress %}
|
||||||
|
|
||||||
<script type="module" defer src="{% static 'scripts/initializers/timezone.js' %}"></script>
|
<script type="module" defer src="{% static 'scripts/initializers/timezone.js' %}"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||||
|
<script type="module" src="{% static 'scripts/sorting.js' %}" defer></script>
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -84,6 +86,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% url 'dashboard:update-sorting' as sorting_url %}
|
||||||
|
{{ sorting_url|json_script:"sorting-url" }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('.message-dissmiss').forEach(dissmissEl => {
|
document.querySelectorAll('.message-dissmiss').forEach(dissmissEl => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user