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:
Jérémy M
2023-08-16 23:18:16 +02:00
committed by GitHub
parent 5890354d21
commit 8863bb0035
74 changed files with 950 additions and 312 deletions

View File

@ -0,0 +1,34 @@
import { useCallback, useState } from 'react';
import styled from '@emotion/styled';
import type XLSX from 'xlsx-ugnis';
import { Modal } from '@/ui/modal/components/Modal';
import { DropZone } from './components/DropZone';
const Content = styled(Modal.Content)`
padding: ${({ theme }) => theme.spacing(6)};
`;
type UploadProps = {
onContinue: (data: XLSX.WorkBook, file: File) => Promise<void>;
};
export const UploadStep = ({ onContinue }: UploadProps) => {
const [isLoading, setIsLoading] = useState(false);
const handleOnContinue = useCallback(
async (data: XLSX.WorkBook, file: File) => {
setIsLoading(true);
await onContinue(data, file);
setIsLoading(false);
},
[onContinue],
);
return (
<Content>
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
</Content>
);
};

View File

@ -0,0 +1,143 @@
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';
import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync';
import { MainButton } from '@/ui/button/components/MainButton';
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
const Container = styled.div`
align-items: center;
background: ${({ theme }) => `
repeating-linear-gradient(
0deg,
${theme.font.color.primary},
${theme.font.color.primary} 10px,
transparent 10px,
transparent 20px,
${theme.font.color.primary} 20px
),
repeating-linear-gradient(
90deg,
${theme.font.color.primary},
${theme.font.color.primary} 10px,
transparent 10px,
transparent 20px,
${theme.font.color.primary} 20px
),
repeating-linear-gradient(
180deg,
${theme.font.color.primary},
${theme.font.color.primary} 10px,
transparent 10px,
transparent 20px,
${theme.font.color.primary} 20px
),
repeating-linear-gradient(
270deg,
${theme.font.color.primary},
${theme.font.color.primary} 10px,
transparent 10px,
transparent 20px,
${theme.font.color.primary} 20px
);
`};
background-position: 0 0, 0 0, 100% 0, 0 100%;
background-repeat: no-repeat;
background-size: 2px 100%, 100% 2px, 2px 100%, 100% 2px;
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
position: relative;
`;
const Overlay = styled.div`
background: ${({ theme }) => theme.background.transparent.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
bottom: 0px;
left: 0px;
position: absolute;
right: 0px;
top: 0px;
`;
const Text = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
text-align: center;
`;
const Button = styled(MainButton)`
margin-top: ${({ theme }) => theme.spacing(2)};
width: 200px;
`;
type DropZoneProps = {
onContinue: (data: XLSX.WorkBook, file: File) => void;
isLoading: boolean;
};
export const DropZone = ({ onContinue, isLoading }: DropZoneProps) => {
const { maxFileSize, dateFormat, parseRaw } = useSpreadsheetImportInternal();
const [loading, setLoading] = useState(false);
const { enqueueSnackBar } = useSnackBar();
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
noClick: true,
noKeyboard: true,
maxFiles: 1,
maxSize: maxFileSize,
accept: {
'application/vnd.ms-excel': ['.xls'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [
'.xlsx',
],
'text/csv': ['.csv'],
},
onDropRejected: (fileRejections) => {
setLoading(false);
fileRejections.forEach((fileRejection) => {
enqueueSnackBar(fileRejection.errors[0].message, {
title: `${fileRejection.file.name} upload rejected`,
variant: 'error',
});
});
},
onDropAccepted: async ([file]) => {
setLoading(true);
const arrayBuffer = await readFileAsync(file);
const workbook = XLSX.read(arrayBuffer, {
cellDates: true,
dateNF: dateFormat,
raw: parseRaw,
dense: true,
});
setLoading(false);
onContinue(workbook, file);
},
});
return (
<Container {...getRootProps()}>
{isDragActive && <Overlay />}
<input {...getInputProps()} />
{isDragActive ? (
<Text>Drop file here...</Text>
) : loading || isLoading ? (
<Text>Processing...</Text>
) : (
<>
<Text>Upload .xlsx, .xls or .csv file</Text>
<Button onClick={open} title="Select file" />
</>
)}
</Container>
);
};

View File

@ -0,0 +1,18 @@
import { useMemo } from 'react';
import { Table } from '@/spreadsheet-import/components/Table';
import type { Fields } from '@/spreadsheet-import/types';
import { generateExampleRow } from '@/spreadsheet-import/utils/generateExampleRow';
import { generateColumns } from './columns';
interface Props<T extends string> {
fields: Fields<T>;
}
export const ExampleTable = <T extends string>({ fields }: Props<T>) => {
const data = useMemo(() => generateExampleRow(fields), [fields]);
const columns = useMemo(() => generateColumns(fields), [fields]);
return <Table rows={data} columns={columns} className={'rdg-example'} />;
};

View File

@ -0,0 +1,53 @@
import type { Column } from 'react-data-grid';
import { createPortal } from 'react-dom';
import styled from '@emotion/styled';
import type { Fields } from '@/spreadsheet-import/types';
import { AppTooltip } from '@/ui/tooltip/AppTooltip';
const HeaderContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
position: relative;
`;
const HeaderLabel = styled.span`
display: flex;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
`;
const DefaultContainer = styled.div`
min-height: 100%;
min-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
`;
export const generateColumns = <T extends string>(fields: Fields<T>) =>
fields.map(
(column): Column<any> => ({
key: column.key,
name: column.label,
minWidth: 150,
headerRenderer: () => (
<HeaderContainer>
<HeaderLabel id={`${column.key}`}>{column.label}</HeaderLabel>
{column.description &&
createPortal(
<AppTooltip
anchorSelect={`#${column.key}`}
place="top"
content={column.description}
/>,
document.body,
)}
</HeaderContainer>
),
formatter: ({ row }) => (
<DefaultContainer>{row[column.key]}</DefaultContainer>
),
}),
);