From 6c94de9416cecf7461eb05bbac362d0688f3f7ca Mon Sep 17 00:00:00 2001 From: Andrew Short Date: Sun, 6 May 2012 18:42:52 +1000 Subject: [PATCH] Implemented a new editing interface compatible with SS3. --- _config.php | 14 +- code/actions/NotifyUsersWorkflowAction.php | 10 +- code/admin/AdvancedWorkflowAdmin.php | 298 +----------------- code/dataobjects/WorkflowAction.php | 12 +- code/dataobjects/WorkflowDefinition.php | 66 ++-- code/dataobjects/WorkflowTransition.php | 8 +- code/extensions/WorkflowApplicable.php | 75 ++--- code/formfields/WorkflowField.php | 69 ++++ .../WorkflowFieldActionController.php | 72 +++++ .../WorkflowFieldItemController.php | 92 ++++++ .../WorkflowFieldTransitionController.php | 70 ++++ css/AdvancedWorkflowAdmin.css | 16 +- css/WorkflowField.css | 20 ++ images/workflow-menu-icon.png | Bin 0 -> 3507 bytes javascript/AdvancedWorkflowAdmin.js | 219 ------------- javascript/WorkflowField.js | 81 +++++ scss/WorkflowField.scss | 138 ++++++++ templates/AdvancedWorkflowAdmin_left.ss | 15 - templates/AdvancedWorkflowAdmin_right.ss | 11 - templates/WorkflowField.ss | 61 ++++ 20 files changed, 710 insertions(+), 637 deletions(-) create mode 100644 code/formfields/WorkflowField.php create mode 100644 code/formfields/WorkflowFieldActionController.php create mode 100644 code/formfields/WorkflowFieldItemController.php create mode 100644 code/formfields/WorkflowFieldTransitionController.php create mode 100644 css/WorkflowField.css create mode 100644 images/workflow-menu-icon.png delete mode 100644 javascript/AdvancedWorkflowAdmin.js create mode 100644 javascript/WorkflowField.js create mode 100644 scss/WorkflowField.scss delete mode 100644 templates/AdvancedWorkflowAdmin_left.ss delete mode 100644 templates/AdvancedWorkflowAdmin_right.ss create mode 100644 templates/WorkflowField.ss diff --git a/_config.php b/_config.php index 47667554..b7fe7b32 100644 --- a/_config.php +++ b/_config.php @@ -4,12 +4,18 @@ * @package advancedworkflow */ -// Add the following to your config to enable workflow +// Add the following to your config to enable workflow // DataObject::add_extension('SiteTree', 'WorkflowApplicable'); Object::add_extension('Member', 'Hierarchy'); Object::add_extension('LeftAndMain', 'AdvancedWorkflowExtension'); -if(($MODULE_DIR = basename(dirname(__FILE__))) != 'advancedworkflow') { - throw new Exception("The advanced workflow module must be in a directory named 'advancedworkflow', not $MODULE_DIR"); -} \ No newline at end of file +define('ADVANCED_WORKFLOW_DIR', basename(dirname(__FILE__))); + +if(ADVANCED_WORKFLOW_DIR != 'advancedworkflow') { + throw new Exception( + "The advanced workflow module must be in a directory named 'advancedworkflow', not " . ADVANCED_WORKFLOW_DIR + ); +} + +LeftAndMain::require_css(ADVANCED_WORKFLOW_DIR . '/css/AdvancedWorkflowAdmin.css'); diff --git a/code/actions/NotifyUsersWorkflowAction.php b/code/actions/NotifyUsersWorkflowAction.php index c6cdb012..332b0663 100644 --- a/code/actions/NotifyUsersWorkflowAction.php +++ b/code/actions/NotifyUsersWorkflowAction.php @@ -25,12 +25,12 @@ public function getCMSFields() { new LiteralField('NotificationNote', '

' . $this->fieldLabel('NotificationNote') . '

