Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Members/csv upload #485

Merged
merged 30 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
908f182
file upload and json conversion
oscarwang20 Sep 17, 2023
fae006c
dependency
oscarwang20 Sep 17, 2023
d8b6143
member update
oscarwang20 Sep 17, 2023
d484106
removed dropzone, using input instead
oscarwang20 Sep 17, 2023
acfa304
file upload input
oscarwang20 Sep 17, 2023
256693e
error message for no file
oscarwang20 Sep 17, 2023
7d39127
fixed styling
oscarwang20 Sep 18, 2023
0cd43d9
removed unnecesary import
oscarwang20 Sep 18, 2023
150a7bc
error handling and current data keeping
oscarwang20 Sep 19, 2023
84fc30d
add new member empty fields
oscarwang20 Sep 19, 2023
586035f
currMember
oscarwang20 Sep 19, 2023
47968e4
ux changes and code duplication
oscarwang20 Sep 19, 2023
ad7d60a
error messages and styling
oscarwang20 Sep 19, 2023
e116396
removed RenderUploadStatus
oscarwang20 Sep 19, 2023
c62b791
text wrapping buttons
oscarwang20 Sep 19, 2023
2877b88
removed members[]
oscarwang20 Sep 19, 2023
9612e20
disable no implicit any rule
oscarwang20 Sep 19, 2023
4be50ac
back to old ways
oscarwang20 Sep 20, 2023
e55d898
upload status rework
oscarwang20 Sep 23, 2023
8895c30
role, subteam, email checks; truthy empty array
oscarwang20 Sep 25, 2023
5029ce3
more specific error messages
oscarwang20 Sep 25, 2023
d39b7ba
code repetition in error checking and subteam contained in formerSubt…
oscarwang20 Sep 25, 2023
031347a
formerSubteam check
oscarwang20 Sep 25, 2023
0672b09
removed console log
oscarwang20 Sep 25, 2023
079baed
former subteam existence check
oscarwang20 Sep 25, 2023
0224ed5
removed console logs
oscarwang20 Sep 25, 2023
28c8d85
refactored based on excel/sheets formatting
oscarwang20 Sep 25, 2023
08ba2d9
sample file download and small fixes
oscarwang20 Sep 25, 2023
2dc3d52
small changes
oscarwang20 Sep 25, 2023
8973c41
removed unnecessary role null check since already in error checker
oscarwang20 Sep 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added frontend/public/sample_csv.zip
Binary file not shown.
23 changes: 23 additions & 0 deletions frontend/src/components/Admin/AddUser/AddUser.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,14 @@

.fullWidth {
width: 100%;
word-wrap: break-word;
flex: inherit;
}

.halfWidth {
width: 50%;
word-wrap: break-word;
flex: inherit;
}

.userEmail {
Expand All @@ -53,3 +57,22 @@
overflow: hidden;
text-overflow: ellipsis;
}

.errorMessage {
color: red;
margin-top: 1rem;
height: 8rem;
overflow: scroll;
}

.successMessage {
color: green;
margin-top: 1rem;
max-height: 8rem;
overflow: scroll;
}

.wrap {
word-wrap: break-word;
flex: inherit;
}
169 changes: 167 additions & 2 deletions frontend/src/components/Admin/AddUser/AddUser.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState } from 'react';
import { Card, Button, Form, Input, Select, TextArea } from 'semantic-ui-react';
import ALL_ROLES from 'common-types/constants';
import csvtojson from 'csvtojson';
import styles from './AddUser.module.css';
import { Member, MembersAPI } from '../../../API/MembersAPI';
import ErrorModal from '../../Modals/ErrorModal';
import { getNetIDFromEmail, getRoleDescriptionFromRoleID, Emitters } from '../../../utils';
import { useMembers } from '../../Common/FirestoreDataProvider';
import { useMembers, useTeams } from '../../Common/FirestoreDataProvider';
import { TeamSearch } from '../../Common/Search/Search';

type CurrentSelectedMember = Omit<Member, 'netid' | 'roleDescription'>;
Expand Down Expand Up @@ -68,12 +70,21 @@ type State = {
readonly isCreatingUser: boolean;
};

type UploadStatus = {
readonly status: 'success' | 'error';
readonly msg: string;
readonly errs?: string[];
};

