The FOSS Enterprise Application Platform

Building admin applications

with the Preside Platform

@dom_watson

@dom_watson

Building admin applications

with the Preside Platform

@dom_watson

Building admin applications

with the Preside Platform

  • Fundamentals, how it fits together
  • Layout(s)
  • Theming and styling
  • Login, permissioning and audit trail
  • Customizing and rendering data tables
  • Customizing forms (quick tips)
  • Linking to admin pages
  • Some cool extensions

@dom_watson

Fundamentals of Preside Admin

1. Admin requests are simple ColdBox requests

https://mysite.com/obfuscated_admin/datamanager/object/

settings.preside_admin_path = "obfuscated_admin";

= Coldbox event name: admin.datamanager.object

OR =: admin.datamanager.object.index

When creating new features:

  1. Always use a handler
  2. Always extend "preside.system.base.AdminHandler"
component extends="preside.system.base.AdminHandler" {

    function index() {
        // stuff
    }

}

@dom_watson

The admin layout

Sidebar

System menu

settings.adminSideBarItems = [
      "sitetree"
    , "assetmanager"
];

settings.adminConfigurationMenuItems = [
      "usermanager"
    , "notification"
    , "passwordPolicyManager"
    , // ...
]
if ( isFeatureEnabled( "datamanager" ) && hasCmsPermission( "datamanager.navigate" ) ) {
    WriteOutput( renderView(
          view = "/admin/layout/sidebar/_menuItem"
        , args = {
              active  = logicForIsActiveHere
            , link    = event.buildAdminLink( linkTo="datamanager" )
            , gotoKey = "d"
            , icon    = "fa-database"
            , title   = translateResource( 'cms:datamanager' )
          }
    ) );
}

Breadcrumb

Icon, title + subtitle

event.addAdminBreadCrumb( 
	  title = "My crumb"
	, link  = linkToPage
);

var breadcrumbs = event.getAdminBreadcrumbs();


prc.pageTitle    = translateResource( "myapp:mypage.title" );
prc.pageSubtitle = translateResource( "myapp:mypage.subTitle" );
prc.pageIcon     = "fa-cfcamp";

@dom_watson

The admin layout

Multi-ball!

// Config.cfc

// simple configuration, using convention for individual settings
settings.adminApplications.append( "eventfolio" );

// detailed configuration, equivalent to the above:
settings.adminApplications.append( {
      id                 = "eventfolio"
    , feature            = "eventfolio"
    , accessPermission   = "eventfolio.access"
    , defaultEvent       = "admin.eventfolio"
    , activeEventPattern = "^admin\.eventfolio\..*"
    , layout             = "eventfolio"
} );

@dom_watson

Theming and styling

+ "Ace admin theme"

http://ace.jeka.by/

@dom_watson

Theming and styling

https://sticker.readthedocs.io/

event.include( "/css/admin/core/" );
event.include( "/css/admin/specific/#currentHandler#/", false );
event.include( "/css/admin/specific/#currentHandler#/#currentAction#/", false );
event.include( "/js/admin/presidecore/" );
event.include( "/js/admin/specific/#currentHandler#/", false );
event.include( "/js/admin/specific/#currentHandler#/#currentAction#/", false );

Example extension with custom CSS/JS:

https://github.com/pixl8/preside-ext-admin-dashboards/tree/stable/assets

@dom_watson

Theming and styling

Adding CSS / JS to the default Admin layout

interceptors.append( { 
      class      = "app.interceptors.MyAdminLayoutInterceptor"
    , properties = {} 
} );
component extends="coldbox.system.Interceptor" {

	public void function configure() {}

	public void function preLayoutRender( event, interceptData={} ) {
		var layout = Trim( interceptData.layout ?: "" );

		if ( layout == "admin" ) {
			event.include( "my-custom-css" );
			event.include( "my-custom-js" );
		}
	}
}

@dom_watson

Login, permissioning and audit trail

// Config.cfc
settings.adminLoginProviders = [ "dummylogin", "preside" ];

// /views/admin/loginprovider/dummyLogin.cfm
<cfset customLoginLink = event.buildAdminLink( 
  linkTo="loginProvider.dummyLogin.dologin" 
)/>

<p class="text-center">
  <a class="btn btn-info" href="#customLoginLink#">
    <i class="fa fa-key fa-fw"></i> 
    #translateResource( "cms:one.click.local.login.btn" )#
  </a>
</p>

// /handlers/admin/loginProvider/DummyLogin.cfc
component {

  public void function dologin( event, rc, prc ) {
    var hardCodedLoginId  = "sysadmin";
    var hardCodedUserData = {
      email_address = "test@test.com"
     , known_as      = "The Sys Admin"
    };

        
    event.doAdminSsoLogin( 
        loginId              = hardCodedLoginId
      , userData             = hardCodedUserData
      , rememberLogin        = true
      , rememberExpiryInDays = 90
    );
  }
}

@dom_watson

Login, permissioning and audit trail

settings.adminPermissions.emailCenter = {
      layouts          = [ "navigate", "configure" ]
    , customTemplates  = [ "navigate", "view", "add", "edit", "delete", "publish", // .. ]
    , systemTemplates  = [ "navigate", "savedraft", "publish", "configurelayout" ]
};

// translates to permission keys

emailcenter.layouts.navigate;
emailcenter.layouts.configure;
emailcenter.customTemplates.navigate;
emailcenter.customTemplates.view;
// ... etc.


// usage:
hasCmsPermission( "emailcenter.layouts.navigate" );
settings.adminRoles.emailadmin = [ "emailcenter.*" ]; 
settings.adminRoles.emaileditor = [ 
      "emailcenter.customTemplates.*"
    , "!emailcenter.customTemplates.delete"
]; 
# /i18n/roles.properties

emailadmin.title=Email center admin
emailadmin.description=Can do all the email things

emaileditor.title=Email template editor
emailadmin.description=Can edit email templates, ready for sending

@dom_watson

Login, permissioning and audit trail

event.audit(
      action   = "someaction"
    , type     = "somtype"
    , recordId = recordThatChanged
    , detail   = { some=data }
);

// or $audit() from services

https://docs.preside.org/devguides/auditing.html

@dom_watson

Data tables

/**
 *
 * @datamanagerGridFields       title,coolness_score,datemodified
 * @datamanagerDefaultSortOrder coolness_score desc
 * @datamanagerHiddenGridFields extra_field
 * @datamanagerSearchFields     title,description
 */

@dom_watson

Data tables

Field renderers

property name="coolness_count" ... renderer="coolness";
component {

// /handlers/renderers/content/coolness.cfc

	private string function default( event, rc, prc, args={} ){
		return args.data ?: "";
	}

	private string function admin( event, rc, prc, args={} ){
		var rendered = args.data ?: "";
		// renderer logic here...
		return rendered;
	}

	private string function adminDatatable( event, rc, prc, args={} ){
		var rendered      = args.data ?: "";
		var currentRecord = args.record ?: {};

		// renderer logic here...
		
		return rendered;
	}

}

@dom_watson

Data tables

Directly render

objectDataTable( objectName="my_object", args={
	  useMultiActions  = false
	, noActions        = true
	, gridFields       = [ "title", "coolness_rating" ]
	, hiddenGridFields = [ "extra_field" ]
	, allowSearch      = false
	, allowFilter      = false
	, allowDataExport  = false
	, clickableRows    = false
	, compact          = true
} );

/preside/system/views/admin/datamanager/_objectDataTable.cfm

@dom_watson

Data tables

Customizations

private string function getAdditionalQueryStringForBuildAjaxListingLink( event, rc, prc, args={} ) {
    if( ( prc.objectName ?: "" ) == "crm_contact" && Len( Trim( prc.recordId ?: "" ) ) ) {
        return "contact_owner=#prc.recordId#";
    }
    if( ( prc.objectName ?: "" ) == "crm_organisation" && Len( Trim( prc.recordId ?: "" ) ) ) {
        return "organisation_owner=#prc.recordId#";
    }

    return "";
}

private void function preFetchRecordsForGridListing( event, rc, prc, args={} ) {
    var contact      = rc.contact_owner ?: "";
    var organisation = rc.organisation_owner ?: "";

    args.extraFilters = args.extraFilters ?: [];

    if ( !IsEmpty( contact )  ) {
        args.extraFilters.append( { filter={ "payment_invoice.contact_owner"=contact } } );
    }
    if ( !IsEmpty( organisation )  ) {
        args.extraFilters.append( { filter={ "payment_invoice.organisation_owner"=organisation } } );
    }
}

https://docs.preside.org/devguides/datamanager/customization.html

@dom_watson

Data tables

Customizations

// /application/handlers/admin/datamanager/pipeline.cfc
component {

    property name="pipelineService" inject="pipelineService";

    private string function renderFooterForGridListing( event, rc, prc, args={} ) {
        var pr = pipelineService.getPipelineTotalReport(
              filter       = args.getRecordsArgs.filter       ?: {}
            , extraFilters = args.getRecordsArgs.extraFilters ?: []
            , searchQuery  = args.getRecordsArgs.searchQuery  ?: ""
            , gridFields   = args.getRecordsArgs.gridFields   ?: []
            , searchFields = args.getRecordsArgs.searchFields ?: []
        );

        return translateResource(
              uri  = "pipeline_table:listing.table.footer"
            , data = [ NumberFormat( pr.total ), NumberFormat( pr.adjusted ), pr.currencySymbol ]
        );
    }

}

https://docs.preside.org/devguides/datamanager/customization/renderFooterForGridListing.html

@dom_watson

Data tables

Batch edit fields

property name="my_field" batcheditable=false;

@dom_watson

Forms

tab.mytab.title=My tab
tab.iconClass=fa-plus green

fieldset.myfieldset.title=My fieldset
fieldset.myfieldset.description=<p class="alert alert-info">This is my fieldset</p>
/forms/preside-objects/my_object.xml
/forms/preside-objects/my_object/admin.add.xml
/forms/preside-objects/my_object/admin.edit.xml

https://docs.preside.org/devguides/presideforms.html

@dom_watson

Linking to admin pages

event.buildAdminLink( linkto="handler.action", queryString="id=#id#" );

# translates to:
event.buildLink( linkto="admin.handler.action", queryString="id=#id#" );
event.buildAdminLInk( objectName="my_object" );
event.buildAdminLInk( objectName="my_object", recordId=id );
event.buildAdminLInk( objectName="my_object", recordId=id, operation="editRecord" );

@dom_watson

Cool extensions :)

#1 Preside "Launcher"

@dom_watson

Cool extensions :)

#2 'Better' View record screen

https://www.preside.org
https://www.preside.org/signup
https://www.preside.org/slack

https://presidecms.atlassian.net

@dom_watson

@dom_watson

CTO @ Pixl8 Group
Preside lead developer

Thanks for listening

Building admin applications with the Preside Platform

By Dominic Watson

Building admin applications with the Preside Platform

The Open Source CFML application development platform, Preside, lets you rapidly build admin applications for your clients and for your own internal tooling and intranets. Preside Lead Developer Dominic Watson takes you through what the platform can bring to your dev team.

  • 1,139