| 1 |
import type { Field } from '@/types/field' |
| 2 |
|
| 3 |
const NAME_FIELD_KEY_PATTERN = /^nameField(\d+)$/i |
| 4 |
|
| 5 |
export function isNameFieldKey(value: unknown): value is string { |
| 6 |
return typeof value === 'string' && NAME_FIELD_KEY_PATTERN.test(value) |
| 7 |
} |
| 8 |
|
| 9 |
function parseNameFieldKeyIndex(value: string): number | null { |
| 10 |
const match = NAME_FIELD_KEY_PATTERN.exec(value) |
| 11 |
return match ? parseInt(match[1], 10) : null |
| 12 |
} |
| 13 |
|
| 14 |
export function getChildFieldSettings(field: Field): Record<string, unknown> { |
| 15 |
let settings: Record<string, unknown> = {} |
| 16 |
|
| 17 |
if (typeof field.settings === 'string') { |
| 18 |
try { |
| 19 |
settings = JSON.parse(field.settings) as Record<string, unknown> |
| 20 |
} catch { |
| 21 |
settings = {} |
| 22 |
} |
| 23 |
} else if (typeof field.settings === 'object' && field.settings !== null) { |
| 24 |
settings = { ...(field.settings as Record<string, unknown>) } |
| 25 |
} |
| 26 |
|
| 27 |
if (field.nameFieldType) { |
| 28 |
settings.nameFieldType = field.nameFieldType |
| 29 |
} |
| 30 |
if (field.subFieldIndex !== undefined) { |
| 31 |
settings.subFieldIndex = field.subFieldIndex |
| 32 |
} |
| 33 |
|
| 34 |
return settings |
| 35 |
} |
| 36 |
|
| 37 |
function inferNameFieldKeyFromLabel(label: unknown): string | null { |
| 38 |
if (typeof label !== 'string' || label.trim() === '') { |
| 39 |
return null |
| 40 |
} |
| 41 |
|
| 42 |
const nameFieldMatch = /^nameField(\d+)$/i.exec(label.trim()) |
| 43 |
if (nameFieldMatch) { |
| 44 |
return `nameField${nameFieldMatch[1]}` |
| 45 |
} |
| 46 |
|
| 47 |
const labelMatch = /name\s*field\s*(\d+)/i.exec(label.trim()) |
| 48 |
if (labelMatch) { |
| 49 |
return `nameField${labelMatch[1]}` |
| 50 |
} |
| 51 |
|
| 52 |
return null |
| 53 |
} |
| 54 |
|
| 55 |
export function resolveNameFieldKey( |
| 56 |
subfield: { type?: string; label?: string }, |
| 57 |
settings?: Record<string, unknown>, |
| 58 |
fallbackIndex?: number, |
| 59 |
): string { |
| 60 |
if (isNameFieldKey(subfield.type)) { |
| 61 |
return subfield.type |
| 62 |
} |
| 63 |
|
| 64 |
const fromSettings = settings?.nameFieldType |
| 65 |
if (isNameFieldKey(fromSettings)) { |
| 66 |
return fromSettings |
| 67 |
} |
| 68 |
|
| 69 |
const fromLabel = inferNameFieldKeyFromLabel(subfield.label) |
| 70 |
if (fromLabel) { |
| 71 |
return fromLabel |
| 72 |
} |
| 73 |
|
| 74 |
if (fallbackIndex !== undefined) { |
| 75 |
return `nameField${fallbackIndex + 1}` |
| 76 |
} |
| 77 |
|
| 78 |
return 'nameField1' |
| 79 |
} |
| 80 |
|
| 81 |
function canonicalizeNameFieldKey(key: string): string { |
| 82 |
const index = parseNameFieldKeyIndex(key) |
| 83 |
return index !== null ? `nameField${index}` : key |
| 84 |
} |
| 85 |
|
| 86 |
function ensureUniqueNameFieldKey(key: string, usedKeys: readonly string[]): string { |
| 87 |
const normalizedKey = isNameFieldKey(key) ? canonicalizeNameFieldKey(key) : key |
| 88 |
const normalizedUsed = usedKeys.map((usedKey) => |
| 89 |
isNameFieldKey(usedKey) ? canonicalizeNameFieldKey(usedKey) : usedKey, |
| 90 |
) |
| 91 |
|
| 92 |
if (!normalizedUsed.includes(normalizedKey)) { |
| 93 |
return normalizedKey |
| 94 |
} |
| 95 |
|
| 96 |
let candidate = 1 |
| 97 |
while (normalizedUsed.includes(`nameField${candidate}`)) { |
| 98 |
candidate++ |
| 99 |
} |
| 100 |
|
| 101 |
return `nameField${candidate}` |
| 102 |
} |
| 103 |
|
| 104 |
export function resolveUniqueNameFieldKey( |
| 105 |
subfield: { type?: string; label?: string }, |
| 106 |
settings: Record<string, unknown> | undefined, |
| 107 |
fallbackIndex: number, |
| 108 |
usedKeys: readonly string[], |
| 109 |
): string { |
| 110 |
const key = resolveNameFieldKey(subfield, settings, fallbackIndex) |
| 111 |
return ensureUniqueNameFieldKey(key, usedKeys) |
| 112 |
} |
| 113 |
|
| 114 |
function getSubFieldDisplayIndex(settings?: Record<string, unknown>): number | null { |
| 115 |
const value = settings?.subFieldIndex |
| 116 |
if (typeof value === 'number' && Number.isFinite(value)) { |
| 117 |
return value |
| 118 |
} |
| 119 |
if (typeof value === 'string' && value.trim() !== '') { |
| 120 |
const parsed = Number(value) |
| 121 |
return Number.isFinite(parsed) ? parsed : null |
| 122 |
} |
| 123 |
return null |
| 124 |
} |
| 125 |
|
| 126 |
export function sortChildrenByDisplayOrder<T extends Field>( |
| 127 |
children: T[], |
| 128 |
apiOrder: Map<T, number>, |
| 129 |
): T[] { |
| 130 |
return [...children].sort((a, b) => { |
| 131 |
const aSettings = getChildFieldSettings(a) |
| 132 |
const bSettings = getChildFieldSettings(b) |
| 133 |
const aIndex = getSubFieldDisplayIndex(aSettings) |
| 134 |
const bIndex = getSubFieldDisplayIndex(bSettings) |
| 135 |
const aHas = aIndex !== null |
| 136 |
const bHas = bIndex !== null |
| 137 |
if (aHas && bHas) return aIndex - bIndex |
| 138 |
if (aHas) return -1 |
| 139 |
if (bHas) return 1 |
| 140 |
return (apiOrder.get(a) || 0) - (apiOrder.get(b) || 0) |
| 141 |
}) |
| 142 |
} |
| 143 |
|
| 144 |
export function assignLegacyNameFieldKeysIfMissing<T extends { type?: string; label?: string }>( |
| 145 |
subfields: T[], |
| 146 |
settingsList: Array<Record<string, unknown>> = [], |
| 147 |
): Array<T & { type: string }> { |
| 148 |
const usedKeys: string[] = [] |
| 149 |
|
| 150 |
return subfields.map((subfield, idx) => { |
| 151 |
const type = resolveUniqueNameFieldKey(subfield, settingsList[idx], idx, usedKeys) |
| 152 |
usedKeys.push(type) |
| 153 |
return { ...subfield, type } |
| 154 |
}) |
| 155 |
} |
| 156 |
|