action slot

This commit is contained in:
John Lancaster
2026-09-01 23:32:14 -05:00
parent bf11b7865d
commit b87b1df642
3 changed files with 261 additions and 38 deletions
@@ -5,6 +5,7 @@
# ]
# ///
from nicegui import events
from nicegui import ui
type TableValue = str | int | float
@@ -17,6 +18,12 @@ STATUS_COLORS = {
}
COLUMNS = [
{
"name": "actions",
"label": "Actions",
"required": True,
"align": "center",
},
{
"name": "name",
"label": "Product",
@@ -47,6 +54,7 @@ COLUMNS = [
"headerStyle": "width: 7rem",
"style": "width: 7rem",
":classes": "row => row.stock < 10 ? 'text-negative font-bold' : ''",
":format": "value => value == null ? '' : `${value} units`",
},
{
"name": "price",
@@ -56,7 +64,7 @@ COLUMNS = [
"align": "right",
"headerStyle": "width: 8rem",
"style": "width: 8rem",
":format": "value => `$${value.toFixed(2)}`",
":format": "value => value == null ? '' : `$${value.toFixed(2)}`",
},
{
"name": "status",
@@ -68,15 +76,85 @@ COLUMNS = [
"style": "width: 8rem",
"colorByValue": STATUS_COLORS,
},
{
"name": "updated_at",
"label": "Updated",
"field": "updated_at",
"sortable": True,
"align": "left",
"headerStyle": "width: 13rem",
"style": "width: 13rem",
":sort": "(left, right) => Date.parse(left) - Date.parse(right)",
":format": """(() => {
const formatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC',
});
return value => {
if (!value) return '';
const timestamp = Date.parse(value);
return Number.isNaN(timestamp) ? 'Invalid date' : formatter.format(timestamp);
};
})()""",
},
]
ROWS: list[TableRow] = [
{"id": 101, "name": "Desk lamp", "category": "Lighting", "stock": 7, "price": 42.5, "status": "Low"},
{"id": 102, "name": "Task chair", "category": "Seating", "stock": 18, "price": 289.0, "status": "Ready"},
{"id": 103, "name": "Monitor arm", "category": "Hardware", "stock": 0, "price": 119.95, "status": "Backorder"},
{"id": 104, "name": "Cable tray", "category": "Hardware", "stock": 34, "price": 31.25, "status": "Ready"},
{"id": 105, "name": "Side table", "category": "Furniture", "stock": 9, "price": 164.5, "status": "Low"},
{"id": 106, "name": "Floor light", "category": "Lighting", "stock": 15, "price": 98.0, "status": "Ready"},
{
"id": 101,
"name": "Desk lamp",
"category": "Lighting",
"stock": 7,
"price": 42.5,
"status": "Low",
"updated_at": "2026-08-31T16:20:00Z",
},
{
"id": 102,
"name": "Task chair",
"category": "Seating",
"stock": 18,
"price": 289.0,
"status": "Ready",
"updated_at": "2026-09-01T08:45:00Z",
},
{
"id": 103,
"name": "Monitor arm",
"category": "Hardware",
"stock": 0,
"price": 119.95,
"status": "Backorder",
"updated_at": "2026-08-29T11:05:00Z",
},
{
"id": 104,
"name": "Cable tray",
"category": "Hardware",
"stock": 34,
"price": 31.25,
"status": "Ready",
"updated_at": "2026-09-01T14:30:00Z",
},
{
"id": 105,
"name": "Side table",
"category": "Furniture",
"stock": 9,
"price": 164.5,
"status": "Low",
"updated_at": "2026-08-30T19:15:00Z",
},
{
"id": 106,
"name": "Floor light",
"category": "Lighting",
"stock": 15,
"price": 98.0,
"status": "Ready",
"updated_at": "2026-09-01T10:10:00Z",
},
]
@@ -112,10 +190,10 @@ def render_table() -> ui.table:
)
)
with table.add_slot("top-left"), ui.row().classes("items-center gap-2"):
ui.icon("inventory_2", size="sm").classes("text-primary")
with table.add_slot("top-left"), ui.row(align_items="center").classes("gap-2"):
ui.icon("inventory_2", size="sm", color="primary")
ui.label("Inventory").classes("text-xl font-medium")
ui.badge(str(len(ROWS)), color="grey-3").props("text-color=grey-9")
ui.badge(str(len(ROWS)), color="grey-3", text_color="grey-9")
with table.add_slot("top-right"):
ui.input(placeholder="Search inventory").props("dense outlined clearable debounce=250").bind_value_to(
@@ -124,20 +202,36 @@ def render_table() -> ui.table:
)
with table.add_slot("body-cell-status"), table.cell("status"):
ui.badge().props(':label="props.value" :color="props.col.colorByValue[props.value] ?? \'grey\'" outline')
ui.badge(outline=True).props(':label="props.value" :color="props.col.colorByValue[props.value] ?? \'grey\'"')
with table.add_slot("no-data"), ui.row().classes("w-full items-center justify-center gap-2 p-6 text-grey-7"):
def open_product(event: events.GenericEventArguments) -> None:
product = next((row for row in table.rows if row[table.row_key] == event.args), None)
if product is None:
ui.notify("Product no longer exists", type="negative")
return
ui.notify(f"Opening {product['name']}")
with (
table.add_slot("body-cell-actions"),
table.cell("actions"),
ui.button(icon="open_in_new")
.props("round flat size='sm'")
.on(
"click.stop",
handler=open_product,
js_handler="() => emit(props.key)",
),
):
ui.tooltip("Open product")
with table.add_slot("no-data"), ui.row(align_items="center").classes("w-full justify-center gap-2 p-6 text-grey-7"):
ui.icon("inventory_2", size="2em").props(":name=\"props.filter ? 'filter_alt_off' : 'inventory_2'\"")
ui.element("span").props(':textContent="props.message"')
optional_columns = [column for column in table.columns if not column.get("required")]
def set_visible_columns(names: list[str]) -> None:
visible = set(names)
for column in optional_columns:
hidden = column["name"] not in visible
column["classes"] = "hidden" if hidden else ""
column["headerClasses"] = "hidden" if hidden else "text-grey-8"
table.props["visible-columns"] = names
table.update()
ui.select(
@@ -145,6 +239,7 @@ def render_table() -> ui.table:
value=[column["name"] for column in optional_columns],
label="Visible columns",
multiple=True,
clearable=True,
on_change=lambda event: set_visible_columns(event.value),
).props("outlined dense options-dense").classes("w-64")
@@ -152,7 +247,7 @@ def render_table() -> ui.table:
if __name__ in {"__main__", "__mp_main__"}:
with ui.column().classes("w-full items-center gap-4 p-4"):
with ui.column(align_items="center").classes("w-full gap-4 p-4"):
render_table()
ui.run(port=8888, reload=True)