fix(ui): Add pasted images to dropzone

This adds pasted images to the dropzone to provide the same experience
as when using the dropzone. This gives the possibility to preview and
delete the image. Additionally it provides a copy button to copy the
markdown code for inserting the image.

Removed the old implementation in `repo-legacy.js` and generalized
everything in `common-global.js` as common implementation.

Replaced old jquery code with plain JS.

Fixes #4588
This commit is contained in:
Beowulf 2025-05-02 00:57:43 +02:00
parent ad1adabcbb
commit 6e66380408
No known key found for this signature in database
GPG key ID: 44225F5F2792841D
10 changed files with 303 additions and 158 deletions

View file

@ -22,6 +22,12 @@ type Attachment struct {
Type string `json:"type"` 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 // EditAttachmentOptions options for editing attachments
// swagger:model // swagger:model
type EditAttachmentOptions struct { type EditAttachmentOptions struct {

View file

@ -3593,9 +3593,9 @@ func GetIssueAttachments(ctx *context.Context) {
if ctx.Written() { if ctx.Written() {
return return
} }
attachments := make([]*api.Attachment, len(issue.Attachments)) attachments := make([]*api.WebAttachment, len(issue.Attachments))
for i := 0; i < len(issue.Attachments); i++ { 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) ctx.JSON(http.StatusOK, attachments)
} }
@ -3628,13 +3628,13 @@ func GetCommentAttachments(ctx *context.Context) {
return return
} }
attachments := make([]*api.Attachment, 0)
if err := comment.LoadAttachments(ctx); err != nil { if err := comment.LoadAttachments(ctx); err != nil {
ctx.ServerError("LoadAttachments", err) ctx.ServerError("LoadAttachments", err)
return return
} }
attachments := make([]*api.WebAttachment, len(comment.Attachments))
for i := 0; i < len(comment.Attachments); i++ { 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) ctx.JSON(http.StatusOK, attachments)
} }

View file

@ -4,6 +4,9 @@
package convert package convert
import ( import (
"mime"
"path/filepath"
repo_model "forgejo.org/models/repo" repo_model "forgejo.org/models/repo"
api "forgejo.org/modules/structs" api "forgejo.org/modules/structs"
) )
@ -20,9 +23,13 @@ func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachm
return attach.DownloadURL() return attach.DownloadURL()
} }
// ToAttachment converts models.Attachment to api.Attachment for API usage // ToWebAttachment converts models.Attachment to api.WebAttachment for API usage
func ToAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.Attachment { func ToWebAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.WebAttachment {
return toAttachment(repo, a, WebAssetDownloadURL) 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 // ToAPIAttachment converts models.Attachment to api.Attachment for API usage

View file

@ -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)
}

View file

@ -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);
});

View file

@ -629,7 +629,12 @@ func TestIssueCommentAttachment(t *testing.T) {
assert.NotEqual(t, 0, id) assert.NotEqual(t, 0, id)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user2", "repo1", 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 // 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)) req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user5", "repo4", id))

View file

@ -208,59 +208,128 @@ export function initGlobalDropzone() {
} }
} }
export function initDropzone(el) { export async function initDropzone(dropzoneEl, zone = undefined) {
const $dropzone = $(el); if (!dropzoneEl) return;
const _promise = createDropzone(el, {
url: $dropzone.data('upload-url'), 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 = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
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 = `<video src="/attachments/${file.uuid}" title="${htmlEscape(name)}" controls></video>`;
}
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}, headers: {'X-Csrf-Token': csrfToken},
maxFiles: $dropzone.data('max-file'), maxFiles: dropzoneEl.getAttribute('data-max-file'),
maxFilesize: $dropzone.data('max-size'), maxFilesize: dropzoneEl.getAttribute('data-max-size'),
acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'), acceptedFiles: (['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')) ? null : dropzoneEl.getAttribute('data-accepts')),
addRemoveLinks: true, addRemoveLinks: true,
dictDefaultMessage: $dropzone.data('default-message'), dictDefaultMessage: dropzoneEl.getAttribute('data-default-message'),
dictInvalidFileType: $dropzone.data('invalid-input-type'), dictInvalidFileType: dropzoneEl.getAttribute('data-invalid-input-type'),
dictFileTooBig: $dropzone.data('file-too-big'), dictFileTooBig: dropzoneEl.getAttribute('data-file-too-big'),
dictRemoveFile: $dropzone.data('remove-file'), dictRemoveFile: dropzoneEl.getAttribute('data-remove-file'),
timeout: 0, timeout: 0,
thumbnailMethod: 'contain', thumbnailMethod: 'contain',
thumbnailWidth: 480, thumbnailWidth: 480,
thumbnailHeight: 480, thumbnailHeight: 480,
init() { init() {
this.on('success', (file, data) => { this.on('success', initFilePreview);
file.uuid = data.uuid; this.on('removedfile', async (file) => {
const $input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid); document.getElementById(file.uuid)?.remove();
$dropzone.find('.files').append($input); if (disableRemovedfileEvent) return;
// Create a "Copy Link" element, to conveniently copy the image if (dropzoneEl.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) {
// or file link as Markdown to the clipboard try {
const copyLinkElement = document.createElement('div'); await POST(dropzoneEl.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})});
copyLinkElement.className = 'tw-text-center'; } catch (error) {
// The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone console.error(error);
copyLinkElement.innerHTML = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`;
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 = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`;
} }
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) { this.on('error', function (file, message) {
showErrorToast(message); showErrorToast(message);
this.removeFile(file); 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);
},
);
}
});
}, },
}); });
} }

View file

