Add AutoCompleteSelectGroup

This commit is contained in:
Antoine Bartuccio 2024-10-20 13:33:44 +02:00
parent 0af3505c2a
commit 8bbebfdb13
4 changed files with 91 additions and 20 deletions

View File

@ -9,7 +9,12 @@ import type {
TomSettings, TomSettings,
} from "tom-select/dist/types/types"; } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/dist/types/utils"; import type { escape_html } from "tom-select/dist/types/utils";
import { type UserProfileSchema, userSearchUsers } from "#openapi"; import {
type GroupSchema,
type UserProfileSchema,
groupSearchGroup,
userSearchUsers,
} from "#openapi";
@registerComponent("autocomplete-select") @registerComponent("autocomplete-select")
class AutocompleteSelect extends inheritHtmlElement("select") { class AutocompleteSelect extends inheritHtmlElement("select") {
@ -56,14 +61,9 @@ class AutocompleteSelect extends inheritHtmlElement("select") {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
// Collect all options nodes and put them into the select node for (const option of Array.from(this.children).filter(
const options: Element[] = []; // We need to make a copy to delete while iterating (child) => child.tagName.toLowerCase() === "option",
for (const child of this.children) { )) {
if (child.tagName.toLowerCase() === "option") {
options.push(child);
}
}
for (const option of options) {
this.removeChild(option); this.removeChild(option);
this.node.appendChild(option); this.node.appendChild(option);
} }
@ -158,6 +158,18 @@ abstract class AjaxSelect extends AutocompleteSelect {
}, },
}; };
} }
protected attachBehaviors() {
super.attachBehaviors();
// Gather selected options, they must be added with slots like `<slot>json</slot>`
for (const value of Array.from(this.children)
.filter((child) => child.tagName.toLowerCase() === "slot")
.map((slot) => JSON.parse(slot.innerHTML))) {
this.widget.addOption(value, true);
this.widget.addItem(value[this.valueField]);
}
}
} }
@registerComponent("user-ajax-select") @registerComponent("user-ajax-select")
@ -172,9 +184,6 @@ export class UserAjaxSelect extends AjaxSelect {
} }
return []; return [];
} }
protected tomSelectSettings(): RecursivePartial<TomSettings> {
return super.tomSelectSettings();
}
protected renderOption(item: UserProfileSchema, sanitize: typeof escape_html) { protected renderOption(item: UserProfileSchema, sanitize: typeof escape_html) {
return `<div class="select-item"> return `<div class="select-item">
@ -191,3 +200,27 @@ export class UserAjaxSelect extends AjaxSelect {
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>`;
} }
} }
@registerComponent("group-ajax-select")
export class GroupsAjaxSelect extends AjaxSelect {
protected valueField = "id";
protected labelField = "name";
protected async search(query: string): Promise<TomOption[]> {
const resp = await groupSearchGroup({ query: { search: query } });
if (resp.data) {
return resp.data.results;
}
return [];
}
protected renderOption(item: GroupSchema, sanitize: typeof escape_html) {
return `<div class="select-item">
<span class="select-item-text">${sanitize(item.name)}</span>
</div>`;
}
protected renderItem(item: GroupSchema, sanitize: typeof escape_html) {
return `<span><i class="fa fa-times"></i>${sanitize(item.name)}</span>`;
}
}

View File

@ -7,4 +7,5 @@
<optgroup label="{{ group_name }}">{% endif %}{% for widget in group_choices %} <optgroup label="{{ group_name }}">{% endif %}{% for widget in group_choices %}
{% include widget.template_name %}{% endfor %}{% if group_name %} {% include widget.template_name %}{% endfor %}{% if group_name %}
</optgroup>{% endif %}{% endfor %} </optgroup>{% endif %}{% endfor %}
{% for sel in selected %}<slot style="display: none">{{ sel }}</slot>{% endfor %}
</{{ component }}> </{{ component }}>

View File

@ -1,11 +1,27 @@
from django.contrib.staticfiles.storage import staticfiles_storage from django.contrib.staticfiles.storage import staticfiles_storage
from django.db.models import Model
from django.forms import Select, SelectMultiple from django.forms import Select, SelectMultiple
from ninja import ModelSchema
from core.models import Group, User
from core.schemas import GroupSchema, UserProfileSchema
class AutoCompleteSelectMixin: class AutoCompleteSelectMixin:
component_name = "autocomplete-select" component_name = "autocomplete-select"
template_name = "core/widgets/autocomplete_select.jinja" template_name = "core/widgets/autocomplete_select.jinja"
is_ajax = False model: Model | None = None
schema: ModelSchema | None = None
pk = "id"
def __init__(self, attrs=None, choices=()):
if self.is_ajax:
choices = () # Avoid computing anything when in ajax mode
super().__init__(attrs=attrs, choices=choices)
@property
def is_ajax(self):
return self.model and self.schema
def optgroups(self, name, value, attrs=None): def optgroups(self, name, value, attrs=None):
"""Don't create option groups when doing ajax""" """Don't create option groups when doing ajax"""
@ -27,6 +43,13 @@ class AutoCompleteSelectMixin:
staticfiles_storage.url("core/components/ajax-select.scss"), staticfiles_storage.url("core/components/ajax-select.scss"),
], ],
} }
if self.is_ajax:
context["selected"] = [
self.schema.from_orm(obj).json()
for obj in self.model.objects.filter(
**{f"{self.pk}__in": context["widget"]["value"]}
).all()
]
return context return context
@ -38,9 +61,23 @@ class AutoCompleteSelectMultiple(AutoCompleteSelectMixin, SelectMultiple): ...
class AutoCompleteSelectUser(AutoCompleteSelectMixin, Select): class AutoCompleteSelectUser(AutoCompleteSelectMixin, Select):
component_name = "user-ajax-select" component_name = "user-ajax-select"
is_ajax = True model = User
schema = UserProfileSchema
class AutoCompleteSelectMultipleUser(AutoCompleteSelectMixin, SelectMultiple): class AutoCompleteSelectMultipleUser(AutoCompleteSelectMixin, SelectMultiple):
component_name = "user-ajax-select" component_name = "user-ajax-select"
is_ajax = True model = User
schema = UserProfileSchema
class AutoCompleteSelectGroup(AutoCompleteSelectMixin, Select):
component_name = "group-ajax-select"
model = Group
schema = GroupSchema
class AutoCompleteSelectMultipleGroup(AutoCompleteSelectMixin, SelectMultiple):
component_name = "group-ajax-select"
model = Group
schema = GroupSchema

View File

@ -15,7 +15,7 @@ from core.views.forms import SelectDateTime
from core.views.widgets.markdown import MarkdownInput from core.views.widgets.markdown import MarkdownInput
from core.views.widgets.select import ( from core.views.widgets.select import (
AutoCompleteSelect, AutoCompleteSelect,
AutoCompleteSelectMultiple, AutoCompleteSelectMultipleGroup,
AutoCompleteSelectUser, AutoCompleteSelectUser,
) )
from election.models import Candidature, Election, ElectionList, Role, Vote from election.models import Candidature, Election, ElectionList, Role, Vote
@ -157,10 +157,10 @@ class ElectionForm(forms.ModelForm):
"candidature_groups", "candidature_groups",
] ]
widgets = { widgets = {
"edit_groups": AutoCompleteSelectMultiple, "edit_groups": AutoCompleteSelectMultipleGroup,
"view_groups": AutoCompleteSelectMultiple, "view_groups": AutoCompleteSelectMultipleGroup,
"vote_groups": AutoCompleteSelectMultiple, "vote_groups": AutoCompleteSelectMultipleGroup,
"candidature_groups": AutoCompleteSelectMultiple, "candidature_groups": AutoCompleteSelectMultipleGroup,
} }
start_date = forms.DateTimeField( start_date = forms.DateTimeField(