Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion plugins/csv-import/src/components/FieldMapperRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,13 +30,15 @@ interface FieldMapperRowProps {
export function FieldMapperRow({
item,
existingFields,
slugFieldName,
onToggleIgnored,
onSetIgnored,
onTargetChange,
onTypeChange,
}: 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
Expand Down Expand Up @@ -98,7 +101,7 @@ export function FieldMapperRow({
}}
>
<option value="__create__">New Field...</option>
{isIgnored && <option value="__ignore__"></option>}
{isIgnored && <option value="__ignore__">{isSlugField ? "Slug Field" : ""}</option>}

{existingFields.length > 0 && <hr />}
{existingFields.map(field => (
Expand Down
134 changes: 60 additions & 74 deletions plugins/csv-import/src/routes/FieldMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ interface FieldMapperProps {
onSubmit: (opts: FieldMapperSubmitOpts) => Promise<void>
}

function isValidSlugColumn(columnName: string, csvRecords: Record<string, string>[]) {
return csvRecords.every(record => record[columnName])
}

function calculatePossibleSlugFields(mappings: FieldMappingItem[], csvRecords: Record<string, string>[]) {
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) {
Expand All @@ -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<string>()

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<string>(
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
Expand All @@ -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) => {
Expand Down Expand Up @@ -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]))

Expand Down Expand Up @@ -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" })
Expand Down Expand Up @@ -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)
}}
Expand Down
1 change: 0 additions & 1 deletion plugins/csv-import/src/utils/prepareImportPayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ export async function prepareImportPayload(opts: ProcessRecordsWithFieldMappingO

const allItemIdBySlug = new Map<string, Map<string, string>>()

// 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<string, string>()
Expand Down
Loading