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 315c2022b..2b4576eb3 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
@@ -4,7 +4,7 @@ import { useState } from 'react';
import Link from 'next/link';
-import { Copy, Edit, MoreHorizontal, Share2Icon, Trash2 } from 'lucide-react';
+import { Copy, Edit, MoreHorizontal, MoveRight, Share2Icon, Trash2 } from 'lucide-react';
import { useSession } from 'next-auth/react';
import { type FindTemplateRow } from '@documenso/lib/server-only/template/find-templates';
@@ -18,6 +18,7 @@ import {
import { DeleteTemplateDialog } from './delete-template-dialog';
import { DuplicateTemplateDialog } from './duplicate-template-dialog';
+import { MoveTemplateDialog } from './move-template-dialog';
import { TemplateDirectLinkDialog } from './template-direct-link-dialog';
export type DataTableActionDropdownProps = {
@@ -36,6 +37,7 @@ export const DataTableActionDropdown = ({
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isTemplateDirectLinkDialogOpen, setTemplateDirectLinkDialogOpen] = useState(false);
const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
+ const [isMoveDialogOpen, setMoveDialogOpen] = useState(false);
if (!session) {
return null;
@@ -73,6 +75,13 @@ export const DataTableActionDropdown = ({
Direct link
+ {!teamId && (
+ setMoveDialogOpen(true)}>
+
+ Move to Team
+
+ )}
+
setDeleteDialogOpen(true)}
@@ -95,6 +104,12 @@ export const DataTableActionDropdown = ({
onOpenChange={setTemplateDirectLinkDialogOpen}
/>
+
+
void;
+};
+
+export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTemplateDialogProps) => {
+ const router = useRouter();
+ const { toast } = useToast();
+ const [selectedTeamId, setSelectedTeamId] = useState(null);
+
+ const { data: teams, isLoading: isLoadingTeams } = trpc.team.getTeams.useQuery();
+ const { mutateAsync: moveTemplate, isLoading } = trpc.template.moveTemplateToTeam.useMutation({
+ onSuccess: () => {
+ router.refresh();
+ toast({
+ title: 'Template moved',
+ description: 'The template has been successfully moved to the selected team.',
+ duration: 5000,
+ });
+ onOpenChange(false);
+ },
+ onError: (error) => {
+ toast({
+ title: 'Error',
+ description: error.message || 'An error occurred while moving the template.',
+ variant: 'destructive',
+ duration: 7500,
+ });
+ },
+ });
+
+ const onMove = async () => {
+ if (!selectedTeamId) {
+ return;
+ }
+
+ await moveTemplate({ templateId, teamId: selectedTeamId });
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/lib/server-only/template/move-template-to-team.ts b/packages/lib/server-only/template/move-template-to-team.ts
new file mode 100644
index 000000000..522aa5e9b
--- /dev/null
+++ b/packages/lib/server-only/template/move-template-to-team.ts
@@ -0,0 +1,57 @@
+import { TRPCError } from '@trpc/server';
+
+import { prisma } from '@documenso/prisma';
+
+export type MoveTemplateToTeamOptions = {
+ templateId: number;
+ teamId: number;
+ userId: number;
+};
+
+export const moveTemplateToTeam = async ({
+ templateId,
+ teamId,
+ userId,
+}: MoveTemplateToTeamOptions) => {
+ return await prisma.$transaction(async (tx) => {
+ const template = await tx.template.findFirst({
+ where: {
+ id: templateId,
+ userId,
+ teamId: null,
+ },
+ });
+
+ if (!template) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'Template not found or already associated with a team.',
+ });
+ }
+
+ const team = await tx.team.findFirst({
+ where: {
+ id: teamId,
+ members: {
+ some: {
+ userId,
+ },
+ },
+ },
+ });
+
+ if (!team) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message: 'You are not a member of this team.',
+ });
+ }
+
+ const updatedTemplate = await tx.template.update({
+ where: { id: templateId },
+ data: { teamId },
+ });
+
+ return updatedTemplate;
+ });
+};
diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts
index 0736dfb34..f4a76a6f5 100644
--- a/packages/trpc/server/template-router/router.ts
+++ b/packages/trpc/server/template-router/router.ts
@@ -12,6 +12,7 @@ import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/de
import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateWithDetailsById } from '@documenso/lib/server-only/template/get-template-with-details-by-id';
+import { moveTemplateToTeam } from '@documenso/lib/server-only/template/move-template-to-team';
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
import { updateTemplateSettings } from '@documenso/lib/server-only/template/update-template-settings';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
@@ -28,6 +29,7 @@ import {
ZDuplicateTemplateMutationSchema,
ZFindTemplatesQuerySchema,
ZGetTemplateWithDetailsByIdQuerySchema,
+ ZMoveTemplatesToTeamSchema,
ZToggleTemplateDirectLinkMutationSchema,
ZUpdateTemplateSettingsMutationSchema,
} from './schema';
@@ -296,4 +298,30 @@ export const templateRouter = router({
throw AppError.parseErrorToTRPCError(error);
}
}),
+
+ moveTemplateToTeam: authenticatedProcedure
+ .input(ZMoveTemplatesToTeamSchema)
+ .mutation(async ({ input, ctx }) => {
+ try {
+ const { templateId, teamId } = input;
+ const userId = ctx.user.id;
+
+ return await moveTemplateToTeam({
+ templateId,
+ teamId,
+ userId,
+ });
+ } catch (err) {
+ console.error(err);
+
+ if (err instanceof TRPCError) {
+ throw err;
+ }
+
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to move this template. Please try again later.',
+ });
+ }
+ }),
});
diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts
index 2e81f822e..80280b406 100644
--- a/packages/trpc/server/template-router/schema.ts
+++ b/packages/trpc/server/template-router/schema.ts
@@ -110,6 +110,11 @@ export const ZGetTemplateWithDetailsByIdQuerySchema = z.object({
id: z.number().min(1),
});
+export const ZMoveTemplatesToTeamSchema = z.object({
+ templateId: z.number(),
+ teamId: z.number(),
+});
+
export type TCreateTemplateMutationSchema = z.infer;
export type TCreateDocumentFromTemplateMutationSchema = z.infer<
typeof ZCreateDocumentFromTemplateMutationSchema
@@ -119,3 +124,4 @@ export type TDeleteTemplateMutationSchema = z.infer;
+export type TMoveTemplatesToSchema = z.infer;