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, ';
+
+ 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, ';
+
+ 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 = ``;
+ const placeholder = ``;
editor.insertPlaceholder(placeholder);
const {uuid} = await uploadFile(img, uploadUrl);
@@ -101,12 +100,11 @@ async function handleClipboardImages(editor, dropzone, images, e) {
const text = ``;
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);