From 5337891a2d38b03ade295a0b37112a11d5692682 Mon Sep 17 00:00:00 2001
From: Roland Bernard <rolbernard@unibz.it>
Date: Tue, 1 Jun 2021 22:03:33 +0200
Subject: [PATCH] Added more error handling

---
 .../components/forms/AssigneesForm/index.tsx  |   6 +-
 .../components/forms/ProjectForm/index.tsx    |   5 +-
 .../forms/ProjectForm/project-form.scss       |   4 +-
 .../forms/RequirementsForm/index.tsx          |   6 +-
 .../forms/RoleForm/RoleChangeForm.tsx         |  11 +-
 .../forms/RoleForm/RoleEditForm.tsx           |  34 +++---
 .../src/components/forms/TaskForm/index.tsx   | 105 ++++++++++--------
 .../src/components/helpers/ProtectedRoute.tsx |   1 +
 .../components/layout/CommentList/index.tsx   |  15 +--
 9 files changed, 107 insertions(+), 80 deletions(-)

diff --git a/client/src/components/forms/AssigneesForm/index.tsx b/client/src/components/forms/AssigneesForm/index.tsx
index b606d24..f684023 100644
--- a/client/src/components/forms/AssigneesForm/index.tsx
+++ b/client/src/components/forms/AssigneesForm/index.tsx
@@ -5,18 +5,18 @@ import { TaskAssignment } from "adapters/task";
 
 import Popup from 'components/ui/Popup';
 import Button from 'components/ui/Button';
