This commit is contained in:
David Nguyen
2025-05-07 15:03:20 +10:00
parent 419bc02171
commit 7abfc9e271
390 changed files with 21254 additions and 12607 deletions

View File

@ -1,187 +0,0 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { AnimatePresence } from 'framer-motion';
import { BellIcon } from 'lucide-react';
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
import { formatTeamUrl } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export const TeamInvitations = () => {
const { data, isLoading } = trpc.team.getTeamInvitations.useQuery();
return (
<AnimatePresence>
{data && data.length > 0 && !isLoading && (
<AnimateGenericFadeInOut>
<Alert variant="secondary">
<div className="flex h-full flex-row items-center p-2">
<BellIcon className="mr-4 h-5 w-5 text-blue-800" />
<AlertDescription className="mr-2">
<Plural
value={data.length}
one={
<span>
You have <strong>1</strong> pending team invitation
</span>
}
other={
<span>
You have <strong>#</strong> pending team invitations
</span>
}
/>
</AlertDescription>
<Dialog>
<DialogTrigger asChild>
<button className="ml-auto text-sm font-medium text-blue-700 hover:text-blue-600">
<Trans>View invites</Trans>
</button>
</DialogTrigger>
<DialogContent position="center">
<DialogHeader>
<DialogTitle>
<Trans>Pending invitations</Trans>
</DialogTitle>
<DialogDescription className="mt-4">
<Plural
value={data.length}
one={
<span>
You have <strong>1</strong> pending team invitation
</span>
}
other={
<span>
You have <strong>#</strong> pending team invitations
</span>
}
/>
</DialogDescription>
</DialogHeader>
<ul className="-mx-6 -mb-6 max-h-[80vh] divide-y overflow-auto px-6 pb-6 xl:max-h-[70vh]">
{data.map((invitation) => (
<li key={invitation.teamId}>
<AvatarWithText
avatarSrc={formatAvatarUrl(invitation.team.avatarImageId)}
className="w-full max-w-none py-4"
avatarFallback={invitation.team.name.slice(0, 1)}
primaryText={
<span className="text-foreground/80 font-semibold">
{invitation.team.name}
</span>
}
secondaryText={formatTeamUrl(invitation.team.url)}
rightSideComponent={
<div className="ml-auto space-x-2">
<DeclineTeamInvitationButton teamId={invitation.team.id} />
<AcceptTeamInvitationButton teamId={invitation.team.id} />
</div>
}
/>
</li>
))}
</ul>
</DialogContent>
</Dialog>
</div>
</Alert>
</AnimateGenericFadeInOut>
)}
</AnimatePresence>
);
};
const AcceptTeamInvitationButton = ({ teamId }: { teamId: number }) => {
const { _ } = useLingui();
const { toast } = useToast();
const {
mutateAsync: acceptTeamInvitation,
isPending,
isSuccess,
} = trpc.team.acceptTeamInvitation.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Accepted team invitation`),
duration: 5000,
});
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Unable to join this team at this time.`),
variant: 'destructive',
duration: 10000,
});
},
});
return (
<Button
onClick={async () => acceptTeamInvitation({ teamId })}
loading={isPending}
disabled={isPending || isSuccess}
>
<Trans>Accept</Trans>
</Button>
);
};
const DeclineTeamInvitationButton = ({ teamId }: { teamId: number }) => {
const { _ } = useLingui();
const { toast } = useToast();
const {
mutateAsync: declineTeamInvitation,
isPending,
isSuccess,
} = trpc.team.declineTeamInvitation.useMutation({
onSuccess: () => {
toast({
title: _(msg`Success`),
description: _(msg`Declined team invitation`),
duration: 5000,
});
},
onError: () => {
toast({
title: _(msg`Something went wrong`),
description: _(msg`Unable to decline this team invitation at this time.`),
variant: 'destructive',
duration: 10000,
});
},
});
return (
<Button
onClick={async () => declineTeamInvitation({ teamId })}
loading={isPending}
disabled={isPending || isSuccess}
variant="ghost"
>
<Trans>Decline</Trans>
</Button>
);
};

View File

