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