Files
rspade_system/app/RSpade/man/modals.txt
root 84ca3dfe42 Fix code quality violations and rename select input components
Move small tasks from wishlist to todo, update npm packages
Replace #[Auth] attributes with manual auth checks and code quality rule
Remove on_jqhtml_ready lifecycle method from framework
Complete ACL system with 100-based role indexing and /dev/acl tester
WIP: ACL system implementation with debug instrumentation
Convert rsx:check JS linting to RPC socket server
Clean up docs and fix $id→$sid in man pages, remove SSR/FPC feature
Reorganize wishlists: priority order, mark sublayouts complete, add email
Update model_fetch docs: mark MVP complete, fix enum docs, reorganize
Comprehensive documentation overhaul: clarity, compression, and critical rules
Convert Contacts/Projects CRUD to Model.fetch() and add fetch_or_null()
Add JS ORM relationship lazy-loading and fetch array handling
Add JS ORM relationship fetching and CRUD documentation
Fix ORM hydration and add IDE resolution for Base_* model stubs
Rename Json_Tree_Component to JS_Tree_Debug_Component and move to framework
Enhance JS ORM infrastructure and add Json_Tree class name badges

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 21:39:43 +00:00

1011 lines
30 KiB
Plaintext
Executable File

================================================================================
MODAL SYSTEM
================================================================================
The Modal system provides a consistent, queue-managed interface for displaying
dialogs throughout the application. All modals are managed by the static Modal
class, which handles queuing, backdrop management, and user interactions.
================================================================================
BASIC DIALOGS
================================================================================
ALERT
-----
Show a simple notification message with an OK button.
await Modal.alert(message)
await Modal.alert(title, message)
await Modal.alert(title, message, button_label)
Examples:
await Modal.alert("File saved successfully");
await Modal.alert("Success", "Your changes have been saved");
await Modal.alert("Notice", "Operation complete", "Got it");
Parameters:
message - Message text (if only 1 arg) or jQuery element
title - Optional title (default: "Notice")
button_label - Optional button text (default: "OK")
Returns: Promise<void>
CONFIRM
-------
Show a confirmation dialog with Cancel and Confirm buttons.
let result = await Modal.confirm(message)
let result = await Modal.confirm(title, message)
let result = await Modal.confirm(title, message, confirm_label, cancel_label)
Examples:
if (await Modal.confirm("Delete this item?")) {
// User confirmed
}
if (await Modal.confirm("Delete Item", "This cannot be undone")) {
// User confirmed
}
Parameters:
message - Message text (if 1-2 args) or jQuery element
title - Optional title (default: "Confirm")
confirm_label - Confirm button text (default: "Confirm")
cancel_label - Cancel button text (default: "Cancel")
Returns: Promise<boolean> - true if confirmed, false if cancelled
PROMPT
------
Show an input dialog for text entry.
let value = await Modal.prompt(message)
let value = await Modal.prompt(title, message)
let value = await Modal.prompt(title, message, default_value)
let value = await Modal.prompt(title, message, default_value, multiline)
let value = await Modal.prompt(title, message, default_value, multiline, error)
Examples:
let name = await Modal.prompt("What is your name?");
if (name) {
console.log("Hello, " + name);
}
let email = await Modal.prompt("Email", "Enter your email:", "user@example.com");
let feedback = await Modal.prompt("Feedback", "Enter your feedback:", "", true);
Rich Content Example:
const $rich = $('<div>')
.append($('<h5 style="color: #2c3e50;">').text('Registration'))
.append($('<p>').html('Enter your <strong>full name</strong>'));
let name = await Modal.prompt($rich);
Validation Pattern (Lazy Re-prompting):
let email = '';
let error = null;
let valid = false;
while (!valid) {
email = await Modal.prompt('Email', 'Enter email:', email, false, error);
if (email === false) return; // Cancelled
// Validate
if (!email.includes('@')) {
error = 'Please enter a valid email address';
} else {
valid = true;
}
}
// email is now valid
Parameters:
message - Prompt message text or jQuery element
title - Optional title (default: "Input")
default_value - Default input value (default: "")
multiline - Show textarea instead of input (default: false)
error - Optional error message to display as validation feedback
Returns: Promise<string|false> - Input value or false if cancelled
Input Constraints:
Standard input: 245px minimum width
Textarea input: 315px minimum width
Spacing: 36px between message and input field
Error Display:
When error parameter is provided:
- Input field marked with .is-invalid class (red border)
- Error message displayed below input as .invalid-feedback
- Input retains previously entered value
- User can correct and resubmit
ERROR
-----
Show an error message dialog.
await Modal.error(error)
await Modal.error(error, title)
Examples:
await Modal.error("File not found");
await Modal.error(exception, "Upload Failed");
await Modal.error({message: "Invalid format"}, "Error");
Parameters:
error - String, error object, or {message: string}
title - Optional title (default: "Error")
Handles various error formats:
- String: "Error message"
- Object: {message: "Error"}
- Laravel response: {responseJSON: {message: "Error"}}
- Field errors: {field: "Error", field2: "Error2"}
Returns: Promise<void>
================================================================================
CUSTOM MODALS
================================================================================
SHOW
----
Display a custom modal with specified content and buttons.
let result = await Modal.show(options)
Options:
title - Modal title (default: "Modal")
body - String, HTML, or jQuery element
buttons - Array of button definitions (see below)
max_width - Maximum width in pixels (default: 800)
closable - Allow ESC/backdrop/X to close (default: true)
Button Definition:
{
label: "Button Text",
value: "return_value",
class: "btn-primary", // Bootstrap button class
default: true, // Make this the default button
callback: async function() {
// Optional: perform action and return result
return custom_value;
}
}
Examples:
// Two button modal
const result = await Modal.show({
title: "Choose Action",
body: "What would you like to do?",
buttons: [
{label: "Cancel", value: false, class: "btn-secondary"},
{label: "Continue", value: true, class: "btn-primary", default: true}
]
});
// Three button modal
const result = await Modal.show({
title: "Save Changes",
body: "How would you like to save?",
buttons: [
{label: "Cancel", value: false, class: "btn-secondary"},
{label: "Save Draft", value: "draft", class: "btn-info"},
{label: "Publish", value: "publish", class: "btn-success", default: true}
]
});
// jQuery content
const $content = $('<div>')
.append($('<p>').text('Custom content'))
.append($('<ul>').append($('<li>').text('Item 1')));
await Modal.show({
title: "Custom Content",
body: $content,
buttons: [{label: "Close", value: true, class: "btn-primary"}]
});
Returns: Promise<any> - Value from clicked button (false if cancelled)
================================================================================
FORM MODALS
================================================================================
FORM
----
Display a form component in a modal with validation support.
let result = await Modal.form(options)
Options:
component - Component class name (string)
component_args - Arguments to pass to component
title - Modal title (default: "Form")
max_width - Maximum width in pixels (default: 800)
closable - Allow ESC/backdrop to close (default: true)
submit_label - Submit button text (default: "Submit")
cancel_label - Cancel button text (default: "Cancel")
on_submit - Callback function (receives form component)
The on_submit callback pattern:
- Receives the form component instance
- Call form.vals() to get current values
- Perform validation/submission
- Return false to keep modal open (for errors)
- Return data to close modal and resolve promise
Simple Example:
const result = await Modal.form({
title: "New User",
component: "User_Form",
on_submit: async (form) => {
const values = form.vals();
if (!values.name) {
await Modal.alert("Name is required");
return false; // Keep modal open
}
await sleep(500); // Simulate save
return values; // Close modal with data
}
});
if (result) {
console.log("Saved:", result);
}
Validation Example:
const result = await Modal.form({
title: "Edit Profile",
component: "Profile_Form",
component_args: {data: user_data},
submit_label: "Update",
on_submit: async (form) => {
const values = form.vals();
// Server-side validation
const response = await User_Controller.update_profile(values);
if (response.errors) {
// Show errors and keep modal open
Form_Utils.apply_form_errors(form.$, response.errors);
return false;
}
// Success - close and return
return response.data;
}
});
Creating Form Components:
Your form component must:
- Extend Jqhtml_Component
- Implement vals() method for getting/setting values
- Use standard form HTML with name attributes
- Include error container: <div $sid="error_container"></div>
Example form component (my_form.jqhtml):
<Define:My_Form tag="div">
<div $sid="error_container"></div>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" $sid="name_input" name="name">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" $sid="email_input" name="email">
</div>
</Define:My_Form>
Example form component class (my_form.js):
class My_Form extends Jqhtml_Component {
on_create() {
this.data.values = this.args.data || {};
}
on_ready() {
if (this.data.values) {
this.vals(this.data.values);
}
}
vals(values) {
if (values) {
// Setter
this.$sid('name_input').val(values.name || '');
this.$sid('email_input').val(values.email || '');
return null;
} else {
// Getter
return {
name: this.$sid('name_input').val(),
email: this.$sid('email_input').val()
};
}
}
}
Validation Error Handling:
Form_Utils.apply_form_errors() automatically handles:
- Field-specific errors (matched by name attribute)
- General error messages
- Multiple error formats (string, array, object)
- Animated error display
- Bootstrap 5 validation classes
Error format examples:
// Field errors
{
name: "Name is required",
email: "Invalid email format"
}
// General errors
"An error occurred"
// Array of errors
["Error 1", "Error 2"]
// Laravel format
{
name: ["Name is required", "Name too short"],
email: ["Invalid format"]
}
Returns: Promise<Object|false> - Form data or false if cancelled
================================================================================
MODAL CLASSES
================================================================================
For complex or reusable modals, create modal classes that extend Modal_Abstract.
Modal classes encapsulate modal lifecycle management and use Modal.form(),
Modal.show(), etc. as building blocks.
PHILOSOPHY
----------
Modal classes are orchestration layers that manage showing a modal, collecting
user input, and returning results. They do NOT contain form validation or
business logic - that belongs in jqhtml components and controller endpoints.
Separation of concerns:
- Modal classes: Handle individual modal lifecycle
- Page JS: Orchestrates sequence, updates UI after modal closes
- Form components: Handle form UI and structure
- Controllers: Handle validation and business logic
MODAL_ABSTRACT BASE CLASS
--------------------------
All modal classes must extend Modal_Abstract and implement static show() method.
File: /rsx/theme/components/modal/modal_abstract.js
Contract:
- Implement: static async show(params)
- Return: Promise that resolves with data (success) or false (cancel)
- Error handling: Show error in modal, keep open, don't resolve
BASIC PATTERN
-------------
Simple form modal with backend validation:
class Add_User_Modal extends Modal_Abstract {
static async show() {
const result = await Modal.form({
title: 'Add User',
component: 'Add_User_Modal_Form',
on_submit: async (form) => {
try {
const values = form.vals();
const result = await Controller.add_user(values);
return result; // Close modal, return data
} catch (error) {
await form.render_error(error);
return false; // Keep modal open
}
},
});
return result || false;
}
}
Modal with data loading before display:
class Edit_User_Modal extends Modal_Abstract {
static async show(user_id) {
// Load data first
let user_data;
try {
user_data = await Controller.get_user_for_edit({user_id});
} catch (error) {
await Modal.error(error, 'Failed to Load User');
return false;
}
// Show modal with loaded data
const result = await Modal.form({
title: 'Edit User',
component: 'Edit_User_Modal_Form',
component_args: {data: user_data},
on_submit: async (form) => {
try {
const values = form.vals();
const result = await Controller.save_user(values);
return result;
} catch (error) {
await form.render_error(error);
return false;
}
},
});
return result || false;
}
}
Custom modal using Modal.show():
class Confirm_Delete_Modal extends Modal_Abstract {
static async show({item_name}) {
return await Modal.confirm(
'Confirm Delete',
`Are you sure you want to delete ${item_name}?`
);
}
}
USAGE PATTERN
-------------
Page JS orchestrates modal flow and handles post-modal actions:
// Page JS file (frontend_settings_user_management.js)
class Frontend_Settings_User_Management {
static async handle_add_user() {
// Show add user modal
const user = await Add_User_Modal.show();
if (user) {
// Refresh the user list
$('.Users_DataGrid').component().reload();
// Show next modal in sequence
await Send_User_Invite_Modal.show(user.id);
}
}
}
Key points:
- Modal classes return data or false
- Page JS checks result and orchestrates next steps
- Page JS updates UI (reload grids, refresh displays)
- Modal classes focus only on their modal's lifecycle
FILE ORGANIZATION
-----------------
Naming Convention:
- Class name must end with _Modal (enforced by convention)
- File name matches class name: add_user_modal.js
- Use descriptive names: Add_User_Modal, Edit_User_Modal
Location Guidelines:
- Feature-specific modals: Place in feature directory
Example: /rsx/app/frontend/settings/user_management/edit_user_modal.js
- Reusable modals: Place in theme components
Example: /rsx/theme/components/modal/confirm_delete_modal.js
WHEN TO USE MODAL CLASSES
--------------------------
Use modal classes when:
- Multi-step forms or complex workflows
- Forms with complex validation or error handling
- Modals called from multiple places in the application
- Modals with backend data loading before display
- Modals that need consistent behavior across the app
WHEN NOT TO USE MODAL CLASSES
------------------------------
Use direct Modal.* calls for simple cases:
// Simple alert
await Modal.alert("File saved successfully");
// Simple confirmation
if (await Modal.confirm("Delete this item?")) {
await Item_Controller.delete(item_id);
}
// Simple prompt
const name = await Modal.prompt("What is your name?");
COMPLETE EXAMPLE
----------------
Full implementation showing modal class, form component, and page integration:
1. Modal Class (add_user_modal.js):
class Add_User_Modal extends Modal_Abstract {
static async show() {
const result = await Modal.form({
title: 'Add User',
component: 'Add_User_Modal_Form',
on_submit: async (form) => {
try {
const values = form.vals();
const result = await Controller.add_user(values);
return result;
} catch (error) {
await form.render_error(error);
return false;
}
},
});
return result || false;
}
}
2. Form Component (add_user_modal_form.jqhtml):
<Define:Add_User_Modal_Form tag="div">
<Form_Field $name="email" $label="Email" $required=true>
<Text_Input $type="email" />
</Form_Field>
<Form_Field $name="first_name" $label="First Name">
<Text_Input />
</Form_Field>
<Form_Field $name="last_name" $label="Last Name">
<Text_Input />
</Form_Field>
</Define:Add_User_Modal_Form>
3. Form Component Class (add_user_modal_form.js):
class Add_User_Modal_Form extends Rsx_Form {
// Extends Rsx_Form for automatic form functionality
// vals() and error handling inherited from parent
}
4. Page JS Integration (page.js):
$('#btn_add_user').on('click', async function() {
const user = await Add_User_Modal.show();
if (user) {
$('.Users_DataGrid').component().reload();
await Modal.alert('User added successfully');
}
});
BEST PRACTICES
--------------
1. Single Responsibility
Each modal class handles exactly one modal type.
2. No Direct Modal Chaining
Modal classes don't call other modal classes directly.
Page JS orchestrates sequences.
Bad:
class Add_User_Modal {
static async show() {
const user = await Modal.form({...});
await Send_Invite_Modal.show(user.id); // DON'T DO THIS
return user;
}
}
Good:
// Page JS
const user = await Add_User_Modal.show();
if (user) {
await Send_Invite_Modal.show(user.id);
}
3. No UI Updates in Modal Classes
Modal classes don't reload grids or update page content.
Page JS handles post-modal UI updates.
4. Consistent Error Handling
Use try/catch in on_submit, render errors with form.render_error(),
return false to keep modal open.
5. Clear Return Values
Return data object on success, false on cancel.
Makes page JS conditional logic clean and obvious.
TROUBLESHOOTING
---------------
Modal Class Not Found
- Verify class extends Modal_Abstract
- Check file is in manifest (must be in /rsx/ directory tree)
- Ensure class name ends with _Modal
- Verify file name matches class name
Modal Doesn't Show Data
- Check component_args passed correctly
- Verify form component implements vals(values) setter
- Ensure data loaded before Modal.form() called
- Check async/await on data loading
Errors Not Displaying
- Ensure form.render_error() called in catch block
- Verify form component extends Rsx_Form
- Check that on_submit returns false after error
- Confirm error object has correct format
Modal Closes Unexpectedly
- Check on_submit isn't throwing unhandled errors
- Verify try/catch wraps submission logic
- Ensure false returned explicitly to keep open
- Check for premature return statements
================================================================================
SPECIAL MODALS
================================================================================
UNCLOSABLE
----------
Display a modal that cannot be closed by user (no ESC, backdrop, or X button).
Must be closed programmatically.
Modal.unclosable(message)
Modal.unclosable(title, message)
Examples:
Modal.unclosable("Processing", "Please wait...");
setTimeout(() => {
Modal.close();
}, 3000);
Parameters:
message - Message text
title - Optional title (default: "Please Wait")
Returns: void (does not wait for close)
Note: Call Modal.close() to dismiss the modal programmatically.
================================================================================
MODAL STATE MANAGEMENT
================================================================================
IS_OPEN
-------
Check if a modal is currently displayed.
if (Modal.is_open()) {
console.log("Modal is open");
}
Returns: boolean
GET_CURRENT
-----------
Get the currently displayed modal instance.
const modal = Modal.get_current();
if (modal) {
console.log("Modal instance exists");
}
Returns: Rsx_Modal instance or null
CLOSE
-----
Programmatically close the current modal.
await Modal.close();
Typically used with unclosable modals or to force-close from external code.
Returns: Promise<void>
APPLY_ERRORS
------------
Apply validation errors to the current modal (if it contains a form).
Modal.apply_errors({
field1: "Error message",
field2: "Another error"
});
This is a convenience method that calls Form_Utils.apply_form_errors() on
the current modal's body element.
Parameters:
errors - Error object (field: message pairs)
Returns: void
================================================================================
MODAL QUEUING
================================================================================
The Modal system automatically queues multiple simultaneous modal requests
and displays them sequentially:
// All three modals are 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]);
Queuing Behavior:
- Single shared backdrop persists across queued modals
- 500ms delay between modals (backdrop stays visible)
- Backdrop fades in at start of queue
- Backdrop fades out when queue is empty
- Each modal appears instantly (no fade animation)
Current Limitations:
- All modals treated equally (no priority levels)
- No concept of "modal sessions" or grouped interactions
- FIFO queue order (first requested, first shown)
Future Considerations:
When implementing real-time notifications or background events, you may
need to distinguish between:
- User-initiated modal sequences (conversational flow)
- Background notifications (should wait for user flow to complete)
Planned features:
- Priority levels for different modal types
- Modal sessions to group related interactions
- External event blocking during active user sessions
================================================================================
MODAL SIZING
================================================================================
Responsive Sizing:
- Desktop: 60% viewport width preferred, max 80%
- Mobile: 90% viewport width
- Minimum width: 400px desktop, 280px mobile
- Minimum height: 260px
Maximum Width:
- Default: 800px
- Configurable via max_width option
- Examples: 500px (forms), 1200px (data tables)
Scrolling:
- Triggers when content exceeds 80% viewport height
- Modal body becomes scrollable
- Header and footer remain fixed
Manual Control:
Modal.show({
max_width: 1200, // Wide modal for tables
body: content
});
================================================================================
STYLING AND UX
================================================================================
Modal Appearance:
- Centered vertically and horizontally
- Gray header background (#f8f9fa)
- Smaller title font (1rem)
- Shorter header padding (0.75rem)
- Subtle drop shadow (0 4px 12px rgba(0,0,0,0.15))
- Buttons centered horizontally as a group
- Modal body text centered (for simple dialogs)
Animations:
- Modal appears instantly (no fade)
- Backdrop fades in/out over 250ms
- Validation errors fade in over 300ms
Body Scroll Lock:
- Page scrolling disabled when modal open
- Scrollbar width calculated and compensated via padding
- Prevents layout shift when scrollbar disappears
- Original body state restored when modal closes
- Managed at backdrop level (first modal locks, last unlocks)
Accessibility:
- ESC key closes modal (if closable)
- Backdrop click closes modal (if closable)
- Focus management (input fields auto-focus)
- Keyboard navigation support
================================================================================
BEST PRACTICES
================================================================================
1. Use Appropriate Dialog Type
- alert() - Notifications, information
- confirm() - Yes/no decisions
- prompt() - Simple text input
- form() - Complex forms with validation
- show() - Custom requirements
2. Handle Cancellations
Always check for false (cancelled) return values:
const result = await Modal.confirm("Delete?");
if (result === false) {
return; // User cancelled
}
3. Validation Feedback
Keep modal open for validation errors:
if (errors) {
Form_Utils.apply_form_errors(form.$, errors);
return false; // Keep open
}
4. Avoid Nested Modals
While technically possible, nested modals create poor UX.
Close the first modal before showing a second:
await Modal.alert("Step 1");
await Modal.alert("Step 2"); // Shows after first closes
5. Loading States
For long operations, use unclosable modals:
Modal.unclosable("Saving", "Please wait...");
await save_operation();
await Modal.close();
6. Rich Content
Use jQuery elements for formatted content:
const $content = $('<div>')
.append($('<h5>').text('Title'))
.append($('<p>').html('<strong>Bold</strong> text'));
await Modal.alert($content);
7. Form Component Design
- Keep vals() method simple and synchronous
- Put async logic in on_submit callback
- Use standard HTML form structure
- Include error_container div for validation
- Match field name attributes to error keys
================================================================================
COMMON PATTERNS
================================================================================
Delete Confirmation:
const confirmed = await Modal.confirm(
"Delete Item",
"This action cannot be undone. Are you sure?",
"Delete Forever",
"Cancel"
);
if (confirmed) {
await Item_Controller.delete(item_id);
await Modal.alert("Item deleted successfully");
}
Save with Validation:
const result = await Modal.form({
title: "Edit Profile",
component: "Profile_Form",
component_args: {data: user},
on_submit: async (form) => {
const values = form.vals();
const response = await User_Controller.save(values);
if (response.errors) {
Form_Utils.apply_form_errors(form.$, response.errors);
return false;
}
return response.data;
}
});
if (result) {
await Modal.alert("Profile updated successfully");
}
Multi-Step Process:
const name = await Modal.prompt("What is your name?");
if (!name) return;
const email = await Modal.prompt("Enter your email:");
if (!email) return;
const confirmed = await Modal.confirm(
"Confirm Registration",
`Register ${name} with ${email}?`
);
if (confirmed) {
await register({name, email});
}
Progressive Disclosure:
const result = await Modal.show({
title: "Choose Action",
body: "What would you like to do?",
buttons: [
{label: "View Details", value: "view"},
{label: "Edit", value: "edit"},
{label: "Delete", value: "delete", class: "btn-danger"}
]
});
if (result === "view") {
// Show details modal
} else if (result === "edit") {
// Show edit form
} else if (result === "delete") {
// Confirm and delete
}
================================================================================
TROUBLESHOOTING
================================================================================
Modal Won't Close
- Check if callback returns false (intentionally keeping open)
- Verify closable: true option is set
- Check for JavaScript errors in callback
- Use Modal.close() to force close
Validation Errors Not Showing
- Ensure form has <div $sid="error_container"></div>
- Verify field name attributes match error keys
- Check that fields are wrapped in .form-group containers
- Use Form_Utils.apply_form_errors(form.$, errors)
Form Values Not Saving
- Verify vals() method returns correct object
- Check that on_submit returns data (not false)
- Ensure callback doesn't throw unhandled errors
- Test vals() method independently
Queue Not Working
- All modals automatically queue
- If backdrop flickers, check for multiple backdrop creation
- Verify using Modal.* static methods (not creating instances)
Component Not Found
- Ensure component class name is correct (case-sensitive)
- Check that component files are in manifest
- Verify component extends Jqhtml_Component
- Component must be in /rsx/ directory tree
================================================================================