Skip to content

Custom Workflows

Simon Yohannes edited this page Jan 22, 2023 · 1 revision

custom workflows are scripted using javascript. as with all backoffice extensions, you start off in the puckweb/Areas/puck/Views/Shared/includes.cshtml file.

here's the skeleton of a workflow:

<script>
    var workflows = [];
    workflows["Homepage"] = {
        comment: function (workflowItem, userObject,startingState,currentState) {
            console.log(workflowItem,userObject,startingState,currentState);
            return "Please enter a comment";
        },
        handler: function (isPublished, workflowItem, userObject, startingState, currentState, commentObject, services) {
            console.log(isPublished,workflowItem,userObject,startingState,currentState,commentObject,services);
            
            services.add("needs-approval","newly created, please approve","Approver","");
            
            return false;
        }
    };
</script>

so as you can see, you specify a workflows array which you then add entries to, for each Type of ViewModel you want workflows for. in the example above, we've added an entry for the Homepage ViewModel and you can see that the workflow object contains two functions, comment and handler.

comment decides whether or not a user needs to add a comment on saving/publishing a ViewModel. this comment is then passed onto the second function, the handler. to decide whether or not a comment is needed, you are passed in the following parameters:

workflowItem:{
   Id:int,
   Status:string,
   Message:string,
   Group:string,
   Complete:bool,
   CompleteDate:datetime
   Timestamp:datetime(created),
   ContentId:string(guid),
   Variant:string,
   LockedBy:string(username),
   LockedUntil:datetime
}
userObject:{
   userName:string,
   userRoles:array<string>,
   userGroups:array<string>
}

as well as the above parameters workflowItem and userObject, the comment function is also passed in the beginning state of the content being edited and the state at the point of saving in the form of FormData objects. you can use the formdata.Get("NodeName") method to get specific values from the content being edited. all of these parameters are passed in to the comment function to help you decide whether or not you want to show a comment entry box after the user hits the save/publish buttons. if you don't want the user to enter a comment, return undefined from the comment function, otherwise, return the string that will be used on the comment entry modal as the title.

the second function, handler, is where you can add workflow entries. starting with the parameters - you have the current workflowItem object, which has been detailed already and the userObject, also detailed above. you also have isPublished to let you know if a user is saving or publishing, startingState which is a FormData object with the state of the content being edited when first loaded and currentState which is another FormData object with the state of the content being edited at the point of saving. you also get passed in a user entered comment if available (in a commentObject that includes the comment string and any user mentions) and a services object which will be detailed below:

services:{
   add:function(status:string,message:string,userGroup:string,assignees:string(csv of usernames),
   complete:function(),
   msg:function(state:true(for positive messages - will display green)/false(for errors - will display red)/undefined(for neutral messages - will display grey),message:string)
}

as you can see above, the services object has an add function which allows you to add new workflow items, all arguments are required apart from assignees, which is optional. you only need to use the complete function (which has no arguments) on the last step of your workflow once everything is complete. moving from one step of your workflow to another you only need to use add as this will automatically complete any previous step. returning true from this function will cancel saving so in that event you may want to display a message and that is what the msg function is for.

users will be notified in the backoffice when you add a workflow item belonging to a User Group that they are in or assigned specifically to them using the assignees argument.

examples

how i see this being used is that you use all the information passed into the handler to decide what step of the workflow you're moving to next. for example, with the information passed in, i can know that the current workflow step status is needs-approval and that the current user editing the content is in the Approver group and i can then complete the workflow. or i can tell that the current step status is needs-approval and the person currently trying to edit the content is not in the Approver group and i can cancel the save event and show a user friendly message.

here's an example workflow:

<script>
var workflows = [];
    workflows["Homepage"] = {
        comment: function (workflowItem, userObject,startingState,currentState) {
            return undefined;
        },
        handler: function (isPublished, workflowItem, userObject, startingState, currentState, commentObject, services) {
            
            if (!workflowItem && userObject.userGroups.includes("Creator")) {
                services.add("needs-approval", "newly created, please approve", "Approver", "");
            } else if (workflowItem && workflowItem.Status == "needs-approval" && userObject.userGroups.includes("Approver")) {
                services.add("approved-for-publishing", "this can now be published, after final checks", "Publisher", "");
            } else if (workflowItem && !workflowItem.Complete && workflowItem.Status=="approved-for-publishing" && userObject.userGroups.includes("Publisher")) {
                services.complete();
            }

            return false;
        }
    };
</script>

as you can see above, the comment handler returns undefined since we don't want any comments being entered. then, there's the first step where we check that the current workflowItem is undefined, meaning that this is likely newly created content and we create our first workflow item with a status of needs-approval. we then move onto the approved state with a status of approved-for-publishing and finally we complete the workflow.

if a particular user is not allowed to modify the content in the current step of the workflow, perhaps because they are not in the correct User Group, you can cancel the save event by returning true and show a message:

<script>
var workflows = [];
    workflows["Homepage"] = {
        comment: function (workflowItem, userObject,startingState,currentState) {
            return undefined;
        },
        handler: function (isPublished, workflowItem, userObject, startingState, currentState, commentObject, services) {
            
            if (workflowItem && workflowItem.Status == "needs-approval" && userObject.userGroups.includes("Approver")) {
                services.add("approved-for-publishing", "this can now be published, after final checks", "Publisher", "");
            } else if (workflowItem && workflowItem.Status == "needs-approval" && !userObject.userGroups.includes("Approver")) {
                services.msg(false,"you are not in the Approver group and cannot modify this content");
                return true;
            } 

            return false;
        }
    };
</script>

in the example above, if the current workflow step/status is needs-approval but the user currently editing the content is not in the Approver User Group, you display an error using services.msg and return true to cancel the save event.

more advanced use cases

what if a user requires more than one save to complete a workflow? an example would be a copy editor who wants to see how the copy/text fits on a given page - they may edit a rich text editor field and save to do a preview multiple times.

in this instance, you may want to add a property to your ViewModel, a boolean flag which indicates that they are finished editing, you can add the Property to a specific tab on the edit screen by using the standard MVC Display attribute and specifying the GroupName option:

public class Homepage:BaseModel
{
[Display(GroupName = "Workflow")]
public bool ChecksComplete { get; set; }
}

you can then check the value of this field in the handler function using the currentState property and decide whether or not to move to the next step in your workflow.