django-unfold 0.47.0__py3-none-any.whl → 0.49.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.
- {django_unfold-0.47.0.dist-info → django_unfold-0.49.0.dist-info}/METADATA +1 -1
- {django_unfold-0.47.0.dist-info → django_unfold-0.49.0.dist-info}/RECORD +37 -31
- unfold/admin.py +17 -412
- unfold/dataclasses.py +8 -0
- unfold/decorators.py +14 -1
- unfold/fields.py +6 -5
- unfold/mixins/__init__.py +4 -0
- unfold/mixins/action_model_admin.py +329 -0
- unfold/mixins/base_model_admin.py +110 -0
- unfold/overrides.py +73 -0
- unfold/settings.py +2 -0
- unfold/sites.py +25 -5
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/fonts/inter/Inter-Bold.woff2 +0 -0
- unfold/static/unfold/fonts/inter/Inter-Medium.woff2 +0 -0
- unfold/static/unfold/fonts/inter/Inter-Regular.woff2 +0 -0
- unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2 +0 -0
- unfold/templates/admin/app_index.html +1 -5
- unfold/templates/admin/base_site.html +1 -1
- unfold/templates/admin/index.html +1 -5
- unfold/templates/admin/login.html +1 -1
- unfold/templates/admin/search_form.html +4 -2
- unfold/templates/admin/submit_line.html +7 -1
- unfold/templates/unfold/helpers/account_links.html +1 -1
- unfold/templates/unfold/helpers/actions_row.html +14 -4
- unfold/templates/unfold/helpers/language_switch.html +1 -1
- unfold/templates/unfold/helpers/navigation_header.html +18 -5
- unfold/templates/unfold/helpers/site_branding.html +9 -0
- unfold/templates/unfold/helpers/site_dropdown.html +19 -0
- unfold/templates/unfold/helpers/site_icon.html +20 -10
- unfold/templates/unfold/helpers/tab_action.html +35 -2
- unfold/templates/unfold/helpers/theme_switch.html +1 -1
- unfold/templates/unfold/layouts/base.html +1 -5
- unfold/typing.py +2 -1
- unfold/widgets.py +2 -0
- {django_unfold-0.47.0.dist-info → django_unfold-0.49.0.dist-info}/LICENSE.md +0 -0
- {django_unfold-0.47.0.dist-info → django_unfold-0.49.0.dist-info}/WHEEL +0 -0
@@ -1,5 +1,5 @@
|
|
1
1
|
unfold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
unfold/admin.py,sha256=
|
2
|
+
unfold/admin.py,sha256=JOT4AbbCcGKDi_Qwi_yuFngkLbo78SHd-aHiHNLaI8M,6093
|
3
3
|
unfold/apps.py,sha256=SlBXPYrUd2uXn67qFbRvbXSUk3XFWrF4-5WELgDCvho,381
|
4
4
|
unfold/checks.py,sha256=8I3i4xR_KgyJdpQyZUZzKNeyYf-sNzg6PAlsREuMfgI,1664
|
5
5
|
unfold/components.py,sha256=vqkQzseYUvLXDohmTVAlbKopALjyX4WA9yglvdfhqu4,1283
|
@@ -72,21 +72,25 @@ unfold/contrib/simple_history/templates/simple_history/object_history.html,sha25
|
|
72
72
|
unfold/contrib/simple_history/templates/simple_history/object_history_form.html,sha256=JKCe-QTBZMB2ol5LPiX3gNuNxFpi_rqlXC9X92PFBPE,2296
|
73
73
|
unfold/contrib/simple_history/templates/simple_history/object_history_list.html,sha256=ls_xEAAgGzbu0IXdjZk7scEyEJx0S7i3Khr9ND98gzY,6842
|
74
74
|
unfold/contrib/simple_history/templates/simple_history/submit_line.html,sha256=y8BleGHPNztA_R37rTB-zjJ68uDbE8flYSvMD0Jfl5Y,1734
|
75
|
-
unfold/dataclasses.py,sha256=
|
76
|
-
unfold/decorators.py,sha256=
|
75
|
+
unfold/dataclasses.py,sha256=p3sqaIRfmGYEMq_-cSQIRYdsTeuqgiJ4ge_neQQrpd4,618
|
76
|
+
unfold/decorators.py,sha256=xofDxfJhVSmeyTh3fXiCXZtUKliuJOlxCQVQg3f1Fss,3740
|
77
77
|
unfold/exceptions.py,sha256=gcCj1ox61E137bk_0Cqy4YC3SttdPgB-fiJUqpmyHSE,43
|
78
|
-
unfold/fields.py,sha256=
|
78
|
+
unfold/fields.py,sha256=J3goIMlP_XOmQ85Xgh2v5MylXSP1o5I7o9_So2FQn2k,7461
|
79
79
|
unfold/forms.py,sha256=BKv7eCbv29eCIuf1d_ZBmD4-_OIIzRaopiFd2nKbXNY,4520
|
80
|
-
unfold/
|
81
|
-
unfold/
|
80
|
+
unfold/mixins/__init__.py,sha256=YlhoDs9Y_DXQ8Ejr3enxuHMS1-Ij9fwukxNwpeOS4Co,190
|
81
|
+
unfold/mixins/action_model_admin.py,sha256=Km-_Tz17EKEgPr8ZvDT21KCx0JCOb_iWEPdxux2K54Y,11717
|
82
|
+
unfold/mixins/base_model_admin.py,sha256=PR4eqU48yB4CNy9Bjd7fjxbOFiFpZM4etYmBOSaRX_M,4140
|
83
|
+
unfold/overrides.py,sha256=ERVY9boZiXgjDYcTEZcIp7KWoeBZ45VM_zN31yAOjyQ,2922
|
84
|
+
unfold/settings.py,sha256=5gs0iewBoUUrsF9LHHqcGOhW1HnyfZWY_zXuATjJOzE,2671
|
85
|
+
unfold/sites.py,sha256=iwpJ9Qykxe0q4-alGE_07bJdBSpmIeTDxZil-C_vWSc,16054
|
82
86
|
unfold/static/admin/js/admin/RelatedObjectLookups.js,sha256=alI0-yq7YPDJJJn-yg1ce79-Cv88yQDUrfaGqFZnsaY,9048
|
83
87
|
unfold/static/admin/js/inlines.js,sha256=gxmQEUlJ9yasELjz9qpluogFZB-bD1KJfHs4PI7UjsA,14687
|
84
88
|
unfold/static/unfold/css/simplebar.css,sha256=5LLaEM11pKi6JFCOLt4XKuZxTpT9rpdq_tNlaQytFlU,4647
|
85
|
-
unfold/static/unfold/css/styles.css,sha256=
|
86
|
-
unfold/static/unfold/fonts/inter/Inter-Bold.woff2,sha256
|
87
|
-
unfold/static/unfold/fonts/inter/Inter-Medium.woff2,sha256=
|
88
|
-
unfold/static/unfold/fonts/inter/Inter-Regular.woff2,sha256=
|
89
|
-
unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2,sha256=
|
89
|
+
unfold/static/unfold/css/styles.css,sha256=ORq4t5sGDPl16B-05v-3ahusgoLVu9r6pH2kiYuNbPY,158172
|
90
|
+
unfold/static/unfold/fonts/inter/Inter-Bold.woff2,sha256=-oiBJ7baAVtlVp8DUfO1w5GtkokElR8cIOn4RiqNleo,114840
|
91
|
+
unfold/static/unfold/fonts/inter/Inter-Medium.woff2,sha256=D_PpRhThST61VjFP0keubEqFp3g7TMhr5TmUDPg_Kkg,114348
|
92
|
+
unfold/static/unfold/fonts/inter/Inter-Regular.woff2,sha256=4G9rG8VTqupORmgCPtCrChRxKcMQf1Ebx9A9NhsK4IU,111268
|
93
|
+
unfold/static/unfold/fonts/inter/Inter-SemiBold.woff2,sha256=XLcQPk5gWYmv68A9mJx5IB5UshtRg9szmB9w25F4owE,114812
|
90
94
|
unfold/static/unfold/fonts/inter/styles.css,sha256=QqdgevXVfcD6vZGo8ciSkaEG_62_YcZg0hvHTNgJ56E,608
|
91
95
|
unfold/static/unfold/fonts/material-symbols/Material-Symbols-Outlined.woff2,sha256=V6mmjk69Ee-coQvMsGV0tcNgV3E1nb7N9IKW8HVUBzk,256424
|
92
96
|
unfold/static/unfold/fonts/material-symbols/styles.css,sha256=ZX1icA_BXCVL0eoY2H6XR8VT_Fxl_KnV6vFBm_OImHA,535
|
@@ -102,12 +106,12 @@ unfold/static/unfold/js/select2.init.js,sha256=siSDCwzy2PsOwA0Kj26JA_ELBtZlS70O8
|
|
102
106
|
unfold/static/unfold/js/simplebar.js,sha256=t-uG1FAD6ZoiMeN--wac0XRS7SxoDVG6zvRnGuEp7X8,27176
|
103
107
|
unfold/styles.css,sha256=XrdDgMYgyCw77ieNcxhHcwhkRLBvQCa-5PBf66JLZvU,18796
|
104
108
|
unfold/templates/admin/actions.html,sha256=aroIid16mORcx-0p8O2dOoWupEoTGX66pt-PAXjSYZ0,3063
|
105
|
-
unfold/templates/admin/app_index.html,sha256=
|
109
|
+
unfold/templates/admin/app_index.html,sha256=GSBry97gDcK3scTP-Fs4FXcSItRxxE87Wv4sDtMTlbc,1244
|
106
110
|
unfold/templates/admin/app_list.html,sha256=VtntgHpbZdXbt_HXBLAtbuMdz1alS7F_S7eG37M5Rig,2855
|
107
111
|
unfold/templates/admin/auth/user/add_form.html,sha256=kZDtUyhrvCIH9rlAq0iXLKYt97j1a-GsrWZQCQDASYM,479
|
108
112
|
unfold/templates/admin/auth/user/change_password.html,sha256=OxpsKnVQm5lDrK5tGH0EAPFCPs288m0Jcy5WNYsdBXw,2896
|
109
113
|
unfold/templates/admin/base.html,sha256=3xQmoD_7cu2UlgEdvnt7v_CibT9t1Iul0b-VU2Awu_U,2395
|
110
|
-
unfold/templates/admin/base_site.html,sha256=
|
114
|
+
unfold/templates/admin/base_site.html,sha256=UOsecaCycpyn_ygJNIKO7MQtDFJuHv--r83-mzaVY30,300
|
111
115
|
unfold/templates/admin/change_form.html,sha256=LfeAeLYpJvNhOMJWQFeb9oSrAznlB4pnFrqTVMCIy4o,5340
|
112
116
|
unfold/templates/admin/change_form_object_tools.html,sha256=eyeH-i2HgEM0Yi-OJA2D1VnKJyC19A_my1IDGxxoP8Y,593
|
113
117
|
unfold/templates/admin/change_list.html,sha256=V2DgxlGaQvcOS1qAxe4no1EJCq-9nIOa10kYLkcE2Ag,5008
|
@@ -121,13 +125,13 @@ unfold/templates/admin/edit_inline/tabular.html,sha256=FBgzUb-U0cZCN28-cpHzRSyPE
|
|
121
125
|
unfold/templates/admin/filter.html,sha256=ZnnJbg-9e6LM0OyKJW7V-DA7mmxkGhJeOpEfnM5JGM8,1998
|
122
126
|
unfold/templates/admin/includes/fieldset.html,sha256=gX-dc-jPLswk21XSKUXzAU8WFl3CFfeVrIAU2bviPyg,812
|
123
127
|
unfold/templates/admin/includes/object_delete_summary.html,sha256=FgwfapZCKWtnFtTHqh1MRuxxLot4bAw92WCcR_V8ImI,444
|
124
|
-
unfold/templates/admin/index.html,sha256=
|
125
|
-
unfold/templates/admin/login.html,sha256=
|
128
|
+
unfold/templates/admin/index.html,sha256=JnNDpNKqFKVKMQvpUQo6ddcFqGadK5g78pxpKAHLuMU,583
|
129
|
+
unfold/templates/admin/login.html,sha256=5khGm_cafDVQaDRwaTtix4g2DH4R4LrwgqO0S4aumaE,4476
|
126
130
|
unfold/templates/admin/nav_sidebar.html,sha256=cadJwXcReRHwVTpXGycF3uv5VZ5JDcIbY1pCabeQlCo,250
|
127
131
|
unfold/templates/admin/object_history.html,sha256=bfad4_lfLQFcoMV67yW7BnAi1Hwrptmgxj2tgPYQjtI,4816
|
128
132
|
unfold/templates/admin/pagination.html,sha256=1UIc6Hm2LnDDAHUE5UjuPzdFdiAat4tF6sk058UfRP0,2251
|
129
|
-
unfold/templates/admin/search_form.html,sha256=
|
130
|
-
unfold/templates/admin/submit_line.html,sha256=
|
133
|
+
unfold/templates/admin/search_form.html,sha256=eQkwq4OWVgNUSjZPzxAjtT4LcxUNcTSgDVKJ4lWwslQ,1447
|
134
|
+
unfold/templates/admin/submit_line.html,sha256=cN2bUfhNBz6OCdCeHqNYhXT2SD01YDwsQ-PVQaCGPPY,4782
|
131
135
|
unfold/templates/auth/widgets/read_only_password_hash.html,sha256=bQiYKDKJkyVP0_2eJG16oKviB4WEA0V7lSp6sGfgows,751
|
132
136
|
unfold/templates/registration/logged_out.html,sha256=GgLzqmOGpiw5FcA-Aw_T6YvhYHhGFWhyES40T28waLQ,1002
|
133
137
|
unfold/templates/registration/password_change_done.html,sha256=9HDpSraz3Kxdy0Fghbn-Lv0GAtpPgG6cuO7fvfMiYg8,1077
|
@@ -147,8 +151,8 @@ unfold/templates/unfold/components/table.html,sha256=_mIfauxVCt7gFC8CJpDdBp3TaGc
|
|
147
151
|
unfold/templates/unfold/components/text.html,sha256=-GjxvdiaBQIaNfPSzT6SSIwnc3R27FkSDQMoF3FDPso,102
|
148
152
|
unfold/templates/unfold/components/title.html,sha256=aMYSO30ybFYqPnqwlJVh2uNmcgnVrRycZkHTjJTjpX8,171
|
149
153
|
unfold/templates/unfold/components/tracker.html,sha256=-PvDHdAAB2G5mKXpQUtljWJYhhavsa1aEoBvLrCiCJo,284
|
150
|
-
unfold/templates/unfold/helpers/account_links.html,sha256=
|
151
|
-
unfold/templates/unfold/helpers/actions_row.html,sha256=
|
154
|
+
unfold/templates/unfold/helpers/account_links.html,sha256=8LBsa4uvwcbqc2kewHfSC8HaQAL3B-hPQIzV81rw_TU,1886
|
155
|
+
unfold/templates/unfold/helpers/actions_row.html,sha256=ZENvQJM2hGG3qvfwm-o9Ip665zXwqV-gl8VBof2WXgQ,2531
|
152
156
|
unfold/templates/unfold/helpers/add_link.html,sha256=Z0TzUavXH8AQ7A-I0SQ79t-UI87dyP_PZEpsDrm13Q8,810
|
153
157
|
unfold/templates/unfold/helpers/app_list.html,sha256=eIvBuP_g8oHvXZTy-IMjyynFCC1CChtMq4qZia6i77E,5751
|
154
158
|
unfold/templates/unfold/helpers/app_list_default.html,sha256=74x9GIhXHp4J-qxfyS7siSaV8A4XlRwc_ivzvvKw_FY,3902
|
@@ -181,7 +185,7 @@ unfold/templates/unfold/helpers/header_back_button.html,sha256=6DXkxsZJnIt0Z_AIL
|
|
181
185
|
unfold/templates/unfold/helpers/help_text.html,sha256=o-2_hyruIr_Xt0fo38aGgrpptPJFhddnVMPOVQOYhE4,113
|
182
186
|
unfold/templates/unfold/helpers/history.html,sha256=QUYUQXcccyIl9pKPwVS_R8Is2Zo096rLZA6inlGNhkw,1921
|
183
187
|
unfold/templates/unfold/helpers/label.html,sha256=pGzRBfuY8tos-imyA9psBxj7r93LUTfr78ff2rYPvmc,900
|
184
|
-
unfold/templates/unfold/helpers/language_switch.html,sha256=
|
188
|
+
unfold/templates/unfold/helpers/language_switch.html,sha256=RB7TMbLFaDejOLoNtE1mQoCtqVMfLTvgpLfTBXj85_s,1486
|
185
189
|
unfold/templates/unfold/helpers/messages/debug.html,sha256=7GhaVXLkrdyecr6yAnLxyyPT5a8TMHkRFq-EfXJrymI,141
|
186
190
|
unfold/templates/unfold/helpers/messages/error.html,sha256=5s51qKr0vHnIWngsc2U1TK9mAV3yBqwa7ekAYMyumlI,592
|
187
191
|
unfold/templates/unfold/helpers/messages/errornote.html,sha256=pJ4Xjmp6ZimpimqMYUdCK6VZcWn_qlCSNTjRyyiZu-E,393
|
@@ -190,20 +194,22 @@ unfold/templates/unfold/helpers/messages/success.html,sha256=FVg3HNLW7i6VVZLqiUz
|
|
190
194
|
unfold/templates/unfold/helpers/messages/warning.html,sha256=1bJj813EfxNSWcdQj6rM9bon4QTgMyrMydZY521h53E,149
|
191
195
|
unfold/templates/unfold/helpers/messages.html,sha256=I9RIKfi4T65AG16-2s0C32RwbFkn0qLhdSqDfXpSbyE,915
|
192
196
|
unfold/templates/unfold/helpers/navigation.html,sha256=HNr9bxZOg84zsbgORkOhJFXKSp0c2im438yiWCfonb0,787
|
193
|
-
unfold/templates/unfold/helpers/navigation_header.html,sha256=
|
197
|
+
unfold/templates/unfold/helpers/navigation_header.html,sha256=upSSMGsqVzk_HfNINcapUtmR_d8kV2yxdwPEjMbRknY,1402
|
194
198
|
unfold/templates/unfold/helpers/pagination_current_item.html,sha256=4cZ2KLVcP0Y7xuGyXgexDQ07r94cgM5Gnmtv11dkRPQ,69
|
195
199
|
unfold/templates/unfold/helpers/pagination_ellipsis.html,sha256=8g0KUUKtqRkXx_EBLGtsJsiYQO4tPS3GazZjxT90e0M,56
|
196
200
|
unfold/templates/unfold/helpers/search.html,sha256=w8Ute3qVfSIqUXzA3TKOx91RzW_qnffFi2Oe9qedtV4,1643
|
197
201
|
unfold/templates/unfold/helpers/search_results.html,sha256=5gxOAfFBPOkRgDziyYio6ZXcDG1bzFw7t-KXGFaWrUk,653
|
198
|
-
unfold/templates/unfold/helpers/
|
202
|
+
unfold/templates/unfold/helpers/site_branding.html,sha256=_pUyOvdjlV624eRq0wjpJkgXpn9DCDGc-0U6Mw0FupI,266
|
203
|
+
unfold/templates/unfold/helpers/site_dropdown.html,sha256=Zwg5ZGVIEHAI2h-YcWDDBz3QD2nYsP9m530wYArFk5I,878
|
204
|
+
unfold/templates/unfold/helpers/site_icon.html,sha256=rB6oc6VBB0T7wtyGOPcrq6qepDRaA54F6-Wxtaru-ks,1297
|
199
205
|
unfold/templates/unfold/helpers/site_logo.html,sha256=S_QJoT2qh0xw0ciaKxoT4GJ6QIH5eqgRSC0abbWWkOI,423
|
200
206
|
unfold/templates/unfold/helpers/submit.html,sha256=4Mgf4lx7Atm8GPqD6LTJK3NA9zoSJjs9VPig7sDp8Ao,203
|
201
|
-
unfold/templates/unfold/helpers/tab_action.html,sha256=
|
207
|
+
unfold/templates/unfold/helpers/tab_action.html,sha256=qnMmPJOeMLAx64uTf1pdPq1OIKm1MQ2Et6vTU6WQb-8,2065
|
202
208
|
unfold/templates/unfold/helpers/tab_list.html,sha256=ULuTZ7VaUFsgtqNr9eCb-A_Qmhp7gqt43V8sdu5fEII,5032
|
203
|
-
unfold/templates/unfold/helpers/theme_switch.html,sha256=
|
209
|
+
unfold/templates/unfold/helpers/theme_switch.html,sha256=Kh3Q8RuBWCUuY8YJLwBwd5yKzDhK6y0Ggx_ERoGEGv8,2218
|
204
210
|
unfold/templates/unfold/helpers/userlinks.html,sha256=oZqiwCxG_zRecAbzYrr8_hQvkndVB6-liP6LEZM1UZc,863
|
205
211
|
unfold/templates/unfold/helpers/welcomemsg.html,sha256=a7VjZ8J1mn-DAo57nhUP096SEcBOqcCWrWt2qoT0cyg,1882
|
206
|
-
unfold/templates/unfold/layouts/base.html,sha256=
|
212
|
+
unfold/templates/unfold/layouts/base.html,sha256=e9T-vuPvrySHf23LLwRuTQXUg0Q4x-6gmT_yXghqc-I,278
|
207
213
|
unfold/templates/unfold/layouts/base_simple.html,sha256=VigjNWnWOt_XtbahvqsroPf8mdwl7cDGbg5fEaLmG9s,1211
|
208
214
|
unfold/templates/unfold/layouts/skeleton.html,sha256=cE29ydXMseyXut1k7Yme3alnUBFXhBDxT0WFZCgrvGQ,4600
|
209
215
|
unfold/templates/unfold/templatetags/preserve_changelist_filters.html,sha256=sx2jUhogNY4emBhY8PqxQ6mXifbXQb_IQI9Hd0boJI0,111
|
@@ -225,11 +231,11 @@ unfold/templates/unfold/widgets/url.html,sha256=IRLgW44VTKN7UrSWeywJwaxQhfG5jhhX
|
|
225
231
|
unfold/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
226
232
|
unfold/templatetags/unfold.py,sha256=C2x_fqjVCWku4WdyaD_u4UjXq9eue1CgnmEz5PCfrS8,12364
|
227
233
|
unfold/templatetags/unfold_list.py,sha256=-KOU6Ib2k4gokuw_CgRooFCto8xJkiDyotbQbKAqkEY,14233
|
228
|
-
unfold/typing.py,sha256=
|
234
|
+
unfold/typing.py,sha256=i7LM2LiwYTAjT5-OLDUPVn5b9X-DMmHnjlZG2toWwSE,692
|
229
235
|
unfold/utils.py,sha256=L0oC9-j1B__BZ21M7i6rTXNbzCQaZ4T0qHYQcNupk0A,5480
|
230
236
|
unfold/views.py,sha256=nqv-meiwGtCeET2r8WBVyijWnE94Dssc4QmdUVMMcEM,997
|
231
|
-
unfold/widgets.py,sha256=
|
232
|
-
django_unfold-0.
|
233
|
-
django_unfold-0.
|
234
|
-
django_unfold-0.
|
235
|
-
django_unfold-0.
|
237
|
+
unfold/widgets.py,sha256=esW1wxdbZwUhF31ensitBF8_6kVq_Gp0_iDgv6F5Rr0,20246
|
238
|
+
django_unfold-0.49.0.dist-info/LICENSE.md,sha256=Ltk_quRyyvV3J5v3brtOqmibeZSw2Hrb8bY1W3ya0Ik,1077
|
239
|
+
django_unfold-0.49.0.dist-info/METADATA,sha256=Rn9yuoaPYQ_xomHRKms5bRBF_qlQvArpTT5lJE38r3Q,4840
|
240
|
+
django_unfold-0.49.0.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
241
|
+
django_unfold-0.49.0.dist-info/RECORD,,
|
unfold/admin.py
CHANGED
@@ -1,127 +1,33 @@
|
|
1
|
-
import copy
|
2
1
|
from functools import update_wrapper
|
3
|
-
from typing import
|
2
|
+
from typing import Optional
|
4
3
|
|
5
4
|
from django import forms
|
6
5
|
from django.contrib.admin import ModelAdmin as BaseModelAdmin
|
7
6
|
from django.contrib.admin import StackedInline as BaseStackedInline
|
8
7
|
from django.contrib.admin import TabularInline as BaseTabularInline
|
9
8
|
from django.contrib.admin import display, helpers
|
10
|
-
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
|
11
|
-
from django.db import models
|
12
9
|
from django.db.models import BLANK_CHOICE_DASH, Model
|
13
|
-
from django.db.models.fields import Field
|
14
|
-
from django.db.models.fields.related import ForeignKey, ManyToManyField
|
15
|
-
from django.forms import Form
|
16
|
-
from django.forms.fields import TypedChoiceField
|
17
|
-
from django.forms.models import ModelChoiceField, ModelMultipleChoiceField
|
18
|
-
from django.forms.widgets import SelectMultiple
|
19
10
|
from django.http import HttpRequest, HttpResponse
|
20
11
|
from django.shortcuts import redirect
|
21
|
-
from django.
|
22
|
-
from django.urls import URLPattern, path, reverse
|
12
|
+
from django.urls import URLPattern, path
|
23
13
|
from django.utils.safestring import mark_safe
|
24
14
|
from django.utils.translation import gettext_lazy as _
|
25
15
|
from django.views import View
|
26
16
|
|
27
|
-
from .checks import UnfoldModelAdminChecks
|
28
|
-
from .
|
29
|
-
from .
|
30
|
-
from .
|
31
|
-
from .
|
32
|
-
from .typing import FieldsetsType
|
33
|
-
from .widgets import
|
34
|
-
SELECT_CLASSES,
|
35
|
-
UnfoldAdminBigIntegerFieldWidget,
|
36
|
-
UnfoldAdminDecimalFieldWidget,
|
37
|
-
UnfoldAdminEmailInputWidget,
|
38
|
-
UnfoldAdminFileFieldWidget,
|
39
|
-
UnfoldAdminImageFieldWidget,
|
40
|
-
UnfoldAdminImageSmallFieldWidget,
|
41
|
-
UnfoldAdminIntegerFieldWidget,
|
42
|
-
UnfoldAdminIntegerRangeWidget,
|
43
|
-
UnfoldAdminMoneyWidget,
|
44
|
-
UnfoldAdminNullBooleanSelectWidget,
|
45
|
-
UnfoldAdminRadioSelectWidget,
|
46
|
-
UnfoldAdminSelectWidget,
|
47
|
-
UnfoldAdminSingleDateWidget,
|
48
|
-
UnfoldAdminSingleTimeWidget,
|
49
|
-
UnfoldAdminSplitDateTimeWidget,
|
50
|
-
UnfoldAdminTextareaWidget,
|
51
|
-
UnfoldAdminTextInputWidget,
|
52
|
-
UnfoldAdminURLInputWidget,
|
53
|
-
UnfoldAdminUUIDInputWidget,
|
54
|
-
UnfoldBooleanSwitchWidget,
|
55
|
-
UnfoldBooleanWidget,
|
56
|
-
UnfoldForeignKeyRawIdWidget,
|
57
|
-
)
|
58
|
-
|
59
|
-
try:
|
60
|
-
from django.contrib.postgres.fields import ArrayField, IntegerRangeField
|
61
|
-
from django.contrib.postgres.search import SearchVectorField
|
62
|
-
|
63
|
-
HAS_PSYCOPG = True
|
64
|
-
except ImportError:
|
65
|
-
HAS_PSYCOPG = False
|
66
|
-
|
67
|
-
try:
|
68
|
-
from djmoney.models.fields import MoneyField
|
69
|
-
|
70
|
-
HAS_MONEY = True
|
71
|
-
except ImportError:
|
72
|
-
HAS_MONEY = False
|
73
|
-
|
74
|
-
FORMFIELD_OVERRIDES = {
|
75
|
-
models.DateTimeField: {
|
76
|
-
"form_class": forms.SplitDateTimeField,
|
77
|
-
"widget": UnfoldAdminSplitDateTimeWidget,
|
78
|
-
},
|
79
|
-
models.DateField: {"widget": UnfoldAdminSingleDateWidget},
|
80
|
-
models.TimeField: {"widget": UnfoldAdminSingleTimeWidget},
|
81
|
-
models.EmailField: {"widget": UnfoldAdminEmailInputWidget},
|
82
|
-
models.CharField: {"widget": UnfoldAdminTextInputWidget},
|
83
|
-
models.URLField: {"widget": UnfoldAdminURLInputWidget},
|
84
|
-
models.GenericIPAddressField: {"widget": UnfoldAdminTextInputWidget},
|
85
|
-
models.UUIDField: {"widget": UnfoldAdminUUIDInputWidget},
|
86
|
-
models.TextField: {"widget": UnfoldAdminTextareaWidget},
|
87
|
-
models.NullBooleanField: {"widget": UnfoldAdminNullBooleanSelectWidget},
|
88
|
-
models.BooleanField: {"widget": UnfoldBooleanSwitchWidget},
|
89
|
-
models.IntegerField: {"widget": UnfoldAdminIntegerFieldWidget},
|
90
|
-
models.BigIntegerField: {"widget": UnfoldAdminBigIntegerFieldWidget},
|
91
|
-
models.DecimalField: {"widget": UnfoldAdminDecimalFieldWidget},
|
92
|
-
models.FloatField: {"widget": UnfoldAdminDecimalFieldWidget},
|
93
|
-
models.FileField: {"widget": UnfoldAdminFileFieldWidget},
|
94
|
-
models.ImageField: {"widget": UnfoldAdminImageFieldWidget},
|
95
|
-
models.JSONField: {"widget": UnfoldAdminTextareaWidget},
|
96
|
-
models.DurationField: {"widget": UnfoldAdminTextInputWidget},
|
97
|
-
}
|
98
|
-
|
99
|
-
if HAS_PSYCOPG:
|
100
|
-
FORMFIELD_OVERRIDES.update(
|
101
|
-
{
|
102
|
-
ArrayField: {"widget": UnfoldAdminTextareaWidget},
|
103
|
-
SearchVectorField: {"widget": UnfoldAdminTextareaWidget},
|
104
|
-
IntegerRangeField: {"widget": UnfoldAdminIntegerRangeWidget},
|
105
|
-
}
|
106
|
-
)
|
107
|
-
|
108
|
-
if HAS_MONEY:
|
109
|
-
FORMFIELD_OVERRIDES.update(
|
110
|
-
{
|
111
|
-
MoneyField: {"widget": UnfoldAdminMoneyWidget},
|
112
|
-
}
|
113
|
-
)
|
114
|
-
|
115
|
-
FORMFIELD_OVERRIDES_INLINE = copy.deepcopy(FORMFIELD_OVERRIDES)
|
116
|
-
|
117
|
-
FORMFIELD_OVERRIDES_INLINE.update(
|
118
|
-
{
|
119
|
-
models.ImageField: {"widget": UnfoldAdminImageSmallFieldWidget},
|
120
|
-
}
|
121
|
-
)
|
17
|
+
from unfold.checks import UnfoldModelAdminChecks
|
18
|
+
from unfold.fields import UnfoldAdminField, UnfoldAdminReadonlyField
|
19
|
+
from unfold.forms import ActionForm
|
20
|
+
from unfold.mixins import ActionModelAdminMixin, BaseModelAdminMixin
|
21
|
+
from unfold.overrides import FORMFIELD_OVERRIDES_INLINE
|
22
|
+
from unfold.typing import FieldsetsType
|
23
|
+
from unfold.widgets import UnfoldBooleanWidget
|
122
24
|
|
123
25
|
checkbox = UnfoldBooleanWidget(
|
124
|
-
{
|
26
|
+
{
|
27
|
+
"class": "action-select",
|
28
|
+
"aria-label": _("Select record"),
|
29
|
+
},
|
30
|
+
lambda value: False,
|
125
31
|
)
|
126
32
|
|
127
33
|
helpers.AdminField = UnfoldAdminField
|
@@ -129,106 +35,8 @@ helpers.AdminField = UnfoldAdminField
|
|
129
35
|
helpers.AdminReadonlyField = UnfoldAdminReadonlyField
|
130
36
|
|
131
37
|
|
132
|
-
class
|
133
|
-
def __init__(self, model, admin_site):
|
134
|
-
overrides = copy.deepcopy(FORMFIELD_OVERRIDES)
|
135
|
-
|
136
|
-
for k, v in self.formfield_overrides.items():
|
137
|
-
overrides.setdefault(k, {}).update(v)
|
138
|
-
|
139
|
-
self.formfield_overrides = overrides
|
140
|
-
|
141
|
-
super().__init__(model, admin_site)
|
142
|
-
|
143
|
-
def formfield_for_choice_field(
|
144
|
-
self, db_field: Field, request: HttpRequest, **kwargs
|
145
|
-
) -> TypedChoiceField:
|
146
|
-
if "widget" not in kwargs:
|
147
|
-
if db_field.name in self.radio_fields:
|
148
|
-
kwargs["widget"] = UnfoldAdminRadioSelectWidget(
|
149
|
-
radio_style=self.radio_fields[db_field.name]
|
150
|
-
)
|
151
|
-
else:
|
152
|
-
kwargs["widget"] = UnfoldAdminSelectWidget()
|
153
|
-
|
154
|
-
if "choices" not in kwargs:
|
155
|
-
kwargs["choices"] = db_field.get_choices(
|
156
|
-
include_blank=db_field.blank, blank_choice=[("", _("Select value"))]
|
157
|
-
)
|
158
|
-
|
159
|
-
return super().formfield_for_choice_field(db_field, request, **kwargs)
|
160
|
-
|
161
|
-
def formfield_for_foreignkey(
|
162
|
-
self, db_field: ForeignKey, request: HttpRequest, **kwargs
|
163
|
-
) -> Optional[ModelChoiceField]:
|
164
|
-
db = kwargs.get("using")
|
165
|
-
|
166
|
-
# Overrides widgets for all related fields
|
167
|
-
if "widget" not in kwargs:
|
168
|
-
if db_field.name in self.raw_id_fields:
|
169
|
-
kwargs["widget"] = UnfoldForeignKeyRawIdWidget(
|
170
|
-
db_field.remote_field, self.admin_site, using=db
|
171
|
-
)
|
172
|
-
elif (
|
173
|
-
db_field.name not in self.get_autocomplete_fields(request)
|
174
|
-
and db_field.name not in self.radio_fields
|
175
|
-
):
|
176
|
-
kwargs["widget"] = UnfoldAdminSelectWidget()
|
177
|
-
kwargs["empty_label"] = _("Select value")
|
178
|
-
|
179
|
-
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
180
|
-
|
181
|
-
def formfield_for_manytomany(
|
182
|
-
self,
|
183
|
-
db_field: ManyToManyField,
|
184
|
-
request: HttpRequest,
|
185
|
-
**kwargs,
|
186
|
-
) -> ModelMultipleChoiceField:
|
187
|
-
if "widget" not in kwargs:
|
188
|
-
if db_field.name in self.raw_id_fields:
|
189
|
-
kwargs["widget"] = UnfoldAdminTextInputWidget()
|
190
|
-
|
191
|
-
form_field = super().formfield_for_manytomany(db_field, request, **kwargs)
|
192
|
-
|
193
|
-
# If M2M uses intermediary model, form_field will be None
|
194
|
-
if not form_field:
|
195
|
-
return None
|
196
|
-
|
197
|
-
if isinstance(form_field.widget, SelectMultiple):
|
198
|
-
form_field.widget.attrs["class"] = " ".join(SELECT_CLASSES)
|
199
|
-
|
200
|
-
return form_field
|
201
|
-
|
202
|
-
def formfield_for_nullboolean_field(
|
203
|
-
self, db_field: Field, request: HttpRequest, **kwargs
|
204
|
-
) -> Optional[Field]:
|
205
|
-
if "widget" not in kwargs:
|
206
|
-
kwargs["widget"] = UnfoldAdminNullBooleanSelectWidget()
|
207
|
-
|
208
|
-
return db_field.formfield(**kwargs)
|
209
|
-
|
210
|
-
def formfield_for_dbfield(
|
211
|
-
self, db_field: Field, request: HttpRequest, **kwargs
|
212
|
-
) -> Optional[Field]:
|
213
|
-
if isinstance(db_field, models.BooleanField) and db_field.null is True:
|
214
|
-
return self.formfield_for_nullboolean_field(db_field, request, **kwargs)
|
215
|
-
|
216
|
-
formfield = super().formfield_for_dbfield(db_field, request, **kwargs)
|
217
|
-
|
218
|
-
if formfield and isinstance(formfield.widget, RelatedFieldWidgetWrapper):
|
219
|
-
formfield.widget.template_name = (
|
220
|
-
"unfold/widgets/related_widget_wrapper.html"
|
221
|
-
)
|
222
|
-
|
223
|
-
return formfield
|
224
|
-
|
225
|
-
|
226
|
-
class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
|
38
|
+
class ModelAdmin(BaseModelAdminMixin, ActionModelAdminMixin, BaseModelAdmin):
|
227
39
|
action_form = ActionForm
|
228
|
-
actions_list = ()
|
229
|
-
actions_row = ()
|
230
|
-
actions_detail = ()
|
231
|
-
actions_submit_line = ()
|
232
40
|
custom_urls = ()
|
233
41
|
add_fieldsets = ()
|
234
42
|
list_horizontal_scrollbar_top = False
|
@@ -269,86 +77,6 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
|
|
269
77
|
return self.add_fieldsets
|
270
78
|
return super().get_fieldsets(request, obj)
|
271
79
|
|
272
|
-
def _filter_unfold_actions_by_permissions(
|
273
|
-
self,
|
274
|
-
request: HttpRequest,
|
275
|
-
actions: list[UnfoldAction],
|
276
|
-
object_id: Optional[Union[int, str]] = None,
|
277
|
-
) -> list[UnfoldAction]:
|
278
|
-
"""Filter out any Unfold actions that the user doesn't have access to."""
|
279
|
-
filtered_actions = []
|
280
|
-
for action in actions:
|
281
|
-
if not hasattr(action.method, "allowed_permissions"):
|
282
|
-
filtered_actions.append(action)
|
283
|
-
continue
|
284
|
-
|
285
|
-
permission_checks = (
|
286
|
-
getattr(self, f"has_{permission}_permission")
|
287
|
-
for permission in action.method.allowed_permissions
|
288
|
-
)
|
289
|
-
|
290
|
-
if object_id:
|
291
|
-
if any(
|
292
|
-
has_permission(request, object_id)
|
293
|
-
for has_permission in permission_checks
|
294
|
-
):
|
295
|
-
filtered_actions.append(action)
|
296
|
-
else:
|
297
|
-
if any(has_permission(request) for has_permission in permission_checks):
|
298
|
-
filtered_actions.append(action)
|
299
|
-
|
300
|
-
return filtered_actions
|
301
|
-
|
302
|
-
def get_actions_list(self, request: HttpRequest) -> list[UnfoldAction]:
|
303
|
-
return self._filter_unfold_actions_by_permissions(
|
304
|
-
request, self._get_base_actions_list()
|
305
|
-
)
|
306
|
-
|
307
|
-
def _get_base_actions_list(self) -> list[UnfoldAction]:
|
308
|
-
"""
|
309
|
-
Returns all available list global actions, prior to any filtering
|
310
|
-
"""
|
311
|
-
return [self.get_unfold_action(action) for action in self.actions_list or []]
|
312
|
-
|
313
|
-
def get_actions_detail(
|
314
|
-
self, request: HttpRequest, object_id: int
|
315
|
-
) -> list[UnfoldAction]:
|
316
|
-
return self._filter_unfold_actions_by_permissions(
|
317
|
-
request, self._get_base_actions_detail(), object_id
|
318
|
-
)
|
319
|
-
|
320
|
-
def _get_base_actions_detail(self) -> list[UnfoldAction]:
|
321
|
-
"""
|
322
|
-
Returns all available detail actions, prior to any filtering
|
323
|
-
"""
|
324
|
-
return [self.get_unfold_action(action) for action in self.actions_detail or []]
|
325
|
-
|
326
|
-
def get_actions_row(self, request: HttpRequest) -> list[UnfoldAction]:
|
327
|
-
return self._filter_unfold_actions_by_permissions(
|
328
|
-
request, self._get_base_actions_row()
|
329
|
-
)
|
330
|
-
|
331
|
-
def _get_base_actions_row(self) -> list[UnfoldAction]:
|
332
|
-
"""
|
333
|
-
Returns all available row actions, prior to any filtering
|
334
|
-
"""
|
335
|
-
return [self.get_unfold_action(action) for action in self.actions_row or []]
|
336
|
-
|
337
|
-
def get_actions_submit_line(
|
338
|
-
self, request: HttpRequest, object_id: int
|
339
|
-
) -> list[UnfoldAction]:
|
340
|
-
return self._filter_unfold_actions_by_permissions(
|
341
|
-
request, self._get_base_actions_submit_line(), object_id
|
342
|
-
)
|
343
|
-
|
344
|
-
def _get_base_actions_submit_line(self) -> list[UnfoldAction]:
|
345
|
-
"""
|
346
|
-
Returns all available submit row actions, prior to any filtering
|
347
|
-
"""
|
348
|
-
return [
|
349
|
-
self.get_unfold_action(action) for action in self.actions_submit_line or []
|
350
|
-
]
|
351
|
-
|
352
80
|
def get_custom_urls(self) -> tuple[tuple[str, str, View], ...]:
|
353
81
|
"""
|
354
82
|
Method to get custom views for ModelAdmin with their urls
|
@@ -416,129 +144,6 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
|
|
416
144
|
name=custom_url[1],
|
417
145
|
)
|
418
146
|
|
419
|
-
def changeform_view(
|
420
|
-
self,
|
421
|
-
request: HttpRequest,
|
422
|
-
object_id: Optional[str] = None,
|
423
|
-
form_url: str = "",
|
424
|
-
extra_context: Optional[dict[str, bool]] = None,
|
425
|
-
) -> Any:
|
426
|
-
if extra_context is None:
|
427
|
-
extra_context = {}
|
428
|
-
|
429
|
-
actions = []
|
430
|
-
if object_id:
|
431
|
-
for action in self.get_actions_detail(request, object_id):
|
432
|
-
actions.append(
|
433
|
-
{
|
434
|
-
"title": action.description,
|
435
|
-
"attrs": action.method.attrs,
|
436
|
-
"path": reverse(
|
437
|
-
f"{self.admin_site.name}:{action.action_name}",
|
438
|
-
args=(object_id,),
|
439
|
-
),
|
440
|
-
}
|
441
|
-
)
|
442
|
-
|
443
|
-
extra_context.update(
|
444
|
-
{
|
445
|
-
"actions_submit_line": self.get_actions_submit_line(request, object_id),
|
446
|
-
"actions_detail": actions,
|
447
|
-
}
|
448
|
-
)
|
449
|
-
|
450
|
-
return super().changeform_view(request, object_id, form_url, extra_context)
|
451
|
-
|
452
|
-
def changelist_view(
|
453
|
-
self, request: HttpRequest, extra_context: Optional[dict[str, str]] = None
|
454
|
-
) -> TemplateResponse:
|
455
|
-
if extra_context is None:
|
456
|
-
extra_context = {}
|
457
|
-
|
458
|
-
actions = [
|
459
|
-
{
|
460
|
-
"title": action.description,
|
461
|
-
"attrs": action.method.attrs,
|
462
|
-
"path": reverse(f"{self.admin_site.name}:{action.action_name}"),
|
463
|
-
}
|
464
|
-
for action in self.get_actions_list(request)
|
465
|
-
]
|
466
|
-
|
467
|
-
actions_row = [
|
468
|
-
{
|
469
|
-
"title": action.description,
|
470
|
-
"attrs": action.method.attrs,
|
471
|
-
"raw_path": f"{self.admin_site.name}:{action.action_name}",
|
472
|
-
}
|
473
|
-
for action in self.get_actions_row(request)
|
474
|
-
]
|
475
|
-
|
476
|
-
extra_context.update(
|
477
|
-
{
|
478
|
-
"actions_list": actions,
|
479
|
-
"actions_row": actions_row,
|
480
|
-
}
|
481
|
-
)
|
482
|
-
|
483
|
-
return super().changelist_view(request, extra_context)
|
484
|
-
|
485
|
-
def get_unfold_action(self, action: str) -> UnfoldAction:
|
486
|
-
"""
|
487
|
-
Converts action name to UnfoldAction
|
488
|
-
:param action:
|
489
|
-
:return:
|
490
|
-
"""
|
491
|
-
method = self._get_instance_method(action)
|
492
|
-
|
493
|
-
return UnfoldAction(
|
494
|
-
action_name=f"{self.model._meta.app_label}_{self.model._meta.model_name}_{action}",
|
495
|
-
method=method,
|
496
|
-
description=self._get_action_description(method, action),
|
497
|
-
path=self._get_action_url(method, action),
|
498
|
-
attrs=method.attrs if hasattr(method, "attrs") else None,
|
499
|
-
)
|
500
|
-
|
501
|
-
@staticmethod
|
502
|
-
def _get_action_url(func: Callable, name: str) -> str:
|
503
|
-
"""
|
504
|
-
Returns action URL if it was specified in @action decorator.
|
505
|
-
If it was not, name of the action is returned.
|
506
|
-
:param func:
|
507
|
-
:param name:
|
508
|
-
:return:
|
509
|
-
"""
|
510
|
-
return getattr(func, "url_path", name)
|
511
|
-
|
512
|
-
def save_model(
|
513
|
-
self, request: HttpRequest, obj: Model, form: Form, change: Any
|
514
|
-
) -> None:
|
515
|
-
super().save_model(request, obj, form, change)
|
516
|
-
|
517
|
-
for action in self.get_actions_submit_line(request, obj.pk):
|
518
|
-
if action.action_name not in request.POST:
|
519
|
-
continue
|
520
|
-
|
521
|
-
action.method(request, obj)
|
522
|
-
|
523
|
-
def _get_instance_method(self, method_name: str) -> Callable:
|
524
|
-
"""
|
525
|
-
Searches for method on self instance based on method_name and returns it if it exists.
|
526
|
-
If it does not exist or is not callable, it raises UnfoldException
|
527
|
-
:param method_name: Name of the method to search for
|
528
|
-
:return: method from self instance
|
529
|
-
"""
|
530
|
-
try:
|
531
|
-
method = getattr(self, method_name)
|
532
|
-
except AttributeError as e:
|
533
|
-
raise UnfoldException(
|
534
|
-
f"Method {method_name} specified does not exist on current object"
|
535
|
-
) from e
|
536
|
-
|
537
|
-
if not callable(method):
|
538
|
-
raise UnfoldException(f"{method_name} is not callable")
|
539
|
-
|
540
|
-
return method
|
541
|
-
|
542
147
|
def get_action_choices(
|
543
148
|
self, request: HttpRequest, default_choices=BLANK_CHOICE_DASH
|
544
149
|
):
|
@@ -564,14 +169,14 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
|
|
564
169
|
return res
|
565
170
|
|
566
171
|
|
567
|
-
class TabularInline(
|
172
|
+
class TabularInline(BaseModelAdminMixin, BaseTabularInline):
|
568
173
|
formfield_overrides = FORMFIELD_OVERRIDES_INLINE
|
569
174
|
readonly_preprocess_fields = {}
|
570
175
|
ordering_field = None
|
571
176
|
hide_ordering_field = False
|
572
177
|
|
573
178
|
|
574
|
-
class StackedInline(
|
179
|
+
class StackedInline(BaseModelAdminMixin, BaseStackedInline):
|
575
180
|
formfield_overrides = FORMFIELD_OVERRIDES_INLINE
|
576
181
|
readonly_preprocess_fields = {}
|
577
182
|
ordering_field = None
|
unfold/dataclasses.py
CHANGED
@@ -12,6 +12,7 @@ class UnfoldAction:
|
|
12
12
|
path: str
|
13
13
|
attrs: Optional[dict] = None
|
14
14
|
object_id: Optional[Union[int, str]] = None
|
15
|
+
icon: Optional[str] = None
|
15
16
|
|
16
17
|
|
17
18
|
@dataclass
|
@@ -20,3 +21,10 @@ class Favicon:
|
|
20
21
|
rel: Optional[str] = None
|
21
22
|
type: Optional[str] = None
|
22
23
|
sizes: Optional[str] = None
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class DropdownItem:
|
28
|
+
title: str
|
29
|
+
link: Union[str, Callable]
|
30
|
+
icon: Optional[str] = None
|