114 lines
5.0 KiB
Vue
114 lines
5.0 KiB
Vue
<script setup>
|
|
const props = defineProps({
|
|
rows: Array,
|
|
entityOptions: Array,
|
|
isCompleted: Boolean,
|
|
detected: Object,
|
|
detectedNote: String,
|
|
duplicateTargets: Object,
|
|
missingCritical: Array,
|
|
mappingSaved: Boolean,
|
|
mappingSavedCount: Number,
|
|
mappingError: String,
|
|
show: { type: Boolean, default: true },
|
|
fieldsForEntity: Function,
|
|
})
|
|
const emits = defineEmits(['update:rows','save'])
|
|
|
|
function duplicateTarget(row){
|
|
if(!row || !row.entity || !row.field) return false
|
|
// parent already marks duplicates in duplicateTargets set keyed as record.field
|
|
return props.duplicateTargets?.has?.(row.entity + '.' + row.field) || false
|
|
}
|
|
</script>
|
|
<template>
|
|
<div v-if="show && rows?.length" class="pt-4">
|
|
<h3 class="font-semibold mb-2">
|
|
Detected Columns ({{ detected?.has_header ? 'header' : 'positional' }})
|
|
<span class="ml-2 text-xs text-gray-500">detected: {{ detected?.columns?.length || 0 }}, rows: {{ rows.length }}, delimiter: {{ detected?.delimiter || 'auto' }}</span>
|
|
</h3>
|
|
<p v-if="detectedNote" class="text-xs text-gray-500 mb-2">{{ detectedNote }}</p>
|
|
<div class="relative border rounded overflow-auto max-h-[420px]">
|
|
<table class="min-w-full bg-white">
|
|
<thead class="sticky top-0 z-10">
|
|
<tr class="bg-gray-50/95 backdrop-blur text-left text-xs uppercase text-gray-600">
|
|
<th class="p-2 border">Source column</th>
|
|
<th class="p-2 border">Entity</th>
|
|
<th class="p-2 border">Field</th>
|
|
<th class="p-2 border">Meta key</th>
|
|
<th class="p-2 border">Meta type</th>
|
|
<th class="p-2 border">Transform</th>
|
|
<th class="p-2 border">Apply mode</th>
|
|
<th class="p-2 border">Skip</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(row, idx) in rows" :key="idx" class="border-t" :class="duplicateTarget(row) ? 'bg-red-50' : ''">
|
|
<td class="p-2 border text-sm">{{ row.source_column }}</td>
|
|
<td class="p-2 border">
|
|
<select v-model="row.entity" class="border rounded p-1 w-full" :disabled="isCompleted">
|
|
<option value="">—</option>
|
|
<option v-for="opt in entityOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
|
|
</select>
|
|
</td>
|
|
<td class="p-2 border">
|
|
<select v-model="row.field" :class="['border rounded p-1 w-full', duplicateTarget(row) ? 'border-red-500 bg-red-50' : '']" :disabled="isCompleted">
|
|
<option value="">—</option>
|
|
<option v-for="f in fieldsForEntity(row.entity)" :key="f" :value="f">{{ f }}</option>
|
|
</select>
|
|
</td>
|
|
<td class="p-2 border">
|
|
<input
|
|
v-if="row.field === 'meta'"
|
|
v-model="(row.options ||= {}).key"
|
|
type="text"
|
|
class="border rounded p-1 w-full"
|
|
placeholder="e.g. monthly_rent"
|
|
:disabled="isCompleted"
|
|
/>
|
|
<span v-else class="text-gray-400 text-xs">—</span>
|
|
</td>
|
|
<td class="p-2 border">
|
|
<select
|
|
v-if="row.field === 'meta'"
|
|
v-model="(row.options ||= {}).type"
|
|
class="border rounded p-1 w-full"
|
|
:disabled="isCompleted"
|
|
>
|
|
<option :value="null">Default (string)</option>
|
|
<option value="string">string</option>
|
|
<option value="number">number</option>
|
|
<option value="date">date</option>
|
|
<option value="boolean">boolean</option>
|
|
</select>
|
|
<span v-else class="text-gray-400 text-xs">—</span>
|
|
</td>
|
|
<td class="p-2 border">
|
|
<select v-model="row.transform" class="border rounded p-1 w-full" :disabled="isCompleted">
|
|
<option value="">None</option>
|
|
<option value="trim">Trim</option>
|
|
<option value="upper">Uppercase</option>
|
|
<option value="lower">Lowercase</option>
|
|
</select>
|
|
</td>
|
|
<td class="p-2 border">
|
|
<select v-model="row.apply_mode" class="border rounded p-1 w-full" :disabled="isCompleted">
|
|
<option value="keyref">Keyref</option>
|
|
<option value="both">Both</option>
|
|
<option value="insert">Insert only</option>
|
|
<option value="update">Update only</option>
|
|
</select>
|
|
</td>
|
|
<td class="p-2 border text-center">
|
|
<input type="checkbox" v-model="row.skip" :disabled="isCompleted" />
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="mappingSaved" class="text-sm text-emerald-700 mt-2">Mappings saved ({{ mappingSavedCount }}).</div>
|
|
<div v-else-if="mappingError" class="text-sm text-red-600 mt-2">{{ mappingError }}</div>
|
|
<div v-if="missingCritical?.length" class="text-xs text-amber-600 mt-1">Missing critical: {{ missingCritical.join(', ') }}</div>
|
|
</div>
|
|
</template>
|