From e97b9b4f1cd9a200e169059593737853fc351957 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 8 Feb 2024 12:33:20 +1100 Subject: [PATCH] feat: add team templates (#912) --- .../documents/[id]/document-page-view.tsx | 4 +- .../app/(dashboard)/documents/[id]/page.tsx | 2 +- .../documents/documents-page-view.tsx | 7 +- .../src/app/(dashboard)/documents/page.tsx | 2 +- .../templates/[id]/edit-template.tsx | 4 +- .../app/(dashboard)/templates/[id]/page.tsx | 81 +------ .../templates/[id]/template-page-view.tsx | 86 ++++++++ .../templates/data-table-action-dropdown.tsx | 25 ++- .../templates/data-table-templates.tsx | 15 +- .../templates/delete-template-dialog.tsx | 35 ++- .../templates/duplicate-template-dialog.tsx | 56 ++--- .../templates/new-template-dialog.tsx | 11 +- .../src/app/(dashboard)/templates/page.tsx | 50 +---- .../templates/templates-page-view.tsx | 73 +++++++ .../t/[teamUrl]/documents/[id]/page.tsx | 4 +- .../(teams)/t/[teamUrl]/documents/page.tsx | 2 +- .../t/[teamUrl]/templates/[id]/page.tsx | 22 ++ .../(teams)/t/[teamUrl]/templates/page.tsx | 26 +++ .../(dashboard)/layout/desktop-nav.tsx | 34 ++- .../(dashboard)/layout/menu-switcher.tsx | 22 +- .../(dashboard)/layout/mobile-navigation.tsx | 2 +- .../e2e/templates/manage-templates.spec.ts | 205 ++++++++++++++++++ packages/lib/constants/teams.ts | 1 + .../field/get-fields-for-template.ts | 15 +- .../field/set-fields-for-template.ts | 15 +- .../recipient/get-recipients-for-template.ts | 15 +- .../recipient/set-recipients-for-template.ts | 15 +- .../template/create-document-from-template.ts | 19 +- .../server-only/template/create-template.ts | 18 +- .../server-only/template/delete-template.ts | 20 +- .../template/duplicate-template.ts | 30 ++- .../server-only/template/find-templates.ts | 56 +++++ .../template/get-template-by-id.ts | 24 +- .../lib/server-only/template/get-templates.ts | 35 --- packages/lib/utils/teams.ts | 4 + .../migration.sql | 5 + packages/prisma/schema.prisma | 5 +- packages/prisma/seed/templates.ts | 36 +++ packages/trpc/server/field-router/router.ts | 2 +- .../trpc/server/recipient-router/router.ts | 4 +- .../trpc/server/template-router/router.ts | 12 +- .../trpc/server/template-router/schema.ts | 4 +- 42 files changed, 831 insertions(+), 272 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/templates-page-view.tsx create mode 100644 apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx create mode 100644 apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx create mode 100644 packages/app-tests/e2e/templates/manage-templates.spec.ts create mode 100644 packages/lib/server-only/template/find-templates.ts delete mode 100644 packages/lib/server-only/template/get-templates.ts create mode 100644 packages/prisma/migrations/20240206051948_add_teams_templates/migration.sql create mode 100644 packages/prisma/seed/templates.ts diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx index 3a46ed5e7..6759d91ac 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view.tsx @@ -25,7 +25,7 @@ export type DocumentPageViewProps = { team?: Team; }; -export default async function DocumentPageView({ params, team }: DocumentPageViewProps) { +export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => { const { id } = params; const documentId = Number(id); @@ -128,4 +128,4 @@ export default async function DocumentPageView({ params, team }: DocumentPageVie )} ); -} +}; diff --git a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx index e7a34889e..5ad224737 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/page.tsx @@ -1,4 +1,4 @@ -import DocumentPageView from './document-page-view'; +import { DocumentPageView } from './document-page-view'; export type DocumentPageProps = { params: { diff --git a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx index ead3e8f4f..9059b8e88 100644 --- a/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx +++ b/apps/web/src/app/(dashboard)/documents/documents-page-view.tsx @@ -33,10 +33,7 @@ export type DocumentsPageViewProps = { team?: Team & { teamEmail?: TeamEmail | null }; }; -export default async function DocumentsPageView({ - searchParams = {}, - team, -}: DocumentsPageViewProps) { +export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => { const { user } = await getRequiredServerComponentSession(); const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL'; @@ -155,4 +152,4 @@ export default async function DocumentsPageView({ ); -} +}; diff --git a/apps/web/src/app/(dashboard)/documents/page.tsx b/apps/web/src/app/(dashboard)/documents/page.tsx index b67ed6f02..67f432a13 100644 --- a/apps/web/src/app/(dashboard)/documents/page.tsx +++ b/apps/web/src/app/(dashboard)/documents/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from 'next'; import type { DocumentsPageViewProps } from './documents-page-view'; -import DocumentsPageView from './documents-page-view'; +import { DocumentsPageView } from './documents-page-view'; export type DocumentsPageProps = { searchParams?: DocumentsPageViewProps['searchParams']; diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx index bdc769e79..f8c7f9a43 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx @@ -28,6 +28,7 @@ export type EditTemplateFormProps = { recipients: Recipient[]; fields: Field[]; documentData: DocumentData; + templateRootPath: string; }; type EditTemplateStep = 'signers' | 'fields'; @@ -40,6 +41,7 @@ export const EditTemplateForm = ({ fields, user: _user, documentData, + templateRootPath, }: EditTemplateFormProps) => { const { toast } = useToast(); const router = useRouter(); @@ -98,7 +100,7 @@ export const EditTemplateForm = ({ duration: 5000, }); - router.push('/templates'); + router.push(templateRootPath); } catch (err) { toast({ title: 'Error', diff --git a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx index 6d234eff2..aa55d1943 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx @@ -1,81 +1,10 @@ import React from 'react'; -import Link from 'next/link'; -import { redirect } from 'next/navigation'; +import type { TemplatePageViewProps } from './template-page-view'; +import { TemplatePageView } from './template-page-view'; -import { ChevronLeft } from 'lucide-react'; +type TemplatePageProps = Pick; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; -import { getFieldsForTemplate } from '@documenso/lib/server-only/field/get-fields-for-template'; -import { getRecipientsForTemplate } from '@documenso/lib/server-only/recipient/get-recipients-for-template'; -import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; - -import { TemplateType } from '~/components/formatter/template-type'; - -import { EditTemplateForm } from './edit-template'; - -export type TemplatePageProps = { - params: { - id: string; - }; -}; - -export default async function TemplatePage({ params }: TemplatePageProps) { - const { id } = params; - - const templateId = Number(id); - - if (!templateId || Number.isNaN(templateId)) { - redirect('/documents'); - } - - const { user } = await getRequiredServerComponentSession(); - - const template = await getTemplateById({ - id: templateId, - userId: user.id, - }).catch(() => null); - - if (!template || !template.templateDocumentData) { - redirect('/documents'); - } - - const { templateDocumentData } = template; - - const [templateRecipients, templateFields] = await Promise.all([ - getRecipientsForTemplate({ - templateId, - userId: user.id, - }), - getFieldsForTemplate({ - templateId, - userId: user.id, - }), - ]); - - return ( -
- - - Templates - - -

- {template.title} -

- -
- -
- - -
- ); +export default function TemplatePage({ params }: TemplatePageProps) { + return ; } diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx new file mode 100644 index 000000000..899e600f1 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx @@ -0,0 +1,86 @@ +import React from 'react'; + +import Link from 'next/link'; +import { redirect } from 'next/navigation'; + +import { ChevronLeft } from 'lucide-react'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import { getFieldsForTemplate } from '@documenso/lib/server-only/field/get-fields-for-template'; +import { getRecipientsForTemplate } from '@documenso/lib/server-only/recipient/get-recipients-for-template'; +import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; +import { formatTemplatesPath } from '@documenso/lib/utils/teams'; +import type { Team } from '@documenso/prisma/client'; + +import { TemplateType } from '~/components/formatter/template-type'; + +import { EditTemplateForm } from './edit-template'; + +export type TemplatePageViewProps = { + params: { + id: string; + }; + team?: Team; +}; + +export const TemplatePageView = async ({ params, team }: TemplatePageViewProps) => { + const { id } = params; + + const templateId = Number(id); + const templateRootPath = formatTemplatesPath(team?.url); + + if (!templateId || Number.isNaN(templateId)) { + redirect(templateRootPath); + } + + const { user } = await getRequiredServerComponentSession(); + + const template = await getTemplateById({ + id: templateId, + userId: user.id, + }).catch(() => null); + + if (!template || !template.templateDocumentData) { + redirect(templateRootPath); + } + + const { templateDocumentData } = template; + + const [templateRecipients, templateFields] = await Promise.all([ + getRecipientsForTemplate({ + templateId, + userId: user.id, + }), + getFieldsForTemplate({ + templateId, + userId: user.id, + }), + ]); + + return ( +
+ + + Templates + + +

+ {template.title} +

+ +
+ +
+ + +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx index 9f26d632c..eee32b920 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx @@ -21,9 +21,15 @@ import { DuplicateTemplateDialog } from './duplicate-template-dialog'; export type DataTableActionDropdownProps = { row: Template; + templateRootPath: string; + teamId?: number; }; -export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { +export const DataTableActionDropdown = ({ + row, + templateRootPath, + teamId, +}: DataTableActionDropdownProps) => { const { data: session } = useSession(); const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); @@ -34,6 +40,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = } const isOwner = row.userId === session.user.id; + const isTeamTemplate = row.teamId === teamId; return ( @@ -44,20 +51,25 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = Action - - + + Edit - {/* onDuplicateButtonClick(row.id)}> */} - setDuplicateDialogOpen(true)}> + setDuplicateDialogOpen(true)} + > Duplicate - setDeleteDialogOpen(true)}> + setDeleteDialogOpen(true)} + > Delete @@ -65,6 +77,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx index 0e8f822c2..309695c88 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx @@ -28,6 +28,9 @@ type TemplatesDataTableProps = { perPage: number; page: number; totalPages: number; + documentRootPath: string; + templateRootPath: string; + teamId?: number; }; export const TemplatesDataTable = ({ @@ -35,6 +38,9 @@ export const TemplatesDataTable = ({ perPage, page, totalPages, + documentRootPath, + templateRootPath, + teamId, }: TemplatesDataTableProps) => { const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); @@ -70,7 +76,7 @@ export const TemplatesDataTable = ({ duration: 5000, }); - router.push(`/documents/${id}`); + router.push(`${documentRootPath}/${id}`); } catch (err) { toast({ title: 'Error', @@ -131,7 +137,12 @@ export const TemplatesDataTable = ({ {!isRowLoading && } Use Template - + + ); }, diff --git a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx index 9075f4677..b31ad2048 100644 --- a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx @@ -35,20 +35,15 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD onOpenChange(false); }, - }); - - const onDeleteTemplate = async () => { - try { - await deleteTemplate({ id }); - } catch { + onError: () => { toast({ title: 'Something went wrong', description: 'This template could not be deleted at this time. Please try again.', variant: 'destructive', duration: 7500, }); - } - }; + }, + }); return ( !isLoading && onOpenChange(value)}> @@ -63,20 +58,18 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD -
- + - -
+
diff --git a/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx index be743ff48..cdd3000c2 100644 --- a/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx @@ -14,12 +14,14 @@ import { useToast } from '@documenso/ui/primitives/use-toast'; type DuplicateTemplateDialogProps = { id: number; + teamId?: number; open: boolean; onOpenChange: (_open: boolean) => void; }; export const DuplicateTemplateDialog = ({ id, + teamId, open, onOpenChange, }: DuplicateTemplateDialogProps) => { @@ -40,22 +42,15 @@ export const DuplicateTemplateDialog = ({ onOpenChange(false); }, + onError: () => { + toast({ + title: 'Error', + description: 'An error occurred while duplicating template.', + variant: 'destructive', + }); + }, }); - const onDuplicate = async () => { - try { - await duplicateTemplate({ - templateId: id, - }); - } catch (err) { - toast({ - title: 'Error', - description: 'An error occurred while duplicating template.', - variant: 'destructive', - }); - } - }; - return ( !isLoading && onOpenChange(value)}> @@ -66,20 +61,27 @@ export const DuplicateTemplateDialog = ({ -
- + - -
+
diff --git a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx index a4aa9bce2..37d60f946 100644 --- a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx @@ -43,8 +43,14 @@ const ZCreateTemplateFormSchema = z.object({ type TCreateTemplateFormSchema = z.infer; -export const NewTemplateDialog = () => { +type NewTemplateDialogProps = { + teamId?: number; + templateRootPath: string; +}; + +export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialogProps) => { const router = useRouter(); + const { data: session } = useSession(); const { toast } = useToast(); @@ -99,6 +105,7 @@ export const NewTemplateDialog = () => { }); const { id } = await createTemplate({ + teamId, title: values.name ? values.name : file.name, templateDocumentDataId, }); @@ -112,7 +119,7 @@ export const NewTemplateDialog = () => { setShowNewTemplateDialog(false); - void router.push(`/templates/${id}`); + router.push(`${templateRootPath}/${id}`); } catch { toast({ title: 'Something went wrong', diff --git a/apps/web/src/app/(dashboard)/templates/page.tsx b/apps/web/src/app/(dashboard)/templates/page.tsx index d3dacd501..7c7bd4e4f 100644 --- a/apps/web/src/app/(dashboard)/templates/page.tsx +++ b/apps/web/src/app/(dashboard)/templates/page.tsx @@ -2,57 +2,17 @@ import React from 'react'; import type { Metadata } from 'next'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; -import { getTemplates } from '@documenso/lib/server-only/template/get-templates'; - -import { TemplatesDataTable } from './data-table-templates'; -import { EmptyTemplateState } from './empty-state'; -import { NewTemplateDialog } from './new-template-dialog'; +import { TemplatesPageView } from './templates-page-view'; +import type { TemplatesPageViewProps } from './templates-page-view'; type TemplatesPageProps = { - searchParams?: { - page?: number; - perPage?: number; - }; + searchParams?: TemplatesPageViewProps['searchParams']; }; export const metadata: Metadata = { title: 'Templates', }; -export default async function TemplatesPage({ searchParams = {} }: TemplatesPageProps) { - const { user } = await getRequiredServerComponentSession(); - const page = Number(searchParams.page) || 1; - const perPage = Number(searchParams.perPage) || 10; - - const { templates, totalPages } = await getTemplates({ - userId: user.id, - page: page, - perPage: perPage, - }); - - return ( -
-
-

Templates

- -
- -
-
- -
- {templates.length > 0 ? ( - - ) : ( - - )} -
-
- ); +export default function TemplatesPage({ searchParams = {} }: TemplatesPageProps) { + return ; } diff --git a/apps/web/src/app/(dashboard)/templates/templates-page-view.tsx b/apps/web/src/app/(dashboard)/templates/templates-page-view.tsx new file mode 100644 index 000000000..4736f4268 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/templates-page-view.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; +import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; +import type { Team } from '@documenso/prisma/client'; +import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar'; + +import { TemplatesDataTable } from './data-table-templates'; +import { EmptyTemplateState } from './empty-state'; +import { NewTemplateDialog } from './new-template-dialog'; + +export type TemplatesPageViewProps = { + searchParams?: { + page?: number; + perPage?: number; + }; + team?: Team; +}; + +export const TemplatesPageView = async ({ searchParams = {}, team }: TemplatesPageViewProps) => { + const { user } = await getRequiredServerComponentSession(); + const page = Number(searchParams.page) || 1; + const perPage = Number(searchParams.perPage) || 10; + + const documentRootPath = formatDocumentsPath(team?.url); + const templateRootPath = formatTemplatesPath(team?.url); + + const { templates, totalPages } = await findTemplates({ + userId: user.id, + teamId: team?.id, + page: page, + perPage: perPage, + }); + + return ( +
+
+
+ {team && ( + + + {team.name.slice(0, 1)} + + + )} + +

Templates

+
+ +
+ +
+
+ +
+ {templates.length > 0 ? ( + + ) : ( + + )} +
+
+ ); +}; diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/page.tsx index b7f610cff..26b1d7c91 100644 --- a/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/page.tsx +++ b/apps/web/src/app/(teams)/t/[teamUrl]/documents/[id]/page.tsx @@ -1,7 +1,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; -import DocumentPageComponent from '~/app/(dashboard)/documents/[id]/document-page-view'; +import { DocumentPageView } from '~/app/(dashboard)/documents/[id]/document-page-view'; export type DocumentPageProps = { params: { @@ -16,5 +16,5 @@ export default async function DocumentPage({ params }: DocumentPageProps) { const { user } = await getRequiredServerComponentSession(); const team = await getTeamByUrl({ userId: user.id, teamUrl }); - return ; + return ; } diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/documents/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/documents/page.tsx index 952aeeeea..d3d5b5bee 100644 --- a/apps/web/src/app/(teams)/t/[teamUrl]/documents/page.tsx +++ b/apps/web/src/app/(teams)/t/[teamUrl]/documents/page.tsx @@ -2,7 +2,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get- import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; import type { DocumentsPageViewProps } from '~/app/(dashboard)/documents/documents-page-view'; -import DocumentsPageView from '~/app/(dashboard)/documents/documents-page-view'; +import { DocumentsPageView } from '~/app/(dashboard)/documents/documents-page-view'; export type TeamsDocumentPageProps = { params: { diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx new file mode 100644 index 000000000..3fe7cbf67 --- /dev/null +++ b/apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; + +import type { TemplatePageViewProps } from '~/app/(dashboard)/templates/[id]/template-page-view'; +import { TemplatePageView } from '~/app/(dashboard)/templates/[id]/template-page-view'; + +type TeamTemplatePageProps = { + params: TemplatePageViewProps['params'] & { + teamUrl: string; + }; +}; + +export default async function TeamTemplatePage({ params }: TeamTemplatePageProps) { + const { teamUrl } = params; + + const { user } = await getRequiredServerComponentSession(); + const team = await getTeamByUrl({ userId: user.id, teamUrl }); + + return ; +} diff --git a/apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx b/apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx new file mode 100644 index 000000000..6954d8e2d --- /dev/null +++ b/apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; +import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; + +import type { TemplatesPageViewProps } from '~/app/(dashboard)/templates/templates-page-view'; +import { TemplatesPageView } from '~/app/(dashboard)/templates/templates-page-view'; + +type TeamTemplatesPageProps = { + searchParams?: TemplatesPageViewProps['searchParams']; + params: { + teamUrl: string; + }; +}; + +export default async function TeamTemplatesPage({ + searchParams = {}, + params, +}: TeamTemplatesPageProps) { + const { teamUrl } = params; + + const { user } = await getRequiredServerComponentSession(); + const team = await getTeamByUrl({ userId: user.id, teamUrl }); + + return ; +} diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx index 2b11c4be2..9eef1f4bd 100644 --- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx +++ b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx @@ -52,24 +52,22 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { {...props} >
- {navigationLinks - .filter(({ href }) => href !== '/templates' || rootHref === '') // Remove templates for team pages. - .map(({ href, label }) => ( - - {label} - - ))} + {navigationLinks.map(({ href, label }) => ( + + {label} + + ))}
diff --git a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx index 35a05baf2..195716d64 100644 --- a/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx +++ b/apps/web/src/components/(dashboard)/layout/menu-switcher.tsx @@ -6,7 +6,7 @@ import { usePathname } from 'next/navigation'; import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react'; import { signOut } from 'next-auth/react'; -import { TEAM_MEMBER_ROLE_MAP } from '@documenso/lib/constants/teams'; +import { TEAM_MEMBER_ROLE_MAP, TEAM_URL_REGEX } from '@documenso/lib/constants/teams'; import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; @@ -71,6 +71,22 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp 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. + */ + const formatRedirectUrlOnSwitch = (teamUrl?: string) => { + const baseUrl = teamUrl ? `/t/${teamUrl}/` : '/'; + + const currentPathname = (pathname ?? '/').replace(TEAM_URL_REGEX, ''); + + if (currentPathname === '/templates') { + return `${baseUrl}templates`; + } + + return baseUrl; + }; + return ( @@ -100,7 +116,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp Personal - + ( - + text !== 'Templates' || href === '/templates'); // Filter out templates for teams. + ]; return ( diff --git a/packages/app-tests/e2e/templates/manage-templates.spec.ts b/packages/app-tests/e2e/templates/manage-templates.spec.ts new file mode 100644 index 000000000..53edc705d --- /dev/null +++ b/packages/app-tests/e2e/templates/manage-templates.spec.ts @@ -0,0 +1,205 @@ +import { expect, test } from '@playwright/test'; + +import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; +import { seedTeam, unseedTeam } from '@documenso/prisma/seed/teams'; +import { seedTemplate } from '@documenso/prisma/seed/templates'; + +import { manualLogin } from '../fixtures/authentication'; + +test.describe.configure({ mode: 'parallel' }); + +test('[TEMPLATES]: view templates', async ({ page }) => { + const team = await seedTeam({ + createTeamMembers: 1, + }); + + const owner = team.owner; + const teamMemberUser = team.members[1].user; + + // Should only be visible to the owner in personal templates. + await seedTemplate({ + title: 'Personal template', + userId: owner.id, + }); + + // Should be visible to team members. + await seedTemplate({ + title: 'Team template 1', + userId: owner.id, + teamId: team.id, + }); + + // Should be visible to team members. + await seedTemplate({ + title: 'Team template 2', + userId: teamMemberUser.id, + teamId: team.id, + }); + + await manualLogin({ + page, + email: owner.email, + redirectPath: '/templates', + }); + + // Owner should see both team templates. + await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`); + await expect(page.getByRole('main')).toContainText('Showing 2 results'); + + // Only should only see their personal template. + await page.goto(`${WEBAPP_BASE_URL}/templates`); + await expect(page.getByRole('main')).toContainText('Showing 1 result'); + + await unseedTeam(team.url); +}); + +test('[TEMPLATES]: delete template', async ({ page }) => { + const team = await seedTeam({ + createTeamMembers: 1, + }); + + const owner = team.owner; + const teamMemberUser = team.members[1].user; + + // Should only be visible to the owner in personal templates. + await seedTemplate({ + title: 'Personal template', + userId: owner.id, + }); + + // Should be visible to team members. + await seedTemplate({ + title: 'Team template 1', + userId: owner.id, + teamId: team.id, + }); + + // Should be visible to team members. + await seedTemplate({ + title: 'Team template 2', + userId: teamMemberUser.id, + teamId: team.id, + }); + + await manualLogin({ + page, + email: owner.email, + redirectPath: '/templates', + }); + + // Owner should be able to delete their personal template. + await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); + await expect(page.getByText('Template deleted').first()).toBeVisible(); + + // Team member should be able to delete all templates. + await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`); + + for (const template of ['Team template 1', 'Team template 2']) { + await page + .getByRole('row', { name: template }) + .getByRole('cell', { name: 'Use Template' }) + .getByRole('button') + .nth(1) + .click(); + + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); + await expect(page.getByText('Template deleted').first()).toBeVisible(); + } + + await unseedTeam(team.url); +}); + +test('[TEMPLATES]: duplicate template', async ({ page }) => { + const team = await seedTeam({ + createTeamMembers: 1, + }); + + const owner = team.owner; + const teamMemberUser = team.members[1].user; + + // Should only be visible to the owner in personal templates. + await seedTemplate({ + title: 'Personal template', + userId: owner.id, + }); + + // Should be visible to team members. + await seedTemplate({ + title: 'Team template 1', + userId: teamMemberUser.id, + teamId: team.id, + }); + + await manualLogin({ + page, + email: owner.email, + redirectPath: '/templates', + }); + + // Duplicate personal template. + await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await page.getByRole('button', { name: 'Duplicate' }).click(); + await expect(page.getByText('Template duplicated').first()).toBeVisible(); + await expect(page.getByRole('main')).toContainText('Showing 2 results'); + + await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`); + + // Duplicate team template. + await page.getByRole('cell', { name: 'Use Template' }).getByRole('button').nth(1).click(); + await page.getByRole('menuitem', { name: 'Duplicate' }).click(); + await page.getByRole('button', { name: 'Duplicate' }).click(); + await expect(page.getByText('Template duplicated').first()).toBeVisible(); + await expect(page.getByRole('main')).toContainText('Showing 2 results'); + + await unseedTeam(team.url); +}); + +test('[TEMPLATES]: use template', async ({ page }) => { + const team = await seedTeam({ + createTeamMembers: 1, + }); + + const owner = team.owner; + const teamMemberUser = team.members[1].user; + + // Should only be visible to the owner in personal templates. + await seedTemplate({ + title: 'Personal template', + userId: owner.id, + }); + + // Should be visible to team members. + await seedTemplate({ + title: 'Team template 1', + userId: teamMemberUser.id, + teamId: team.id, + }); + + await manualLogin({ + page, + email: owner.email, + redirectPath: '/templates', + }); + + // Use personal template. + await page.getByRole('button', { name: 'Use Template' }).click(); + await page.waitForURL(/documents/); + await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); + await page.waitForURL('/documents'); + await expect(page.getByRole('main')).toContainText('Showing 1 result'); + + await page.goto(`${WEBAPP_BASE_URL}/t/${team.url}/templates`); + + // Use team template. + await page.getByRole('button', { name: 'Use Template' }).click(); + await page.waitForURL(/\/t\/.+\/documents/); + await page.getByRole('main').getByRole('link', { name: 'Documents' }).click(); + await page.waitForURL(`/t/${team.url}/documents`); + await expect(page.getByRole('main')).toContainText('Showing 1 result'); + + await unseedTeam(team.url); +}); diff --git a/packages/lib/constants/teams.ts b/packages/lib/constants/teams.ts index 47705bb14..67f3ef16f 100644 --- a/packages/lib/constants/teams.ts +++ b/packages/lib/constants/teams.ts @@ -1,6 +1,7 @@ import { TeamMemberRole } from '@documenso/prisma/client'; export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+$'); +export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+'); export const TEAM_MEMBER_ROLE_MAP: Record = { ADMIN: 'Admin', diff --git a/packages/lib/server-only/field/get-fields-for-template.ts b/packages/lib/server-only/field/get-fields-for-template.ts index c174d7eff..724ec75fb 100644 --- a/packages/lib/server-only/field/get-fields-for-template.ts +++ b/packages/lib/server-only/field/get-fields-for-template.ts @@ -10,7 +10,20 @@ export const getFieldsForTemplate = async ({ templateId, userId }: GetFieldsForT where: { templateId, Template: { - userId, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], }, }, orderBy: { diff --git a/packages/lib/server-only/field/set-fields-for-template.ts b/packages/lib/server-only/field/set-fields-for-template.ts index 9431666bf..2062e06bc 100644 --- a/packages/lib/server-only/field/set-fields-for-template.ts +++ b/packages/lib/server-only/field/set-fields-for-template.ts @@ -27,7 +27,20 @@ export const setFieldsForTemplate = async ({ const template = await prisma.template.findFirst({ where: { id: templateId, - userId, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], }, }); diff --git a/packages/lib/server-only/recipient/get-recipients-for-template.ts b/packages/lib/server-only/recipient/get-recipients-for-template.ts index ab6f860eb..4b393353d 100644 --- a/packages/lib/server-only/recipient/get-recipients-for-template.ts +++ b/packages/lib/server-only/recipient/get-recipients-for-template.ts @@ -13,7 +13,20 @@ export const getRecipientsForTemplate = async ({ where: { templateId, Template: { - userId, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], }, }, orderBy: { diff --git a/packages/lib/server-only/recipient/set-recipients-for-template.ts b/packages/lib/server-only/recipient/set-recipients-for-template.ts index c21c8cbf9..7c96bcf44 100644 --- a/packages/lib/server-only/recipient/set-recipients-for-template.ts +++ b/packages/lib/server-only/recipient/set-recipients-for-template.ts @@ -20,7 +20,20 @@ export const setRecipientsForTemplate = async ({ const template = await prisma.template.findFirst({ where: { id: templateId, - userId, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], }, }); diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index 1c23d8f85..c520d4ce1 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -11,7 +11,23 @@ export const createDocumentFromTemplate = async ({ userId, }: CreateDocumentFromTemplateOptions) => { const template = await prisma.template.findUnique({ - where: { id: templateId, userId }, + where: { + id: templateId, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], + }, include: { Recipient: true, Field: true, @@ -34,6 +50,7 @@ export const createDocumentFromTemplate = async ({ const document = await prisma.document.create({ data: { userId, + teamId: template.teamId, title: template.title, documentDataId: documentData.id, Recipient: { diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts index d00526a64..e51d69485 100644 --- a/packages/lib/server-only/template/create-template.ts +++ b/packages/lib/server-only/template/create-template.ts @@ -1,20 +1,36 @@ import { prisma } from '@documenso/prisma'; -import { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; +import type { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; export type CreateTemplateOptions = TCreateTemplateMutationSchema & { userId: number; + teamId?: number; }; export const createTemplate = async ({ title, userId, + teamId, templateDocumentDataId, }: CreateTemplateOptions) => { + if (teamId) { + await prisma.team.findFirstOrThrow({ + where: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + }); + } + return await prisma.template.create({ data: { title, userId, templateDocumentDataId, + teamId, }, }); }; diff --git a/packages/lib/server-only/template/delete-template.ts b/packages/lib/server-only/template/delete-template.ts index f693bcec0..c24cc1333 100644 --- a/packages/lib/server-only/template/delete-template.ts +++ b/packages/lib/server-only/template/delete-template.ts @@ -8,5 +8,23 @@ export type DeleteTemplateOptions = { }; export const deleteTemplate = async ({ id, userId }: DeleteTemplateOptions) => { - return await prisma.template.delete({ where: { id, userId } }); + return await prisma.template.delete({ + where: { + id, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], + }, + }); }; diff --git a/packages/lib/server-only/template/duplicate-template.ts b/packages/lib/server-only/template/duplicate-template.ts index 6078a1945..97b3f0a0b 100644 --- a/packages/lib/server-only/template/duplicate-template.ts +++ b/packages/lib/server-only/template/duplicate-template.ts @@ -1,14 +1,39 @@ import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; +import type { Prisma } from '@documenso/prisma/client'; import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & { userId: number; }; -export const duplicateTemplate = async ({ templateId, userId }: DuplicateTemplateOptions) => { +export const duplicateTemplate = async ({ + templateId, + userId, + teamId, +}: DuplicateTemplateOptions) => { + let templateWhereFilter: Prisma.TemplateWhereUniqueInput = { + id: templateId, + userId, + teamId: null, + }; + + if (teamId !== undefined) { + templateWhereFilter = { + id: templateId, + teamId, + team: { + members: { + some: { + userId, + }, + }, + }, + }; + } + const template = await prisma.template.findUnique({ - where: { id: templateId, userId }, + where: templateWhereFilter, include: { Recipient: true, Field: true, @@ -31,6 +56,7 @@ export const duplicateTemplate = async ({ templateId, userId }: DuplicateTemplat const duplicatedTemplate = await prisma.template.create({ data: { userId, + teamId, title: template.title + ' (copy)', templateDocumentDataId: documentData.id, Recipient: { diff --git a/packages/lib/server-only/template/find-templates.ts b/packages/lib/server-only/template/find-templates.ts new file mode 100644 index 000000000..d453d28a0 --- /dev/null +++ b/packages/lib/server-only/template/find-templates.ts @@ -0,0 +1,56 @@ +import { prisma } from '@documenso/prisma'; +import type { Prisma } from '@documenso/prisma/client'; + +export type FindTemplatesOptions = { + userId: number; + teamId?: number; + page: number; + perPage: number; +}; + +export const findTemplates = async ({ + userId, + teamId, + page = 1, + perPage = 10, +}: FindTemplatesOptions) => { + let whereFilter: Prisma.TemplateWhereInput = { + userId, + teamId: null, + }; + + if (teamId !== undefined) { + whereFilter = { + team: { + id: teamId, + members: { + some: { + userId, + }, + }, + }, + }; + } + + const [templates, count] = await Promise.all([ + prisma.template.findMany({ + where: whereFilter, + include: { + templateDocumentData: true, + Field: true, + }, + skip: Math.max(page - 1, 0) * perPage, + orderBy: { + createdAt: 'desc', + }, + }), + prisma.template.count({ + where: whereFilter, + }), + ]); + + return { + templates, + totalPages: Math.ceil(count / perPage), + }; +}; diff --git a/packages/lib/server-only/template/get-template-by-id.ts b/packages/lib/server-only/template/get-template-by-id.ts index 56f959a9b..c4295c3c3 100644 --- a/packages/lib/server-only/template/get-template-by-id.ts +++ b/packages/lib/server-only/template/get-template-by-id.ts @@ -1,4 +1,5 @@ import { prisma } from '@documenso/prisma'; +import type { Prisma } from '@documenso/prisma/client'; export interface GetTemplateByIdOptions { id: number; @@ -6,11 +7,26 @@ export interface GetTemplateByIdOptions { } export const getTemplateById = async ({ id, userId }: GetTemplateByIdOptions) => { + const whereFilter: Prisma.TemplateWhereInput = { + id, + OR: [ + { + userId, + }, + { + team: { + members: { + some: { + userId, + }, + }, + }, + }, + ], + }; + return await prisma.template.findFirstOrThrow({ - where: { - id, - userId, - }, + where: whereFilter, include: { templateDocumentData: true, }, diff --git a/packages/lib/server-only/template/get-templates.ts b/packages/lib/server-only/template/get-templates.ts deleted file mode 100644 index 5f802d278..000000000 --- a/packages/lib/server-only/template/get-templates.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { prisma } from '@documenso/prisma'; - -export type GetTemplatesOptions = { - userId: number; - page: number; - perPage: number; -}; - -export const getTemplates = async ({ userId, page = 1, perPage = 10 }: GetTemplatesOptions) => { - const [templates, count] = await Promise.all([ - prisma.template.findMany({ - where: { - userId, - }, - include: { - templateDocumentData: true, - Field: true, - }, - skip: Math.max(page - 1, 0) * perPage, - orderBy: { - createdAt: 'desc', - }, - }), - prisma.template.count({ - where: { - userId, - }, - }), - ]); - - return { - templates, - totalPages: Math.ceil(count / perPage), - }; -}; diff --git a/packages/lib/utils/teams.ts b/packages/lib/utils/teams.ts index eb9be2c2b..c6dfd27fd 100644 --- a/packages/lib/utils/teams.ts +++ b/packages/lib/utils/teams.ts @@ -12,6 +12,10 @@ export const formatDocumentsPath = (teamUrl?: string) => { return teamUrl ? `/t/${teamUrl}/documents` : '/documents'; }; +export const formatTemplatesPath = (teamUrl?: string) => { + return teamUrl ? `/t/${teamUrl}/templates` : '/templates'; +}; + /** * Determines whether a team member can execute a given action. * diff --git a/packages/prisma/migrations/20240206051948_add_teams_templates/migration.sql b/packages/prisma/migrations/20240206051948_add_teams_templates/migration.sql new file mode 100644 index 000000000..3a79168bf --- /dev/null +++ b/packages/prisma/migrations/20240206051948_add_teams_templates/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Template" ADD COLUMN "teamId" INTEGER; + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 79dcdf6aa..fc128efc1 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -334,7 +334,8 @@ model Team { owner User @relation(fields: [ownerUserId], references: [id]) subscription Subscription? - document Document[] + document Document[] + templates Template[] } model TeamPending { @@ -415,10 +416,12 @@ model Template { type TemplateType @default(PRIVATE) title String userId Int + teamId Int? templateDocumentDataId String createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade) User User @relation(fields: [userId], references: [id], onDelete: Cascade) Recipient Recipient[] diff --git a/packages/prisma/seed/templates.ts b/packages/prisma/seed/templates.ts new file mode 100644 index 000000000..7f1b2f8e9 --- /dev/null +++ b/packages/prisma/seed/templates.ts @@ -0,0 +1,36 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { prisma } from '..'; +import { DocumentDataType } from '../client'; + +const examplePdf = fs + .readFileSync(path.join(__dirname, '../../../assets/example.pdf')) + .toString('base64'); + +type SeedTemplateOptions = { + title?: string; + userId: number; + teamId?: number; +}; + +export const seedTemplate = async (options: SeedTemplateOptions) => { + const { title = 'Untitled', userId, teamId } = options; + + const documentData = await prisma.documentData.create({ + data: { + type: DocumentDataType.BYTES_64, + data: examplePdf, + initialData: examplePdf, + }, + }); + + return await prisma.template.create({ + data: { + title, + templateDocumentDataId: documentData.id, + userId: userId, + teamId, + }, + }); +}; diff --git a/packages/trpc/server/field-router/router.ts b/packages/trpc/server/field-router/router.ts index 07cdcd347..5ae3cbe4b 100644 --- a/packages/trpc/server/field-router/router.ts +++ b/packages/trpc/server/field-router/router.ts @@ -39,7 +39,7 @@ export const fieldRouter = router({ throw new TRPCError({ code: 'BAD_REQUEST', - message: 'We were unable to sign this field. Please try again later.', + message: 'We were unable to set this field. Please try again later.', }); } }), diff --git a/packages/trpc/server/recipient-router/router.ts b/packages/trpc/server/recipient-router/router.ts index 1ada3d0d3..9553a8aae 100644 --- a/packages/trpc/server/recipient-router/router.ts +++ b/packages/trpc/server/recipient-router/router.ts @@ -33,7 +33,7 @@ export const recipientRouter = router({ throw new TRPCError({ code: 'BAD_REQUEST', - message: 'We were unable to sign this field. Please try again later.', + message: 'We were unable to set this field. Please try again later.', }); } }), @@ -58,7 +58,7 @@ export const recipientRouter = router({ throw new TRPCError({ code: 'BAD_REQUEST', - message: 'We were unable to sign this field. Please try again later.', + message: 'We were unable to set this field. Please try again later.', }); } }), diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index 28e919e92..7417e7d00 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -19,11 +19,12 @@ export const templateRouter = router({ .input(ZCreateTemplateMutationSchema) .mutation(async ({ input, ctx }) => { try { - const { title, templateDocumentDataId } = input; + const { teamId, title, templateDocumentDataId } = input; return await createTemplate({ - title, userId: ctx.user.id, + teamId, + title, templateDocumentDataId, }); } catch (err) { @@ -64,11 +65,12 @@ export const templateRouter = router({ .input(ZDuplicateTemplateMutationSchema) .mutation(async ({ input, ctx }) => { try { - const { templateId } = input; + const { teamId, templateId } = input; return await duplicateTemplate({ - templateId, userId: ctx.user.id, + teamId, + templateId, }); } catch (err) { console.error(err); @@ -88,7 +90,7 @@ export const templateRouter = router({ const userId = ctx.user.id; - return await deleteTemplate({ id, userId }); + return await deleteTemplate({ userId, id }); } catch (err) { console.error(err); diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index bc7161f74..3d87d4b4f 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -1,7 +1,8 @@ import { z } from 'zod'; export const ZCreateTemplateMutationSchema = z.object({ - title: z.string().min(1), + title: z.string().min(1).trim(), + teamId: z.number().optional(), templateDocumentDataId: z.string().min(1), }); @@ -11,6 +12,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({ export const ZDuplicateTemplateMutationSchema = z.object({ templateId: z.number(), + teamId: z.number().optional(), }); export const ZDeleteTemplateMutationSchema = z.object({