# 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): ```python 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): ```jsonnet { 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): ```typescript /** 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` 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`: ```typescript 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({ 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`: ```tsx // Badge cell: badgeProps(value) spreads straight into . field("status", { header: "Status", cell: (row) => , 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. ``` For an enum that reaches the frontend but you never badge or pick (e.g. a `ColumnType`), use `noEnumDisplay("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](fields.md), [actions](actions.md), and [table configuration](table-configuration.md).