Fix Form_Utils to use component.$sid() instead of data-sid selector

Add response helper functions and use _message as reserved metadata key

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-23 22:38:48 +00:00
parent 678ff17ad6
commit 69af4e87d4
5 changed files with 73 additions and 14 deletions

View File

@@ -107,7 +107,8 @@ class Form_Utils {
// Resolve the promise once all animations are complete // Resolve the promise once all animations are complete
Promise.all(animations).then(() => { Promise.all(animations).then(() => {
// Scroll to error container if it exists // Scroll to error container if it exists
const $error_container = $parent.find('[data-sid="error_container"]').first(); const component = $parent.component();
const $error_container = component ? component.$sid('error_container') : $();
if ($error_container.length > 0) { if ($error_container.length > 0) {
const container_top = $error_container.offset().top; const container_top = $error_container.offset().top;
@@ -251,9 +252,10 @@ class Form_Utils {
} }
// Convert Laravel validator format {field: [msg1, msg2]} to {field: msg1} // Convert Laravel validator format {field: [msg1, msg2]} to {field: msg1}
// Skip reserved keys (prefixed with underscore) - these are metadata, not field errors
const normalized = {}; const normalized = {};
for (const field in errors) { for (const field in errors) {
if (errors.hasOwnProperty(field)) { if (errors.hasOwnProperty(field) && !field.startsWith('_')) {
const value = errors[field]; const value = errors[field];
if (Array.isArray(value) && value.length > 0) { if (Array.isArray(value) && value.length > 0) {
normalized[field] = value[0]; normalized[field] = value[0];
@@ -345,7 +347,8 @@ class Form_Utils {
*/ */
static _apply_combined_error($parent, summary_msg, unmatched_errors) { static _apply_combined_error($parent, summary_msg, unmatched_errors) {
const animations = []; const animations = [];
const $error_container = $parent.find('[data-sid="error_container"]').first(); const component = $parent.component();
const $error_container = component ? component.$sid('error_container') : $();
const $target = $error_container.length > 0 ? $error_container : $parent; const $target = $error_container.length > 0 ? $error_container : $parent;
// Create alert with summary message and bulleted list of unmatched errors // Create alert with summary message and bulleted list of unmatched errors
@@ -386,7 +389,8 @@ class Form_Utils {
const animations = []; const animations = [];
// Look for a specific error container div (e.g., in Rsx_Form component) // Look for a specific error container div (e.g., in Rsx_Form component)
const $error_container = $parent.find('[data-sid="error_container"]').first(); const component = $parent.component();
const $error_container = component ? component.$sid('error_container') : $();
const $target = $error_container.length > 0 ? $error_container : $parent; const $target = $error_container.length > 0 ? $error_container : $parent;
if (typeof messages === 'string') { if (typeof messages === 'string') {

View File

@@ -28,16 +28,16 @@ class Error_Response extends Rsx_Response_Abstract
if ($metadata === null) { if ($metadata === null) {
$this->metadata = []; $this->metadata = [];
} elseif (is_string($metadata)) { } elseif (is_string($metadata)) {
$this->metadata = ['message' => $metadata]; $this->metadata = ['_message' => $metadata];
} elseif (is_array($metadata)) { } elseif (is_array($metadata)) {
$this->metadata = $metadata; $this->metadata = $metadata;
} else { } else {
$this->metadata = ['message' => (string)$metadata]; $this->metadata = ['_message' => (string)$metadata];
} }
// Set reason from message or use default // Set reason from message or use default
if (isset($this->metadata['message'])) { if (isset($this->metadata['_message'])) {
$this->reason = $this->metadata['message']; $this->reason = $this->metadata['_message'];
} else { } else {
$this->reason = Ajax::get_default_message($error_code); $this->reason = Ajax::get_default_message($error_code);
} }

View File

@@ -1089,6 +1089,57 @@ function response_not_found(?string $message = null)
return response_error(\App\RSpade\Core\Ajax\Ajax::ERROR_NOT_FOUND, $message); return response_error(\App\RSpade\Core\Ajax\Ajax::ERROR_NOT_FOUND, $message);
} }
/**
* Create a form validation error response
*
* Use this for validation errors with field-specific messages.
* The client-side form handling will apply errors to matching fields.
*
* @param string $message Summary message for the error
* @param array $field_errors Field-specific errors as ['field_name' => 'error message']
* @return \App\RSpade\Core\Response\Error_Response
*/
function response_form_error(string $message, array $field_errors = [])
{
return response_error(
\App\RSpade\Core\Ajax\Ajax::ERROR_VALIDATION,
array_merge(['_message' => $message], $field_errors)
);
}
/**
* Create an authentication required error response
*
* Use this when the user is not logged in and needs to authenticate.
* Distinct from response_unauthorized() which is for permission denied.
*
* @param string|null $message Custom error message (optional)
* @return \App\RSpade\Core\Response\Error_Response
*/
function response_auth_required(?string $message = null)
{
return response_error(\App\RSpade\Core\Ajax\Ajax::ERROR_AUTH_REQUIRED, $message);
}
/**
* Create a fatal error response
*
* Use this for unrecoverable errors that prevent the operation from completing.
* These are typically logged and displayed prominently to the user.
*
* @param string|null $message Error message
* @param array $details Additional error details (e.g., file, line, backtrace)
* @return \App\RSpade\Core\Response\Error_Response
*/
function response_fatal_error(?string $message = null, array $details = [])
{
$metadata = $details;
if ($message !== null) {
$metadata['_message'] = $message;
}
return response_error(\App\RSpade\Core\Ajax\Ajax::ERROR_FATAL, $metadata ?: $message);
}
/** /**
* Check if the current request is from a loopback IP address * Check if the current request is from a loopback IP address
* *

View File

@@ -97,17 +97,21 @@ class Frontend_Clients_Edit {
### Change ### Change
Adopted unified `response_error()` function with error code constants. Same constant names on server and client for zero mental translation. Adopted unified `response_error()` function with error code constants, plus convenience helpers. Same constant names on server and client for zero mental translation.
**Note**: The fragmented helper functions (`response_form_error()`, `response_auth_required()`, etc.) shown in earlier documentation were a planned design that was superseded before implementation. Only `response_error()` exists.
### Pattern ### Pattern
```php ```php
// Server - single function with constants // Server - base function with constants
return response_error(Ajax::ERROR_VALIDATION, ['email' => 'Invalid']); return response_error(Ajax::ERROR_VALIDATION, ['email' => 'Invalid']);
return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found'); return response_error(Ajax::ERROR_NOT_FOUND, 'Project not found');
return response_error(Ajax::ERROR_UNAUTHORIZED); // Auto-message
// Convenience helpers (recommended for clarity)
return response_form_error('Validation failed', ['email' => 'Invalid']);
return response_not_found('Project not found');
return response_unauthorized();
return response_auth_required();
return response_fatal_error('Something went wrong');
``` ```
```javascript ```javascript

View File

@@ -912,7 +912,7 @@ async on_load() {
// Controller: save() receives all form values, validates, persists // Controller: save() receives all form values, validates, persists
#[Ajax_Endpoint] #[Ajax_Endpoint]
public static function save(Request $request, array $params = []) { public static function save(Request $request, array $params = []) {
if (empty($params['title'])) return response_form_error('Error', ['title' => 'Required']); if (empty($params['title'])) return response_form_error('Validation failed', ['title' => 'Required']);
$record = $params['id'] ? My_Model::find($params['id']) : new My_Model(); $record = $params['id'] ? My_Model::find($params['id']) : new My_Model();
$record->title = $params['title']; $record->title = $params['title'];
$record->save(); $record->save();