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:

  1. Field Lock - Locks most fields when status reaches certain value (e.g., "Submitted")
  2. 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:

  • dnSubmit
  • dnReviewed (optional)
  • dnFollowUp (optional)
  • dnResolved (optional)

Other Status Methods

Check the API documentation for additional status methods:

  • p_s_r_complete - Pending → Submitted → Reviewed → Complete
  • p_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:

  1. Calls mApp.setPermissions() to determine user's role
  2. Calls mApp.setFieldLocks() to lock fields based on status
  3. 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:

  1. Calls mApp.getStatus() to calculate new status
  2. 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:

  1. Gets current user's name
  2. Extracts initials
  3. Sets value in mApp.dnCoordinatorInitials field

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:

  1. Checks if photo has GPS coordinates
  2. If requireLandscape, checks orientation
  3. 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:

  1. Shows alert explaining why record creation is disabled
  2. 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


Additional Resources