django-unfold 0.34.0__py3-none-any.whl → 0.35.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-unfold
3
- Version: 0.34.0
3
+ Version: 0.35.0
4
4
  Summary: Modern Django admin theme for seamless interface development
5
5
  Home-page: https://unfoldadmin.com
6
6
  License: MIT
@@ -179,7 +179,7 @@ class CustomAdminClass(ModelAdmin):
179
179
  from django.contrib import admin
180
180
  from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
181
181
  from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
182
- from django.contrib.auth.models import User
182
+ from django.contrib.auth.models import User, Group
183
183
 
184
184
  from unfold.admin import ModelAdmin
185
185
 
@@ -360,6 +360,9 @@ class CustomAdminClass(ModelAdmin):
360
360
  # Display fields in changeform in compressed mode
361
361
  compressed_fields = True # Default: False
362
362
 
363
+ # Warn before leaving unsaved changes in changeform
364
+ warn_unsaved_form = True # Default: False
365
+
363
366
  # Preprocess content of readonly fields before render
364
367
  readonly_preprocess_fields = {
365
368
  "model_field_name": "html.unescape",
@@ -645,7 +648,14 @@ The difference between them is that `ChoicesDropdownFilter` will collect a list
645
648
  from django.contrib import admin
646
649
  from django.contrib.auth.models import User
647
650
  from unfold.admin import ModelAdmin
648
- from unfold.contrib.filters.admin import ChoicesDropdownFilter, RelatedDropdownFilter, DropdownFilter
651
+ from unfold.contrib.filters.admin import (
652
+ ChoicesDropdownFilter,
653
+ MultipleChoicesDropdownFilter,
654
+ RelatedDropdownFilter,
655
+ MultipleRelatedDropdownFilter,
656
+ DropdownFilter,
657
+ MultipleDropdownFilter
658
+ )
649
659
 
650
660
 
651
661
  class CustomDropdownFilter(DropdownFilter):
@@ -672,7 +682,9 @@ class MyAdmin(ModelAdmin):
672
682
  list_filter = [
673
683
  CustomDropdownFilter,
674
684
  ("modelfield_with_choices", ChoicesDropdownFilter),
685
+ ("modelfield_with_choices_multiple", MultipleChoicesDropdownFilter),
675
686
  ("modelfield_with_foreign_key", RelatedDropdownFilter)
687
+ ("modelfield_with_foreign_key_multiple", MultipleRelatedDropdownFilter)
676
688
  ]
677
689
  ```
678
690
 
@@ -1363,8 +1375,8 @@ def dashboard_callback(request: HttpRequest) -> Dict:
1363
1375
  ```
1364
1376
 
1365
1377
  ```django-html
1366
- {% component "unfold/components/card" with title="Card title" %}
1367
- {% component "unfold/components/table.html" with table=table_data card_included=1 striped=1 %}{% encomponent %}
1378
+ {% component "unfold/components/card.html" with title="Card title" %}
1379
+ {% component "unfold/components/table.html" with table=table_data card_included=1 striped=1 %}{% endcomponent %}
1368
1380
  {% endcomponent %}
1369
1381
  ```
1370
1382
 
@@ -1,12 +1,12 @@
1
1
  unfold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- unfold/admin.py,sha256=3yBehQeJIt9JbNJMhBdnGFyovYmTr-JPFBpoCiUWpeE,19404
2
+ unfold/admin.py,sha256=nUAouLLyo57CE3EpOREHDMjOaj68P7MZO6KEPYlutYU,19587
3
3
  unfold/apps.py,sha256=SlBXPYrUd2uXn67qFbRvbXSUk3XFWrF4-5WELgDCvho,381
4
4
  unfold/checks.py,sha256=Smgji9w19hnYjJElJ_FJnnyTEAE-E-OUB6otHu7lasY,1670
5
5
  unfold/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  unfold/contrib/filters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- unfold/contrib/filters/admin.py,sha256=hkrw-CthPsSubwsDpInqOiNw5vsl_zbQeYW23DnKSsY,20068
7
+ unfold/contrib/filters/admin.py,sha256=4dwK-LCXaopQeqW3818wUiZJhkJEDLNwbDLJboVJPHk,21138
8
8
  unfold/contrib/filters/apps.py,sha256=wEySJy0gMLzFLb9XNKE-RexiO05X7NaQ5QmxZyziJ_k,136
9
- unfold/contrib/filters/forms.py,sha256=cdBFNB45PPgKVzAbjrwbZVdYF6rZBUQPxxWMwZaKCpM,4736
9
+ unfold/contrib/filters/forms.py,sha256=N25YiD7YfBxg57-goSnaXc-eyEQFp1XYgDmmQwg7jQ0,5696
10
10
  unfold/contrib/filters/static/unfold/filters/css/nouislider.min.css,sha256=rddL_jOGGVEY6wR-aw0VYovAfz5fPeAIsulrlSNb1hc,4221
11
11
  unfold/contrib/filters/static/unfold/filters/js/DateTimeShortcuts.js,sha256=jgFNBDf6aHvUlyv0LEDHggXO-xA8pWOCWFWcVupdA30,19332
12
12
  unfold/contrib/filters/static/unfold/filters/js/admin-numeric-filter.js,sha256=nTkiiJk4Abn9d6KigxPSEsereFhFq-v5n_boiiY1eII,1668
@@ -65,7 +65,7 @@ unfold/contrib/simple_history/templates/simple_history/object_history_form.html,
65
65
  unfold/contrib/simple_history/templates/simple_history/object_history_list.html,sha256=f0RxTiPfC957uQzNfSkKoyMUzu8LRw4-MS6d3xZ2gew,6821
66
66
  unfold/contrib/simple_history/templates/simple_history/submit_line.html,sha256=9v7PfS8M6N0NccTEb_9QyTG28NgDBiIkjuBOfWGIQ18,1703
67
67
  unfold/dataclasses.py,sha256=4PAWLVlUhyGMq1Mwai7afCgrRO0Inulw0opFUKVEOBk,483
68
- unfold/decorators.py,sha256=6E4vPVwK0IQDAiDPg9pgyypRqciX_gR0jwITDcrSc8U,3367
68
+ unfold/decorators.py,sha256=jyjE2sunazp8gK1QIsNRJGEEDwO7RDKWDKnOI3GDWZE,3338
69
69
  unfold/exceptions.py,sha256=gcCj1ox61E137bk_0Cqy4YC3SttdPgB-fiJUqpmyHSE,43
70
70
  unfold/fields.py,sha256=yhtpDfqycpOxqdQlbncCg9qhELxGk3AtXizkZyzzH0g,7410
71
71
  unfold/forms.py,sha256=8-IoFZLwUNJKTNdev_oJwdxm-_xekvrn_84IXKse4Q8,4100
@@ -83,9 +83,10 @@ unfold/static/unfold/fonts/material-symbols/styles.css,sha256=U0oiGd2DS7LXnbuqgs
83
83
  unfold/static/unfold/js/alpine.anchor.js,sha256=2Jg9aUq749pjFynzr_H1NK3lf-nXrbHMtO9wvlWJyIQ,15524
84
84
  unfold/static/unfold/js/alpine.js,sha256=NY2a-7GrW--i9IBhowd25bzXcH9BCmBrqYX5i8OxwDQ,44659
85
85
  unfold/static/unfold/js/alpine.persist.js,sha256=jFBwr6faTqqhp3sVi4_VTxJ0FpaF9YGZN1ZGLl_5QYM,837
86
- unfold/static/unfold/js/app.js,sha256=2-56Kc20KH69Prjcj6ezURAKID3U8V2mHgbhPjeH6HA,6314
86
+ unfold/static/unfold/js/app.js,sha256=kL7gJu4bDcY7v61KYtYtjn7KxWd-rHVvL49GTD_q770,6884
87
87
  unfold/static/unfold/js/chart.js,sha256=22W6cFERR-CElMOKRgMMicueMVP0Vf7FBEBYH8Z8tCk,200633
88
88
  unfold/static/unfold/js/htmx.js,sha256=XOLqvnZiyEx46EW9vaJTBUaaWg8CGVVfXJkVsUmJbpI,42820
89
+ unfold/static/unfold/js/select2.init.js,sha256=SzpjSL1mD9tHedjSeXlmEwHjuuyqlnpilpSeEEKeYLE,265
89
90
  unfold/static/unfold/js/simplebar.js,sha256=t-uG1FAD6ZoiMeN--wac0XRS7SxoDVG6zvRnGuEp7X8,27176
90
91
  unfold/styles.css,sha256=egNdWhCzaGS5Y_tCiceIU6BTNn1MkrMMooX5zDv_r6I,18223
91
92
  unfold/templates/admin/actions.html,sha256=8GqELAxzywGyfQJiQeuhGIQUakdK_WeneILY06C7mEo,2746
@@ -95,7 +96,7 @@ unfold/templates/admin/auth/user/add_form.html,sha256=iLig-vd2YExXsj0xGBwYhZ4kGU
95
96
  unfold/templates/admin/auth/user/change_password.html,sha256=-Wa9ml3yss-kDz0YQxCiwoxs91KQD8eetCt5l6xekWM,2892
96
97
  unfold/templates/admin/base.html,sha256=RptiEpgXZ6seYrPu-1Stoe8ECHpi9PjkJjFheGecYF0,2358
97
98
  unfold/templates/admin/base_site.html,sha256=3ckWrcAdd7Pw1hk6Zwyknab_Qb-rteV9-mXhMnfo6VI,361
98
- unfold/templates/admin/change_form.html,sha256=oWK5wr0qv6QMJrFQ9Veacozg_CN1DwmBqbzPOuDtiA4,4443
99
+ unfold/templates/admin/change_form.html,sha256=FiVfbB1j1wb6qGfaLukdMUrLRXsO_sJ4wirKq1bit60,4528
99
100
  unfold/templates/admin/change_form_object_tools.html,sha256=eyeH-i2HgEM0Yi-OJA2D1VnKJyC19A_my1IDGxxoP8Y,593
100
101
  unfold/templates/admin/change_list.html,sha256=o8XnKa1xFot53-BxGeB8afJ0TF8N7TcJhyTA4vPnop0,5124
101
102
  unfold/templates/admin/change_list_object_tools.html,sha256=cmMiT2nT20Ph5yfpj9aHPr76Z-JP4aSXp0o-Rnad28s,147
@@ -114,7 +115,7 @@ unfold/templates/admin/nav_sidebar.html,sha256=cKuC28XU9NT17lrCVugn2cHf39kgkoX49
114
115
  unfold/templates/admin/object_history.html,sha256=mRD6nbIVmnsz2OG6UsuXdI55bflY3Vs8zbIE9X-Oi74,4816
115
116
  unfold/templates/admin/pagination.html,sha256=KrwOM6gRizfpTMP59TESQaNGcQzkga2k02opl_7BxLo,1130
116
117
  unfold/templates/admin/search_form.html,sha256=vKuruVUEMarx8FPrBp86yqqFtUXfFPzR_9GxhnOCs4U,1173
117
- unfold/templates/admin/submit_line.html,sha256=JpD8HhRxtEIlWsGvpUGbLeKYJd_X7Rm_IBzFIcgeq6M,4222
118
+ unfold/templates/admin/submit_line.html,sha256=6V9qtE9BecN905-YkI5ZOLY3ZYyb_mhN_p9Zkhv4h8Q,4248
118
119
  unfold/templates/auth/widgets/read_only_password_hash.html,sha256=I08wFPcDr0Gjrg64nlcKiB12Tz2g_nnJXemiy8tkoZg,785
119
120
  unfold/templates/registration/logged_out.html,sha256=4rjAazcxt8ZCqA5bConKRgAk4VrljNTksdtg7D_btrU,1028
120
121
  unfold/templates/registration/password_change_done.html,sha256=i1ZzfTwZHWNWoN9_xHZDdcgLdTOVbTFFD1HUSuG0LkY,1062
@@ -194,8 +195,8 @@ unfold/templatetags/unfold_list.py,sha256=q02u7jbMWyDi_6_rOk17W-jW6Yqe9dkuz7Cc_m
194
195
  unfold/typing.py,sha256=1P8PWM2oeaceUJtA5j071RbKEBpHYaux441u7Hd6wv4,643
195
196
  unfold/utils.py,sha256=zZdJE4FmwRd7p5a7sJiAoZjBOJitXJduOq7BulyppWM,4803
196
197
  unfold/views.py,sha256=hQCyeeMa9kcJV1IZeeYqj8PGW7J4QWME8n-5n0UGmiU,1003
197
- unfold/widgets.py,sha256=p95xr5NoG_CI10fSZUYZ37w3vqqHjSpWPOLAo4Z6CDM,16049
198
- django_unfold-0.34.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
199
- django_unfold-0.34.0.dist-info/METADATA,sha256=H7CNN1F46aqwHb5_aie9z1pcpZOdjtmfJD4EqAK1FTQ,57500
200
- django_unfold-0.34.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
201
- django_unfold-0.34.0.dist-info/RECORD,,
198
+ unfold/widgets.py,sha256=6hQp13LmuhDJ41YxTG70SPJEg0agmHcmUhCtCUwI6SU,16369
199
+ django_unfold-0.35.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
200
+ django_unfold-0.35.0.dist-info/METADATA,sha256=_lkCvpMErEFx3xEWaKWziYEDzsvy4jxiLlfVkJav5u0,57888
201
+ django_unfold-0.35.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
202
+ django_unfold-0.35.0.dist-info/RECORD,,
unfold/admin.py CHANGED
@@ -236,6 +236,7 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
236
236
  list_disable_select_all = False
237
237
  compressed_fields = False
238
238
  readonly_preprocess_fields = {}
239
+ warn_unsaved_form = False
239
240
  checks_class = UnfoldModelAdminChecks
240
241
 
241
242
  @property
@@ -244,13 +245,14 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
244
245
  additional_media = forms.Media()
245
246
 
246
247
  for filter in self.list_filter:
247
- if not isinstance(filter, (tuple, list)):
248
- continue
249
-
250
- if hasattr(filter[1], "form_class") and hasattr(
251
- filter[1].form_class, "Media"
248
+ if (
249
+ isinstance(filter, (tuple, list))
250
+ and hasattr(filter[1], "form_class")
251
+ and hasattr(filter[1].form_class, "Media")
252
252
  ):
253
253
  additional_media += forms.Media(filter[1].form_class.Media)
254
+ elif hasattr(filter, "form_class") and hasattr(filter.form_class, "Media"):
255
+ additional_media += forms.Media(filter.form_class.Media)
254
256
 
255
257
  return media + additional_media
256
258
 
@@ -41,12 +41,23 @@ class ValueMixin:
41
41
  )
42
42
 
43
43
 
44
+ class MultiValueMixin:
45
+ def value(self) -> Optional[List[str]]:
46
+ return (
47
+ self.lookup_val
48
+ if self.lookup_val not in EMPTY_VALUES
49
+ and isinstance(self.lookup_val, List)
50
+ and len(self.lookup_val) > 0
51
+ else self.lookup_val
52
+ )
53
+
54
+
44
55
  class DropdownMixin:
45
56
  template = "unfold/filters/filters_field.html"
46
57
  form_class = DropdownForm
47
58
  all_option = ["", _("All")]
48
59
 
49
- def queryset(self, request, queryset) -> QuerySet:
60
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
50
61
  if self.value() not in EMPTY_VALUES:
51
62
  return super().queryset(request, queryset)
52
63
 
@@ -112,11 +123,23 @@ class DropdownFilter(admin.SimpleListFilter):
112
123
  name=self.parameter_name,
113
124
  choices=[self.all_option, *self.lookup_choices],
114
125
  data={self.parameter_name: self.value()},
126
+ multiple=self.multiple if hasattr(self, "multiple") else False,
115
127
  ),
