big rework
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "nicegui==3.16.0",
|
||||
# "pandas",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pandas as pd
|
||||
from nicegui import binding
|
||||
from nicegui import events
|
||||
from nicegui import ui
|
||||
|
||||
STATUS_OPTIONS = ["draft", "active", "archived"]
|
||||
EDITABLE_FIELDS = ("name", "quantity", "status")
|
||||
TableValue = str | int
|
||||
TableRow = dict[str, TableValue]
|
||||
|
||||
|
||||
@binding.bindable_dataclass(bindable_fields=EDITABLE_FIELDS)
|
||||
class EditableRow:
|
||||
id: int
|
||||
name: str
|
||||
quantity: int
|
||||
status: str
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class EditableTableState:
|
||||
rows_by_id: dict[int, EditableRow]
|
||||
table_rows_by_id: dict[int, TableRow]
|
||||
|
||||
def table_rows(self) -> list[TableRow]:
|
||||
return list(self.table_rows_by_id.values())
|
||||
|
||||
|
||||
def dataframe_to_state(dataframe: pd.DataFrame) -> EditableTableState:
|
||||
required_columns = {"id", *EDITABLE_FIELDS}
|
||||
missing_columns = required_columns.difference(dataframe.columns)
|
||||
if missing_columns:
|
||||
raise ValueError(f"Missing columns: {sorted(missing_columns)}")
|
||||
if not dataframe["id"].is_unique:
|
||||
raise ValueError("The id column must contain unique row keys")
|
||||
|
||||
rows_by_id: dict[int, EditableRow] = {}
|
||||
table_rows_by_id: dict[int, TableRow] = {}
|
||||
for record in dataframe.to_dict(orient="records"):
|
||||
row_state = EditableRow(
|
||||
id=int(record["id"]),
|
||||
name=str(record["name"]),
|
||||
quantity=int(record["quantity"]),
|
||||
status=str(record["status"]),
|
||||
)
|
||||
if row_state.status not in STATUS_OPTIONS:
|
||||
raise ValueError(f"Unknown status {row_state.status!r}")
|
||||
if row_state.id in rows_by_id:
|
||||
raise ValueError("Row keys must remain unique after normalization")
|
||||
|
||||
table_row: TableRow = {
|
||||
"id": row_state.id,
|
||||
"name": row_state.name,
|
||||
"quantity": row_state.quantity,
|
||||
"status": row_state.status,
|
||||
}
|
||||
for field_name in EDITABLE_FIELDS:
|
||||
binding.bind_to(
|
||||
row_state,
|
||||
field_name,
|
||||
table_row,
|
||||
field_name,
|
||||
other_strict=True,
|
||||
)
|
||||
rows_by_id[row_state.id] = row_state
|
||||
table_rows_by_id[row_state.id] = table_row
|
||||
|
||||
return EditableTableState(rows_by_id, table_rows_by_id)
|
||||
|
||||
|
||||
def normalize_edit(field: str, raw_value: object) -> TableValue:
|
||||
match field:
|
||||
case "name":
|
||||
if not isinstance(raw_value, str) or not (name := raw_value.strip()):
|
||||
raise ValueError("Name is required")
|
||||
return name
|
||||
case "quantity":
|
||||
if isinstance(raw_value, bool) or not isinstance(raw_value, (int, float, str)):
|
||||
raise TypeError("Quantity must be an integer")
|
||||
if isinstance(raw_value, float) and not raw_value.is_integer():
|
||||
raise ValueError("Quantity must be an integer")
|
||||
try:
|
||||
quantity = int(raw_value)
|
||||
except (ValueError, OverflowError) as error:
|
||||
raise ValueError("Quantity must be an integer") from error
|
||||
if not 0 <= quantity <= 1_000:
|
||||
raise ValueError("Quantity must be between 0 and 1000")
|
||||
return quantity
|
||||
case "status":
|
||||
if not isinstance(raw_value, str) or raw_value not in STATUS_OPTIONS:
|
||||
raise ValueError("Unknown status")
|
||||
return raw_value
|
||||
case _:
|
||||
raise ValueError(f"Field {field!r} is not editable")
|
||||
|
||||
|
||||
def save_row(dataframe: pd.DataFrame, row_state: EditableRow) -> None:
|
||||
matching_rows = dataframe["id"].eq(row_state.id)
|
||||
if int(matching_rows.sum()) != 1:
|
||||
raise ValueError("This row no longer exists")
|
||||
dataframe.loc[matching_rows, "name"] = row_state.name
|
||||
dataframe.loc[matching_rows, "quantity"] = row_state.quantity
|
||||
dataframe.loc[matching_rows, "status"] = row_state.status
|
||||
|
||||
|
||||
def render_table(dataframe: pd.DataFrame) -> EditableTableState:
|
||||
state = dataframe_to_state(dataframe)
|
||||
columns = [
|
||||
{"name": "name", "label": "Name", "field": "name", "align": "left"},
|
||||
{"name": "quantity", "label": "Quantity", "field": "quantity", "align": "right"},
|
||||
{"name": "status", "label": "Status", "field": "status", "align": "left"},
|
||||
]
|
||||
table = ui.table(
|
||||
columns=columns,
|
||||
rows=state.table_rows(),
|
||||
row_key="id",
|
||||
selection="multiple",
|
||||
).classes("w-full")
|
||||
|
||||
def apply_edit(event: events.GenericEventArguments) -> None:
|
||||
try:
|
||||
raw_row_id, raw_field, raw_value = event.args
|
||||
row_id = int(raw_row_id)
|
||||
field_name = str(raw_field)
|
||||
row_state = state.rows_by_id.get(row_id)
|
||||
if row_state is None:
|
||||
raise ValueError("This row no longer exists")
|
||||
|
||||
normalized_value = normalize_edit(field_name, raw_value)
|
||||
previous_value = getattr(row_state, field_name)
|
||||
setattr(row_state, field_name, normalized_value)
|
||||
try:
|
||||
save_row(dataframe, row_state)
|
||||
except Exception:
|
||||
setattr(row_state, field_name, previous_value)
|
||||
raise
|
||||
except (TypeError, ValueError) as error:
|
||||
ui.notify(str(error), type="negative")
|
||||
finally:
|
||||
table.update_rows(state.table_rows(), clear_selection=False)
|
||||
|
||||
with table.add_slot("body-cell-name"), table.cell("name"):
|
||||
ui.input().props(':model-value="props.value" dense borderless debounce=400').on(
|
||||
"update:model-value",
|
||||
handler=apply_edit,
|
||||
js_handler="(value) => emit(props.row.id, props.col.name, value)",
|
||||
)
|
||||
|
||||
with table.add_slot("body-cell-quantity"), table.cell("quantity"):
|
||||
ui.number(min=0, max=1_000).props(':model-value="props.value" dense borderless debounce=400').on(
|
||||
"update:model-value",
|
||||
handler=apply_edit,
|
||||
js_handler="(value) => emit(props.row.id, props.col.name, value)",
|
||||
)
|
||||
|
||||
with table.add_slot("body-cell-status"), table.cell("status"):
|
||||
ui.select(STATUS_OPTIONS).props(':model-value="props.value" dense borderless options-dense').on(
|
||||
"update:model-value",
|
||||
handler=apply_edit,
|
||||
js_handler="(value) => emit(props.row.id, props.col.name, value)",
|
||||
)
|
||||
|
||||
return state
|
||||
|
||||
|
||||
if __name__ in {"__main__", "__mp_main__"}:
|
||||
items = pd.DataFrame(
|
||||
[
|
||||
{"id": 101, "name": "Desk", "quantity": 4, "status": "active"},
|
||||
{"id": 102, "name": "Lamp", "quantity": 12, "status": "draft"},
|
||||
]
|
||||
)
|
||||
table_state = render_table(items)
|
||||
|
||||
ui.run(port=8888, reload=True)
|
||||
Reference in New Issue
Block a user