mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 17:13:08 +00:00 
			
		
		
		
	Create select widget based on tomselect on django backend
Replace make_ajax in elections by the new widget
This commit is contained in:
		| @@ -13,12 +13,18 @@ import { type UserProfileSchema, userSearchUsers } from "#openapi"; | |||||||
|  |  | ||||||
| @registerComponent("autocomplete-select") | @registerComponent("autocomplete-select") | ||||||
| class AutocompleteSelect extends inheritHtmlElement("select") { | class AutocompleteSelect extends inheritHtmlElement("select") { | ||||||
|   static observedAttributes = ["delay", "placeholder", "max"]; |   static observedAttributes = [ | ||||||
|  |     "delay", | ||||||
|  |     "placeholder", | ||||||
|  |     "max", | ||||||
|  |     "min-characters-for-search", | ||||||
|  |   ]; | ||||||
|   public widget: TomSelect; |   public widget: TomSelect; | ||||||
|  |  | ||||||
|   private delay: number | null = null; |   protected minCharNumberForSearch = 0; | ||||||
|   private placeholder = ""; |   protected delay: number | null = null; | ||||||
|   private max: number | null = null; |   protected placeholder = ""; | ||||||
|  |   protected max: number | null = null; | ||||||
|  |  | ||||||
|   protected attributeChangedCallback( |   protected attributeChangedCallback( | ||||||
|     name: string, |     name: string, | ||||||
| @@ -38,26 +44,59 @@ class AutocompleteSelect extends inheritHtmlElement("select") { | |||||||
|         this.max = Number.parseInt(newValue) ?? null; |         this.max = Number.parseInt(newValue) ?? null; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|  |       case "min-characters-for-search": { | ||||||
|  |         this.minCharNumberForSearch = Number.parseInt(newValue) ?? 0; | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|       default: { |       default: { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   constructor() { |   connectedCallback() { | ||||||
|     super(); |     super.connectedCallback(); | ||||||
|  |     // Collect all options nodes and put them into the select node | ||||||
|     window.addEventListener("DOMContentLoaded", () => { |     const options: Element[] = []; // We need to make a copy to delete while iterating | ||||||
|  |     for (const child of this.children) { | ||||||
|  |       if (child.tagName.toLowerCase() === "option") { | ||||||
|  |         options.push(child); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     for (const option of options) { | ||||||
|  |       this.removeChild(option); | ||||||
|  |       this.node.appendChild(option); | ||||||
|  |     } | ||||||
|     this.widget = new TomSelect(this.node, this.tomSelectSettings()); |     this.widget = new TomSelect(this.node, this.tomSelectSettings()); | ||||||
|     this.attachBehaviors(); |     this.attachBehaviors(); | ||||||
|     }); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected tomSelectSettings(): RecursivePartial<TomSettings> { |   protected tomSelectSettings(): RecursivePartial<TomSettings> { | ||||||
|     return { |     return { | ||||||
|       maxItems: this.max, |       maxItems: this.node.multiple ? this.max : 1, | ||||||
|       loadThrottle: this.delay, |       loadThrottle: this.delay, | ||||||
|       placeholder: this.placeholder, |       placeholder: this.placeholder, | ||||||
|  |       shouldLoad: (query: string) => { | ||||||
|  |         return query.length >= this.minCharNumberForSearch; // Avoid launching search with less than 2 characters | ||||||
|  |       }, | ||||||
|  |       render: { | ||||||
|  |         option: (item: TomOption, sanitize: typeof escape_html) => { | ||||||
|  |           return `<div class="select-item"> | ||||||
|  |             <span class="select-item-text">${sanitize(item.text)}</span> | ||||||
|  |           </div>`; | ||||||
|  |         }, | ||||||
|  |         item: (item: TomOption, sanitize: typeof escape_html) => { | ||||||
|  |           return `<span><i class="fa fa-times"></i>${sanitize(item.text)}</span>`; | ||||||
|  |         }, | ||||||
|  |         // biome-ignore lint/style/useNamingConvention: that's how it's defined | ||||||
|  |         not_loading: (data: TomOption, _sanitize: typeof escape_html) => { | ||||||
|  |           return `<div class="no-results">${interpolate(gettext("You need to type %(number)s more characters"), { number: this.minCharNumberForSearch - data.input.length }, true)}</div>`; | ||||||
|  |         }, | ||||||
|  |         // biome-ignore lint/style/useNamingConvention: that's how it's defined | ||||||
|  |         no_results: (_data: TomOption, _sanitize: typeof escape_html) => { | ||||||
|  |           return `<div class="no-results">${gettext("No results found")}</div>`; | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -76,23 +115,8 @@ class AutocompleteSelect extends inheritHtmlElement("select") { | |||||||
| @registerComponent("user-ajax-select") | @registerComponent("user-ajax-select") | ||||||
| export class UserAjaxSelect extends AutocompleteSelect { | export class UserAjaxSelect extends AutocompleteSelect { | ||||||
|   public filter?: <T>(items: T[]) => T[]; |   public filter?: <T>(items: T[]) => T[]; | ||||||
|   static observedAttributes = [ |  | ||||||
|     "min-characters-for-search", |  | ||||||
|     ...AutocompleteSelect.observedAttributes, |  | ||||||
|   ]; |  | ||||||
|  |  | ||||||
|   private minCharNumberForSearch = 2; |   protected minCharNumberForSearch = 2; | ||||||
|  |  | ||||||
|   protected attributeChangedCallback( |  | ||||||
|     name: string, |  | ||||||
|     _oldValue?: string, |  | ||||||
|     newValue?: string, |  | ||||||
|   ) { |  | ||||||
|     super.attributeChangedCallback(name, _oldValue, newValue); |  | ||||||
|     if (name === "min-characters-for-search") { |  | ||||||
|       this.minCharNumberForSearch = Number.parseInt(newValue) ?? 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected tomSelectSettings(): RecursivePartial<TomSettings> { |   protected tomSelectSettings(): RecursivePartial<TomSettings> { | ||||||
|     return { |     return { | ||||||
| @@ -103,9 +127,6 @@ export class UserAjaxSelect extends AutocompleteSelect { | |||||||
|       valueField: "id", |       valueField: "id", | ||||||
|       labelField: "display_name", |       labelField: "display_name", | ||||||
|       searchField: [], // Disable local search filter and rely on tested backend |       searchField: [], // Disable local search filter and rely on tested backend | ||||||
|       shouldLoad: (query: string) => { |  | ||||||
|         return query.length >= this.minCharNumberForSearch; // Avoid launching search with less than 2 characters |  | ||||||
|       }, |  | ||||||
|       load: (query: string, callback: TomLoadCallback) => { |       load: (query: string, callback: TomLoadCallback) => { | ||||||
|         userSearchUsers({ |         userSearchUsers({ | ||||||
|           query: { |           query: { | ||||||
| @@ -124,6 +145,7 @@ export class UserAjaxSelect extends AutocompleteSelect { | |||||||
|         }); |         }); | ||||||
|       }, |       }, | ||||||
|       render: { |       render: { | ||||||
|  |         ...super.tomSelectSettings().render, | ||||||
|         option: (item: UserProfileSchema, sanitize: typeof escape_html) => { |         option: (item: UserProfileSchema, sanitize: typeof escape_html) => { | ||||||
|           return `<div class="select-item"> |           return `<div class="select-item"> | ||||||
|             <img |             <img | ||||||
| @@ -137,14 +159,6 @@ export class UserAjaxSelect extends AutocompleteSelect { | |||||||
|         item: (item: UserProfileSchema, sanitize: typeof escape_html) => { |         item: (item: UserProfileSchema, sanitize: typeof escape_html) => { | ||||||
|           return `<span><i class="fa fa-times"></i>${sanitize(item.display_name)}</span>`; |           return `<span><i class="fa fa-times"></i>${sanitize(item.display_name)}</span>`; | ||||||
|         }, |         }, | ||||||
|         // biome-ignore lint/style/useNamingConvention: that's how it's defined |  | ||||||
|         not_loading: (data: TomOption, _sanitize: typeof escape_html) => { |  | ||||||
|           return `<div class="no-results">${interpolate(gettext("You need to type %(number)s more characters"), { number: this.minCharNumberForSearch - data.input.length }, true)}</div>`; |  | ||||||
|         }, |  | ||||||
|         // biome-ignore lint/style/useNamingConvention: that's how it's defined |  | ||||||
|         no_results: (_data: TomOption, _sanitize: typeof escape_html) => { |  | ||||||
|           return `<div class="no-results">${gettext("No results found")}</div>`; |  | ||||||
|         }, |  | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								core/templates/core/widgets/autocomplete_select.jinja
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								core/templates/core/widgets/autocomplete_select.jinja
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <script-once src="{{ statics.js }}" defer></script-once> | ||||||
|  | {% for css in statics.csss %} | ||||||
|  |   <link-once rel="stylesheet" type="text/css" href="{{ css }}" defer></link-once> | ||||||
|  | {% endfor %} | ||||||
|  |  | ||||||
|  | <autocomplete-select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %} | ||||||
|  |   <optgroup label="{{ group_name }}">{% endif %}{% for widget in group_choices %} | ||||||
|  |     {% include widget.template_name %}{% endfor %}{% if group_name %} | ||||||
|  |       </optgroup>{% endif %}{% endfor %} | ||||||
|  | </autocomplete-select> | ||||||
| @@ -36,9 +36,11 @@ from django.forms import ( | |||||||
|     CheckboxSelectMultiple, |     CheckboxSelectMultiple, | ||||||
|     DateInput, |     DateInput, | ||||||
|     DateTimeInput, |     DateTimeInput, | ||||||
|  |     SelectMultiple, | ||||||
|     Textarea, |     Textarea, | ||||||
|     TextInput, |     TextInput, | ||||||
| ) | ) | ||||||
|  | from django.forms.widgets import Select | ||||||
| from django.utils.translation import gettext | from django.utils.translation import gettext | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
| from phonenumber_field.widgets import RegionalPhoneNumberWidget | from phonenumber_field.widgets import RegionalPhoneNumberWidget | ||||||
| @@ -72,12 +74,37 @@ class MarkdownInput(Textarea): | |||||||
|         context = super().get_context(name, value, attrs) |         context = super().get_context(name, value, attrs) | ||||||
|  |  | ||||||
|         context["statics"] = { |         context["statics"] = { | ||||||
|             "js": staticfiles_storage.url("webpack/easymde-index.ts"), |             "js": staticfiles_storage.url("webpack/core/components/easymde-index.ts"), | ||||||
|             "css": staticfiles_storage.url("webpack/easymde-index.css"), |             "css": staticfiles_storage.url("webpack/core/components/easymde-index.css"), | ||||||
|         } |         } | ||||||
|         return context |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AutoCompleteSelectMixin: | ||||||
|  |     template_name = "core/widgets/autocomplete_select.jinja" | ||||||
|  |  | ||||||
|  |     def get_context(self, name, value, attrs): | ||||||
|  |         context = super().get_context(name, value, attrs) | ||||||
|  |         context["statics"] = { | ||||||
|  |             "js": staticfiles_storage.url( | ||||||
|  |                 "webpack/core/components/ajax-select-index.ts" | ||||||
|  |             ), | ||||||
|  |             "csss": [ | ||||||
|  |                 staticfiles_storage.url( | ||||||
|  |                     "webpack/core/components/ajax-select-index.css" | ||||||
|  |                 ), | ||||||
|  |                 staticfiles_storage.url("core/components/ajax-select.scss"), | ||||||
|  |             ], | ||||||
|  |         } | ||||||
|  |         return context | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AutoCompleteSelect(AutoCompleteSelectMixin, Select): ... | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AutoCompleteSelectMultiple(AutoCompleteSelectMixin, SelectMultiple): ... | ||||||
|  |  | ||||||
|  |  | ||||||
| class NFCTextInput(TextInput): | class NFCTextInput(TextInput): | ||||||
|     template_name = "core/widgets/nfc.jinja" |     template_name = "core/widgets/nfc.jinja" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| from typing import TYPE_CHECKING | from typing import TYPE_CHECKING | ||||||
|  |  | ||||||
| from ajax_select import make_ajax_field |  | ||||||
| from ajax_select.fields import AutoCompleteSelectField | from ajax_select.fields import AutoCompleteSelectField | ||||||
| from django import forms | from django import forms | ||||||
| from django.core.exceptions import PermissionDenied | from django.core.exceptions import PermissionDenied | ||||||
| @@ -13,7 +12,11 @@ from django.views.generic import DetailView, ListView | |||||||
| from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView | from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView | ||||||
|  |  | ||||||
| from core.views import CanCreateMixin, CanEditMixin, CanViewMixin | from core.views import CanCreateMixin, CanEditMixin, CanViewMixin | ||||||
| from core.views.forms import MarkdownInput, SelectDateTime | from core.views.forms import ( | ||||||
|  |     AutoCompleteSelectMultiple, | ||||||
|  |     MarkdownInput, | ||||||
|  |     SelectDateTime, | ||||||
|  | ) | ||||||
| from election.models import Candidature, Election, ElectionList, Role, Vote | from election.models import Candidature, Election, ElectionList, Role, Vote | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
| @@ -146,23 +149,12 @@ class ElectionForm(forms.ModelForm): | |||||||
|             "vote_groups", |             "vote_groups", | ||||||
|             "candidature_groups", |             "candidature_groups", | ||||||
|         ] |         ] | ||||||
|  |         widgets = { | ||||||
|     edit_groups = make_ajax_field( |             "edit_groups": AutoCompleteSelectMultiple, | ||||||
|         Election, "edit_groups", "groups", help_text="", label=_("edit groups") |             "view_groups": AutoCompleteSelectMultiple, | ||||||
|     ) |             "vote_groups": AutoCompleteSelectMultiple, | ||||||
|     view_groups = make_ajax_field( |             "candidature_groups": AutoCompleteSelectMultiple, | ||||||
|         Election, "view_groups", "groups", help_text="", label=_("view groups") |         } | ||||||
|     ) |  | ||||||
|     vote_groups = make_ajax_field( |  | ||||||
|         Election, "vote_groups", "groups", help_text="", label=_("vote groups") |  | ||||||
|     ) |  | ||||||
|     candidature_groups = make_ajax_field( |  | ||||||
|         Election, |  | ||||||
|         "candidature_groups", |  | ||||||
|         "groups", |  | ||||||
|         help_text="", |  | ||||||
|         label=_("candidature groups"), |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     start_date = forms.DateTimeField( |     start_date = forms.DateTimeField( | ||||||
|         label=_("Start date"), widget=SelectDateTime, required=True |         label=_("Start date"), widget=SelectDateTime, required=True | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user