mirror of
https://github.com/sissbruecker/linkding
synced 2024-11-22 11:23:02 +00:00
Refresh file list when there are queued snapshots (#697)
* add destroy hook * refresh details modal in interval * refactor to refresh assets list * disable create snapshot button when there is a pending snapshot
This commit is contained in:
parent
df9f0095cc
commit
1b7731e506
16 changed files with 267 additions and 85 deletions
|
@ -1,8 +1,8 @@
|
||||||
import { registerBehavior } from "./index";
|
import { Behavior, registerBehavior } from "./index";
|
||||||
|
|
||||||
class BookmarkItem {
|
class BookmarkItem extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
this.element = element;
|
super(element);
|
||||||
|
|
||||||
// Toggle notes
|
// Toggle notes
|
||||||
const notesToggle = element.querySelector(".toggle-notes");
|
const notesToggle = element.querySelector(".toggle-notes");
|
||||||
|
@ -13,9 +13,11 @@ class BookmarkItem {
|
||||||
// Add tooltip to title if it is truncated
|
// Add tooltip to title if it is truncated
|
||||||
const titleAnchor = element.querySelector(".title > a");
|
const titleAnchor = element.querySelector(".title > a");
|
||||||
const titleSpan = titleAnchor.querySelector("span");
|
const titleSpan = titleAnchor.querySelector("span");
|
||||||
|
requestAnimationFrame(() => {
|
||||||
if (titleSpan.offsetWidth > titleAnchor.offsetWidth) {
|
if (titleSpan.offsetWidth > titleAnchor.offsetWidth) {
|
||||||
titleAnchor.dataset.tooltip = titleSpan.textContent;
|
titleAnchor.dataset.tooltip = titleSpan.textContent;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onToggleNotes(event) {
|
onToggleNotes(event) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { registerBehavior } from "./index";
|
import { Behavior, registerBehavior } from "./index";
|
||||||
|
|
||||||
class BulkEdit {
|
class BulkEdit extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
this.element = element;
|
super(element);
|
||||||
|
|
||||||
this.active = false;
|
this.active = false;
|
||||||
|
|
||||||
this.onToggleActive = this.onToggleActive.bind(this);
|
this.onToggleActive = this.onToggleActive.bind(this);
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
import { registerBehavior } from "./index";
|
import { Behavior, registerBehavior } from "./index";
|
||||||
|
|
||||||
class ConfirmButtonBehavior {
|
class ConfirmButtonBehavior extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
const button = element;
|
super(element);
|
||||||
button.dataset.type = button.type;
|
element.dataset.type = element.type;
|
||||||
button.dataset.name = button.name;
|
element.dataset.name = element.name;
|
||||||
button.dataset.value = button.value;
|
element.dataset.value = element.value;
|
||||||
button.removeAttribute("type");
|
element.removeAttribute("type");
|
||||||
button.removeAttribute("name");
|
element.removeAttribute("name");
|
||||||
button.removeAttribute("value");
|
element.removeAttribute("value");
|
||||||
button.addEventListener("click", this.onClick.bind(this));
|
element.addEventListener("click", this.onClick.bind(this));
|
||||||
this.button = button;
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
Behavior.interacting = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(event) {
|
onClick(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
Behavior.interacting = true;
|
||||||
|
|
||||||
const container = document.createElement("span");
|
const container = document.createElement("span");
|
||||||
container.className = "confirmation";
|
container.className = "confirmation";
|
||||||
|
|
||||||
const icon = this.button.getAttribute("ld-confirm-icon");
|
const icon = this.element.getAttribute("ld-confirm-icon");
|
||||||
if (icon) {
|
if (icon) {
|
||||||
const iconElement = document.createElementNS(
|
const iconElement = document.createElementNS(
|
||||||
"http://www.w3.org/2000/svg",
|
"http://www.w3.org/2000/svg",
|
||||||
|
@ -31,27 +35,27 @@ class ConfirmButtonBehavior {
|
||||||
container.append(iconElement);
|
container.append(iconElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
const question = this.button.getAttribute("ld-confirm-question");
|
const question = this.element.getAttribute("ld-confirm-question");
|
||||||
if (question) {
|
if (question) {
|
||||||
const questionElement = document.createElement("span");
|
const questionElement = document.createElement("span");
|
||||||
questionElement.innerText = question;
|
questionElement.innerText = question;
|
||||||
container.append(question);
|
container.append(question);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttonClasses = Array.from(this.button.classList.values())
|
const buttonClasses = Array.from(this.element.classList.values())
|
||||||
.filter((cls) => cls.startsWith("btn"))
|
.filter((cls) => cls.startsWith("btn"))
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
const cancelButton = document.createElement(this.button.nodeName);
|
const cancelButton = document.createElement(this.element.nodeName);
|
||||||
cancelButton.type = "button";
|
cancelButton.type = "button";
|
||||||
cancelButton.innerText = question ? "No" : "Cancel";
|
cancelButton.innerText = question ? "No" : "Cancel";
|
||||||
cancelButton.className = `${buttonClasses} mr-1`;
|
cancelButton.className = `${buttonClasses} mr-1`;
|
||||||
cancelButton.addEventListener("click", this.reset.bind(this));
|
cancelButton.addEventListener("click", this.reset.bind(this));
|
||||||
|
|
||||||
const confirmButton = document.createElement(this.button.nodeName);
|
const confirmButton = document.createElement(this.element.nodeName);
|
||||||
confirmButton.type = this.button.dataset.type;
|
confirmButton.type = this.element.dataset.type;
|
||||||
confirmButton.name = this.button.dataset.name;
|
confirmButton.name = this.element.dataset.name;
|
||||||
confirmButton.value = this.button.dataset.value;
|
confirmButton.value = this.element.dataset.value;
|
||||||
confirmButton.innerText = question ? "Yes" : "Confirm";
|
confirmButton.innerText = question ? "Yes" : "Confirm";
|
||||||
confirmButton.className = buttonClasses;
|
confirmButton.className = buttonClasses;
|
||||||
confirmButton.addEventListener("click", this.reset.bind(this));
|
confirmButton.addEventListener("click", this.reset.bind(this));
|
||||||
|
@ -59,14 +63,15 @@ class ConfirmButtonBehavior {
|
||||||
container.append(cancelButton, confirmButton);
|
container.append(cancelButton, confirmButton);
|
||||||
this.container = container;
|
this.container = container;
|
||||||
|
|
||||||
this.button.before(container);
|
this.element.before(container);
|
||||||
this.button.classList.add("d-none");
|
this.element.classList.add("d-none");
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
Behavior.interacting = false;
|
||||||
this.container.remove();
|
this.container.remove();
|
||||||
this.button.classList.remove("d-none");
|
this.element.classList.remove("d-none");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { registerBehavior } from "./index";
|
import { Behavior, registerBehavior } from "./index";
|
||||||
|
|
||||||
class DropdownBehavior {
|
class DropdownBehavior extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
this.element = element;
|
super(element);
|
||||||
this.opened = false;
|
this.opened = false;
|
||||||
this.onOutsideClick = this.onOutsideClick.bind(this);
|
this.onOutsideClick = this.onOutsideClick.bind(this);
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,31 @@
|
||||||
import { fireEvents, registerBehavior, swap } from "./index";
|
import { Behavior, fireEvents, registerBehavior, swap } from "./index";
|
||||||
|
|
||||||
class FetchBehavior {
|
class FetchBehavior extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
this.element = element;
|
super(element);
|
||||||
const eventName = element.getAttribute("ld-on");
|
|
||||||
|
|
||||||
element.addEventListener(eventName, this.onFetch.bind(this));
|
const eventName = element.getAttribute("ld-on");
|
||||||
|
const interval = parseInt(element.getAttribute("ld-interval")) * 1000;
|
||||||
|
|
||||||
|
this.onFetch = this.onFetch.bind(this);
|
||||||
|
this.onInterval = this.onInterval.bind(this);
|
||||||
|
|
||||||
|
element.addEventListener(eventName, this.onFetch);
|
||||||
|
if (interval) {
|
||||||
|
this.intervalId = setInterval(this.onInterval, interval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onFetch(event) {
|
destroy() {
|
||||||
event.preventDefault();
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onFetch(maybeEvent) {
|
||||||
|
if (maybeEvent) {
|
||||||
|
maybeEvent.preventDefault();
|
||||||
|
}
|
||||||
const url = this.element.getAttribute("ld-fetch");
|
const url = this.element.getAttribute("ld-fetch");
|
||||||
const html = await fetch(url).then((response) => response.text());
|
const html = await fetch(url).then((response) => response.text());
|
||||||
|
|
||||||
|
@ -20,6 +36,13 @@ class FetchBehavior {
|
||||||
const events = this.element.getAttribute("ld-fire");
|
const events = this.element.getAttribute("ld-fire");
|
||||||
fireEvents(events);
|
fireEvents(events);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInterval() {
|
||||||
|
if (Behavior.interacting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.onFetch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerBehavior("ld-fetch", FetchBehavior);
|
registerBehavior("ld-fetch", FetchBehavior);
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { fireEvents, registerBehavior } from "./index";
|
import { Behavior, fireEvents, registerBehavior } from "./index";
|
||||||
|
|
||||||
class FormBehavior {
|
class FormBehavior extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
this.element = element;
|
super(element);
|
||||||
|
|
||||||
element.addEventListener("submit", this.onSubmit.bind(this));
|
element.addEventListener("submit", this.onSubmit.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +29,10 @@ class FormBehavior {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AutoSubmitBehavior {
|
class AutoSubmitBehavior extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
|
super(element);
|
||||||
|
|
||||||
element.addEventListener("change", () => {
|
element.addEventListener("change", () => {
|
||||||
const form = element.closest("form");
|
const form = element.closest("form");
|
||||||
form.dispatchEvent(new Event("submit", { cancelable: true }));
|
form.dispatchEvent(new Event("submit", { cancelable: true }));
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { registerBehavior } from "./index";
|
import { Behavior, registerBehavior } from "./index";
|
||||||
|
|
||||||
|
class GlobalShortcuts extends Behavior {
|
||||||
|
constructor(element) {
|
||||||
|
super(element);
|
||||||
|
|
||||||
class GlobalShortcuts {
|
|
||||||
constructor() {
|
|
||||||
document.addEventListener("keydown", this.onKeyDown.bind(this));
|
document.addEventListener("keydown", this.onKeyDown.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,35 @@
|
||||||
const behaviorRegistry = {};
|
const behaviorRegistry = {};
|
||||||
|
const debug = false;
|
||||||
|
|
||||||
|
const mutationObserver = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
mutation.removedNodes.forEach((node) => {
|
||||||
|
if (node instanceof HTMLElement && !node.isConnected) {
|
||||||
|
destroyBehaviors(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mutation.addedNodes.forEach((node) => {
|
||||||
|
if (node instanceof HTMLElement && node.isConnected) {
|
||||||
|
applyBehaviors(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mutationObserver.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export class Behavior {
|
||||||
|
constructor(element) {
|
||||||
|
this.element = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior.interacting = false;
|
||||||
|
|
||||||
export function registerBehavior(name, behavior) {
|
export function registerBehavior(name, behavior) {
|
||||||
behaviorRegistry[name] = behavior;
|
behaviorRegistry[name] = behavior;
|
||||||
|
@ -33,6 +64,34 @@ export function applyBehaviors(container, behaviorNames = null) {
|
||||||
|
|
||||||
const behaviorInstance = new behavior(element);
|
const behaviorInstance = new behavior(element);
|
||||||
element.__behaviors.push(behaviorInstance);
|
element.__behaviors.push(behaviorInstance);
|
||||||
|
if (debug) {
|
||||||
|
console.log(
|
||||||
|
`[Behavior] ${behaviorInstance.constructor.name} initialized`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function destroyBehaviors(element) {
|
||||||
|
const behaviorNames = Object.keys(behaviorRegistry);
|
||||||
|
|
||||||
|
behaviorNames.forEach((behaviorName) => {
|
||||||
|
const elements = Array.from(element.querySelectorAll(`[${behaviorName}]`));
|
||||||
|
elements.push(element);
|
||||||
|
|
||||||
|
elements.forEach((element) => {
|
||||||
|
if (!element.__behaviors) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.__behaviors.forEach((behavior) => {
|
||||||
|
behavior.destroy();
|
||||||
|
if (debug) {
|
||||||
|
console.log(`[Behavior] ${behavior.constructor.name} destroyed`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
delete element.__behaviors;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -63,10 +122,11 @@ export function swap(element, html, options) {
|
||||||
break;
|
break;
|
||||||
case "innerHTML":
|
case "innerHTML":
|
||||||
default:
|
default:
|
||||||
targetElement.innerHTML = "";
|
Array.from(targetElement.children).forEach((child) => {
|
||||||
|
child.remove();
|
||||||
|
});
|
||||||
targetElement.append(...contents);
|
targetElement.append(...contents);
|
||||||
}
|
}
|
||||||
contents.forEach((content) => applyBehaviors(content));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fireEvents(events) {
|
export function fireEvents(events) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { registerBehavior } from "./index";
|
import { Behavior, registerBehavior } from "./index";
|
||||||
|
|
||||||
class ModalBehavior {
|
class ModalBehavior extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
this.element = element;
|
super(element);
|
||||||
|
|
||||||
const modalOverlay = element.querySelector(".modal-overlay");
|
const modalOverlay = element.querySelector(".modal-overlay");
|
||||||
const closeButton = element.querySelector("button.close");
|
const closeButton = element.querySelector("button.close");
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { registerBehavior } from "./index";
|
import { Behavior, registerBehavior } from "./index";
|
||||||
import TagAutoCompleteComponent from "../components/TagAutocomplete.svelte";
|
import TagAutoCompleteComponent from "../components/TagAutocomplete.svelte";
|
||||||
import { ApiClient } from "../api";
|
import { ApiClient } from "../api";
|
||||||
|
|
||||||
class TagAutocomplete {
|
class TagAutocomplete extends Behavior {
|
||||||
constructor(element) {
|
constructor(element) {
|
||||||
|
super(element);
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
const apiBaseUrl = document.documentElement.dataset.apiBaseUrl || "";
|
const apiBaseUrl = document.documentElement.dataset.apiBaseUrl || "";
|
||||||
const apiClient = new ApiClient(apiBaseUrl);
|
const apiClient = new ApiClient(apiBaseUrl);
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
<div {% if details.has_pending_assets %}
|
||||||
|
ld-fetch="{% url 'bookmarks:details_assets' details.bookmark.id %}"
|
||||||
|
ld-interval="5" ld-target="self|outerHTML"
|
||||||
|
{% endif %}>
|
||||||
{% if details.assets %}
|
{% if details.assets %}
|
||||||
<div class="assets">
|
<div class="assets">
|
||||||
{% for asset in details.assets %}
|
{% for asset in details.assets %}
|
||||||
|
@ -32,6 +36,9 @@
|
||||||
|
|
||||||
{% if details.is_editable %}
|
{% if details.is_editable %}
|
||||||
<div class="assets-actions">
|
<div class="assets-actions">
|
||||||
<button type="submit" name="create_snapshot" class="btn btn-link">Create HTML snapshot</button>
|
<button type="submit" name="create_snapshot" class="btn btn-link"
|
||||||
|
{% if details.has_pending_assets %}disabled{% endif %}>Create HTML snapshot
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
@ -105,6 +106,12 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||||
def test_access_with_sharing(self):
|
def test_access_with_sharing(self):
|
||||||
self.details_route_sharing_access_test(self.get_view_name(), True)
|
self.details_route_sharing_access_test(self.get_view_name(), True)
|
||||||
|
|
||||||
|
def test_assets_access(self):
|
||||||
|
self.details_route_access_test("bookmarks:details_assets", True)
|
||||||
|
|
||||||
|
def test_assets_access_with_sharing(self):
|
||||||
|
self.details_route_sharing_access_test("bookmarks:details_assets", True)
|
||||||
|
|
||||||
def test_displays_title(self):
|
def test_displays_title(self):
|
||||||
# with title
|
# with title
|
||||||
bookmark = self.setup_bookmark(title="Test title")
|
bookmark = self.setup_bookmark(title="Test title")
|
||||||
|
@ -753,6 +760,27 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
self.assertTrue(BookmarkAsset.objects.filter(id=asset.id).exists())
|
self.assertTrue(BookmarkAsset.objects.filter(id=asset.id).exists())
|
||||||
|
|
||||||
|
@override_settings(LD_ENABLE_SNAPSHOTS=True)
|
||||||
|
def test_assets_refresh_when_having_pending_asset(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
asset = self.setup_asset(bookmark, status=BookmarkAsset.STATUS_COMPLETE)
|
||||||
|
fetch_url = reverse("bookmarks:details_assets", args=[bookmark.id])
|
||||||
|
|
||||||
|
# no pending asset
|
||||||
|
soup = self.get_details(bookmark)
|
||||||
|
files_section = self.find_section(soup, "Files")
|
||||||
|
assets_wrapper = files_section.find("div", {"ld-fetch": fetch_url})
|
||||||
|
self.assertIsNone(assets_wrapper)
|
||||||
|
|
||||||
|
# with pending asset
|
||||||
|
asset.status = BookmarkAsset.STATUS_PENDING
|
||||||
|
asset.save()
|
||||||
|
|
||||||
|
soup = self.get_details(bookmark)
|
||||||
|
files_section = self.find_section(soup, "Files")
|
||||||
|
assets_wrapper = files_section.find("div", {"ld-fetch": fetch_url})
|
||||||
|
self.assertIsNotNone(assets_wrapper)
|
||||||
|
|
||||||
@override_settings(LD_ENABLE_SNAPSHOTS=True)
|
@override_settings(LD_ENABLE_SNAPSHOTS=True)
|
||||||
def test_create_snapshot(self):
|
def test_create_snapshot(self):
|
||||||
with patch.object(
|
with patch.object(
|
||||||
|
@ -765,3 +793,27 @@ class BookmarkDetailsModalTestCase(TestCase, BookmarkFactoryMixin, HtmlTestMixin
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
self.assertEqual(bookmark.bookmarkasset_set.count(), 1)
|
self.assertEqual(bookmark.bookmarkasset_set.count(), 1)
|
||||||
|
|
||||||
|
@override_settings(LD_ENABLE_SNAPSHOTS=True)
|
||||||
|
def test_create_snapshot_is_disabled_when_having_pending_asset(self):
|
||||||
|
bookmark = self.setup_bookmark()
|
||||||
|
asset = self.setup_asset(bookmark, status=BookmarkAsset.STATUS_COMPLETE)
|
||||||
|
|
||||||
|
# no pending asset
|
||||||
|
soup = self.get_details(bookmark)
|
||||||
|
files_section = self.find_section(soup, "Files")
|
||||||
|
create_button = files_section.find(
|
||||||
|
"button", string=re.compile("Create HTML snapshot")
|
||||||
|
)
|
||||||
|
self.assertFalse(create_button.has_attr("disabled"))
|
||||||
|
|
||||||
|
# with pending asset
|
||||||
|
asset.status = BookmarkAsset.STATUS_PENDING
|
||||||
|
asset.save()
|
||||||
|
|
||||||
|
soup = self.get_details(bookmark)
|
||||||
|
files_section = self.find_section(soup, "Files")
|
||||||
|
create_button = files_section.find(
|
||||||
|
"button", string=re.compile("Create HTML snapshot")
|
||||||
|
)
|
||||||
|
self.assertTrue(create_button.has_attr("disabled"))
|
||||||
|
|
|
@ -44,6 +44,11 @@ urlpatterns = [
|
||||||
views.bookmarks.details_modal,
|
views.bookmarks.details_modal,
|
||||||
name="details_modal",
|
name="details_modal",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"bookmarks/<int:bookmark_id>/details_assets",
|
||||||
|
views.bookmarks.details_assets,
|
||||||
|
name="details_assets",
|
||||||
|
),
|
||||||
# Assets
|
# Assets
|
||||||
path(
|
path(
|
||||||
"assets/<int:asset_id>",
|
"assets/<int:asset_id>",
|
||||||
|
|
|
@ -172,6 +172,10 @@ def details_modal(request, bookmark_id: int):
|
||||||
return _details(request, bookmark_id, "bookmarks/details_modal.html")
|
return _details(request, bookmark_id, "bookmarks/details_modal.html")
|
||||||
|
|
||||||
|
|
||||||
|
def details_assets(request, bookmark_id: int):
|
||||||
|
return _details(request, bookmark_id, "bookmarks/details/assets.html")
|
||||||
|
|
||||||
|
|
||||||
def convert_tag_string(tag_string: str):
|
def convert_tag_string(tag_string: str):
|
||||||
# Tag strings coming from inputs are space-separated, however services.bookmarks functions expect comma-separated
|
# Tag strings coming from inputs are space-separated, however services.bookmarks functions expect comma-separated
|
||||||
# strings
|
# strings
|
||||||
|
|
|
@ -390,3 +390,6 @@ class BookmarkDetailsContext:
|
||||||
self.assets = [
|
self.assets = [
|
||||||
BookmarkAssetItem(asset) for asset in bookmark.bookmarkasset_set.all()
|
BookmarkAssetItem(asset) for asset in bookmark.bookmarkasset_set.all()
|
||||||
]
|
]
|
||||||
|
self.has_pending_assets = any(
|
||||||
|
asset.status == BookmarkAsset.STATUS_PENDING for asset in self.assets
|
||||||
|
)
|
||||||
|
|
|
@ -26,6 +26,20 @@
|
||||||
"required": false
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "ld-select",
|
||||||
|
"description": "The content element(s) to select from the fetched content, for example `#main-content`",
|
||||||
|
"value": {
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ld-interval",
|
||||||
|
"description": "Automatically fetches the content of the given URL at the given interval, in seconds",
|
||||||
|
"value": {
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "ld-fire",
|
"name": "ld-fire",
|
||||||
"description": "Fires one or more events once a behavior, such as ld-fetch or ld-form, is finished",
|
"description": "Fires one or more events once a behavior, such as ld-fetch or ld-form, is finished",
|
||||||
|
|
Loading…
Reference in a new issue