Fix NIL leaking into Python service calls, add mobile navigation menu
Strip NIL values at I/O primitive boundaries (frag, query, action, service) to prevent _Nil objects from reaching Python code that expects None. Add mobile_nav_sx() helper that auto-populates the hamburger menu from nav_tree and auth_menu context fragments when no menu slot is provided. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,26 @@ def root_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||
)
|
||||
|
||||
|
||||
def mobile_nav_sx(ctx: dict) -> str:
|
||||
"""Build mobile navigation panel from context fragments (nav_tree, auth_menu)."""
|
||||
nav_tree = ctx.get("nav_tree") or ""
|
||||
auth_menu = ctx.get("auth_menu") or ""
|
||||
if not nav_tree and not auth_menu:
|
||||
return ""
|
||||
parts: list[str] = []
|
||||
if nav_tree:
|
||||
nav_tree_sx = _as_sx(nav_tree)
|
||||
parts.append(
|
||||
f'(div :class "flex flex-col gap-2 p-3 text-sm" {nav_tree_sx})'
|
||||
)
|
||||
if auth_menu:
|
||||
auth_sx = _as_sx(auth_menu)
|
||||
parts.append(
|
||||
f'(div :class "p-3 border-t border-stone-200" {auth_sx})'
|
||||
)
|
||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||
|
||||
|
||||
def search_mobile_sx(ctx: dict) -> str:
|
||||
"""Build mobile search input as sx call string."""
|
||||
return sx_call("search-mobile",
|
||||
@@ -279,6 +299,9 @@ def full_page_sx(ctx: dict, *, header_rows: str,
|
||||
meta_html: raw HTML injected into the <head> shell (legacy).
|
||||
meta: sx source for meta tags — auto-hoisted to <head> by sx.js.
|
||||
"""
|
||||
# Auto-generate mobile nav from context when no menu provided
|
||||
if not menu:
|
||||
menu = mobile_nav_sx(ctx)
|
||||
body_sx = sx_call("app-body",
|
||||
header_rows=SxExpr(f"(<> {header_rows})") if header_rows else None,
|
||||
filter=SxExpr(filter) if filter else None,
|
||||
|
||||
@@ -107,6 +107,12 @@ async def execute_io(
|
||||
# Individual handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _clean_kwargs(kwargs: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Strip None and NIL values from kwargs for Python interop."""
|
||||
from .types import NIL
|
||||
return {k: v for k, v in kwargs.items() if v is not None and v is not NIL}
|
||||
|
||||
|
||||
async def _io_frag(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> str:
|
||||
@@ -115,7 +121,7 @@ async def _io_frag(
|
||||
raise ValueError("frag requires service and fragment type")
|
||||
service = str(args[0])
|
||||
frag_type = str(args[1])
|
||||
params = {k: v for k, v in kwargs.items() if v is not None}
|
||||
params = _clean_kwargs(kwargs)
|
||||
|
||||
from shared.infrastructure.fragments import fetch_fragment
|
||||
return await fetch_fragment(service, frag_type, params=params or None)
|
||||
@@ -129,7 +135,7 @@ async def _io_query(
|
||||
raise ValueError("query requires service and query name")
|
||||
service = str(args[0])
|
||||
query_name = str(args[1])
|
||||
params = {k: v for k, v in kwargs.items() if v is not None}
|
||||
params = _clean_kwargs(kwargs)
|
||||
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
return await fetch_data(service, query_name, params=params or None)
|
||||
@@ -143,7 +149,7 @@ async def _io_action(
|
||||
raise ValueError("action requires service and action name")
|
||||
service = str(args[0])
|
||||
action_name = str(args[1])
|
||||
payload = {k: v for k, v in kwargs.items() if v is not None}
|
||||
payload = _clean_kwargs(kwargs)
|
||||
|
||||
from shared.infrastructure.actions import call_action
|
||||
return await call_action(service, action_name, payload=payload or None)
|
||||
@@ -195,8 +201,12 @@ async def _io_service(
|
||||
if method is None:
|
||||
raise RuntimeError(f"Service has no method: {method_name}")
|
||||
|
||||
# Convert kwarg keys from kebab-case to snake_case
|
||||
clean_kwargs = {k.replace("-", "_"): v for k, v in kwargs.items()}
|
||||
# Convert kwarg keys from kebab-case to snake_case, NIL → None
|
||||
from .types import NIL
|
||||
clean_kwargs = {
|
||||
k.replace("-", "_"): (None if v is NIL else v)
|
||||
for k, v in kwargs.items()
|
||||
}
|
||||
from quart import g
|
||||
result = await method(g.s, **clean_kwargs)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user