Skip to content

Commit 8262e21

Browse files
authored
Dev nshp3 (#135)
* triv: added Tabulator options & updated btn-only-icon styles * feat: added option to have prefix and suffix on field * feat: added JS to scroll to field with error * fix: fixed not working modal 500 error * feat: added JS to scroll to field with error -> open collapse if presented * feat: enhance form widgets with input prefix and suffix support; update CSS selectors for label handling * feat: update button styles in ArticleAdmin and add regression tests for modal media serialization * triv: bump up version
1 parent 8972b99 commit 8262e21

17 files changed

Lines changed: 374 additions & 54 deletions

File tree

AGENTS.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This document provides key patterns and gotchas for developers and AI assistants
1212
| [SBAdminField](#sbadminfield---list-display-columns) | Defining list columns, annotations, `supporting_annotates`, admin methods, ordering with computed fields, `sbadmin_list_display_data` |
1313
| [Configuration](#configuration) | `INSTALLED_APPS`, role config, menu items, queryset restrictions, custom permissions |
1414
| [Filter Widgets](#filter-widgets) | Built-in widgets, custom filters, `filter_query_lambda` for M2M filtering |
15-
| [Form Widgets](#form-widgets) | `SBAdminTextTagsWidget`, `Meta.widgets` initialization, required select placeholders, `SBAdminJsonEditorWidget` for schema-driven JSON |
15+
| [Form Widgets](#form-widgets) | `SBAdminTextTagsWidget`, input prefix/suffix on text and number widgets, `Meta.widgets` initialization, required select placeholders, `SBAdminJsonEditorWidget` for schema-driven JSON |
1616
| [Admin Registration](#admin-registration) | `@admin.register` with `sb_admin_site`, `sbadmin_list_filter` vs `list_filter` |
1717
| [Selection Actions](#selection-actions-bulk-actions) | Modal forms for bulk operations, `ListActionModalView`, confirmation modals, `SBAdminCustomAction` params, per-action permissions, success/error handling |
1818
| [Row Actions](#row-actions-per-row-list-buttons) | Per-row icon buttons with `SBAdminRowAction`, `RowActionModalView`, and row-aware enablement |
@@ -145,6 +145,24 @@ class ArticleAdmin(SBAdmin):
145145
| `filter_disabled` | bool | Disable filtering for this field |
146146
| `python_formatter` | callable | Format value: `(obj_id, value) -> formatted_value` |
147147
| `list_visible` | bool | Show/hide column in list |
148+
| `tabulator_options` | TabulatorFieldOptions | Per-column Tabulator settings (width, grow, max, custom SBAdmin options) |
149+
150+
### Tabulator Options (table)
151+
152+
| Option | Type | Description |
153+
|--------|------|-------------|
154+
| `sbadminKeepDataWidth` | bool | Keep column natural width (prevent stretch) when using `fitDataFillAvailableSpace`. Best for icon/utility columns. |
155+
156+
```python
157+
from django_smartbase_admin.engine.field import SBAdminField, TabulatorFieldOptions
158+
159+
sbadmin_list_display = (
160+
SBAdminField(
161+
name="id",
162+
tabulator_options=TabulatorFieldOptions(sbadminKeepDataWidth=True),
163+
),
164+
)
165+
```
148166

149167
### Admin Methods (like Django admin)
150168

@@ -1078,6 +1096,37 @@ class ArticleTagNamesForm(SBAdminBaseFormInit, forms.Form):
10781096
- Duplicate values are prevented client-side.
10791097
- Works with dynamically-added rows in SBAdmin formsets and wizard formsets.
10801098

1099+
### Input prefix and suffix (text and number widgets)
1100+
1101+
Pass optional `prefix` and/or `suffix` strings to `SBAdminTextInputWidget` or `SBAdminNumberWidget` (e.g. currency, units, URL stem). Omit both for a normal input.
1102+
1103+
```python
1104+
from django import forms
1105+
1106+
from django_smartbase_admin.admin.admin_base import SBAdminBaseForm
1107+
from django_smartbase_admin.admin.widgets import (
1108+
SBAdminNumberWidget,
1109+
SBAdminTextInputWidget,
1110+
)
1111+
1112+
from blog.models import Article
1113+
1114+
1115+
class ArticleForm(SBAdminBaseForm):
1116+
class Meta:
1117+
model = Article
1118+
fields = ("slug", "price", "discount")
1119+
widgets = {
1120+
"slug": SBAdminTextInputWidget(prefix="https://blog.example.com/"),
1121+
"price": SBAdminNumberWidget(suffix=""),
1122+
"discount": SBAdminNumberWidget(prefix="-", suffix="%"),
1123+
}
1124+
```
1125+
1126+
**Key points:**
1127+
- Addons are display-only; they do not change the stored field value.
1128+
- Other widgets can support the same pattern via `SBAdminInputAffixMixin` (mix in and forward `prefix` / `suffix` in `__init__`).
1129+
10811130
### `Meta.widgets` are initialized automatically in `SBAdminBaseForm`
10821131

10831132
When a form inherits from `SBAdminBaseForm`, widgets defined in `Meta.widgets` are initialized even if the field is not re-declared on the form class.
@@ -1532,7 +1581,7 @@ class ArticleAdmin(SBAdmin):
15321581
title=lambda row: _("Archive %(title)s") % {"title": row.get("title", "")},
15331582
icon="Delete",
15341583
view=self,
1535-
css_class=lambda row: "btn-icon btn-destructive",
1584+
css_class=lambda row: "btn btn-small btn-only-icon btn-destructive",
15361585
enabled_if=lambda row: row.get("status") != "archived",
15371586
),
15381587
# Mode 3: plain link. MODIFIER_OBJECT_ID is replaced with the row pk.

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.2.0"
3+
version = "1.3.0"
44
description = ""
55
authors = ["SmartBase <info@smartbase.sk>"]
66
readme = "README.md"

src/django_smartbase_admin/admin/admin_base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -943,10 +943,11 @@ def render_change_form(
943943
):
944944
if context.get("sbadmin_is_modal"):
945945
media = context["media"]
946+
js_assets = [str(asset) for asset in getattr(media, "_js", [])]
946947
media_json = {
947-
"js": list(getattr(media, "_js", [])),
948+
"js": js_assets,
948949
"css": {
949-
medium: list(paths)
950+
medium: [str(path) for path in paths]
950951
for medium, paths in getattr(media, "_css", {}).items()
951952
},
952953
}

src/django_smartbase_admin/admin/widgets.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,41 @@ def get_context(self, name, value, attrs):
108108
return context
109109

110110

111-
class SBAdminTextInputWidget(SBAdminBaseWidget, forms.TextInput):
111+
class SBAdminInputAffixMixin:
112+
def __init__(self, *args, prefix=None, suffix=None, **kwargs):
113+
self.prefix = prefix
114+
self.suffix = suffix
115+
super().__init__(*args, **kwargs)
116+
117+
def get_context(self, name, value, attrs):
118+
context = super().get_context(name, value, attrs)
119+
context["widget"]["prefix"] = self.prefix
120+
context["widget"]["suffix"] = self.suffix
121+
return context
122+
123+
def get_attrs_with_affix_classes(self, attrs, prefix=None, suffix=None):
124+
attrs = dict(attrs or {})
125+
classes = attrs.get("class", "")
126+
if prefix:
127+
classes = f"{classes} rounded-l-none".strip()
128+
if suffix:
129+
classes = f"{classes} rounded-r-none".strip()
130+
attrs["class"] = classes
131+
return attrs
132+
133+
134+
class SBAdminTextInputWidget(
135+
SBAdminInputAffixMixin, SBAdminBaseWidget, forms.TextInput
136+
):
112137
template_name = "sb_admin/widgets/text.html"
113138

114-
def __init__(self, form_field=None, attrs=None):
115-
super().__init__(form_field, attrs={"class": "input", **(attrs or {})})
139+
def __init__(self, form_field=None, attrs=None, prefix=None, suffix=None):
140+
attrs = self.get_attrs_with_affix_classes(
141+
{"class": "input", **(attrs or {})},
142+
prefix=prefix,
143+
suffix=suffix,
144+
)
145+
super().__init__(form_field, attrs=attrs, prefix=prefix, suffix=suffix)
116146

117147

118148
class SBAdminTextTagsWidget(SBAdminBaseWidget, forms.TextInput):
@@ -159,12 +189,17 @@ def __init__(self, form_field=None, attrs=None):
159189
super().__init__(form_field, attrs={"class": "input", **(attrs or {})})
160190

161191

162-
class SBAdminNumberWidget(SBAdminBaseWidget, forms.NumberInput):
192+
class SBAdminNumberWidget(SBAdminInputAffixMixin, SBAdminBaseWidget, forms.NumberInput):
163193
class_name = "input"
164194
template_name = "sb_admin/widgets/number.html"
165195

166-
def __init__(self, form_field=None, attrs=None):
167-
super().__init__(form_field, attrs={"class": self.class_name, **(attrs or {})})
196+
def __init__(self, form_field=None, attrs=None, prefix=None, suffix=None):
197+
attrs = self.get_attrs_with_affix_classes(
198+
{"class": self.class_name, **(attrs or {})},
199+
prefix=prefix,
200+
suffix=suffix,
201+
)
202+
super().__init__(form_field, attrs=attrs, prefix=prefix, suffix=suffix)
168203

169204

170205
class SBAdminCheckboxWidget(SBAdminBaseWidget, forms.CheckboxInput):

src/django_smartbase_admin/audit/tests/test_action_processing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def test_row_actions_are_processed_into_table_column_and_row_descriptors(self):
179179
"url": "/actions/PublishArticleView/7/",
180180
"title": "Publish Draft Article",
181181
"icon": "Check-correct",
182-
"css_class": "btn btn-tiny btn-icon",
182+
"css_class": "btn btn-small btn-only-icon",
183183
"open_in_modal": True,
184184
"is_method_action": False,
185185
"open_in_new_tab": False,
@@ -197,7 +197,7 @@ def test_row_actions_are_processed_into_table_column_and_row_descriptors(self):
197197
"url": "/articles/7/",
198198
"title": "Open",
199199
"icon": "Preview-open",
200-
"css_class": "btn btn-tiny btn-icon",
200+
"css_class": "btn btn-small btn-only-icon",
201201
"open_in_modal": False,
202202
"is_method_action": False,
203203
"open_in_new_tab": True,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Regression tests for modal media serialization in SBAdmin change form."""
2+
3+
import json
4+
from unittest.mock import patch
5+
6+
from django.test import RequestFactory, TestCase
7+
from js_asset import JS
8+
9+
from ckeditor.widgets import CKEditorWidget
10+
11+
from django_smartbase_admin.admin.admin_base import SBAdmin
12+
13+
14+
class ModalMediaSerializationTests(TestCase):
15+
def setUp(self):
16+
self.request = RequestFactory().get("/")
17+
self.admin = SBAdmin.__new__(SBAdmin)
18+
19+
@patch("django.contrib.admin.options.ModelAdmin.render_change_form")
20+
def test_modal_ckeditor_widget_media_is_json_serializable(
21+
self, mock_render_change_form
22+
):
23+
media = CKEditorWidget().media
24+
context = {"sbadmin_is_modal": True, "media": media}
25+
self.admin.render_change_form(self.request, context)
26+
json.dumps(context["media_json"])

src/django_smartbase_admin/engine/actions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ class SBAdminRowAction(SBAdminCustomAction):
115115
"""
116116

117117
target_view = None
118-
css_class = "btn btn-tiny btn-icon"
118+
css_class = "btn btn-small btn-only-icon"
119119
open_in_new_tab = False
120120
enabled_if = None
121121
enabled_field = None

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@
3131
.column-widget-columns {
3232
& > li {
3333
@apply relative px-12 py-8 flex items-center;
34-
.checkbox.checkbox-icon:not(:checked) + label {
34+
.checkbox.checkbox-icon:not(:checked) + :where(label, .label) {
3535
@media (pointer:fine) {
3636
opacity: 0;
3737
}
3838
}
3939
&:hover {
40-
.checkbox.checkbox-icon + label {
40+
.checkbox.checkbox-icon + :where(label, .label) {
4141
opacity: 1;
4242
}
4343
}

src/django_smartbase_admin/static/sb_admin/src/css/components/_button.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@
202202
}
203203
}
204204

205+
.btn-only-icon {
206+
@apply px-0 aspect-square !justify-center;
207+
>span {
208+
@apply hidden;
209+
}
210+
>svg {
211+
@apply mx-0;
212+
}
213+
}
214+
205215
.btn-group {
206216
@apply flex;
207217
> * {

0 commit comments

Comments
 (0)