'), new TextField('EmailSubject', $this->fieldLabel('EmailSubject')), new TextField('EmailFrom', $this->fieldLabel('EmailFrom')), - - new TextareaField('EmailTemplate', $this->fieldLabel('EmailTemplate'), 10), + + new TextareaField('EmailTemplate', $this->fieldLabel('EmailTemplate')), new ToggleCompositeField('FormattingHelpContainer', $this->fieldLabel('FormattingHelp'), new LiteralField('FormattingHelp', $this->getFormattingHelp())) )); - + if (class_exists('ListingPage')) { // allow the user to select an existing 'listing template'. The "getItems()" for that template // will be the list of items in the workflow @@ -39,10 +39,10 @@ public function getCMSFields() { if ($templates) { $opts = $templates->map(); } - + $fields->addFieldToTab('Root.Main', new DropdownField('ListingTemplateID', $this->fieldLabel('ListingTemplateID'), $opts, '', null, '(choose)'), 'EmailTemplate'); } - + if ($this->ListingTemplateID) { $fields->removeFieldFromTab('Root.Main', 'EmailTemplate'); } diff --git a/code/admin/AdvancedWorkflowAdmin.php b/code/admin/AdvancedWorkflowAdmin.php index fbfcac3a..5eac1802 100644 --- a/code/admin/AdvancedWorkflowAdmin.php +++ b/code/admin/AdvancedWorkflowAdmin.php @@ -1,300 +1,16 @@ array('record_controller' => 'AdvancedWorkflowAdmin_RecordController'), - 'WorkflowAction' => array('record_controller' => 'AdvancedWorkflowAdmin_RecordController'), - 'WorkflowTransition' => array('record_controller' => 'AdvancedWorkflowAdmin_RecordController') - ); - - public static $allowed_actions = array( - 'tree', - 'sort', - 'CreateDefinitionForm', - 'CreateActionForm', - 'CreateTransitionForm' - ); - - /** - * @return string - */ - public function tree($request) { - $data = array(); - $class = $request->getVar('class'); - $id = $request->getVar('id'); - - if($id == 0) { - $items = singleton('WorkflowService')->getDefinitions(); - $type = 'WorkflowDefinition'; - } elseif($class == 'WorkflowDefinition') { - $items = DataObject::get('WorkflowAction', '"WorkflowDefID" = ' . (int) $id); - $type = 'WorkflowAction'; - } else { - $items = DataObject::get('WorkflowTransition', '"ActionID" = ' . (int) $id); - $type = 'WorkflowTransition'; - } - - if($items) foreach($items as $item) { - $new = array( - 'data' => array( - 'title' => $item->Title, - 'attr' => array('href' => $this->Link("$type/{$item->ID}/edit")), - 'icon' => $item->stat('icon')), - 'attr' => array( - 'id' => "{$type}_{$item->ID}", - 'title' => Convert::raw2att($item->Title), - 'data-id' => $item->ID, - 'data-type' => $type, - 'data-class' => $item->class) - ); - - if($item->numChildren() > 0) { - $new['state'] = 'closed'; - } - - $data[] = $new; - } - - return Convert::raw2json($data); - } - - /** - * @return string - */ - public function sort($request) { - $service = singleton('WorkflowService'); - $type = $request->postVar('type'); - $order = $request->postVar('ids'); - $parentId = $request->postVar('parent_id'); - - switch($type) { - case 'WorkflowDefinition': - $current = $service->getDefinitions(); - break; - case 'WorkflowAction': - $current = DataObject::get('WorkflowAction', sprintf('"WorkflowDefID" = %d', $parentId)); - break; - case 'WorkflowTransition': - $current = DataObject::get('WorkflowTransition', sprintf('"ActionID" = %d', $parentId)); - break; - default: - return $this->httpError(400, _t('AdvancedWorkflowAdmin.INVALIDSORTTYPE', 'Invalid sort type.')); - } - - if(!$order || count($order) != count($current)) { - return new SS_HTTPResponse( - null, 400, _t('AdvancedWorkflowAdmin.INVALIDSORT', 'An invalid sort order was specified.') - ); - } - - $service->reorder($current, $order); - - return new SS_HTTPResponse( - null, 200, _t('AdvancedWorkflowAdmin.SORTORDERSAVED', 'The sort order has been saved.') - ); - } - - /** - * @return Form - */ - public function CreateDefinitionForm() { - return new Form( - $this, - 'CreateDefinitionForm', - new FieldSet( - $this->getClassCreationField('WorkflowDefinition')), - new FieldSet( - new FormAction('doCreateWorkflowItem', _t('AdvancedWorkflowAdmin.CREATE', 'Create'))) - ); - } - - /** - * @return Form - */ - public function CreateActionForm() { - return new Form( - $this, - 'CreateActionForm', - new FieldSet( - $this->getClassCreationField('WorkflowAction', false), - new HiddenField('ParentID')), - new FieldSet( - new FormAction('doCreateWorkflowItem', _t('AdvancedWorkflowAdmin.CREATE', 'Create'))) - ); - } - - /** - * @return Form - */ - public function CreateTransitionForm() { - return new Form( - $this, - 'CreateTransitionForm', - new FieldSet( - $this->getClassCreationField('WorkflowTransition'), - new HiddenField('ParentID')), - new FieldSet( - new FormAction('doCreateWorkflowItem', _t('AdvancedWorkflowAdmin.CREATE', 'Create'))) - ); - } - - /** - * Creates a workflow item - a definition, action, transition or any subclasses - * of these. - * - * @param array $data - * @param Form $form - * @return string - */ - public function doCreateWorkflowItem($data, $form) { - // assume the form name is in the form CreateTypeForm - $data = $form->getData(); - $type = 'Workflow' . substr($form->Name(), 6, -4); - $allowSelf = ($type != 'WorkflowAction'); - - // determine the class to create - if it is manually specified then use that, - // falling back to creating an object of the root type if allowed. - if(isset($data['Class']) && class_exists($data['Class'])) { - $class = $data['Class']; - $valid = is_subclass_of($class, $type) || ($allowSelf && $class == $type); - - if(!$valid) return new SS_HTTPResponse( - null, 400, _t('AdvancedWorkflowAdmin.INVALIDITEM', 'An invalid workflow item was specified.') - ); - } else { - $class = $type; - - if(!$allowSelf) return new SS_HTTPResponse( - null, 400, _t('AdvancedWorkflowAdmin.MUSTSPECIFYITEM', 'You must specify a workflow item to create.') - ); - } - - // check that workflow actions and transitions have valid parent id values. - if($type != 'WorkflowDefinition') { - $parentId = $data['ParentID']; - $parentClass = ($type == 'WorkflowAction') ? 'WorkflowDefinition' : 'WorkflowAction'; + public static $menu_title = 'Workflows'; + public static $menu_priority = -1; + public static $url_segment = 'workflows'; - if(!is_numeric($parentId) || !DataObject::get_by_id($parentClass, $parentId)) { - return new SS_HTTPResponse( - null, 400, _t('AdvancedWorkflowAdmin.INVALIDPARENT', 'An invalid parent was specified.') - ); - } - } - - // if an add form can be returned without writing a new rcord to the database, - // then just do that - if(array_key_exists($class, $this->getManagedModels())) { - $form = $this->$type()->AddForm(); - $title = singleton($type)->singular_name(); - - if($type == 'WorkflowTransition') { - $form->dataFieldByName('ActionID')->setValue($parentId); - } - } else { - $record = new $class; - $record->Title = sprintf(_t('AdvancedWorkflowAdmin.NEWITEM', 'New %s'), $record->singular_name()); - - if($type == 'WorkflowAction') { - $record->WorkflowDefID = $parentId; - } elseif($type == 'WorkflowTransition') { - $record->ActionID = $parentId; - } - - $record->write(); - - $control = $this->getRecordControllerClass('WorkflowDefinition'); - $control = new $control($this->$type(), null, $record->ID); - $form = $control->EditForm(); - $title = $record->singular_name(); - } - - return new SS_HTTPResponse( - $this->isAjax() ? $form->forAjaxTemplate() : $form->forTemplate(), 200, - sprintf(_t('AdvancedWorkflowAdmin.CREATEITEM', 'Fill out this form to create a "%s".'), $title) - ); - } - - /** - * Returns a dropdown createable classes which all have a common parent class, - * or a label field if only one option is available. - * - * @param string $class The parent class. - * @param bool $includeSelf Include the parent class itself. - * @return DropdownField|LabelField - */ - protected function getClassCreationField($class, $includeSelf = true) { - $classes = ClassInfo::subclassesFor($class); - $createable = array(); - - if(!$includeSelf) { - array_shift($classes); - } - - foreach($classes as $class) { - if(singleton($class)->canCreate()) $createable[$class] = singleton($class)->singular_name(); - } - - if(count($classes) == 1) { - return new LabelField('Class', current($createable)); - } - - return new DropdownField( - 'Class', '', $createable, '', null, ($includeSelf ? false : _t('AdvancedWorkflowAdmin.SELECT', '(select)')) - ); - } + public static $managed_models = 'WorkflowDefinition'; + public static $model_importers = array(); } - -/** - * A record controller that hides the "Back" button, and shows a message on deletion rather than redirecting to - * the search form. - * - * @package advancedworkflow - * @subpackage admin - */ -class AdvancedWorkflowAdmin_RecordController extends ModelAdmin_RecordController { - - /** - * @return Form - */ - public function EditForm() { - $form = parent::EditForm(); - $form->Actions()->removeByName('action_goBack'); - - return $form; - } - - /** - * @return string - */ - public function doDelete() { - if($this->currentRecord->canDelete()) { - $this->currentRecord->delete(); - - $form = new Form( - $this, - 'EditForm', - new FieldSet(new LiteralField( - 'RecordDeleted', - '

