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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.9.6"
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 always available (read operation)
117
- items.append(action_item("View", "visibility", "view"))
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
- # Empty state
210
- if not table_rows:
211
- empty_row = {col["key"]: "" for col in columns}
212
- empty_row[columns[0]["key"]] = Span(empty_message, cls="small-text grey-text")
213
- empty_row["actions"] = ""
214
- table_rows.append(empty_row)
215
-
216
- # Build header content
217
- header_content = DivFullySpaced(
218
- H5(title),
219
- Span(f"{total} total", cls="small-text grey-text")
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
- # Build search and create button row
223
- toolbar_items = [
224
- Field(
225
- Input(
226
- placeholder=search_placeholder,
227
- name="search",
228
- value=search,
229
- hx_get=f"{base_route}?page_size={page_size}",
230
- hx_trigger="keyup changed delay:500ms",
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
- hx_vals='{"page":1}',
234
- autocomplete="off",
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="border round",
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("add"),
248
- Span(create_label),
249
- cls=ButtonT.primary,
250
- hx_get=f"{base_route}/action?{create_query}",
251
- hx_target=f"#{feedback_id}",
252
- hx_swap="outerHTML"
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
- # Build footer with pagination - count on left, paginator centered below table
257
- footer_content = Div(
258
- Div(Span(summary, cls="small-text grey-text")),
264
+ # Search form
265
+ search_form = Form(
259
266
  Div(
260
- _page_size_select(page_size, search, base_route, container_id, page_sizes),
261
- Pagination(
262
- page,
263
- total_pages,
264
- f"{base_route}?{base_query}",
265
- hx_target=f"#{container_id}"
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="row center-align middle-align small-space"
279
+ cls="field prefix"
268
280
  ),
269
- cls="grid"
281
+ Input(type="hidden", name="page_size", value=page_size),
282
+ cls="row"
270
283
  )
271
284
 
272
- # Create header label mapping for custom rendering
273
- label_map = dict(zip(header_keys, header_labels))
274
-
275
- # Build the card
276
- card = Card(
277
- DivFullySpaced(*toolbar_items, cls="padding"),
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
- footer=footer_content,
285
- header=header_content,
286
- cls="surface-container border round"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fh-matui
3
- Version: 0.9.7
3
+ Version: 0.9.8
4
4
  Summary: material-ui for fasthtml
5
5
  Home-page: https://github.com/abhisheksreesaila/fh-matui
6
6
  Author: abhishek sreesaila
@@ -1,14 +1,14 @@
1
- fh_matui/__init__.py,sha256=DUP796j17_h9tFqkdOpSu_66sw_6eSK4S2hN7yd9i9o,23
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=SeqQ1Tz7KPJdaM9tjWH8er7od_D22O0MDW-VsTmddUo,31714
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.7.dist-info/licenses/LICENSE,sha256=xV8xoN4VOL0uw9X8RSs2IMuD_Ss_a9yAbtGNeBWZwnw,11337
10
- fh_matui-0.9.7.dist-info/METADATA,sha256=NaRFkfivpA9VdJASWuvs80jxZVKh3PWDvkZv9tyaszY,10490
11
- fh_matui-0.9.7.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
12
- fh_matui-0.9.7.dist-info/entry_points.txt,sha256=zn4CR4gNTiAAxbFsCxHAf2tQhtW29_YOffjbUTgeoWI,38
13
- fh_matui-0.9.7.dist-info/top_level.txt,sha256=l80d5eoA2ZjqtPYwAorLMS5PiHxUxz3zKzxMJ41Xoso,9
14
- fh_matui-0.9.7.dist-info/RECORD,,
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,,