django-unfold 0.50.0__py3-none-any.whl → 0.52.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.
Files changed (31) hide show
  1. {django_unfold-0.50.0.dist-info → django_unfold-0.52.0.dist-info}/METADATA +2 -2
  2. {django_unfold-0.50.0.dist-info → django_unfold-0.52.0.dist-info}/RECORD +30 -29
  3. unfold/contrib/simple_history/templates/simple_history/object_history.html +2 -2
  4. unfold/decorators.py +3 -0
  5. unfold/sections.py +82 -0
  6. unfold/static/unfold/css/styles.css +1 -1
  7. unfold/styles.css +15 -5
  8. unfold/templates/admin/change_form.html +1 -1
  9. unfold/templates/admin/change_list_results.html +36 -6
  10. unfold/templates/admin/filter.html +1 -1
  11. unfold/templates/admin/submit_line.html +1 -1
  12. unfold/templates/unfold/components/card.html +1 -1
  13. unfold/templates/unfold/components/table.html +42 -26
  14. unfold/templates/unfold/helpers/change_list_filter_actions.html +1 -1
  15. unfold/templates/unfold/helpers/display_dropdown.html +33 -0
  16. unfold/templates/unfold/helpers/fieldset_row_checkbox.html +1 -1
  17. unfold/templates/unfold/helpers/fieldset_row_field.html +1 -1
  18. unfold/templates/unfold/helpers/messages/debug.html +2 -2
  19. unfold/templates/unfold/helpers/messages/info.html +2 -2
  20. unfold/templates/unfold/helpers/messages/success.html +2 -2
  21. unfold/templates/unfold/helpers/messages/warning.html +2 -2
  22. unfold/templates/unfold/widgets/date.html +1 -1
  23. unfold/templates/unfold/widgets/split_datetime.html +0 -8
  24. unfold/templates/unfold/widgets/time.html +1 -1
  25. unfold/templatetags/unfold.py +15 -3
  26. unfold/templatetags/unfold_list.py +20 -16
  27. unfold/utils.py +14 -0
  28. unfold/widgets.py +8 -10
  29. unfold/templates/unfold/widgets/textarea_expandable.html +0 -7
  30. {django_unfold-0.50.0.dist-info → django_unfold-0.52.0.dist-info}/LICENSE.md +0 -0
  31. {django_unfold-0.50.0.dist-info → django_unfold-0.52.0.dist-info}/WHEEL +0 -0
unfold/styles.css CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
5
  @tailwind utilities;
6
6
 
7
+ @layer utilities {
8
+ .field-sizing-content {
9
+ field-sizing: content;
10
+ }
11
+ }
7
12
 
