From 803edf5b1665ee8f730044749e96e418290e0be6 Mon Sep 17 00:00:00 2001 From: Catalin Pit Date: Thu, 7 Aug 2025 08:37:55 +0300 Subject: [PATCH] feat: implement Drag-n-Drop for templates (#1791) --- .../template/template-drop-zone-wrapper.tsx | 129 ++++++++++++++++++ .../t.$teamUrl+/templates._index.tsx | 82 +++++------ .../e2e/folders/team-account-folders.spec.ts | 3 +- 3 files changed, 174 insertions(+), 40 deletions(-) create mode 100644 apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx diff --git a/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx b/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx new file mode 100644 index 000000000..0d9c3fc9e --- /dev/null +++ b/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx @@ -0,0 +1,129 @@ +import { type ReactNode, useState } from 'react'; + +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react'; +import { Trans } from '@lingui/react/macro'; +import { Loader } from 'lucide-react'; +import { useDropzone } from 'react-dropzone'; +import { useNavigate, useParams } from 'react-router'; + +import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; +import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; +import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; +import { formatTemplatesPath } from '@documenso/lib/utils/teams'; +import { trpc } from '@documenso/trpc/react'; +import { cn } from '@documenso/ui/lib/utils'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { useCurrentTeam } from '~/providers/team'; + +export interface TemplateDropZoneWrapperProps { + children: ReactNode; + className?: string; +} + +export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZoneWrapperProps) => { + const { _ } = useLingui(); + const { toast } = useToast(); + const { folderId } = useParams(); + + const team = useCurrentTeam(); + + const navigate = useNavigate(); + + const [isLoading, setIsLoading] = useState(false); + + const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation(); + + const onFileDrop = async (file: File) => { + try { + setIsLoading(true); + + const documentData = await putPdfFile(file); + + const { id } = await createTemplate({ + title: file.name, + templateDocumentDataId: documentData.id, + folderId: folderId ?? undefined, + }); + + toast({ + title: _(msg`Template uploaded`), + description: _( + msg`Your template has been uploaded successfully. You will be redirected to the template page.`, + ), + duration: 5000, + }); + + await navigate(`${formatTemplatesPath(team.url)}/${id}/edit`); + } catch { + toast({ + title: _(msg`Something went wrong`), + description: _(msg`Please try again later.`), + variant: 'destructive', + }); + } finally { + setIsLoading(false); + } + }; + + const onFileDropRejected = () => { + toast({ + title: _(msg`Your template failed to upload.`), + description: _(msg`File cannot be larger than ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT}MB`), + duration: 5000, + variant: 'destructive', + }); + }; + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + accept: { + 'application/pdf': ['.pdf'], + }, + //disabled: isUploadDisabled, + multiple: false, + maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT), + onDrop: ([acceptedFile]) => { + if (acceptedFile) { + void onFileDrop(acceptedFile); + } + }, + onDropRejected: () => { + void onFileDropRejected(); + }, + noClick: true, + noDragEventsBubbling: true, + }); + + return ( +
+ + {children} + + {isDragActive && ( +
+
+

+ Upload Template +

+ +

+ Drag and drop your PDF file here +

+
+
+ )} + + {isLoading && ( +
+
+ +

+ Uploading template... +

+
+
+ )} +
+ ); +}; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx index d5751d80c..bd9f0de99 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx @@ -12,6 +12,7 @@ import { FolderGrid } from '~/components/general/folder/folder-grid'; import { TemplatesTable } from '~/components/tables/templates-table'; import { useCurrentTeam } from '~/providers/team'; import { appMetaTags } from '~/utils/meta'; +import { TemplateDropZoneWrapper } from '~/components/general/template/template-drop-zone-wrapper'; export function meta() { return appMetaTags('Templates'); @@ -36,51 +37,54 @@ export default function TemplatesPage() { }); return ( -
- - -
-
- - {team.avatarImageId && } - - {team.name.slice(0, 1)} - - - -

- Templates -

-
+ +
+
- {data && data.count === 0 ? ( -
- +
+ + {team.avatarImageId && } + + {team.name.slice(0, 1)} + + -
-

- We're all empty -

+

+ Templates +

+
-

- - You have not yet created any templates. To create a template please upload one. - -

+
+ {data && data.count === 0 ? ( +
+ + +
+

+ We're all empty +

+ +

+ + You have not yet created any templates. To create a template please upload + one. + +

+
-
- ) : ( - - )} + ) : ( + + )} +
-
+
); } diff --git a/packages/app-tests/e2e/folders/team-account-folders.spec.ts b/packages/app-tests/e2e/folders/team-account-folders.spec.ts index 09272ee96..71649be8d 100644 --- a/packages/app-tests/e2e/folders/team-account-folders.spec.ts +++ b/packages/app-tests/e2e/folders/team-account-folders.spec.ts @@ -379,10 +379,11 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page }) .filter({ hasText: /^Upload Template DocumentDrag & drop your PDF here\.$/ }) .nth(2) .click(); - await page.locator('input[type="file"]').waitFor({ state: 'attached' }); + await page.locator('input[type="file"]').nth(0).waitFor({ state: 'attached' }); await page .locator('input[type="file"]') + .nth(0) .setInputFiles(path.join(__dirname, '../../../assets/documenso-supporter-pledge.pdf')); await page.waitForTimeout(3000);