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