-import { possibleMember } from "components/forms/TaskForm";
+import { PossibleMember } from "components/forms/TaskForm";
 
 import './assignees-form.scss';
 
 interface Props {
     assignees: TaskAssignment[];
     setAssignees: Function;
-    members: possibleMember[]
+    members: PossibleMember[]
 }
 
 export default function AssigneesForm({ assignees, setAssignees, members }: Props) {
-    const [possibleMembers, setPossibleMembers] = useState<possibleMember[]>([]);
+    const [possibleMembers, setPossibleMembers] = useState<PossibleMember[]>([]);
     const [addNew, setAddNew] = useState(false);
     const [selectedMember, setSelectedMember] = useState('');
     const [selectedTime, setSelectedTime] = useState('');
diff --git a/client/src/components/forms/ProjectForm/index.tsx b/client/src/components/forms/ProjectForm/index.tsx
index 1fb6439..1011a91 100644
--- a/client/src/components/forms/ProjectForm/index.tsx
+++ b/client/src/components/forms/ProjectForm/index.tsx
@@ -70,8 +70,9 @@ export default function ProjectForm({ project, onSubmit }: Props) {
     const [allTeams, setAllTeams] = useState<Team[]>([]);
 
     useEffect(() => {
+        setLoadError(false);
         Promise.all([
-            Promise.all(teams.map(team => getTeam(team))),
+            project?.teams ? Promise.all(project?.teams.map(team => getTeam(team))) : [],
             getTeams(),
         ]).then(([projectTeams, userTeams]) => {
             const teamIds = new Set<string>();
@@ -85,7 +86,7 @@ export default function ProjectForm({ project, onSubmit }: Props) {
             setAllTeams(teams);
         })
         .catch(() => setLoadError(true))
-    }, [teams, loadError])
+    }, [project?.teams, loadError])
 
     const colors = Object.values(ProjectColors);
     const allStatus = Object.values(Status);
diff --git a/client/src/components/forms/ProjectForm/project-form.scss b/client/src/components/forms/ProjectForm/project-form.scss
index cabf9ec..fe16091 100644
--- a/client/src/components/forms/ProjectForm/project-form.scss
+++ b/client/src/components/forms/ProjectForm/project-form.scss
@@ -23,13 +23,13 @@
             filter: saturate(50%);
             transform: scale(90%);
 
-            @include mx.breakpoint(medium) {
+            @include mx.breakpoint(small) {
                 width: calc(100% / 5 - 10px);
                 padding-bottom: calc(100% / 5 - 10px);
 
             }
 
-            @include mx.breakpoint(large) {
+            @include mx.breakpoint(medium) {
                 margin-right: 20px;
                 margin-bottom: 20px;
                 width: calc(100% / 7 - 20px);
diff --git a/client/src/components/forms/RequirementsForm/index.tsx b/client/src/components/forms/RequirementsForm/index.tsx
index 997c2a5..4273955 100644
--- a/client/src/components/forms/RequirementsForm/index.tsx
+++ b/client/src/components/forms/RequirementsForm/index.tsx
@@ -3,20 +3,20 @@ import { useCallback, useEffect, useState } from 'react';
 
 import { TaskRequirement } from 'adapters/task';
 
-import { possibleRole } from 'components/forms/TaskForm';
+import { PossibleRole } from 'components/forms/TaskForm';
 import Popup from 'components/ui/Popup';
 import Button from 'components/ui/Button';
 
 import './requirements-form.scss';
 
 interface Props {
-    roles: possibleRole[],
+    roles: PossibleRole[],
     requirements: TaskRequirement[],
     setRequirements: Function
 }
 
 export default function RequirementsForm({ roles, requirements, setRequirements }: Props) {
-    const [possibleRoles, setPossibleRoles] = useState<possibleRole[]>([]);
+    const [possibleRoles, setPossibleRoles] = useState<PossibleRole[]>([]);
     const [addNew, setAddNew] = useState(false);
     const [selectedRole, setSelectedRole] = useState('');
     const [selectedTime, setSelectedTime] = useState('');
diff --git a/client/src/components/forms/RoleForm/RoleChangeForm.tsx b/client/src/components/forms/RoleForm/RoleChangeForm.tsx
index 23bbd00..0d874fc 100644
--- a/client/src/components/forms/RoleForm/RoleChangeForm.tsx
+++ b/client/src/components/forms/RoleForm/RoleChangeForm.tsx
@@ -27,8 +27,12 @@ export default function RoleForm({ roles, setEdit, member, team, setResult, setA
                 setResult(currentRole);
             }
             if (member) {
-                await updateTeamMember(team.id, { user: member.id, role: currentRole });
-                reload();
+                try {
+                    await updateTeamMember(team.id, { user: member.id, role: currentRole });
+                    reload();
+                } catch {
+                    setError('Failed to update team member.');
+                }
             }
         }
     }, [currentRole, member, team, setResult]);
@@ -38,9 +42,8 @@ export default function RoleForm({ roles, setEdit, member, team, setResult, setA
             await deleteTeamRole(team.id, id);
             setAllRoles((state: any) => state.filter((role: any) => role.id !== id));
         } catch {
-            setError('There are still users assigned to this role.')
+            setError('Unable to delete this role.');
         }
-
     }, [team, setAllRoles]);
 
     return (
diff --git a/client/src/components/forms/RoleForm/RoleEditForm.tsx b/client/src/components/forms/RoleForm/RoleEditForm.tsx
index aef4860..7512d8c 100644
--- a/client/src/components/forms/RoleForm/RoleEditForm.tsx
+++ b/client/src/components/forms/RoleForm/RoleEditForm.tsx
@@ -3,8 +3,9 @@ import { FormEvent, useCallback, useState } from 'react';
 
 import { createTeamRole, Team, TeamRole, updateTeamRole } from 'adapters/team';
 
-import TextInput from 'components/ui/TextInput';
 import Button from 'components/ui/Button';
+import Callout from 'components/ui/Callout';
+import TextInput from 'components/ui/TextInput';
 
 interface Props {
     role?: TeamRole;
@@ -22,28 +23,32 @@ export function validateName(name: string): string | null {
 
 export default function RoleEditForm({ role, team, setEdit, setAllRoles }: Props) {
     const [name, setName] = useState(role?.name ?? '');
+    const [error, setError] = useState('');
 
     const onSubmit = useCallback(async (e: FormEvent) => {
         e.preventDefault();
         if (validateName(name) === null) {
             if (!role?.id) {
-                const newRole = await createTeamRole(team.id, name);
-                setAllRoles((state: any) => [...state, newRole]);
-                setEdit(null);
+                try {
+                    const newRole = await createTeamRole(team.id, name);
+                    setAllRoles((state: any) => [...state, newRole]);
+                    setEdit(null);
+                } catch(e) {
+                    setError('Failed to create role.');
+                }
             } else {
-                if(updateTeamRole(team.id, role.id, name)) {
+                try {
+                    await updateTeamRole(team.id, role.id, name);
                     setAllRoles((state: any) => {
-                        state = state.filter((r: any) => r.id !== role.id);
                         return [
-                            ...state,
-                            {
-                                ...role,
-                                name: name
-                            }
-                        ]
+                            ...state.filter((r: any) => r.id !== role.id),
+                            { ...role, name: name }
+                        ];
                     });
+                    setEdit(null);
+                } catch(e) {
+                    setError('Failed to update role.');
                 }
-                setEdit(null);
             }
         }
     }, [name, team, setEdit, role, setAllRoles]);
@@ -51,6 +56,9 @@ export default function RoleEditForm({ role, team, setEdit, setAllRoles }: Props
     return (
         <form className="role-edit-form" onSubmit={onSubmit}>
             <h2>{!role?.id ? 'Create a new role' : 'Edit role ' + role.name}</h2>
+            {
+                error && <Callout message={error} />
+            }
             <TextInput
                 label="Role name"
                 name="name"
diff --git a/client/src/components/forms/TaskForm/index.tsx b/client/src/components/forms/TaskForm/index.tsx
index 1cd2fda..8ce4b8e 100644
--- a/client/src/components/forms/TaskForm/index.tsx
+++ b/client/src/components/forms/TaskForm/index.tsx
@@ -7,12 +7,13 @@ import { getTeam, getTeamMembers, getTeamRoles } from 'adapters/team';
 import { Priority, Task, TaskAssignment, TaskRequirement } from 'adapters/task';
 import { Status } from 'adapters/common';
 
+import Button from 'components/ui/Button';
 import Callout from 'components/ui/Callout';
 import TextInput from 'components/ui/TextInput';
+import ErrorScreen from 'components/ui/ErrorScreen';
 import CheckboxGroup from 'components/ui/CheckboxGroup';
-import RequirementsForm from 'components/forms/RequirementsForm';
 import AssigneesForm from 'components/forms/AssigneesForm';
-import Button from 'components/ui/Button';
+import RequirementsForm from 'components/forms/RequirementsForm';
 
 import './task-form.scss';
 import '../form.scss';
@@ -64,11 +65,11 @@ function validatePriority(priority: string): string | null {
     }
 }
 
-export interface possibleRole {
+export interface PossibleRole {
     id: string;
     label: string;
 }
-export interface possibleMember {
+export interface PossibleMember {
     id: string;
     label: string;
 }
@@ -79,8 +80,9 @@ export default function TaskForm({ task, onSubmit, project }: Props) {
     const [icon, setIcon] = useState(task?.icon);
     const [priority, setPriority] = useState(task?.priority);
     const [status, setStatus] = useState(task?.status);
-    const [error, setError] = useState('');
     const [tasks, setTasks] = useState(task?.dependencies ?? []);
+    const [error, setError] = useState('');
+    const [loadError, setLoadError] = useState(false);
 
     const [requirements, setRequirements] = useState(task?.requirements ?? []);
     const [assignees, setAssignees] = useState(task?.assigned ?? []);
@@ -88,33 +90,38 @@ export default function TaskForm({ task, onSubmit, project }: Props) {
     const allPriorities = Object.values(Priority);
     const allStatus = Object.values(Status);
     const [allTasks, setAllTasks] = useState<Task[]>([]);
-    const [allRoles, setAllRoles] = useState<possibleRole[]>([]);
-    const [allMembers, setAllMembers] = useState<possibleMember[]>([]);
+    const [allRoles, setAllRoles] = useState<PossibleRole[]>([]);
+    const [allMembers, setAllMembers] = useState<PossibleMember[]>([]);
 
     useEffect(() => {
+        setLoadError(false);
         getProjectTasks(project.id).then((tasks) => {
             setAllTasks(tasks.filter(cTask => task?.id !== cTask.id));
-        });
-        project.teams.forEach((teamId) => {
-            getTeam(teamId).then(team => {
-                getTeamRoles(teamId).then((roles) => {
-                    setAllRoles(state => [...state, ...roles.map(role => {
-                        return {
-                            id: role.id,
-                            label: team.name + ': ' + role.name
-                        }
-                    })]);
-                })
-                getTeamMembers(teamId).then((members) => {
-                    setAllMembers(state => [...state, ...members.map(member => {
-                        return {
-                            id: member.id,
-                            label: team.name + ': ' + (member.realname ?? member.username)
-                        }
-                    })]);
-                })
-            })
         })
+        .catch(() => setLoadError(true));
+        Promise.all([
+            Promise.all(project.teams.map(getTeam)),
+            Promise.all(project.teams.map(getTeamRoles)),
+            Promise.all(project.teams.map(getTeamMembers))
+        ]).then(([teams, roles, members]) => {
+            setAllRoles(roles.map((roles, i) => roles.map(role => ({
+                id: role.id,
+                label: role.name + ' (' + teams[i]?.name + ')',
+            }))).flat());
+            const memberIds = new Set<string>();
+            const uniqueMembers = [];
+            for (const member of members.flat()) {
+                if (!memberIds.has(member.id)) {
+                    uniqueMembers.push({
+                        id: member.id,
+                        label: member.realname ?? member.username,
+                    });
+                    memberIds.add(member.id);
+                }
+            }
+            setAllMembers(uniqueMembers);
+        })
+        .catch(() => setLoadError(true));
     }, [task, project]);
 
 
@@ -218,27 +225,33 @@ export default function TaskForm({ task, onSubmit, project }: Props) {
             </div>
             <h2>Dependencies</h2>
             <p>Pick tasks of this project that have to be done before this one.</p>
-            {
-                allTasks.length > 0 ? (
-                    <CheckboxGroup choices={allTasks ?? []} setChosen={setTasks} chosen={tasks ?? []} />
-                ) : <div className="error">No other tasks in this project</div>
-            }
-            <div className="fields-row">
-                <div className="col">
+            {loadError
+                ? <ErrorScreen />
+                : <>
                     {
-                        allRoles.length > 0 && (
-                            <RequirementsForm setRequirements={setRequirements} roles={allRoles} requirements={requirements} />
-                        )
+                            (allTasks.length > 0
+                                ? <CheckboxGroup choices={allTasks ?? []} setChosen={setTasks} chosen={tasks ?? []} />
+                                : <div className="error">No other tasks in this project</div>
+                            )
                     }
-                </div>
-                <div className="col">
-                    {
-                        allMembers.length > 0 && (
-                            <AssigneesForm members={allMembers} setAssignees={setAssignees} assignees={assignees} />
-                        )
-                    }
-                </div>
-            </div>
+                    <div className="fields-row">
+                        <div className="col">
+                            {
+                                allRoles.length > 0 && (
+                                    <RequirementsForm setRequirements={setRequirements} roles={allRoles} requirements={requirements} />
+                                )
+                            }
+                        </div>
+                        <div className="col">
+                            {
+                                allMembers.length > 0 && (
+                                    <AssigneesForm members={allMembers} setAssignees={setAssignees} assignees={assignees} />
+                                )
+                            }
+                        </div>
+                    </div>
+                </>
+            }
             <div className="button-container">
                 <Button type="submit" className="expanded">
                     {task ? 'Update task' : 'Create task' }
diff --git a/client/src/components/helpers/ProtectedRoute.tsx b/client/src/components/helpers/ProtectedRoute.tsx
index f29ad24..883d10f 100644
--- a/client/src/components/helpers/ProtectedRoute.tsx
+++ b/client/src/components/helpers/ProtectedRoute.tsx
@@ -20,6 +20,7 @@ export default function ProtectedRoute(props: RouteProps) {
     });
 
     useEffect(() => {
+        setError(false);
         getTeams().then(teams => {
             if (teams.length === 0) {
                 history.push('/introduction');
diff --git a/client/src/components/layout/CommentList/index.tsx b/client/src/components/layout/CommentList/index.tsx
index 9827c23..344ce0d 100644
--- a/client/src/components/layout/CommentList/index.tsx
+++ b/client/src/components/layout/CommentList/index.tsx
@@ -20,15 +20,9 @@ export default function CommentList({ comments, taskId }: Props) {
     const [user, setUser] = useState<User>();
     const [comment, setComment] = useState<string>('');
     const [allComments, setComments] = useState<Comment[]>([]);
-    
-    useEffect(() => {
-        getCurrentUser()
-            .then(setUser)
-            .catch(() => setError(true));
-        setComments(comments);
-    }, [comments]);
 
     const handleSubmit = useCallback((e: FormEvent) => {
+        setError(false);
         e.preventDefault();
         if (comment.length > 0) {
             createComment({ task: taskId, text: comment }).then(id => {
@@ -41,6 +35,13 @@ export default function CommentList({ comments, taskId }: Props) {
             .catch(() => setError(true))
         }
     }, [comment, taskId]);
+    
+    useEffect(() => {
+        getCurrentUser()
+            .then(setUser)
+            .catch(() => setError(true));
+        setComments(comments);
+    }, [comments]);
 
     const onError = useCallback(() => {
         setError(true)
-- 
GitLab