Team delete functionality.

This commit is contained in:
Mike Cao 2023-02-02 11:59:38 -08:00
parent 835289a1f8
commit 0ce2d1fbfc
11 changed files with 128 additions and 56 deletions

View File

@ -65,6 +65,7 @@ export const labels = defineMessages({
singleDay: { id: 'label.single-day', defaultMessage: 'Single day' },
dateRange: { id: 'label.date-range', defaultMessage: 'Date range' },
viewDetails: { id: 'label.view-details', defaultMessage: 'View details' },
deleteTeam: { id: 'label.delete-team', defaultMessage: 'Delete team' },
});
export const messages = defineMessages({
@ -132,6 +133,10 @@ export const messages = defineMessages({
id: 'message.team-not-found',
defaultMessage: 'Team not found.',
},
deleteTeam: {
id: 'message.delete-team',
defaultMessage: 'To delete this team, type {confirmation} in the box below to confirm.',
},
});
export const devices = defineMessages({

View File

@ -0,0 +1,44 @@
import {
Button,
Form,
FormRow,
FormButtons,
FormInput,
SubmitButton,
TextField,
} from 'react-basics';
import { useIntl } from 'react-intl';
import { labels, messages } from 'components/messages';
import useApi from 'hooks/useApi';
const CONFIRM_VALUE = 'DELETE';
export default function TeamDeleteForm({ teamId, onSave, onClose }) {
const { formatMessage } = useIntl();
const { del, useMutation } = useApi();
const { mutate, error } = useMutation(data => del(`/teams/${teamId}`, data));
const handleSubmit = async data => {
mutate(data, {
onSuccess: async () => {
onSave();
onClose();
},
});
};
return (
<Form onSubmit={handleSubmit} error={error}>
<p>{formatMessage(messages.deleteTeam, { confirmation: CONFIRM_VALUE })}</p>
<FormRow label={formatMessage(labels.confirm)}>
<FormInput name="confirmation" rules={{ validate: value => value === CONFIRM_VALUE }}>
<TextField autoComplete="off" />
</FormInput>
</FormRow>
<FormButtons flex>
<SubmitButton variant="danger">{formatMessage(labels.delete)}</SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons>
</Form>
);
}

View File

@ -16,7 +16,7 @@ import { labels } from 'components/messages';
const generateId = () => getRandomChars(16);
export default function TeamEditForm({ teamId, data, onSave }) {
export default function TeamEditForm({ teamId, data, onSave, readOnly }) {
const { formatMessage } = useIntl();
const { post, useMutation } = useApi();
const { mutate, error } = useMutation(data => post(`/teams/${teamId}`, data));
@ -47,19 +47,26 @@ export default function TeamEditForm({ teamId, data, onSave }) {
<TextField value={teamId} readOnly allowCopy />
</FormRow>
<FormRow label={formatMessage(labels.name)}>
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
<TextField />
</FormInput>
{!readOnly && (
<FormInput name="name" rules={{ required: formatMessage(labels.required) }}>
<TextField />
</FormInput>
)}
{readOnly && data.name}
</FormRow>
<FormRow label={formatMessage(labels.accessCode)}>
<Flexbox gap={10}>
<TextField value={accessCode} readOnly allowCopy />
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
{!readOnly && (
<Button onClick={handleRegenerate}>{formatMessage(labels.regenerate)}</Button>
)}
</Flexbox>
</FormRow>
<FormButtons>
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
</FormButtons>
{!readOnly && (
<FormButtons>
<SubmitButton variant="primary">{formatMessage(labels.save)}</SubmitButton>
</FormButtons>
)}
</Form>
);
}

View File

@ -2,7 +2,7 @@ import { Loading } from 'react-basics';
import useApi from 'hooks/useApi';
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
export default function TeamMembers({ teamId }) {
export default function TeamMembers({ teamId, readOnly }) {
const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(['teams:users', teamId], () =>
get(`/teams/${teamId}/users`),
@ -12,5 +12,5 @@ export default function TeamMembers({ teamId }) {
return <Loading icon="dots" position="block" />;
}
return <TeamMembersTable data={data} />;
return <TeamMembersTable data={data} readOnly={readOnly} />;
}

View File

@ -16,7 +16,7 @@ import { ROLES } from 'lib/constants';
import { labels } from 'components/messages';
import useUser from 'hooks/useUser';
export default function TeamMembersTable({ data = [] }) {
export default function TeamMembersTable({ data = [], readOnly }) {
const { formatMessage } = useIntl();
const { user } = useUser();
@ -44,9 +44,9 @@ export default function TeamMembersTable({ data = [] }) {
role: formatMessage(
labels[Object.keys(ROLES).find(key => ROLES[key] === row.role) || labels.unknown],
),
action: (
action: !readOnly && (
<Flexbox flex={1} justifyContent="end">
<Button disabled={user.id === row?.user?.id}>
<Button disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}>
<Icon>
<Icons.Close />
</Icon>

View File

@ -1,17 +1,20 @@
import { useEffect, useState } from 'react';
import { useIntl } from 'react-intl';
import { Breadcrumbs, Item, Tabs, useToast } from 'react-basics';
import useApi from 'hooks/useApi';
import Link from 'next/link';
import Page from 'components/layout/Page';
import TeamEditForm from 'components/pages/settings/teams/TeamEditForm';
import PageHeader from 'components/layout/PageHeader';
import TeamMembers from 'components/pages/settings/teams/TeamMembers';
import { labels, messages } from 'components/messages';
import { ROLES } from 'lib/constants';
import useUser from 'hooks/useUser';
import useApi from 'hooks/useApi';
import TeamEditForm from './TeamEditForm';
import TeamMembers from './TeamMembers';
import TeamWebsites from './TeamWebsites';
export default function TeamSettings({ teamId }) {
const { formatMessage } = useIntl();
const { user } = useUser();
const [values, setValues] = useState(null);
const [tab, setTab] = useState('details');
const { get, useQuery } = useApi();
@ -25,6 +28,9 @@ export default function TeamSettings({ teamId }) {
},
{ cacheTime: 0 },
);
const canEdit = data?.teamUser?.find(
({ userId, role }) => role === ROLES.teamOwner && userId === user.id,
);
const handleSave = data => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
@ -55,9 +61,11 @@ export default function TeamSettings({ teamId }) {
<Item key="members">{formatMessage(labels.members)}</Item>
<Item key="websites">{formatMessage(labels.websites)}</Item>
</Tabs>
{tab === 'details' && <TeamEditForm teamId={teamId} data={values} onSave={handleSave} />}
{tab === 'members' && <TeamMembers teamId={teamId} />}
{tab === 'websites' && <TeamWebsites teamId={teamId} />}
{tab === 'details' && (
<TeamEditForm teamId={teamId} data={values} onSave={handleSave} readOnly={!canEdit} />
)}
{tab === 'members' && <TeamMembers teamId={teamId} readOnly={!canEdit} />}
{tab === 'websites' && <TeamWebsites teamId={teamId} readOnly={!canEdit} />}
</Page>
);
}

View File

@ -25,6 +25,12 @@ export default function TeamsList() {
};
const handleJoin = () => {
setUpdate(state => state + 1);
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
const handleDelete = () => {
setUpdate(state => state + 1);
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
@ -67,7 +73,7 @@ export default function TeamsList() {
</Flexbox>
)}
</PageHeader>
{hasData && <TeamsTable data={data} />}
{hasData && <TeamsTable data={data} onDelete={handleDelete} />}
{!hasData && (
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
{createButton}

View File

@ -11,12 +11,15 @@ import {
Flexbox,
Icons,
Text,
ModalTrigger,
Modal,
} from 'react-basics';
import { useIntl } from 'react-intl';
import { labels } from 'components/messages';
import { ROLES } from 'lib/constants';
import TeamDeleteForm from './TeamDeleteForm';
export default function TeamsTable({ data = [] }) {
export default function TeamsTable({ data = [], onDelete }) {
const { formatMessage } = useIntl();
const columns = [
@ -44,7 +47,7 @@ export default function TeamsTable({ data = [] }) {
...row,
owner: row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username,
action: (
<Flexbox flex={1} justifyContent="end">
<Flexbox flex={1} gap={10} justifyContent="end">
<Link href={`/settings/teams/${id}`}>
<a>
<Button>
@ -55,6 +58,17 @@ export default function TeamsTable({ data = [] }) {
</Button>
</a>
</Link>
<ModalTrigger>
<Button>
<Icon>
<Icons.Trash />
</Icon>
<Text>{formatMessage(labels.delete)}</Text>
</Button>
<Modal title={formatMessage(labels.deleteTeam)}>
{close => <TeamDeleteForm teamId={row.id} onSave={onDelete} onClose={close} />}
</Modal>
</ModalTrigger>
</Flexbox>
),
};

View File

@ -1,4 +1,5 @@
import debug from 'debug';
import { validate } from 'uuid';
import cache from 'lib/cache';
import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants';
import { secret } from 'lib/crypto';
@ -60,10 +61,6 @@ export async function canViewWebsite({ user }: Auth, websiteId: string) {
return user.id === website.userId;
}
if (website.teamId) {
return getTeamUser(website.teamId, user.id);
}
return false;
}
@ -86,18 +83,16 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
return true;
}
if (!validate(websiteId)) {
return false;
}
const website = await cache.fetchWebsite(websiteId);
if (website.userId) {
return user.id === website.userId;
}
if (website.teamId) {
const teamUser = await getTeamUser(website.teamId, user.id);
return hasPermission(teamUser.role, PERMISSIONS.websiteUpdate);
}
return false;
}
@ -112,12 +107,6 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
return user.id === website.userId;
}
if (website.teamId) {
const teamUser = await getTeamUser(website.teamId, user.id);
return hasPermission(teamUser.role, PERMISSIONS.websiteDelete);
}
return false;
}
@ -144,9 +133,13 @@ export async function canUpdateTeam({ user }: Auth, teamId: string) {
return true;
}
const teamUser = await getTeamUser(teamId, user.id);
if (validate(teamId)) {
const teamUser = await getTeamUser(teamId, user.id);
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
}
return false;
}
export async function canDeleteTeam({ user }: Auth, teamId: string) {
@ -154,9 +147,13 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
return true;
}
const teamUser = await getTeamUser(teamId, user.id);
if (validate(teamId)) {
const teamUser = await getTeamUser(teamId, user.id);
return hasPermission(teamUser.role, PERMISSIONS.teamDelete);
return hasPermission(teamUser.role, PERMISSIONS.teamDelete);
}
return false;
}
export async function canCreateUser({ user }: Auth) {

View File

@ -33,7 +33,6 @@ export const ROLES = {
user: 'user',
teamOwner: 'team-owner',
teamMember: 'team-member',
teamGuest: 'team-guest',
} as const;
export const PERMISSIONS = {
@ -54,19 +53,8 @@ export const ROLE_PERMISSIONS = {
PERMISSIONS.websiteDelete,
PERMISSIONS.teamCreate,
],
[ROLES.teamOwner]: [
PERMISSIONS.teamUpdate,
PERMISSIONS.teamDelete,
PERMISSIONS.websiteCreate,
PERMISSIONS.websiteUpdate,
PERMISSIONS.websiteDelete,
],
[ROLES.teamMember]: [
PERMISSIONS.websiteCreate,
PERMISSIONS.websiteUpdate,
PERMISSIONS.websiteDelete,
],
[ROLES.teamGuest]: [],
[ROLES.teamOwner]: [PERMISSIONS.teamUpdate, PERMISSIONS.teamDelete],
[ROLES.teamMember]: [],
} as const;
export const THEME_COLORS = {

View File

@ -6,6 +6,9 @@ import { ROLES } from 'lib/constants';
export async function getTeam(where: Prisma.TeamWhereInput): Promise<Team> {
return prisma.client.team.findFirst({
where,
include: {
teamUser: true,
},
});
}