forked from retoor/devplacepy
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b65360be40 |
File diff suppressed because one or more lines are too long
@@ -8,11 +8,6 @@ export class CommentManager {
|
|||||||
this.initCommentReply();
|
this.initCommentReply();
|
||||||
this.initCommentEdit();
|
this.initCommentEdit();
|
||||||
this.initCommentDelete();
|
this.initCommentDelete();
|
||||||
document.addEventListener("paste", (event) => {
|
|
||||||
const textarea = event.target.closest("form textarea[name='body'], form textarea[name='content']");
|
|
||||||
if (!textarea) return;
|
|
||||||
this._onPaste(event, textarea);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initCommentDelete() {
|
initCommentDelete() {
|
||||||
@@ -213,41 +208,4 @@ export class CommentManager {
|
|||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPaste(event, textarea) {
|
|
||||||
const items = event.clipboardData?.items;
|
|
||||||
if (!items) return;
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
|
||||||
event.preventDefault();
|
|
||||||
const file = item.getAsFile();
|
|
||||||
const namedFile = new File([file], file.name || "clipboard.png", { type: file.type });
|
|
||||||
this._uploadImage(namedFile, textarea);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _uploadImage(file, textarea) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file, file.name);
|
|
||||||
try {
|
|
||||||
const response = await fetch("/uploads/upload", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"X-Requested-With": "fetch",
|
|
||||||
},
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(data.error || `Upload failed (${response.status})`);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
window.TextInput.insertAtCursor(textarea, ``);
|
|
||||||
} catch (err) {
|
|
||||||
Http.notifyError(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,48 +12,6 @@ export class IssueReporter {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.submit(form);
|
this.submit(form);
|
||||||
});
|
});
|
||||||
document.addEventListener("paste", (event) => {
|
|
||||||
const textarea = event.target.closest("form[data-issue-create] [name='description']");
|
|
||||||
if (!textarea) return;
|
|
||||||
this._onPaste(event, textarea);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onPaste(event, textarea) {
|
|
||||||
const items = event.clipboardData?.items;
|
|
||||||
if (!items) return;
|
|
||||||
for (const item of items) {
|
|
||||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
|
||||||
event.preventDefault();
|
|
||||||
const file = item.getAsFile();
|
|
||||||
const namedFile = new File([file], file.name || "clipboard.png", { type: file.type });
|
|
||||||
this._uploadImage(namedFile, textarea);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _uploadImage(file, textarea) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file, file.name);
|
|
||||||
try {
|
|
||||||
const response = await fetch("/uploads/upload", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"X-Requested-With": "fetch",
|
|
||||||
},
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(data.error || `Upload failed (${response.status})`);
|
|
||||||
}
|
|
||||||
const result = await response.json();
|
|
||||||
window.TextInput.insertAtCursor(textarea, ``);
|
|
||||||
} catch (err) {
|
|
||||||
Http.notifyError(err.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit(form) {
|
async submit(form) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{% macro comment_cell(item, depth, parent_author=None) %}
|
{% macro comment_cell(item, depth, parent_author=None, parent_url=None) %}
|
||||||
<div class="comment" style="--comment-depth: {{ depth }};" data-depth="{{ depth }}">
|
<div class="comment" style="--comment-depth: {{ depth }};" data-depth="{{ depth }}">
|
||||||
<div class="comment-votes" role="group" aria-label="Comment votes">
|
<div class="comment-votes" role="group" aria-label="Comment votes">
|
||||||
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
|
<form method="POST" action="/votes/comment/{{ item.comment['uid'] }}">
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="comment-actions">
|
<div class="comment-actions">
|
||||||
<button type="button" class="comment-action-btn" data-action="reply"{{ guest_disabled(user) }}><span class="icon">đź’¬</span><span class="label"> Reply</span></button>
|
<button type="button" class="comment-action-btn" data-action="reply"{{ guest_disabled(user) }}><span class="icon">đź’¬</span><span class="label"> Reply</span></button>
|
||||||
|
<button type="button" class="comment-action-btn" data-share="{{ parent_url }}#comment-{{ item.comment['uid'] }}"><span class="icon">🔗</span><span class="label"> Share</span></button>
|
||||||
{% if owns(item.comment, user) %}
|
{% if owns(item.comment, user) %}
|
||||||
<button type="button" class="comment-action-btn" data-action="edit" data-edit-url="/comments/edit/{{ item.comment['uid'] }}"><span class="icon">✏️</span><span class="label"> Edit</span></button>
|
<button type="button" class="comment-action-btn" data-action="edit" data-edit-url="/comments/edit/{{ item.comment['uid'] }}"><span class="icon">✏️</span><span class="label"> Edit</span></button>
|
||||||
<form method="POST" action="/comments/delete/{{ item.comment['uid'] }}" class="inline-form comment-delete-form" data-comment-uid="{{ item.comment['uid'] }}">
|
<form method="POST" action="/comments/delete/{{ item.comment['uid'] }}" class="inline-form comment-delete-form" data-comment-uid="{{ item.comment['uid'] }}">
|
||||||
@@ -39,21 +40,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_comment(item, depth=0) %}
|
{% macro render_comment(item, depth=0, parent_url=None) %}
|
||||||
{% set max_depth = 2 %}
|
{% set max_depth = 2 %}
|
||||||
{% set visual_depth = depth if depth < max_depth else max_depth %}
|
{% set visual_depth = depth if depth < max_depth else max_depth %}
|
||||||
{% call comment_cell(item, visual_depth) %}
|
{% call comment_cell(item, visual_depth, parent_url=parent_url) %}
|
||||||
{% if item.children %}
|
{% if item.children %}
|
||||||
{% if depth < max_depth %}
|
{% if depth < max_depth %}
|
||||||
<div class="comment-replies">
|
<div class="comment-replies">
|
||||||
{% for child in item.children %}
|
{% for child in item.children %}
|
||||||
{{ render_comment(child, depth + 1) }}
|
{{ render_comment(child, depth + 1, parent_url) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="comment-replies comment-replies-flat">
|
<div class="comment-replies comment-replies-flat">
|
||||||
{% for child in item.children %}
|
{% for child in item.children %}
|
||||||
{{ render_comment_flat(child, max_depth, item.author) }}
|
{{ render_comment_flat(child, max_depth, item.author, parent_url) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -61,9 +62,9 @@
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro render_comment_flat(item, depth, parent_author) %}
|
{% macro render_comment_flat(item, depth, parent_author, parent_url=None) %}
|
||||||
{% call comment_cell(item, depth, parent_author) %}{% endcall %}
|
{% call comment_cell(item, depth, parent_author, parent_url=parent_url) %}{% endcall %}
|
||||||
{% for child in item.children %}
|
{% for child in item.children %}
|
||||||
{{ render_comment_flat(child, depth, item.author) }}
|
{{ render_comment_flat(child, depth, item.author, parent_url) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
{% from "_comment.html" import render_comment with context %}
|
{% from "_comment.html" import render_comment with context %}
|
||||||
|
|
||||||
|
{% set parent_url = resolve_object_url(target_type, target_uid) %}
|
||||||
|
|
||||||
{% for item in comments %}
|
{% for item in comments %}
|
||||||
{{ render_comment(item, 0) }}
|
{{ render_comment(item, 0, parent_url) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="no-comments-msg">No comments yet. Start the discussion.</p>
|
<p class="no-comments-msg">No comments yet. Start the discussion.</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from devplacepy.cache import TTLCache
|
|||||||
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
|
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
|
||||||
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
from devplacepy.constants import TOPICS, REACTION_EMOJI
|
||||||
from devplacepy.database import get_int_setting, get_setting, get_table
|
from devplacepy.database import get_int_setting, get_setting, get_table
|
||||||
|
from devplacepy.database.content import resolve_object_url
|
||||||
from devplacepy.avatar import avatar_url, avatar_seed
|
from devplacepy.avatar import avatar_url, avatar_seed
|
||||||
from devplacepy.utils import format_date as _format_date
|
from devplacepy.utils import format_date as _format_date
|
||||||
from devplacepy.utils import time_ago as _time_ago
|
from devplacepy.utils import time_ago as _time_ago
|
||||||
@@ -49,6 +50,7 @@ templates.env.globals["is_self"] = is_self
|
|||||||
templates.env.globals["guest_disabled"] = guest_disabled
|
templates.env.globals["guest_disabled"] = guest_disabled
|
||||||
templates.env.globals["is_online"] = presence.is_online
|
templates.env.globals["is_online"] = presence.is_online
|
||||||
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
|
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
|
||||||
|
templates.env.globals["resolve_object_url"] = resolve_object_url
|
||||||
|
|
||||||
from devplacepy.docs_devrant import devrant_endpoints
|
from devplacepy.docs_devrant import devrant_endpoints
|
||||||
|
|
||||||
|
|||||||
@@ -240,17 +240,3 @@ def test_delete_own_allowed_other_user_forbidden(app_server):
|
|||||||
bob = _session_uploads()
|
bob = _session_uploads()
|
||||||
assert bob.delete(f"{BASE_URL}/uploads/delete/{uid}").status_code == 403
|
assert bob.delete(f"{BASE_URL}/uploads/delete/{uid}").status_code == 403
|
||||||
assert alice.delete(f"{BASE_URL}/uploads/delete/{uid}").status_code == 200
|
assert alice.delete(f"{BASE_URL}/uploads/delete/{uid}").status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_paste_image_upload_returns_url(app_server):
|
|
||||||
s = _session_uploads()
|
|
||||||
r = s.post(
|
|
||||||
f"{BASE_URL}/uploads/upload",
|
|
||||||
files={"file": ("clipboard.png", _png_bytes_uploads(), "image/png")},
|
|
||||||
)
|
|
||||||
assert r.status_code == 201, r.text
|
|
||||||
data = r.json()
|
|
||||||
assert "url" in data
|
|
||||||
assert data["url"].startswith("/static/uploads/attachments/")
|
|
||||||
assert data["uid"]
|
|
||||||
assert data["is_image"] is True
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
python3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/usr/bin/python3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
python3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
lib
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
home = /usr/bin
|
|
||||||
include-system-site-packages = false
|
|
||||||
version = 3.11.2
|
|
||||||
executable = /usr/bin/python3.11
|
|
||||||
command = /usr/bin/python3 -m venv /workspace/repo/venv
|
|
||||||
Reference in New Issue
Block a user