@ -1,7 +1,16 @@
import type { HTMLAttributes } from 'react';
import { Trans } from '@lingui/react/macro';
import { Braces, CreditCard, Globe2Icon, Settings, Settings2, Users, Webhook } from 'lucide-react';
import {
Braces,
CreditCard,
Globe2Icon,
GroupIcon,
Settings,
Settings2,
Users,
Webhook,
} from 'lucide-react';
import { Link, useLocation, useParams } from 'react-router';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
@ -20,6 +29,7 @@ export const TeamSettingsNavDesktop = ({ className, ...props }: TeamSettingsNavD
const preferencesPath = `/t/${teamUrl}/settings/preferences`;
const publicProfilePath = `/t/${teamUrl}/settings/public-profile`;
const membersPath = `/t/${teamUrl}/settings/members`;
const groupsPath = `/t/${teamUrl}/settings/groups`;
const tokensPath = `/t/${teamUrl}/settings/tokens`;
const webhooksPath = `/t/${teamUrl}/settings/webhooks`;
const billingPath = `/t/${teamUrl}/settings/billing`;
@ -76,6 +86,16 @@ export const TeamSettingsNavDesktop = ({ className, ...props }: TeamSettingsNavD
</Button>
</Link>
<Link to={groupsPath}>
<Button
variant="ghost"
className={cn('w-full justify-start', pathname?.startsWith(groupsPath) && 'bg-secondary')}
>
<GroupIcon className="mr-2 h-5 w-5" />
<Trans>Groups</Trans>
</Button>
</Link>
<Link to={tokensPath}>
<Button
variant="ghost"

View File

@ -1,7 +1,16 @@
import type { HTMLAttributes } from 'react';
import { Trans } from '@lingui/react/macro';
import { Braces, CreditCard, Globe2Icon, Key, Settings2, User, Webhook } from 'lucide-react';
import {
Braces,
CreditCard,
Globe2Icon,
GroupIcon,
Key,
Settings2,
User,
Webhook,
} from 'lucide-react';
import { Link, useLocation, useParams } from 'react-router';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
@ -20,6 +29,7 @@ export const TeamSettingsNavMobile = ({ className, ...props }: TeamSettingsNavMo
const preferencesPath = `/t/${teamUrl}/preferences`;
const publicProfilePath = `/t/${teamUrl}/settings/public-profile`;
const membersPath = `/t/${teamUrl}/settings/members`;
const groupsPath = `/t/${teamUrl}/settings/groups`;
const tokensPath = `/t/${teamUrl}/settings/tokens`;
const webhooksPath = `/t/${teamUrl}/settings/webhooks`;
const billingPath = `/t/${teamUrl}/settings/billing`;
@ -85,6 +95,16 @@ export const TeamSettingsNavMobile = ({ className, ...props }: TeamSettingsNavMo
</Button>
</Link>
<Link to={groupsPath}>
<Button
variant="ghost"
className={cn('w-full justify-start', pathname?.startsWith(groupsPath) && 'bg-secondary')}
>
<GroupIcon className="mr-2 h-5 w-5" />
<Trans>Groups</Trans>
</Button>
</Link>
<Link to={tokensPath}>
<Button
variant="ghost"

View File

@ -1,126 +0,0 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TeamMemberRole, TeamTransferVerification } from '@prisma/client';
import { AnimatePresence } from 'framer-motion';
import { useRevalidator } from 'react-router';
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
import { isTokenExpired } from '@documenso/lib/utils/token-verification';
import { trpc } from '@documenso/trpc/react';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import { cn } from '@documenso/ui/lib/utils';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type TeamTransferStatusProps = {
className?: string;
currentUserTeamRole: TeamMemberRole;
teamId: number;
transferVerification: Pick<TeamTransferVerification, 'email' | 'expiresAt' | 'name'> | null;
};
export const TeamTransferStatus = ({
className,
currentUserTeamRole,
teamId,
transferVerification,
}: TeamTransferStatusProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { revalidate } = useRevalidator();
const isExpired = transferVerification && isTokenExpired(transferVerification.expiresAt);
const { mutateAsync: deleteTeamTransferRequest, isPending } =
trpc.team.deleteTeamTransferRequest.useMutation({
onSuccess: async () => {
if (!isExpired) {
toast({
title: _(msg`Success`),
description: _(msg`The team transfer invitation has been successfully deleted.`),
duration: 5000,
});
}
await revalidate();
},
onError: () => {
toast({
title: _(msg`An unknown error occurred`),
description: _(
msg`We encountered an unknown error while attempting to remove this transfer. Please try again or contact support.`,
),
variant: 'destructive',
});
},
});
return (
<AnimatePresence>
{transferVerification && (
<AnimateGenericFadeInOut>
<Alert
variant={isExpired ? 'destructive' : 'warning'}
className={cn(
'flex flex-col justify-between p-6 sm:flex-row sm:items-center',
className,
)}
>
<div>
<AlertTitle>
{isExpired ? (
<Trans>Team transfer request expired</Trans>
) : (
<Trans>Team transfer in progress</Trans>
)}
</AlertTitle>
<AlertDescription>
{isExpired ? (
<p className="text-sm">
<Trans>
The team transfer request to <strong>{transferVerification.name}</strong> has
expired.
</Trans>
</p>
) : (
<section className="text-sm">
<p>
<Trans>
A request to transfer the ownership of this team has been sent to{' '}
<strong>
{transferVerification.name} ({transferVerification.email})
</strong>
</Trans>
</p>
<p>
<Trans>
If they accept this request, the team will be transferred to their account.
</Trans>
</p>
</section>
)}
</AlertDescription>
</div>
{canExecuteTeamAction('DELETE_TEAM_TRANSFER_REQUEST', currentUserTeamRole) && (
<Button
onClick={async () => deleteTeamTransferRequest({ teamId })}
loading={isPending}
variant={isExpired ? 'destructive' : 'ghost'}
className={cn('ml-auto', {
'hover:bg-transparent hover:text-blue-800': !isExpired,
})}
>
{isExpired ? <Trans>Close</Trans> : <Trans>Cancel</Trans>}
</Button>
)}
</Alert>
</AnimateGenericFadeInOut>
)}
</AnimatePresence>
);
};