fh-matui 0.9.9__tar.gz → 0.9.10__tar.gz
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.
- {fh_matui-0.9.9/fh_matui.egg-info → fh_matui-0.9.10}/PKG-INFO +1 -1
- fh_matui-0.9.10/fh_matui/__init__.py +1 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui/components.py +60 -22
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui/datatable.py +108 -46
- {fh_matui-0.9.9 → fh_matui-0.9.10/fh_matui.egg-info}/PKG-INFO +1 -1
- {fh_matui-0.9.9 → fh_matui-0.9.10}/settings.ini +1 -1
- fh_matui-0.9.9/fh_matui/__init__.py +0 -1
- {fh_matui-0.9.9 → fh_matui-0.9.10}/LICENSE +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/MANIFEST.in +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/README.md +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui/_modidx.py +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui/app_pages.py +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui/core.py +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui/foundations.py +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui/web_pages.py +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui.egg-info/SOURCES.txt +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui.egg-info/dependency_links.txt +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui.egg-info/entry_points.txt +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui.egg-info/not-zip-safe +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui.egg-info/requires.txt +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/fh_matui.egg-info/top_level.txt +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/pyproject.toml +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/setup.cfg +0 -0
- {fh_matui-0.9.9 → fh_matui-0.9.10}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.9.9"
|
|
@@ -331,9 +331,14 @@ def NavBar(*children, brand=None, sticky=False, cls='', **kwargs):
|
|
|
331
331
|
return Nav(*children, cls=f"padding {nav_cls}", **kwargs)
|
|
332
332
|
|
|
333
333
|
# %% ../nbs/02_components.ipynb 35
|
|
334
|
-
def Modal(*c, id=None, footer=None, active=False, overlay=
|
|
335
|
-
"""BeerCSS modal dialog with
|
|
334
|
+
def Modal(*c, id=None, footer=None, active=False, overlay='default', position=None, cls=(), **kwargs):
|
|
335
|
+
"""BeerCSS modal dialog with position and overlay options."""
|
|
336
336
|
modal_cls = normalize_tokens(cls)
|
|
337
|
+
|
|
338
|
+
# Add position class if specified
|
|
339
|
+
if position in ['left', 'right', 'top', 'bottom']:
|
|
340
|
+
modal_cls.append(position)
|
|
341
|
+
|
|
337
342
|
if active:
|
|
338
343
|
modal_cls.append('active')
|
|
339
344
|
|
|
@@ -349,9 +354,22 @@ def Modal(*c, id=None, footer=None, active=False, overlay=True, cls=(), **kwargs
|
|
|
349
354
|
# Create the dialog
|
|
350
355
|
dialog = Dialog(*children, id=id, cls=cls_str, **kwargs)
|
|
351
356
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
357
|
+
# Handle overlay
|
|
358
|
+
if overlay and overlay not in [False, None]:
|
|
359
|
+
overlay_classes = ['overlay']
|
|
360
|
+
|
|
361
|
+
# Map overlay parameter to BeerCSS classes
|
|
362
|
+
if overlay == 'blur':
|
|
363
|
+
overlay_classes.append('blur')
|
|
364
|
+
elif overlay == 'small-blur':
|
|
365
|
+
overlay_classes.append('small-blur')
|
|
366
|
+
elif overlay == 'medium-blur':
|
|
367
|
+
overlay_classes.append('medium-blur')
|
|
368
|
+
elif overlay == 'large-blur':
|
|
369
|
+
overlay_classes.append('large-blur')
|
|
370
|
+
# 'default' means plain overlay with no blur class
|
|
371
|
+
|
|
372
|
+
overlay_cls = ' '.join(overlay_classes)
|
|
355
373
|
if active:
|
|
356
374
|
overlay_cls += " active"
|
|
357
375
|
|
|
@@ -717,15 +735,14 @@ def Toolbar(*items, cls='', elevate='large', fill=True, **kwargs):
|
|
|
717
735
|
if cls: classes.append(cls)
|
|
718
736
|
return Nav(*items, cls=' '.join(classes), **kwargs)
|
|
719
737
|
|
|
720
|
-
# %% ../nbs/02_components.ipynb
|
|
738
|
+
# %% ../nbs/02_components.ipynb 93
|
|
721
739
|
#| code-fold: true
|
|
722
|
-
def Toast(*c, cls='', position='top', variant='', action=None,
|
|
723
|
-
"""BeerCSS snackbar/toast notification with position and variant options."""
|
|
740
|
+
def Toast(*c, cls='', position='top', variant='', action=None, active=False, dur=None, **kwargs):
|
|
741
|
+
"""BeerCSS snackbar/toast notification with position ('top' or 'bottom') and variant options."""
|
|
724
742
|
classes = ['snackbar']
|
|
725
743
|
if variant: classes.append(variant)
|
|
726
|
-
if position:
|
|
727
|
-
|
|
728
|
-
classes.append(position_map.get(position, position))
|
|
744
|
+
if position in ['top', 'bottom']:
|
|
745
|
+
classes.append(position)
|
|
729
746
|
if active: classes.append('active')
|
|
730
747
|
if cls: classes.append(cls)
|
|
731
748
|
|
|
@@ -735,13 +752,34 @@ def Toast(*c, cls='', position='top', variant='', action=None, dur=5.0, active=F
|
|
|
735
752
|
if isinstance(action, str): content.append(A(action, cls='inverse-link'))
|
|
736
753
|
else: content.append(action)
|
|
737
754
|
else: content.extend(c)
|
|
738
|
-
|
|
755
|
+
|
|
756
|
+
snackbar = Div(*content, cls=' '.join(classes), **kwargs)
|
|
757
|
+
|
|
758
|
+
# If duration is specified and there's an id, generate auto-hide script
|
|
759
|
+
if dur and 'id' in kwargs:
|
|
760
|
+
timeout_ms = int(dur * 1000)
|
|
761
|
+
toast_id = kwargs['id']
|
|
762
|
+
# Wait for Beer CSS module to load before calling ui()
|
|
763
|
+
script = Script(f"""
|
|
764
|
+
window.addEventListener('load', function() {{
|
|
765
|
+
function showToast() {{
|
|
766
|
+
if (typeof ui !== 'undefined') {{
|
|
767
|
+
try {{ ui('#{toast_id}', {timeout_ms}); }}
|
|
768
|
+
catch (e) {{ console.error('Toast error:', e.message); }}
|
|
769
|
+
}}
|
|
770
|
+
}}
|
|
771
|
+
setTimeout(showToast, 50);
|
|
772
|
+
}});
|
|
773
|
+
""")
|
|
774
|
+
return (snackbar, script)
|
|
775
|
+
|
|
776
|
+
return snackbar
|
|
739
777
|
|
|
740
778
|
def Snackbar(*c, **kwargs):
|
|
741
779
|
"""Alias for Toast component."""
|
|
742
780
|
return Toast(*c, **kwargs)
|
|
743
781
|
|
|
744
|
-
# %% ../nbs/02_components.ipynb
|
|
782
|
+
# %% ../nbs/02_components.ipynb 96
|
|
745
783
|
#| code-fold: true
|
|
746
784
|
class ContainerT(VEnum):
|
|
747
785
|
"""Container size options (BeerCSS). Most alias to 'responsive'; use 'expand' for full-width."""
|
|
@@ -752,7 +790,7 @@ class ContainerT(VEnum):
|
|
|
752
790
|
xl = 'responsive'
|
|
753
791
|
expand = 'responsive max'
|
|
754
792
|
|
|
755
|
-
# %% ../nbs/02_components.ipynb
|
|
793
|
+
# %% ../nbs/02_components.ipynb 98
|
|
756
794
|
#| code-fold: true
|
|
757
795
|
def _get_form_config(col: dict) -> dict:
|
|
758
796
|
"""Extract form config from column, with sensible defaults."""
|
|
@@ -828,7 +866,7 @@ def FormField(
|
|
|
828
866
|
**attrs
|
|
829
867
|
)
|
|
830
868
|
|
|
831
|
-
# %% ../nbs/02_components.ipynb
|
|
869
|
+
# %% ../nbs/02_components.ipynb 100
|
|
832
870
|
#| code-fold: true
|
|
833
871
|
from typing import Callable, Any
|
|
834
872
|
|
|
@@ -947,7 +985,7 @@ def FormModal(
|
|
|
947
985
|
cls="large-width"
|
|
948
986
|
)
|
|
949
987
|
|
|
950
|
-
# %% ../nbs/02_components.ipynb
|
|
988
|
+
# %% ../nbs/02_components.ipynb 102
|
|
951
989
|
#| code-fold: true
|
|
952
990
|
def NavContainer(*li, title=None, brand=None, position='left', close_button=True, cls='active', id=None, **kwargs):
|
|
953
991
|
"""Slide-out navigation drawer with header and close button."""
|
|
@@ -1004,7 +1042,7 @@ def BottomNav(*c, cls='bottom', size='s', **kwargs):
|
|
|
1004
1042
|
final_cls = f"{cls} {size_cls}".strip()
|
|
1005
1043
|
return Nav(*c, cls=final_cls, **kwargs)
|
|
1006
1044
|
|
|
1007
|
-
# %% ../nbs/02_components.ipynb
|
|
1045
|
+
# %% ../nbs/02_components.ipynb 105
|
|
1008
1046
|
#| code-fold: true
|
|
1009
1047
|
def NavSideBarHeader(*c, cls='', **kwargs):
|
|
1010
1048
|
"""Sidebar header section for menu buttons and branding."""
|
|
@@ -1024,7 +1062,7 @@ def NavSideBarContainer(*children, position='left', size='m', cls='', active=Fal
|
|
|
1024
1062
|
nav_cls = f"{base_cls} {cls}".strip()
|
|
1025
1063
|
return Nav(*children, cls=nav_cls, **kwargs)
|
|
1026
1064
|
|
|
1027
|
-
# %% ../nbs/02_components.ipynb
|
|
1065
|
+
# %% ../nbs/02_components.ipynb 107
|
|
1028
1066
|
#| code-fold: true
|
|
1029
1067
|
def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
|
|
1030
1068
|
main_bg='surface', sidebar_id='app-sidebar', cls='', **kwargs):
|
|
@@ -1069,7 +1107,7 @@ def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_s
|
|
|
1069
1107
|
final_cls = f"surface-container {cls}".strip() if cls else "surface-container"
|
|
1070
1108
|
return Div(*layout_children, cls=final_cls, **kwargs)
|
|
1071
1109
|
|
|
1072
|
-
# %% ../nbs/02_components.ipynb
|
|
1110
|
+
# %% ../nbs/02_components.ipynb 111
|
|
1073
1111
|
#| code-fold: true
|
|
1074
1112
|
class TextT(VEnum):
|
|
1075
1113
|
"""Text styles using BeerCSS typography classes."""
|
|
@@ -1101,7 +1139,7 @@ class TextPresets(VEnum):
|
|
|
1101
1139
|
primary_link = 'link primary-text'
|
|
1102
1140
|
muted_link = 'link secondary-text'
|
|
1103
1141
|
|
|
1104
|
-
# %% ../nbs/02_components.ipynb
|
|
1142
|
+
# %% ../nbs/02_components.ipynb 112
|
|
1105
1143
|
#| code-fold: true
|
|
1106
1144
|
def CodeSpan(*c, cls=(), **kwargs):
|
|
1107
1145
|
"""Inline code snippet."""
|
|
@@ -1157,7 +1195,7 @@ def Sup(*c, cls=(), **kwargs):
|
|
|
1157
1195
|
cls_str = stringify(cls) if cls else None
|
|
1158
1196
|
return fc.Sup(*c, cls=cls_str, **kwargs) if cls_str else fc.Sup(*c, **kwargs)
|
|
1159
1197
|
|
|
1160
|
-
# %% ../nbs/02_components.ipynb
|
|
1198
|
+
# %% ../nbs/02_components.ipynb 114
|
|
1161
1199
|
#| code-fold: true
|
|
1162
1200
|
def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str = ''):
|
|
1163
1201
|
"""Collapsible FAQ item using details/summary.
|
|
@@ -1175,7 +1213,7 @@ def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str
|
|
|
1175
1213
|
Summary(Article(Nav(Div(question, cls=f"max bold {question_cls}".strip()), I("expand_more")), cls="round surface-variant border no-elevate")),
|
|
1176
1214
|
Article(P(answer, cls=f"secondary-text {answer_cls}".strip()), cls="round border padding"))
|
|
1177
1215
|
|
|
1178
|
-
# %% ../nbs/02_components.ipynb
|
|
1216
|
+
# %% ../nbs/02_components.ipynb 118
|
|
1179
1217
|
#| code-fold: true
|
|
1180
1218
|
def CookiesBanner(message='We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.',
|
|
1181
1219
|
accept_text='Accept', decline_text='Decline', settings_text=None, policy_link='/cookies', policy_text='Learn more',
|
|
@@ -123,6 +123,7 @@ def _action_menu(
|
|
|
123
123
|
row: dict,
|
|
124
124
|
row_id: Any,
|
|
125
125
|
crud_ops: dict,
|
|
126
|
+
crud_enabled: dict,
|
|
126
127
|
base_route: str,
|
|
127
128
|
search: str,
|
|
128
129
|
page: int,
|
|
@@ -153,14 +154,14 @@ def _action_menu(
|
|
|
153
154
|
)
|
|
154
155
|
|
|
155
156
|
items = []
|
|
156
|
-
# View is conditional (read operation)
|
|
157
|
-
if
|
|
157
|
+
# View is conditional (read operation) - use crud_enabled for callable support
|
|
158
|
+
if crud_enabled.get("view", True):
|
|
158
159
|
items.append(action_item("View", "visibility", "view"))
|
|
159
160
|
|
|
160
|
-
if
|
|
161
|
+
if crud_enabled.get("update", False):
|
|
161
162
|
items.append(action_item("Edit", "edit", "edit"))
|
|
162
163
|
|
|
163
|
-
if
|
|
164
|
+
if crud_enabled.get("delete", False):
|
|
164
165
|
items.append(action_item("Delete", "delete", "delete", confirm="Delete this record?"))
|
|
165
166
|
|
|
166
167
|
# Custom actions
|
|
@@ -197,6 +198,7 @@ def DataTable(
|
|
|
197
198
|
search: str = '',
|
|
198
199
|
columns: list[dict] = None,
|
|
199
200
|
crud_ops: dict = None,
|
|
201
|
+
crud_enabled: dict = None,
|
|
200
202
|
base_route: str = '',
|
|
201
203
|
row_id_field: str = 'id',
|
|
202
204
|
title: str = 'Records',
|
|
@@ -209,6 +211,7 @@ def DataTable(
|
|
|
209
211
|
"Generic data table with server-side pagination, search, and row actions."
|
|
210
212
|
# Defaults
|
|
211
213
|
crud_ops = crud_ops or {"create": False, "update": False, "delete": False}
|
|
214
|
+
crud_enabled = crud_enabled or {k: bool(v) for k, v in crud_ops.items() if k != 'custom_actions'}
|
|
212
215
|
page_sizes = page_sizes or PAGE_SIZES
|
|
213
216
|
container_id = container_id or f"crud-table-{base_route.replace('/', '-').strip('-')}"
|
|
214
217
|
feedback_id = f"{container_id}-feedback"
|
|
@@ -227,7 +230,6 @@ def DataTable(
|
|
|
227
230
|
start_index = (page - 1) * page_size + 1 if total else 0
|
|
228
231
|
end_index = min(start_index + page_size - 1, total) if total else 0
|
|
229
232
|
summary = f"{start_index}-{end_index} of {total} records" if total else "No matching records"
|
|
230
|
-
|
|
231
233
|
base_query = urlencode({"search": search, "page_size": page_size})
|
|
232
234
|
|
|
233
235
|
# Build table header keys and labels
|
|
@@ -255,6 +257,7 @@ def DataTable(
|
|
|
255
257
|
row=row,
|
|
256
258
|
row_id=row_id,
|
|
257
259
|
crud_ops=crud_ops,
|
|
260
|
+
crud_enabled=crud_enabled,
|
|
258
261
|
base_route=base_route,
|
|
259
262
|
search=search,
|
|
260
263
|
page=page,
|
|
@@ -264,9 +267,9 @@ def DataTable(
|
|
|
264
267
|
)
|
|
265
268
|
table_rows.append(row_dict)
|
|
266
269
|
|
|
267
|
-
# Create button
|
|
270
|
+
# Create button - use crud_enabled for callable support
|
|
268
271
|
create_button = None
|
|
269
|
-
if
|
|
272
|
+
if crud_enabled.get("create", False):
|
|
270
273
|
query = urlencode({"action": "create", "search": search, "page": page, "page_size": page_size})
|
|
271
274
|
create_button = Button(
|
|
272
275
|
Icon("add"),
|
|
@@ -335,6 +338,24 @@ def DataTable(
|
|
|
335
338
|
cls="border"
|
|
336
339
|
) if data else Div(empty_message, cls="center-align padding")
|
|
337
340
|
|
|
341
|
+
# Footer section with page size selector and pagination
|
|
342
|
+
footer = None
|
|
343
|
+
if total > 0:
|
|
344
|
+
page_info_row = Div(
|
|
345
|
+
_page_size_select(page_size, search, base_route, container_id, page_sizes),
|
|
346
|
+
Span(summary, cls="small-text grey-text"),
|
|
347
|
+
cls="row"
|
|
348
|
+
)
|
|
349
|
+
if pagination:
|
|
350
|
+
# Two-row layout: info on top, centered pagination below
|
|
351
|
+
footer = Div(
|
|
352
|
+
page_info_row,
|
|
353
|
+
Div(pagination, cls="row center-align"),
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
# Single row when no pagination needed
|
|
357
|
+
footer = page_info_row
|
|
358
|
+
|
|
338
359
|
return Article(
|
|
339
360
|
Div(id=feedback_id), # Feedback container for modals/toasts
|
|
340
361
|
Div(
|
|
@@ -343,15 +364,7 @@ def DataTable(
|
|
|
343
364
|
cls="row"
|
|
344
365
|
),
|
|
345
366
|
data_table,
|
|
346
|
-
|
|
347
|
-
Div(
|
|
348
|
-
_page_size_select(page_size, search, base_route, container_id, page_sizes),
|
|
349
|
-
Span(summary, cls="small-text grey-text"),
|
|
350
|
-
cls="row"
|
|
351
|
-
),
|
|
352
|
-
pagination,
|
|
353
|
-
cls="grid"
|
|
354
|
-
) if total > 0 else None,
|
|
367
|
+
footer,
|
|
355
368
|
id=container_id,
|
|
356
369
|
hx_trigger=f"{container_id}-refresh from:body",
|
|
357
370
|
hx_get=f"{base_route}?{base_query}",
|
|
@@ -453,6 +466,7 @@ class CrudContext:
|
|
|
453
466
|
tbl: Optional[Any] = None # request.state.tables[table_name] (if available)
|
|
454
467
|
record: dict = None # Form data dict
|
|
455
468
|
record_id: Optional[Any] = None # ID for update/delete (None for create)
|
|
469
|
+
feedback_id: Optional[str] = None # Target div ID for HTMX swap (for override handlers)
|
|
456
470
|
|
|
457
471
|
# %% ../nbs/05_datatable.ipynb 13
|
|
458
472
|
from typing import Callable, Optional, Any, Union
|
|
@@ -574,6 +588,20 @@ class DataTableResource:
|
|
|
574
588
|
if not callable(action["handler"]):
|
|
575
589
|
raise ValueError(f"Custom action '{action['name']}' handler must be callable")
|
|
576
590
|
|
|
591
|
+
# Parse crud_ops for callable overrides vs boolean enable/disable
|
|
592
|
+
# Callable = override handler (action is enabled)
|
|
593
|
+
# True = use default handler (action is enabled)
|
|
594
|
+
# False = action is disabled
|
|
595
|
+
self.crud_overrides = {}
|
|
596
|
+
self.crud_enabled = {}
|
|
597
|
+
for action_name in ['create', 'update', 'delete', 'view']:
|
|
598
|
+
value = self.crud_ops.get(action_name, action_name == 'view') # view defaults to True
|
|
599
|
+
if callable(value):
|
|
600
|
+
self.crud_overrides[action_name] = value
|
|
601
|
+
self.crud_enabled[action_name] = True # Callable means action is enabled
|
|
602
|
+
else:
|
|
603
|
+
self.crud_enabled[action_name] = bool(value)
|
|
604
|
+
|
|
577
605
|
# CRUD hooks
|
|
578
606
|
self.on_create_hook = on_create
|
|
579
607
|
self.on_update_hook = on_update
|
|
@@ -603,7 +631,7 @@ class DataTableResource:
|
|
|
603
631
|
return await hook(ctx)
|
|
604
632
|
return hook(ctx)
|
|
605
633
|
|
|
606
|
-
def _build_context(self, req, record: dict = None, record_id: Any = None) -> CrudContext:
|
|
634
|
+
def _build_context(self, req, record: dict = None, record_id: Any = None, include_feedback_id: bool = False) -> CrudContext:
|
|
607
635
|
"""🏗️ Build CrudContext from request."""
|
|
608
636
|
user = None
|
|
609
637
|
db = None
|
|
@@ -629,7 +657,8 @@ class DataTableResource:
|
|
|
629
657
|
db=db,
|
|
630
658
|
tbl=tbl,
|
|
631
659
|
record=record or {},
|
|
632
|
-
record_id=record_id
|
|
660
|
+
record_id=record_id,
|
|
661
|
+
feedback_id=self.feedback_id if include_feedback_id else None
|
|
633
662
|
)
|
|
634
663
|
|
|
635
664
|
def _wrap_response(self, content, req):
|
|
@@ -647,8 +676,8 @@ class DataTableResource:
|
|
|
647
676
|
return self._handle_table(req)
|
|
648
677
|
|
|
649
678
|
@rt(f"{self.base_route}/action")
|
|
650
|
-
def _action_handler(req):
|
|
651
|
-
return self._handle_action(req)
|
|
679
|
+
async def _action_handler(req):
|
|
680
|
+
return await self._handle_action(req)
|
|
652
681
|
|
|
653
682
|
@rt(f"{self.base_route}/save")
|
|
654
683
|
async def _save_handler(req):
|
|
@@ -704,6 +733,7 @@ class DataTableResource:
|
|
|
704
733
|
search=search,
|
|
705
734
|
columns=self.columns,
|
|
706
735
|
crud_ops=self.crud_ops,
|
|
736
|
+
crud_enabled=self.crud_enabled,
|
|
707
737
|
base_route=self.base_route,
|
|
708
738
|
row_id_field=self.row_id_field,
|
|
709
739
|
title=self.title,
|
|
@@ -731,8 +761,8 @@ class DataTableResource:
|
|
|
731
761
|
# Wrap with layout if full-page request
|
|
732
762
|
return self._wrap_response(content, req)
|
|
733
763
|
|
|
734
|
-
def _handle_action(self, req):
|
|
735
|
-
"""Handle action route (view/edit/create/delete)."""
|
|
764
|
+
async def _handle_action(self, req):
|
|
765
|
+
"""Handle action route (view/edit/create/delete) with override support."""
|
|
736
766
|
params = getattr(req, "query_params", {})
|
|
737
767
|
getter = params.get if hasattr(params, "get") else (lambda k, d=None: params[k] if k in params else d)
|
|
738
768
|
|
|
@@ -755,9 +785,47 @@ class DataTableResource:
|
|
|
755
785
|
cancel_url = f"{self.base_route}/action?dismiss=1"
|
|
756
786
|
save_url = f"{self.base_route}/save?{return_params}"
|
|
757
787
|
|
|
758
|
-
#
|
|
788
|
+
# Get record for actions that need it (not create)
|
|
789
|
+
record = None
|
|
790
|
+
if record_id:
|
|
791
|
+
raw_record = self.get_by_id(req, record_id)
|
|
792
|
+
record = _to_dict(raw_record) if raw_record else None
|
|
793
|
+
|
|
794
|
+
# Map action names to internal action names for overrides
|
|
795
|
+
action_map = {'edit': 'update'} # 'edit' action uses 'update' override
|
|
796
|
+
override_key = action_map.get(action, action)
|
|
797
|
+
|
|
798
|
+
# Check for CRUD override FIRST (callable in crud_ops)
|
|
799
|
+
if override_key in self.crud_overrides:
|
|
800
|
+
try:
|
|
801
|
+
ctx = self._build_context(req, record=record, record_id=record_id, include_feedback_id=True)
|
|
802
|
+
handler = self.crud_overrides[override_key]
|
|
803
|
+
|
|
804
|
+
# Execute handler (sync or async)
|
|
805
|
+
if asyncio.iscoroutinefunction(handler):
|
|
806
|
+
result = await handler(ctx)
|
|
807
|
+
else:
|
|
808
|
+
result = handler(ctx)
|
|
809
|
+
|
|
810
|
+
# Handle return value:
|
|
811
|
+
# - FT component: Return directly (full control to user)
|
|
812
|
+
# - str: Wrap in success toast
|
|
813
|
+
# - None: Default success message
|
|
814
|
+
if result is None:
|
|
815
|
+
return self._success_toast(f"{action.title()} completed successfully.")
|
|
816
|
+
elif isinstance(result, str):
|
|
817
|
+
return self._success_toast(result)
|
|
818
|
+
else:
|
|
819
|
+
# FT component - wrap in feedback div
|
|
820
|
+
return Div(result, id=self.feedback_id)
|
|
821
|
+
|
|
822
|
+
except Exception as e:
|
|
823
|
+
logger.error(f"CRUD override '{action}' failed: {e}", exc_info=True)
|
|
824
|
+
return self._error_toast(f"{action.title()} failed: {str(e)}")
|
|
825
|
+
|
|
826
|
+
# Handle CREATE (default behavior)
|
|
759
827
|
if action == "create":
|
|
760
|
-
if not self.
|
|
828
|
+
if not self.crud_enabled.get("create"):
|
|
761
829
|
return self._error_toast("Create operation not enabled.")
|
|
762
830
|
|
|
763
831
|
modal = FormModal(
|
|
@@ -773,16 +841,11 @@ class DataTableResource:
|
|
|
773
841
|
)
|
|
774
842
|
return self._wrap_modal(modal)
|
|
775
843
|
|
|
776
|
-
#
|
|
777
|
-
record = None
|
|
778
|
-
if record_id:
|
|
779
|
-
raw_record = self.get_by_id(req, record_id)
|
|
780
|
-
record = _to_dict(raw_record) if raw_record else None
|
|
781
|
-
|
|
844
|
+
# Record required for remaining actions
|
|
782
845
|
if not record:
|
|
783
846
|
return self._error_toast("Record not found.")
|
|
784
847
|
|
|
785
|
-
# Handle VIEW
|
|
848
|
+
# Handle VIEW (default behavior)
|
|
786
849
|
if action == "view":
|
|
787
850
|
modal = FormModal(
|
|
788
851
|
columns=self.columns,
|
|
@@ -795,9 +858,9 @@ class DataTableResource:
|
|
|
795
858
|
)
|
|
796
859
|
return self._wrap_modal(modal)
|
|
797
860
|
|
|
798
|
-
# Handle EDIT
|
|
861
|
+
# Handle EDIT (default behavior)
|
|
799
862
|
if action == "edit":
|
|
800
|
-
if not self.
|
|
863
|
+
if not self.crud_enabled.get("update"):
|
|
801
864
|
return self._error_toast("Update operation not enabled.")
|
|
802
865
|
|
|
803
866
|
modal = FormModal(
|
|
@@ -813,9 +876,9 @@ class DataTableResource:
|
|
|
813
876
|
)
|
|
814
877
|
return self._wrap_modal(modal)
|
|
815
878
|
|
|
816
|
-
# Handle DELETE
|
|
879
|
+
# Handle DELETE (default behavior)
|
|
817
880
|
if action == "delete":
|
|
818
|
-
if not self.
|
|
881
|
+
if not self.crud_enabled.get("delete"):
|
|
819
882
|
return self._error_toast("Delete operation not enabled.")
|
|
820
883
|
|
|
821
884
|
try:
|
|
@@ -823,11 +886,7 @@ class DataTableResource:
|
|
|
823
886
|
|
|
824
887
|
# Use on_delete hook if provided
|
|
825
888
|
if self.on_delete_hook:
|
|
826
|
-
|
|
827
|
-
loop = asyncio.get_event_loop()
|
|
828
|
-
loop.run_until_complete(self.on_delete_hook(ctx))
|
|
829
|
-
else:
|
|
830
|
-
self.on_delete_hook(ctx)
|
|
889
|
+
await self._call_hook(self.on_delete_hook, ctx)
|
|
831
890
|
else:
|
|
832
891
|
# Default: use delete_fn
|
|
833
892
|
self.delete_fn(req, record_id)
|
|
@@ -842,19 +901,22 @@ class DataTableResource:
|
|
|
842
901
|
for custom_action in custom_actions:
|
|
843
902
|
if action == custom_action["name"]:
|
|
844
903
|
try:
|
|
845
|
-
ctx = self._build_context(req, record=record, record_id=record_id)
|
|
904
|
+
ctx = self._build_context(req, record=record, record_id=record_id, include_feedback_id=True)
|
|
846
905
|
handler = custom_action["handler"]
|
|
847
906
|
|
|
848
907
|
# Execute handler (sync or async)
|
|
849
908
|
if asyncio.iscoroutinefunction(handler):
|
|
850
|
-
|
|
851
|
-
result = loop.run_until_complete(handler(ctx))
|
|
909
|
+
result = await handler(ctx)
|
|
852
910
|
else:
|
|
853
911
|
result = handler(ctx)
|
|
854
912
|
|
|
855
|
-
#
|
|
856
|
-
|
|
857
|
-
|
|
913
|
+
# Handle return value same as overrides
|
|
914
|
+
if result is None:
|
|
915
|
+
return self._success_toast(f"{custom_action['label']} completed successfully.")
|
|
916
|
+
elif isinstance(result, str):
|
|
917
|
+
return self._success_toast(result)
|
|
918
|
+
else:
|
|
919
|
+
return Div(result, id=self.feedback_id)
|
|
858
920
|
|
|
859
921
|
except Exception as e:
|
|
860
922
|
logger.error(f"Custom action '{action}' failed: {e}", exc_info=True)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.9.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|