Add TSV export/import UI for mappings
- Export button downloads unmapped + existing mappings as TSV with sample column showing distinct source field values for context - Import button uploads filled TSV, any non-system column treated as an output key - Exclude *.tsv files from git Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1ed08755c1
commit
6f2992b315
2
.gitignore
vendored
2
.gitignore
vendored
@ -27,3 +27,5 @@ Thumbs.db
|
|||||||
# Uploads
|
# Uploads
|
||||||
uploads/*
|
uploads/*
|
||||||
!uploads/.gitkeep
|
!uploads/.gitkeep
|
||||||
|
|
||||||
|
*.tsv
|
||||||
|
|||||||
@ -53,6 +53,12 @@ export const api = {
|
|||||||
// Mappings
|
// Mappings
|
||||||
getMappings: (source, rule) => request('GET', `/mappings/source/${source}${rule ? `?rule_name=${rule}` : ''}`),
|
getMappings: (source, rule) => request('GET', `/mappings/source/${source}${rule ? `?rule_name=${rule}` : ''}`),
|
||||||
getUnmapped: (source, rule) => request('GET', `/mappings/source/${source}/unmapped${rule ? `?rule_name=${rule}` : ''}`),
|
getUnmapped: (source, rule) => request('GET', `/mappings/source/${source}/unmapped${rule ? `?rule_name=${rule}` : ''}`),
|
||||||
|
exportMappingsUrl: (source, rule) => `${BASE}/mappings/source/${source}/export.tsv${rule ? `?rule_name=${rule}` : ''}`,
|
||||||
|
importMappingsCSV: (source, file) => {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', file)
|
||||||
|
return request('POST', `/mappings/source/${source}/import-csv`, fd, true)
|
||||||
|
},
|
||||||
createMapping: (body) => request('POST', '/mappings', body),
|
createMapping: (body) => request('POST', '/mappings', body),
|
||||||
bulkMappings: (mappings) => request('POST', '/mappings/bulk', { mappings }),
|
bulkMappings: (mappings) => request('POST', '/mappings/bulk', { mappings }),
|
||||||
updateMapping: (id, body) => request('PUT', `/mappings/${id}`, body),
|
updateMapping: (id, body) => request('PUT', `/mappings/${id}`, body),
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export default function Mappings({ source }) {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [editingId, setEditingId] = useState(null)
|
const [editingId, setEditingId] = useState(null)
|
||||||
const [editDrafts, setEditDrafts] = useState({})
|
const [editDrafts, setEditDrafts] = useState({})
|
||||||
|
const [importing, setImporting] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!source) return
|
if (!source) return
|
||||||
@ -102,6 +103,30 @@ export default function Mappings({ source }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleImportCSV(e) {
|
||||||
|
const file = e.target.files[0]
|
||||||
|
if (!file) return
|
||||||
|
e.target.value = ''
|
||||||
|
setImporting(true)
|
||||||
|
try {
|
||||||
|
const result = await api.importMappingsCSV(source, file)
|
||||||
|
alert(`Imported ${result.count} mapping${result.count !== 1 ? 's' : ''}.`)
|
||||||
|
// Refresh current tab
|
||||||
|
const rule = selectedRule || undefined
|
||||||
|
const [u, m] = await Promise.all([
|
||||||
|
api.getUnmapped(source, rule),
|
||||||
|
tab === 'mapped' ? api.getMappings(source, rule) : Promise.resolve([])
|
||||||
|
])
|
||||||
|
setUnmapped(u)
|
||||||
|
setMapped(m)
|
||||||
|
setDrafts({})
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message)
|
||||||
|
} finally {
|
||||||
|
setImporting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteMapping(id) {
|
async function deleteMapping(id) {
|
||||||
try {
|
try {
|
||||||
await api.deleteMapping(id)
|
await api.deleteMapping(id)
|
||||||
@ -157,6 +182,19 @@ export default function Mappings({ source }) {
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h1 className="text-xl font-semibold text-gray-800">Mappings — {source}</h1>
|
<h1 className="text-xl font-semibold text-gray-800">Mappings — {source}</h1>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<a
|
||||||
|
href={api.exportMappingsUrl(source, selectedRule)}
|
||||||
|
download
|
||||||
|
className="text-sm px-3 py-1.5 border border-gray-200 rounded hover:bg-gray-50 text-gray-600"
|
||||||
|
>
|
||||||
|
Export CSV
|
||||||
|
</a>
|
||||||
|
<label className={`text-sm px-3 py-1.5 border border-gray-200 rounded cursor-pointer hover:bg-gray-50 text-gray-600 ${importing ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
|
{importing ? 'Importing…' : 'Import CSV'}
|
||||||
|
<input type="file" accept=".tsv,.txt" className="hidden" onChange={handleImportCSV} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rule filter */}
|
{/* Rule filter */}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user