Skip to content

feat: paste images from the clipboard wherever you can upload one - #1646

Open
doug-w wants to merge 1 commit into
sysadminsmedia:mainfrom
doug-w:feat/paste-image-upload
Open

feat: paste images from the clipboard wherever you can upload one#1646
doug-w wants to merge 1 commit into
sysadminsmedia:mainfrom
doug-w:feat/paste-image-upload

Conversation

@doug-w

@doug-w doug-w commented Jul 29, 2026

Copy link
Copy Markdown

What type of PR is this?

  • feature

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:

Spot File
Entity create modal (items + locations) frontend/components/Form/PhotoUploader.vue
Item edit → Attachments card frontend/pages/item/[id]/index/edit.vue
Location edit → Attachments card frontend/pages/location/[id]/index/edit.vue

Two ways in, because neither alone is enough:

  • Ctrl/Cmd+V anywhere on the page or in the modal.
  • A "Paste Image" button, for discoverability and for anyone not reaching for the keyboard. It hides itself where 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 a DataTransfer (falling back from .files to .items, which Safari needs); readClipboardImages() is the async-clipboard path for the button; usePasteImage() registers the paste listener through VueUse's useEventListener, 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 existing filesToPhotoPreviews(), so Entity/CreateModal.vue needed no change at all; preview, rotate, set-primary and the per-photo upload on submit all work as they already did.
  • The two edit pages — pastes feed the existing uploadAttachment(files, AttachmentTypes.Photo), matching what dropping on the "Photos" DropZone already 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 become pasted-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:

Permissions-Policy: … clipboard-read=(), clipboard-write=(self) …

clipboard-read=() disables the API for everyone including same-origin, so navigator.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 now clipboard-read=(self).

What that does and does not give up:

  • Cross-origin frames stay blocked, exactly as before.
  • The browser still requires a user gesture, and still shows its own permission prompt, before any read succeeds.
  • Same-origin JS could now read the clipboard on user interaction — that is the point of the feature, and it's the same permission the paste event already grants implicitly.
  • Ctrl+V never depended on this. If you'd rather not relax the header at all, dropping this one line still leaves the keyboard path fully working; the button would just hide itself everywhere. I'd understand that call and it's a clean revert.

Smaller things reviewers might want to weigh in on:

  • AttachmentTypes.Photo for pasted images on the edit pages, versus the null the file-picker uses. Photo seemed right for something that is by definition an image, and matches the "Photos" drop zone.
  • I noticed while working here that PhotoUploader.vue's openFilePicker() is effectively a no-op — ui/input/Input.vue has no defineExpose, so the ref is 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

Unitfrontend/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 .items fallback path.

pnpm exec vitest --run --config ./test/vitest.config.ts composables/use-paste-image.test.ts

E2Efrontend/test/e2e/paste-image.browser.spec.ts, 5 tests, run against a Docker build of this branch:

  1. Paste into the create modal → preview appears → item is created → the attachment really lands, with the expected pasted-image-*.png name and image/png MIME.
  2. Paste on the item edit page → toast → attachment count goes 0 → 1, confirmed via the API.
  3. Same on the location edit page.
  4. The Paste Image button, driving the real async clipboard.
  5. A real Ctrl+V of text into the item name field still fills the field — the listener doesn't swallow it.
E2E_BASE_URL=http://localhost:7745 pnpm exec playwright test -c ./test/playwright.config.ts paste-image

Also run: task swag --force and task typescript-types --force (both produce zero diff — no API surface changed), task ui:fix, pnpm run lint:ci clean, go build ./... clean.

Two honest caveats:

  • task go:lint reports 26 pre-existing issues (tagalign ×24, testifylint ×2) in repo_entity_templates.go, repo_entities.go and service_user_login_test.go. None are in the file I touched and all are present on main — I left them alone rather than mix unrelated fixes into this PR.
  • The E2E tests skip on Firefox. Firefox constructs a ClipboardEvent with an empty copy of the DataTransfer it'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.
  • The spec runs mode: "serial". Concurrent attachment writes against SQLite hit SQLITE_BUSY under parallel workers, which is the same reason pnpm test:ci uses --no-file-parallelism.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c5dad98-fa13-4ad7-917f-6439bb0b543f

