mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Fix joining team dupe. Add loading to team member remove. Fix messages.
This commit is contained in:
parent
a22d50a597
commit
6d5aeb3bd1
@ -7,6 +7,7 @@ export const labels = defineMessages({
|
|||||||
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
|
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
|
||||||
continue: { id: 'label.continue', defaultMessage: 'Continue' },
|
continue: { id: 'label.continue', defaultMessage: 'Continue' },
|
||||||
delete: { id: 'label.delete', defaultMessage: 'Delete' },
|
delete: { id: 'label.delete', defaultMessage: 'Delete' },
|
||||||
|
leave: { id: 'label.leave', defaultMessage: 'Leave' },
|
||||||
users: { id: 'label.users', defaultMessage: 'Users' },
|
users: { id: 'label.users', defaultMessage: 'Users' },
|
||||||
createUser: { id: 'label.create-user', defaultMessage: 'Create user' },
|
createUser: { id: 'label.create-user', defaultMessage: 'Create user' },
|
||||||
username: { id: 'label.username', defaultMessage: 'Username' },
|
username: { id: 'label.username', defaultMessage: 'Username' },
|
||||||
@ -67,6 +68,7 @@ export const labels = defineMessages({
|
|||||||
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
|
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
|
||||||
viewDetails: { id: 'label.view-details', defaultMessage: 'View details' },
|
viewDetails: { id: 'label.view-details', defaultMessage: 'View details' },
|
||||||
deleteTeam: { id: 'label.delete-team', defaultMessage: 'Delete team' },
|
deleteTeam: { id: 'label.delete-team', defaultMessage: 'Delete team' },
|
||||||
|
leaveTeam: { id: 'label.leave-team', defaultMessage: 'Leave team' },
|
||||||
refresh: { id: 'label.refresh', defaultMessage: 'Refresh' },
|
refresh: { id: 'label.refresh', defaultMessage: 'Refresh' },
|
||||||
pages: { id: 'label.pages', defaultMessage: 'Pages' },
|
pages: { id: 'label.pages', defaultMessage: 'Pages' },
|
||||||
referrers: { id: 'label.referrers', defaultMessage: 'Referrers' },
|
referrers: { id: 'label.referrers', defaultMessage: 'Referrers' },
|
||||||
@ -123,6 +125,10 @@ export const messages = defineMessages({
|
|||||||
id: 'message.delete-user-warning',
|
id: 'message.delete-user-warning',
|
||||||
defaultMessage: 'Are you sure you want to delete the user {username}?',
|
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: {
|
deleteTeamWarning: {
|
||||||
id: 'message.delete-team-warning',
|
id: 'message.delete-team-warning',
|
||||||
defaultMessage: 'Are you sure you want to delete the team {name}?',
|
defaultMessage: 'Are you sure you want to delete the team {name}?',
|
||||||
|
32
components/pages/settings/teams/TeamLeaveForm.js
Normal file
32
components/pages/settings/teams/TeamLeaveForm.js
Normal file
@ -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 (
|
||||||
|
<Form onSubmit={handleSubmit} error={error}>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage {...messages.leaveTeamWarning} values={{ name: <b>{teamName}</b> }} />
|
||||||
|
</p>
|
||||||
|
<FormButtons flex>
|
||||||
|
<SubmitButton variant="danger" disabled={isLoading}>
|
||||||
|
{formatMessage(labels.leave)}
|
||||||
|
</SubmitButton>
|
||||||
|
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
@ -6,7 +6,7 @@ import useMessages from 'hooks/useMessages';
|
|||||||
export default function TeamMembers({ teamId, readOnly }) {
|
export default function TeamMembers({ teamId, readOnly }) {
|
||||||
const { toast, showToast } = useToast();
|
const { toast, showToast } = useToast();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, messages } = useMessages();
|
||||||
const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () =>
|
const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () =>
|
||||||
get(`/teams/${teamId}/users`),
|
get(`/teams/${teamId}/users`),
|
||||||
);
|
);
|
||||||
@ -17,7 +17,7 @@ export default function TeamMembers({ teamId, readOnly }) {
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
showToast({ message: formatMessage(labels.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
Button,
|
LoadingButton,
|
||||||
Icon,
|
Icon,
|
||||||
Icons,
|
Icons,
|
||||||
Flexbox,
|
Flexbox,
|
||||||
@ -15,12 +15,14 @@ import { ROLES } from 'lib/constants';
|
|||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function TeamMembersTable({ data = [], onSave, readOnly }) {
|
export default function TeamMembersTable({ data = [], onSave, readOnly }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { del, useMutation } = useApi();
|
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 = [
|
const columns = [
|
||||||
{ name: 'username', label: formatMessage(labels.username), style: { flex: 2 } },
|
{ name: 'username', label: formatMessage(labels.username), style: { flex: 2 } },
|
||||||
@ -29,12 +31,18 @@ export default function TeamMembersTable({ data = [], onSave, readOnly }) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleRemoveTeamMember = teamUserId => {
|
const handleRemoveTeamMember = teamUserId => {
|
||||||
|
setLoadingIds(prev => [...prev, teamUserId]);
|
||||||
|
|
||||||
mutate(
|
mutate(
|
||||||
{ teamUserId },
|
{ teamUserId },
|
||||||
{
|
{
|
||||||
onSuccess: async () => {
|
onSuccess: () => {
|
||||||
|
setLoadingIds(loadingIds.filter(a => a !== teamUserId));
|
||||||
onSave();
|
onSave();
|
||||||
},
|
},
|
||||||
|
onError: () => {
|
||||||
|
setLoadingIds(loadingIds.filter(a => a !== teamUserId));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -59,15 +67,16 @@ export default function TeamMembersTable({ data = [], onSave, readOnly }) {
|
|||||||
),
|
),
|
||||||
action: !readOnly && (
|
action: !readOnly && (
|
||||||
<Flexbox flex={1} justifyContent="end">
|
<Flexbox flex={1} justifyContent="end">
|
||||||
<Button
|
<LoadingButton
|
||||||
onClick={() => handleRemoveTeamMember(row.id)}
|
onClick={() => handleRemoveTeamMember(row.id)}
|
||||||
disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}
|
disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}
|
||||||
|
loading={isLoading && loadingIds.some(a => a === row.id)}
|
||||||
>
|
>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Close />
|
<Icons.Close />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(labels.remove)}</Text>
|
<Text>{formatMessage(labels.remove)}</Text>
|
||||||
</Button>
|
</LoadingButton>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
import TeamDeleteForm from './TeamDeleteForm';
|
import TeamDeleteForm from './TeamDeleteForm';
|
||||||
|
import TeamLeaveForm from './TeamLeaveForm';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
import { ROLES } from 'lib/constants';
|
import { ROLES } from 'lib/constants';
|
||||||
@ -42,9 +43,10 @@ export default function TeamsTable({ data = [], onDelete }) {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{(row, keys, rowIndex) => {
|
{(row, keys, rowIndex) => {
|
||||||
const { id } = row;
|
const { id, teamUser } = row;
|
||||||
const owner = row.teamUser.find(({ role }) => role === ROLES.teamOwner);
|
const owner = row.teamUser.find(({ role }) => role === ROLES.teamOwner);
|
||||||
const showDelete = user.id === owner?.userId;
|
const showDelete = user.id === owner?.userId;
|
||||||
|
const teamUserId = teamUser.find(a => a.userId === user.id).id;
|
||||||
|
|
||||||
const rowData = {
|
const rowData = {
|
||||||
...row,
|
...row,
|
||||||
@ -54,9 +56,9 @@ export default function TeamsTable({ data = [], onDelete }) {
|
|||||||
<Link href={`/settings/teams/${id}`}>
|
<Link href={`/settings/teams/${id}`}>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon>
|
<Icon>
|
||||||
<Icons.Edit />
|
<Icons.Show />
|
||||||
</Icon>
|
</Icon>
|
||||||
<Text>{formatMessage(labels.edit)}</Text>
|
<Text>{formatMessage(labels.view)}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{showDelete && (
|
{showDelete && (
|
||||||
@ -79,6 +81,26 @@ export default function TeamsTable({ data = [], onDelete }) {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</ModalTrigger>
|
</ModalTrigger>
|
||||||
)}
|
)}
|
||||||
|
{!showDelete && (
|
||||||
|
<ModalTrigger>
|
||||||
|
<Button>
|
||||||
|
<Icon>
|
||||||
|
<Icons.ArrowRight />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.leave)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal title={formatMessage(labels.leaveTeam)}>
|
||||||
|
{close => (
|
||||||
|
<TeamLeaveForm
|
||||||
|
teamUserId={teamUserId}
|
||||||
|
teamName={row.name}
|
||||||
|
onSave={onDelete}
|
||||||
|
onClose={close}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</ModalTrigger>
|
||||||
|
)}
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -175,6 +175,10 @@ export async function canDeleteTeamUser({ user }: Auth, teamUserId: string) {
|
|||||||
if (validate(teamUserId)) {
|
if (validate(teamUserId)) {
|
||||||
const removeUser = await getTeamUserById(teamUserId);
|
const removeUser = await getTeamUserById(teamUserId);
|
||||||
|
|
||||||
|
if (removeUser.userId === user.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const teamUser = await getTeamUser(removeUser.teamId, user.id);
|
const teamUser = await getTeamUser(removeUser.teamId, user.id);
|
||||||
|
|
||||||
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
|
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
|
||||||
|
@ -3,7 +3,7 @@ import { NextApiRequestQueryBody } from 'lib/types';
|
|||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { methodNotAllowed, ok, notFound } from 'next-basics';
|
import { methodNotAllowed, ok, notFound } from 'next-basics';
|
||||||
import { createTeamUser, getTeam } from 'queries';
|
import { createTeamUser, getTeam, getTeamUser } from 'queries';
|
||||||
import { ROLES } from 'lib/constants';
|
import { ROLES } from 'lib/constants';
|
||||||
|
|
||||||
export interface TeamsJoinRequestBody {
|
export interface TeamsJoinRequestBody {
|
||||||
@ -25,6 +25,12 @@ export default async (
|
|||||||
return notFound(res, 'message.team-not-found');
|
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);
|
await createTeamUser(req.auth.user.id, team.id, ROLES.teamMember);
|
||||||
|
|
||||||
return ok(res, team);
|
return ok(res, team);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user