diff --git a/vueManager/src/components/ProductDataFields.vue b/vueManager/src/components/ProductDataFields.vue index 473db07e7..946238239 100644 --- a/vueManager/src/components/ProductDataFields.vue +++ b/vueManager/src/components/ProductDataFields.vue @@ -7,6 +7,7 @@ import { useToast } from 'primevue/usetoast' import { computed, onMounted, ref } from 'vue' import request from '../request.js' +import { groupProductDataSections } from '../utils/groupProductDataSections.js' import { isFullWidthExtraFieldXtype, parseStructuredExtraFieldValue, @@ -201,32 +202,11 @@ const visibleFields = computed(() => { }) /** - * Group fields by sections - * Show only !hidden sections + * Group fields by sections (array ordered by section.sort_order). + * Do not return an id-keyed object: Vue/JS enumerates those keys by ascending id (#611). */ const fieldsBySections = computed(() => { - const sections = {} - - visibleFields.value.forEach(field => { - const sectionKey = field.section || 'default' - const sectionConfig = fieldsConfig.value.sections[sectionKey] - - // Skip hidden sections - if (sectionConfig && sectionConfig.hidden === true) { - return - } - - if (!sections[sectionKey]) { - sections[sectionKey] = { - ...sectionConfig, - fields: [], - } - } - - sections[sectionKey].fields.push(field) - }) - - return sections + return groupProductDataSections(visibleFields.value, fieldsConfig.value.sections || {}) }) // Load configuration on mount @@ -253,9 +233,9 @@ onMounted(() => {
} fields Visible field configs (already filtered). + * @param {Object} sectionsById Map from section id → section config. + * @returns {Array<{ key: string|number, fields: Array, [string]: any }>} + */ +export function groupProductDataSections(fields, sectionsById = {}) { + const grouped = new Map() + + for (const field of fields) { + // Normalize to string so Map keys match JSON object keys from getPageFields. + const sectionKey = field.section == null || field.section === '' ? 'default' : String(field.section) + const sectionConfig = sectionsById[sectionKey] + + if (sectionConfig?.hidden === true) { + continue + } + + if (!grouped.has(sectionKey)) { + grouped.set(sectionKey, { + // Fallback identity when section is missing from the map; API `key` (section_key) wins via spread. + key: sectionKey, + ...(sectionConfig || {}), + fields: [], + }) + } + + grouped.get(sectionKey).fields.push(field) + } + + const sections = Array.from(grouped.values()) + + sections.sort((a, b) => { + const sortA = Number.isFinite(Number(a.sort_order)) ? Number(a.sort_order) : Number.MAX_SAFE_INTEGER + const sortB = Number.isFinite(Number(b.sort_order)) ? Number(b.sort_order) : Number.MAX_SAFE_INTEGER + if (sortA !== sortB) { + return sortA - sortB + } + + const idA = Number(a.id ?? a.key) + const idB = Number(b.id ?? b.key) + if (Number.isFinite(idA) && Number.isFinite(idB) && idA !== idB) { + return idA - idB + } + + return String(a.id ?? a.key).localeCompare(String(b.id ?? b.key)) + }) + + return sections +} diff --git a/vueManager/src/utils/groupProductDataSections.test.js b/vueManager/src/utils/groupProductDataSections.test.js new file mode 100644 index 000000000..68e04fdcd --- /dev/null +++ b/vueManager/src/utils/groupProductDataSections.test.js @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' + +import { groupProductDataSections } from './groupProductDataSections.js' + +describe('groupProductDataSections', () => { + it('orders sections by sort_order even when section ids ascend differently', () => { + const sectionsById = { + 1: { id: 1, key: 'main', sort_order: 20, label: 'Main' }, + 3: { id: 3, key: 'first', sort_order: 10, label: 'First' }, + 5: { id: 5, key: 'currency', sort_order: 15, label: 'Currency' }, + } + const fields = [ + { name: 'article', section: 1 }, + { name: 'price', section: 3 }, + { name: 'currency', section: 5 }, + ] + + const result = groupProductDataSections(fields, sectionsById) + + expect(result.map(s => s.id)).toEqual([3, 5, 1]) + expect(result.map(s => s.key)).toEqual(['first', 'currency', 'main']) + expect(result.map(s => s.label)).toEqual(['First', 'Currency', 'Main']) + }) + + it('skips hidden sections', () => { + const sectionsById = { + 1: { id: 1, key: 'main', sort_order: 10, hidden: false }, + 2: { id: 2, key: 'hidden', sort_order: 5, hidden: true }, + } + const fields = [ + { name: 'a', section: 1 }, + { name: 'b', section: 2 }, + ] + + expect(groupProductDataSections(fields, sectionsById).map(s => s.id)).toEqual([1]) + }) + + it('keeps fields inside a section in input order', () => { + const sectionsById = { + 1: { id: 1, sort_order: 10 }, + } + const fields = [ + { name: 'second', section: 1, sort_order: 20 }, + { name: 'first', section: 1, sort_order: 10 }, + ] + + expect(groupProductDataSections(fields, sectionsById)[0].fields.map(f => f.name)).toEqual([ + 'second', + 'first', + ]) + }) + + it('falls back to default section when field.section is missing', () => { + const result = groupProductDataSections([{ name: 'orphan' }], {}) + expect(result).toHaveLength(1) + expect(result[0].key).toBe('default') + expect(result[0].fields[0].name).toBe('orphan') + }) +})