Author SHA1 Message Date
klmp200andGitHub ebfac638de Merge pull request #1465 from ae-utbm/hotfix
Fix broken product widget
2026-08-30 22:40:08 +02:00
klmp200andGitHub 839536661b Merge pull request #1466 from ae-utbm/htmx4
Migrate to HTMX4
2026-08-30 22:39:52 +02:00
thomas girodandGitHub b1b3639dc9 Merge pull request #1467 from ae-utbm/pedagogy-style
Pedagogy style
2026-08-30 22:38:34 +02:00
klmp200 56c832ba06 Migrate to HTMX4 2026-08-30 22:07:53 +02:00
imperosol 884019d803 style: standardize UE search style 2026-08-30 21:53:00 +02:00
klmp200 dfc714ff3d Fix broken ProductAjaxSelect widget 2026-08-28 15:05:17 +02:00
13 changed files with 84 additions and 139 deletions
@@ -7,7 +7,7 @@
<form <form
hx-post="{{ url('club:club_new_members', club_id=club.id) }}" hx-post="{{ url('club:club_new_members', club_id=club.id) }}"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-target="#member-fragment-container" hx-target="#member-fragment-container"
id="add_club_members_form" id="add_club_members_form"
+10 -5
View File
@@ -6,10 +6,15 @@
* for more efficient tree-shaking and gzip compression. * for more efficient tree-shaking and gzip compression.
*/ */
// Must be loaded before Apline
import htmx from "htmx.org";
import "htmx.org/dist/ext/hx-alpine-compat.js";
import "htmx.org/dist/ext/hx-prompt.js";
import "htmx.org/dist/ext/hx-download.js";
import sort from "@alpinejs/sort"; import sort from "@alpinejs/sort";
import Alpine from "alpinejs"; import Alpine from "alpinejs";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill"; import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import htmx from "htmx.org";
import { limitedChoices } from "#core:alpine/limited-choices"; import { limitedChoices } from "#core:alpine/limited-choices";
import { expireOldStorage } from "#core:core/localstorage"; import { expireOldStorage } from "#core:core/localstorage";
import { default as navbar } from "#core:core/navbar"; import { default as navbar } from "#core:core/navbar";
@@ -44,16 +49,16 @@ polyfillCountryFlagEmojis();
* HTMX * HTMX
*/ */
document.body.addEventListener( document.body.addEventListener(
"htmx:beforeRequest" as keyof HTMLElementEventMap, "htmx:before:request" as keyof HTMLElementEventMap,
(event) => { (event) => {
(event as CustomEvent).detail.target.ariaBusy = true; (event as CustomEvent).detail.ctx.target.ariaBusy = true;
}, },
); );
document.body.addEventListener( document.body.addEventListener(
"htmx:beforeSwap" as keyof HTMLElementEventMap, "htmx:before:swap" as keyof HTMLElementEventMap,
(event) => { (event) => {
(event as CustomEvent).detail.target.ariaBusy = null; (event as CustomEvent).detail.ctx.target.ariaBusy = null;
}, },
); );
@@ -1,6 +1,6 @@
<form <form
hx-post="{{ url("core:user_visibility_fragment", user_id=form.instance.id) }}" hx-post="{{ url("core:user_visibility_fragment", user_id=form.instance.id) }}"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
hx-swap="outerHTML" x-data="{ isViewable: {{ form.is_viewable.value()|tojson }} }" hx-swap="outerHTML" x-data="{ isViewable: {{ form.is_viewable.value()|tojson }} }"
> >
{% for message in messages %} {% for message in messages %}
@@ -28,18 +28,23 @@ export class ProductAjaxSelect extends AjaxSelect {
return []; return [];
} }
private getName(item: SimpleProductSchema, sanitize: typeof escape_html): string { // In the context in which this method is called, `this` might be shadowed
// We need to call it explicitly from the class itself
private static getName(
item: SimpleProductSchema,
sanitize: typeof escape_html,
): string {
return item.code ? `${sanitize(item.code)} - ${sanitize(item.name)}` : item.name; return item.code ? `${sanitize(item.code)} - ${sanitize(item.name)}` : item.name;
} }
protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) { protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `<div class="select-item"> return `<div class="select-item">
<span class="select-item-text">${this.getName(item, sanitize)}</span> <span class="select-item-text">${ProductAjaxSelect.getName(item, sanitize)}</span>
</div>`; </div>`;
} }
protected renderItem(item: SimpleProductSchema, sanitize: typeof escape_html) { protected renderItem(item: SimpleProductSchema, sanitize: typeof escape_html) {
return `<span>${this.getName(item, sanitize)}</span>`; return `<span>${ProductAjaxSelect.getName(item, sanitize)}</span>`;
} }
} }
@@ -1,5 +1,10 @@
<div id="student_card_form"> <div id="student_card_form">
<form hx-post="{{ action }}" hx-swap="outerHTML" hx-target="#student_card_form"> <form
hx-post="{{ action }}"
hx-swap="outerHTML"
hx-target="#student_card_form"
hx-disable="input[type='submit']"
>
{% csrf_token %} {% csrf_token %}
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p> <p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
<input type="submit" value="{% trans %}Confirm{% endtrans %}" /> <input type="submit" value="{% trans %}Confirm{% endtrans %}" />
+1 -1
View File
@@ -217,7 +217,7 @@ C'est une technologie simple et puissante qui se veut comme le jQuery du web mod
### Htmx ### Htmx
[Site officiel](https://htmx.org/) [Site officiel](https://four.htmx.org/)
En plus de AlpineJS, linteractivité sur le site est augmentée via Htmx. En plus de AlpineJS, linteractivité sur le site est augmentée via Htmx.
C'est une librairie js qui s'utilise également au moyen d'attributs HTML à C'est une librairie js qui s'utilise également au moyen d'attributs HTML à
@@ -41,7 +41,7 @@
hx-post="{{ url("election:apply_result", election_id=form.election.id) }}" hx-post="{{ url("election:apply_result", election_id=form.election.id) }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-target="#apply-election-result-fragment" hx-target="#apply-election-result-fragment"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
> >
{% csrf_token %} {% csrf_token %}
{{ form }} {{ form }}
+11 -5
View File
@@ -30,7 +30,7 @@
"easymde": "^2.21.0", "easymde": "^2.21.0",
"glob": "^13.0.6", "glob": "^13.0.6",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"htmx.org": "^2.0.10", "htmx.org": "^4.0.0",
"js-cookie": "^3.0.8", "js-cookie": "^3.0.8",
"lit-html": "^3.3.3", "lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1", "native-file-system-adapter": "^3.0.1",
@@ -3801,10 +3801,16 @@
} }
}, },
"node_modules/htmx.org": { "node_modules/htmx.org": {
"version": "2.0.10", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.10.tgz", "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-4.0.0.tgz",
"integrity": "sha512-kdeJe7ZVwaS6QMz/ebBIVtZdpwen6L0OQ5GOhPV9MKBb196TCZeZu4yA7ZIQsaLKv7EpXz+So7KSXNuHXhj7Cw==", "integrity": "sha512-T/171FUY93Kdfp8t+DnHdk45QvKRiBhVhhrwSzrXgUi4pHKvhp77dUA/qg8FAjsFWPIHNbmUuIdCrcVHuiZWng==",
"license": "0BSD" "license": "BSD-0-Clause",
"workspaces": [
"ext/*"
],
"bin": {
"upgrade-check": "dist/scripts/upgrade-check.js"
}
}, },
"node_modules/ical.js": { "node_modules/ical.js": {
"version": "1.5.0", "version": "1.5.0",
+2 -2
View File
@@ -34,8 +34,8 @@
"@types/cytoscape-klay": "^3.1.5", "@types/cytoscape-klay": "^3.1.5",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^26.2.0", "@types/node": "^26.2.0",
"rollup-plugin-visualizer": "^7.1.1",
"@typescript/native": "npm:typescript@^7.0.2", "@typescript/native": "npm:typescript@^7.0.2",
"rollup-plugin-visualizer": "^7.1.1",
"typescript": "npm:@typescript/typescript6@^6.0.2", "typescript": "npm:@typescript/typescript6@^6.0.2",
"vite": "^8.2.2" "vite": "^8.2.2"
}, },
@@ -61,7 +61,7 @@
"easymde": "^2.21.0", "easymde": "^2.21.0",
"glob": "^13.0.6", "glob": "^13.0.6",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"htmx.org": "^2.0.10", "htmx.org": "^4.0.0",
"js-cookie": "^3.0.8", "js-cookie": "^3.0.8",
"lit-html": "^3.3.3", "lit-html": "^3.3.3",
"native-file-system-adapter": "^3.0.1", "native-file-system-adapter": "^3.0.1",
+11 -88
View File
@@ -64,100 +64,23 @@ $pedagogy-white-text: #f0f0f0;
} }
#search_form { #search_form {
.search-form-container {
display: grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto auto;
grid-template-areas:
"action-bar action-bar"
"search-bar search-bar"
"radio-department radio-department"
"radio-credit-type radio-semester";
}
.action-bar { .radio-guide fieldset {
grid-area: action-bar; input[type="checkbox"] {
margin-bottom: 10px; display: none;
}
.search-bar {
grid-area: search-bar;
display: grid;
grid-template-columns: auto 200px;
grid-template-rows: auto;
grid-template-areas: "search-bar-input search-bar-button";
@media screen and (max-width: $medium-devices) {
grid-template-columns: auto auto;
grid-template-rows: auto;
grid-template-areas: "search-bar-input search-bar-button";
} }
@media screen and (max-width: $small-devices) {
grid-template-columns: auto;
grid-template-rows: auto;
grid-template-areas: "search-bar-input";
.search-bar-button {
display: none;
}
}
.search-bar-input {
grid-area: search-bar-input;
background: $pedagogy-light-blue;
}
.search-bar-button {
grid-area: search-bar-button;
background: $pedagogy-orange;
color: white;
font-weight: bold;
margin-left: 20px;
}
}
.radio-department {
grid-area: radio-department;
}
.radio-credit-type {
grid-area: radio-credit-type;
}
.radio-semester {
grid-area: radio-semester;
}
.radio-guide input[type="radio"],
input[type="checkbox"] {
display: none;
}
.radio-guide {
margin-top: 10px; margin-top: 10px;
margin-bottom: 0;
color: white; color: white;
}
.radio-guide label { label {
display: inline-block; padding: 10px 20px;
background-color: $pedagogy-blue; }
padding: 10px 20px;
font-family: Arial, sans-serif;
font-size: 16px;
border-radius: 4px;
}
.radio-guide input[type="radio"]:checked+label { input[type="checkbox"]:checked+label {
background-color: $pedagogy-orange; background-color: $pedagogy-orange;
} @include shadow;
}
.radio-guide input[type="checkbox"]:checked+label {
background-color: $pedagogy-orange;
}
.radio-guide label:hover {
background-color: $pedagogy-hover-blue;
} }
} }
+30 -29
View File
@@ -14,7 +14,7 @@
{% endblock %} {% endblock %}
{% block additional_js %} {% block additional_js %}
<script type="module" src="{{ static('bundled/pedagogy/guide-index.js') }}"></script> <script type="module" src="{{ static('bundled/pedagogy/guide-index.ts') }}"></script>
{% endblock %} {% endblock %}
{% block head %} {% block head %}
@@ -24,30 +24,28 @@
{% block content %} {% block content %}
{% if user.has_perm("pedagogy.add_ue") %} {% if user.has_perm("pedagogy.add_ue") %}
<div class="action-bar"> <div class="row gap">
<p> <a href="{{ url('pedagogy:ue_create') }}" class="btn btn-blue"><i class="fa fa-plus"></i>{% trans %}Create UE{% endtrans %}</a>
<a href="{{ url('pedagogy:ue_create') }}">{% trans %}Create UE{% endtrans %}</a> <a href="{{ url('pedagogy:moderation') }}" class="btn btn-grey">{% trans %}Moderate comments{% endtrans %}</a>
</p>
<p>
<a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a>
</p>
</div> </div>
<br/> <br/>
{% endif %} {% endif %}
<div class="pedagogy" x-data="ue_search" x-cloak> <div class="pedagogy" x-data="ue_search" x-cloak>
<form id="search_form"> <form id="search_form" class="">
<div class="search-form-container"> <div class="search-form-container">
<div class="search-bar"> <fieldset>
<label for="search_input">{% trans %}Search UE{% endtrans %}</label>
<input <input
id="search_input" id="search_input"
class="search-bar-input" class="search-bar-input"
type="text" type="search"
name="search" name="search"
placeholder=""
x-model.debounce.500ms="search" x-model.debounce.500ms="search"
/> />
</div> </fieldset>
<div class="radio-department"> <div class="row gap-3x margin-bottom radio-guide">
<div class="radio-guide"> <fieldset>
{% set departments = [ {% set departments = [
("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"), ("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"),
("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC") ("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC")
@@ -56,16 +54,16 @@
<input <input
type="checkbox" type="checkbox"
name="department" name="department"
id="radio{{ real_name }}" id="radio_{{ real_name }}"
value="{{ real_name }}" value="{{ real_name }}"
x-model="department" x-model="department"
/> />
<label for="radio{{ real_name }}">{% trans %}{{ display_name }}{% endtrans %}</label> <label for="radio_{{ real_name }}" class="btn btn-blue">
{% trans %}{{ display_name }}{% endtrans %}
</label>
{% endfor %} {% endfor %}
</div> </fieldset>
</div> <fieldset>
<div class="radio-credit-type">
<div class="radio-guide">
{% for credit_type in ["CS", "TM", "EC", "QC", "OM"] %} {% for credit_type in ["CS", "TM", "EC", "QC", "OM"] %}
<input <input
type="checkbox" type="checkbox"
@@ -74,18 +72,21 @@
value="{{ credit_type }}" value="{{ credit_type }}"
x-model="credit_type" x-model="credit_type"
/> />
<label for="radio{{ credit_type }}">{% trans %}{{ credit_type }}{% endtrans %}</label> <label for="radio{{ credit_type }}" class="btn btn-blue">
{% trans %}{{ credit_type }}{% endtrans %}
</label>
{% endfor %} {% endfor %}
</div> </fieldset>
</div> <fieldset>
<div class="radio-semester">
<div class="radio-guide">
<input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/> <input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/>
<label for="radioAUTUMN"><i class="fa fa-leaf"></i></label> <label for="radioAUTUMN" class="btn btn-no-text btn-blue">
<i class="fa fa-leaf"></i>
</label>
<input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/> <input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/>
<label for="radioSPRING"><i class="fa-regular fa-sun"></i></label> <label for="radioSPRING" class="btn btn-no-text btn-blue">
</div> <i class="fa-regular fa-sun"></i>
</label>
</fieldset>
</div> </div>
</div> </div>
</form> </form>
@@ -1,7 +1,7 @@
<form <form
hx-post="{{ url("subscription:fragment-existing-user") }}" hx-post="{{ url("subscription:fragment-existing-user") }}"
hx-target="this" hx-target="this"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
{% csrf_token %} {% csrf_token %}
@@ -1,7 +1,7 @@
<form <form
hx-post="{{ url("subscription:fragment-new-user") }}" hx-post="{{ url("subscription:fragment-new-user") }}"
hx-target="this" hx-target="this"
hx-disabled-elt="find input[type='submit']" hx-disable="find input[type='submit']"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
{% csrf_token %} {% csrf_token %}