116
128
  },
117
129
  )
118
130
 
119
131
 
132
+ class MultipleDropdownFilter(DropdownFilter):
133
+ multiple = True
134
+
135
+ def __init__(self, request, params, model, model_admin):
136
+ self.request = request
137
+ super().__init__(request, params, model, model_admin)
138
+
139
+ def value(self):
140
+ return self.request.GET.getlist(self.parameter_name)
141
+
142
+
120
143
  class ChoicesDropdownFilter(ValueMixin, DropdownMixin, admin.ChoicesFieldListFilter):
121
144
  def choices(self, changelist: ChangeList):
122
145
  choices = [self.all_option, *self.field.flatchoices]
@@ -127,10 +150,15 @@ class ChoicesDropdownFilter(ValueMixin, DropdownMixin, admin.ChoicesFieldListFil
127
150
  name=self.lookup_kwarg,
128
151
  choices=choices,
129
152
  data={self.lookup_kwarg: self.value()},
153
+ multiple=self.multiple if hasattr(self, "multiple") else False,
130
154
  ),
131
155
  }
132
156
 
133
157
 
158
+ class MultipleChoicesDropdownFilter(MultiValueMixin, ChoicesDropdownFilter):
159
+ multiple = True
160
+
161
+
134
162
  class RelatedDropdownFilter(ValueMixin, DropdownMixin, admin.RelatedFieldListFilter):
