From 0bdc261d2c99dc353623e09b1783979c2ca58500 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 24 Mar 2026 17:42:39 -0400 Subject: [PATCH 1/2] fix(policies): use backend endpoint for bulk policy PDF download The client-side PDF generator silently dropped TipTap node types it didn't handle (no default fallback), causing blank pages for policies with content wrapped in doc nodes or using codeBlock/blockquote/table. Switch to the existing backend endpoint which handles all node types correctly via recursive fallback. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../all/components/PolicyPageActions.test.tsx | 5 --- .../all/components/PolicyPageActions.tsx | 39 +++++++++---------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyPageActions.test.tsx b/apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyPageActions.test.tsx index 20a1dc08df..3846332b4a 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyPageActions.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyPageActions.test.tsx @@ -20,11 +20,6 @@ vi.mock('@/components/sheets/create-policy-sheet', () => ({ CreatePolicySheet: () =>
, })); -// Mock pdf-generator -vi.mock('@/lib/pdf-generator', () => ({ - downloadAllPolicies: vi.fn(), -})); - // Mock api client vi.mock('@/lib/api-client', () => ({ api: { get: vi.fn() }, diff --git a/apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyPageActions.tsx b/apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyPageActions.tsx index bcea5f082f..d3b64c3299 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyPageActions.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/all/components/PolicyPageActions.tsx @@ -3,18 +3,13 @@ import { CreatePolicySheet } from '@/components/sheets/create-policy-sheet'; import { api } from '@/lib/api-client'; import { Add, Download } from '@carbon/icons-react'; -import type { AuditLog, Member, Organization, Policy, User } from '@db'; +import type { Policy } from '@db'; import { Button, HStack } from '@trycompai/design-system'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { useState } from 'react'; +import { toast } from 'sonner'; import { usePermissions } from '@/hooks/use-permissions'; -type AuditLogWithRelations = AuditLog & { - user: User | null; - member: Member | null; - organization: Organization; -}; - interface PolicyPageActionsProps { policies: Policy[]; } @@ -29,21 +24,23 @@ export function PolicyPageActions({ policies }: PolicyPageActionsProps) { const handleDownloadAll = async () => { setIsDownloadingAll(true); try { - const logsEntries = await Promise.all( - policies.map(async (policy) => { - const res = await api.get<{ data: AuditLogWithRelations[] }>( - `/v1/audit-logs?entityType=policy&entityId=${policy.id}`, - ); - const allLogs = Array.isArray(res.data?.data) ? res.data.data : []; - const approvalLogs = allLogs.filter((log) => - log.description?.toLowerCase().includes('published'), - ); - return [policy.id, approvalLogs] as const; - }), + const res = await api.get<{ downloadUrl: string; name: string; policyCount: number }>( + '/v1/policies/download-all', ); - const policyLogs = Object.fromEntries(logsEntries); - const { downloadAllPolicies } = await import('@/lib/pdf-generator'); - await downloadAllPolicies(policies, policyLogs); + + if (res.error || !res.data?.downloadUrl) { + toast.error('Failed to generate PDF. Please try again.'); + return; + } + + const link = document.createElement('a'); + link.href = res.data.downloadUrl; + link.download = `${res.data.name ?? 'all-policies'}.pdf`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch { + toast.error('Failed to download policies.'); } finally { setIsDownloadingAll(false); } From 9d77b89c54a460dfe1ee82da5044a9c7ce0b2d69 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 24 Mar 2026 17:47:08 -0400 Subject: [PATCH 2/2] fix(policies): use content-disposition attachment for bulk PDF download Without Content-Disposition: attachment, cross-origin S3 presigned URLs open the PDF in-browser instead of triggering a file download. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/api/src/policies/policies.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/api/src/policies/policies.service.ts b/apps/api/src/policies/policies.service.ts index 47263ed59c..0b8d0ee1ba 100644 --- a/apps/api/src/policies/policies.service.ts +++ b/apps/api/src/policies/policies.service.ts @@ -1435,8 +1435,12 @@ export class PoliciesService { organizationId, ); + const fileName = `${organizationName} - All Policies.pdf`; const downloadUrl = - await this.attachmentsService.getPresignedDownloadUrl(key); + await this.attachmentsService.getPresignedDownloadUrlWithFilename( + key, + fileName, + ); this.logger.log( `Generated PDF bundle for organization ${organizationId} with ${policies.length} policies`,