@ -82,9 +82,8 @@ class CodeMirrorEditor {
async function handleClipboardImages(editor, dropzone, images, e) { async function handleClipboardImages(editor, dropzone, images, e) {
const uploadUrl = dropzone.getAttribute('data-upload-url'); 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.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -92,7 +91,7 @@ async function handleClipboardImages(editor, dropzone, images, e) {
for (const img of images) { for (const img of images) {
const name = img.name.slice(0, img.name.lastIndexOf('.')); const name = img.name.slice(0, img.name.lastIndexOf('.'));
const placeholder = `![${name}](uploading ...)`; const placeholder = `![${name}](uploading...)`;
editor.insertPlaceholder(placeholder); editor.insertPlaceholder(placeholder);
const {uuid} = await uploadFile(img, uploadUrl); const {uuid} = await uploadFile(img, uploadUrl);
@ -101,12 +100,11 @@ async function handleClipboardImages(editor, dropzone, images, e) {
const text = `![${name}](${url})`; const text = `![${name}](${url})`;
editor.replacePlaceholder(placeholder, text); editor.replacePlaceholder(placeholder, text);
const input = document.createElement('input'); const attachment = {uuid, name: img.name, browser_download_url: url, size: img.size, type: img.type};
input.setAttribute('name', 'files'); dropzone.dropzone.emit('addedfile', attachment);
input.setAttribute('type', 'hidden'); dropzone.dropzone.emit('create-thumbnail', attachment, img);
input.setAttribute('id', uuid); dropzone.dropzone.emit('complete', attachment);
input.value = uuid; dropzone.dropzone.emit('success', attachment, {uuid});
filesContainer.append(input);
} }
} }

View file

@ -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 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. // When the form is submitted and partially reload, none of them is initialized.
const dropzone = $form.find('.dropzone')[0]; 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 = await initComboMarkdownEditor($form.find('.combo-markdown-editor'));
} }
editor.focus(); editor.focus();
@ -580,7 +580,7 @@ export function initRepoPullRequestReview() {
$td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed'); $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
$td.find("input[name='path']").val(path); $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')); const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
editor.focus(); editor.focus();
} catch (error) { } catch (error) {

View file

@ -16,7 +16,6 @@ import {
import {initCitationFileCopyContent} from './citation.js'; import {initCitationFileCopyContent} from './citation.js';
import {initCompLabelEdit} from './comp/LabelEdit.js'; import {initCompLabelEdit} from './comp/LabelEdit.js';
import {initRepoDiffConversationNav} from './repo-diff.js'; import {initRepoDiffConversationNav} from './repo-diff.js';
import {createDropzone} from './dropzone.js';
import {showErrorToast} from '../modules/toast.js'; import {showErrorToast} from '../modules/toast.js';
import {initCommentContent, initMarkupContent} from '../markup/content.js'; import {initCommentContent, initMarkupContent} from '../markup/content.js';
import {initCompReactionSelector} from './comp/ReactionSelector.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 {hideElem, showElem} from '../utils/dom.js';
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
import {attachRefIssueContextPopup} from './contextpopup.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 {MarkdownQuote} from '@github/quote-selection';
import {toAbsoluteUrl} from '../utils.js'; import {toAbsoluteUrl} from '../utils.js';
import {initGlobalShowModal} from './common-global.js'; import {initDropzone, initGlobalShowModal} from './common-global.js';
const {csrfToken} = window.config;
export function initRepoCommentForm() { export function initRepoCommentForm() {
const $commentForm = $('.comment.form'); const $commentForm = $('.comment.form');
@ -312,115 +309,27 @@ async function onEditContent(event) {
let comboMarkdownEditor; 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) => { const cancelAndReset = (e) => {
e.preventDefault(); e.preventDefault();
showElem(renderContent); showElem(renderContent);
hideElem(editContentZone); hideElem(editContentZone);
comboMarkdownEditor.value(rawContent.textContent); comboMarkdownEditor.value(rawContent.textContent);
comboMarkdownEditor.attachedDropzoneInst?.emit('reload'); editContentZone.querySelector('.dropzone')?.dropzone?.emit('reload');
}; };
const saveAndRefresh = async (e) => { const saveAndRefresh = async (e) => {
e.preventDefault(); e.preventDefault();
showElem(renderContent); showElem(renderContent);
hideElem(editContentZone); 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 { try {
const params = new URLSearchParams({ const params = new URLSearchParams({
content: comboMarkdownEditor.value(), content: comboMarkdownEditor.value(),
context: editContentZone.getAttribute('data-context'), context: editContentZone.getAttribute('data-context'),
content_version: editContentZone.getAttribute('data-content-version'), 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) { for (const fileInput of files) {
params.append('files[]', fileInput.value); params.append('files[]', fileInput.value);
} }
@ -451,8 +360,7 @@ async function onEditContent(event) {
} else { } else {
content.querySelector('.dropzone-attachments').outerHTML = data.attachments; content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
} }
dropzoneInst?.emit('submit'); dropzone?.emit('submit');
dropzoneInst?.emit('reload');
initMarkupContent(); initMarkupContent();
initCommentContent(); initCommentContent();
} catch (error) { } catch (error) {
@ -463,8 +371,10 @@ async function onEditContent(event) {
comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
if (!comboMarkdownEditor) { if (!comboMarkdownEditor) {
editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML; 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 = 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.addEventListener('ce-quick-submit', saveAndRefresh);
editContentZone.querySelector('button[data-button-name="cancel-edit"]').addEventListener('click', cancelAndReset); editContentZone.querySelector('button[data-button-name="cancel-edit"]').addEventListener('click', cancelAndReset);
editContentZone.querySelector('button[data-button-name="save-edit"]').addEventListener('click', saveAndRefresh); editContentZone.querySelector('button[data-button-name="save-edit"]').addEventListener('click', saveAndRefresh);