📥 Commits

Reviewing files that changed from the base of the PR and between ea7a620 and fa564ae.

📒 Files selected for processing (9)
  • backend/internal/web/mid/security.go
  • frontend/components/Form/PasteImageButton.vue
  • frontend/components/Form/PhotoUploader.vue
  • frontend/composables/use-paste-image.test.ts
  • frontend/composables/use-paste-image.ts
  • frontend/locales/en.json
  • frontend/pages/item/[id]/index/edit.vue
  • frontend/pages/location/[id]/index/edit.vue
  • frontend/test/e2e/paste-image.browser.spec.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • frontend/pages/item/[id]/index/edit.vue
  • frontend/components/Form/PhotoUploader.vue
  • frontend/composables/use-paste-image.test.ts
  • frontend/composables/use-paste-image.ts
  • frontend/pages/location/[id]/index/edit.vue
  • frontend/test/e2e/paste-image.browser.spec.ts
  • frontend/components/Form/PasteImageButton.vue

Summary by CodeRabbit

  • New Features
    • Added clipboard image pasting support via Ctrl/Cmd+V, including a new Paste Image button in attachment areas.
    • Item and location attachment flows now accept pasted images as photo uploads and show matching preview/confirmation.
    • Updated attachment prompts plus new localized clipboard error messages.
  • Bug Fixes
    • Ensures pasted images are detected correctly from clipboard data (while preserving normal text paste behavior).
  • Tests
    • Added automated coverage for clipboard image pasting, filename behavior, uploads, and browser compatibility.

Walkthrough

Adds 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.

Changes

Clipboard Image Pasting

Layer / File(s) Summary
Clipboard extraction and permissions
frontend/composables/use-paste-image.ts, backend/internal/web/mid/security.go
Clipboard images are extracted, renamed, read through the Clipboard API, and handled without intercepting text pastes; same-origin clipboard reads are permitted.
Shared paste UI and uploader integration
frontend/components/Form/PasteImageButton.vue, frontend/components/Form/PhotoUploader.vue, frontend/locales/en.json
A localized paste button and shared image-processing flow support clipboard images alongside file selection.
Item and location attachment uploads
frontend/pages/item/..., frontend/pages/location/...
Pasted images are uploaded as photo attachments, with updated attachment prompts and controls.
Clipboard behavior validation
frontend/composables/use-paste-image.test.ts, frontend/test/e2e/paste-image.browser.spec.ts
Unit and browser tests cover extraction, naming, uploads, clipboard permissions, and ordinary text pastes.

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
Loading

Possibly related PRs

Suggested reviewers: tankerkiller125

Poem

Images take flight from clipboard bright,
Through buttons, paste events, and upload light.
Text stays safe in its field,
While photo attachments yield—
A tiny pixel joins the site! 📋✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding clipboard image pasting for upload flows.
Description check ✅ Passed The description covers the required sections with a clear feature summary, rationale, reviewer notes, and testing details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
✨ Simplify code
  • Create PR with simplified 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from tankerkiller125 July 29, 2026 05:39
@coderabbitai coderabbitai Bot added the go Pull requests that update Go code label Jul 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9d6c930 and ea7a620.

📒 Files selected for processing (9)
  • backend/internal/web/mid/security.go
  • frontend/components/Form/PasteImageButton.vue
  • frontend/components/Form/PhotoUploader.vue
  • frontend/composables/use-paste-image.test.ts
  • frontend/composables/use-paste-image.ts
  • frontend/locales/en.json
  • frontend/pages/item/[id]/index/edit.vue
  • frontend/pages/location/[id]/index/edit.vue
  • frontend/test/e2e/paste-image.browser.spec.ts

Comment thread frontend/components/Form/PhotoUploader.vue
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.
@doug-w
doug-w force-pushed the feat/paste-image-upload branch from ea7a620 to fa564ae Compare July 29, 2026 06:48
@coderabbitai coderabbitai Bot removed the go Pull requests that update Go code label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant