forked from enviPath/enviPy
[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 <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#236 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
This commit is contained in:
@ -9,7 +9,7 @@
|
||||
>
|
||||
<div class="hero-overlay"></div>
|
||||
<!-- Predict Pathway text over the image -->
|
||||
<div class="absolute bottom-40 left-1/8 z-10 -translate-x-8">
|
||||
<div class="absolute bottom-40 left-1/8 -translate-x-8">
|
||||
<h2 class="text-base-100 text-left text-3xl text-shadow-lg">
|
||||
Predict Your Pathway
|
||||
</h2>
|
||||
@ -20,16 +20,68 @@
|
||||
<div class="bg-base-200 mx-auto max-w-5xl shadow-md">
|
||||
<!-- Predict Pathway Section -->
|
||||
<div
|
||||
class="relative z-20 mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
|
||||
class="relative mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
|
||||
>
|
||||
<div
|
||||
class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out"
|
||||
x-data="{
|
||||
drawMode: false,
|
||||
smiles: '',
|
||||
loadExample(smilesStr, linkEl) {
|
||||
if (this.drawMode && window.indexKetcher && window.indexKetcher.setMolecule) {
|
||||
window.indexKetcher.setMolecule(smilesStr);
|
||||
} else {
|
||||
this.smiles = smilesStr;
|
||||
}
|
||||
const original = linkEl.textContent;
|
||||
linkEl.textContent = 'loaded!';
|
||||
setTimeout(() => linkEl.textContent = original, 1000);
|
||||
},
|
||||
syncFromKetcher() {
|
||||
const ketcher = getKetcherInstance('index-ketcher');
|
||||
if (ketcher && ketcher.getSmiles) {
|
||||
try {
|
||||
const s = ketcher.getSmiles();
|
||||
if (s && s.trim()) this.smiles = s;
|
||||
} catch (err) {
|
||||
console.error('Failed to sync from Ketcher:', err);
|
||||
}
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
let finalSmiles = '';
|
||||
if (this.drawMode) {
|
||||
const ketcher = getKetcherInstance('index-ketcher');
|
||||
if (ketcher && ketcher.getSmiles) {
|
||||
try {
|
||||
finalSmiles = ketcher.getSmiles().trim();
|
||||
} catch (err) {
|
||||
console.error('Failed to get SMILES from Ketcher:', err);
|
||||
alert('Unable to extract structure. Please try again or switch to SMILES input.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
alert('The drawing editor is still loading. Please wait and try again.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
finalSmiles = this.smiles.trim();
|
||||
}
|
||||
if (!finalSmiles) {
|
||||
alert('Please enter a SMILES string or draw a structure.');
|
||||
return;
|
||||
}
|
||||
document.getElementById('index-form-smiles').value = finalSmiles;
|
||||
document.getElementById('index-form').submit();
|
||||
}
|
||||
}"
|
||||
x-init="$watch('drawMode', value => { if (!value) syncFromKetcher(); })"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div class="my-4 ml-8 flex h-fit flex-row items-center justify-start">
|
||||
<div class="flex items-center gap-1">
|
||||
<label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
|
||||
<input type="checkbox" />
|
||||
<input type="checkbox" x-model="drawMode" />
|
||||
<span class="swap-on flex items-center gap-1">
|
||||
<div
|
||||
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
|
||||
@ -82,16 +134,24 @@
|
||||
|
||||
<fieldset
|
||||
class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
|
||||
:class="drawMode ? 'p-4' : 'p-8'"
|
||||
>
|
||||
<form
|
||||
id="index-form"
|
||||
action="{{ meta.current_package.url }}/pathway"
|
||||
method="POST"
|
||||
@submit.prevent="submitForm()"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<div
|
||||
id="text-input-container"
|
||||
class="scale-100 transform opacity-100 transition-all duration-300 ease-in-out"
|
||||
x-show="!drawMode"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
>
|
||||
<div class="join mx-auto w-full">
|
||||
<input
|
||||
@ -99,6 +159,7 @@
|
||||
id="index-form-text-input"
|
||||
placeholder="canonical SMILES string"
|
||||
class="input input-md join-item grow"
|
||||
x-model="smiles"
|
||||
/>
|
||||
<button class="btn btn-neutral join-item">Predict!</button>
|
||||
</div>
|
||||
@ -107,15 +168,15 @@
|
||||
<a
|
||||
href="#"
|
||||
class="example-link hover:text-primary cursor-pointer"
|
||||
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
|
||||
title="load example"
|
||||
@click.prevent="loadExample('CN1C=NC2=C1C(=O)N(C(=O)N2C)C', $el)"
|
||||
>Caffeine</a
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
class="example-link hover:text-primary cursor-pointer"
|
||||
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
|
||||
title="load example"
|
||||
@click.prevent="loadExample('CC(C)CC1=CC=C(C=C1)C(C)C(=O)O', $el)"
|
||||
>Ibuprofen</a
|
||||
>
|
||||
</div>
|
||||
@ -128,7 +189,14 @@
|
||||
</div>
|
||||
<div
|
||||
id="ketcher-container"
|
||||
class="hidden w-full scale-95 transform opacity-0 transition-all duration-300 ease-in-out"
|
||||
x-show="drawMode"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-300"
|
||||
x-transition:leave-start="opacity-100 scale-100"
|
||||
x-transition:leave-end="opacity-0 scale-95"
|
||||
class="w-full"
|
||||
>
|
||||
<iframe
|
||||
id="index-ketcher"
|
||||
@ -337,166 +405,20 @@
|
||||
// Make render function globally available
|
||||
window.renderDiscourseTopics = renderDiscourseTopics;
|
||||
|
||||
// Toggle functionality with smooth animations
|
||||
function toggleInputMode() {
|
||||
const toggle = $('input[type="checkbox"]');
|
||||
const textContainer = $("#text-input-container");
|
||||
const ketcherContainer = $("#ketcher-container");
|
||||
const formCard = $(".card");
|
||||
const fieldset = $(".fieldset");
|
||||
|
||||
if (toggle.is(":checked")) {
|
||||
// Draw mode - show Ketcher, hide text input
|
||||
textContainer.addClass("opacity-0 transform scale-95");
|
||||
textContainer.removeClass("opacity-100 transform scale-100");
|
||||
|
||||
// Adjust fieldset padding for Ketcher mode - reduce padding and make more compact
|
||||
fieldset.removeClass("p-8");
|
||||
fieldset.addClass("p-4");
|
||||
|
||||
// Wait for fade out to complete, then hide and show new content
|
||||
setTimeout(() => {
|
||||
textContainer.addClass("hidden");
|
||||
ketcherContainer.removeClass("hidden opacity-0 transform scale-95");
|
||||
ketcherContainer.addClass("opacity-100 transform scale-100");
|
||||
|
||||
// Force re-evaluation of iframe size
|
||||
const iframe = document.getElementById("index-ketcher");
|
||||
if (iframe) {
|
||||
iframe.style.height = "400px";
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
// SMILES mode - show text input, hide Ketcher
|
||||
ketcherContainer.addClass("opacity-0 transform scale-95");
|
||||
ketcherContainer.removeClass("opacity-100 transform scale-100");
|
||||
|
||||
// Restore fieldset padding for text input mode
|
||||
fieldset.removeClass("p-4");
|
||||
fieldset.addClass("p-8");
|
||||
|
||||
// Wait for fade out to complete, then hide and show new content
|
||||
setTimeout(() => {
|
||||
ketcherContainer.addClass("hidden");
|
||||
textContainer.removeClass("hidden opacity-0 transform scale-95");
|
||||
textContainer.addClass("opacity-100 transform scale-100");
|
||||
}, 300);
|
||||
|
||||
// Transfer SMILES from Ketcher to text input if available
|
||||
const ketcher = getKetcherInstance("index-ketcher");
|
||||
if (ketcher && ketcher.getSmiles) {
|
||||
try {
|
||||
const smiles = ketcher.getSmiles();
|
||||
if (smiles && smiles.trim() !== "") {
|
||||
$("#index-form-text-input").val(smiles);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to sync Ketcher to text input:", err);
|
||||
// Non-critical error, just log it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ketcher integration
|
||||
function indexKetcherToTextInput() {
|
||||
$("#index-form-smiles").val(this.ketcher.getSmiles());
|
||||
}
|
||||
|
||||
$(function () {
|
||||
// Initialize fieldset with proper padding
|
||||
$(".fieldset").addClass("p-8");
|
||||
|
||||
// Toggle event listener
|
||||
$('input[type="checkbox"]').on("change", toggleInputMode);
|
||||
|
||||
// Ketcher iframe load handler
|
||||
$("#index-ketcher").on("load", function () {
|
||||
// Ketcher iframe load handler - set up change event to sync SMILES
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const indexKetcher = document.getElementById("index-ketcher");
|
||||
indexKetcher.addEventListener("load", function () {
|
||||
const checkKetcherReady = () => {
|
||||
const win = this.contentWindow;
|
||||
if (win.ketcher && "editor" in win.ketcher) {
|
||||
window.indexKetcher = win.ketcher;
|
||||
win.ketcher.editor.event.change.handlers.push({
|
||||
once: false,
|
||||
priority: 0,
|
||||
f: indexKetcherToTextInput,
|
||||
ketcher: win.ketcher,
|
||||
});
|
||||
} else {
|
||||
setTimeout(checkKetcherReady, 100);
|
||||
}
|
||||
};
|
||||
checkKetcherReady();
|
||||
});
|
||||
|
||||
// Handle example link clicks
|
||||
$(".example-link").on("click", function (e) {
|
||||
e.preventDefault();
|
||||
const smiles = $(this).data("smiles");
|
||||
const title = $(this).attr("title");
|
||||
|
||||
// Check if we're in Ketcher mode or text input mode
|
||||
if ($('input[type="checkbox"]').is(":checked")) {
|
||||
// In Ketcher mode - set the SMILES in Ketcher
|
||||
if (window.indexKetcher && window.indexKetcher.setMolecule) {
|
||||
window.indexKetcher.setMolecule(smiles);
|
||||
}
|
||||
} else {
|
||||
// In text input mode - set the SMILES in the text input
|
||||
$("#index-form-text-input").val(smiles);
|
||||
}
|
||||
|
||||
// Show a brief feedback
|
||||
const originalText = $(this).text();
|
||||
$(this).text(`loaded!`);
|
||||
setTimeout(() => {
|
||||
$(this).text(originalText);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Handle form submission on Enter
|
||||
$("#index-form").on("submit", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var textSmiles = "";
|
||||
|
||||
// Check if we're in Ketcher mode and extract SMILES
|
||||
if ($('input[type="checkbox"]').is(":checked")) {
|
||||
// Use the robust getter function
|
||||
const ketcher = getKetcherInstance("index-ketcher");
|
||||
if (ketcher && ketcher.getSmiles) {
|
||||
try {
|
||||
textSmiles = ketcher.getSmiles().trim();
|
||||
} catch (err) {
|
||||
console.error("Failed to get SMILES from Ketcher:", err);
|
||||
alert(
|
||||
"Unable to extract structure from the drawing editor. Please try again or switch to SMILES input mode.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.warn("Ketcher not available, possibly still loading");
|
||||
alert(
|
||||
"The drawing editor is still loading. Please wait a moment and try again.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
textSmiles = $("#index-form-text-input").val().trim();
|
||||
}
|
||||
|
||||
if (textSmiles === "") {
|
||||
alert("Please enter a SMILES string or draw a structure.");
|
||||
return;
|
||||
}
|
||||
|
||||
$("#index-form-smiles").val(textSmiles);
|
||||
$("#index-form").attr("action", currentPackage + "/pathway");
|
||||
$("#index-form").attr("method", "POST");
|
||||
this.submit();
|
||||
});
|
||||
|
||||
// Discourse topics are now loaded automatically by discourse-api.js
|
||||
});
|
||||
</script>
|
||||
{% endblock main_content %}
|
||||
|
||||
Reference in New Issue
Block a user