Customizing Data Manager

Introduction

As of Preside 10.9.0, Data Manager comes with a customization system that allows you to customize many aspects of the Data Manager both globally and per object. In addition, you are able to use all the features of Data Manager for your object without needing to list your object in the Data Manager homepage. This means that you can create your own custom navigation to your object and not need to write any further code to create your CRUD admin interface - perfect for building custom admin interfaces with dedicated navigation.

Customization system overview

Customizations are implemented as convention based ColdBox handlers. Customizations that should be applied globally belong in /handlers/admin/datamanager/GlobalCustomizations.cfc. Customizations that should be applied to a specific object go in /handlers/admin/datamanager/objectname.cfc. For example, if you wish to supply customizations for a blog_author object, you would create a handler file: /handlers/admin/datamanager/blog_author.cfc.

The Data Manager implements a large number of customizations. Each customization will be implemented in your handlers as a private handler action. The return type (if any) and arguments supplied to the action will depend on the specific customization.

For example, you may wish to do some extra processing after saving an employee record using the postEditRecordAction customization:

// /application/handlers/datamanager/employee.cfc

component {

	// as this is a regular coldbox handler
	// we can use wirebox to inject and access our service layer
	property name="notificationService" inject="notificationService";

	private void function postEditRecordAction( event, rc, prc, args={} ) {
		// the args struct values will vary depending on the customization point.
		// in this case, we get new and old data (as well as many other fields)
		var newData    = args.formData       ?: {};
		var oldData    = args.existingRecord ?: {};
		var employeeId = args.recordId       ?: {}

		// here, as an example, we use the notification service to
		// raise a "Date of birth change" notification when the DOB changes
		if ( newData.keyExists( "dob" ) && newData.dob != oldData.dob ) {
			notificationService.createNotification( topic="DOBChange", type="info", data={ employeeId=employeeId } )
		}

		// of course, we could do anything we like here. For instance,
		// we could redirect the user to a different screen than the
		// normal "post-edit" behaviour for Data Manager.
	}

}

Building and customizing links

With the new 10.9.0 customization system comes a new method of building data manager links for objects. Use event.buildAdminLink( objectName=objectName ) along with optional arguments, operation and recordId to build various links. For example, to link to the data manager listing page for an object, use the following:

event.buildAdminLink( objectName=objectName );

To link to the default view for a record, use:

event.buildAdminLink( objectName=objectName, recordId=recordId );

To link to a specific page or action URL for an object or record, add the operation argument, e.g.

event.buildAdminLink( objectName=objectName, operation="addRecord" );
event.buildAdminLink( objectName=objectName, operation="editRecord", recordId=recordId );
// etc.

The core, "out-of-box" operations are:

  • listing
  • viewRecord
  • addRecord
  • addRecordAction
  • editRecord
  • editRecordAction
  • deleteRecordAction
  • translateRecord
  • sortRecords
  • managePerms
  • ajaxListing
  • multiRecordAction
  • exportDataAction
  • dataExportConfigModal
  • recordHistory
  • getNodesForTreeView

Tip

You can pass extra query string parameters to any of these links with the queryString argument. For example:

event.buildAdminLink(
	  objectName  = objectName
	, operation   = "addRecord"
	, queryString = "categoryId=#categoryId#"
);

Custom link builders

There is a naming convention for providing a custom link builder for an operation: build{operation}Link. There are therefore Data Manager customizations named buildListingLink, buildViewRecordLink, and so on. For example, to provide a completely different link for a view record screen for your object, you could do:

// /application/handlers/admin/datamanager/blog_author.cfc

component {

	private string function buildViewRecordLink( event, rc, prc, args={} ) {
		var recordId = args.recordId    ?: "";
		var extraQs  = args.queryString ?: "";
		var qs       = "id=#recordId#";

		if ( extraQs.len() ) {
			qs &= "&#extraQs#";
		}

		// e.g. here we would have a coldbox handler /admin/BlogAuthors.cfc
		// with a public 'view' method for completely controlling the entire
		// view record request outside of Data Manager
		return event.buildAdminLink( linkto="blogauthors.view", querystring=qs );
	}
}

Adding your own operations

If you are extending Data Manager to add extra pages for a particular object (for example), you can create new operations by following the same link building convention above. For example, say we wanted to build a "preview" link for an article, we can use the following:

