mirror of
https://github.com/documenso/documenso.git
synced 2025-11-16 17:51:49 +10:00
fix: wip
This commit is contained in:
@ -1,10 +1,8 @@
|
||||
import { type HTMLAttributes, useEffect, useState } from 'react';
|
||||
|
||||
import { MenuIcon, SearchIcon } from 'lucide-react';
|
||||
import { Link, useLocation, useParams } from 'react-router';
|
||||
import { Link, useParams } from 'react-router';
|
||||
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import { getRootHref } from '@documenso/lib/utils/params';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
|
||||
@ -15,14 +13,10 @@ import { AppNavDesktop } from './app-nav-desktop';
|
||||
import { AppNavMobile } from './app-nav-mobile';
|
||||
import { MenuSwitcher } from './menu-switcher';
|
||||
|
||||
export type HeaderProps = HTMLAttributes<HTMLDivElement> & {
|
||||
user: SessionUser;
|
||||
teams: TGetTeamsResponse;
|
||||
};
|
||||
export type HeaderProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
export const Header = ({ className, ...props }: HeaderProps) => {
|
||||
const params = useParams();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [isCommandMenuOpen, setIsCommandMenuOpen] = useState(false);
|
||||
const [isHamburgerMenuOpen, setIsHamburgerMenuOpen] = useState(false);
|
||||
@ -38,16 +32,6 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
const isPathTeamUrl = (teamUrl: string) => {
|
||||
if (!pathname || !pathname.startsWith(`/t/`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pathname.split('/')[2] === teamUrl;
|
||||
};
|
||||
|
||||
const selectedTeam = teams?.find((team) => isPathTeamUrl(team.url));
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
@ -59,7 +43,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
>
|
||||
<div className="mx-auto flex w-full max-w-screen-xl items-center justify-between gap-x-4 px-4 md:justify-normal md:px-8">
|
||||
<Link
|
||||
to={`${getRootHref(params, { returnEmptyRootString: true })}/documents`}
|
||||
to={`${getRootHref(params, { returnEmptyRootString: true })}`}
|
||||
className="focus-visible:ring-ring ring-offset-background hidden rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 md:inline"
|
||||
>
|
||||
<BrandingLogo className="h-6 w-auto" />
|
||||
@ -67,11 +51,8 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
||||
|
||||
<AppNavDesktop setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
||||
|
||||
<div
|
||||
className="flex gap-x-4 md:ml-8"
|
||||
title={selectedTeam ? selectedTeam.name : (user.name ?? '')}
|
||||
>
|
||||
<MenuSwitcher user={user} teams={teams} />
|
||||
<div className="flex gap-x-4 md:ml-8">
|
||||
<MenuSwitcher />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center space-x-4 md:hidden">
|
||||
|
||||
@ -4,6 +4,8 @@ import { useEffect, useState } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { motion } from 'framer-motion';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { Search } from 'lucide-react';
|
||||
import { Link, useLocation, useParams } from 'react-router';
|
||||
|
||||
@ -55,23 +57,34 @@ export const AppNavDesktop = ({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-baseline gap-x-6">
|
||||
{navigationLinks.map(({ href, label }) => (
|
||||
<Link
|
||||
key={href}
|
||||
to={`${rootHref}${href}`}
|
||||
className={cn(
|
||||
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
||||
{
|
||||
'text-foreground dark:text-muted-foreground': pathname?.startsWith(
|
||||
`${rootHref}${href}`,
|
||||
),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{_(label)}
|
||||
</Link>
|
||||
))}
|
||||
<div>
|
||||
<AnimatePresence>
|
||||
{params.teamUrl && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="flex items-baseline gap-x-6"
|
||||
>
|
||||
{navigationLinks.map(({ href, label }) => (
|
||||
<Link
|
||||
key={href}
|
||||
to={`${rootHref}${href}`}
|
||||
className={cn(
|
||||
'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
|
||||
{
|
||||
'text-foreground dark:text-muted-foreground': pathname?.startsWith(
|
||||
`${rootHref}${href}`,
|
||||
),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{_(label)}
|
||||
</Link>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
@ -47,6 +47,7 @@ export type DocumentSigningPageViewProps = {
|
||||
completedFields: CompletedField[];
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
includeSenderDetails: boolean;
|
||||
};
|
||||
|
||||
export const DocumentSigningPageView = ({
|
||||
@ -56,18 +57,16 @@ export const DocumentSigningPageView = ({
|
||||
completedFields,
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
includeSenderDetails,
|
||||
}: DocumentSigningPageViewProps) => {
|
||||
const { documentData, documentMeta } = document;
|
||||
|
||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
||||
|
||||
const shouldUseTeamDetails =
|
||||
document.teamId && document.team?.teamGlobalSettings?.includeSenderDetails === false;
|
||||
|
||||
let senderName = document.user.name ?? '';
|
||||
let senderEmail = `(${document.user.email})`;
|
||||
|
||||
if (shouldUseTeamDetails) {
|
||||
if (includeSenderDetails) {
|
||||
senderName = document.team?.name ?? '';
|
||||
senderEmail = document.team?.teamEmail?.email ? `(${document.team.teamEmail.email})` : '';
|
||||
}
|
||||
@ -92,7 +91,7 @@ export const DocumentSigningPageView = ({
|
||||
<span className="text-muted-foreground">
|
||||
{match(recipient.role)
|
||||
.with(RecipientRole.VIEWER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
includeSenderDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to view this document
|
||||
</Trans>
|
||||
@ -101,7 +100,7 @@ export const DocumentSigningPageView = ({
|
||||
),
|
||||
)
|
||||
.with(RecipientRole.SIGNER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
includeSenderDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to sign this document
|
||||
</Trans>
|
||||
@ -110,7 +109,7 @@ export const DocumentSigningPageView = ({
|
||||
),
|
||||
)
|
||||
.with(RecipientRole.APPROVER, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
includeSenderDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to approve this document
|
||||
</Trans>
|
||||
@ -119,7 +118,7 @@ export const DocumentSigningPageView = ({
|
||||
),
|
||||
)
|
||||
.with(RecipientRole.ASSISTANT, () =>
|
||||
document.teamId && !shouldUseTeamDetails ? (
|
||||
includeSenderDetails ? (
|
||||
<Trans>
|
||||
on behalf of "{document.team?.name}" has invited you to assist this document
|
||||
</Trans>
|
||||
|
||||
@ -29,7 +29,7 @@ import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
|
||||
import { Stepper } from '@documenso/ui/primitives/stepper';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type DocumentEditFormProps = {
|
||||
className?: string;
|
||||
@ -53,7 +53,7 @@ export const DocumentEditForm = ({
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
const team = useOptionalCurrentTeam();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const [isDocumentPdfLoaded, setIsDocumentPdfLoaded] = useState(false);
|
||||
|
||||
@ -355,7 +355,7 @@ export const DocumentEditForm = ({
|
||||
key={recipients.length}
|
||||
documentFlow={documentFlow.settings}
|
||||
document={document}
|
||||
currentTeamMemberRole={team?.currentTeamMember?.role}
|
||||
currentTeamMemberRole={team.currentTeamRole}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
isDocumentEnterprise={isDocumentEnterprise}
|
||||
@ -382,7 +382,7 @@ export const DocumentEditForm = ({
|
||||
fields={fields}
|
||||
onSubmit={onAddFieldsFormSubmit}
|
||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||
teamId={team?.id}
|
||||
teamId={team.id}
|
||||
/>
|
||||
|
||||
<AddSubjectFormPartial
|
||||
|
||||
@ -37,7 +37,7 @@ import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialo
|
||||
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
|
||||
import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog';
|
||||
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type DocumentPageViewDropdownProps = {
|
||||
document: Document & {
|
||||
@ -53,7 +53,7 @@ export const DocumentPageViewDropdown = ({ document }: DocumentPageViewDropdownP
|
||||
const { _ } = useLingui();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const team = useOptionalCurrentTeam();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
||||
|
||||
@ -20,7 +20,7 @@ import { cn } from '@documenso/ui/lib/utils';
|
||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type DocumentUploadDropzoneProps = {
|
||||
className?: string;
|
||||
@ -31,7 +31,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp
|
||||
const { toast } = useToast();
|
||||
const { user } = useSession();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const analytics = useAnalytics();
|
||||
|
||||
@ -1,21 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { motion } from 'framer-motion';
|
||||
import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
|
||||
import {
|
||||
Building2Icon,
|
||||
ChevronsUpDown,
|
||||
Plus,
|
||||
Settings2Icon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
} from 'lucide-react';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||
import { TEAM_MEMBER_ROLE_MAP, TEAM_URL_REGEX } from '@documenso/lib/constants/teams';
|
||||
import type { TGetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { EXTENDED_ORGANISATION_MEMBER_ROLE_MAP } from '@documenso/lib/constants/organisations';
|
||||
import { EXTENDED_TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams';
|
||||
import { formatAvatarUrl } from '@documenso/lib/utils/avatars';
|
||||
import { isAdmin } from '@documenso/lib/utils/is-admin';
|
||||
import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
|
||||
import { LanguageSwitcherDialog } from '@documenso/ui/components/common/language-switcher-dialog';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
|
||||
@ -24,71 +31,60 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@documenso/ui/primitives/dropdown-menu';
|
||||
|
||||
const MotionLink = motion(Link);
|
||||
import { useOptionalCurrentOrganisation } from '~/providers/organisation';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type MenuSwitcherProps = {
|
||||
user: SessionUser;
|
||||
teams: TGetTeamsResponse;
|
||||
};
|
||||
|
||||
export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProps) => {
|
||||
export const MenuSwitcher = () => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const { user, organisations } = useSession();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [languageSwitcherOpen, setLanguageSwitcherOpen] = useState(false);
|
||||
const [hoveredOrgId, setHoveredOrgId] = useState<string | null>(null);
|
||||
|
||||
const isUserAdmin = isAdmin(user);
|
||||
|
||||
const { data: teamsQueryResult } = trpc.team.getTeams.useQuery(undefined, {
|
||||
initialData: initialTeamsData,
|
||||
});
|
||||
|
||||
const teams = teamsQueryResult && teamsQueryResult.length > 0 ? teamsQueryResult : null;
|
||||
|
||||
const isPathTeamUrl = (teamUrl: string) => {
|
||||
if (!pathname || !pathname.startsWith(`/t/`)) {
|
||||
const isPathOrgUrl = (orgUrl: string) => {
|
||||
if (!pathname || !pathname.startsWith(`/org/`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pathname.split('/')[2] === teamUrl;
|
||||
return pathname.split('/')[2] === orgUrl;
|
||||
};
|
||||
|
||||
const selectedTeam = teams?.find((team) => isPathTeamUrl(team.url));
|
||||
const selectedOrg = organisations.find((org) => isPathOrgUrl(org.url));
|
||||
const hoveredOrg = organisations.find((org) => org.id === hoveredOrgId);
|
||||
|
||||
const formatAvatarFallback = (teamName?: string) => {
|
||||
if (teamName !== undefined) {
|
||||
return teamName.slice(0, 1).toUpperCase();
|
||||
const currentOrganisation = useOptionalCurrentOrganisation();
|
||||
const currentTeam = useOptionalCurrentTeam();
|
||||
|
||||
// Use hovered org for teams display if available,
|
||||
// otherwise use current team's org if in a team,
|
||||
// finally fallback to selected org
|
||||
const displayedOrg = hoveredOrg || currentOrganisation || selectedOrg;
|
||||
|
||||
const formatAvatarFallback = (name?: string) => {
|
||||
if (name !== undefined) {
|
||||
return name.slice(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
return user.name ? extractInitials(user.name) : user.email.slice(0, 1).toUpperCase();
|
||||
};
|
||||
|
||||
const formatSecondaryAvatarText = (team?: typeof selectedTeam) => {
|
||||
if (!team) {
|
||||
return _(msg`Personal Account`);
|
||||
}
|
||||
|
||||
if (team.ownerUserId === user.id) {
|
||||
return _(msg`Owner`);
|
||||
}
|
||||
|
||||
return _(TEAM_MEMBER_ROLE_MAP[team.currentTeamMember.role]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the redirect URL so we can switch between documents and templates page
|
||||
* seemlessly between teams and personal accounts.
|
||||
* seemlessly between organisations and personal accounts.
|
||||
*/
|
||||
const formatRedirectUrlOnSwitch = (teamUrl?: string) => {
|
||||
const baseUrl = teamUrl ? `/t/${teamUrl}` : '';
|
||||
const formatRedirectUrlOnSwitch = (orgUrl?: string) => {
|
||||
const baseUrl = orgUrl ? `/org/${orgUrl}` : '';
|
||||
|
||||
const currentPathname = (pathname ?? '/').replace(TEAM_URL_REGEX, '');
|
||||
const currentPathname = (pathname ?? '/').replace(/^\/org\/[^/]+/, '');
|
||||
|
||||
if (currentPathname === '/templates') {
|
||||
return `${baseUrl}/templates`;
|
||||
@ -97,8 +93,45 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
||||
return baseUrl;
|
||||
};
|
||||
|
||||
const dropdownMenuAvatarText = useMemo(() => {
|
||||
if (currentTeam) {
|
||||
return {
|
||||
avatarSrc: formatAvatarUrl(currentTeam.avatarImageId),
|
||||
avatarFallback: formatAvatarFallback(currentTeam.name),
|
||||
primaryText: currentTeam.name,
|
||||
secondaryText: _(EXTENDED_TEAM_MEMBER_ROLE_MAP[currentTeam.currentTeamRole]),
|
||||
};
|
||||
}
|
||||
|
||||
if (currentOrganisation) {
|
||||
return {
|
||||
avatarSrc: formatAvatarUrl(currentOrganisation.avatarImageId),
|
||||
avatarFallback: formatAvatarFallback(currentOrganisation.name),
|
||||
primaryText: currentOrganisation.name,
|
||||
secondaryText: _(
|
||||
EXTENDED_ORGANISATION_MEMBER_ROLE_MAP[currentOrganisation.currentOrganisationRole],
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
avatarSrc: formatAvatarUrl(user.avatarImageId),
|
||||
avatarFallback: formatAvatarFallback(user.name ?? user.email),
|
||||
primaryText: user.name,
|
||||
secondaryText: _(msg`Personal Account`),
|
||||
};
|
||||
}, [currentTeam, currentOrganisation, user]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
setHoveredOrgId(currentOrganisation?.id || null);
|
||||
}
|
||||
|
||||
setIsOpen(open);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="menu-switcher"
|
||||
@ -106,12 +139,10 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
||||
className="relative flex h-12 flex-row items-center px-0 py-2 ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-transparent md:px-2"
|
||||
>
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(
|
||||
selectedTeam ? selectedTeam.avatarImageId : user.avatarImageId,
|
||||
)}
|
||||
avatarFallback={formatAvatarFallback(selectedTeam?.name)}
|
||||
primaryText={selectedTeam ? selectedTeam.name : user.name}
|
||||
secondaryText={formatSecondaryAvatarText(selectedTeam)}
|
||||
avatarSrc={dropdownMenuAvatarText.avatarSrc}
|
||||
avatarFallback={dropdownMenuAvatarText.avatarFallback}
|
||||
primaryText={dropdownMenuAvatarText.primaryText}
|
||||
secondaryText={dropdownMenuAvatarText.secondaryText}
|
||||
rightSideComponent={
|
||||
<ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" />
|
||||
}
|
||||
@ -121,166 +152,199 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
className={cn('z-[60] ml-6 w-full md:ml-0', teams ? 'min-w-[20rem]' : 'min-w-[12rem]')}
|
||||
className={cn(
|
||||
'divide-border z-[60] ml-6 flex w-full min-w-[40rem] divide-x p-0 md:ml-0',
|
||||
// organisations ? 'min-w-[24rem]' : 'min-w-[12rem]', // Todo: orgs
|
||||
)}
|
||||
align="end"
|
||||
forceMount
|
||||
>
|
||||
{teams ? (
|
||||
<>
|
||||
<DropdownMenuLabel>
|
||||
<Trans>Personal</Trans>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to={formatRedirectUrlOnSwitch()}>
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(user.avatarImageId)}
|
||||
avatarFallback={formatAvatarFallback()}
|
||||
primaryText={user.name}
|
||||
secondaryText={formatSecondaryAvatarText()}
|
||||
rightSideComponent={
|
||||
!pathname?.startsWith(`/t/`) && (
|
||||
<CheckCircle2 className="ml-auto fill-black text-white dark:fill-white dark:text-black" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator className="mt-2" />
|
||||
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<p>
|
||||
<Trans>Teams</Trans>
|
||||
</p>
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
title={_(msg`Manage teams`)}
|
||||
variant="ghost"
|
||||
className="text-muted-foreground flex h-5 w-5 items-center justify-center p-0"
|
||||
asChild
|
||||
>
|
||||
<Link to="/settings/teams">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
title={_(msg`Create team`)}
|
||||
variant="ghost"
|
||||
className="text-muted-foreground flex h-5 w-5 items-center justify-center p-0"
|
||||
asChild
|
||||
>
|
||||
<Link to="/settings/teams?action=add-team">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<div className="custom-scrollbar max-h-[40vh] overflow-auto">
|
||||
{teams.map((team) => (
|
||||
<DropdownMenuItem asChild key={team.id}>
|
||||
<MotionLink
|
||||
initial="initial"
|
||||
animate="initial"
|
||||
whileHover="animate"
|
||||
to={formatRedirectUrlOnSwitch(team.url)}
|
||||
>
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(team.avatarImageId)}
|
||||
avatarFallback={formatAvatarFallback(team.name)}
|
||||
primaryText={team.name}
|
||||
textSectionClassName="w-[200px]"
|
||||
secondaryText={
|
||||
<div className="relative w-full">
|
||||
<motion.span
|
||||
className="overflow-hidden"
|
||||
variants={{
|
||||
initial: { opacity: 1, translateY: 0 },
|
||||
animate: { opacity: 0, translateY: '100%' },
|
||||
}}
|
||||
>
|
||||
{formatSecondaryAvatarText(team)}
|
||||
</motion.span>
|
||||
|
||||
<motion.span
|
||||
className="absolute inset-0"
|
||||
variants={{
|
||||
initial: { opacity: 0, translateY: '100%' },
|
||||
animate: { opacity: 1, translateY: 0 },
|
||||
}}
|
||||
>{`/t/${team.url}`}</motion.span>
|
||||
</div>
|
||||
}
|
||||
rightSideComponent={
|
||||
isPathTeamUrl(team.url) && (
|
||||
<CheckCircle2 className="ml-auto fill-black text-white dark:fill-white dark:text-black" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MotionLink>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<div className="flex h-[400px] w-full divide-x">
|
||||
{/* Organisations column */}
|
||||
<div className="flex w-1/3 flex-col">
|
||||
<div className="flex h-12 items-center border-b p-2">
|
||||
<h3 className="text-muted-foreground flex items-center px-2 text-sm font-medium">
|
||||
<Building2Icon className="mr-2 h-3.5 w-3.5" />
|
||||
<Trans>Organisations</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link
|
||||
to="/settings/teams?action=add-team"
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<Trans>Create team</Trans>
|
||||
<Plus className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<div className="flex-1 space-y-1 overflow-y-auto p-1.5">
|
||||
{organisations.map((org) => (
|
||||
<div
|
||||
className="group relative"
|
||||
key={org.id}
|
||||
onMouseEnter={() => setHoveredOrgId(org.id)}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
'text-muted-foreground w-full px-4 py-2',
|
||||
org.id === currentOrganisation?.id && !hoveredOrgId && 'bg-accent',
|
||||
org.id === hoveredOrgId && 'bg-accent',
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
<Link to={`/org/${org.url}`} className="flex items-center space-x-2 pr-8">
|
||||
<span
|
||||
className={cn('min-w-0 flex-1 truncate', {
|
||||
'font-semibold': org.id === selectedOrg?.id,
|
||||
})}
|
||||
>
|
||||
{org.name}
|
||||
</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
{canExecuteOrganisationAction(
|
||||
'MANAGE_ORGANISATION',
|
||||
org.currentOrganisationRole,
|
||||
) && (
|
||||
<div className="absolute bottom-0 right-0 top-0 flex items-center justify-center">
|
||||
<Link
|
||||
to={`/org/${org.url}/settings`}
|
||||
className="text-muted-foreground mr-2 rounded-sm border p-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
||||
>
|
||||
<Settings2Icon className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isUserAdmin && (
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/admin">
|
||||
<Trans>Admin panel</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<Button variant="ghost" className="w-full justify-start" asChild>
|
||||
<Link to="/settings/organisations?action=add-organization">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<Trans>Create Organization</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/settings/profile">
|
||||
<Trans>User settings</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{/* Teams column */}
|
||||
<div className="flex w-1/3 flex-col">
|
||||
<div className="flex h-12 items-center border-b p-2">
|
||||
<h3 className="text-muted-foreground flex items-center px-2 text-sm font-medium">
|
||||
<UsersIcon className="mr-2 h-3.5 w-3.5" />
|
||||
<Trans>Teams</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1 overflow-y-auto p-1.5">
|
||||
<AnimateGenericFadeInOut key={displayedOrg ? 'displayed-org' : 'no-org'}>
|
||||
{hoveredOrg ? (
|
||||
hoveredOrg.teams.map((team) => (
|
||||
<div className="group relative" key={team.id}>
|
||||
<DropdownMenuItem
|
||||
className={cn(
|
||||
'text-muted-foreground w-full px-4 py-2',
|
||||
team.id === currentTeam?.id && 'bg-accent',
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
<Link to={`/t/${team.url}`} className="flex items-center space-x-2 pr-8">
|
||||
<span
|
||||
className={cn('min-w-0 flex-1 truncate', {
|
||||
'font-semibold': team.id === currentTeam?.id,
|
||||
})}
|
||||
>
|
||||
{team.name}
|
||||
</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{selectedTeam &&
|
||||
canExecuteTeamAction('MANAGE_TEAM', selectedTeam.currentTeamMember.role) && (
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to={`/t/${selectedTeam.url}/settings`}>
|
||||
<Trans>Team settings</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canExecuteTeamAction('MANAGE_TEAM', team.currentTeamRole) && (
|
||||
<div className="absolute bottom-0 right-0 top-0 flex items-center justify-center">
|
||||
<Link
|
||||
to={`/t/${team.url}/settings`}
|
||||
className="text-muted-foreground mr-2 rounded-sm border p-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100"
|
||||
>
|
||||
<Settings2Icon className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-muted-foreground my-12 flex items-center justify-center px-2 text-center text-sm">
|
||||
<Trans>Select an organisation to view teams</Trans>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-muted-foreground px-4 py-2"
|
||||
onClick={() => setLanguageSwitcherOpen(true)}
|
||||
>
|
||||
<Trans>Language</Trans>
|
||||
</DropdownMenuItem>
|
||||
{displayedOrg && (
|
||||
<Button variant="ghost" className="w-full justify-start" asChild>
|
||||
<Link to={`/org/${displayedOrg.url}/settings/teams?action=add-team`}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<Trans>Create Team</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</AnimateGenericFadeInOut>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-destructive/90 hover:!text-destructive px-4 py-2"
|
||||
onSelect={async () => authClient.signOut()}
|
||||
>
|
||||
<Trans>Sign Out</Trans>
|
||||
</DropdownMenuItem>
|
||||
{/* Settings column */}
|
||||
<div className="flex w-1/3 flex-col">
|
||||
<div className="flex h-12 items-center border-b p-2">
|
||||
<h3 className="text-muted-foreground flex items-center px-2 text-sm font-medium">
|
||||
<SettingsIcon className="mr-2 h-3.5 w-3.5" />
|
||||
<Trans>Settings</Trans>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-1.5">
|
||||
{isUserAdmin && (
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/admin">
|
||||
<Trans>Admin panel</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/dashboard">
|
||||
<Trans>Dashboard</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to="/settings/profile">
|
||||
<Trans>Account</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{currentOrganisation &&
|
||||
canExecuteOrganisationAction(
|
||||
'MANAGE_ORGANISATION',
|
||||
currentOrganisation.currentOrganisationRole,
|
||||
) && (
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to={`/org/${currentOrganisation.url}/settings`}>
|
||||
<Trans>Organisation settings</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{currentTeam && canExecuteTeamAction('MANAGE_TEAM', currentTeam.currentTeamRole) && (
|
||||
<DropdownMenuItem className="text-muted-foreground px-4 py-2" asChild>
|
||||
<Link to={`/t/${currentTeam.url}/settings`}>
|
||||
<Trans>Team settings</Trans>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-muted-foreground px-4 py-2"
|
||||
onClick={() => setLanguageSwitcherOpen(true)}
|
||||
>
|
||||
<Trans>Language</Trans>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="text-muted-foreground hover:!text-muted-foreground px-4 py-2"
|
||||
onSelect={async () => authClient.signOut()}
|
||||
>
|
||||
<Trans>Sign Out</Trans>
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
|
||||
<LanguageSwitcherDialog open={languageSwitcherOpen} setOpen={setLanguageSwitcherOpen} />
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Plural, Trans } from '@lingui/react/macro';
|
||||
import { OrganisationMemberInviteStatus } from '@prisma/client';
|
||||
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';
|
||||
@ -21,8 +21,10 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export const TeamInvitations = () => {
|
||||
const { data, isLoading } = trpc.team.getTeamInvitations.useQuery();
|
||||
export const OrganisationInvitations = () => {
|
||||
const { data, isLoading } = trpc.organisation.member.invite.getMany.useQuery({
|
||||
status: OrganisationMemberInviteStatus.PENDING,
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
@ -37,12 +39,12 @@ export const TeamInvitations = () => {
|
||||
value={data.length}
|
||||
one={
|
||||
<span>
|
||||
You have <strong>1</strong> pending team invitation
|
||||
You have <strong>1</strong> pending invitation
|
||||
</span>
|
||||
}
|
||||
other={
|
||||
<span>
|
||||
You have <strong>#</strong> pending team invitations
|
||||
You have <strong>#</strong> pending invitations
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
@ -66,12 +68,12 @@ export const TeamInvitations = () => {
|
||||
value={data.length}
|
||||
one={
|
||||
<span>
|
||||
You have <strong>1</strong> pending team invitation
|
||||
You have <strong>1</strong> pending invitation
|
||||
</span>
|
||||
}
|
||||
other={
|
||||
<span>
|
||||
You have <strong>#</strong> pending team invitations
|
||||
You have <strong>#</strong> pending invitations
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
@ -80,21 +82,21 @@ export const TeamInvitations = () => {
|
||||
|
||||
<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}>
|
||||
<li key={invitation.id}>
|
||||
<AvatarWithText
|
||||
avatarSrc={formatAvatarUrl(invitation.team.avatarImageId)}
|
||||
avatarSrc={formatAvatarUrl(invitation.organisation.avatarImageId)}
|
||||
className="w-full max-w-none py-4"
|
||||
avatarFallback={invitation.team.name.slice(0, 1)}
|
||||
avatarFallback={invitation.organisation.name.slice(0, 1)}
|
||||
primaryText={
|
||||
<span className="text-foreground/80 font-semibold">
|
||||
{invitation.team.name}
|
||||
{invitation.organisation.name}
|
||||
</span>
|
||||
}
|
||||
secondaryText={formatTeamUrl(invitation.team.url)}
|
||||
// secondaryText={formatOrganisationUrl(invitation.team.url)}
|
||||
rightSideComponent={
|
||||
<div className="ml-auto space-x-2">
|
||||
<DeclineTeamInvitationButton teamId={invitation.team.id} />
|
||||
<AcceptTeamInvitationButton teamId={invitation.team.id} />
|
||||
<DeclineOrganisationInvitationButton token={invitation.token} />
|
||||
<AcceptOrganisationInvitationButton token={invitation.token} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@ -111,26 +113,26 @@ export const TeamInvitations = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const AcceptTeamInvitationButton = ({ teamId }: { teamId: number }) => {
|
||||
const AcceptOrganisationInvitationButton = ({ token }: { token: string }) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const {
|
||||
mutateAsync: acceptTeamInvitation,
|
||||
mutateAsync: acceptOrganisationInvitation,
|
||||
isPending,
|
||||
isSuccess,
|
||||
} = trpc.team.acceptTeamInvitation.useMutation({
|
||||
} = trpc.organisation.member.invite.accept.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Accepted team invitation`),
|
||||
description: _(msg`Invitation accepted`),
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`Unable to join this team at this time.`),
|
||||
description: _(msg`Unable to join this organisation at this time.`),
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
});
|
||||
@ -139,7 +141,7 @@ const AcceptTeamInvitationButton = ({ teamId }: { teamId: number }) => {
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={async () => acceptTeamInvitation({ teamId })}
|
||||
onClick={async () => acceptOrganisationInvitation({ token })}
|
||||
loading={isPending}
|
||||
disabled={isPending || isSuccess}
|
||||
>
|
||||
@ -148,26 +150,26 @@ const AcceptTeamInvitationButton = ({ teamId }: { teamId: number }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const DeclineTeamInvitationButton = ({ teamId }: { teamId: number }) => {
|
||||
const DeclineOrganisationInvitationButton = ({ token }: { token: string }) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const {
|
||||
mutateAsync: declineTeamInvitation,
|
||||
mutateAsync: declineOrganisationInvitation,
|
||||
isPending,
|
||||
isSuccess,
|
||||
} = trpc.team.declineTeamInvitation.useMutation({
|
||||
} = trpc.organisation.member.invite.decline.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Declined team invitation`),
|
||||
description: _(msg`Invitation declined`),
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`Unable to decline this team invitation at this time.`),
|
||||
description: _(msg`Unable to decline this invitation at this time.`),
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
});
|
||||
@ -176,7 +178,7 @@ const DeclineTeamInvitationButton = ({ teamId }: { teamId: number }) => {
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={async () => declineTeamInvitation({ teamId })}
|
||||
onClick={async () => declineOrganisationInvitation({ token })}
|
||||
loading={isPending}
|
||||
disabled={isPending || isSuccess}
|
||||
variant="ghost"
|
||||
@ -1,7 +1,7 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Braces, CreditCard, Globe2Icon, Lock, User, Users, Webhook } from 'lucide-react';
|
||||
import { CreditCard, Lock, User, Users } from 'lucide-react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
@ -31,29 +31,16 @@ export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavPr
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/public-profile">
|
||||
<Link to="/settings/organisations">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/public-profile') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Globe2Icon className="mr-2 h-5 w-5" />
|
||||
<Trans>Public Profile</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/teams">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/teams') && 'bg-secondary',
|
||||
pathname?.startsWith('/settings/organisations') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Users className="mr-2 h-5 w-5" />
|
||||
<Trans>Teams</Trans>
|
||||
<Trans>Organisations</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@ -70,32 +57,6 @@ export const SettingsDesktopNav = ({ className, ...props }: SettingsDesktopNavPr
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/tokens">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/tokens') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Braces className="mr-2 h-5 w-5" />
|
||||
<Trans>API Tokens</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/webhooks">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/webhooks') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Webhook className="mr-2 h-5 w-5" />
|
||||
<Trans>Webhooks</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isBillingEnabled && (
|
||||
<Link to="/settings/billing">
|
||||
<Button
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Braces, CreditCard, Globe2Icon, Lock, User, Users, Webhook } from 'lucide-react';
|
||||
import { CreditCard, Lock, User, Users } from 'lucide-react';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
@ -33,29 +33,16 @@ export const SettingsMobileNav = ({ className, ...props }: SettingsMobileNavProp
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/public-profile">
|
||||
<Link to="/settings/organisations">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/public-profile') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Globe2Icon className="mr-2 h-5 w-5" />
|
||||
<Trans>Public Profile</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/teams">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/teams') && 'bg-secondary',
|
||||
pathname?.startsWith('/settings/organisations') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Users className="mr-2 h-5 w-5" />
|
||||
<Trans>Teams</Trans>
|
||||
<Trans>Organisations</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
@ -72,32 +59,6 @@ export const SettingsMobileNav = ({ className, ...props }: SettingsMobileNavProp
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/tokens">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/tokens') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Braces className="mr-2 h-5 w-5" />
|
||||
<Trans>API Tokens</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link to="/settings/webhooks">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'w-full justify-start',
|
||||
pathname?.startsWith('/settings/webhooks') && 'bg-secondary',
|
||||
)}
|
||||
>
|
||||
<Webhook className="mr-2 h-5 w-5" />
|
||||
<Trans>Webhooks</Trans>
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{isBillingEnabled && (
|
||||
<Link to="/settings/billing">
|
||||
<Button
|
||||
|
||||
@ -7,7 +7,7 @@ import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||
export default function DocumentEditSkeleton() {
|
||||
return (
|
||||
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
||||
<Link to="/documents" className="flex grow-0 items-center text-[#7AC455] hover:opacity-80">
|
||||
<Link to="/" className="flex grow-0 items-center text-[#7AC455] hover:opacity-80">
|
||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||
<Trans>Documents</Trans>
|
||||
</Link>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -26,7 +26,7 @@ import { AddTemplateSettingsFormPartial } from '@documenso/ui/primitives/templat
|
||||
import type { TAddTemplateSettingsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-settings.types';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export type TemplateEditFormProps = {
|
||||
className?: string;
|
||||
@ -48,7 +48,7 @@ export const TemplateEditForm = ({
|
||||
const { toast } = useToast();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const team = useOptionalCurrentTeam();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const [step, setStep] = useState<EditTemplateStep>('settings');
|
||||
|
||||
@ -256,7 +256,7 @@ export const TemplateEditForm = ({
|
||||
<AddTemplateSettingsFormPartial
|
||||
key={recipients.length}
|
||||
template={template}
|
||||
currentTeamMemberRole={team?.currentTeamMember?.role}
|
||||
currentTeamMemberRole={team.currentTeamRole}
|
||||
documentFlow={documentFlow.settings}
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
|
||||
@ -28,7 +28,7 @@ import { StackAvatarsWithTooltip } from '~/components/general/stack-avatars-with
|
||||
import { DocumentsTableActionButton } from '~/components/tables/documents-table-action-button';
|
||||
import { DocumentsTableActionDropdown } from '~/components/tables/documents-table-action-dropdown';
|
||||
import { DataTableTitle } from '~/components/tables/documents-table-title';
|
||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
import { PeriodSelector } from '../period-selector';
|
||||
|
||||
@ -61,7 +61,7 @@ export const TemplatePageViewDocumentsTable = ({
|
||||
const [searchParams] = useSearchParams();
|
||||
const updateSearchParams = useUpdateSearchParams();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const parsedSearchParams = ZDocumentSearchParamsSchema.parse(
|
||||
Object.fromEntries(searchParams ?? []),
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { User } from '@prisma/client';
|
||||
import { File, User2 } from 'lucide-react';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
@ -9,7 +8,10 @@ import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
export type UserProfileSkeletonProps = {
|
||||
className?: string;
|
||||
user: Pick<User, 'name' | 'url'>;
|
||||
user: {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user