Overview
The ViewController extends BaseController and is designed for read-only document access. It enforces view-only mode by automatically redirecting all non-view actions to the view action and sets the client rendering mode to ‘view’.
Class Definition
import BaseController from './base-controller.js' ;
export default class ViewController extends BaseController {
client = "view" ;
constructor ( props ) {
super ( props );
this . action !== 'view' && this . redirect ( 'view' );
}
}
Source: packages/loopar/core/controller/view-controller.js:5
When to Use
For read-only document display
When you want to prevent any modifications to documents
For public or restricted viewing of data
For documents that should only be viewed, never edited
For display-only pages like reports, archives, or historical records
Key Features
Automatic redirection of all actions to ‘view’
Client rendering mode set to ‘view’ for optimized display
Inherits all BaseController functionality (available programmatically)
Simplified read-only document rendering
No edit, create, or delete capabilities through UI
Constructor
Configuration object containing controller initialization parameters:
action: The current action being performed (will be redirected if not ‘view’)
document: The document type being controlled
name: The name/identifier of the specific document instance
data: Request data
req: HTTP request object
res: HTTP response object
Other inherited controller properties
Behavior: Automatically redirects to ‘view’ action if any other action is attempted.
Properties
Client-side rendering mode, set to ‘view’ for read-only optimized rendering
Current action being executed - always ‘view’ due to automatic redirection
Document type name being controlled
The name/identifier of the specific document instance to view
Default action inherited from BaseController (redirected to ‘view’)
Whether to display sidebar navigation (inherited from BaseController)
Inherited Methods
ViewController inherits all methods from:
BaseController - Full CRUD operations (programmatic access only)
CoreController - Rendering, error handling, authentication
AuthController - User authentication and authorization
Available Inherited Methods
While UI access is restricted to ‘view’, these methods are available programmatically:
Loads and renders the document in read-only mode. Returns: Promise<object> - Rendered document viewSource: Inherited from BaseController:85
Lists documents (available programmatically, not via UI routing). Returns: Promise<object> - Document list with pagination
success(message, options)
async success ( message , options = {})
Returns a success response. Returns: Promise<object> - Success response with notification
error(message, options, status)
async error ( message , options , status )
Returns an error response. Returns: Promise<object> - Error response with notification
Usage Examples
Creating a Read-Only Document Viewer
import ViewController from '@loopar/core/controller/view-controller' ;
import { loopar } from 'loopar' ;
export default class InvoiceViewController extends ViewController {
constructor ( props ) {
super ( props );
}
// Override to add custom view logic
async actionView () {
const document = await loopar . getDocument ( this . document , this . name );
// Add computed fields for display
document . totalAmount = this . calculateTotal ( document . items );
document . taxAmount = this . calculateTax ( document . totalAmount );
document . grandTotal = document . totalAmount + document . taxAmount ;
// Add related data
document . customer = await loopar . getDocument ( 'Customer' , document . customer_id );
document . payments = await this . getPayments ( document . name );
return await this . render ( document );
}
calculateTotal ( items ) {
return items . reduce (( sum , item ) => sum + ( item . quantity * item . rate ), 0 );
}
calculateTax ( amount ) {
return amount * 0.1 ; // 10% tax
}
async getPayments ( invoiceName ) {
return await loopar . getList ( 'Payment' , {
data: { invoice: invoiceName }
});
}
}
Archive Document Viewer
import ViewController from '@loopar/core/controller/view-controller' ;
import { loopar } from 'loopar' ;
export default class ArchivedDocumentController extends ViewController {
constructor ( props ) {
super ( props );
}
async actionView () {
const document = await loopar . getDocument ( this . document , this . name );
// Check if document is archived
if ( document . status !== 'Archived' ) {
return this . error ( 'This document is not archived' , {
redirect: `/desk/ ${ this . document } /view?name= ${ this . name } `
});
}
// Add archive metadata
document . archivedBy = await loopar . getDocument ( 'User' , document . archived_by );
document . archivedDate = document . archived_at ;
document . archiveReason = document . archive_reason ;
// Add warning banner
document . isArchived = true ;
document . archiveWarning = 'This is an archived document and cannot be modified.' ;
return await this . render ( document );
}
// Custom action to view archive history
async actionHistory () {
const history = await loopar . getList ( 'Document Version' , {
data: {
document_type: this . document ,
document_name: this . name
},
orderBy: 'creation DESC'
});
return this . success ( 'History retrieved' , { history: history . rows });
}
}
Read-Only Report Viewer
import ViewController from '@loopar/core/controller/view-controller' ;
import { loopar } from 'loopar' ;
export default class ReportViewController extends ViewController {
constructor ( props ) {
super ( props );
}
async actionView () {
const report = await loopar . getDocument ( this . document , this . name );
// Generate report data
const reportData = await this . generateReportData ( report );
report . data = reportData ;
report . generatedAt = new Date ();
report . generatedBy = loopar . currentUser ?. name ;
return await this . render ( report );
}
async generateReportData ( report ) {
const { startDate , endDate , filters } = report . config ;
// Generate sales report
const sales = await loopar . db . getAll (
`SELECT
DATE(creation) as date,
SUM(grand_total) as total_sales,
COUNT(*) as order_count
FROM \` Sales Order \`
WHERE creation BETWEEN ? AND ?
GROUP BY DATE(creation)
ORDER BY date` ,
[ startDate , endDate ]
);
return {
sales ,
summary: {
totalRevenue: sales . reduce (( sum , row ) => sum + row . total_sales , 0 ),
totalOrders: sales . reduce (( sum , row ) => sum + row . order_count , 0 ),
averageOrderValue: sales . reduce (( sum , row ) => sum + row . total_sales , 0 ) / sales . length
}
};
}
// Allow exporting report data
async actionExport () {
const report = await loopar . getDocument ( this . document , this . name );
const reportData = await this . generateReportData ( report );
// Generate CSV
const csv = this . generateCSV ( reportData . sales );
return {
status: 200 ,
headers: {
'Content-Type' : 'text/csv' ,
'Content-Disposition' : `attachment; filename=" ${ this . name } -report.csv"`
},
body: csv
};
}
generateCSV ( data ) {
const headers = Object . keys ( data [ 0 ]). join ( ',' );
const rows = data . map ( row => Object . values ( row ). join ( ',' )). join ( ' \n ' );
return ` ${ headers } \n ${ rows } ` ;
}
}
Public Document Viewer with Access Control
import ViewController from '@loopar/core/controller/view-controller' ;
import { loopar } from 'loopar' ;
export default class PublicDocumentController extends ViewController {
constructor ( props ) {
super ( props );
}
async beforeAction () {
// Custom access control
const hasAccess = await this . checkDocumentAccess ();
if ( ! hasAccess ) {
return loopar . throw ({
code: 403 ,
message: 'You do not have permission to view this document'
});
}
return await super . beforeAction ();
}
async checkDocumentAccess () {
const document = await loopar . getDocument ( this . document , this . name );
// Check if document is public
if ( document . is_public ) return true ;
// Check if user has specific access
const user = loopar . currentUser ;
if ( ! user ) return false ;
const hasAccess = await loopar . db . getValue ( 'Document Access' , 'name' , {
document_type: this . document ,
document_name: this . name ,
user: user . name
});
return !! hasAccess ;
}
async actionView () {
const document = await loopar . getDocument ( this . document , this . name );
// Track view
await this . trackView ( document );
// Redact sensitive information for non-admin users
if ( ! loopar . currentUser ?. is_admin ) {
document . sensitiveField = '[REDACTED]' ;
document . internalNotes = null ;
}
return await this . render ( document );
}
async trackView ( document ) {
const view = await loopar . newDocument ( 'Document View' , {
document_type: this . document ,
document_name: document . name ,
user: loopar . currentUser ?. name || 'Anonymous' ,
viewed_at: new Date (),
ip_address: this . req . ip
});
await view . save ();
}
}
Historical Record Viewer
import ViewController from '@loopar/core/controller/view-controller' ;
import { loopar } from 'loopar' ;
export default class HistoricalRecordController extends ViewController {
constructor ( props ) {
super ( props );
}
async actionView () {
const document = await loopar . getDocument ( this . document , this . name );
// Add historical context
document . viewMode = 'historical' ;
document . isEditable = false ;
// Load related historical data
document . timeline = await this . getDocumentTimeline ( document );
document . relatedDocuments = await this . getRelatedHistoricalDocuments ( document );
// Add comparison with current state if document was updated
if ( document . has_current_version ) {
document . currentVersion = await this . getCurrentVersion ( document );
document . changes = this . compareVersions ( document , document . currentVersion );
}
return await this . render ( document );
}
async getDocumentTimeline ( document ) {
return await loopar . getList ( 'Document History' , {
data: {
document_type: this . document ,
document_name: document . name
},
orderBy: 'creation ASC'
});
}
async getRelatedHistoricalDocuments ( document ) {
return await loopar . getList ( this . document , {
data: {
related_to: document . name ,
status: 'Historical'
},
limit: 10
});
}
async getCurrentVersion ( document ) {
return await loopar . db . getDoc ( this . document , {
name: document . name ,
status: { '!=' : 'Historical' }
});
}
compareVersions ( historical , current ) {
const changes = [];
for ( const [ key , value ] of Object . entries ( historical )) {
if ( current [ key ] !== value && key !== 'modified' ) {
changes . push ({
field: key ,
oldValue: value ,
newValue: current [ key ]
});
}
}
return changes ;
}
}
Best Practices
Read-Only Mode : ViewController enforces view-only access. All modification attempts are automatically redirected to view action.
The client = "view" property affects client-side rendering. Ensure your client-side code properly handles view-only mode.
Do’s
Use ViewController for documents that should never be edited through UI
Override actionView() to add computed fields and related data
Implement custom actions for read-only operations (e.g., export, print)
Add access control in beforeAction() when needed
Track document views for analytics
Provide clear visual indicators that the document is read-only
Don’ts
Don’t expect edit, create, or delete actions to work via URL routing
Don’t use for documents that require modification capabilities
Don’t skip access control checks for sensitive documents
Avoid heavy computation in actionView() without caching
Don’t expose sensitive data without proper redaction
Client-Side Rendering
The client = "view" property affects client-side rendering:
// In CoreController's clientImporter method
const getClient = () => {
if ([ "Page" , "View" ]. includes ( Document . Entity . type )) return "view" ;
if ( this . client ) return this . client ; // Returns 'view' for ViewController
// ... other logic
}
This ensures the correct client-side entry point is used for optimized read-only rendering.
Security Considerations
Access Control : Even in view-only mode, implement proper access control to prevent unauthorized viewing of sensitive documents.
import ViewController from '@loopar/core/controller/view-controller' ;
export default class SecureViewController extends ViewController {
async beforeAction () {
// Check authentication
const authenticated = await super . beforeAction ();
if ( ! authenticated ) return false ;
// Check document-level permissions
const hasPermission = await this . checkPermission ();
if ( ! hasPermission ) {
return loopar . throw ({
code: 403 ,
message: 'Access denied'
});
}
return true ;
}
async checkPermission () {
const user = loopar . currentUser ;
const document = await loopar . getDocument ( this . document , this . name );
// Admin can view all
if ( user . is_admin ) return true ;
// Owner can view own documents
if ( document . owner === user . name ) return true ;
// Check explicit permissions
return await loopar . permissions . has ({
user: user . name ,
document_type: this . document ,
document_name: this . name ,
permission: 'read'
});
}
async actionView () {
const document = await loopar . getDocument ( this . document , this . name );
// Redact sensitive fields based on user role
document . redactedFields = await this . redactSensitiveData ( document );
return await this . render ( document );
}
async redactSensitiveData ( document ) {
const user = loopar . currentUser ;
const sensitiveFields = [ 'ssn' , 'credit_card' , 'password' ];
if ( ! user . is_admin ) {
sensitiveFields . forEach ( field => {
if ( document [ field ]) {
document [ field ] = '[REDACTED]' ;
}
});
}
return sensitiveFields ;
}
}
Comparison with Other Controllers
Feature ViewController FormController SingleController BaseController Multiple instances Yes Yes No Yes List view Redirected Redirected Redirected Yes Edit capability No No Yes Yes Client mode ’view’ Inherited Inherited Dynamic Best for Read-only Forms Settings Full CRUD Action restriction View only View only View/Update All actions
Common Use Cases
Invoice Viewing : Read-only invoice display for customers
Archive Access : Viewing archived or historical documents
Report Display : Showing generated reports without edit capability
Public Records : Displaying public documents with access control
Document Preview : Preview mode before editing
Audit Logs : Viewing system audit logs (read-only)
Certificate Display : Showing certificates or credentials
Troubleshooting
Cannot access edit or create actions
This is by design. ViewController restricts all actions to ‘view’. If you need edit capabilities, use BaseController or SingleController instead.
Redirect loop when accessing document
Ensure you’re accessing the document with the ‘view’ action: /desk/Invoice/view?name=INV-001
Not: /desk/Invoice/update?name=INV-001 // Will redirect to view
Client rendering doesn't use 'view' mode
Check that the client property is not being overridden in your controller. The client property should remain set to ‘view’.
Need to allow specific modifications
Implement custom actions that programmatically modify data while keeping the UI in view-only mode: async actionApprove () {
const document = await loopar . getDocument ( this . document , this . name );
document . status = 'Approved' ;
await document . save ();
return this . success ( 'Document approved' );
}