' . _t('AdvancedWorkflowAdmin.RECORDDELETED', 'This record has been deleted.') . '

' - )), - new FieldSet() - ); - return $form->forTemplate(); - } else { - return $this->redirectBack(); - } - } - -} \ No newline at end of file diff --git a/code/dataobjects/WorkflowAction.php b/code/dataobjects/WorkflowAction.php index 1b6167b2..c513379f 100644 --- a/code/dataobjects/WorkflowAction.php +++ b/code/dataobjects/WorkflowAction.php @@ -124,16 +124,22 @@ public function numChildren() { } public function getCMSFields() { - $fields = new FieldSet(new TabSet('Root')); + $fields = new FieldList(new TabSet('Root')); $fields->addFieldToTab('Root.Main', new TextField('Title', _t('WorkflowAction.TITLE', 'Title'))); $label = _t('WorkflowAction.ALLOW_EDITING', 'Allow editing during this step?'); $fields->addFieldToTab('Root.Main', new DropdownField('AllowEditing', $label, $this->dbObject('AllowEditing')->enumValues(), 'No')); return $fields; } - + + public function getValidator() { + return new RequiredFields('Title'); + } + public function summaryFields() { return array('Title' => 'Title', 'Transitions' => 'Transitions'); } - + public function Icon() { + return $this->stat('icon'); + } } \ No newline at end of file diff --git a/code/dataobjects/WorkflowDefinition.php b/code/dataobjects/WorkflowDefinition.php index 4a4819c7..6bdb91d1 100644 --- a/code/dataobjects/WorkflowDefinition.php +++ b/code/dataobjects/WorkflowDefinition.php @@ -76,7 +76,7 @@ public function numChildren() { /** */ public function getCMSFields() { - $fields = new FieldSet(new TabSet('Root')); + $fields = new FieldList(new TabSet('Root')); $fields->addFieldToTab('Root.Main', new TextField('Title', _t('WorkflowDefinition.TITLE', 'Title'))); $fields->addFieldToTab('Root.Main', new TextareaField('Description', _t('WorkflowDefinition.DESCRIPTION', 'Description'))); @@ -95,49 +95,41 @@ public function getCMSFields() { )); } - if ($this->ID && Permission::check('VIEW_ACTIVE_WORKFLOWS')) { - $fields->addFieldToTab('Root.ActiveInstances', $active = new ComplexTableField( - $this, - 'Instances', - 'WorkflowInstance', - array( - 'Title' => 'Title', - 'Target.Title' => 'Target Title', - 'WorkflowStatus' => 'Status', - 'CurrentAction.Title' => 'Current Action', - 'LastEdited' => 'Last Actioned' - ), - 'getInstanceManagementFields', - '"WorkflowStatus" IN (\'Active\', \'Paused\')', - '"LastEdited" DESC' + if($this->ID) { + $fields->addFieldToTab('Root.Main', new WorkflowField( + 'Workflow', _t('WorkflowDefinition.WORKFLOW', 'Workflow'), $this )); + } else { + $message = _t( + 'WorkflowDefinition.ADDAFTERSAVING', + 'You can add workflow steps after you save for the first time.' + ); + $fields->addFieldToTab('Root.Main', new LiteralField( + 'AddAfterSaving', "

$message

" + )); + } - if (Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) { - $active->setPermissions(array('show', 'edit')); - } else { - $active->setPermissions(array('show')); + if($this->ID && Permission::check('VIEW_ACTIVE_WORKFLOWS')) { + $active = $this->Instances()->filter(array( + 'WorkflowStatus' => array('Active', 'Paused') + )); + $active = new GridField('Active', 'Active Workflow Instances', $active); + + if(!Permission::check('REASSIGN_ACTIVE_WORKFLOWS')) { + $active->removeComponentsByType('GridFieldEditButton'); + $active->removeComponentsByType('GridFieldDeleteAction'); } - - $fields->addFieldToTab('Root.Completed', $complete = new ComplexTableField( - $this, - 'CompletedInstances', - 'WorkflowInstance', - array( - 'Title' => 'Title', - 'Target.Title' => 'Target Title', - 'WorkflowStatus' => 'Status', - 'CurrentAction.Title' => 'Current Action', - 'LastEdited' => 'Last Actioned' - ), - 'getActionsSummaryFields', - '"WorkflowStatus" IN (\'Complete\', \'Cancelled\')', - '"LastEdited" DESC' + + $completed = $this->Instances()->filter(array( + 'WorkflowStatus' => array('Complete', 'Cancelled') )); + $completed = new GridField('Completed', 'Completed Workflow Instances', $completed); - $complete->setPermissions(array('show')); + $fields->addFieldToTab('Root.Active', $active); + $fields->addFieldToTab('Root.Completed', $completed); } return $fields; } -} \ No newline at end of file +} diff --git a/code/dataobjects/WorkflowTransition.php b/code/dataobjects/WorkflowTransition.php index f5918d40..8df54756 100644 --- a/code/dataobjects/WorkflowTransition.php +++ b/code/dataobjects/WorkflowTransition.php @@ -72,11 +72,11 @@ public function validate() { /* CMS FUNCTIONS */ public function getCMSFields() { - $fields = new FieldSet(new TabSet('Root')); + $fields = new FieldList(new TabSet('Root')); $fields->addFieldToTab('Root.Main', new TextField('Title', _t('WorkflowAction.TITLE', 'Title'))); $filter = ''; - + $reqParent = isset($_REQUEST['ParentID']) ? (int) $_REQUEST['ParentID'] : 0; $attachTo = $this->ActionID ? $this->ActionID : $reqParent; @@ -107,6 +107,10 @@ public function getCMSFields() { return $fields; } + public function getValidator() { + return new RequiredFields('Title', 'ActionID', 'NextActionID'); + } + public function numChildren() { return 0; } diff --git a/code/extensions/WorkflowApplicable.php b/code/extensions/WorkflowApplicable.php index 7c9d96c8..e6402c38 100644 --- a/code/extensions/WorkflowApplicable.php +++ b/code/extensions/WorkflowApplicable.php @@ -26,48 +26,42 @@ public function extraStatics($class = null, $extension = null) { ); } - public function updateCMSFields(FieldSet $fields) { - $service = singleton('WorkflowService'); + public function updateSettingsFields(FieldList $fields) { + $this->updateFields($fields); + } - if($effective = $service->getDefinitionFor($this->owner)) { - $effectiveTitle = $effective->Title; - } else { - $effectiveTitle = _t('WorkflowApplicable.NONE', '(none)'); - } + public function updateCMSFields(FieldList $fields) { + if(!$this->owner->hasMethod('getSettingsFields')) $this->updateFields($fields); + } + + public function updateFields(FieldList $fields) { + $service = singleton('WorkflowService'); + $effective = $service->getDefinitionFor($this->owner); + $tab = $fields->fieldByName('Root') ? 'Root.Workflow' : 'BottomRoot.Workflow'; - $allDefinitions = array(_t('WorkflowApplicable.INHERIT', 'Inherit from parent')); + if(Permission::check('APPLY_WORKFLOW')) { + $definition = new DropdownField('WorkflowDefinitionID', _t('WorkflowApplicable.DEFINITION', 'Applied Workflow')); + $definition->setSource($service->getDefinitions()->map()); + $definition->setEmptyString(_t('WorkflowApplicable.INHERIT', 'Inherit from parent')); - if($definitions = $service->getDefinitions()) { - $allDefinitions += $definitions->map(); + $fields->addFieldToTab($tab, $definition); } - - $tab = $fields->fieldByName('Root') ? 'Root.Workflow' : 'BottomRoot.Workflow'; - - $applyWorkflowField = null; - - - $fields->addFieldToTab($tab, new HeaderField('AppliedWorkflowHeader', _t('WorkflowApplicable.APPLIEDWORKFLOW', 'Applied Workflow'))); - - if (Permission::check('APPLY_WORKFLOW')) { - $fields->addFieldToTab($tab, new DropdownField('WorkflowDefinitionID', - _t('WorkflowApplicable.DEFINITION', 'Applied Workflow'), $allDefinitions)); - + + $fields->addFieldToTab($tab, new ReadonlyField( + 'EffectiveWorkflow', + _t('WorkflowApplicable.EFFECTIVE_WORKFLOW', 'Effective Workflow'), + $effective ? $effective->Title : _t('WorkflowApplicable.NONE', '(none)') + )); + + if($this->owner->ID) { + $insts = $this->owner->WorkflowInstances(); + $log = new GridField('WorkflowLog', _t('WorkflowApplicable.WORKFLOWLOG', 'Workflow Log'), $insts); + + $fields->addFieldToTab($tab, $log); } - - $fields->addFieldToTab($tab, new ReadonlyField('EffectiveWorkflow', - _t('WorkflowApplicable.EFFECTIVE_WORKFLOW', 'Effective Workflow'), $effectiveTitle)); - $fields->addFieldToTab($tab, new HeaderField('WorkflowLogHeader', _t('WorkflowApplicable.WORKFLOWLOG', 'Workflow Log'))); - $fields->addFieldToTab($tab, $logTable = new ComplexTableField( - $this->owner, 'WorkflowLog', 'WorkflowInstance', null, 'getActionsSummaryFields', - sprintf('"TargetClass" = \'%s\' AND "TargetID" = %d', $this->owner->class, $this->owner->ID) - )); - - $logTable->setRelationAutoSetting(false); - $logTable->setPermissions(array('show')); - $logTable->setPopupSize(760, 420); } - public function updateCMSActions($actions) { + public function updateCMSActions(FieldList $actions) { $svc = singleton('WorkflowService'); $active = $svc->getWorkflowFor($this->owner); @@ -96,6 +90,13 @@ public function onAfterWrite() { } } + public function WorkflowInstances() { + return DataList::create('WorkflowInstance')->filter(array( + 'TargetClass' => $this->ownerBaseClass, + 'TargetID' => $this->owner->ID + )); + } + /** * Gets the current instance of workflow * @@ -126,13 +127,13 @@ public function canPublish() { if ($effective = singleton('WorkflowService')->getDefinitionFor($this->owner)) { return false; } - + } /** * Can only edit content that's NOT in another person's content changeset */ - public function canEdit() { + public function canEdit($member) { if ($active = $this->getWorkflowInstance()) { return $active->canEditTarget($this->owner); } diff --git a/code/formfields/WorkflowField.php b/code/formfields/WorkflowField.php new file mode 100644 index 00000000..a16368b8 --- /dev/null +++ b/code/formfields/WorkflowField.php @@ -0,0 +1,69 @@ +definition = $definition; + $this->addExtraClass('workflow-field'); + + parent::__construct($name, $title); + } + + public function action() { + return new WorkflowFieldActionController($this, 'action'); + } + + public function transition() { + return new WorkflowFieldTransitionController($this, 'transition'); + } + + public function getTemplate() { + return 'WorkflowField'; + } + + public function FieldHolder($properties = array()) { + Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); + Requirements::javascript(THIRDPARTY_DIR . '/jquery-entwine/dist/jquery.entwine-dist.js'); + Requirements::javascript(ADVANCED_WORKFLOW_DIR . '/javascript/WorkflowField.js'); + Requirements::css(ADVANCED_WORKFLOW_DIR . '/css/WorkflowField.css'); + + return $this->Field($properties); + } + + public function Definition() { + return $this->definition; + } + + public function CreateableActions() { + $list = new ArrayList(); + $classes = ClassInfo::subclassesFor('WorkflowAction'); + + array_shift($classes); + sort($classes); + + foreach($classes as $class) { + $reflect = new ReflectionClass($class); + $can = singleton($class)->canCreate() && !$reflect->isAbstract(); + + if($can) $list->push(new ArrayData(array( + 'Title' => singleton($class)->singular_name(), + 'Class' => $class + ))); + } + + return $list; + } + +} diff --git a/code/formfields/WorkflowFieldActionController.php b/code/formfields/WorkflowFieldActionController.php new file mode 100644 index 00000000..f77e7569 --- /dev/null +++ b/code/formfields/WorkflowFieldActionController.php @@ -0,0 +1,72 @@ + 'handleAdd', + 'item/$ID' => 'handleItem' + ); + + public static $allowed_actions = array( + 'handleAdd', + 'handleItem' + ); + + protected $parent; + protected $name; + + public function __construct($parent, $name) { + $this->parent = $parent; + $this->name = $name; + + parent::__construct(); + } + + public function handleAdd() { + $class = $this->request->param('Class'); + + if(!class_exists($class) || !is_subclass_of($class, 'WorkflowAction')) { + $this->httpError(400); + } + + $reflector = new ReflectionClass($class); + + if($reflector->isAbstract() || !singleton($class)->canCreate()) { + $this->httpError(400); + } + + $record = new $class(); + $record->WorkflowDefID = $this->parent->Definition()->ID; + + return new WorkflowFieldItemController($this, "new/$class", $record); + } + + public function handleItem() { + $id = $this->request->param('ID'); + $defn = $this->parent->Definition(); + $action = $defn->Actions()->byID($id); + + if(!$action) { + $this->httpError(404); + } + + if(!$action->canEdit()) { + $this->httpError(403); + } + + return new WorkflowFieldItemController($this, "item/$id", $action); + } + + public function RootField() { + return $this->parent; + } + + public function Link($action = null) { + return Controller::join_links($this->parent->Link(), $this->name, $action); + } + +} \ No newline at end of file diff --git a/code/formfields/WorkflowFieldItemController.php b/code/formfields/WorkflowFieldItemController.php new file mode 100644 index 00000000..a9e8d2ab --- /dev/null +++ b/code/formfields/WorkflowFieldItemController.php @@ -0,0 +1,92 @@ +parent = $parent; + $this->name = $name; + $this->record = $record; + + parent::__construct(); + } + + public function index() { + return $this->edit(); + } + + public function edit() { + return $this->Form()->forTemplate(); + } + + public function Form() { + $record = $this->record; + $fields = $record->getCMSFields(); + $validator = $record->hasMethod('getValidator') ? $record->getValidator() : null; + + $save = FormAction::create('doSave', 'Save'); + $save->addExtraClass('ss-ui-button ss-ui-action-constructive') + ->setAttribute('data-icon', 'accept') + ->setUseButtonTag(true); + + $form = new Form($this, 'Form', $fields, new FieldList($save), $validator); + $form->loadDataFrom($record); + return $form; + } + + public function doSave($data, $form) { + $record = $form->getRecord(); + + if(!$record->canEdit()) { + $this->httpError(403); + } + + if(!$record->isInDb()) { + $record->write(); + } + + $form->saveInto($record); + $record->write(); + + return $this->RootField()->forTemplate(); + } + + public function delete($request) { + if(!SecurityToken::inst()->checkRequest($request)) { + $this->httpError(400); + } + + if(!$request->isPOST()) { + $this->httpError(400); + } + + if(!$this->record->canDelete()) { + $this->httpError(403); + } + + $this->record->delete(); + return $this->RootField()->forTemplate(); + } + + public function RootField() { + return $this->parent->RootField(); + } + + public function Link($action = null) { + return Controller::join_links($this->parent->Link(), $this->name, $action); + } + +} \ No newline at end of file diff --git a/code/formfields/WorkflowFieldTransitionController.php b/code/formfields/WorkflowFieldTransitionController.php new file mode 100644 index 00000000..a77444fa --- /dev/null +++ b/code/formfields/WorkflowFieldTransitionController.php @@ -0,0 +1,70 @@ + 'handleAdd', + 'item/$ID!' => 'handleItem' + ); + + public static $allowed_actions = array( + 'handleAdd', + 'handleItem' + ); + + protected $parent; + protected $name; + + public function __construct($parent, $name) { + $this->parent = $parent; + $this->name = $name; + + parent::__construct(); + } + + public function handleAdd() { + $parent = $this->request->param('ParentID'); + $action = WorkflowAction::get()->byID($this->request->param('ParentID')); + + if(!$action || $action->WorkflowDefID != $this->RootField()->Definition()->ID) { + $this->httpError(404); + } + + if(!singleton('WorkflowTransition')->canCreate()) { + $this->httpError(403); + } + + $transition = new WorkflowTransition(); + $transition->ActionID = $action->ID; + + return new WorkflowFieldItemController($this, "new/$parent", $transition); + } + + public function handleItem() { + $id = $this->request->param('ID'); + $trans = WorkflowTransition::get()->byID($id); + + if(!$trans || $trans->Action()->WorkflowDefID != $this->RootField()->Definition()->ID) { + $this->httpError(404); + } + + if(!$trans->canEdit()) { + $this->httpError(403); + } + + return new WorkflowFieldItemController($this, "item/$id", $trans); + } + + public function RootField() { + return $this->parent; + } + + public function Link($action = null) { + return Controller::join_links($this->parent->Link(), $this->name, $action); + } + +} \ No newline at end of file diff --git a/css/AdvancedWorkflowAdmin.css b/css/AdvancedWorkflowAdmin.css index edbcc78d..8a64ea5f 100644 --- a/css/AdvancedWorkflowAdmin.css +++ b/css/AdvancedWorkflowAdmin.css @@ -1,13 +1,3 @@ -#left { background: #FFF; } -#left #ToggleForms { height: 24px; } -#ToggleForms, #Form_CreateDefinitionForm { background: #EEE; border-bottom: 1px solid #CCC; padding: 5px; } -#left form { overflow: auto; } -#left fieldset { float: left; } -#left .Actions { float: right; } -#left label { font-size: 12px; line-height: 24px; } -#left .dropdown .middleColumn { padding-top: 2px; } -#left select { font-size: 12px; width: 185px !important; } -#left .jstree { margin-top: 5px; } -#left .jstree-focused { background: none; } -#left .jstree-clicked, #left .jstree-hovered { border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px; } -#Form_CreateActionForm, #Form_CreateTransitionForm { display: none; } \ No newline at end of file +.icon.icon-16.icon-advancedworkflowadmin { + background-image: url(../images/workflow-menu-icon.png); +} \ No newline at end of file diff --git a/css/WorkflowField.css b/css/WorkflowField.css new file mode 100644 index 00000000..8f333660 --- /dev/null +++ b/css/WorkflowField.css @@ -0,0 +1,20 @@ +.workflow-field .workflow-field-header { background: #95a5ab; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 3px 3px 0 0; } +.workflow-field .workflow-field-header h3 { background: #7f9198; border-bottom: 1px solid rgba(0, 0, 0, 0.1); color: #FFF; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); margin: 0; padding: 8px; } +.workflow-field .workflow-field-create { padding: 4px 8px; } +.workflow-field .workflow-field-create .chzn-container { float: left; margin-right: 4px; width: 250px !important; } +.workflow-field .workflow-field-create .chzn-container .chzn-single { border-color: rgba(0, 0, 0, 0.2); border-radius: 3px; box-shadow: none; } +.workflow-field .workflow-field-create .chzn-container .chzn-single-with-drop { border-radius: 3px 3px 0 0; } +.workflow-field .workflow-field-create .ss-ui-button { border-color: rgba(0, 0, 0, 0.2); } +.workflow-field .workflow-field-actions { background: #FFF; border: 1px solid rgba(0, 0, 0, 0.2); border-top: none; border-radius: 0 0 3px 3px; padding: 8px; position: relative; } +.workflow-field .workflow-field-actions .workflow-field-action { margin-bottom: 4px; } +.workflow-field .workflow-field-actions .workflow-field-action:last-child { margin-bottom: 0; } +.workflow-field .workflow-field-actions .workflow-field-action-header { background: #d7dde0; border: 1px solid #95a5ab; border-left: none; overflow: auto; padding: 2px 8px 2px 0; } +.workflow-field .workflow-field-actions .workflow-field-action-header .workflow-field-action-icon, .workflow-field .workflow-field-actions .workflow-field-action-header .workflow-field-action-drag { display: block; float: left; } +.workflow-field .workflow-field-actions .workflow-field-action-header .workflow-field-action-drag { background: #95a5ab; cursor: move; margin: -2px 0; width: 4px; height: 33px; } +.workflow-field .workflow-field-actions .workflow-field-action-header .workflow-field-action-icon { width: 16px; height: 16px; margin: 7px 8px 0 8px; } +.workflow-field .workflow-field-actions .workflow-field-action-header h4 { float: left; margin: 7px 8px 0 0; min-width: 360px; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4); } +.workflow-field .workflow-field-actions .workflow-field-action-transitions li { background: #f8f8f8; border: 1px solid #d7dde0; border-left: none; border-top: none; overflow: auto; padding: 4px 4px 4px 0; } +.workflow-field .workflow-field-actions .workflow-field-action-transitions li .workflow-field-action-drag { background: #d7dde0; cursor: move; float: left; margin: -4px 0; width: 4px; height: 24px; } +.workflow-field .workflow-field-actions .workflow-field-action-transitions li .workflow-field-transition-title { float: left; min-width: 390px; overflow: auto; } +.workflow-field .workflow-field-actions .workflow-field-action-transitions li .ui-icon, .workflow-field .workflow-field-actions .workflow-field-action-transitions li span { float: left; } +.workflow-field .workflow-field-loading { background: rgba(0, 0, 0, 0.15) url(../../sapphire/admin/images/spinner.gif) no-repeat center center; border-radius: 0 0 3px 3px; bottom: 0; display: none; left: 0; position: absolute; right: 0; top: 0; z-index: 999; } diff --git a/images/workflow-menu-icon.png b/images/workflow-menu-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c48317fa0585e260df4d09fe80b25ed1aeda2a3d GIT binary patch literal 3507 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 hwzs#zTy)OA2LR(DDt").addClass("ss-ui-dialog").appendTo("body")); + this.setClassSel(this.find(".workflow-field-create-class")); + this.setCreateBtn(this.find(".workflow-field-do-create")); + this.setLoadingInd(this.find(".workflow-field-loading")); + + this.getDialogEl().delegate("button", "click", function() { + $(this).addClass("loading ui-state-disabled").attr("disabled", "disabled"); + }); + + this.getDialogEl().delegate("form", "submit", function() { + $(this).ajaxSubmit(function(response) { + if($(response).is(".workflow-field")) { + self.getDialogEl().empty().dialog("close"); + self.replaceWith(response); + } else { + self.getDialogEl().html(response); + } + }); + + return false; + }); + + this.getClassSel().chosen().addClass("has-chzn").change(function() { + self.getCreateBtn().toggleClass("ui-state-disabled", !this.value); + }); + + this.getCreateBtn().click(function() { + if(self.getClassSel().val()) { + self.dialog(self.getClassSel().val()); + } + return false; + }); + + this.find("a.workflow-field-dialog").click(function() { + self.dialog(this.href); + return false; + }); + + this.find(".workflow-field-delete").click(function() { + if(confirm("Are you sure you want to permanently delete this?")) { + self.getLoadingInd().show(); + $.post(this.href).done(function(field) { + self.replaceWith(field); + }); + } + return false; + }); + }, + onunmatch: function() { + this.getDialogEl().dialog("destroy").remove(); + }, + dialog: function(url) { + var el = this.getDialogEl(); + + el.empty().dialog({ + width: 800, + height: 600, + modal: true + }); + el.parent().addClass("loading"); + + $.get(url).done(function(body) { + el.html(body).parent().removeClass("loading"); + }); + } + }); + + $(".workflow-field-action").entwine({ + onmatch: function() { + } + }); +})(jQuery); \ No newline at end of file diff --git a/scss/WorkflowField.scss b/scss/WorkflowField.scss new file mode 100644 index 00000000..4616a3c1 --- /dev/null +++ b/scss/WorkflowField.scss @@ -0,0 +1,138 @@ +$dark-blue: #7F9198; +$med-blue: #95A5AB; +$light-blue: #D7DDE0; +$light-grey: #F8F8F8; + +$grid: 8px; +$radius: 3px; + +.workflow-field { + .workflow-field-header { + background: $med-blue; + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: $radius $radius 0 0; + + h3 { + background: $dark-blue; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + color: #FFF; + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); + margin: 0; + padding: $grid; + } + } + + .workflow-field-create { + padding: $grid/2 $grid; + + .chzn-container { + float: left; + margin-right: 4px; + width: 250px !important; + + .chzn-single { + border-color: rgba(0, 0, 0, 0.2); + border-radius: $radius; + box-shadow: none; + } + .chzn-single-with-drop { + border-radius: $radius $radius 0 0; + } + } + + .ss-ui-button { + border-color: rgba(0, 0, 0, 0.2); + } + } + + .workflow-field-actions { + $height: 28px; + $min-width: 360px; + + background: #FFF; + border: 1px solid rgba(0, 0, 0, 0.2); + border-top: none; + border-radius: 0 0 $radius $radius; + padding: $grid; + position: relative; + + .workflow-field-action { + margin-bottom: $grid/2; + + &:last-child { + margin-bottom: 0; + } + } + + .workflow-field-action-header { + background: $light-blue; + border: 1px solid $med-blue; + border-left: none; + overflow: auto; + padding: $grid/4 $grid $grid/4 0; + + .workflow-field-action-icon, .workflow-field-action-drag { + display: block; + float: left; + } + .workflow-field-action-drag { + background: $med-blue; + cursor: move; + margin: -$grid/4 0; + width: 4px; + height: $height + $grid/2 + 1; + } + .workflow-field-action-icon { + width: 16px; + height: 16px; + margin: $height/4 $grid 0 $grid; + } + h4 { + float: left; + margin: $height/4 $grid 0 0; + min-width: $min-width; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4); + } + } + + .workflow-field-action-transitions { + li { + background: $light-grey; + border: 1px solid $light-blue; + border-left: none; + border-top: none; + overflow: auto; + padding: $grid/2 $grid/2 $grid/2 0; + + .workflow-field-action-drag { + background: $light-blue; + cursor: move; + float: left; + margin: -$grid/2 0; + width: 4px; + height: 24px; + } + .workflow-field-transition-title { + float: left; + min-width: $min-width + 30px; + overflow: auto; + } + .ui-icon, span { + float: left; + } + } + } + } + + .workflow-field-loading { + background: rgba(0, 0, 0, 0.15) url(../../sapphire/admin/images/spinner.gif) no-repeat center center; + border-radius: 0 0 $radius $radius; + bottom: 0; + display: none; + left: 0; + position: absolute; + right: 0; + top: 0; + z-index: 999; + } +} diff --git a/templates/AdvancedWorkflowAdmin_left.ss b/templates/AdvancedWorkflowAdmin_left.ss deleted file mode 100644 index 7e274c67..00000000 --- a/templates/AdvancedWorkflowAdmin_left.ss +++ /dev/null @@ -1,15 +0,0 @@ - -<% require javascript(sapphire/thirdparty/jquery-livequery/jquery.livequery.js) %> -<% require javascript(advancedworkflow/thirdparty/jquery-jstree/jquery.jstree.js) %> -<% require javascript(advancedworkflow/javascript/AdvancedWorkflowAdmin.js) %> -<% require css(advancedworkflow/css/AdvancedWorkflowAdmin.css) %> - -

