Fix contact add link to use Contacts_Edit_Action SPA route

Add multi-route support for controllers and SPA actions
Add screenshot feature to rsx:debug and convert contacts edit to SPA

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-11-20 19:55:49 +00:00
parent 0e19c811e3
commit ff77724e2b
11 changed files with 555 additions and 61 deletions

View File

@@ -248,7 +248,7 @@ class Rsx {
pattern = Rsx._routes[class_name][action_name];
} else {
// Not found in PHP routes - check if it's a SPA action
pattern = Rsx._try_spa_action_route(class_name);
pattern = Rsx._try_spa_action_route(class_name, params_obj);
if (!pattern) {
// Route not found - use default pattern /_/{controller}/{action}
@@ -261,6 +261,60 @@ class Rsx {
return Rsx._generate_url_from_pattern(pattern, params_obj);
}
/**
* Select the best matching route pattern from available patterns based on provided parameters
*
* Selection algorithm:
* 1. Filter patterns where all required parameters can be satisfied by provided params
* 2. Among satisfiable patterns, prioritize those with MORE parameters (more specific)
* 3. If tie, any pattern works (deterministic by using first match)
*
* @param {Array<string>} patterns Array of route patterns
* @param {Object} params_obj Provided parameters
* @returns {string|null} Selected pattern or null if none match
*/
static _select_best_route_pattern(patterns, params_obj) {
const satisfiable = [];
for (const pattern of patterns) {
// Extract required parameters from pattern
const required_params = [];
const matches = pattern.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
if (matches) {
// Remove the : prefix from each match
for (const match of matches) {
required_params.push(match.substring(1));
}
}
// Check if all required parameters are provided
let can_satisfy = true;
for (const required of required_params) {
if (!(required in params_obj)) {
can_satisfy = false;
break;
}
}
if (can_satisfy) {
satisfiable.push({
pattern: pattern,
param_count: required_params.length
});
}
}
if (satisfiable.length === 0) {
return null;
}
// Sort by parameter count descending (most parameters first)
satisfiable.sort((a, b) => b.param_count - a.param_count);
// Return the pattern with the most parameters
return satisfiable[0].pattern;
}
/**
* Generate URL from route pattern by replacing parameters
*
@@ -327,9 +381,10 @@ class Rsx {
* Returns the route pattern or null if not found
*
* @param {string} class_name The action class name
* @param {Object} params_obj The parameters for route selection
* @returns {string|null} The route pattern or null
*/
static _try_spa_action_route(class_name) {
static _try_spa_action_route(class_name, params_obj) {
// Get all classes from manifest
const all_classes = Manifest.get_all_classes();
@@ -346,8 +401,18 @@ class Rsx {
const routes = class_object._spa_routes || [];
if (routes.length > 0) {
// Return the first route pattern
return routes[0];
// Select best matching route based on parameters
const selected = Rsx._select_best_route_pattern(routes, params_obj);
if (!selected) {
// Routes exist but none are satisfiable
throw new Error(
`No suitable route found for SPA action ${class_name} with provided parameters. ` +
`Available routes: ${routes.join(', ')}`
);
}
return selected;
}
}