As is typical, though, pat-on-the-backs for a job well done generally come bundled with a list of 20 more things that need to be done…yesterday. In our case, management wants us to implement a role-based workflow system that will allow them to more granularly track how inventory products go from initial creation to a final “Approved” status.

This is no problem for us, however. By now, we’ve been around the block a few times with ExtJS 4. Sure, the details vary from task to task, but we’ve found the sweet spot for attacking problems and solving them with code. All that’s left is to implement the skills that we are mastering on a daily basis.

The Requirements

As mentioned, we need to implement a simple workflow process into our inventory management. While the workflow process will not alter the “how” of editing inventory records, it will add a layer of reporting and “sign off” for the various stages. WIth this new functionality, we want to accomplish at least the following:

If a role has permissions to an active status, they should be able to “Reject” it and send it back to the previous step (e.g., an Auditor rejects an inventory item in “In-Review” status, which sends it back to the Content Manager at “Initiated”)

Each workflow action should require a justification entered by the approver

Each inventory record should have a “history” of all workflow actions that have occurred for easy review

Easy enough, right? Let’s do it.

The Data Model

Let’s knock out the data model real quick. In our database, let’s create a new table to store the following data:

Car

LastStatus (the original status)

NextStatus (the new status)

Staff

Notes

Approved (whether the action was an approval [forward] or rejection [backward])

The Approach

Before we plunge head-first into code, let’s talk strategy. While there are a number of ways to accomplish what we want, I think the following will make our addition very unobtrusive to what we’ve already got in place:

Workflow Actions: Since our workflow actions need to occur on each inventory record, we can simply add the workflow actions to our already-existent context menu. That way, our workflow managers can quickly select actions without having to open the entire editing form.

Justification: When the workflow action is enacted, we can use the Ext.MessageBox.confirm() to pop-up a modal window with a multi-line text form for entering the justification text.

Workflow History: Since this will be a list of history records, the most obvious choice is, you guessed it, another grid! We are pro at making these by now, so there’s not much to talk about here.

Workflows.js

Since this workflow management represents something of a new “section” within our application, we should create a new controller for it. Let’s name it Workflows.js. In our current set of requirements, Workflows.js is going to be pretty thin. However, as is likely to happen, our application will inevitably grow, so segregating it now will future-proof our application against a need for a messy refactoring later on.

In our first pass, our Workflows.js has a single method: hasWorkflowPermission. This will allow us to pass in a status and determine whether or not the currently logged in user is in a role that has access to that particular status. In this example, permissions are cumulative, so an Auditor has access to all status beneath the Auditor permission level, the Admin has access to everything below the Admin level, and so on.

With this in place, we can now implement our Inventory grid context menu additions.

First, you’ll notice that we’ve modified how the context menu is built. In previous versions of our app, our menu items were added directly to the items[] config. In this version, however, we are building an array of menu elements dynamically, based on certain conditions. This will allow us to avoid adding every single menu element; instead, we only add the ones that we need based on the user’s permissions AND the current workflow “state” of the Inventory record.

Next, you can see we’re using our new Workflows.js controller method hasWorkflowPermission to determine which workflow actions to add to the menu. We can access this controller from Cars.js in a number of ways, but a super-easy way is via the dynamically generated getter that ExtJS creates for us within the application: getWorkflowsController.

Finally, you’ll notice that for each workflow action we are firing some events (reject, restart, approve). However, we’re firing these events on our Workflows.js controller itself. This is great, because we can maintain separation between what occurs in the interface (e.g., the clicks on the Inventory grid records) and the business logic that needs to occur for workflows.

Now that this is in place, let’s add the listeners and methods in our Workflows.js controller:

There’s a lot going on here, but the most important bits are highlighted in bold.

First, we setup a couple listeners (approve, reject, restart) to match the events we fire in Cars.js when workflow actions are activated. Notice how we’ve placed these listeners in the controller section of our this.listen(). The Ext.app.domain.Controller (controller domain) allows us to listen to events that occur in any of our application’s registered controllers. While we could use a wildcard (“*”) for the selector, we’ve decided to be specific in this example, and used the id of our controller (remember, we may expand later on…easier to be specific now, than to have to refactor later).

Each of our listeners fire specific methods, which all in turn call the handleWorkflowAction method. This method does two main things:

Now you might be wondering why we do a vanilla Ext.Ajax.request(), instead of adding a record to a store, syncing it, and persisting the record in that way. We could certainly do that, but then we’d have to instantiate the store, add data to it, and handle all the aspects related to that. In this scenario, I found it simpler to just handle the creation of workflow records in a more ad-hoc manner.

NOTE: While the approach to saving a record is a bit different here, it hopefully illustrates the flexibility of ExtJS 4 in how you interact with the server. Whether you are proxying the server via a store, or simply interacting with Ext.Ajax, you have a variety of options available…the hardest part is just deciding the best one to use by scenario.

And once the result is returned in our Ajax request’s success, we simply update the Inventory record with new status. Cake.

Workflow History

The last piece we need to implement is the ability to view the workflow history of individual Inventory records. Since we’ve already being hacking on our Inventory context menu, let’s add one more option there and call it “View Workflow History”:

In this case, we’ve decided to ditch creating a custom “window” class for containing our grid. Since we don’t have any particular actions associated with workflow history viewing (e.g. not toolbar buttons, search forms, etc.), we didn’t feel the need to create a whole class just to display our grid.

The important thing to notice here, however, is that when we instantiate our workflow history grid, we are setting a special config called CarID. This will allow us to easily retrieve the ID of the inventory record when we load the workflow history; so instead of querying the database of ALL history records, we can retrieve only those for the selected inventory record.

And finally, per our listener, we will call loadWorkflowHistory before our workflow history grid is rendered:

This is nothing different than what we’ve done before. The only change is that we manipulate the store’s proxy url before loading, allowing us to include the CarID config that we set when instantiating our store’s grid (see above).

Awesome. Put all together, we should get something that looks like this:

Bonus: RowExpander

In the screenshot above, you may have noticed the “+” ticks next to each row in the workflow history grid. These are created by a special plugin (Ext.grid.plugin.RowExpander) that allows an additional row to be added after each row. The additional row supports a special renderer into which any arbitrary HTML can be inserted. The config for this is incredibly simple. Check out this snippet from our workflow history grid:

All we have to do is to add the RowExpander via the grid’s plugins[] config. Within the RowExpander configuration, we use the rowBodyTpl to create a new Ext.XTemplate which will render the new row’s content. The content which we place into the new row can be anything, but in this example, we simply want to render the Notes property from our model.

NOTE: The RowExpander is especially ideal for displaying model fields that may contain a large amount of information that does not lend itself to tabular display. By using an entire row, the RowExpander helps keep your grid nice and compact, but still provides the ability to see larger sections of information without disrupting the layout of your grid.

Wrapping Up

And there we have it…another section in the bag. By this point, we should be finding our legs with ExtJS 4, and our development speed should be rapidly increasing. Since we’re getting more and more comfortable with how all the pieces fit together, we can spend much more time making awesome stuff happen, instead of spinning our wheels figuring out how to make it work. This is when ExtJS 4 really becomes a joy to work with.