Skip to content

Commit 289b7b8

Browse files
authored
feat: fixed sorting in stacked inlines and added option to collapse i… (#83)
* feat: fixed sorting in stacked inlines and added option to collapse individual stacked inlines and also global button to toggle collapse Added option SBAdminStackedInline.default_collapsed: bool * feat: fixed sorting in stacked inlines and added option to collapse individual stacked inlines and also global button to toggle collapse Added option SBAdminStackedInline.default_collapsed: bool * feat: fixed sorting in stacked inlines and added option to collapse individual stacked inlines and also global button to toggle collapse Added option SBAdminStackedInline.default_collapsed: bool * feat: fixed sorting in stacked inlines and added option to collapse individual stacked inlines and also global button to toggle collapse Added option SBAdminStackedInline.default_collapsed: bool
1 parent 1bb3158 commit 289b7b8

File tree

9 files changed

+199
-29
lines changed

9 files changed

+199
-29
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-smartbase-admin"
3-
version = "1.0.25"
3+
version = "1.0.26"
44
description = ""
55
authors = ["SmartBase <[email protected]>"]
66
readme = "README.md"

src/django_smartbase_admin/admin/admin_base.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,13 +1147,36 @@ class SBAdminGenericTableInlinePaginated(SBAdminGenericTableInline):
11471147
per_page = 50
11481148

11491149

1150-
class SBAdminStackedInline(SBAdminInline, NestedStackedInline):
1150+
class SBAdminStackedInlineBase(SBAdminInline):
1151+
default_collapsed = False
1152+
1153+
def get_sbadmin_default_collapsed(self, request):
1154+
return self.default_collapsed
1155+
1156+
def get_context_data(self, request) -> dict[str, Any]:
1157+
context_data = super().get_context_data(request)
1158+
context_data["default_collapsed"] = self.get_sbadmin_default_collapsed(request)
1159+
return context_data
1160+
1161+
def get_sbadmin_inline_list_actions(self, request) -> list:
1162+
actions = super().get_sbadmin_inline_list_actions(request)
1163+
actions.append(
1164+
SBAdminCustomAction(
1165+
title="Collapse",
1166+
css_class=f"collapse-all-stacked-inlines {'collapsed' if self.get_sbadmin_default_collapsed(request) else ''}",
1167+
url=request.get_full_path(),
1168+
)
1169+
)
1170+
return actions
1171+
1172+
1173+
class SBAdminStackedInline(SBAdminStackedInlineBase, NestedStackedInline):
11511174
template = "sb_admin/inlines/stacked_inline.html"
11521175
fieldset_template = "sb_admin/includes/inline_fieldset.html"
11531176
formset = SBAdminNestedInlineFormSet
11541177

11551178

1156-
class SBAdminGenericStackedInline(SBAdminInline, NestedGenericStackedInline):
1179+
class SBAdminGenericStackedInline(SBAdminStackedInlineBase, NestedGenericStackedInline):
11571180
template = "sb_admin/inlines/stacked_inline.html"
11581181
fieldset_template = "sb_admin/includes/inline_fieldset.html"
11591182
formset = SBAdminGenericInlineFormSet
101 Bytes
Binary file not shown.

src/django_smartbase_admin/locale/sk/LC_MESSAGES/django.po

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ msgid ""
77
msgstr ""
88
"Project-Id-Version: \n"
99
"Report-Msgid-Bugs-To: \n"
10-
"POT-Creation-Date: 2025-07-29 12:46+0200\n"
10+
"POT-Creation-Date: 2025-09-11 16:32+0200\n"
1111
"PO-Revision-Date: 2023-11-15 15:21+0100\n"
1212
"Last-Translator: \n"
1313
"Language-Team: \n"
@@ -481,6 +481,9 @@ msgstr "alebo uložiť ako"
481481
msgid "Cancel"
482482
msgstr "Zrušiť"
483483

484+
msgid "Calendar"
485+
msgstr "Kalendár"
486+
484487
msgid "Yes"
485488
msgstr "Áno"
486489

@@ -731,6 +734,12 @@ msgstr "Posunúť nižšie"
731734
msgid "Reorder"
732735
msgstr "Preusporiadať"
733736

737+
msgid "Expand"
738+
msgstr "Rozbaliť"
739+
740+
msgid "Collapse"
741+
msgstr "Zbaliť"
742+
734743
#, python-brace-format
735744
msgid "<strong>${from} - ${to}</strong> of <strong>${total}</strong><span class=\"max-xs:hidden\"> items</span>"
736745
msgstr "<strong>${from} - ${to}</strong> z <strong>${total}</strong><span class=\"max-xs:hidden\"> položiek</span>"

src/django_smartbase_admin/static/sb_admin/src/css/_components.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88

99
@layer components {
1010
.card {
11+
--level: 1;
1112
@apply bg-bg-elevated sm:rounded border border-dark-200 p-20 md:p-24 max-xs:border-b-0 max-xs:py-32 min-w-0;
13+
--mix: clamp(0%, calc((var(--level) - 1) / 3 * 100%), 100%);
14+
background-color: color-mix(in oklch, var(--color-bg-elevated), var(--color-bg) var(--mix));
15+
1216
>header {
1317
@apply flex items-center text-18 mb-24 font-semibold;
1418
}
@@ -18,6 +22,10 @@
1822
}
1923
}
2024
}
25+
.card.level-1 { --level: 1; }
26+
.card.level-2 { --level: 2; }
27+
.card.level-3 { --level: 3; }
28+
.card.level-4 { --level: 4; }
2129
.column-widget-columns {
2230
& > li {
2331
@apply relative px-12 py-8 flex items-center;

src/django_smartbase_admin/static/sb_admin/src/css/_inlines.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,14 @@ fieldset.module > header {
200200
z-index: 1;
201201
}
202202

203+
.js-collapse-stacked-inline svg {
204+
transition: transform 0.3s ease-in-out;
205+
}
206+
207+
.js-collapse-stacked-inline[aria-expanded="true"] svg {
208+
transform: rotate(0deg);
209+
}
210+
211+
.js-collapse-stacked-inline[aria-expanded="false"] svg {
212+
transform: rotate(-90deg);
213+
}

src/django_smartbase_admin/static/sb_admin/src/js/main.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Collapse from 'bootstrap/js/dist/collapse'
33
import Tab from 'bootstrap/js/dist/tab'
44
import Modal from 'bootstrap/js/dist/modal'
55
import Tooltip from 'bootstrap/js/dist/tooltip'
6+
import debounce from 'lodash/debounce'
67

78
// remove Modal focus trap to fix interaction with fields in modals inside another modal
89
Modal.prototype._initializeFocusTrap = function () {
@@ -100,10 +101,12 @@ class Main {
100101
this.saveState(e)
101102
this.fileDownload(e)
102103
this.passwordToggleFnc(e)
104+
this.collapseStackedInlineButtons(e)
103105
})
104106
this.initFileInputs()
105107
this.initAliasName()
106108
this.handleLocationHashFromTabs()
109+
this.initCollapseEventListeners()
107110
}
108111

109112
isDarkMode() {
@@ -397,6 +400,8 @@ class Main {
397400
fieldElem.dispatchEvent(new CustomEvent('clear', {detail: {refresh: true}}))
398401
}
399402

403+
404+
400405
executeListAction(table_id, action_url, no_params, open_in_new_tab = false) {
401406
if (window.SBAdminTable && window.SBAdminTable[table_id]) {
402407
window.SBAdminTable[table_id].executeListAction(action_url, no_params, open_in_new_tab)
@@ -408,6 +413,108 @@ class Main {
408413
}
409414
}
410415
}
416+
isCurrentlyCollapsed(parentWrapper) {
417+
const collapseElements = parentWrapper.querySelectorAll('.collapse')
418+
return Array.from(collapseElements).every(el => {
419+
if(el.closest('.djn-empty-form')) {
420+
return true
421+
}
422+
return !el.classList.contains('show')
423+
})
424+
}
425+
426+
updateCollapseAllButton(parentWrapper) {
427+
const collapseAll = parentWrapper.querySelector('.collapse-all-stacked-inlines')
428+
if (!collapseAll) return
429+
430+
const isCollapsed = this.isCurrentlyCollapsed(parentWrapper)
431+
const expandText = `<svg class="mr-8"><use xlink:href="#View-grid-list"></use></svg><span>${window.sb_admin_translation_strings["expand"]}</span>`
432+
const collapseText = `<svg class="mr-8"><use xlink:href="#List-checkbox"></use></svg><span>${window.sb_admin_translation_strings["collapse"]}</span>`
433+
434+
if (isCollapsed) {
435+
collapseAll.classList.add('collapsed')
436+
collapseAll.innerHTML = expandText
437+
} else {
438+
collapseAll.classList.remove('collapsed')
439+
collapseAll.innerHTML = collapseText
440+
}
441+
}
442+
443+
initCollapseEventListeners() {
444+
this.initCollapseAllButtons()
445+
446+
const debouncedUpdateCollapseAllButton = debounce((parentWrapper) => {
447+
this.updateCollapseAllButton(parentWrapper)
448+
}, 50)
449+
450+
document.addEventListener('shown.bs.collapse', (e) => {
451+
const parentWrapper = e.target.closest('.djn-fieldset')
452+
if (parentWrapper) {
453+
debouncedUpdateCollapseAllButton(parentWrapper)
454+
}
455+
})
456+
457+
document.addEventListener('hidden.bs.collapse', (e) => {
458+
const parentWrapper = e.target.closest('.djn-fieldset')
459+
if (parentWrapper) {
460+
debouncedUpdateCollapseAllButton(parentWrapper)
461+
}
462+
})
463+
}
464+
465+
466+
467+
initCollapseAllButtons() {
468+
const collapseAllButtons = document.querySelectorAll('.collapse-all-stacked-inlines')
469+
collapseAllButtons.forEach(button => {
470+
const parentWrapper = button.closest('.djn-fieldset')
471+
if (parentWrapper) {
472+
this.updateCollapseAllButton(parentWrapper)
473+
}
474+
})
475+
}
476+
477+
collapseStackedInlineButtons(event) {
478+
const collapseStackedInline = event.target.closest('.js-collapse-stacked-inline')
479+
if(collapseStackedInline) {
480+
const collapseEl = event.target.closest('.djn-inline-form').querySelector('.collapse')
481+
const instance = Collapse.getOrCreateInstance(collapseEl)
482+
instance.toggle()
483+
collapseStackedInline.setAttribute('aria-expanded', collapseStackedInline.getAttribute('aria-expanded') !== 'true')
484+
}
485+
486+
const collapseAll = event.target.closest('.collapse-all-stacked-inlines')
487+
if (collapseAll) {
488+
event.preventDefault()
489+
const parentWrapper = collapseAll.closest('.djn-fieldset')
490+
const collapseElements = parentWrapper.querySelectorAll('.collapse')
491+
const collapseTriggers = parentWrapper.querySelectorAll('.js-collapse-stacked-inline')
492+
const isCurrentlyCollapsed = this.isCurrentlyCollapsed(parentWrapper)
493+
494+
collapseTriggers.forEach(el => {
495+
if(el.closest('.djn-empty-form')) {
496+
return
497+
}
498+
if (isCurrentlyCollapsed) {
499+
el.setAttribute('aria-expanded', 'true')
500+
} else {
501+
el.setAttribute('aria-expanded', 'false')
502+
}
503+
})
504+
505+
collapseElements.forEach(el => {
506+
if(el.closest('.djn-empty-form')) {
507+
return
508+
}
509+
const instance = Collapse.getOrCreateInstance(el)
510+
if (isCurrentlyCollapsed) {
511+
instance.show()
512+
} else {
513+
instance.hide()
514+
}
515+
})
516+
}
517+
}
411518
}
412519

413520
window.addEventListener('DOMContentLoaded', () => {

src/django_smartbase_admin/templates/sb_admin/inlines/stacked_inline.html

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@ <h2 class="stacked-inline-heading">
5151
data-is-initial="{% if inline_admin_form.pk_field.field.value %}true{% else %}false{% endif %}"
5252
{% endif %}
5353
id="{{ inline_admin_formset.formset.prefix }}-{% if forloop.last and inline_admin_formset.has_add_permission %}empty{% else %}{{ inline_admin_form.form|form_index }}{% endif %}">
54-
<div class="card p-0 sm:mb-24 {% if inline_admin_form.formset.nesting_depth > 1 %}border-0{% endif %}">
55-
{% if not inline_opts.sortable_options or not inline_opts.sortable_options.disabled %}<div class="djn-drag-handler"></div>{% endif %}
56-
<div class="p-24 pb-0">
54+
<div class="djn-tools card level-{{ inline_admin_form.formset.nesting_depth }} p-0 sm:mb-24 {% if inline_admin_form.formset.nesting_depth > 1 %}border-0{% endif %}">
55+
<div class="p-24">
5756
{# <b>{{ inline_admin_formset.opts.verbose_name|capfirst }}:</b>&nbsp;#}
5857
<div class="flex items-center">
58+
{% if not inline_opts.sortable_options or not inline_opts.sortable_options.disabled %}
59+
<div class="drag-handler btn btn-empty cursor-move static transform-none mr-8">
60+
<svg class="w-20 h-20 text-dark-400"><use xlink:href="#Drag"></use></svg>
61+
</div>
62+
{% endif %}
5963
<span class="text-dark-900 font-semibold">{% if inline_admin_form.original %}{{ inline_admin_form.original }}{% else %}#{{ forloop.counter }}{% endif %}</span>
6064
<div class="ml-auto">
6165
{% if inline_admin_form.original and inline_admin_form.model_admin.show_change_link and inline_admin_form.model_admin.has_registered_model %}
@@ -64,8 +68,8 @@ <h2 class="stacked-inline-heading">
6468
</a>
6569
{% endif %}
6670
{% if inline_admin_form.show_url %}<a href="{{ inline_admin_form.absolute_url }}" class="ml-8">{% trans "View on site" %}</a>{% endif %}
67-
{% if inline_admin_formset.formset.can_delete %}
68-
<div class="ml-8">
71+
<div class="ml-8 flex items-center gap-8">
72+
{% if inline_admin_formset.formset.can_delete %}
6973
{% if inline_admin_form.original %}
7074
<div class="delete djn-delete-handler {{ inline_admin_formset.handler_classes|join:" " }}">
7175
<div class="relative flex items-center h-40">
@@ -78,8 +82,14 @@ <h2 class="stacked-inline-heading">
7882
{% else %}
7983
<span><a class="inline-deletelink djn-remove-handler {{ inline_admin_formset.handler_classes|join:" " }}" href="javascript:void(0)">{% trans 'Delete' %}</a></span>
8084
{% endif %}
81-
</div>
82-
{% endif %}
85+
{% endif %}
86+
<div class="w-2 bg-dark-200 self-stretch"></div>
87+
<button type="button" class="btn btn-empty js-collapse-stacked-inline" aria-expanded="{{ context_data.default_collapsed|yesno:"false,true" }}">
88+
<svg class="w-20 h-20 text-dark-400">
89+
<use xlink:href="#Down"></use>
90+
</svg>
91+
</button>
92+
</div>
8393
</div>
8494
</div>
8595

@@ -90,25 +100,25 @@ <h2 class="stacked-inline-heading">
90100
{% endif %}
91101
</div>
92102

93-
94-
{% for fieldset in inline_admin_form %}
95-
<div class="p-24 stacked-row{% if not forloop.first %} border-t border-dark-200{% endif %}">
96-
{% include inline_admin_formset.opts.fieldset_template %}
97-
</div>
98-
{% endfor %}
99-
{% if inline_admin_form.has_auto_field or inline_admin_form.needs_explicit_pk_field %}
100-
{{ inline_admin_form.pk_field.field }}
101-
{% endif %}
102-
{% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %}
103+
<div class="collapse flex flex-col {% if not context_data.default_collapsed %}show{% endif %}">
104+
{% for fieldset in inline_admin_form %}
105+
<div class="pt-0 pb-24 px-24 stacked-row{% if not forloop.first %} border-t border-dark-200{% endif %}">
106+
{% include inline_admin_formset.opts.fieldset_template %}
107+
</div>
108+
{% endfor %}
109+
{% if inline_admin_form.has_auto_field or inline_admin_form.needs_explicit_pk_field %}
110+
{{ inline_admin_form.pk_field.field }}
111+
{% endif %}
112+
{% if inline_admin_form.fk_field %}{{ inline_admin_form.fk_field.field }}{% endif %}
113+
{% if inline_admin_form.form.inlines %}
114+
{% for nested in inline_admin_form.form.inlines %}
115+
<div class="card level-{{ nested.formset.nesting_depth }} p-0 mx-24 sm:my-24">
116+
{% include nested.opts.template with inline_admin_formset=nested %}
117+
</div>
118+
{% endfor %}
119+
{% endif %}
120+
</div>
103121
</div>
104-
{% if inline_admin_form.form.inlines %}
105-
{% for nested in inline_admin_form.form.inlines %}
106-
<div class="card p-0 sm:mb-24 {% if nested.formset.nesting_depth > 2 %}border-x-0 rounded-none{% endif %}">
107-
{% include nested.opts.template with inline_admin_formset=nested %}
108-
</div>
109-
{% endfor %}
110-
{% endif %}
111-
112122
</div>
113123
{% endfor %}
114124
{% endwith %}

src/django_smartbase_admin/templates/sb_admin/sb_admin_js_trans.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
window.sb_admin_translation_strings["up"] = '{% trans "Move up" %}';
77
window.sb_admin_translation_strings["down"] = '{% trans "Move down" %}';
88
window.sb_admin_translation_strings["reorder"] = '{% trans "Reorder" %}';
9+
window.sb_admin_translation_strings["expand"] = '{% trans "Expand" %}';
10+
window.sb_admin_translation_strings["collapse"] = '{% trans "Collapse" %}';
911
window.sb_admin_translation_strings["page"] = '{% blocktrans %}<strong>${from} - ${to}</strong> of <strong>${total}</strong><span class="max-xs:hidden"> items</span>{% endblocktrans %}'
1012
window.sb_admin_translation_strings["page_empty"] = '{% blocktrans %}<strong>0</strong> items{% endblocktrans %}'
1113
window.sb_admin_translation_strings["selected"] = '{% blocktrans %}${value} selected{% endblocktrans %}'

0 commit comments

Comments
 (0)