mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 08:42:12 +10:00
feat: wip
This commit is contained in:
@ -3,40 +3,17 @@
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
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 CreateTeamDialog from '~/components/(teams)/dialogs/create-team-dialog';
|
||||
import UserTeamsPageDataTable from '~/components/(teams)/tables/user-teams-page-data-table';
|
||||
|
||||
import TeamEmailUsage from './team-email-usage';
|
||||
import { TeamInvitations } from './team-invitations';
|
||||
|
||||
export default function TeamsSettingsPage() {
|
||||
const { toast } = useToast();
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<SettingsHeader title="Teams" subtitle="Manage all teams you are currently associated with.">
|
||||
@ -58,35 +35,7 @@ export default function TeamsSettingsPage() {
|
||||
opacity: 0,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Todo: Teams - Add 'are you sure'. */}
|
||||
<Button
|
||||
variant="destructive"
|
||||
loading={isDeletingTeamEmail}
|
||||
onClick={async () => deleteTeamEmail({ teamId: teamEmail.team.id })}
|
||||
>
|
||||
Revoke access
|
||||
</Button>
|
||||
</div>
|
||||
<TeamEmailUsage teamEmail={teamEmail} />
|
||||
</motion.section>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
103
apps/web/src/app/(dashboard)/settings/teams/team-email-usage.tsx
Normal file
103
apps/web/src/app/(dashboard)/settings/teams/team-email-usage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -84,7 +84,9 @@ export const ConfirmTeamEmailTemplate = ({
|
||||
</Text>
|
||||
|
||||
<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">
|
||||
Allow document recipients to reply directly to this email address
|
||||
</li>
|
||||
|
||||
@ -8,15 +8,12 @@ export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, string> =
|
||||
|
||||
export const TEAM_MEMBER_ROLE_PERMISSIONS_MAP = {
|
||||
/**
|
||||
* Includes updating team name, url, logo, emails.
|
||||
*
|
||||
* Todo: Teams - Clean this up, merge etc.
|
||||
* Includes permissions to:
|
||||
* - Manage team members
|
||||
* - Manage team settings, changing name, url, etc.
|
||||
*/
|
||||
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],
|
||||
UPDATE_TEAM_MEMBERS: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER],
|
||||
} satisfies Record<string, TeamMemberRole[]>;
|
||||
|
||||
/**
|
||||
|
||||
@ -30,7 +30,7 @@ export const deleteTeamMemberInvitations = async ({
|
||||
userId,
|
||||
teamId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_INVITATIONS'],
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -36,7 +36,7 @@ export const deleteTeamMembers = async ({
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['DELETE_TEAM_MEMBERS'],
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
@ -27,7 +27,7 @@ export const updateTeamMember = async ({
|
||||
some: {
|
||||
userId,
|
||||
role: {
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['UPDATE_TEAM_MEMBERS'],
|
||||
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP['MANAGE_TEAM'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -84,8 +84,11 @@ export function Combobox<T = OptionValue>({
|
||||
return selectedOptions.map((option) => option.label).join(', ');
|
||||
}, [selectedOptions, emptySelectionPlaceholder, loading]);
|
||||
|
||||
const showClearButton = enableClearAllButton && selectedValues.length > 0;
|
||||
|
||||
return (
|
||||
<Popover open={open && !loading} onOpenChange={setOpen}>
|
||||
<div className="relative">
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -114,20 +117,11 @@ export function Combobox<T = OptionValue>({
|
||||
>
|
||||
<span className="truncate">{buttonLabel}</span>
|
||||
|
||||
<div className="ml-2 flex flex-row items-center">
|
||||
{enableClearAllButton && selectedValues.length > 0 && (
|
||||
// Todo: Teams - Can't have nested buttons.
|
||||
<button
|
||||
className="mr-1 flex h-4 w-4 items-center justify-center rounded-full bg-gray-300"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onChange([]);
|
||||
}}
|
||||
<div
|
||||
className={cn('ml-2 flex flex-row items-center', {
|
||||
'ml-6': showClearButton,
|
||||
})}
|
||||
>
|
||||
<XIcon className="text-muted-foreground h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -136,6 +130,19 @@ export function Combobox<T = OptionValue>({
|
||||
</Button>
|
||||
</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">
|
||||
<Command>
|
||||
<CommandInput placeholder={inputPlaceholder} />
|
||||
|
||||
Reference in New Issue
Block a user