fix: folder view all page nested navigation and search filtering (#2450)

Add parentId query param support to documents/templates folder index
pages so View All correctly shows subfolders. Fix search not filtering
unpinned folders on documents page and broken mt- Tailwind class on
templates page.
This commit is contained in:
Catalin Pit 2026-03-17 12:02:32 +02:00 committed by GitHub
parent 647dc5fc2d
commit 455fef70bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 260 additions and 220 deletions

View file

@ -115,7 +115,7 @@ export function AssistantConfirmationDialog({
<div className="mt-4 flex flex-col gap-4">
{!isEditingNextSigner && (
<div>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
<Trans>
The next recipient to sign this document will be{' '}
<span className="font-semibold">{form.watch('name')}</span> (

View file

@ -40,13 +40,21 @@ type TCreateFolderFormSchema = z.infer<typeof ZCreateFolderFormSchema>;
export type FolderCreateDialogProps = {
type: FolderType;
trigger?: React.ReactNode;
parentFolderId?: string | null;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
export const FolderCreateDialog = ({ type, trigger, ...props }: FolderCreateDialogProps) => {
export const FolderCreateDialog = ({
type,
trigger,
parentFolderId,
...props
}: FolderCreateDialogProps) => {
const { t } = useLingui();
const { toast } = useToast();
const { folderId } = useParams();
const parentId = parentFolderId ?? folderId;
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
const { mutateAsync: createFolder } = trpc.folder.createFolder.useMutation();
@ -62,7 +70,7 @@ export const FolderCreateDialog = ({ type, trigger, ...props }: FolderCreateDial
try {
await createFolder({
name: data.name,
parentId: folderId,
parentId,
type,
});

View file

@ -60,11 +60,11 @@ export const ConfigureDocumentAdvancedSettings = ({
return (
<div>
<h3 className="text-foreground mb-1 text-lg font-medium">
<h3 className="mb-1 text-lg font-medium text-foreground">
<Trans>Advanced Settings</Trans>
</h3>
<p className="text-muted-foreground mb-6 text-sm">
<p className="mb-6 text-sm text-muted-foreground">
<Trans>Configure additional options and preferences</Trans>
</p>
@ -100,7 +100,7 @@ export const ConfigureDocumentAdvancedSettings = ({
}))}
selectedValues={field.value}
onChange={field.onChange}
className="bg-background w-full"
className="w-full bg-background"
emptySelectionPlaceholder={t`Select signature types`}
/>
</FormControl>
@ -204,7 +204,7 @@ export const ConfigureDocumentAdvancedSettings = ({
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
Add a URL to redirect the user to once the document is signed
</Trans>
@ -279,7 +279,7 @@ export const ConfigureDocumentAdvancedSettings = ({
<FormControl>
<Input
id="subject"
className="bg-background mt-2"
className="mt-2 bg-background"
disabled={isSubmitting || !isEmailDistribution}
{...field}
/>
@ -302,7 +302,7 @@ export const ConfigureDocumentAdvancedSettings = ({
<FormControl>
<Textarea
id="message"
className="bg-background mt-2 h-32 resize-none"
className="mt-2 h-32 resize-none bg-background"
disabled={isSubmitting || !isEmailDistribution}
{...field}
/>

View file

@ -3,7 +3,7 @@ import { Loader } from 'lucide-react';
export const EmbedClientLoading = () => {
return (
<div className="bg-background fixed left-0 top-0 z-[9999] flex h-full w-full items-center justify-center">
<div className="fixed left-0 top-0 z-[9999] flex h-full w-full items-center justify-center bg-background">
<Loader className="mr-2 h-4 w-4 animate-spin" />
<span>

View file

@ -97,13 +97,13 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
{menuNavigationLinks.map(({ href, text }) => (
<Link
key={href}
className="text-foreground hover:text-foreground/80 flex items-center gap-2 text-2xl font-semibold"
className="flex items-center gap-2 text-2xl font-semibold text-foreground hover:text-foreground/80"
to={href}
onClick={() => handleMenuItemClick()}
>
{text}
{href === '/inbox' && unreadCountData && unreadCountData.count > 0 && (
<span className="bg-primary text-primary-foreground flex h-6 min-w-[1.5rem] items-center justify-center rounded-full px-1.5 text-xs font-semibold">
<span className="flex h-6 min-w-[1.5rem] items-center justify-center rounded-full bg-primary px-1.5 text-xs font-semibold text-primary-foreground">
{unreadCountData.count > 99 ? '99+' : unreadCountData.count}
</span>
)}
@ -111,7 +111,7 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
))}
<button
className="text-foreground hover:text-foreground/80 text-2xl font-semibold"
className="text-2xl font-semibold text-foreground hover:text-foreground/80"
onClick={async () => authClient.signOut()}
>
<Trans>Sign Out</Trans>
@ -123,7 +123,7 @@ export const AppNavMobile = ({ isMenuOpen, onMenuOpenChange }: AppNavMobileProps
<ThemeSwitcher />
</div>
<p className="text-muted-foreground text-sm">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Documenso, Inc.
<br />
<Trans>All rights reserved.</Trans>

View file

@ -167,7 +167,7 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
</DialogTitle>
</DialogHeader>
<div className="text-muted-foreground max-w-[50ch]">
<div className="max-w-[50ch] text-muted-foreground">
<p>
<Trans>
When you sign a document, we can automatically fill in and sign the following fields

View file

@ -131,16 +131,16 @@ export const DocumentSigningFieldContainer = ({
return (
<FieldRootContainer
color={getRecipientColorStyles(field.fieldMeta?.readOnly ? 'readOnly' : 0)}
field={field}
>
{!field.inserted && !loading && !readOnlyField && (
<button
type="submit"
className="absolute inset-0 z-10 h-full w-full rounded-[2px]"
onClick={async () => handleInsertField()}
/>
)}
color={getRecipientColorStyles(field.fieldMeta?.readOnly ? 'readOnly' : 0)}
field={field}
>
{!field.inserted && !loading && !readOnlyField && (
<button
type="submit"
className="absolute inset-0 z-10 h-full w-full rounded-[2px]"
onClick={async () => handleInsertField()}
/>
)}
{type === 'Checkbox' && field.inserted && !loading && !readOnlyField && (
<button

View file

@ -53,6 +53,10 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
const rootPath =
type === FolderType.DOCUMENT ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
if (parentId) {
return `${rootPath}/folders?parentId=${parentId}`;
}
return `${rootPath}/folders`;
};
@ -189,13 +193,13 @@ export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
</div>
)}
{foldersData.folders.length > 12 && (
{unpinnedFolders.length > 12 && (
<div className="mt-2 flex items-center justify-center">
<Link
className="text-sm font-medium text-muted-foreground hover:text-foreground"
to={formatViewAllFoldersPath()}
>
View all folders
<Trans>View all folders</Trans>
</Link>
</div>
)}

View file

@ -21,17 +21,17 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
return (
<div
className={cn(
'dark:bg-background flex flex-col items-center rounded-xl bg-neutral-100 p-4',
'flex flex-col items-center rounded-xl bg-neutral-100 p-4 dark:bg-background',
className,
)}
>
<div className="border-border bg-background text-muted-foreground inline-block max-w-full truncate rounded-md border px-2.5 py-1.5 text-sm lowercase">
<div className="inline-block max-w-full truncate rounded-md border border-border bg-background px-2.5 py-1.5 text-sm lowercase text-muted-foreground">
{baseUrl.host}/u/{user.url}
</div>
<div className="mt-4">
<div className="bg-primary/10 rounded-full p-1.5">
<div className="bg-background flex h-20 w-20 items-center justify-center rounded-full border-2">
<div className="rounded-full bg-primary/10 p-1.5">
<div className="flex h-20 w-20 items-center justify-center rounded-full border-2 bg-background">
<User2 className="h-12 w-12 text-[hsl(228,10%,90%)]" />
</div>
</div>
@ -41,16 +41,16 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
<div className="flex items-center justify-center gap-x-2">
<h2 className="max-w-[12rem] truncate text-2xl font-semibold">{user.name}</h2>
<VerifiedIcon className="text-primary h-8 w-8" />
<VerifiedIcon className="h-8 w-8 text-primary" />
</div>
<div className="dark:bg-foreground/30 mx-auto mt-4 h-2 w-52 rounded-full bg-neutral-300" />
<div className="dark:bg-foreground/20 mx-auto mt-2 h-2 w-36 rounded-full bg-neutral-200" />
<div className="mx-auto mt-4 h-2 w-52 rounded-full bg-neutral-300 dark:bg-foreground/30" />
<div className="mx-auto mt-2 h-2 w-36 rounded-full bg-neutral-200 dark:bg-foreground/20" />
</div>
<div className="mt-8 w-full">
<div className="dark:divide-foreground/30 dark:border-foreground/30 divide-y-2 divide-neutral-200 overflow-hidden rounded-lg border-2 border-neutral-200">
<div className="text-muted-foreground dark:bg-foreground/20 bg-neutral-50 p-4 font-medium">
<div className="divide-y-2 divide-neutral-200 overflow-hidden rounded-lg border-2 border-neutral-200 dark:divide-foreground/30 dark:border-foreground/30">
<div className="bg-neutral-50 p-4 font-medium text-muted-foreground dark:bg-foreground/20">
<Trans>Documents</Trans>
</div>
@ -59,14 +59,14 @@ export const UserProfileSkeleton = ({ className, user, rows = 2 }: UserProfileSk
.map((_, index) => (
<div
key={index}
className="bg-background flex items-center justify-between gap-x-6 p-4"
className="flex items-center justify-between gap-x-6 bg-background p-4"
>
<div className="flex items-center gap-x-2">
<File className="text-muted-foreground/80 h-8 w-8" strokeWidth={1.5} />
<File className="h-8 w-8 text-muted-foreground/80" strokeWidth={1.5} />
<div className="space-y-2">
<div className="dark:bg-foreground/30 h-1.5 w-24 rounded-full bg-neutral-300 md:w-36" />
<div className="dark:bg-foreground/20 h-1.5 w-16 rounded-full bg-neutral-200 md:w-24" />
<div className="h-1.5 w-24 rounded-full bg-neutral-300 md:w-36 dark:bg-foreground/30" />
<div className="h-1.5 w-16 rounded-full bg-neutral-200 md:w-24 dark:bg-foreground/20" />
</div>
</div>

View file

@ -74,7 +74,7 @@ export const SettingsPublicProfileTemplatesTable = () => {
return (
<div>
<div className="dark:divide-foreground/30 dark:border-foreground/30 mt-6 divide-y divide-neutral-200 overflow-hidden rounded-lg border border-neutral-200">
<div className="mt-6 divide-y divide-neutral-200 overflow-hidden rounded-lg border border-neutral-200 dark:divide-foreground/30 dark:border-foreground/30">
{/* Loading and error handling states. */}
{publicDirectTemplates.length === 0 && (
<>
@ -84,10 +84,10 @@ export const SettingsPublicProfileTemplatesTable = () => {
.map((_, index) => (
<div
key={index}
className="bg-background flex items-center justify-between gap-x-6 p-4"
className="flex items-center justify-between gap-x-6 bg-background p-4"
>
<div className="flex gap-x-2">
<FileIcon className="text-muted-foreground/40 h-8 w-8" strokeWidth={1.5} />
<FileIcon className="h-8 w-8 text-muted-foreground/40" strokeWidth={1.5} />
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
@ -95,12 +95,12 @@ export const SettingsPublicProfileTemplatesTable = () => {
</div>
</div>
<MoreHorizontalIcon className="text-muted-foreground h-5 w-5" />
<MoreHorizontalIcon className="h-5 w-5 text-muted-foreground" />
</div>
))}
{isLoadingError && (
<div className="text-muted-foreground flex h-32 flex-col items-center justify-center text-sm">
<div className="flex h-32 flex-col items-center justify-center text-sm text-muted-foreground">
<Trans>Unable to load your public profile templates at this time</Trans>
<button
onClick={(e) => {
@ -114,12 +114,12 @@ export const SettingsPublicProfileTemplatesTable = () => {
)}
{!isLoading && (
<div className="text-muted-foreground flex h-32 flex-col items-center justify-center text-sm">
<div className="flex h-32 flex-col items-center justify-center text-sm text-muted-foreground">
<Trans>No public profile templates found</Trans>
<ManagePublicTemplateDialog
directTemplates={privateDirectTemplates}
trigger={
<button className="hover:text-muted-foreground/80 mt-1 text-xs">
<button className="mt-1 text-xs hover:text-muted-foreground/80">
<Trans>Click here to get started</Trans>
</button>
}
@ -133,23 +133,23 @@ export const SettingsPublicProfileTemplatesTable = () => {
{publicDirectTemplates.map((template) => (
<div
key={template.id}
className="bg-background flex items-center justify-between gap-x-6 p-4"
className="flex items-center justify-between gap-x-6 bg-background p-4"
>
<div className="flex gap-x-2">
<FileIcon
className="text-muted-foreground/40 h-8 w-8 flex-shrink-0"
className="h-8 w-8 flex-shrink-0 text-muted-foreground/40"
strokeWidth={1.5}
/>
<div>
<p className="text-sm break-all">{template.publicTitle}</p>
<p className="text-xs text-neutral-400 break-all">{template.publicDescription}</p>
<p className="break-all text-sm">{template.publicTitle}</p>
<p className="break-all text-xs text-neutral-400">{template.publicDescription}</p>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger>
<MoreHorizontalIcon className="text-muted-foreground h-5 w-5" />
<MoreHorizontalIcon className="h-5 w-5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-52" align="center" side="left">

View file

@ -1,14 +1,13 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { HomeIcon, Loader2, SearchIcon } from 'lucide-react';
import { useNavigate } from 'react-router';
import { FolderIcon, HomeIcon, Loader2, SearchIcon } from 'lucide-react';
import { Link, useSearchParams } from 'react-router';
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input';
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
@ -26,8 +25,10 @@ export function meta() {
export default function DocumentsFoldersPage() {
const { t } = useLingui();
const navigate = useNavigate();
const team = useCurrentTeam();
const [searchParams] = useSearchParams();
const parentId = searchParams.get('parentId');
const [isMovingFolder, setIsMovingFolder] = useState(false);
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
@ -39,46 +40,59 @@ export default function DocumentsFoldersPage() {
const { data: foldersData, isLoading: isFoldersLoading } = trpc.folder.getFolders.useQuery({
type: FolderType.DOCUMENT,
parentId: null,
parentId: parentId,
});
const navigateToFolder = (folderId?: string | null) => {
const documentsPath = formatDocumentsPath(team.url);
if (folderId) {
void navigate(`${documentsPath}/f/${folderId}`);
} else {
void navigate(documentsPath);
}
};
const normalizedSearchTerm = searchTerm.trim().toLowerCase();
const isFolderMatchingSearch = (folder: TFolderWithSubfolders) =>
folder.name.toLowerCase().includes(searchTerm.toLowerCase());
folder.name.toLowerCase().includes(normalizedSearchTerm);
const filteredFolders = foldersData?.folders.filter(isFolderMatchingSearch) ?? [];
const pinnedFolders = filteredFolders.filter((folder) => folder.pinned);
const unpinnedFolders = filteredFolders.filter((folder) => !folder.pinned);
const hasFolders = (foldersData?.folders.length ?? 0) > 0;
const hasSearchResults = filteredFolders.length > 0;
const formatBreadcrumbPath = (folderId: string) => {
const documentsPath = formatDocumentsPath(team.url);
return `${documentsPath}/f/${folderId}`;
};
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<div className="flex w-full items-center justify-between">
<div className="flex flex-1 items-center">
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-0 hover:bg-transparent"
onClick={() => navigateToFolder(null)}
<div className="flex flex-1 items-center text-sm font-medium text-muted-foreground">
<Link
to={formatDocumentsPath(team.url)}
className="flex items-center hover:text-muted-foreground/80"
>
<HomeIcon className="h-4 w-4" />
<span>
<Trans>Home</Trans>
</span>
</Button>
<HomeIcon className="mr-2 h-4 w-4" />
<Trans>Home</Trans>
</Link>
{foldersData?.breadcrumbs.map((folder) => (
<div key={folder.id} className="flex items-center">
<span className="px-3">/</span>
<Link
to={formatBreadcrumbPath(folder.id)}
className="flex items-center hover:text-muted-foreground/80"
>
<FolderIcon className="mr-2 h-4 w-4" />
<span>{folder.name}</span>
</Link>
</div>
))}
</div>
<div className="flex flex-col gap-y-4 sm:flex-row sm:justify-end sm:gap-x-4">
<FolderCreateDialog type={FolderType.DOCUMENT} />
<FolderCreateDialog type={FolderType.DOCUMENT} parentFolderId={parentId} />
</div>
</div>
<div className="relative w-full max-w-md py-6">
<SearchIcon className="text-muted-foreground absolute left-2 top-9 h-4 w-4" />
<SearchIcon className="absolute left-2 top-9 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t`Search folders...`}
value={searchTerm}
@ -93,50 +107,14 @@ export default function DocumentsFoldersPage() {
{isFoldersLoading ? (
<div className="mt-6 flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<>
{foldersData?.folders?.some(
(folder) => folder.pinned && isFolderMatchingSearch(folder),
) && (
{pinnedFolders.length > 0 && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned && isFolderMatchingSearch(folder))
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
)}
<div>
{searchTerm && foldersData?.folders.filter(isFolderMatchingSearch).length === 0 && (
<div className="text-muted-foreground mt-6 text-center">
<Trans>No folders found matching "{searchTerm}"</Trans>
</div>
)}
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => !folder.pinned)
.map((folder) => (
{pinnedFolders.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
@ -154,6 +132,42 @@ export default function DocumentsFoldersPage() {
}}
/>
))}
</div>
</div>
)}
<div>
{searchTerm && !hasSearchResults && (
<div className="mt-6 text-center text-muted-foreground">
<Trans>No folders found matching "{searchTerm}"</Trans>
</div>
)}
{!searchTerm && !hasFolders && (
<div className="mt-6 text-center text-muted-foreground">
<Trans>No folders yet.</Trans>
</div>
)}
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{unpinnedFolders.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
</>

View file

@ -1,14 +1,13 @@
import { useState } from 'react';
import { Trans, useLingui } from '@lingui/react/macro';
import { HomeIcon, Loader2, SearchIcon } from 'lucide-react';
import { useNavigate } from 'react-router';
import { FolderIcon, HomeIcon, Loader2, SearchIcon } from 'lucide-react';
import { Link, useSearchParams } from 'react-router';
import { FolderType } from '@documenso/lib/types/folder-type';
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import { Input } from '@documenso/ui/primitives/input';
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
@ -26,8 +25,10 @@ export function meta() {
export default function TemplatesFoldersPage() {
const { t } = useLingui();
const navigate = useNavigate();
const team = useCurrentTeam();
const [searchParams] = useSearchParams();
const parentId = searchParams.get('parentId');
const [isMovingFolder, setIsMovingFolder] = useState(false);
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
@ -39,46 +40,59 @@ export default function TemplatesFoldersPage() {
const { data: foldersData, isLoading: isFoldersLoading } = trpc.folder.getFolders.useQuery({
type: FolderType.TEMPLATE,
parentId: null,
parentId: parentId,
});
const navigateToFolder = (folderId?: string | null) => {
const templatesPath = formatTemplatesPath(team.url);
if (folderId) {
void navigate(`${templatesPath}/f/${folderId}`);
} else {
void navigate(templatesPath);
}
};
const normalizedSearchTerm = searchTerm.trim().toLowerCase();
const isFolderMatchingSearch = (folder: TFolderWithSubfolders) =>
folder.name.toLowerCase().includes(searchTerm.toLowerCase());
folder.name.toLowerCase().includes(normalizedSearchTerm);
const filteredFolders = foldersData?.folders.filter(isFolderMatchingSearch) ?? [];
const pinnedFolders = filteredFolders.filter((folder) => folder.pinned);
const unpinnedFolders = filteredFolders.filter((folder) => !folder.pinned);
const hasFolders = (foldersData?.folders.length ?? 0) > 0;
const hasSearchResults = filteredFolders.length > 0;
const formatBreadcrumbPath = (folderId: string) => {
const templatesPath = formatTemplatesPath(team.url);
return `${templatesPath}/f/${folderId}`;
};
return (
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
<div className="flex w-full items-center justify-between">
<div className="flex flex-1 items-center">
<Button
variant="ghost"
size="sm"
className="flex items-center space-x-2 pl-0 hover:bg-transparent"
onClick={() => navigateToFolder(null)}
<div className="flex flex-1 items-center text-sm font-medium text-muted-foreground">
<Link
to={formatTemplatesPath(team.url)}
className="flex items-center hover:text-muted-foreground/80"
>
<HomeIcon className="h-4 w-4" />
<span>
<Trans>Home</Trans>
</span>
</Button>
<HomeIcon className="mr-2 h-4 w-4" />
<Trans>Home</Trans>
</Link>
{foldersData?.breadcrumbs.map((folder) => (
<div key={folder.id} className="flex items-center">
<span className="px-3">/</span>
<Link
to={formatBreadcrumbPath(folder.id)}
className="flex items-center hover:text-muted-foreground/80"
>
<FolderIcon className="mr-2 h-4 w-4" />
<span>{folder.name}</span>
</Link>
</div>
))}
</div>
<div className="flex flex-col gap-y-4 sm:flex-row sm:justify-end sm:gap-x-4">
<FolderCreateDialog type={FolderType.TEMPLATE} />
<FolderCreateDialog type={FolderType.TEMPLATE} parentFolderId={parentId} />
</div>
</div>
<div className="relative w-full max-w-md py-6">
<SearchIcon className="text-muted-foreground absolute left-2 top-9 h-4 w-4" />
<SearchIcon className="absolute left-2 top-9 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t`Search folders...`}
value={searchTerm}
@ -92,51 +106,15 @@ export default function TemplatesFoldersPage() {
</h1>
{isFoldersLoading ? (
<div className="mt- flex justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
<div className="mt-6 flex justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : (
<>
{foldersData?.folders?.some(
(folder) => folder.pinned && isFolderMatchingSearch(folder),
) && (
{pinnedFolders.length > 0 && (
<div className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData.folders
.filter((folder) => folder.pinned && isFolderMatchingSearch(folder))
.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
)}
<div>
{searchTerm && foldersData?.folders.filter(isFolderMatchingSearch).length === 0 && (
<div className="text-muted-foreground mt-6 text-center">
<Trans>No folders found matching "{searchTerm}"</Trans>
</div>
)}
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{foldersData?.folders
.filter((folder) => !folder.pinned && isFolderMatchingSearch(folder))
.map((folder) => (
{pinnedFolders.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
@ -154,6 +132,42 @@ export default function TemplatesFoldersPage() {
}}
/>
))}
</div>
</div>
)}
<div>
{searchTerm && !hasSearchResults && (
<div className="mt-6 text-center text-muted-foreground">
<Trans>No folders found matching "{searchTerm}"</Trans>
</div>
)}
{!searchTerm && !hasFolders && (
<div className="mt-6 text-center text-muted-foreground">
<Trans>No folders yet.</Trans>
</div>
)}
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{unpinnedFolders.map((folder) => (
<FolderCard
key={folder.id}
folder={folder}
onMove={(folder) => {
setFolderToMove(folder);
setIsMovingFolder(true);
}}
onSettings={(folder) => {
setFolderToSettings(folder);
setIsSettingsFolderOpen(true);
}}
onDelete={(folder) => {
setFolderToDelete(folder);
setIsDeletingFolder(true);
}}
/>
))}
</div>
</div>
</>

View file

@ -64,7 +64,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
return (
<div className="flex flex-col items-center justify-center py-4 sm:py-32">
<div className="flex flex-col items-center">
<Avatar className="dark:border-border h-24 w-24 border-2 border-solid">
<Avatar className="h-24 w-24 border-2 border-solid dark:border-border">
{publicProfile.avatarImageId && (
<AvatarImage src={formatAvatarUrl(publicProfile.avatarImageId)} />
)}
@ -99,10 +99,10 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
/>
<div className="ml-2">
<p className="text-foreground text-base font-semibold">
<p className="text-base font-semibold text-foreground">
{BADGE_DATA[publicProfile.badge.type].name}
</p>
<p className="text-muted-foreground mt-0.5 text-sm">
<p className="mt-0.5 text-sm text-muted-foreground">
<Trans>
Since {DateTime.fromJSDate(publicProfile.badge.since).toFormat('LLL yy')}
</Trans>
@ -113,7 +113,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
)}
</div>
<div className="text-muted-foreground mt-4 space-y-1">
<div className="mt-4 space-y-1 text-muted-foreground">
{(profile.bio ?? '').split('\n').map((line, index) => (
<p
key={index}
@ -127,7 +127,7 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
{templates.length === 0 && (
<div className="mt-4 w-full max-w-xl border-t pt-4">
<p className="text-muted-foreground max-w-[60ch] whitespace-pre-wrap break-words text-center text-sm leading-relaxed">
<p className="max-w-[60ch] whitespace-pre-wrap break-words text-center text-sm leading-relaxed text-muted-foreground">
<Trans>
It looks like {publicProfile.name} hasn't added any documents to their profile yet.
</Trans>{' '}
@ -167,19 +167,19 @@ export default function PublicProfilePage({ loaderData }: Route.ComponentProps)
<TableBody>
{templates.map((template) => (
<TableRow key={template.id}>
<TableCell className="text-muted-foreground flex flex-col justify-between overflow-hidden text-sm sm:flex-row">
<TableCell className="flex flex-col justify-between overflow-hidden text-sm text-muted-foreground sm:flex-row">
<div className="flex flex-1 items-start justify-start gap-2">
<FileIcon
className="text-muted-foreground/40 h-8 w-8 flex-shrink-0"
className="h-8 w-8 flex-shrink-0 text-muted-foreground/40"
strokeWidth={1.5}
/>
<div className="flex flex-1 flex-col gap-4 overflow-hidden md:flex-row md:items-start md:justify-between">
<div>
<p className="text-foreground text-sm font-semibold leading-none break-all">
<p className="break-all text-sm font-semibold leading-none text-foreground">
{template.publicTitle}
</p>
<p className="text-muted-foreground mt-1 line-clamp-3 max-w-[70ch] whitespace-normal text-xs break-all">
<p className="mt-1 line-clamp-3 max-w-[70ch] whitespace-normal break-all text-xs text-muted-foreground">
{template.publicDescription}
</p>
</div>

View file

@ -7,9 +7,9 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { isEmailDomainAllowedForSignup } from '@documenso/lib/constants/auth';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { onCreateUserHook } from '@documenso/lib/server-only/user/create-user';
import { env } from '@documenso/lib/utils/env';
import { deletedServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/deleted-account';
import { legacyServiceAccountEmail } from '@documenso/lib/server-only/user/service-accounts/legacy-service-account';
import { env } from '@documenso/lib/utils/env';
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
import { prisma } from '@documenso/prisma';

View file

@ -201,7 +201,7 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
res: signupLimited,
});
}
if (!isEmailDomainAllowedForSignup(email)) {
throw new AppError(AuthenticationErrorCode.SignupDisabled, {
statusCode: 400,

View file

@ -93,7 +93,7 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
const duplicatedTemplateType =
envelope.templateType === 'ORGANISATION' && envelope.teamId !== teamId
? 'PRIVATE'
: envelope.templateType ?? undefined;
: (envelope.templateType ?? undefined);
const duplicatedEnvelope = await prisma.envelope.create({
data: {
@ -150,7 +150,7 @@ export const duplicateEnvelope = async ({ id, userId, teamId }: DuplicateEnvelop
await pMap(
envelope.recipients,
(recipient) =>
async (recipient) =>
prisma.recipient.create({
data: {
envelopeId: duplicatedEnvelope.id,

View file

@ -191,7 +191,7 @@ export function EnvelopeRecipientFieldTooltip({
</span>
</p>
<p className="text-muted-foreground mt-1 text-center text-xs">
<p className="mt-1 text-center text-xs text-muted-foreground">
{getRecipientDisplayText(field.recipient)}
</p>

View file

@ -242,7 +242,7 @@ export const AddSettingsFormPartial = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
<Trans>
Controls the language for the document, including the language to be used
for email notifications, and the final certificate that is generated and
@ -361,11 +361,11 @@ export const AddSettingsFormPartial = ({
<Accordion type="multiple" className="mt-6">
<AccordionItem value="advanced-options" className="border-none">
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
<AccordionTrigger className="mb-2 rounded border px-3 py-2 text-left text-foreground hover:bg-neutral-200/30 hover:no-underline">
<Trans>Advanced Options</Trans>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
<AccordionContent className="-mx-1 px-1 pt-2 text-sm leading-relaxed text-muted-foreground">
<div className="flex flex-col space-y-6">
<FormField
control={form.control}
@ -379,7 +379,7 @@ export const AddSettingsFormPartial = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
Add an external ID to the document. This can be used to identify
the document in external systems.
@ -418,7 +418,7 @@ export const AddSettingsFormPartial = ({
field.onChange(value);
void handleAutoSave();
}}
className="bg-background w-full"
className="w-full bg-background"
emptySelectionPlaceholder={t`Select signature types`}
/>
</FormControl>
@ -506,7 +506,7 @@ export const AddSettingsFormPartial = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
Add a URL to redirect the user to once the document is signed
</Trans>

View file

@ -114,7 +114,7 @@ export const DropdownFieldAdvancedSettings = ({
handleFieldChange('defaultValue', val);
}}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
<SelectTrigger className="mt-2 w-full bg-background text-muted-foreground">
<SelectValue defaultValue={defaultValue} placeholder={`-- ${_(msg`Select`)} --`} />
</SelectTrigger>
<SelectContent position="popper">
@ -152,7 +152,7 @@ export const DropdownFieldAdvancedSettings = ({
</div>
</div>
<Button
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 mt-2 border"
className="mt-2 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
variant="outline"
onClick={() => setShowValidation((prev) => !prev)}
>
@ -183,7 +183,7 @@ export const DropdownFieldAdvancedSettings = ({
</div>
))}
<Button
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 ml-9 mt-4 border"
className="ml-9 mt-4 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
variant="outline"
onClick={addValue}
>

View file

@ -122,7 +122,7 @@ export const RadioFieldAdvancedSettings = ({
</Label>
<Input
id="label"
className="bg-background mt-2"
className="mt-2 bg-background"
placeholder={_(msg`Field label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
@ -150,7 +150,7 @@ export const RadioFieldAdvancedSettings = ({
</div>
</div>
<Button
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 mt-2 border"
className="mt-2 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
variant="outline"
onClick={() => setShowValidation((prev) => !prev)}
>
@ -167,7 +167,7 @@ export const RadioFieldAdvancedSettings = ({
{values.map((value) => (
<div key={value.id} className="mt-2 flex items-center gap-4">
<Checkbox
className="data-[state=checked]:bg-documenso border-foreground/30 data-[state=checked]:ring-primary dark:data-[state=checked]:ring-offset-background h-5 w-5 rounded-full data-[state=checked]:ring-1 data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white"
className="h-5 w-5 rounded-full border-foreground/30 data-[state=checked]:bg-documenso data-[state=checked]:ring-1 data-[state=checked]:ring-primary data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white dark:data-[state=checked]:ring-offset-background"
checked={value.checked}
onCheckedChange={(checked) => handleCheckedChange(Boolean(checked), value.id)}
/>
@ -186,7 +186,7 @@ export const RadioFieldAdvancedSettings = ({
</div>
))}
<Button
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 ml-9 mt-4 border"
className="ml-9 mt-4 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
variant="outline"
onClick={addValue}
>

View file

@ -101,7 +101,7 @@ export const RecipientSelector = ({
variant="outline"
role="combobox"
className={cn(
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
'justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
getRecipientColorStyles(recipients.findIndex((r) => r.id === selectedRecipient?.id))
.comboBoxTrigger,
className,
@ -122,21 +122,21 @@ export const RecipientSelector = ({
<CommandInput />
<CommandEmpty>
<span className="text-muted-foreground inline-block px-4">
<span className="inline-block px-4 text-muted-foreground">
<Trans>No recipient matching this description was found.</Trans>
</span>
</CommandEmpty>
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
<div className="mb-1 ml-2 mt-2 text-xs font-medium text-muted-foreground">
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
</div>
{roleRecipients.length === 0 && (
<div
key={`${role}-empty`}
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
className="px-4 pb-4 pt-2.5 text-center text-xs text-muted-foreground/80"
>
<Trans>No recipients with this role</Trans>
</div>
@ -160,7 +160,7 @@ export const RecipientSelector = ({
disabled={recipient.signingStatus !== SigningStatus.NOT_SIGNED}
>
<span
className={cn('text-foreground/70 truncate', {
className={cn('truncate text-foreground/70', {
'text-foreground/80': recipient.id === selectedRecipient?.id,
})}
>
@ -182,7 +182,7 @@ export const RecipientSelector = ({
<Info className="ml-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
This document has already been sent to this recipient. You can no longer
edit this recipient.