Add skills documentation and misc updates

Add form value persistence across cache revalidation re-renders

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-12-29 04:38:06 +00:00
parent 432d167dda
commit 1b46c5270c
21 changed files with 5540 additions and 323 deletions

View File

@@ -74,11 +74,19 @@ class Rsx_Breadcrumb_Resolver {
return generation === Rsx_Breadcrumb_Resolver._generation;
}
/**
* Build cache key including build_key for invalidation on deploy
*/
static _cache_key(url) {
const build_key = window.rsxapp?.build_key || '';
return Rsx_Breadcrumb_Resolver.CACHE_PREFIX + build_key + ':' + url;
}
/**
* Get cached chain for a URL
*/
static _get_cached_chain(url) {
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
const cache_key = Rsx_Breadcrumb_Resolver._cache_key(url);
return Rsx_Storage.session_get(cache_key);
}
@@ -86,7 +94,7 @@ class Rsx_Breadcrumb_Resolver {
* Cache a resolved chain
*/
static _cache_chain(url, chain) {
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
const cache_key = Rsx_Breadcrumb_Resolver._cache_key(url);
// Store simplified chain data (url, label, is_active)
const cache_data = chain.map(item => ({
url: item.url,
@@ -250,7 +258,7 @@ class Rsx_Breadcrumb_Resolver {
*/
static clear_cache(url) {
if (url) {
const cache_key = Rsx_Breadcrumb_Resolver.CACHE_PREFIX + url;
const cache_key = Rsx_Breadcrumb_Resolver._cache_key(url);
Rsx_Storage.session_remove(cache_key);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\RSpade\CodeQuality\Rules\JavaScript;
use App\RSpade\CodeQuality\Rules\CodeQualityRule_Abstract;
use App\RSpade\CodeQuality\RuntimeChecks\YoureDoingItWrongException;
use App\RSpade\Core\Cache\RsxCache;
use App\RSpade\Core\Manifest\Manifest;
/**
* Check Component implementations for common AI agent mistakes
@@ -47,8 +48,26 @@ class JqhtmlComponentImplementation_CodeQualityRule extends CodeQualityRule_Abst
return;
}
// Skip if not a Component subclass
if (!isset($metadata['extends']) || $metadata['extends'] !== 'Component') {
// Skip if no class defined
if (!isset($metadata['class'])) {
return;
}
// Check if this is a Component subclass (directly or indirectly)
$is_component_subclass = false;
if (isset($metadata['extends']) && $metadata['extends'] === 'Component') {
$is_component_subclass = true;
} elseif (isset($metadata['extends'])) {
// Check inheritance chain via manifest
try {
$is_component_subclass = Manifest::js_is_subclass_of($metadata['class'], 'Component');
} catch (\Exception $e) {
// Manifest not ready or class not found - skip
$is_component_subclass = false;
}
}
if (!$is_component_subclass) {
return;
}
@@ -59,6 +78,16 @@ class JqhtmlComponentImplementation_CodeQualityRule extends CodeQualityRule_Abst
return;
}
// Check for on_destroy method using manifest metadata (the correct method is on_stop)
if (!empty($metadata['public_instance_methods'])) {
foreach ($metadata['public_instance_methods'] as $method_name => $method_info) {
if ($method_name === 'on_destroy') {
$line = $method_info['line'] ?? 1;
$this->throw_on_destroy_error($file_path, $line, $metadata['class']);
}
}
}
$lines = explode("\n", $contents);
// Check for render() method and incorrect lifecycle methods
@@ -151,11 +180,48 @@ class JqhtmlComponentImplementation_CodeQualityRule extends CodeQualityRule_Abst
$error_message .= "- on_create(): Called when component is created\n";
$error_message .= "- on_load(): Called to load async data\n";
$error_message .= "- on_ready(): Called when component is ready in DOM\n";
$error_message .= "- on_destroy(): Called when component is destroyed\n\n";
$error_message .= "- on_stop(): Called when component is destroyed\n\n";
$error_message .= "FIX:\n";
$error_message .= "Rename '{$method_name}()' to 'on_{$method_name}()'\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
private function throw_on_destroy_error(string $file_path, int $line_number, string $class_name): void
{
$error_message = "==========================================\n";
$error_message .= "FATAL: Jqhtml component uses incorrect lifecycle method name\n";
$error_message .= "==========================================\n\n";
$error_message .= "File: {$file_path}\n";
$error_message .= "Line: {$line_number}\n";
$error_message .= "Class: {$class_name}\n\n";
$error_message .= "The method 'on_destroy()' does not exist in jqhtml components.\n\n";
$error_message .= "PROBLEM:\n";
$error_message .= "The correct lifecycle method for cleanup is 'on_stop()', not 'on_destroy()'.\n";
$error_message .= "This is a common mistake from other frameworks.\n\n";
$error_message .= "INCORRECT:\n";
$error_message .= " class {$class_name} extends Component {\n";
$error_message .= " on_destroy() {\n";
$error_message .= " // cleanup code\n";
$error_message .= " }\n";
$error_message .= " }\n\n";
$error_message .= "CORRECT:\n";
$error_message .= " class {$class_name} extends Component {\n";
$error_message .= " on_stop() {\n";
$error_message .= " // cleanup code\n";
$error_message .= " }\n";
$error_message .= " }\n\n";
$error_message .= "LIFECYCLE METHODS:\n";
$error_message .= "- on_create(): Called when component is created (sync)\n";
$error_message .= "- on_render(): Called after template renders (sync)\n";
$error_message .= "- on_load(): Called to load async data\n";
$error_message .= "- on_ready(): Called when component is ready in DOM\n";
$error_message .= "- on_stop(): Called when component is destroyed (sync)\n\n";
$error_message .= "FIX:\n";
$error_message .= "Rename 'on_destroy()' to 'on_stop()'\n";
$error_message .= "==========================================";
throw new YoureDoingItWrongException($error_message);
}
}

View File

@@ -0,0 +1,129 @@
FORM VALUE PERSISTENCE - MIGRATION GUIDE
Date: 2025-12-29
SUMMARY
When using stale-while-revalidate caching, forms may render from cache and then
re-render with fresh data after cache revalidation. If a user modifies an input
between these two renders, their changes would be lost. This update adds automatic
tracking and restoration of user-modified form values across re-renders within
the same parent action context.
The system works by storing changed values on the parent component (or the form
itself if no parent exists). When the form re-renders, cached values are merged
back into the form state before widgets are populated.
AFFECTED FILES
Theme form component:
- rsx/theme/components/forms/rsx_form.js
Widget components (contract requirement):
- Any custom Widget that doesn't fire 'input' events won't participate in caching
CHANGES REQUIRED
1. Add on_render() Method to Rsx_Form
Add this method after on_create():
on_render() {
// Form value persistence across cache revalidation re-renders.
// When a form renders from cache, the user may change inputs before
// the cache revalidates and the form re-renders with fresh data.
// This system caches user changes and re-applies them after re-render.
// Determine cache storage location and key
const cache_location = this.parent() || this;
const cache_key = this.parent() ? `__formvals_${this._cid}` : '__this_formvals';
// Initialize cache if it doesn't exist
if (!cache_location[cache_key]) {
cache_location[cache_key] = {};
}
const cache = cache_location[cache_key];
// If cache has values from prior render, merge into state.values
// These will be applied when on_ready calls vals()
if (Object.keys(cache).length > 0) {
Object.assign(this.state.values, cache);
}
// Register input listeners on all widgets to track user changes
const that = this;
this.$.shallowFind('.Widget').each(function () {
const $widget = $(this);
const component = $widget.component();
if (component && 'on' in component) {
const widget_name = $widget.data('name');
if (widget_name) {
component.on('input', function (comp, value) {
cache[widget_name] = value;
});
}
}
});
}
2. Widget Contract Requirements
For form value persistence to work, Widget components must:
a) Have a data-name attribute (set via $name on Form_Field)
b) Implement component.on('input', callback) event registration
c) Fire the 'input' event when the user changes the value
d) NOT fire 'input' when val() is called programmatically
Standard framework widgets (Text_Input, Select_Input, Checkbox_Input, etc.)
already meet these requirements. Custom widgets should be verified.
HOW IT WORKS
1. First Render (from cache):
- on_create: parses $data into this.state.values
- on_render: initializes cache on parent, registers input listeners
- on_ready: loads values into widgets via vals()
- User modifies a checkbox
- Input listener fires, stores { checkbox_name: new_value } in cache
2. Second Render (cache revalidated):
- on_create: parses fresh $data into this.state.values
- on_render: finds existing cache, merges cached values into this.state.values
- on_ready: loads merged values (server data + user changes) into widgets
- User's checkbox change is preserved
Cache Storage:
- If form has a parent: stored at parent.__formvals_{form_cid}
- If form has no parent: stored at form.__this_formvals
Cache Lifecycle:
- Created when form first renders
- Persists as long as parent component exists
- Automatically cleared when parent is destroyed (navigating away)
CONFIGURATION
No configuration required. The feature is automatic.
VERIFICATION
1. Open a page with a form that uses stale-while-revalidate caching
2. Quickly modify a form input (checkbox, text field, etc.) before
the cache revalidates
3. Wait for the form to re-render with fresh data
4. Verify that your change is preserved after re-render
5. In browser console, you can inspect the cache:
// If form has a parent:
Spa.action.__formvals_form
// Or check what's stored:
console.log(Object.keys(Spa.action).filter(k => k.startsWith('__formvals')));
REFERENCE
php artisan rsx:man form_conventions
rsx/theme/components/forms/rsx_form.js

1314
docs/CLAUDE.archive.12.29.25.md Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -410,74 +410,33 @@ The process involves creating Action classes with @route decorators and converti
**Note**: SPA pages are the preferred standard. Use Blade only for SEO-critical public pages or authentication flows.
```blade
@rsx_id('Frontend_Index') {{-- Every view starts with this --}}
<body class="{{ rsx_body_class() }}"> {{-- Adds view class --}}
```
Pattern recognition:
- `@rsx_id('View_Name')` - required first line
- `rsx_body_class()` - adds view class for CSS scoping
- `@rsx_page_data()` - pass data to JS
- `on_app_ready()` with page guard for JS (fires for ALL pages in bundle)
**NO inline styles, scripts, or event handlers** - Use companion `.scss` and `.js` files.
**jqhtml components** work fully in Blade (no slots).
**Detailed guidance in `blade-views` skill** - auto-activates when building Blade pages.
### SCSS Component-First Architecture
---
SCSS in `rsx/app/` and `rsx/theme/components/` must wrap in a single component class matching the jqhtml/blade file. Components auto-render with `class="Component_Name"` on root. `rsx/lib/` is non-visual. `rsx/theme/` (outside components/) holds primitives, variables, Bootstrap overrides.
## SCSS ARCHITECTURE
**BEM**: Child classes use component's exact name as prefix. `.Component_Name { &__element }` → HTML: `<div class="Component_Name__element">` (no kebab-case).
**Component-first**: Every styled element is a component with scoped SCSS. No generic classes scattered across files.
**Variables**: `rsx/theme/variables.scss` - must be included before directory includes in bundles. Multiple SCSS files can target same component if primary file exists.
**Scoping rules**:
- Files in `rsx/app/` and `rsx/theme/components/` **must** wrap in single component class
- Wrapper class matches JS class or Blade `@rsx_id`
- BEM children use PascalCase: `.Component_Name__element` (NOT kebab-case)
Details: `php artisan rsx:man scss`
**Responsive breakpoints** (Bootstrap defaults do NOT work):
- Tier 1: `mobile` (0-1023px), `desktop` (1024px+)
- Tier 2: `phone`, `tablet`, `desktop-sm`, `desktop-md`, `desktop-lg`, `desktop-xl`
- SCSS: `@include mobile { }`, `@include desktop-xl { }`
- Classes: `.col-mobile-12`, `.d-tablet-none`, `.mobile-only`
- JS: `Responsive.is_mobile()`, `Responsive.is_phone()`
### Responsive Breakpoints
RSX replaces Bootstrap breakpoints with semantic names. **Bootstrap defaults (col-md-6, d-lg-none) do NOT work.**
| Tier 1 | Range | Tier 2 | Range |
|--------|-------|--------|-------|
| `mobile` | 0-1023px | `phone` | 0-799px |
| `desktop` | 1024px+ | `tablet` | 800-1023px |
| | | `desktop-sm` | 1024-1199px |
| | | `desktop-md` | 1200-1639px |
| | | `desktop-lg` | 1640-2199px |
| | | `desktop-xl` | 2200px+ |
**SCSS**: `@include mobile { }`, `@include desktop { }`, `@include phone { }`, `@include desktop-xl { }`, etc.
**Classes**: `.col-mobile-6`, `.d-desktop-none`, `.mobile-only`, `.hide-tablet`
**JS**: `Responsive.is_mobile()`, `Responsive.is_phone()`, `Responsive.is_desktop_xl()`, etc.
Details: `php artisan rsx:man responsive`
### JavaScript for Blade Pages
Unlike SPA actions (which use component lifecycle), Blade pages use static `on_app_ready()` with a page guard:
```javascript
class My_Page { // Matches @rsx_id('My_Page')
static on_app_ready() {
if (!$('.My_Page').exists()) return; // Guard required - fires for ALL pages in bundle
// Page code here
}
}
```
### Passing Data to JavaScript
Use `@rsx_page_data` for page-specific data needed by JavaScript (IDs, config, etc.):
```blade
@rsx_page_data(['user_id' => $user->id, 'mode' => 'edit'])
@section('content')
<!-- Your HTML -->
@endsection
```
Access in JavaScript:
```javascript
const user_id = window.rsxapp.page_data.user_id;
```
Use when data doesn't belong in DOM attributes. Multiple calls merge together.
**Detailed guidance in `scss` skill** - auto-activates when styling components.
---
@@ -732,174 +691,27 @@ this.$sid('result_container').component('My_Component', {
## FORM COMPONENTS
**Form fields** (`<Rsx_Form>` with `$data`, `$controller`, `$method`):
```blade
<Rsx_Form $data="{!! json_encode($form_data) !!}" $controller="Controller" $method="save">
<Form_Field $name="email" $label="Email" $required=true>
<Text_Input $type="email" />
</Form_Field>
Forms use `<Rsx_Form>` with `$data`, `$controller`, `$method` for automatic data binding. Key components: `Form_Field`, `Form_Field_Hidden`, input types (`Text_Input`, `Select_Input`, `Checkbox_Input`).
<Form_Field_Hidden $name="id" />
</Rsx_Form>
```
Pattern recognition:
- `vals()` dual-mode method for get/set
- `Form_Utils.apply_form_errors()` for validation
- Action loads data, Controller saves it
- `$disabled=true` still returns values (unlike HTML)
- **Form_Field** - Standard formatted field with label, errors, help text
- **Form_Field_Hidden** - Single-tag hidden input (extends Form_Field_Abstract)
- **Form_Field_Abstract** - Base class for custom formatting (advanced)
**Disabled fields**: Use `$disabled=true` attribute on input components to disable fields. Unlike standard HTML, disabled fields still return values via `vals()` (useful for read-only data that should be submitted).
```blade
<Text_Input $type="email" $disabled=true />
<Select_Input $options="{!! json_encode($options) !!}" $disabled=true />
<Checkbox_Input $label="Subscribe" $disabled=true />
```
**Form component classes** use the **vals() dual-mode pattern**:
```javascript
class My_Form extends Component {
vals(values) {
if (values) {
// Setter - populate form
this.$sid('name').val(values.name || '');
return null;
} else {
// Getter - extract values
return {name: this.$sid('name').val()};
}
}
}
```
**Validation**: `Form_Utils.apply_form_errors(form.$, errors)` - Matches by `name` attribute.
### Form Conventions (Action/Controller Pattern)
Forms follow a load/save pattern mirroring traditional Laravel: Action loads data, Controller saves it.
```javascript
// Action: on_create() sets defaults, on_load() fetches for edit mode
on_create() {
this.data.form_data = { title: '', status_id: Model.STATUS_ACTIVE };
this.data.is_edit = !!this.args.id;
}
async on_load() {
if (!this.data.is_edit) return;
const record = await My_Model.fetch(this.args.id);
this.data.form_data = { id: record.id, title: record.title };
}
```
```php
// 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('Validation failed', ['title' => 'Required']);
$record = $params['id'] ? My_Model::find($params['id']) : new My_Model();
$record->title = $params['title'];
$record->save();
return ['redirect' => Rsx::Route('View_Action', $record->id)];
}
```
**Key principles**: form_data must be serializable (plain objects, no models) | Keep load/save in same controller for field alignment | on_load() loads data, on_ready() is UI-only
Details: `php artisan rsx:man form_conventions`
### Polymorphic Form Fields
For fields that can reference multiple model types (e.g., an Activity linked to either a Contact or Project), use JSON-encoded polymorphic values.
```php
use App\RSpade\Core\Polymorphic_Field_Helper;
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
Contact_Model::class,
Project_Model::class,
]);
if ($error = $eventable->validate('Please select an entity')) {
$errors['eventable'] = $error;
}
$model->eventable_type = $eventable->model;
$model->eventable_id = $eventable->id;
```
Client submits: `{"model":"Contact_Model","id":123}`. Always use `Model::class` for the whitelist.
Details: `php artisan rsx:man polymorphic`
**Detailed guidance in `forms` skill** - auto-activates when building forms.
---
## MODALS
### Built-in Dialog Types
Built-in dialogs: `Modal.alert()`, `Modal.confirm()`, `Modal.prompt()`, `Modal.select()`, `Modal.error()`.
| Method | Returns | Description |
|--------|---------|-------------|
| `Modal.alert(body)` | `void` | Simple notification |
| `Modal.alert(title, body, buttonLabel?)` | `void` | Alert with title |
| `Modal.confirm(body)` | `boolean` | Yes/no confirmation |
| `Modal.confirm(title, body, confirmLabel?, cancelLabel?)` | `boolean` | Confirmation with labels |
| `Modal.prompt(body)` | `string\|false` | Text input |
| `Modal.prompt(title, body, default?, multiline?)` | `string\|false` | Prompt with options |
| `Modal.select(body, options)` | `string\|false` | Dropdown selection |
| `Modal.select(title, body, options, default?, placeholder?)` | `string\|false` | Select with options |
| `Modal.error(error, title?)` | `void` | Error with red styling |
| `Modal.unclosable(title, body)` | `void` | Modal user cannot close |
Form modals: `Modal.form({title, component, on_submit})` - component must implement `vals()`.
```javascript
await Modal.alert("File saved");
if (await Modal.confirm("Delete?")) { /* confirmed */ }
const name = await Modal.prompt("Enter name:");
const choice = await Modal.select("Choose:", [{value: 'a', label: 'A'}, {value: 'b', label: 'B'}]);
await Modal.error("Something went wrong");
```
Reusable modals: Extend `Modal_Abstract`, implement static `show()`.
### Form Modals
```javascript
const result = await Modal.form({
title: "Edit User",
component: "User_Form",
component_args: {data: user},
on_submit: async (form) => {
const response = await User_Controller.save(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false; // Keep open
}
return response.data; // Close and return
}
});
```
Form component must implement `vals()` and include `<div $sid="error_container"></div>`.
### Modal Classes
For complex/reusable modals, create dedicated classes:
```javascript
class Add_User_Modal extends Modal_Abstract {
static async show() {
return await Modal.form({...}) || false;
}
}
// Usage
const user = await Add_User_Modal.show();
if (user) {
grid.reload();
await Next_Modal.show(user.id);
}
```
Pattern: Extend `Modal_Abstract`, implement static `show()`, return data or `false`.
Details: `php artisan rsx:man modals`
**Detailed guidance in `modals` skill** - auto-activates when building modals.
---
@@ -934,46 +746,15 @@ User_Model::create(['email' => $email]);
### Enums
**CRITICAL: Read `php artisan rsx:man enum` for complete documentation before implementing.**
Integer-backed enums with model-level mapping. Define in `$enums` array with `constant`, `label`, and custom properties.
Integer-backed enums with model-level mapping to constants, labels, and custom properties. Uses BEM-style double underscore naming for magic properties.
Pattern recognition:
- BEM-style access: `$model->status_id__label`, `$model->status_id__badge`
- JS methods: `Model.status_id__enum()`, `Model.status_id__enum_select()`
- Static constants: `Model::STATUS_ACTIVE`
- Use BIGINT columns, run `rsx:migrate:document_models` after changes
```php
class Project_Model extends Rsx_Model_Abstract {
public static $enums = [
'status_id' => [
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'badge' => 'bg-success', 'order' => 1],
2 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false, 'order' => 99],
],
];
}
// Usage - BEM-style: field__property (double underscore)
$project->status_id = Project_Model::STATUS_ACTIVE;
echo $project->status_id__label; // "Active"
echo $project->status_id__badge; // "bg-success" (custom property)
```
**Special properties**:
- `order` - Sort position in dropdowns (default: 0, lower first)
- `selectable` - Include in dropdown options (default: true). Non-selectable items excluded from `field__enum_select()` but still shown if current value.
**JavaScript static methods** (BEM-style double underscore):
```js
// Get all enum data
Project_Model.status_id__enum()
// Get specific enum's metadata by ID
Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE).badge // "bg-success"
Project_Model.status_id__enum(2).selectable // false
// Other methods
Project_Model.status_id__enum_select() // For dropdowns
Project_Model.status_id__enum_labels() // id => label map
Project_Model.status_id__enum_ids() // Array of valid IDs
```
**Migration:** Use BIGINT for enum columns, TINYINT(1) for booleans. Run `rsx:migrate:document_models` after adding enums.
**Detailed guidance in `model-enums` skill** - auto-activates when implementing enums.
### Model Fetch
@@ -1033,27 +814,13 @@ No `IF EXISTS`, no `information_schema` queries, no fallbacks. Know current stat
Files upload UNATTACHED → validate → assign via API. Session-based validation prevents cross-user file assignment.
```php
// Controller: Assign uploaded file
$attachment = File_Attachment_Model::find_by_key($params['photo_key']);
if ($attachment && $attachment->can_user_assign_this_file()) {
$attachment->attach_to($user, 'profile_photo'); // Single (replaces)
$attachment->add_to($project, 'documents'); // Multiple (adds)
}
Pattern recognition:
- `File_Attachment_Model::find_by_key()` + `can_user_assign_this_file()`
- `attach_to()` (single/replaces) vs `add_to()` (multiple/adds)
- `get_attachment()` / `get_attachments()` for retrieval
- `get_url()`, `get_download_url()`, `get_thumbnail_url()`
// Model: Retrieve attachments
$photo = $user->get_attachment('profile_photo');
$documents = $project->get_attachments('documents');
// Display
$photo->get_thumbnail_url('cover', 128, 128);
$photo->get_url();
$photo->get_download_url();
```
**Endpoints:** `POST /_upload`, `GET /_download/:key`, `GET /_thumbnail/:key/:type/:width/:height`
Details: `php artisan rsx:man file_upload`
**Detailed guidance in `file-attachments` skill** - auto-activates when handling uploads.
---
@@ -1180,48 +947,15 @@ Sessions persist 365 days. Never implement "Remember Me".
**Two Classes - Strict Separation**: `Rsx_Time` (datetimes with timezone) | `Rsx_Date` (calendar dates, no timezone)
**String-Based Philosophy**: RSpade uses ISO strings, not Carbon objects. Dates are `"2025-12-24"`, datetimes are `"2025-12-24T15:30:00-06:00"`. Same format in PHP, JavaScript, JSON, database queries. No object serialization issues.
**String-Based**: ISO strings, not Carbon. Never use `$casts` with `'date'`/`'datetime'` - blocked by `rsx:check`.
**Model Casts**: `Rsx_Model_Abstract` auto-applies `Rsx_Date_Cast` (DATE columns) and `Rsx_DateTime_Cast` (DATETIME columns). Never define `$casts` with `'date'`, `'datetime'`, or `'timestamp'` - these use Carbon and are blocked by `rsx:check`.
Pattern recognition:
- `Rsx_Time::now()`, `Rsx_Time::format()`, `Rsx_Time::relative()`
- `Rsx_Date::today()`, `Rsx_Date::format()`, `Rsx_Date::is_past()`
- `Rsx_Time::to_database()` for UTC storage
- Functions throw if wrong type passed
### Rsx_Time - Moments in Time
```php
use App\RSpade\Core\Time\Rsx_Time;
Rsx_Time::now(); // Current time in user's timezone
Rsx_Time::now_iso(); // ISO 8601 format: 2025-12-24T15:30:00-06:00
Rsx_Time::format($datetime); // "Dec 24, 2025 3:30 PM"
Rsx_Time::format_short($datetime); // "Dec 24, 3:30 PM"
Rsx_Time::to_database($datetime); // UTC for storage
Rsx_Time::get_user_timezone(); // User's timezone or default
```
```javascript
Rsx_Time.now(); // Current moment (timezone-aware)
Rsx_Time.format(datetime); // Formatted for display
Rsx_Time.relative(datetime); // "2 hours ago", "in 3 days"
```
### Rsx_Date - Calendar Dates
```php
use App\RSpade\Core\Time\Rsx_Date;
Rsx_Date::today(); // "2025-12-24" (user's timezone)
Rsx_Date::format($date); // "Dec 24, 2025"
Rsx_Date::is_today($date); // Boolean
Rsx_Date::is_past($date); // Boolean
```
**Key Principle**: Functions throw if wrong type passed (datetime to date function or vice versa).
### Server Time Sync
Client time syncs automatically via rsxapp data on page load and AJAX responses. No manual sync required.
### User Timezone
Stored in `login_users.timezone` (IANA format). Falls back to `config('rsx.datetime.default_timezone')`.
Details: `php artisan rsx:man time`
**Detailed guidance in `date-time` skill** - auto-activates when working with dates/times.
---
@@ -1321,6 +1055,33 @@ Run `rsx:check` before commits. Enforces naming, prohibits animations on non-act
---
## SKILLS (Auto-Activated)
Detailed guidance for specific tasks is available via Claude Code skills. These activate automatically when relevant - no action needed.
| Skill | Activates When |
|-------|---------------|
| `forms` | Building forms, validation, vals() pattern |
| `modals` | Creating dialogs, Modal.form(), Modal_Abstract |
| `model-enums` | Implementing status_id, type_id, enum properties |
| `file-attachments` | Upload flow, attach_to(), thumbnails |
| `date-time` | Rsx_Time, Rsx_Date, timezone handling |
| `blade-views` | Server-rendered pages, @rsx_id, @rsx_page_data |
| `scss` | Component styling, BEM, responsive breakpoints |
| `ajax-error-handling` | response_error(), Form_Utils, error codes |
| `model-fetch` | Model.fetch(), lazy relationships, #[Ajax_Endpoint_Model_Fetch] |
| `jquery-extensions` | .click() override, .exists(), .shallowFind() |
| `background-tasks` | #[Task], #[Schedule], Task::dispatch() |
| `crud-patterns` | List/view/edit structure, DataGrid, dual-route actions |
| `js-decorators` | @route, @spa, @layout, @mutex, custom decorators |
| `event-hooks` | #[OnEvent], filters, gates, actions |
| `migrations` | Raw SQL, make:migration:safe, forward-only |
| `polymorphic` | Type refs, morphTo, Polymorphic_Field_Helper |
Skills location: `~/.claude/skills/` (symlinked from `system/docs/skills/`)
---
## GETTING HELP
```bash

View File

@@ -0,0 +1,245 @@
---
name: ajax-error-handling
description: Handling Ajax errors in RSX including response formats, error codes, client-side error display, and Form_Utils. Use when implementing error handling for Ajax calls, working with response_error(), form validation errors, or debugging Ajax failures.
---
# RSX Ajax Error Handling
## Response Architecture
RSX returns **HTTP 200 for ALL Ajax responses** (success and errors). Success/failure is encoded in the response body via `_success` field.
```javascript
// Success response
{
"_success": true,
"_ajax_return_value": { /* your data */ }
}
// Error response
{
"_success": false,
"error_code": "validation|not_found|unauthorized|auth_required|fatal",
"reason": "User-friendly message",
"metadata": { /* field errors for validation */ }
}
```
**Rationale**: Batch requests need uniform status codes. Always get parseable response body. Non-200 only means "couldn't reach PHP at all".
---
## Error Codes
| Constant | Purpose |
|----------|---------|
| `Ajax::ERROR_VALIDATION` | Field validation failures |
| `Ajax::ERROR_NOT_FOUND` | Resource not found |
| `Ajax::ERROR_UNAUTHORIZED` | User lacks permission |
| `Ajax::ERROR_AUTH_REQUIRED` | User not logged in |
| `Ajax::ERROR_FATAL` | Uncaught PHP exceptions |
Constants available in both PHP (`Ajax::ERROR_*`) and JavaScript (`Ajax.ERROR_*`).
---
## Server-Side: Returning Errors
Use `response_error()` helper - never return `_success` manually:
```php
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
// Validation error with field-specific messages
if (empty($params['email'])) {
return response_error(Ajax::ERROR_VALIDATION, [
'email' => 'Email is required'
]);
}
// Not found error
$user = User_Model::find($params['id']);
if (!$user) {
return response_error(Ajax::ERROR_NOT_FOUND, 'User not found');
}
// Success - just return data (framework wraps it)
$user->name = $params['name'];
$user->save();
return ['id' => $user->id];
}
```
### Let Exceptions Bubble
Don't wrap database/framework operations in try/catch. Let exceptions bubble to the global handler:
```php
// WRONG - Don't catch framework exceptions
try {
$user->save();
} catch (Exception $e) {
return ['error' => $e->getMessage()];
}
// CORRECT - Let it throw
$user->save(); // Framework catches and formats
```
**When try/catch IS appropriate**: File uploads, external API calls, user input parsing (expected failures).
---
## Client-Side: Handling Errors
Ajax.js automatically unwraps responses:
- `_success: true` → Promise resolves with `_ajax_return_value`
- `_success: false` → Promise rejects with Error object
```javascript
try {
const user = await User_Controller.get_user(123);
console.log(user.name); // Already unwrapped
} catch (error) {
console.log(error.code); // 'validation', 'not_found', etc.
console.log(error.message); // User-displayable message
console.log(error.metadata); // Field errors for validation
}
```
### Automatic Error Display
Uncaught Ajax errors automatically display via `Modal.error()`:
```javascript
// No try/catch - error shows in modal automatically
const user = await User_Controller.get_user(123);
```
---
## Form Error Handling
### With Rsx_Form (Recommended)
```javascript
const result = await Modal.form({
title: 'Add User',
component: 'User_Form',
on_submit: async (form) => {
try {
const result = await Controller.save(form.vals());
return result; // Success - close modal
} catch (error) {
await form.render_error(error);
return false; // Keep modal open
}
}
});
```
`form.render_error()` handles all error types:
- **validation**: Shows inline field errors + alert for unmatched errors
- **fatal/network/auth**: Shows error in form's error container
### With Form_Utils (Non-Rsx_Form)
```javascript
try {
const result = await Controller.save(form_data);
} catch (error) {
if (error.code === Ajax.ERROR_VALIDATION) {
Form_Utils.apply_form_errors($form, error.metadata);
} else {
Rsx.render_error(error, '#error_container');
}
}
```
`Form_Utils.apply_form_errors()`:
- Matches errors to fields by `name` attribute
- Adds `.is-invalid` class and inline error text
- Shows alert ONLY for errors that couldn't match to fields
---
## Error Display Methods
### Modal.error() - Critical Errors
```javascript
await Modal.error(error, 'Operation Failed');
```
Red danger modal, can stack over other modals.
### Rsx.render_error() - Container Display
```javascript
Rsx.render_error(error, '#error_container');
Rsx.render_error(error, this.$sid('error'));
```
Displays error in any container element.
---
## Developer vs Production Mode
**Developer Mode** (`IS_DEVELOPER=true`):
- Full exception message
- File path and line number
- SQL queries with parameters
- Stack trace (up to 10 frames)
**Production Mode**:
- Generic message: "An unexpected error occurred"
- No technical details exposed
- Errors logged server-side
---
## Common Patterns
### Simple Validation
```php
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
$errors = [];
if (empty($params['email'])) {
$errors['email'] = 'Email is required';
}
if (empty($params['name'])) {
$errors['name'] = 'Name is required';
}
if ($errors) {
return response_error(Ajax::ERROR_VALIDATION, $errors);
}
// ... save logic
}
```
### Check Specific Error Type
```javascript
try {
const data = await Controller.get_data(id);
} catch (error) {
if (error.code === Ajax.ERROR_NOT_FOUND) {
show_not_found_message();
} else if (error.code === Ajax.ERROR_UNAUTHORIZED) {
redirect_to_login();
} else {
// Let default handler show modal
throw error;
}
}
```
## More Information
Details: `php artisan rsx:man ajax_error_handling`

View File

@@ -0,0 +1,262 @@
---
name: background-tasks
description: RSX background task system including Task and Schedule attributes, immediate CLI execution, scheduled cron jobs, and async queued tasks. Use when implementing background jobs, scheduled tasks, Task::dispatch(), or working with Rsx_Service_Abstract.
---
# RSX Task System
## Overview
RSX provides a unified task execution system with three modes:
| Mode | Use Case | Tracking |
|------|----------|----------|
| Immediate CLI | Manual/interactive runs | None |
| Scheduled | Recurring cron jobs | Database |
| Queued | Async from application | Full status |
All tasks are Service methods with `#[Task]` attribute.
---
## Service Structure
Tasks live in Service classes that extend `Rsx_Service_Abstract`:
```php
class Report_Service extends Rsx_Service_Abstract
{
#[Task('Generate monthly report')]
public static function generate_report(Task_Instance $task, array $params = [])
{
$task->log("Starting report generation...");
// ... task logic
return ['status' => 'complete', 'rows' => 1500];
}
}
```
**Location**: `/rsx/services/report_service.php`
---
## Mode 1: Immediate CLI
Run tasks directly from command line:
```bash
php artisan rsx:task:run Report_Service generate_report --month=12 --year=2025
```
Characteristics:
- Synchronous execution
- Output to STDOUT
- No timeout enforcement
- No database tracking
---
## Mode 2: Scheduled (Cron)
Add `#[Schedule]` attribute with cron syntax:
```php
#[Task('Clean thumbnails daily')]
#[Schedule('0 3 * * *')] // 3am daily
public static function clean_thumbnails(Task_Instance $task, array $params = [])
{
$task->log("Starting cleanup...");
// ... cleanup logic
}
```
### Cron Syntax Examples
| Schedule | Meaning |
|----------|---------|
| `0 3 * * *` | Daily at 3am |
| `*/15 * * * *` | Every 15 minutes |
| `0 */6 * * *` | Every 6 hours |
| `0 2 * * 1` | Mondays at 2am |
| `0 0 1 * *` | First of each month |
### Cron Setup
Add to system crontab:
```bash
* * * * * cd /var/www/html && php artisan rsx:task:process
```
Characteristics:
- Automatic execution when scheduled
- Debounced (no parallel execution of same task)
- If missed, runs as soon as possible
- Database tracking (next_run_at, started_at, completed_at)
---
## Mode 3: Queued (Async)
Dispatch tasks from application code:
```php
#[Task('Transcode video', queue: 'video', timeout: 3600)]
public static function transcode(Task_Instance $task, array $params = [])
{
$task->set_status('progress', 0);
// ... transcoding logic
$task->set_status('progress', 100);
return ['output_path' => $task->get_temp_directory() . '/output.mp4'];
}
```
### Dispatching Tasks
```php
// From controller or other code
$task_id = Task::dispatch('Video_Service', 'transcode', [
'video_id' => 123,
'format' => 'mp4'
]);
// Check status later
$status = Task::status($task_id);
// Returns: pending, running, complete, failed
```
### Task Options
```php
#[Task(
'Task description',
queue: 'default', // Queue name (for worker separation)
timeout: 120, // Max execution time in seconds
retries: 3 // Retry count on failure
)]
```
---
## Task Instance Methods
The `$task` parameter provides these methods:
```php
public static function my_task(Task_Instance $task, array $params = [])
{
// Logging
$task->log("Processing item...");
$task->info("Informational message");
$task->warning("Warning message");
$task->error("Error message");
// Progress tracking (queued tasks)
$task->set_status('progress', 50);
// Temporary directory (cleaned up after task)
$temp_dir = $task->get_temp_directory();
// Check if cancellation requested
if ($task->is_cancelled()) {
return ['status' => 'cancelled'];
}
// Return result
return ['processed' => 100];
}
```
---
## Listing Tasks
```bash
# List all registered tasks
php artisan rsx:task:list
# List scheduled tasks with next run times
php artisan rsx:task:list --scheduled
```
---
## Common Patterns
### Data Export Task
```php
#[Task('Export contacts to CSV')]
public static function export_contacts(Task_Instance $task, array $params = [])
{
$site_id = $params['site_id'];
$contacts = Contact_Model::where('site_id', $site_id)->get();
$csv = "Name,Email,Phone\n";
foreach ($contacts as $contact) {
$csv .= "{$contact->name},{$contact->email},{$contact->phone}\n";
}
$attachment = File_Attachment_Model::create_from_string(
$csv,
'contacts-export.csv',
['site_id' => $site_id]
);
return ['file_key' => $attachment->file_key];
}
// Dispatch from controller
$task_id = Task::dispatch('Export_Service', 'export_contacts', [
'site_id' => Session::get_site_id()
]);
```
### Cleanup Task (Scheduled)
```php
#[Task('Clean old sessions')]
#[Schedule('0 4 * * *')] // 4am daily
public static function clean_sessions(Task_Instance $task, array $params = [])
{
$deleted = Session_Model::where('expires_at', '<', now()->subDays(30))
->delete();
$task->log("Deleted {$deleted} expired sessions");
return ['deleted' => $deleted];
}
```
### Long-Running Task with Progress
```php
#[Task('Process large dataset', timeout: 3600)]
public static function process_dataset(Task_Instance $task, array $params = [])
{
$items = Item_Model::where('status', 'pending')->get();
$total = count($items);
foreach ($items as $i => $item) {
// Check for cancellation
if ($task->is_cancelled()) {
return ['status' => 'cancelled', 'processed' => $i];
}
// Process item
$item->process();
// Update progress
$task->set_status('progress', round(($i + 1) / $total * 100));
}
return ['processed' => $total];
}
```
## More Information
Details: `php artisan rsx:man tasks`

149
docs/skills/blade-views/SKILL.md Executable file
View File

@@ -0,0 +1,149 @@
---
name: blade-views
description: Creating Blade views and server-rendered pages in RSX. Use when building SEO pages, login/auth pages, server-rendered content, working with @rsx_id, @rsx_page_data, or Blade-specific patterns.
---
# RSX Blade Views
**Note**: SPA pages are the preferred standard. Use Blade only for:
- SEO-critical public pages
- Authentication flows (login, password reset)
- Marketing/landing pages
## Basic View Structure
Every Blade view starts with `@rsx_id`:
```blade
@rsx_id('Frontend_Index')
<body class="{{ rsx_body_class() }}">
@section('content')
<h1>Welcome</h1>
@endsection
</body>
```
- `@rsx_id('View_Name')` - Required first line, identifies the view
- `rsx_body_class()` - Adds view class for CSS scoping
## View Rules
- **NO inline styles** - Use companion `.scss` files
- **NO inline scripts** - Use companion `.js` files
- **NO inline event handlers** - Use `on_app_ready()` pattern
- **jqhtml components** work fully in Blade (but no slots)
---
## JavaScript for Blade Pages
Unlike SPA actions (which use component lifecycle), Blade pages use static `on_app_ready()`:
```javascript
class My_Page {
static on_app_ready() {
// Guard required - fires for ALL pages in bundle
if (!$('.My_Page').exists()) return;
// Page initialization code
$('.My_Page .btn').on('click', () => {
// Handle click
});
}
}
```
The guard (`if (!$('.My_Page').exists()) return;`) is essential because `on_app_ready()` fires for every page in the bundle.
---
## Passing Data to JavaScript
Use `@rsx_page_data` for data needed by JavaScript:
```blade
@rsx_page_data(['user_id' => $user->id, 'mode' => 'edit', 'config' => $config])
@section('content')
<div class="Editor">...</div>
@endsection
```
Access in JavaScript:
```javascript
class Editor {
static on_app_ready() {
if (!$('.Editor').exists()) return;
const user_id = window.rsxapp.page_data.user_id;
const mode = window.rsxapp.page_data.mode;
const config = window.rsxapp.page_data.config;
}
}
```
- Use when data doesn't belong in DOM attributes
- Multiple calls merge together
- Available after page load in `window.rsxapp.page_data`
---
## Using jqhtml Components in Blade
jqhtml components work in Blade templates:
```blade
<Dashboard>
<Stats_Panel $user_id="{{ $user->id }}" />
<Recent_Activity $limit="10" />
</Dashboard>
```
**Note**: Blade slots (`@slot`) don't work with jqhtml components. Use component attributes instead.
---
## Controller Pattern for Blade Routes
Use standard #[Route] controllers with Blade templates:
```php
/**
* @auth-exempt Login routes are public
*/
class Login_Controller extends Rsx_Controller_Abstract {
#[Route('/login', methods: ['GET', 'POST'])]
public static function index(Request $request, array $params = []) {
if ($request->is_post()) {
// Handle form submission
}
return rsx_view('Login_Index', ['data' => $data]);
}
}
```
**Key points**:
- GET/POST in same method
- Returns `rsx_view('Template_Name', $data)`
- jqhtml works but not server-rendered for SEO
---
## Converting Blade to SPA
When converting server-rendered Blade pages to SPA actions:
```bash
php artisan rsx:man blade_to_spa
```
The process involves:
1. Creating Action classes with `@route` decorators
2. Converting templates from Blade to jqhtml syntax
3. Moving data loading from controller to `on_load()`
## More Information
Details: `php artisan rsx:man blade`

View File

@@ -0,0 +1,286 @@
---
name: crud-patterns
description: Standard CRUD implementation patterns in RSX including directory structure, DataGrid lists, view pages, dual-route edit actions, and three-state loading. Use when building list/view/edit pages, implementing DataGrid, creating add/edit forms, or following RSX CRUD conventions.
---
# RSX CRUD Patterns
## Directory Structure
Each CRUD feature follows this organization:
```
rsx/app/frontend/{feature}/
├── {feature}_controller.php # Ajax endpoints
├── list/
│ ├── {Feature}_Index_Action.js # List page
│ ├── {Feature}_Index_Action.jqhtml
│ ├── {feature}_datagrid.php # DataGrid backend
│ └── {feature}_datagrid.jqhtml # DataGrid template
├── view/
│ ├── {Feature}_View_Action.js # Detail view
│ └── {Feature}_View_Action.jqhtml
└── edit/
├── {Feature}_Edit_Action.js # Add/Edit (dual-route)
└── {Feature}_Edit_Action.jqhtml
rsx/models/{feature}_model.php # With fetch() method
```
---
## The Three Subdirectories
| Directory | Purpose | Route Pattern |
|-----------|---------|---------------|
| `list/` | Index with DataGrid | `/contacts` |
| `view/` | Single record detail | `/contacts/:id` |
| `edit/` | Add AND edit form | `/contacts/add`, `/contacts/:id/edit` |
---
## Feature Controller
The controller provides Ajax endpoints for all operations:
```php
class Frontend_Contacts_Controller extends Rsx_Controller_Abstract
{
public static function pre_dispatch(Request $request, array $params = [])
{
if (!Session::is_logged_in()) return response_unauthorized();
return null;
}
#[Ajax_Endpoint]
public static function datagrid_fetch(Request $request, array $params = [])
{
return Contacts_DataGrid::fetch($params);
}
#[Ajax_Endpoint]
public static function save(Request $request, array $params = [])
{
// Validation
if (empty($params['name'])) {
return response_error(Ajax::ERROR_VALIDATION, ['name' => 'Required']);
}
// Create or update
$contact = $params['id'] ? Contact_Model::find($params['id']) : new Contact_Model();
$contact->name = $params['name'];
$contact->email = $params['email'];
$contact->save();
return ['redirect' => Rsx::Route('Contacts_View_Action', $contact->id)];
}
#[Ajax_Endpoint]
public static function delete(Request $request, array $params = [])
{
$contact = Contact_Model::find($params['id']);
$contact->delete();
return ['redirect' => Rsx::Route('Contacts_Index_Action')];
}
}
```
---
## List Page (Index Action)
```javascript
@route('/contacts')
@layout('Frontend_Layout')
@spa('Frontend_Spa_Controller::index')
class Contacts_Index_Action extends Spa_Action {
async on_load() {
// DataGrid fetches its own data
}
}
```
```jqhtml
<Define:Contacts_Index_Action tag="div">
<div class="d-flex justify-content-between mb-3">
<h1>Contacts</h1>
<a href="<%= Rsx.Route('Contacts_Edit_Action') %>" class="btn btn-primary">
Add Contact
</a>
</div>
<Contacts_DataGrid />
</Define:Contacts_Index_Action>
```
---
## DataGrid Backend
```php
class Contacts_DataGrid extends DataGrid_Abstract
{
protected static function query(): Builder
{
return Contact_Model::query()
->where('site_id', Session::get_site_id());
}
protected static function columns(): array
{
return [
'name' => ['label' => 'Name', 'sortable' => true],
'email' => ['label' => 'Email', 'sortable' => true],
'created_at' => ['label' => 'Created', 'sortable' => true],
];
}
protected static function default_sort(): array
{
return ['name' => 'asc'];
}
}
```
---
## View Page (Three-State Pattern)
View pages use the three-state loading pattern: loading → error → content.
```javascript
@route('/contacts/:id')
@layout('Frontend_Layout')
@spa('Frontend_Spa_Controller::index')
class Contacts_View_Action extends Spa_Action {
on_create() {
this.data.contact = null;
this.data.error = null;
}
async on_load() {
try {
this.data.contact = await Contact_Model.fetch(this.args.id);
} catch (e) {
this.data.error = e;
}
}
}
```
```jqhtml
<Define:Contacts_View_Action tag="div">
<% if (!this.data.contact && !this.data.error) { %>
<Loading_Spinner />
<% } else if (this.data.error) { %>
<Universal_Error_Page_Component $error="<%= this.data.error %>" />
<% } else { %>
<h1><%= this.data.contact.name %></h1>
<p>Email: <%= this.data.contact.email %></p>
<a href="<%= Rsx.Route('Contacts_Edit_Action', this.data.contact.id) %>"
class="btn btn-primary">Edit</a>
<% } %>
</Define:Contacts_View_Action>
```
---
## Edit Page (Dual-Route Action)
A single action handles both add and edit modes:
```javascript
@route('/contacts/add')
@route('/contacts/:id/edit')
@layout('Frontend_Layout')
@spa('Frontend_Spa_Controller::index')
class Contacts_Edit_Action extends Spa_Action {
on_create() {
this.data.form_data = { name: '', email: '' };
this.data.is_edit = !!this.args.id;
this.data.error = null;
}
async on_load() {
if (!this.data.is_edit) return;
try {
const contact = await Contact_Model.fetch(this.args.id);
this.data.form_data = {
id: contact.id,
name: contact.name,
email: contact.email
};
} catch (e) {
this.data.error = e;
}
}
}
```
```jqhtml
<Define:Contacts_Edit_Action tag="div">
<% if (this.data.is_edit && !this.data.form_data.id && !this.data.error) { %>
<Loading_Spinner />
<% } else if (this.data.error) { %>
<Universal_Error_Page_Component $error="<%= this.data.error %>" />
<% } else { %>
<h1><%= this.data.is_edit ? 'Edit Contact' : 'Add Contact' %></h1>
<Rsx_Form $controller="Frontend_Contacts_Controller" $method="save"
$data="<%= JSON.stringify(this.data.form_data) %>">
<Form_Hidden_Field $name="id" />
<Form_Field $name="name" $label="Name" $required=true>
<Text_Input />
</Form_Field>
<Form_Field $name="email" $label="Email">
<Text_Input $type="email" />
</Form_Field>
<button type="submit" class="btn btn-primary">
<%= this.data.is_edit ? 'Save Changes' : 'Create Contact' %>
</button>
</Rsx_Form>
<% } %>
</Define:Contacts_Edit_Action>
```
---
## Model with fetch()
```php
class Contact_Model extends Rsx_Model_Abstract
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
if (!Session::is_logged_in()) {
return false;
}
$contact = static::where('site_id', Session::get_site_id())
->find($id);
return $contact ?: false;
}
}
```
---
## Key Principles
1. **SPA controllers = Ajax endpoints only** - No page rendering
2. **Single action handles add/edit** - Dual `@route` decorators
3. **Models implement `fetch()`** - With `#[Ajax_Endpoint_Model_Fetch]`
4. **DataGrids extend `DataGrid_Abstract`** - Query + columns + sorting
5. **Three-state pattern** - Loading → Error → Content
6. **form_data must be serializable** - Plain objects, not models
## More Information
Details: `php artisan rsx:man crud`

244
docs/skills/date-time/SKILL.md Executable file
View File

@@ -0,0 +1,244 @@
---
name: date-time
description: Working with dates and times in RSX using Rsx_Time and Rsx_Date classes. Use when formatting dates/times for display, handling user timezones, working with datetime columns, converting between formats, or displaying relative times like "2 hours ago".
---
# RSX Date & Time Handling
## Two Classes - Strict Separation
| Class | Purpose | Example |
|-------|---------| --------|
| `Rsx_Time` | Moments in time (with timezone) | `2025-12-24T15:30:00Z` |
| `Rsx_Date` | Calendar dates (no timezone) | `2025-12-24` |
**Critical**: Functions throw if wrong type passed (datetime to date function or vice versa). This is intentional - mixing types causes bugs.
---
## Strings, Not Objects
RSX uses ISO strings, not Carbon objects:
- **Dates**: `"2025-12-24"`
- **Datetimes**: `"2025-12-24T15:30:00Z"`
Same format in PHP, JavaScript, JSON, and database queries. No serialization surprises.
```php
$model->created_at // "2025-12-24T15:30:45.123Z" (string)
$model->due_date // "2025-12-24" (string)
Rsx_Time::now_iso() // "2025-12-24T15:30:45.123Z" (string)
```
Carbon is used internally for calculations, never exposed externally.
---
## Model Casts (Automatic)
`Rsx_Model_Abstract` auto-applies casts based on column type:
- DATE columns → `Rsx_Date_Cast`
- DATETIME columns → `Rsx_DateTime_Cast`
**Never define** `$casts` with `'date'`, `'datetime'`, or `'timestamp'` - these use Carbon and are blocked by `rsx:check`.
---
## Rsx_Time - Moments in Time
### PHP Usage
```php
use App\RSpade\Core\Time\Rsx_Time;
// Current time
Rsx_Time::now(); // Carbon (for calculations only)
Rsx_Time::now_iso(); // ISO 8601: 2025-12-24T15:30:00Z
// Formatting for display
Rsx_Time::format_datetime($datetime); // "Dec 24, 2025 3:30 PM"
Rsx_Time::format_datetime_with_tz($datetime); // "Dec 24, 2025 3:30 PM CST"
Rsx_Time::format_time($datetime); // "3:30 PM"
Rsx_Time::relative($datetime); // "2 hours ago"
// Database storage (always UTC)
Rsx_Time::to_database($datetime); // Converts to MySQL format
// Timezone
Rsx_Time::get_user_timezone(); // User's timezone or default
Rsx_Time::to_user_timezone($datetime); // Convert to user's timezone
```
### JavaScript Usage
```javascript
// Current time (synced with server)
Rsx_Time.now(); // Date object
Rsx_Time.now_iso(); // ISO string
// Formatting
Rsx_Time.format_datetime(datetime); // "Dec 24, 2025 3:30 PM"
Rsx_Time.format_datetime_with_tz(datetime); // "Dec 24, 2025 3:30 PM CST"
Rsx_Time.format_time(datetime); // "3:30 PM"
Rsx_Time.relative(datetime); // "2 hours ago", "in 3 days"
// Arithmetic
Rsx_Time.add(datetime, 3600); // Add seconds, returns ISO string
Rsx_Time.subtract(datetime, 3600); // Subtract seconds
```
---
## Rsx_Date - Calendar Dates
### PHP Usage
```php
use App\RSpade\Core\Time\Rsx_Date;
// Current date
Rsx_Date::today(); // "2025-12-24" (user's timezone)
// Formatting
Rsx_Date::format($date); // "Dec 24, 2025"
// Comparisons
Rsx_Date::is_today($date); // Boolean
Rsx_Date::is_past($date); // Boolean
Rsx_Date::is_future($date); // Boolean
Rsx_Date::diff_days($date1, $date2); // Days between
```
### JavaScript Usage
```javascript
// Current date
Rsx_Date.today(); // "2025-12-24"
// Formatting
Rsx_Date.format(date); // "Dec 24, 2025"
// Comparisons
Rsx_Date.is_today(date); // Boolean
Rsx_Date.is_past(date); // Boolean
```
---
## Live Countdown/Countup (JavaScript)
For real-time updating displays:
```javascript
// Countdown to future time
const ctrl = Rsx_Time.countdown(this.$sid('timer'), deadline, {
short: true, // "2h 30m" vs "2 hours and 30 minutes"
on_complete: () => this.reload()
});
// Stop countdown when leaving page
this.on_stop(() => ctrl.stop());
// Countup from past time (elapsed time)
Rsx_Time.countup(this.$sid('elapsed'), started_at, { short: true });
```
---
## Server Time Sync
Client time syncs automatically via `rsxapp` data on page load and AJAX responses. No manual sync required.
```javascript
// Client always has accurate server time
const server_now = Rsx_Time.now(); // Synced with server, corrects for clock skew
```
---
## User Timezone
Stored in `login_users.timezone` (IANA format, e.g., `America/Chicago`).
Falls back to `config('rsx.datetime.default_timezone')`.
```php
// Get user's timezone
$tz = Rsx_Time::get_user_timezone();
// All Rsx_Time methods automatically use user's timezone for display
```
---
## Component Expectations
### Date_Picker Component
- `val()` returns `"YYYY-MM-DD"` or `null`
- `val(value)` accepts `"YYYY-MM-DD"` or `null`
- **Throws** if passed datetime format
- Display shows localized format (e.g., "Dec 24, 2025")
### Datetime_Picker Component
- `val()` returns ISO 8601 string or `null`
- `val(value)` accepts ISO 8601 string or `null`
- **Throws** if passed date-only format
- Display shows localized time in user's timezone
---
## Common Patterns
### Display in Template
```jqhtml
<span><%= Rsx_Time.format_datetime(this.data.record.created_at) %></span>
<span><%= Rsx_Time.relative(this.data.record.updated_at) %></span>
<span><%= Rsx_Date.format(this.data.record.due_date) %></span>
```
### Conditional Display
```jqhtml
<% if (Rsx_Date.is_past(this.data.task.due_date)) { %>
<span class="text-danger">Overdue</span>
<% } %>
```
### Save to Database
```php
// Datetime - store in UTC
$record->scheduled_at = Rsx_Time::to_database($params['scheduled_at']);
// Date - store as-is
$record->due_date = $params['due_date']; // Already "YYYY-MM-DD"
```
### Parse User Input
```php
// If user enters a datetime string
$datetime = Rsx_Time::parse($params['datetime']); // Returns Carbon
$iso = Rsx_Time::to_iso($datetime); // Convert back to string
// If user enters a date string
$date = Rsx_Date::parse($params['date']); // Returns "YYYY-MM-DD"
```
---
## Key Rules
1. **Never use Carbon directly** - RSX uses string-based dates
2. **Never use PHP's date()** - Use Rsx_Time/Rsx_Date
3. **Store datetimes in UTC** - Use `Rsx_Time::to_database()`
4. **Display in user's timezone** - Automatic via Rsx_Time format methods
5. **Dates have no timezone** - Use Rsx_Date for calendar dates
6. **Wrong types throw** - Date functions reject datetimes and vice versa
## More Information
Details: `php artisan rsx:man time`

252
docs/skills/event-hooks/SKILL.md Executable file
View File

@@ -0,0 +1,252 @@
---
name: event-hooks
description: RSX PHP event system with filters, gates, and actions using the #[OnEvent] attribute. Use when extending framework behavior, implementing authorization hooks, transforming data through handlers, or triggering custom events.
---
# RSX Event Hooks
## Overview
RSX provides an attribute-based event system with three event types:
| Type | Purpose | Return Handling |
|------|---------|-----------------|
| **Filter** | Transform data through chain | Each handler modifies and returns data |
| **Gate** | Authorization checks | First non-true stops execution |
| **Action** | Side effects | Return values ignored |
Unlike Laravel's EventServiceProvider, RSX uses `#[OnEvent]` attributes with automatic manifest discovery.
---
## Creating Event Handlers
Place handler classes in `/rsx/handlers/`:
```php
namespace Rsx\Handlers;
class Upload_Handlers
{
#[OnEvent('file.upload.authorize', priority: 10)]
public static function require_auth($data)
{
if (!Session::is_logged_in()) {
return response()->json(['error' => 'Auth required'], 403);
}
return true;
}
#[OnEvent('file.upload.params', priority: 20)]
public static function add_metadata($params)
{
$params['uploaded_by'] = Session::get_user_id();
return $params;
}
}
```
Handlers are automatically discovered - no manual registration needed.
---
## Filter Events
Transform data through a chain of handlers. Each handler receives the result of the previous handler.
```php
// Trigger
$result = Rsx::trigger_filter('event.name', $data);
// Handler - MUST return modified data
#[OnEvent('post.content.filter', priority: 10)]
public static function sanitize_html($content)
{
return strip_tags($content, '<p><a><strong>');
}
#[OnEvent('post.content.filter', priority: 20)]
public static function add_signature($content)
{
return $content . "\n\nPosted via RSX";
}
```
**Flow**: Handler 1 (priority 10) → Handler 2 (priority 20) → Final result
---
## Gate Events
Authorization checks where first non-true response halts execution.
```php
// Trigger
$result = Rsx::trigger_gate('api.access.authorize', $data);
if ($result !== true) {
return $result; // Access denied
}
// Handler - return true to allow, anything else to deny
#[OnEvent('api.access.authorize', priority: 10)]
public static function check_auth($data)
{
if (!Session::is_logged_in()) {
return response()->json(['error' => 'Login required'], 401);
}
return true;
}
#[OnEvent('api.access.authorize', priority: 20)]
public static function check_permissions($data)
{
if (!$data['user']->can($data['permission'])) {
return response()->json(['error' => 'Forbidden'], 403);
}
return true;
}
```
**Flow**: Handler 1 returns true → Handler 2 returns false → STOPS, returns false
---
## Action Events
Fire-and-forget side effects. Return values are ignored.
```php
// Trigger
Rsx::trigger_action('user.created', ['user' => $user]);
// Handler - no return value needed
#[OnEvent('user.created', priority: 10)]
public static function send_welcome_email($data): void
{
Email_Service::send_welcome($data['user']);
}
#[OnEvent('user.created', priority: 20)]
public static function log_signup($data): void
{
Activity_Model::log('signup', $data['user']->id);
}
```
---
## Priority
Handlers execute in priority order (lowest number first):
```php
#[OnEvent('my.event', priority: 10)] // Runs first
#[OnEvent('my.event', priority: 20)] // Runs second
#[OnEvent('my.event', priority: 30)] // Runs third
```
Default priority is 10 if not specified.
---
## Common Use Cases
### File Upload Authorization
```php
#[OnEvent('file.upload.authorize', priority: 10)]
public static function require_auth($data)
{
if (!Session::is_logged_in()) {
return response()->json(['error' => 'Auth required'], 403);
}
return true;
}
#[OnEvent('file.download.authorize', priority: 10)]
public static function check_file_access($data)
{
$attachment = $data['attachment'];
if ($attachment->site_id !== Session::get_site_id()) {
return response()->json(['error' => 'Access denied'], 403);
}
return true;
}
```
### Transforming Request Data
```php
#[OnEvent('contact.save.params', priority: 10)]
public static function normalize_phone($params)
{
if (!empty($params['phone'])) {
$params['phone'] = preg_replace('/[^0-9]/', '', $params['phone']);
}
return $params;
}
```
### Logging Actions
```php
#[OnEvent('project.deleted', priority: 10)]
public static function log_deletion($data): void
{
Audit_Log::create([
'action' => 'project_deleted',
'entity_id' => $data['project']->id,
'user_id' => Session::get_user_id(),
]);
}
```
---
## Triggering Custom Events
```php
// In your controller or service
class Project_Controller
{
#[Ajax_Endpoint]
public static function delete(Request $request, array $params = [])
{
$project = Project_Model::find($params['id']);
// Gate: Check if deletion allowed
$auth = Rsx::trigger_gate('project.delete.authorize', [
'project' => $project,
'user' => Session::get_user()
]);
if ($auth !== true) {
return $auth;
}
$project->delete();
// Action: Notify listeners
Rsx::trigger_action('project.deleted', ['project' => $project]);
return ['success' => true];
}
}
```
---
## Handler Organization
```
rsx/handlers/
├── auth_handlers.php # Authentication hooks
├── file_handlers.php # File upload/download hooks
├── notification_handlers.php
└── audit_handlers.php
```
Each file can contain multiple handler methods with different event subscriptions.
## More Information
Details: `php artisan rsx:man event_hooks`

View File

@@ -0,0 +1,300 @@
---
name: file-attachments
description: Handling file uploads and attachments in RSX including upload flow, attaching files to models, retrieving attachments, and generating URLs. Use when implementing file uploads, working with File_Attachment_Model, attaching files to records, displaying thumbnails, or handling document downloads.
---
# RSX File Attachments
## Two-Model Architecture
RSX separates physical storage from logical metadata:
| Model | Purpose |
|-------|---------|
| `File_Storage_Model` | Physical files on disk (framework model) |
| `File_Attachment_Model` | Logical uploads with metadata (user model) |
This enables **deduplication** - identical files share physical storage while maintaining separate metadata.
---
## Upload Flow
Files follow a secure two-step process:
1. **Upload** - File uploaded UNATTACHED via `POST /_upload`
2. **Attach** - Controller validates and assigns to model
This session-based validation prevents cross-user file assignment.
---
## Uploading Files
### Frontend Component
Use the built-in file upload component:
```jqhtml
<File_Upload
$name="document"
$accept=".pdf,.doc,.docx"
$max_size="10MB"
/>
```
### Upload Response
Upload returns a `file_key` that identifies the unattached file:
```javascript
// After upload completes
const file_key = upload_component.get_file_key();
```
---
## Attaching Files to Models
### Single Attachment (Replaces)
For one-to-one relationships (e.g., profile photo):
```php
#[Ajax_Endpoint]
public static function save_photo(Request $request, array $params = []) {
$user = User_Model::find($params['user_id']);
$attachment = File_Attachment_Model::find_by_key($params['photo_key']);
if ($attachment && $attachment->can_user_assign_this_file()) {
$attachment->attach_to($user, 'profile_photo');
}
return ['success' => true];
}
```
`attach_to()` replaces any existing attachment with that slot name.
### Multiple Attachments (Adds)
For one-to-many relationships (e.g., project documents):
```php
$attachment = File_Attachment_Model::find_by_key($params['document_key']);
if ($attachment && $attachment->can_user_assign_this_file()) {
$attachment->add_to($project, 'documents');
}
```
`add_to()` adds to the collection without removing existing files.
### Detaching Files
```php
$attachment->detach();
```
---
## Retrieving Attachments
### Single Attachment
```php
$photo = $user->get_attachment('profile_photo');
if ($photo) {
echo $photo->get_url();
}
```
### Multiple Attachments
```php
$documents = $project->get_attachments('documents');
foreach ($documents as $doc) {
echo $doc->file_name;
}
```
---
## Displaying Files
### Direct URL
```php
$url = $attachment->get_url();
// Returns: /_download/{key}
```
### Download URL (Forces Download)
```php
$url = $attachment->get_download_url();
// Returns: /_download/{key}?download=1
```
### Thumbnail URL
For images with automatic resizing:
```php
// Crop to exact dimensions
$url = $attachment->get_thumbnail_url('cover', 128, 128);
// Fit within dimensions (maintains aspect ratio)
$url = $attachment->get_thumbnail_url('contain', 200, 200);
// Scale to width, auto height
$url = $attachment->get_thumbnail_url('width', 300);
```
Thumbnail types:
- `cover` - Crop to fill exact dimensions
- `contain` - Fit within dimensions
- `width` - Scale to width, maintain aspect ratio
---
## Template Usage
```jqhtml
<% if (this.data.user.profile_photo) { %>
<img src="<%= this.data.user.profile_photo.get_thumbnail_url('cover', 64, 64) %>"
alt="Profile photo" />
<% } %>
<% for (const doc of this.data.project.documents) { %>
<a href="<%= doc.get_download_url() %>">
<%= doc.file_name %>
</a>
<% } %>
```
---
## File Attachment Model Properties
```php
$attachment->file_name; // Original uploaded filename
$attachment->mime_type; // MIME type (e.g., 'image/jpeg')
$attachment->file_size; // Size in bytes
$attachment->file_key; // Unique identifier
$attachment->created_at; // Upload timestamp
```
---
## Creating Attachments Programmatically
### From Disk
```php
$attachment = File_Attachment_Model::create_from_disk(
'/tmp/import/document.pdf',
[
'site_id' => $site->id,
'filename' => 'imported-document.pdf',
'fileable_category' => 'import'
]
);
```
### From String Content
```php
$csv = "Name,Email\nJohn,john@example.com";
$attachment = File_Attachment_Model::create_from_string(
$csv,
'export.csv',
['site_id' => $site->id, 'fileable_category' => 'export']
);
```
### From URL
```php
$attachment = File_Attachment_Model::create_from_url(
'https://example.com/logo.png',
['site_id' => $site->id, 'fileable_category' => 'logo']
);
```
---
## Security Considerations
### Always Validate Before Attaching
```php
$attachment = File_Attachment_Model::find_by_key($params['file_key']);
// REQUIRED: Check user can assign this file
if (!$attachment || !$attachment->can_user_assign_this_file()) {
return response_error(Ajax::ERROR_UNAUTHORIZED, 'Invalid file');
}
// Now safe to attach
$attachment->attach_to($model, 'slot_name');
```
### File Validation
Check file type before attaching:
```php
if (!in_array($attachment->mime_type, ['image/jpeg', 'image/png', 'image/gif'])) {
return response_form_error('Validation failed', ['photo' => 'Must be an image']);
}
```
---
## Event Hooks for Authorization
Control access with event hooks:
```php
// Require auth for uploads
#[OnEvent('file.upload.authorize', priority: 10)]
public static function require_auth($data) {
if (!Session::is_logged_in()) {
return response()->json(['error' => 'Authentication required'], 403);
}
return true;
}
// Restrict thumbnail access
#[OnEvent('file.thumbnail.authorize', priority: 10)]
public static function check_file_access($data) {
if ($data['attachment']->created_by !== $data['user']?->id) {
return response()->json(['error' => 'Access denied'], 403);
}
return true;
}
// Additional restrictions for downloads
#[OnEvent('file.download.authorize', priority: 10)]
public static function require_premium($data) {
if (!$data['user']?->has_premium()) {
return response()->json(['error' => 'Premium required'], 403);
}
return true;
}
```
---
## System Endpoints
| Endpoint | Purpose |
|----------|---------|
| `POST /_upload` | Upload new file |
| `GET /_download/:key` | Download/view file |
| `GET /_thumbnail/:key/:type/:width/:height` | Get resized image |
| `GET /_icon_by_extension/:extension` | Get file type icon |
## More Information
Details: `php artisan rsx:man file_upload`

284
docs/skills/forms/SKILL.md Executable file
View File

@@ -0,0 +1,284 @@
---
name: forms
description: Building RSX forms with Rsx_Form, Form_Field, input components, data binding, validation, and the vals() pattern. Use when creating forms, handling form submissions, implementing form validation, working with Form_Field or Form_Input components, or implementing polymorphic form fields.
---
# RSX Form Components
## Core Form Structure
Forms use `<Rsx_Form>` with automatic data binding:
```jqhtml
<Rsx_Form $data="<%= JSON.stringify(this.data.form_data) %>"
$controller="Controller" $method="save">
<Form_Field $name="email" $label="Email" $required=true>
<Text_Input $type="email" />
</Form_Field>
<Form_Hidden_Field $name="id" />
</Rsx_Form>
```
## Field Components
| Component | Purpose |
|-----------|---------|
| `Form_Field` | Standard formatted field with label, errors, help text |
| `Form_Hidden_Field` | Single-tag hidden input (extends Form_Field_Abstract) |
| `Form_Field_Abstract` | Base class for custom formatting (advanced) |
## Input Components
| Component | Usage |
|-----------|-------|
| `Text_Input` | Text, email, url, tel, number, textarea |
| `Select_Input` | Dropdown with options array |
| `Checkbox_Input` | Checkbox with optional label |
| `Radio_Input` | Radio button group |
| `Wysiwyg_Input` | Rich text editor (Quill) |
### Text_Input Attributes
```jqhtml
<Text_Input $type="email" $placeholder="user@example.com" />
<Text_Input $type="textarea" $rows="5" />
<Text_Input $type="number" $min="0" $max="100" />
<Text_Input $prefix="@" $placeholder="username" />
<Text_Input $maxlength="100" />
```
### Select_Input Formats
```jqhtml
<%-- Simple array --%>
<Select_Input $options="<%= JSON.stringify(['Option 1', 'Option 2']) %>" />
<%-- Value/label objects --%>
<Select_Input $options="<%= JSON.stringify([
{value: 'opt1', label: 'Option 1'},
{value: 'opt2', label: 'Option 2'}
]) %>" />
<%-- From model enum --%>
<Select_Input $options="<%= JSON.stringify(Project_Model.status_id__enum_select()) %>" />
```
---
## Disabled Fields
Use `$disabled=true` on input components. Unlike standard HTML, disabled fields still return values via `vals()` (useful for read-only data that should be submitted).
```jqhtml
<Text_Input $type="email" $disabled=true />
<Select_Input $options="..." $disabled=true />
```
---
## Multi-Column Layouts
Use Bootstrap grid for multi-column field layouts:
```jqhtml
<div class="row">
<div class="col-md-6">
<Form_Field $name="first_name" $label="First Name">
<Text_Input />
</Form_Field>
</div>
<div class="col-md-6">
<Form_Field $name="last_name" $label="Last Name">
<Text_Input />
</Form_Field>
</div>
</div>
```
---
## The vals() Dual-Mode Pattern
Form components implement `vals()` for get/set:
```javascript
class My_Form extends Component {
vals(values) {
if (values) {
// Setter - populate form
this.$sid('name').val(values.name || '');
return null;
} else {
// Getter - extract values
return {name: this.$sid('name').val()};
}
}
}
```
---
## Form Validation
Apply server-side validation errors:
```javascript
const response = await Controller.save(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
}
```
Errors match by `name` attribute on form fields.
---
## Action/Controller Pattern
Forms follow load/save mirroring traditional Laravel:
**Action (loads data):**
```javascript
on_create() {
this.data.form_data = { title: '', status_id: Model.STATUS_ACTIVE };
this.data.is_edit = !!this.args.id;
}
async on_load() {
if (!this.data.is_edit) return;
const record = await My_Model.fetch(this.args.id);
this.data.form_data = { id: record.id, title: record.title };
}
```
**Controller (saves data):**
```php
#[Ajax_Endpoint]
public static function save(Request $request, array $params = []) {
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();
return ['redirect' => Rsx::Route('View_Action', $record->id)];
}
```
**Key principles:**
- `form_data` must be serializable (plain objects, no models)
- Keep load/save in same controller for field alignment
- `on_load()` loads data, `on_ready()` is UI-only
---
## Repeater Fields
For arrays of values (relationships, multiple items):
**Simple repeaters (array of IDs):**
```javascript
// form_data
this.data.form_data = {
client_ids: [1, 5, 12],
};
// Controller receives
$params['client_ids'] // [1, 5, 12]
// Sync
$project->clients()->sync($params['client_ids'] ?? []);
```
**Complex repeaters (array of objects):**
```javascript
// form_data
this.data.form_data = {
team_members: [
{user_id: 1, role_id: 2},
{user_id: 5, role_id: 1},
],
};
// Controller receives
$params['team_members'] // [{user_id: 1, role_id: 2}, ...]
// Sync with pivot data
$project->team()->detach();
foreach ($params['team_members'] ?? [] as $member) {
$project->team()->attach($member['user_id'], [
'role_id' => $member['role_id'],
]);
}
```
---
## Test Data (Debug Mode)
Widgets can implement `seed()` for debug mode test data. Rsx_Form displays "Fill Test Data" button when `window.rsxapp.debug` is true.
```jqhtml
<Text_Input $seeder="company_name" />
<Text_Input $seeder="email" />
<Text_Input $seeder="phone" />
```
---
## Creating Custom Input Components
Extend `Form_Input_Abstract`:
```javascript
class My_Custom_Input extends Form_Input_Abstract {
on_create() {
// NO on_load() - never use this.data
}
on_ready() {
// Render elements EMPTY - form calls val(value) to populate AFTER render
}
// Required: get/set value
val(value) {
if (value !== undefined) {
// Set value
this.$sid('input').val(value);
} else {
// Get value
return this.$sid('input').val();
}
}
}
```
Reference implementations: `Select_Input`, `Text_Input`, `Checkbox_Input`
---
## Polymorphic Form Fields
For fields that can reference multiple model types:
```php
use App\RSpade\Core\Polymorphic_Field_Helper;
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
Contact_Model::class,
Project_Model::class,
]);
if ($error = $eventable->validate('Please select an entity')) {
$errors['eventable'] = $error;
}
$model->eventable_type = $eventable->model;
$model->eventable_id = $eventable->id;
```
Client submits: `{"model":"Contact_Model","id":123}`. Always use `Model::class` for the whitelist.
## More Information
Details: `php artisan rsx:man form_conventions`, `php artisan rsx:man forms_and_widgets`

View File

@@ -0,0 +1,202 @@
---
name: jquery-extensions
description: RSX jQuery extensions including the click() override that auto-prevents default, existence checks, component-aware traversal, and form validation helpers. Use when working with click handlers, checking element existence, finding sibling components, or understanding why links don't navigate.
---
# RSX jQuery Extensions
## Critical: Click Override
**RSX overrides jQuery `.click()` to automatically call `e.preventDefault()`**
This is the most important thing to know. All click handlers prevent default behavior automatically:
```javascript
// RSX - preventDefault is automatic
$('.btn').click(function(e) {
do_something(); // Link won't navigate, form won't submit
});
// Vanilla jQuery - must remember to call it
$('.btn').click(function(e) {
e.preventDefault(); // Easy to forget!
do_something();
});
```
**Why**: `preventDefault` is correct 95% of the time. Making it automatic eliminates a common source of bugs.
---
## When You Need Native Behavior
Use `.click_allow_default()` for the rare cases where you want native browser behavior:
```javascript
// Analytics tracking before navigation
$('a.external').click_allow_default(function(e) {
analytics.track('external_link');
// Navigation happens after handler
});
// Conditional preventDefault
$('button[type=submit]').click_allow_default(function(e) {
if (!validate_form()) {
e.preventDefault(); // Only prevent if invalid
}
});
```
**Valid use cases for `.click_allow_default()`**:
- Analytics tracking before navigation
- Conditional form submission
- Progressive enhancement fallbacks
**Invalid use cases** (use standard `.click()` instead):
- Opening modals
- Triggering Ajax actions
- Any case where you don't want navigation
---
## Existence Checks
```javascript
// RSX - cleaner syntax
if ($('.element').exists()) {
// Element is in DOM
}
// Vanilla jQuery equivalent
if ($('.element').length > 0) { ... }
```
---
## Visibility and State
```javascript
// Is element visible (not display:none)?
if ($('.modal').is_visible()) {
$('.modal').fadeOut();
}
// Is element attached to DOM?
if ($('.dynamic').is_in_dom()) {
// Element is live in the page
}
// Is element in viewport?
if ($('.lazy-image').is_in_viewport()) {
load_image($(this));
}
```
---
## Component-Aware Traversal
### shallowFind(selector)
Finds child elements without descending into nested components of the same type:
```javascript
// Only finds direct Form_Field children, not fields in nested sub-forms
this.$.shallowFind('.Form_Field').each(function() {
// Process only this form's fields
});
```
Example DOM:
```
Component_A
└── div
└── Widget (found)
└── span
└── Widget (not found - has Widget parent)
```
### closest_sibling(selector)
Searches for elements within progressively higher ancestor containers. Useful for component-to-component communication:
```javascript
// Country selector finding its related state selector
this.tom_select.on('change', () => {
const state_input = this.$.closest_sibling('.State_Select_Input');
if (state_input.exists()) {
state_input.component().set_country_code(this.val());
}
});
```
Algorithm:
1. Get parent, search within it
2. If not found, move to parent's parent
3. Repeat until found or reaching `<body>`
---
## Form Validation
```javascript
// Check if form passes HTML5 validation
if ($('form').checkValidity()) {
submit_form();
}
// Show browser's native validation UI
if (!$('form').reportValidity()) {
return; // Browser shows validation errors
}
// Programmatically submit (triggers validation)
$('form').requestSubmit();
```
---
## Other Helpers
```javascript
// Get lowercase tag name
if ($element.tagname() === 'a') {
// It's a link
}
// Check if link is external
if ($('a').is_external()) {
$(this).attr('target', '_blank');
}
// Scroll to bring element into view
$('.error-field').scroll_up_to(300); // 300ms animation
```
---
## RSX vs Vanilla jQuery Comparison
| Operation | Vanilla jQuery | RSX |
|-----------|---------------|-----|
| Click with preventDefault | `e.preventDefault()` required | Automatic |
| Existence check | `.length > 0` | `.exists()` |
| Form validation | `$('form')[0].checkValidity()` | `$('form').checkValidity()` |
| Native click behavior | `.click()` | `.click_allow_default()` |
---
## Troubleshooting
**Problem**: Links not navigating when they should
**Solution**: Use `.click_allow_default()` instead of `.click()`
**Problem**: Form submitting unexpectedly
**Solution**: This shouldn't happen - `.click()` prevents submission. If using `.click_allow_default()`, add explicit `e.preventDefault()`
**Problem**: Want to use `.on('click')` to avoid preventDefault
**Solution**: Don't - it defeats the framework's safety. Use `.click_allow_default()` to make intent explicit
## More Information
Details: `php artisan rsx:man jquery`

View File

@@ -0,0 +1,243 @@
---
name: js-decorators
description: RSX JavaScript decorators including @route, @spa, @layout, @mutex, and custom decorator creation. Use when adding route decorators to actions, understanding decorator restrictions, creating custom decorators, or troubleshooting decorator errors.
---
# RSX JavaScript Decorators
## Overview
RSX uses decorators to enhance static methods. Unlike standard JavaScript, RSX requires explicit whitelisting via `@decorator` marker to prevent arbitrary code injection.
**Key difference**: Only functions marked with `@decorator` can be used as decorators elsewhere.
---
## Common Framework Decorators
### @route - SPA Routing
Defines URL paths for SPA actions:
```javascript
@route('/contacts')
class Contacts_Index_Action extends Spa_Action { }
@route('/contacts/:id')
class Contacts_View_Action extends Spa_Action { }
// Dual routes (add/edit in one action)
@route('/contacts/add')
@route('/contacts/:id/edit')
class Contacts_Edit_Action extends Spa_Action { }
```
### @spa - SPA Bootstrap
Links action to its SPA bootstrap controller:
```javascript
@route('/contacts')
@spa('Frontend_Spa_Controller::index')
class Contacts_Index_Action extends Spa_Action { }
```
### @layout - Layout Assignment
Assigns layout(s) to action:
```javascript
@route('/contacts')
@layout('Frontend_Layout')
@spa('Frontend_Spa_Controller::index')
class Contacts_Index_Action extends Spa_Action { }
// Nested layouts (sublayouts)
@route('/settings/general')
@layout('Frontend_Layout')
@layout('Settings_Layout')
@spa('Frontend_Spa_Controller::index')
class Settings_General_Action extends Spa_Action { }
```
### @mutex - Mutual Exclusion
Prevents concurrent execution of async methods:
```javascript
class My_Component extends Component {
@mutex()
async save() {
// Only one save() can run at a time
await Controller.save(this.vals());
}
}
```
---
## Decorator Restrictions
### Static Methods Only
Decorators can only be applied to static methods, not instance methods:
```javascript
class Example {
@myDecorator
static validMethod() { } // ✅ Valid
@myDecorator
invalidMethod() { } // ❌ Invalid - instance method
}
```
### No Class Name Identifiers in Parameters
Decorator parameters **must not** use class name identifiers (bundle ordering issues):
```javascript
// WRONG - class identifier as parameter
@some_decorator(User_Model)
class My_Action extends Spa_Action { }
// CORRECT - use string literal
@some_decorator('User_Model')
class My_Action extends Spa_Action { }
```
**Why**: Bundle compilation doesn't guarantee class definition order for decorator parameters.
### Multiple Decorators
Execute in reverse order (bottom to top):
```javascript
@logExecutionTime // Executes second (outer)
@validateParams // Executes first (inner)
static transform(data) { }
```
---
## Creating Custom Decorators
### Basic Decorator
```javascript
@decorator
function myCustomDecorator(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${key}`);
return original.apply(this, args);
};
return descriptor;
}
```
### Using Custom Decorator
```javascript
class MyClass {
@myCustomDecorator
static myMethod() {
return "result";
}
}
```
---
## Common Decorator Patterns
### Logging
```javascript
@decorator
function logCalls(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling ${target.name}.${key}`, args);
const result = original.apply(this, args);
console.log(`${target.name}.${key} returned`, result);
return result;
};
return descriptor;
}
```
### Async Error Handling
```javascript
@decorator
function catchAsync(target, key, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
try {
return await original.apply(this, args);
} catch (error) {
console.error(`Error in ${key}:`, error);
throw error;
}
};
return descriptor;
}
```
### Rate Limiting
```javascript
@decorator
function rateLimit(target, key, descriptor) {
const calls = new Map();
const original = descriptor.value;
descriptor.value = function(...args) {
const now = Date.now();
const lastCall = calls.get(key) || 0;
if (now - lastCall < 1000) {
throw new Error(`${key} called too frequently`);
}
calls.set(key, now);
return original.apply(this, args);
};
return descriptor;
}
```
---
## Decorator Function Signature
```javascript
function myDecorator(target, key, descriptor) {
// target: The class being decorated
// key: The method name being decorated
// descriptor: Property descriptor for the method
const original = descriptor.value;
descriptor.value = function(...args) {
// Pre-execution logic
const result = original.apply(this, args);
// Post-execution logic
return result;
};
return descriptor;
}
```
**Important**: Always use `original.apply(this, args)` to preserve context.
---
## Troubleshooting
| Error | Solution |
|-------|----------|
| "Decorator 'X' not whitelisted" | Add `@decorator` marker to function |
| "Decorator 'X' not found" | Ensure decorator loaded before usage |
| "Decorators can only be applied to static methods" | Change to static or remove decorator |
## More Information
Details: `php artisan rsx:man js_decorators`

198
docs/skills/migrations/SKILL.md Executable file
View File

@@ -0,0 +1,198 @@
---
name: migrations
description: RSX database migrations with raw SQL enforcement, forward-only philosophy, and automatic normalization. Use when creating database tables, understanding migration workflow, or troubleshooting Schema builder violations.
---
# RSX Database Migrations
## Philosophy
RSX enforces a forward-only migration strategy with raw SQL:
1. **Forward-only** - No rollbacks, no `down()` methods
2. **Raw SQL only** - Direct MySQL statements, no Schema builder
3. **Fail loud** - Migrations must succeed or fail with clear errors
4. **Snapshot safety** - Development requires database snapshots
---
## Schema Builder is Prohibited
All migrations **must** use `DB::statement()` with raw SQL:
```php
// ✅ CORRECT
DB::statement("CREATE TABLE products (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00
)");
// ❌ WRONG - Schema builder prohibited
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
});
```
**Prohibited**: `Schema::create()`, `Schema::table()`, `Schema::drop()`, `Blueprint`, `$table->` chains
---
## Development Workflow
```bash
# 1. Create snapshot (required)
php artisan migrate:begin
# 2. Create migration
php artisan make:migration:safe create_products_table
# 3. Write migration with raw SQL
# 4. Run migrations
php artisan migrate
# 5. If successful
php artisan migrate:commit
# 6. If failed - auto-rollback to snapshot
```
---
## Automatic Normalization
The system auto-normalizes types after migration. You can write simpler SQL:
| You Write | System Converts To |
|-----------|-------------------|
| `INT` | `BIGINT` |
| `TEXT` | `LONGTEXT` |
| `FLOAT` | `DOUBLE` |
| `TINYINT(1)` | Preserved (boolean) |
**Auto-added columns** (don't include manually):
- `created_at TIMESTAMP(3)`
- `updated_at TIMESTAMP(3)`
- `created_by BIGINT`
- `updated_by BIGINT`
---
## Migration Examples
### Simple Table (Recommended)
```php
public function up()
{
DB::statement("
CREATE TABLE products (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10,2) NOT NULL DEFAULT 0.00,
stock_quantity INT NOT NULL DEFAULT 0,
is_active TINYINT(1) NOT NULL DEFAULT 1,
category_id INT NULL,
INDEX idx_category (category_id),
INDEX idx_active (is_active)
)
");
}
```
**Notes**:
- `INT` becomes `BIGINT` automatically
- `TEXT` becomes `LONGTEXT` automatically
- `created_at`/`updated_at` added automatically
- `TINYINT(1)` preserved for booleans
### Adding Columns
```php
public function up()
{
DB::statement("
ALTER TABLE products
ADD COLUMN sku VARCHAR(50) NULL AFTER name,
ADD COLUMN weight DECIMAL(8,2) NULL,
ADD INDEX idx_sku (sku)
");
}
```
### Foreign Keys
```php
public function up()
{
DB::statement("
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE CASCADE
");
}
```
---
## Required Table Structure
**Every table MUST have**:
```sql
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
```
This is non-negotiable. Use SIGNED (not UNSIGNED) for easier future migrations.
---
## Foreign Key Columns
Foreign key columns **must match** the referenced column type exactly:
```sql
-- If users.id is BIGINT, then:
user_id BIGINT NULL -- ✅ Matches
-- Column names ending in _id are assumed to be foreign keys
```
---
## Production Workflow
```bash
# No snapshot protection in production
php artisan migrate --production
```
Ensure migrations are thoroughly tested in development/staging first.
---
## Validation
The migration validator automatically checks for:
- Schema builder usage
- `down()` methods (auto-removed)
- Proper SQL syntax
Violations show clear error messages with remediation advice.
---
## Troubleshooting
| Error | Solution |
|-------|----------|
| "Found forbidden Schema builder usage" | Replace with `DB::statement()` |
| "Validation failed" | Check migration for prohibited patterns |
| Foreign key constraint fails | Ensure column types match exactly |
## More Information
Details: `php artisan rsx:man migrations`

254
docs/skills/modals/SKILL.md Executable file
View File

@@ -0,0 +1,254 @@
---
name: modals
description: Creating modal dialogs in RSX including alerts, confirms, prompts, selects, and form modals. Use when implementing Modal.alert, Modal.confirm, Modal.prompt, Modal.form, creating modal classes extending Modal_Abstract, or building dialog-based user interactions.
---
# RSX Modal System
## Built-in Dialog Types
All modal methods are async and return appropriate values:
| Method | Returns | Description |
|--------|---------|-------------|
| `Modal.alert(body)` | `void` | Simple notification |
| `Modal.alert(title, body, buttonLabel?)` | `void` | Alert with title |
| `Modal.confirm(body)` | `boolean` | Yes/no confirmation |
| `Modal.confirm(title, body, confirmLabel?, cancelLabel?)` | `boolean` | Confirmation with labels |
| `Modal.prompt(body)` | `string\|false` | Text input |
| `Modal.prompt(title, body, default?, multiline?)` | `string\|false` | Prompt with options |
| `Modal.select(body, options)` | `string\|false` | Dropdown selection |
| `Modal.select(title, body, options, default?, placeholder?)` | `string\|false` | Select with options |
| `Modal.error(error, title?)` | `void` | Error with red styling |
| `Modal.unclosable(title, body)` | `void` | Modal user cannot close |
## Basic Usage Examples
```javascript
// Simple alert
await Modal.alert("File saved successfully");
// Alert with title
await Modal.alert("Success", "Your changes have been saved.");
// Confirmation
if (await Modal.confirm("Are you sure you want to delete this item?")) {
await Controller.delete(id);
}
// Confirmation with custom labels
const confirmed = await Modal.confirm(
"Delete Project",
"This will permanently delete the project.\n\nThis action cannot be undone.",
"Delete", // confirm button label
"Keep Project" // cancel button label
);
// Text prompt
const name = await Modal.prompt("Enter your name:");
if (name) {
// User entered something
}
// Multiline prompt
const notes = await Modal.prompt("Notes", "Enter description:", "", true);
// Selection dropdown
const choice = await Modal.select("Choose an option:", [
{value: 'a', label: 'Option A'},
{value: 'b', label: 'Option B'}
]);
// Unclosable modal (for critical operations)
Modal.unclosable("Processing", "Please wait...");
await long_operation();
await Modal.close(); // Must close programmatically
```
**Text formatting**: Use `\n\n` for paragraph breaks in modal body text.
---
## Form Modals
For complex data entry, use `Modal.form()`:
```javascript
const result = await Modal.form({
title: "Edit User",
component: "User_Form", // Component name (must implement vals())
component_args: {data: user}, // Args passed to component
max_width: 800, // Width in pixels (default: 800)
on_submit: async (form) => {
const response = await User_Controller.save(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false; // Keep modal open
}
return response.data; // Close modal and return data
}
});
if (result) {
// Modal closed with data
console.log(result.id);
}
```
### Form Component Requirements
The component used in `Modal.form()` must:
1. Implement `vals()` method (get/set form values)
2. Include `<div $sid="error_container"></div>` for validation errors
```javascript
class User_Form extends Component {
vals(values) {
if (values) {
this.$sid('name').val(values.name || '');
this.$sid('email').val(values.email || '');
return null;
} else {
return {
name: this.$sid('name').val(),
email: this.$sid('email').val()
};
}
}
}
```
---
## Modal Classes (Reusable Modals)
For complex or frequently-used modals, create dedicated classes:
```javascript
class Add_User_Modal extends Modal_Abstract {
static async show(initial_data = {}) {
return await Modal.form({
title: 'Add User',
component: 'User_Form',
component_args: {data: initial_data},
on_submit: async (form) => {
const response = await User_Controller.create(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false;
}
return response.data;
}
}) || false;
}
}
class Edit_User_Modal extends Modal_Abstract {
static async show(user_id) {
// Load data first
const user = await User_Model.fetch(user_id);
return await Modal.form({
title: 'Edit User',
component: 'User_Form',
component_args: {data: user},
on_submit: async (form) => {
const response = await User_Controller.update(form.vals());
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false;
}
return response.data;
}
}) || false;
}
}
```
### Usage Pattern
```javascript
// Create new user
const new_user = await Add_User_Modal.show();
if (new_user) {
grid.reload();
}
// Edit existing user
const updated_user = await Edit_User_Modal.show(user_id);
if (updated_user) {
// Refresh display
}
// Chain modals (page JS orchestrates, not modals)
const user = await Add_User_Modal.show();
if (user) {
await Assign_Role_Modal.show(user.id);
}
```
**Pattern**: Extend `Modal_Abstract`, implement static `show()`, return data or `false`.
---
## Modal Options
Options for `Modal.form()`:
```javascript
await Modal.form({
title: "Form Title",
component: "Form_Component",
component_args: {},
max_width: 800, // Width in pixels (default: 800)
closable: true, // Allow ESC/backdrop/X to close (default: true)
submit_label: "Save", // Submit button text
cancel_label: "Cancel", // Cancel button text
on_submit: async (form) => { /* ... */ }
});
```
Options for `Modal.show()` (custom modals):
```javascript
await Modal.show({
title: "Choose Action",
body: "What would you like to do?", // String, HTML, or jQuery element
max_width: 500, // Width in pixels
closable: true,
buttons: [
{label: "Cancel", value: false, class: "btn-secondary"},
{label: "Continue", value: true, class: "btn-primary", default: true}
]
});
```
---
## Modal Queuing
Multiple simultaneous modal requests are queued and shown sequentially:
```javascript
// All three modals queued and shown one after another
const p1 = Modal.alert("First");
const p2 = Modal.alert("Second");
const p3 = Modal.alert("Third");
await Promise.all([p1, p2, p3]);
```
Backdrop persists across queued modals with 500ms delay between.
---
## Best Practices
1. **Use appropriate type**: `alert()` for info, `confirm()` for decisions, `form()` for complex input
2. **Handle cancellations**: Always check for `false` return value
3. **Modal classes don't chain**: Page JS orchestrates sequences, not modal classes
4. **No UI updates in modals**: Page JS handles post-modal UI updates
5. **Loading states**: Use `Modal.unclosable()` + `Modal.close()` for long operations
## More Information
Details: `php artisan rsx:man modals`

246
docs/skills/model-enums/SKILL.md Executable file
View File

@@ -0,0 +1,246 @@
---
name: model-enums
description: Implementing model enums in RSX with integer-backed values, constants, labels, and custom properties. Use when adding enum fields to models, working with status_id or type_id columns, accessing enum labels/properties via BEM-style syntax, or populating dropdowns with enum values.
---
# RSX Model Enums
Integer-backed enums with model-level mapping to constants, labels, and custom properties. Uses BEM-style double underscore naming for magic properties.
## Defining Enums
```php
class Project_Model extends Rsx_Model_Abstract {
public static $enums = [
'status_id' => [
1 => ['constant' => 'STATUS_ACTIVE', 'label' => 'Active', 'badge' => 'bg-success', 'order' => 1],
2 => ['constant' => 'STATUS_ON_HOLD', 'label' => 'On Hold', 'badge' => 'bg-warning', 'order' => 2],
3 => ['constant' => 'STATUS_ARCHIVED', 'label' => 'Archived', 'selectable' => false, 'order' => 99],
],
'priority_id' => [
1 => ['constant' => 'PRIORITY_LOW', 'label' => 'Low', 'color' => '#999', 'days' => 30],
2 => ['constant' => 'PRIORITY_MEDIUM', 'label' => 'Medium', 'color' => '#f90', 'days' => 14],
3 => ['constant' => 'PRIORITY_HIGH', 'label' => 'High', 'color' => '#f00', 'days' => 7],
],
];
}
```
## Required Properties
| Property | Purpose |
|----------|---------|
| `constant` | Static constant name (generates `Model::STATUS_ACTIVE`) |
| `label` | Human-readable display text |
## Special Properties
| Property | Default | Purpose |
|----------|---------|---------|
| `order` | 0 | Sort position in dropdowns (lower first) |
| `selectable` | true | Include in dropdown options |
Non-selectable items are excluded from `field__enum_select()` but still display correctly when they're the current value.
## Custom Properties
Add any properties for business logic - they become accessible via BEM-style syntax:
```php
'status_id' => [
1 => [
'constant' => 'STATUS_ACTIVE',
'label' => 'Active',
'badge' => 'bg-success', // CSS class
'icon' => 'fa-check', // Icon class
'can_edit' => true, // Business rule
'permissions' => ['edit', 'view'], // Permission list
],
],
```
---
## PHP Usage
```php
// Set using constant
$project->status_id = Project_Model::STATUS_ACTIVE;
// BEM-style property access (field__property)
echo $project->status_id__label; // "Active"
echo $project->status_id__badge; // "bg-success"
echo $project->status_id__icon; // "fa-check"
// Business logic flags
if ($project->status_id__can_edit) {
// Allow editing
}
```
---
## JavaScript Usage
### Static Constants
```javascript
project.status_id = Project_Model.STATUS_ACTIVE;
```
### Instance Property Access
```javascript
const project = await Project_Model.fetch(1);
console.log(project.status_id__label); // "Active"
console.log(project.status_id__badge); // "bg-success"
```
### Static Enum Methods
All use BEM-style double underscore (`field__method()`):
```javascript
// Get all enum data
Project_Model.status_id__enum()
// Returns: {1: {label: 'Active', badge: 'bg-success', ...}, 2: {...}, ...}
// Get specific enum's metadata by ID
Project_Model.status_id__enum(Project_Model.STATUS_ACTIVE)
// Returns: {label: 'Active', badge: 'bg-success', order: 1, ...}
Project_Model.status_id__enum(2).selectable // true or false
// For dropdown population (respects 'selectable' and 'order')
Project_Model.status_id__enum_select()
// Returns: [{value: 1, label: 'Active'}, {value: 2, label: 'On Hold'}]
// Note: Archived excluded because selectable: false
// Simple id => label map
Project_Model.status_id__enum_labels()
// Returns: {1: 'Active', 2: 'On Hold', 3: 'Archived'}
// Array of valid IDs
Project_Model.status_id__enum_ids()
// Returns: [1, 2, 3]
```
---
## Template Usage
```jqhtml
<span class="badge <%= this.args.project.status_id__badge %>">
<%= this.args.project.status_id__label %>
</span>
<Select_Input
$name="status_id"
$options="<%= JSON.stringify(Project_Model.status_id__enum_select()) %>"
/>
```
---
## Boolean Fields
For boolean fields, use 0/1 as keys:
```php
'is_verified' => [
0 => ['constant' => 'NOT_VERIFIED', 'label' => 'Not Verified', 'icon' => 'fa-times'],
1 => ['constant' => 'VERIFIED', 'label' => 'Verified', 'icon' => 'fa-check'],
]
```
---
## Context-Specific Labels
Define different labels for different contexts:
```php
1 => [
'constant' => 'STATUS_NEW',
'label' => 'New Listing', // Backend/default
'label_frontend' => 'Coming Soon', // Public-facing
'label_short' => 'New', // Abbreviated
]
```
Access: `$item->status__label_frontend`
---
## Permission Systems
Use custom properties for complex permission logic:
```php
'role_id' => [
1 => [
'constant' => 'ROLE_ADMIN',
'label' => 'Administrator',
'permissions' => ['users.create', 'users.delete', 'settings.edit'],
'can_admin_roles' => [2, 3, 4], // Can manage these role IDs
],
2 => [
'constant' => 'ROLE_MANAGER',
'label' => 'Manager',
'permissions' => ['users.view', 'reports.view'],
'can_admin_roles' => [3, 4],
],
]
```
```php
// Check permissions
if (in_array('users.create', $user->role_id__permissions)) {
// User can create users
}
```
---
## Anti-Aliasing Policy
**NEVER alias enum properties in fetch()** - the BEM-style naming exists for grepability:
```php
// WRONG - Aliasing obscures data source
$data['type_label'] = $record->type_id__label; // BAD
// RIGHT - Use full BEM-style names in JavaScript
contact.type_id__label // Good - grepable, self-documenting
```
The `fetch()` function's purpose is SECURITY (removing private data), not aliasing.
---
## Database Migration
**Always use BIGINT for enum columns:**
```php
$table->bigInteger('status_id')->default(Project_Model::STATUS_ACTIVE);
```
**Never use:**
- VARCHAR (wastes space)
- MySQL ENUM type (inflexible)
- TINYINT (too small for future expansion)
---
## After Adding Enums
Run model documentation generator:
```bash
php artisan rsx:migrate:document_models
```
This updates the model's documentation comments with enum information.
## More Information
Details: `php artisan rsx:man enum`

287
docs/skills/model-fetch/SKILL.md Executable file
View File

@@ -0,0 +1,287 @@
---
name: model-fetch
description: Loading model data from JavaScript using Model.fetch() with secure opt-in, authorization, and lazy relationships. Use when implementing fetch() methods on models, loading records from JavaScript, accessing relationships via await, or understanding the Ajax_Endpoint_Model_Fetch attribute.
---
# RSX Model Fetch System
## Overview
RSX allows JavaScript to securely access ORM models through explicit opt-in. Unlike Laravel's API routes which can expose all fields, RSX requires each model to implement its own `fetch()` method with authorization and data filtering.
---
## Security Model
- **Explicit Opt-In**: Models must implement `fetch()` with `#[Ajax_Endpoint_Model_Fetch]`
- **No Default Access**: No models are fetchable by default
- **Individual Authorization**: Each model controls who can fetch its records
- **Data Filtering**: Complete control over what data JavaScript receives
---
## Implementing fetch()
### Basic Implementation
```php
use Ajax_Endpoint_Model_Fetch;
class Product_Model extends Rsx_Model_Abstract
{
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
// Authorization check
if (!Session::is_logged_in()) {
return false;
}
// Fetch single record
$model = static::find($id);
return $model ?: false;
}
}
```
### With Data Filtering
```php
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
if (!Session::is_logged_in()) {
return false;
}
$user = static::find($id);
if (!$user) {
return false;
}
// Remove sensitive fields
unset($user->password_hash);
unset($user->remember_token);
return $user;
}
```
### With Authorization Check
```php
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
$current_user = Session::get_user();
if (!$current_user) {
return false;
}
$order = static::find($id);
if (!$order) {
return false;
}
// Only allow access to own orders or admin users
if ($order->user_id !== $current_user->id && !$current_user->is_admin) {
return false;
}
return $order;
}
```
### Augmented Array Return (Computed Fields)
```php
#[Ajax_Endpoint_Model_Fetch]
public static function fetch($id)
{
if (!Session::is_logged_in()) {
return false;
}
$contact = static::find($id);
if (!$contact) {
return false;
}
// Start with toArray() to preserve __MODEL for hydration
$data = $contact->toArray();
// Add computed fields
$data['full_name'] = $contact->full_name();
$data['avatar_url'] = $contact->get_avatar_url();
return $data;
}
```
**Important**: Always use `toArray()` as the base - it preserves `__MODEL` for JavaScript hydration.
---
## JavaScript Usage
### Fetch Single Record
```javascript
// Throws if not found
const project = await Project_Model.fetch(123);
console.log(project.name);
// Returns null if not found
const maybe = await Project_Model.fetch_or_null(999);
if (maybe) {
console.log(maybe.name);
}
```
### Enum Properties (BEM-Style)
```javascript
const project = await Project_Model.fetch(1);
// Instance properties (from fetched data)
console.log(project.status_id__label); // "Active"
console.log(project.status_id__badge); // "bg-success"
// Static constants
if (project.status_id === Project_Model.STATUS_ACTIVE) {
// ...
}
```
---
## Lazy Relationships
Relationships can be loaded on-demand from JavaScript. The related model must also implement `fetch()` with `#[Ajax_Endpoint_Model_Fetch]`.
### belongsTo Relationship
```php
// In Project_Model
#[Ajax_Endpoint_Model_Fetch]
public function client()
{
return $this->belongsTo(Client_Model::class);
}
```
```javascript
const project = await Project_Model.fetch(123);
const client = await project.client(); // Returns Client_Model or null
console.log(client.name);
```
### hasMany Relationship
```php
// In Project_Model
#[Ajax_Endpoint_Model_Fetch]
public function tasks()
{
return $this->hasMany(Task_Model::class);
}
```
```javascript
const project = await Project_Model.fetch(123);
const tasks = await project.tasks(); // Returns Task_Model[]
for (const task of tasks) {
console.log(task.title);
}
```
### morphTo Relationship
```php
// In Activity_Model
#[Ajax_Endpoint_Model_Fetch]
public function subject()
{
return $this->morphTo();
}
```
```javascript
const activity = await Activity_Model.fetch(1);
const subject = await activity.subject(); // Returns polymorphic model
```
---
## Return Value Rules
| Return | Meaning |
|--------|---------|
| Model object | Serialized via `toArray()`, includes `__MODEL` for hydration |
| Array (from `toArray()`) | Preserves `__MODEL`, can add computed fields |
| `false` | Record not found or unauthorized |
**MUST return `false`** (not `null`) when record is not found or unauthorized.
---
## Anti-Aliasing Policy
**NEVER alias enum properties in fetch()** - BEM-style naming exists for grepability:
```php
// WRONG - Aliasing obscures data source
$data['type_label'] = $record->type_id__label;
// RIGHT - Use full BEM-style names in JavaScript
contact.type_id__label // Grepable, self-documenting
```
The `fetch()` method's purpose is **security** (removing private data), not aliasing.
---
## Common Patterns
### In SPA Action on_load()
```javascript
async on_load() {
const project = await Project_Model.fetch(this.args.id);
this.data.project = project;
}
```
### Loading with Relationships
```javascript
async on_load() {
const project = await Project_Model.fetch(this.args.id);
const [client, tasks] = await Promise.all([
project.client(),
project.tasks()
]);
this.data.project = project;
this.data.client = client;
this.data.tasks = tasks;
}
```
### Conditional Relationship Loading
```javascript
async on_load() {
const order = await Order_Model.fetch(this.args.id);
this.data.order = order;
// Only load customer if needed
if (this.args.show_customer) {
this.data.customer = await order.customer();
}
}
```
## More Information
Details: `php artisan rsx:man model_fetch`

236
docs/skills/polymorphic/SKILL.md Executable file
View File

@@ -0,0 +1,236 @@
---
name: polymorphic
description: RSX polymorphic relationships with type references storing integers instead of class names. Use when implementing morphTo relationships, defining type_ref_columns, handling polymorphic form fields, or using polymorphic join helpers.
---
# RSX Polymorphic Relationships
## Overview
RSX uses a type reference system that stores **integers** in the database but transparently converts to/from class name strings in PHP.
```php
$activity->eventable_type = 'Contact_Model'; // Stores integer in DB
echo $activity->eventable_type; // Returns "Contact_Model"
```
**Benefits**:
- Efficient integer storage (not VARCHAR class names)
- Automatic type discovery
- Transparent conversion
- Laravel morphTo() compatibility
---
## Defining Type Reference Columns
Declare which columns are type references in your model:
```php
class Activity_Model extends Rsx_Model_Abstract
{
protected static $type_ref_columns = ['eventable_type'];
public function eventable()
{
return $this->morphTo();
}
}
```
The cast is automatically applied - no manual `$casts` needed.
---
## Database Schema
Type reference columns must be **BIGINT**, not VARCHAR:
```sql
CREATE TABLE activities (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
eventable_type BIGINT NULL,
eventable_id BIGINT NULL,
action VARCHAR(50) NOT NULL,
INDEX idx_eventable (eventable_type, eventable_id)
);
```
---
## Usage
### Setting Values
```php
$activity = new Activity_Model();
$activity->eventable_type = 'Contact_Model'; // Use class name
$activity->eventable_id = 123;
$activity->save();
```
### Reading Values
```php
echo $activity->eventable_type; // "Contact_Model" (string)
$related = $activity->eventable; // Returns Contact_Model instance
```
### Querying
Class names are automatically converted to IDs in WHERE clauses:
```php
// All work - class names auto-converted
Activity_Model::where('eventable_type', 'Contact_Model')->get();
Activity_Model::whereIn('eventable_type', ['Contact_Model', 'Project_Model'])->get();
```
---
## Polymorphic Join Helpers
Join tables with polymorphic columns:
```php
// INNER JOIN - contacts that have attachments
Contact_Model::query()
->joinMorph('file_attachments', 'fileable')
->select('contacts.*', 'file_attachments.filename')
->get();
// LEFT JOIN - all contacts, with attachments if they exist
Contact_Model::query()
->leftJoinMorph('file_attachments', 'fileable')
->get();
// RIGHT JOIN
Contact_Model::query()
->rightJoinMorph('file_attachments', 'fileable')
->get();
```
**Parameters**:
- `$table` - Table with polymorphic columns (e.g., 'file_attachments')
- `$morphName` - Column prefix (e.g., 'fileable' for fileable_type/fileable_id)
- `$morphClass` - Optional explicit class (defaults to current model)
---
## Form Handling
### Client-Side Format
Polymorphic fields submit as JSON:
```javascript
eventable={"model":"Contact_Model","id":123}
```
### Server-Side Parsing
```php
use App\RSpade\Core\Polymorphic_Field_Helper;
#[Ajax_Endpoint]
public static function save(Request $request, array $params = [])
{
$eventable = Polymorphic_Field_Helper::parse($params['eventable'], [
Contact_Model::class,
Project_Model::class,
]);
// Validate
if ($error = $eventable->validate('Please select an entity')) {
return response_error(Ajax::ERROR_VALIDATION, ['eventable' => $error]);
}
// Use
$activity = new Activity_Model();
$activity->eventable_type = $eventable->model; // "Contact_Model"
$activity->eventable_id = $eventable->id; // 123
$activity->save();
}
```
**Important**: Always use `Model::class` for the whitelist.
---
## Auto-Discovery
When storing a new class name that isn't in `_type_refs` yet:
```php
$attachment->fileable_type = 'Custom_Model';
$attachment->save();
```
RSX will:
1. Verify `Custom_Model` exists and extends `Rsx_Model_Abstract`
2. Create a new `_type_refs` entry with next available ID
3. Store that ID in the column
Any model can be used without pre-registration.
---
## Common Patterns
### File Attachments to Multiple Models
```php
class File_Attachment_Model extends Rsx_Model_Abstract
{
protected static $type_ref_columns = ['fileable_type'];
public function fileable()
{
return $this->morphTo();
}
}
// Attach to contact
$attachment->fileable_type = 'Contact_Model';
$attachment->fileable_id = $contact->id;
// Attach to project
$attachment->fileable_type = 'Project_Model';
$attachment->fileable_id = $project->id;
```
### Activity Log
```php
class Activity_Model extends Rsx_Model_Abstract
{
protected static $type_ref_columns = ['subject_type'];
public function subject()
{
return $this->morphTo();
}
}
// Log activity for any model
Activity_Model::log('updated', $contact); // subject_type = 'Contact_Model'
Activity_Model::log('created', $project); // subject_type = 'Project_Model'
```
---
## Simple Names Only
Always use simple class names (basename), never FQCNs:
```php
// ✅ Correct
$activity->eventable_type = 'Contact_Model';
// ❌ Wrong - fully qualified
$activity->eventable_type = 'App\\Models\\Contact_Model';
```
## More Information
Details: `php artisan rsx:man polymorphic`

251
docs/skills/scss/SKILL.md Executable file
View File

@@ -0,0 +1,251 @@
---
name: scss
description: SCSS styling architecture in RSX including component scoping, BEM naming, responsive breakpoints, and variables. Use when writing SCSS files, styling components, working with responsive design, or troubleshooting CSS conflicts.
---
# RSX SCSS Architecture
## Component-First Philosophy
Every styled element is a component with scoped SCSS. No CSS spaghetti - no generic classes like `.page-header` scattered across files.
**Pattern vs Unique Decision**:
- If you're copy-pasting markup, extract a component
- Reusable structures → shared component with slots
- One-off structures → page-specific component
---
## Directory Rules
| Location | Purpose | Scoping |
|----------|---------|---------|
| `rsx/app/` | Feature components | Must wrap in component class |
| `rsx/theme/components/` | Shared components | Must wrap in component class |
| `rsx/theme/` (outside components/) | Primitives, variables, Bootstrap overrides | Global |
| `rsx/lib/` | Non-visual utilities | No styles |
---
## Component Scoping (Required)
SCSS in `rsx/app/` and `rsx/theme/components/` **must** wrap in a single component class:
```scss
// dashboard_index_action.scss
.Dashboard_Index_Action {
padding: 2rem;
.card {
margin-bottom: 1rem;
}
.stats-grid {
display: grid;
gap: 1rem;
}
}
```
- Wrapper class matches JS class or Blade `@rsx_id`
- Filename must match associated `.js` or `.blade.php` file
- Components auto-render with `class="Component_Name"` on root
---
## BEM Child Classes
Child classes use exact PascalCase component name as prefix:
```scss
.DataGrid_Kanban {
&__loading { /* .DataGrid_Kanban__loading */ }
&__board { /* .DataGrid_Kanban__board */ }
&__column { /* .DataGrid_Kanban__column */ }
}
```
```html
<!-- Correct -->
<div class="DataGrid_Kanban__loading">
<!-- WRONG - kebab-case doesn't match compiled CSS -->
<div class="datagrid-kanban__loading"> <!-- No styles! -->
```
**No kebab-case** in component BEM classes.
---
## Variables
Define in `rsx/theme/variables.scss`. **Check this file before writing new SCSS.**
In bundles, variables.scss must be included before directory includes:
```php
'include' => [
'rsx/theme/variables.scss', // First
'rsx/theme', // Then directories
'rsx/app/frontend',
],
```
Variables can be declared outside the wrapper for sharing:
```scss
// frontend_spa_layout.scss
$sidebar-width: 215px;
$header-height: 57px;
.Frontend_Spa_Layout {
.sidebar { width: $sidebar-width; }
}
```
---
## Variables-Only Files
Files with only `$var: value;` declarations (no selectors) are valid without wrapper:
```scss
// _variables.scss
$primary-color: #0d6efd;
$border-radius: 0.375rem;
```
---
## Supplemental SCSS Files
Split large SCSS by breakpoint or feature:
```
frontend_spa_layout.scss # Primary (required)
frontend_spa_layout_mobile.scss # Supplemental
frontend_spa_layout_print.scss # Supplemental
```
Supplemental files use the **same wrapper class** as primary:
```scss
// frontend_spa_layout_mobile.scss
.Frontend_Spa_Layout {
@media (max-width: 768px) {
.sidebar { width: 100%; }
}
}
```
---
## Responsive Breakpoints
RSX replaces Bootstrap breakpoints. **Bootstrap's `.col-md-6`, `.d-lg-none` do NOT work.**
### Tier 1 (Simple)
| Name | Range |
|------|-------|
| `mobile` | 0-1023px |
| `desktop` | 1024px+ |
### Tier 2 (Granular)
| Name | Range |
|------|-------|
| `phone` | 0-799px |
| `tablet` | 800-1023px |
| `desktop-sm` | 1024-1199px |
| `desktop-md` | 1200-1639px |
| `desktop-lg` | 1640-2199px |
| `desktop-xl` | 2200px+ |
### SCSS Mixins
```scss
.Component {
padding: 2rem;
@include mobile {
padding: 1rem;
}
@include phone {
padding: 0.5rem;
}
@include desktop-xl {
max-width: 1800px;
}
}
```
### Utility Classes
```html
<div class="col-mobile-12 col-desktop-6">...</div>
<div class="d-mobile-none">Hidden on mobile</div>
<div class="mobile-only">Only visible on mobile</div>
<div class="hide-tablet">Hidden on tablet only</div>
```
### JavaScript Detection
```javascript
if (Responsive.is_mobile()) {
// Mobile behavior
}
if (Responsive.is_desktop_xl()) {
// Extra large desktop
}
```
---
## Slot-Based Composition
Use slots to separate structure from content:
```jqhtml
<Define:Datagrid_Card>
<div class="card">
<div class="card-header"><%= content('toolbar') %></div>
<div class="card-body"><%= content('body') %></div>
</div>
</Define:Datagrid_Card>
<!-- Usage -->
<Datagrid_Card>
<Slot:toolbar><button>Add</button></Slot:toolbar>
<Slot:body><My_Datagrid /></Slot:body>
</Datagrid_Card>
```
Component owns layout/styling; pages provide content via slots.
---
## What Remains Shared (Unscoped)
Only primitives should be unscoped:
- Buttons (`.btn-primary`, `.btn-secondary`)
- Spacing utilities (`.mb-3`, `.p-2`)
- Typography (`.text-muted`, `.fw-bold`)
- Bootstrap overrides
Everything else → component-scoped SCSS.
---
## No Exemptions
There are **no exemptions** to scoping rules for files in `rsx/app/` or `rsx/theme/components/`. If a file can't be associated with a component, it likely belongs in:
- `rsx/theme/base/` for global utilities
- A dedicated partial imported via `@use`
## More Information
Details: `php artisan rsx:man scss`, `php artisan rsx:man responsive`