mirror of
https://github.com/sissbruecker/linkding.git
synced 2026-02-28 15:03:12 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfbba20275 | ||
|
|
f67c4605fd | ||
|
|
1f0a2201ba | ||
|
|
d52caefe2c | ||
|
|
c998dd35b7 | ||
|
|
397eb6d316 |
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,5 +1,58 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.45.0 (06/01/2026)
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
* API token management by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1248
|
||||||
|
* Add option to disable login form by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1269
|
||||||
|
* Turn scheme-less URLs into HTTPS instead of HTTP links by @Maaxxs in https://github.com/sissbruecker/linkding/pull/1225
|
||||||
|
* Disable bulk execute button when no bookmarks selected by @emanuelebeffa in https://github.com/sissbruecker/linkding/pull/1241
|
||||||
|
* Add option to run supervisor as main process by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1270
|
||||||
|
* Allow setting date_added and date_modified for new bookmarks through REST API by @jmason in https://github.com/sissbruecker/linkding/pull/1063
|
||||||
|
* Download PDF instead of creating HTML snapshot if URL points at PDF by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1271
|
||||||
|
* Allow sandboxed scripts when viewing assets by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1252
|
||||||
|
* Allow viewing video assets by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1259
|
||||||
|
* Remove absolute URIs from settings page by @packrat386 in https://github.com/sissbruecker/linkding/pull/1261
|
||||||
|
* Move tag management forms into dialogs by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1253
|
||||||
|
* Move bulk edit checkboxes into bookmark list container by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1257
|
||||||
|
* Remove registration switch by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1268
|
||||||
|
* Add linkdinger to community projects by @lmmendes in https://github.com/sissbruecker/linkding/pull/1266
|
||||||
|
|
||||||
|
### New Contributors
|
||||||
|
* @packrat386 made their first contribution in https://github.com/sissbruecker/linkding/pull/1261
|
||||||
|
* @lmmendes made their first contribution in https://github.com/sissbruecker/linkding/pull/1266
|
||||||
|
* @Maaxxs made their first contribution in https://github.com/sissbruecker/linkding/pull/1225
|
||||||
|
* @emanuelebeffa made their first contribution in https://github.com/sissbruecker/linkding/pull/1241
|
||||||
|
* @jmason made their first contribution in https://github.com/sissbruecker/linkding/pull/1063
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.44.2...v1.45.0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.44.2 (13/12/2025)
|
||||||
|
|
||||||
|
### What's Changed
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> *This resolves a [security vulnerability](https://github.com/sissbruecker/linkding/security/advisories/GHSA-3pf9-5cjv-2w7q) in linkding. Everyone is encouraged to upgrade to the latest version as soon as possible.*
|
||||||
|
|
||||||
|
* Use sandbox CSP for viewing assets by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1245
|
||||||
|
* Fix devcontainer by @m3eno in https://github.com/sissbruecker/linkding/pull/1208
|
||||||
|
* Fix tag cloud highlighting first char when tags are not grouped by @sissbruecker in https://github.com/sissbruecker/linkding/pull/1209
|
||||||
|
* Bump supervisor to 4.3.0 to fix warning by @simonhammes in https://github.com/sissbruecker/linkding/pull/1216
|
||||||
|
* Added Javascript client and library for Linkding REST API by @vbsampath in https://github.com/sissbruecker/linkding/pull/1195
|
||||||
|
* Add Komrade project to community resources by @dev-inside in https://github.com/sissbruecker/linkding/pull/1236
|
||||||
|
|
||||||
|
### New Contributors
|
||||||
|
* @m3eno made their first contribution in https://github.com/sissbruecker/linkding/pull/1208
|
||||||
|
* @vbsampath made their first contribution in https://github.com/sissbruecker/linkding/pull/1195
|
||||||
|
* @dev-inside made their first contribution in https://github.com/sissbruecker/linkding/pull/1236
|
||||||
|
* @simonhammes made their first contribution in https://github.com/sissbruecker/linkding/pull/1216
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/sissbruecker/linkding/compare/v1.44.1...v1.44.2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.44.1 (11/10/2025)
|
## v1.44.1 (11/10/2025)
|
||||||
|
|
||||||
### What's Changed
|
### What's Changed
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
init:
|
init:
|
||||||
uv sync
|
uv sync
|
||||||
|
[ -d data ] || mkdir data data/assets data/favicons data/previews
|
||||||
uv run manage.py migrate
|
uv run manage.py migrate
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
@@ -34,4 +35,4 @@ e2e:
|
|||||||
uv run pytest bookmarks/tests_e2e -n auto -o "python_files=e2e_test_*.py"
|
uv run pytest bookmarks/tests_e2e -n auto -o "python_files=e2e_test_*.py"
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|||||||
257
bookmarks/frontend/components/dev-tool.js
Normal file
257
bookmarks/frontend/components/dev-tool.js
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import { LitElement, html, css } from "lit";
|
||||||
|
|
||||||
|
class DevTool extends LitElement {
|
||||||
|
static properties = {
|
||||||
|
profile: { type: Object, state: true },
|
||||||
|
formAction: { type: String, attribute: "data-form-action" },
|
||||||
|
csrfToken: { type: String, attribute: "data-csrf-token" },
|
||||||
|
isOpen: { type: Boolean, state: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: var(--btn-primary-bg-color);
|
||||||
|
color: var(--btn-primary-text-color);
|
||||||
|
border: none;
|
||||||
|
padding: var(--unit-2);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--btn-box-shadow);
|
||||||
|
cursor: pointer;
|
||||||
|
height: auto;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: var(--body-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
padding: var(--unit-2);
|
||||||
|
margin-bottom: var(--unit-2);
|
||||||
|
min-width: 220px;
|
||||||
|
box-shadow: var(--box-shadow-lg);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([open]) .overlay {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 var(--unit-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--unit-1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(select) {
|
||||||
|
margin-bottom: var(--unit-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(select) span {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: var(--unit-2) 0;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
static fields = [
|
||||||
|
{
|
||||||
|
type: "select",
|
||||||
|
key: "theme",
|
||||||
|
label: "Theme",
|
||||||
|
options: [
|
||||||
|
{ value: "auto", label: "Auto" },
|
||||||
|
{ value: "light", label: "Light" },
|
||||||
|
{ value: "dark", label: "Dark" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "select",
|
||||||
|
key: "bookmark_date_display",
|
||||||
|
label: "Date",
|
||||||
|
options: [
|
||||||
|
{ value: "relative", label: "Relative" },
|
||||||
|
{ value: "absolute", label: "Absolute" },
|
||||||
|
{ value: "hidden", label: "Hidden" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "select",
|
||||||
|
key: "bookmark_description_display",
|
||||||
|
label: "Description",
|
||||||
|
options: [
|
||||||
|
{ value: "inline", label: "Inline" },
|
||||||
|
{ value: "separate", label: "Separate" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ type: "checkbox", key: "enable_favicons", label: "Favicons" },
|
||||||
|
{ type: "checkbox", key: "enable_preview_images", label: "Preview images" },
|
||||||
|
{ type: "checkbox", key: "display_url", label: "Display URL" },
|
||||||
|
{ type: "checkbox", key: "permanent_notes", label: "Permanent notes" },
|
||||||
|
{ type: "checkbox", key: "collapse_side_panel", label: "Collapse sidebar" },
|
||||||
|
{ type: "checkbox", key: "sticky_pagination", label: "Sticky pagination" },
|
||||||
|
{ type: "checkbox", key: "hide_bundles", label: "Hide bundles" },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.isOpen = false;
|
||||||
|
this.profile = {};
|
||||||
|
this._onOutsideClick = this._onOutsideClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
const profileData = document.getElementById("json_profile");
|
||||||
|
this.profile = JSON.parse(profileData.textContent || "{}");
|
||||||
|
document.addEventListener("click", this._onOutsideClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
document.removeEventListener("click", this._onOutsideClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onOutsideClick(e) {
|
||||||
|
if (!this.contains(e.target) && this.isOpen) {
|
||||||
|
this.isOpen = false;
|
||||||
|
this.removeAttribute("open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_toggle() {
|
||||||
|
this.isOpen = !this.isOpen;
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.setAttribute("open", "");
|
||||||
|
} else {
|
||||||
|
this.removeAttribute("open");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleChange(key, value) {
|
||||||
|
this.profile = { ...this.profile, [key]: value };
|
||||||
|
if (key === "theme") {
|
||||||
|
const themeLinks = document.head.querySelectorAll('link[href*="theme"]');
|
||||||
|
themeLinks.forEach((link) => link.remove());
|
||||||
|
}
|
||||||
|
this._submitForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderField(field) {
|
||||||
|
switch (field.type) {
|
||||||
|
case "checkbox":
|
||||||
|
return html`
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
.checked=${this.profile[field.key] || false}
|
||||||
|
@change=${(e) => this._handleChange(field.key, e.target.checked)}
|
||||||
|
/>
|
||||||
|
${field.label}
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
case "select":
|
||||||
|
return html`
|
||||||
|
<label>
|
||||||
|
<span>${field.label}:</span>
|
||||||
|
<select
|
||||||
|
@change=${(e) => this._handleChange(field.key, e.target.value)}
|
||||||
|
>
|
||||||
|
${field.options.map(
|
||||||
|
(opt) => html`
|
||||||
|
<option
|
||||||
|
value=${opt.value}
|
||||||
|
?selected=${this.profile[field.key] === opt.value}
|
||||||
|
>
|
||||||
|
${opt.label}
|
||||||
|
</option>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
case "divider":
|
||||||
|
return html`<hr />`;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _submitForm() {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("csrfmiddlewaretoken", this.csrfToken);
|
||||||
|
|
||||||
|
// Profile fields
|
||||||
|
for (const [key, value] of Object.entries(this.profile)) {
|
||||||
|
if (typeof value === "boolean" && value) {
|
||||||
|
formData.append(key, "on");
|
||||||
|
} else if (typeof value !== "boolean") {
|
||||||
|
formData.append(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit button name that settings.update expects
|
||||||
|
formData.append("update_profile", "1");
|
||||||
|
|
||||||
|
await fetch(this.formAction, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.searchParams.set("ts", Date.now().toString());
|
||||||
|
window.history.replaceState({}, "", url);
|
||||||
|
|
||||||
|
Turbo.visit(url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<button class="button" @click=${() => this._toggle()}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path
|
||||||
|
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065"
|
||||||
|
/>
|
||||||
|
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="overlay">
|
||||||
|
<h3>Dev Tools</h3>
|
||||||
|
${DevTool.fields.map((field) => this._renderField(field))}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("ld-dev-tool", DevTool);
|
||||||
@@ -3,6 +3,7 @@ import "./components/bookmark-page.js";
|
|||||||
import "./components/clear-button.js";
|
import "./components/clear-button.js";
|
||||||
import "./components/confirm-dropdown.js";
|
import "./components/confirm-dropdown.js";
|
||||||
import "./components/details-modal.js";
|
import "./components/details-modal.js";
|
||||||
|
import "./components/dev-tool.js";
|
||||||
import "./components/dropdown.js";
|
import "./components/dropdown.js";
|
||||||
import "./components/filter-drawer.js";
|
import "./components/filter-drawer.js";
|
||||||
import "./components/form.js";
|
import "./components/form.js";
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
<div>No bookmarks match the current bundle.</div>
|
<div>No bookmarks match the current bundle.</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="mb-4">Found {{ bookmark_list.bookmarks_total }} bookmarks matching this bundle.</div>
|
<div class="mb-4">Found {{ bookmark_list.bookmarks_total }} bookmarks matching this bundle.</div>
|
||||||
{% include 'bookmarks/bookmark_list.html' %}
|
{% with pagination_frame="preview" %}
|
||||||
|
{% include 'bookmarks/bookmark_list.html' %}
|
||||||
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</turbo-frame>
|
</turbo-frame>
|
||||||
|
|||||||
9
bookmarks/templates/shared/dev_tool.html
Normal file
9
bookmarks/templates/shared/dev_tool.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% load shared %}
|
||||||
|
{% if debug and request.user.is_authenticated %}
|
||||||
|
{{ request.user.profile|model_to_dict|json_script:"json_profile" }}
|
||||||
|
<ld-dev-tool id="dev-tool"
|
||||||
|
data-csrf-token="{{ csrf_token }}"
|
||||||
|
data-form-action="{% url 'linkding:settings.update' %}"
|
||||||
|
data-turbo-permanent>
|
||||||
|
</ld-dev-tool>
|
||||||
|
{% endif %}
|
||||||
@@ -46,5 +46,6 @@
|
|||||||
<div class="modals">
|
<div class="modals">
|
||||||
{% block overlays %}{% endblock %}
|
{% block overlays %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
{% include 'shared/dev_tool.html' %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<ul class="pagination">
|
<ul class="pagination">
|
||||||
{% if prev_link %}
|
{% if prev_link %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a href="{{ prev_link }}" tabindex="-1">Previous</a>
|
<a href="{{ prev_link }}"
|
||||||
|
tabindex="-1"
|
||||||
|
data-turbo-frame="{{ pagination_frame }}">Previous</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
@@ -12,7 +14,7 @@
|
|||||||
{% for page_link in page_links %}
|
{% for page_link in page_links %}
|
||||||
{% if page_link %}
|
{% if page_link %}
|
||||||
<li class="page-item {% if page_link.active %}active{% endif %}">
|
<li class="page-item {% if page_link.active %}active{% endif %}">
|
||||||
<a href="{{ page_link.link }}">{{ page_link.number }}</a>
|
<a href="{{ page_link.link }}" data-turbo-frame="{{ pagination_frame }}">{{ page_link.number }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
@@ -22,7 +24,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if next_link %}
|
{% if next_link %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a href="{{ next_link }}" tabindex="-1">Next</a>
|
<a href="{{ next_link }}"
|
||||||
|
tabindex="-1"
|
||||||
|
data-turbo-frame="{{ pagination_frame }}">Next</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item disabled">
|
<li class="page-item disabled">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<turbo-frame id="tag-modal">
|
<turbo-frame id="tag-modal">
|
||||||
<form method="post"
|
<form method="post"
|
||||||
action="{% url 'linkding:tags.edit' tag.id %}"
|
action="{% url 'linkding:tags.edit' tag.id %}?{{ request.GET.urlencode }}"
|
||||||
data-turbo-frame="_top"
|
data-turbo-frame="tag-main"
|
||||||
novalidate>
|
novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<ld-modal class="modal tag-edit-modal active"
|
<ld-modal class="modal tag-edit-modal active"
|
||||||
data-close-url="{% url 'linkding:tags.index' %}"
|
data-close-url="{% url 'linkding:tags.index' %}?{{ request.GET.urlencode }}"
|
||||||
data-turbo-frame="tag-modal">
|
data-turbo-frame="tag-modal">
|
||||||
<div class="modal-overlay" data-close-modal></div>
|
<div class="modal-overlay" data-close-modal></div>
|
||||||
<div class="modal-container" role="dialog" aria-modal="true">
|
<div class="modal-container" role="dialog" aria-modal="true">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="tags-page crud-page">
|
<div class="tags-page crud-page">
|
||||||
|
<turbo-frame id="tag-main">
|
||||||
<main aria-labelledby="main-heading">
|
<main aria-labelledby="main-heading">
|
||||||
<div class="crud-header">
|
<div class="crud-header">
|
||||||
<h1 id="main-heading">Tags</h1>
|
<h1 id="main-heading">Tags</h1>
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
{# Filters #}
|
{# Filters #}
|
||||||
<div class="crud-filters">
|
<div class="crud-filters">
|
||||||
<ld-form data-form-reset>
|
<ld-form data-form-reset>
|
||||||
<form method="get" class="mb-2">
|
<form method="get" class="mb-2" data-turbo-frame="_top">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label text-assistive" for="search">Search tags</label>
|
<label class="form-label text-assistive" for="search">Search tags</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@@ -97,7 +98,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<a class="btn btn-link"
|
<a class="btn btn-link"
|
||||||
href="{% url 'linkding:tags.edit' tag.id %}"
|
href="{% url 'linkding:tags.edit' tag.id %}?{{ request.GET.urlencode }}"
|
||||||
data-turbo-frame="tag-modal">Edit</a>
|
data-turbo-frame="tag-modal">Edit</a>
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
name="delete_tag"
|
name="delete_tag"
|
||||||
@@ -123,6 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</main>
|
</main>
|
||||||
|
<turbo-frame id="tag-modal"></turbo-frame>
|
||||||
|
</turbo-frame>
|
||||||
</div>
|
</div>
|
||||||
<turbo-frame id="tag-modal"></turbo-frame>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ register = template.Library()
|
|||||||
@register.inclusion_tag("shared/pagination.html", name="pagination", takes_context=True)
|
@register.inclusion_tag("shared/pagination.html", name="pagination", takes_context=True)
|
||||||
def pagination(context, page: Page):
|
def pagination(context, page: Page):
|
||||||
request = context["request"]
|
request = context["request"]
|
||||||
|
pagination_frame = context.get("pagination_frame", "_top")
|
||||||
base_url = request.path
|
base_url = request.path
|
||||||
|
|
||||||
# remove page number and details from query parameters
|
# remove page number and details from query parameters
|
||||||
@@ -51,6 +52,7 @@ def pagination(context, page: Page):
|
|||||||
"prev_link": prev_link,
|
"prev_link": prev_link,
|
||||||
"next_link": next_link,
|
"next_link": next_link,
|
||||||
"page_links": page_links,
|
"page_links": page_links,
|
||||||
|
"pagination_frame": pagination_frame,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import markdown
|
|||||||
from bleach.linkifier import DEFAULT_CALLBACKS, Linker
|
from bleach.linkifier import DEFAULT_CALLBACKS, Linker
|
||||||
from bleach_allowlist import markdown_attrs, markdown_tags
|
from bleach_allowlist import markdown_attrs, markdown_tags
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.forms.models import model_to_dict
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from bookmarks import utils
|
from bookmarks import utils
|
||||||
@@ -60,6 +61,12 @@ def humanize_relative_date(value):
|
|||||||
return utils.humanize_relative_date(value)
|
return utils.humanize_relative_date(value)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="model_to_dict")
|
||||||
|
def model_to_dict_filter(value):
|
||||||
|
result = model_to_dict(value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@register.tag
|
@register.tag
|
||||||
def htmlmin(parser, token):
|
def htmlmin(parser, token):
|
||||||
nodelist = parser.parse(("endhtmlmin",))
|
nodelist = parser.parse(("endhtmlmin",))
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ from bookmarks.tests.helpers import BookmarkFactoryMixin
|
|||||||
|
|
||||||
class PaginationTagTest(TestCase, BookmarkFactoryMixin):
|
class PaginationTagTest(TestCase, BookmarkFactoryMixin):
|
||||||
def render_template(
|
def render_template(
|
||||||
self, num_items: int, page_size: int, current_page: int, url: str = "/test"
|
self,
|
||||||
|
num_items: int,
|
||||||
|
page_size: int,
|
||||||
|
current_page: int,
|
||||||
|
url: str = "/test",
|
||||||
|
frame: str = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
rf = RequestFactory()
|
rf = RequestFactory()
|
||||||
request = rf.get(url)
|
request = rf.get(url)
|
||||||
@@ -16,7 +21,11 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
paginator = Paginator(range(0, num_items), page_size)
|
paginator = Paginator(range(0, num_items), page_size)
|
||||||
page = paginator.page(current_page)
|
page = paginator.page(current_page)
|
||||||
|
|
||||||
context = RequestContext(request, {"page": page})
|
context_dict = {"page": page}
|
||||||
|
if frame:
|
||||||
|
context_dict["pagination_frame"] = frame
|
||||||
|
context = RequestContext(request, context_dict)
|
||||||
|
|
||||||
template_to_render = Template("{% load pagination %}{% pagination page %}")
|
template_to_render = Template("{% load pagination %}{% pagination page %}")
|
||||||
return template_to_render.render(context)
|
return template_to_render.render(context)
|
||||||
|
|
||||||
@@ -30,12 +39,14 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
html,
|
html,
|
||||||
)
|
)
|
||||||
|
|
||||||
def assertPrevLink(self, html: str, page_number: int, href: str = None):
|
def assertPrevLink(
|
||||||
|
self, html: str, page_number: int, href: str = None, frame: str = "_top"
|
||||||
|
):
|
||||||
href = href if href else f"/test?page={page_number}"
|
href = href if href else f"/test?page={page_number}"
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
f"""
|
f"""
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a href="{href}" tabindex="-1">Previous</a>
|
<a href="{href}" tabindex="-1" data-turbo-frame="{frame}">Previous</a>
|
||||||
</li>
|
</li>
|
||||||
""",
|
""",
|
||||||
html,
|
html,
|
||||||
@@ -51,12 +62,14 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
html,
|
html,
|
||||||
)
|
)
|
||||||
|
|
||||||
def assertNextLink(self, html: str, page_number: int, href: str = None):
|
def assertNextLink(
|
||||||
|
self, html: str, page_number: int, href: str = None, frame: str = "_top"
|
||||||
|
):
|
||||||
href = href if href else f"/test?page={page_number}"
|
href = href if href else f"/test?page={page_number}"
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
f"""
|
f"""
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a href="{href}" tabindex="-1">Next</a>
|
<a href="{href}" tabindex="-1" data-turbo-frame="{frame}">Next</a>
|
||||||
</li>
|
</li>
|
||||||
""",
|
""",
|
||||||
html,
|
html,
|
||||||
@@ -69,13 +82,14 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
active: bool,
|
active: bool,
|
||||||
count: int = 1,
|
count: int = 1,
|
||||||
href: str = None,
|
href: str = None,
|
||||||
|
frame: str = "_top",
|
||||||
):
|
):
|
||||||
active_class = "active" if active else ""
|
active_class = "active" if active else ""
|
||||||
href = href if href else f"/test?page={page_number}"
|
href = href if href else f"/test?page={page_number}"
|
||||||
self.assertInHTML(
|
self.assertInHTML(
|
||||||
f"""
|
f"""
|
||||||
<li class="page-item {active_class}">
|
<li class="page-item {active_class}">
|
||||||
<a href="{href}">{page_number}</a>
|
<a href="{href}" data-turbo-frame="{frame}">{page_number}</a>
|
||||||
</li>
|
</li>
|
||||||
""",
|
""",
|
||||||
html,
|
html,
|
||||||
@@ -188,3 +202,10 @@ class PaginationTagTest(TestCase, BookmarkFactoryMixin):
|
|||||||
self.assertPageLink(rendered_template, 1, False, href="/test?page=1")
|
self.assertPageLink(rendered_template, 1, False, href="/test?page=1")
|
||||||
self.assertPageLink(rendered_template, 2, True, href="/test?page=2")
|
self.assertPageLink(rendered_template, 2, True, href="/test?page=2")
|
||||||
self.assertNextLink(rendered_template, 3, href="/test?page=3")
|
self.assertNextLink(rendered_template, 3, href="/test?page=3")
|
||||||
|
|
||||||
|
def test_respects_pagination_frame(self):
|
||||||
|
rendered_template = self.render_template(100, 10, 2, frame="my_frame")
|
||||||
|
self.assertPrevLink(rendered_template, 1, frame="my_frame")
|
||||||
|
self.assertPageLink(rendered_template, 1, False, frame="my_frame")
|
||||||
|
self.assertPageLink(rendered_template, 2, True, frame="my_frame")
|
||||||
|
self.assertNextLink(rendered_template, 3, frame="my_frame")
|
||||||
|
|||||||
@@ -90,20 +90,17 @@ class TagsEditViewTestCase(TestCase, BookmarkFactoryMixin):
|
|||||||
tag2.refresh_from_db()
|
tag2.refresh_from_db()
|
||||||
self.assertEqual(tag2.name, "tag1")
|
self.assertEqual(tag2.name, "tag1")
|
||||||
|
|
||||||
def test_update_shows_success_message(self):
|
def test_update_tag_preserves_query_parameters(self):
|
||||||
tag = self.setup_tag(name="old_name")
|
tag = self.setup_tag(name="old_name")
|
||||||
|
|
||||||
response = self.client.post(
|
url = (
|
||||||
reverse("linkding:tags.edit", args=[tag.id]),
|
reverse("linkding:tags.edit", args=[tag.id])
|
||||||
{"name": "new_name"},
|
+ "?search=search&unused=true&page=2&sort=name-desc"
|
||||||
follow=True,
|
|
||||||
)
|
)
|
||||||
|
response = self.client.post(url, {"name": "new_name"})
|
||||||
|
|
||||||
self.assertInHTML(
|
expected_redirect = (
|
||||||
"""
|
reverse("linkding:tags.index")
|
||||||
<div class="toast toast-success" role="alert">
|
+ "?search=search&unused=true&page=2&sort=name-desc"
|
||||||
Tag "new_name" updated successfully.
|
|
||||||
</div>
|
|
||||||
""",
|
|
||||||
response.content.decode(),
|
|
||||||
)
|
)
|
||||||
|
self.assertRedirects(response, expected_redirect)
|
||||||
|
|||||||
@@ -153,24 +153,6 @@ class TagsIndexViewTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
|
|||||||
self.assertRedirects(response, reverse("linkding:tags.index"))
|
self.assertRedirects(response, reverse("linkding:tags.index"))
|
||||||
self.assertFalse(Tag.objects.filter(id=tag.id).exists())
|
self.assertFalse(Tag.objects.filter(id=tag.id).exists())
|
||||||
|
|
||||||
def test_tag_delete_action_shows_success_message(self):
|
|
||||||
tag = self.setup_tag(name="tag_to_delete")
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("linkding:tags.index"), {"delete_tag": tag.id}, follow=True
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
self.assertInHTML(
|
|
||||||
"""
|
|
||||||
<div class="toast toast-success" role="alert">
|
|
||||||
Tag "tag_to_delete" deleted successfully.
|
|
||||||
</div>
|
|
||||||
""",
|
|
||||||
response.content.decode(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_tag_delete_action_preserves_query_parameters(self):
|
def test_tag_delete_action_preserves_query_parameters(self):
|
||||||
tag = self.setup_tag(name="search_tag")
|
tag = self.setup_tag(name="search_tag")
|
||||||
|
|
||||||
|
|||||||
@@ -113,9 +113,6 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
|
|||||||
# Verify modal is closed
|
# Verify modal is closed
|
||||||
expect(modal).not_to_be_visible()
|
expect(modal).not_to_be_visible()
|
||||||
|
|
||||||
# Verify the success message is shown
|
|
||||||
self.verify_success_message('Tag "new-name" updated successfully.')
|
|
||||||
|
|
||||||
# Verify the updated tag is shown in the list
|
# Verify the updated tag is shown in the list
|
||||||
expect(self.locate_tag_row("new-name")).to_be_visible()
|
expect(self.locate_tag_row("new-name")).to_be_visible()
|
||||||
expect(self.locate_tag_row("old-name")).not_to_be_visible()
|
expect(self.locate_tag_row("old-name")).not_to_be_visible()
|
||||||
@@ -157,6 +154,89 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
|
|||||||
tag.refresh_from_db()
|
tag.refresh_from_db()
|
||||||
self.assertEqual(tag.name, "tag-to-edit")
|
self.assertEqual(tag.name, "tag-to-edit")
|
||||||
|
|
||||||
|
def test_edit_tag_preserves_query_and_scroll_position(self):
|
||||||
|
# Create enough tags to have multiple pages (50 per page)
|
||||||
|
for i in range(70):
|
||||||
|
self.setup_tag(name=f"test-tag-{i:02d}")
|
||||||
|
|
||||||
|
# Open tags page 2 with search query
|
||||||
|
url = reverse("linkding:tags.index") + "?search=test&page=2"
|
||||||
|
self.open(url)
|
||||||
|
|
||||||
|
# Verify we're on page 2
|
||||||
|
expect(self.locate_tag_row("test-tag-00")).not_to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-50")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-60")).to_be_visible()
|
||||||
|
|
||||||
|
# Scroll down
|
||||||
|
self.page.evaluate("window.scrollTo(0, 300)")
|
||||||
|
initial_scroll = self.page.evaluate("window.scrollY")
|
||||||
|
self.assertGreater(initial_scroll, 0)
|
||||||
|
|
||||||
|
# Edit tag
|
||||||
|
tag_row = self.locate_tag_row("test-tag-55")
|
||||||
|
tag_row.get_by_role("link", name="Edit").click()
|
||||||
|
|
||||||
|
modal = self.locate_tag_modal()
|
||||||
|
|
||||||
|
name_input = modal.get_by_label("Name")
|
||||||
|
name_input.fill("test-tag-55-edited")
|
||||||
|
|
||||||
|
modal.get_by_text("Save").click()
|
||||||
|
|
||||||
|
expect(modal).not_to_be_visible()
|
||||||
|
|
||||||
|
# Verify query parameters and scroll position are preserved
|
||||||
|
current_url = self.page.url
|
||||||
|
self.assertIn("search=test", current_url)
|
||||||
|
self.assertIn("page=2", current_url)
|
||||||
|
|
||||||
|
expect(self.locate_tag_row("test-tag-00")).not_to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-50")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-55-edited")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-60")).to_be_visible()
|
||||||
|
|
||||||
|
final_scroll = self.page.evaluate("window.scrollY")
|
||||||
|
self.assertEqual(initial_scroll, final_scroll)
|
||||||
|
|
||||||
|
def test_delete_tag_preserves_query_and_scroll_position(self):
|
||||||
|
# Create enough tags to have multiple pages (50 per page)
|
||||||
|
for i in range(70):
|
||||||
|
self.setup_tag(name=f"test-tag-{i:02d}")
|
||||||
|
|
||||||
|
# Open tags page 2 with search query
|
||||||
|
url = reverse("linkding:tags.index") + "?search=test&page=2"
|
||||||
|
self.open(url)
|
||||||
|
|
||||||
|
# Verify we're on page 2
|
||||||
|
expect(self.locate_tag_row("test-tag-00")).not_to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-50")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-55")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-60")).to_be_visible()
|
||||||
|
|
||||||
|
# Scroll down
|
||||||
|
self.page.evaluate("window.scrollTo(0, 300)")
|
||||||
|
initial_scroll = self.page.evaluate("window.scrollY")
|
||||||
|
self.assertGreater(initial_scroll, 0)
|
||||||
|
|
||||||
|
# Delete tag
|
||||||
|
tag_row = self.locate_tag_row("test-tag-55")
|
||||||
|
tag_row.get_by_role("button", name="Remove").click()
|
||||||
|
self.locate_confirm_dialog().get_by_text("Confirm").click()
|
||||||
|
|
||||||
|
# Verify query parameters and scroll position are preserved
|
||||||
|
current_url = self.page.url
|
||||||
|
self.assertIn("search=test", current_url)
|
||||||
|
self.assertIn("page=2", current_url)
|
||||||
|
|
||||||
|
expect(self.locate_tag_row("test-tag-00")).not_to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-50")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-55")).not_to_be_visible()
|
||||||
|
expect(self.locate_tag_row("test-tag-60")).to_be_visible()
|
||||||
|
|
||||||
|
final_scroll = self.page.evaluate("window.scrollY")
|
||||||
|
self.assertEqual(initial_scroll, final_scroll)
|
||||||
|
|
||||||
def test_merge_tags(self):
|
def test_merge_tags(self):
|
||||||
target_tag = self.setup_tag(name="target-tag")
|
target_tag = self.setup_tag(name="target-tag")
|
||||||
merge_tag1 = self.setup_tag(name="merge-tag1")
|
merge_tag1 = self.setup_tag(name="merge-tag1")
|
||||||
@@ -254,3 +334,28 @@ class TagManagementE2ETestCase(LinkdingE2ETestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Tag.objects.filter(owner=self.get_or_create_test_user()).count(), 2
|
Tag.objects.filter(owner=self.get_or_create_test_user()).count(), 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_search_updates_url_query_params(self):
|
||||||
|
self.setup_tag(name="python")
|
||||||
|
self.setup_tag(name="javascript")
|
||||||
|
self.setup_tag(name="typescript")
|
||||||
|
|
||||||
|
self.open(reverse("linkding:tags.index"))
|
||||||
|
|
||||||
|
# Verify all tags are visible initially
|
||||||
|
expect(self.locate_tag_row("python")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("javascript")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("typescript")).to_be_visible()
|
||||||
|
|
||||||
|
# Enter search term and submit
|
||||||
|
search_input = self.page.get_by_placeholder("Search tags...")
|
||||||
|
search_input.fill("script")
|
||||||
|
self.page.get_by_role("button", name="Search").click()
|
||||||
|
|
||||||
|
# Wait for filtered results to appear
|
||||||
|
expect(self.locate_tag_row("python")).not_to_be_visible()
|
||||||
|
expect(self.locate_tag_row("javascript")).to_be_visible()
|
||||||
|
expect(self.locate_tag_row("typescript")).to_be_visible()
|
||||||
|
|
||||||
|
# Verify URL contains search query param
|
||||||
|
self.assertIn("search=script", self.page.url)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.urls import reverse
|
|||||||
from bookmarks.forms import TagForm, TagMergeForm
|
from bookmarks.forms import TagForm, TagMergeForm
|
||||||
from bookmarks.models import Bookmark, Tag
|
from bookmarks.models import Bookmark, Tag
|
||||||
from bookmarks.type_defs import HttpRequest
|
from bookmarks.type_defs import HttpRequest
|
||||||
|
from bookmarks.utils import redirect_with_query
|
||||||
from bookmarks.views import turbo
|
from bookmarks.views import turbo
|
||||||
|
|
||||||
|
|
||||||
@@ -18,15 +19,8 @@ def tags_index(request: HttpRequest):
|
|||||||
if request.method == "POST" and "delete_tag" in request.POST:
|
if request.method == "POST" and "delete_tag" in request.POST:
|
||||||
tag_id = request.POST.get("delete_tag")
|
tag_id = request.POST.get("delete_tag")
|
||||||
tag = get_object_or_404(Tag, id=tag_id, owner=request.user)
|
tag = get_object_or_404(Tag, id=tag_id, owner=request.user)
|
||||||
tag_name = tag.name
|
|
||||||
tag.delete()
|
tag.delete()
|
||||||
messages.success(request, f'Tag "{tag_name}" deleted successfully.')
|
return redirect_with_query(request, reverse("linkding:tags.index"))
|
||||||
|
|
||||||
redirect_url = reverse("linkding:tags.index")
|
|
||||||
if request.GET:
|
|
||||||
redirect_url += "?" + request.GET.urlencode()
|
|
||||||
|
|
||||||
return HttpResponseRedirect(redirect_url)
|
|
||||||
|
|
||||||
search = request.GET.get("search", "").strip()
|
search = request.GET.get("search", "").strip()
|
||||||
unused_only = request.GET.get("unused", "") == "true"
|
unused_only = request.GET.get("unused", "") == "true"
|
||||||
@@ -100,8 +94,7 @@ def tag_edit(request: HttpRequest, tag_id: int):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
form.save()
|
||||||
messages.success(request, f'Tag "{tag.name}" updated successfully.')
|
return redirect_with_query(request, reverse("linkding:tags.index"))
|
||||||
return HttpResponseRedirect(reverse("linkding:tags.index"))
|
|
||||||
else:
|
else:
|
||||||
return turbo.stream(
|
return turbo.stream(
|
||||||
turbo.replace(
|
turbo.replace(
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ Linkding can automatically create HTML snapshots whenever a bookmark is added. T
|
|||||||
|
|
||||||
The snapshots are created using [singlefile-cli](https://github.com/gildas-lormeau/single-file-cli), which effectively runs a headless Chromium instance on the server to convert the web page into a single HTML file. Linkding will also load the [uBlock Origin Lite extension](https://github.com/uBlockOrigin/uBOL-home) into Chromium to attempt to block ads and other unwanted content.
|
The snapshots are created using [singlefile-cli](https://github.com/gildas-lormeau/single-file-cli), which effectively runs a headless Chromium instance on the server to convert the web page into a single HTML file. Linkding will also load the [uBlock Origin Lite extension](https://github.com/uBlockOrigin/uBOL-home) into Chromium to attempt to block ads and other unwanted content.
|
||||||
|
|
||||||
<!--
|
|
||||||
When bookmarking a URL that points directly to a PDF file, linkding will download the PDF instead of creating an HTML snapshot. This happens automatically based on the content type of the URL, and the downloaded PDF will be stored as an asset alongside the bookmark, just like HTML snapshots.
|
When bookmarking a URL that points directly to a PDF file, linkding will download the PDF instead of creating an HTML snapshot. This happens automatically based on the content type of the URL, and the downloaded PDF will be stored as an asset alongside the bookmark, just like HTML snapshots.
|
||||||
-->
|
|
||||||
|
|
||||||
This method is fairly easy to set up, but also has several downsides:
|
This method is fairly easy to set up, but also has several downsides:
|
||||||
- The Docker image is significantly larger than the base image, as it includes a Chromium installation.
|
- The Docker image is significantly larger than the base image, as it includes a Chromium installation.
|
||||||
|
|||||||
@@ -3,6 +3,17 @@ import terser from '@rollup/plugin-terser';
|
|||||||
|
|
||||||
const production = !process.env.ROLLUP_WATCH;
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
|
// Custom plugin to exclude dev-tool.js from production builds
|
||||||
|
const excludeDevTool = {
|
||||||
|
name: 'exclude-dev-tool',
|
||||||
|
load(id) {
|
||||||
|
if (production && id.endsWith('dev-tool.js')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'bookmarks/frontend/index.js',
|
input: 'bookmarks/frontend/index.js',
|
||||||
output: {
|
output: {
|
||||||
@@ -13,6 +24,7 @@ export default {
|
|||||||
file: 'bookmarks/static/bundle.js',
|
file: 'bookmarks/static/bundle.js',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
excludeDevTool,
|
||||||
// If you have external dependencies installed from
|
// If you have external dependencies installed from
|
||||||
// npm, you'll most likely need these plugins. In
|
// npm, you'll most likely need these plugins. In
|
||||||
// some cases you'll need additional configuration —
|
// some cases you'll need additional configuration —
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -455,7 +455,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linkding"
|
name = "linkding"
|
||||||
version = "1.44.2"
|
version = "1.45.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "beautifulsoup4" },
|
{ name = "beautifulsoup4" },
|
||||||
|
|||||||
Reference in New Issue
Block a user