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