feat: add HTMX trigger to filter inputs for dynamic checkbox selection
When users type a filter, HTMX now fetches the matching animals and displays a checkbox list for subset selection. Changes: - Add hx_get="/api/selection-preview" to filter inputs in all forms - Wrap selection component in #selection-container for HTMX targeting - Add subset_mode hidden field to checkbox list component - Handle single-animal case with simple count display (no checkboxes) Forms updated: outcome, tag-add, tag-end, attrs, move 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,11 +74,26 @@ def selection_preview(request: Request):
|
||||
animal_repo = AnimalRepository(db)
|
||||
animals = animal_repo.get_by_ids(resolution.animal_ids)
|
||||
|
||||
from fasthtml.common import Div, P, Span, to_xml
|
||||
|
||||
# For single animal, show simple count display (no checkboxes needed)
|
||||
if len(animals) == 1:
|
||||
return HTMLResponse(
|
||||
content=to_xml(
|
||||
Div(
|
||||
P(
|
||||
Span("1", cls="font-bold text-lg"),
|
||||
" animal selected",
|
||||
cls="text-sm",
|
||||
),
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# If no pre-selection, default to all selected
|
||||
if selected_ids is None:
|
||||
selected_ids = [a.animal_id for a in animals]
|
||||
|
||||
# Render checkbox list
|
||||
from fasthtml.common import to_xml
|
||||
|
||||
# Render checkbox list for multiple animals
|
||||
return HTMLResponse(content=to_xml(animal_checkbox_list(animals, selected_ids)))
|
||||
|
||||
@@ -566,29 +566,36 @@ def tag_add_form(
|
||||
error_component = Alert(error, cls=AlertT.warning)
|
||||
|
||||
# Selection component - show checkboxes if animals provided and > 1
|
||||
selection_component = None
|
||||
# Wrapped in a container with ID for HTMX targeting
|
||||
selection_content = None
|
||||
subset_mode = False
|
||||
if animals and len(animals) > 1:
|
||||
# Show checkbox list for subset selection
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("Select animals for this action:", cls="text-sm text-stone-400 mb-2"),
|
||||
animal_checkbox_list(animals, resolved_ids),
|
||||
cls="mb-4",
|
||||
)
|
||||
subset_mode = True
|
||||
elif resolved_count > 0:
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P(
|
||||
Span(f"{resolved_count}", cls="font-bold text-lg"),
|
||||
" animals selected",
|
||||
cls="text-sm",
|
||||
),
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md mb-4",
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md",
|
||||
)
|
||||
elif filter_str:
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("No animals match this filter", cls="text-sm text-amber-600"),
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md mb-4",
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md",
|
||||
)
|
||||
|
||||
# Container for HTMX updates - always present so it can be targeted
|
||||
selection_container = Div(
|
||||
selection_content,
|
||||
id="selection-container",
|
||||
cls="mb-4" if selection_content else "",
|
||||
)
|
||||
|
||||
# Hidden fields for resolved_ids (as multiple values)
|
||||
@@ -600,16 +607,20 @@ def tag_add_form(
|
||||
H2("Add Tag", cls="text-xl font-bold mb-4"),
|
||||
# Error message if present
|
||||
error_component,
|
||||
# Filter input
|
||||
# Filter input with HTMX to fetch selection preview
|
||||
LabelInput(
|
||||
"Filter",
|
||||
id="filter",
|
||||
name="filter",
|
||||
value=filter_str,
|
||||
placeholder='e.g., location:"Strip 1" species:duck',
|
||||
hx_get="/api/selection-preview",
|
||||
hx_trigger="change, keyup delay:500ms changed",
|
||||
hx_target="#selection-container",
|
||||
hx_swap="innerHTML",
|
||||
),
|
||||
# Selection component (checkboxes or simple count)
|
||||
selection_component,
|
||||
# Selection container - updated via HTMX when filter changes
|
||||
selection_container,
|
||||
# Tag input
|
||||
LabelInput(
|
||||
"Tag",
|
||||
@@ -768,29 +779,36 @@ def tag_end_form(
|
||||
error_component = Alert(error, cls=AlertT.warning)
|
||||
|
||||
# Selection component - show checkboxes if animals provided and > 1
|
||||
selection_component = None
|
||||
# Wrapped in a container with ID for HTMX targeting
|
||||
selection_content = None
|
||||
subset_mode = False
|
||||
if animals and len(animals) > 1:
|
||||
# Show checkbox list for subset selection
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("Select animals for this action:", cls="text-sm text-stone-400 mb-2"),
|
||||
animal_checkbox_list(animals, resolved_ids),
|
||||
cls="mb-4",
|
||||
)
|
||||
subset_mode = True
|
||||
elif resolved_count > 0:
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P(
|
||||
Span(f"{resolved_count}", cls="font-bold text-lg"),
|
||||
" animals selected",
|
||||
cls="text-sm",
|
||||
),
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md mb-4",
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md",
|
||||
)
|
||||
elif filter_str:
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("No animals match this filter", cls="text-sm text-amber-600"),
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md mb-4",
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md",
|
||||
)
|
||||
|
||||
# Container for HTMX updates - always present so it can be targeted
|
||||
selection_container = Div(
|
||||
selection_content,
|
||||
id="selection-container",
|
||||
cls="mb-4" if selection_content else "",
|
||||
)
|
||||
|
||||
# Build tag options from active_tags
|
||||
@@ -807,16 +825,20 @@ def tag_end_form(
|
||||
H2("End Tag", cls="text-xl font-bold mb-4"),
|
||||
# Error message if present
|
||||
error_component,
|
||||
# Filter input
|
||||
# Filter input with HTMX to fetch selection preview
|
||||
LabelInput(
|
||||
"Filter",
|
||||
id="filter",
|
||||
name="filter",
|
||||
value=filter_str,
|
||||
placeholder="e.g., tag:layer-birds species:duck",
|
||||
hx_get="/api/selection-preview",
|
||||
hx_trigger="change, keyup delay:500ms changed",
|
||||
hx_target="#selection-container",
|
||||
hx_swap="innerHTML",
|
||||
),
|
||||
# Selection component (checkboxes or simple count)
|
||||
selection_component,
|
||||
# Selection container - updated via HTMX when filter changes
|
||||
selection_container,
|
||||
# Tag dropdown
|
||||
LabelSelect(
|
||||
*tag_options,
|
||||
@@ -976,29 +998,36 @@ def attrs_form(
|
||||
error_component = Alert(error, cls=AlertT.warning)
|
||||
|
||||
# Selection component - show checkboxes if animals provided and > 1
|
||||
selection_component = None
|
||||
# Wrapped in a container with ID for HTMX targeting
|
||||
selection_content = None
|
||||
subset_mode = False
|
||||
if animals and len(animals) > 1:
|
||||
# Show checkbox list for subset selection
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("Select animals for this action:", cls="text-sm text-stone-400 mb-2"),
|
||||
animal_checkbox_list(animals, resolved_ids),
|
||||
cls="mb-4",
|
||||
)
|
||||
subset_mode = True
|
||||
elif resolved_count > 0:
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P(
|
||||
Span(f"{resolved_count}", cls="font-bold text-lg"),
|
||||
" animals selected",
|
||||
cls="text-sm",
|
||||
),
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md mb-4",
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md",
|
||||
)
|
||||
elif filter_str:
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("No animals match this filter", cls="text-sm text-amber-600"),
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md mb-4",
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md",
|
||||
)
|
||||
|
||||
# Container for HTMX updates - always present so it can be targeted
|
||||
selection_container = Div(
|
||||
selection_content,
|
||||
id="selection-container",
|
||||
cls="mb-4" if selection_content else "",
|
||||
)
|
||||
|
||||
# Build sex options
|
||||
@@ -1036,16 +1065,20 @@ def attrs_form(
|
||||
H2("Update Attributes", cls="text-xl font-bold mb-4"),
|
||||
# Error message if present
|
||||
error_component,
|
||||
# Filter input
|
||||
# Filter input with HTMX to fetch selection preview
|
||||
LabelInput(
|
||||
"Filter",
|
||||
id="filter",
|
||||
name="filter",
|
||||
value=filter_str,
|
||||
placeholder="e.g., species:duck life_stage:juvenile",
|
||||
hx_get="/api/selection-preview",
|
||||
hx_trigger="change, keyup delay:500ms changed",
|
||||
hx_target="#selection-container",
|
||||
hx_swap="innerHTML",
|
||||
),
|
||||
# Selection component (checkboxes or simple count)
|
||||
selection_component,
|
||||
# Selection container - updated via HTMX when filter changes
|
||||
selection_container,
|
||||
# Attribute dropdowns
|
||||
LabelSelect(
|
||||
*sex_options,
|
||||
@@ -1222,30 +1255,37 @@ def outcome_form(
|
||||
error_component = Alert(error, cls=AlertT.warning)
|
||||
|
||||
# Selection component - show checkboxes if animals provided and > 1
|
||||
selection_component = None
|
||||
# Wrapped in a container with ID for HTMX targeting
|
||||
selection_content = None
|
||||
subset_mode = False
|
||||
if animals and len(animals) > 1:
|
||||
# Show checkbox list for subset selection
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("Select animals for this action:", cls="text-sm text-stone-400 mb-2"),
|
||||
animal_checkbox_list(animals, resolved_ids),
|
||||
cls="mb-4",
|
||||
)
|
||||
subset_mode = True
|
||||
elif resolved_count > 0:
|
||||
# Fallback to simple count display
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P(
|
||||
Span(f"{resolved_count}", cls="font-bold text-lg"),
|
||||
" animals selected",
|
||||
cls="text-sm",
|
||||
),
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md mb-4",
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md",
|
||||
)
|
||||
elif filter_str:
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("No animals match this filter", cls="text-sm text-amber-600"),
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md mb-4",
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md",
|
||||
)
|
||||
|
||||
# Container for HTMX updates - always present so it can be targeted
|
||||
selection_container = Div(
|
||||
selection_content,
|
||||
id="selection-container",
|
||||
cls="mb-4" if selection_content else "",
|
||||
)
|
||||
|
||||
# Build outcome options
|
||||
@@ -1323,15 +1363,20 @@ def outcome_form(
|
||||
return Form(
|
||||
H2("Record Outcome", cls="text-xl font-bold mb-4"),
|
||||
error_component,
|
||||
selection_component,
|
||||
# Filter field
|
||||
# Filter field with HTMX to fetch selection preview
|
||||
LabelInput(
|
||||
label="Filter (DSL)",
|
||||
id="filter",
|
||||
name="filter",
|
||||
value=filter_str,
|
||||
placeholder="e.g., species:duck location:Coop1",
|
||||
hx_get="/api/selection-preview",
|
||||
hx_trigger="change, keyup delay:500ms changed",
|
||||
hx_target="#selection-container",
|
||||
hx_swap="innerHTML",
|
||||
),
|
||||
# Selection container - updated via HTMX when filter changes
|
||||
selection_container,
|
||||
# Outcome selection
|
||||
LabelSelect(
|
||||
*outcome_options,
|
||||
|
||||
@@ -89,6 +89,8 @@ def animal_checkbox_list(
|
||||
*items,
|
||||
cls="max-h-64 overflow-y-auto",
|
||||
),
|
||||
# Hidden field to indicate subset selection mode
|
||||
Input(type="hidden", name="subset_mode", value="true"),
|
||||
# Hidden field for roster_hash - will be updated via JS
|
||||
Input(type="hidden", name="roster_hash", id="roster-hash-field"),
|
||||
# Script for selection management
|
||||
|
||||
@@ -66,31 +66,38 @@ def move_form(
|
||||
)
|
||||
|
||||
# Selection component - show checkboxes if animals provided and > 1
|
||||
selection_component = None
|
||||
# Wrapped in a container with ID for HTMX targeting
|
||||
selection_content = None
|
||||
subset_mode = False
|
||||
if animals and len(animals) > 1:
|
||||
# Show checkbox list for subset selection
|
||||
location_info = f" from {from_location_name}" if from_location_name else ""
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P(f"Select animals to move{location_info}:", cls="text-sm text-stone-400 mb-2"),
|
||||
animal_checkbox_list(animals, resolved_ids),
|
||||
cls="mb-4",
|
||||
)
|
||||
subset_mode = True
|
||||
elif resolved_count > 0:
|
||||
location_info = f" from {from_location_name}" if from_location_name else ""
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P(
|
||||
Span(f"{resolved_count}", cls="font-bold text-lg"),
|
||||
f" animals selected{location_info}",
|
||||
cls="text-sm",
|
||||
),
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md mb-4",
|
||||
cls="p-3 bg-slate-100 dark:bg-slate-800 rounded-md",
|
||||
)
|
||||
elif filter_str:
|
||||
selection_component = Div(
|
||||
selection_content = Div(
|
||||
P("No animals match this filter", cls="text-sm text-amber-600"),
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md mb-4",
|
||||
cls="p-3 bg-amber-50 dark:bg-amber-900/20 rounded-md",
|
||||
)
|
||||
|
||||
# Container for HTMX updates - always present so it can be targeted
|
||||
selection_container = Div(
|
||||
selection_content,
|
||||
id="selection-container",
|
||||
cls="mb-4" if selection_content else "",
|
||||
)
|
||||
|
||||
# Hidden fields for resolved_ids (as multiple values)
|
||||
@@ -102,16 +109,20 @@ def move_form(
|
||||
H2("Move Animals", cls="text-xl font-bold mb-4"),
|
||||
# Error message if present
|
||||
error_component,
|
||||
# Filter input
|
||||
# Filter input with HTMX to fetch selection preview
|
||||
LabelInput(
|
||||
"Filter",
|
||||
id="filter",
|
||||
name="filter",
|
||||
value=filter_str,
|
||||
placeholder='e.g., location:"Strip 1" species:duck',
|
||||
hx_get="/api/selection-preview",
|
||||
hx_trigger="change, keyup delay:500ms changed",
|
||||
hx_target="#selection-container",
|
||||
hx_swap="innerHTML",
|
||||
),
|
||||
# Selection component (checkboxes or simple count)
|
||||
selection_component,
|
||||
# Selection container - updated via HTMX when filter changes
|
||||
selection_container,
|
||||
# Destination dropdown
|
||||
LabelSelect(
|
||||
*location_options,
|
||||
|
||||
Reference in New Issue
Block a user