8
13
  /*******************************************************
9
14
  Classes
@@ -310,7 +315,7 @@ h3 span:nth-child(3) {
310
315
  .select2-container.select2-container--admin-autocomplete
311
316
  .select2-selection--single
312
317
  .select2-selection__rendered {
313
- @apply font-medium h-9 px-3 py-2 text-base-500 text-sm dark:text-base-300;
318
+ @apply font-medium h-9 px-3 py-2 text-font-default-light text-sm dark:text-font-default-dark;
314
319
  }
315
320
 
316
321
  .select2-container.select2-container--admin-autocomplete
@@ -361,7 +366,7 @@ h3 span:nth-child(3) {
361
366
  .select2-container.select2-container--admin-autocomplete
362
367
  .select2-search--dropdown
363
368
  .select2-search__field {
364
- @apply bg-base-50 border border-base-200 border-solid flex-grow font-medium mx-0 outline-none px-3 py-2 rounded shadow-sm text-base-500 text-sm w-full dark:bg-base-800 dark:border-base-800 dark:text-base-300;
369
+ @apply bg-base-50 border border-base-200 border-solid flex-grow font-medium mx-0 outline-none px-3 py-2 rounded shadow-sm text-font-default-light text-sm w-full dark:bg-base-800 dark:border-base-800 dark:text-font-default-dark;
365
370
  }
366
371
 
367
372
  .select2-container.select2-container--admin-autocomplete.select2-container--open.select2-container--above {
@@ -386,12 +391,17 @@ h3 span:nth-child(3) {
386
391
 
387
392
  .select2-container.select2-container--admin-autocomplete
388
393
  .select2-results__option {
389
- @apply block px-3 py-2 text-base-500 text-sm transition-all dark:text-base-300;
394
+ @apply block px-3 py-2 text-font-default-light text-sm transition-all dark:text-font-default-dark;
390
395
  }
391
396
 
392
397
  .select2-container.select2-container--admin-autocomplete
393
- .select2-results__option--highlighted[aria-selected] {
394
- @apply text-primary-500;
398
+ .select2-results__option[aria-selected="true"] {
399
+ @apply text-primary-600 dark:text-primary-500;
400
+ }
401
+
402
+ .select2-container.select2-container--admin-autocomplete
403
+ .select2-results__option--highlighted[aria-selected="true"] {
404
+ @apply text-primary-600 dark:text-primary-500;
395
405
  }
396
406
 
397
407
  .select2-container.select2-container--admin-autocomplete
@@ -58,7 +58,7 @@
58
58
  {% endblock %}
59
59
 
60
60
  {% block content %}
61
- <div id="content-main">
61
+ <div id="content-main" x-data="{ changeFormWidth: 0 }" x-resize="changeFormWidth = $width">
62
62
  {% block form_before %}{% endblock %}
63
63
 
64
64
  {% if adminform.model_admin.change_form_outer_before_template %}
@@ -1,4 +1,4 @@
1
- {% load admin_urls i18n %}
1
+ {% load admin_urls i18n unfold %}
2
2
 
3
3
  {% if result_hidden_fields %}
4
4
  <div class="hiddenfields">
@@ -11,6 +11,10 @@
11
11
  <table id="result_list" class="block border-base-200 border-spacing-none border-separate w-full lg:table">
12
12
  <thead>
13
13
  <tr>
14
+ {% if cl.model_admin.list_sections|length > 0 %}
15
+ <th></th>
16
+ {% endif %}
17
+
14
18
  {% for header in result_headers %}
15
19
  <th class="align-middle font-semibold py-2 text-left text-font-important-light dark:text-font-important-dark whitespace-nowrap {{ header.class_attrib }} {% if "action-toggle" in header.text and forloop.counter == 1 %}lg:px-3 lg:w-10{% else %}hidden px-3 lg:table-cell{% endif %}" scope="col">
16
20
  <div class="flex items-center">
@@ -65,8 +69,8 @@
65
69
  </tr>
66
70
  </thead>
67
71
 
68
- <tbody class="block lg:table-row-group">
69
- {% for result in results %}
72
+ {% for result in results %}
73
+ <tbody class="block lg:table-row-group" x-data="{rowOpen: false}">
70
74
  {% if result.form and result.form.non_field_errors %}
71
75
  <tr>
72
76
  <td class="text-left" colspan="{{ result|length }}">
@@ -76,13 +80,39 @@
76
80
  {% endif %}
77
81
 
78
82
  <tr class="{% cycle '' 'bg-base-50 dark:bg-white/[.02]' %} block border mb-3 rounded shadow-sm lg:table-row lg:border-none lg:mb-0 lg:shadow-none dark:border-base-800">
83
+ {% if cl.model_admin.list_sections|length > 0 %}
84
+ <td class="align-middle cursor-pointer flex border-b border-base-200 font-normal px-2.5 py-2 relative text-left before:font-semibold before:text-font-important-light before:block before:capitalize before:content-[attr(data-label)] before:mr-auto lg:before:hidden lg:border-b-0 lg:border-t lg:pl-3 lg:pr-0 lg:py-3 lg:table-cell dark:border-base-800 dark:lg:border-base-800 dark:before:text-font-important-dark lg:w-px"
85
+ data-label="{% trans "Expand row" %}"
86
+ x-on:click="rowOpen = !rowOpen">
87
+ <div class="absolute bg-primary-600 -bottom-px hidden left-0 top-0 w-0.5 z-10 lg:block" x-show="rowOpen"></div>
88
+ <span class="material-symbols-outlined select-none !block -rotate-90 transition-all" x-bind:class="rowOpen && 'rotate-0'">
89
+ expand_more
90
+ </span>
91
+ </td>
92
+ {% endif %}
93
+
79
94
  {% for item in result %}
80
95
  {{ item }}
81
96
  {% endfor %}
82
- {% include 'unfold/helpers/actions_row.html' with actions=actions_row instance_pk=result.instance_pk %}
97
+
98
+ {% include 'unfold/helpers/actions_row.html' with actions=actions_row instance_pk=result.instance.pk %}
83
99
  </tr>
84
- {% endfor %}
85
- </tbody>
100
+
101
+ {% if cl.model_admin.list_sections|length > 0 %}
102
+ <tr class="block mb-3 lg:table-row" x-show="rowOpen">
103
+ <td colspan="{{ result|length|add:1 }}" class="border bg-base-200/10 block border-base-200 relative rounded p-3 lg:shadow-inner lg:border-0 lg:border-t lg:rounded-none lg:table-cell dark:border-base-800">
104
+ <div class="absolute bg-primary-600 h-full hidden left-0 top-0 w-0.5 lg:block"></div>
105
+
106
+ <div class="grid gap-3 {{ cl.model_admin.list_sections_classes }}">
107
+ {% for section in cl.model_admin.list_sections %}
108
+ {% render_section cl.model_admin.list_sections|index:forloop.counter0 result.instance %}
109
+ {% endfor %}
110
+ </div>
111
+ </td>
112
+ </tr>
113
+ {% endif %}
114
+ </tbody>
115
+ {% endfor %}
86
116
  </table>
87
117
  </div>
88
118
  {% else %}
@@ -15,7 +15,7 @@
15
15
  <ul class="dark:bg-base-900 border border-base-200 flex min-w-20 rounded shadow-sm text-font-default-light dark:border-base-700 dark:text-font-default-dark w-full">
16
16
  {% for choice in choices %}
17
17
  <li class="basis-1/3 border-r border-base-200 flex-grow truncate last:border-r-0 dark:border-base-700 {% if choice.selected %}font-semibold text-primary-600 dark:text-primary-500 {% else %}hover:text-base-700 dark:hover:text-base-200{% endif %}">
18
- <a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}" class="block px-3 py-2 text-center hover:text-primary-600 dark:hover:text-primary-500">
18
+ <a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}" class="block py-2 text-center hover:text-primary-600 dark:hover:text-primary-500">
19
19
  {{ choice.display }}
20
20
  </a>
21
21
  </li>
@@ -3,7 +3,7 @@
3
3
  <div {% if not is_popup %}id="submit-row"{% endif %} class="relative {% if not is_popup %}lg:mt-16{% endif %} z-20">
4
4
  <div class="{% if not is_popup %}max-w-full lg:bottom-0 lg:fixed lg:left-0 lg:right-0{% endif %}" {% if not is_popup %}x-bind:class="{'xl:left-0': !sidebarDesktopOpen, 'xl:left-72': sidebarDesktopOpen}" x-bind:style="'width: ' + mainWidth + 'px'"{% endif %}>
5
5
  <div class="backdrop-blur-sm bg-white/80 pb-4 dark:bg-base-900/80 {% if not is_popup %}lg:border-t lg:border-base-200 lg:py-4 relative lg:scrollable-top lg:px-8 dark:border-base-800{% endif %}">
6
- <div class="flex flex-col-reverse gap-3 items-center mx-auto lg:flex-row-reverse">
6
+ <div class="flex flex-col-reverse gap-3 items-center mx-auto lg:flex-row-reverse" x-bind:style="'width: ' + changeFormWidth + 'px'">
7
7
  {% block submit-row %}
8
8
  {% if show_save %}
9
9
  <button type="submit" name="_save" class="bg-primary-600 block border border-transparent font-medium px-3 py-2 rounded text-white w-full lg:w-auto">
@@ -1,4 +1,4 @@
1
- <div class="border flex flex-col flex-grow overflow-hidden p-6 relative rounded shadow-sm dark:border-base-800 {% if class %} {{ class }}{% endif %}">
1
+ <div class="bg-white border flex flex-col flex-grow overflow-hidden p-6 relative rounded shadow-sm dark:bg-base-900 dark:border-base-800 {% if class %} {{ class }}{% endif %}">
2
2
  {% if title %}
3
3
  <h2 class="bg-base-50 border-b font-semibold mb-6 -mt-6 -mx-6 py-4 px-6 text-font-important-light dark:text-font-important-dark dark:border-base-800 dark:bg-white/[.02]">
4
4
  {{ title }}
@@ -1,31 +1,47 @@
1
- {% load unfold %}
1
+ {% load i18n unfold %}
2
2
 
3
- <div class="{% if card_included == 1 %}-m-6{% else %}lg:border lg:rounded lg:shadow-sm{% endif %} overflow-x-auto lg:dark:border-base-800">
4
- <table class="block border-base-200 border-spacing-none border-separate w-full lg:table">
5
- {% if table.headers %}
6
- <thead class="text-base-900 dark:text-base-100">
7
- <tr class="bg-base-50 dark:bg-white/[.02]">
8
- {% for header in table.headers %}
9
- <th class="align-middle font-semibold py-2 text-left text-sm whitespace-nowrap sortable column-description hidden px-3 lg:table-cell {% if card_included == 1 %}first:pl-6 last:pr-6{% endif %}">
10
- {{ header }}
11
- </th>
12
- {% endfor %}
13
- </tr>
14
- </thead>
15
- {% endif %}
3
+ <div class="flex flex-col">
4
+ {% if title %}
5
+ <h3 class="font-semibold mb-1 text-font-important-light text-sm dark:text-font-important-dark">
6
+ {{ title }}
7
+ </h3>
8
+ {% endif %}
9
+
10
+ <div class="{% if card_included == 1 %}-m-6{% else %} bg-white flex flex-col grow lg:border lg:border-base-200 lg:overflow-hidden lg:rounded lg:shadow-sm{% endif %} lg:dark:border-base-800 dark:bg-base-900">
11
+ <div {% if height %}style="max-height: {{ height }}px;" data-simplebar{% endif %}>
12
+ <table class="block border-spacing-none border-separate w-full lg:table">
13
+ {% if table.headers %}
14
+ <thead class="text-base-900 dark:text-base-100 {% if height %}sticky top-0{% endif %}">
15
+ <tr class="bg-base-50 dark:bg-base-900">
16
+ {% for header in table.headers %}
17
+ <th class="align-middle border-b border-base-200 font-semibold py-2 text-left text-sm whitespace-nowrap sortable column-description hidden px-3 lg:table-cell dark:border-base-800 dark:bg-white/[.02] {% if card_included == 1 %}first:pl-6 last:pr-6{% endif %}">
18
+ {{ header|capfirst }}
19
+ </th>
20
+ {% endfor %}
21
+ </tr>
22
+ </thead>
23
+ {% endif %}
16
24
 
17
- {% if table.rows %}
18
- <tbody class="block lg:table-row-group">
19
- {% for row in table.rows %}
20
- <tr class="{% if striped == 1 %}{% cycle '' 'bg-base-50 dark:bg-white/[.02]' %}{% endif %} block {% if not card_included == 1 %}border mb-3 rounded shadow-sm{% else %}border-t{% endif %} lg:table-row lg:border-none lg:mb-0 lg:shadow-none dark:border-base-800">
21
- {% for cell in row %}
22
- <td class="px-3 py-2 align-middle flex border-t border-base-200 font-normal gap-4 min-w-0 overflow-hidden text-left before:flex before:capitalize before:content-[attr(data-label)] before:items-center before:mr-auto first:border-t-0 lg:before:hidden lg:first:border-t lg:py-3 lg:table-cell dark:border-base-800 {% if card_included == 1 %}lg:first:pl-6 lg:last:pr-6{% endif %}" {% if table.headers %}data-label="{{ table.headers|index:forloop.counter0 }}"{% endif %}>
23
- {{ cell }}
24
- </td>
25
+ {% if table.rows %}
26
+ <tbody class="block lg:table-row-group">
27
+ {% for row in table.rows %}
28
+ <tr class="{% if striped == 1 %}{% cycle '' 'bg-base-50 dark:bg-white/[.02]' %}{% endif %} block group {% if forloop.first %}first-row{% endif %} {% if not card_included == 1 %}border mb-3 rounded shadow-sm{% else %}border-b{% endif %} lg:table-row lg:border-none lg:mb-0 lg:shadow-none dark:border-base-800">
29
+ {% for cell in row %}
30
+ <td class="px-3 py-2 align-middle flex border-t border-base-200 font-normal gap-4 min-w-0 overflow-hidden text-left before:flex before:capitalize before:content-[attr(data-label)] before:items-center before:mr-auto first:border-t-0 lg:group-[.first-row]:border-t-0 lg:before:hidden lg:first:border-t lg:py-3 lg:table-cell dark:border-base-800 {% if card_included == 1 %}lg:first:pl-6 lg:last:pr-6{% endif %}" {% if table.headers %}data-label="{{ table.headers|index:forloop.counter0 }}"{% endif %}>
31
+ {{ cell }}
32
+ </td>
33
+ {% endfor %}
34
+ </tr>
25
35
  {% endfor %}
26
- </tr>
27
- {% endfor %}
28
- </tbody>
36
+ </tbody>
37
+ {% endif %}
38
+ </table>
39
+ </div>
40
+
41
+ {% if not table.rows %}
42
+ <p class="bg-white border border-base-200 flex grow items-center justify-center py-2 rounded shadow-sm dark:bg-base-900 lg:border-0 lg:rounded-none lg:shadow-none">
43
+ {% trans "No data" %}
44
+ </p>
29
45
  {% endif %}
30
- </table>
46
+ </div>
31
47
  </div>
@@ -22,7 +22,7 @@
22
22
  {% endif %}
23
23
 
24
24
  {% if cl.has_active_filters %}
25
- <a href="{{ cl.clear_all_filters_qs }}" class="border flex-grow font-medium px-3 py-2 rounded text-center transition-all w-full hover:bg-base-50 lg:w-auto dark:border-base-700 dark:hover:text-base-200">
25
+ <a href="{{ cl.clear_all_filters_qs }}" class="border flex-grow font-medium px-3 py-2 rounded text-center transition-all w-full lg:w-auto dark:border-base-700 dark:hover:text-base-200">
26
26
  {% trans "Clear all filters" %}
27
27
  </a>
28
28
  {% endif %}
@@ -0,0 +1,33 @@
1
+ {% with dropdown_id=instance.pk|cut:"-"|add:field_name %}
2
+ {% with total=value.items|length %}
3
+ <div x-data="{ openDropdownId{{ dropdown_id }}: false }">
4
+ <div class="flex flex-row gap-1.5 items-center {% if total > 0 or value.content %}cursor-pointer{% endif %}" {% if total > 0 or value.content %}x-ref="rowDropdown{{ dropdown_id }}" x-on:click="openDropdownId{{ dropdown_id }} = !openDropdownId{{ dropdown_id }}"{% endif %}>
5
+ {% if total > 0 or value.content %}
6
+ <span class="material-symbols-outlined">unfold_more</span>
7
+ {% endif %}
8
+
9
+ <span>
10
+ {{ value.title }}
11
+ </span>
12
+ </div>
13
+
14
+ {% if total > 0 or value.content %}
15
+ <template x-teleport="body">
16
+ {% if value.content %}
17
+ <div class="bg-white border overflow-y-auto overflow-x-hidden p-3 rounded shadow-lg text-sm top-7 z-50 w-48 dark:bg-base-800 dark:border-base-700" data-simplebar x-cloak x-transition x-anchor.bottom-start.offset.4="$refs.rowDropdown{{ dropdown_id }}" x-show="openDropdownId{{ dropdown_id }}"x-on:click.outside="openDropdownId{{ dropdown_id }} = false" {% if value.width or value.height %}style="{% if value.width %}width: {{ value.width }}px;{% endif %}{% if value.height %}height: {{ value.height }}px;{% endif %}"{% endif %}>
18
+ {{ value.content }}
19
+ </div>
20
+ {% else %}
21
+ <nav class="bg-white border overflow-y-auto overflow-x-hidden flex flex-col py-1 rounded shadow-lg text-sm top-7 z-50 w-48 dark:bg-base-800 dark:border-base-700" data-simplebar x-cloak x-transition x-anchor.bottom-start.offset.4="$refs.rowDropdown{{ dropdown_id }}" x-show="openDropdownId{{ dropdown_id }}"x-on:click.outside="openDropdownId{{ dropdown_id }} = false" {% if value.width or value.height %}style="{% if value.width %}width: {{ value.width }}px;{% endif %}{% if value.height %}height: {{ value.height }}px;{% endif %}"{% endif %}>
22
+ {% for item in value.items %}
23
+ <{% if item.link %}a{% else %}span{% endif %} {% if item.link %}href="{{ item.link }}"{% endif %} class="flex items-center gap-2 mx-1 px-3 py-2 max-h-[30px] rounded hover:bg-base-100 dark:hover:bg-base-700 dark:hover:text-base-200 {% if value.striped %}{% cycle '' 'bg-base-50 dark:bg-white/[.04]' %}{% endif %}">
24
+ <span class="grow truncate">{{ item.title }}</span>
25
+ </{% if item.link %}a{% else %}span{% endif %}>
26
+ {% endfor %}
27
+ </nav>
28
+ {% endif %}
29
+ </template>
30
+ {% endif %}
31
+ </div>
32
+ {% endwith %}
33
+ {% endwith %}
@@ -1,6 +1,6 @@
1
1
  {% if adminform.model_admin.compressed_fields %}
2
2
  <div class="flex flex-col gap-4 py-2 lg:flex-row">
3
- <div class="font-medium flex items-start -ml-2 min-w-32 w-32 lg:min-w-48 lg:w-48">
3
+ <div class="font-medium flex items-start -ml-2 min-w-32 w-32 lg:min-w-56 lg:w-56">
4
4
  {{ field.label_tag }}
5
5
  </div>
6
6
 
@@ -1,4 +1,4 @@
1
- <div class="{% if adminform.model_admin.compressed_fields %}lg:min-w-48 lg:mt-2 lg:w-48{% endif %}">
1
+ <div class="{% if adminform.model_admin.compressed_fields %}lg:min-w-56 lg:mt-2 lg:w-56{% endif %}">
2
2
  {{ field.label_tag }}
3
3
  </div>
4
4
 
@@ -1,3 +1,3 @@
1
- <p class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-base-100 text-base-700 dark:bg-base-500/20 dark:text-base-400">
1
+ <div class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-base-100 text-base-700 dark:bg-base-500/20 dark:text-base-400">
2
2
  {{ message }}
3
- </p>
3
+ </div>
@@ -1,3 +1,3 @@
1
- <p class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400">
1
+ <div class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-400">
2
2
  {{ message }}
3
- </p>
3
+ </div>
@@ -1,3 +1,3 @@
1
- <p class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400">
1
+ <div class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400">
2
2
  {{ message }}
3
- </p>
3
+ </div>
@@ -1,3 +1,3 @@
1
- <p class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-400">
1
+ <div class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-400">
2
2
  {{ message }}
3
- </p>
3
+ </div>
@@ -1,3 +1,3 @@
1
- <div class="flex flex-col max-w-2xl min-w-48 relative w-full">
1
+ <div class="flex flex-col max-w-2xl min-w-56 relative w-full">
2
2
  {% include "django/forms/widgets/input.html" %}
3
3
  </div>
@@ -1,9 +1,5 @@
1
1
  <div class="datetime flex flex-col gap-2 max-w-2xl lg:flex-row lg:group-[.field-row]:flex-row lg:group-[.field-row]:items-center lg:group-[.field-tabular]:flex-row lg:group-[.field-tabular]:items-center">
2
2
  <div class="basis-1/2 flex flex-col lg:group-[.field-row]:flex-row group-[.field-row]:gap-2 lg:group-[.field-tabular]:flex-row group-[.field-tabular]:gap-2">
3
- <div class="font-medium mb-2 group-[.field-row]:mb-0 group-[.field-row]:flex group-[.field-row]:items-center group-[.field-tabular]:mb-0 group-[.field-tabular]:flex group-[.field-tabular]:items-center">
4
- {{ date_label }}
5
- </div>
6
-
7
3
  {% with widget=widget.subwidgets.0 %}
8
4
  <div class="flex flex-col min-w-52 relative group-[.field-row]:flex-grow group-[.field-tabular]:flex-grow">
9
5
  {% include widget.template_name %}
@@ -12,10 +8,6 @@
12
8
  </div>
13
9
 
14
10
  <div class="basis-1/2 flex flex-col lg:ml-auto md:mt-0 lg:group-[.field-row]:flex-row group-[.field-row]:gap-2 lg:group-[.field-tabular]:flex-row group-[.field-tabular]:gap-2">
15
- <div class="font-medium mb-2 group-[.field-row]:mb-0 group-[.field-row]:flex group-[.field-row]:items-center group-[.field-tabular]:mb-0 group-[.field-tabular]:flex group-[.field-tabular]:items-center">
16
- {{ time_label }}
17
- </div>
18
-
19
11
  {% with widget=widget.subwidgets.1 %}
20
12
  <div class="flex flex-col min-w-52 relative group-[.field-row]:flex-grow group-[.field-tabular]:flex-grow">
21
13
  {% include widget.template_name %}
@@ -1,3 +1,3 @@
1
- <div class="flex flex-col max-w-2xl min-w-48 relative w-full">
1
+ <div class="flex flex-col max-w-2xl min-w-56 relative w-full">
2
2
  {% include "django/forms/widgets/input.html" %}
3
3
  </div>
@@ -4,6 +4,7 @@ from typing import Any, Optional, Union
4
4
  from django import template
5
5
  from django.contrib.admin.helpers import AdminForm, Fieldset
6
6
  from django.contrib.admin.views.main import ChangeList
7
+ from django.db.models import Model
7
8
  from django.db.models.options import Options
8
9
  from django.forms import Field
9
10
  from django.http import HttpRequest
@@ -85,6 +86,11 @@ def tab_list(context: RequestContext, page: str, opts: Optional[Options] = None)
85
86
  )
86
87
 
87
88
 
89
+ @register.simple_tag(name="render_section", takes_context=True)
90
+ def render_section(context: Context, section_class, instance: Model) -> str:
91
+ return section_class(context.request, instance).render()
92
+
93
+
88
94
  @register.simple_tag(name="has_nav_item_active")
89
95
  def has_nav_item_active(items: list) -> bool:
90
96
  for item in items:
@@ -214,14 +220,17 @@ class RenderComponentNode(template.Node):
214
220
 
215
221
  if "component_class" in values:
216
222
  values = ComponentRegistry.create_instance(
217
- values["component_class"], request=context.request
223
+ values["component_class"],
224
+ request=context.request if hasattr(context, "request") else None,
218
225
  ).get_context_data(**values)
219
226
 
220
227
  if self.include_context:
221
228
  values.update(context.flatten())
222
229
 
223
230
  return render_to_string(
224
- self.template_name, request=context.request, context=values
231
+ self.template_name,
232
+ request=context.request if hasattr(context, "request") else None,
233
+ context=values,
225
234
  )
226
235
 
227
236
 
@@ -360,6 +369,7 @@ def fieldset_row_classes(context: Context) -> str:
360
369
  [
361
370
  "lg:border-b",
362
371
  "lg:border-base-200",
372
+ "lg:border-dashed",
363
373
  "dark:lg:border-base-800",
364
374
  "last:lg:border-b-0",
365
375
  ]
@@ -388,7 +398,8 @@ def fieldset_line_classes(context: Context) -> str:
388
398
  "flex-col",
389
399
  "flex-grow",
390
400
  "group/line",
391
- "p-3",
401
+ "px-3",
402
+ "py-2.5",
392
403
  ]
393
404
  field = context.get("field")
394
405
  adminform = context.get("adminform")
@@ -408,6 +419,7 @@ def fieldset_line_classes(context: Context) -> str:
408
419
  [
409
420
  "border-b",
410
421
  "border-base-200",
422
+ "border-dashed",
411
423
  "group-[.last]/row:border-b-0",
412
424
  "lg:border-b-0",
413
425
  "lg:border-l",
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ from collections.abc import Generator
2
3
  from typing import Any, Optional, Union
3
4
 
4
5
  from django.contrib.admin.templatetags.admin_list import (
@@ -16,8 +17,8 @@ from django.contrib.admin.views.main import (
16
17
  )
17
18
  from django.core.exceptions import ObjectDoesNotExist
18
19
  from django.db import models
20
+ from django.db.models import Model
19
21
  from django.forms import Form
20
- from django.http import HttpRequest
21
22
  from django.template import Library
22
23
  from django.template.base import Parser, Token
23
24
  from django.template.loader import render_to_string
@@ -26,13 +27,14 @@ from django.utils.html import format_html
26
27
  from django.utils.safestring import SafeText, mark_safe
27
28
  from django.utils.translation import gettext_lazy as _
28
29
 
29
- from ..utils import (
30
+ from unfold.utils import (
31
+ display_for_dropdown,
30
32
  display_for_field,
31
33
  display_for_header,
32
34
  display_for_label,
33
35
  display_for_value,
34
36
  )
35
- from ..widgets import UnfoldBooleanWidget
37
+ from unfold.widgets import UnfoldBooleanWidget
36
38
 
37
39
  register = Library()
38
40
 
@@ -188,11 +190,9 @@ def result_headers(cl):
188
190
  }
189
191
 
190
192
 
191
- def items_for_result(cl: ChangeList, result: HttpRequest, form) -> SafeText:
192
- """
193
- Generate the actual list of data.
194
- """
195
-
193
+ def items_for_result(
194
+ cl: ChangeList, result: Model, form
195
+ ) -> Generator[SafeText, None, None]:
196
196
  def link_in_col(is_first: bool, field_name: str, cl: ChangeList) -> bool:
197
197
  if cl.list_display_links is None:
198
198
  return False
@@ -225,9 +225,14 @@ def items_for_result(cl: ChangeList, result: HttpRequest, form) -> SafeText:
225
225
  boolean = getattr(attr, "boolean", False)
226
226
  label = getattr(attr, "label", False)
227
227
  header = getattr(attr, "header", False)
228
+ dropdown = getattr(attr, "dropdown", False)
228
229
 
229
230
  if label:
230
231
  result_repr = display_for_label(value, empty_value_display, label)
232
+ elif dropdown:
233
+ result_repr = display_for_dropdown(
234
+ result, field_name, value, empty_value_display
235
+ )
231
236
  elif header:
232
237
  result_repr = display_for_header(value, empty_value_display)
233
238
  else:
@@ -336,23 +341,22 @@ def items_for_result(cl: ChangeList, result: HttpRequest, form) -> SafeText:
336
341
 
337
342
  class UnfoldResultList(ResultList):
338
343
  def __init__(
339
- self, instance_pk: Union[int, str], form: Optional[Form], *items: Any
344
+ self,
345
+ instance: Model,
346
+ form: Optional[Form],
347
+ *items: Any,
340
348
  ) -> None:
341
- self.instance_pk = instance_pk
349
+ self.instance = instance
342
350
  super().__init__(form, *items)
343
351
 
344
352
 
345
353
  def results(cl: ChangeList):
346
354
  if cl.formset:
347
355
  for res, form in zip(cl.result_list, cl.formset.forms):
348
- pk = cl.lookup_opts.pk.attname
349
- pk_value = getattr(res, pk)
350
- yield UnfoldResultList(pk_value, form, items_for_result(cl, res, form))
356
+ yield UnfoldResultList(res, form, items_for_result(cl, res, form))
351
357
  else:
352
358
  for res in cl.result_list:
353
- pk = cl.lookup_opts.pk.attname
354
- pk_value = getattr(res, pk)
355
- yield UnfoldResultList(pk_value, None, items_for_result(cl, res, None))
359
+ yield UnfoldResultList(res, None, items_for_result(cl, res, None))
356
360
 
357
361
 
358
362
  def result_list(context: dict[str, Any], cl: ChangeList) -> dict[str, Any]:
unfold/utils.py CHANGED
@@ -6,6 +6,7 @@ from typing import Any, Optional
6
6
 
7
7
  from django.conf import settings
8
8
  from django.db import models
9
+ from django.db.models import Model
9
10
  from django.template.loader import render_to_string
10
11
  from django.utils import formats, timezone
11
12
  from django.utils.hashable import make_hashable
@@ -33,6 +34,19 @@ def display_for_header(value: Iterable, empty_value_display: str) -> SafeText:
33
34
  )
34
35
 
35
36
 
37
+ def display_for_dropdown(
38
+ result: Model, field_name: str, value: Iterable, empty_value_display: str
39
+ ) -> SafeText:
40
+ return render_to_string(
41
+ "unfold/helpers/display_dropdown.html",
42
+ {
43
+ "instance": result,
44
+ "field_name": field_name,
45
+ "value": value,
46
+ },
47
+ )
48
+
49
+
36
50
  def display_for_label(value: Any, empty_value_display: str, label: Any) -> SafeText:
37
51
  label_type = None
38
52
  multiple = False
unfold/widgets.py CHANGED
@@ -104,12 +104,9 @@ TEXTAREA_CLASSES = [
104
104
  ]
105
105
 
106
106
  TEXTAREA_EXPANDABLE_CLASSES = [
107
- "absolute",
108
- "bottom-0",
109
- "left-0",
110
- "right-0",
111
- "top-0",
112
- "h-full",
107
+ "block",
108
+ "field-sizing-content",
109
+ "!max-w-2xl",
113
110
  ]
114
111
 
115
112
  SELECT_CLASSES = [
@@ -472,9 +469,7 @@ class UnfoldAdminTextareaWidget(AdminTextareaWidget):
472
469
  )
473
470
 
474
471
 
475
- class UnfoldAdminExpandableTextareaWidget(AdminTextareaWidget):
476
- template_name = "unfold/widgets/textarea_expandable.html"
477
-
472
+ class UnfoldAdminExpandableTextareaWidget(UnfoldAdminTextareaWidget):
478
473
  def __init__(self, attrs: Optional[dict[str, Any]] = None) -> None:
479
474
  attrs = attrs or {}
480
475
 
@@ -499,7 +494,10 @@ class UnfoldAdminSplitDateTimeWidget(AdminSplitDateTime):
499
494
  template_name = "unfold/widgets/split_datetime.html"
500
495
 
501
496
  def __init__(self, attrs: Optional[dict[str, Any]] = None) -> None:
502
- widgets = [UnfoldAdminDateWidget, UnfoldAdminTimeWidget]
497
+ widgets = [
498
+ UnfoldAdminDateWidget(attrs={"placeholder": _("Date")}),
499
+ UnfoldAdminTimeWidget(attrs={"placeholder": _("Time")}),
500
+ ]
503
501
  MultiWidget.__init__(self, widgets, attrs)
504
502
 
505
503
 
@@ -1,7 +0,0 @@
1
- <div class="relative">
2
- <div class="border border-base-200 break-words font-medium invisible max-w-4xl px-3 py-2 text-sm" style="min-height: 64px">
3
- {% if widget.value %}{{ widget.value|linebreaks }}{% endif %}
4
- </div>
5
-
6
- <textarea onInput="this.previousElementSibling.innerText = this.value + String.fromCharCode(10)" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
7
- </div>