django-smartbase-admin 1.0.5__py3-none-any.whl → 1.0.6b2__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 (25) hide show
  1. django_smartbase_admin/admin/admin_base.py +142 -35
  2. django_smartbase_admin/admin/widgets.py +63 -1
  3. django_smartbase_admin/engine/admin_base_view.py +30 -21
  4. django_smartbase_admin/engine/configuration.py +30 -21
  5. django_smartbase_admin/static/sb_admin/dist/main.js +1 -1
  6. django_smartbase_admin/static/sb_admin/dist/main_style.css +1 -1
  7. django_smartbase_admin/static/sb_admin/sprites/sb_admin/Caution.svg +3 -0
  8. django_smartbase_admin/static/sb_admin/src/css/components/_input.css +19 -15
  9. django_smartbase_admin/static/sb_admin/src/js/autocomplete.js +18 -0
  10. django_smartbase_admin/static/sb_admin/src/js/choices.js +8 -0
  11. django_smartbase_admin/static/sb_admin/src/js/datepicker.js +7 -5
  12. django_smartbase_admin/static/sb_admin/src/js/main.js +64 -26
  13. django_smartbase_admin/static/sb_admin/src/js/range.js +3 -2
  14. django_smartbase_admin/templates/sb_admin/actions/change_form.html +75 -36
  15. django_smartbase_admin/templates/sb_admin/inlines/table_inline.html +49 -33
  16. django_smartbase_admin/templates/sb_admin/sprites/sb_admin.svg +1 -1
  17. django_smartbase_admin/templates/sb_admin/widgets/autocomplete.html +13 -2
  18. django_smartbase_admin/templates/sb_admin/widgets/date.html +1 -1
  19. django_smartbase_admin/templates/sb_admin/widgets/includes/related_item_buttons.html +31 -0
  20. django_smartbase_admin/templates/sb_admin/widgets/time.html +1 -1
  21. django_smartbase_admin/utils.py +5 -0
  22. {django_smartbase_admin-1.0.5.dist-info → django_smartbase_admin-1.0.6b2.dist-info}/METADATA +2 -2
  23. {django_smartbase_admin-1.0.5.dist-info → django_smartbase_admin-1.0.6b2.dist-info}/RECORD +25 -23
  24. {django_smartbase_admin-1.0.5.dist-info → django_smartbase_admin-1.0.6b2.dist-info}/LICENSE.md +0 -0
  25. {django_smartbase_admin-1.0.5.dist-info → django_smartbase_admin-1.0.6b2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2
+ <path d="M7.99999 1C8.23789 1 8.45795 1.12713 8.57714 1.33301L15.9102 13.999C16.0295 14.2052 16.03 14.4596 15.9111 14.666C15.7921 14.8725 15.5713 15 15.333 15H0.666987C0.428667 15 0.207898 14.8725 0.0888618 14.666C-0.0299797 14.4596 -0.0295034 14.2052 0.0898383 13.999L7.42285 1.33301C7.54204 1.12713 7.7621 1 7.99999 1ZM7.99999 11C7.63202 11 7.33335 11.2981 7.333 11.666V12C7.33313 12.3681 7.63189 12.666 7.99999 12.666C8.3681 12.666 8.66685 12.3681 8.66699 12V11.666C8.66663 11.2981 8.36797 11 7.99999 11ZM7.99902 5.66699C7.6311 5.66753 7.33283 5.96605 7.333 6.33398L7.33593 9.66699C7.33625 10.035 7.63492 10.3331 8.00292 10.333C8.37106 10.3327 8.66916 10.0341 8.66894 9.66602L8.66699 6.33301C8.66655 5.96493 8.36713 5.66669 7.99902 5.66699Z"/>
3
+ </svg>
@@ -235,22 +235,26 @@
235
235
  }
236
236
  }
237
237
  }
238
- &-delete {
239
- + label {
240
- @apply text-dark-300 transition-colors;
241
- @apply p-8 h-full w-full flex-center;
242
- &:before,
243
- &:after {
244
- @apply hidden;
245
- }
246
- &:hover {
247
- @apply text-dark-400;
248
- }
238
+ }
239
+
240
+ input.checkbox-delete {
241
+ + label {
242
+ @apply text-dark-300 transition-colors;
243
+ @apply p-8 h-full w-full flex-center;
244
+
245
+ &:before,
246
+ &:after {
247
+ @apply hidden;
249
248
  }
250
- &:checked {
251
- + label {
252
- @apply text-negative;
253
- }
249
+
250
+ &:hover {
251
+ @apply text-dark-400;
252
+ }
253
+ }
254
+
255
+ &:checked {
256
+ + label {
257
+ @apply text-negative;
254
258
  }
255
259
  }
256
260
  }
@@ -25,6 +25,10 @@ export default class Autocomplete {
25
25
  this.handleDynamiclyAddedAutocomplete(document.getElementById('sb-admin-modal'))
26
26
  })
27
27
  this.handleDynamiclyAddedAutocomplete(document)
28
+ document.body.addEventListener('sbadmin:modal-change-form-response', (event) => {
29
+ const {field, id, label} = event.detail
30
+ this.selectAutocompleteItem(field, id, label)
31
+ })
28
32
  }
29
33
 
30
34
  handleDynamiclyAddedAutocomplete(el) {
@@ -88,6 +92,10 @@ export default class Autocomplete {
88
92
  choicesJS.SBhasNextPage = true
89
93
  this.search(choicesJS.SBcurrentSearchTerm, choicesJS, inputEl, autocompleteData, choicesJS.SBcurrentPage)
90
94
  }, 200))
95
+
96
+ choiceInput.addEventListener('selectItem', (event) => {
97
+ choicesJSListeners.selectItem(event.detail, inputEl)
98
+ })
91
99
  choiceInput.addEventListener('addItem', () => {
92
100
  choicesJSListeners.addItem(choicesJS, inputEl)
93
101
  })
@@ -277,4 +285,14 @@ export default class Autocomplete {
277
285
  choicesJS.SBcurrentPage = requestedPage
278
286
  })
279
287
  }
288
+
289
+ selectAutocompleteItem(field, id, label) {
290
+ const selectEl = document.querySelector(`select.js-autocomplete[data-autocomplete-data-id="${field}_data"]`)
291
+ if (selectEl) {
292
+ selectEl.dispatchEvent(new CustomEvent('selectItem', {
293
+ detail: {value: id, label: label},
294
+ }))
295
+ document.getElementById(`${field}-value`).textContent = label
296
+ }
297
+ }
280
298
  }
