Enhance refactor commands with controller-aware Route() updates and fix code quality violations

Add semantic token highlighting for 'that' variable and comment file references in VS Code extension
Add Phone_Text_Input and Currency_Input components with formatting utilities
Implement client widgets, form standardization, and soft delete functionality
Add modal scroll lock and update documentation
Implement comprehensive modal system with form integration and validation
Fix modal component instantiation using jQuery plugin API
Implement modal system with responsive sizing, queuing, and validation support
Implement form submission with validation, error handling, and loading states
Implement country/state selectors with dynamic data loading and Bootstrap styling
Revert Rsx::Route() highlighting in Blade/PHP files
Target specific PHP scopes for Rsx::Route() highlighting in Blade
Expand injection selector for Rsx::Route() highlighting
Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls
Update jqhtml packages to v2.2.165
Add bundle path validation for common mistakes (development mode only)
Create Ajax_Select_Input widget and Rsx_Reference_Data controller
Create Country_Select_Input widget with default country support
Initialize Tom Select on Select_Input widgets
Add Tom Select bundle for enhanced select dropdowns
Implement ISO 3166 geographic data system for country/region selection
Implement widget-based form system with disabled state support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-30 06:21:56 +00:00
parent e678b987c2
commit f6ac36c632
5683 changed files with 5854736 additions and 22329 deletions

View File

