diff --git a/app/RSpade/Core/Js/Form_Utils.js b/app/RSpade/Core/Js/Form_Utils.js index c05d3f1de..cdc934c86 100755 --- a/app/RSpade/Core/Js/Form_Utils.js +++ b/app/RSpade/Core/Js/Form_Utils.js @@ -107,7 +107,8 @@ class Form_Utils { // Resolve the promise once all animations are complete Promise.all(animations).then(() => { // 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) { const container_top = $error_container.offset().top; @@ -251,9 +252,10 @@ class Form_Utils { } // Convert Laravel validator format {field: [msg1, msg2]} to {field: msg1} + // Skip reserved keys (prefixed with underscore) - these are metadata, not field errors const normalized = {}; for (const field in errors) { - if (errors.hasOwnProperty(field)) { + if (errors.hasOwnProperty(field) && !field.startsWith('_')) { const value = errors[field]; if (Array.isArray(value) && value.length > 0) { normalized[field] = value[0]; @@ -345,7 +347,8 @@ class Form_Utils { */ static _apply_combined_error($parent, summary_msg, unmatched_errors) { 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; // Create alert with summary message and bulleted list of unmatched errors @@ -386,7 +389,8 @@ class Form_Utils { const animations = []; // 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; if (typeof messages === 'string') { diff --git a/app/RSpade/Core/Response/Error_Response.php b/app/RSpade/Core/Response/Error_Response.php index 5fc0bf63d..bf5cb56b3 100755 --- a/app/RSpade/Core/Response/Error_Response.php +++ b/app/RSpade/Core/Response/Error_Response.php @@ -28,16 +28,16 @@ class Error_Response extends Rsx_Response_Abstract if ($metadata === null) { $this->metadata = []; } elseif (is_string($metadata)) { - $this->metadata = ['message' => $metadata]; + $this->metadata = ['_message' => $metadata]; } elseif (is_array($metadata)) { $this->metadata = $metadata; } else { - $this->metadata = ['message' => (string)$metadata]; + $this->metadata = ['_message' => (string)$metadata]; } // Set reason from message or use default - if (isset($this->metadata['message'])) { - $this->reason = $this->metadata['message']; + if (isset($this->metadata['_message'])) { + $this->reason = $this->metadata['_message']; } else { $this->reason = Ajax::get_default_message($error_code); } diff --git a/app/RSpade/helpers.php b/app/RSpade/helpers.php index 68ff37f79..4a32f28fc 100755 --- a/app/RSpade/helpers.php +++ b/app/RSpade/helpers.php @@ -1089,6 +1089,57 @@ function response_not_found(?string $message = null) 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 * diff --git a/docs/BREAKING_CHANGES.md b/docs/BREAKING_CHANGES.md index 3426a2713..108eb2538 100755 --- a/docs/BREAKING_CHANGES.md +++ b/docs/BREAKING_CHANGES.md @@ -97,17 +97,21 @@ class Frontend_Clients_Edit { ### Change -Adopted unified `response_error()` function with error code constants. 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. +Adopted unified `response_error()` function with error code constants, plus convenience helpers. Same constant names on server and client for zero mental translation. ### Pattern ```php -// Server - single function with constants +// Server - base function with constants return response_error(Ajax::ERROR_VALIDATION, ['email' => 'Invalid']); 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 diff --git a/docs/CLAUDE.dist.md b/docs/CLAUDE.dist.md index 59b7cc034..69f82f2f3 100644 --- a/docs/CLAUDE.dist.md +++ b/docs/CLAUDE.dist.md @@ -912,7 +912,7 @@ async on_load() { // Controller: save() receives all form values, validates, persists #[Ajax_Endpoint] 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->title = $params['title']; $record->save();