fix(admin): resolve options display and add full 8-language translations for product attributes
This commit is contained in:
1 parent
593d357ad4
commit
911614f862
4 files changed
+1252
-192
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user