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:
- Always use a handler
- 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,274