feat: wip

This commit is contained in:
David Nguyen
2023-12-27 17:32:56 +11:00
parent ca703fc221
commit 917a1271bf
8 changed files with 170 additions and 112 deletions

View File

@ -3,40 +3,17 @@
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
import SettingsHeader from '~/components/(dashboard)/settings/layout/header'; import SettingsHeader from '~/components/(dashboard)/settings/layout/header';
import CreateTeamDialog from '~/components/(teams)/dialogs/create-team-dialog'; import CreateTeamDialog from '~/components/(teams)/dialogs/create-team-dialog';
import UserTeamsPageDataTable from '~/components/(teams)/tables/user-teams-page-data-table'; import UserTeamsPageDataTable from '~/components/(teams)/tables/user-teams-page-data-table';
import TeamEmailUsage from './team-email-usage';
import { TeamInvitations } from './team-invitations'; import { TeamInvitations } from './team-invitations';
export default function TeamsSettingsPage() { export default function TeamsSettingsPage() {
const { toast } = useToast();
const { data: teamEmail } = trpc.team.getTeamEmailByEmail.useQuery(); const { data: teamEmail } = trpc.team.getTeamEmailByEmail.useQuery();
const { mutateAsync: deleteTeamEmail, isLoading: isDeletingTeamEmail } =
trpc.team.deleteTeamEmail.useMutation({
onSuccess: () => {
toast({
title: 'Success',
description: 'You have successfully revoked access.',
duration: 5000,
});
},
onError: () => {
toast({
title: 'Something went wrong',
variant: 'destructive',
duration: 10000,
description:
'We encountered an unknown error while attempting to revoke access. Please try again or contact support.',
});
},
});
return ( return (
<div> <div>
<SettingsHeader title="Teams" subtitle="Manage all teams you are currently associated with."> <SettingsHeader title="Teams" subtitle="Manage all teams you are currently associated with.">
@ -58,35 +35,7 @@ export default function TeamsSettingsPage() {
opacity: 0, opacity: 0,
}} }}
> >
<div className="mt-8 flex flex-row items-center justify-between rounded-lg bg-gray-50/70 p-6"> <TeamEmailUsage teamEmail={teamEmail} />
<div className="text-sm">
<h3 className="text-base font-medium">Team email</h3>
<p className="text-muted-foreground">
Your email is currently being used by team{' '}
<span className="font-semibold">{teamEmail.team.name}</span> ({teamEmail.team.url}
).
</p>
<p className="text-muted-foreground mt-1">
They have permission on your behalf to:
</p>
<ul className="text-muted-foreground mt-0.5 list-inside list-disc">
<li>Display your name and email in documents</li>
<li>View all documents sent to your account</li>
</ul>
</div>
{/* Todo: Teams - Add 'are you sure'. */}
<Button
variant="destructive"
loading={isDeletingTeamEmail}
onClick={async () => deleteTeamEmail({ teamId: teamEmail.team.id })}
>
Revoke access
</Button>
</div>
</motion.section> </motion.section>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@ -0,0 +1,103 @@
'use client';
import { useState } from 'react';
import type { TeamEmail } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type TeamEmailUsageProps = {
teamEmail: TeamEmail & { team: { name: string; url: string } };
};
export default function TeamEmailUsage({ teamEmail }: TeamEmailUsageProps) {
const [open, setOpen] = useState(false);
const { toast } = useToast();
const { mutateAsync: deleteTeamEmail, isLoading: isDeletingTeamEmail } =
trpc.team.deleteTeamEmail.useMutation({
onSuccess: () => {
toast({
title: 'Success',
description: 'You have successfully revoked access.',
duration: 5000,
});
},
onError: () => {
toast({
title: 'Something went wrong',
variant: 'destructive',
duration: 10000,
description:
'We encountered an unknown error while attempting to revoke access. Please try again or contact support.',
});
},
});
return (
<div className="mt-8 flex flex-row items-center justify-between rounded-lg bg-gray-50/70 p-6">
<div className="text-sm">
<h3 className="text-base font-medium">Team email</h3>
<p className="text-muted-foreground">
Your email is currently being used by team{' '}
<span className="font-semibold">{teamEmail.team.name}</span> ({teamEmail.team.url}
).
</p>
<p className="text-muted-foreground mt-1">They have permission on your behalf to:</p>
<ul className="text-muted-foreground mt-0.5 list-inside list-disc">
<li>Display your name and email in documents</li>
<li>View all documents sent to your account</li>
</ul>
</div>
<Dialog open={open} onOpenChange={(value) => !isDeletingTeamEmail && setOpen(value)}>
<DialogTrigger asChild>
<Button variant="destructive">Revoke access</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription className="mt-4">
You are about to revoke access for team{' '}
<span className="font-semibold">{teamEmail.team.name}</span> ({teamEmail.team.url}) to
use your email.
</DialogDescription>
</DialogHeader>
<fieldset disabled={isDeletingTeamEmail}>
<DialogFooter className="space-x-4">
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
type="submit"
variant="destructive"
loading={isDeletingTeamEmail}
onClick={async () => deleteTeamEmail({ teamId: teamEmail.teamId })}
>
Revoke
</Button>
</DialogFooter>
</fieldset>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -84,7 +84,9 @@ export const ConfirmTeamEmailTemplate = ({
</Text> </Text>
<ul className="mb-0 mt-2"> <ul className="mb-0 mt-2">
<li className="text-sm">View all documents sent to this email address</li> <li className="text-sm">
View all documents sent to and from this email address
</li>
<li className="mt-1 text-sm"> <li className="mt-1 text-sm">
Allow document recipients to reply directly to this email address Allow document recipients to reply directly to this email address
</li> </li>

View File

@ -8,15 +8,12 @@ export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, string> =
export const TEAM_MEMBER_ROLE_PERMISSIONS_MAP = { export const TEAM_MEMBER_ROLE_PERMISSIONS_MAP = {
/** /**
* Includes updating team name, url, logo, emails. * Includes permissions to:
* * - Manage team members
* Todo: Teams - Clean this up, merge etc. * - Manage team settings, changing name, url, etc.
*/ */
MANAGE_TEAM: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER], MANAGE_TEAM: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
DELETE_INVITATIONS: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
DELETE_TEAM_MEMBERS: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
DELETE_TEAM_TRANSFER_REQUEST: [TeamMemberRole.ADMIN], DELETE_TEAM_TRANSFER_REQUEST: [TeamMemberRole.ADMIN],
UPDATE_TEAM_MEMBERS: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
} satisfies Record<string, TeamMemberRole[]>; } satisfies Record<string, TeamMemberRole[]>;
/** /**

View File

@ -30,7 +30,7 @@ export const deleteTeamMemberInvitations = async ({
userId, userId,
teamId, teamId,
role: { role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_INVITATIONS'], in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
}, },
}, },
}); });

View File

@ -36,7 +36,7 @@ export const deleteTeamMembers = async ({
some: { some: {
userId, userId,
role: { role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_TEAM_MEMBERS'], in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
}, },
}, },
}, },

View File

@ -1,5 +1,5 @@
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { TeamMemberRole } from '@documenso/prisma/client'; import type { TeamMemberRole } from '@documenso/prisma/client';
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams'; import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../../constants/teams';
@ -27,7 +27,7 @@ export const updateTeamMember = async ({
some: { some: {
userId, userId,
role: { role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['UPDATE_TEAM_MEMBERS'], in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
}, },
}, },
}, },

View File

@ -84,8 +84,11 @@ export function Combobox<T = OptionValue>({
return selectedOptions.map((option) => option.label).join(', '); return selectedOptions.map((option) => option.label).join(', ');
}, [selectedOptions, emptySelectionPlaceholder, loading]); }, [selectedOptions, emptySelectionPlaceholder, loading]);
const showClearButton = enableClearAllButton && selectedValues.length > 0;
return ( return (
<Popover open={open && !loading} onOpenChange={setOpen}> <Popover open={open && !loading} onOpenChange={setOpen}>
<div className="relative">
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
variant="outline" variant="outline"
@ -114,20 +117,11 @@ export function Combobox<T = OptionValue>({
> >
<span className="truncate">{buttonLabel}</span> <span className="truncate">{buttonLabel}</span>
<div className="ml-2 flex flex-row items-center"> <div
{enableClearAllButton && selectedValues.length > 0 && ( className={cn('ml-2 flex flex-row items-center', {
// Todo: Teams - Can't have nested buttons. 'ml-6': showClearButton,
<button })}
className="mr-1 flex h-4 w-4 items-center justify-center rounded-full bg-gray-300"
onClick={(e) => {
e.preventDefault();
onChange([]);
}}
> >
<XIcon className="text-muted-foreground h-3.5 w-3.5" />
</button>
)}
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
</div> </div>
</motion.div> </motion.div>
@ -136,6 +130,19 @@ export function Combobox<T = OptionValue>({
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
{/* This is placed outside the trigger since we can't have nested buttons. */}
{showClearButton && (
<div className="absolute bottom-0 right-8 top-0 flex items-center justify-center">
<button
className="flex h-4 w-4 items-center justify-center rounded-full bg-gray-300"
onClick={() => onChange([])}
>
<XIcon className="text-muted-foreground h-3.5 w-3.5" />
</button>
</div>
)}
</div>
<PopoverContent className="w-[200px] p-0"> <PopoverContent className="w-[200px] p-0">
<Command> <Command>
<CommandInput placeholder={inputPlaceholder} /> <CommandInput placeholder={inputPlaceholder} />