Add live search to admin licenses overview

- Add AJAX handler for real-time license search
- Create admin-licenses.js with debounced search and keyboard navigation
- Display search results with highlighted matches
- Support navigation with arrow keys and Enter to select
- Add CSS for dropdown results styling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 22:52:51 +01:00
parent fa639dfbaa
commit bfb24f078d
3 changed files with 412 additions and 0 deletions

View File

@@ -468,3 +468,114 @@
.licenses-table .license-actions {
width: 220px;
}
/* Live Search Styles */
.wclp-live-search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
min-width: 400px;
max-width: 600px;
max-height: 400px;
overflow-y: auto;
background: #fff;
border: 1px solid #c3c4c7;
border-radius: 0 0 4px 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.wclp-live-search-results:empty {
display: none;
}
.wclp-search-result-item {
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.15s ease;
}
.wclp-search-result-item:last-child {
border-bottom: none;
}
.wclp-search-result-item:hover,
.wclp-search-result-item.active {
background-color: #f0f6fc;
}
.wclp-result-main {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 4px;
}
.wclp-result-key code {
font-size: 13px;
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
}
.wclp-result-key mark {
background-color: #fff8c5;
padding: 0;
}
.wclp-result-details {
display: flex;
gap: 15px;
font-size: 12px;
color: #646970;
margin-bottom: 2px;
}
.wclp-result-domain mark {
background-color: #fff8c5;
padding: 0;
}
.wclp-result-product {
color: #2271b1;
}
.wclp-result-customer {
font-size: 11px;
color: #8c8f94;
}
.wclp-result-customer small {
opacity: 0.8;
}
.wclp-search-loading,
.wclp-search-no-results,
.wclp-search-error {
padding: 15px;
text-align: center;
color: #646970;
font-size: 13px;
}
.wclp-search-loading .spinner {
float: none;
margin: 0 5px 0 0;
vertical-align: middle;
}
.wclp-search-error {
color: #d63638;
}
/* Search box positioning */
.wclp-filter-form .search-box {
position: relative;
}
#license-search-input {
width: 280px;
}

237
assets/js/admin-licenses.js Normal file
View File

