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

View File

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

View File

@ -6,10 +6,10 @@
<th>Visible in listings</th>
</tr>
</thead>
<tbody>
<tbody class="sortable" data-model="product" data-filter="category" data-fid="{{ category.pk }}">
{% for product in product_list %}
<tr class="is-link" onclick="window.location='{% url 'dashboard:product-detail' product.pk %}'">
<td>{{ product.sorting }}</td>
<tr class="is-link" data-id="{{ product.pk }}" onclick="window.location='{% url 'dashboard:product-detail' product.pk %}'">
<td class="handle">&#9776;</td>
<td>
<figure class="product-figure">
<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>
</tr>
</thead>
<tbody>
<tbody class="sortable" data-model="productvariant" data-filter="product" data-fid="{{ product.pk }}">
{% for variant in product.variants.all %}
<tr>
<td>{{ variant.sorting }}</td>
<tr data-id="{{ variant.pk }}">
<td class="handle">&#9776;</td>
<td>
<h3>{{ variant.name }}</h3>
</td>
@ -124,18 +124,18 @@
<h4>Photos</h4>
<a href="{% url 'dashboard:prodphoto-create' product.pk %}" class="btn">+ Upload new photo</a>
</header>
<div class="gallery panel-section">
{% for photo in product.productphoto_set.all %}
<figure class="gallery-item">
<img src="{{ photo.image.url }}">
<figcaption>
<form action="{% url 'dashboard:prodphoto-delete' product.pk photo.pk %}" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-warning" value="Delete photo">
</form>
</figcaption>
</figure>
{% endfor %}
<div class="gallery panel-section sortable" data-model="productphoto" data-filter="product" data-fid="{{ product.pk }}">
{% for photo in product.productphoto_set.all %}
<figure class="gallery-item handle" data-id="{{ photo.pk }}">
<img src="{{ photo.image.url }}">
<figcaption>
<form action="{% url 'dashboard:prodphoto-delete' product.pk photo.pk %}" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-warning" value="Delete photo">
</form>
</figcaption>
</figure>
{% endfor %}
</div>
</section>
</article>

View File

@ -3,6 +3,11 @@
{% 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 %}
<article>
<header class="object-header">

View File

@ -231,6 +231,12 @@ urlpatterns = [
])),
])),
path(
'update-sorting/',
views.update_sorting,
name='update-sorting'
),
path(
'customers/',
views.CustomerListView.as_view(),

View File

@ -1,10 +1,12 @@
import logging
import json
from datetime import datetime
from django.conf import settings
from django.utils import timezone
from django import forms
from django.apps import apps
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.views.generic.base import RedirectView, TemplateView
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.list import ListView
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.contrib.auth.mixins import (
LoginRequiredMixin, PermissionRequiredMixin
)
@ -616,6 +619,27 @@ class ProductOptionDeleteView(
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):
model = User
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-align: center;
}
.handle {
cursor: grab;
}

View File

@ -22,6 +22,8 @@
{% endcompress %}
<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 %}
{% endblock %}
@ -84,6 +86,9 @@
</div>
{% endif %}
{% url 'dashboard:update-sorting' as sorting_url %}
{{ sorting_url|json_script:"sorting-url" }}
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.message-dissmiss').forEach(dissmissEl => {