From 1a2c9bb543a0fba7d2ceca082686b9ecfc3abde2 Mon Sep 17 00:00:00 2001 From: Tobias O Date: Wed, 26 Nov 2025 23:16:44 +1300 Subject: [PATCH] [Feature] Modern UI roll out (#236) This PR moves all the collection pages into the new UI in a rough push. I did not put the same amount of care into these as into search, index, and predict. ## Major changes - All modals are now migrated to a state based alpine.js implementation. - jQuery is no longer present in the base layout; ajax is replace by native fetch api - most of the pps.js is now obsolte (as I understand it; the code is not referenced any more @jebus please double check) - in-memory pagination for large result lists (set to 50; we can make that configurable later; performance degrades at around 1k) stukk a bit rough tracked in #235 ## Minor things - Sarch and index also use alpine now - The loading spinner is now CSS animated (not sure if it currently gets correctly called) ## Not done - Ihave not even cheked the admin pages. Not sure If these need migrations - The temporary migration pages still use the old template. Not sure what is supposed to happen with those? @jebus ## What I did to test - opend all pages in browse, and user ; plus all pages reachable from there. - Interacted and tested the functionality of each modal superfically with exception of the API key modal (no functional test). --- This PR is massive sorry for that; just did not want to push half-brokenn state. @jebus @liambrydon I would be glad if you could click around and try to break it :) Finally closes #133 Co-authored-by: Tim Lorsbach Reviewed-on: https://git.envipath.com/enviPath/enviPy/pulls/236 Co-authored-by: Tobias O Co-committed-by: Tobias O --- .gitea/workflows/ci.yaml | 10 + envipath/urls.py | 6 + epdb/views.py | 66 +- pyproject.toml | 71 +- scripts/dev_server.py | 201 +++ scripts/pnpm_wrapper.py | 59 + static/css/input.css | 27 + static/images/wait.gif | Bin 814 -> 0 bytes static/js/alpine/index.js | 265 ++++ static/js/alpine/pagination.js | 133 ++ static/js/alpine/search.js | 145 ++ static/js/pps.js | 1171 +++++++++-------- static/js/pw.js | 239 +++- templates/actions/collections/compound.html | 5 +- .../collections/compound_structure.html | 3 +- templates/actions/collections/edge.html | 5 +- templates/actions/collections/group.html | 5 +- templates/actions/collections/model.html | 5 +- templates/actions/collections/node.html | 5 +- templates/actions/collections/package.html | 13 +- templates/actions/collections/reaction.html | 5 +- templates/actions/collections/rule.html | 5 +- templates/actions/collections/scenario.html | 5 +- templates/actions/collections/setting.html | 5 +- templates/actions/objects/compound.html | 33 +- .../actions/objects/compound_structure.html | 21 +- templates/actions/objects/edge.html | 15 +- templates/actions/objects/group.html | 10 +- templates/actions/objects/model.html | 20 +- templates/actions/objects/node.html | 20 +- templates/actions/objects/package.html | 28 +- templates/actions/objects/pathway.html | 54 +- templates/actions/objects/reaction.html | 28 +- templates/actions/objects/rule.html | 25 +- templates/actions/objects/scenario.html | 11 +- templates/actions/objects/user.html | 18 +- templates/collections/joblog.html | 110 +- templates/collections/objects_list.html | 865 ++++++------ templates/errors/400_bad_request.html | 87 +- templates/errors/403_access_denied.html | 90 +- templates/errors/404_not_found.html | 87 +- templates/errors/error.html | 77 +- templates/errors/user_account_inactive.html | 84 +- templates/framework.html | 15 +- templates/framework_modern.html | 30 +- templates/includes/navbar.html | 379 ++++-- templates/index/index.html | 236 ++-- templates/modals/cite_modal.html | 48 - .../import_legacy_package_modal.html | 147 ++- .../collections/import_package_modal.html | 145 +- .../collections/new_compound_modal.html | 184 +-- .../modals/collections/new_group_modal.html | 158 ++- .../modals/collections/new_model_modal.html | 441 ++++--- .../modals/collections/new_package_modal.html | 153 ++- .../modals/collections/new_pathway_modal.html | 376 ------ .../new_prediction_setting_modal.html | 313 +++-- .../collections/new_reaction_modal.html | 164 +-- .../modals/collections/new_rule_modal.html | 218 +-- .../collections/new_scenario_modal.html | 262 ++-- .../add_additional_information_modal.html | 179 +-- .../objects/add_pathway_edge_modal.html | 261 ++-- .../objects/add_pathway_node_modal.html | 184 +-- .../modals/objects/add_structure_modal.html | 184 +-- .../objects/delete_pathway_edge_modal.html | 147 ++- .../objects/delete_pathway_node_modal.html | 148 ++- .../objects/download_pathway_csv_modal.html | 112 +- .../objects/download_pathway_image_modal.html | 94 +- .../modals/objects/edit_compound_modal.html | 155 ++- .../edit_compound_structure_modal.html | 155 ++- .../objects/edit_group_member_modal.html | 227 ++-- .../modals/objects/edit_model_modal.html | 159 ++- templates/modals/objects/edit_node_modal.html | 155 ++- .../modals/objects/edit_package_modal.html | 155 ++- .../edit_package_permissions_modal.html | 475 +++---- .../modals/objects/edit_password_modal.html | 195 +-- .../modals/objects/edit_pathway_modal.html | 156 ++- .../edit_prediction_setting_modal.html | 311 +++-- .../modals/objects/edit_reaction_modal.html | 154 ++- templates/modals/objects/edit_rule_modal.html | 154 ++- templates/modals/objects/edit_user_modal.html | 232 ++-- .../modals/objects/evaluate_model_modal.html | 170 ++- .../modals/objects/export_package_modal.html | 101 +- .../objects/generic_copy_object_modal.html | 223 ++-- .../modals/objects/generic_delete_modal.html | 147 ++- .../objects/generic_set_aliases_modal.html | 348 +++-- .../generic_set_external_reference_modal.html | 194 +-- .../objects/generic_set_scenario_modal.html | 228 ++-- .../objects/identify_missing_rules_modal.html | 142 +- .../objects/manage_api_token_modal.html | 246 ++-- .../modals/objects/publish_package_modal.html | 125 +- .../modals/objects/retrain_model_modal.html | 109 +- .../modals/objects/set_license_modal.html | 371 +++--- ...scenario_additional_information_modal.html | 119 +- templates/modals/predict_modal.html | 178 --- templates/modals/search_modal.html | 718 ++++------ templates/objects/composite_rule.html | 269 ++-- templates/objects/compound.html | 519 +++----- templates/objects/compound_structure.html | 228 ++-- templates/objects/edge.html | 359 +++-- templates/objects/enzymelink.html | 244 ++-- templates/objects/group.html | 150 ++- templates/objects/model.html | 881 ++++++------- templates/objects/node.html | 280 ++-- templates/objects/package.html | 142 +- templates/objects/pathway.html | 735 ++++++----- templates/objects/reaction.html | 518 +++----- templates/objects/scenario.html | 268 ++-- templates/objects/simple_rule.html | 514 +++----- templates/objects/user.html | 294 ++--- templates/predict_pathway.html | 31 +- 110 files changed, 10784 insertions(+), 9465 deletions(-) create mode 100755 scripts/dev_server.py create mode 100755 scripts/pnpm_wrapper.py delete mode 100644 static/images/wait.gif create mode 100644 static/js/alpine/index.js create mode 100644 static/js/alpine/pagination.js create mode 100644 static/js/alpine/search.js delete mode 100644 templates/modals/cite_modal.html delete mode 100644 templates/modals/collections/new_pathway_modal.html delete mode 100644 templates/modals/predict_modal.html diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 4db0bfa5..7de35661 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -103,6 +103,16 @@ jobs: source .venv/bin/activate playwright install --with-deps + - name: Run PNPM Commands + run: | + uv run python scripts/pnpm_wrapper.py install + cat << 'EOF' > pnpm-workspace.yaml + onlyBuiltDependencies: + - '@parcel/watcher' + - '@tailwindcss/oxide' + EOF + uv run python scripts/pnpm_wrapper.py run build + - name: Wait for services run: | until pg_isready -h postgres -U postgres; do sleep 2; done diff --git a/envipath/urls.py b/envipath/urls.py index efdf92f5..799d89bd 100644 --- a/envipath/urls.py +++ b/envipath/urls.py @@ -34,3 +34,9 @@ if "migration" in s.INSTALLED_APPS: if s.MS_ENTRA_ENABLED: urlpatterns.append(path("", include("epauth.urls"))) + +# Custom error handlers +handler400 = "epdb.views.handler400" +handler403 = "epdb.views.handler403" +handler404 = "epdb.views.handler404" +handler500 = "epdb.views.handler500" diff --git a/epdb/views.py b/epdb/views.py index 7b8caf6d..57ab0a43 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -62,6 +62,26 @@ def log_post_params(request): logger.debug(f"{k}\t{v}") +def get_error_handler_context(request, for_user=None) -> Dict[str, Any]: + current_user = _anonymous_or_real(request) + + if for_user: + current_user = for_user + + ctx = { + "title": "enviPath", + "meta": { + "site_id": s.MATOMO_SITE_ID, + "version": "0.0.1", + "server_url": s.SERVER_URL, + "user": current_user, + "enabled_features": s.FLAGS, + "debug": s.DEBUG, + }, + } + return ctx + + def error(request, message: str, detail: str, code: int = 400): context = get_base_context(request) error_context = { @@ -76,6 +96,48 @@ def error(request, message: str, detail: str, code: int = 400): return render(request, "errors/error.html", context, status=code) +def handler400(request, exception): + """Custom 400 Bad Request error handler""" + context = get_error_handler_context(request) + context["public_mode"] = True + return render(request, "errors/400_bad_request.html", context, status=400) + + +def handler403(request, exception): + """Custom 403 Forbidden error handler""" + context = get_error_handler_context(request) + context["public_mode"] = True + return render(request, "errors/403_access_denied.html", context, status=403) + + +def handler404(request, exception): + """Custom 404 Not Found error handler""" + context = get_error_handler_context(request) + context["public_mode"] = True + return render(request, "errors/404_not_found.html", context, status=404) + + +def handler500(request): + """Custom 500 Internal Server Error handler""" + context = get_error_handler_context(request) + + error_context = {} + error_context["error_message"] = "Internal Server Error" + error_context["error_detail"] = "An unexpected error occurred. Please try again later." + + if request.headers.get("Accept") == "application/json": + return JsonResponse(error_context, status=500) + + context["public_mode"] = True + context["error_code"] = 500 + context["error_description"] = ( + "We encountered an unexpected error while processing your request. Our team has been notified and is working to resolve the issue." + ) + context.update(**error_context) + + return render(request, "errors/error.html", context, status=500) + + def login(request): context = get_base_context(request) @@ -192,8 +254,8 @@ def register(request): def editable(request, user): - if user.is_superuser: - return True + # if user.is_superuser: + # return True url = request.build_absolute_uri(request.path) if PackageManager.is_package_url(url): diff --git a/pyproject.toml b/pyproject.toml index afa77b85..3a0879dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [tool.uv.sources] enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" } envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" } -envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"} +envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7" } envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" } [project.optional-dependencies] @@ -46,7 +46,7 @@ dev = [ "pre-commit>=4.3.0", "ruff>=0.13.3", "pytest-playwright>=0.7.1", - "pytest-django>=4.11.1" + "pytest-django>=4.11.1", ] [tool.ruff] @@ -68,47 +68,31 @@ docstring-code-format = true [tool.poe.tasks] # Main tasks -setup = { sequence = ["db-up", "migrate", "bootstrap"], help = "Complete setup: start database, run migrations, and bootstrap data" } -dev = { shell = """ -# Start pnpm CSS watcher in background -pnpm run dev & -PNPM_PID=$! -echo "Started CSS watcher (PID: $PNPM_PID)" - -# Cleanup function -cleanup() { - echo "\nShutting down..." - if kill -0 $PNPM_PID 2>/dev/null; then - kill $PNPM_PID - echo "✓ CSS watcher stopped" - fi - if [ ! -z "${DJ_PID:-}" ] && kill -0 $DJ_PID 2>/dev/null; then - kill $DJ_PID - echo "✓ Django server stopped" - fi -} - -# Set trap for cleanup -trap cleanup EXIT INT TERM - -# Start Django dev server in background -uv run python manage.py runserver & -DJ_PID=$! - -# Wait for Django to finish -wait $DJ_PID -""", help = "Start the development server with CSS watcher", deps = ["db-up", "js-deps"] } -build = { sequence = ["build-frontend", "collectstatic", "frontend-test-setup"], help = "Build frontend assets and collect static files" } +setup = { sequence = [ + "db-up", + "migrate", + "bootstrap", +], help = "Complete setup: start database, run migrations, and bootstrap data" } +dev = { cmd = "uv run python scripts/dev_server.py", help = "Start the development server with CSS watcher", deps = [ + "db-up", + "js-deps", +] } +build = { sequence = [ + "build-frontend", + "collectstatic", +], help = "Build frontend assets and collect static files" } # Database tasks db-up = { cmd = "docker compose -f docker-compose.dev.yml up -d", help = "Start PostgreSQL database using Docker Compose" } db-down = { cmd = "docker compose -f docker-compose.dev.yml down", help = "Stop PostgreSQL database" } # Frontend tasks -js-deps = { cmd = "pnpm install", help = "Install frontend dependencies" } +js-deps = { cmd = "uv run python scripts/pnpm_wrapper.py install", help = "Install frontend dependencies" } # Full cleanup tasks -clean = { sequence = ["clean-db"], help = "Remove model files and database volumes (WARNING: destroys all data!)" } +clean = { sequence = [ + "clean-db", +], help = "Remove model files and database volumes (WARNING: destroys all data!)" } clean-db = { cmd = "docker compose -f docker-compose.dev.yml down -v", help = "Removes the database container and volume." } # Django tasks @@ -123,10 +107,17 @@ echo "Default admin credentials:" echo " Username: admin" echo " Email: admin@envipath.com" echo " Password: SuperSafe" -""", help = "Bootstrap initial data (anonymous user, packages, models)" } +""", help = "Bootstrap initial data (anonymous user, packages, models)" } shell = { cmd = "uv run python manage.py shell", help = "Open Django shell" } -# Build tasks -build-frontend = { cmd = "pnpm run build", help = "Build frontend assets using pnpm", deps = ["js-deps"] } -collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = ["build-frontend"] } -frontend-test-setup = {cmd = "playwright install", help = "Install the browsers required for frontend testing"} + +build-frontend = { cmd = "uv run python scripts/pnpm_wrapper.py run build", help = "Build frontend assets using pnpm", deps = [ + "js-deps", +] } # Build tasks + + +collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = [ + "build-frontend", +] } + +frontend-test-setup = { cmd = "playwright install", help = "Install the browsers required for frontend testing" } diff --git a/scripts/dev_server.py b/scripts/dev_server.py new file mode 100755 index 00000000..fc853151 --- /dev/null +++ b/scripts/dev_server.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +""" +Cross-platform development server script. +Starts pnpm CSS watcher and Django dev server, handling cleanup on exit. +Works on both Windows and Unix systems. +""" + +import atexit +import shutil +import signal +import subprocess +import sys +import time + + +def find_pnpm(): + """ + Find pnpm executable on the system. + Returns the path to pnpm or None if not found. + """ + # Try to find pnpm using shutil.which + # On Windows, this will find pnpm.cmd if it's in PATH + pnpm_path = shutil.which("pnpm") + + if pnpm_path: + return pnpm_path + + # On Windows, also try pnpm.cmd explicitly + if sys.platform == "win32": + pnpm_cmd = shutil.which("pnpm.cmd") + if pnpm_cmd: + return pnpm_cmd + + return None + + +class DevServerManager: + """Manages background processes for development server.""" + + def __init__(self): + self.processes = [] + self._cleanup_registered = False + + def start_process(self, command, description, shell=False): + """Start a background process and return the process object.""" + print(f"Starting {description}...") + try: + if shell: + # Use shell=True for commands that need shell interpretation + process = subprocess.Popen( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + else: + # Split command into list for subprocess + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) + self.processes.append((process, description)) + print(f"✓ Started {description} (PID: {process.pid})") + return process + except Exception as e: + print(f"✗ Failed to start {description}: {e}", file=sys.stderr) + self.cleanup() + sys.exit(1) + + def cleanup(self): + """Terminate all running processes.""" + if not self.processes: + return + + print("\nShutting down...") + for process, description in self.processes: + if process.poll() is None: # Process is still running + try: + # Try graceful termination first + if sys.platform == "win32": + process.terminate() + else: + process.send_signal(signal.SIGTERM) + + # Wait up to 5 seconds for graceful shutdown + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + # Force kill if graceful shutdown failed + if sys.platform == "win32": + process.kill() + else: + process.send_signal(signal.SIGKILL) + process.wait() + + print(f"✓ {description} stopped") + except Exception as e: + print(f"✗ Error stopping {description}: {e}", file=sys.stderr) + + self.processes.clear() + + def register_cleanup(self): + """Register cleanup handlers for various exit scenarios.""" + if self._cleanup_registered: + return + + self._cleanup_registered = True + + # Register atexit handler (works on all platforms) + atexit.register(self.cleanup) + + # Register signal handlers (Unix only) + if sys.platform != "win32": + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGTERM, self._signal_handler) + + def _signal_handler(self, signum, frame): + """Handle Unix signals.""" + self.cleanup() + sys.exit(0) + + def wait_for_process(self, process, description): + """Wait for a process to finish and handle its output.""" + try: + # Stream output from the process + for line in iter(process.stdout.readline, ""): + if line: + print(f"[{description}] {line.rstrip()}") + + process.wait() + return process.returncode + except KeyboardInterrupt: + # Handle Ctrl+C + self.cleanup() + sys.exit(0) + except Exception as e: + print(f"Error waiting for {description}: {e}", file=sys.stderr) + self.cleanup() + sys.exit(1) + + +def main(): + """Main entry point.""" + manager = DevServerManager() + manager.register_cleanup() + + # Find pnpm executable + pnpm_path = find_pnpm() + if not pnpm_path: + print("Error: pnpm not found in PATH.", file=sys.stderr) + print("\nPlease install pnpm:", file=sys.stderr) + print(" Windows: https://pnpm.io/installation#on-windows", file=sys.stderr) + print(" Unix: https://pnpm.io/installation#on-posix-systems", file=sys.stderr) + sys.exit(1) + + # Determine shell usage based on platform + use_shell = sys.platform == "win32" + + # Start pnpm CSS watcher + # Use the found pnpm path to ensure it works on Windows + pnpm_command = f'"{pnpm_path}" run dev' if use_shell else [pnpm_path, "run", "dev"] + manager.start_process( + pnpm_command, + "CSS watcher", + shell=use_shell, + ) + + # Give pnpm a moment to start + time.sleep(1) + + # Start Django dev server + django_process = manager.start_process( + ["uv", "run", "python", "manage.py", "runserver"], + "Django server", + shell=False, + ) + + print("\nDevelopment servers are running. Press Ctrl+C to stop.\n") + + try: + # Wait for Django server (main process) + # If Django exits, we should clean up everything + return_code = manager.wait_for_process(django_process, "Django") + + # If Django exited unexpectedly, clean up and exit + if return_code != 0: + manager.cleanup() + sys.exit(return_code) + except KeyboardInterrupt: + # Ctrl+C was pressed + manager.cleanup() + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/scripts/pnpm_wrapper.py b/scripts/pnpm_wrapper.py new file mode 100755 index 00000000..6bade5d8 --- /dev/null +++ b/scripts/pnpm_wrapper.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Cross-platform pnpm command wrapper. +Finds pnpm correctly on Windows (handles pnpm.cmd) and Unix systems. +""" + +import shutil +import subprocess +import sys + + +def find_pnpm(): + """ + Find pnpm executable on the system. + Returns the path to pnpm or None if not found. + """ + # Try to find pnpm using shutil.which + # On Windows, this will find pnpm.cmd if it's in PATH + pnpm_path = shutil.which("pnpm") + + if pnpm_path: + return pnpm_path + + # On Windows, also try pnpm.cmd explicitly + if sys.platform == "win32": + pnpm_cmd = shutil.which("pnpm.cmd") + if pnpm_cmd: + return pnpm_cmd + + return None + + +def main(): + """Main entry point - execute pnpm with provided arguments.""" + pnpm_path = find_pnpm() + + if not pnpm_path: + print("Error: pnpm not found in PATH.", file=sys.stderr) + print("\nPlease install pnpm:", file=sys.stderr) + print(" Windows: https://pnpm.io/installation#on-windows", file=sys.stderr) + print(" Unix: https://pnpm.io/installation#on-posix-systems", file=sys.stderr) + sys.exit(1) + + # Get all arguments passed to this script + args = sys.argv[1:] + + # Execute pnpm with the provided arguments + try: + sys.exit(subprocess.call([pnpm_path] + args)) + except KeyboardInterrupt: + # Handle Ctrl+C gracefully + sys.exit(130) + except Exception as e: + print(f"Error executing pnpm: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/static/css/input.css b/static/css/input.css index 98ad1678..00694158 100644 --- a/static/css/input.css +++ b/static/css/input.css @@ -34,3 +34,30 @@ } @import "./daisyui-theme.css"; + +/* Loading Spinner - Benzene Ring */ +.loading-spinner { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; +} + +.loading-spinner svg { + width: 48px; + height: 48px; + animation: spin 2s linear infinite; +} + +.loading-spinner .hexagon, +.loading-spinner .double-bonds { + fill: none; + stroke: currentColor; + stroke-width: 2; +} + +@keyframes spin { + 100% { + transform: rotate(360deg); + } +} diff --git a/static/images/wait.gif b/static/images/wait.gif deleted file mode 100644 index dd7eab8b280a1c6a1153a91323dad8160d22c364..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 814 zcmZ?wbhEHb)MYSX_#(mp1cHKsN=izqs;UMC2FAw5E-o&fo}NKLL7}0cX=!QY<>j@t zwH+NDGiT0RwQAMw-Mi16Idk{!-FNTa{r~?Tq#rL({K>)qvPJPfx1VcBu(M-;tC5}o zGb2z)2c!|?3hO@WV-YXb|b%KPU_7&tkPvIqE7oN#kkF#Bqp zhlK7156$905yKryQkzv&i_KfzmS26bRr5BNFF*U4>Xy!%DxHiIrrCvN*m5(l33^&o zDCMd1GcguLmMHUicPdrYlqQG-PhhT95pHgst;Wl&#L&|>VX4|GkqxT++_ToHZrHTl zXyu@a zzr6fn*W1_c?_PZN`OBMcw?EBEJNZ~-%7!1_Ul$zj*WagbAj{!}Z>Q0A6_p3ZOu=h~ zv(0ol9I~b{FtYl3JYsAN6Ki;Oa6<)2_;3fcXRi;)2q(o_xB@P7CA-&;Fx z>NGrz)mnYQLx_!m+g&YNEiZweiK(cwq)mj+yCPDUyQ;>Ikxz*^GeNbfWl|zASQlyb z^r-`bb+s+`Jhcs*He2u5<*{e)?v<)L4;((Ea^$G;{?n({A3uBU*v8A3E*?2~?#zX2 zSMFWBd+)}>>*h zCg@YjlqqbXd)8=psqxH_t$yor@p72qg2hTgQ`4p}U8-0sF{4C^U$}AQm#S6< G25SISNJ-iN diff --git a/static/js/alpine/index.js b/static/js/alpine/index.js new file mode 100644 index 00000000..494d0880 --- /dev/null +++ b/static/js/alpine/index.js @@ -0,0 +1,265 @@ +/** + * Alpine.js Components for enviPath + * + * This module provides reusable Alpine.js data components for modals, + * form validation, and form submission. + */ + +document.addEventListener('alpine:init', () => { + /** + * Modal Form Component + * + * Provides form validation using HTML5 Constraint Validation API, + * loading states for submission, and error message management. + * + * Basic Usage: + * + *
+ * + *
+ * + *
+ * + * With Custom State: + * + * +
Recent Jobs
+
+
+ + + + + + + + + + + {% for job in jobs %} @@ -58,7 +42,11 @@ {% if job.task_result and job.task_result|is_url == True %} - + {% elif job.task_result %} {% else %} @@ -70,19 +58,31 @@
IDNameStatusQueuedDoneResult
{{ job.created }} {{ job.done_at }}Result + Result + {{ job.task_result|slice:"40" }}...
- - - + + {% if objects %} + +
+
+ +
+
+ {% endif %} {% endblock content %} diff --git a/templates/collections/objects_list.html b/templates/collections/objects_list.html index bd214465..e574d862 100644 --- a/templates/collections/objects_list.html +++ b/templates/collections/objects_list.html @@ -1,28 +1,32 @@ -{% extends "framework.html" %} +{% extends "framework_modern.html" %} {% load static %} {% block content %} - {% if object_type != 'package' %} -
- + {# Serialize objects data for Alpine pagination #} + {# prettier-ignore-start #} + {# FIXME: This is a hack to get the objects data into the JavaScript code. #} + + {# prettier-ignore-end #} + {% if object_type != 'package' %} +
-

{% endif %} @@ -56,423 +60,474 @@ {% endif %} {% endblock action_modals %} -
-
-
- {% if object_type == 'package' %} - Packages - {% elif object_type == 'compound' %} - Compounds - {% elif object_type == 'structure' %} - Compound structures - {% elif object_type == 'rule' %} - Rules - {% elif object_type == 'reaction' %} - Reactions - {% elif object_type == 'pathway' %} - Pathways - {% elif object_type == 'node' %} - Nodes - {% elif object_type == 'edge' %} - Edges - {% elif object_type == 'scenario' %} - Scenarios - {% elif object_type == 'model' %} - Model - {% elif object_type == 'setting' %} - Settings - {% elif object_type == 'user' %} - Users - {% elif object_type == 'group' %} - Groups - {% endif %} - - - - + {% if objects %} + +
+
+ +
+
+ {% endif %}
- {# prettier-ignore-start #} - - {# prettier-ignore-end #} + } + + // 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(); + }); + } + }); + {% endblock content %} diff --git a/templates/errors/400_bad_request.html b/templates/errors/400_bad_request.html index 0848847d..7f59b2cd 100644 --- a/templates/errors/400_bad_request.html +++ b/templates/errors/400_bad_request.html @@ -1,18 +1,77 @@ -{% extends "framework.html" %} +{% extends "framework_modern.html" %} {% load static %} {% block content %} -
diff --git a/templates/modals/collections/import_package_modal.html b/templates/modals/collections/import_package_modal.html index 2d39489b..87262db9 100644 --- a/templates/modals/collections/import_package_modal.html +++ b/templates/modals/collections/import_package_modal.html @@ -1,70 +1,83 @@ -