From 5b4f997f3d3e32e75565a2cae903018cfeac9e4f Mon Sep 17 00:00:00 2001 From: rubenwardy Date: Sat, 19 Aug 2023 02:31:40 +0100 Subject: [PATCH] Add ability to add packages from collection page --- app/blueprints/collections/__init__.py | 31 ++++- app/public/static/collection_editor.js | 147 ++++++++++++++++++++- app/templates/collections/create_edit.html | 63 +++++---- 3 files changed, 203 insertions(+), 38 deletions(-) diff --git a/app/blueprints/collections/__init__.py b/app/blueprints/collections/__init__.py index dd2ac047..86e6a0f7 100644 --- a/app/blueprints/collections/__init__.py +++ b/app/blueprints/collections/__init__.py @@ -122,7 +122,7 @@ def create_edit(author=None, name=None): if collection: for item in collection.items: form.descriptions.append_entry(item.description) - form.package_ids.append_entry(item.package.id) + form.package_ids.append_entry(item.package.get_id()) form.package_removed.append_entry("0") else: form.name = None @@ -177,14 +177,31 @@ def handle_create_edit(collection: Collection, form: CollectionForm, form.populate_obj(collection) collection.name = name + order = 1 for i, package_id in enumerate(form.package_ids): - item = next((x for x in collection.items if str(x.package.id) == package_id.data), None) - if item is None: - continue + link = next((x for x in collection.items if str(x.package.get_id()) == package_id.data), None) + to_delete = form.package_removed[i].data == "1" + if link is None: + if to_delete: + continue - item.description = form.descriptions[i].data - if form.package_removed[i].data == "1": - db.session.delete(item) + package = Package.get_by_key(package_id.data) + if package is None: + abort(400) + + link = CollectionPackage() + link.package = package + link.collection = collection + link.description = form.descriptions[i].data + link.order = order + order += 1 + db.session.add(link) + elif to_delete: + db.session.delete(link) + else: + link.description = form.descriptions[i].data + link.order = order + order += 1 add_audit_log(severity, current_user, f"Edited collection {collection.author.username}/{collection.name}", diff --git a/app/public/static/collection_editor.js b/app/public/static/collection_editor.js index f3c587ac..ad1303b7 100644 --- a/app/public/static/collection_editor.js +++ b/app/public/static/collection_editor.js @@ -2,16 +2,150 @@ // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later -function handleRemovePackage(card) { - if (!confirm(card.getAttribute("data-delete-confirm"))) { +function removePackage(card) { + const message = document.getElementById("confirm_delete").innerText.trim(); + const title = card.querySelector("h5 a").innerText.trim(); + if (!confirm(message.replace("{title}", title))) { return; } + card.querySelector("input[name^=package_removed]").value = "1"; card.classList.add("d-none"); + onPackageQueryUpdate(); } + +function restorePackage(id) { + const idElement = document.querySelector(`[value='${id}']`); + if (!idElement) { + return false; + } + + const card = idElement.parentNode.parentNode; + card.classList.remove("d-none"); + card.querySelector("input[name^=package_removed]").value = "0"; + card.scrollIntoView(); + onPackageQueryUpdate(); + return true; +} + + +function getAddedPackages() { + const ids = document.querySelectorAll("#package_list > article:not(.d-none) input[name^=package_ids]"); + return [...ids].map(x => x.value); +} + + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + + +function addPackage(pkg) { + document.getElementById("add_package").value = ""; + document.getElementById("add_package_results").innerHTML = ""; + + const id = `${pkg.author}/${pkg.name}`; + if (restorePackage(id)) { + return; + } + + const nextId = document.querySelectorAll("input[name^=package_ids-]").length; + const url = `/packages/${id}/`; + const temp = document.createElement("div"); + temp.innerHTML = ` +
+
+ +
+ + ${escapeHtml(pkg.title)} by ${escapeHtml(pkg.author)} + +
+

+ ${escapeHtml(pkg.short_description)} +

+
+ + + You can replace the description with your own +
+ + +
+
+ `; + + const newElement = temp.children[0]; + document.getElementById("package_list").appendChild(newElement); + newElement.scrollIntoView(); +} + + +function updateResults(packages) { + const results = document.getElementById("add_package_results"); + results.innerHTML = ""; + document.getElementById("add_package_empty").style.display = packages.length === 0 ? "block" : "none"; + + const alreadyAdded = getAddedPackages(); + packages.filter(pkg => !alreadyAdded.includes(`${pkg.author}/${pkg.name}`)).slice(0, 5).forEach(pkg => { + const result = document.createElement("a"); + result.classList.add("list-group-item"); + result.classList.add("list-group-item-action"); + result.innerText = `${pkg.title} by ${pkg.author}`; + result.addEventListener("click", () => addPackage(pkg)); + results.appendChild(result); + }); +} + + +let currentRequestId; + +async function fetchPackagesAndUpdateResults(query) { + const requestId = Math.random() * 1000000; + currentRequestId = requestId; + if (query === "") { + updateResults([]); + return; + } + + const url = new URL("/api/packages/", window.location.origin); + url.searchParams.set("q", query); + const resp = await fetch(url.toString()); + if (!resp.ok) { + return; + } + + const packages = await resp.json(); + if (currentRequestId !== requestId) { + return; + } + + updateResults(packages); +} + + +let timeoutHandle; +function onPackageQueryUpdate() { + const query = document.getElementById("add_package").value.trim(); + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + timeoutHandle = setTimeout( + () => fetchPackagesAndUpdateResults(query).catch(console.error), + 200); +} + + window.onload = () => { - console.log("Loaded"); document.querySelectorAll(".remove-package").forEach(button => { const card = button.parentNode.parentNode; const field = card.querySelector("input[name^=package_removed]"); @@ -20,7 +154,12 @@ window.onload = () => { if (field && field.value === "1") { card.classList.add("d-none"); } else { - button.addEventListener("click", () => handleRemovePackage(card)); + button.addEventListener("click", () => removePackage(card)); } }); + + const addPackageQuery = document.getElementById("add_package"); + addPackageQuery.value = ""; + addPackageQuery.classList.remove("d-none"); + addPackageQuery.addEventListener("input", onPackageQueryUpdate); }; diff --git a/app/templates/collections/create_edit.html b/app/templates/collections/create_edit.html index 9b50f375..e1d79465 100644 --- a/app/templates/collections/create_edit.html +++ b/app/templates/collections/create_edit.html @@ -9,7 +9,7 @@ {% endblock %} {% block scriptextra %} - + {% endblock %} {% block content %} @@ -34,36 +34,45 @@ {% if collection and collection.items %}

{{ _("Packages") }}

-

- {{ _("To add or remove a package, go to the package's page and click 'Add to collection'") }} -

- {% for item in collection.items %} - {% set package = item.package %} -
-
- -
- - {{ _("%(title)s by %(author)s", title=package.title, author=package.author.display_name) }} - -
-

- {{ package.short_desc }} -

- {{ render_field(form.descriptions[loop.index - 1], hint=_("You can replace the description with your own")) }} - {{ form.package_ids[loop.index - 1]() }} - {{ form.package_removed[loop.index - 1]() }} -
-
- {% endfor %} +
+ + + +
+
+
+ {% for item in collection.items %} + {% set package = item.package %} + + {% endfor %} +
{% endif %}
{{ render_submit_field(form.submit) }}
+ + {{ _("Are you sure you want to remove {title}?") }} + {% endblock %}