mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-07-07 18:05:40 +02:00
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:
parent
ad1adabcbb
commit
6e66380408
10 changed files with 303 additions and 158 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
56
services/convert/attachment_test.go
Normal file
56
services/convert/attachment_test.go
Normal 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)
|
||||
}
|
94
tests/e2e/issue-comment-dropzone.test.e2e.ts
Normal file
94
tests/e2e/issue-comment-dropzone.test.e2e.ts
Normal 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, ';
|
||||
|
||||
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);
|
||||
});
|
|
@ -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))
|
||||
|
|
|
@ -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 = `<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},
|
||||
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 = $(`<input id="${data.uuid}" name="files" type="hidden">`).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 = `<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>`;
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue