fh-matui 0.9.7__py3-none-any.whl → 0.9.8__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.
- fh_matui/__init__.py +1 -1
- fh_matui/datatable.py +138 -69
- {fh_matui-0.9.7.dist-info → fh_matui-0.9.8.dist-info}/METADATA +1 -1
- {fh_matui-0.9.7.dist-info → fh_matui-0.9.8.dist-info}/RECORD +8 -8
- {fh_matui-0.9.7.dist-info → fh_matui-0.9.8.dist-info}/WHEEL +0 -0
- {fh_matui-0.9.7.dist-info → fh_matui-0.9.8.dist-info}/entry_points.txt +0 -0
- {fh_matui-0.9.7.dist-info → fh_matui-0.9.8.dist-info}/licenses/LICENSE +0 -0
- {fh_matui-0.9.7.dist-info → fh_matui-0.9.8.dist-info}/top_level.txt +0 -0
fh_matui/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.9.
|
|
1
|
+
__version__ = "0.9.7"
|
fh_matui/datatable.py
CHANGED
|
@@ -113,8 +113,9 @@ def _action_menu(
|
|
|
113
113
|
)
|
|
114
114
|
|
|
115
115
|
items = []
|
|
116
|
-
# View is
|
|
117
|
-
|
|
116
|
+
# View is conditional (read operation)
|
|
117
|
+
if crud_ops.get("view", True):
|
|
118
|
+
items.append(action_item("View", "visibility", "view"))
|
|
118
119
|
|
|
119
120
|
if crud_ops.get("update", False):
|
|
120
121
|
items.append(action_item("Edit", "edit", "edit"))
|
|
@@ -122,6 +123,20 @@ def _action_menu(
|
|
|
122
123
|
if crud_ops.get("delete", False):
|
|
123
124
|
items.append(action_item("Delete", "delete", "delete", confirm="Delete this record?"))
|
|
124
125
|
|
|
126
|
+
# Custom actions
|
|
127
|
+
for custom_action in crud_ops.get("custom_actions", []):
|
|
128
|
+
# Check per-row condition if provided
|
|
129
|
+
condition = custom_action.get("condition")
|
|
130
|
+
if condition and not condition(row):
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
items.append(action_item(
|
|
134
|
+
label=custom_action["label"],
|
|
135
|
+
icon=custom_action["icon"],
|
|
136
|
+
action=custom_action["name"],
|
|
137
|
+
confirm=custom_action.get("confirm")
|
|
138
|
+
))
|
|
139
|
+
|
|
125
140
|
return Div(
|
|
126
141
|
Button(
|
|
127
142
|
Icon("more_vert"),
|
|
@@ -206,87 +221,95 @@ def DataTable(
|
|
|
206
221
|
)
|
|
207
222
|
table_rows.append(row_dict)
|
|
208
223
|
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
224
|
+
# Create button
|
|
225
|
+
create_button = None
|
|
226
|
+
if crud_ops.get("create", False):
|
|
227
|
+
query = urlencode({"action": "create", "search": search, "page": page, "page_size": page_size})
|
|
228
|
+
create_button = Button(
|
|
229
|
+
Icon("add"),
|
|
230
|
+
create_label,
|
|
231
|
+
hx_get=f"{base_route}/action?{query}",
|
|
232
|
+
hx_target=f"#{feedback_id}",
|
|
233
|
+
hx_swap="outerHTML",
|
|
234
|
+
cls=ButtonT.primary
|
|
235
|
+
)
|
|
221
236
|
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
# Pagination controls
|
|
238
|
+
pagination = None
|
|
239
|
+
if total_pages > 1:
|
|
240
|
+
prev_params = urlencode({"search": search, "page": max(1, page - 1), "page_size": page_size})
|
|
241
|
+
next_params = urlencode({"search": search, "page": min(total_pages, page + 1), "page_size": page_size})
|
|
242
|
+
|
|
243
|
+
pagination = Div(
|
|
244
|
+
Button(
|
|
245
|
+
Icon("navigate_before"),
|
|
246
|
+
hx_get=f"{base_route}?{prev_params}",
|
|
231
247
|
hx_target=f"#{container_id}",
|
|
232
248
|
hx_push_url="true",
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
onfocus="this.parentElement.classList.add('max')",
|
|
236
|
-
onblur="if(!this.value) this.parentElement.classList.remove('max')"
|
|
249
|
+
disabled=page == 1,
|
|
250
|
+
cls="circle"
|
|
237
251
|
),
|
|
238
|
-
cls="
|
|
239
|
-
style="transition: flex-grow 0.3s ease"
|
|
240
|
-
)
|
|
241
|
-
]
|
|
242
|
-
|
|
243
|
-
if crud_ops.get("create", False):
|
|
244
|
-
create_query = urlencode({"action": "create", "search": search, "page": page, "page_size": page_size})
|
|
245
|
-
toolbar_items.append(
|
|
252
|
+
Span(f"Page {page} of {total_pages}", cls="middle"),
|
|
246
253
|
Button(
|
|
247
|
-
Icon("
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
)
|
|
254
|
+
Icon("navigate_next"),
|
|
255
|
+
hx_get=f"{base_route}?{next_params}",
|
|
256
|
+
hx_target=f"#{container_id}",
|
|
257
|
+
hx_push_url="true",
|
|
258
|
+
disabled=page == total_pages,
|
|
259
|
+
cls="circle"
|
|
260
|
+
),
|
|
261
|
+
cls="row center-align"
|
|
254
262
|
)
|
|
255
263
|
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
Div(Span(summary, cls="small-text grey-text")),
|
|
264
|
+
# Search form
|
|
265
|
+
search_form = Form(
|
|
259
266
|
Div(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
267
|
+
Icon("search", cls="small"),
|
|
268
|
+
Input(
|
|
269
|
+
type="search",
|
|
270
|
+
name="search",
|
|
271
|
+
value=search,
|
|
272
|
+
placeholder=search_placeholder,
|
|
273
|
+
hx_get=base_route,
|
|
274
|
+
hx_trigger="input changed delay:300ms, search",
|
|
275
|
+
hx_target=f"#{container_id}",
|
|
276
|
+
hx_push_url="true",
|
|
277
|
+
hx_include="[name='page_size']"
|
|
266
278
|
),
|
|
267
|
-
cls="
|
|
279
|
+
cls="field prefix"
|
|
268
280
|
),
|
|
269
|
-
|
|
281
|
+
Input(type="hidden", name="page_size", value=page_size),
|
|
282
|
+
cls="row"
|
|
270
283
|
)
|
|
271
284
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
TableFromDicts(
|
|
279
|
-
header_keys,
|
|
280
|
-
table_rows,
|
|
281
|
-
header_cell_render=lambda k: Th(label_map.get(k, k)),
|
|
282
|
-
cls="border"
|
|
285
|
+
return Article(
|
|
286
|
+
Div(id=feedback_id), # Feedback container for modals/toasts
|
|
287
|
+
Div(
|
|
288
|
+
search_form,
|
|
289
|
+
create_button,
|
|
290
|
+
cls="grid"
|
|
283
291
|
),
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
292
|
+
Table(
|
|
293
|
+
*table_rows,
|
|
294
|
+
headers=header_labels,
|
|
295
|
+
keys=header_keys,
|
|
296
|
+
cls="border"
|
|
297
|
+
) if data else Div(empty_message, cls="center-align padding"),
|
|
298
|
+
Div(
|
|
299
|
+
Div(
|
|
300
|
+
_page_size_select(page_size, search, base_route, container_id, page_sizes),
|
|
301
|
+
Span(summary, cls="small-text grey-text"),
|
|
302
|
+
cls="row"
|
|
303
|
+
),
|
|
304
|
+
pagination,
|
|
305
|
+
cls="grid"
|
|
306
|
+
) if total > 0 else None,
|
|
307
|
+
id=container_id,
|
|
308
|
+
hx_trigger=f"{container_id}-refresh from:body",
|
|
309
|
+
hx_get=f"{base_route}?{base_query}",
|
|
310
|
+
hx_target=f"#{container_id}",
|
|
311
|
+
hx_swap="outerHTML"
|
|
287
312
|
)
|
|
288
|
-
|
|
289
|
-
return Div(card, Div(id=feedback_id), id=container_id)
|
|
290
313
|
|
|
291
314
|
# %% ../nbs/05_datatable.ipynb 9
|
|
292
315
|
import asyncio
|
|
@@ -479,6 +502,29 @@ class DataTableResource:
|
|
|
479
502
|
}
|
|
480
503
|
else:
|
|
481
504
|
self.crud_ops = crud_ops
|
|
505
|
+
|
|
506
|
+
# Validate custom_actions if provided
|
|
507
|
+
custom_actions = crud_ops.get("custom_actions", [])
|
|
508
|
+
if custom_actions:
|
|
509
|
+
reserved_names = {"view", "edit", "delete", "create"}
|
|
510
|
+
for action in custom_actions:
|
|
511
|
+
# Check required fields
|
|
512
|
+
if "name" not in action:
|
|
513
|
+
raise ValueError("Custom action missing required 'name' field")
|
|
514
|
+
if "label" not in action:
|
|
515
|
+
raise ValueError(f"Custom action '{action['name']}' missing required 'label' field")
|
|
516
|
+
if "icon" not in action:
|
|
517
|
+
raise ValueError(f"Custom action '{action['name']}' missing required 'icon' field")
|
|
518
|
+
if "handler" not in action:
|
|
519
|
+
raise ValueError(f"Custom action '{action['name']}' missing required 'handler' field")
|
|
520
|
+
|
|
521
|
+
# Check for reserved name collisions
|
|
522
|
+
if action["name"] in reserved_names:
|
|
523
|
+
raise ValueError(f"Custom action name '{action['name']}' conflicts with reserved action name")
|
|
524
|
+
|
|
525
|
+
# Validate handler is callable
|
|
526
|
+
if not callable(action["handler"]):
|
|
527
|
+
raise ValueError(f"Custom action '{action['name']}' handler must be callable")
|
|
482
528
|
|
|
483
529
|
# CRUD hooks
|
|
484
530
|
self.on_create_hook = on_create
|
|
@@ -743,6 +789,29 @@ class DataTableResource:
|
|
|
743
789
|
logger.error(f"Delete failed: {e}", exc_info=True)
|
|
744
790
|
return self._error_toast(f"Delete failed: {str(e)}")
|
|
745
791
|
|
|
792
|
+
# Handle CUSTOM ACTIONS
|
|
793
|
+
custom_actions = self.crud_ops.get("custom_actions", [])
|
|
794
|
+
for custom_action in custom_actions:
|
|
795
|
+
if action == custom_action["name"]:
|
|
796
|
+
try:
|
|
797
|
+
ctx = self._build_context(req, record=record, record_id=record_id)
|
|
798
|
+
handler = custom_action["handler"]
|
|
799
|
+
|
|
800
|
+
# Execute handler (sync or async)
|
|
801
|
+
if asyncio.iscoroutinefunction(handler):
|
|
802
|
+
loop = asyncio.get_event_loop()
|
|
803
|
+
result = loop.run_until_complete(handler(ctx))
|
|
804
|
+
else:
|
|
805
|
+
result = handler(ctx)
|
|
806
|
+
|
|
807
|
+
# Return success toast with refresh trigger
|
|
808
|
+
message = result if isinstance(result, str) else f"{custom_action['label']} completed successfully."
|
|
809
|
+
return self._success_toast(message)
|
|
810
|
+
|
|
811
|
+
except Exception as e:
|
|
812
|
+
logger.error(f"Custom action '{action}' failed: {e}", exc_info=True)
|
|
813
|
+
return self._error_toast(f"{custom_action['label']} failed: {str(e)}")
|
|
814
|
+
|
|
746
815
|
return Div(id=self.feedback_id)
|
|
747
816
|
|
|
748
817
|
async def _handle_save(self, req):
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
fh_matui/__init__.py,sha256=
|
|
1
|
+
fh_matui/__init__.py,sha256=83qFJMFAgvdHV5e8Nh9ByUaQinNxBUPlTKjxfpge3xM,23
|
|
2
2
|
fh_matui/_modidx.py,sha256=naHwPQ4kCo-5saE_uozmdPA881kp1gnqvKhmaG-Ya-4,23914
|
|
3
3
|
fh_matui/app_pages.py,sha256=Sn9-tgBpaPNbR-0nZtPLoSCmAWLOGB4UQ88IkFvzBRY,10361
|
|
4
4
|
fh_matui/components.py,sha256=KjdTHzWRXpVWBEIGskW1HfhjPpzRYzi6UA_yRjZyMWM,48254
|
|
5
5
|
fh_matui/core.py,sha256=xtVBN8CtC50ZJ4Iu7o-mUhaA87tWdnz8gBfKRk63Zhs,10680
|
|
6
|
-
fh_matui/datatable.py,sha256=
|
|
6
|
+
fh_matui/datatable.py,sha256=UWS4PlFl2WjmgU54GLc4CJCS4wiLpDsUyX9MDdwRXWY,35117
|
|
7
7
|
fh_matui/foundations.py,sha256=b7PnObJpKN8ZAU9NzCm9xpfnHzFjjAROU7E2YvA_tj4,1820
|
|
8
8
|
fh_matui/web_pages.py,sha256=4mF-jpfVcZTVepfQ-aMGgIUp-nBp0YCkvcdsWhUYeaA,34879
|
|
9
|
-
fh_matui-0.9.
|
|
10
|
-
fh_matui-0.9.
|
|
11
|
-
fh_matui-0.9.
|
|
12
|
-
fh_matui-0.9.
|
|
13
|
-
fh_matui-0.9.
|
|
14
|
-
fh_matui-0.9.
|
|
9
|
+
fh_matui-0.9.8.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
|
|
10
|
+
fh_matui-0.9.8.dist-info/METADATA,sha256=WcGOgoryZOSjmfJPM21WIEbrB_55uVwtzD-Y0JCbbQA,10490
|
|
11
|
+
fh_matui-0.9.8.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
12
|
+
fh_matui-0.9.8.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
|
|
13
|
+
fh_matui-0.9.8.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
|
|
14
|
+
fh_matui-0.9.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|