@@ -3,37 +3,143 @@
/**
* Client-side Ajax class for making API calls to RSX controllers
*
* Mirrors the PHP Ajax::call (Ajax::internal) functionality for browser-side JavaScript
* Automatically batches multiple calls into single HTTP requests to reduce network overhead.
* Batches up to 20 calls or flushes after setTimeout(0) debounce.
*/
class Ajax {
/**
* Make an AJAX call to an RSX controller action
*
* All calls are automatically batched using Rsx_Ajax_Batch unless
* window.rsxapp.ajax_disable_batching is true (for debugging).
*
* @param {string} controller - The controller class name (e.g., 'User_Controller')
* @param {string} action - The action method name (e.g., 'get_profile')
* @param {object} params - Parameters to send to the action
* @returns {Promise} - Resolves with the return value, rejects with error
* Initialize Ajax system
* Called automatically when class is loaded
*/
static async call(controller, action, params = {}) {
// Route through batch system
return Rsx_Ajax_Batch.call(controller, action, params);
static _on_framework_core_init() {
// Queue of pending calls waiting to be batched
Ajax._pending_calls = {};
// Timer for batching flush
Ajax._flush_timeout = null;
// Call counter for generating unique call IDs
Ajax._call_counter = 0;
// Maximum batch size before forcing immediate flush
Ajax.MAX_BATCH_SIZE = 20;
// Debounce time in milliseconds
Ajax.DEBOUNCE_MS = 0;
}
/**
* DEPRECATED: Direct call implementation (preserved for reference)
* This is now handled by Rsx_Ajax_Batch
* Make an AJAX call to an RSX controller action
*
* All calls are automatically batched unless window.rsxapp.ajax_disable_batching is true.
*
* @param {string|object|function} url - The Ajax URL (e.g., '/_ajax/Controller_Name/action_name') or an object/function with a .path property
* @param {object} params - Parameters to send to the action
* @returns {Promise} - Resolves with the return value, rejects with error
*/
static async call(url, params = {}) {
// If url is an object or function with a .path property, use that as the URL
if (url && typeof url === 'object' && url.path) {
url = url.path;
} else if (url && typeof url === 'function' && url.path) {
url = url.path;
}
// Validate url is a non-empty string
if (typeof url !== 'string' || url.length === 0) {
throw new Error('Ajax.call() requires a non-empty string URL or an object/function with a .path property');
}
// Extract controller and action from URL
const { controller, action } = Ajax.ajax_url_to_controller_action(url);
console.log('Ajax:', controller, action, params);
// Check if batching is disabled for debugging
if (window.rsxapp && window.rsxapp.ajax_disable_batching) {
return Ajax._call_direct(controller, action, params);
}
return Ajax._call_batch(controller, action, params);
}
/**
* Make a batched Ajax call
* @private
*/
static _call_batch(controller, action, params = {}) {
console.log('Ajax Batch:', controller, action, params);
return new Promise((resolve, reject) => {
// Generate call key for deduplication
const call_key = Ajax._generate_call_key(controller, action, params);
// Check if this exact call is already pending
if (Ajax._pending_calls[call_key]) {
const existing_call = Ajax._pending_calls[call_key];
// If call already completed (cached), return immediately
if (existing_call.is_complete) {
if (existing_call.is_error) {
reject(existing_call.error);
} else {
resolve(existing_call.result);
}
return;
}
// Call is pending, add this promise to callbacks
existing_call.callbacks.push({ resolve, reject });
return;
}
// Create new pending call
const call_id = Ajax._call_counter++;
const pending_call = {
call_id: call_id,
call_key: call_key,
controller: controller,
action: action,
params: params,
callbacks: [{ resolve, reject }],
is_complete: false,
is_error: false,
result: null,
error: null,
};
// Add to pending queue
Ajax._pending_calls[call_key] = pending_call;
// Count pending calls
const pending_count = Object.keys(Ajax._pending_calls).filter((key) => !Ajax._pending_calls[key].is_complete).length;
// If we've hit the batch size limit, flush immediately
if (pending_count >= Ajax.MAX_BATCH_SIZE) {
clearTimeout(Ajax._flush_timeout);
Ajax._flush_timeout = null;
Ajax._flush_pending_calls();
} else {
// Schedule batch flush with debounce
clearTimeout(Ajax._flush_timeout);
Ajax._flush_timeout = setTimeout(() => {
Ajax._flush_pending_calls();
}, Ajax.DEBOUNCE_MS);
}
});
}
/**
* Make a direct (non-batched) Ajax call
* @private
*/
static async _call_direct(controller, action, params = {}) {
// Build the endpoint URL
// Construct URL from controller and action
const url = `/_ajax/${controller}/${action}`;
// Log the AJAX call using console_debug
if (typeof Debugger !== 'undefined' && Debugger.console_debug) {
Debugger.console_debug('AJAX', `Calling ${controller}.${action}`, params);
Debugger.console_debug('AJAX', `Calling ${controller}.${action} (unbatched)`, params);
}
return new Promise((resolve, reject) => {
@@ -42,27 +148,24 @@ class Ajax {
method: 'POST',
data: params,
dataType: 'json',
__local_integration: true, // Bypass $.ajax override - this is the official Ajax endpoint pattern
__local_integration: true, // Bypass $.ajax override
success: (response) => {
// Handle console_debug messages if present
// Handle console_debug messages
if (response.console_debug && Array.isArray(response.console_debug)) {
response.console_debug.forEach((msg) => {
// Messages must be structured as [channel, [arguments]]
if (!Array.isArray(msg) || msg.length !== 2) {
throw new Error('Invalid console_debug message format - expected [channel, [arguments]]');
}
const [channel, args] = msg;
// Output with channel as first argument, then spread the arguments
console.log(channel, ...args);
});
}
// Check if the response was successful
if (response.success === true) {
// Process the return value to instantiate any ORM models
const processedValue = Rsx_Js_Model._instantiate_models_recursive(response._ajax_return_value);
// Return the processed value
resolve(processedValue);
// @JS-AJAX-02-EXCEPTION - Unwrap server responses with _ajax_return_value
const processed_value = Rsx_Js_Model._instantiate_models_recursive(response._ajax_return_value);
resolve(processed_value);
} else {
// Handle error responses
const error_type = response.error_type || 'unknown_error';
@@ -75,7 +178,6 @@ class Ajax {
console.error(
'The user is no longer authenticated, this is a placeholder for future code which handles this scenario.'
);
// Create an error object similar to PHP exceptions
const auth_error = new Error(reason);
auth_error.type = 'auth_required';
auth_error.details = details;
@@ -93,7 +195,6 @@ class Ajax {
break;
case 'response_form_error':
// Form validation errors
const form_error = new Error(reason);
form_error.type = 'form_error';
form_error.details = details;
@@ -101,7 +202,6 @@ class Ajax {
break;
case 'response_fatal_error':
// Fatal errors
const fatal_error = new Error(reason);
fatal_error.type = 'fatal_error';
fatal_error.details = details;
@@ -118,7 +218,6 @@ class Ajax {
break;
default:
// Unknown error type
const generic_error = new Error(reason);
generic_error.type = error_type;
generic_error.details = details;
@@ -128,25 +227,7 @@ class Ajax {
}
},
error: (xhr, status, error) => {
// Handle network or server errors
let error_message = 'Network or server error';
if (xhr.responseJSON && xhr.responseJSON.message) {
error_message = xhr.responseJSON.message;
} else if (xhr.responseText) {
try {
const response = JSON.parse(xhr.responseText);
if (response.message) {
error_message = response.message;
}
} catch (e) {
// If response is not JSON, use the status text
error_message = `${status}: ${error}`;
}
} else {
error_message = `${status}: ${error}`;
}
const error_message = Ajax._extract_error_message(xhr);
const network_error = new Error(error_message);
network_error.type = 'network_error';
network_error.status = xhr.status;
@@ -169,15 +250,182 @@ class Ajax {
});
}
/**
* Flush all pending calls by sending batch request
* @private
*/
static async _flush_pending_calls() {
// Collect all pending calls
const calls_to_send = [];
const call_map = {}; // Map call_id to pending_call object
for (const call_key in Ajax._pending_calls) {
const pending_call = Ajax._pending_calls[call_key];
if (!pending_call.is_complete) {
calls_to_send.push({
call_id: pending_call.call_id,
controller: pending_call.controller,
action: pending_call.action,
params: pending_call.params,
});
call_map[pending_call.call_id] = pending_call;
}
}
// Nothing to send
if (calls_to_send.length === 0) {
return;
}
// Log batch for debugging
if (typeof Debugger !== 'undefined' && Debugger.console_debug) {
Debugger.console_debug(
'AJAX_BATCH',
`Sending batch of ${calls_to_send.length} calls`,
calls_to_send.map((c) => `${c.controller}.${c.action}`)
);
}
try {
// Send batch request
const response = await $.ajax({
url: '/_ajax/_batch',
method: 'POST',
data: { batch_calls: JSON.stringify(calls_to_send) },
dataType: 'json',
__local_integration: true, // Bypass $.ajax override
});
// Process batch response
// Response format: { C_0: {success, _ajax_return_value}, C_1: {...}, ... }
for (const response_key in response) {
if (!response_key.startsWith('C_')) {
continue;
}
const call_id = parseInt(response_key.substring(2), 10);
const call_response = response[response_key];
const pending_call = call_map[call_id];
if (!pending_call) {
console.error('Received response for unknown call_id:', call_id);
continue;
}
// Handle console_debug messages if present
if (call_response.console_debug && Array.isArray(call_response.console_debug)) {
call_response.console_debug.forEach((msg) => {
if (!Array.isArray(msg) || msg.length !== 2) {
throw new Error('Invalid console_debug message format - expected [channel, [arguments]]');
}
const [channel, args] = msg;
console.log(channel, ...args);
});
}
// Mark call as complete
pending_call.is_complete = true;
// Check if successful
if (call_response.success === true) {
// @JS-AJAX-02-EXCEPTION - Batch system unwraps server responses with _ajax_return_value
const processed_value = Rsx_Js_Model._instantiate_models_recursive(call_response._ajax_return_value);
pending_call.result = processed_value;
// Resolve all callbacks
pending_call.callbacks.forEach(({ resolve }) => {
resolve(processed_value);
});
} else {
// Handle error
const error_type = call_response.error_type || 'unknown_error';
const reason = call_response.reason || 'Unknown error occurred';
const details = call_response.details || {};
const error = new Error(reason);
error.type = error_type;
error.details = details;
pending_call.is_error = true;
pending_call.error = error;
// Reject all callbacks
pending_call.callbacks.forEach(({ reject }) => {
reject(error);
});
}
}
} catch (xhr_error) {
// Network or server error - reject all pending calls
const error_message = Ajax._extract_error_message(xhr_error);
const error = new Error(error_message);
error.type = 'network_error';
for (const call_id in call_map) {
const pending_call = call_map[call_id];
pending_call.is_complete = true;
pending_call.is_error = true;
pending_call.error = error;
pending_call.callbacks.forEach(({ reject }) => {
reject(error);
});
}
console.error('Batch Ajax request failed:', error_message);
}
}
/**
* Generate a unique key for deduplicating calls
* @private
*/
static _generate_call_key(controller, action, params) {
// Create a stable string representation of the call
// Sort params keys for consistent hashing
const sorted_params = {};
Object.keys(params)
.sort()
.forEach((key) => {
sorted_params[key] = params[key];
});
return `${controller}::${action}::${JSON.stringify(sorted_params)}`;
}
/**
* Extract error message from jQuery XHR object
* @private
*/
static _extract_error_message(xhr) {
if (xhr.responseJSON && xhr.responseJSON.message) {
return xhr.responseJSON.message;
} else if (xhr.responseText) {
try {
const response = JSON.parse(xhr.responseText);
if (response.message) {
return response.message;
}
} catch (e) {
// Not JSON
}
}
return `${xhr.status}: ${xhr.statusText || 'Unknown error'}`;
}
/**
* Parses an AJAX URL into controller and action
* @param {string} url - URL in format '/_ajax/Controller_Name/action_name'
* Supports both /_ajax/ and /_/ URL prefixes
* @param {string} url - URL in format '/_ajax/Controller_Name/action_name' or '/_/Controller_Name/action_name'
* @returns {Object} Object with {controller: string, action: string}
* @throws {Error} If URL doesn't start with /_ajax or has invalid structure
* @throws {Error} If URL doesn't start with /_ajax or /_ or has invalid structure
*/
static ajax_url_to_controller_action(url) {
if (!url.startsWith('/_ajax')) {
throw new Error(`URL must start with /_ajax, got: ${url}`);
if (!url.startsWith('/_ajax') && !url.startsWith('/_/')) {
throw new Error(`URL must start with /_ajax or /_, got: ${url}`);
}
const parts = url.split('/').filter((part) => part !== '');
@@ -195,4 +443,11 @@ class Ajax {
return { controller, action };
}
/**
* Auto-initialize static properties when class is first loaded
*/
static on_core_define() {
Ajax._on_framework_core_init();
}
}