<% _t('WORKFLOWS', 'Workflows') %>

- -$CreateDefinitionForm -
- $CreateActionForm - $CreateTransitionForm -
- -
\ No newline at end of file diff --git a/templates/AdvancedWorkflowAdmin_right.ss b/templates/AdvancedWorkflowAdmin_right.ss deleted file mode 100644 index 5502bf9f..00000000 --- a/templates/AdvancedWorkflowAdmin_right.ss +++ /dev/null @@ -1,11 +0,0 @@ -
- <% if EditForm %> - $EditForm - <% else %> -
-

$ApplicationName

-

<% _t('SELECTWORKFLOW', 'Please select a workflow to view.') %>

-
- <% end_if %> -
- \ No newline at end of file diff --git a/templates/WorkflowField.ss b/templates/WorkflowField.ss new file mode 100644 index 00000000..d2d5a0ed --- /dev/null +++ b/templates/WorkflowField.ss @@ -0,0 +1,61 @@ +
+
+

$Title

+
+ + +
+
+
+
+
+ + <% loop $Definition.Actions %> +
+ + + <% if $Transitions %> +
    + <% loop $Transitions %> +
  1. +
    + +
    + $Title + + $NextAction.Title +
    + Edit + Delete +
  2. + <% end_loop %> +
+ <% end_if %> +
+ <% end_loop %> +
+
\ No newline at end of file