Fix MonsterUI Select bug, UI improvements, enable animal rename
- Replace broken MonsterUI LabelSelect with raw HTML Select elements (was sending label text instead of value attribute) - Fix wrong ReproStatus options in promote form (use enum values) - Add spacing between name and details in animal selection list - Fix registry filter layout, add Clear button - Use phonetic ID in animal details panel title - Change feed price input from cents to euros - Allow renaming already-identified animals (remove identified check) - Fix FeedGiven 422 error (same MonsterUI Select bug) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -408,13 +408,13 @@ def promote_index(request: Request, animal_id: str):
|
|||||||
if animal.status != "alive":
|
if animal.status != "alive":
|
||||||
return HTMLResponse(content="Only alive animals can be promoted", status_code=400)
|
return HTMLResponse(content="Only alive animals can be promoted", status_code=400)
|
||||||
|
|
||||||
if animal.identified:
|
# Title depends on whether animal is already identified (rename vs promote)
|
||||||
return HTMLResponse(content="Animal is already identified", status_code=400)
|
title = "Rename Animal - AnimalTrack" if animal.identified else "Promote Animal - AnimalTrack"
|
||||||
|
|
||||||
return render_page(
|
return render_page(
|
||||||
request,
|
request,
|
||||||
promote_form(animal),
|
promote_form(animal),
|
||||||
title="Promote Animal - AnimalTrack",
|
title=title,
|
||||||
active_nav=None,
|
active_nav=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -448,9 +448,6 @@ async def animal_promote(request: Request):
|
|||||||
if animal.status != "alive":
|
if animal.status != "alive":
|
||||||
return _render_promote_error(request, animal, "Only alive animals can be promoted", form)
|
return _render_promote_error(request, animal, "Only alive animals can be promoted", form)
|
||||||
|
|
||||||
if animal.identified:
|
|
||||||
return _render_promote_error(request, animal, "Animal is already identified", form)
|
|
||||||
|
|
||||||
# Create payload
|
# Create payload
|
||||||
try:
|
try:
|
||||||
payload = AnimalPromotedPayload(
|
payload = AnimalPromotedPayload(
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ async def feed_purchased(request: Request, session):
|
|||||||
feed_type_code = form.get("feed_type_code", "")
|
feed_type_code = form.get("feed_type_code", "")
|
||||||
bag_size_kg_str = form.get("bag_size_kg", "0")
|
bag_size_kg_str = form.get("bag_size_kg", "0")
|
||||||
bags_count_str = form.get("bags_count", "0")
|
bags_count_str = form.get("bags_count", "0")
|
||||||
bag_price_cents_str = form.get("bag_price_cents", "0")
|
bag_price_euros_str = form.get("bag_price_euros", "0")
|
||||||
vendor = form.get("vendor") or None
|
vendor = form.get("vendor") or None
|
||||||
notes = form.get("notes") or None
|
notes = form.get("notes") or None
|
||||||
nonce = form.get("nonce")
|
nonce = form.get("nonce")
|
||||||
@@ -329,9 +329,10 @@ async def feed_purchased(request: Request, session):
|
|||||||
"Bags count must be at least 1",
|
"Bags count must be at least 1",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate bag_price_cents
|
# Validate bag_price_euros and convert to cents
|
||||||
try:
|
try:
|
||||||
bag_price_cents = int(bag_price_cents_str)
|
bag_price_euros = float(bag_price_euros_str)
|
||||||
|
bag_price_cents = int(round(bag_price_euros * 100))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return _render_purchase_error(
|
return _render_purchase_error(
|
||||||
request,
|
request,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fasthtml.common import H2, H3, Div, Form, Hidden, Input, Option, P, Span
|
from fasthtml.common import H2, H3, Div, Form, Hidden, Input, Option, P, Select, Span
|
||||||
from monsterui.all import (
|
from monsterui.all import (
|
||||||
Alert,
|
Alert,
|
||||||
AlertT,
|
AlertT,
|
||||||
@@ -436,6 +436,11 @@ def promote_form(
|
|||||||
"""
|
"""
|
||||||
display_id = f"{animal.animal_id[:8]}..."
|
display_id = f"{animal.animal_id[:8]}..."
|
||||||
|
|
||||||
|
# Title and action text depend on whether animal is already identified
|
||||||
|
is_rename = animal.identified
|
||||||
|
form_title = "Rename Animal" if is_rename else "Promote Animal"
|
||||||
|
action_text = "Renaming" if is_rename else "Promoting"
|
||||||
|
|
||||||
# Build sex options (optional - can refine current value)
|
# Build sex options (optional - can refine current value)
|
||||||
sexes = [
|
sexes = [
|
||||||
("", "Keep current"),
|
("", "Keep current"),
|
||||||
@@ -446,13 +451,13 @@ def promote_form(
|
|||||||
for code, label in sexes:
|
for code, label in sexes:
|
||||||
sex_options.append(Option(label, value=code, selected=code == selected_sex))
|
sex_options.append(Option(label, value=code, selected=code == selected_sex))
|
||||||
|
|
||||||
# Build repro status options (optional)
|
# Build repro status options (optional) - must match ReproStatus enum
|
||||||
repro_statuses = [
|
repro_statuses = [
|
||||||
("", "Unknown"),
|
("", "Keep current"),
|
||||||
("breeding", "Breeding"),
|
("intact", "Intact"),
|
||||||
("non_breeding", "Non-Breeding"),
|
("wether", "Wether (castrated male)"),
|
||||||
("broody", "Broody"),
|
("spayed", "Spayed (female)"),
|
||||||
("molting", "Molting"),
|
("unknown", "Unknown"),
|
||||||
]
|
]
|
||||||
repro_status_options = []
|
repro_status_options = []
|
||||||
for code, label in repro_statuses:
|
for code, label in repro_statuses:
|
||||||
@@ -466,36 +471,34 @@ def promote_form(
|
|||||||
error_component = Alert(error, cls=AlertT.warning)
|
error_component = Alert(error, cls=AlertT.warning)
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
H2("Promote Animal", cls="text-xl font-bold mb-4"),
|
H2(form_title, cls="text-xl font-bold mb-4"),
|
||||||
# Animal info
|
# Animal info
|
||||||
Div(
|
Div(
|
||||||
P(f"Promoting: {display_id}", cls="text-sm text-stone-400"),
|
P(f"{action_text}: {display_id}", cls="text-sm text-stone-400"),
|
||||||
P(f"Species: {animal.species_code}, Sex: {animal.sex}", cls="text-sm text-stone-400"),
|
P(f"Species: {animal.species_code}, Sex: {animal.sex}", cls="text-sm text-stone-400"),
|
||||||
cls="p-3 bg-slate-800 rounded-md mb-4",
|
cls="p-3 bg-slate-800 rounded-md mb-4",
|
||||||
),
|
),
|
||||||
# Error message if present
|
# Error message if present
|
||||||
error_component,
|
error_component,
|
||||||
# Nickname input (optional)
|
# Nickname input - label changes for rename vs promote
|
||||||
LabelInput(
|
LabelInput(
|
||||||
"Nickname (optional)",
|
"New Name" if is_rename else "Nickname (optional)",
|
||||||
id="nickname",
|
id="nickname",
|
||||||
name="nickname",
|
name="nickname",
|
||||||
value=nickname_value,
|
value=nickname_value or (animal.nickname or ""),
|
||||||
placeholder="Give this animal a name",
|
placeholder="Enter a name for this animal",
|
||||||
),
|
),
|
||||||
# Sex refinement dropdown (optional)
|
# Sex refinement dropdown (optional) - using raw Select to fix value handling
|
||||||
LabelSelect(
|
Div(
|
||||||
*sex_options,
|
FormLabel("Refine Sex (optional)", _for="sex"),
|
||||||
label="Refine Sex (optional)",
|
Select(*sex_options, name="sex", id="sex", cls="uk-select"),
|
||||||
id="sex",
|
cls="space-y-2",
|
||||||
name="sex",
|
|
||||||
),
|
),
|
||||||
# Repro status dropdown (optional)
|
# Repro status dropdown (optional) - using raw Select to fix value handling
|
||||||
LabelSelect(
|
Div(
|
||||||
*repro_status_options,
|
FormLabel("Reproductive Status", _for="repro_status"),
|
||||||
label="Reproductive Status",
|
Select(*repro_status_options, name="repro_status", id="repro_status", cls="uk-select"),
|
||||||
id="repro_status",
|
cls="space-y-2",
|
||||||
name="repro_status",
|
|
||||||
),
|
),
|
||||||
# Distinguishing traits (optional)
|
# Distinguishing traits (optional)
|
||||||
LabelTextArea(
|
LabelTextArea(
|
||||||
@@ -515,8 +518,12 @@ def promote_form(
|
|||||||
# Hidden fields
|
# Hidden fields
|
||||||
Hidden(name="animal_id", value=animal.animal_id),
|
Hidden(name="animal_id", value=animal.animal_id),
|
||||||
Hidden(name="nonce", value=str(ULID())),
|
Hidden(name="nonce", value=str(ULID())),
|
||||||
# Submit button
|
# Submit button - text changes for rename vs promote
|
||||||
Button("Promote to Identified", type="submit", cls=ButtonT.primary),
|
Button(
|
||||||
|
"Save Changes" if is_rename else "Promote to Identified",
|
||||||
|
type="submit",
|
||||||
|
cls=ButtonT.primary,
|
||||||
|
),
|
||||||
# Form submission via standard action/method (hx-boost handles AJAX)
|
# Form submission via standard action/method (hx-boost handles AJAX)
|
||||||
action=action,
|
action=action,
|
||||||
method="post",
|
method="post",
|
||||||
@@ -1071,24 +1078,21 @@ def attrs_form(
|
|||||||
),
|
),
|
||||||
# Selection container - updated via HTMX when filter changes
|
# Selection container - updated via HTMX when filter changes
|
||||||
selection_container,
|
selection_container,
|
||||||
# Attribute dropdowns
|
# Attribute dropdowns - using raw Select to fix value handling
|
||||||
LabelSelect(
|
Div(
|
||||||
*sex_options,
|
FormLabel("Sex", _for="sex"),
|
||||||
label="Sex",
|
Select(*sex_options, name="sex", id="sex", cls="uk-select"),
|
||||||
id="sex",
|
cls="space-y-2",
|
||||||
name="sex",
|
|
||||||
),
|
),
|
||||||
LabelSelect(
|
Div(
|
||||||
*life_stage_options,
|
FormLabel("Life Stage", _for="life_stage"),
|
||||||
label="Life Stage",
|
Select(*life_stage_options, name="life_stage", id="life_stage", cls="uk-select"),
|
||||||
id="life_stage",
|
cls="space-y-2",
|
||||||
name="life_stage",
|
|
||||||
),
|
),
|
||||||
LabelSelect(
|
Div(
|
||||||
*repro_status_options,
|
FormLabel("Reproductive Status", _for="repro_status"),
|
||||||
label="Reproductive Status",
|
Select(*repro_status_options, name="repro_status", id="repro_status", cls="uk-select"),
|
||||||
id="repro_status",
|
cls="space-y-2",
|
||||||
name="repro_status",
|
|
||||||
),
|
),
|
||||||
# Optional notes
|
# Optional notes
|
||||||
LabelTextArea(
|
LabelTextArea(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Any
|
|||||||
from fasthtml.common import H2, H3, A, Div, Li, P, Span, Ul
|
from fasthtml.common import H2, H3, A, Div, Li, P, Span, Ul
|
||||||
from monsterui.all import Button, ButtonT, Card, Grid
|
from monsterui.all import Button, ButtonT, Card, Grid
|
||||||
|
|
||||||
|
from animaltrack.id_gen import format_animal_id
|
||||||
from animaltrack.repositories.animal_timeline import (
|
from animaltrack.repositories.animal_timeline import (
|
||||||
AnimalDetail,
|
AnimalDetail,
|
||||||
MergeInfo,
|
MergeInfo,
|
||||||
@@ -61,7 +62,7 @@ def back_to_registry_link() -> Div:
|
|||||||
|
|
||||||
def animal_header_card(animal: AnimalDetail, merge_info: MergeInfo | None) -> Card:
|
def animal_header_card(animal: AnimalDetail, merge_info: MergeInfo | None) -> Card:
|
||||||
"""Header card with animal summary."""
|
"""Header card with animal summary."""
|
||||||
display_name = animal.nickname or f"{animal.animal_id[:8]}..."
|
display_name = format_animal_id(animal.animal_id, animal.nickname)
|
||||||
status_badge = status_badge_component(animal.status)
|
status_badge = status_badge_component(animal.status)
|
||||||
|
|
||||||
tags_display = (
|
tags_display = (
|
||||||
@@ -160,10 +161,11 @@ def quick_actions_card(animal: AnimalDetail) -> Card:
|
|||||||
href=f"/actions/tag-add?filter=animal_id:{animal.animal_id}",
|
href=f"/actions/tag-add?filter=animal_id:{animal.animal_id}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not animal.identified:
|
# Show "Promote" for unidentified animals, "Rename" for identified ones
|
||||||
|
promote_label = "Rename" if animal.identified else "Promote"
|
||||||
actions.append(
|
actions.append(
|
||||||
A(
|
A(
|
||||||
Button("Promote", cls=ButtonT.default + " w-full"),
|
Button(promote_label, cls=ButtonT.default + " w-full"),
|
||||||
href=f"/actions/promote/{animal.animal_id}",
|
href=f"/actions/promote/{animal.animal_id}",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,9 +47,9 @@ def animal_checkbox_list(
|
|||||||
cls="uk-checkbox mr-2",
|
cls="uk-checkbox mr-2",
|
||||||
hx_on_change="updateSelectionCount()",
|
hx_on_change="updateSelectionCount()",
|
||||||
),
|
),
|
||||||
Span(display_name, cls="text-stone-200"),
|
Span(display_name, cls="text-stone-200 mr-1"),
|
||||||
Span(
|
Span(
|
||||||
f" ({animal.species_code}, {sex_code}, {stage_abbr}, {animal.location_name})",
|
f"({animal.species_code}, {sex_code}, {stage_abbr}, {animal.location_name})",
|
||||||
cls="text-stone-500 text-sm",
|
cls="text-stone-500 text-sm",
|
||||||
),
|
),
|
||||||
cls="flex items-center py-1 hover:bg-stone-800/30 px-2 rounded cursor-pointer",
|
cls="flex items-center py-1 hover:bg-stone-800/30 px-2 rounded cursor-pointer",
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Ul
|
from fasthtml.common import H1, H2, A, Div, Form, Hidden, Li, Option, P, Select, Ul
|
||||||
from monsterui.all import (
|
from monsterui.all import (
|
||||||
Button,
|
Button,
|
||||||
ButtonT,
|
ButtonT,
|
||||||
|
FormLabel,
|
||||||
LabelInput,
|
LabelInput,
|
||||||
LabelSelect,
|
|
||||||
LabelTextArea,
|
LabelTextArea,
|
||||||
TabContainer,
|
TabContainer,
|
||||||
)
|
)
|
||||||
@@ -170,19 +170,17 @@ def give_feed_form(
|
|||||||
H2("Give Feed", cls="text-xl font-bold mb-4"),
|
H2("Give Feed", cls="text-xl font-bold mb-4"),
|
||||||
error_component,
|
error_component,
|
||||||
warning_component,
|
warning_component,
|
||||||
# Location dropdown
|
# Location dropdown - using raw Select to fix value handling
|
||||||
LabelSelect(
|
Div(
|
||||||
*location_options,
|
FormLabel("Location", _for="location_id"),
|
||||||
label="Location",
|
Select(*location_options, name="location_id", id="location_id", cls="uk-select"),
|
||||||
id="location_id",
|
cls="space-y-2",
|
||||||
name="location_id",
|
|
||||||
),
|
),
|
||||||
# Feed type dropdown
|
# Feed type dropdown - using raw Select to fix value handling
|
||||||
LabelSelect(
|
Div(
|
||||||
*feed_type_options,
|
FormLabel("Feed Type", _for="feed_type_code"),
|
||||||
label="Feed Type",
|
Select(*feed_type_options, name="feed_type_code", id="feed_type_code", cls="uk-select"),
|
||||||
id="feed_type_code",
|
cls="space-y-2",
|
||||||
name="feed_type_code",
|
|
||||||
),
|
),
|
||||||
# Amount input
|
# Amount input
|
||||||
LabelInput(
|
LabelInput(
|
||||||
@@ -247,12 +245,16 @@ def purchase_feed_form(
|
|||||||
return Form(
|
return Form(
|
||||||
H2("Purchase Feed", cls="text-xl font-bold mb-4"),
|
H2("Purchase Feed", cls="text-xl font-bold mb-4"),
|
||||||
error_component,
|
error_component,
|
||||||
# Feed type dropdown
|
# Feed type dropdown - using raw Select to fix value handling
|
||||||
LabelSelect(
|
Div(
|
||||||
|
FormLabel("Feed Type", _for="purchase_feed_type_code"),
|
||||||
|
Select(
|
||||||
*feed_type_options,
|
*feed_type_options,
|
||||||
label="Feed Type",
|
|
||||||
id="purchase_feed_type_code",
|
|
||||||
name="feed_type_code",
|
name="feed_type_code",
|
||||||
|
id="purchase_feed_type_code",
|
||||||
|
cls="uk-select",
|
||||||
|
),
|
||||||
|
cls="space-y-2",
|
||||||
),
|
),
|
||||||
# Bag size
|
# Bag size
|
||||||
LabelInput(
|
LabelInput(
|
||||||
@@ -276,15 +278,15 @@ def purchase_feed_form(
|
|||||||
value="1",
|
value="1",
|
||||||
required=True,
|
required=True,
|
||||||
),
|
),
|
||||||
# Price per bag (cents)
|
# Price per bag (euros)
|
||||||
LabelInput(
|
LabelInput(
|
||||||
"Price per Bag (cents)",
|
"Price per Bag (€)",
|
||||||
id="bag_price_cents",
|
id="bag_price_euros",
|
||||||
name="bag_price_cents",
|
name="bag_price_euros",
|
||||||
type="number",
|
type="number",
|
||||||
min="0",
|
min="0",
|
||||||
step="1",
|
step="0.01",
|
||||||
placeholder="e.g., 2400 for 24.00",
|
placeholder="e.g., 24.00",
|
||||||
required=True,
|
required=True,
|
||||||
),
|
),
|
||||||
# Optional vendor
|
# Optional vendor
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ from datetime import UTC, datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from fasthtml.common import H2, A, Div, Form, P, Span, Table, Tbody, Td, Th, Thead, Tr
|
from fasthtml.common import H2, A, Div, Form, Input, P, Span, Table, Tbody, Td, Th, Thead, Tr
|
||||||
from monsterui.all import Button, ButtonT, Grid, LabelInput
|
from monsterui.all import Button, ButtonT, FormLabel, Grid
|
||||||
|
|
||||||
from animaltrack.id_gen import format_animal_id
|
from animaltrack.id_gen import format_animal_id
|
||||||
from animaltrack.models.reference import Location, Species
|
from animaltrack.models.reference import Location, Species
|
||||||
@@ -75,21 +75,28 @@ def registry_header(filter_str: str, total_count: int) -> Div:
|
|||||||
),
|
),
|
||||||
# Filter form - full width, prominent
|
# Filter form - full width, prominent
|
||||||
Form(
|
Form(
|
||||||
|
# Label above the input row
|
||||||
|
FormLabel("Filter", _for="filter", cls="mb-2 block"),
|
||||||
Div(
|
Div(
|
||||||
# Filter input - wider with flex-grow
|
# Filter input - takes most of the width
|
||||||
Div(
|
Input(
|
||||||
LabelInput(
|
|
||||||
"Filter",
|
|
||||||
id="filter",
|
id="filter",
|
||||||
name="filter",
|
name="filter",
|
||||||
value=filter_str,
|
value=filter_str,
|
||||||
placeholder='species:duck status:alive location:"Strip 1"',
|
placeholder='species:duck status:alive location:"Strip 1"',
|
||||||
|
cls="uk-input flex-1",
|
||||||
),
|
),
|
||||||
cls="flex-1 min-w-0", # min-w-0 prevents flex overflow
|
# Apply button
|
||||||
),
|
Button("Apply", type="submit", cls=f"{ButtonT.primary} px-4"),
|
||||||
# Apply button - fixed size
|
# Clear button (only shown if filter is active)
|
||||||
Button("Apply", type="submit", cls=f"{ButtonT.primary} shrink-0"),
|
A(
|
||||||
cls="flex gap-3 items-end",
|
"Clear",
|
||||||
|
href="/registry",
|
||||||
|
cls="px-3 py-2 text-stone-400 hover:text-stone-200",
|
||||||
|
)
|
||||||
|
if filter_str
|
||||||
|
else None,
|
||||||
|
cls="flex gap-2 items-center",
|
||||||
),
|
),
|
||||||
action="/registry",
|
action="/registry",
|
||||||
method="get",
|
method="get",
|
||||||
|
|||||||
Reference in New Issue
Block a user