diff --git a/plugins/csv-import/src/components/FieldMapperRow.tsx b/plugins/csv-import/src/components/FieldMapperRow.tsx index 905137026..4fd1caced 100644 --- a/plugins/csv-import/src/components/FieldMapperRow.tsx +++ b/plugins/csv-import/src/components/FieldMapperRow.tsx @@ -20,6 +20,7 @@ export interface FieldMappingItem { interface FieldMapperRowProps { item: FieldMappingItem existingFields: Field[] + slugFieldName: string | null onToggleIgnored: () => void onSetIgnored: (ignored: boolean) => void onTargetChange: (targetFieldId: string | null) => void @@ -29,6 +30,7 @@ interface FieldMapperRowProps { export function FieldMapperRow({ item, existingFields, + slugFieldName, onToggleIgnored, onSetIgnored, onTargetChange, @@ -36,6 +38,7 @@ export function FieldMapperRow({ }: FieldMapperRowProps) { const { inferredField, action, targetFieldId, hasTypeMismatch, overrideType } = item const isIgnored = action === "ignore" + const isSlugField = slugFieldName && inferredField.columnName === slugFieldName // Find the target field when mapping to an existing field const targetField = targetFieldId ? existingFields.find(f => f.id === targetFieldId) : null @@ -98,7 +101,7 @@ export function FieldMapperRow({ }} > - {isIgnored && } + {isIgnored && } {existingFields.length > 0 &&
} {existingFields.map(field => ( diff --git a/plugins/csv-import/src/routes/FieldMapper.tsx b/plugins/csv-import/src/routes/FieldMapper.tsx index b67f44475..a316be760 100644 --- a/plugins/csv-import/src/routes/FieldMapper.tsx +++ b/plugins/csv-import/src/routes/FieldMapper.tsx @@ -27,10 +27,12 @@ interface FieldMapperProps { onSubmit: (opts: FieldMapperSubmitOpts) => Promise } +function isValidSlugColumn(columnName: string, csvRecords: Record[]) { + return csvRecords.every(record => record[columnName]) +} + function calculatePossibleSlugFields(mappings: FieldMappingItem[], csvRecords: Record[]) { - return mappings - .filter(m => csvRecords.every(record => record[m.inferredField.columnName]) && m.action !== "ignore") - .map(m => m.inferredField) + return mappings.filter(m => isValidSlugColumn(m.inferredField.columnName, csvRecords)).map(m => m.inferredField) } export function FieldMapper({ collection, csvRecords, onSubmit }: FieldMapperProps) { @@ -49,39 +51,53 @@ export function FieldMapper({ collection, csvRecords, onSubmit }: FieldMapperPro const fields = await collection.getFields() setExistingFields(fields) - // Track which existing fields get mapped - const mappedFieldIds = new Set() - const inferredFields = inferFieldsFromCSV(csvRecords) + // Existing Slug field match against CSV column if any + const matchedSlugColumnName = collection.slugFieldName + ? inferredFields.find(field => field.columnName === collection.slugFieldName)?.columnName + : undefined + + // Column we will suggest as slug field on the UI + const suggestedSlugColumnName = + matchedSlugColumnName ?? + inferredFields.find(field => isValidSlugColumn(field.columnName, csvRecords))?.columnName + // Create initial mappings based on name matching const initialMappings: FieldMappingItem[] = inferredFields.map(inferredField => { + // ignore slug field if it matches the collection's slugFieldName + // If it was auto-detected, keep it enabled + const isSlugField = matchedSlugColumnName + ? inferredField.columnName === matchedSlugColumnName + : false + // Try to find an existing field with matching name const matchingField = fields.find(f => f.name.toLowerCase() === inferredField.name.toLowerCase()) - if (matchingField) { const hasTypeMismatch = !isTypeCompatible(inferredField.inferredType, matchingField.type) - mappedFieldIds.add(matchingField.id) + return { inferredField, - action: "map", - targetFieldId: matchingField.id, - hasTypeMismatch, + action: isSlugField ? "ignore" : "map", + targetFieldId: isSlugField ? undefined : matchingField.id, + hasTypeMismatch: isSlugField ? false : hasTypeMismatch, } } - // No match - create new field + // No match - create new field or ignore if it's the slug field return { inferredField, - action: "create", + action: isSlugField ? "ignore" : "create", hasTypeMismatch: false, } }) - setMappings(initialMappings) + const mappedFieldIds = new Set( + initialMappings.filter(m => m.action === "map" && m.targetFieldId).map(m => m.targetFieldId ?? "") + ) - const possibleSlugFields = calculatePossibleSlugFields(initialMappings, csvRecords) - setSelectedSlugFieldName(possibleSlugFields[0]?.columnName ?? null) + setMappings(initialMappings) + setSelectedSlugFieldName(suggestedSlugColumnName ?? null) // Find fields that exist in collection but are not mapped from CSV const initialMissingFields: MissingFieldItem[] = fields @@ -103,64 +119,41 @@ export function FieldMapper({ collection, csvRecords, onSubmit }: FieldMapperPro void loadFields() }, [collection, csvRecords]) - const toggleIgnored = useCallback( - (columnName: string) => { - setMappings(prev => { - const currentItem = prev.find(item => item.inferredField.columnName === columnName) - const willBeIgnored = currentItem?.action !== "ignore" - - const newMappings = prev.map(item => { - if (item.inferredField.columnName !== columnName) return item - - if (item.action === "ignore") { - // Un-ignore: restore to create mode - return { ...item, action: "create" as const, targetFieldId: undefined, hasTypeMismatch: false } - } else { - // Ignore - return { ...item, action: "ignore" as const, targetFieldId: undefined, hasTypeMismatch: false } - } - }) + const toggleIgnored = useCallback((columnName: string) => { + setMappings(prev => { + const newMappings = prev.map(item => { + if (item.inferredField.columnName !== columnName) return item - // If ignoring the current slug field, switch to another available one - if (willBeIgnored && columnName === selectedSlugFieldName) { - setSelectedSlugFieldName(null) + if (item.action === "ignore") { + // Un-ignore: restore to create mode + return { ...item, action: "create" as const, targetFieldId: undefined, hasTypeMismatch: false } + } else { + // Ignore + return { ...item, action: "ignore" as const, targetFieldId: undefined, hasTypeMismatch: false } } - - return newMappings }) - }, - [selectedSlugFieldName] - ) - - const setIgnored = useCallback( - (columnName: string, ignored: boolean) => { - setMappings(prev => { - const newMappings = prev.map(item => { - if (item.inferredField.columnName !== columnName) return item - if (ignored) { - return { ...item, action: "ignore" as const, targetFieldId: undefined, hasTypeMismatch: false } - } else if (item.action === "ignore") { - // Un-ignore: restore to create mode - return { ...item, action: "create" as const, targetFieldId: undefined, hasTypeMismatch: false } - } - return item - }) + return newMappings + }) + }, []) - // If ignoring the current slug field, switch to another available one - if (ignored && columnName === selectedSlugFieldName) { - const newSlugField = newMappings - .filter(m => m.action !== "ignore") - .find(m => csvRecords.every(record => record[m.inferredField.columnName])) + const setIgnored = useCallback((columnName: string, ignored: boolean) => { + setMappings(prev => { + const newMappings = prev.map(item => { + if (item.inferredField.columnName !== columnName) return item - setSelectedSlugFieldName(newSlugField?.inferredField.columnName ?? null) + if (ignored) { + return { ...item, action: "ignore" as const, targetFieldId: undefined, hasTypeMismatch: false } + } else if (item.action === "ignore") { + // Un-ignore: restore to create mode + return { ...item, action: "create" as const, targetFieldId: undefined, hasTypeMismatch: false } } - - return newMappings + return item }) - }, - [selectedSlugFieldName, csvRecords] - ) + + return newMappings + }) + }, []) const updateTarget = useCallback( (columnName: string, targetFieldId: string | null) => { @@ -198,7 +191,6 @@ export function FieldMapper({ collection, csvRecords, onSubmit }: FieldMapperPro const mappedFieldIds = new Set( newMappings.filter(m => m.action === "map" && m.targetFieldId).map(m => m.targetFieldId) ) - setMissingFields(prev => { const prevActionMap = new Map(prev.map(item => [item.field.id, item.action])) @@ -237,13 +229,6 @@ export function FieldMapper({ collection, csvRecords, onSubmit }: FieldMapperPro return } - // Check if the slug field is being ignored - const slugMapping = mappings.find(m => m.inferredField.columnName === selectedSlugFieldName) - if (slugMapping?.action === "ignore") { - framer.notify("The slug field cannot be ignored.", { variant: "warning" }) - return - } - // Check if all required fields are mapped if (unmappedRequiredFields.length > 0) { framer.notify("All required fields must be mapped before importing.", { variant: "warning" }) @@ -321,6 +306,7 @@ export function FieldMapper({ collection, csvRecords, onSubmit }: FieldMapperPro key={item.inferredField.columnName} item={item} existingFields={existingFields} + slugFieldName={selectedSlugFieldName} onToggleIgnored={() => { toggleIgnored(item.inferredField.columnName) }} diff --git a/plugins/csv-import/src/utils/prepareImportPayload.ts b/plugins/csv-import/src/utils/prepareImportPayload.ts index 9bcd9ec86..98bfe6c9b 100644 --- a/plugins/csv-import/src/utils/prepareImportPayload.ts +++ b/plugins/csv-import/src/utils/prepareImportPayload.ts @@ -208,7 +208,6 @@ export async function prepareImportPayload(opts: ProcessRecordsWithFieldMappingO const allItemIdBySlug = new Map>() - // TODO: what's the significance of this? We can do joins between collections? Needs QA to ensure it still works for (const field of fields) { if (field.type === "collectionReference" || field.type === "multiCollectionReference") { const collectionIdBySlug = allItemIdBySlug.get(field.collectionId) ?? new Map()