diff --git a/apps/backoffice/src/app/features/certificate-designer/hooks/useTemplatePreview.ts b/apps/backoffice/src/app/features/certificate-designer/hooks/useTemplatePreview.ts index 761a89e15..c2714f856 100644 --- a/apps/backoffice/src/app/features/certificate-designer/hooks/useTemplatePreview.ts +++ b/apps/backoffice/src/app/features/certificate-designer/hooks/useTemplatePreview.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { notifications } from '@mantine/notifications'; import { useTranslation } from 'react-i18next'; import { extractErrorMessage } from '@ema-platform/api'; @@ -13,12 +13,14 @@ interface PreviewArgs { /** * Renders the editor's current contents, not the saved row, so unsaved edits - * are what you see. Opened as a blob so it never leaves a file behind. + * are what you see. Opened as a blob into `PdfPreviewModal` rather than a new + * tab, so the designer never loses their place. */ export function useTemplatePreview() { const { t } = useTranslation(); + const [previewUrl, setPreviewUrl] = useState(null); - return useCallback( + const open = useCallback( async ({ hbsSource, licenseTypeId, landscape }: PreviewArgs) => { try { // The preview returns a PDF stream, not JSON, so it bypasses RTK Query @@ -38,10 +40,7 @@ export function useTemplatePreview() { }), }); if (!response.ok) throw new Error(await response.text()); - const url = URL.createObjectURL(await response.blob()); - window.open(url, '_blank', 'noopener'); - // Give the new tab time to read it before revoking. - setTimeout(() => URL.revokeObjectURL(url), 60_000); + setPreviewUrl(URL.createObjectURL(await response.blob())); } catch (err) { notifications.show({ color: 'red', @@ -52,4 +51,13 @@ export function useTemplatePreview() { }, [t], ); + + const close = useCallback(() => { + setPreviewUrl((current) => { + if (current) URL.revokeObjectURL(current); + return null; + }); + }, []); + + return { previewUrl, open, close }; } diff --git a/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx b/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx index 7f49af593..d30aa5d2f 100644 --- a/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx +++ b/apps/backoffice/src/app/features/certificate-designer/pages/CertificateDesignerPage.tsx @@ -31,7 +31,7 @@ import { useUpdateLicenseValidityMutation, useUpdateLicenseTemplateMutation, } from '@ema-platform/api'; -import { EmptyState, ErrorState, PageHeader } from '@ema-platform/ui'; +import { EmptyState, ErrorState, PageHeader, PdfPreviewModal } from '@ema-platform/ui'; import { usePermissions, LICENSE_PERMISSIONS as PERMISSIONS } from '@ema-platform/auth'; import { BlockPropertiesPanel } from '../components/BlockPropertiesPanel'; import { DesignerToolbar } from '../components/DesignerToolbar'; @@ -86,7 +86,7 @@ export function CertificateDesignerPage() { const draft = useTemplateDraft(templates); const run = useDesignerActions(); - const openPreview = useTemplatePreview(); + const { previewUrl, open: openPreview, close: closePreview } = useTemplatePreview(); const [newOpen, setNewOpen] = useState(false); const [newName, setNewName] = useState(''); @@ -377,6 +377,13 @@ export function CertificateDesignerPage() { }, t('designer.created', 'Draft created')) } /> + + ); } diff --git a/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/index.tsx b/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/index.tsx index 039c7a350..d4bc9e434 100644 --- a/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/index.tsx +++ b/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/index.tsx @@ -23,7 +23,12 @@ import { IconPaperclip, IconStethoscope, } from '@tabler/icons-react'; -import { AdvancedTable, notify, type AdvancedColumn } from '@ema-platform/ui'; +import { + AdvancedTable, + notify, + PdfPreviewModal, + type AdvancedColumn, +} from '@ema-platform/ui'; import { useDateDisplayer } from '@ema-platform/shared'; import { extractErrorMessage, @@ -109,6 +114,9 @@ function AttachmentsModal({ { ownerType, ownerId: ownerId ?? '' }, { skip: !ownerId }, ); + const [preview, setPreview] = useState<{ url: string; title: string } | null>( + null, + ); const files = useMemo( () => (attachments ?? []).flatMap((a) => a.files ?? []), @@ -156,10 +164,9 @@ function AttachmentsModal({ size="compact-xs" variant="light" leftSection={} - component="a" - href={file.url} - target="_blank" - rel="noopener noreferrer" + onClick={() => + setPreview({ url: file.url as string, title: file.originalName }) + } > {t('recordVerification.view', 'View')} @@ -173,6 +180,12 @@ function AttachmentsModal({ )) )} + setPreview(null)} + url={preview?.url ?? ''} + title={preview?.title} + /> ); } diff --git a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx index 8b3eac530..9b7295f94 100644 --- a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx +++ b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx @@ -8,7 +8,6 @@ import { Divider, Group, Loader, - Modal, Paper, SimpleGrid, Stack, @@ -36,6 +35,7 @@ import { useGetMySeaServiceRecordsQuery, useGetMyMedicalCertificatesQuery, } from '@ema-platform/api'; +import { PdfPreviewModal } from '@ema-platform/ui'; // --------------------------------------------------------------------------- // Mock data @@ -353,21 +353,12 @@ export function CertificatesPage() { )} - {/* Preview modal */} - setPreviewUrl(null)} - title={{previewTitle}} - size="95vw" - radius="lg" - fullScreen - > -