diff --git a/website/src/components/Submission/FileUpload/ColumnMapping.spec.ts b/website/src/components/Submission/FileUpload/ColumnMapping.spec.ts index f6f3c41ed..d987dc6ec 100644 --- a/website/src/components/Submission/FileUpload/ColumnMapping.spec.ts +++ b/website/src/components/Submission/FileUpload/ColumnMapping.spec.ts @@ -6,13 +6,9 @@ import { RawFile } from './fileProcessing'; describe('ColumnMapping', () => { it('should create a mapping from columns', () => { const sourceColumns = ['date', 'location', 'Foo Bar']; - const targetColumns = new Map([ - ['date', null], - ['location', null], - ['foo', 'Foo Bar'], - ]); + const inputFields = [{ name: 'date' }, { name: 'location' }, { name: 'foo', displayName: 'Foo Bar' }]; - const mapping = ColumnMapping.fromColumns(sourceColumns, targetColumns); + const mapping = ColumnMapping.fromColumns(sourceColumns, inputFields); const entries = mapping.entries(); expect(entries).toEqual([ @@ -24,11 +20,11 @@ describe('ColumnMapping', () => { it('should update a specific mapping', () => { const sourceColumns = ['loc']; - const targetColumns = new Map([ - ['location', 'Location'], - ['date', 'Date'], - ]); - const mapping = ColumnMapping.fromColumns(sourceColumns, targetColumns); + const inputFields = [ + { name: 'location', displayName: 'Location' }, + { name: 'date', displayName: 'Date' }, + ]; + const mapping = ColumnMapping.fromColumns(sourceColumns, inputFields); const updatedMapping = mapping.updateWith('loc', 'date'); @@ -38,11 +34,8 @@ describe('ColumnMapping', () => { it('should apply a mapping correctly', async () => { const sourceColumns = ['loc', 'date']; - const targetColumns = new Map([ - ['date', null], - ['location', 'Location'], - ]); - const mapping = ColumnMapping.fromColumns(sourceColumns, targetColumns); + const inputFields = [{ name: 'date' }, { name: 'location', displayName: 'Location' }]; + const mapping = ColumnMapping.fromColumns(sourceColumns, inputFields); const updatedMapping = mapping.updateWith('loc', 'location'); const tsvContent = 'date\tloc\n' + '2023-01-01\tUSA\n' + '2023-01-02\tCanada'; diff --git a/website/src/components/Submission/FileUpload/ColumnMapping.ts b/website/src/components/Submission/FileUpload/ColumnMapping.ts index 6502cf4e9..b445925c0 100644 --- a/website/src/components/Submission/FileUpload/ColumnMapping.ts +++ b/website/src/components/Submission/FileUpload/ColumnMapping.ts @@ -1,57 +1,53 @@ import { stringSimilarity } from 'string-similarity-js'; import { type ProcessedFile } from './fileProcessing'; +import type { InputField } from '../../../types/config'; export class ColumnMapping { private readonly map: ReadonlyMap; - private readonly displayNames: ReadonlyMap; - private constructor(map: ReadonlyMap, displayNames: ReadonlyMap) { + private constructor(map: ReadonlyMap) { this.map = map; - this.displayNames = displayNames; } - private static getBestMatchingTargetColumn( - sourceColumn: string, - targetColumns: ReadonlyMap, - ): string | null { - const [bestMatch, score] = Array.from(targetColumns.entries()) - .map(([targetColName, targetColDisplayName]): [string, number] => { + private static getBestMatchingTargetColumn(sourceColumn: string, inputFields: InputField[]): string | null { + const [bestMatch, score] = inputFields + .map((field): [string, number] => { const score = Math.max( - stringSimilarity(sourceColumn, targetColName), - stringSimilarity(sourceColumn, targetColDisplayName ?? ''), + stringSimilarity(sourceColumn, field.name), + stringSimilarity(sourceColumn, field.displayName ?? ''), ); - return [targetColName, score]; + return [field.name, score]; }) .reduce((maxItem, currentItem) => (currentItem[1] > maxItem[1] ? currentItem : maxItem)); return score > 0.5 ? bestMatch : null; } /* Create a new mapping with the given columns, doing a best-effort to pre-match columns. */ - public static fromColumns(sourceColumns: string[], targetColumns: Map) { + public static fromColumns(sourceColumns: string[], inputFields: InputField[]) { const mapping = new Map( sourceColumns.map((sourceColumn) => [ sourceColumn, - this.getBestMatchingTargetColumn(sourceColumn, targetColumns), + this.getBestMatchingTargetColumn(sourceColumn, inputFields), ]), ); - return new ColumnMapping(mapping, targetColumns); + return new ColumnMapping(mapping); } /* Update the mapping with new source and target columns, trying to keep as much of the mapping intact as possible. */ - public update(newSourceColumns: string[], newTargetColumns: Map): ColumnMapping { + public update(newSourceColumns: string[], inputFields: InputField[]): ColumnMapping { const newMapping = new Map( newSourceColumns.map((newSourceCol) => { const prevTargetCol = this.map.get(newSourceCol); - if (prevTargetCol && Array.from(newTargetColumns.keys()).includes(prevTargetCol)) { + if (prevTargetCol && inputFields.map((f) => f.name).includes(prevTargetCol)) { return [newSourceCol, prevTargetCol]; } else { - return [newSourceCol, ColumnMapping.getBestMatchingTargetColumn(newSourceCol, newTargetColumns)]; + return [newSourceCol, ColumnMapping.getBestMatchingTargetColumn(newSourceCol, inputFields)]; } }), ); - return new ColumnMapping(newMapping, newTargetColumns); + return new ColumnMapping(newMapping); } /* Returns the entries in the mapping as a list. Each item in the list has: @@ -69,7 +65,7 @@ export class ColumnMapping { public updateWith(sourceColumn: string, targetColumn: string | null): ColumnMapping { const newMapping = new Map(this.map); newMapping.set(sourceColumn, targetColumn); - return new ColumnMapping(newMapping, this.displayNames); + return new ColumnMapping(newMapping); } /* Apply this mapping to a TSV file, returning a new file with remapped columns. */ @@ -96,6 +92,7 @@ export class ColumnMapping { // eslint-disable-next-line @typescript-eslint/no-explicit-any const mapsAreEqual = (m1: ReadonlyMap, m2: ReadonlyMap) => m1.size === m2.size && Array.from(m1.keys()).every((key) => m1.get(key) === m2.get(key)); - return mapsAreEqual(this.displayNames, other.displayNames) && mapsAreEqual(this.map, other.map); + + return mapsAreEqual(this.map, other.map); } } diff --git a/website/src/components/Submission/FileUpload/ColumnMappingModal.tsx b/website/src/components/Submission/FileUpload/ColumnMappingModal.tsx index a0dd41e6e..12b90200c 100644 --- a/website/src/components/Submission/FileUpload/ColumnMappingModal.tsx +++ b/website/src/components/Submission/FileUpload/ColumnMappingModal.tsx @@ -44,15 +44,11 @@ export const ColumnMappingModal: FC = ({ useEffect(() => { if (inputColumns === null) return; - const targetColumnsWithDisplayNames = new Map( - Array.from(possibleTargetColumns.values()) - .flat() - .map((inputField) => [inputField.name, inputField.displayName ?? null]), - ); + const inputFields = Array.from(possibleTargetColumns.values()).flat(); if (columnMapping !== null) { - setCurrentMapping(columnMapping.update(inputColumns, targetColumnsWithDisplayNames)); + setCurrentMapping(columnMapping.update(inputColumns, inputFields)); } else { - setCurrentMapping(ColumnMapping.fromColumns(inputColumns, targetColumnsWithDisplayNames)); + setCurrentMapping(ColumnMapping.fromColumns(inputColumns, inputFields)); } }, [inputColumns, columnMapping, possibleTargetColumns, setCurrentMapping]); @@ -168,11 +164,12 @@ export const ColumnSelectorRow: FC = ({ selectedOption, setColumnMapping, }) => { - const selectedOptionText = selectedOption + const selectedField = selectedOption ? Array.from(options.values()) .flat() - .find((o) => o.name === selectedOption)?.displayName + .find((o) => o.name === selectedOption) : undefined; + const selectedOptionText = selectedField?.displayName ?? selectedField?.name; return (