Add ability to drag-n-drop sort Products, Variants, and Photos

This commit is contained in:
Nathan Chapman 2023-01-29 15:31:24 -07:00
parent 7673da7e3d
commit 985697f8ee
10 changed files with 116 additions and 21 deletions

View File

@ -19,7 +19,7 @@
<h3>{{ category }}</h3> <h3>{{ category }}</h3>
<a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category &rarr;</a> <a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category &rarr;</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 %}

View File

@ -20,7 +20,7 @@
<h3>{{ category }}</h3> <h3>{{ category }}</h3>
<a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category &rarr;</a> <a href="{% url 'dashboard:category-detail' category.pk %}" class="btn">View category &rarr;</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>

View File

@ -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">&#9776;</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 }}">

View File

@ -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">&#9776;</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>

View File

@ -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">

View File

@ -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(),

View File

@ -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
View 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)
})

View File

@ -643,3 +643,7 @@ main > article > header {
.text-center { .text-center {
text-align: center; text-align: center;
} }
.handle {
cursor: grab;
}

View File

@ -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 => {