Skip to content

Commit 0dd5850

Browse files
committed
fix(ui): preserve rich image pastes
1 parent 9813193 commit 0dd5850

4 files changed

Lines changed: 36 additions & 1 deletion

File tree

apps/sim/app/_shell/paste-admission-guard.test.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ let root: Root
2020
function dispatchPaste(
2121
target: Element,
2222
text: string,
23-
options: { selectionContext?: string; html?: string } = {}
23+
options: { selectionContext?: string; html?: string; imageFile?: boolean } = {}
2424
): Event {
2525
const event = new Event('paste', {
2626
bubbles: true,
@@ -35,6 +35,8 @@ function dispatchPaste(
3535
if (type === 'text/html') return options.html ?? ''
3636
return ''
3737
},
38+
files: options.imageFile ? [new File(['image'], 'pasted.png', { type: 'image/png' })] : [],
39+
items: options.imageFile ? [{ kind: 'file', type: 'image/png' }] : [],
3840
},
3941
})
4042
target.dispatchEvent(event)
@@ -140,4 +142,23 @@ describe('PasteAdmissionGuard', () => {
140142
true
141143
)
142144
})
145+
146+
it('lets an opted-in rich editor handle clipboard image files before text admission', () => {
147+
const editable = document.createElement('div')
148+
editable.setAttribute('contenteditable', 'true')
149+
editable.dataset.pasteMaxBytes = '4'
150+
editable.dataset.pasteMaxHtmlBytes = '4'
151+
editable.dataset.pasteHandlesImages = 'true'
152+
host.appendChild(editable)
153+
154+
const targetHandler = vi.fn()
155+
editable.addEventListener('paste', targetHandler)
156+
const event = dispatchPaste(editable, '12345', {
157+
html: '<img src="data:image/png;base64,large">',
158+
imageFile: true,
159+
})
160+
161+
expect(event.defaultPrevented).toBe(false)
162+
expect(targetHandler).toHaveBeenCalledOnce()
163+
})
143164
})

apps/sim/app/_shell/paste-admission-guard.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,20 @@ function finitePositiveAttribute(element: Element | null, name: string): number
1515
return Number.isFinite(value) && value > 0 ? value : undefined
1616
}
1717

18+
function clipboardHasImageFile(data: DataTransfer | null): boolean {
19+
if (!data) return false
20+
if (Array.from(data.files).some((file) => file.type.startsWith('image/'))) return true
21+
return Array.from(data.items).some(
22+
(item) => item.kind === 'file' && item.type.startsWith('image/')
23+
)
24+
}
25+
1826
/**
1927
* Last-resort admission for every editable workspace surface. Specialized editors publish their
2028
* downstream ceiling on an ancestor with `data-paste-max-bytes`; controls without one inherit a
2129
* crash-only fallback. This layer bounds only the clipboard payload, so a small paste into an already
2230
* large field keeps native behavior. Editors with a real result-size contract enforce it themselves.
31+
* Targets that explicitly handle clipboard images may claim those file events before the text guards.
2332
* The capture listener runs before React, ProseMirror, Monaco, and xterm parse the clipboard value.
2433
*/
2534
export function PasteAdmissionGuard() {
@@ -36,6 +45,9 @@ export function PasteAdmissionGuard() {
3645
const acceptsSelectionContext = event.target.closest('[data-paste-selection-context]')
3746
if (acceptsSelectionContext && readSelectionContextFromClipboard(event.clipboardData)) return
3847

48+
const handlesImageFiles = event.target.closest('[data-paste-handles-images="true"]')
49+
if (handlesImageFiles && clipboardHasImageFile(event.clipboardData)) return
50+
3951
const text = event.clipboardData?.getData('text/plain') ?? ''
4052
const policyElement = event.target.closest('[data-paste-max-bytes]')
4153
const maxPastedBytes =

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ export function LoadedRichMarkdownEditor({
557557
'data-owned-shortcuts': 'Mod+K',
558558
'data-paste-max-bytes': String(PASTE_LIMITS.RICH_MARKDOWN_BYTES),
559559
'data-paste-max-html-bytes': String(PASTE_LIMITS.RICH_MARKDOWN_BYTES),
560+
'data-paste-handles-images': 'true',
560561
},
561562
handleKeyDown: (_view, event) => {
562563
const isSaveShortcut = (event.metaKey || event.ctrlKey) && event.key?.toLowerCase() === 's'

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ function LoadedRichMarkdownField({
250250
'data-owned-shortcuts': 'Mod+K',
251251
'data-paste-max-bytes': String(PASTE_LIMITS.RICH_MARKDOWN_BYTES),
252252
'data-paste-max-html-bytes': String(PASTE_LIMITS.RICH_MARKDOWN_BYTES),
253+
'data-paste-handles-images': uploadImage ? 'true' : 'false',
253254
},
254255
handlePaste: (view, event) => {
255256
const images = uploadImageRef.current ? extractImageFiles(event.clipboardData) : []

0 commit comments

Comments
 (0)