diff --git a/components/messages.js b/components/messages.js index 3f888e90..46586916 100644 --- a/components/messages.js +++ b/components/messages.js @@ -7,6 +7,7 @@ export const labels = defineMessages({ cancel: { id: 'label.cancel', defaultMessage: 'Cancel' }, continue: { id: 'label.continue', defaultMessage: 'Continue' }, delete: { id: 'label.delete', defaultMessage: 'Delete' }, + leave: { id: 'label.leave', defaultMessage: 'Leave' }, users: { id: 'label.users', defaultMessage: 'Users' }, createUser: { id: 'label.create-user', defaultMessage: 'Create user' }, username: { id: 'label.username', defaultMessage: 'Username' }, @@ -67,6 +68,7 @@ export const labels = defineMessages({ dateRange: { id: 'label.date-range', defaultMessage: 'Date range' }, viewDetails: { id: 'label.view-details', defaultMessage: 'View details' }, deleteTeam: { id: 'label.delete-team', defaultMessage: 'Delete team' }, + leaveTeam: { id: 'label.leave-team', defaultMessage: 'Leave team' }, refresh: { id: 'label.refresh', defaultMessage: 'Refresh' }, pages: { id: 'label.pages', defaultMessage: 'Pages' }, referrers: { id: 'label.referrers', defaultMessage: 'Referrers' }, @@ -123,6 +125,10 @@ export const messages = defineMessages({ id: 'message.delete-user-warning', defaultMessage: 'Are you sure you want to delete the user {username}?', }, + leaveTeamWarning: { + id: 'message.leave-team-warning', + defaultMessage: 'Are you sure you want to leave the team {name}?', + }, deleteTeamWarning: { id: 'message.delete-team-warning', defaultMessage: 'Are you sure you want to delete the team {name}?', diff --git a/components/pages/settings/teams/TeamLeaveForm.js b/components/pages/settings/teams/TeamLeaveForm.js new file mode 100644 index 00000000..8b75ddb9 --- /dev/null +++ b/components/pages/settings/teams/TeamLeaveForm.js @@ -0,0 +1,32 @@ +import { Button, Form, FormButtons, SubmitButton } from 'react-basics'; +import useApi from 'hooks/useApi'; +import useMessages from 'hooks/useMessages'; + +export default function TeamLeaveForm({ teamUserId, teamName, onSave, onClose }) { + const { formatMessage, labels, messages, FormattedMessage } = useMessages(); + const { del, useMutation } = useApi(); + const { mutate, error, isLoading } = useMutation(data => del(`/teamUsers/${teamUserId}`, data)); + + const handleSubmit = async data => { + mutate(data, { + onSuccess: async () => { + onSave(); + onClose(); + }, + }); + }; + + return ( +
+

+ {teamName} }} /> +

+ + + {formatMessage(labels.leave)} + + + +
+ ); +} diff --git a/components/pages/settings/teams/TeamMembers.js b/components/pages/settings/teams/TeamMembers.js index 2e9b87f7..ab435c4d 100644 --- a/components/pages/settings/teams/TeamMembers.js +++ b/components/pages/settings/teams/TeamMembers.js @@ -6,7 +6,7 @@ import useMessages from 'hooks/useMessages'; export default function TeamMembers({ teamId, readOnly }) { const { toast, showToast } = useToast(); const { get, useQuery } = useApi(); - const { formatMessage, labels } = useMessages(); + const { formatMessage, messages } = useMessages(); const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () => get(`/teams/${teamId}/users`), ); @@ -17,7 +17,7 @@ export default function TeamMembers({ teamId, readOnly }) { const handleSave = async () => { await refetch(); - showToast({ message: formatMessage(labels.saved), variant: 'success' }); + showToast({ message: formatMessage(messages.saved), variant: 'success' }); }; return ( diff --git a/components/pages/settings/teams/TeamMembersTable.js b/components/pages/settings/teams/TeamMembersTable.js index 8498a19d..0d405fec 100644 --- a/components/pages/settings/teams/TeamMembersTable.js +++ b/components/pages/settings/teams/TeamMembersTable.js @@ -5,7 +5,7 @@ import { TableRow, TableCell, TableColumn, - Button, + LoadingButton, Icon, Icons, Flexbox, @@ -15,12 +15,14 @@ import { ROLES } from 'lib/constants'; import useUser from 'hooks/useUser'; import useApi from 'hooks/useApi'; import useMessages from 'hooks/useMessages'; +import { useState } from 'react'; export default function TeamMembersTable({ data = [], onSave, readOnly }) { const { formatMessage, labels } = useMessages(); const { user } = useUser(); const { del, useMutation } = useApi(); - const { mutate } = useMutation(data => del(`/teamUsers/${data.teamUserId}`)); + const { mutate, isLoading } = useMutation(data => del(`/teamUsers/${data.teamUserId}`)); + const [loadingIds, setLoadingIds] = useState([]); const columns = [ { name: 'username', label: formatMessage(labels.username), style: { flex: 2 } }, @@ -29,12 +31,18 @@ export default function TeamMembersTable({ data = [], onSave, readOnly }) { ]; const handleRemoveTeamMember = teamUserId => { + setLoadingIds(prev => [...prev, teamUserId]); + mutate( { teamUserId }, { - onSuccess: async () => { + onSuccess: () => { + setLoadingIds(loadingIds.filter(a => a !== teamUserId)); onSave(); }, + onError: () => { + setLoadingIds(loadingIds.filter(a => a !== teamUserId)); + }, }, ); }; @@ -59,15 +67,16 @@ export default function TeamMembersTable({ data = [], onSave, readOnly }) { ), action: !readOnly && ( - + ), }; diff --git a/components/pages/settings/teams/TeamsTable.js b/components/pages/settings/teams/TeamsTable.js index ce37bb28..4b27bdfe 100644 --- a/components/pages/settings/teams/TeamsTable.js +++ b/components/pages/settings/teams/TeamsTable.js @@ -15,6 +15,7 @@ import { Text, } from 'react-basics'; import TeamDeleteForm from './TeamDeleteForm'; +import TeamLeaveForm from './TeamLeaveForm'; import useMessages from 'hooks/useMessages'; import useUser from 'hooks/useUser'; import { ROLES } from 'lib/constants'; @@ -42,9 +43,10 @@ export default function TeamsTable({ data = [], onDelete }) { {(row, keys, rowIndex) => { - const { id } = row; + const { id, teamUser } = row; const owner = row.teamUser.find(({ role }) => role === ROLES.teamOwner); const showDelete = user.id === owner?.userId; + const teamUserId = teamUser.find(a => a.userId === user.id).id; const rowData = { ...row, @@ -54,9 +56,9 @@ export default function TeamsTable({ data = [], onDelete }) { {showDelete && ( @@ -79,6 +81,26 @@ export default function TeamsTable({ data = [], onDelete }) { )} + {!showDelete && ( + + + + {close => ( + + )} + + + )} ), }; diff --git a/lib/auth.ts b/lib/auth.ts index 5a2289c9..614a47ef 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -175,6 +175,10 @@ export async function canDeleteTeamUser({ user }: Auth, teamUserId: string) { if (validate(teamUserId)) { const removeUser = await getTeamUserById(teamUserId); + if (removeUser.userId === user.id) { + return true; + } + const teamUser = await getTeamUser(removeUser.teamId, user.id); return hasPermission(teamUser.role, PERMISSIONS.teamUpdate); diff --git a/pages/api/teams/join.ts b/pages/api/teams/join.ts index 95a41423..17c9bf32 100644 --- a/pages/api/teams/join.ts +++ b/pages/api/teams/join.ts @@ -3,7 +3,7 @@ import { NextApiRequestQueryBody } from 'lib/types'; import { useAuth } from 'lib/middleware'; import { NextApiResponse } from 'next'; import { methodNotAllowed, ok, notFound } from 'next-basics'; -import { createTeamUser, getTeam } from 'queries'; +import { createTeamUser, getTeam, getTeamUser } from 'queries'; import { ROLES } from 'lib/constants'; export interface TeamsJoinRequestBody { @@ -25,6 +25,12 @@ export default async ( return notFound(res, 'message.team-not-found'); } + const teamUser = await getTeamUser(team.id, req.auth.user.id); + + if (teamUser) { + return methodNotAllowed(res, 'message.team-already-member'); + } + await createTeamUser(req.auth.user.id, team.id, ROLES.teamMember); return ok(res, team);