Import company and person from csv file (#1236)
* feat: wip implement back-end call csv import * fix: rebase IconBrandTwitter missing * feat: person and company csv import * fix: test & clean * fix: clean & test
This commit is contained in:
@ -195,6 +195,7 @@ export const MatchColumnSelect = ({
|
||||
value?.value !== option.value &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
key={option.value}
|
||||
anchorSelect={`#${option.value}`}
|
||||
content="You are already importing this column."
|
||||
place="right"
|
||||
@ -1,7 +1,7 @@
|
||||
import type React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { Modal } from '@/ui/modal/components/Modal';
|
||||
|
||||
import { ModalCloseButton } from './ModalCloseButton';
|
||||
@ -27,7 +27,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const ModalWrapper = ({ children, isOpen, onClose }: Props) => {
|
||||
const { rtl } = useRsi();
|
||||
const { rtl } = useSpreadsheetImportInternal();
|
||||
|
||||
return (
|
||||
<StyledModal isOpen={isOpen}>
|
||||
@ -1,25 +1,21 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { RsiProps } from '@/spreadsheet-import/types';
|
||||
import type { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
|
||||
export const RsiContext = createContext({} as any);
|
||||
|
||||
type ProvidersProps<T extends string> = {
|
||||
children: React.ReactNode;
|
||||
rsiValues: RsiProps<T>;
|
||||
values: SpreadsheetOptions<T>;
|
||||
};
|
||||
|
||||
export const rootId = 'chakra-modal-rsi';
|
||||
|
||||
export const Providers = <T extends string>({
|
||||
children,
|
||||
rsiValues,
|
||||
values,
|
||||
}: ProvidersProps<T>) => {
|
||||
if (!rsiValues.fields) {
|
||||
if (!values.fields) {
|
||||
throw new Error('Fields must be provided to spreadsheet-import');
|
||||
}
|
||||
|
||||
return (
|
||||
<RsiContext.Provider value={rsiValues}>{children}</RsiContext.Provider>
|
||||
);
|
||||
return <RsiContext.Provider value={values}>{children}</RsiContext.Provider>;
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
import type { RsiProps } from '../types';
|
||||
|
||||
import { ModalWrapper } from './core/ModalWrapper';
|
||||
import { Providers } from './core/Providers';
|
||||
import { Steps } from './steps/Steps';
|
||||
|
||||
export const defaultRSIProps: Partial<RsiProps<any>> = {
|
||||
autoMapHeaders: true,
|
||||
allowInvalidSubmit: true,
|
||||
autoMapDistance: 2,
|
||||
uploadStepHook: async (value) => value,
|
||||
selectHeaderStepHook: async (headerValues, data) => ({ headerValues, data }),
|
||||
matchColumnsStepHook: async (table) => table,
|
||||
dateFormat: 'yyyy-mm-dd', // ISO 8601,
|
||||
parseRaw: true,
|
||||
} as const;
|
||||
|
||||
export const SpreadsheetImport = <T extends string>(props: RsiProps<T>) => {
|
||||
return (
|
||||
<Providers rsiValues={props}>
|
||||
<ModalWrapper isOpen={props.isOpen} onClose={props.onClose}>
|
||||
<Steps />
|
||||
</ModalWrapper>
|
||||
</Providers>
|
||||
);
|
||||
};
|
||||
|
||||
SpreadsheetImport.defaultProps = defaultRSIProps;
|
||||
@ -1,7 +1,7 @@
|
||||
import DataGrid, { DataGridProps } from 'react-data-grid';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { rgba } from '@/ui/theme/constants/colors';
|
||||
|
||||
const StyledDataGrid = styled(DataGrid)`
|
||||
@ -112,7 +112,7 @@ type Props<Data> = DataGridProps<Data> & {
|
||||
};
|
||||
|
||||
export const Table = <Data,>(props: Props<Data>) => {
|
||||
const { rtl } = useRsi();
|
||||
const { rtl } = useSpreadsheetImportInternal();
|
||||
|
||||
return (
|
||||
<StyledDataGrid direction={rtl ? 'rtl' : 'ltr'} rowHeight={52} {...props} />
|
||||
@ -1,11 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { SetRequired } from 'type-fest';
|
||||
|
||||
import { RsiContext } from '@/spreadsheet-import/components/core/Providers';
|
||||
import { defaultRSIProps } from '@/spreadsheet-import/components/SpreadsheetImport';
|
||||
import { RsiProps } from '@/spreadsheet-import/types';
|
||||
|
||||
export const useRsi = <T extends string>() =>
|
||||
useContext<SetRequired<RsiProps<T>, keyof typeof defaultRSIProps>>(
|
||||
RsiContext,
|
||||
);
|
||||
@ -1,13 +1,13 @@
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
|
||||
import { RsiProps } from '@/spreadsheet-import/types';
|
||||
import { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
|
||||
export function useSpreadsheetImport() {
|
||||
export function useSpreadsheetImport<T extends string>() {
|
||||
const setSpreadSheetImport = useSetRecoilState(spreadsheetImportState);
|
||||
|
||||
const openSpreadsheetImport = (
|
||||
options: Omit<RsiProps<string>, 'isOpen' | 'onClose'>,
|
||||
options: Omit<SpreadsheetOptions<T>, 'isOpen' | 'onClose'>,
|
||||
) => {
|
||||
setSpreadSheetImport({
|
||||
isOpen: true,
|
||||
@ -1,8 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { StepType } from '@/spreadsheet-import/components/steps/UploadFlow';
|
||||
import { StepType } from '@/spreadsheet-import/steps/components/UploadFlow';
|
||||
|
||||
export const useRsiInitialStep = (initialStep?: StepType) => {
|
||||
export const useSpreadsheetImportInitialStep = (initialStep?: StepType) => {
|
||||
const steps = ['uploadStep', 'matchColumnsStep', 'validationStep'] as const;
|
||||
|
||||
const initialStepNumber = useMemo(() => {
|
||||
@ -0,0 +1,14 @@
|
||||
import { useContext } from 'react';
|
||||
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';
|
||||
|
||||
export const useSpreadsheetImportInternal = <T extends string>() =>
|
||||
useContext<
|
||||
SetRequired<
|
||||
SpreadsheetOptions<T>,
|
||||
keyof typeof defaultSpreadsheetImportProps
|
||||
>
|
||||
>(RsiContext);
|
||||
@ -0,0 +1,29 @@
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/Providers';
|
||||
import { Steps } from '@/spreadsheet-import/steps/components/Steps';
|
||||
import type { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
|
||||
export const defaultSpreadsheetImportProps: Partial<SpreadsheetOptions<any>> = {
|
||||
autoMapHeaders: true,
|
||||
allowInvalidSubmit: true,
|
||||
autoMapDistance: 2,
|
||||
uploadStepHook: async (value) => value,
|
||||
selectHeaderStepHook: async (headerValues, data) => ({ headerValues, data }),
|
||||
matchColumnsStepHook: async (table) => table,
|
||||
dateFormat: 'yyyy-mm-dd', // ISO 8601,
|
||||
parseRaw: true,
|
||||
} as const;
|
||||
|
||||
export const SpreadsheetImport = <T extends string>(
|
||||
props: SpreadsheetOptions<T>,
|
||||
) => {
|
||||
return (
|
||||
<Providers values={props}>
|
||||
<ModalWrapper isOpen={props.isOpen} onClose={props.onClose}>
|
||||
<Steps />
|
||||
</ModalWrapper>
|
||||
</Providers>
|
||||
);
|
||||
};
|
||||
|
||||
SpreadsheetImport.defaultProps = defaultSpreadsheetImportProps;
|
||||
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { spreadsheetImportState } from '../states/spreadsheetImportState';
|
||||
import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetImportState';
|
||||
|
||||
import { SpreadsheetImport } from './SpreadsheetImport';
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
import { RsiProps } from '../types';
|
||||
import { SpreadsheetOptions } from '../types';
|
||||
|
||||
export type SpreadsheetImportState<T extends string> = {
|
||||
isOpen: boolean;
|
||||
options: Omit<RsiProps<T>, 'isOpen' | 'onClose'> | null;
|
||||
options: Omit<SpreadsheetOptions<T>, 'isOpen' | 'onClose'> | null;
|
||||
};
|
||||
|
||||
export const spreadsheetImportState = atom<SpreadsheetImportState<string>>({
|
||||
export const spreadsheetImportState = atom<SpreadsheetImportState<any>>({
|
||||
key: 'spreadsheetImportState',
|
||||
default: {
|
||||
isOpen: false,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton';
|
||||
import { Heading } from '@/spreadsheet-import/components/core/Heading';
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton';
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import type { Field, RawData } from '@/spreadsheet-import/types';
|
||||
import { findUnmatchedRequiredFields } from '@/spreadsheet-import/utils/findUnmatchedRequiredFields';
|
||||
import { getMatchedColumns } from '@/spreadsheet-import/utils/getMatchedColumns';
|
||||
@ -114,7 +114,8 @@ export const MatchColumnsStep = <T extends string>({
|
||||
const { enqueueDialog } = useDialog();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const dataExample = data.slice(0, 2);
|
||||
const { fields, autoMapHeaders, autoMapDistance } = useRsi<T>();
|
||||
const { fields, autoMapHeaders, autoMapDistance } =
|
||||
useSpreadsheetImportInternal<T>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [columns, setColumns] = useState<Columns<T>>(
|
||||
// Do not remove spread, it indexes empty array elements, otherwise map() skips over them
|
||||
@ -1,7 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/core/MatchColumnSelect';
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { getFieldOptions } from '@/spreadsheet-import/utils/getFieldOptions';
|
||||
|
||||
@ -35,7 +35,7 @@ export const SubMatchingSelect = <T extends string>({
|
||||
column,
|
||||
onSubChange,
|
||||
}: Props<T>) => {
|
||||
const { fields } = useRsi<T>();
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const options = getFieldOptions(fields, column.value) as SelectOption[];
|
||||
const value = options.find((opt) => opt.value === option.value);
|
||||
|
||||
@ -8,8 +8,8 @@ import {
|
||||
} from '@chakra-ui/accordion';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/core/MatchColumnSelect';
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import type { Fields } from '@/spreadsheet-import/types';
|
||||
import { IconChevronDown, IconForbid } from '@/ui/icon';
|
||||
|
||||
@ -86,7 +86,7 @@ export const TemplateColumn = <T extends string>({
|
||||
onChange,
|
||||
onSubChange,
|
||||
}: TemplateColumnProps<T>) => {
|
||||
const { fields } = useRsi<T>();
|
||||
const { fields } = useSpreadsheetImportInternal<T>();
|
||||
const column = columns[columnIndex];
|
||||
const isIgnored = column.type === ColumnType.ignored;
|
||||
const isSelect = 'matchedOptions' in column;
|
||||
@ -1,8 +1,8 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton';
|
||||
import { Heading } from '@/spreadsheet-import/components/core/Heading';
|
||||
import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton';
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import type { RawData } from '@/spreadsheet-import/types';
|
||||
import { Modal } from '@/ui/modal/components/Modal';
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Table } from '@/spreadsheet-import/components/core/Table';
|
||||
import { Table } from '@/spreadsheet-import/components/Table';
|
||||
import type { RawData } from '@/spreadsheet-import/types';
|
||||
|
||||
import { generateSelectionColumns } from './SelectColumn';
|
||||
@ -1,13 +1,12 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Heading } from '@/spreadsheet-import/components/core/Heading';
|
||||
import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton';
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { Radio } from '@/ui/input/radio/components/Radio';
|
||||
import { RadioGroup } from '@/ui/input/radio/components/RadioGroup';
|
||||
import { Modal } from '@/ui/modal/components/Modal';
|
||||
|
||||
import { ContinueButton } from '../../core/ContinueButton';
|
||||
|
||||
const Content = styled(Modal.Content)`
|
||||
align-items: center;
|
||||
`;
|
||||
@ -1,7 +1,7 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { useRsiInitialStep } from '@/spreadsheet-import/hooks/useRsiInitialStep';
|
||||
import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { Modal } from '@/ui/modal/components/Modal';
|
||||
import { StepBar } from '@/ui/step-bar/components/StepBar';
|
||||
import { useStepBar } from '@/ui/step-bar/hooks/useStepBar';
|
||||
@ -24,9 +24,11 @@ const stepTitles = {
|
||||
} as const;
|
||||
|
||||
export const Steps = () => {
|
||||
const { initialStepState } = useRsi();
|
||||
const { initialStepState } = useSpreadsheetImportInternal();
|
||||
|
||||
const { steps, initialStep } = useRsiInitialStep(initialStepState?.type);
|
||||
const { steps, initialStep } = useSpreadsheetImportInitialStep(
|
||||
initialStepState?.type,
|
||||
);
|
||||
|
||||
const { nextStep, activeStep } = useStepBar({
|
||||
initialStep,
|
||||
@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import type XLSX from 'xlsx-ugnis';
|
||||
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import type { RawData } from '@/spreadsheet-import/types';
|
||||
import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords';
|
||||
import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook';
|
||||
@ -29,6 +29,7 @@ export enum StepType {
|
||||
selectHeader = 'selectHeader',
|
||||
matchColumns = 'matchColumns',
|
||||
validateData = 'validateData',
|
||||
loading = 'loading',
|
||||
}
|
||||
export type StepState =
|
||||
| {
|
||||
@ -50,6 +51,9 @@ export type StepState =
|
||||
| {
|
||||
type: StepType.validateData;
|
||||
data: any[];
|
||||
}
|
||||
| {
|
||||
type: StepType.loading;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@ -58,7 +62,7 @@ interface Props {
|
||||
|
||||
export const UploadFlow = ({ nextStep }: Props) => {
|
||||
const theme = useTheme();
|
||||
const { initialStepState } = useRsi();
|
||||
const { initialStepState } = useSpreadsheetImportInternal();
|
||||
const [state, setState] = useState<StepState>(
|
||||
initialStepState || { type: StepType.upload },
|
||||
);
|
||||
@ -68,7 +72,7 @@ export const UploadFlow = ({ nextStep }: Props) => {
|
||||
uploadStepHook,
|
||||
selectHeaderStepHook,
|
||||
matchColumnsStepHook,
|
||||
} = useRsi();
|
||||
} = useSpreadsheetImportInternal();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const errorToast = useCallback(
|
||||
@ -191,7 +195,18 @@ export const UploadFlow = ({ nextStep }: Props) => {
|
||||
if (!uploadedFile) {
|
||||
throw new Error('File not found');
|
||||
}
|
||||
return <ValidationStep initialData={state.data} file={uploadedFile} />;
|
||||
return (
|
||||
<ValidationStep
|
||||
initialData={state.data}
|
||||
file={uploadedFile}
|
||||
onSubmitStart={() =>
|
||||
setState({
|
||||
type: StepType.loading,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
case StepType.loading:
|
||||
default:
|
||||
return (
|
||||
<ProgressBarContainer>
|
||||
@ -3,7 +3,7 @@ import { useDropzone } from 'react-dropzone';
|
||||
import styled from '@emotion/styled';
|
||||
import * as XLSX from 'xlsx-ugnis';
|
||||
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync';
|
||||
import { MainButton } from '@/ui/button/components/MainButton';
|
||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||
@ -83,7 +83,7 @@ type DropZoneProps = {
|
||||
};
|
||||
|
||||
export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
|
||||
const { maxFileSize, dateFormat, parseRaw } = useRsi();
|
||||
const { maxFileSize, dateFormat, parseRaw } = useSpreadsheetImportInternal();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Table } from '@/spreadsheet-import/components/core/Table';
|
||||
import { Table } from '@/spreadsheet-import/components/Table';
|
||||
import type { Fields } from '@/spreadsheet-import/types';
|
||||
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
|
||||
|
||||
@ -2,10 +2,10 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
import type { RowsChangeData } from 'react-data-grid';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ContinueButton } from '@/spreadsheet-import/components/core/ContinueButton';
|
||||
import { Heading } from '@/spreadsheet-import/components/core/Heading';
|
||||
import { Table } from '@/spreadsheet-import/components/core/Table';
|
||||
import { useRsi } from '@/spreadsheet-import/hooks/useRsi';
|
||||
import { ContinueButton } from '@/spreadsheet-import/components/ContinueButton';
|
||||
import { Heading } from '@/spreadsheet-import/components/Heading';
|
||||
import { Table } from '@/spreadsheet-import/components/Table';
|
||||
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
|
||||
import type { Data } from '@/spreadsheet-import/types';
|
||||
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
|
||||
import { Button, ButtonVariant } from '@/ui/button/components/Button';
|
||||
@ -56,14 +56,17 @@ const NoRowsContainer = styled.div`
|
||||
type Props<T extends string> = {
|
||||
initialData: Data<T>[];
|
||||
file: File;
|
||||
onSubmitStart?: () => void;
|
||||
};
|
||||
|
||||
export const ValidationStep = <T extends string>({
|
||||
initialData,
|
||||
file,
|
||||
onSubmitStart,
|
||||
}: Props<T>) => {
|
||||
const { enqueueDialog } = useDialog();
|
||||
const { fields, onClose, onSubmit, rowHook, tableHook } = useRsi<T>();
|
||||
const { fields, onClose, onSubmit, rowHook, tableHook } =
|
||||
useSpreadsheetImportInternal<T>();
|
||||
|
||||
const [data, setData] = useState<(Data<T> & Meta)[]>(
|
||||
useMemo(
|
||||
@ -146,7 +149,8 @@ export const ValidationStep = <T extends string>({
|
||||
},
|
||||
{ validData: [] as Data<T>[], invalidData: [] as Data<T>[], all: data },
|
||||
);
|
||||
onSubmit(calculatedData, file);
|
||||
onSubmitStart?.();
|
||||
await onSubmit(calculatedData, file);
|
||||
onClose();
|
||||
};
|
||||
const onContinue = () => {
|
||||
@ -2,7 +2,7 @@ import { Column, useRowSelection } from 'react-data-grid';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/core/MatchColumnSelect';
|
||||
import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect';
|
||||
import type { Data, Fields } from '@/spreadsheet-import/types';
|
||||
import {
|
||||
Checkbox,
|
||||
@ -118,14 +118,13 @@ export const generateColumns = <T extends string>(
|
||||
),
|
||||
editable: column.fieldType.type !== 'checkbox',
|
||||
editor: ({ row, onRowChange, onClose }) => {
|
||||
const columnKey = column.key as keyof (Data<T> & Meta);
|
||||
let component;
|
||||
|
||||
switch (column.fieldType.type) {
|
||||
case 'select': {
|
||||
const value = column.fieldType.options.find(
|
||||
(option) =>
|
||||
option.value ===
|
||||
(row[column.key as keyof (Data<T> & Meta)] as string),
|
||||
(option) => option.value === (row[columnKey] as string),
|
||||
);
|
||||
|
||||
component = (
|
||||
@ -139,7 +138,7 @@ export const generateColumns = <T extends string>(
|
||||
: value
|
||||
}
|
||||
onChange={(value) => {
|
||||
onRowChange({ ...row, [column.key]: value?.value }, true);
|
||||
onRowChange({ ...row, [columnKey]: value?.value }, true);
|
||||
}}
|
||||
options={column.fieldType.options}
|
||||
/>
|
||||
@ -149,9 +148,9 @@ export const generateColumns = <T extends string>(
|
||||
default:
|
||||
component = (
|
||||
<TextInput
|
||||
value={row[column.key] as string}
|
||||
value={row[columnKey] as string}
|
||||
onChange={(value: string) => {
|
||||
onRowChange({ ...row, [column.key]: value });
|
||||
onRowChange({ ...row, [columnKey]: value });
|
||||
}}
|
||||
autoFocus={true}
|
||||
onBlur={() => onClose(true)}
|
||||
@ -165,23 +164,24 @@ export const generateColumns = <T extends string>(
|
||||
editOnClick: true,
|
||||
},
|
||||
formatter: ({ row, onRowChange }) => {
|
||||
const columnKey = column.key as keyof (Data<T> & Meta);
|
||||
let component;
|
||||
|
||||
switch (column.fieldType.type) {
|
||||
case 'checkbox':
|
||||
component = (
|
||||
<ToggleContainer
|
||||
id={`${column.key}-${row.__index}`}
|
||||
id={`${columnKey}-${row.__index}`}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Toggle
|
||||
value={row[column.key] as boolean}
|
||||
value={row[columnKey] as boolean}
|
||||
onChange={() => {
|
||||
onRowChange({
|
||||
...row,
|
||||
[column.key]: !row[column.key as T],
|
||||
[columnKey]: !row[columnKey],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
@ -190,30 +190,30 @@ export const generateColumns = <T extends string>(
|
||||
break;
|
||||
case 'select':
|
||||
component = (
|
||||
<DefaultContainer id={`${column.key}-${row.__index}`}>
|
||||
<DefaultContainer id={`${columnKey}-${row.__index}`}>
|
||||
{column.fieldType.options.find(
|
||||
(option) => option.value === row[column.key as T],
|
||||
(option) => option.value === row[columnKey as T],
|
||||
)?.label || null}
|
||||
</DefaultContainer>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
component = (
|
||||
<DefaultContainer id={`${column.key}-${row.__index}`}>
|
||||
{row[column.key as T]}
|
||||
<DefaultContainer id={`${columnKey}-${row.__index}`}>
|
||||
{row[columnKey]}
|
||||
</DefaultContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (row.__errors?.[column.key]) {
|
||||
if (row.__errors?.[columnKey]) {
|
||||
return (
|
||||
<>
|
||||
{component}
|
||||
{createPortal(
|
||||
<AppTooltip
|
||||
anchorSelect={`#${column.key}-${row.__index}`}
|
||||
anchorSelect={`#${columnKey}-${row.__index}`}
|
||||
place="top"
|
||||
content={row.__errors?.[column.key]?.message}
|
||||
content={row.__errors?.[columnKey]?.message}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
@ -1,8 +1,8 @@
|
||||
import { Meta } from '@storybook/react';
|
||||
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/core/Providers';
|
||||
import { MatchColumnsStep } from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/Providers';
|
||||
import { MatchColumnsStep } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
|
||||
|
||||
const meta: Meta<typeof MatchColumnsStep> = {
|
||||
@ -58,7 +58,7 @@ const mockData = [
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<Providers rsiValues={mockRsiValues}>
|
||||
<Providers values={mockRsiValues}>
|
||||
<ModalWrapper isOpen={true} onClose={() => null}>
|
||||
<MatchColumnsStep
|
||||
headerValues={mockData[0] as string[]}
|
||||
@ -1,8 +1,8 @@
|
||||
import { Meta } from '@storybook/react';
|
||||
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/core/Providers';
|
||||
import { SelectHeaderStep } from '@/spreadsheet-import/components/steps/SelectHeaderStep/SelectHeaderStep';
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/Providers';
|
||||
import { SelectHeaderStep } from '@/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep';
|
||||
import {
|
||||
headerSelectionTableFields,
|
||||
mockRsiValues,
|
||||
@ -20,7 +20,7 @@ export default meta;
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<Providers rsiValues={mockRsiValues}>
|
||||
<Providers values={mockRsiValues}>
|
||||
<ModalWrapper isOpen={true} onClose={() => null}>
|
||||
<SelectHeaderStep
|
||||
data={headerSelectionTableFields}
|
||||
@ -1,8 +1,8 @@
|
||||
import { Meta } from '@storybook/react';
|
||||
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/core/Providers';
|
||||
import { SelectSheetStep } from '@/spreadsheet-import/components/steps/SelectSheetStep/SelectSheetStep';
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/Providers';
|
||||
import { SelectSheetStep } from '@/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep';
|
||||
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
|
||||
|
||||
const meta: Meta<typeof SelectSheetStep> = {
|
||||
@ -19,7 +19,7 @@ const sheetNames = ['Sheet1', 'Sheet2', 'Sheet3'];
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<Providers rsiValues={mockRsiValues}>
|
||||
<Providers values={mockRsiValues}>
|
||||
<ModalWrapper isOpen={true} onClose={() => null}>
|
||||
<SelectSheetStep
|
||||
sheetNames={sheetNames}
|
||||
@ -1,8 +1,8 @@
|
||||
import { Meta } from '@storybook/react';
|
||||
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/core/Providers';
|
||||
import { UploadStep } from '@/spreadsheet-import/components/steps/UploadStep/UploadStep';
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/Providers';
|
||||
import { UploadStep } from '@/spreadsheet-import/steps/components/UploadStep/UploadStep';
|
||||
import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues';
|
||||
|
||||
const meta: Meta<typeof UploadStep> = {
|
||||
@ -17,7 +17,7 @@ export default meta;
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<Providers rsiValues={mockRsiValues}>
|
||||
<Providers values={mockRsiValues}>
|
||||
<ModalWrapper isOpen={true} onClose={() => null}>
|
||||
<UploadStep onContinue={() => Promise.resolve()} />
|
||||
</ModalWrapper>
|
||||
@ -1,8 +1,8 @@
|
||||
import { Meta } from '@storybook/react';
|
||||
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/core/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/core/Providers';
|
||||
import { ValidationStep } from '@/spreadsheet-import/components/steps/ValidationStep/ValidationStep';
|
||||
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
|
||||
import { Providers } from '@/spreadsheet-import/components/Providers';
|
||||
import { ValidationStep } from '@/spreadsheet-import/steps/components/ValidationStep/ValidationStep';
|
||||
import {
|
||||
editableTableInitialData,
|
||||
mockRsiValues,
|
||||
@ -22,7 +22,7 @@ const file = new File([''], 'file.csv');
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<Providers rsiValues={mockRsiValues}>
|
||||
<Providers values={mockRsiValues}>
|
||||
<ModalWrapper isOpen={true} onClose={() => null}>
|
||||
<ValidationStep initialData={editableTableInitialData} file={file} />
|
||||
</ModalWrapper>
|
||||
@ -1,5 +1,5 @@
|
||||
import { defaultRSIProps } from '@/spreadsheet-import/components/SpreadsheetImport';
|
||||
import type { RsiProps } from '@/spreadsheet-import/types';
|
||||
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
|
||||
import type { SpreadsheetOptions } from '@/spreadsheet-import/types';
|
||||
|
||||
const fields = [
|
||||
{
|
||||
@ -87,13 +87,14 @@ const fields = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
const mockComponentBehaviourForTypes = <T extends string>(props: RsiProps<T>) =>
|
||||
props;
|
||||
const mockComponentBehaviourForTypes = <T extends string>(
|
||||
props: SpreadsheetOptions<T>,
|
||||
) => props;
|
||||
|
||||
export const mockRsiValues = mockComponentBehaviourForTypes({
|
||||
...defaultRSIProps,
|
||||
...defaultSpreadsheetImportProps,
|
||||
fields: fields,
|
||||
onSubmit: (data) => {
|
||||
onSubmit: async (data) => {
|
||||
console.log(data.all.map((value) => value));
|
||||
},
|
||||
isOpen: true,
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import { Columns } from '../components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
import { StepState } from '../components/steps/UploadFlow';
|
||||
import { Meta } from '../components/steps/ValidationStep/types';
|
||||
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';
|
||||
|
||||
export type RsiProps<T extends string> = {
|
||||
export type SpreadsheetOptions<Keys extends string> = {
|
||||
// Is modal visible.
|
||||
isOpen: boolean;
|
||||
// callback when RSI is closed before final submit
|
||||
onClose: () => void;
|
||||
// Field description for requested data
|
||||
fields: Fields<T>;
|
||||
fields: Fields<Keys>;
|
||||
// Runs after file upload step, receives and returns raw sheet data
|
||||
uploadStepHook?: (data: RawData[]) => Promise<RawData[]>;
|
||||
// Runs after header selection step, receives and returns raw sheet data
|
||||
@ -20,16 +20,16 @@ export type RsiProps<T extends string> = {
|
||||
) => Promise<{ headerValues: RawData; data: RawData[] }>;
|
||||
// Runs once before validation step, used for data mutations and if you want to change how columns were matched
|
||||
matchColumnsStepHook?: (
|
||||
table: Data<T>[],
|
||||
table: Data<Keys>[],
|
||||
rawData: RawData[],
|
||||
columns: Columns<T>,
|
||||
) => Promise<Data<T>[]>;
|
||||
columns: Columns<Keys>,
|
||||
) => Promise<Data<Keys>[]>;
|
||||
// Runs after column matching and on entry change
|
||||
rowHook?: RowHook<T>;
|
||||
rowHook?: RowHook<Keys>;
|
||||
// Runs after column matching and on entry change
|
||||
tableHook?: TableHook<T>;
|
||||
tableHook?: TableHook<Keys>;
|
||||
// Function called after user finishes the flow
|
||||
onSubmit: (data: Result<T>, file: File) => void;
|
||||
onSubmit: (data: Result<Keys>, file: File) => Promise<void>;
|
||||
// Allows submitting with errors. Default: true
|
||||
allowInvalidSubmit?: boolean;
|
||||
// Theme configuration passed to underlying Chakra-UI
|
||||
@ -110,7 +110,8 @@ export type Input = {
|
||||
export type Validation =
|
||||
| RequiredValidation
|
||||
| UniqueValidation
|
||||
| RegexValidation;
|
||||
| RegexValidation
|
||||
| FunctionValidation;
|
||||
|
||||
export type RequiredValidation = {
|
||||
rule: 'required';
|
||||
@ -133,6 +134,13 @@ export type RegexValidation = {
|
||||
level?: ErrorLevel;
|
||||
};
|
||||
|
||||
export type FunctionValidation = {
|
||||
rule: 'function';
|
||||
isValid: (value: string) => boolean;
|
||||
errorMessage: string;
|
||||
level?: ErrorLevel;
|
||||
};
|
||||
|
||||
export type RowHook<T extends string> = (
|
||||
row: Data<T>,
|
||||
addError: (fieldKey: T, error: Info) => void,
|
||||
|
||||
@ -3,7 +3,7 @@ import { v4 } from 'uuid';
|
||||
import type {
|
||||
Errors,
|
||||
Meta,
|
||||
} from '@/spreadsheet-import/components/steps/ValidationStep/types';
|
||||
} from '@/spreadsheet-import/steps/components/ValidationStep/types';
|
||||
import type {
|
||||
Data,
|
||||
Fields,
|
||||
@ -93,8 +93,9 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
case 'regex': {
|
||||
const regex = new RegExp(validation.value, validation.flags);
|
||||
data.forEach((entry, index) => {
|
||||
const value = entry[field.key]?.toString() ?? '';
|
||||
if (!value.match(regex)) {
|
||||
const value = entry[field.key]?.toString();
|
||||
|
||||
if (value && !value.match(regex)) {
|
||||
errors[index] = {
|
||||
...errors[index],
|
||||
[field.key]: {
|
||||
@ -108,6 +109,22 @@ export const addErrorsAndRunHooks = <T extends string>(
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'function': {
|
||||
data.forEach((entry, index) => {
|
||||
const value = entry[field.key]?.toString();
|
||||
|
||||
if (value && !validation.isValid(value)) {
|
||||
errors[index] = {
|
||||
...errors[index],
|
||||
[field.key]: {
|
||||
level: validation.level || 'error',
|
||||
message: validation.errorMessage || 'Field is invalid',
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { Columns } from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
import type { Columns } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import type { Fields } from '@/spreadsheet-import/types';
|
||||
|
||||
export const findUnmatchedRequiredFields = <T extends string>(
|
||||
|
||||
@ -4,7 +4,7 @@ import type {
|
||||
Column,
|
||||
Columns,
|
||||
MatchColumnsProps,
|
||||
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import type { Field, Fields } from '@/spreadsheet-import/types';
|
||||
|
||||
import { findMatch } from './findMatch';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
Columns,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import type { Data, Fields, RawData } from '@/spreadsheet-import/types';
|
||||
|
||||
import { normalizeCheckboxValue } from './normalizeCheckboxValue';
|
||||
|
||||
@ -2,7 +2,7 @@ import {
|
||||
Column,
|
||||
ColumnType,
|
||||
MatchColumnsProps,
|
||||
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
import type { Field } from '@/spreadsheet-import/types';
|
||||
|
||||
import { uniqueEntries } from './uniqueEntries';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {
|
||||
Column,
|
||||
ColumnType,
|
||||
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
|
||||
export const setIgnoreColumn = <T extends string>({
|
||||
header,
|
||||
|
||||
@ -3,7 +3,7 @@ import {
|
||||
MatchedOptions,
|
||||
MatchedSelectColumn,
|
||||
MatchedSelectOptionsColumn,
|
||||
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
|
||||
export const setSubColumn = <T>(
|
||||
oldColumn: MatchedSelectColumn<T> | MatchedSelectOptionsColumn<T>,
|
||||
|
||||
@ -3,7 +3,7 @@ import uniqBy from 'lodash/uniqBy';
|
||||
import type {
|
||||
MatchColumnsProps,
|
||||
MatchedOptions,
|
||||
} from '@/spreadsheet-import/components/steps/MatchColumnsStep/MatchColumnsStep';
|
||||
} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
|
||||
|
||||
export const uniqueEntries = <T extends string>(
|
||||
data: MatchColumnsProps<T>['data'],
|
||||
|
||||
Reference in New Issue
Block a user