├── _config └── relations.yml ├── templates ├── GridFieldRelationHandlerButtons.ss ├── GridFieldHasOneRelationHandlerItem.ss └── GridFieldManyRelationHandlerItem.ss ├── composer.json ├── model └── GridFieldRelationHandlerRelationList.php ├── javascript └── GridFieldRelationHandler.js ├── gridfield ├── GridFieldHasOneRelationHandler.php ├── GridFieldManyRelationHandler.php └── GridFieldRelationHandler.php └── README.md /_config/relations.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/GridFieldRelationHandlerButtons.ss: -------------------------------------------------------------------------------- 1 |
2 | <% loop Fields %> 3 | $Me 4 | <% end_loop %> 5 |
6 | -------------------------------------------------------------------------------- /templates/GridFieldHasOneRelationHandlerItem.ss: -------------------------------------------------------------------------------- 1 | checked<% end_if %> /> 2 | -------------------------------------------------------------------------------- /templates/GridFieldManyRelationHandlerItem.ss: -------------------------------------------------------------------------------- 1 | checked<% end_if %> <% if Disabled %>disabled<% end_if %> /> 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simonwelsh/gridfieldrelationhandler", 3 | "description": "This module provides two GridField components that aid in managing relationships within SilverStripe.", 4 | "type": "silverstripe-module", 5 | "license": "BSD-3-Clause", 6 | "keywords": ["silverstripe", "gridfield", "component"], 7 | "authors": [ 8 | { 9 | "name": "Simon Welsh", 10 | "homepage": "http://simon.geek.nz/", 11 | "email": "simon@simon.geek.nz" 12 | } 13 | ], 14 | "require": { 15 | "silverstripe/framework": "~3.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /model/GridFieldRelationHandlerRelationList.php: -------------------------------------------------------------------------------- 1 | foreignKey; 13 | } 14 | 15 | public function getForeignIDFilter(RelationList $on = null) { 16 | if(!$on) { 17 | return; 18 | } 19 | return $on->foreignIDFilter(); 20 | } 21 | } 22 | 23 | class GridFieldManyRelationHandler_ManyManyList extends ManyManyList { 24 | public function __construct() { 25 | 26 | } 27 | 28 | public function getJoinTable(ManyManyList $on = null) { 29 | if(!$on) { 30 | return; 31 | } 32 | return $on->joinTable; 33 | } 34 | 35 | public function getLocalKey(ManyManyList $on = null) { 36 | if(!$on) { 37 | return; 38 | } 39 | return $on->localKey; 40 | } 41 | 42 | public function getForeignKey(ManyManyList $on = null) { 43 | if(!$on) { 44 | return; 45 | } 46 | return $on->foreignKey; 47 | } 48 | 49 | public function getForeignIDFilter(RelationList $on = null) { 50 | if(!$on) { 51 | return; 52 | } 53 | return $on->foreignIDFilter(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /javascript/GridFieldRelationHandler.js: -------------------------------------------------------------------------------- 1 | jQuery(function($) { 2 | $.entwine('ss', function($) { 3 | $('.ss-gridfield .ss-gridfield-item .col-noedit').entwine({ 4 | onclick: function(e) { 5 | e.stopPropagation(); 6 | e.stopImmediatePropagation(); 7 | } 8 | }); 9 | $('.ss-gridfield .ss-gridfield-item .col-noedit input').entwine({ 10 | getState: function () { 11 | return this.getGridField().getState().GridFieldRelationHandler; 12 | }, 13 | setState: function(val) { 14 | this.getGridField().setState('GridFieldRelationHandler', val); 15 | }, 16 | onchange: function(e) { 17 | var state = this.getState(); 18 | var input = $(e.target).closest('input'); 19 | if(input.hasClass('radio')) { 20 | state.RelationVal = input.val(); 21 | } else if(input.hasClass('checkbox')) { 22 | if(state.RelationVal.indexOf) { 23 | if(input.is(':checked')) { 24 | state.RelationVal.push(input.val()); 25 | } else { 26 | var index = state.RelationVal.indexOf(input.val()); 27 | if(index != -1) { 28 | var left = state.RelationVal.slice(0, index); 29 | var right = state.RelationVal.slice(index+1); 30 | state.RelationVal = left.concat(right); 31 | } 32 | } 33 | } else if(input.is(':checked')) { 34 | state.RelationVal = [input.val()]; 35 | } else { 36 | state.RelationVal = []; 37 | } 38 | } 39 | this.setState(state); 40 | } 41 | }); 42 | }); 43 | }); 44 | 45 | if (!Array.prototype.indexOf) { 46 | Array.prototype.indexOf = function(elt /*, from*/) 47 | { 48 | var len = this.length >>> 0; 49 | 50 | var from = Number(arguments[1]) || 0; 51 | from = (from < 0) 52 | ? Math.ceil(from) 53 | : Math.floor(from); 54 | if (from < 0) 55 | from += len; 56 | 57 | for (; from < len; from++) 58 | { 59 | if (from in this && 60 | this[from] === elt) 61 | return from; 62 | } 63 | return -1; 64 | }; 65 | } 66 | -------------------------------------------------------------------------------- /gridfield/GridFieldHasOneRelationHandler.php: -------------------------------------------------------------------------------- 1 | onObject = $onObject; 11 | $this->relationName = $relationName; 12 | 13 | $hasOne = $onObject->has_one($relationName); 14 | if(!$hasOne) { 15 | user_error('Unable to find a has_one relation named ' . $relationName . ' on ' . $onObject->ClassName, E_USER_WARNING); 16 | } 17 | $this->targetObject = $hasOne; 18 | 19 | parent::__construct(false, $targetFragment); 20 | } 21 | 22 | protected function setupState($state, $extra = null) { 23 | parent::setupState($state, $extra); 24 | if($state->FirstTime) { 25 | $state->RelationVal = $this->onObject->{$this->relationName}()->ID; 26 | } 27 | } 28 | 29 | public function getColumnContent($gridField, $record, $columnName) { 30 | $class = $gridField->getModelClass(); 31 | if(!($class == $this->targetObject || is_subclass_of($class, $this->targetObject))) { 32 | user_error($class . ' is not a subclass of ' . $this->targetObject . '. Perhaps you wanted to use ' . $this->targetObject . '::get() as the list for this GridField?', E_USER_WARNING); 33 | } 34 | 35 | $state = $this->getState($gridField); 36 | 37 | $checked = $state->RelationVal == $record->ID; 38 | $field = new ArrayData(array('Checked' => $checked, 'Value' => $record->ID, 'Name' => $this->relationName . 'ID')); 39 | return $field->renderWith('GridFieldHasOneRelationHandlerItem'); 40 | } 41 | 42 | protected function saveGridRelation(GridField $gridField, $arguments, $data) { 43 | $field = $this->relationName . 'ID'; 44 | $state = $this->getState($gridField); 45 | $id = intval($state->RelationVal); 46 | $this->onObject->{$field} = $id; 47 | $this->onObject->write(); 48 | parent::saveGridRelation($gridField, $arguments, $data); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gridfield/GridFieldManyRelationHandler.php: -------------------------------------------------------------------------------- 1 | cheatList = new GridFieldManyRelationHandler_HasManyList; 10 | $this->cheatManyList = new GridFieldManyRelationHandler_ManyManyList; 11 | } 12 | 13 | public function getColumnContent($gridField, $record, $columnName) { 14 | $list = $gridField->getList(); 15 | if(!$list instanceof RelationList) { 16 | user_error('GridFieldManyRelationHandler requires the GridField to have a RelationList. Got a ' . get_class($list) . ' instead.', E_USER_WARNING); 17 | } 18 | 19 | $state = $this->getState($gridField); 20 | $checked = in_array($record->ID, $state->RelationVal->toArray()); 21 | $field = array('Checked' => $checked, 'Value' => $record->ID, 'Name' => $this->relationName($gridField)); 22 | if($list instanceof HasManyList) { 23 | $key = $record->{$this->cheatList->getForeignKey($list)}; 24 | if($key && !$checked) { 25 | $field['Disabled'] = true; 26 | } 27 | } 28 | $field = new ArrayData($field); 29 | return $field->renderWith('GridFieldManyRelationHandlerItem'); 30 | } 31 | 32 | public function getManipulatedData(GridField $gridField, SS_List $list) { 33 | if(!$list instanceof RelationList) { 34 | user_error('GridFieldManyRelationHandler requires the GridField to have a RelationList. Got a ' . get_class($list) . ' instead.', E_USER_WARNING); 35 | } 36 | 37 | $state = $this->getState($gridField); 38 | 39 | // We don't use setupState() as we need the list 40 | if($state->FirstTime) { 41 | $state->RelationVal = array_values($list->getIdList()) ?: array(); 42 | } 43 | if(!$state->ShowingRelation && $this->useToggle) { 44 | return $list; 45 | } 46 | 47 | $query = clone $list->dataQuery(); 48 | try { 49 | $query->removeFilterOn($this->cheatList->getForeignIDFilter($list)); 50 | } catch(InvalidArgumentException $e) { /* NOP */ } 51 | $orgList = $list; 52 | $list = new DataList($list->dataClass()); 53 | $list = $list->setDataQuery($query); 54 | if($orgList instanceof ManyManyList) { 55 | $joinTable = $this->cheatManyList->getJoinTable($orgList); 56 | $baseClass = ClassInfo::baseDataClass($list->dataClass()); 57 | $localKey = $this->cheatManyList->getLocalKey($orgList); 58 | $query->leftJoin($joinTable, "\"$joinTable\".\"$localKey\" = \"$baseClass\".\"ID\""); 59 | $list = $list->setDataQuery($query); 60 | } 61 | return $list; 62 | } 63 | 64 | protected function relationName($gridField) { 65 | return $gridField->getName() . get_class($gridField->getList()); 66 | } 67 | 68 | protected function cancelGridRelation(GridField $gridField, $arguments, $data) { 69 | parent::cancelGridRelation($gridField, $arguments, $data); 70 | 71 | $state = $this->getState($gridField); 72 | $state->RelationVal = array_values($gridField->getList()->getIdList()) ?: array(); 73 | } 74 | 75 | protected function saveGridRelation(GridField $gridField, $arguments, $data) { 76 | $state = $this->getState($gridField); 77 | $gridField->getList()->setByIdList($state->RelationVal->toArray()); 78 | parent::saveGridRelation($gridField, $arguments, $data); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GridFieldRelationHandler 2 | 3 | This module provides two [`GridField`](http://doc.silverstripe.org/framework/en/topics/grid-field) components 4 | that aid in managing relationships within [SilverStripe](http://www.silverstripe.org). The 5 | [`GridFieldHasOneRelationHandler`](#gridfieldhasonerelationhandler) component allows a `SS_List` to be used to select the value of a has_one 6 | relation and the [`GridFieldManyRelationHandler`](#gridfieldmanyrelationhandler) component manages a `RelationList`. 7 | 8 | ## Installation ## 9 | 10 | If the version of SilverStripe you are running is earlier than 3.0.3, first apply [this patch](https://github.com/silverstripe/sapphire/commit/d2b4e0df01f82fdbe613890c8ae909af404640a5) to `GridFieldPaginator`. 11 | 12 | Download this module and extract it into your site's root folder. Flush your site's manifest by visiting 13 | http://yoursite.com/?flush=1. These components are now ready for use. 14 | 15 | ## GridFieldHasOneRelationHandler ## 16 | 17 | ![](https://files.app.net/zb9sU5vs.png) 18 | 19 | The `GridFieldHasOneRelationHandler` component provides radio buttons for selecting the object that the 20 | has_one points to. Its constructor takes the object the relation exists on, the name of the relation and 21 | an optional target fragment which describes the position of the save relation button. 22 | 23 | ### Example ### 24 | 25 | :::php 26 | $config->addComponent(new GridFieldHasOneRelationHandler($this, 'MainImage')); 27 | 28 | ## GridFieldManyRelationHandler ## 29 | 30 | ![](https://files.app.net/zb9r7VqE.png) 31 | 32 | The `GridFieldManyRelationHandler` component provides check boxes for selecting the objects that a 33 | has_many or many_many point to. Its constructor takes an optional target fragment which describes 34 | the position of the save relation button. 35 | 36 | If your `GridField` also has a `GridFieldPaginator` component, this component must be inserted before 37 | it for the pagination to work properly. 38 | 39 | ### Example ### 40 | 41 | :::php 42 | // The second argument here ensures that this component is placed before any GridFieldPaginator 43 | $config->addComponent(new GridFieldManyRelationHandler(), 'GridFieldPaginator'); 44 | 45 | ## License ## 46 | 47 | Copyright (c) 2014, Simon Welsh - simon.geek.nz 48 | All rights reserved. 49 | 50 | Redistribution and use in source and binary forms, with or without 51 | modification, are permitted provided that the following conditions are met: 52 | * Redistributions of source code must retain the above copyright 53 | notice, this list of conditions and the following disclaimer. 54 | * Redistributions in binary form must reproduce the above copyright 55 | notice, this list of conditions and the following disclaimer in the 56 | documentation and/or other materials provided with the distribution. 57 | * The name of Simon Welsh may not be used to endorse or promote products 58 | derived from this software without specific prior written permission. 59 | 60 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 61 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 62 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 63 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 64 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 65 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 66 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 67 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 68 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 69 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 70 | 71 | -------------------------------------------------------------------------------- /gridfield/GridFieldRelationHandler.php: -------------------------------------------------------------------------------- 1 | 'Save changes', 10 | 'CANCELSAVE_RELATION' => 'Cancel changes', 11 | 'TOGGLE_RELATION' => 'Change relation status', 12 | ); 13 | 14 | public function __construct($useToggle = true, $targetFragment = 'before') { 15 | $this->targetFragment = $targetFragment; 16 | $this->useToggle = $useToggle; 17 | } 18 | 19 | public function setUseToggle($useToggle) { 20 | $this->useToggle = (bool)$useToggle; 21 | return $this; 22 | } 23 | 24 | public function setColumnTitle($columnTitle) { 25 | $this->columnTitle = $columnTitle; 26 | return $this; 27 | } 28 | 29 | public function getColumnTitle() { 30 | return $this->columnTitle; 31 | } 32 | 33 | public function setButtonTitle($name, $title) { 34 | $this->buttonTitles[$name] = $title; 35 | return $this; 36 | } 37 | 38 | public function getButtonTitle($name) { 39 | if(isset($this->buttonTitles[$name])) { 40 | $value = $this->buttonTitles[$name]; 41 | $key = sprintf('GridFieldRelationHandler.%s-%s', $name, str_replace(' ', '', $value)); 42 | return _t($key, $value); 43 | } else { 44 | return _t('GridFieldRelationHandler.' . $name); 45 | } 46 | } 47 | 48 | protected function getState($gridField) { 49 | static $state = null; 50 | if(!$state) { 51 | $state = $gridField->State->GridFieldRelationHandler; 52 | $this->setupState($state); 53 | } 54 | return $state; 55 | } 56 | 57 | protected function setupState($state) { 58 | if(!isset($state->RelationVal)) { 59 | $state->RelationVal = 0; 60 | $state->FirstTime = 1; 61 | } else { 62 | $state->FirstTime = 0; 63 | } 64 | if(!isset($state->ShowingRelation)) { 65 | $state->ShowingRelation = 0; 66 | } 67 | } 68 | 69 | public function augmentColumns($gridField, &$columns) { 70 | $state = $this->getState($gridField); 71 | if($state->ShowingRelation || !$this->useToggle) { 72 | if(!in_array('RelationSetter', $columns)) { 73 | array_unshift($columns, 'RelationSetter'); 74 | } 75 | if($this->useToggle && ($key = array_search('Actions', $columns)) !== false) { 76 | unset($columns[$key]); 77 | } 78 | } 79 | } 80 | 81 | public function getColumnsHandled($gridField) { 82 | return array('RelationSetter'); 83 | } 84 | 85 | public function getColumnMetadata($gridField, $columnName) { 86 | if($columnName == 'RelationSetter') { 87 | return array( 88 | 'title' => $this->columnTitle 89 | ); 90 | } 91 | return array(); 92 | } 93 | 94 | public function getColumnAttributes($gridField, $record, $columnName) { 95 | return array('class' => 'col-noedit'); 96 | } 97 | 98 | protected function getFields($gridField) { 99 | $state = $this->getState($gridField); 100 | if(!$this->useToggle) { 101 | $fields = array( 102 | Object::create( 103 | 'GridField_FormAction', 104 | $gridField, 105 | 'relationhandler-saverel', 106 | $this->getButtonTitle('SAVE_RELATION'), 107 | 'saveGridRelation', 108 | null 109 | ) 110 | ); 111 | } elseif($state->ShowingRelation) { 112 | $fields = array( 113 | Object::create( 114 | 'GridField_FormAction', 115 | $gridField, 116 | 'relationhandler-cancelrel', 117 | $this->getButtonTitle('CANCELSAVE_RELATION'), 118 | 'cancelGridRelation', 119 | null 120 | ), 121 | Object::create( 122 | 'GridField_FormAction', 123 | $gridField, 124 | 'relationhandler-saverel', 125 | $this->getButtonTitle('SAVE_RELATION'), 126 | 'saveGridRelation', 127 | null 128 | ) 129 | ); 130 | } else { 131 | $fields = array( 132 | Object::create( 133 | 'GridField_FormAction', 134 | $gridField, 135 | 'relationhandler-togglerel', 136 | $this->getButtonTitle('TOGGLE_RELATION'), 137 | 'toggleGridRelation', 138 | null 139 | ) 140 | ); 141 | } 142 | return new ArrayList($fields); 143 | } 144 | 145 | public function getHTMLFragments($gridField) { 146 | Requirements::javascript(basename(dirname(__DIR__)) . '/javascript/GridFieldRelationHandler.js'); 147 | $saveRelation = 148 | $data = new ArrayData(array( 149 | 'Fields' => $this->getFields($gridField) 150 | )); 151 | return array( 152 | $this->targetFragment => $data->renderWith('GridFieldRelationHandlerButtons') 153 | ); 154 | } 155 | 156 | public function getActions($gridField) { 157 | return array('saveGridRelation', 'cancelGridRelation', 'toggleGridRelation'); 158 | } 159 | 160 | public function handleAction(GridField $gridField, $actionName, $arguments, $data) { 161 | if(in_array($actionName, array_map('strtolower', $this->getActions($gridField)))) { 162 | return $this->$actionName($gridField, $arguments, $data); 163 | } 164 | } 165 | 166 | protected function toggleGridRelation(GridField $gridField, $arguments, $data) { 167 | $state = $this->getState($gridField); 168 | $state->ShowingRelation = true; 169 | } 170 | 171 | protected function cancelGridRelation(GridField $gridField, $arguments, $data) { 172 | $state = $this->getState($gridField); 173 | $state->ShowingRelation = false; 174 | $state->FirstTime = true; 175 | } 176 | 177 | protected function saveGridRelation(GridField $gridField, $arguments, $data) { 178 | $state = $this->getState($gridField); 179 | $state->ShowingRelation = false; 180 | } 181 | } 182 | --------------------------------------------------------------------------------