@@ -0,0 +1,237 @@
/**
* WC Licensed Product - Admin Licenses Live Search
*
* @package Jeremias\WcLicensedProduct
*/
(function($) {
'use strict';
var searchTimeout = null;
var $searchInput = $('#license-search-input');
var $resultsDropdown = null;
var isSearching = false;
/**
* Initialize live search
*/
function init() {
if (!$searchInput.length) {
return;
}
// Create results dropdown
$resultsDropdown = $('<div class="wclp-live-search-results"></div>');
$searchInput.parent().css('position', 'relative').append($resultsDropdown);
// Bind events
$searchInput.on('input', handleSearchInput);
$searchInput.on('keydown', handleKeydown);
$searchInput.on('focus', function() {
if ($resultsDropdown.children().length > 0) {
$resultsDropdown.show();
}
});
// Close dropdown when clicking outside
$(document).on('click', function(e) {
if (!$(e.target).closest('.search-box').length) {
$resultsDropdown.hide();
}
});
}
/**
* Handle search input with debouncing
*/
function handleSearchInput() {
var query = $searchInput.val().trim();
// Clear previous timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Hide results if query is too short
if (query.length < 2) {
$resultsDropdown.hide().empty();
return;
}
// Show loading state
showLoading();
// Debounce search
searchTimeout = setTimeout(function() {
performSearch(query);
}, 300);
}
/**
* Handle keyboard navigation
*/
function handleKeydown(e) {
var $items = $resultsDropdown.find('.wclp-search-result-item');
var $active = $items.filter('.active');
var index = $items.index($active);
switch (e.keyCode) {
case 40: // Down arrow
e.preventDefault();
if (index < $items.length - 1) {
$items.removeClass('active');
$items.eq(index + 1).addClass('active');
} else if (index === -1 && $items.length > 0) {
$items.eq(0).addClass('active');
}
break;
case 38: // Up arrow
e.preventDefault();
if (index > 0) {
$items.removeClass('active');
$items.eq(index - 1).addClass('active');
}
break;
case 13: // Enter
if ($active.length) {
e.preventDefault();
window.location.href = $active.data('url');
}
break;
case 27: // Escape
$resultsDropdown.hide();
break;
}
}
/**
* Show loading state
*/
function showLoading() {
$resultsDropdown.html(
'<div class="wclp-search-loading">' +
'<span class="spinner is-active"></span> ' +
wclpAdmin.strings.searching +
'</div>'
).show();
}
/**
* Perform AJAX search
*/
function performSearch(query) {
if (isSearching) {
return;
}
isSearching = true;
$.ajax({
url: wclpAdmin.ajaxUrl,
type: 'GET',
data: {
action: 'wclp_live_search',
nonce: wclpAdmin.nonce,
search: query
},
success: function(response) {
if (response.success && response.data.results) {
renderResults(response.data.results, query);
} else {
showNoResults();
}
},
error: function() {
$resultsDropdown.html(
'<div class="wclp-search-error">' + wclpAdmin.strings.error + '</div>'
).show();
},
complete: function() {
isSearching = false;
}
});
}
/**
* Render search results
*/
function renderResults(results, query) {
if (results.length === 0) {
showNoResults();
return;
}
var html = '';
results.forEach(function(item) {
var statusClass = 'license-status-' + item.status;
var highlightedKey = highlightMatch(item.license_key, query);
var highlightedDomain = highlightMatch(item.domain, query);
html += '<div class="wclp-search-result-item" data-url="' + escapeHtml(item.view_url) + '">' +
'<div class="wclp-result-main">' +
'<span class="wclp-result-key"><code>' + highlightedKey + '</code></span>' +
'<span class="license-status ' + statusClass + '">' + escapeHtml(item.status) + '</span>' +
'</div>' +
'<div class="wclp-result-details">' +
'<span class="wclp-result-domain">' + highlightedDomain + '</span>' +
'<span class="wclp-result-product">' + escapeHtml(item.product_name) + '</span>' +
'</div>' +
'<div class="wclp-result-customer">' +
escapeHtml(item.customer_name) +
(item.customer_email ? ' <small>(' + escapeHtml(item.customer_email) + ')</small>' : '') +
'</div>' +
'</div>';
});
$resultsDropdown.html(html).show();
// Make items clickable
$resultsDropdown.find('.wclp-search-result-item').on('click', function() {
window.location.href = $(this).data('url');
}).on('mouseenter', function() {
$resultsDropdown.find('.wclp-search-result-item').removeClass('active');
$(this).addClass('active');
});
}
/**
* Show no results message
*/
function showNoResults() {
$resultsDropdown.html(
'<div class="wclp-search-no-results">' + wclpAdmin.strings.noResults + '</div>'
).show();
}
/**
* Highlight matching text
*/
function highlightMatch(text, query) {
if (!text || !query) {
return escapeHtml(text || '');
}
var escaped = escapeHtml(text);
var queryEscaped = escapeHtml(query);
var regex = new RegExp('(' + queryEscaped.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return escaped.replace(regex, '<mark>$1</mark>');
}
/**
* Escape HTML entities
*/
function escapeHtml(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initialize when document is ready
$(document).ready(init);
})(jQuery);

View File

@@ -52,6 +52,9 @@ final class AdminController
// Add to WooCommerce Reports
add_filter('woocommerce_admin_reports', [$this, 'addLicenseReports']);
// AJAX handler for live search
add_action('wp_ajax_wclp_live_search', [$this, 'handleLiveSearch']);
}
/**
@@ -108,6 +111,67 @@ final class AdminController
[],
WC_LICENSED_PRODUCT_VERSION
);
// Enqueue live search script on licenses page
if ($isLicensePage) {
wp_enqueue_script(
'wc-licensed-product-admin',
WC_LICENSED_PRODUCT_PLUGIN_URL . 'assets/js/admin-licenses.js',
['jquery'],
WC_LICENSED_PRODUCT_VERSION,
true
);
wp_localize_script('wc-licensed-product-admin', 'wclpAdmin', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wclp_live_search'),
'strings' => [
'noResults' => __('No licenses found', 'wc-licensed-product'),
'searching' => __('Searching...', 'wc-licensed-product'),
'error' => __('Search failed', 'wc-licensed-product'),
],
]);
}
}
/**
* Handle AJAX live search request
*/
public function handleLiveSearch(): void
{
check_ajax_referer('wclp_live_search', 'nonce');
if (!current_user_can('manage_woocommerce')) {
wp_send_json_error(['message' => __('Permission denied.', 'wc-licensed-product')], 403);
}
$search = isset($_GET['search']) ? sanitize_text_field($_GET['search']) : '';
if (strlen($search) < 2) {
wp_send_json_success(['results' => []]);
}
$filters = ['search' => $search];
$licenses = $this->licenseManager->getAllLicenses(1, 10, $filters);
$results = [];
foreach ($licenses as $license) {
$product = wc_get_product($license->getProductId());
$customer = get_userdata($license->getCustomerId());
$results[] = [
'id' => $license->getId(),
'license_key' => $license->getLicenseKey(),
'domain' => $license->getDomain(),
'status' => $license->getStatus(),
'product_name' => $product ? $product->get_name() : __('Unknown', 'wc-licensed-product'),
'customer_name' => $customer ? $customer->display_name : __('Guest', 'wc-licensed-product'),
'customer_email' => $customer ? $customer->user_email : '',
'view_url' => admin_url('admin.php?page=wc-licenses&s=' . urlencode($license->getLicenseKey())),
];
}
wp_send_json_success(['results' => $results]);
}
/**