mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 16:51:38 +10:00
feat: wip
This commit is contained in:
@ -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>
|
||||||
|
|||||||
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>
|
</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>
|
||||||
|
|||||||
@ -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[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
Reference in New Issue
Block a user