diff --git a/README.md b/README.md index 430aad73..2ec24a21 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 715ffbd5..e6754e0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-spreadsheet-import", - "version": "4.3.0", + "version": "4.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-spreadsheet-import", - "version": "4.3.0", + "version": "4.4.1", "license": "MIT", "dependencies": { "@chakra-ui/react": "^2.8.1", diff --git a/package.json b/package.json index 3a8f5f31..df2afaba 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ReactSpreadsheetImport.tsx b/src/ReactSpreadsheetImport.tsx index 2de1d97a..dcbacee0 100644 --- a/src/ReactSpreadsheetImport.tsx +++ b/src/ReactSpreadsheetImport.tsx @@ -11,6 +11,7 @@ export const defaultTheme = themeOverrides export const defaultRSIProps: Partial> = { autoMapHeaders: true, + autoMapSelectValues: false, allowInvalidSubmit: true, autoMapDistance: 2, translations: translations, diff --git a/src/steps/MatchColumnsStep/MatchColumnsStep.tsx b/src/steps/MatchColumnsStep/MatchColumnsStep.tsx index 5a8bc2cc..9e5399bb 100644 --- a/src/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/src/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -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 = { data: RawData[] headerValues: RawData - onContinue: (data: any[], rawData: RawData[], columns: Columns) => void + onContinue: (data: any[], rawData: RawData[], columns: Columns, fields: Fields) => void onBack?: () => void } @@ -62,6 +63,7 @@ export type Column = | MatchedSelectOptionsColumn export type Columns = Column[] +export type HeaderCustomFieldsMap = Record[]> export const MatchColumnsStep = ({ data, @@ -71,23 +73,29 @@ export const MatchColumnsStep = ({ }: MatchColumnsProps) => { const toast = useToast() const dataExample = data.slice(0, 2) - const { fields, autoMapHeaders, autoMapDistance, translations } = useRsi() + const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations, customFieldsHook } = useRsi() const [isLoading, setIsLoading] = useState(false) const [columns, setColumns] = useState>( // 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 + 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 const existingFieldIndex = columns.findIndex((column) => "value" in column && column.value === field.key) setColumns( columns.map>((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", @@ -105,6 +113,8 @@ export const MatchColumnsStep = ({ ) }, [ + headerCustomFieldsMap, + autoMapSelectValues, columns, data, fields, @@ -145,24 +155,30 @@ export const MatchColumnsStep = ({ setShowUnmatchedFieldsAlert(true) } else { setIsLoading(true) - await onContinue(normalizeTableData(columns, data, fields), data, columns) + const mergedFields = mergeCustomFields(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(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 + setColumns(getMatchedColumns(columns, mergedFields, data, autoMapDistance, autoMapSelectValues)) + } + }, // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + [], + ) return ( <> @@ -185,7 +201,14 @@ export const MatchColumnsStep = ({ entries={dataExample.map((row) => row[column.index])} /> )} - templateColumn={(column) => } + templateColumn={(column) => ( + + )} /> ) diff --git a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx index 6fbebd30..0ba30b2d 100644 --- a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx +++ b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx @@ -11,18 +11,19 @@ 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 = (fields: Fields, column: Column, 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})` } @@ -30,18 +31,29 @@ type TemplateColumnProps = { onChange: (val: T, index: number) => void onSubChange: (val: T, index: number, option: string) => void column: Column + headerCustomFieldsMap: HeaderCustomFieldsMap } -export const TemplateColumn = ({ column, onChange, onSubChange }: TemplateColumnProps) => { - const { translations, fields } = useRsi() +export const TemplateColumn = ({ + column, + onChange, + onSubChange, + headerCustomFieldsMap, +}: TemplateColumnProps) => { + const { translations, fields: originalFields } = useRsi() const styles = useStyleConfig("MatchColumnsStep") as Styles + const customFields = selectColumnCustomFields(column, headerCustomFieldsMap) + const fields = [...originalFields, ...customFields] as Fields 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 ( diff --git a/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx b/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx index 7de9751b..ec1a3ea5 100644 --- a/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx +++ b/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx @@ -180,6 +180,192 @@ describe("Match Columns automatic matching", () => { expect(onContinue.mock.calls[0][0]).toEqual(result) }) + test("AutoMatches select values on mount", async () => { + const header = ["first name", "count", "Email"] + const OPTION_RESULT_ONE = "John" + const OPTION_RESULT_ONE_VALUE = "1" + const OPTION_RESULT_TWO = "Dane" + const OPTION_RESULT_TWO_VALUE = "2" + const OPTION_RESULT_THREE = "Kane" + const data = [ + // match by option label + [OPTION_RESULT_ONE, "123", "j@j.com"], + // match by option value + [OPTION_RESULT_TWO_VALUE, "333", "dane@bane.com"], + // do not match + [OPTION_RESULT_THREE, "534", "kane@linch.com"], + ] + const options = [ + { label: OPTION_RESULT_ONE, value: OPTION_RESULT_ONE_VALUE }, + { label: OPTION_RESULT_TWO, value: OPTION_RESULT_TWO_VALUE }, + ] + // finds only names with automatic matching + const result = [{ name: OPTION_RESULT_ONE_VALUE }, { name: OPTION_RESULT_TWO_VALUE }, { name: undefined }] + + const alternativeFields = [ + { + label: "Name", + key: "name", + alternateMatches: ["first name"], + fieldType: { + type: "select", + options, + }, + example: "Stephanie", + }, + ] as const + + const onContinue = jest.fn() + render( + + {}}> + + + , + ) + + expect(screen.getByText(/1 Unmatched/)).toBeInTheDocument() + + const nextButton = screen.getByRole("button", { + name: "Next", + }) + + await userEvent.click(nextButton) + + await waitFor(() => { + expect(onContinue).toBeCalled() + }) + expect(onContinue.mock.calls[0][0]).toEqual(result) + }) + + test("Does not auto match select values when autoMapSelectValues:false", async () => { + const header = ["first name", "count", "Email"] + const OPTION_RESULT_ONE = "John" + const OPTION_RESULT_ONE_VALUE = "1" + const OPTION_RESULT_TWO = "Dane" + const OPTION_RESULT_TWO_VALUE = "2" + const OPTION_RESULT_THREE = "Kane" + const data = [ + // match by option label + [OPTION_RESULT_ONE, "123", "j@j.com"], + // match by option value + [OPTION_RESULT_TWO_VALUE, "333", "dane@bane.com"], + // do not match + [OPTION_RESULT_THREE, "534", "kane@linch.com"], + ] + const options = [ + { label: OPTION_RESULT_ONE, value: OPTION_RESULT_ONE_VALUE }, + { label: OPTION_RESULT_TWO, value: OPTION_RESULT_TWO_VALUE }, + ] + const result = [{ name: undefined }, { name: undefined }, { name: undefined }] + + const alternativeFields = [ + { + label: "Name", + key: "name", + alternateMatches: ["first name"], + fieldType: { + type: "select", + options, + }, + example: "Stephanie", + }, + ] as const + + const onContinue = jest.fn() + render( + + {}}> + + + , + ) + + expect(screen.getByText(/3 Unmatched/)).toBeInTheDocument() + + const nextButton = screen.getByRole("button", { + name: "Next", + }) + + await userEvent.click(nextButton) + + await waitFor(() => { + expect(onContinue).toBeCalled() + }) + expect(onContinue.mock.calls[0][0]).toEqual(result) + }) + + test("AutoMatches select values on select", async () => { + const header = ["first name", "count", "Email"] + const OPTION_RESULT_ONE = "John" + const OPTION_RESULT_ONE_VALUE = "1" + const OPTION_RESULT_TWO = "Dane" + const OPTION_RESULT_TWO_VALUE = "2" + const OPTION_RESULT_THREE = "Kane" + const data = [ + // match by option label + [OPTION_RESULT_ONE, "123", "j@j.com"], + // match by option value + [OPTION_RESULT_TWO_VALUE, "333", "dane@bane.com"], + // do not match + [OPTION_RESULT_THREE, "534", "kane@linch.com"], + ] + const options = [ + { label: OPTION_RESULT_ONE, value: OPTION_RESULT_ONE_VALUE }, + { label: OPTION_RESULT_TWO, value: OPTION_RESULT_TWO_VALUE }, + ] + // finds only names with automatic matching + const result = [{ name: OPTION_RESULT_ONE_VALUE }, { name: OPTION_RESULT_TWO_VALUE }, { name: undefined }] + + const alternativeFields = [ + { + label: "Name", + key: "name", + fieldType: { + type: "select", + options, + }, + example: "Stephanie", + }, + ] as const + + const onContinue = jest.fn() + render( + + {}}> + +
+ + , + ) + + await selectEvent.select(screen.getByLabelText(header[0]), alternativeFields[0].label, { + container: document.getElementById(SELECT_DROPDOWN_ID)!, + }) + + expect(screen.getByText(/1 Unmatched/)).toBeInTheDocument() + + const nextButton = screen.getByRole("button", { + name: "Next", + }) + + await userEvent.click(nextButton) + + await waitFor(() => { + expect(onContinue).toBeCalled() + }) + expect(onContinue.mock.calls[0][0]).toEqual(result) + }) + test("Boolean-like values are returned as Booleans", async () => { const header = ["namezz", "is_cool", "Email"] const data = [ diff --git a/src/steps/MatchColumnsStep/utils/customFields.ts b/src/steps/MatchColumnsStep/utils/customFields.ts new file mode 100644 index 00000000..1da0bb7f --- /dev/null +++ b/src/steps/MatchColumnsStep/utils/customFields.ts @@ -0,0 +1,31 @@ +import { Column, Columns, HeaderCustomFieldsMap } from "../MatchColumnsStep" +import { CustomFieldsHook, Field, Fields } from "../../../types" + +export const createHeaderCustomFieldsMap = (columns: Column[], customFieldsHook?: CustomFieldsHook) => { + const result: HeaderCustomFieldsMap = {} + if (!customFieldsHook) return result + for (const column of columns) { + result[column.header as string] = customFieldsHook(column) + } + return result +} + +export const mergeCustomFields = ( + columns: Columns, + fields: Fields, + headerCustomFieldsMap: HeaderCustomFieldsMap, +) => { + const customFields: Field[] = [] + for (const column of columns) { + if (!("value" in column)) continue + const columnCustomField = selectColumnCustomFields(column, headerCustomFieldsMap) + const customField = columnCustomField.find((field) => field.key === column.value) + if (!customField) continue + customFields.push(customField) + } + return [...fields, ...customFields] as Fields +} + +export const selectColumnCustomFields = (column: Column, headerCustomFieldsMap: HeaderCustomFieldsMap) => { + return headerCustomFieldsMap[column.header as string] ?? [] +} diff --git a/src/steps/MatchColumnsStep/utils/getMatchedColumns.ts b/src/steps/MatchColumnsStep/utils/getMatchedColumns.ts index fc605ce8..4fdea0c8 100644 --- a/src/steps/MatchColumnsStep/utils/getMatchedColumns.ts +++ b/src/steps/MatchColumnsStep/utils/getMatchedColumns.ts @@ -10,6 +10,7 @@ export const getMatchedColumns = ( fields: Fields, data: MatchColumnsProps["data"], autoMapDistance: number, + autoMapSelectValues?: boolean, ) => columns.reduce[]>((arr, column) => { const autoMatch = findMatch(column.header, fields, autoMapDistance) @@ -21,7 +22,7 @@ export const getMatchedColumns = ( return lavenstein(duplicate.value, duplicate.header) < lavenstein(autoMatch, column.header) ? [ ...arr.slice(0, duplicateIndex), - setColumn(arr[duplicateIndex], field, data), + setColumn(arr[duplicateIndex], field, data, autoMapSelectValues), ...arr.slice(duplicateIndex + 1), setColumn(column), ] @@ -29,10 +30,10 @@ export const getMatchedColumns = ( ...arr.slice(0, duplicateIndex), setColumn(arr[duplicateIndex]), ...arr.slice(duplicateIndex + 1), - setColumn(column, field, data), + setColumn(column, field, data, autoMapSelectValues), ] } else { - return [...arr, setColumn(column, field, data)] + return [...arr, setColumn(column, field, data, autoMapSelectValues)] } } else { return [...arr, column] diff --git a/src/steps/MatchColumnsStep/utils/setColumn.ts b/src/steps/MatchColumnsStep/utils/setColumn.ts index 6bba5009..6c7a811c 100644 --- a/src/steps/MatchColumnsStep/utils/setColumn.ts +++ b/src/steps/MatchColumnsStep/utils/setColumn.ts @@ -1,19 +1,32 @@ import type { Field } from "../../../types" -import { Column, ColumnType, MatchColumnsProps } from "../MatchColumnsStep" +import { Column, ColumnType, MatchColumnsProps, MatchedOptions } from "../MatchColumnsStep" import { uniqueEntries } from "./uniqueEntries" export const setColumn = ( oldColumn: Column, field?: Field, data?: MatchColumnsProps["data"], + autoMapSelectValues?: boolean, ): Column => { switch (field?.fieldType.type) { case "select": + const fieldOptions = field.fieldType.options + const uniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions[] + const matchedOptions = autoMapSelectValues + ? uniqueData.map((record) => { + const value = fieldOptions.find( + (fieldOption) => fieldOption.value === record.entry || fieldOption.label === record.entry, + )?.value + return value ? ({ ...record, value } as MatchedOptions) : (record as MatchedOptions) + }) + : uniqueData + const allMatched = matchedOptions.filter((o) => o.value).length == uniqueData?.length + return { ...oldColumn, - type: ColumnType.matchedSelect, + type: allMatched ? ColumnType.matchedSelectOptions : ColumnType.matchedSelect, value: field.key, - matchedOptions: uniqueEntries(data || [], oldColumn.index), + matchedOptions, } case "checkbox": return { index: oldColumn.index, type: ColumnType.matchedCheckbox, value: field.key, header: oldColumn.header } diff --git a/src/steps/UploadFlow.tsx b/src/steps/UploadFlow.tsx index b70fc5dc..432e61f4 100644 --- a/src/steps/UploadFlow.tsx +++ b/src/steps/UploadFlow.tsx @@ -10,7 +10,7 @@ import { addErrorsAndRunHooks } from "./ValidationStep/utils/dataMutations" import { MatchColumnsStep } from "./MatchColumnsStep/MatchColumnsStep" import { exceedsMaxRecords } from "../utils/exceedsMaxRecords" import { useRsi } from "../hooks/useRsi" -import type { RawData } from "../types" +import type { Fields, RawData } from "../types" export enum StepType { upload = "upload", @@ -39,6 +39,8 @@ export type StepState = | { type: StepType.validateData data: any[] + // RsiProps.fields + custom fields + fields?: Fields } interface Props { @@ -147,13 +149,14 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => { { + onContinue={async (values, rawData, columns, fields) => { try { const data = await matchColumnsStepHook(values, rawData, columns) const dataWithMeta = await addErrorsAndRunHooks(data, fields, rowHook, tableHook) onNext({ type: StepType.validateData, data: dataWithMeta, + fields, }) } catch (e) { errorToast((e as Error).message) @@ -163,7 +166,14 @@ export const UploadFlow = ({ state, onNext, onBack }: Props) => { /> ) case StepType.validateData: - return + return ( + } + onBack={onBack} + /> + ) default: return } diff --git a/src/steps/ValidationStep/ValidationStep.tsx b/src/steps/ValidationStep/ValidationStep.tsx index b6ae8ed9..60e4a41f 100644 --- a/src/steps/ValidationStep/ValidationStep.tsx +++ b/src/steps/ValidationStep/ValidationStep.tsx @@ -7,18 +7,21 @@ import { addErrorsAndRunHooks } from "./utils/dataMutations" import { generateColumns } from "./components/columns" import { Table } from "../../components/Table" import { SubmitDataAlert } from "../../components/Alerts/SubmitDataAlert" -import type { Data } from "../../types" +import type { Data, Fields } from "../../types" import type { themeOverrides } from "../../theme" import type { RowsChangeData } from "react-data-grid" type Props = { + fields?: Fields initialData: (Data & Meta)[] file: File onBack?: () => void } -export const ValidationStep = ({ initialData, file, onBack }: Props) => { - const { translations, fields, onClose, onSubmit, rowHook, tableHook } = useRsi() +export const ValidationStep = ({ initialData, file, fields: mergedFields, onBack }: Props) => { + const { translations, onClose, onSubmit, rowHook, tableHook, fields: originalFields } = useRsi() + // override original fields with fields from props + const fields = mergedFields ?? originalFields const styles = useStyleConfig( "ValidationStep", ) as (typeof themeOverrides)["components"]["ValidationStep"]["baseStyle"] diff --git a/src/stories/mockRsiValues.ts b/src/stories/mockRsiValues.ts index 6bbb200b..dc1d9af9 100644 --- a/src/stories/mockRsiValues.ts +++ b/src/stories/mockRsiValues.ts @@ -92,6 +92,32 @@ export const mockRsiValues = mockComponentBehaviourForTypes({ }, isOpen: true, onClose: () => {}, + customFieldsHook: (c) => { + return [ + { + dropDownLabel: `(+)CF boolean`, + // user can convert csv unknown header into acceptable for backend header + // for example: (original)"MyFancyHeader" -> (result csv)"CF:boolean:MyFancyHeader" + // as result backend can understand that this is custom field with unknown key + // so it should be stored in custom fields collection + key: `CF:boolean:${c.header}`, + label: `${c.header} - label`, + fieldType: { + type: "checkbox", + }, + }, + { + dropDownLabel: `(+)CF string`, + key: `${c.header} - key string`, + label: `${c.header} - label`, + fieldType: { + type: "input", + }, + // ability to automatically map custom fields + alternateMatches: c.header.includes("custom") ? [c.header] : undefined, + }, + ] + }, // uploadStepHook: async (data) => { // await new Promise((resolve) => { // setTimeout(() => resolve(data), 4000) diff --git a/src/theme.ts b/src/theme.ts index 3226ff5b..8c16b26a 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -340,8 +340,8 @@ export const themeOverrides = { }, dialog: { outline: "unset", - minH: "calc(100vh - 4rem)", - maxW: "calc(100vw - 4rem)", + minH: "calc(var(--chakra-vh) - 4rem)", + maxW: "calc(var(--chakra-vw) - 4rem)", my: "2rem", borderRadius: "3xl", overflow: "hidden", @@ -357,6 +357,17 @@ export const themeOverrides = { }, styles: { global: { + // supporting older browsers but avoiding fill-available CSS as it doesn't work https://github.com/chakra-ui/chakra-ui/blob/073bbcd21a9caa830d71b61d6302f47aaa5c154d/packages/components/css-reset/src/css-reset.tsx#L5 + ":root": { + "--chakra-vh": "100vh", + "--chakra-vw": "100vw", + }, + "@supports (height: 100dvh) and (width: 100dvw) ": { + ":root": { + "--chakra-vh": "100dvh", + "--chakra-vw": "100dvw", + }, + }, ".rdg": { contain: "size layout style paint", borderRadius: "lg", diff --git a/src/types.ts b/src/types.ts index 931dde83..9f6ee55e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,7 @@ import type { Meta } from "./steps/ValidationStep/types" import type { DeepReadonly } from "ts-essentials" import type { TranslationsRSIProps } from "./translationsRSIProps" -import type { Columns } from "./steps/MatchColumnsStep/MatchColumnsStep" +import type { Columns, Column } from "./steps/MatchColumnsStep/MatchColumnsStep" import type { StepState } from "./steps/UploadFlow" export type RsiProps = { @@ -11,6 +11,9 @@ export type RsiProps = { onClose: () => void // Field description for requested data fields: Fields + // Runs on Match Columns step for each column when their state did change. + // Used to define custom fields with keys that are can be generated only from csv column header. + customFieldsHook?: CustomFieldsHook // Runs after file upload step, receives and returns raw sheet data uploadStepHook?: (data: RawData[]) => Promise // Runs after header selection step, receives and returns raw sheet data @@ -37,6 +40,8 @@ export type RsiProps = { 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 autoMapDistance?: number // Initial Step state to be rendered on load @@ -59,6 +64,8 @@ export type Fields = DeepReadonly[]> export type Field = { // UI-facing field label label: string + // UI-facing label for dropdown option + dropDownLabel?: string // Field's unique identifier key: T // UI-facing additional information displayed via tooltip and ? icon @@ -128,6 +135,7 @@ export type TableHook = ( table: Data[], addError: (rowIndex: number, fieldKey: T, error: Info) => void, ) => Data[] | Promise[]> +export type CustomFieldsHook = (column: Column) => Field[] export type ErrorLevel = "info" | "warning" | "error"