From 42fa8304ddb811b0f725f245130f70c0f5e86a6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 04 Nov 2025 06:12:02 +0000
Subject: [PATCH] =Refactored LoginManager to be more extensible and configurable, as well as an AjaxRateLimiter
---
assets/js/dash/TaxonomyCreator.js | 244 +++++++++++++++++++++++++++++++++++-------------
1 files changed, 178 insertions(+), 66 deletions(-)
diff --git a/assets/js/dash/TaxonomyCreator.js b/assets/js/dash/TaxonomyCreator.js
index d14ac9d..3c783c7 100644
--- a/assets/js/dash/TaxonomyCreator.js
+++ b/assets/js/dash/TaxonomyCreator.js
@@ -1,6 +1,6 @@
/**
- * This separates out all create logic from the base TaxonomySelector.js, so that we only enqueue create logic if it's creatable
- * Updated to work with the refactored centralized TaxonomySelector
+ * This separates out all create logic from the base TaxonomySelector.js
+ * Updated to work with centralized DataStore architecture
*/
class TaxonomyCreator {
@@ -24,7 +24,8 @@
}
initListeners() {
- document.addEventListener('click', this.handleClick.bind(this));
+ this.clickHandler = this.handleClick.bind(this);
+ document.addEventListener('click', this.clickHandler);
}
handleClick(e) {
@@ -42,37 +43,52 @@
async handleTermCreation(e) {
const termName = this.form.querySelector('input[name="term_name"]').value.trim();
- const parentId = this.form.querySelector('input#select_parent')?.value;
+ const parentId = parseInt(this.form.querySelector('input#select_parent')?.value) || 0;
+
+ if (!termName) return;
try {
this.form.querySelector('button').disabled = true;
const response = await this.createTerm(termName, parentId);
- if (response.success) {
+ if (response.success && response.term) {
let term = response.term;
// Close the create new section
this.createNew.open = false;
- // Add to the terms list UI
- this.selector.createTermElement({
- id: parseInt(term.id),
- name: term.name,
- hasChildren: term.hasChildren || false,
- path: term.path || term.name,
- show: false
- });
+ // Invalidate the cache for this taxonomy
+ await this.selector.store.invalidate({ taxonomy: this.taxonomy });
// Add to current modal selection
- this.selector.addSelectedTermToModal(term.id, term.name, term.path);
+ this.selector.addSelectedTermToModal(term.id, term.name, term.path || term.name);
+
+ // If we're viewing the parent category where this was created, refresh the list
+ const currentParent = this.selector.store.filters.parent || 0;
+ if (currentParent === parentId) {
+ await this.selector.store.setFilters({
+ taxonomy: this.taxonomy,
+ parent: parentId,
+ page: 1,
+ search: ''
+ });
+ }
// Clear the form
this.form.querySelector('input[name="term_name"]').value = '';
+
+ // Clear suggestions
+ const suggestionContainer = this.createNew.querySelector('.term-suggestions');
+ if (suggestionContainer) {
+ suggestionContainer.hidden = true;
+ }
}
} catch (error) {
console.error('Error creating term:', error);
- this.selector.showError?.('Failed to create term') ||
- console.error('Failed to create term');
+ this.selector.error?.log(error, {
+ component: 'TaxonomyCreator',
+ action: 'handleTermCreation'
+ }) || console.error('Failed to create term');
} finally {
this.form.querySelector('button').disabled = false;
}
@@ -100,25 +116,39 @@
window.removeChildren(select);
select.append(defaultOption.cloneNode(true));
- // Add current parent if we're in a sub-category
- if (this.selector.currentParentName !== '') {
- let parentOption = defaultOption.cloneNode(true);
- parentOption.value = this.selector.currentParent;
- parentOption.textContent = this.selector.currentParentName;
- select.append(parentOption);
+ // Get current parent from store filters
+ const currentParent = this.selector.store.filters.parent || 0;
+
+ // If we're in a sub-category, add the current parent as an option
+ if (currentParent !== 0) {
+ const parentTerm = this.selector.store.data.get(currentParent);
+ if (parentTerm) {
+ let parentOption = defaultOption.cloneNode(true);
+ parentOption.value = parentTerm.id;
+ parentOption.textContent = parentTerm.name;
+ select.append(parentOption);
+ }
}
- // Add terms from current taxonomy cache
- const taxonomyTerms = this.selector.currentTerms;
- if (taxonomyTerms && taxonomyTerms.length > 0) {
- taxonomyTerms.forEach(term => {
- let option = defaultOption.cloneNode(true);
- option.id = `select-parent-${term.id}`;
- option.value = term.id;
- option.textContent = ' — ' + term.name;
- select.append(option);
- });
- }
+ // Add all terms currently visible in the taxonomy (from store cache)
+ const visibleTerms = [];
+ this.selector.store.data.forEach(term => {
+ if (term.taxonomy === this.taxonomy && term.parent === currentParent) {
+ visibleTerms.push(term);
+ }
+ });
+
+ // Sort by name
+ visibleTerms.sort((a, b) => a.name.localeCompare(b.name));
+
+ // Add to select
+ visibleTerms.forEach(term => {
+ let option = defaultOption.cloneNode(true);
+ option.id = `select-parent-${term.id}`;
+ option.value = term.id;
+ option.textContent = ' — ' + term.name;
+ select.append(option);
+ });
}
async createTerm(name, parent = 0) {
@@ -136,36 +166,22 @@
text.textContent = 'Checking term...';
}
- // Check if term already exists by searching
- const originalSearchQuery = this.selector.searchQuery;
- const originalFetchSpecific = this.selector.fetchSpecificTerms;
+ // Search for existing terms with this name
+ const searchResults = await this.searchExistingTerms(name);
- this.selector.searchQuery = name;
- this.selector.fetchSpecificTerms = false; // We want to search, not fetch specific IDs
-
- const existingTerms = await this.selector.fetchTerms(
- this.selector.activeField,
- false,
- true // isSearch = true
- );
-
- // Restore original search state
- this.selector.searchQuery = originalSearchQuery;
- this.selector.fetchSpecificTerms = originalFetchSpecific;
-
- // Check if any existing terms match exactly
- const exactMatches = existingTerms.filter(term =>
+ // Check for exact matches
+ const exactMatches = searchResults.filter(term =>
term.name.toLowerCase() === name.toLowerCase()
);
if (exactMatches.length > 0) {
- this.showTermSuggestions(exactMatches);
+ this.showTermSuggestions(exactMatches, true);
return { success: false, reason: 'exists' };
}
// Show similar terms if found
- if (existingTerms.length > 0) {
- this.showTermSuggestions(existingTerms);
+ if (searchResults.length > 0) {
+ this.showTermSuggestions(searchResults, false);
return { success: false, reason: 'similar' };
}
@@ -191,8 +207,7 @@
throw new Error(`Server error: ${response.status}`);
}
- const result = await response.json();
- return result;
+ return await response.json();
} catch (error) {
console.error('Error creating term:', error);
@@ -205,8 +220,35 @@
}
}
- // Helper method to show term suggestions when similar terms exist
- showTermSuggestions(suggestions) {
+ /**
+ * Search for existing terms using the store
+ */
+ async searchExistingTerms(searchQuery) {
+ return new Promise((resolve) => {
+ // Set up a one-time listener for the search results
+ const handleSearchResults = (event, data) => {
+ if (event === 'data-loaded') {
+ this.selector.store.unsubscribe(handleSearchResults);
+ resolve(data.data?.items || []);
+ }
+ };
+
+ this.selector.store.subscribe(handleSearchResults);
+
+ // Trigger search
+ this.selector.store.setFilters({
+ taxonomy: this.taxonomy,
+ search: searchQuery,
+ page: 1,
+ parent: 0
+ });
+ });
+ }
+
+ /**
+ * Show term suggestions when similar terms exist
+ */
+ showTermSuggestions(suggestions, isExact = false) {
const suggestionContainer = this.createNew.querySelector('.term-suggestions') ||
this.createSuggestionContainer();
@@ -215,7 +257,9 @@
// Add heading
const heading = document.createElement('h4');
- heading.textContent = 'Similar terms already exist:';
+ heading.textContent = isExact ?
+ 'This term already exists:' :
+ 'Similar terms already exist:';
suggestionContainer.appendChild(heading);
// Create list of suggestions
@@ -225,18 +269,15 @@
suggestions.forEach(term => {
const item = document.createElement('li');
- // Create term path display if available
- let termDisplay = term.path || term.name;
-
const button = document.createElement('button');
button.type = 'button';
button.className = 'use-existing-term';
button.setAttribute('data-id', term.id);
- button.textContent = termDisplay;
+ button.textContent = term.path || term.name;
button.addEventListener('click', () => {
// Add this term to modal selection
- this.selector.addSelectedTermToModal(term.id, term.name, term.path);
+ this.selector.addSelectedTermToModal(term.id, term.name, term.path || term.name);
// Close the create new section
this.createNew.open = false;
@@ -256,7 +297,9 @@
suggestionContainer.hidden = false;
}
- // Create container for term suggestions if it doesn't exist
+ /**
+ * Create container for term suggestions if it doesn't exist
+ */
createSuggestionContainer() {
const container = document.createElement('div');
container.className = 'term-suggestions';
@@ -268,10 +311,79 @@
}
/**
+ * Create "Create new term" option for autocomplete dropdown
+ */
+ createAutocompleteOption(query, field) {
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'autocomplete-item create-term';
+ button.innerHTML = `<span>Create "${query}"</span>`;
+ button.dataset.query = query;
+ button.dataset.fieldId = field.id;
+
+ button.addEventListener('click', async () => {
+ await this.handleAutocompleteCreate(button, query, field);
+ });
+
+ return button;
+ }
+
+ /**
+ * Handle term creation from autocomplete
+ */
+ async handleAutocompleteCreate(button, termName, field) {
+ if (!field) return;
+
+ const originalHTML = button.innerHTML;
+
+ try {
+ button.disabled = true;
+ button.innerHTML = '<span>Creating...</span>';
+
+ const parentId = 0; // Autocomplete always creates at root level
+ const result = await this.createTerm(termName, parentId);
+
+ if (result.success && result.term) {
+ const term = result.term;
+
+ // Add to field
+ field.selectedTerms.add(parseInt(term.id));
+ this.selector.addTermToDisplay(field.id, term.id, term.name, term.path || term.name);
+
+ // Update input
+ field.input.value = Array.from(field.selectedTerms).join(',');
+ field.input.dispatchEvent(new Event('change', { bubbles: true }));
+
+ // Invalidate cache
+ await this.selector.store.invalidate({ taxonomy: field.taxonomy });
+
+ // Clear and hide dropdown
+ field.autocompleteDropdown.hidden = true;
+ const input = field.container.querySelector('input[data-autocomplete]');
+ if (input) input.value = '';
+ }
+ // If result.success is false, suggestions are already shown
+
+ } catch (error) {
+ console.error('Error creating term:', error);
+ button.innerHTML = originalHTML;
+ button.disabled = false;
+ this.selector.error?.log(error, {
+ component: 'TaxonomyCreator',
+ action: 'handleAutocompleteCreate'
+ });
+ }
+ }
+
+ /**
* Clean up when modal closes
*/
destroy() {
- // Remove event listeners if needed
+ // Remove event listeners
+ if (this.clickHandler) {
+ document.removeEventListener('click', this.clickHandler);
+ }
+
// Clear any pending operations
const loadingMessage = this.createNew?.querySelector('.loading-message.create-term');
if (loadingMessage) {
--
Gitblit v1.10.0