feat: paste images from the clipboard wherever you can upload one - #1646
feat: paste images from the clipboard wherever you can upload one#1646doug-w wants to merge 1 commit into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (7)
Summary by CodeRabbit
WalkthroughAdds clipboard image pasting through shared utilities, a paste button, item and location attachment integration, same-origin clipboard permissions, localized messages, and unit/end-to-end coverage. ChangesClipboard Image Pasting
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant usePasteImage
participant ItemOrLocationEdit
participant AttachmentAPI
Browser->>usePasteImage: dispatch image paste
usePasteImage->>ItemOrLocationEdit: provide File[]
ItemOrLocationEdit->>AttachmentAPI: upload photo attachment
Browser->>ItemOrLocationEdit: click Paste Image
ItemOrLocationEdit->>Browser: read clipboard images
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/components/Form/PhotoUploader.vue`:
- Around line 8-24: Update the visual Button inside the file picker wrapper to
remove it from keyboard navigation, such as by setting tabindex="-1", while
keeping the overlaid file input as the only focusable control. Preserve the
existing click behavior and accessibility attributes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c055bd4e-717d-42d9-8869-aea2c66e7a08
📒 Files selected for processing (9)
backend/internal/web/mid/security.gofrontend/components/Form/PasteImageButton.vuefrontend/components/Form/PhotoUploader.vuefrontend/composables/use-paste-image.test.tsfrontend/composables/use-paste-image.tsfrontend/locales/en.jsonfrontend/pages/item/[id]/index/edit.vuefrontend/pages/location/[id]/index/edit.vuefrontend/test/e2e/paste-image.browser.spec.ts
Closes the request in discussion sysadminsmedia#721: grabbing a product photo off a web page meant saving it to disk and browsing back to it. Now every spot that takes a picture also takes a paste - the entity create modal, and the item and location attachment cards. Both routes in are covered - Ctrl/Cmd+V anywhere on the page or modal, and a Paste Image button for people not reaching for the keyboard. The paste listener only acts when the clipboard actually holds an image, so pasting text into a form field is untouched, and the button hides itself where the async clipboard API is missing. Clipboard images arrive named "image.png" or with no name at all, so they are renamed to pasted-image-<timestamp>.<ext> before upload to keep attachment names unique and correctly suffixed. The Permissions-Policy header sent clipboard-read=(), which blocked navigator.clipboard.read() outright; it is now granted to self. Browsers still require a user gesture and their own permission prompt, and cross-origin frames stay blocked.
ea7a620 to
fa564ae
Compare
What type of PR is this?
What this PR does / why we need it:
Lets you paste an image from the clipboard anywhere you can currently pick one from disk. This is the request from discussion #721 — copying a product photo off a web page today means "save image as…", find the file, browse back to it. As @crispybegs put it there, "downloading then uploading hundreds of images is a painful procedure."
Three spots gain it, which is every place Homebox takes a picture:
frontend/components/Form/PhotoUploader.vuefrontend/pages/item/[id]/index/edit.vuefrontend/pages/location/[id]/index/edit.vueTwo ways in, because neither alone is enough:
navigator.clipboard.read()is unavailable, leaving Ctrl+V as the path.File-by-file:
frontend/composables/use-paste-image.ts(new) —imageFilesFromClipboard()pulls images out of aDataTransfer(falling back from.filesto.items, which Safari needs);readClipboardImages()is the async-clipboard path for the button;usePasteImage()registers thepastelistener through VueUse'suseEventListener, so it unregisters on unmount and is naturally scoped to the mounted page or open modal.frontend/components/Form/PasteImageButton.vue(new) — shared button so the affordance is identical in all three spots, with toasts for the empty-clipboard and permission-denied cases.frontend/components/Form/PhotoUploader.vue— pastes feed the existingfilesToPhotoPreviews(), soEntity/CreateModal.vueneeded no change at all; preview, rotate, set-primary and the per-photo upload on submit all work as they already did.uploadAttachment(files, AttachmentTypes.Photo), matching what dropping on the "Photos"DropZonealready does. The item page's URL-drop handler is untouched.backend/internal/web/mid/security.go— see the note to reviewers below.frontend/locales/en.json— four new keys, English only.Two decisions worth calling out:
The paste listener only fires when the clipboard actually holds an image. A paste carrying only text falls straight through to the focused field, so typing into the item name or the markdown description behaves exactly as before. There's a test for this specifically.
Pasted images get renamed. Browsers hand clipboard images over as
image.png, or with no name at all, which would produce a pile of identically-named attachments. They becomepasted-image-<yyyymmdd-hhmmss>[-n].<ext>, with the extension derived from the MIME type.Scope note: #721 asks for "attachments or pictures" — this PR does images only. Clipboards essentially never hold a PDF or a spreadsheet, so wiring the non-image case seemed like dead code. Easy to extend if you'd rather it were broader.
Which issue(s) this PR fixes:
Implements the request in discussion #721.
Special notes for your reviewer:
There is one backend change, and it's a security header — please look at this closely.
SecurityHeaders()was sending:clipboard-read=()disables the API for everyone including same-origin, sonavigator.clipboard.read()was blocked outright and the Paste Image button could never have worked. I discovered this only because the button silently failed against a real build. It's nowclipboard-read=(self).What that does and does not give up:
Smaller things reviewers might want to weigh in on:
AttachmentTypes.Photofor pasted images on the edit pages, versus thenullthe file-picker uses. Photo seemed right for something that is by definition an image, and matches the "Photos" drop zone.PhotoUploader.vue'sopenFilePicker()is effectively a no-op —ui/input/Input.vuehas nodefineExpose, so therefis a restricted component proxy rather than the<input>element. The invisible overlaid input is what actually receives the click. I did not fix that, since it's pre-existing and out of scope, but I preserved the overlay structure carefully when adding the button beside it, and left a comment marking the trap.Testing
Unit —
frontend/composables/use-paste-image.test.ts, 8 tests: images kept and non-images dropped, empty payload, MIME→extension mapping, unique names across a multi-image paste, and the.itemsfallback path.E2E —
frontend/test/e2e/paste-image.browser.spec.ts, 5 tests, run against a Docker build of this branch:pasted-image-*.pngname andimage/pngMIME.Also run:
task swag --forceandtask typescript-types --force(both produce zero diff — no API surface changed),task ui:fix,pnpm run lint:ciclean,go build ./...clean.Two honest caveats:
task go:lintreports 26 pre-existing issues (tagalign×24,testifylint×2) inrepo_entity_templates.go,repo_entities.goandservice_user_login_test.go. None are in the file I touched and all are present onmain— I left them alone rather than mix unrelated fixes into this PR.ClipboardEventwith an empty copy of theDataTransferit's handed, so a synthetic paste can't carry a file there. That's a limit on what the harness can fake, not on the feature — real Firefox pastes are unaffected. The spec detects this at runtime rather than hardcoding browser names. Chromium is fully covered; I have not empirically verified Firefox or WebKit.mode: "serial". Concurrent attachment writes against SQLite hitSQLITE_BUSYunder parallel workers, which is the same reasonpnpm test:ciuses--no-file-parallelism.