Skip to content

Generalise the media picker: upload video / PDF, not just images #239

Description

@djay

Problem

You cannot upload a video, or a PDF, from anywhere in the editor — including on mobile,
where there is no other route to get a file onto the site.

The data-edit-media tag is not the limitation. Hydra's getMediaFields() is entirely
type-agnostic: it collects named elements with a rect and reports them. Everything that
constrains the flow to images is admin-side.

What is actually hardcoded

Site Hardcoded
Iframe/OpenObjectBrowser.jsx:66 mode: 'image' for the field-media click path
customizations/.../LinkButton/AddLinkForm.jsx:324 upload button gated on objectBrowserPickerType === 'image'
customizations/.../LinkButton/AddLinkForm.jsx:329 accept="image/*" on the file input
customizations/.../Widgets/ImageWidget.jsx:393,434 accept="image/*" on both Dropzones
customizations/.../Widgets/ImageWidget.jsx:203 '@type': 'Image', bytes under an image key

Note AddLinkForm — the picker — already renders the file input and calls an injected
this.props.onFileUpload(file). ImageWidget merely supplies that handler. So the picker is
already the shared surface across the sidebar widget, the link editor, and the iframe media
tap. It just only ever gets handed an image-shaped upload.

That is why the fix belongs in the picker, not in ImageWidget. ImageWidget is the widget
for an image field and should stay image-specific. And generalising it would not help the
link case at all: linking to a PDF goes through AddLinkForm, never through ImageWidget.

Why the PDF case forces the design

Uploading a PDF as '@type': 'Image' would be rejected or produce a broken object. The
portal_type and the bytes key both have to vary with the mime type.

Volto already has exactly this logic, in ContentsUploadModal.jsx:122-131:

const image = fields[1].split('/')[0] === 'image';
return {
  '@type': image ? 'Image' : 'File',
  title: file.name,
  [image ? 'image' : 'file']: { data, encoding, 'content-type': , filename },
};

And manage/Widgets/FileWidget.jsx already takes an accept prop and validates against
it ("File is not of the accepted type {accept}"). So "the widget declares what it accepts"
is an existing Volto pattern, not an invention.

validateFileUploadSize is mime-agnostic (size only). restrictFileUpload is just a boolean
that disables drag/upload. Neither blocks this.

Proposal

One upload-capable picker that takes two things from its caller, both derived from the schema:

  1. accept — mime filter for the file input (image/*, video/*, unset = anything).
  2. target content typeImage for image/*, File otherwise; bytes under image /
    file accordingly. Reuse the ContentsUploadModal rule rather than reinventing it.

Then each entry point derives accept from the field's declared widget instead of asserting it:

  • sidebar image field → image/* → creates Image
  • video block urlvideo/* → creates File
  • link editor → unrestricted → creates File ← the PDF case
  • hydra data-edit-media tap → resolves the field's widget from blockPathMap, replacing
    View.jsx's hardcoded mode: 'image'

This keeps the chrome pattern intact (docs/architecture.md): the frontend's tag says which
field
, the schema says what it accepts, the admin renders the picker. The tag follows the
widget.

Bonus: the video block already plays uploaded files

core/.../Blocks/Video/Body.jsx:142-152 renders a native <video> whose src is
isInternalURL(data.url) ? '…/@@download/file' : data.url. So the render side is done —
only the "get a File into data.url" half is missing.

Two gotchas for whoever picks this up:

  • That player is gated on data.url.match('.mp4'), so a .webm/.mov upload renders
    nothing. The accept list and that check must agree.
  • Volto's video block schema declares url: { widget: 'url' } (a plain text input) and its
    Edit placeholder says "Youtube, Vimeo, Peertube" — it was designed as an external-embed
    block. Giving it a media widget is a deliberate behaviour change, not a bug fix.

Acceptance criteria

  • Upload a video from a phone into a video block; it plays from Plone (@@download/file).
  • Upload a PDF from the link editor and link to it.
  • Image upload still creates '@type': 'Image' and is unchanged in the sidebar.
  • A field declaring accept="video/*" rejects a PDF in the file dialog.
  • View.jsx no longer hardcodes mode: 'image'.

Notes

  • ImageWidget and AddLinkForm are already shadows in this repo
    (packages/volto-hydra/src/customizations/), so this touches our shadows, not core/
    (which is git-ignored).
  • Separately, View.jsx:4708 builds a NamedBlobImage value shape for page-level image
    fields. Different concern, but it's the third place that assumes "Image".

Researched in the mobile-editing session; filed to be done as its own PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions