forked from enviPath/enviPy
## Major Changes - Implement a REST style API app in epapi - Currently implements a GET method for all entity types in the browse menu (both package level and global) - Provides paginated results per default with query style filtering for reviewed vs unreviewed. - Provides new paginated templates with thin wrappers per entity types for easier maintainability - Implements e2e tests for the API ## Minor changes - Added more comprehensive gitignore to cover coverage reports and other test/node.js etc. data. - Add additional CI file for API tests that only gets triggered on API relevant changes. ## ⚠️ Currently only works with session-based authentication. Token based will be added in new PR. Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Co-authored-by: jebus <lorsbach@envipath.com> Reviewed-on: enviPath/enviPy#243 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
324 lines
12 KiB
HTML
324 lines
12 KiB
HTML
{% extends "framework_modern.html" %}
|
|
{% load static %}
|
|
{% block content %}
|
|
|
|
{# Serialize objects data for Alpine pagination #}
|
|
{# prettier-ignore-start #}
|
|
{# FIXME: This is a hack to get the objects data into the JavaScript code. #}
|
|
{% if object_type != 'scenario' %}
|
|
<script>
|
|
window.reviewedObjects = [
|
|
{% for obj in reviewed_objects %}
|
|
{ "name": "{{ obj.name|escapejs }}", "url": "{{ obj.url }}" }{% if not forloop.last %},{% endif %}
|
|
{% endfor %}
|
|
];
|
|
window.unreviewedObjects = [
|
|
{% for obj in unreviewed_objects %}
|
|
{ "name": "{{ obj.name|escapejs }}", "url": "{{ obj.url }}" }{% if not forloop.last %},{% endif %}
|
|
{% endfor %}
|
|
];
|
|
</script>
|
|
{% endif %}
|
|
{# prettier-ignore-end #}
|
|
|
|
<div class="px-8 py-4">
|
|
<input
|
|
type="text"
|
|
id="object-search"
|
|
class="input input-bordered hidden w-full max-w-xs"
|
|
placeholder="Search by name"
|
|
/>
|
|
</div>
|
|
|
|
{% block action_modals %}
|
|
{% if object_type == 'node' %}
|
|
{% include "modals/collections/new_node_modal.html" %}
|
|
{% elif object_type == 'edge' %}
|
|
{% include "modals/collections/new_edge_modal.html" %}
|
|
{% endif %}
|
|
{% endblock action_modals %}
|
|
|
|
<div class="px-8 py-4">
|
|
<!-- Header Section -->
|
|
<div class="card bg-base-100">
|
|
<div class="card-body px-0 py-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="card-title text-2xl">
|
|
{% if object_type == 'node' %}
|
|
Nodes
|
|
{% elif object_type == 'edge' %}
|
|
Edges
|
|
{% endif %}
|
|
</h2>
|
|
<div id="actionsButton" class="dropdown dropdown-end hidden">
|
|
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="lucide lucide-wrench"
|
|
>
|
|
<path
|
|
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
|
/>
|
|
</svg>
|
|
Actions
|
|
</div>
|
|
<ul
|
|
tabindex="-1"
|
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
|
>
|
|
{% block actions %}
|
|
{% if object_type == 'node' %}
|
|
{% include "actions/collections/node.html" %}
|
|
{% elif object_type == 'edge' %}
|
|
{% include "actions/collections/edge.html" %}
|
|
{% endif %}
|
|
{% endblock %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2">
|
|
{% if object_type == 'node' %}
|
|
<p>
|
|
Nodes represent the (predicted) compounds in a graph.
|
|
<a
|
|
target="_blank"
|
|
href="https://wiki.envipath.org/index.php/nodes"
|
|
class="link link-primary"
|
|
>Learn more >></a
|
|
>
|
|
</p>
|
|
{% elif object_type == 'edge' %}
|
|
<p>
|
|
Edges represent the links between nodes in a graph.
|
|
<a
|
|
target="_blank"
|
|
href="https://wiki.envipath.org/index.php/edges"
|
|
class="link link-primary"
|
|
>Learn more >></a
|
|
>
|
|
</p>
|
|
{% endif %}
|
|
|
|
{% if reviewed_objects and unreviewed_objects %}
|
|
{% if reviewed_objects|length == 0 and unreviewed_objects|length == 0 %}
|
|
<p class="mt-4">
|
|
Nothing found. There are two possible reasons:<br /><br />
|
|
1. There is no content yet.<br />
|
|
2. You have no reading permissions.<br /><br />
|
|
Please ensure you have at least reading permissions.
|
|
</p>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lists Container -->
|
|
<div class="w-full">
|
|
{% if reviewed_objects %}
|
|
{% if reviewed_objects|length > 0 %}
|
|
<!-- Reviewed -->
|
|
<div
|
|
class="collapse-arrow bg-base-200 collapse order-2 w-full"
|
|
x-data="paginatedList(window.reviewedObjects || [], { isReviewed: true, instanceId: 'reviewed' })"
|
|
>
|
|
<input type="checkbox" checked />
|
|
<div class="collapse-title text-xl font-medium">
|
|
Reviewed
|
|
<span
|
|
class="badge badge-sm badge-neutral ml-2"
|
|
x-text="totalItems"
|
|
></span>
|
|
</div>
|
|
<div class="collapse-content w-full">
|
|
<ul class="menu bg-base-100 rounded-box w-full">
|
|
<template x-for="obj in paginatedItems" :key="obj.url">
|
|
<li>
|
|
<a :href="obj.url" class="hover:bg-base-200">
|
|
<span x-text="obj.name"></span>
|
|
<span
|
|
class="tooltip tooltip-left ml-auto"
|
|
data-tip="Reviewed"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="lucide lucide-star"
|
|
>
|
|
<polygon
|
|
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
<!-- Pagination Controls -->
|
|
<div
|
|
x-show="totalPages > 1"
|
|
class="mt-4 flex items-center justify-between px-2"
|
|
>
|
|
<span class="text-base-content/70 text-sm">
|
|
Showing <span x-text="showingStart"></span>-<span
|
|
x-text="showingEnd"
|
|
></span>
|
|
of <span x-text="totalItems"></span>
|
|
</span>
|
|
<div class="join">
|
|
<button
|
|
class="join-item btn btn-sm"
|
|
:disabled="currentPage === 1"
|
|
@click="prevPage()"
|
|
>
|
|
«
|
|
</button>
|
|
<template x-for="item in pageNumbers" :key="item.key">
|
|
<button
|
|
class="join-item btn btn-sm"
|
|
:class="{ 'btn-active': item.page === currentPage }"
|
|
:disabled="item.isEllipsis"
|
|
@click="!item.isEllipsis && goToPage(item.page)"
|
|
x-text="item.page"
|
|
></button>
|
|
</template>
|
|
<button
|
|
class="join-item btn btn-sm"
|
|
:disabled="currentPage === totalPages"
|
|
@click="nextPage()"
|
|
>
|
|
»
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if unreviewed_objects %}
|
|
<!-- Unreviewed -->
|
|
<div
|
|
class="collapse-arrow bg-base-200 collapse order-1 w-full"
|
|
x-data="paginatedList(window.unreviewedObjects || [], { isReviewed: false, instanceId: 'unreviewed' })"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
{% if reviewed_objects|length == 0 %}checked{% endif %}
|
|
/>
|
|
<div class="collapse-title text-xl font-medium">
|
|
Unreviewed
|
|
<span
|
|
class="badge badge-sm badge-neutral ml-2"
|
|
x-text="totalItems"
|
|
></span>
|
|
</div>
|
|
<div class="collapse-content w-full">
|
|
<ul class="menu bg-base-100 rounded-box w-full">
|
|
<template x-for="obj in paginatedItems" :key="obj.url">
|
|
<li>
|
|
<a
|
|
:href="obj.url"
|
|
class="hover:bg-base-200"
|
|
x-text="obj.name"
|
|
></a>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
<!-- Pagination Controls -->
|
|
<div
|
|
x-show="totalPages > 1"
|
|
class="mt-4 flex items-center justify-between px-2"
|
|
>
|
|
<span class="text-base-content/70 text-sm">
|
|
Showing <span x-text="showingStart"></span>-<span
|
|
x-text="showingEnd"
|
|
></span>
|
|
of <span x-text="totalItems"></span>
|
|
</span>
|
|
<div class="join">
|
|
<button
|
|
class="join-item btn btn-sm"
|
|
:disabled="currentPage === 1"
|
|
@click="prevPage()"
|
|
>
|
|
«
|
|
</button>
|
|
<template x-for="item in pageNumbers" :key="item.key">
|
|
<button
|
|
class="join-item btn btn-sm"
|
|
:class="{ 'btn-active': item.page === currentPage }"
|
|
:disabled="item.isEllipsis"
|
|
@click="!item.isEllipsis && goToPage(item.page)"
|
|
x-text="item.page"
|
|
></button>
|
|
</template>
|
|
<button
|
|
class="join-item btn btn-sm"
|
|
:disabled="currentPage === totalPages"
|
|
@click="nextPage()"
|
|
>
|
|
»
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
// Show actions button if there are actions
|
|
const actionsButton = document.getElementById("actionsButton");
|
|
const actionsList = actionsButton?.querySelector("ul");
|
|
if (actionsList && actionsList.children.length > 0) {
|
|
actionsButton?.classList.remove("hidden");
|
|
}
|
|
|
|
// Show search input and connect to Alpine pagination
|
|
const objectSearch = document.getElementById("object-search");
|
|
if (objectSearch) {
|
|
objectSearch.classList.remove("hidden");
|
|
objectSearch.addEventListener("input", function () {
|
|
const query = this.value;
|
|
// Dispatch search to all paginatedList components
|
|
document
|
|
.querySelectorAll('[x-data*="paginatedList"]')
|
|
.forEach((el) => {
|
|
if (el._x_dataStack && el._x_dataStack[0]) {
|
|
el._x_dataStack[0].search(query);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Delete form submit handler
|
|
const deleteSubmit = document.getElementById("modal-form-delete-submit");
|
|
const deleteForm = document.getElementById("modal-form-delete");
|
|
if (deleteSubmit && deleteForm) {
|
|
deleteSubmit.addEventListener("click", function (e) {
|
|
e.preventDefault();
|
|
deleteForm.submit();
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock content %}
|