Files
rspade_system/app/RSpade/man/modals.txt
root 77b4d10af8 Refactor filename naming system and apply convention-based renames
Standardize settings file naming and relocate documentation files
Fix code quality violations from rsx:check
Reorganize user_management directory into logical subdirectories
Move Quill Bundle to core and align with Tom Select pattern
Simplify Site Settings page to focus on core site information
Complete Phase 5: Multi-tenant authentication with login flow and site selection
Add route query parameter rule and synchronize filename validation logic
Fix critical bug in UpdateNpmCommand causing missing JavaScript stubs
Implement filename convention rule and resolve VS Code auto-rename conflict
Implement js-sanitizer RPC server to eliminate 900+ Node.js process spawns
Implement RPC server architecture for JavaScript parsing
WIP: Add RPC server infrastructure for JS parsing (partial implementation)
Update jqhtml terminology from destroy to stop, fix datagrid DOM preservation
Add JQHTML-CLASS-01 rule and fix redundant class names
Improve code quality rules and resolve violations
Remove legacy fatal error format in favor of unified 'fatal' error type
Filter internal keys from window.rsxapp output
Update button styling and comprehensive form/modal documentation
Add conditional fly-in animation for modals
Fix non-deterministic bundle compilation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 19:10:02 +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 $id="error_container"></div>
Example form component (my_form.jqhtml):
<Define:My_Form tag="div">
<div $id="error_container"></div>
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" $id="name_input" name="name">
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" $id="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.$id('name_input').val(values.name || '');
this.$id('email_input').val(values.email || '');
return null;
} else {
// Getter
return {
name: this.$id('name_input').val(),
email: this.$id('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 $id="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
================================================================================