mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 17:21:41 +10:00
feat: add team templates (#912)
This commit is contained in:
@ -25,7 +25,7 @@ export type DocumentPageViewProps = {
|
|||||||
team?: Team;
|
team?: Team;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function DocumentPageView({ params, team }: DocumentPageViewProps) {
|
export const DocumentPageView = async ({ params, team }: DocumentPageViewProps) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
const documentId = Number(id);
|
const documentId = Number(id);
|
||||||
@ -128,4 +128,4 @@ export default async function DocumentPageView({ params, team }: DocumentPageVie
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import DocumentPageView from './document-page-view';
|
import { DocumentPageView } from './document-page-view';
|
||||||
|
|
||||||
export type DocumentPageProps = {
|
export type DocumentPageProps = {
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@ -33,10 +33,7 @@ export type DocumentsPageViewProps = {
|
|||||||
team?: Team & { teamEmail?: TeamEmail | null };
|
team?: Team & { teamEmail?: TeamEmail | null };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function DocumentsPageView({
|
export const DocumentsPageView = async ({ searchParams = {}, team }: DocumentsPageViewProps) => {
|
||||||
searchParams = {},
|
|
||||||
team,
|
|
||||||
}: DocumentsPageViewProps) {
|
|
||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
|
|
||||||
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
const status = isExtendedDocumentStatus(searchParams.status) ? searchParams.status : 'ALL';
|
||||||
@ -155,4 +152,4 @@ export default async function DocumentsPageView({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import type { DocumentsPageViewProps } from './documents-page-view';
|
import type { DocumentsPageViewProps } from './documents-page-view';
|
||||||
import DocumentsPageView from './documents-page-view';
|
import { DocumentsPageView } from './documents-page-view';
|
||||||
|
|
||||||
export type DocumentsPageProps = {
|
export type DocumentsPageProps = {
|
||||||
searchParams?: DocumentsPageViewProps['searchParams'];
|
searchParams?: DocumentsPageViewProps['searchParams'];
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export type EditTemplateFormProps = {
|
|||||||
recipients: Recipient[];
|
recipients: Recipient[];
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
documentData: DocumentData;
|
documentData: DocumentData;
|
||||||
|
templateRootPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type EditTemplateStep = 'signers' | 'fields';
|
type EditTemplateStep = 'signers' | 'fields';
|
||||||
@ -40,6 +41,7 @@ export const EditTemplateForm = ({
|
|||||||
fields,
|
fields,
|
||||||
user: _user,
|
user: _user,
|
||||||
documentData,
|
documentData,
|
||||||
|
templateRootPath,
|
||||||
}: EditTemplateFormProps) => {
|
}: EditTemplateFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -98,7 +100,7 @@ export const EditTemplateForm = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/templates');
|
router.push(templateRootPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
|||||||
@ -1,81 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import type { TemplatePageViewProps } from './template-page-view';
|
||||||
import { redirect } from 'next/navigation';
|
import { TemplatePageView } from './template-page-view';
|
||||||
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
type TemplatePageProps = Pick<TemplatePageViewProps, 'params'>;
|
||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
export default function TemplatePage({ params }: TemplatePageProps) {
|
||||||
import { getFieldsForTemplate } from '@documenso/lib/server-only/field/get-fields-for-template';
|
return <TemplatePageView params={params} />;
|
||||||
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 (
|
|
||||||
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
|
||||||
<Link href="/templates" className="flex items-center text-[#7AC455] hover:opacity-80">
|
|
||||||
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
|
||||||
Templates
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}>
|
|
||||||
{template.title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="mt-2.5 flex items-center gap-x-6">
|
|
||||||
<TemplateType inheritColor type={template.type} className="text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditTemplateForm
|
|
||||||
className="mt-8"
|
|
||||||
template={template}
|
|
||||||
user={user}
|
|
||||||
recipients={templateRecipients}
|
|
||||||
fields={templateFields}
|
|
||||||
documentData={templateDocumentData}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||||
|
<Link href="/templates" className="flex items-center text-[#7AC455] hover:opacity-80">
|
||||||
|
<ChevronLeft className="mr-2 inline-block h-5 w-5" />
|
||||||
|
Templates
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}>
|
||||||
|
{template.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="mt-2.5 flex items-center gap-x-6">
|
||||||
|
<TemplateType inheritColor type={template.type} className="text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditTemplateForm
|
||||||
|
className="mt-8"
|
||||||
|
template={template}
|
||||||
|
user={user}
|
||||||
|
recipients={templateRecipients}
|
||||||
|
fields={templateFields}
|
||||||
|
documentData={templateDocumentData}
|
||||||
|
templateRootPath={templateRootPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -21,9 +21,15 @@ import { DuplicateTemplateDialog } from './duplicate-template-dialog';
|
|||||||
|
|
||||||
export type DataTableActionDropdownProps = {
|
export type DataTableActionDropdownProps = {
|
||||||
row: Template;
|
row: Template;
|
||||||
|
templateRootPath: string;
|
||||||
|
teamId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => {
|
export const DataTableActionDropdown = ({
|
||||||
|
row,
|
||||||
|
templateRootPath,
|
||||||
|
teamId,
|
||||||
|
}: DataTableActionDropdownProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
|
||||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@ -34,6 +40,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isOwner = row.userId === session.user.id;
|
const isOwner = row.userId === session.user.id;
|
||||||
|
const isTeamTemplate = row.teamId === teamId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@ -44,20 +51,25 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
<DropdownMenuContent className="w-52" align="start" forceMount>
|
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||||
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuItem disabled={!isOwner} asChild>
|
<DropdownMenuItem disabled={!isOwner && !isTeamTemplate} asChild>
|
||||||
<Link href={`/templates/${row.id}`}>
|
<Link href={`${templateRootPath}/${row.id}`}>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{/* <DropdownMenuItem disabled={!isOwner} onClick={async () => onDuplicateButtonClick(row.id)}> */}
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem disabled={!isOwner} onClick={() => setDuplicateDialogOpen(true)}>
|
disabled={!isOwner && !isTeamTemplate}
|
||||||
|
onClick={() => setDuplicateDialogOpen(true)}
|
||||||
|
>
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
Duplicate
|
Duplicate
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem disabled={!isOwner} onClick={() => setDeleteDialogOpen(true)}>
|
<DropdownMenuItem
|
||||||
|
disabled={!isOwner && !isTeamTemplate}
|
||||||
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
|
>
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
Delete
|
Delete
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -65,6 +77,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
|||||||
|
|
||||||
<DuplicateTemplateDialog
|
<DuplicateTemplateDialog
|
||||||
id={row.id}
|
id={row.id}
|
||||||
|
teamId={teamId}
|
||||||
open={isDuplicateDialogOpen}
|
open={isDuplicateDialogOpen}
|
||||||
onOpenChange={setDuplicateDialogOpen}
|
onOpenChange={setDuplicateDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -28,6 +28,9 @@ type TemplatesDataTableProps = {
|
|||||||
perPage: number;
|
perPage: number;
|
||||||
page: number;
|
page: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
|
documentRootPath: string;
|
||||||
|
templateRootPath: string;
|
||||||
|
teamId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TemplatesDataTable = ({
|
export const TemplatesDataTable = ({
|
||||||
@ -35,6 +38,9 @@ export const TemplatesDataTable = ({
|
|||||||
perPage,
|
perPage,
|
||||||
page,
|
page,
|
||||||
totalPages,
|
totalPages,
|
||||||
|
documentRootPath,
|
||||||
|
templateRootPath,
|
||||||
|
teamId,
|
||||||
}: TemplatesDataTableProps) => {
|
}: TemplatesDataTableProps) => {
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const updateSearchParams = useUpdateSearchParams();
|
const updateSearchParams = useUpdateSearchParams();
|
||||||
@ -70,7 +76,7 @@ export const TemplatesDataTable = ({
|
|||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push(`/documents/${id}`);
|
router.push(`${documentRootPath}/${id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
@ -131,7 +137,12 @@ export const TemplatesDataTable = ({
|
|||||||
{!isRowLoading && <Plus className="-ml-1 mr-2 h-4 w-4" />}
|
{!isRowLoading && <Plus className="-ml-1 mr-2 h-4 w-4" />}
|
||||||
Use Template
|
Use Template
|
||||||
</Button>
|
</Button>
|
||||||
<DataTableActionDropdown row={row.original} />
|
|
||||||
|
<DataTableActionDropdown
|
||||||
|
row={row.original}
|
||||||
|
teamId={teamId}
|
||||||
|
templateRootPath={templateRootPath}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -35,20 +35,15 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
|||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
});
|
onError: () => {
|
||||||
|
|
||||||
const onDeleteTemplate = async () => {
|
|
||||||
try {
|
|
||||||
await deleteTemplate({ id });
|
|
||||||
} catch {
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Something went wrong',
|
title: 'Something went wrong',
|
||||||
description: 'This template could not be deleted at this time. Please try again.',
|
description: 'This template could not be deleted at this time. Please try again.',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
duration: 7500,
|
duration: 7500,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||||
@ -63,20 +58,18 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
disabled={isLoading}
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="flex-1"
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button type="button" loading={isLoading} onClick={onDeleteTemplate} className="flex-1">
|
<Button type="button" loading={isLoading} onClick={async () => deleteTemplate({ id })}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -14,12 +14,14 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
|
|
||||||
type DuplicateTemplateDialogProps = {
|
type DuplicateTemplateDialogProps = {
|
||||||
id: number;
|
id: number;
|
||||||
|
teamId?: number;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DuplicateTemplateDialog = ({
|
export const DuplicateTemplateDialog = ({
|
||||||
id,
|
id,
|
||||||
|
teamId,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DuplicateTemplateDialogProps) => {
|
}: DuplicateTemplateDialogProps) => {
|
||||||
@ -40,21 +42,14 @@ export const DuplicateTemplateDialog = ({
|
|||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
});
|
onError: () => {
|
||||||
|
|
||||||
const onDuplicate = async () => {
|
|
||||||
try {
|
|
||||||
await duplicateTemplate({
|
|
||||||
templateId: id,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
description: 'An error occurred while duplicating template.',
|
description: 'An error occurred while duplicating template.',
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
||||||
@ -66,20 +61,27 @@ export const DuplicateTemplateDialog = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
disabled={isLoading}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className="flex-1"
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button type="button" loading={isLoading} onClick={onDuplicate} className="flex-1">
|
<Button
|
||||||
|
type="button"
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={async () =>
|
||||||
|
duplicateTemplate({
|
||||||
|
templateId: id,
|
||||||
|
teamId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
Duplicate
|
Duplicate
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -43,8 +43,14 @@ const ZCreateTemplateFormSchema = z.object({
|
|||||||
|
|
||||||
type TCreateTemplateFormSchema = z.infer<typeof ZCreateTemplateFormSchema>;
|
type TCreateTemplateFormSchema = z.infer<typeof ZCreateTemplateFormSchema>;
|
||||||
|
|
||||||
export const NewTemplateDialog = () => {
|
type NewTemplateDialogProps = {
|
||||||
|
teamId?: number;
|
||||||
|
templateRootPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialogProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -99,6 +105,7 @@ export const NewTemplateDialog = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { id } = await createTemplate({
|
const { id } = await createTemplate({
|
||||||
|
teamId,
|
||||||
title: values.name ? values.name : file.name,
|
title: values.name ? values.name : file.name,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId,
|
||||||
});
|
});
|
||||||
@ -112,7 +119,7 @@ export const NewTemplateDialog = () => {
|
|||||||
|
|
||||||
setShowNewTemplateDialog(false);
|
setShowNewTemplateDialog(false);
|
||||||
|
|
||||||
void router.push(`/templates/${id}`);
|
router.push(`${templateRootPath}/${id}`);
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: 'Something went wrong',
|
title: 'Something went wrong',
|
||||||
|
|||||||
@ -2,57 +2,17 @@ import React from 'react';
|
|||||||
|
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { TemplatesPageView } from './templates-page-view';
|
||||||
import { getTemplates } from '@documenso/lib/server-only/template/get-templates';
|
import type { TemplatesPageViewProps } from './templates-page-view';
|
||||||
|
|
||||||
import { TemplatesDataTable } from './data-table-templates';
|
|
||||||
import { EmptyTemplateState } from './empty-state';
|
|
||||||
import { NewTemplateDialog } from './new-template-dialog';
|
|
||||||
|
|
||||||
type TemplatesPageProps = {
|
type TemplatesPageProps = {
|
||||||
searchParams?: {
|
searchParams?: TemplatesPageViewProps['searchParams'];
|
||||||
page?: number;
|
|
||||||
perPage?: number;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Templates',
|
title: 'Templates',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function TemplatesPage({ searchParams = {} }: TemplatesPageProps) {
|
export default function TemplatesPage({ searchParams = {} }: TemplatesPageProps) {
|
||||||
const { user } = await getRequiredServerComponentSession();
|
return <TemplatesPageView searchParams={searchParams} />;
|
||||||
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 (
|
|
||||||
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
|
||||||
<div className="flex items-baseline justify-between">
|
|
||||||
<h1 className="mb-5 mt-2 truncate text-2xl font-semibold md:text-3xl">Templates</h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<NewTemplateDialog />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative">
|
|
||||||
{templates.length > 0 ? (
|
|
||||||
<TemplatesDataTable
|
|
||||||
templates={templates}
|
|
||||||
page={page}
|
|
||||||
perPage={perPage}
|
|
||||||
totalPages={totalPages}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EmptyTemplateState />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
<div className="mx-auto max-w-screen-xl px-4 md:px-8">
|
||||||
|
<div className="flex items-baseline justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
{team && (
|
||||||
|
<Avatar className="dark:border-border mr-3 h-12 w-12 border-2 border-solid border-white">
|
||||||
|
<AvatarFallback className="text-xs text-gray-400">
|
||||||
|
{team.name.slice(0, 1)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h1 className="truncate text-2xl font-semibold md:text-3xl">Templates</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<NewTemplateDialog templateRootPath={templateRootPath} teamId={team?.id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative mt-5">
|
||||||
|
{templates.length > 0 ? (
|
||||||
|
<TemplatesDataTable
|
||||||
|
templates={templates}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
documentRootPath={documentRootPath}
|
||||||
|
templateRootPath={templateRootPath}
|
||||||
|
teamId={team?.id}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyTemplateState />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
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 = {
|
export type DocumentPageProps = {
|
||||||
params: {
|
params: {
|
||||||
@ -16,5 +16,5 @@ export default async function DocumentPage({ params }: DocumentPageProps) {
|
|||||||
const { user } = await getRequiredServerComponentSession();
|
const { user } = await getRequiredServerComponentSession();
|
||||||
const team = await getTeamByUrl({ userId: user.id, teamUrl });
|
const team = await getTeamByUrl({ userId: user.id, teamUrl });
|
||||||
|
|
||||||
return <DocumentPageComponent params={params} team={team} />;
|
return <DocumentPageView params={params} team={team} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
|
|||||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||||
|
|
||||||
import type { DocumentsPageViewProps } from '~/app/(dashboard)/documents/documents-page-view';
|
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 = {
|
export type TeamsDocumentPageProps = {
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
22
apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx
Normal file
22
apps/web/src/app/(teams)/t/[teamUrl]/templates/[id]/page.tsx
Normal file
@ -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 <TemplatePageView params={params} team={team} />;
|
||||||
|
}
|
||||||
26
apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx
Normal file
26
apps/web/src/app/(teams)/t/[teamUrl]/templates/page.tsx
Normal file
@ -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 <TemplatesPageView searchParams={searchParams} team={team} />;
|
||||||
|
}
|
||||||
@ -52,9 +52,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex items-baseline gap-x-6">
|
<div className="flex items-baseline gap-x-6">
|
||||||
{navigationLinks
|
{navigationLinks.map(({ href, label }) => (
|
||||||
.filter(({ href }) => href !== '/templates' || rootHref === '') // Remove templates for team pages.
|
|
||||||
.map(({ href, label }) => (
|
|
||||||
<Link
|
<Link
|
||||||
key={href}
|
key={href}
|
||||||
href={`${rootHref}${href}`}
|
href={`${rootHref}${href}`}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { usePathname } from 'next/navigation';
|
|||||||
import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
|
import { CheckCircle2, ChevronsUpDown, Plus, Settings2 } from 'lucide-react';
|
||||||
import { signOut } from 'next-auth/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 { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
|
||||||
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
import type { GetTeamsResponse } from '@documenso/lib/server-only/team/get-teams';
|
||||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
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];
|
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 (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@ -100,7 +116,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
<DropdownMenuLabel>Personal</DropdownMenuLabel>
|
<DropdownMenuLabel>Personal</DropdownMenuLabel>
|
||||||
|
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/">
|
<Link href={formatRedirectUrlOnSwitch()}>
|
||||||
<AvatarWithText
|
<AvatarWithText
|
||||||
avatarFallback={formatAvatarFallback()}
|
avatarFallback={formatAvatarFallback()}
|
||||||
primaryText={user.name}
|
primaryText={user.name}
|
||||||
@ -152,7 +168,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
|
|
||||||
{teams.map((team) => (
|
{teams.map((team) => (
|
||||||
<DropdownMenuItem asChild key={team.id}>
|
<DropdownMenuItem asChild key={team.id}>
|
||||||
<Link href={`/t/${team.url}`}>
|
<Link href={formatRedirectUrlOnSwitch(team.url)}>
|
||||||
<AvatarWithText
|
<AvatarWithText
|
||||||
avatarFallback={formatAvatarFallback(team.name)}
|
avatarFallback={formatAvatarFallback(team.name)}
|
||||||
primaryText={team.name}
|
primaryText={team.name}
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
href: '/settings/profile',
|
href: '/settings/profile',
|
||||||
text: 'Settings',
|
text: 'Settings',
|
||||||
},
|
},
|
||||||
].filter(({ text, href }) => text !== 'Templates' || href === '/templates'); // Filter out templates for teams.
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||||
|
|||||||
205
packages/app-tests/e2e/templates/manage-templates.spec.ts
Normal file
205
packages/app-tests/e2e/templates/manage-templates.spec.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+$');
|
export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+$');
|
||||||
|
export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+');
|
||||||
|
|
||||||
export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, string> = {
|
export const TEAM_MEMBER_ROLE_MAP: Record<keyof typeof TeamMemberRole, string> = {
|
||||||
ADMIN: 'Admin',
|
ADMIN: 'Admin',
|
||||||
|
|||||||
@ -10,8 +10,21 @@ export const getFieldsForTemplate = async ({ templateId, userId }: GetFieldsForT
|
|||||||
where: {
|
where: {
|
||||||
templateId,
|
templateId,
|
||||||
Template: {
|
Template: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'asc',
|
id: 'asc',
|
||||||
|
|||||||
@ -27,8 +27,21 @@ export const setFieldsForTemplate = async ({
|
|||||||
const template = await prisma.template.findFirst({
|
const template = await prisma.template.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
|
|||||||
@ -13,8 +13,21 @@ export const getRecipientsForTemplate = async ({
|
|||||||
where: {
|
where: {
|
||||||
templateId,
|
templateId,
|
||||||
Template: {
|
Template: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
id: 'asc',
|
id: 'asc',
|
||||||
|
|||||||
@ -20,8 +20,21 @@ export const setRecipientsForTemplate = async ({
|
|||||||
const template = await prisma.template.findFirst({
|
const template = await prisma.template.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: templateId,
|
id: templateId,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!template) {
|
if (!template) {
|
||||||
|
|||||||
@ -11,7 +11,23 @@ export const createDocumentFromTemplate = async ({
|
|||||||
userId,
|
userId,
|
||||||
}: CreateDocumentFromTemplateOptions) => {
|
}: CreateDocumentFromTemplateOptions) => {
|
||||||
const template = await prisma.template.findUnique({
|
const template = await prisma.template.findUnique({
|
||||||
where: { id: templateId, userId },
|
where: {
|
||||||
|
id: templateId,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
Field: true,
|
Field: true,
|
||||||
@ -34,6 +50,7 @@ export const createDocumentFromTemplate = async ({
|
|||||||
const document = await prisma.document.create({
|
const document = await prisma.document.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
|
teamId: template.teamId,
|
||||||
title: template.title,
|
title: template.title,
|
||||||
documentDataId: documentData.id,
|
documentDataId: documentData.id,
|
||||||
Recipient: {
|
Recipient: {
|
||||||
|
|||||||
@ -1,20 +1,36 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
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 & {
|
export type CreateTemplateOptions = TCreateTemplateMutationSchema & {
|
||||||
userId: number;
|
userId: number;
|
||||||
|
teamId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTemplate = async ({
|
export const createTemplate = async ({
|
||||||
title,
|
title,
|
||||||
userId,
|
userId,
|
||||||
|
teamId,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId,
|
||||||
}: CreateTemplateOptions) => {
|
}: CreateTemplateOptions) => {
|
||||||
|
if (teamId) {
|
||||||
|
await prisma.team.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return await prisma.template.create({
|
return await prisma.template.create({
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
userId,
|
userId,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId,
|
||||||
|
teamId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,5 +8,23 @@ export type DeleteTemplateOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTemplate = async ({ id, userId }: 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,14 +1,39 @@
|
|||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import type { Prisma } from '@documenso/prisma/client';
|
||||||
import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
|
import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
|
||||||
|
|
||||||
export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
|
export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & {
|
||||||
userId: number;
|
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({
|
const template = await prisma.template.findUnique({
|
||||||
where: { id: templateId, userId },
|
where: templateWhereFilter,
|
||||||
include: {
|
include: {
|
||||||
Recipient: true,
|
Recipient: true,
|
||||||
Field: true,
|
Field: true,
|
||||||
@ -31,6 +56,7 @@ export const duplicateTemplate = async ({ templateId, userId }: DuplicateTemplat
|
|||||||
const duplicatedTemplate = await prisma.template.create({
|
const duplicatedTemplate = await prisma.template.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
|
teamId,
|
||||||
title: template.title + ' (copy)',
|
title: template.title + ' (copy)',
|
||||||
templateDocumentDataId: documentData.id,
|
templateDocumentDataId: documentData.id,
|
||||||
Recipient: {
|
Recipient: {
|
||||||
|
|||||||
56
packages/lib/server-only/template/find-templates.ts
Normal file
56
packages/lib/server-only/template/find-templates.ts
Normal file
@ -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),
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import type { Prisma } from '@documenso/prisma/client';
|
||||||
|
|
||||||
export interface GetTemplateByIdOptions {
|
export interface GetTemplateByIdOptions {
|
||||||
id: number;
|
id: number;
|
||||||
@ -6,11 +7,26 @@ export interface GetTemplateByIdOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getTemplateById = async ({ id, userId }: GetTemplateByIdOptions) => {
|
export const getTemplateById = async ({ id, userId }: GetTemplateByIdOptions) => {
|
||||||
return await prisma.template.findFirstOrThrow({
|
const whereFilter: Prisma.TemplateWhereInput = {
|
||||||
where: {
|
|
||||||
id,
|
id,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return await prisma.template.findFirstOrThrow({
|
||||||
|
where: whereFilter,
|
||||||
include: {
|
include: {
|
||||||
templateDocumentData: true,
|
templateDocumentData: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -12,6 +12,10 @@ export const formatDocumentsPath = (teamUrl?: string) => {
|
|||||||
return teamUrl ? `/t/${teamUrl}/documents` : '/documents';
|
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.
|
* Determines whether a team member can execute a given action.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -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;
|
||||||
@ -335,6 +335,7 @@ model Team {
|
|||||||
subscription Subscription?
|
subscription Subscription?
|
||||||
|
|
||||||
document Document[]
|
document Document[]
|
||||||
|
templates Template[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamPending {
|
model TeamPending {
|
||||||
@ -415,10 +416,12 @@ model Template {
|
|||||||
type TemplateType @default(PRIVATE)
|
type TemplateType @default(PRIVATE)
|
||||||
title String
|
title String
|
||||||
userId Int
|
userId Int
|
||||||
|
teamId Int?
|
||||||
templateDocumentDataId String
|
templateDocumentDataId String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||||
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)
|
templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade)
|
||||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
Recipient Recipient[]
|
Recipient Recipient[]
|
||||||
|
|||||||
36
packages/prisma/seed/templates.ts
Normal file
36
packages/prisma/seed/templates.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -39,7 +39,7 @@ export const fieldRouter = router({
|
|||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
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.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const recipientRouter = router({
|
|||||||
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
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({
|
throw new TRPCError({
|
||||||
code: 'BAD_REQUEST',
|
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.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -19,11 +19,12 @@ export const templateRouter = router({
|
|||||||
.input(ZCreateTemplateMutationSchema)
|
.input(ZCreateTemplateMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { title, templateDocumentDataId } = input;
|
const { teamId, title, templateDocumentDataId } = input;
|
||||||
|
|
||||||
return await createTemplate({
|
return await createTemplate({
|
||||||
title,
|
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
|
teamId,
|
||||||
|
title,
|
||||||
templateDocumentDataId,
|
templateDocumentDataId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -64,11 +65,12 @@ export const templateRouter = router({
|
|||||||
.input(ZDuplicateTemplateMutationSchema)
|
.input(ZDuplicateTemplateMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const { templateId } = input;
|
const { teamId, templateId } = input;
|
||||||
|
|
||||||
return await duplicateTemplate({
|
return await duplicateTemplate({
|
||||||
templateId,
|
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
|
teamId,
|
||||||
|
templateId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -88,7 +90,7 @@ export const templateRouter = router({
|
|||||||
|
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
|
|
||||||
return await deleteTemplate({ id, userId });
|
return await deleteTemplate({ userId, id });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const ZCreateTemplateMutationSchema = z.object({
|
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),
|
templateDocumentDataId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
|
|
||||||
export const ZDuplicateTemplateMutationSchema = z.object({
|
export const ZDuplicateTemplateMutationSchema = z.object({
|
||||||
templateId: z.number(),
|
templateId: z.number(),
|
||||||
|
teamId: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDeleteTemplateMutationSchema = z.object({
|
export const ZDeleteTemplateMutationSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user