frontend/src/views/teams.js (383 lines of code) (raw):
import React, { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Link, useNavigate } from '@reach/router';
import ReactPlaceholder from 'react-placeholder';
import { TextBlock, RectShape } from 'react-placeholder/lib/placeholders';
import { FormattedMessage } from 'react-intl';
import { Form } from 'react-final-form';
import messages from './messages';
import { useFetch } from '../hooks/UseFetch';
import { useEditTeamAllowed } from '../hooks/UsePermissions';
import { useSetTitleTag } from '../hooks/UseMetaTags';
import { fetchLocalJSONAPI, pushToLocalJSONAPI } from '../network/genericJSONRequest';
import {
getMembersDiff,
filterActiveMembers,
filterActiveManagers,
filterInactiveMembersAndManagers,
formatMemberObject,
} from '../utils/teamMembersDiff';
import { Members, JoinRequests } from '../components/teamsAndOrgs/members';
import {
TeamInformation,
TeamForm,
TeamsManagement,
TeamSideBar,
} from '../components/teamsAndOrgs/teams';
import { MessageMembers } from '../components/teamsAndOrgs/messageMembers';
import { Projects } from '../components/teamsAndOrgs/projects';
import { FormSubmitButton, CustomButton } from '../components/button';
import { DeleteModal } from '../components/deleteModal';
import { NotFound } from './notFound';
export function ManageTeams() {
useSetTitleTag('Manage teams');
return <ListTeams managementView={true} />;
}
export function MyTeams() {
useSetTitleTag('My teams');
return (
<div className="w-100 cf bg-tan blue-dark">
<ListTeams />
</div>
);
}
export function ListTeams({ managementView = false }: Object) {
const userDetails = useSelector((state) => state.auth.get('userDetails'));
const token = useSelector((state) => state.auth.get('token'));
const [teams, setTeams] = useState(null);
const [userTeamsOnly, setUserTeamsOnly] = useState(true);
useEffect(() => {
if (token && userDetails && userDetails.id) {
let queryParam;
if (managementView) {
queryParam = userTeamsOnly ? `?manager=${userDetails.id}` : '';
} else {
queryParam = `?member=${userDetails.id}`;
}
fetchLocalJSONAPI(`teams/${queryParam}`, token).then((res) => setTeams(res.teams));
}
}, [userDetails, token, managementView, userTeamsOnly]);
const placeHolder = (
<div className="pb4 bg-tan">
<div className="w-50-ns w-100 cf ph6-l ph4">
<TextBlock rows={1} className="bg-grey-light h3" />
<TextBlock rows={1} className="bg-grey-light h2 mt2" />
</div>
<RectShape className="bg-white dib mv2 mh6" style={{ width: 250, height: 300 }} />
<RectShape className="bg-white dib mv2 mh6" style={{ width: 250, height: 300 }} />
</div>
);
return (
<ReactPlaceholder
showLoadingAnimation={true}
customPlaceholder={placeHolder}
delay={10}
ready={teams !== null}
>
<TeamsManagement
teams={teams}
userDetails={userDetails}
managementView={managementView}
userTeamsOnly={userTeamsOnly}
setUserTeamsOnly={setUserTeamsOnly}
/>
</ReactPlaceholder>
);
}
const joinTeamRequest = (team_id, username, role, token) => {
pushToLocalJSONAPI(
`teams/${team_id}/actions/join/`,
JSON.stringify({ username: username, role: role }),
token,
'POST',
);
};
const leaveTeamRequest = (team_id, username, role, token) => {
pushToLocalJSONAPI(
`teams/${team_id}/actions/leave/`,
JSON.stringify({ username: username, role: role }),
token,
'POST',
);
};
export function CreateTeam() {
useSetTitleTag('Create new team');
const navigate = useNavigate();
const userDetails = useSelector((state) => state.auth.get('userDetails'));
const token = useSelector((state) => state.auth.get('token'));
const [managers, setManagers] = useState([]);
const [members, setMembers] = useState([]);
useEffect(() => {
if (userDetails && userDetails.username && managers.length === 0) {
setManagers([{ username: userDetails.username, pictureUrl: userDetails.pictureUrl }]);
}
}, [userDetails, managers]);
const addManagers = (values) => {
const newValues = values.filter(
(newUser) => !managers.map((i) => i.username).includes(newUser.username),
);
setManagers(managers.concat(newValues));
};
const removeManagers = (username) => {
setManagers(managers.filter((i) => i.username !== username));
};
const addMembers = (values) => {
const newValues = values.filter(
(newUser) => !members.map((i) => i.username).includes(newUser.username),
);
setMembers(members.concat(newValues));
};
const removeMembers = (username) => {
setMembers(members.filter((i) => i.username !== username));
};
const createTeam = (payload) => {
delete payload['organisation'];
pushToLocalJSONAPI('teams/', JSON.stringify(payload), token, 'POST').then((result) => {
managers
.filter((user) => user.username !== userDetails.username)
.map((user) => joinTeamRequest(result.teamId, user.username, 'MANAGER', token));
members.map((user) => joinTeamRequest(result.teamId, user.username, 'MEMBER', token));
navigate(`/manage/teams/${result.teamId}`);
});
};
return (
<Form
onSubmit={(values) => createTeam(values)}
render={({ handleSubmit, pristine, form, submitting, values }) => {
return (
<form onSubmit={handleSubmit} className="blue-grey">
<div className="cf pb5">
<h3 className="f2 mb3 ttu blue-dark fw7 barlow-condensed">
<FormattedMessage {...messages.newTeam} />
</h3>
<div className="w-40-l w-100 fl">
<div className="bg-white b--grey-light ba pa4 mb3">
<h3 className="f3 blue-dark mv0 fw6">
<FormattedMessage {...messages.teamInfo} />
</h3>
<TeamInformation />
</div>
</div>
<div className="w-40-l w-100 fl pl5-l pl0 ">
<div className="mb3">
<Members
addMembers={addManagers}
removeMembers={removeManagers}
members={managers}
resetMembersFn={setManagers}
creationMode={true}
/>
</div>
<div className="mb3">
<Members
addMembers={addMembers}
removeMembers={removeMembers}
members={members}
resetMembersFn={setMembers}
creationMode={true}
type={'members'}
/>
</div>
</div>
</div>
<div className="fixed left-0 right-0 bottom-0 cf bg-white h3">
<div className="w-80-ns w-60-m w-50 h-100 fl tr">
<Link to={'../'}>
<CustomButton className="bg-white mr5 pr2 h-100 bn bg-white blue-dark">
<FormattedMessage {...messages.cancel} />
</CustomButton>
</Link>
</div>
<div className="w-20-l w-40-m w-50 h-100 fr">
<FormSubmitButton
disabled={submitting || pristine || !values.organisation_id || !values.visibility}
className="w-100 h-100 bg-primary white"
disabledClassName="bg-primary o-50 white w-100 h-100"
>
<FormattedMessage {...messages.createTeam} />
</FormSubmitButton>
</div>
</div>
</form>
);
}}
></Form>
);
}
export function EditTeam(props) {
const userDetails = useSelector((state) => state.auth.get('userDetails'));
const token = useSelector((state) => state.auth.get('token'));
const [error, loading, team] = useFetch(`teams/${props.id}/`);
const [initManagers, setInitManagers] = useState(false);
const [managers, setManagers] = useState([]);
const [members, setMembers] = useState([]);
const [requests, setRequests] = useState([]);
const [canUserEditTeam] = useEditTeamAllowed(team);
useEffect(() => {
if (!initManagers && team && team.members) {
setManagers(filterActiveManagers(team.members));
setMembers(filterActiveMembers(team.members));
setRequests(filterInactiveMembersAndManagers(team.members));
setInitManagers(true);
}
}, [team, managers, initManagers]);
useSetTitleTag(`Edit ${team.name}`);
const addManagers = (values) => {
const newValues = values
.filter((newUser) => !managers.map((i) => i.username).includes(newUser.username))
.map((user) => formatMemberObject(user, true));
setManagers(managers.concat(newValues));
};
const removeManagers = (username) => {
setManagers(managers.filter((i) => i.username !== username));
};
const addMembers = (values) => {
const newValues = values
.filter((newUser) => !members.map((i) => i.username).includes(newUser.username))
.map((user) => formatMemberObject(user));
setMembers(members.concat(newValues));
};
const removeMembers = (username) => {
setMembers(members.filter((i) => i.username !== username));
};
const updateManagers = () => {
const { usersAdded, usersRemoved } = getMembersDiff(team.members, managers, true);
usersAdded.forEach((user) => joinTeamRequest(team.teamId, user, 'MANAGER', token));
usersRemoved.forEach((user) => leaveTeamRequest(team.teamId, user, 'MANAGER', token));
team.members = team.members
.filter((user) => user.function === 'MEMBER' || user.active === false)
.concat(managers);
};
const updateMembers = () => {
const { usersAdded, usersRemoved } = getMembersDiff(team.members, members);
usersAdded.forEach((user) => joinTeamRequest(team.teamId, user, 'MEMBER', token));
usersRemoved.forEach((user) => leaveTeamRequest(team.teamId, user, 'MEMBER', token));
team.members = team.members
.filter((user) => user.function === 'MANAGER' || user.active === false)
.concat(members);
};
const updateTeam = (payload) => {
pushToLocalJSONAPI(`teams/${props.id}/`, JSON.stringify(payload), token, 'PATCH');
};
if (team && team.teamId && !canUserEditTeam) {
return (
<div className="cf w-100 pv5">
<div className="tc">
<h3 className="f3 fw8 mb4 barlow-condensed">
<FormattedMessage {...messages.teamEditNotAllowed} />
</h3>
</div>
</div>
);
}
return (
<div className="cf pb4 bg-tan">
<div className="cf mt4">
<h3 className="f2 ttu blue-dark fw7 barlow-condensed v-mid ma0 dib ttu">
<FormattedMessage {...messages.manageTeam} />
</h3>
<DeleteModal id={team.teamId} name={team.name} type="teams" />
</div>
<div className="w-40-l w-100 mt4 fl">
<TeamForm
userDetails={userDetails}
team={{
name: team.name,
description: team.description,
inviteOnly: team.inviteOnly,
visibility: team.visibility,
organisation_id: team.organisation_id,
}}
updateTeam={updateTeam}
disabledForm={error || loading}
/>
</div>
<div className="w-40-l w-100 mt4 pl5-l pl0 fl">
<Members
addMembers={addManagers}
removeMembers={removeManagers}
saveMembersFn={updateManagers}
resetMembersFn={setManagers}
members={managers}
/>
<div className="h1"></div>
<Members
addMembers={addMembers}
removeMembers={removeMembers}
saveMembersFn={updateMembers}
resetMembersFn={setMembers}
members={members}
type="members"
/>
<div className="h1"></div>
<JoinRequests
requests={requests}
teamId={team.teamId}
addMembers={addMembers}
updateRequests={setRequests}
/>
<div className="h1"></div>
<MessageMembers teamId={team.teamId} />
</div>
</div>
);
}
export function TeamDetail(props) {
useSetTitleTag(`Team #${props.id}`);
const userDetails = useSelector((state) => state.auth.get('userDetails'));
const token = useSelector((state) => state.auth.get('token'));
const [error, loading, team] = useFetch(`teams/${props.id}/`);
// eslint-disable-next-line
const [projectsError, projectsLoading, projects] = useFetch(
`projects/?teamId=${props.id}&omitMapResults=true`,
props.id,
);
const [isMember, setIsMember] = useState(false);
const [managers, setManagers] = useState([]);
const [members, setMembers] = useState([]);
useEffect(() => {
if (team && team.members) {
setManagers(filterActiveManagers(team.members));
setMembers(filterActiveMembers(team.members));
const membersFiltered = team.members.filter(
(member) => member.username === userDetails.username,
);
if (membersFiltered.length) {
setIsMember(membersFiltered.filter((i) => i.active === true).length ? true : 'requested');
}
}
}, [team, userDetails.username]);
const joinTeam = () => {
pushToLocalJSONAPI(
`teams/${props.id}/actions/join/`,
JSON.stringify({ role: 'MEMBER', username: userDetails.username }),
token,
'POST',
).then((res) => setIsMember(team.inviteOnly ? 'requested' : true));
};
const leaveTeam = () => {
pushToLocalJSONAPI(
`teams/${props.id}/actions/leave/`,
JSON.stringify({ username: userDetails.username }),
token,
'POST',
).then((res) => setIsMember(false));
};
if (!loading && error) {
return <NotFound />;
} else {
return (
<>
<div className="cf pa4-ns pa2 bg-tan blue-dark overflow-y-scroll-ns vh-minus-185-ns h-100">
<div className="w-40-l w-100 mt2 fl">
<TeamSideBar
team={team}
members={members}
managers={managers}
requestedToJoin={isMember === 'requested'}
/>
</div>
<div className="w-60-l w-100 mt2 pl5-l pl0 fl">
<Projects
projects={projects}
viewAllEndpoint={`/explore/?team=${props.id}`}
ownerEntity="team"
showManageButtons={false}
/>
</div>
</div>
<div className="fixed bottom-0 cf bg-white h3 w-100">
<div className="w-80-ns w-60-m w-50 h-100 fl tr">
<Link to={'/contributions/teams'}>
<CustomButton className="bg-white mr5 pr2 h-100 bn bg-white blue-dark">
<FormattedMessage {...messages.myTeams} />
</CustomButton>
</Link>
</div>
<div className="w-20-l w-40-m w-50 h-100 fr">
{isMember ? (
<CustomButton
className="w-100 h-100 bg-primary white"
disabledClassName="bg-primary o-50 white w-100 h-100"
onClick={() => leaveTeam()}
>
<FormattedMessage
{...messages[isMember === 'requested' ? 'cancelRequest' : 'leaveTeam']}
/>
</CustomButton>
) : (
<CustomButton
className="w-100 h-100 bg-primary white"
disabledClassName="bg-primary o-50 white w-100 h-100"
onClick={() => joinTeam()}
>
<FormattedMessage {...messages.joinTeam} />
</CustomButton>
)}
</div>
</div>
</>
);
}
}