5425 - Introducing support for all Composite Fields Import (#5470)
Adding support for all Composite Fields while using the "import" functionality. This includes: - Currency - Address Edit : - Refactored a lot of types in the spreadsheet import module - Renamed a lot of functions, hooks and types that were not self-explanatory enough --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,13 +1,13 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const RsiContext = createContext({} as any);
|
||||
|
||||
type ProvidersProps<T extends string> = {
|
||||
children: React.ReactNode;
|
||||
values: SpreadsheetOptions<T>;
|
||||
values: SpreadsheetImportDialogOptions<T>;
|
||||
};
|
||||
|
||||
export const Providers = <T extends string>({
|
||||
|
||||
@ -1,50 +1,57 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot, useRecoilState } from 'recoil';
|
||||
|
||||
import { useSpreadsheetImport } from '@/spreadsheet-import/hooks/useSpreadsheetImport';
|
||||
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
|
||||
import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog';
|
||||
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
|
||||
import { StepType } from '@/spreadsheet-import/steps/components/UploadFlow';
|
||||
import { RawData, SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
import {
|
||||
ImportedRow,
|
||||
SpreadsheetImportDialogOptions,
|
||||
} from '@/spreadsheet-import/types';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
);
|
||||
type SpreadsheetKey = 'spreadsheet_key';
|
||||
|
||||
export const mockedSpreadsheetOptions: SpreadsheetOptions<SpreadsheetKey> = {
|
||||
isOpen: true,
|
||||
onClose: () => {},
|
||||
fields: [],
|
||||
uploadStepHook: async () => [],
|
||||
selectHeaderStepHook: async (headerValues: RawData, data: RawData[]) => ({
|
||||
headerValues,
|
||||
data,
|
||||
}),
|
||||
matchColumnsStepHook: async () => [],
|
||||
rowHook: () => ({ spreadsheet_key: 'rowHook' }),
|
||||
tableHook: () => [{ spreadsheet_key: 'tableHook' }],
|
||||
onSubmit: async () => {},
|
||||
allowInvalidSubmit: false,
|
||||
customTheme: {},
|
||||
maxRecords: 10,
|
||||
maxFileSize: 50,
|
||||
autoMapHeaders: true,
|
||||
autoMapDistance: 1,
|
||||
initialStepState: {
|
||||
type: StepType.upload,
|
||||
},
|
||||
dateFormat: 'MM/DD/YY',
|
||||
parseRaw: true,
|
||||
rtl: false,
|
||||
selectHeader: true,
|
||||
};
|
||||
export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions<SpreadsheetKey> =
|
||||
{
|
||||
isOpen: true,
|
||||
onClose: () => {},
|
||||
fields: [],
|
||||
uploadStepHook: async () => [],
|
||||
selectHeaderStepHook: async (
|
||||
headerValues: ImportedRow,
|
||||
data: ImportedRow[],
|
||||
) => ({
|
||||
headerRow: headerValues,
|
||||
importedRows: data,
|
||||
}),
|
||||
matchColumnsStepHook: async () => [],
|
||||
rowHook: () => ({ spreadsheet_key: 'rowHook' }),
|
||||
tableHook: () => [{ spreadsheet_key: 'tableHook' }],
|
||||
onSubmit: async () => {},
|
||||
allowInvalidSubmit: false,
|
||||
customTheme: {},
|
||||
maxRecords: 10,
|
||||
maxFileSize: 50,
|
||||
autoMapHeaders: true,
|
||||
autoMapDistance: 1,
|
||||
initialStepState: {
|
||||
type: StepType.upload,
|
||||
},
|
||||
dateFormat: 'MM/DD/YY',
|
||||
parseRaw: true,
|
||||
rtl: false,
|
||||
selectHeader: true,
|
||||
};
|
||||
|
||||
describe('useSpreadsheetImport', () => {
|
||||
it('should set isOpen to true, and update the options in the Recoil state', async () => {
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
useSpreadsheetImport: useSpreadsheetImport<SpreadsheetKey>(),
|
||||
spreadsheetImportState: useRecoilState(spreadsheetImportState)[0],
|
||||
useSpreadsheetImport: useOpenSpreadsheetImportDialog<SpreadsheetKey>(),
|
||||
spreadsheetImportState: useRecoilState(spreadsheetImportDialogState)[0],
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
@ -55,7 +62,7 @@ describe('useSpreadsheetImport', () => {
|
||||
options: null,
|
||||
});
|
||||
act(() => {
|
||||
result.current.useSpreadsheetImport.openSpreadsheetImport(
|
||||
result.current.useSpreadsheetImport.openSpreadsheetImportDialog(
|
||||
mockedSpreadsheetOptions,
|
||||
);
|
||||
});
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
|
||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||
|
||||
export const useOpenSpreadsheetImportDialog = <T extends string>() => {
|
||||
const setSpreadSheetImport = useSetRecoilState(spreadsheetImportDialogState);
|
||||
|
||||
const openSpreadsheetImportDialog = (
|
||||
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'>,
|
||||
) => {
|
||||
setSpreadSheetImport({
|
||||
isOpen: true,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
return { openSpreadsheetImportDialog };
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
|
||||
import { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
|
||||
export const useSpreadsheetImport = <T extends string>() => {
|
||||
const setSpreadSheetImport = useSetRecoilState(spreadsheetImportState);
|
||||
|
||||
const openSpreadsheetImport = (
|
||||
options: Omit<SpreadsheetOptions<T>, 'isOpen' | 'onClose'>,
|
||||
) => {
|
||||
setSpreadSheetImport({
|
||||
isOpen: true,
|
||||
options,
|
||||
});
|
||||
};
|
||||
|
||||
return { openSpreadsheetImport };
|
||||
};
|
||||
@ -3,12 +3,12 @@ import { SetRequired } from 'type-fest';
|
||||
|
||||
import { RsiContext } from '@/spreadsheet-import/components/Providers';
|
||||
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
||||
import { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
|
||||
|
||||
export const useSpreadsheetImportInternal = <T extends string>() =>
|
||||
useContext<
|
||||
SetRequired<
|
||||
SpreadsheetOptions<T>,
|
||||
SpreadsheetImportDialogOptions<T>,
|
||||
keyof typeof defaultSpreadsheetImportProps
|
||||
>
|
||||
>(RsiContext);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/Providers';
|
||||
import { Steps } from '@/spreadsheet-import/steps/components/Steps';
|
||||
import { SpreadsheetOptions as SpreadsheetImportProps } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetImportDialogOptions as SpreadsheetImportProps } from '@/spreadsheet-import/types';
|
||||
|
||||
export const defaultSpreadsheetImportProps: Partial<
|
||||
SpreadsheetImportProps<any>
|
||||
@ -10,7 +10,10 @@ export const defaultSpreadsheetImportProps: Partial<
|
||||
allowInvalidSubmit: true,
|
||||
autoMapDistance: 2,
|
||||
uploadStepHook: async (value) => value,
|
||||
selectHeaderStepHook: async (headerValues, data) => ({ headerValues, data }),
|
||||
selectHeaderStepHook: async (headerValues, data) => ({
|
||||
headerRow: headerValues,
|
||||
importedRows: data,
|
||||
}),
|
||||
matchColumnsStepHook: async (table) => table,
|
||||
dateFormat: 'yyyy-mm-dd', // ISO 8601,
|
||||
parseRaw: true,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
|
||||
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
|
||||
|
||||
import { SpreadsheetImport } from './SpreadsheetImport';
|
||||
|
||||
@ -10,12 +10,12 @@ type SpreadsheetImportProviderProps = React.PropsWithChildren;
|
||||
export const SpreadsheetImportProvider = (
|
||||
props: SpreadsheetImportProviderProps,
|
||||
) => {
|
||||
const [spreadsheetImport, setSpreadsheetImport] = useRecoilState(
|
||||
spreadsheetImportState,
|
||||
const [spreadsheetImportDialog, setSpreadsheetImportDialog] = useRecoilState(
|
||||
spreadsheetImportDialogState,
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setSpreadsheetImport({
|
||||
setSpreadsheetImportDialog({
|
||||
isOpen: false,
|
||||
options: null,
|
||||
});
|
||||
@ -24,12 +24,12 @@ export const SpreadsheetImportProvider = (
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
{spreadsheetImport.isOpen && spreadsheetImport.options && (
|
||||
{spreadsheetImportDialog.isOpen && spreadsheetImportDialog.options && (
|
||||
<SpreadsheetImport
|
||||
isOpen={true}
|
||||
onClose={handleClose}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...spreadsheetImport.options}
|
||||
{...spreadsheetImportDialog.options}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { SpreadsheetImportDialogOptions } from '../types';
|
||||
|
||||
export type SpreadsheetImportDialogState<T extends string> = {
|
||||
isOpen: boolean;
|
||||
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'> | null;
|
||||
};
|
||||
|
||||
export const spreadsheetImportDialogState = createState<
|
||||
SpreadsheetImportDialogState<any>
|
||||
>({
|
||||
key: 'spreadsheetImportDialogState',
|
||||
defaultValue: {
|
||||
isOpen: false,
|
||||
options: null,
|
||||
},
|
||||
});
|
||||
@ -1,16 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { SpreadsheetOptions } from '../types';
|
||||
|
||||
export type SpreadsheetImportState<T extends string> = {
|
||||
isOpen: boolean;
|
||||
options: Omit<SpreadsheetOptions<T>, 'isOpen' | 'onClose'> | null;
|
||||
};
|
||||
|
||||
export const spreadsheetImportState = createState<SpreadsheetImportState<any>>({
|
||||
key: 'spreadsheetImportState',
|
||||
defaultValue: {
|
||||
isOpen: false,
|
||||
options: null,
|
||||
},
|
||||
});
|
||||
@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { Field, RawData } from '@/spreadsheet-import/types';
|
||||
import { Field, ImportedRow } from '@/spreadsheet-import/types';
|
||||
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
||||
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
|
||||
import { normalizeTableData } from '@/spreadsheet-import/utils/normalizeTableData';
|
||||
@ -46,9 +46,13 @@ const StyledColumn = styled.span`
|
||||
`;
|
||||
|
||||
export type MatchColumnsStepProps<T extends string> = {
|
||||
data: RawData[];
|
||||
headerValues: RawData;
|
||||
onContinue: (data: any[], rawData: RawData[], columns: Columns<T>) => void;
|
||||
data: ImportedRow[];
|
||||
headerValues: ImportedRow;
|
||||
onContinue: (
|
||||
data: any[],
|
||||
rawData: ImportedRow[],
|
||||
columns: Columns<T>,
|
||||
) => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
@ -67,23 +71,27 @@ export type MatchedOptions<T> = {
|
||||
};
|
||||
|
||||
type EmptyColumn = { type: ColumnType.empty; index: number; header: string };
|
||||
|
||||
type IgnoredColumn = {
|
||||
type: ColumnType.ignored;
|
||||
index: number;
|
||||
header: string;
|
||||
};
|
||||
|
||||
type MatchedColumn<T> = {
|
||||
type: ColumnType.matched;
|
||||
index: number;
|
||||
header: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
type MatchedSwitchColumn<T> = {
|
||||
type: ColumnType.matchedCheckbox;
|
||||
index: number;
|
||||
header: string;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type MatchedSelectColumn<T> = {
|
||||
type: ColumnType.matchedSelect;
|
||||
index: number;
|
||||
@ -91,6 +99,7 @@ export type MatchedSelectColumn<T> = {
|
||||
value: T;
|
||||
matchedOptions: Partial<MatchedOptions<T>>[];
|
||||
};
|
||||
|
||||
export type MatchedSelectOptionsColumn<T> = {
|
||||
type: ColumnType.matchedSelectOptions;
|
||||
index: number;
|
||||
@ -271,7 +280,7 @@ export const MatchColumnsStep = <T extends string>({
|
||||
renderUserColumn={(columns, columnIndex) => (
|
||||
<UserTableColumn
|
||||
column={columns[columnIndex]}
|
||||
entries={dataExample.map(
|
||||
importedRow={dataExample.map(
|
||||
(row) => row[columns[columnIndex].index],
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RawData } from '@/spreadsheet-import/types';
|
||||
import { ImportedRow } from '@/spreadsheet-import/types';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { Column } from '../MatchColumnsStep';
|
||||
@ -31,20 +31,22 @@ const StyledExample = styled.span`
|
||||
|
||||
type UserTableColumnProps<T extends string> = {
|
||||
column: Column<T>;
|
||||
entries: RawData;
|
||||
importedRow: ImportedRow;
|
||||
};
|
||||
|
||||
export const UserTableColumn = <T extends string>({
|
||||
column,
|
||||
entries,
|
||||
importedRow,
|
||||
}: UserTableColumnProps<T>) => {
|
||||
const { header } = column;
|
||||
const entry = entries.find(isDefined);
|
||||
const firstDefinedValue = importedRow.find(isDefined);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledValue>{header}</StyledValue>
|
||||
{entry && <StyledExample>{`ex: ${entry}`}</StyledExample>}
|
||||
{firstDefinedValue && (
|
||||
<StyledExample>{`ex: ${firstDefinedValue}`}</StyledExample>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
|
||||
import { RawData } from '@/spreadsheet-import/types';
|
||||
import { ImportedRow } from '@/spreadsheet-import/types';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
|
||||
import { SelectHeaderTable } from './components/SelectHeaderTable';
|
||||
@ -19,29 +19,36 @@ const StyledTableContainer = styled.div`
|
||||
`;
|
||||
|
||||
type SelectHeaderStepProps = {
|
||||
data: RawData[];
|
||||
onContinue: (headerValues: RawData, data: RawData[]) => Promise<void>;
|
||||
importedRows: ImportedRow[];
|
||||
onContinue: (
|
||||
headerValues: ImportedRow,
|
||||
importedRows: ImportedRow[],
|
||||
) => Promise<void>;
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const SelectHeaderStep = ({
|
||||
data,
|
||||
importedRows,
|
||||
onContinue,
|
||||
onBack,
|
||||
}: SelectHeaderStepProps) => {
|
||||
const [selectedRows, setSelectedRows] = useState<ReadonlySet<number>>(
|
||||
new Set([0]),
|
||||
);
|
||||
const [selectedRowIndexes, setSelectedRowIndexes] = useState<
|
||||
ReadonlySet<number>
|
||||
>(new Set([0]));
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleContinue = useCallback(async () => {
|
||||
const [selectedRowIndex] = Array.from(new Set(selectedRows));
|
||||
const [selectedRowIndex] = Array.from(new Set(selectedRowIndexes));
|
||||
// We consider data above header to be redundant
|
||||
const trimmedData = data.slice(selectedRowIndex + 1);
|
||||
const trimmedData = importedRows.slice(selectedRowIndex + 1);
|
||||
|
||||
setIsLoading(true);
|
||||
await onContinue(data[selectedRowIndex], trimmedData);
|
||||
|
||||
await onContinue(importedRows[selectedRowIndex], trimmedData);
|
||||
|
||||
setIsLoading(false);
|
||||
}, [onContinue, data, selectedRows]);
|
||||
}, [onContinue, importedRows, selectedRowIndexes]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -49,9 +56,9 @@ export const SelectHeaderStep = ({
|
||||
<StyledHeading title="Select header row" />
|
||||
<StyledTableContainer>
|
||||
<SelectHeaderTable
|
||||
data={data}
|
||||
selectedRows={selectedRows}
|
||||
setSelectedRows={setSelectedRows}
|
||||
importedRows={importedRows}
|
||||
selectedRowIndexes={selectedRowIndexes}
|
||||
setSelectedRowIndexes={setSelectedRowIndexes}
|
||||
/>
|
||||
</StyledTableContainer>
|
||||
</Modal.Content>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// @ts-expect-error // Todo: remove usage of react-data-grid
|
||||
import { Column, FormatterProps, useRowSelection } from 'react-data-grid';
|
||||
|
||||
import { RawData } from '@/spreadsheet-import/types';
|
||||
import { ImportedRow } from '@/spreadsheet-import/types';
|
||||
import { Radio } from '@/ui/input/components/Radio';
|
||||
|
||||
const SELECT_COLUMN_KEY = 'select-row';
|
||||
@ -39,7 +39,7 @@ export const SelectColumn: Column<any, any> = {
|
||||
formatter: SelectFormatter,
|
||||
};
|
||||
|
||||
export const generateSelectionColumns = (data: RawData[]) => {
|
||||
export const generateSelectionColumns = (data: ImportedRow[]) => {
|
||||
const longestRowLength = data.reduce(
|
||||
(acc, curr) => (acc > curr.length ? acc : curr.length),
|
||||
0,
|
||||
|
||||
@ -1,41 +1,44 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Table } from '@/spreadsheet-import/components/Table';
|
||||
import { RawData } from '@/spreadsheet-import/types';
|
||||
import { ImportedRow } from '@/spreadsheet-import/types';
|
||||
|
||||
import { generateSelectionColumns } from './SelectColumn';
|
||||
|
||||
interface SelectHeaderTableProps {
|
||||
data: RawData[];
|
||||
selectedRows: ReadonlySet<number>;
|
||||
setSelectedRows: (rows: ReadonlySet<number>) => void;
|
||||
}
|
||||
type SelectHeaderTableProps = {
|
||||
importedRows: ImportedRow[];
|
||||
selectedRowIndexes: ReadonlySet<number>;
|
||||
setSelectedRowIndexes: (rowIndexes: ReadonlySet<number>) => void;
|
||||
};
|
||||
|
||||
export const SelectHeaderTable = ({
|
||||
data,
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
importedRows,
|
||||
selectedRowIndexes,
|
||||
setSelectedRowIndexes,
|
||||
}: SelectHeaderTableProps) => {
|
||||
const columns = useMemo(() => generateSelectionColumns(data), [data]);
|
||||
const columns = useMemo(
|
||||
() => generateSelectionColumns(importedRows),
|
||||
[importedRows],
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
// Todo: remove usage of react-data-grid
|
||||
rowKeyGetter={(row: any) => data.indexOf(row)}
|
||||
rows={data}
|
||||
rowKeyGetter={(row: any) => importedRows.indexOf(row)}
|
||||
rows={importedRows}
|
||||
columns={columns}
|
||||
selectedRows={selectedRows}
|
||||
onSelectedRowsChange={(newRows: any) => {
|
||||
selectedRowIndexes={selectedRowIndexes}
|
||||
onSelectedRowIndexesChange={(newRowIndexes: number[]) => {
|
||||
// allow selecting only one row
|
||||
newRows.forEach((value: any) => {
|
||||
if (!selectedRows.has(value as number)) {
|
||||
setSelectedRows(new Set([value as number]));
|
||||
newRowIndexes.forEach((value: any) => {
|
||||
if (!selectedRowIndexes.has(value as number)) {
|
||||
setSelectedRowIndexes(new Set([value as number]));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: any) => {
|
||||
setSelectedRows(new Set([data.indexOf(row)]));
|
||||
setSelectedRowIndexes(new Set([importedRows.indexOf(row)]));
|
||||
}}
|
||||
headerRowHeight={0}
|
||||
/>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { WorkBook } from 'xlsx-ugnis';
|
||||
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { RawData } from '@/spreadsheet-import/types';
|
||||
import { ImportedRow } from '@/spreadsheet-import/types';
|
||||
import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords';
|
||||
import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook';
|
||||
import { CircularProgressBar } from '@/ui/feedback/progress-bar/components/CircularProgressBar';
|
||||
@ -42,12 +42,12 @@ export type StepState =
|
||||
}
|
||||
| {
|
||||
type: StepType.selectHeader;
|
||||
data: RawData[];
|
||||
data: ImportedRow[];
|
||||
}
|
||||
| {
|
||||
type: StepType.matchColumns;
|
||||
data: RawData[];
|
||||
headerValues: RawData;
|
||||
data: ImportedRow[];
|
||||
headerValues: ImportedRow;
|
||||
}
|
||||
| {
|
||||
type: StepType.validateData;
|
||||
@ -131,10 +131,8 @@ export const UploadFlow = ({ nextStep, prevStep }: UploadFlowProps) => {
|
||||
// Automatically select first row as header
|
||||
const trimmedData = mappedWorkbook.slice(1);
|
||||
|
||||
const { data, headerValues } = await selectHeaderStepHook(
|
||||
mappedWorkbook[0],
|
||||
trimmedData,
|
||||
);
|
||||
const { importedRows: data, headerRow: headerValues } =
|
||||
await selectHeaderStepHook(mappedWorkbook[0], trimmedData);
|
||||
|
||||
setState({
|
||||
type: StepType.matchColumns,
|
||||
@ -186,12 +184,11 @@ export const UploadFlow = ({ nextStep, prevStep }: UploadFlowProps) => {
|
||||
case StepType.selectHeader:
|
||||
return (
|
||||
<SelectHeaderStep
|
||||
data={state.data}
|
||||
importedRows={state.data}
|
||||
onContinue={async (...args) => {
|
||||
try {
|
||||
const { data, headerValues } = await selectHeaderStepHook(
|
||||
...args,
|
||||
);
|
||||
const { importedRows: data, headerRow: headerValues } =
|
||||
await selectHeaderStepHook(...args);
|
||||
setState({
|
||||
type: StepType.matchColumns,
|
||||
data,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import styled from '@emotion/styled';
|
||||
import * as XLSX from 'xlsx-ugnis';
|
||||
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
@ -79,11 +79,7 @@ const StyledText = styled.span`
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(MainButton)`
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
width: 200px;
|
||||
padding: 15px;
|
||||
`;
|
||||
|
||||
type DropZoneProps = {
|
||||
@ -151,7 +147,7 @@ export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||
) : (
|
||||
<>
|
||||
<StyledText>Upload .xlsx, .xls or .csv file</StyledText>
|
||||
<StyledButton onClick={open} title="Select file" />
|
||||
<MainButton onClick={open} title="Select file" />
|
||||
</>
|
||||
)}
|
||||
</StyledContainer>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// @ts-expect-error Todo: remove usage of react-data-grid
|
||||
import { RowsChangeData } from 'react-data-grid';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconTrash } from 'twenty-ui';
|
||||
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
@ -12,7 +12,10 @@ import {
|
||||
Columns,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Data } from '@/spreadsheet-import/types';
|
||||
import {
|
||||
ImportedStructuredRow,
|
||||
ImportValidationResult,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
|
||||
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
@ -21,7 +24,7 @@ import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { generateColumns } from './components/columns';
|
||||
import { Meta } from './types';
|
||||
import { ImportedStructuredRowMetadata } from './types';
|
||||
|
||||
const StyledContent = styled(Modal.Content)`
|
||||
padding-left: ${({ theme }) => theme.spacing(6)};
|
||||
@ -65,7 +68,7 @@ const StyledNoRowsContainer = styled.div`
|
||||
`;
|
||||
|
||||
type ValidationStepProps<T extends string> = {
|
||||
initialData: Data<T>[];
|
||||
initialData: ImportedStructuredRow<T>[];
|
||||
importedColumns: Columns<string>;
|
||||
file: File;
|
||||
onSubmitStart?: () => void;
|
||||
@ -83,7 +86,9 @@ export const ValidationStep = <T extends string>({
|
||||
const { fields, onClose, onSubmit, rowHook, tableHook } =
|
||||
useSpreadsheetImportInternal<T>();
|
||||
|
||||
const [data, setData] = useState<(Data<T> & Meta)[]>(
|
||||
const [data, setData] = useState<
|
||||
(ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[]
|
||||
>(
|
||||
useMemo(
|
||||
() => addErrorsAndRunHooks<T>(initialData, fields, rowHook, tableHook),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -173,7 +178,11 @@ export const ValidationStep = <T extends string>({
|
||||
return data;
|
||||
}, [data, filterByErrors]);
|
||||
|
||||
const rowKeyGetter = useCallback((row: Data<T> & Meta) => row.__index, []);
|
||||
const rowKeyGetter = useCallback(
|
||||
(row: ImportedStructuredRow<T> & ImportedStructuredRowMetadata) =>
|
||||
row.__index,
|
||||
[],
|
||||
);
|
||||
|
||||
const submitData = async () => {
|
||||
const calculatedData = data.reduce(
|
||||
@ -182,15 +191,23 @@ export const ValidationStep = <T extends string>({
|
||||
if (isDefined(__errors)) {
|
||||
for (const key in __errors) {
|
||||
if (__errors[key].level === 'error') {
|
||||
acc.invalidData.push(values as unknown as Data<T>);
|
||||
acc.invalidStructuredRows.push(
|
||||
values as unknown as ImportedStructuredRow<T>,
|
||||
);
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
acc.validData.push(values as unknown as Data<T>);
|
||||
acc.validStructuredRows.push(
|
||||
values as unknown as ImportedStructuredRow<T>,
|
||||
);
|
||||
return acc;
|
||||
},
|
||||
{ validData: [] as Data<T>[], invalidData: [] as Data<T>[], all: data },
|
||||
{
|
||||
validStructuredRows: [] as ImportedStructuredRow<T>[],
|
||||
invalidStructuredRows: [] as ImportedStructuredRow<T>[],
|
||||
allStructuredRows: data,
|
||||
} satisfies ImportValidationResult<T>,
|
||||
);
|
||||
onSubmitStart?.();
|
||||
await onSubmit(calculatedData, file);
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import styled from '@emotion/styled';
|
||||
// @ts-expect-error // Todo: remove usage of react-data-grid
|
||||
import { Column, useRowSelection } from 'react-data-grid';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import { AppTooltip } from 'twenty-ui';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import { Data, Fields } from '@/spreadsheet-import/types';
|
||||
import { Fields, ImportedStructuredRow } from '@/spreadsheet-import/types';
|
||||
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { Toggle } from '@/ui/input/components/Toggle';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { Meta } from '../types';
|
||||
import { ImportedStructuredRowMetadata } from '../types';
|
||||
|
||||
const StyledHeaderContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -63,7 +63,7 @@ const SELECT_COLUMN_KEY = 'select-row';
|
||||
|
||||
export const generateColumns = <T extends string>(
|
||||
fields: Fields<T>,
|
||||
): Column<Data<T> & Meta>[] => [
|
||||
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata>[] => [
|
||||
{
|
||||
key: SELECT_COLUMN_KEY,
|
||||
name: '',
|
||||
@ -96,7 +96,9 @@ export const generateColumns = <T extends string>(
|
||||
},
|
||||
},
|
||||
...fields.map(
|
||||
(column): Column<Data<T> & Meta> => ({
|
||||
(
|
||||
column,
|
||||
): Column<ImportedStructuredRow<T> & ImportedStructuredRowMetadata> => ({
|
||||
key: column.key,
|
||||
name: column.label,
|
||||
minWidth: 150,
|
||||
@ -120,7 +122,8 @@ export const generateColumns = <T extends string>(
|
||||
editable: column.fieldType.type !== 'checkbox',
|
||||
// Todo: remove usage of react-data-grid
|
||||
editor: ({ row, onRowChange, onClose }: any) => {
|
||||
const columnKey = column.key as keyof (Data<T> & Meta);
|
||||
const columnKey = column.key as keyof (ImportedStructuredRow<T> &
|
||||
ImportedStructuredRowMetadata);
|
||||
let component;
|
||||
|
||||
switch (column.fieldType.type) {
|
||||
@ -167,7 +170,8 @@ export const generateColumns = <T extends string>(
|
||||
},
|
||||
// Todo: remove usage of react-data-grid
|
||||
formatter: ({ row, onRowChange }: { row: any; onRowChange: any }) => {
|
||||
const columnKey = column.key as keyof (Data<T> & Meta);
|
||||
const columnKey = column.key as keyof (ImportedStructuredRow<T> &
|
||||
ImportedStructuredRowMetadata);
|
||||
let component;
|
||||
|
||||
switch (column.fieldType.type) {
|
||||
@ -226,7 +230,7 @@ export const generateColumns = <T extends string>(
|
||||
|
||||
return component;
|
||||
},
|
||||
cellClass: (row: Meta) => {
|
||||
cellClass: (row: ImportedStructuredRowMetadata) => {
|
||||
switch (row.__errors?.[column.key]?.level) {
|
||||
case 'error':
|
||||
return 'rdg-cell-error';
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { Info } from '@/spreadsheet-import/types';
|
||||
|
||||
export type Meta = { __index: string; __errors?: Error | null };
|
||||
export type ImportedStructuredRowMetadata = {
|
||||
__index: string;
|
||||
__errors?: Error | null;
|
||||
};
|
||||
export type Error = { [key: string]: Info };
|
||||
export type Errors = { [id: string]: Error };
|
||||
|
||||
@ -24,7 +24,7 @@ export const Default = () => (
|
||||
<Providers values={mockRsiValues}>
|
||||
<ModalWrapper isOpen={true} onClose={() => null}>
|
||||
<SelectHeaderStep
|
||||
data={headerSelectionTableFields}
|
||||
importedRows={headerSelectionTableFields}
|
||||
onContinue={() => Promise.resolve()}
|
||||
onBack={() => Promise.resolve()}
|
||||
/>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
||||
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Fields, SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
import {
|
||||
Fields,
|
||||
SpreadsheetImportDialogOptions,
|
||||
} from '@/spreadsheet-import/types';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
const fields = [
|
||||
@ -13,7 +16,7 @@ const fields = [
|
||||
type: 'input',
|
||||
},
|
||||
example: 'Stephanie',
|
||||
validations: [
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'required',
|
||||
errorMessage: 'Name is required',
|
||||
@ -29,7 +32,7 @@ const fields = [
|
||||
type: 'input',
|
||||
},
|
||||
example: 'McDonald',
|
||||
validations: [
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'unique',
|
||||
errorMessage: 'Last name must be unique',
|
||||
@ -47,7 +50,7 @@ const fields = [
|
||||
type: 'input',
|
||||
},
|
||||
example: '23',
|
||||
validations: [
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'regex',
|
||||
value: '^\\d+$',
|
||||
@ -69,7 +72,7 @@ const fields = [
|
||||
],
|
||||
},
|
||||
example: 'Team one',
|
||||
validations: [
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'required',
|
||||
errorMessage: 'Team is required',
|
||||
@ -117,7 +120,7 @@ export const importedColums: Columns<string> = [
|
||||
];
|
||||
|
||||
const mockComponentBehaviourForTypes = <T extends string>(
|
||||
props: SpreadsheetOptions<T>,
|
||||
props: SpreadsheetImportDialogOptions<T>,
|
||||
) => props;
|
||||
|
||||
export const mockRsiValues = mockComponentBehaviourForTypes({
|
||||
@ -142,8 +145,8 @@ export const mockRsiValues = mockComponentBehaviourForTypes({
|
||||
}),
|
||||
);
|
||||
return {
|
||||
headerValues: hData,
|
||||
data,
|
||||
headerRow: hData,
|
||||
importedRows: data,
|
||||
};
|
||||
},
|
||||
// Runs after column matching and on entry change, more performant
|
||||
|
||||
@ -3,34 +3,37 @@ import { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { StepState } from '@/spreadsheet-import/steps/components/UploadFlow';
|
||||
import { Meta } from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
||||
import { ImportedStructuredRowMetadata } from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
||||
|
||||
export type SpreadsheetOptions<Keys extends string> = {
|
||||
export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
|
||||
// Is modal visible.
|
||||
isOpen: boolean;
|
||||
// callback when RSI is closed before final submit
|
||||
onClose: () => void;
|
||||
// Field description for requested data
|
||||
fields: Fields<Keys>;
|
||||
fields: Fields<FieldNames>;
|
||||
// Runs after file upload step, receives and returns raw sheet data
|
||||
uploadStepHook?: (data: RawData[]) => Promise<RawData[]>;
|
||||
uploadStepHook?: (importedRows: ImportedRow[]) => Promise<ImportedRow[]>;
|
||||
// Runs after header selection step, receives and returns raw sheet data
|
||||
selectHeaderStepHook?: (
|
||||
headerValues: RawData,
|
||||
data: RawData[],
|
||||
) => Promise<{ headerValues: RawData; data: RawData[] }>;
|
||||
headerRow: ImportedRow,
|
||||
importedRows: ImportedRow[],
|
||||
) => Promise<{ headerRow: ImportedRow; importedRows: ImportedRow[] }>;
|
||||
// Runs once before validation step, used for data mutations and if you want to change how columns were matched
|
||||
matchColumnsStepHook?: (
|
||||
table: Data<Keys>[],
|
||||
rawData: RawData[],
|
||||
columns: Columns<Keys>,
|
||||
) => Promise<Data<Keys>[]>;
|
||||
importedStructuredRows: ImportedStructuredRow<FieldNames>[],
|
||||
importedRows: ImportedRow[],
|
||||
columns: Columns<FieldNames>,
|
||||
) => Promise<ImportedStructuredRow<FieldNames>[]>;
|
||||
// Runs after column matching and on entry change
|
||||
rowHook?: RowHook<Keys>;
|
||||
rowHook?: RowHook<FieldNames>;
|
||||
// Runs after column matching and on entry change
|
||||
tableHook?: TableHook<Keys>;
|
||||
tableHook?: TableHook<FieldNames>;
|
||||
// Function called after user finishes the flow
|
||||
onSubmit: (data: Result<Keys>, file: File) => Promise<void>;
|
||||
onSubmit: (
|
||||
validationResult: ImportValidationResult<FieldNames>,
|
||||
file: File,
|
||||
) => Promise<void>;
|
||||
// Allows submitting with errors. Default: true
|
||||
allowInvalidSubmit?: boolean;
|
||||
// Theme configuration passed to underlying Chakra-UI
|
||||
@ -55,9 +58,9 @@ export type SpreadsheetOptions<Keys extends string> = {
|
||||
selectHeader?: boolean;
|
||||
};
|
||||
|
||||
export type RawData = Array<string | undefined>;
|
||||
export type ImportedRow = Array<string | undefined>;
|
||||
|
||||
export type Data<T extends string> = {
|
||||
export type ImportedStructuredRow<T extends string> = {
|
||||
[key in T]: string | boolean | undefined;
|
||||
};
|
||||
|
||||
@ -76,7 +79,7 @@ export type Field<T extends string> = {
|
||||
// Alternate labels used for fields' auto-matching, e.g. "fname" -> "firstName"
|
||||
alternateMatches?: string[];
|
||||
// Validations used for field entries
|
||||
validations?: Validation[];
|
||||
fieldValidationDefinitions?: FieldValidationDefinition[];
|
||||
// Field entry component, default: Input
|
||||
fieldType: Checkbox | Select | Input;
|
||||
// UI-facing values shown to user as field examples pre-upload phase
|
||||
@ -110,11 +113,19 @@ export type Input = {
|
||||
type: 'input';
|
||||
};
|
||||
|
||||
export type Validation =
|
||||
export type FieldValidationDefinition =
|
||||
| RequiredValidation
|
||||
| UniqueValidation
|
||||
| RegexValidation
|
||||
| FunctionValidation;
|
||||
| FunctionValidation
|
||||
| ObjectValidation;
|
||||
|
||||
export type ObjectValidation = {
|
||||
rule: 'object';
|
||||
isValid: (objectValue: any) => boolean;
|
||||
errorMessage: string;
|
||||
level?: ErrorLevel;
|
||||
};
|
||||
|
||||
export type RequiredValidation = {
|
||||
rule: 'required';
|
||||
@ -145,14 +156,15 @@ export type FunctionValidation = {
|
||||
};
|
||||
|
||||
export type RowHook<T extends string> = (
|
||||
row: Data<T>,
|
||||
row: ImportedStructuredRow<T>,
|
||||
addError: (fieldKey: T, error: Info) => void,
|
||||
table: Data<T>[],
|
||||
) => Data<T>;
|
||||
table: ImportedStructuredRow<T>[],
|
||||
) => ImportedStructuredRow<T>;
|
||||
|
||||
export type TableHook<T extends string> = (
|
||||
table: Data<T>[],
|
||||
table: ImportedStructuredRow<T>[],
|
||||
addError: (rowIndex: number, fieldKey: T, error: Info) => void,
|
||||
) => Data<T>[];
|
||||
) => ImportedStructuredRow<T>[];
|
||||
|
||||
export type ErrorLevel = 'info' | 'warning' | 'error';
|
||||
|
||||
@ -161,8 +173,9 @@ export type Info = {
|
||||
level: ErrorLevel;
|
||||
};
|
||||
|
||||
export type Result<T extends string> = {
|
||||
validData: Data<T>[];
|
||||
invalidData: Data<T>[];
|
||||
all: (Data<T> & Meta)[];
|
||||
export type ImportValidationResult<T extends string> = {
|
||||
validStructuredRows: ImportedStructuredRow<T>[];
|
||||
invalidStructuredRows: ImportedStructuredRow<T>[];
|
||||
allStructuredRows: (ImportedStructuredRow<T> &
|
||||
ImportedStructuredRowMetadata)[];
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {
|
||||
Data,
|
||||
Field,
|
||||
ImportedStructuredRow,
|
||||
Info,
|
||||
RowHook,
|
||||
TableHook,
|
||||
@ -8,11 +8,11 @@ import {
|
||||
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
|
||||
|
||||
describe('addErrorsAndRunHooks', () => {
|
||||
type FullData = Data<'name' | 'age' | 'country'>;
|
||||
type FullData = ImportedStructuredRow<'name' | 'age' | 'country'>;
|
||||
const requiredField: Field<'name'> = {
|
||||
key: 'name',
|
||||
label: 'Name',
|
||||
validations: [{ rule: 'required' }],
|
||||
fieldValidationDefinitions: [{ rule: 'required' }],
|
||||
icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
};
|
||||
@ -20,7 +20,7 @@ describe('addErrorsAndRunHooks', () => {
|
||||
const regexField: Field<'age'> = {
|
||||
key: 'age',
|
||||
label: 'Age',
|
||||
validations: [
|
||||
fieldValidationDefinitions: [
|
||||
{ rule: 'regex', value: '\\d+', errorMessage: 'Regex error' },
|
||||
],
|
||||
icon: null,
|
||||
@ -30,7 +30,7 @@ describe('addErrorsAndRunHooks', () => {
|
||||
const uniqueField: Field<'country'> = {
|
||||
key: 'country',
|
||||
label: 'Country',
|
||||
validations: [{ rule: 'unique' }],
|
||||
fieldValidationDefinitions: [{ rule: 'unique' }],
|
||||
icon: null,
|
||||
fieldType: { type: 'input' },
|
||||
};
|
||||
@ -38,7 +38,7 @@ describe('addErrorsAndRunHooks', () => {
|
||||
const functionValidationFieldTrue: Field<'email'> = {
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
validations: [
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'function',
|
||||
isValid: () => true,
|
||||
@ -52,7 +52,7 @@ describe('addErrorsAndRunHooks', () => {
|
||||
const functionValidationFieldFalse: Field<'email'> = {
|
||||
key: 'email',
|
||||
label: 'Email',
|
||||
validations: [
|
||||
fieldValidationDefinitions: [
|
||||
{
|
||||
rule: 'function',
|
||||
isValid: () => false,
|
||||
@ -63,8 +63,11 @@ describe('addErrorsAndRunHooks', () => {
|
||||
fieldType: { type: 'input' },
|
||||
};
|
||||
|
||||
const validData: Data<'name' | 'age'> = { name: 'John', age: '30' };
|
||||
const dataWithoutNameAndInvalidAge: Data<'name' | 'age'> = {
|
||||
const validData: ImportedStructuredRow<'name' | 'age'> = {
|
||||
name: 'John',
|
||||
age: '30',
|
||||
};
|
||||
const dataWithoutNameAndInvalidAge: ImportedStructuredRow<'name' | 'age'> = {
|
||||
name: '',
|
||||
age: 'Invalid',
|
||||
};
|
||||
@ -74,7 +77,7 @@ describe('addErrorsAndRunHooks', () => {
|
||||
country: 'Brazil',
|
||||
};
|
||||
|
||||
const data: Data<'name' | 'age'>[] = [
|
||||
const data: ImportedStructuredRow<'name' | 'age'>[] = [
|
||||
validData,
|
||||
dataWithoutNameAndInvalidAge,
|
||||
];
|
||||
@ -180,7 +183,12 @@ describe('addErrorsAndRunHooks', () => {
|
||||
it('should not add errors for unique field with empty values if allowEmpty is true', () => {
|
||||
const result = addErrorsAndRunHooks(
|
||||
[{ country: '' }, { country: '' }],
|
||||
[{ ...uniqueField, validations: [{ rule: 'unique', allowEmpty: true }] }],
|
||||
[
|
||||
{
|
||||
...uniqueField,
|
||||
fieldValidationDefinitions: [{ rule: 'unique', allowEmpty: true }],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(result[0].__errors).toBeUndefined();
|
||||
|
||||
@ -2,7 +2,7 @@ import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Field, Validation } from '@/spreadsheet-import/types';
|
||||
import { Field, FieldValidationDefinition } from '@/spreadsheet-import/types';
|
||||
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
||||
|
||||
const nameField: Field<'Name'> = {
|
||||
@ -22,9 +22,15 @@ const ageField: Field<'Age'> = {
|
||||
type: 'input',
|
||||
},
|
||||
};
|
||||
const validations: Validation[] = [{ rule: 'required' }];
|
||||
const nameFieldWithValidations: Field<'Name'> = { ...nameField, validations };
|
||||
const ageFieldWithValidations: Field<'Age'> = { ...ageField, validations };
|
||||
const validations: FieldValidationDefinition[] = [{ rule: 'required' }];
|
||||
const nameFieldWithValidations: Field<'Name'> = {
|
||||
...nameField,
|
||||
fieldValidationDefinitions: validations,
|
||||
};
|
||||
const ageFieldWithValidations: Field<'Age'> = {
|
||||
...ageField,
|
||||
fieldValidationDefinitions: validations,
|
||||
};
|
||||
|
||||
type ColumnValues = 'Name' | 'Age';
|
||||
|
||||
|
||||
@ -3,11 +3,11 @@ import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
Errors,
|
||||
Meta,
|
||||
ImportedStructuredRowMetadata,
|
||||
} from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
||||
import {
|
||||
Data,
|
||||
Fields,
|
||||
ImportedStructuredRow,
|
||||
Info,
|
||||
RowHook,
|
||||
TableHook,
|
||||
@ -16,11 +16,11 @@ import { isDefined } from '~/utils/isDefined';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const addErrorsAndRunHooks = <T extends string>(
|
||||
data: (Data<T> & Partial<Meta>)[],
|
||||
data: (ImportedStructuredRow<T> & Partial<ImportedStructuredRowMetadata>)[],
|
||||
fields: Fields<T>,
|
||||
rowHook?: RowHook<T>,
|
||||
tableHook?: TableHook<T>,
|
||||
): (Data<T> & Meta)[] => {
|
||||
): (ImportedStructuredRow<T> & ImportedStructuredRowMetadata)[] => {
|
||||
const errors: Errors = {};
|
||||
|
||||
const addHookError = (rowIndex: number, fieldKey: T, error: Info) => {
|
||||
@ -41,8 +41,8 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
field.validations?.forEach((validation) => {
|
||||
switch (validation.rule) {
|
||||
field.fieldValidationDefinitions?.forEach((fieldValidationDefinition) => {
|
||||
switch (fieldValidationDefinition.rule) {
|
||||
case 'unique': {
|
||||
const values = data.map((entry) => entry[field.key as T]);
|
||||
|
||||
@ -51,7 +51,7 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
|
||||
values.forEach((value) => {
|
||||
if (
|
||||
validation.allowEmpty === true &&
|
||||
fieldValidationDefinition.allowEmpty === true &&
|
||||
(isUndefinedOrNull(value) || value === '' || !value)
|
||||
) {
|
||||
// If allowEmpty is set, we will not validate falsy fields such as undefined or empty string.
|
||||
@ -70,8 +70,10 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
errors[index] = {
|
||||
...errors[index],
|
||||
[field.key]: {
|
||||
level: validation.level || 'error',
|
||||
message: validation.errorMessage || 'Field must be unique',
|
||||
level: fieldValidationDefinition.level || 'error',
|
||||
message:
|
||||
fieldValidationDefinition.errorMessage ||
|
||||
'Field must be unique',
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -88,8 +90,10 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
errors[index] = {
|
||||
...errors[index],
|
||||
[field.key]: {
|
||||
level: validation.level || 'error',
|
||||
message: validation.errorMessage || 'Field is required',
|
||||
level: fieldValidationDefinition.level || 'error',
|
||||
message:
|
||||
fieldValidationDefinition.errorMessage ||
|
||||
'Field is required',
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -97,7 +101,10 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
break;
|
||||
}
|
||||
case 'regex': {
|
||||
const regex = new RegExp(validation.value, validation.flags);
|
||||
const regex = new RegExp(
|
||||
fieldValidationDefinition.value,
|
||||
fieldValidationDefinition.flags,
|
||||
);
|
||||
data.forEach((entry, index) => {
|
||||
const value = entry[field.key]?.toString();
|
||||
|
||||
@ -105,10 +112,10 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
errors[index] = {
|
||||
...errors[index],
|
||||
[field.key]: {
|
||||
level: validation.level || 'error',
|
||||
level: fieldValidationDefinition.level || 'error',
|
||||
message:
|
||||
validation.errorMessage ||
|
||||
`Field did not match the regex /${validation.value}/${validation.flags} `,
|
||||
fieldValidationDefinition.errorMessage ||
|
||||
`Field did not match the regex /${fieldValidationDefinition.value}/${fieldValidationDefinition.flags} `,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -119,12 +126,17 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
data.forEach((entry, index) => {
|
||||
const value = entry[field.key]?.toString();
|
||||
|
||||
if (isNonEmptyString(value) && !validation.isValid(value)) {
|
||||
if (
|
||||
isNonEmptyString(value) &&
|
||||
!fieldValidationDefinition.isValid(value)
|
||||
) {
|
||||
errors[index] = {
|
||||
...errors[index],
|
||||
[field.key]: {
|
||||
level: validation.level || 'error',
|
||||
message: validation.errorMessage || 'Field is invalid',
|
||||
level: fieldValidationDefinition.level || 'error',
|
||||
message:
|
||||
fieldValidationDefinition.errorMessage ||
|
||||
'Field is invalid',
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -140,7 +152,8 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
if (!('__index' in value)) {
|
||||
value.__index = v4();
|
||||
}
|
||||
const newValue = value as Data<T> & Meta;
|
||||
const newValue = value as ImportedStructuredRow<T> &
|
||||
ImportedStructuredRowMetadata;
|
||||
|
||||
if (isDefined(errors[index])) {
|
||||
return { ...newValue, __errors: errors[index] };
|
||||
|
||||
@ -8,7 +8,9 @@ export const findUnmatchedRequiredFields = <T extends string>(
|
||||
fields
|
||||
.filter(
|
||||
(field) =>
|
||||
field.validations?.some((validation) => validation.rule === 'required'),
|
||||
field.fieldValidationDefinitions?.some(
|
||||
(validation) => validation.rule === 'required',
|
||||
),
|
||||
)
|
||||
.filter(
|
||||
(field) =>
|
||||
|
||||
@ -2,13 +2,17 @@ import {
|
||||
Columns,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { Data, Fields, RawData } from '@/spreadsheet-import/types';
|
||||
import {
|
||||
Fields,
|
||||
ImportedRow,
|
||||
ImportedStructuredRow,
|
||||
} from '@/spreadsheet-import/types';
|
||||
|
||||
import { normalizeCheckboxValue } from './normalizeCheckboxValue';
|
||||
|
||||
export const normalizeTableData = <T extends string>(
|
||||
columns: Columns<T>,
|
||||
data: RawData[],
|
||||
data: ImportedRow[],
|
||||
fields: Fields<T>,
|
||||
) =>
|
||||
data.map((row) =>
|
||||
@ -63,5 +67,5 @@ export const normalizeTableData = <T extends string>(
|
||||
default:
|
||||
return acc;
|
||||
}
|
||||
}, {} as Data<T>),
|
||||
}, {} as ImportedStructuredRow<T>),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user