// /handlers/admin/datamanager/article.cfc
component extends="preside.system.base.AdminHandler" {

// Public events for extra admin pages and actions
	public void function preview() {
		event.initializeDatamanagerPage(
			  objectName = "article"
			, recordId   = rc.id ?: ""
		);

		event.addAdminBreadCrumb(
			  title = translateResource( "preside-objects.article:preview.breadcrumb.title" )
			, linke = ""
		);

		prc.pageTitle = translateResource( "preside-objects.article:preview.page.title" );
		prc.pageSubTitle = translateResource( "preside-objects.article:preview.page.subtitle" );
	}

// customizations
	private string function buildPreviewLink( event, rc, prc, args={} ) {
		var qs = "id=#( args.recordId ?: "" )#";

		if ( Len( Trim( args.queryString ?: "" ) ) ) {
			qs &= "&#args.queryString#";
		}

		return event.buildAdminLink( linkto="datamanager.article.preview", querystring=qs );
	}



}

Linking to the "preview" operation can then be done with:

event.buildAdminLink( objectName="article", operation="preview", id=recordId );

Info

Notice that the handler extends preside.system.base.AdminHandler. This base handler supplies a preAction that sets the admin layout and checks for logged in users. You should do this when supplying additional public handler actions in your customization.

event.initializeDatamanagerPage()

Notice the handy event.initializeDatamanagerPage() in the example, above. This method will setup standard breadcrumbs for your page as well as setting up common variables that are available to other data manager pages such as:

  • prc.recordId: id of the current record being viewed
  • prc.record: current record being viewed
  • prc.recordLabel: rendered label field for the current record
  • prc.objectName: current object name
  • prc.objectTitle: translated title of the current object
  • prc.objectTitlePlural: translated plural title of the current object

The method expects either one, or two arguments: objectName, the name of the object, and recordId, the ID of the current record (if applicable).

Customization reference

There are currently more than 60 customization points in the Data Manager and this number is set to grow. We have grouped them into categories below for your reference:

Record listing table / grid

Info

In addition to the specific customizations, below, you can also use the following helper functions in your handlers and views to render a data table / tree view for an object:

renderedListingTable = objectDataTable( objectName="blog_post", args={} );
renderedTreeView = objectTreeView( objectName="article", args={} );

Adding records

Viewing records

Info

The customizations below allow you to override or decorate the core record rendering system in Data Manager. In addition to these, you should also familiarize yourself with Admin record views as the core view record screen can also be customized using annotations within your Preside Objects.

Editing records

Cloning records

Deleting records

Building links

Permissioning

General

Interception points

Your application can listen into several core interception points to enhance the features of the Data manager customization, e.g. to implement custom authentication. See the ColdBox Interceptor's documentation for detailed documentation on interceptors.

The Interception points are:

postExtraTopRightButtonsForObject

Fired after the extraTopRightButtonsForObject customization action had run. Takes objectName and actions as arguments.

postGetExtraQsForBuildAjaxListingLink

Fired after the getAdditionalQueryStringForBuildAjaxListingLink customization action (if any) had run. Takes objectName and extraQs as arguments.

postExtraRecordActionsForGridListing

Fired after the extraRecordActionsForGridListing customization action had run. Takes record, objectName and actions as arguments.

onGetListingBatchActions

Fired during the getListingMultiActions customisation action. Takes args as arguments.

postGetExtraListingMultiActions

Fired after the getExtraListingMultiActions customization action had run. Takes args as arguments.

postGetExtraAddRecordActionButtons

Fired after the getExtraAddRecordActionButtons customization action had run. Takes args as arguments.

postExtraTopRightButtonsForAddRecord

Fired after the extraTopRightButtonsForAddRecord customization action had run. Takes objectName and actions as arguments.

postExtraTopRightButtonsForViewRecord

Fired after the extraTopRightButtonsForViewRecord customization action had run. Takes objectName and actions as arguments.

postGetExtraEditRecordActionButtons

Fired after the getExtraEditRecordActionButtons customization action had run. Takes args as arguments.

postExtraTopRightButtonsForEditRecord

Fired after the extraTopRightButtonsForEditRecord customization action had run. Takes objectName and actions as arguments.

postGetExtraCloneRecordActionButtons

