From d642692319c85d420c9d400eea797f1989961055 Mon Sep 17 00:00:00 2001 From: Yo1L <15970286+Yo1L@users.noreply.github.com> Date: Mon, 12 Dec 2022 17:40:24 +0100 Subject: [PATCH 01/16] Auto Select values (Column with select fields) Simple solution to select the right value for select and tick the column header if all values matched :) --- src/steps/MatchColumnsStep/utils/setColumn.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/steps/MatchColumnsStep/utils/setColumn.ts b/src/steps/MatchColumnsStep/utils/setColumn.ts index 6bba5009..3bed7d76 100644 --- a/src/steps/MatchColumnsStep/utils/setColumn.ts +++ b/src/steps/MatchColumnsStep/utils/setColumn.ts @@ -9,11 +9,17 @@ export const setColumn = ( ): Column => { switch (field?.fieldType.type) { case "select": + const matchedOptions = uniqueEntries(data || [], oldColumn.index)?.map(option => { + const value = options.find(o => o.value == option.value || o.label == option.entry)?.value + return value ? {...option, value} as MatchedOptions : option as MatchedOptions + }) + const allMatched = matchedOptions.filter(o => o.value).length == options.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 } From 07c15581b083efa62c864e356a0b76d9e315a1cf Mon Sep 17 00:00:00 2001 From: Karolis Masiulis Date: Mon, 6 Nov 2023 14:56:37 +0200 Subject: [PATCH 02/16] Update src/steps/MatchColumnsStep/utils/setColumn.ts Co-authored-by: Harry Bragg --- src/steps/MatchColumnsStep/utils/setColumn.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/steps/MatchColumnsStep/utils/setColumn.ts b/src/steps/MatchColumnsStep/utils/setColumn.ts index 3bed7d76..267c3fbf 100644 --- a/src/steps/MatchColumnsStep/utils/setColumn.ts +++ b/src/steps/MatchColumnsStep/utils/setColumn.ts @@ -9,11 +9,12 @@ export const setColumn = ( ): Column => { switch (field?.fieldType.type) { case "select": - const matchedOptions = uniqueEntries(data || [], oldColumn.index)?.map(option => { - const value = options.find(o => o.value == option.value || o.label == option.entry)?.value + const uniqueData = uniqueEntries(data || [], oldColumn.index) + const matchedOptions = uniqueData?.map(option => { + const value = field.fieldType.options.find(o => o.value == option.value || o.label == option.entry)?.value return value ? {...option, value} as MatchedOptions : option as MatchedOptions }) - const allMatched = matchedOptions.filter(o => o.value).length == options.length + const allMatched = matchedOptions.filter(o => o.value).length == uniqueData?.length return { ...oldColumn, From d3dc5c307ae2c3c8287343246f42cccd5b71e4a8 Mon Sep 17 00:00:00 2001 From: masiulis Date: Tue, 7 Nov 2023 15:43:03 +0200 Subject: [PATCH 03/16] Add autoMapSelectValues flag and automatic matching. --- README.md | 2 ++ src/ReactSpreadsheetImport.tsx | 1 + .../MatchColumnsStep/MatchColumnsStep.tsx | 6 +++--- .../components/TemplateColumn.tsx | 2 +- .../utils/getMatchedColumns.ts | 7 ++++--- src/steps/MatchColumnsStep/utils/setColumn.ts | 20 +++++++++++-------- src/types.ts | 2 ++ 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 467b986e..6b87cd68 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: true + autoMapSelectValues?: boolean // Headers matching accuracy: 1 for strict and up for more flexible matching. Default: 2 autoMapDistance?: number ``` 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 23413215..c6ef0753 100644 --- a/src/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/src/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -65,7 +65,7 @@ export type Columns = Column[] export const MatchColumnsStep = ({ data, headerValues, onContinue }: MatchColumnsProps) => { const toast = useToast() const dataExample = data.slice(0, 2) - const { fields, autoMapHeaders, autoMapDistance, translations } = useRsi() + const { fields, autoMapHeaders, autoMapSelectValues, autoMapDistance, translations } = useRsi() const [isLoading, setIsLoading] = useState(false) const [columns, setColumns] = useState>( // Do not remove spread, it indexes empty array elements, otherwise map() skips over them @@ -81,7 +81,7 @@ export const MatchColumnsStep = ({ data, headerValues, onConti 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", @@ -153,7 +153,7 @@ export const MatchColumnsStep = ({ data, headerValues, onConti useEffect(() => { if (autoMapHeaders) { - setColumns(getMatchedColumns(columns, fields, data, autoMapDistance)) + setColumns(getMatchedColumns(columns, fields, data, autoMapDistance, autoMapSelectValues)) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx index 6fbebd30..4cc1a0d4 100644 --- a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx +++ b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx @@ -22,7 +22,7 @@ import type { Styles } from "./ColumnGrid" 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})` } 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 267c3fbf..08fd063e 100644 --- a/src/steps/MatchColumnsStep/utils/setColumn.ts +++ b/src/steps/MatchColumnsStep/utils/setColumn.ts @@ -1,26 +1,30 @@ 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 uniqueData = uniqueEntries(data || [], oldColumn.index) - const matchedOptions = uniqueData?.map(option => { - const value = field.fieldType.options.find(o => o.value == option.value || o.label == option.entry)?.value - return value ? {...option, value} as MatchedOptions : option as MatchedOptions - }) - const allMatched = matchedOptions.filter(o => o.value).length == uniqueData?.length + const options = field.fieldType.options + const uniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions[] + const matchedOptions = autoMapSelectValues + ? uniqueData.map((option) => { + const value = options.find((o) => o.value == option.value || o.label == option.entry)?.value + return value ? ({ ...option, value } as MatchedOptions) : (option as MatchedOptions) + }) + : uniqueData + const allMatched = matchedOptions.filter((o) => o.value).length == uniqueData?.length return { ...oldColumn, type: allMatched ? ColumnType.matchedSelectOptions : ColumnType.matchedSelect, value: field.key, - matchedOptions + matchedOptions, } case "checkbox": return { index: oldColumn.index, type: ColumnType.matchedCheckbox, value: field.key, header: oldColumn.header } diff --git a/src/types.ts b/src/types.ts index ed3c77ba..9cc90b24 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,6 +35,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 From d6fa618b80743b07f32fa65d454415d77c895e2c Mon Sep 17 00:00:00 2001 From: masiulis Date: Tue, 7 Nov 2023 16:54:00 +0200 Subject: [PATCH 04/16] Add tests for automatching --- .../tests/MatchColumnsStep.test.tsx | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx b/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx index 7de9751b..6e603f94 100644 --- a/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx +++ b/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx @@ -180,6 +180,197 @@ 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 OPTION_RESULT_THREE_VALUE = "3" + const data = [ + [OPTION_RESULT_ONE, "123", "j@j.com"], + [OPTION_RESULT_TWO, "333", "dane@bane.com"], + [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 }, + { label: OPTION_RESULT_THREE, value: OPTION_RESULT_THREE_VALUE }, + ] + // finds only names with automatic matching + const result = [ + { name: OPTION_RESULT_ONE_VALUE }, + { name: OPTION_RESULT_TWO_VALUE }, + { name: OPTION_RESULT_THREE_VALUE }, + ] + + 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(/0 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 OPTION_RESULT_THREE_VALUE = "3" + const data = [ + [OPTION_RESULT_ONE, "123", "j@j.com"], + [OPTION_RESULT_TWO, "333", "dane@bane.com"], + [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 }, + { label: OPTION_RESULT_THREE, value: OPTION_RESULT_THREE_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 OPTION_RESULT_THREE_VALUE = "3" + const data = [ + [OPTION_RESULT_ONE, "123", "j@j.com"], + [OPTION_RESULT_TWO, "333", "dane@bane.com"], + [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 }, + { label: OPTION_RESULT_THREE, value: OPTION_RESULT_THREE_VALUE }, + ] + // finds only names with automatic matching + const result = [ + { name: OPTION_RESULT_ONE_VALUE }, + { name: OPTION_RESULT_TWO_VALUE }, + { name: OPTION_RESULT_THREE_VALUE }, + ] + + 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(/0 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 = [ From 4e1a12da9f61cc607746cb092fad678c59c0a8ab Mon Sep 17 00:00:00 2001 From: masiulis Date: Thu, 9 Nov 2023 01:17:26 +0200 Subject: [PATCH 05/16] Match select values by field option value or label --- .../tests/MatchColumnsStep.test.tsx | 37 ++++++++----------- src/steps/MatchColumnsStep/utils/setColumn.ts | 4 +- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx b/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx index 6e603f94..ec1a3ea5 100644 --- a/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx +++ b/src/steps/MatchColumnsStep/tests/MatchColumnsStep.test.tsx @@ -187,23 +187,20 @@ describe("Match Columns automatic matching", () => { const OPTION_RESULT_TWO = "Dane" const OPTION_RESULT_TWO_VALUE = "2" const OPTION_RESULT_THREE = "Kane" - const OPTION_RESULT_THREE_VALUE = "3" const data = [ + // match by option label [OPTION_RESULT_ONE, "123", "j@j.com"], - [OPTION_RESULT_TWO, "333", "dane@bane.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 }, - { label: OPTION_RESULT_THREE, value: OPTION_RESULT_THREE_VALUE }, ] // finds only names with automatic matching - const result = [ - { name: OPTION_RESULT_ONE_VALUE }, - { name: OPTION_RESULT_TWO_VALUE }, - { name: OPTION_RESULT_THREE_VALUE }, - ] + const result = [{ name: OPTION_RESULT_ONE_VALUE }, { name: OPTION_RESULT_TWO_VALUE }, { name: undefined }] const alternativeFields = [ { @@ -230,7 +227,7 @@ describe("Match Columns automatic matching", () => { , ) - expect(screen.getByText(/0 Unmatched/)).toBeInTheDocument() + expect(screen.getByText(/1 Unmatched/)).toBeInTheDocument() const nextButton = screen.getByRole("button", { name: "Next", @@ -251,16 +248,17 @@ describe("Match Columns automatic matching", () => { const OPTION_RESULT_TWO = "Dane" const OPTION_RESULT_TWO_VALUE = "2" const OPTION_RESULT_THREE = "Kane" - const OPTION_RESULT_THREE_VALUE = "3" const data = [ + // match by option label [OPTION_RESULT_ONE, "123", "j@j.com"], - [OPTION_RESULT_TWO, "333", "dane@bane.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 }, - { label: OPTION_RESULT_THREE, value: OPTION_RESULT_THREE_VALUE }, ] const result = [{ name: undefined }, { name: undefined }, { name: undefined }] @@ -310,23 +308,20 @@ describe("Match Columns automatic matching", () => { const OPTION_RESULT_TWO = "Dane" const OPTION_RESULT_TWO_VALUE = "2" const OPTION_RESULT_THREE = "Kane" - const OPTION_RESULT_THREE_VALUE = "3" const data = [ + // match by option label [OPTION_RESULT_ONE, "123", "j@j.com"], - [OPTION_RESULT_TWO, "333", "dane@bane.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 }, - { label: OPTION_RESULT_THREE, value: OPTION_RESULT_THREE_VALUE }, ] // finds only names with automatic matching - const result = [ - { name: OPTION_RESULT_ONE_VALUE }, - { name: OPTION_RESULT_TWO_VALUE }, - { name: OPTION_RESULT_THREE_VALUE }, - ] + const result = [{ name: OPTION_RESULT_ONE_VALUE }, { name: OPTION_RESULT_TWO_VALUE }, { name: undefined }] const alternativeFields = [ { @@ -357,7 +352,7 @@ describe("Match Columns automatic matching", () => { container: document.getElementById(SELECT_DROPDOWN_ID)!, }) - expect(screen.getByText(/0 Unmatched/)).toBeInTheDocument() + expect(screen.getByText(/1 Unmatched/)).toBeInTheDocument() const nextButton = screen.getByRole("button", { name: "Next", diff --git a/src/steps/MatchColumnsStep/utils/setColumn.ts b/src/steps/MatchColumnsStep/utils/setColumn.ts index 08fd063e..93f01a2a 100644 --- a/src/steps/MatchColumnsStep/utils/setColumn.ts +++ b/src/steps/MatchColumnsStep/utils/setColumn.ts @@ -14,7 +14,9 @@ export const setColumn = ( const uniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions[] const matchedOptions = autoMapSelectValues ? uniqueData.map((option) => { - const value = options.find((o) => o.value == option.value || o.label == option.entry)?.value + const value = options.find( + (fieldOption) => fieldOption.value === option.entry || fieldOption.label === option.entry, + )?.value return value ? ({ ...option, value } as MatchedOptions) : (option as MatchedOptions) }) : uniqueData From 73e6a1dc056777c1b3cd3299c42c1fcfd4f5c694 Mon Sep 17 00:00:00 2001 From: masiulis Date: Thu, 9 Nov 2023 11:30:00 +0200 Subject: [PATCH 06/16] Rename option to record --- src/steps/MatchColumnsStep/utils/setColumn.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/steps/MatchColumnsStep/utils/setColumn.ts b/src/steps/MatchColumnsStep/utils/setColumn.ts index 93f01a2a..6c7a811c 100644 --- a/src/steps/MatchColumnsStep/utils/setColumn.ts +++ b/src/steps/MatchColumnsStep/utils/setColumn.ts @@ -10,14 +10,14 @@ export const setColumn = ( ): Column => { switch (field?.fieldType.type) { case "select": - const options = field.fieldType.options + const fieldOptions = field.fieldType.options const uniqueData = uniqueEntries(data || [], oldColumn.index) as MatchedOptions[] const matchedOptions = autoMapSelectValues - ? uniqueData.map((option) => { - const value = options.find( - (fieldOption) => fieldOption.value === option.entry || fieldOption.label === option.entry, + ? uniqueData.map((record) => { + const value = fieldOptions.find( + (fieldOption) => fieldOption.value === record.entry || fieldOption.label === record.entry, )?.value - return value ? ({ ...option, value } as MatchedOptions) : (option as MatchedOptions) + return value ? ({ ...record, value } as MatchedOptions) : (record as MatchedOptions) }) : uniqueData const allMatched = matchedOptions.filter((o) => o.value).length == uniqueData?.length From dedf4041adad30043324d0f8b62a610336fc63de Mon Sep 17 00:00:00 2001 From: masiulis Date: Thu, 9 Nov 2023 13:44:58 +0200 Subject: [PATCH 07/16] Fix exhaustive deps --- src/steps/MatchColumnsStep/MatchColumnsStep.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/steps/MatchColumnsStep/MatchColumnsStep.tsx b/src/steps/MatchColumnsStep/MatchColumnsStep.tsx index c6ef0753..28a566f9 100644 --- a/src/steps/MatchColumnsStep/MatchColumnsStep.tsx +++ b/src/steps/MatchColumnsStep/MatchColumnsStep.tsx @@ -99,6 +99,7 @@ export const MatchColumnsStep = ({ data, headerValues, onConti ) }, [ + autoMapSelectValues, columns, data, fields, @@ -151,12 +152,15 @@ export const MatchColumnsStep = ({ data, headerValues, onConti setIsLoading(false) }, [onContinue, columns, data, fields]) - useEffect(() => { - if (autoMapHeaders) { - setColumns(getMatchedColumns(columns, fields, data, autoMapDistance, autoMapSelectValues)) - } + useEffect( + () => { + if (autoMapHeaders) { + setColumns(getMatchedColumns(columns, fields, data, autoMapDistance, autoMapSelectValues)) + } + }, // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + [], + ) return ( <> From 48249b9a4567231f3416f10f28a3004d2b8a4733 Mon Sep 17 00:00:00 2001 From: masiulis Date: Thu, 9 Nov 2023 14:11:41 +0200 Subject: [PATCH 08/16] 4.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 715ffbd5..9e99f32f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-spreadsheet-import", - "version": "4.3.0", + "version": "4.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-spreadsheet-import", - "version": "4.3.0", + "version": "4.4.0", "license": "MIT", "dependencies": { "@chakra-ui/react": "^2.8.1", diff --git a/package.json b/package.json index 3a8f5f31..cc43b73d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-spreadsheet-import", - "version": "4.3.0", + "version": "4.4.0", "description": "React spreadsheet import for xlsx and csv files with column matching and validation", "main": "./dist-commonjs/index.js", "module": "./dist/index.js", From 645dd4259b66b4b99b1a765129c453aa3c6abe7a Mon Sep 17 00:00:00 2001 From: Karolis Masiulis Date: Thu, 9 Nov 2023 14:13:45 +0200 Subject: [PATCH 09/16] Update README.md with correct default --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b87cd68..53ebf4f0 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ 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: true + // 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 From 87f7984036bc4b957086857690bd654ec5b67748 Mon Sep 17 00:00:00 2001 From: masiulis Date: Thu, 9 Nov 2023 16:20:15 +0200 Subject: [PATCH 10/16] Fix mobile viewport size calculation --- src/theme.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/theme.ts b/src/theme.ts index 8b41a44f..7cc14ed8 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -338,8 +338,8 @@ export const themeOverrides = { }, dialog: { outline: "unset", - minH: "calc(100vh - 4rem)", - maxW: "calc(100vw - 4rem)", + minH: "calc(100dvh - 4rem)", + maxW: "calc(100dvw - 4rem)", my: "2rem", borderRadius: "3xl", overflow: "hidden", From 226287955b34f046b3371cc208ed81dba918ddd8 Mon Sep 17 00:00:00 2001 From: masiulis Date: Thu, 9 Nov 2023 19:21:50 +0200 Subject: [PATCH 11/16] Use polyfilled version for older browsers --- src/theme.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/theme.ts b/src/theme.ts index 7cc14ed8..2dd67342 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -338,8 +338,8 @@ export const themeOverrides = { }, dialog: { outline: "unset", - minH: "calc(100dvh - 4rem)", - maxW: "calc(100dvw - 4rem)", + minH: "calc(var(--chakra-vh) - 4rem)", + maxW: "calc(100vw - 4rem)", my: "2rem", borderRadius: "3xl", overflow: "hidden", From 1a16a9164464d9106236f779ccfc2419f53d349a Mon Sep 17 00:00:00 2001 From: masiulis Date: Fri, 10 Nov 2023 11:41:12 +0200 Subject: [PATCH 12/16] Enforce vh or dvh as fill-available cannot be used in calc() --- src/theme.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/theme.ts b/src/theme.ts index 2dd67342..2835e901 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -339,7 +339,7 @@ export const themeOverrides = { dialog: { outline: "unset", minH: "calc(var(--chakra-vh) - 4rem)", - maxW: "calc(100vw - 4rem)", + maxW: "calc(var(--chakra-vw) - 4rem)", my: "2rem", borderRadius: "3xl", overflow: "hidden", @@ -355,6 +355,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", From 6a3168afd3d8344a118d11a2c8deb526d437fb21 Mon Sep 17 00:00:00 2001 From: masiulis Date: Fri, 10 Nov 2023 12:24:54 +0200 Subject: [PATCH 13/16] 4.4.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e99f32f..e6754e0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-spreadsheet-import", - "version": "4.4.0", + "version": "4.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-spreadsheet-import", - "version": "4.4.0", + "version": "4.4.1", "license": "MIT", "dependencies": { "@chakra-ui/react": "^2.8.1", diff --git a/package.json b/package.json index cc43b73d..df2afaba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-spreadsheet-import", - "version": "4.4.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", From f4d553d2e3b811b2dbe22d0b3614e1608c2b873a Mon Sep 17 00:00:00 2001 From: Artur Perepelytsia Date: Tue, 14 Nov 2023 14:27:27 +0200 Subject: [PATCH 14/16] feat(custom-fields-ugn-master): * add customFieldsHook into RsiProps: 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. * implement customFieldsHook logic * add dropDownLabel into Field - UI-facing label for dropdown option --- .../MatchColumnsStep/MatchColumnsStep.tsx | 41 ++++++++++++++----- .../MatchColumnsStep/components/MatchIcon.tsx | 6 ++- .../components/TemplateColumn.tsx | 23 ++++++++--- .../MatchColumnsStep/utils/customFields.ts | 30 ++++++++++++++ src/steps/UploadFlow.tsx | 15 +++++-- src/steps/ValidationStep/ValidationStep.tsx | 9 ++-- src/stories/mockRsiValues.ts | 21 ++++++++++ src/types.ts | 8 +++- 8 files changed, 128 insertions(+), 25 deletions(-) create mode 100644 src/steps/MatchColumnsStep/utils/customFields.ts diff --git a/src/steps/MatchColumnsStep/MatchColumnsStep.tsx b/src/steps/MatchColumnsStep/MatchColumnsStep.tsx index 28a566f9..e142a31f 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 } export enum ColumnType { @@ -61,25 +62,32 @@ export type Column = | MatchedSelectOptionsColumn export type Columns = Column[] +export type HeaderCustomFieldsMap = Record[]> export const MatchColumnsStep = ({ data, headerValues, onContinue }: MatchColumnsProps) => { const toast = useToast() const dataExample = data.slice(0, 2) - const { fields, autoMapHeaders, autoMapSelectValues, 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, autoMapSelectValues) } else if (index === existingFieldIndex) { @@ -99,6 +107,7 @@ export const MatchColumnsStep = ({ data, headerValues, onConti ) }, [ + headerCustomFieldsMap, autoMapSelectValues, columns, data, @@ -140,22 +149,25 @@ export const MatchColumnsStep = ({ data, headerValues, onConti 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, autoMapSelectValues)) + const mergedFields = [...fields, ...Object.values(headerCustomFieldsMap).flat()] as Fields + setColumns(getMatchedColumns(columns, mergedFields, data, autoMapDistance, autoMapSelectValues)) } }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -182,7 +194,14 @@ export const MatchColumnsStep = ({ data, headerValues, onConti entries={dataExample.map((row) => row[column.index])} /> )} - templateColumn={(column) => } + templateColumn={(column) => ( + + )} /> ) diff --git a/src/steps/MatchColumnsStep/components/MatchIcon.tsx b/src/steps/MatchColumnsStep/components/MatchIcon.tsx index cca299d4..aeee7536 100644 --- a/src/steps/MatchColumnsStep/components/MatchIcon.tsx +++ b/src/steps/MatchColumnsStep/components/MatchIcon.tsx @@ -1,7 +1,7 @@ import { chakra, useStyleConfig, Flex } from "@chakra-ui/react" import { dataAttr } from "@chakra-ui/utils" import { motion } from "framer-motion" -import { CgCheck } from "react-icons/cg" +import { CgAdd, CgCheck } from "react-icons/cg" const MotionFlex = motion(Flex) @@ -15,10 +15,12 @@ const animationConfig = { } type MatchIconProps = { isChecked: boolean + isCustom?: boolean } export const MatchIcon = (props: MatchIconProps) => { const style = useStyleConfig("MatchIcon", props) + const Icon = props.isCustom ? CgAdd : CgCheck return ( { > {props.isChecked && ( - + )} diff --git a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx index 4cc1a0d4..1c751619 100644 --- a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx +++ b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx @@ -11,13 +11,14 @@ 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 @@ -30,18 +31,30 @@ 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 isCustom = "value" in column && customFields.some((e) => e.key === column.value) 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 ( @@ -60,7 +73,7 @@ export const TemplateColumn = ({ column, onChange, onSubChange name={column.header} /> - + {isSelect && ( diff --git a/src/steps/MatchColumnsStep/utils/customFields.ts b/src/steps/MatchColumnsStep/utils/customFields.ts new file mode 100644 index 00000000..58cad0b8 --- /dev/null +++ b/src/steps/MatchColumnsStep/utils/customFields.ts @@ -0,0 +1,30 @@ +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 mergedFields = [...fields] as Field[] + for (const column of columns) { + if (!("value" in column)) continue + const customField = headerCustomFieldsMap[column.header as string]?.find((field) => field.key === column.value) + if (!customField) continue + mergedFields.splice(column.index, 0, customField) + } + return mergedFields as unknown as Fields +} + +export const selectColumnCustomFields = (column: Column, headerCustomFieldsMap: HeaderCustomFieldsMap) => { + return headerCustomFieldsMap[column.header as string] ?? [] +} diff --git a/src/steps/UploadFlow.tsx b/src/steps/UploadFlow.tsx index 567c8622..de87e679 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 { @@ -148,13 +150,14 @@ export const UploadFlow = ({ nextStep }: Props) => { { + onContinue={async (values, rawData, columns, fields) => { try { const data = await matchColumnsStepHook(values, rawData, columns) const dataWithMeta = await addErrorsAndRunHooks(data, fields, rowHook, tableHook) setState({ type: StepType.validateData, data: dataWithMeta, + fields, }) nextStep() } catch (e) { @@ -164,7 +167,13 @@ export const UploadFlow = ({ nextStep }: Props) => { /> ) case StepType.validateData: - return + return ( + } + /> + ) default: return } diff --git a/src/steps/ValidationStep/ValidationStep.tsx b/src/steps/ValidationStep/ValidationStep.tsx index 7bb4b0b1..08217a70 100644 --- a/src/steps/ValidationStep/ValidationStep.tsx +++ b/src/steps/ValidationStep/ValidationStep.tsx @@ -7,17 +7,20 @@ 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 } -export const ValidationStep = ({ initialData, file }: Props) => { - const { translations, fields, onClose, onSubmit, rowHook, tableHook } = useRsi() +export const ValidationStep = ({ initialData, file, fields: mergedFields }: 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..bc6c316b 100644 --- a/src/stories/mockRsiValues.ts +++ b/src/stories/mockRsiValues.ts @@ -92,6 +92,27 @@ export const mockRsiValues = mockComponentBehaviourForTypes({ }, isOpen: true, onClose: () => {}, + customFieldsHook: (c) => { + return [ + { + dropDownLabel: `(+)CF boolean`, + key: `${c.header} - key boolean`, + label: `${c.header} - label`, + fieldType: { + type: "checkbox", + }, + }, + { + dropDownLabel: `(+)CF string`, + key: `${c.header} - key string`, + label: `${c.header} - label`, + fieldType: { + type: "input", + }, + alternateMatches: c.header === "custom_f_F2" ? ["custom_f_F2"] : undefined, + }, + ] + }, // uploadStepHook: async (data) => { // await new Promise((resolve) => { // setTimeout(() => resolve(data), 4000) diff --git a/src/types.ts b/src/types.ts index 9cc90b24..c4ffa436 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 @@ -59,6 +62,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 +133,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" From b46b71642bfbef61ae9f327ad18882609a59d55e Mon Sep 17 00:00:00 2001 From: Artur Perepelytsia Date: Thu, 16 Nov 2023 10:41:05 +0200 Subject: [PATCH 15/16] feat/custom-fields-hive - revert MatchIcon changes - more generic example of custom fields - mergeCustomFields - add custom fields to end of fields array --- src/steps/MatchColumnsStep/components/MatchIcon.tsx | 6 ++---- src/steps/MatchColumnsStep/components/TemplateColumn.tsx | 3 +-- src/steps/MatchColumnsStep/utils/customFields.ts | 9 +++++---- src/stories/mockRsiValues.ts | 3 ++- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/steps/MatchColumnsStep/components/MatchIcon.tsx b/src/steps/MatchColumnsStep/components/MatchIcon.tsx index aeee7536..cca299d4 100644 --- a/src/steps/MatchColumnsStep/components/MatchIcon.tsx +++ b/src/steps/MatchColumnsStep/components/MatchIcon.tsx @@ -1,7 +1,7 @@ import { chakra, useStyleConfig, Flex } from "@chakra-ui/react" import { dataAttr } from "@chakra-ui/utils" import { motion } from "framer-motion" -import { CgAdd, CgCheck } from "react-icons/cg" +import { CgCheck } from "react-icons/cg" const MotionFlex = motion(Flex) @@ -15,12 +15,10 @@ const animationConfig = { } type MatchIconProps = { isChecked: boolean - isCustom?: boolean } export const MatchIcon = (props: MatchIconProps) => { const style = useStyleConfig("MatchIcon", props) - const Icon = props.isCustom ? CgAdd : CgCheck return ( { > {props.isChecked && ( - + )} diff --git a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx index 1c751619..0ba30b2d 100644 --- a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx +++ b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx @@ -44,7 +44,6 @@ export const TemplateColumn = ({ const styles = useStyleConfig("MatchColumnsStep") as Styles const customFields = selectColumnCustomFields(column, headerCustomFieldsMap) const fields = [...originalFields, ...customFields] as Fields - const isCustom = "value" in column && customFields.some((e) => e.key === column.value) const isIgnored = column.type === ColumnType.ignored const isChecked = column.type === ColumnType.matched || @@ -73,7 +72,7 @@ export const TemplateColumn = ({ name={column.header} /> - + {isSelect && ( diff --git a/src/steps/MatchColumnsStep/utils/customFields.ts b/src/steps/MatchColumnsStep/utils/customFields.ts index 58cad0b8..c2706c6b 100644 --- a/src/steps/MatchColumnsStep/utils/customFields.ts +++ b/src/steps/MatchColumnsStep/utils/customFields.ts @@ -15,14 +15,15 @@ export const mergeCustomFields = ( fields: Fields, headerCustomFieldsMap: HeaderCustomFieldsMap, ) => { - const mergedFields = [...fields] as Field[] + const customFields: Field[] = [] for (const column of columns) { if (!("value" in column)) continue - const customField = headerCustomFieldsMap[column.header as string]?.find((field) => field.key === column.value) + const columnCustomField = selectColumnCustomFields(column, headerCustomFieldsMap) + const customField = columnCustomField.find((field) => field.key === column.value) if (!customField) continue - mergedFields.splice(column.index, 0, customField) + customFields.push(customField as Field) } - return mergedFields as unknown as Fields + return [...fields, ...customFields] } export const selectColumnCustomFields = (column: Column, headerCustomFieldsMap: HeaderCustomFieldsMap) => { diff --git a/src/stories/mockRsiValues.ts b/src/stories/mockRsiValues.ts index 0c51ce10..dc1d9af9 100644 --- a/src/stories/mockRsiValues.ts +++ b/src/stories/mockRsiValues.ts @@ -113,7 +113,8 @@ export const mockRsiValues = mockComponentBehaviourForTypes({ fieldType: { type: "input", }, - alternateMatches: c.header === "custom_f_F2" ? ["custom_f_F2"] : undefined, + // ability to automatically map custom fields + alternateMatches: c.header.includes("custom") ? [c.header] : undefined, }, ] }, From 0a46791a6546aa28e001374c9dccac92b7c312b4 Mon Sep 17 00:00:00 2001 From: Artur Perepelytsia Date: Thu, 16 Nov 2023 10:46:46 +0200 Subject: [PATCH 16/16] feat/custom-fields-hive - revert MatchIcon changes - more generic example of custom fields - mergeCustomFields - add custom fields to end of fields array --- src/steps/MatchColumnsStep/components/MatchIcon.tsx | 6 ++---- src/steps/MatchColumnsStep/components/TemplateColumn.tsx | 3 +-- src/steps/MatchColumnsStep/utils/customFields.ts | 9 +++++---- src/stories/mockRsiValues.ts | 3 ++- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/steps/MatchColumnsStep/components/MatchIcon.tsx b/src/steps/MatchColumnsStep/components/MatchIcon.tsx index aeee7536..cca299d4 100644 --- a/src/steps/MatchColumnsStep/components/MatchIcon.tsx +++ b/src/steps/MatchColumnsStep/components/MatchIcon.tsx @@ -1,7 +1,7 @@ import { chakra, useStyleConfig, Flex } from "@chakra-ui/react" import { dataAttr } from "@chakra-ui/utils" import { motion } from "framer-motion" -import { CgAdd, CgCheck } from "react-icons/cg" +import { CgCheck } from "react-icons/cg" const MotionFlex = motion(Flex) @@ -15,12 +15,10 @@ const animationConfig = { } type MatchIconProps = { isChecked: boolean - isCustom?: boolean } export const MatchIcon = (props: MatchIconProps) => { const style = useStyleConfig("MatchIcon", props) - const Icon = props.isCustom ? CgAdd : CgCheck return ( { > {props.isChecked && ( - + )} diff --git a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx index 1c751619..0ba30b2d 100644 --- a/src/steps/MatchColumnsStep/components/TemplateColumn.tsx +++ b/src/steps/MatchColumnsStep/components/TemplateColumn.tsx @@ -44,7 +44,6 @@ export const TemplateColumn = ({ const styles = useStyleConfig("MatchColumnsStep") as Styles const customFields = selectColumnCustomFields(column, headerCustomFieldsMap) const fields = [...originalFields, ...customFields] as Fields - const isCustom = "value" in column && customFields.some((e) => e.key === column.value) const isIgnored = column.type === ColumnType.ignored const isChecked = column.type === ColumnType.matched || @@ -73,7 +72,7 @@ export const TemplateColumn = ({ name={column.header} /> - + {isSelect && ( diff --git a/src/steps/MatchColumnsStep/utils/customFields.ts b/src/steps/MatchColumnsStep/utils/customFields.ts index 58cad0b8..1da0bb7f 100644 --- a/src/steps/MatchColumnsStep/utils/customFields.ts +++ b/src/steps/MatchColumnsStep/utils/customFields.ts @@ -15,14 +15,15 @@ export const mergeCustomFields = ( fields: Fields, headerCustomFieldsMap: HeaderCustomFieldsMap, ) => { - const mergedFields = [...fields] as Field[] + const customFields: Field[] = [] for (const column of columns) { if (!("value" in column)) continue - const customField = headerCustomFieldsMap[column.header as string]?.find((field) => field.key === column.value) + const columnCustomField = selectColumnCustomFields(column, headerCustomFieldsMap) + const customField = columnCustomField.find((field) => field.key === column.value) if (!customField) continue - mergedFields.splice(column.index, 0, customField) + customFields.push(customField) } - return mergedFields as unknown as Fields + return [...fields, ...customFields] as Fields } export const selectColumnCustomFields = (column: Column, headerCustomFieldsMap: HeaderCustomFieldsMap) => { diff --git a/src/stories/mockRsiValues.ts b/src/stories/mockRsiValues.ts index bc6c316b..651424e7 100644 --- a/src/stories/mockRsiValues.ts +++ b/src/stories/mockRsiValues.ts @@ -109,7 +109,8 @@ export const mockRsiValues = mockComponentBehaviourForTypes({ fieldType: { type: "input", }, - alternateMatches: c.header === "custom_f_F2" ? ["custom_f_F2"] : undefined, + // ability to automatically map custom fields + alternateMatches: c.header.includes("custom") ? [c.header] : undefined, }, ] },