Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ Common date-time formats can be viewed [here](https://docs.sheetjs.com/docs/csf/
maxFileSize?: number
// Automatically map imported headers to specified fields if possible. Default: true
autoMapHeaders?: boolean
// When field type is "select", automatically match values if possible. Default: false
autoMapSelectValues?: boolean
// Headers matching accuracy: 1 for strict and up for more flexible matching. Default: 2
autoMapDistance?: number
// Enable navigation in stepper component and show back button. Default: false
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-spreadsheet-import",
"version": "4.3.0",
"version": "4.4.1",
"description": "React spreadsheet import for xlsx and csv files with column matching and validation",
"main": "./dist-commonjs/index.js",
"module": "./dist/index.js",
Expand Down
1 change: 1 addition & 0 deletions src/ReactSpreadsheetImport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const defaultTheme = themeOverrides

export const defaultRSIProps: Partial<RsiProps<any>> = {
autoMapHeaders: true,
autoMapSelectValues: false,
allowInvalidSubmit: true,
autoMapDistance: 2,
translations: translations,
Expand Down
55 changes: 39 additions & 16 deletions src/steps/MatchColumnsStep/MatchColumnsStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import { setColumn } from "./utils/setColumn"
import { setIgnoreColumn } from "./utils/setIgnoreColumn"
import { setSubColumn } from "./utils/setSubColumn"
import { normalizeTableData } from "./utils/normalizeTableData"
import type { Field, RawData } from "../../types"
import type { Field, Fields, RawData } from "../../types"
import { getMatchedColumns } from "./utils/getMatchedColumns"
import { UnmatchedFieldsAlert } from "../../components/Alerts/UnmatchedFieldsAlert"
import { findUnmatchedRequiredFields } from "./utils/findUnmatchedRequiredFields"
import { createHeaderCustomFieldsMap, mergeCustomFields, selectColumnCustomFields } from "./utils/customFields"

export type MatchColumnsProps<T extends string> = {
data: RawData[]
headerValues: RawData
onContinue: (data: any[], rawData: RawData[], columns: Columns<T>) => void
onContinue: (data: any[], rawData: RawData[], columns: Columns<T>, fields: Fields<T>) => void
onBack?: () => void
}

Expand Down Expand Up @@ -62,6 +63,7 @@ export type Column<T extends string> =
| MatchedSelectOptionsColumn<T>

export type Columns<T extends string> = Column<T>[]
export type HeaderCustomFieldsMap = Record<string, Field<string>[]>

export const MatchColumnsStep = <T extends string>({
data,
Expand All @@ -71,23 +73,29 @@ export const MatchColumnsStep = <T extends string>({
}: MatchColumnsProps<T>) => {
const toast = useToast()
const dataExample = data.slice(0, 2)
const { fields, autoMapHeaders, autoMapDistance, translations } = useRsi<T>()
const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations, customFieldsHook } = useRsi<T>()
const [isLoading, setIsLoading] = useState(false)
const [columns, setColumns] = useState<Columns<T>>(
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
([...headerValues] as string[]).map((value, index) => ({ type: ColumnType.empty, index, header: value ?? "" })),
)

const headerCustomFieldsMap = useMemo(
() => createHeaderCustomFieldsMap(columns, customFieldsHook),
[columns, customFieldsHook],
)
const [showUnmatchedFieldsAlert, setShowUnmatchedFieldsAlert] = useState(false)

const onChange = useCallback(
(value: T, columnIndex: number) => {
const field = fields.find((field) => field.key === value) as unknown as Field<T>
const customFields = selectColumnCustomFields(columns[columnIndex], headerCustomFieldsMap)
const customField = customFields.find((field) => field.key === value)
const field = (customField || fields.find((field) => field.key === value)) as Field<T>
const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key)
setColumns(
columns.map<Column<T>>((column, index) => {
columnIndex === index ? setColumn(column, field, data) : column
if (columnIndex === index) {
return setColumn(column, field, data)
return setColumn(column, field, data, autoMapSelectValues)
} else if (index === existingFieldIndex) {
toast({
status: "warning",
Expand All @@ -105,6 +113,8 @@ export const MatchColumnsStep = <T extends string>({
)
},
[
headerCustomFieldsMap,
autoMapSelectValues,
columns,
data,
fields,
Expand Down Expand Up @@ -145,24 +155,30 @@ export const MatchColumnsStep = <T extends string>({
setShowUnmatchedFieldsAlert(true)
} else {
setIsLoading(true)
await onContinue(normalizeTableData(columns, data, fields), data, columns)
const mergedFields = mergeCustomFields<T>(columns, fields, headerCustomFieldsMap)
await onContinue(normalizeTableData(columns, data, mergedFields), data, columns, mergedFields)
setIsLoading(false)
}
}, [unmatchedRequiredFields.length, onContinue, columns, data, fields])
}, [unmatchedRequiredFields.length, onContinue, columns, data, fields, headerCustomFieldsMap])

const handleAlertOnContinue = useCallback(async () => {
setShowUnmatchedFieldsAlert(false)
setIsLoading(true)
await onContinue(normalizeTableData(columns, data, fields), data, columns)
const mergedFields = mergeCustomFields<T>(columns, fields, headerCustomFieldsMap)
await onContinue(normalizeTableData(columns, data, mergedFields), data, columns, mergedFields)
setIsLoading(false)
}, [onContinue, columns, data, fields])
}, [onContinue, columns, data, fields, headerCustomFieldsMap])

useEffect(() => {
if (autoMapHeaders) {
setColumns(getMatchedColumns(columns, fields, data, autoMapDistance))
}
useEffect(
() => {
if (autoMapHeaders) {
const mergedFields = [...fields, ...Object.values(headerCustomFieldsMap).flat()] as Fields<T>
setColumns(getMatchedColumns(columns, mergedFields, data, autoMapDistance, autoMapSelectValues))
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
[],
)

return (
<>
Expand All @@ -185,7 +201,14 @@ export const MatchColumnsStep = <T extends string>({
entries={dataExample.map((row) => row[column.index])}
/>
)}
templateColumn={(column) => <TemplateColumn column={column} onChange={onChange} onSubChange={onSubChange} />}
templateColumn={(column) => (
<TemplateColumn
column={column}
onChange={onChange}
onSubChange={onSubChange}
headerCustomFieldsMap={headerCustomFieldsMap}
/>
)}
/>
</>
)
Expand Down
22 changes: 17 additions & 5 deletions src/steps/MatchColumnsStep/components/TemplateColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,49 @@ import {
} from "@chakra-ui/react"
import { useRsi } from "../../../hooks/useRsi"
import type { Column } from "../MatchColumnsStep"
import { ColumnType } from "../MatchColumnsStep"
import { ColumnType, HeaderCustomFieldsMap } from "../MatchColumnsStep"
import { MatchIcon } from "./MatchIcon"
import type { Fields } from "../../../types"
import type { Translations } from "../../../translationsRSIProps"
import { MatchColumnSelect } from "../../../components/Selects/MatchColumnSelect"
import { SubMatchingSelect } from "./SubMatchingSelect"
import type { Styles } from "./ColumnGrid"
import { selectColumnCustomFields } from "../utils/customFields"

const getAccordionTitle = <T extends string>(fields: Fields<T>, column: Column<T>, translations: Translations) => {
const fieldLabel = fields.find((field) => "value" in column && field.key === column.value)!.label
return `${translations.matchColumnsStep.matchDropdownTitle} ${fieldLabel} (${
"matchedOptions" in column && column.matchedOptions.length
"matchedOptions" in column && column.matchedOptions.filter((option) => !option.value).length
} ${translations.matchColumnsStep.unmatched})`
}

type TemplateColumnProps<T extends string> = {
onChange: (val: T, index: number) => void
onSubChange: (val: T, index: number, option: string) => void
column: Column<T>
headerCustomFieldsMap: HeaderCustomFieldsMap
}

export const TemplateColumn = <T extends string>({ column, onChange, onSubChange }: TemplateColumnProps<T>) => {
const { translations, fields } = useRsi<T>()
export const TemplateColumn = <T extends string>({
column,
onChange,
onSubChange,
headerCustomFieldsMap,
}: TemplateColumnProps<T>) => {
const { translations, fields: originalFields } = useRsi<T>()
const styles = useStyleConfig("MatchColumnsStep") as Styles
const customFields = selectColumnCustomFields(column, headerCustomFieldsMap)
const fields = [...originalFields, ...customFields] as Fields<T>
const isIgnored = column.type === ColumnType.ignored
const isChecked =
column.type === ColumnType.matched ||
column.type === ColumnType.matchedCheckbox ||
column.type === ColumnType.matchedSelectOptions
const isSelect = "matchedOptions" in column
const selectOptions = fields.map(({ label, key }) => ({ value: key, label }))
const selectOptions = fields.map(({ key, label, dropDownLabel }) => ({
value: key,
label: dropDownLabel ?? label,
}))
const selectValue = selectOptions.find(({ value }) => "value" in column && column.value === value)

return (
Expand Down
Loading