Skip to content

When PARIWISATA Screams Inside a Dropdown That Should Look Tidy

Adityo Guni Waluyo

Raw UPPERCASE enum values leaked into four admin render spots; one formatter became the single display door, the ?? fallback stays double-edged.

TL;DR

The database correctly stored bidang values in uppercase, but four UI spots were displaying the raw values instead of the formatted labels. An existing formatBidang helper was already available but hadn't been wired into those components. The fix routes all display through that single formatter while keeping raw values for backend validation, touching just six lines across two files.

I opened the admin panel during the bidang rollout, and one line in the information form looked loud: the dropdown options were written PARIWISATA, OLAHRAGA, all caps, while every other label on the same page sat prettily in Title Case.

First guess: the data was wrong. Maybe a migration failed, maybe the values saved to the DB came out mangled. A direct check on the table: the values are indeed UPPERCASE, which is exactly the storage contract we agreed on. The data is healthy. What leaked is the display.

Four leak spots, one cause

A short search found four render spots that skipped the formatter: the bidang select options in the information editor, one hint line showing the bidang from the session user, the chip in the users table next to the role badge, and the select in the users form. All of them printed the raw value instead of the formatted label. The function already existed: formatBidang in frontend/src/lib/utils.ts was born with yesterday's badge commit, it just never got wired into these spots.

This is a classic boundary situation: what is stored and what is displayed are two different contracts. The database keeps storing UPPERCASE so values stay consistent when compared literally in the backend. Conversion into human language is the display layer's job, and that layer needs one door.

One door through Record and a fallback

The door already had its shape: one small function with a label map. Its type is Record<string, string>, exactly the pattern described in the TypeScript handbook [4] for mapping keys to another type. Unknown values fall to labels[bidang] ?? bidang, using nullish coalescing [5], which returns the right side only when the left side is null or undefined.

export function formatBidang(bidang: string): string {
  const labels: Record<string, string> = {
    ALL: "Semua Bidang",
    PARIWISATA: "Pariwisata",
    OLAHRAGA: "Olahraga",
    BUDAYA: "Kebudayaan",
    KEPEMUDAAN: "Kepemudaan",
  };
  return labels[bidang] ?? bidang;
}

This commit only changed the wiring: each select option now simply calls formatBidang(b), while the value stays the raw enum so the payload sent to the backend does not change. The ALL option label in the users form got tidied into Semua bidang (ALL) too, and the hint that used to interpolate the raw session value now goes through the same door.

The nice part: all four spots were fixed in one small diff: six lines changed across two files, no new schema, no new dependency. A reviewer only needs to ask one thing about each line touching a bidang value: has it gone through the door? If the answer is yes, the display cannot look loud again, no matter how many render spots get added later.

A small detail came along: the ALL option label in the users form became Semua bidang (ALL). A sentinel deserves to look like a sentinel. Users need to know they are picking a super-scope across bidang, not one bidang named All. Friendly formatting must not disguise a different meaning.

A fallback that helps and leaks at once

That double-question-mark fallback is a double-edged sword. On one side the app becomes resilient: if a new value slips in before the map gets updated, the UI does not crash, it just shows the raw value. The uncomfortable flip side: TypeScript cannot enforce exhaustiveness on a dynamic map like this. A render site that skips the formatter will not fail compilation; it quietly looks loud again, exactly like what I saw that afternoon.

My decision: the fallback stays, and this single formatter is agreed to be the only exit door for bidang values. Adding a new value means adding one label in one place, and code review boils down to one question: has this spot gone through the door?

One more angle worth writing down: why not just store the friendly values? Because those values are not only for looking at. They are sent back to the server every time a form saves, and the server compares them literally against its enum list. Prettified values would fail validation, and users would get errors despite entering correct data. So the pattern stays disciplined: the value attribute keeps the raw value, only the text between the tags is formatted. The separation of value and label in a select element was designed for exactly this, and it is why the fix could be six lines in two files.

Sources

Related articles