Saat PARIWISATA Teriak di Tengah Dropdown yang Seharusnya Rapi
Nilai enum UPPERCASE bocor ke empat titik render admin; cukup satu formatter jadi pintu tunggal display, fallback ?? tetap jadi pedang dua mata.
Ringkasan
Dropdown bidang nampilin huruf kapital semua padahal data di DB emang sengaja disimpan uppercase. Ternyata ada empat titik yang kelupaan pakai formatter formatBidang jadi masih ngasih nilai mentah. Fix-nya simpel tinggal colok formatter itu biar label jadi Title Case tapi value tetap uppercase biar nggak error validasi.
Buka panel admin pas rollout bidang, dan di form informasi ada satu baris yang norak: opsi dropdown ke-tulis PARIWISATA, OLAHRAGA, huruf kapital semua, padahal label lain di halaman yang sama manis-manis aja Title Case.
Tebakan pertama: datanya yang salah. Mungkin migrasi gagal, mungkin nilai yang ke-save ke DB bentuknya aneh. Cek langsung ke tabel: nilainya emang UPPERCASE, tapi itu justru sesuai kontrak penyimpanan yang udah disepakati. Data sehat. Yang bocor itu tampilannya.
Empat titik bocor, satu penyebab
Nyari-nyari sebentar, ketemu empat titik render yang lolos manggil formatter: opsi select bidang di editor informasi, satu baris hint yang nunjukin bidang dari sesi user, chip di tabel user sebelah badge role, dan select di form user. Semua nampilin nilai mentah, bukan hasil format. Fungsinya aja udah ada: formatBidang di frontend/src/lib/utils.ts terbentuk sejak commit badge kemarin, cuma kelewat dipasang di titik-titik ini.
Situasinya klasik boundary: yang disimpan dan yang ditampilkan itu dua kontrak beda. Database tetap simpan UPPERCASE biar konsisten dibandingkan secara literal di backend. Konversi ke bahasa manusia itu urusan layer tampilan, dan layer ini butuh satu pintu.
Satu pintu lewat Record dan fallback
Pintunya udah terlanjur berbentuk: satu fungsi kecil dengan map label. Typenya Record<string, string>, persis pola yang dijelaskan di handbook TypeScript [4] buat memetakan kunci ke tipe lain. Nilai yang nggak dikenal jatuh ke labels[bidang] ?? bidang, memanfaatkan nullish coalescing [5] yang balikin sisi kanan cuma pas sisi kirinya null atau 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;
}
Di commit ini yang diubah cuma wiring-nya: tiap opsi select jadi cukup nulis formatBidang(b), value-nya tetap enum mentah biar payload ke backend nggak berubah. Label opsi ALL di form user ikut dirapikan jadi Semua bidang (ALL), dan hint yang tadinya nyisipin nilai mentah dari sesi sekarang lewat pintu yang sama.
Fallback yang membantu sekaligus bocor
Fallback tanda tanya ganda itu pedang dua mata. Di satu sisi aplikasi jadi tahan banting: kalau nanti ada nilai baru yang keburu masuk sebelum map-nya ke-update, UI nggak crash, cukup nampilin nilai mentahnya. Sisi baliknya kurang nyaman: TypeScript nggak bisa negoin exhaustiveness di map dinamis kayak gini. Site render yang kelewat manggil formatter nggak bakal gagal kompilasi; dia diam-diam kelihatan norak lagi, persis kayak yang saya lihat sore itu.
Keputusan saya: fallback tetap dipertahankan, tapi satu formatter ini disepakati jadi pintu keluar tunggal buat nilai bidang. Nambah nilai baru berarti nambah label di satu tempat, dan code review tinggal nanya satu hal: titik ini udah lewat pintu belum?
Pertanyaannya lalu bergeser: kenapa nggak sekalian ganti isinya jadi Title Case aja? Karena nilai itu bukan cuma buat dipandang. Ia dikirim balik ke server tiap form di-save, dan server membandingkannya secara literal sama daftar enum-nya. Value yang dimanis-kan bakal kena penolakan validasi, dan user dapat error padahal datanya benar. Maka polanya disiplin: atribut value tetap nilai mentah, cuma teks di antara tag yang diformat. Pemisahan value dan label di elemen select memang didesain buat urusan beginian, dan inilah alasan fix-nya bisa sekecil enam baris di dua file.
Detail kecil yang ikut kebagian: label opsi ALL di form user diganti jadi Semua bidang (ALL). Sentinel memang lebih baik kelihatan sebagai sentinel. Pemakainya perlu tau kalau yang sedang dipilih itu super-scope lintas bidang, bukan satu bidang bernama Semua. Format yang ramah nggak boleh nyamarin makna yang beda.
Yang bikin enteng, keempat titik itu dibenerin di satu diff kecil: enam baris berubah di dua file, tanpa schema baru, tanpa dependency masuk. Reviewer cukup nanya satu hal di tiap baris yang menyentuh nilai bidang: udah lewat pintu belum? Kalau jawabannya iya, tampilan nggak mungkin norak lagi, berapa pun titik render yang nambah nanti.