Make product types dynamically orderable.

This commit is contained in:
imperosol 2024-12-17 00:53:47 +01:00
parent c79c251ba7
commit 47876e3971
10 changed files with 269 additions and 113 deletions

View File

@ -1,5 +1,7 @@
import sort from "@alpinejs/sort";
import Alpine from "alpinejs"; import Alpine from "alpinejs";
Alpine.plugin(sort);
window.Alpine = Alpine; window.Alpine = Alpine;
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {

View File

@ -87,3 +87,21 @@ a:not(.button) {
color: $primary-color; color: $primary-color;
} }
} }
form {
.row {
label {
margin: unset;
}
}
fieldset {
margin-bottom: 1rem;
}
.helptext {
margin-top: .25rem;
font-size: 80%;
}
}

View File

@ -314,6 +314,17 @@ body {
} }
} }
.snackbar {
width: 250px;
margin-left: -125px;
box-sizing: border-box;
position: fixed;
z-index: 1;
left: 50%;
top: 60px;
text-align: center;
}
.tabs { .tabs {
border-radius: 5px; border-radius: 5px;

View File

@ -0,0 +1,64 @@
import Alpine from "alpinejs";
import { producttypeReorder } from "#openapi";
document.addEventListener("alpine:init", () => {
Alpine.data("productTypesList", () => ({
loading: false,
alertMessage: {
open: false,
success: true,
content: "",
timeout: null,
},
async reorder(itemId: number, newPosition: number) {
// The sort plugin of Alpine doesn't manage dynamic lists with x-sort
// (cf. https://github.com/alpinejs/alpine/discussions/4157).
// There is an open PR that fixes this issue
// (cf. https://github.com/alpinejs/alpine/pull/4361).
// However, it hasn't been merged yet.
// To overcome this, I get the list of DOM elements
// And fetch the `x-sort:item` attribute, which value is
// the id of the object in database.
// Please make this a little bit cleaner when the fix has been merged
// into the main Alpine repo.
this.loading = true;
const productTypes = this.$refs.productTypes
.childNodes as NodeListOf<HTMLLIElement>;
const getId = (elem: HTMLLIElement) =>
Number.parseInt(elem.getAttribute("x-sort:item"));
const query =
newPosition === 0
? { above: getId(productTypes.item(1)) }
: { below: getId(productTypes.item(newPosition - 1)) };
const response = await producttypeReorder({
// biome-ignore lint/style/useNamingConvention: api is snake_case
path: { type_id: itemId },
query: query,
});
this.openAlertMessage(response.response);
this.loading = false;
},
openAlertMessage(response: Response) {
if (response.ok) {
this.alertMessage.success = true;
this.alertMessage.content = gettext("Products types successfully reordered");
} else {
this.alertMessage.success = false;
this.alertMessage.content = interpolate(
gettext("Product type reorganisation failed with status code : %d"),
[response.status],
);
}
this.alertMessage.open = true;
if (this.alertMessage.timeout !== null) {
clearTimeout(this.alertMessage.timeout);
}
this.alertMessage.timeout = setTimeout(() => {
this.alertMessage.open = false;
}, 2000);
this.loading = false;
},
}));
});

View File

@ -0,0 +1,15 @@
.product-type-list {
li {
list-style: none;
margin-bottom: 10px;
i {
cursor: grab;
visibility: hidden;
}
}
}
body:not(.sorting) .product-type-list li:hover i {
visibility: visible;
}

View File

@ -4,21 +4,49 @@
{% trans %}Product type list{% endtrans %} {% trans %}Product type list{% endtrans %}
{% endblock %} {% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("counter/css/product_type.scss") }}">
{% endblock %}
{% block additional_js %}
<script type="module" src="{{ static("bundled/counter/product-type-index.ts") }}"></script>
{% endblock %}
{% block content %} {% block content %}
<p><a href="{{ url('counter:new_producttype') }}">{% trans %}New product type{% endtrans %}</a></p> <p><a href="{{ url('counter:new_producttype') }}">{% trans %}New product type{% endtrans %}</a></p>
{% if producttype_list %} {% if producttype_list %}
<div x-data="productTypesList">
<p
class="alert snackbar"
:class="alertMessage.success ? 'alert-green' : 'alert-red'"
x-show="alertMessage.open"
x-transition.duration.500ms
x-text="alertMessage.content"
></p>
<h3>{% trans %}Product type list{% endtrans %}</h3> <h3>{% trans %}Product type list{% endtrans %}</h3>
<ul> <ul
{% for t in producttype_list %} x-sort="($item, $position) => reorder($item, $position)"
<li><a href="{{ url('counter:producttype_edit', type_id=t.id) }}">{{ t }}</a></li> x-ref="productTypes"
{% endfor %} class="product-type-list"
:aria-busy="loading"
>
{%- for t in producttype_list -%}
<li x-sort:item="{{ t.id }}">
<i class="fa fa-grip-vertical"></i>
<a href="{{ url('counter:producttype_edit', type_id=t.id) }}">{{ t }}</a>
</li>
{%- endfor -%}
</ul> </ul>
</div>
{% else %} {% else %}
{% trans %}There is no product types in this website.{% endtrans %} {% trans %}There is no product types in this website.{% endtrans %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block script %}
{% endblock %}

View File

@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-17 13:09+0100\n" "POT-Creation-Date: 2024-12-17 13:10+0100\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"

View File

@ -7,7 +7,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-14 10:24+0100\n" "POT-Creation-Date: 2024-12-17 00:46+0100\n"
"PO-Revision-Date: 2024-09-17 11:54+0200\n" "PO-Revision-Date: 2024-09-17 11:54+0200\n"
"Last-Translator: Sli <antoine@bartuccio.fr>\n" "Last-Translator: Sli <antoine@bartuccio.fr>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@ -17,119 +17,128 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: core/static/bundled/core/components/ajax-select-base.ts:68
msgid "Remove"
msgstr "Retirer"
#: core/static/bundled/core/components/ajax-select-base.ts:90
msgid "You need to type %(number)s more characters"
msgstr "Vous devez taper %(number)s caractères de plus"
#: core/static/bundled/core/components/ajax-select-base.ts:94
msgid "No results found"
msgstr "Aucun résultat trouvé"
#: core/static/bundled/core/components/easymde-index.ts:38
msgid "Heading"
msgstr "Titre"
#: core/static/bundled/core/components/easymde-index.ts:44
msgid "Italic"
msgstr "Italique"
#: core/static/bundled/core/components/easymde-index.ts:50
msgid "Bold"
msgstr "Gras"
#: core/static/bundled/core/components/easymde-index.ts:56
msgid "Strikethrough"
msgstr "Barré"
#: core/static/bundled/core/components/easymde-index.ts:65
msgid "Underline"
msgstr "Souligné"
#: core/static/bundled/core/components/easymde-index.ts:74
msgid "Superscript"
msgstr "Exposant"
#: core/static/bundled/core/components/easymde-index.ts:83
msgid "Subscript"
msgstr "Indice"
#: core/static/bundled/core/components/easymde-index.ts:89
msgid "Code"
msgstr "Code"
#: core/static/bundled/core/components/easymde-index.ts:96
msgid "Quote"
msgstr "Citation"
#: core/static/bundled/core/components/easymde-index.ts:102
msgid "Unordered list"
msgstr "Liste non ordonnée"
#: core/static/bundled/core/components/easymde-index.ts:108
msgid "Ordered list"
msgstr "Liste ordonnée"
#: core/static/bundled/core/components/easymde-index.ts:115
msgid "Insert link"
msgstr "Insérer lien"
#: core/static/bundled/core/components/easymde-index.ts:121
msgid "Insert image"
msgstr "Insérer image"
#: core/static/bundled/core/components/easymde-index.ts:127
msgid "Insert table"
msgstr "Insérer tableau"
#: core/static/bundled/core/components/easymde-index.ts:134
msgid "Clean block"
msgstr "Nettoyer bloc"
#: core/static/bundled/core/components/easymde-index.ts:141
msgid "Toggle preview"
msgstr "Activer la prévisualisation"
#: core/static/bundled/core/components/easymde-index.ts:147
msgid "Toggle side by side"
msgstr "Activer la vue côte à côte"
#: core/static/bundled/core/components/easymde-index.ts:153
msgid "Toggle fullscreen"
msgstr "Activer le plein écran"
#: core/static/bundled/core/components/easymde-index.ts:160
msgid "Markdown guide"
msgstr "Guide markdown"
#: core/static/bundled/core/components/nfc-input-index.ts:26
msgid "Unsupported NFC card"
msgstr "Carte NFC non supportée"
#: core/static/bundled/user/family-graph-index.js:233
msgid "family_tree.%(extension)s"
msgstr "arbre_genealogique.%(extension)s"
#: core/static/bundled/user/pictures-index.js:76
msgid "pictures.%(extension)s"
msgstr "photos.%(extension)s"
#: core/static/user/js/user_edit.js:91 #: core/static/user/js/user_edit.js:91
#, javascript-format #, javascript-format
msgid "captured.%s" msgid "captured.%s"
msgstr "capture.%s" msgstr "capture.%s"
#: core/static/webpack/core/components/ajax-select-base.ts:68 #: counter/static/bundled/counter/product-type-index.ts:36
msgid "Remove" msgid "Products types successfully reordered"
msgstr "Retirer" msgstr "Types de produits réordonnés."
#: core/static/webpack/core/components/ajax-select-base.ts:88 #: counter/static/bundled/counter/product-type-index.ts:40
msgid "You need to type %(number)s more characters" #, javascript-format
msgstr "Vous devez taper %(number)s caractères de plus" msgid "Product type reorganisation failed with status code : %d"
msgstr "La réorganisation des types de produit a échoué avec le code : %d"
#: core/static/webpack/core/components/ajax-select-base.ts:92
msgid "No results found"
msgstr "Aucun résultat trouvé"
#: core/static/webpack/core/components/easymde-index.ts:38
msgid "Heading"
msgstr "Titre"
#: core/static/webpack/core/components/easymde-index.ts:44
msgid "Italic"
msgstr "Italique"
#: core/static/webpack/core/components/easymde-index.ts:50
msgid "Bold"
msgstr "Gras"
#: core/static/webpack/core/components/easymde-index.ts:56
msgid "Strikethrough"
msgstr "Barré"
#: core/static/webpack/core/components/easymde-index.ts:65
msgid "Underline"
msgstr "Souligné"
#: core/static/webpack/core/components/easymde-index.ts:74
msgid "Superscript"
msgstr "Exposant"
#: core/static/webpack/core/components/easymde-index.ts:83
msgid "Subscript"
msgstr "Indice"
#: core/static/webpack/core/components/easymde-index.ts:89
msgid "Code"
msgstr "Code"
#: core/static/webpack/core/components/easymde-index.ts:96
msgid "Quote"
msgstr "Citation"
#: core/static/webpack/core/components/easymde-index.ts:102
msgid "Unordered list"
msgstr "Liste non ordonnée"
#: core/static/webpack/core/components/easymde-index.ts:108
msgid "Ordered list"
msgstr "Liste ordonnée"
#: core/static/webpack/core/components/easymde-index.ts:115
msgid "Insert link"
msgstr "Insérer lien"
#: core/static/webpack/core/components/easymde-index.ts:121
msgid "Insert image"
msgstr "Insérer image"
#: core/static/webpack/core/components/easymde-index.ts:127
msgid "Insert table"
msgstr "Insérer tableau"
#: core/static/webpack/core/components/easymde-index.ts:134
msgid "Clean block"
msgstr "Nettoyer bloc"
#: core/static/webpack/core/components/easymde-index.ts:141
msgid "Toggle preview"
msgstr "Activer la prévisualisation"
#: core/static/webpack/core/components/easymde-index.ts:147
msgid "Toggle side by side"
msgstr "Activer la vue côte à côte"
#: core/static/webpack/core/components/easymde-index.ts:153
msgid "Toggle fullscreen"
msgstr "Activer le plein écran"
#: core/static/webpack/core/components/easymde-index.ts:160
msgid "Markdown guide"
msgstr "Guide markdown"
#: core/static/webpack/core/components/nfc-input-index.ts:24
msgid "Unsupported NFC card"
msgstr "Carte NFC non supportée"
#: core/static/webpack/user/family-graph-index.js:233
msgid "family_tree.%(extension)s"
msgstr "arbre_genealogique.%(extension)s"
#: core/static/webpack/user/pictures-index.js:76
msgid "pictures.%(extension)s"
msgstr "photos.%(extension)s"
#: eboutic/static/eboutic/js/makecommand.js:56 #: eboutic/static/eboutic/js/makecommand.js:56
msgid "Incorrect value" msgid "Incorrect value"
msgstr "Valeur incorrecte" msgstr "Valeur incorrecte"
#: sas/static/webpack/sas/viewer-index.ts:271 #: sas/static/bundled/sas/viewer-index.ts:271
msgid "Couldn't moderate picture" msgid "Couldn't moderate picture"
msgstr "Il n'a pas été possible de modérer l'image" msgstr "Il n'a pas été possible de modérer l'image"
#: sas/static/webpack/sas/viewer-index.ts:284 #: sas/static/bundled/sas/viewer-index.ts:284
msgid "Couldn't delete picture" msgid "Couldn't delete picture"
msgstr "Il n'a pas été possible de supprimer l'image" msgstr "Il n'a pas été possible de supprimer l'image"

16
package-lock.json generated
View File

@ -9,12 +9,13 @@
"version": "3", "version": "3",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"@alpinejs/sort": "^3.14.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@hey-api/client-fetch": "^0.4.0", "@hey-api/client-fetch": "^0.4.0",
"@sentry/browser": "^8.34.0", "@sentry/browser": "^8.34.0",
"@zip.js/zip.js": "^2.7.52", "@zip.js/zip.js": "^2.7.52",
"3d-force-graph": "^1.73.4", "3d-force-graph": "^1.73.4",
"alpinejs": "^3.14.1", "alpinejs": "^3.14.7",
"chart.js": "^4.4.4", "chart.js": "^4.4.4",
"cytoscape": "^3.30.2", "cytoscape": "^3.30.2",
"cytoscape-cxtmenu": "^3.5.0", "cytoscape-cxtmenu": "^3.5.0",
@ -44,6 +45,12 @@
"vite-plugin-static-copy": "^2.1.0" "vite-plugin-static-copy": "^2.1.0"
} }
}, },
"node_modules/@alpinejs/sort": {
"version": "3.14.7",
"resolved": "https://registry.npmjs.org/@alpinejs/sort/-/sort-3.14.7.tgz",
"integrity": "sha512-EJzxTBSoKvOxKHAUFeTSgxJR4rJQQPm10b4dB38kGcsxjUtOeNkbBF3xV4nlc0ZyTv7DarTWdppdoR/iP8jfdQ==",
"license": "MIT"
},
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@ -3064,9 +3071,10 @@
} }
}, },
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.14.1", "version": "3.14.7",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.1.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.14.7.tgz",
"integrity": "sha512-ICar8UsnRZAYvv/fCNfNeKMXNoXGUfwHrjx7LqXd08zIP95G2d9bAOuaL97re+1mgt/HojqHsfdOLo/A5LuWgQ==", "integrity": "sha512-ScnbydNBcWVnCiVupD3wWUvoMPm8244xkvDNMxVCspgmap9m4QuJ7pjc+77UtByU+1+Ejg0wzYkP4mQaOMcvng==",
"license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
} }

View File

@ -33,12 +33,13 @@
"vite-plugin-static-copy": "^2.1.0" "vite-plugin-static-copy": "^2.1.0"
}, },
"dependencies": { "dependencies": {
"@alpinejs/sort": "^3.14.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@hey-api/client-fetch": "^0.4.0", "@hey-api/client-fetch": "^0.4.0",
"@sentry/browser": "^8.34.0", "@sentry/browser": "^8.34.0",
"@zip.js/zip.js": "^2.7.52", "@zip.js/zip.js": "^2.7.52",
"3d-force-graph": "^1.73.4", "3d-force-graph": "^1.73.4",
"alpinejs": "^3.14.1", "alpinejs": "^3.14.7",
"chart.js": "^4.4.4", "chart.js": "^4.4.4",
"cytoscape": "^3.30.2", "cytoscape": "^3.30.2",
"cytoscape-cxtmenu": "^3.5.0", "cytoscape-cxtmenu": "^3.5.0",