Skip to content

Commit ced6035

Browse files
authored
feat: new type of visual for tabulator header filters (#13)
* feat: new type of visual for tabulator header filters * feat: new type of visual for tabulator header filters * feat: added confirmation.html to be able to delete view * feat: added confirmation.html to be able to delete view * triv: refactor * triv: added column picker --------- Co-authored-by: Viliam Mihalik <[email protected]>
1 parent 44124f5 commit ced6035

File tree

20 files changed

+532
-134
lines changed

20 files changed

+532
-134
lines changed

src/django_smartbase_admin/actions/admin_action_list.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.core.paginator import Paginator
55
from django.db.models import Q
66
from django.utils import timezone
7+
from django.utils.text import smart_split, unescape_string_literal
78

89
from django_smartbase_admin.engine.actions import SBAdminAction
910
from django_smartbase_admin.engine.const import (
@@ -29,6 +30,8 @@
2930
OBJECT_ID_PLACEHOLDER,
3031
ANNOTATE_KEY,
3132
CONFIG_NAME,
33+
TABLE_PARAMS_FULL_TEXT_SEARCH,
34+
TABLE_PARAMS_SELECTED_FILTER_TYPE,
3235
)
3336
from django_smartbase_admin.services.views import SBAdminViewService
3437

@@ -135,6 +138,8 @@ def get_template_data(self):
135138
"COLUMNS_DATA_COLLAPSED_NAME": COLUMNS_DATA_COLLAPSED_NAME,
136139
"TABLE_PARAMS_NAME": TABLE_PARAMS_NAME,
137140
"TABLE_PARAMS_SIZE_NAME": TABLE_PARAMS_SIZE_NAME,
141+
"TABLE_PARAMS_FULL_TEXT_SEARCH": TABLE_PARAMS_FULL_TEXT_SEARCH,
142+
"TABLE_PARAMS_SELECTED_FILTER_TYPE": TABLE_PARAMS_SELECTED_FILTER_TYPE,
138143
"FILTER_DATA_NAME": FILTER_DATA_NAME,
139144
"BASE_PARAMS_NAME": BASE_PARAMS_NAME,
140145
"TABLE_PARAMS_PAGE_NAME": TABLE_PARAMS_PAGE_NAME,
@@ -162,6 +167,10 @@ def get_template_data(self):
162167
"tabulator_definition": tabulator_definition,
163168
"id_column_name": id_column_name,
164169
"filters": self.get_filters(),
170+
"tabulator_header_template_name": self.view.get_tabulator_header_template_name(
171+
self.threadsafe_request
172+
),
173+
"search_field_placeholder": self.view.get_search_field_placeholder(),
165174
"list_actions": self.view.process_actions_permissions(
166175
self.threadsafe_request, list_actions
167176
),
@@ -192,12 +201,26 @@ def get_filter_from_request(self):
192201
self.threadsafe_request, self.column_fields, self.filter_data
193202
)
194203

204+
def get_search_fields(self, request):
205+
search_fields_definition = self.view.get_search_fields(request)
206+
search_fields = []
207+
for field in self.column_fields:
208+
if field.name in search_fields_definition:
209+
search_fields.append(field)
210+
return search_fields
211+
195212
def get_filter_fields_from_request(self):
196-
return list(
213+
filter_fields = list(
197214
SBAdminViewService.get_filter_fields_and_values_from_request(
198-
self.threadsafe_request, self.column_fields, self.filter_data
215+
self.threadsafe_request,
216+
self.column_fields,
217+
self.filter_data,
199218
).keys()
200219
)
220+
if self.is_search_query():
221+
search_fields = self.get_search_fields(self.threadsafe_request)
222+
filter_fields.extend(search_fields)
223+
return filter_fields
201224

202225
def get_annotates(self, visible_fields=None):
203226
column_fields = (
@@ -233,14 +256,68 @@ def get_data_queryset_values(self):
233256
values.extend(self.view.sbadmin_list_display_data)
234257
return values
235258

259+
def get_search_results(self, request, queryset, search_term):
260+
"""
261+
Return a tuple containing a queryset to implement the search
262+
and a boolean indicating if the results may contain duplicates.
263+
"""
264+
265+
# Apply keyword searches.
266+
def construct_search(field_name):
267+
if field_name.startswith("^"):
268+
return "%s__istartswith" % field_name[1:]
269+
elif field_name.startswith("="):
270+
return "%s__iexact" % field_name[1:]
271+
elif field_name.startswith("@"):
272+
return "%s__search" % field_name[1:]
273+
# Otherwise, use the field with icontains.
274+
return "%s__icontains" % field_name
275+
276+
search_fields = self.get_search_fields(request)
277+
if search_fields and search_term:
278+
orm_lookups = [
279+
construct_search(str(search_field.filter_field))
280+
for search_field in search_fields
281+
]
282+
term_queries = []
283+
for bit in smart_split(search_term):
284+
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
285+
bit = unescape_string_literal(bit)
286+
or_queries = Q.create(
287+
[(orm_lookup, bit) for orm_lookup in orm_lookups],
288+
connector=Q.OR,
289+
)
290+
term_queries.append(or_queries)
291+
queryset = queryset.filter(Q.create(term_queries))
292+
return queryset
293+
294+
def is_search_query(self):
295+
full_text_search_query_value = self.filter_data.get(
296+
TABLE_PARAMS_FULL_TEXT_SEARCH, None
297+
)
298+
return bool(full_text_search_query_value)
299+
300+
def search_in_queryset(self, base_qs):
301+
full_text_search_query_value = self.filter_data.get(
302+
TABLE_PARAMS_FULL_TEXT_SEARCH, None
303+
)
304+
if not full_text_search_query_value:
305+
return base_qs
306+
base_qs = self.get_search_results(
307+
self.threadsafe_request, base_qs, full_text_search_query_value
308+
)
309+
return base_qs
310+
236311
def build_final_data_count_queryset(self, additional_filter=None):
237312
additional_filter = additional_filter or Q()
238313
filter_fields = self.get_filter_fields_from_request()
239-
return (
314+
base_qs = (
240315
self.get_data_queryset(visible_fields=filter_fields)
241316
.filter(self.get_filter_from_request())
242317
.filter(additional_filter)
243318
)
319+
base_qs = self.search_in_queryset(base_qs)
320+
return base_qs
244321

245322
def build_final_data_queryset(self, page_num, page_size, additional_filter=None):
246323
additional_filter = additional_filter or Q()
@@ -252,6 +329,7 @@ def build_final_data_queryset(self, page_num, page_size, additional_filter=None)
252329
.filter(self.get_filter_from_request())
253330
.filter(additional_filter)
254331
)
332+
base_qs = self.search_in_queryset(base_qs)
255333
return base_qs.order_by(*self.get_order_by_from_request())[from_item:to_item]
256334

257335
def get_data(self, page_num=None, page_size=None, additional_filter=None):

src/django_smartbase_admin/engine/admin_base_view.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
DETAIL_STRUCTURE_RIGHT_CLASS,
2424
GLOBAL_FILTER_ALIAS_WIDGET_ID,
2525
OVERRIDE_CONTENT_OF_NOTIFICATION,
26+
FilterVersions,
2627
)
2728
from django_smartbase_admin.services.views import SBAdminViewService
2829
from django_smartbase_admin.services.xlsx_export import (
@@ -202,6 +203,8 @@ class SBAdminBaseListView(SBAdminBaseView):
202203
sbadmin_xlsx_options = None
203204
sbadmin_table_history_enabled = True
204205
sbadmin_list_reorder_field = None
206+
search_field_placeholder = _("Search...")
207+
filters_version = None
205208

206209
def activate_reorder(self, request):
207210
request.reorder_active = True
@@ -359,6 +362,8 @@ def get_tabulator_definition(self, request):
359362
"filterModule",
360363
"tableParamsModule",
361364
"detailViewModule",
365+
"fullTextSearchModule",
366+
"filterOptionsModule",
362367
],
363368
"tabulatorOptions": {
364369
"renderVertical": "basic",
@@ -623,3 +628,19 @@ def get_new_url(self):
623628

624629
def get_context_data(self, request):
625630
return {}
631+
632+
def get_filters_version(self, request):
633+
return (
634+
self.filters_version or request.request_data.configuration.filters_version
635+
)
636+
637+
def get_tabulator_header_template_name(self, request):
638+
filters_version = self.get_filters_version(request)
639+
if filters_version is FilterVersions.FILTERS_VERSION_2:
640+
return "sb_admin/actions/partials/tabulator_header_v2.html"
641+
else:
642+
# default
643+
return "sb_admin/actions/partials/tabulator_header_v1.html"
644+
645+
def get_search_field_placeholder(self):
646+
return self.search_field_placeholder

src/django_smartbase_admin/engine/configuration.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from django_smartbase_admin.admin.site import sb_admin_site
77
from django_smartbase_admin.engine.actions import SBAdminCustomAction
8-
from django_smartbase_admin.engine.const import GLOBAL_FILTER_DATA_KEY
8+
from django_smartbase_admin.engine.const import GLOBAL_FILTER_DATA_KEY, FilterVersions
99
from django_smartbase_admin.utils import to_list
1010

1111

@@ -36,13 +36,15 @@ class SBAdminRoleConfiguration(metaclass=Singleton):
3636
autocomplete_map = None
3737
menu_items = None
3838
global_filter_form = None
39+
filters_version = FilterVersions.FILTERS_VERSION_1
3940

4041
def __init__(
4142
self,
4243
default_view=None,
4344
registered_views=None,
4445
menu_items=None,
4546
global_filter_form=None,
47+
filters_version=None,
4648
) -> None:
4749
super().__init__()
4850
self.default_view = default_view or self.default_view or []
@@ -51,6 +53,7 @@ def __init__(
5153
self.global_filter_form = global_filter_form or self.global_filter_form
5254
self.init_configuration_static()
5355
self.autocomplete_map = {}
56+
self.filters_version = filters_version or self.filters_version
5457

5558
def init_registered_views(self):
5659
registered_views = []

src/django_smartbase_admin/engine/const.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ class Formatter(Enum):
2222
HTML = "html"
2323

2424

25+
class FilterVersions(Enum):
26+
FILTERS_VERSION_1 = "version_1"
27+
FILTERS_VERSION_2 = "version_2"
28+
29+
2530
DEFAULT_PAGE_SIZE = 20
2631
PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
2732
AUTOCOMPLETE_PAGE_SIZE = 20
@@ -45,6 +50,8 @@ class Formatter(Enum):
4550
TABLE_PARAMS_SIZE_NAME = "size"
4651
TABLE_PARAMS_PAGE_NAME = "page"
4752
TABLE_PARAMS_SORT_NAME = "sort"
53+
TABLE_PARAMS_FULL_TEXT_SEARCH = "sb_admin_full_search"
54+
TABLE_PARAMS_SELECTED_FILTER_TYPE = "sb_selected_filter_type"
4855
FILTER_DATA_NAME = "filterData"
4956
BASE_PARAMS_NAME = "params"
5057
AUTOCOMPLETE_SEARCH_NAME = "__search_term__"

src/django_smartbase_admin/services/views.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from django.db.models import Q, FilteredRelation, F, Value, CharField
55
from django.shortcuts import redirect
66

7-
from django_smartbase_admin.engine.const import BASE_PARAMS_NAME, FILTER_DATA_NAME
7+
from django_smartbase_admin.engine.const import (
8+
BASE_PARAMS_NAME,
9+
FILTER_DATA_NAME,
10+
)
811
from django_smartbase_admin.engine.request import SBAdminViewRequestData
912
from django_smartbase_admin.services.translations import SBAdminTranslationsService
10-
from django_smartbase_admin.utils import to_list
1113

1214

1315
class SBAdminViewService(object):

src/django_smartbase_admin/static/sb_admin/build/webpack.common.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const entries = {
1010
chart: './src/django_smartbase_admin/static/sb_admin/src/js/chart.js',
1111
main_style: './src/django_smartbase_admin/static/sb_admin/src/css/style.css',
1212
translations: './src/django_smartbase_admin/static/sb_admin/src/js/translations.js',
13+
confirmation_modal: './src/django_smartbase_admin/static/sb_admin/src/js/confirmation_modal.js',
1314
};
1415

1516
const projectRoot = process.env.PWD || process.cwd();
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
How to use:
3+
- Modal is triggered by element with data attribute [data-show-confirm]
4+
- Modal emits events if closed:
5+
--- event 'cancel' is dispatched by clicking on Cancel or X button
6+
--- event 'confirm' is dispatched by clicking on Submit button
7+
--- element which dispatch the event is defined by 'data-response-target' attribute of 'js-confirmation' element, default is 'body'
8+
--- the event can be used to trigger htmx request with 'hx-trigger' e.g. hx-trigger="confirm from:body" for default value of data-response-target
9+
- 'js-confirmation' element can also define Header and Body of confirmation modal with data-confirm-header and data-confirm-body attributes
10+
*/
11+
12+
export class Confirmation {
13+
constructor() {
14+
this.modalEl = document.getElementById('confirmation-modal')
15+
if(!this.modalEl) {
16+
return
17+
}
18+
this.modal = new window.bootstrap5.Modal(this.modalEl)
19+
20+
this.defaultModalData = {
21+
'responseTarget': 'body',
22+
'confirmBody': null,
23+
'confirmIcon': null,
24+
'confirmFooter': null,
25+
'confirmSubmit': 'Confirm',
26+
'confirmClose': 'Cancel',
27+
'submitEvent': 'confirm',
28+
'cancelEvent': 'cancel',
29+
}
30+
31+
this.modalEl.addEventListener('click', event => {
32+
const closingOrSubmittingButton = event.target.closest('.js-modal-close')
33+
if(closingOrSubmittingButton) {
34+
let eventType
35+
let isSubmitButton = closingOrSubmittingButton.classList.contains('js-modal-submit')
36+
if (isSubmitButton) {
37+
eventType = this.modalData.submitEvent
38+
} else {
39+
eventType = this.modalData.cancelEvent
40+
}
41+
this.fireEventToTarget(eventType)
42+
this.modal.hide()
43+
}
44+
})
45+
46+
document.addEventListener('click', event => {
47+
const confirmationButton = event.target.closest('[data-show-confirm]')
48+
if (confirmationButton) {
49+
this.updateModalData(confirmationButton)
50+
this.updateModalContent()
51+
this.modal.show()
52+
}
53+
})
54+
}
55+
56+
fireEventToTarget(eventType) {
57+
const target = document.querySelector(this.modalData.responseTarget)
58+
const event = new Event(eventType)
59+
target.dispatchEvent(event)
60+
}
61+
62+
updateModalData(target) {
63+
this.modalData = {...this.defaultModalData}
64+
Object.keys(this.modalData).forEach(key => {
65+
if(target.dataset[key]) {
66+
this.modalData[key] = target.dataset[key]
67+
}
68+
})
69+
}
70+
71+
updateModalContent() {
72+
if(this.modalData.confirmSubmit) {
73+
this.modalEl.querySelector('.js-modal-submit').innerHTML = this.modalData.confirmSubmit
74+
}
75+
if(this.modalData.confirmClose) {
76+
this.modalEl.querySelector('.js-modal-close-button').innerHTML = this.modalData.confirmClose
77+
}
78+
if(this.modalData.confirmBody) {
79+
this.modalEl.querySelector('.js-modal-body').innerHTML = this.modalData.confirmBody
80+
}
81+
if(this.modalData.confirmIcon) {
82+
this.modalEl.querySelector('.js-modal-icon').innerHTML = this.modalData.confirmIcon
83+
}
84+
if(this.modalData.confirmFooter) {
85+
this.modalEl.querySelector('.js-modal-footer').innerHTML = this.modalData.confirmFooter
86+
}
87+
}
88+
}
89+
90+
window.ConfirmationClass = Confirmation
91+
92+
window.addEventListener("DOMContentLoaded", () => {
93+
window.Confirmation = new Confirmation()
94+
})

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class Main {
7171
if(window.location.hash) {
7272
document.querySelector(`#tab_${window.location.hash.slice(1)}`)?.click()
7373
}
74-
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]')
74+
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]:not([data-bs-disable-history])')
7575
tabEls.forEach(tab => {
7676
tab.addEventListener('shown.bs.tab', function (event) {
7777
window.location.hash = event.target.id.split("tab_")[1]

src/django_smartbase_admin/static/sb_admin/src/js/table.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {DetailViewModule} from "./table_modules/detail_view_module"
77
import {FilterModule} from "./table_modules/filter_module"
88
import {MovableColumnsModule} from "./table_modules/movable_columns_module"
99
import {DataEditModule} from "./table_modules/data_edit_module"
10+
import {FullTextSearchModule} from "./table_modules/full_text_search_module"
11+
import { FilterOptionsModule } from "./table_modules/filter_options_module"
1012

1113

1214
class SBAdminTable {
@@ -276,4 +278,6 @@ window.SBAdminTableModulesClass = {
276278
'detailViewModule': DetailViewModule,
277279
'movableColumnsModule': MovableColumnsModule,
278280
'dataEditModule': DataEditModule,
281+
'fullTextSearchModule': FullTextSearchModule,
282+
'filterOptionsModule': FilterOptionsModule,
279283
}

src/django_smartbase_admin/static/sb_admin/src/js/table_modules/column_display_module.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ export class ColumnDisplayModule extends SBAdminTableModule {
6868
const cols = this.table.tabulator.getColumns(true)
6969

7070
const widget = document.querySelector(`#${this.table.columnWidgetId}`)
71+
if(!widget) {
72+
// column selector is not used
73+
return
74+
}
7175
widget.innerHTML = ''
7276

7377
cols.forEach((colProxy) => {

0 commit comments

Comments
 (0)