Skip to main content

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

props
object
required
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
string
default:"view"
Client-side rendering mode, set to ‘view’ for read-only optimized rendering
action
string
Current action being executed - always ‘view’ due to automatic redirection
document
string
Document type name being controlled
name
string
The name/identifier of the specific document instance to view
data
object
Request data object
defaultAction
string
default:"list"
Default action inherited from BaseController (redirected to ‘view’)
hasSidebar
boolean
default:"true"
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:
async actionView()
Loads and renders the document in read-only mode.Returns: Promise<object> - Rendered document viewSource: Inherited from BaseController:85
async actionList()
Lists documents (available programmatically, not via UI routing).Returns: Promise<object> - Document list with pagination
async render(meta)
Renders the view with client mode set to ‘view’.Returns: Promise<object> - Rendered response with metadata
async success(message, options = {})
Returns a success response.Returns: Promise<object> - Success response with notification
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

FeatureViewControllerFormControllerSingleControllerBaseController
Multiple instancesYesYesNoYes
List viewRedirectedRedirectedRedirectedYes
Edit capabilityNoNoYesYes
Client mode’view’InheritedInheritedDynamic
Best forRead-onlyFormsSettingsFull CRUD
Action restrictionView onlyView onlyView/UpdateAll actions

Common Use Cases

  1. Invoice Viewing: Read-only invoice display for customers
  2. Archive Access: Viewing archived or historical documents
  3. Report Display: Showing generated reports without edit capability
  4. Public Records: Displaying public documents with access control
  5. Document Preview: Preview mode before editing
  6. Audit Logs: Viewing system audit logs (read-only)
  7. Certificate Display: Showing certificates or credentials

Troubleshooting

This is by design. ViewController restricts all actions to ‘view’. If you need edit capabilities, use BaseController or SingleController instead.
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
Check that the client property is not being overridden in your controller. The client property should remain set to ‘view’.
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');
}

Build docs developers (and LLMs) love