MerjentApp (Mapp) Module
Overview
MerjentApp (abbreviated as Mapp or mapp) is a class that streamlines the creation of Fulcrum apps with standard Merjent workflows. It handles permission control, status updates, and field locking automatically, reducing boilerplate code and ensuring consistency across apps.
The Problem
Building Merjent Fulcrum apps typically requires:
- Role-based permissions - Field staff can't edit PM-only fields
- Status workflows - Forms progress through Pending → Submitted → Reviewed → Complete
- Field locking - Submitted forms can't be edited (except by PMs)
- Photo validation - Photos must have location data
- Coordinator approvals - Tracking who reviewed/approved
Writing this logic from scratch in every app is:
- Time-consuming (100+ lines of repetitive code)
- Error-prone (easy to miss edge cases)
- Inconsistent (different implementations across apps)
The Solution
MerjentApp provides pre-built mixins for common workflows:
const mApp = module.exports.MerjentApp;
// Configure once
mApp.dnSubmit = 'submit_report';
mApp.completeLockCondition = ['Complete'];
// Use everywhere
ON('load-record', mApp.start); // Auto-handles permissions & locking
ON('change', mApp.dnSubmit, mApp.updateStatus); // Auto-updates status
Important Things to Remember
Before configuring MerjentApp, keep these critical points in mind:
✅ Configure BEFORE Event Handlers
// ✓ CORRECT - Configure first, then attach events
mApp.dnSubmit = 'submit_report';
mApp.completeLockCondition = ['Complete'];
mApp.getStatus = mApp.statusMethods.p_s_complete;
ON('load-record', mApp.start);
ON('change', mApp.dnSubmit, mApp.updateStatus);
// ✗ WRONG - Events attached before configuration
ON('load-record', mApp.start); // Won't work correctly
mApp.dnSubmit = 'submit_report'; // Too late!
✅ Required Properties
Always set these three properties:
mApp.dnSubmit = 'submit_report'; // Required
mApp.completeLockCondition = ['Complete']; // Required
mApp.getStatus = mApp.statusMethods.p_s_complete; // Required
✅ Use Property References
// ✓ GOOD - Use mApp properties
ON('change', mApp.dnSubmit, mApp.updateStatus);
// ✗ AVOID - Hardcoded data names
ON('change', 'submit_report', mApp.updateStatus);
If you change the field data name, you only need to update one line.
✅ Always Use mApp.start on Load
// ✓ REQUIRED
ON('load-record', mApp.start);
This sets permissions AND locks fields. Don't skip it!
✅ Test with Different User Roles
Always test your app as:
- Admin user
- Editor user
- Regular user (default permission level)
- User not in any list
✅ Use Correct Case
// ✓ CORRECT
const mApp = module.exports.MerjentApp
mApp.dnSubmit = 'submit_report';
// ✗ WRONG - lowercase 'app'
const mApp = module.exports.MerjentApp
mapp.dnSubmit = 'submit_report'; // Undefined!
Installation
MerjentApp is included in merjent-tools-mApp.mmin.js:
//#region !START MERJENT FULCRUM TOOLS!
// ... paste merjent-tools-mApp.mmin.js content here ...
//#endregion !END MERJENT FULCRUM TOOLS!
// Instantiate both Mtools and MApp
const M = module.exports.Mtools
const mApp = module.exports.MerjentApp
Note: merjent-tools-mApp.mmin.js includes everything from merjent-tools-de.mmin.js plus the MApp class.
Core Concepts
Permission Levels
MerjentApp recognizes four user roles (from least to most privileged):
| Level | Description | Typical Users |
|---|---|---|
| Viewer | Read-only access | Stakeholders, clients |
| User | Standard field staff | Field biologists, surveyors |
| Editor | Project coordinators | Coord |
inators, senior staff | | Admin | Project managers | PMs, administrators |
Status Workflow
Typical progression:
Pending → Submitted → Reviewed → Complete
↓ ↓ ↓ ↓
(editing) (locked) (PM review) (archived)
Field Locking
Two types of locks:
- Field Lock - Locks most fields when status reaches certain value (e.g., "Submitted")
- Complete Lock - Locks ALL fields when status reaches final value (e.g., "Complete")
Both support exclusion lists for fields that should remain editable.
Basic Configuration
Minimal Setup
const mApp = module.exports.MerjentApp
// Required: Status field data name
mApp.dnSubmit = 'submit_report';
// Required: Lock condition (status values that trigger locking)
mApp.completeLockCondition = ['Complete', 'Resolved'];
// Required: Status calculation method
mApp.getStatus = mApp.statusMethods.p_s_complete;
// Attach to data events
ON('load-record', mApp.start);
ON('change', mApp.dnSubmit, mApp.updateStatus);
Full Configuration Example
const mApp = module.exports.MerjentApp
//********** User Permissions **********
mApp.adminNames = ['Jane Smith', 'John Doe'];
mApp.editorNames = ['Alice Johnson'];
mApp.editorRoles = ['Coordinator'];
mApp.defaultPermissionLevel = 'User';
//********** Status Fields **********
mApp.dnSubmit = 'submit_report';
mApp.dnReviewed = 'coordinator_reviewed';
mApp.dnCoordinatorInitials = 'coordinator_initials';
mApp.dnFollowUp = 'follow_up_required';
mApp.dnResolved = 'follow_up_closed';
//********** Status Method **********
mApp.getStatus = mApp.statusMethods.p_s_complete;
//********** Field Locking **********
mApp.fieldLockCondition = ['Submitted'];
mApp.fieldLockExclude = [mApp.dnSubmit];
mApp.completeLockCondition = ['Complete', 'Resolved'];
mApp.completeLockExclude = [mApp.dnSubmit, mApp.dnReviewed];
//********** Photo Settings **********
mApp.dnPhotoLoc = 'site_photos';
mApp.allPhotos = ['site_photos', 'equipment_photos'];
mApp.requireLandscape = false;
mApp.setLocoff1photo = true;
//********** Other Settings **********
mApp.disableLocationEdit = false;
mApp.dnAutoName = 'inspector_name'; // Auto-fill with user's name on new record
mApp.dnDt = 'date';
mApp.dnReportID = 'report_id';
//********** Data Events **********
ON('load-record', mApp.start);
ON('change', mApp.dnSubmit, mApp.updateStatus);
ON('change', mApp.dnReviewed, mApp.addCoordinatorInitials);
ON('add-photo', mApp.dnPhotoLoc, mApp.validatePhoto);
Configuration Reference
Permission Settings
mApp.adminNames
Type: Array<string>
Default: []
Description: Names of users with Admin permission level
mApp.adminNames = ['Jane Smith', 'John Doe'];
mApp.editorNames
Type: Array<string>
Default: []
Description: Names of users with Editor permission level
mApp.editorNames = ['Alice Johnson', 'Bob Wilson'];
mApp.editorRoles
Type: Array<string>
Default: []
Description: Fulcrum role names that get Editor permission level
mApp.editorRoles = ['Coordinator', 'Senior Staff'];
mApp.defaultPermissionLevel
Type: string
Default: 'User'
Description: Default permission level for users not in other lists
mApp.defaultPermissionLevel = 'Viewer'; // Make most users read-only
Status Field Settings
mApp.dnSubmit ⚠️ Required
Type: string
Description: Data name of the submission field (Yes/No or Choice field)
mApp.dnSubmit = 'submit_report';
mApp.dnReviewed
Type: string
Description: Data name of the reviewed/approved field
mApp.dnReviewed = 'coordinator_reviewed';
mApp.dnCoordinatorInitials
Type: string
Description: Data name of field to store coordinator's initials
mApp.dnCoordinatorInitials = 'coordinator_initials';
mApp.dnFollowUp
Type: string
Description: Data name of follow-up required field
mApp.dnFollowUp = 'follow_up_required';
mApp.dnResolved
Type: string
Description: Data name of follow-up resolved field
mApp.dnResolved = 'follow_up_closed';
Locking Settings
mApp.fieldLockCondition
Type: Array<string> or null
Default: ['Submitted']
Description: Status values that trigger field locking (partial lock)
mApp.fieldLockCondition = ['Submitted', 'In Review'];
Set to null to disable field locking:
mApp.fieldLockCondition = null;
mApp.fieldLockExclude
Type: Array<string>
Default: []
Description: Data names of fields excluded from field lock
mApp.fieldLockExclude = [mApp.dnSubmit, 'notes'];
mApp.completeLockCondition ⚠️ Required
Type: Array<string> or null
Default: ['Complete', 'Resolved']
Description: Status values that trigger complete lock (all fields)
mApp.completeLockCondition = ['Complete', 'Archived'];
Set to null to disable complete locking:
mApp.completeLockCondition = null;
mApp.completeLockExclude
Type: Array<string>
Default: []
Description: Data names of fields excluded from complete lock (Admin only)
mApp.completeLockExclude = [mApp.dnSubmit, mApp.dnReviewed];
Photo Settings
mApp.dnPhotoLoc
Type: string
Description: Data name of primary location photo field
mApp.dnPhotoLoc = 'site_photos';
mApp.allPhotos
Type: Array<string>
Description: Data names of all photo fields in form
mApp.allPhotos = ['site_photos', 'equipment_photos', 'species_photos'];
mApp.requireLandscape
Type: boolean
Default: false
Description: Require photos to be in landscape orientation
mApp.requireLandscape = true;
mApp.setLocoff1photo
Type: boolean
Default: false
Description: Turn off location services after first photo with location
mApp.setLocoff1photo = true;
Other Settings
mApp.disableLocationEdit
Type: boolean
Default: false
Description: Prevent users from manually editing record location
mApp.disableLocationEdit = true;
mApp.dnAutoName
Type: string or null
Default: null
Description: Data name of field to auto-populate with user's full name on new record creation. Set to null to disable.
mApp.dnAutoName = 'inspector_name';
When set, mApp.start() registers a new-record event handler that automatically sets the specified field to the current user's full name via USERFULLNAME().
Use case: Pre-fill inspector/surveyor name fields so users don't have to type their name on every new record.
mApp.dnDt
Type: string
Description: Data name of date field
mApp.dnDt = 'observation_date';
mApp.dnReportID
Type: string
Description: Data name of report ID field
mApp.dnReportID = 'report_id';
Status Methods
The getStatus property determines how status is calculated. MerjentApp provides several pre-built status methods:
mApp.statusMethods.p_s_complete
Pattern: Pending → Submitted → Complete
mApp.getStatus = mApp.statusMethods.p_s_complete;
Logic:
- If follow-up resolved: "Resolved"
- If follow-up required: "Follow-up Required"
- If reviewed: "Complete"
- If submitted: "Submitted"
- Otherwise: "Pending"
Required Fields:
dnSubmitdnReviewed(optional)dnFollowUp(optional)dnResolved(optional)
Other Status Methods
Check the API documentation for additional status methods:
p_s_r_complete- Pending → Submitted → Reviewed → Completep_complete- Pending → Complete- Custom methods can be added
Common Configuration Patterns
Pattern 1: Standard Merjent App (Default)
Most Merjent apps use this configuration:
mApp.dnSubmit = 'submit_report';
mApp.dnReviewed = 'coordinator_reviewed';
mApp.dnCoordinatorInitials = 'coordinator_initials';
mApp.completeLockCondition = ['Complete', 'Resolved'];
mApp.fieldLockCondition = ['Submitted'];
mApp.fieldLockExclude = [mApp.dnSubmit];
mApp.completeLockExclude = [mApp.dnSubmit, mApp.dnReviewed];
mApp.getStatus = mApp.statusMethods.p_s_complete;
ON('load-record', mApp.start);
ON('change', mApp.dnSubmit, mApp.updateStatus);
ON('change', mApp.dnReviewed, mApp.addCoordinatorInitials);
Use when: Standard PM/Coordinator/Field Staff workflow
Pattern 2: No PM Review (User-Only App)
For apps where everyone has the same permissions:
mApp.dnSubmit = 'submit_report';
mApp.completeLockCondition = null; // No complete lock
mApp.fieldLockCondition = ['Submitted'];
mApp.fieldLockExclude = [mApp.dnSubmit];
mApp.getStatus = mApp.statusMethods.p_s_complete;
ON('load-record', mApp.start);
ON('change', mApp.dnSubmit, mApp.updateStatus);
Use when: No PM approval needed, just prevent editing after submission
Pattern 3: No Status/Locking (Permissions Only)
For simple apps that only need permission control:
mApp.adminNames = ['Jane Smith'];
mApp.editorNames = ['Bob Wilson'];
mApp.dnSubmit = null;
mApp.completeLockCondition = null;
mApp.fieldLockCondition = null;
ON('load-record', mApp.setPermissions); // Only set permissions, no locking
Use when: You want role-based field restrictions but no workflow
Pattern 4: Read-Only Archive
For completed projects where data should be locked:
mApp.adminNames = ['Jane Smith']; // Only PM can edit
mApp.dnSubmit = null;
mApp.completeLockCondition = null;
mApp.fieldLockCondition = null;
mApp.defaultPermissionLevel = 'Viewer'; // Everyone else is read-only
ON('load-record', mApp.setPermissions);
Use when: Project complete, data archived, minimal editing needed
Data Event Handlers
mApp.start()
Description: Main initialization handler - sets permissions and locks fields
Usage:
ON('load-record', mApp.start);
What it does:
- Calls
mApp.setPermissions()to determine user's role - Calls
mApp.setFieldLocks()to lock fields based on status - Calls
mApp.setCompleteLock()to lock all fields if complete
When to use: Always use this in the load-record event
mApp.updateStatus()
Description: Automatically updates form status based on field values
Usage:
ON('change', mApp.dnSubmit, mApp.updateStatus);
What it does:
- Calls
mApp.getStatus()to calculate new status - Updates form status using
SETSTATUS()
When to use: Whenever status-related fields change
mApp.addCoordinatorInitials()
Description: Adds coordinator's initials when they review the form
Usage:
ON('change', mApp.dnReviewed, mApp.addCoordinatorInitials);
What it does:
- Gets current user's name
- Extracts initials
- Sets value in
mApp.dnCoordinatorInitialsfield
When to use: When coordinators review/approve forms
mApp.validatePhoto()
Description: Validates photo has location data and correct orientation
Usage:
ON('add-photo', mApp.dnPhotoLoc, mApp.validatePhoto);
What it does:
- Checks if photo has GPS coordinates
- If
requireLandscape, checks orientation - Shows alert if validation fails
When to use: For photo fields that require location data
mApp.disableNewRecordCreation()
Description: Prevents users from creating new records
Usage:
ON('new-record', mApp.disableNewRecordCreation);
What it does:
- Shows alert explaining why record creation is disabled
- Calls
QUIT()to exit
When to use: For forms where only admins should create records, or project is complete
Advanced Usage
Custom Status Calculation
Create your own status method:
mApp.getStatus = function() {
const submitted = this.getVal(this.dnSubmit);
const approved = this.getVal('pm_approved');
const deployed = this.getVal('equipment_deployed');
if (deployed === 'yes') return 'Deployed';
if (approved === 'yes') return 'Approved';
if (submitted === 'yes') return 'Submitted';
return 'Draft';
};
ON('change', mApp.dnSubmit, mApp.updateStatus);
ON('change', 'pm_approved', mApp.updateStatus);
ON('change', 'equipment_deployed', mApp.updateStatus);
Conditional Locking
Customize field locks based on complex logic:
function customLockFields() {
mApp.setPermissions(); // Set user's permission level first
const status = STATUS();
const userLevel = mApp.permissionLevel;
// Custom locking logic
if (status === 'Submitted' && userLevel === 'User') {
// Lock everything for Users
const allFields = FIELDNAMES();
allFields.forEach(dn => {
if (dn !== mApp.dnSubmit) {
SETFORMATTRIBUTES(dn, {disabled: true});
}
});
}
}
ON('load-record', customLockFields);
Dynamic Permission Assignment
Assign permissions based on record data:
ON('load-record', () => {
mApp.setPermissions();
const projectLead = mApp.getVal('project_lead');
const currentUser = CURRENTUSER().name;
// Project lead gets Editor permissions
if (projectLead === currentUser && mApp.permissionLevel === 'User') {
mApp.permissionLevel = 'Editor';
}
mApp.setFieldLocks();
mApp.setCompleteLock();
});
Troubleshooting
Issue: Fields not locking properly
Cause: MApp not initialized or status conditions incorrect
Solution:
// Make sure you call mApp.start() on load-record
ON('load-record', mApp.start);
// Check status conditions match your status values exactly
console.log('Current status:', STATUS());
console.log('Field lock condition:', mApp.fieldLockCondition);
console.log('Complete lock condition:', mApp.completeLockCondition);
Issue: Permissions not working
Cause: Names don't match exactly, or user has no Fulcrum role
Solution:
// Check user's name and roles
ON('load-record', () => {
console.log('Current user:', CURRENTUSER().name);
console.log('User roles:', CURRENTUSER().roles);
console.log('Permission level:', mApp.permissionLevel);
});
// Names must match EXACTLY (case-sensitive)
mApp.adminNames = ['Jane Smith']; // Won't match 'jane smith' or 'Jane Smith'
Issue: Status not updating
Cause: getStatus method not set, or event handler not attached
Solution:
// Required: Set status method
mApp.getStatus = mApp.statusMethods.p_s_complete;
// Required: Attach event handler
ON('change', mApp.dnSubmit, mApp.updateStatus);
// Debug: Check what status should be
console.log('Calculated status:', mApp.getStatus());
Best Practices
1. Always Configure Before Event Handlers
// CORRECT: Configure first
mApp.dnSubmit = 'submit_report';
mApp.getStatus = mApp.statusMethods.p_s_complete;
ON('load-record', mApp.start);
// WRONG: Event handlers before configuration
ON('load-record', mApp.start); // Won't work right
mApp.dnSubmit = 'submit_report'; // Too late!
2. Use Property References in Event Handlers
// CORRECT: Use mApp.dnSubmit
ON('change', mApp.dnSubmit, mApp.updateStatus);
// WRONG: Hardcoded data name
ON('change', 'submit_report', mApp.updateStatus);
This way, if you change the data name, you only update one line.
3. Test with Different User Roles
Always test your app with:
- Admin user
- Editor user
- Regular user
- User not in any list (default permission level)
4. Document Your Configuration
//********************************************* MERJENT APP *************************************/
// APP CONFIGURATION:
// - Admins: Jane Smith, John Doe
// - Editors: Coordinator role
// - Status: Pending → Submitted → Complete
// - Field Lock: Submitted (users can't edit)
// - Complete Lock: Complete (only admins can edit excluded fields)
mApp.adminNames = ['Jane Smith', 'John Doe'];
mApp.editorRoles = ['Coordinator'];
// ... rest of configuration
Next Steps
- Review Mtools Tutorial for helper methods
- Explore PDF Reports for report generation
- Check the API documentation for all methods and mixins