135
163
  def choices(self, changelist: ChangeList):
136
164
  yield {
@@ -139,10 +167,15 @@ class RelatedDropdownFilter(ValueMixin, DropdownMixin, admin.RelatedFieldListFil
139
167
  name=self.lookup_kwarg,
140
168
  choices=[self.all_option, *self.lookup_choices],
141
169
  data={self.lookup_kwarg: self.value()},
170
+ multiple=self.multiple if hasattr(self, "multiple") else False,
142
171
  ),
143
172
  }
144
173
 
145
174
 
175
+ class MultipleRelatedDropdownFilter(MultiValueMixin, RelatedDropdownFilter):
176
+ multiple = True
177
+
178
+
146
179
  class SingleNumericFilter(admin.FieldListFilter):
147
180
  request = None
148
181
  parameter_name = None
@@ -1,8 +1,10 @@
1
1
  from django import forms
2
+ from django.forms import ChoiceField, MultipleChoiceField
2
3
  from django.utils.translation import gettext_lazy as _
3
4
 
4
5
  from ...widgets import (
5
6
  INPUT_CLASSES,
7
+ UnfoldAdminSelectMultipleWidget,
6
8
  UnfoldAdminSelectWidget,
7
9
  UnfoldAdminSplitDateTimeVerticalWidget,
8
10
  UnfoldAdminTextInputWidget,
@@ -21,15 +23,46 @@ class SearchForm(forms.Form):
21
23
 
22
24
 
23
25
  class DropdownForm(forms.Form):
24
- def __init__(self, name, label, choices, *args, **kwargs):
26
+ widget = UnfoldAdminSelectWidget(
27
+ attrs={
28
+ "data-theme": "admin-autocomplete",
29
+ "class": "admin-autocomplete",
30
+ }
31
+ )
32
+ field = ChoiceField
33
+
34
+ def __init__(self, name, label, choices, multiple=False, *args, **kwargs):
25
35
  super().__init__(*args, **kwargs)
26
36
 
27
- self.fields[name] = forms.ChoiceField(
37
+ if multiple:
38
+ self.widget = UnfoldAdminSelectMultipleWidget(
39
+ attrs={
40
+ "data-theme": "admin-autocomplete",
41
+ "class": "admin-autocomplete",
42
+ }
43
+ )
44
+ self.field = MultipleChoiceField
45
+
46
+ self.fields[name] = self.field(
28
47
  label=label,
29
48
  required=False,
30
49
  choices=choices,
31
- widget=UnfoldAdminSelectWidget,
50
+ widget=self.widget,
51
+ )
52
+
53
+ class Media:
54
+ js = (
55
+ "admin/js/vendor/jquery/jquery.js",
56
+ "admin/js/vendor/select2/select2.full.js",
57
+ "admin/js/jquery.init.js",
58
+ "unfold/js/select2.init.js",
32
59
  )
60
+ css = {
61
+ "screen": (
62
+ "admin/css/vendor/select2/select2.css",
63
+ "admin/css/autocomplete.css",
64
+ ),
65
+ }
33
66
 
34
67
 
35
68
  class SingleNumericForm(forms.Form):
unfold/decorators.py CHANGED
@@ -29,12 +29,12 @@ def action(
29
29
  getattr(model_admin, f"has_{permission}_permission")
30
30
  for permission in permissions
31
31
  )
32
- # TODO add obj parameter into has_permission method call.
33
32
  # Permissions methods have following syntax: has_<some>_permission(self, request, obj=None):
34
33
  # But obj is not examined by default in django admin and it would also require additional
35
34
  # fetch from database, therefore it is not supported yet
36
35
  if not any(
37
- has_permission(request) for has_permission in permission_checks
36
+ has_permission(request, kwargs.get("object_id"))
37
+ for has_permission in permission_checks
38
38
  ):
39
39
  raise PermissionDenied
40
40
  return func(model_admin, request, *args, **kwargs)
@@ -8,8 +8,31 @@ window.addEventListener("load", (e) => {
8
8
  renderCharts();
9
9
 
10
10
  filterForm();
11
+
12
+ warnWithoutSaving();
11
13
  });
12
14
 
15
+ /*************************************************************
16
+ * Warn without saving
17
+ *************************************************************/
18
+ const warnWithoutSaving = () => {
19
+ let formChanged = false;
20
+
21
+ Array.from(
22
+ document.querySelectorAll(
23
+ "form.warn-unsaved-form input, form.warn-unsaved-form select"
24
+ )
25
+ ).forEach((field) => {
26
+ field.addEventListener("change", (e) => (formChanged = true));
27
+ });
28
+
29
+ window.addEventListener("beforeunload", (e) => {
30
+ if (formChanged) {
31
+ e.preventDefault();
32
+ }
33
+ });
34
+ };
35
+
13
36
  /*************************************************************
14
37
  * Filter form
15
38
  *************************************************************/
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ {
3
+ const $ = django.jQuery;
4
+
5
+ $.fn.djangoCustomSelect2 = function () {
6
+ $.each(this, function (i, element) {
7
+ $(element).select2();
8
+ });
9
+ return this;
10
+ };
11
+
12
+ $(function () {
13
+ $(".admin-autocomplete").djangoCustomSelect2();
14
+ });
15
+ }
@@ -59,7 +59,7 @@
59
59
 
60
60
  {% block content %}
61
61
  <div id="content-main">
62
- <form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" novalidate>
62
+ <form {% if has_file_field %}enctype="multipart/form-data" {% endif %}{% if form_url %}action="{{ form_url }}" {% endif %}method="post" id="{{ opts.model_name }}_form" {% if adminform.model_admin.warn_unsaved_form %}class="warn-unsaved-form"{% endif %} novalidate>
63
63
  {% csrf_token %}
64
64
 
65
65
  {% block form_top %}{% endblock %}
@@ -42,7 +42,7 @@
42
42
  {% endif %}
43
43
 
44
44
  {% if show_save_as_new %}
45
- <button type="submit" name="_saveasnew" class="border font-medium px-3 py-2 rounded-md transition-all w-full hover:bg-gray-50 dark:border-gray-700 dark:hover:text-gray-200 dark:hover:bg-gray-900">
45
+ <button type="submit" name="_saveasnew" class="border font-medium hidden px-3 py-2 rounded-md transition-all w-full hover:bg-gray-50 lg:block lg:w-auto dark:border-gray-700 dark:hover:text-gray-200 dark:hover:bg-gray-900">
46
46
  {% translate 'Save as new' %}
47
47
  </button>
48
48
  {% endif %}
unfold/widgets.py CHANGED
@@ -25,6 +25,7 @@ from django.forms import (
25
25
  NumberInput,
26
26
  PasswordInput,
27
27
  Select,
28
+ SelectMultiple,
28
29
  )
29
30
  from django.utils.translation import gettext_lazy as _
30
31
 
@@ -480,7 +481,16 @@ class UnfoldAdminSelectWidget(Select):
480
481
  if attrs is None:
481
482
  attrs = {}
482
483
 
483
- attrs["class"] = " ".join(SELECT_CLASSES)
484
+ attrs["class"] = " ".join([*SELECT_CLASSES, attrs.get("class", "")])
485
+ super().__init__(attrs, choices)
486
+
487
+
488
+ class UnfoldAdminSelectMultipleWidget(SelectMultiple):
489
+ def __init__(self, attrs=None, choices=()):
490
+ if attrs is None:
491
+ attrs = {}
492
+
493
+ attrs["class"] = " ".join([*SELECT_CLASSES, attrs.get("class", "")])
484
494
  super().__init__(attrs, choices)
485
495
 
486
496