Fired after the getExtraCloneRecordActionButtons customization action had run. Takes args as arguments.

postExtraTopRightButtons

Fired after the extraTopRightButtons customization action had run. Takes objectName, action and actions as arguments.

Creating your own customizations

You may wish to utilize the customization system in your extensions to allow implementations to easily override additional data manager features that you may provide. To do so, you can inject the DataManagerCustomizationService into your handler or service and make use of the methods:

For example:

if ( datamanagerCustomizationService.objectHasCustomization( objectName, "printPreview" ) ) {
	printPreview = datamanagerCustomizationService.runCustomization(
		  objectName = objectName
		, action     = "printPreview"
		, args       = args
	);
} else {
	printPreview = renderView( view=defaultView, args=args );
}

Or:

printPreview = datamanagerCustomizationService.runCustomization(
	  objectName     = objectName
	, action         = "printPreview"
	, defaultHandler = "myhandler.printPreview"
	, args           = args
);

Custom navigation to your objects

One of the most powerful changes in 10.9.0 is the ability to have objects use the Data Manager system without needing to be listed in the Data Manager homepage. This means that you could have a main navigation link directly to your object(s), for example. In short, you can build highly custom admin interfaces much quicker and with much less code.

Remove from Data Manager homepage

To allow an object to use Data Manager without appearing in the Data Manager homepage listing, use the @datamanagerEnabled true annotation and not the @datamanagerGroup annotation. For example:

// /application/preside-objects/blog.cfc
/**
 * @datamanagerEnabled true
 *
 */
component {
    // ...
}

Example: Add to the admin left-hand menu

Tip

See Modifying the administrator left hand menu for a full guide to customizing the left-hand menu/navigation.

In your application or extension's Config.cfc file, modify the settings.adminSideBarItems to add a new entry for your object. For example:

settings.adminSideBarItems.append( "blog" );

Then, create a corresponding view at /views/admin/layout/sidebar/blog.cfm. For example:

// /views/admin/layout/sidebar/blog.cfm
hasPermission = hasCmsPermission(
	  permissionKey = "read"
	, context       = "datamanager"
	, contextKeys   = [ "blog" ]
);
if ( hasPermission ) {
    Echo( renderView(
          view = "/admin/layout/sidebar/_menuItem"
        , args = {
              active  = ReFindNoCase( "^admin\.datamanager", event.getCurrentEvent() ) && ( prc.objectName ?: "" ) == "blog"
            , link    = event.buildAdminLink( objectName="blog" )
            , gotoKey = "b"
            , icon    = "fa-comments"
            , title   = translateResource( 'preside-objects.blog:menu.title' )
          }
    ) );
}

Modify the breadcrumb

By default, your object will get breadcrumbs that start with a link to the Data Manager homepage. Use the breadcrumb customizations to modify this:

For example:

// /application/handlers/admin/datamanager/blog.cfc

component {

	private void function rootBreadcrumb() {
		// Deliberately do nothing so as to remove the root
		// 'Data manager' breadcrumb just for the 'blog' object.

		// We could, instead, call event.addAdminBreadCrumb( title=title, link=link )
		// to provide an alternative root breadcrumb
	}

}

Modify core default page titles and other layout changes

A really useful customization is the preLayoutRender customization. This fires before the full admin page layout is rendered and allows you to make adjustments after all the handler logic has run. For example:

// /application/handlers/admin/datamanager/blog.cfc

component {

    private void function preLayoutRender( event, rc, prc, args={} ) {
        prc.pageTitle = translateResource(
              uri          = "preside-objects.blog:#args.action#.page.title"
            , defaultValue = prc.pageTitle ?: ""
        );
        prc.pageSubTitle = translateResource(
              uri          = "preside-objects.blog:#args.action#.page.subtitle"
            , defaultValue = prc.pageSubTitle ?: ""
        );
        prc.pageIcon = "fa-comments";
    }

    private void function preLayoutRenderForEditRecord( event, rc, prc, args={} ) {
        prc.pageTitle = translateResource(
              uri  = "preside-objects.blog:editRecord.page.title"
            , data = [ prc.recordLabel ?: "" ]
        );

        // modify the title of the last breadcrumb
        var breadCrumbs = event.getAdminBreadCrumbs();
        breadCrumbs[ breadCrumbs.len() ].title = prc.pageTitle;
    }
}