diff --git a/modules/structs/attachment.go b/modules/structs/attachment.go index 0a3d4140c2..746f618cf0 100644 --- a/modules/structs/attachment.go +++ b/modules/structs/attachment.go @@ -22,6 +22,12 @@ type Attachment struct { Type string `json:"type"` } +// WebAttachment the generic attachment with mime type +type WebAttachment struct { + *Attachment + MimeType string `json:"mime_type"` +} + // EditAttachmentOptions options for editing attachments // swagger:model type EditAttachmentOptions struct { diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a34e3b7c78..a4f6f97a05 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -3593,9 +3593,9 @@ func GetIssueAttachments(ctx *context.Context) { if ctx.Written() { return } - attachments := make([]*api.Attachment, len(issue.Attachments)) + attachments := make([]*api.WebAttachment, len(issue.Attachments)) for i := 0; i < len(issue.Attachments); i++ { - attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i]) + attachments[i] = convert.ToWebAttachment(ctx.Repo.Repository, issue.Attachments[i]) } ctx.JSON(http.StatusOK, attachments) } @@ -3628,13 +3628,13 @@ func GetCommentAttachments(ctx *context.Context) { return } - attachments := make([]*api.Attachment, 0) if err := comment.LoadAttachments(ctx); err != nil { ctx.ServerError("LoadAttachments", err) return } + attachments := make([]*api.WebAttachment, len(comment.Attachments)) for i := 0; i < len(comment.Attachments); i++ { - attachments = append(attachments, convert.ToAttachment(ctx.Repo.Repository, comment.Attachments[i])) + attachments[i] = convert.ToWebAttachment(ctx.Repo.Repository, comment.Attachments[i]) } ctx.JSON(http.StatusOK, attachments) } diff --git a/services/convert/attachment.go b/services/convert/attachment.go index 6617aac906..74ae7c509c 100644 --- a/services/convert/attachment.go +++ b/services/convert/attachment.go @@ -4,6 +4,9 @@ package convert import ( + "mime" + "path/filepath" + repo_model "forgejo.org/models/repo" api "forgejo.org/modules/structs" ) @@ -20,9 +23,13 @@ func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachm return attach.DownloadURL() } -// ToAttachment converts models.Attachment to api.Attachment for API usage -func ToAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.Attachment { - return toAttachment(repo, a, WebAssetDownloadURL) +// ToWebAttachment converts models.Attachment to api.WebAttachment for API usage +func ToWebAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.WebAttachment { + attachment := toAttachment(repo, a, WebAssetDownloadURL) + return &api.WebAttachment{ + Attachment: attachment, + MimeType: mime.TypeByExtension(filepath.Ext(attachment.Name)), + } } // ToAPIAttachment converts models.Attachment to api.Attachment for API usage diff --git a/services/convert/attachment_test.go b/services/convert/attachment_test.go new file mode 100644 index 0000000000..d7bf0c1ee7 --- /dev/null +++ b/services/convert/attachment_test.go @@ -0,0 +1,56 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package convert + +import ( + "fmt" + "testing" + "time" + + repo_model "forgejo.org/models/repo" + "forgejo.org/models/unittest" + "forgejo.org/modules/setting" + api "forgejo.org/modules/structs" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToWebAttachment(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + attachment := &repo_model.Attachment{ + ID: 10, + UUID: "uuidxxx", + RepoID: 1, + IssueID: 1, + ReleaseID: 0, + UploaderID: 0, + CommentID: 0, + Name: "test.png", + DownloadCount: 90, + Size: 30, + NoAutoTime: false, + CreatedUnix: 9342, + CustomDownloadURL: "", + ExternalURL: "", + } + + webAttachment := ToWebAttachment(headRepo, attachment) + + assert.NotNil(t, webAttachment) + assert.Equal(t, &api.WebAttachment{ + Attachment: &api.Attachment{ + ID: 10, + Name: "test.png", + Created: time.Unix(9342, 0), + DownloadCount: 90, + Size: 30, + UUID: "uuidxxx", + DownloadURL: fmt.Sprintf("%sattachments/uuidxxx", setting.AppURL), + Type: "attachment", + }, + MimeType: "image/png", + }, webAttachment) +} diff --git a/tests/e2e/issue-comment-dropzone.test.e2e.ts b/tests/e2e/issue-comment-dropzone.test.e2e.ts new file mode 100644 index 0000000000..33ea2c9403 --- /dev/null +++ b/tests/e2e/issue-comment-dropzone.test.e2e.ts @@ -0,0 +1,94 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// web_src/js/features/common-global.js +// web_src/js/features/comp/Paste.js +// web_src/js/features/repo-issue.js +// web_src/js/features/repo-legacy.js +// @watch end + +import {expect, type Locator, type Page, type TestInfo} from '@playwright/test'; +import {test, save_visual, dynamic_id} from './utils_e2e.ts'; + +test.use({user: 'user2'}); + +async function pasteImage(el: Locator) { + await el.evaluate(async (el) => { + const base64 = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAHklEQVQoU2MUk1P7z0AGYBzViDvURgMHT4oaQoEDAFuJEu2fuGfhAAAAAElFTkSuQmCC`; + // eslint-disable-next-line no-restricted-syntax + const response = await fetch(base64); + const blob = await response.blob(); + + el.focus(); + + let pasteEvent = new Event('paste', {bubbles: true, cancelable: true}); + pasteEvent = Object.assign(pasteEvent, { + clipboardData: { + items: [ + { + kind: 'file', + type: 'image/png', + getAsFile() { + return new File([blob], 'foo.png', {type: blob.type}); + }, + }, + ], + }, + }); + + el.dispatchEvent(pasteEvent); + }); +} + +async function assertCopy(page: Page, workerInfo: TestInfo, startWith: string) { + const project = workerInfo.project.name; + if (project === 'webkit' || project === 'Mobile Safari') return; + + const dropzone = page.locator('.dropzone'); + const preview = dropzone.locator('.dz-preview'); + const copyLink = preview.locator('.octicon-copy').locator('..'); + await copyLink.click(); + + const clipboardContent = await page.evaluate(() => navigator.clipboard.readText()); + expect(clipboardContent).toContain(startWith); +} + +test('Paste image in new comment', async ({page}, workerInfo) => { + await page.goto('/user2/repo1/issues/new'); + + await pasteImage(page.locator('.markdown-text-editor')); + + const dropzone = page.locator('.dropzone'); + await expect(dropzone.locator('.files')).toHaveCount(1); + const preview = dropzone.locator('.dz-preview'); + await expect(preview).toHaveCount(1); + await expect(preview.locator('.dz-filename')).toHaveText('foo.png'); + await expect(preview.locator('.octicon-copy')).toBeVisible(); + await assertCopy(page, workerInfo, '![foo]('); + + await save_visual(page); +}); + +test('Re-add images to dropzone on edit', async ({page}, workerInfo) => { + await page.goto('/user2/repo1/issues/new'); + + const issueTitle = dynamic_id(); + await page.locator('#issue_title').fill(issueTitle); + await pasteImage(page.locator('.markdown-text-editor')); + await page.getByRole('button', {name: 'Create issue'}).click(); + + await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/); + await page.click('.comment-container .context-menu'); + await page.click('.comment-container .menu > .edit-content'); + + const dropzone = page.locator('.dropzone'); + await expect(dropzone.locator('.files').first()).toHaveCount(1); + const preview = dropzone.locator('.dz-preview'); + await expect(preview).toHaveCount(1); + await expect(preview.locator('.dz-filename')).toHaveText('foo.png'); + await expect(preview.locator('.octicon-copy')).toBeVisible(); + await assertCopy(page, workerInfo, '![foo]('); + + await save_visual(page); +}); diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 34a6e53a37..72fe2a4e49 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -629,7 +629,12 @@ func TestIssueCommentAttachment(t *testing.T) { assert.NotEqual(t, 0, id) req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user2", "repo1", id)) - session.MakeRequest(t, req, http.StatusOK) + resp = session.MakeRequest(t, req, http.StatusOK) + var attachments []*api.WebAttachment + DecodeJSON(t, resp, &attachments) + assert.Len(t, attachments, 1) + assert.Equal(t, attachments[0].UUID, uuid) + assert.Equal(t, "image/png", attachments[0].MimeType) // Using the ID of a comment that does not belong to the repository must fail req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user5", "repo4", id)) diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 7d553f9692..2102e995d8 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -208,59 +208,128 @@ export function initGlobalDropzone() { } } -export function initDropzone(el) { - const $dropzone = $(el); - const _promise = createDropzone(el, { - url: $dropzone.data('upload-url'), +export async function initDropzone(dropzoneEl, zone = undefined) { + if (!dropzoneEl) return; + + let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event + let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone + + const initFilePreview = (file, data, isReload = false) => { + file.uuid = data.uuid; + fileUuidDict[file.uuid] = {submitted: isReload}; + const input = document.createElement('input'); + input.id = data.uuid; + input.name = 'files'; + input.type = 'hidden'; + input.value = data.uuid; + dropzoneEl.querySelector('.files').append(input); + + // Create a "Copy Link" element, to conveniently copy the image + // or file link as Markdown to the clipboard + const copyLinkElement = document.createElement('div'); + copyLinkElement.className = 'tw-text-center'; + // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone + copyLinkElement.innerHTML = `${svg('octicon-copy', 14, 'copy link')} Copy link`; + copyLinkElement.addEventListener('click', async (e) => { + e.preventDefault(); + const name = file.name.slice(0, file.name.lastIndexOf('.')); + let fileMarkdown = `[${name}](/attachments/${file.uuid})`; + if (file.type.startsWith('image/')) { + fileMarkdown = `!${fileMarkdown}`; + } else if (file.type.startsWith('video/')) { + fileMarkdown = ``; + } + const success = await clippie(fileMarkdown); + showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); + }); + file.previewTemplate.append(copyLinkElement); + }; + const updateDropzoneState = () => { + if (dropzoneEl.querySelector('.dz-preview')) { + dropzoneEl.classList.add('dz-started'); + } else { + dropzoneEl.classList.remove('dz-started'); + } + }; + + const dz = await createDropzone(dropzoneEl, { + url: dropzoneEl.getAttribute('data-upload-url'), headers: {'X-Csrf-Token': csrfToken}, - maxFiles: $dropzone.data('max-file'), - maxFilesize: $dropzone.data('max-size'), - acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'), + maxFiles: dropzoneEl.getAttribute('data-max-file'), + maxFilesize: dropzoneEl.getAttribute('data-max-size'), + acceptedFiles: (['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')) ? null : dropzoneEl.getAttribute('data-accepts')), addRemoveLinks: true, - dictDefaultMessage: $dropzone.data('default-message'), - dictInvalidFileType: $dropzone.data('invalid-input-type'), - dictFileTooBig: $dropzone.data('file-too-big'), - dictRemoveFile: $dropzone.data('remove-file'), + dictDefaultMessage: dropzoneEl.getAttribute('data-default-message'), + dictInvalidFileType: dropzoneEl.getAttribute('data-invalid-input-type'), + dictFileTooBig: dropzoneEl.getAttribute('data-file-too-big'), + dictRemoveFile: dropzoneEl.getAttribute('data-remove-file'), timeout: 0, thumbnailMethod: 'contain', thumbnailWidth: 480, thumbnailHeight: 480, init() { - this.on('success', (file, data) => { - file.uuid = data.uuid; - const $input = $(``).val(data.uuid); - $dropzone.find('.files').append($input); - // Create a "Copy Link" element, to conveniently copy the image - // or file link as Markdown to the clipboard - const copyLinkElement = document.createElement('div'); - copyLinkElement.className = 'tw-text-center'; - // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone - copyLinkElement.innerHTML = `${svg('octicon-copy', 14, 'copy link')} Copy link`; - copyLinkElement.addEventListener('click', async (e) => { - e.preventDefault(); - let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; - if (file.type.startsWith('image/')) { - fileMarkdown = `!${fileMarkdown}`; - } else if (file.type.startsWith('video/')) { - fileMarkdown = ``; + this.on('success', initFilePreview); + this.on('removedfile', async (file) => { + document.getElementById(file.uuid)?.remove(); + if (disableRemovedfileEvent) return; + if (dropzoneEl.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) { + try { + await POST(dropzoneEl.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})}); + } catch (error) { + console.error(error); } - const success = await clippie(fileMarkdown); - showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); - }); - file.previewTemplate.append(copyLinkElement); - }); - this.on('removedfile', (file) => { - $(`#${file.uuid}`).remove(); - if ($dropzone.data('remove-url')) { - POST($dropzone.data('remove-url'), { - data: new URLSearchParams({file: file.uuid}), - }); } + updateDropzoneState(); }); this.on('error', function (file, message) { showErrorToast(message); this.removeFile(file); }); + this.on('reload', async () => { + if (!zone || !dz.removeAllFiles) return; + try { + const response = await GET(zone.getAttribute('data-attachment-url')); + const data = await response.json(); + // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server + disableRemovedfileEvent = true; + dz.removeAllFiles(true); + dropzoneEl.querySelector('.files').innerHTML = ''; + for (const element of dropzoneEl.querySelectorAll('.dz-preview')) element.remove(); + fileUuidDict = {}; + disableRemovedfileEvent = false; + + for (const attachment of data) { + attachment.type = attachment.mime_type; + dz.emit('addedfile', attachment); + dz.emit('complete', attachment); + if (attachment.type.startsWith('image/')) { + const imgSrc = `${dropzoneEl.getAttribute('data-link-url')}/${attachment.uuid}`; + dz.emit('thumbnail', attachment, imgSrc); + } + initFilePreview(attachment, {uuid: attachment.uuid}, true); + fileUuidDict[attachment.uuid] = {submitted: true}; + } + } catch (error) { + console.error(error); + } + updateDropzoneState(); + }); + this.on('create-thumbnail', (attachment, file) => { + if (attachment.type && /image.*/.test(attachment.type)) { + // When a new issue is created, a thumbnail cannot be fetch, so we need to create it locally. + // The implementation is took from the dropzone library (`dropzone.js` > `_processThumbnailQueue()`) + dz.createThumbnail( + file, + dz.options.thumbnailWidth, + dz.options.thumbnailHeight, + dz.options.thumbnailMethod, + true, + (dataUrl) => { + dz.emit('thumbnail', attachment, dataUrl); + }, + ); + } + }); }, }); } diff --git a/web_src/js/features/comp/Paste.js b/web_src/js/features/comp/Paste.js index 7e4ecbbeda..0fb6cf4615 100644 --- a/web_src/js/features/comp/Paste.js +++ b/web_src/js/features/comp/Paste.js @@ -82,9 +82,8 @@ class CodeMirrorEditor { async function handleClipboardImages(editor, dropzone, images, e) { const uploadUrl = dropzone.getAttribute('data-upload-url'); - const filesContainer = dropzone.querySelector('.files'); - if (!dropzone || !uploadUrl || !filesContainer || !images.length) return; + if (!dropzone || !uploadUrl || !images.length) return; e.preventDefault(); e.stopPropagation(); @@ -92,7 +91,7 @@ async function handleClipboardImages(editor, dropzone, images, e) { for (const img of images) { const name = img.name.slice(0, img.name.lastIndexOf('.')); - const placeholder = `![${name}](uploading ...)`; + const placeholder = `![${name}](uploading...)`; editor.insertPlaceholder(placeholder); const {uuid} = await uploadFile(img, uploadUrl); @@ -101,12 +100,11 @@ async function handleClipboardImages(editor, dropzone, images, e) { const text = `![${name}](${url})`; editor.replacePlaceholder(placeholder, text); - const input = document.createElement('input'); - input.setAttribute('name', 'files'); - input.setAttribute('type', 'hidden'); - input.setAttribute('id', uuid); - input.value = uuid; - filesContainer.append(input); + const attachment = {uuid, name: img.name, browser_download_url: url, size: img.size, type: img.type}; + dropzone.dropzone.emit('addedfile', attachment); + dropzone.dropzone.emit('create-thumbnail', attachment, img); + dropzone.dropzone.emit('complete', attachment); + dropzone.dropzone.emit('success', attachment, {uuid}); } } diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index bf76453428..d678d9195b 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -445,7 +445,7 @@ export async function handleReply($el) { // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized. // When the form is submitted and partially reload, none of them is initialized. const dropzone = $form.find('.dropzone')[0]; - if (!dropzone.dropzone) initDropzone(dropzone); + if (!dropzone.dropzone) await initDropzone(dropzone); editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor')); } editor.focus(); @@ -580,7 +580,7 @@ export function initRepoPullRequestReview() { $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed'); $td.find("input[name='path']").val(path); - initDropzone($td.find('.dropzone')[0]); + await initDropzone($td.find('.dropzone')[0]); const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor')); editor.focus(); } catch (error) { diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 25ed181616..e6af4cbf04 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -16,7 +16,6 @@ import { import {initCitationFileCopyContent} from './citation.js'; import {initCompLabelEdit} from './comp/LabelEdit.js'; import {initRepoDiffConversationNav} from './repo-diff.js'; -import {createDropzone} from './dropzone.js'; import {showErrorToast} from '../modules/toast.js'; import {initCommentContent, initMarkupContent} from '../markup/content.js'; import {initCompReactionSelector} from './comp/ReactionSelector.js'; @@ -26,12 +25,10 @@ import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js'; import {hideElem, showElem} from '../utils/dom.js'; import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; import {attachRefIssueContextPopup} from './contextpopup.js'; -import {POST, GET} from '../modules/fetch.js'; +import {POST} from '../modules/fetch.js'; import {MarkdownQuote} from '@github/quote-selection'; import {toAbsoluteUrl} from '../utils.js'; -import {initGlobalShowModal} from './common-global.js'; - -const {csrfToken} = window.config; +import {initDropzone, initGlobalShowModal} from './common-global.js'; export function initRepoCommentForm() { const $commentForm = $('.comment.form'); @@ -312,115 +309,27 @@ async function onEditContent(event) { let comboMarkdownEditor; - /** - * @param {HTMLElement} dropzone - */ - const setupDropzone = async (dropzone) => { - if (!dropzone) return null; - - let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event - let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone - const dz = await createDropzone(dropzone, { - url: dropzone.getAttribute('data-upload-url'), - headers: {'X-Csrf-Token': csrfToken}, - maxFiles: dropzone.getAttribute('data-max-file'), - maxFilesize: dropzone.getAttribute('data-max-size'), - acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'), - addRemoveLinks: true, - dictDefaultMessage: dropzone.getAttribute('data-default-message'), - dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'), - dictFileTooBig: dropzone.getAttribute('data-file-too-big'), - dictRemoveFile: dropzone.getAttribute('data-remove-file'), - timeout: 0, - thumbnailMethod: 'contain', - thumbnailWidth: 480, - thumbnailHeight: 480, - init() { - this.on('success', (file, data) => { - file.uuid = data.uuid; - fileUuidDict[file.uuid] = {submitted: false}; - const input = document.createElement('input'); - input.id = data.uuid; - input.name = 'files'; - input.type = 'hidden'; - input.value = data.uuid; - dropzone.querySelector('.files').append(input); - }); - this.on('removedfile', async (file) => { - document.getElementById(file.uuid)?.remove(); - if (disableRemovedfileEvent) return; - if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) { - try { - await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})}); - } catch (error) { - console.error(error); - } - } - }); - this.on('submit', () => { - for (const fileUuid of Object.keys(fileUuidDict)) { - fileUuidDict[fileUuid].submitted = true; - } - }); - this.on('reload', async () => { - try { - const response = await GET(editContentZone.getAttribute('data-attachment-url')); - const data = await response.json(); - // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server - disableRemovedfileEvent = true; - dz.removeAllFiles(true); - dropzone.querySelector('.files').innerHTML = ''; - for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove(); - fileUuidDict = {}; - disableRemovedfileEvent = false; - - for (const attachment of data) { - const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`; - dz.emit('addedfile', attachment); - dz.emit('thumbnail', attachment, imgSrc); - dz.emit('complete', attachment); - fileUuidDict[attachment.uuid] = {submitted: true}; - dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%'; - const input = document.createElement('input'); - input.id = attachment.uuid; - input.name = 'files'; - input.type = 'hidden'; - input.value = attachment.uuid; - dropzone.querySelector('.files').append(input); - } - if (!dropzone.querySelector('.dz-preview')) { - dropzone.classList.remove('dz-started'); - } - } catch (error) { - console.error(error); - } - }); - }, - }); - dz.emit('reload'); - return dz; - }; - const cancelAndReset = (e) => { e.preventDefault(); showElem(renderContent); hideElem(editContentZone); comboMarkdownEditor.value(rawContent.textContent); - comboMarkdownEditor.attachedDropzoneInst?.emit('reload'); + editContentZone.querySelector('.dropzone')?.dropzone?.emit('reload'); }; const saveAndRefresh = async (e) => { e.preventDefault(); showElem(renderContent); hideElem(editContentZone); - const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst; + const dropzone = editContentZone.querySelector('.dropzone')?.dropzone; + for (const element of dropzone?.element?.querySelectorAll('.dz-preview') ?? []) element.classList.remove('dz-success'); try { const params = new URLSearchParams({ content: comboMarkdownEditor.value(), context: editContentZone.getAttribute('data-context'), content_version: editContentZone.getAttribute('data-content-version'), }); - const files = dropzoneInst?.element?.querySelectorAll('.files [name=files]') ?? []; + const files = dropzone?.element?.querySelectorAll('.files [name=files]') ?? []; for (const fileInput of files) { params.append('files[]', fileInput.value); } @@ -451,8 +360,7 @@ async function onEditContent(event) { } else { content.querySelector('.dropzone-attachments').outerHTML = data.attachments; } - dropzoneInst?.emit('submit'); - dropzoneInst?.emit('reload'); + dropzone?.emit('submit'); initMarkupContent(); initCommentContent(); } catch (error) { @@ -463,8 +371,10 @@ async function onEditContent(event) { comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); if (!comboMarkdownEditor) { editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML; + const dropzone = editContentZone.querySelector('.dropzone'); + if (!dropzone.dropzone) await initDropzone(dropzone, editContentZone); comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); - comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone')); + dropzone.dropzone.emit('reload'); editContentZone.addEventListener('ce-quick-submit', saveAndRefresh); editContentZone.querySelector('button[data-button-name="cancel-edit"]').addEventListener('click', cancelAndReset); editContentZone.querySelector('button[data-button-name="save-edit"]').addEventListener('click', saveAndRefresh);