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