forked from enviPath/enviPy
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>
146 lines
3.7 KiB
JavaScript
146 lines
3.7 KiB
JavaScript
/**
|
|
* Search Modal Alpine.js Component
|
|
*
|
|
* Provides package selection, search mode switching, and results display
|
|
* for the search modal.
|
|
*/
|
|
|
|
document.addEventListener('alpine:init', () => {
|
|
/**
|
|
* Search Modal Component
|
|
*
|
|
* Usage:
|
|
* <dialog x-data="searchModal()" @close="reset()">
|
|
* ...
|
|
* </dialog>
|
|
*/
|
|
Alpine.data('searchModal', () => ({
|
|
// Package selector state
|
|
selectedPackages: [],
|
|
|
|
// Search state
|
|
searchMode: 'text',
|
|
searchModeLabel: 'Text',
|
|
query: '',
|
|
|
|
// Results state
|
|
results: null,
|
|
isSearching: false,
|
|
error: null,
|
|
|
|
// Initialize on modal open
|
|
init() {
|
|
// Load reviewed packages by default
|
|
this.loadInitialSelection();
|
|
|
|
// Watch for modal open to focus searchbar
|
|
this.$watch('$el.open', (open) => {
|
|
if (open) {
|
|
setTimeout(() => {
|
|
this.$refs.searchbar.focus();
|
|
}, 320);
|
|
}
|
|
});
|
|
},
|
|
|
|
loadInitialSelection() {
|
|
// Select all reviewed packages by default
|
|
const menuItems = this.$refs.packageDropdown.querySelectorAll('li');
|
|
|
|
for (const item of menuItems) {
|
|
// Stop at 'Unreviewed Packages' section
|
|
if (item.classList.contains('menu-title') &&
|
|
item.textContent.trim() === 'Unreviewed Packages') {
|
|
break;
|
|
}
|
|
|
|
const packageOption = item.querySelector('.package-option');
|
|
if (packageOption) {
|
|
this.selectedPackages.push({
|
|
url: packageOption.dataset.packageUrl,
|
|
name: packageOption.dataset.packageName
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
togglePackage(url, name) {
|
|
const index = this.selectedPackages.findIndex(pkg => pkg.url === url);
|
|
|
|
if (index !== -1) {
|
|
this.selectedPackages.splice(index, 1);
|
|
} else {
|
|
this.selectedPackages.push({ url, name });
|
|
}
|
|
},
|
|
|
|
removePackage(url) {
|
|
const index = this.selectedPackages.findIndex(pkg => pkg.url === url);
|
|
if (index !== -1) {
|
|
this.selectedPackages.splice(index, 1);
|
|
}
|
|
},
|
|
|
|
isPackageSelected(url) {
|
|
return this.selectedPackages.some(pkg => pkg.url === url);
|
|
},
|
|
|
|
setSearchMode(mode, label) {
|
|
this.searchMode = mode;
|
|
this.searchModeLabel = label;
|
|
this.$refs.modeDropdown.hidePopover();
|
|
},
|
|
|
|
async performSearch(serverBase) {
|
|
if (!this.query.trim()) {
|
|
return;
|
|
}
|
|
|
|
if (this.selectedPackages.length < 1) {
|
|
this.results = { error: 'no_packages' };
|
|
return;
|
|
}
|
|
|
|
const params = new URLSearchParams();
|
|
this.selectedPackages.forEach(pkg => params.append('packages', pkg.url));
|
|
params.append('search', this.query.trim());
|
|
params.append('mode', this.searchModeLabel.toLowerCase());
|
|
|
|
this.isSearching = true;
|
|
this.results = null;
|
|
this.error = null;
|
|
|
|
try {
|
|
const response = await fetch(`${serverBase}/search?${params.toString()}`, {
|
|
method: 'GET',
|
|
headers: { 'Accept': 'application/json' }
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Search request failed');
|
|
}
|
|
|
|
this.results = await response.json();
|
|
} catch (err) {
|
|
console.error('Search error:', err);
|
|
this.error = 'Search failed. Please try again.';
|
|
} finally {
|
|
this.isSearching = false;
|
|
}
|
|
},
|
|
|
|
hasResults() {
|
|
if (!this.results || this.results.error) return false;
|
|
const categories = ['Compounds', 'Compound Structures', 'Rules', 'Reactions', 'Pathways'];
|
|
return categories.some(cat => this.results[cat] && this.results[cat].length > 0);
|
|
},
|
|
|
|
reset() {
|
|
this.query = '';
|
|
this.results = null;
|
|
this.error = null;
|
|
this.isSearching = false;
|
|
}
|
|
}));
|
|
});
|