Enums¶
Layer: Backend · a field type + its frontend display
An enum field pins a column to a fixed set of string choices owned by a Python
StrEnum. Reach for one when a column is a small closed vocabulary — a status,
category, or lifecycle state.
How it works¶
Codegen imports your StrEnum by its dotted path — it never invents values —
annotates the pydantic schema, inlines the choices into enum filters, and emits
a TypeScript string-union that types the scaffolded display map.
The config you write¶
You write — be/inventory/models/asset.py (the StrEnum + its column):
class AssetStatus(enum.StrEnum):
"""Lifecycle state -- drives the FE filter dropdown."""
# enum.auto() lowercases each member name into its wire value, so
# members rename in Python without re-typing the string.
AVAILABLE = enum.auto()
ASSIGNED = enum.auto()
IN_REPAIR = enum.auto()
RETIRED = enum.auto()
# The column binds to it with TextEnum. Codegen resolves the dotted path
# "inventory.models.AssetStatus" against THIS class -- it must exist before
# codegen runs; codegen imports it, it does not generate it.
status: Mapped[AssetStatus] = mapped_column(
TextEnum(AssetStatus),
default=AssetStatus.AVAILABLE,
nullable=False,
)
You write — be/config/inventory_resources/asset.jsonnet (the enum field + enum filter):
{
representations: [
{
name: "list_item",
fields: [
fields.id(),
{ name: "name", type: "str" },
// type: "enum" + dotted enum: path. Works anywhere a field list does
// -- read dumps, create/update bodies, comms_summary. See fields.md.
{ name: "status", type: "enum", enum: "inventory.models.AssetStatus" },
],
},
],
operations: [
list.searchable(
representation="list_item",
filters=[
{ name: "id", values: "self" },
{ name: "category_id", values: "ref", ref_resource: "category" },
// An enum list filter: values: "enum" + the same dotted path. Choices
// inline in GET /_filters and serve from the field's _values endpoint;
// defaults to eq + in operators. See search-and-filters.md.
{ name: "status", values: "enum", enum: "inventory.models.AssetStatus" },
],
order=["name"],
default_order="name",
),
],
}
The frontend codegen emits¶
Codegen emits the enum as a string-union, regenerated every run:
Generated — fe/src/_generated/types/inventory/asset.gen.ts (do not edit):
/** Lifecycle state -- drives the FE filter dropdown. */
export type AssetStatus = "available" | "assigned" | "in_repair" | "retired";
Then it scaffolds a display module once (safe to edit — re-running codegen
won’t overwrite it). defineEnumDisplay<AssetStatus> is typed against the union,
so a value the backend adds later fails to compile here until you give it a label
and variant:
Scaffolded, safe to edit — fe/src/enums/assetStatus.ts:
import { defineEnumDisplay } from "@fsh/components-library";
import type { AssetStatus } from "../_generated/types/inventory/asset.gen";
// variant is a Badge colour role: success / primary / warning / danger /
// neutral. projectType.ts colours every value "neutral" because those values
// aren't status-like. Unknown values degrade to their raw string + a neutral
// badge, so a backend running ahead never blanks the UI.
export const assetStatus = defineEnumDisplay<AssetStatus>({
available: { label: "Available", variant: "success" },
assigned: { label: "Assigned", variant: "primary" },
in_repair: { label: "In repair", variant: "warning" },
retired: { label: "Retired", variant: "danger" },
});
The display object exposes everything a render site needs, so no call site re-derives the mapping with ad-hoc ternaries:
You write — fe/src/fields/inventory/asset.tsx + fe/src/actions/inventory/asset.tsx:
// Badge cell: badgeProps(value) spreads straight into <Badge>.
field("status", {
header: "Status",
cell: (row) => <Badge {...assetStatus.badgeProps(row.status)} />,
table: { resizable: true, width: "sm" },
}),
// Select input: useSelectProps() is a searchable, locally-filtered picker
// bundle. Also on the display: .selectProps (plain, non-searchable),
// .options ({value,label}[] for filter dropdowns), .label(v), .values.
<SelectInput name="status" label="Status" {...assetStatus.useSelectProps()} />
For an enum that reaches the frontend but you never badge or pick (e.g. a
ColumnType), use noEnumDisplay<V>("Name") instead of hand-colouring values
you’ll never show — it satisfies the same type but throws on any access, so
mis-wiring is loud at the call site. See fields,
actions, and table configuration.