Skip to content

Commit b0d67c8

Browse files
Single importer (#137)
* single importer built out, needs refactoring and testing * refactored and tested single importer * fixes to single importer * ui improvement for single importer
1 parent 91101ce commit b0d67c8

File tree

4 files changed

+184
-73
lines changed

4 files changed

+184
-73
lines changed
Lines changed: 34 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,40 @@
1-
import { useToast } from '@chakra-ui/react';
2-
import { UserRole } from '@prisma/client';
3-
import { useState } from 'react';
4-
import { Importer, ImporterField } from 'react-csv-importer';
5-
import 'react-csv-importer/dist/index.css';
6-
import { trpc } from '../../utils/trpc';
7-
import { EMAIL_REGEX } from '../../utils/constants';
8-
9-
interface ImportedUser {
10-
email: string;
11-
role: UserRole;
1+
import {Radio, RadioGroup, Stack, Text} from '@chakra-ui/react';
2+
import {UserRole} from '@prisma/client';
3+
import {useState} from 'react';
4+
import {trpc} from '../../utils/trpc';
5+
import {ImportNumberPossibilities, ImportNumberPossibilitiesType} from "../../utils/utils";
6+
import SingleImporter from "./import/SingleImporter";
7+
import BatchImporter from "./import/BatchImporter";
8+
9+
export interface ImportedUser {
10+
email: string;
11+
role: UserRole;
1212
}
1313

1414
const ImportUsers = () => {
15-
const [invalidEmails, setInvalidEmails] = useState<string[]>([]);
16-
const [invlidRoles, setInvalidRoles] = useState<string[]>([]);
17-
const addUserMutation = trpc.user.addUsers.useMutation();
18-
const toast = useToast();
19-
20-
const handleAddUsers = async (users: ImportedUser[]) => {
21-
await addUserMutation.mutateAsync(users);
22-
};
23-
24-
return (
25-
<Importer
26-
// chunkSize={10000}
27-
assumeNoHeaders={false}
28-
restartable
29-
processChunk={async rows => {
30-
const users = rows.filter(user => {
31-
const userRole = user.role as string;
32-
const userEmail = user.email as string;
33-
34-
// Check if userEmail is valid email
35-
if (!userEmail.match(EMAIL_REGEX)) {
36-
setInvalidEmails(prev => [...prev, userEmail]);
37-
return false;
38-
}
39-
40-
// Check if user role is valid, meaning it's in the UserRole enum
41-
if (!Object.values(UserRole).includes(userRole.toUpperCase() as UserRole)) {
42-
setInvalidRoles(prev => [...prev, userRole]);
43-
return false;
44-
}
45-
46-
return true;
47-
});
48-
49-
handleAddUsers(users as unknown as ImportedUser[]);
50-
}}
51-
onComplete={() => {
52-
if (invalidEmails.length > 0) {
53-
toast({
54-
title: 'Invalid emails',
55-
description: `Added valid users. The following emails are invalid: ${invalidEmails.join(', ')}`,
56-
status: 'error',
57-
isClosable: true,
58-
position: 'top-right',
59-
});
60-
}
61-
62-
if (invlidRoles.length > 0) {
63-
toast({
64-
title: 'Invalid roles',
65-
description: `Added valid usres. The following roles are invalid: ${invlidRoles.join(', ')}`,
66-
status: 'error',
67-
isClosable: true,
68-
position: 'top-right',
69-
});
70-
}
71-
}}
72-
>
73-
<ImporterField name='email' label='Email' />
74-
<ImporterField name='role' label='Role' />
75-
</Importer>
76-
);
15+
const [importType, setImportType] = useState<ImportNumberPossibilitiesType>(ImportNumberPossibilities.SINGLE_IMPORT);
16+
const addUserMutation = trpc.user.addUsers.useMutation();
17+
18+
const handleAddUsers = async (users: ImportedUser[]) => {
19+
await addUserMutation.mutateAsync(users);
20+
};
21+
22+
return (
23+
<>
24+
<Text fontSize='xl' mb={1}>Import method</Text>
25+
<RadioGroup onChange={(val: ImportNumberPossibilitiesType) => setImportType(val)} value={importType}>
26+
<Stack spacing={5} direction='row'>
27+
<Radio value={ImportNumberPossibilities.SINGLE_IMPORT}>Single import</Radio>
28+
<Radio value={ImportNumberPossibilities.BATCH_IMPORT}>Batch import</Radio>
29+
</Stack>
30+
</RadioGroup>
31+
{
32+
importType === ImportNumberPossibilities.BATCH_IMPORT
33+
? <BatchImporter handleAddUsers={handleAddUsers}/>
34+
: <SingleImporter handleAddUsers={handleAddUsers}/>
35+
}
36+
</>
37+
);
7738
};
7839

7940
export default ImportUsers;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {Importer, ImporterField} from "react-csv-importer";
2+
import {EMAIL_REGEX} from "../../../utils/constants";
3+
import {UserRole} from "@prisma/client";
4+
import {ImportedUser} from "../ImportUsers";
5+
import {useState} from "react";
6+
import {useToast} from "@chakra-ui/react";
7+
import 'react-csv-importer/dist/index.css';
8+
9+
interface BatchImporterProps {
10+
handleAddUsers: (users: ImportedUser[]) => Promise<void>
11+
}
12+
13+
const BatchImporter = ({handleAddUsers}: BatchImporterProps) => {
14+
const [invalidEmails, setInvalidEmails] = useState<string[]>([]);
15+
const [invalidRoles, setInvalidRoles] = useState<string[]>([]);
16+
const toast = useToast();
17+
18+
return (
19+
<Importer
20+
// chunkSize={10000}
21+
assumeNoHeaders={false}
22+
restartable
23+
processChunk={async rows => {
24+
const users = rows.filter(user => {
25+
const userRole = user.role as string;
26+
const userEmail = user.email as string;
27+
28+
// Check if userEmail is valid email
29+
if (!userEmail.match(EMAIL_REGEX)) {
30+
setInvalidEmails(prev => [...prev, userEmail]);
31+
return false;
32+
}
33+
34+
// Check if user role is valid, meaning it's in the UserRole enum
35+
if (!Object.values(UserRole).includes(userRole.toUpperCase() as UserRole)) {
36+
setInvalidRoles(prev => [...prev, userRole]);
37+
return false;
38+
}
39+
40+
return true;
41+
});
42+
await handleAddUsers(users as unknown as ImportedUser[]);
43+
}}
44+
onComplete={() => {
45+
if (invalidEmails.length > 0) {
46+
toast({
47+
title: 'Invalid emails',
48+
description: `Added valid users. The following emails are invalid: ${invalidEmails.join(', ')}`,
49+
status: 'error',
50+
isClosable: true,
51+
position: 'top-right',
52+
});
53+
}
54+
55+
if (invalidRoles.length > 0) {
56+
toast({
57+
title: 'Invalid roles',
58+
description: `Added valid users. The following roles are invalid: ${invalidRoles.join(', ')}`,
59+
status: 'error',
60+
isClosable: true,
61+
position: 'top-right',
62+
});
63+
}
64+
}}
65+
>
66+
<ImporterField name='email' label='Email'/>
67+
<ImporterField name='role' label='Role'/>
68+
</Importer>
69+
);
70+
}
71+
72+
73+
export default BatchImporter;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {useState} from "react";
2+
import {UserRole} from "@prisma/client";
3+
import {Button, Flex, Input, useToast} from "@chakra-ui/react";
4+
import {Select, SingleValue} from "chakra-react-select"
5+
import {EMAIL_REGEX} from "../../../utils/constants";
6+
import {ImportedUser} from "../ImportUsers";
7+
8+
interface SingleImporterProps {
9+
handleAddUsers: (users: ImportedUser[]) => Promise<void>
10+
}
11+
12+
const SingleImporter = ({handleAddUsers}: SingleImporterProps) => {
13+
const [email, setEmail] = useState<string>('');
14+
const [role, setRole] = useState<UserRole | undefined | string>();
15+
const toast = useToast();
16+
17+
const handleAddUser = async () => {
18+
if (!email.match(EMAIL_REGEX)) {
19+
toast({
20+
title: 'Incorrect email',
21+
description: `Please input a correct email. ${email} is not correct`,
22+
status: 'error',
23+
isClosable: true,
24+
position: 'top-right',
25+
});
26+
return;
27+
} else if ((role === undefined) || (role === '')) {
28+
toast({
29+
title: 'Missing role',
30+
description: 'Please select a role',
31+
status: 'error',
32+
isClosable: true,
33+
position: 'top-right',
34+
});
35+
return;
36+
} else {
37+
const user: ImportedUser = {email: email, role: role as UserRole}
38+
await handleAddUsers([user]).then(
39+
() => toast({
40+
title: 'User added',
41+
description: `${email} added with role ${role}`,
42+
status: 'success',
43+
isClosable: true,
44+
position: 'top-right',
45+
})
46+
).catch(() => toast({
47+
title: 'Error adding user',
48+
description: `${email} error adding with role ${role}`,
49+
status: 'error',
50+
isClosable: true,
51+
position: 'top-right',
52+
}));
53+
54+
}
55+
56+
}
57+
58+
const userRoles = Object.values(UserRole).map((role: UserRole) => ({ label: role, value: role }));
59+
60+
return (
61+
<Flex direction='row'>
62+
<Input placeholder='Input email' w='400px' value={email} onChange={e => setEmail(e.target.value)}/>
63+
<Select options={userRoles} onChange={(val: SingleValue<{label: UserRole, value: UserRole}>) => setRole(val?.value)}/>
64+
<Button onClick={handleAddUser}>Import</Button>
65+
</Flex>
66+
);
67+
}
68+
69+
export default SingleImporter;

src/utils/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ export const ImportUsersMethodPossiblities = {
114114

115115
export type ImportUsersMethodPossiblitiesType = 'IMPORT_STAFF' | 'IMPORT_STAFF_AND_STUDENTS';
116116

117+
// I don't think there's a way to include this enum in the SiteSettingsValues enum
118+
export const ImportNumberPossibilities = {
119+
SINGLE_IMPORT: 'SINGLE_IMPORT' as const,
120+
BATCH_IMPORT: 'BATCH_IMPORT' as const,
121+
};
122+
123+
export type ImportNumberPossibilitiesType = 'SINGLE_IMPORT' | 'BATCH_IMPORT';
124+
117125
export const resolveTime = (t: TicketStats) => {
118126
if (!t.resolvedAt || !t.createdAt) {
119127
return 0;

0 commit comments

Comments
 (0)