mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
This PR is handles the changes required to support envelopes. The new envelope editor/signing page will be hidden during release. The core changes here is to migrate the documents and templates model to a centralized envelopes model. Even though Documents and Templates are removed, from the user perspective they will still exist as we remap envelopes to documents and templates.
253 lines
9.0 KiB
TypeScript
253 lines
9.0 KiB
TypeScript
import { useState } from 'react';
|
|
|
|
import { Trans } from '@lingui/react/macro';
|
|
import { FolderType } from '@prisma/client';
|
|
import { FolderIcon, HomeIcon } from 'lucide-react';
|
|
import { Link } from 'react-router';
|
|
|
|
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
|
import { trpc } from '@documenso/trpc/react';
|
|
import { type TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
|
|
|
import { FolderCreateDialog } from '~/components/dialogs/folder-create-dialog';
|
|
import { FolderDeleteDialog } from '~/components/dialogs/folder-delete-dialog';
|
|
import { FolderMoveDialog } from '~/components/dialogs/folder-move-dialog';
|
|
import { FolderUpdateDialog } from '~/components/dialogs/folder-update-dialog';
|
|
import { TemplateCreateDialog } from '~/components/dialogs/template-create-dialog';
|
|
import { DocumentUploadButton } from '~/components/general/document/document-upload-button';
|
|
import { FolderCard, FolderCardEmpty } from '~/components/general/folder/folder-card';
|
|
import { useCurrentTeam } from '~/providers/team';
|
|
|
|
export type FolderGridProps = {
|
|
type: FolderType;
|
|
parentId: string | null;
|
|
};
|
|
|
|
export const FolderGrid = ({ type, parentId }: FolderGridProps) => {
|
|
const team = useCurrentTeam();
|
|
|
|
const [isMovingFolder, setIsMovingFolder] = useState(false);
|
|
const [folderToMove, setFolderToMove] = useState<TFolderWithSubfolders | null>(null);
|
|
const [isDeletingFolder, setIsDeletingFolder] = useState(false);
|
|
const [folderToDelete, setFolderToDelete] = useState<TFolderWithSubfolders | null>(null);
|
|
const [isSettingsFolderOpen, setIsSettingsFolderOpen] = useState(false);
|
|
const [folderToSettings, setFolderToSettings] = useState<TFolderWithSubfolders | null>(null);
|
|
|
|
const { mutateAsync: pinFolder } = trpc.folder.pinFolder.useMutation();
|
|
const { mutateAsync: unpinFolder } = trpc.folder.unpinFolder.useMutation();
|
|
|
|
const { data: foldersData, isPending } = trpc.folder.getFolders.useQuery({
|
|
type,
|
|
parentId,
|
|
});
|
|
|
|
const formatBreadCrumbPath = (folderId: string) => {
|
|
const rootPath =
|
|
type === FolderType.DOCUMENT ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
|
|
|
|
return `${rootPath}/f/${folderId}`;
|
|
};
|
|
|
|
const formatViewAllFoldersPath = () => {
|
|
const rootPath =
|
|
type === FolderType.DOCUMENT ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url);
|
|
|
|
return `${rootPath}/folders`;
|
|
};
|
|
|
|
const formatRootPath = () => {
|
|
return type === FolderType.DOCUMENT
|
|
? formatDocumentsPath(team.url)
|
|
: formatTemplatesPath(team.url);
|
|
};
|
|
|
|
const pinnedFolders = foldersData?.folders.filter((folder) => folder.pinned) || [];
|
|
const unpinnedFolders = foldersData?.folders.filter((folder) => !folder.pinned) || [];
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-4 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
|
<div
|
|
className="text-muted-foreground hover:text-muted-foreground/80 flex flex-1 items-center text-sm font-medium"
|
|
data-testid="folder-grid-breadcrumbs"
|
|
>
|
|
<Link to={formatRootPath()} className="flex items-center">
|
|
<HomeIcon className="mr-2 h-4 w-4" />
|
|
<Trans>Home</Trans>
|
|
</Link>
|
|
|
|
{isPending && parentId ? (
|
|
<div className="flex items-center">
|
|
<Skeleton className="mx-3 h-4 w-1 rotate-12" />
|
|
|
|
<Skeleton className="h-4 w-20" />
|
|
</div>
|
|
) : (
|
|
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">
|
|
<FolderIcon className="mr-2 h-4 w-4" />
|
|
<span>{folder.name}</span>
|
|
</Link>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex gap-4 sm:flex-row sm:justify-end">
|
|
{/* Todo: Envelopes - Feature flag */}
|
|
{/* <EnvelopeUploadButton type={type} folderId={parentId || undefined} /> */}
|
|
|
|
{type === FolderType.DOCUMENT ? (
|
|
<DocumentUploadButton />
|
|
) : (
|
|
<TemplateCreateDialog folderId={parentId ?? undefined} />
|
|
)}
|
|
|
|
<FolderCreateDialog type={type} />
|
|
</div>
|
|
</div>
|
|
|
|
{isPending ? (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
<div key={index} className="border-border bg-card h-full rounded-lg border px-4 py-5">
|
|
<div className="flex items-center gap-3">
|
|
<Skeleton className="h-8 w-8 rounded" />
|
|
<div className="flex w-full items-center justify-between">
|
|
<div className="flex-1">
|
|
<Skeleton className="mb-2 h-4 w-24" />
|
|
<div className="flex space-x-2">
|
|
<Skeleton className="h-3 w-16" />
|
|
<Skeleton className="h-3 w-3" />
|
|
<Skeleton className="h-3 w-12" />
|
|
</div>
|
|
</div>
|
|
<Skeleton className="h-8 w-2 rounded" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : foldersData && foldersData.folders.length === 0 ? (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
|
<FolderCreateDialog
|
|
type={type}
|
|
trigger={
|
|
<button>
|
|
<FolderCardEmpty type={type} />
|
|
</button>
|
|
}
|
|
/>
|
|
</div>
|
|
) : (
|
|
foldersData && (
|
|
<div key="content" className="space-y-4">
|
|
{pinnedFolders.length > 0 && (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
|
{pinnedFolders.map((folder) => (
|
|
<FolderCard
|
|
key={folder.id}
|
|
folder={folder}
|
|
onMove={(folder) => {
|
|
setFolderToMove(folder);
|
|
setIsMovingFolder(true);
|
|
}}
|
|
onPin={(folderId) => void pinFolder({ folderId })}
|
|
onUnpin={(folderId) => void unpinFolder({ folderId })}
|
|
onSettings={(folder) => {
|
|
setFolderToSettings(folder);
|
|
setIsSettingsFolderOpen(true);
|
|
}}
|
|
onDelete={(folder) => {
|
|
setFolderToDelete(folder);
|
|
setIsDeletingFolder(true);
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{unpinnedFolders.length > 0 && (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
|
{unpinnedFolders.slice(0, 12).map((folder) => (
|
|
<FolderCard
|
|
key={folder.id}
|
|
folder={folder}
|
|
onMove={(folder) => {
|
|
setFolderToMove(folder);
|
|
setIsMovingFolder(true);
|
|
}}
|
|
onPin={(folderId) => void pinFolder({ folderId })}
|
|
onUnpin={(folderId) => void unpinFolder({ folderId })}
|
|
onSettings={(folder) => {
|
|
setFolderToSettings(folder);
|
|
setIsSettingsFolderOpen(true);
|
|
}}
|
|
onDelete={(folder) => {
|
|
setFolderToDelete(folder);
|
|
setIsDeletingFolder(true);
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{foldersData.folders.length > 12 && (
|
|
<div className="mt-2 flex items-center justify-center">
|
|
<Link
|
|
className="text-muted-foreground hover:text-foreground text-sm font-medium"
|
|
to={formatViewAllFoldersPath()}
|
|
>
|
|
View all folders
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
)}
|
|
|
|
<FolderMoveDialog
|
|
foldersData={foldersData?.folders}
|
|
folder={folderToMove}
|
|
isOpen={isMovingFolder}
|
|
onOpenChange={(open) => {
|
|
setIsMovingFolder(open);
|
|
|
|
if (!open) {
|
|
setFolderToMove(null);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<FolderUpdateDialog
|
|
folder={folderToSettings}
|
|
isOpen={isSettingsFolderOpen}
|
|
onOpenChange={(open) => {
|
|
setIsSettingsFolderOpen(open);
|
|
|
|
if (!open) {
|
|
setFolderToSettings(null);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{folderToDelete && (
|
|
<FolderDeleteDialog
|
|
folder={folderToDelete}
|
|
isOpen={isDeletingFolder}
|
|
onOpenChange={(open) => {
|
|
setIsDeletingFolder(open);
|
|
|
|
if (!open) {
|
|
setFolderToDelete(null);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|