From dd56566d8ae767e7a5a39c71bb0e718bd0f02647 Mon Sep 17 00:00:00 2001 From: Marcus Nyeholt Date: Fri, 5 Dec 2014 12:27:59 +1100 Subject: [PATCH] API Support for multiple workflow definitions * As well as the default worklfow definition, additional workflow definitions can be bound to an object for selection by users --- code/dataobjects/WorkflowDefinition.php | 7 +- code/dataobjects/WorkflowInstance.php | 7 +- code/extensions/AdvancedWorkflowExtension.php | 9 +- code/extensions/WorkflowApplicable.php | 105 ++++++++++++------ code/services/WorkflowService.php | 65 ++++++++++- javascript/advanced-workflow-cms.js | 38 +++++++ javascript/advancedworkflow-cms.js | 20 ---- 7 files changed, 185 insertions(+), 66 deletions(-) create mode 100644 javascript/advanced-workflow-cms.js delete mode 100644 javascript/advancedworkflow-cms.js diff --git a/code/dataobjects/WorkflowDefinition.php b/code/dataobjects/WorkflowDefinition.php index 54d0403c..da64979c 100644 --- a/code/dataobjects/WorkflowDefinition.php +++ b/code/dataobjects/WorkflowDefinition.php @@ -23,7 +23,8 @@ class WorkflowDefinition extends DataObject { 'Template' => 'Varchar', 'TemplateVersion' => 'Varchar', 'RemindDays' => 'Int', - 'Sort' => 'Int' + 'Sort' => 'Int', + 'InitialActionButtonText' => 'Varchar' ); private static $default_sort = 'Sort'; @@ -173,6 +174,10 @@ public function getCMSFields() { $fields->addFieldToTab('Root.Main', new TextField('Title', $this->fieldLabel('Title'))); $fields->addFieldToTab('Root.Main', new TextareaField('Description', $this->fieldLabel('Description'))); + $fields->addFieldToTab('Root.Main', TextField::create( + 'InitialActionButtonText', + _t('WorkflowDefinition.INITIAL_ACTION_BUTTON_TEXT', 'Initial Action Button Text') + )); if($this->ID) { $fields->addFieldToTab('Root.Main', new CheckboxSetField('Users', _t('WorkflowDefinition.USERS', 'Users'), $cmsUsers)); $fields->addFieldToTab('Root.Main', new TreeMultiselectField('Groups', _t('WorkflowDefinition.GROUPS', 'Groups'), 'Group')); diff --git a/code/dataobjects/WorkflowInstance.php b/code/dataobjects/WorkflowInstance.php index 5bf0fb13..8f870202 100644 --- a/code/dataobjects/WorkflowInstance.php +++ b/code/dataobjects/WorkflowInstance.php @@ -441,7 +441,7 @@ protected function userHasAccess($member) { $inWorkflowGroupOrUserTables = ($member->inGroups($this->Groups()) || $this->Users()->find('ID', $member->ID)); // This method is used in more than just the ModelAdmin. Check for the current controller to determine where canView() expectations differ if($this->getTarget() && Controller::curr()->getAction() == 'index' && !$inWorkflowGroupOrUserTables) { - if($this->getVersionedConnection($this->getTarget()->ID,$member->ID,$this->DefinitionID)) { + if($this->getVersionedConnection($this->getTarget()->ID, $member->ID)) { return true; } return false; @@ -608,13 +608,12 @@ public function doFrontEndAction(array $data, Form $form, SS_HTTPRequest $reques * * @param number $recordID * @param number $userID - * @param number $definitionID * @param number $wasPublished * @return boolean */ - public function getVersionedConnection($recordID,$userID,$definitionID,$wasPublished=0) { + public function getVersionedConnection($recordID, $userID, $wasPublished = 0) { // Turn this into an array and run through implode() - $filter = "\"AuthorID\" = '".$userID."' AND \"RecordID\" = '".$recordID."' AND \"WorkflowDefinitionID\" = '".$definitionID."' AND \"WasPublished\" = '".$wasPublished."'"; + $filter = "RecordID = {$recordID} AND AuthorID = {$userID} AND WasPublished = {$wasPublished}"; $query = new SQLQuery(); $query->setFrom('"SiteTree_versions"')->setSelect('COUNT("ID")')->setWhere($filter); $query->firstRow(); diff --git a/code/extensions/AdvancedWorkflowExtension.php b/code/extensions/AdvancedWorkflowExtension.php index 14ab60e0..54586e13 100644 --- a/code/extensions/AdvancedWorkflowExtension.php +++ b/code/extensions/AdvancedWorkflowExtension.php @@ -11,10 +11,12 @@ class AdvancedWorkflowExtension extends LeftAndMainExtension { private static $allowed_actions = array( 'updateworkflow', + 'startworkflow' ); public function startworkflow($data, $form, $request) { $item = $form->getRecord(); + $workflowID = isset($data['TriggeredWorkflowID']) ? intval($data['TriggeredWorkflowID']) : 0; if (!$item || !$item->canEdit()) { return; @@ -24,8 +26,8 @@ public function startworkflow($data, $form, $request) { $this->saveAsDraftWithAction($form, $item); $svc = singleton('WorkflowService'); - $svc->startWorkflow($item); - + $svc->startWorkflow($item, $workflowID); + $negotiator = method_exists($this->owner, 'getResponseNegotiator') ? $this->owner->getResponseNegotiator() : Controller::curr()->getResponseNegotiator(); return $negotiator->respond($this->owner->getRequest()); } @@ -37,8 +39,7 @@ public function startworkflow($data, $form, $request) { * @param Form $form */ public function updateEditForm(Form $form) { - Requirements::javascript('advancedworkflow/javascript/advancedworkflow-cms.js'); - + Requirements::javascript(ADVANCED_WORKFLOW_DIR . '/javascript/advanced-workflow-cms.js'); $svc = singleton('WorkflowService'); $p = $form->getRecord(); $active = $svc->getWorkflowFor($p); diff --git a/code/extensions/WorkflowApplicable.php b/code/extensions/WorkflowApplicable.php index b4e82792..d2052036 100644 --- a/code/extensions/WorkflowApplicable.php +++ b/code/extensions/WorkflowApplicable.php @@ -14,6 +14,10 @@ class WorkflowApplicable extends DataExtension { 'WorkflowDefinition' => 'WorkflowDefinition', ); + private static $many_many = array( + 'AdditionalWorkflowDefinitions' => 'WorkflowDefinition' + ); + private static $dependencies = array( 'workflowService' => '%$WorkflowService', ); @@ -70,31 +74,51 @@ public function updateSettingsFields(FieldList $fields) { public function updateCMSFields(FieldList $fields) { if(!$this->owner->hasMethod('getSettingsFields')) $this->updateFields($fields); + + // Instantiate a hidden form field to pass the triggered workflow definition through, allowing a dynamic form action. + + $fields->push(HiddenField::create( + 'TriggeredWorkflowID' + )); } public function updateFields(FieldList $fields) { if (!$this->owner->ID) { return $fields; } - $effective = $this->workflowService->getDefinitionFor($this->owner); $tab = $fields->fieldByName('Root') ? $fields->findOrMakeTab('Root.Workflow') : $fields; if(Permission::check('APPLY_WORKFLOW')) { $definition = new DropdownField('WorkflowDefinitionID', _t('WorkflowApplicable.DEFINITION', 'Applied Workflow')); - $definition->setSource($this->workflowService->getDefinitions()->map()); + $definitions = $this->workflowService->getDefinitions()->map()->toArray(); + $definition->setSource($definitions); $definition->setEmptyString(_t('WorkflowApplicable.INHERIT', 'Inherit from parent')); - $tab->push($definition); - -// $fields->addFieldToTab($tab, $definition); + + // Allow an optional selection of additional workflow definitions. + + if($this->owner->WorkflowDefinitionID) { + unset($definitions[$this->owner->WorkflowDefinitionID]); + $tab->push($additional = ListboxField::create( + 'AdditionalWorkflowDefinitions', + _t('WorkflowApplicable.ADDITIONAL_WORKFLOW_DEFINITIONS', 'Additional Workflows') + )); + $additional->setSource($definitions); + $additional->setMultiple(true); + } } - $tab->push(new ReadonlyField( - 'EffectiveWorkflow', - _t('WorkflowApplicable.EFFECTIVE_WORKFLOW', 'Effective Workflow'), - $effective ? $effective->Title : _t('WorkflowApplicable.NONE', '(none)') - )); + // Display the effective workflow definition. + + if($effective = $this->getWorkflowInstance()) { + $title = $effective->Definition()->Title; + $tab->push(ReadonlyField::create( + 'EffectiveWorkflow', + _t('WorkflowApplicable.EFFECTIVE_WORKFLOW', 'Effective Workflow'), + $title + )); + } if($this->owner->ID) { $config = new GridFieldConfig_Base(); @@ -112,7 +136,6 @@ public function updateCMSActions(FieldList $actions) { $active = $this->workflowService->getWorkflowFor($this->owner); $c = Controller::curr(); if ($c && $c->hasExtension('AdvancedWorkflowExtension')) { - if ($active) { if ($this->canEditWorkflow()) { $workflowOptions = new Tab( @@ -144,12 +167,43 @@ public function updateCMSActions(FieldList $actions) { // $actions->fieldByName('MajorActions') ? $actions->fieldByName('MajorActions')->push($action) : $actions->push($action); } } else { - $effective = $this->workflowService->getDefinitionFor($this->owner); - if ($effective && $effective->getInitialAction()) { - $action = FormAction::create('startworkflow', $effective->getInitialAction()->Title) - ->setAttribute('data-icon', 'navigation'); - $actions->fieldByName('MajorActions') ? $actions->fieldByName('MajorActions')->push($action) : $actions->push($action); + // Instantiate the workflow definition initial actions. + $definitions = $this->workflowService->getDefinitionsFor($this->owner); + if($definitions) { + $menu = $actions->fieldByName('ActionMenus'); + if(is_null($menu)) { + + // Instantiate a new action menu for any data objects. + + $menu = $this->createActionMenus(); + $actions->push($menu); + } + $tab = Tab::create( + 'AdditionalWorkflows' + ); + $menu->insertBefore($tab, 'MoreOptions'); + $addedFirst = false; + foreach($definitions as $definition) { + if($definition->getInitialAction()) { + $action = FormAction::create( + "startworkflow-{$definition->ID}", + $definition->InitialActionButtonText ? $definition->InitialActionButtonText : $definition->getInitialAction()->Title + )->addExtraClass('start-workflow')->setAttribute('data-workflow', $definition->ID); + + // The first element is the main workflow definition, and will be displayed as a major action. + + if(!$addedFirst) { + $addedFirst = true; + $action->setAttribute('data-icon', 'navigation'); + $majorActions = $actions->fieldByName('MajorActions'); + $majorActions ? $majorActions->push($action) : $actions->push($action); + } else { + $tab->push($action); + } + } + } } + } } } @@ -160,25 +214,6 @@ protected function createActionMenu() { return $rootTabSet; } - public function updateFrontendActions($actions){ - $active = $this->workflowService->getWorkflowFor($this->owner); - - if ($active) { - if ($this->canEditWorkflow()) { - $action = FormAction::create('updateworkflow', _t('WorkflowApplicable.UPDATE_WORKFLOW', 'Update Workflow')); - $actions->fieldByName('MajorActions') ? $actions->fieldByName('MajorActions')->push($action) : $actions->push($action); - } - } else { - $effective = $this->workflowService->getDefinitionFor($this->owner); - if ($effective) { - // we can add an action for starting off the workflow at least - $initial = $effective->getInitialAction(); - $action = FormAction::create('startworkflow', $initial->Title); - $actions->fieldByName('MajorActions') ? $actions->fieldByName('MajorActions')->push($action) : $actions->push($action); - } - } - } - /** * Included in CMS-generated email templates for a NotifyUsersWorkflowAction. * Returns an absolute link to the CMS UI for a Page object diff --git a/code/services/WorkflowService.php b/code/services/WorkflowService.php index 87f9d3ff..f6d7d150 100644 --- a/code/services/WorkflowService.php +++ b/code/services/WorkflowService.php @@ -82,6 +82,55 @@ public function getDefinitionFor(DataObject $dataObject) { return null; } + /** + * Retrieves a workflow definition by ID for a data object. + * + * @param data object + * @param integer + * @return workflow definition + */ + + public function getDefinitionByID($object, $workflowID) { + + // Make sure the correct extensions have been applied to the data object. + + $workflow = null; + if($object->hasExtension('WorkflowApplicable') || $object->hasExtension('FileWorkflowApplicable')) { + + // Validate the workflow ID against the data object. + + if(($object->WorkflowDefinitionID == $workflowID) || ($workflow = $object->AdditionalWorkflowDefinitions()->byID($workflowID))) { + if(is_null($workflow)) { + $workflow = DataObject::get_by_id('WorkflowDefinition', $workflowID); + } + } + } + return $workflow ? $workflow : null; + } + + /** + * Retrieves and collates the workflow definitions for a data object, where the first element will be the main workflow definition. + * + * @param data object + * @return array + */ + + public function getDefinitionsFor($object) { + + // Retrieve the main workflow definition. + + $default = $this->getDefinitionFor($object); + if($default) { + + // Merge the additional workflow definitions. + + return array_merge(array( + $default + ), $object->AdditionalWorkflowDefinitions()->toArray()); + } + return null; + } + /** * Gets the workflow for the given item * @@ -165,13 +214,25 @@ public function executeTransition(DataObject $target, $transitionId) { * * @param DataObject $object */ - public function startWorkflow(DataObject $object) { + public function startWorkflow(DataObject $object, $workflowID = null) { $existing = $this->getWorkflowFor($object); if ($existing) { throw new ExistingWorkflowException(_t('WorkflowService.EXISTING_WORKFLOW_ERROR', "That object already has a workflow running")); } - $definition = $this->getDefinitionFor($object); + $definition = null; + if($workflowID) { + + // Retrieve the workflow definition that has been triggered. + + $definition = $this->getDefinitionByID($object, $workflowID); + } + if(is_null($definition)) { + + // Fall back to the main workflow definition. + + $definition = $this->getDefinitionFor($object); + } if ($definition) { $instance = new WorkflowInstance(); diff --git a/javascript/advanced-workflow-cms.js b/javascript/advanced-workflow-cms.js new file mode 100644 index 00000000..ea1fb163 --- /dev/null +++ b/javascript/advanced-workflow-cms.js @@ -0,0 +1,38 @@ +;(function($) { + $(function() { + + $.entwine('ss', function($) { + + $('.cms-edit-form .Actions #ActionMenus_WorkflowOptions .action').entwine({ + onclick: function(e) { + var transitionId = $(this).attr('data-transitionid'); + var buttonName = $(this).attr('name'); + + buttonName = buttonName.replace(/-\d+/, ''); + $(this).attr('name', buttonName); + + $('input[name=TransitionID]').val(transitionId); + + this._super(e); + } + }); + + $('.cms-edit-form .Actions .action.start-workflow').entwine({ + onmouseup: function(e) { + + // Populate the hidden form field with the selected workflow definition. + + var action = $(this); + $('input[name=TriggeredWorkflowID]').val(action.data('workflow')); + + // Update the element name to exclude the ID, therefore submitting correctly. + + var name = action.attr('name'); + action.attr('name', name.replace(/-\d+/, '')); + this._super(e); + } + }); + }); + + }); +})(jQuery); diff --git a/javascript/advancedworkflow-cms.js b/javascript/advancedworkflow-cms.js deleted file mode 100644 index 47303b5c..00000000 --- a/javascript/advancedworkflow-cms.js +++ /dev/null @@ -1,20 +0,0 @@ - -;(function ($) { - $(function () { - $.entwine('ss', function($) { - $('.cms-edit-form .Actions #ActionMenus_WorkflowOptions .action').entwine({ - onclick: function(e) { - var transitionId = $(this).attr('data-transitionid'); - var buttonName = $(this).attr('name'); - - buttonName = buttonName.replace(/-\d+/, ''); - $(this).attr('name', buttonName); - - $('input[name=TransitionID]').val(transitionId); - - this._super(e); - } - }); - }); - }); -})(jQuery); \ No newline at end of file