From c396c2d2ae87c1c76aecdc4d3d211a8d8ab228d1 Mon Sep 17 00:00:00 2001 From: Stig Lindqvist Date: Wed, 25 Jan 2012 17:31:27 +1300 Subject: [PATCH] API CHANGE Security admin supports adding, removing and searching for members by relations via gridfield This contains some experimental API's when using GridFieldPopupForms on GridFieldPopupForms. - GridFieldRelationAdd - GridFieldRelationDelete --- admin/code/SecurityAdmin.php | 14 +- forms/gridfield/GridField.php | 16 +- forms/gridfield/GridFieldPopupForms.php | 106 +++++++++-- forms/gridfield/GridFieldRelationAdd.php | 198 ++++++++++++++++++++ forms/gridfield/GridFieldRelationDelete.php | 99 ++++++++++ javascript/GridFieldSearch.js | 36 ++++ security/Group.php | 18 +- templates/Includes/GridFieldItemEditView.ss | 3 + templates/Includes/GridFieldRelationAdd.ss | 4 + 9 files changed, 458 insertions(+), 36 deletions(-) mode change 100644 => 100755 admin/code/SecurityAdmin.php mode change 100644 => 100755 forms/gridfield/GridFieldPopupForms.php create mode 100755 forms/gridfield/GridFieldRelationAdd.php create mode 100644 forms/gridfield/GridFieldRelationDelete.php create mode 100644 javascript/GridFieldSearch.js mode change 100644 => 100755 security/Group.php create mode 100644 templates/Includes/GridFieldItemEditView.ss create mode 100644 templates/Includes/GridFieldRelationAdd.ss diff --git a/admin/code/SecurityAdmin.php b/admin/code/SecurityAdmin.php old mode 100644 new mode 100755 index 60f954d8f..652277ecc --- a/admin/code/SecurityAdmin.php +++ b/admin/code/SecurityAdmin.php @@ -107,12 +107,14 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider { * @return FieldList */ function RootForm() { - $memberList = new MemberTableField( - $this, - "Members" - ); - // unset 'inlineadd' permission, we don't want inline addition - $memberList->setPermissions(array('edit', 'delete', 'add')); + $config = new GridFieldConfig(); + $config->addComponent(new GridFieldRelationAdd('Name')); + $config->addComponent(new GridFieldDefaultColumns()); + $config->addComponent(new GridFieldSortableHeader()); + $config->addComponent(new GridFieldPaginator()); + $config->addComponent(new GridFieldAction_Edit()); + $config->addComponent(new GridFieldPopupForms($this, 'RootForm')); + $memberList = new GridField('Members', 'All members', DataList::create('Member'), $config); $fields = new FieldList( $root = new TabSet( diff --git a/forms/gridfield/GridField.php b/forms/gridfield/GridField.php index eb09c34cb..c1e334cbe 100755 --- a/forms/gridfield/GridField.php +++ b/forms/gridfield/GridField.php @@ -511,7 +511,11 @@ class GridField extends FormField { $actionName = $stateChange['actionName']; $args = isset($stateChange['args']) ? $stateChange['args'] : array(); - $grid->handleAction($actionName, $args, $data); + $html = $grid->handleAction($actionName, $args, $data); + + if($html) { + return $html; + } switch($request->getHeader('X-Get-Fragment')) { case 'CurrentField': @@ -539,13 +543,13 @@ class GridField extends FormField { */ public function handleAction($actionName, $args, $data) { $actionName = strtolower($actionName); - foreach($this->components as $item) { - if(!($item instanceof GridField_ActionProvider)) { + foreach($this->components as $component) { + if(!($component instanceof GridField_ActionProvider)) { continue; } - if(in_array($actionName, array_map('strtolower', $item->getActions($this)))) { - return $item->handleAction($this, $actionName, $args, $data); + if(in_array($actionName, array_map('strtolower', $component->getActions($this)))) { + return $component->handleAction($this, $actionName, $args, $data); } } throw new InvalidArgumentException("Can't handle action '$actionName'"); @@ -564,8 +568,6 @@ class GridField extends FormField { $this->request = $request; $this->setModel($model); - /// - foreach($this->components as $component) { if(!($component instanceof GridField_URLHandler)) { continue; diff --git a/forms/gridfield/GridFieldPopupForms.php b/forms/gridfield/GridFieldPopupForms.php old mode 100644 new mode 100755 index 621865312..dc3425141 --- a/forms/gridfield/GridFieldPopupForms.php +++ b/forms/gridfield/GridFieldPopupForms.php @@ -13,16 +13,51 @@ class GridFieldPopupForms implements GridField_URLHandler { * @var String */ protected $template = 'GridFieldItemEditView'; + + /** + * + * @var Controller + */ + protected $popupController; + /** + * + * @var string + */ + protected $popupFormName; + function getURLHandlers($gridField) { return array( 'item/$ID' => 'handleItem', + 'autocomplete' => 'handleAutocomplete', ); } - function handleItem($gridField, $request) { + /** + * Create a popup component. The two arguments will specify how the popup form's HTML and + * behaviour is created. The given controller will be customised, putting the edit form into the + * template with the given name. + * + * The arguments are experimental API's to support partial content to be passed back to whatever + * controller who wants to display the getCMSFields + * + * @param Controller $popupController The controller object that will be used to render the pop-up forms + * @param string $popupFormName The name of the edit form to place into the pop-up form + */ + public function __construct($popupController, $popupFormName) { + $this->popupController = $popupController; + $this->popupFormName = $popupFormName; + } + + /** + * + * @param type $gridField + * @param type $request + * @return GridFieldPopupForm_ItemRequest + */ + public function handleItem($gridField, $request) { $record = $gridField->getList()->byId($request->param("ID")); - $handler = new GridFieldPopupForm_ItemRequest($gridField, $this, $record); + $handler = new GridFieldPopupForm_ItemRequest($gridField, $this, $record, $this->popupController, $this->popupFormName); $handler->setTemplate($this->template); return $handler; } @@ -44,12 +79,36 @@ class GridFieldPopupForms implements GridField_URLHandler { class GridFieldPopupForm_ItemRequest extends RequestHandler { + /** + * + * @var GridField + */ protected $gridField; + /** + * + * @var GridField_URLHandler + */ protected $component; + /** + * + * @var DataObject + */ protected $record; + /** + * + * @var Controller + */ + protected $popupController; + + /** + * + * @var string + */ + protected $popupFormName; + /** * @var String */ @@ -57,45 +116,66 @@ class GridFieldPopupForm_ItemRequest extends RequestHandler { static $url_handlers = array( '$Action!' => '$Action', - '' => 'index', + '' => 'edit', ); - function __construct($gridField, $component, $record) { + /** + * + * @param GridFIeld $gridField + * @param GridField_URLHandler $component + * @param DataObject $record + * @param Controller $popupController + * @param string $popupFormName + */ + public function __construct($gridField, $component, $record, $popupController, $popupFormName) { $this->gridField = $gridField; $this->component = $gridField; $this->record = $record; - + $this->popupController = $popupController; + $this->popupFormName = $popupFormName; parent::__construct(); } - function Link($action = null) { + public function Link($action = null) { return Controller::join_links($this->gridField->Link('item'), $this->record->ID, $action); } function edit($request) { - $controller = $this->gridField->getForm()->Controller(); - + $controller = $this->popupController; + $return = $this->customise(array( - 'Backlink' => $controller->Link(), + 'Backlink' => $this->gridField->getForm()->Controller()->Link(), 'ItemEditForm' => $this->ItemEditForm($this->gridField, $request), ))->renderWith($this->template); if($controller->isAjax()) { return $return; } else { + // If not requested by ajax, we need to render it within the controller context+template return $controller->customise(array( - $this->gridField->getForm()->Name() => $return, + $this->popupFormName => $return, )); } } + /** + * Builds an item edit form. The arguments to getCMSFields() are the popupController and + * popupFormName, however this is an experimental API and may change. + * + * In the future, we will probably need to come up with a tigher object representing a partially + * complete controller with gaps for extra functionality. This, for example, would be a better way + * of letting Security/login put its log-in form inside a UI specified elsewhere. + * + * @return Form + */ function ItemEditForm() { - $request = $this->gridField->getForm()->Controller()->getRequest(); + $request = $this->popupController->getRequest(); $form = new Form( $this, 'ItemEditForm', - $this->record->getCMSFields(), + // WARNING: The arguments passed here are a little arbitrary. This API will need cleanup + $this->record->getCMSFields($this->popupController, $this->popupFormName), new FieldList( $saveAction = new FormAction('doSave', _t('GridFieldDetailsForm.Save', 'Save')) ) @@ -124,7 +204,7 @@ class GridFieldPopupForm_ItemRequest extends RequestHandler { $form->sessionMessage($message, 'good'); - return $this->gridField->getForm()->Controller()->redirectBack(); + return $this->popupController->redirectBack(); } /** diff --git a/forms/gridfield/GridFieldRelationAdd.php b/forms/gridfield/GridFieldRelationAdd.php new file mode 100755 index 000000000..b735d3c21 --- /dev/null +++ b/forms/gridfield/GridFieldRelationAdd.php @@ -0,0 +1,198 @@ +fieldToSearch = $fieldToSearch; + $this->useAutoSuggestion = $autoSuggestion; + } + + /** + * + * @param GridField $gridField + * @return string - HTML + */ + public function getHTMLFragments($gridField) { + $searchState = $gridField->State->GridFieldSearchRelation; + + + if($this->useAutoSuggestion){ + Requirements::css(THIRDPARTY_DIR . '/jquery-ui-themes/smoothness/jquery-ui.css'); + Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang'); + Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); + Requirements::javascript(SAPPHIRE_DIR . '/javascript/jquery_improvements.js'); + Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery-ui/jquery-ui.js'); + Requirements::javascript(SAPPHIRE_DIR . "/javascript/GridFieldSearch.js"); + } + + $forTemplate = new ArrayData(array()); + $forTemplate->Fields = new ArrayList(); + + $value = $this->findSingleEntry($gridField, $this->fieldToSearch, $searchState, $gridField->getList()->dataClass); + $searchField = new TextField('gridfield_relationsearch', 'Auto Suggest Search field', $value); + // Apparently the data-* needs to be double qouted for the jQuery.meta data plugin + // + $searchField->setAttribute('data-search-url', '\''.Controller::join_links($gridField->Link('search').'\'')); + + $findAction = new GridField_Action($gridField, 'gridfield_relationfind', 'Find', 'find', 'find'); + $addAction = new GridField_Action($gridField, 'gridfield_relationadd', 'Add Relation', 'addto', 'addto'); + + // If an object is not found, disable the action + if(!is_int($gridField->State->GridFieldAddRelation)) { + $addAction->setReadonly(true); + } + + $forTemplate->Fields->push($searchField); + $forTemplate->Fields->push($findAction); + $forTemplate->Fields->push($addAction); + return array('before' => $forTemplate->renderWith($this->itemClass)); + } + + /** + * + * @param GridField $gridField + * @return array + */ + public function getActions($gridField) { + return array('addto', 'find'); + } + + /** + * Manipulate the state to either add a new relation, or doing a small search + * + * @param GridField $gridField + * @param string $actionName + * @param string $arguments + * @param string $data + * @return string + */ + public function handleAction(GridField $gridField, $actionName, $arguments, $data) { + switch($actionName) { + case 'addto': + if(isset($data['relationID']) && $data['relationID']){ + $gridField->State->GridFieldAddRelation = $data['relationID']; + } + $gridField->State->GridFieldSearchRelation = ''; + break; + case 'find' && isset($data['autosuggest_search']): + $gridField->State->GridFieldSearchRelation = $data['autosuggest_search']; + break; + } + } + + /** + * If an object ID is set, add the object to the list + * + * @param GridField $gridField + * @param SS_List $dataList + * @return SS_List + */ + public function getManipulatedData(GridField $gridField, SS_List $dataList) { + if(!$gridField->State->GridFieldAddRelation) { + return $dataList; + } + $objectID = Convert::raw2sql($gridField->State->GridFieldAddRelation); + if($objectID) { + $object = DataObject::get_by_id($dataList->dataclass(), $objectID); + if($object) { + $dataList->add($object); + } + } + $gridField->State->GridFieldAddRelation = null; + return $dataList; + } + + /** + * + * @param GridField $gridField + * @return array + */ + public function getURLHandlers($gridField) { + return array( + 'search/$ID' => 'doSearch', + ); + } + + /** + * Returns a json array of a search results that can be used by for example Jquery.ui.autosuggestion + * + * @param GridField $gridField + * @param SS_HTTPRequest $request + */ + public function doSearch($gridField, $request) { + $allList = DataList::create($gridField->getList()->dataClass()); + $results = $allList->subtract($gridField->getList())->filter($this->fieldToSearch.':StartsWith',$request->param('ID')); + $results->sort($this->fieldToSearch, 'ASC'); + + $json = array(); + foreach($results as $result) { + $json[$result->ID] = $result->{$this->fieldToSearch}; + } + return Convert::array2json($json); + } + + /** + * This will provide a StartsWith search that only returns a value if we are + * matching ONE object only. We wouldn't want to attach used any object to + * the list. + * + * @param GridField $gridField + * @param string $field + * @param string $searchTerm + * @param string $dataclass + * @return string + */ + protected function findSingleEntry($gridField, $field, $searchTerm, $dataclass) { + $fullList = DataList::create($dataclass); + $searchTerm = Convert::raw2sql($searchTerm); + if(!$searchTerm) { + return; + } + $existingList = clone $gridField->getList(); + $searchResults = $fullList->subtract($existingList->limit(0))->filter($field.':StartsWith', $searchTerm); + + // If more than one, skip + if($searchResults->count() != 1) { + return ''; + } + + $gridField->State->GridFieldAddRelation = $searchResults->first()->ID; + return $searchResults->first()->$field; + } +} \ No newline at end of file diff --git a/forms/gridfield/GridFieldRelationDelete.php b/forms/gridfield/GridFieldRelationDelete.php new file mode 100644 index 000000000..caf9fe5de --- /dev/null +++ b/forms/gridfield/GridFieldRelationDelete.php @@ -0,0 +1,99 @@ + ''); + } + } + + /** + * Which columns are handled by this component + * + * @param type $gridField + * @return type + */ + public function getColumnsHandled($gridField) { + return array('UnlinkRelation'); + } + + /** + * Which GridField actions are this component handling + * + * @param GridField $gridField + * @return array + */ + public function getActions($gridField) { + return array('unlinkrelation'); + } + + /** + * + * @param GridField $gridField + * @param DataObject $record + * @param string $columnName + * @return string - the HTML for the column + */ + public function getColumnContent($gridField, $record, $columnName) { + $field = new GridField_Action( + $gridField, + 'UnlinkRelation'.$record->ID, + _t('GridAction.UnlinkRelation', "Unlink"), + "unlinkrelation", + array('RecordID' => $record->ID) + ); + $output = $field->Field(); + return $output; + } + + /** + * Handle the actions and apply any changes to the GridField + * + * @param GridField $gridField + * @param string $actionName + * @param mixed $arguments + * @param array $data - form data + * @return void + */ + public function handleAction(GridField $gridField, $actionName, $arguments, $data) { + $id = $arguments['RecordID']; + $item = $gridField->getList()->byID($id); + if(!$item) return; + if($actionName == 'unlinkrelation') { + $gridField->getList()->remove($item); + } + } +} diff --git a/javascript/GridFieldSearch.js b/javascript/GridFieldSearch.js new file mode 100644 index 000000000..e8a96dcf9 --- /dev/null +++ b/javascript/GridFieldSearch.js @@ -0,0 +1,36 @@ +jQuery(function($){ + + $(document).delegate("#gridfield_relationsearch", "focus", function (event) { + $(this).autocomplete({ + source: function(request, response){ + var searchField = $(this.element); + var form = $(this.element).closest("form"); + // Due to some very weird behaviout of jquery.metadata, the url have to be double quoted + var suggestionUrl = $(searchField).attr('data-search-url').substr(1,$(searchField).attr('data-search-url').length-2); + $.ajax({ + headers: { + "X-Get-Fragment" : 'Partial' + }, + type: "GET", + url: suggestionUrl+'/'+request.term, + data: form.serialize()+'&'+escape(searchField.attr('name'))+'='+escape(searchField.val()), + success: function(data) { + response( $.map(JSON.parse(data), function( name, id ) { + return { label: name, value: name, id: id } + })); + }, + error: function(e) { + alert(ss.i18n._t('GRIDFIELD.ERRORINTRANSACTION', 'An error occured while fetching data from the server\n Please try again later.')); + } + }); + }, + select: function(event, ui) { + $(this).closest("fieldset.ss-gridfield").find("#action_gridfield_relationfind").replaceWith( + '' + ); + $(this).closest("fieldset.ss-gridfield").find("#action_gridfield_relationadd").removeAttr('disabled'); + } + }); + }); + +}); \ No newline at end of file diff --git a/security/Group.php b/security/Group.php old mode 100644 new mode 100755 index e6c00102c..a3c0bcf3f --- a/security/Group.php +++ b/security/Group.php @@ -62,17 +62,18 @@ class Group extends DataObject { public function getCMSFields() { Requirements::javascript(SAPPHIRE_DIR . '/javascript/PermissionCheckboxSetField.js'); + $config = new GridFieldConfig_ManyManyEditor('FirstName', true, 20); + $config->addComponent(new GridFieldPopupForms(Controller::curr(), 'EditForm')); + $memberList = new GridField('Members','Members', $this->Members(), $config); + + // @todo Implement permission checking on GridField + //$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd')); + //$memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User')); $fields = new FieldList( new TabSet("Root", new Tab('Members', _t('SecurityAdmin.MEMBERS', 'Members'), new TextField("Title", $this->fieldLabel('Title')), - $memberList = new MemberTableField( - (Controller::has_curr()) ? Controller::curr() : new Controller(), - "Members", - $this, - null, - false - ) + $memberList ), $permissionsTab = new Tab('Permissions', _t('SecurityAdmin.PERMISSIONS', 'Permissions'), @@ -152,9 +153,6 @@ class Group extends DataObject { $rolesField->setDisabledItems($inheritedRoles->column('ID')); } - $memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd')); - $memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User')); - $fields->push($idField = new HiddenField("ID")); $this->extend('updateCMSFields', $fields); diff --git a/templates/Includes/GridFieldItemEditView.ss b/templates/Includes/GridFieldItemEditView.ss new file mode 100644 index 000000000..ed700a4a4 --- /dev/null +++ b/templates/Includes/GridFieldItemEditView.ss @@ -0,0 +1,3 @@ + Go back the way you came! + +$ItemEditForm \ No newline at end of file diff --git a/templates/Includes/GridFieldRelationAdd.ss b/templates/Includes/GridFieldRelationAdd.ss new file mode 100644 index 000000000..f50e6385f --- /dev/null +++ b/templates/Includes/GridFieldRelationAdd.ss @@ -0,0 +1,4 @@ +
<% control Fields %> + $Field + <% end_control %> +
\ No newline at end of file