fix(admin): resolve options display and add full 8-language translations for product attributes

This commit is contained in:
Your Name committed 2026-10-19 19:39:13 +07:00
1 parent 593d357ad4
commit 911614f862
4 files changed
+1252 -192

No files matched your search

+120 -52
View File
@@ -139,25 +139,51 @@ function emptyDefinition(): EditableDefinition {
function parseDefinition(definition: AttributeDefinition): EditableDefinition {
const translations = Object.fromEntries(
LANGUAGES.map((lang) => [
lang,
{
label:
typeof definition.label === 'string'
? lang === 'th'
? definition.label
: ''
: definition.label?.[lang] || '',
description:
typeof definition.description === 'string'
? lang === 'th'
? definition.description
: ''
: definition.description?.[lang] || '',
},
])
LANGUAGES.map((lang) => {
let label = ''
let description = ''
if (typeof definition.label === 'string') {
label = lang === 'th' ? definition.label : ''
} else if (definition.label && typeof definition.label === 'object') {
label = (definition.label as any)[lang] || ''
}
if (typeof definition.description === 'string') {
description = lang === 'th' ? definition.description : ''
} else if (definition.description && typeof definition.description === 'object') {
description = (definition.description as any)[lang] || ''
}
// Check translations array from API if label is still empty
if (!label && Array.isArray((definition as any).translations)) {
const tr = (definition as any).translations.find((t: any) => t.langCode === lang)
if (tr) {
label = tr.label || ''
description = tr.description || description
}
}
return [lang, { label, description }]
})
)
const rawOptions = Array.isArray(definition.options) ? definition.options : []
const options = rawOptions.map((option: any) => {
if (typeof option === 'string') {
return { value: option, th: option, en: option }
}
if (typeof option === 'object' && option !== null) {
const norm: Record<string, string> = { ...option }
// Normalize th and en labels from label_th / label_en / label
if (!norm.th) norm.th = norm.label_th || norm.label || norm.value || ''
if (!norm.en) norm.en = norm.label_en || norm.label || norm.value || ''
if (!norm.value) norm.value = norm.key || norm.id || norm.en || norm.th || ''
return norm
}
return {}
})
return {
id: definition.id,
code: definition.code,
@@ -168,9 +194,7 @@ function parseDefinition(definition: AttributeDefinition): EditableDefinition {
sortOrder: definition.sortOrder,
categoryId: definition.categoryId ?? null,
translations,
options: Array.isArray(definition.options)
? definition.options.map((option) => (typeof option === 'string' ? { th: option } : option))
: [],
options,
}
}
@@ -438,11 +462,21 @@ export default function ProductAttributeEditPage() {
options:
form.type === 'SELECT'
? activeOptions
.map((opt) =>
Object.fromEntries(
.map((opt) => {
const cleaned = Object.fromEntries(
Object.entries(opt).filter(([, val]) => typeof val === 'string' && val.trim())
)
)
if (!cleaned.value && (cleaned.en || cleaned.th)) {
cleaned.value = (cleaned.en || cleaned.th)
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')
}
if (!cleaned.label_th && cleaned.th) cleaned.label_th = cleaned.th
if (!cleaned.label_en && cleaned.en) cleaned.label_en = cleaned.en
if (!cleaned.label) cleaned.label = cleaned.th || cleaned.en || cleaned.value || ''
return cleaned
})
.filter((opt) => Object.keys(opt).length > 0)
: undefined,
}
@@ -1391,37 +1425,71 @@ Return ONLY a valid JSON Array of objects in this exact structure (no markdown f
</div>
) : (
<div className="space-y-2.5">
{form.options.map((opt, idx) => (
<div
key={idx}
className="flex items-center gap-3 p-2.5 bg-white/[0.02] border border-white/5 rounded-xl hover:border-cyan-glow/30 transition-all"
>
<span className="w-7 text-center font-mono text-xs text-slate-500">#{idx + 1}</span>
<input
type="text"
value={opt[activeLang] || ''}
onChange={(e) => {
const nextOptions = [...form.options]
nextOptions[idx] = { ...nextOptions[idx], [activeLang]: e.target.value }
setForm({ ...form, options: nextOptions })
}}
className="flex-1 px-4 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
placeholder={`Option value in ${activeLang.toUpperCase()} (e.g. CBR 650R, Mono Shock)`}
/>
{form.options.map((opt, idx) => {
const optValue = opt.value || opt.key || ''
const currentLabel = opt[activeLang] || ''
const fallbackLabel = opt.th || opt.en || opt.label || ''
<button
type="button"
onClick={() => {
const nextOptions = form.options.filter((_, i) => i !== idx)
setForm({ ...form, options: nextOptions })
}}
className="p-2 text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-xl transition-all cursor-pointer"
title="Remove Option"
return (
<div
key={idx}
className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 p-3 bg-white/[0.02] border border-white/5 rounded-xl hover:border-cyan-glow/30 transition-all"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
<span className="w-7 text-center font-mono text-xs text-slate-500 self-center">#{idx + 1}</span>
{/* System Code / Key */}
<div className="w-full sm:w-44 flex-shrink-0">
<label className="sm:hidden text-[10px] uppercase font-bold text-slate-500 mb-1 block">System Code</label>
<input
type="text"
value={optValue}
onChange={(e) => {
const nextOptions = [...form.options]
nextOptions[idx] = { ...nextOptions[idx], value: e.target.value }
setForm({ ...form, options: nextOptions })
}}
placeholder="code_value"
className="w-full px-3 py-2 bg-black/40 border border-white/10 rounded-lg text-xs font-mono text-cyan-glow placeholder-slate-600 focus:border-cyan-glow/40 outline-none"
title="System code stored in database / API"
/>
</div>
{/* Localized Label */}
<div className="flex-1">
<label className="sm:hidden text-[10px] uppercase font-bold text-slate-500 mb-1 block">
Label ({activeLang.toUpperCase()})
</label>
<input
type="text"
value={currentLabel}
onChange={(e) => {
const nextOptions = [...form.options]
nextOptions[idx] = { ...nextOptions[idx], [activeLang]: e.target.value }
setForm({ ...form, options: nextOptions })
}}
className="w-full px-4 py-2 bg-night-950 border border-white/10 rounded-xl text-sm text-white focus:border-cyan-glow/40 outline-none"
placeholder={
fallbackLabel && !currentLabel
? `${fallbackLabel} (${activeLang === 'th' ? 'TH' : 'Fallback'})`
: `Label in ${activeLang.toUpperCase()}`
}
/>
</div>
<button
type="button"
onClick={() => {
const nextOptions = form.options.filter((_, i) => i !== idx)
setForm({ ...form, options: nextOptions })
}}
className="p-2 text-slate-500 hover:text-rose-400 hover:bg-rose-500/10 rounded-xl transition-all cursor-pointer self-center"
title="Remove Option"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
)
})}
</div>
)}
</section>
+13 -2
View File
@@ -843,8 +843,19 @@ export default function ProductEditPage() {
>
<option value="">Select an option</option>
{(definition.options || []).map((option, optionIdx) => {
const optionValue = typeof option === 'string' ? option : option.th || option.en || Object.values(option)[0]
const optionLabel = typeof option === 'string' ? option : option[activeLang] || option.th || option.en || Object.values(option)[0]
const optionValue =
typeof option === 'string'
? option
: option.value || option.key || option.id || option.th || option.en || Object.values(option)[0]
const optionLabel =
typeof option === 'string'
? option
: option[activeLang] ||
option[`label_${activeLang}`] ||
option.label ||
option.th ||
option.en ||
optionValue
return (
<option key={`${optionValue}-${optionIdx}`} value={optionValue}>
{optionLabel}