@@ -50,6 +50,14 @@ export const choicesJSOptions = (choiceInput) => ({
50
50
 
51
51
 
52
52
  export const choicesJSListeners = {
53
+ 'selectItem': (item, inputEl) => {
54
+ if (!item) return
55
+ const choiceValue = [{
56
+ value: item.value,
57
+ label: item.label
58
+ }]
59
+ inputEl.value = JSON.stringify(choiceValue)
60
+ },
53
61
  'addItem': (choicesJS, inputEl) => {
54
62
  const choiceValue = []
55
63
  let choicesJSValue = choicesJS.getValue()
@@ -8,7 +8,8 @@ import {
8
8
 
9
9
 
10
10
  export default class Datepicker {
11
- constructor() {
11
+ constructor(target) {
12
+ target = target || document
12
13
  let documentLocale = document.documentElement.lang || 'default'
13
14
  documentLocale = documentLocale.split('-')[0]
14
15
  if (documentLocale === 'en') {
@@ -16,10 +17,11 @@ export default class Datepicker {
16
17
  }
17
18
  flatpickr.localize(this.getLocale(documentLocale))
18
19
  this.initWidgets()
19
-
20
- document.addEventListener('formset:added', (e) => {
21
- this.initWidgets(e.target)
22
- })
20
+ if(target === document) {
21
+ document.addEventListener('formset:added', (e) => {
22
+ this.initWidgets(e.target)
23
+ })
24
+ }
23
25
  }
24
26
 
25
27
  getLocale(locale) {
@@ -5,7 +5,13 @@ import Modal from 'bootstrap/js/dist/modal'
5
5
  import Tooltip from 'bootstrap/js/dist/tooltip'
6
6
 
7
7
  // remove Modal focus trap to fix interaction with fields in modals inside another modal
8
- Modal.prototype._initializeFocusTrap = function () { return { activate: function () { }, deactivate: function () { } } }
8
+ Modal.prototype._initializeFocusTrap = function () {
9
+ return {
10
+ activate: function () {
11
+ }, deactivate: function () {
12
+ }
13
+ }
14
+ }
9
15
 
10
16
  window.bootstrap5 = {
11
17
  Modal: Modal,
@@ -31,7 +37,7 @@ class Main {
31
37
  const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
32
38
  tooltipTriggerList.map((tooltipTriggerEl) => {
33
39
  const tooltipEl = tooltipTriggerEl.closest('.js-tooltip')
34
- if(tooltipEl) {
40
+ if (tooltipEl) {
35
41
  return new Tooltip(tooltipTriggerEl, {container: tooltipEl})
36
42
  }
37
43
  return null
@@ -49,10 +55,10 @@ class Main {
49
55
  window.open(e.detail.url, e.detail?.target || '_blank')
50
56
  })
51
57
 
52
- if(window.htmx){
58
+ if (window.htmx) {
53
59
  window.htmx.on("htmx:afterSwap", (detail) => {
54
60
  const requestEl = detail.detail.requestConfig.elt.closest('[hx-swap]')
55
- if(requestEl && requestEl.getAttribute('hx-swap') === "none") {
61
+ if (requestEl && requestEl.getAttribute('hx-swap') === "none") {
56
62
  // do not process afterSwap if none swap is performed
57
63
  // this should prevent double processing of afterSwap for first oob-swapped element
58
64
  // which in case of hx-swap=none is returned here in the detail.target
@@ -60,16 +66,18 @@ class Main {
60
66
  }
61
67
  this.initFileInputs(detail.target)
62
68
  this.initDropdowns(detail.target)
69
+ this.initInputs(detail.target)
70
+ this.autocomplete.handleDynamiclyAddedAutocomplete(detail.target)
71
+ this.initInlines(detail.target)
72
+ this.initCKEditor(detail.target)
63
73
  })
64
74
  }
65
75
 
66
76
  new Sidebar()
67
- this.datepicker = new Datepicker()
68
- this.range = new Range()
77
+ this.initInputs()
69
78
  new Sorting()
70
79
  this.autocomplete = new Autocomplete()
71
80
  new ChoicesJS()
72
- this.multiselect = new Multiselect()
73
81
  document.addEventListener('click', (e) => {
74
82
  this.closeAlert(e)
75
83
  this.selectAll(e)
@@ -82,8 +90,22 @@ class Main {
82
90
  this.handleLocationHashFromTabs()
83
91
  }
84
92
 
93
+ initInlines(target) {
94
+ target = target || document
95
+ const inlineGroups = target.querySelectorAll('.inline-group')
96
+ inlineGroups.forEach(group => {
97
+ window.django.jQuery(group).djangoFormset()
98
+ })
99
+ }
100
+
101
+ initInputs(target) {
102
+ this.datepicker = new Datepicker(target)
103
+ this.range = new Range(null, null, target)
104
+ this.multiselect = new Multiselect()
105
+ }
106
+
85
107
  handleLocationHashFromTabs() {
86
- if(window.location.hash) {
108
+ if (window.location.hash) {
87
109
  document.querySelector(`#tab_${window.location.hash.slice(1)}`)?.click()
88
110
  }
89
111
  const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]:not([data-bs-disable-history])')
@@ -116,7 +138,7 @@ class Main {
116
138
 
117
139
  fileDownload(event) {
118
140
  const button = event.target.closest('.js-file-button')
119
- if(button) {
141
+ if (button) {
120
142
  event.preventDefault()
121
143
  event.stopPropagation()
122
144
  const download_window = window.open(button.getAttribute("href"))
@@ -132,21 +154,20 @@ class Main {
132
154
  const dropdowns = [].slice.call(target.querySelectorAll('[data-bs-toggle="dropdown"]'))
133
155
  dropdowns.map((dropdownToggleEl) => {
134
156
  let offset = dropdownToggleEl.dataset['bsOffset']
135
- if(offset) {
157
+ if (offset) {
136
158
  offset = JSON.parse(dropdownToggleEl.dataset['bsOffset'])
137
- }
138
- else {
139
- offset = [0,8]
159
+ } else {
160
+ offset = [0, 8]
140
161
  }
141
162
  return new Dropdown(dropdownToggleEl, {
142
163
  autoClose: 'outside',
143
164
  offset: offset,
144
165
  popperConfig(defaultBsPopperConfig) {
145
166
  const elementConf = {}
146
- if(dropdownToggleEl.dataset['bsPopperPlacement']) {
167
+ if (dropdownToggleEl.dataset['bsPopperPlacement']) {
147
168
  elementConf['placement'] = dropdownToggleEl.dataset['bsPopperPlacement']
148
169
  }
149
- return { ...defaultBsPopperConfig, ...elementConf, strategy: 'fixed' }
170
+ return {...defaultBsPopperConfig, ...elementConf, strategy: 'fixed'}
150
171
  }
151
172
  })
152
173
  })
@@ -154,7 +175,7 @@ class Main {
154
175
 
155
176
  initAliasName() {
156
177
  const aliasGroup = document.getElementById(window.sb_admin_const.GLOBAL_FILTER_ALIAS_WIDGET_ID)
157
- if(!aliasGroup) {
178
+ if (!aliasGroup) {
158
179
  return
159
180
  }
160
181
 
@@ -180,9 +201,9 @@ class Main {
180
201
  const saveStateEl = event.target.closest('.js-save-state')
181
202
  if (saveStateEl) {
182
203
  const isBsToggle = saveStateEl.dataset['bsToggle']
183
- if(isBsToggle === 'collapse') {
204
+ if (isBsToggle === 'collapse') {
184
205
  const expanded = saveStateEl.getAttribute('aria-expanded') === 'true'
185
- setCookie(saveStateEl.id, expanded, expanded?1:0)
206
+ setCookie(saveStateEl.id, expanded, expanded ? 1 : 0)
186
207
  }
187
208
  }
188
209
  }
@@ -196,7 +217,7 @@ class Main {
196
217
  selectAll(event) {
197
218
  const wrapper = event.target.closest('.js-select-all-wrapper')
198
219
 
199
- if(wrapper) {
220
+ if (wrapper) {
200
221
  const selectAll = event.target.closest('.js-select-all')
201
222
  const clearAll = event.target.closest('.js-clear-all')
202
223
  if (selectAll) {
@@ -231,17 +252,16 @@ class Main {
231
252
  const input = fileInput.querySelector('input[type="file"]')
232
253
  const delete_checkbox = fileInput.querySelector('input[type="checkbox"]')
233
254
  input?.addEventListener('change', e => {
234
- if(delete_checkbox) {
255
+ if (delete_checkbox) {
235
256
  delete_checkbox.checked = false
236
257
  }
237
- if(e.target.files[0]) {
258
+ if (e.target.files[0]) {
238
259
  fileInput.classList.add('filled')
239
260
  fileInput.querySelectorAll('.js-input-file-image').forEach(el => {
240
261
  el.src = URL.createObjectURL(e.target.files[0])
241
262
  })
242
263
  fileInput.querySelector('.js-input-file-filename').innerHTML = e.target.files[0].name
243
- }
244
- else {
264
+ } else {
245
265
  fileInput.classList.remove('filled')
246
266
  fileInput.querySelector('.js-input-file-filename').innerHTML = ""
247
267
  }
@@ -251,13 +271,25 @@ class Main {
251
271
  deleteButton?.addEventListener('click', () => {
252
272
  input.value = ""
253
273
  input.dispatchEvent(new Event('change'))
254
- if(delete_checkbox) {
274
+ if (delete_checkbox) {
255
275
  delete_checkbox.checked = true
256
276
  }
257
277
  })
258
278
  })
259
279
  }
260
280
 
281
+ initCKEditor(target) {
282
+ target = target || document
283
+ target.querySelectorAll('textarea[data-type="ckeditortype"]').forEach((textarea) => {
284
+ const id = textarea.id
285
+ if (!id) return
286
+ if (window.CKEDITOR.instances[id]) {
287
+ return
288
+ }
289
+ window.CKEDITOR.replace(id)
290
+ })
291
+ }
292
+
261
293
  clearFilter(inputId) {
262
294
  const fieldElem = document.querySelector(`#${inputId}`)
263
295
  fieldElem.value = ''
@@ -266,9 +298,9 @@ class Main {
266
298
  }
267
299
 
268
300
  executeListAction(table_id, action_url, no_params) {
269
- if(window.SBAdminTable && window.SBAdminTable[table_id]){
301
+ if (window.SBAdminTable && window.SBAdminTable[table_id]) {
270
302
  window.SBAdminTable[table_id].executeListAction(action_url, no_params)
271
- } else{
303
+ } else {
272
304
  window.location.href = action_url
273
305
  }
274
306
  }
@@ -277,3 +309,9 @@ class Main {
277
309
  window.addEventListener('DOMContentLoaded', () => {
278
310
  window.SBAdmin = new Main()
279
311
  })
312
+
313
+ document.body.addEventListener('sbadmin:modal-change-form-response', function (event) {
314
+ if (event.detail.reload) {
315
+ window.location.reload()
316
+ }
317
+ })
@@ -1,9 +1,10 @@
1
1
 
2
2
  export default class Range {
3
- constructor(selector_override, options_override) {
3
+ constructor(selector_override, options_override, target) {
4
+ target = target || document
4
5
  const selector = selector_override || '.js-range'
5
6
  this.separator = ' - '
6
- document.querySelectorAll(selector).forEach(el => {
7
+ target.querySelectorAll(selector).forEach(el => {
7
8
  this.initRange(el, options_override)
8
9
  })
9
10
  }
@@ -10,7 +10,9 @@
10
10
  {% block view_class %}change-form{% endblock %}
11
11
 
12
12
  {% block js %}
13
- {{ block.super }}
13
+ {% if not sbadmin_is_modal %}
14
+ {{ block.super }}
15
+ {% endif %}
14
16
  <script type="text/javascript"
15
17
  id="django-admin-form-add-constants"
16
18
  src="{% static 'admin/js/change_form.js' %}"
@@ -109,7 +111,8 @@
109
111
 
110
112
  {% block form %}
111
113
  <form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post"
112
- id="{{ opts.model_name }}_form" class="detail-view-form flex-grow flex flex-col w-full max-w-1180 mx-auto" novalidate>
114
+ id="{% if sbadmin_is_modal %}modal__{% endif %}{{ opts.model_name }}_form" class="detail-view-form flex-grow flex flex-col w-full max-w-1180 mx-auto" novalidate>
115
+ <div class=" {% if sbadmin_is_modal %}max-h-[70vh] overflow-y-auto sbadmin_is_modal realative flex-grow{% endif %}">
113
116
  {% csrf_token %}
114
117
  {% if errors %}
115
118
  <div class="alert bg-negative-50 border border-negative-100 text-negative-900 mb-24">
@@ -133,8 +136,8 @@
133
136
  {% for tab, tab_content_object in tabular_context.context.items %}
134
137
  <li role="presentation">
135
138
  <button class="relative{% for class in tab_content_object.classes %} {{ class }}{% endfor %}" id="tab_{{ tab|slugify }}" data-bs-toggle="tab"
136
- data-bs-target="#tabcontent_{{ tab|slugify }}" type="button" role="tab"
137
- aria-controls="tabcontent_{{ tab|slugify }}" aria-selected="true">
139
+ data-bs-target="#{% if sbadmin_is_modal %}modal__{% endif %}{{ opts.model_name }}_tabcontent_{{ tab|slugify }}" type="button" role="tab"
140
+ aria-controls="{% if sbadmin_is_modal %}modal__{% endif %}{{ opts.model_name }}_tabcontent_{{ tab|slugify }}" aria-selected="true">
138
141
  {{ tab }}
139
142
  </button>
140
143
  </li>
@@ -142,7 +145,7 @@
142
145
  </ul>
143
146
  {% endif %}
144
147
  {% for tab, tab_content_object in tabular_context.context.items %}
145
- <div class="detail-view-tab tab-pane fade{% for class in tab_content_object.classes %} {{ class }}{% endfor %} pt-20" id="tabcontent_{{ tab|slugify }}" role="tabpanel"
148
+ <div class="detail-view-tab tab-pane fade{% for class in tab_content_object.classes %} {{ class }}{% endfor %} pt-20" id="{% if sbadmin_is_modal %}modal__{% endif %}{{ opts.model_name }}_tabcontent_{{ tab|slugify }}" role="tabpanel"
146
149
  aria-labelledby="tab_{{ tab|slugify }}">
147
150
  <div class="flex max-md:flex-wrap w-full lg:gap-24">
148
151
  <div class="min-w-0 w-full">
@@ -150,7 +153,7 @@
150
153
  {% if tab_content.type == 'fieldset' %}
151
154
  {% block tab_content_fieldset %}
152
155
  {% with tab_content.value as fieldset %}
153
- {% if DETAIL_STRUCTURE_RIGHT_CLASS not in fieldset.classes %}
156
+ {% if DETAIL_STRUCTURE_RIGHT_CLASS not in fieldset.classes or sbadmin_is_modal %}
154
157
  {% include "sb_admin/includes/fieldset.html" %}
155
158
  {% endif %}
156
159
  {% endwith %}
@@ -170,7 +173,7 @@
170
173
  {% for tab_content in tab_content_object.content %}
171
174
  {% if tab_content.type == 'fieldset' %}
172
175
  {% with tab_content.value as fieldset %}
173
- {% if DETAIL_STRUCTURE_RIGHT_CLASS in fieldset.classes %}
176
+ {% if DETAIL_STRUCTURE_RIGHT_CLASS in fieldset.classes and not sbadmin_is_modal %}
174
177
  {% include "sb_admin/includes/fieldset.html" %}
175
178
  {% endif %}
176
179
  {% endwith %}
@@ -191,41 +194,77 @@
191
194
  {% endif %}>
192
195
  </script>
193
196
  {% endblock %}
197
+ </div>
194
198
  {% if is_popup %}
195
199
  {# TODO: check sticky bar in popups #}
196
- {% endif %}
197
- {% block action_bar %}
198
- <div class="detail-view-action-bar">
199
- <div>
200
- <h2 class="text-dark-900 font-semibold text-18 mr-16 line-clamp-1">
201
- {% include 'sb_admin/includes/change_form_title.html' %}
202
- </h2>
203
- {% if previous_url or next_url %}
204
- <div class="flex items-center gap-8 mr-8">
205
- <{% if previous_url %}a href="{{ previous_url }}"{% else %}button type="button" disabled{% endif %} class="btn btn-small" title="{% trans 'Prev' %}">
206
- <svg class="w-20 h-20">
207
- <use xlink:href="#Left"></use>
208
- </svg>
209
- </{% if previous_url %}a{% else %}button{% endif %}>
210
- <div class="text-12">{% blocktrans %}<strong>{{ current_index }}</strong> / {{ all_objects_count }}{% endblocktrans %}</div>
211
- <{% if next_url %}a href="{{ next_url }}"{% else %}button type="button" disabled{% endif %} class="btn btn-small" title="{% trans 'Next' %}">
212
- <svg class="w-20 h-20">
213
- <use xlink:href="#Right"></use>
214
- </svg>
215
- </{% if next_url %}a{% else %}button{% endif %}>
216
- </div>
217
- {% endif %}
218
- <div class="flex ml-auto gap-8">
219
- {% if not is_popup and has_view_permission %}
220
- {% url opts|admin_urlname:'changelist' as changelist_url %}
221
- <a href="{% add_preserved_filters changelist_url %}" class="btn btn-empty">{% trans 'Back' %}</a>
200
+ {% endif %}
201
+ {% if sbadmin_is_modal %}
202
+ <div class="bg-light bottom-0 left-0 w-full px-24 py-24 flex justify-between gap-8">
203
+ {% if original %}
204
+ {% url opts|admin_urlname:'change' original.pk|admin_urlquote as post_url %}
205
+ {% else %}
206
+ {% url opts|admin_urlname:'add' as post_url %}
207
+ {% endif %}
208
+ <button
209
+ form="{% if sbadmin_is_modal %}modal__{% endif %}{{ opts.model_name }}_form"
210
+ hx-post="{{ post_url }}"
211
+ hx-target="#sb-admin-modal .modal-content"
212
+ hx-swap="innerHTML"
213
+ hx-encoding="multipart/form-data"
214
+ hx-vals='{
215
+ "sbadmin_is_modal": 1,
216
+ "sb_admin_source_field": "{{ request.GET.sb_admin_source_field }}",
217
+ "sbadmin_reload_on_save": {% if sbadmin_reload_on_save %}1{% else %}0{% endif %},
218
+ "sbadmin_parent_instance_field": "{{ request.GET.sbadmin_parent_instance_field }}",
219
+ "sbadmin_parent_instance_pk": "{{ request.GET.sbadmin_parent_instance_pk | default:"" }}",
220
+ "sbadmin_parent_instance_label": "{{ request.GET.sbadmin_parent_instance_label }}"
221
+ }'
222
+ class="btn btn-primary flex-grow flex items-center justify-center">
223
+ <svg class="w-20 h-20 mr-8">
224
+ <use xlink:href="#Save"></use>
225
+ </svg>
226
+ {% trans 'Save' %}
227
+ </button>
228
+ <button type="button"
229
+ class="btn flex-grow"
230
+ data-bs-dismiss="modal" aria-label="Close">
231
+ {% trans 'Close' %}
232
+ </button>
233
+ </div>
234
+ {% else %}
235
+ {% block action_bar %}
236
+ <div class="detail-view-action-bar">
237
+ <div>
238
+ <h2 class="text-dark-900 font-semibold text-18 mr-16 line-clamp-1">
239
+ {% include 'sb_admin/includes/change_form_title.html' %}
240
+ </h2>
241
+ {% if previous_url or next_url %}
242
+ <div class="flex items-center gap-8 mr-8">
243
+ <{% if previous_url %}a href="{{ previous_url }}"{% else %}button type="button" disabled{% endif %} class="btn btn-small" title="{% trans 'Prev' %}">
244
+ <svg class="w-20 h-20">
245
+ <use xlink:href="#Left"></use>
246
+ </svg>
247
+ </{% if previous_url %}a{% else %}button{% endif %}>
248
+ <div class="text-12">{% blocktrans %}<strong>{{ current_index }}</strong> / {{ all_objects_count }}{% endblocktrans %}</div>
249
+ <{% if next_url %}a href="{{ next_url }}"{% else %}button type="button" disabled{% endif %} class="btn btn-small" title="{% trans 'Next' %}">
250
+ <svg class="w-20 h-20">
251
+ <use xlink:href="#Right"></use>
252
+ </svg>
253
+ </{% if next_url %}a{% else %}button{% endif %}>
254
+ </div>
222
255
  {% endif %}
256
+ <div class="flex ml-auto gap-8">
257
+ {% if not is_popup and has_view_permission %}
258
+ {% url opts|admin_urlname:'changelist' as changelist_url %}
259
+ <a href="{% add_preserved_filters changelist_url %}" class="btn btn-empty">{% trans 'Back' %}</a>
260
+ {% endif %}
223
261
 
224
- {% submit_row %}
262
+ {% submit_row %}
263
+ </div>
225
264
  </div>
226
265
  </div>
227
- </div>
228
- {% endblock %}
266
+ {% endblock %}
267
+ {% endif %}
229
268
  </form>
230
269
  {% endblock %}
231
270
  {% endblock %}
@@ -24,12 +24,28 @@
24
24
  class="btn {{ list_action.css_class|default_if_none:'' }}">{{ list_action.title }}</a>
25
25
  {% endfor %}
26
26
  {% if inline_admin_formset.has_add_permission %}
27
- <a href="javascript://" class="add-handler djn-add-handler btn btn-icon ml-auto {{ inline_admin_formset.handler_classes|join:" " }}">
28
- <svg class="w-20 h-20 md:mr-8">
29
- <use xlink:href="#Plus"></use>
30
- </svg>
31
- <span>{% trans 'Add' %}</span>
32
- </a>
27
+ {% if context_data.add_url and not sbadmin_is_modal %}
28
+ <a
29
+ class="btn btn-icon ml-auto {{ inline_admin_formset.handler_classes|join:" " }}"
30
+ data-bs-toggle="modal"
31
+ data-bs-target="#sb-admin-modal"
32
+ hx-get="{{ context_data.add_url }}?_popup=1&sbadmin_is_modal=1&sbadmin_reload_on_save=1&sbadmin_parent_instance_field={{ context_data.parent_data.sbadmin_parent_instance_field }}&sbadmin_parent_instance_pk={{ context_data.parent_data.sbadmin_parent_instance_pk }}&sbadmin_parent_instance_label={{ context_data.parent_data.sbadmin_parent_instance_label }}"
33
+ hx-target="#sb-admin-modal .modal-content"
34
+ hx-swap="innerHTML"
35
+ >
36
+ <svg class="w-20 h-20 md:mr-8">
37
+ <use xlink:href="#Plus"></use>
38
+ </svg>
39
+ <span>{% trans 'Add' %}</span>
40
+ </a>
41
+ {% else %}
42
+ <a href="javascript://" class="add-handler djn-add-handler btn btn-icon ml-auto {{ inline_admin_formset.handler_classes|join:" " }}">
43
+ <svg class="w-20 h-20 md:mr-8">
44
+ <use xlink:href="#Plus"></use>
45
+ </svg>
46
+ <span>{% trans 'Add' %}</span>
47
+ </a>
48
+ {% endif %}
33
49
  {% endif %}
34
50
  </div>
35
51
  </header>
@@ -47,35 +63,35 @@
47
63
  <table class="djn-items inline-related djn-table">
48
64
  {% with inline_admin_formset.opts.sortable_field_name|default:"" as sortable_field_name %}
49
65
  <thead class="djn-module djn-thead">
50
- <tr>
51
- {% if context_data.is_sortable_active %}
52
- <th class="original{% if sortable_field_name %} is-sortable{% endif %}"></th>
53
- {% endif %}
54
- {% for field in inline_admin_formset.fields %}
55
- {% if not field.widget.is_hidden and not field|is_row_class_field %}
56
- <th class="djn-th
66
+ <tr>
67
+ {% if context_data.is_sortable_active %}
68
+ <th class="original{% if sortable_field_name %} is-sortable{% endif %}"></th>
69
+ {% endif %}
70
+ {% for field in inline_admin_formset.fields %}
71
+ {% if not field.widget.is_hidden and not field|is_row_class_field %}
72
+ <th class="djn-th
57
73
  {{ field.label|lower|slugify }}{% if field.required %} required{% endif %}">
58
- <div class="flex items-center">
59
- <span class="mr-auto">{{ field.label|capfirst }}{% if field.required %}<span class="ml-4 text-negative">*</span>{% endif %}</span>
60
- {% if field.help_text %}&nbsp;
61
- <div class="ml-4">
62
- <div class="js-tooltip flex" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ field.help_text|striptags }}">
63
- <svg class="w-12 h-12">
64
- <use xlink:href="#Help"></use>
65
- </svg>
66
- </div>
74
+ <div class="flex items-center">
75
+ <span class="mr-auto">{{ field.label|capfirst }}{% if field.required %}<span class="ml-4 text-negative">*</span>{% endif %}</span>
76
+ {% if field.help_text %}&nbsp;
77
+ <div class="ml-4">
78
+ <div class="js-tooltip flex" data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ field.help_text|striptags }}">
79
+ <svg class="w-12 h-12">
80
+ <use xlink:href="#Help"></use>
81
+ </svg>
67
82
  </div>
68
- {% endif %}
69
- </div>
70
- </th>
71
- {% endif %}
72
- {% endfor %}
73
- {% block table_inline_delete_table_head %}
74
- {% if inline_admin_formset.formset.can_delete %}
75
- <th class="djn-th sticky-table-head" style="min-width: 54px; width: 54px;"></th>
76
- {% endif %}
77
- {% endblock %}
78
- </tr>
83
+ </div>
84
+ {% endif %}
85
+ </div>
86
+ </th>
87
+ {% endif %}
88
+ {% endfor %}
89
+ {% block table_inline_delete_table_head %}
90
+ {% if inline_admin_formset.formset.can_delete %}
91
+ <th class="djn-th sticky-table-head" style="min-width: 54px; width: 54px;"></th>
92
+ {% endif %}
93
+ {% endblock %}
94
+ </tr>
79
95
  </thead>
80
96
 
81
97