export default function AddUser(): JSX.Element {
const allMembers = useMembers();
const validSubteams = useTeams().map((t) => t.name);
const [state, setState] = useState<State>({
currentSelectedMember: allMembers[0],
isCreatingUser: false
});
const [csvFile, setCsvFile] = useState<File | undefined>(undefined);
const [uploadStatus, setUploadStatus] = useState<UploadStatus>();

function createNewUser(): void {
setState({
Expand Down Expand Up @@ -125,6 +136,121 @@ export default function AddUser(): JSX.Element {
});
}

function processJson(json: any[]): void {
for (const m of json) {
const netId = getNetIDFromEmail(m.email);
const currMember = allMembers.find((mem) => mem.netid === netId);
if (currMember) {
const updatedMember = {
netid: netId,
email: m.email,
firstName: m.firstName || currMember.firstName,
lastName: m.lastName || currMember.lastName,
pronouns: m.pronouns || currMember.pronouns,
graduation: m.graduation || currMember.graduation,
major: m.major || currMember.major,
doubleMajor: m.doubleMajor || currMember.doubleMajor,
minor: m.minor || currMember.minor,
website: m.website || currMember.website,
linkedin: m.linkedin || currMember.linkedin,
github: m.github || currMember.github,
hometown: m.hometown || currMember.hometown,
about: m.about || currMember.about,
subteams: m.subteam ? [m.subteam] : currMember.subteams,
formerSubteams: m.formerSubteams
? m.formerSubteams.split(', ')
: currMember.formerSubteams,
role: m.role || currMember.role,
roleDescription: getRoleDescriptionFromRoleID(m.role)
} as IdolMember;
MembersAPI.updateMember(updatedMember);
} else {
const updatedMember = {
netid: netId,
email: m.email,
firstName: m.firstName || '',
lastName: m.lastName || '',
pronouns: m.pronouns || '',
graduation: m.graduation || '',
major: m.major || '',
doubleMajor: m.doubleMajor || '',
minor: m.minor || '',
website: m.website || '',
linkedin: m.linkedin || '',
github: m.github || '',
hometown: m.hometown || '',
about: m.about || '',
subteams: m.subteam ? [m.subteam] : [],
formerSubteams: m.formerSubteams ? m.formerSubteams.split(', ') : [],
role: m.role || ('' as Role),
roleDescription: getRoleDescriptionFromRoleID(m.role)
} as IdolMember;
MembersAPI.setMember(updatedMember);
}
}
}

async function uploadUsersCsv(csvFile: File | undefined): Promise<void> {
if (csvFile) {
const csv = await csvFile.text();
const columnHeaders = csv.split('\n')[0].split(',');
if (!columnHeaders.includes('email')) {
setUploadStatus({
status: 'error',
msg: 'Error: CSV must contain an email column'
});
return;
}
if (!columnHeaders.includes('role')) {
setUploadStatus({
status: 'error',
msg: 'Error: CSV must contain a role column'
});
return;
}
const json = await csvtojson().fromString(csv);
const errors = json
.map((m) => {
const [email, role, subteam] = [m.email, m.role, m.subteam];
const formerSubteams: string[] = m.formerSubteams ? m.formerSubteams.split(', ') : [];
const err = [];
if (!email) {
err.push('missing email');
}
if (!role) {
err.push('missing role');
}
if (role && !ALL_ROLES.includes(role as Role)) {
err.push('invalid role');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: role && !ALL_ROLES.includes(role as Role), so that "missing role" cases don't fall into this category.

}
if (subteam && !validSubteams.includes(subteam)) {
err.push('invalid subteam');
}
if (formerSubteams.some((t) => !validSubteams.includes(t))) {
err.push('at least one invalid former subteam');
}
if (formerSubteams.includes(subteam)) {
err.push('subteam cannot be in former subteams');
}
return err.length > 0 ? `Row ${json.indexOf(m) + 1}: ${err.join(', ')}` : '';
})
.filter((err) => err.length > 0);
if (errors.length > 0) {
setUploadStatus({
status: 'error',
msg: `Error: ${errors.length} ${errors.length === 1 ? 'row is' : 'rows are'} invalid!`,
errs: errors
});
} else {
processJson(json);
setUploadStatus({
status: 'success',
msg: `Successfully uploaded ${json.length} members!`
});
}
}
}

function setCurrentlySelectedMember(setter: (m: CurrentSelectedMember) => CurrentSelectedMember) {
setState((s) => {
if (!s.currentSelectedMember) return s;
Expand Down Expand Up @@ -160,7 +286,7 @@ export default function AddUser(): JSX.Element {
))}
</Card.Content>
</div>
<Card.Content extra>
<Card.Content>
<div className={`ui one buttons ${styles.fullWidth}`}>
<Button
basic
Expand All @@ -182,6 +308,45 @@ export default function AddUser(): JSX.Element {
</Button>
</div>
</Card.Content>
<Card.Content>
{csvFile ? (
<div className={`ui one buttons ${styles.fullWidth}`}>
<Button
basic
color="green"
className={styles.fullWidth}
onClick={() => {
uploadUsersCsv(csvFile);
}}
>
{`Upload ${csvFile.name}`}
</Button>
</div>
) : undefined}
<input
className={styles.wrap}
type="file"
accept=".csv"
onChange={(e) => setCsvFile(e.target.files?.[0])}
/>
<a href="/sample_csv.zip">Download sample .csv file</a>
{uploadStatus ? (
<div
className={
uploadStatus.status === 'error' ? styles.errorMessage : styles.successMessage
}
>
<p>{`${uploadStatus.msg}`}</p>
{uploadStatus.errs ? (
<div>
{uploadStatus.errs.map((err) => (
<p>{err}</p>
))}
</div>
) : undefined}
</div>
) : undefined}
</Card.Content>
</Card>
{state.currentSelectedMember !== undefined ? (
<Card className={styles.userEditor}>
Expand Down
Loading