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:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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'} />;
|
||||
};
|
||||
@ -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>
|
||||
),
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user