Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions src/blocks/Document/Component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,46 @@ import { getMediaURL } from '@/utilities/getURL'
import { isValidRelationship } from '@/utilities/relationships'
import { getHostnameFromTenant } from '@/utilities/tenancy/getHostnameFromTenant'
import { cn } from '@/utilities/ui'
import { FileDown } from 'lucide-react'

type Props = DocumentBlockProps & {
isLayoutBlock: boolean
// layout is present in the block config but absent from generated types until pnpm generate:types is run
layout?: 'download' | 'embed' | null
}

export const DocumentBlockComponent = (props: Props) => {
const { document, isLayoutBlock = true } = props
const { document, layout, isLayoutBlock = true } = props
const { tenant } = useTenant()

if (!isValidRelationship(document) || !document.url) {
return null
}

const src = getMediaURL(document.url, null, getHostnameFromTenant(tenant))
const filename = document.filename ?? 'Download'

// Treat missing layout as 'embed' so existing blocks keep their current behavior
const resolvedLayout = layout ?? 'embed'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a good assumption to make but I think we should fallback to download due to the browser behavior that might result in a file being automatically downloaded if the browser can't render it in an iframe. See my other comment.

We only have one page with DocumentBlocks in production, so this would be easy enough to go and update manually after we deploy this.


if (resolvedLayout === 'download') {
return (
<div className={cn('my-4', { container: isLayoutBlock })}>
<a
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clicking this triggers the nextjs-toploader loader:

Image

If you add target="_blank" we can avoid that.

href={src}
download={filename}
className="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium hover:bg-accent transition-colors"
>
<FileDown className="h-4 w-4 shrink-0" />
<span>{filename}</span>
</a>
</div>
)
}

return (
<div className={cn('my-4', { container: isLayoutBlock })}>
<iframe src={src} width="100%" height="600px" title="Document PDF" />
<iframe src={src} width="100%" height="600px" title="Document" />
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is no browser viewer for a given file type and the layout is set to embed, the file is automatically downloaded when visiting the page that the DocumentBlock is on:

Image

I can't find the behavior documented anywhere but it seems that if we set an iframe's src attribute to a file with a MIME type that it does not have a built-in viewer for, the file is automatically downloaded when the iframe is rendered. We definitely don't want this behavior so we need to account for this.

It seems that PDF, HTML, Plain text, and XML/KML can all be rendered in an iframe. So we need conditional logic in the collection that only allows setting the displayAs field to embed when the file's MIME type is one of ['application/pdf', 'text/html', 'text/plain', 'text/xml', 'application/xml', 'application/vnd.google-earth.kml+xml', '.kml']. If we discover there are other MIME types that browsers can reliably render, we could add to that list.

</div>
)
}
9 changes: 9 additions & 0 deletions src/blocks/Document/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,14 @@ export const DocumentBlock: Block = {
relationTo: 'documents',
required: true,
},
{
name: 'layout',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name this displayAs instead? I think that might be clearer to editors.

type: 'select',
defaultValue: 'download',
options: [
{ label: 'Download Link', value: 'download' },
{ label: 'Embed (iframe)', value: 'embed' },
],
},
],
}
23 changes: 23 additions & 0 deletions src/collections/Documents/hooks/validateNotImageOrVideo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { APIError, CollectionConfig } from 'payload'

type BeforeOperationHook = Exclude<
Exclude<CollectionConfig['hooks'], undefined>['beforeOperation'],
undefined
>[number]

export const validateNotImageOrVideo: BeforeOperationHook = ({ operation, req }) => {
if ((operation !== 'create' && operation !== 'update') || !req.file) {
return
}

const { mimetype } = req.file

if (mimetype.startsWith('image/') || mimetype.startsWith('video/')) {
throw new APIError(
'Images and videos must be uploaded to the Media collection, not Documents.',
400,
null,
true,
)
}
}
12 changes: 2 additions & 10 deletions src/collections/Documents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import path from 'path'
import { fileURLToPath } from 'url'
import { prefixFilenameWithTenant } from '../Media/hooks/prefixFilenameWithTenant'
import { revalidateDocuments, revalidateDocumentsDelete } from './hooks/revalidateDocuments'
import { validateNotImageOrVideo } from './hooks/validateNotImageOrVideo'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
Expand Down Expand Up @@ -38,18 +39,9 @@ export const Documents: CollectionConfig = {
],
upload: {
staticDir: path.resolve(dirname, '../../../public/documents'),
mimeTypes: [
'application/pdf',
'text/x-php',
'text/php',
'application/xml',
'application/octet-stream',
'application/vnd.google-earth.kml+xml',
'.kml',
],
},
hooks: {
beforeOperation: [prefixFilenameWithTenant],
beforeOperation: [validateNotImageOrVideo, prefixFilenameWithTenant],
afterChange: [revalidateDocuments],
afterDelete: [revalidateDocumentsDelete],
},
Expand Down
Loading
Loading