diff --git a/CHANGELOG.md b/CHANGELOG.md index 24356341b..624e03bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +* **Build 229** (2015-03-19) + - Belongs-to-many model relations now support defining a custom pivot model with the `pivotModel` option (see Database > Model docs). + - The config definitions for behavior `RelationController` have been refactored. When using `pivot` mode all columns and fields should now reside in a `pivot[]` array (see Backend > Relations docs). + - Record Finder form widget now supports nested attributes for relations. + - List columns now support using array names (eg: `relation[attribute]`) which acts as an alias for the `valueFrom` option with `searchable` and `sortable` disabled. + * **Build 226** (2015-03-16) - Form Tabs now support specifying a default tab using the `defaultTab` option (see Backend > Forms docs). - Improved the Theme management features: Edit properties, import, export, duplicate and delete. diff --git a/modules/backend/ServiceProvider.php b/modules/backend/ServiceProvider.php index 35d063548..4f23977b8 100644 --- a/modules/backend/ServiceProvider.php +++ b/modules/backend/ServiceProvider.php @@ -98,6 +98,7 @@ class ServiceProvider extends ModuleServiceProvider 'category' => SettingsManager::CATEGORY_SYSTEM, 'icon' => 'icon-paint-brush', 'class' => 'Backend\Models\BrandSettings', + 'permissions' => ['backend.manage_branding'], 'order' => 500 ], 'editor' => [ @@ -134,7 +135,7 @@ class ServiceProvider extends ModuleServiceProvider 'category' => SettingsManager::CATEGORY_LOGS, 'icon' => 'icon-lock', 'url' => Backend::url('backend/accesslogs'), - 'permissions' => ['backend.access_admin_logs'], + 'permissions' => ['system.access_logs'], 'order' => 800 ] ]); @@ -152,6 +153,10 @@ class ServiceProvider extends ModuleServiceProvider 'backend.manage_users' => [ 'label' => 'system::lang.permissions.manage_other_administrators', 'tab' => 'system::lang.permissions.name' + ], + 'backend.manage_branding' => [ + 'label' => 'system::lang.permissions.manage_branding', + 'tab' => 'system::lang.permissions.name' ] ]); }); diff --git a/modules/backend/assets/css/controls.css b/modules/backend/assets/css/controls.css index 83c71edf1..3a61874f7 100644 --- a/modules/backend/assets/css/controls.css +++ b/modules/backend/assets/css/controls.css @@ -183,6 +183,7 @@ table.table.data tbody.clickable{cursor:pointer;-webkit-user-select:none;-moz-us table.table.data tbody td.column-compact{padding:0} table.table.data tfoot a{color:#666666;text-decoration:none} table.table.data tfoot td,table.table.data tfoot th{border-color:#e2e2e2;padding:10px 15px} +table.table.data th.list-cell-type-switch,table.table.data td.list-cell-type-switch{text-align:center} table.table.data .list-checkbox{padding-left:16px;padding-right:8px;width:52px;vertical-align:top;border-right:1px solid #eeeeee} table.table.data .list-checkbox .checkbox{margin:0} table.table.data .list-checkbox .custom-checkbox{position:relative;top:5px;left:-2px} diff --git a/modules/backend/assets/js/october-min.js b/modules/backend/assets/js/october-min.js index 261daf430..1138315a3 100644 --- a/modules/backend/assets/js/october-min.js +++ b/modules/backend/assets/js/october-min.js @@ -137,7 +137,7 @@ this.scrollClassContainer=this.options.scrollClassContainer?$(this.options.scrol if(this.options.scrollMarkerContainer) $(this.options.scrollMarkerContainer).append($('')) $el.mousewheel(function(event){if(!self.options.allowScroll) -return;var offset=self.options.vertical?((event.deltaFactor*event.deltaY)*-1):((event.deltaFactor*event.deltaX)*-1) +return;var offset=self.options.vertical?((event.deltaFactor*event.deltaY)*-1):(event.deltaFactor*event.deltaX) return!scrollWheel(offset)}) $el.on('mousedown',function(event){startDrag(event) return false}) @@ -806,7 +806,7 @@ if(isTouch){this.$el.on('touchstart',function(event){var touchEvent=event.origin event.stopPropagation()}})} else{this.$thumb.on('mousedown',function(event){startDrag(event)}) this.$track.on('mouseup',function(event){moveDrag(event)})} -$el.mousewheel(function(event){var offset=self.options.vertical?((event.deltaFactor*event.deltaY)*-1):((event.deltaFactor*event.deltaX)*-1) +$el.mousewheel(function(event){var offset=self.options.vertical?((event.deltaFactor*event.deltaY)*-1):(event.deltaFactor*event.deltaX) return!scrollWheel(offset*self.options.scrollSpeed)}) $el.on('oc.scrollbar.gotoStart',function(event){self.options.vertical?$el.scrollTop(0):$el.scrollLeft(0) self.update() diff --git a/modules/backend/assets/js/october.dragscroll.js b/modules/backend/assets/js/october.dragscroll.js index c7436e705..c2ccef89e 100644 --- a/modules/backend/assets/js/october.dragscroll.js +++ b/modules/backend/assets/js/october.dragscroll.js @@ -60,7 +60,7 @@ var offset = self.options.vertical ? ((event.deltaFactor * event.deltaY) * -1) - : ((event.deltaFactor * event.deltaX) * -1) + : (event.deltaFactor * event.deltaX) return !scrollWheel(offset) }) diff --git a/modules/backend/assets/js/october.scrollbar.js b/modules/backend/assets/js/october.scrollbar.js index cbc95db3c..14f09e920 100644 --- a/modules/backend/assets/js/october.scrollbar.js +++ b/modules/backend/assets/js/october.scrollbar.js @@ -68,7 +68,7 @@ $el.mousewheel(function (event){ var offset = self.options.vertical ? ((event.deltaFactor * event.deltaY) * -1) - : ((event.deltaFactor * event.deltaX) * -1) + : (event.deltaFactor * event.deltaX) return !scrollWheel(offset * self.options.scrollSpeed) }) diff --git a/modules/backend/assets/less/controls/lists.less b/modules/backend/assets/less/controls/lists.less index 069b69e62..6ed45f52d 100644 --- a/modules/backend/assets/less/controls/lists.less +++ b/modules/backend/assets/less/controls/lists.less @@ -272,6 +272,11 @@ table.table.data { } } + th.list-cell-type-switch, + td.list-cell-type-switch { + text-align: center; + } + .list-checkbox { padding-left: 16px; padding-right: 8px; diff --git a/modules/backend/behaviors/FormController.php b/modules/backend/behaviors/FormController.php index d50f12d86..11e050cc0 100644 --- a/modules/backend/behaviors/FormController.php +++ b/modules/backend/behaviors/FormController.php @@ -7,7 +7,6 @@ use Event; use Input; use Redirect; use Backend; -use Backend\Classes\FormField; use Backend\Classes\ControllerBehavior; use October\Rain\Router\Helper as RouterHelper; use ApplicationException; @@ -22,6 +21,8 @@ use Exception; */ class FormController extends ControllerBehavior { + use \Backend\Traits\FormModelSaver; + /** * @var string Default context for "create" pages. */ @@ -59,11 +60,6 @@ class FormController extends ControllerBehavior */ protected $context; - /** - * @var array List of prepared models that require saving. - */ - protected $modelsToSave = []; - /** * @var Model The initialized model used by the form. */ @@ -701,43 +697,4 @@ class FormController extends ControllerBehavior }); } - // - // Internals - // - - protected function prepareModelsToSave($model, $saveData) - { - $this->modelsToSave = []; - $this->setModelAttributes($model, $saveData); - return $this->modelsToSave; - } - - /** - * Sets a data collection to a model attributes, relations will also be set. - * @param array $saveData Data to save. - * @param Model $model Model to save to - * @return array The collection of models to save. - */ - protected function setModelAttributes($model, $saveData) - { - $this->modelsToSave[] = $model; - - if (!is_array($saveData)) { - return; - } - - $singularTypes = ['belongsTo', 'hasOne', 'morphOne']; - foreach ($saveData as $attribute => $value) { - if ( - is_array($value) && - $model->hasRelation($attribute) && - in_array($model->getRelationType($attribute), $singularTypes) - ) { - $this->setModelAttributes($model->{$attribute}, $value); - } - elseif ($value !== FormField::NO_SAVE_DATA) { - $model->{$attribute} = $value; - } - } - } } diff --git a/modules/backend/behaviors/RelationController.php b/modules/backend/behaviors/RelationController.php index 3c50db8c6..ac10a4d98 100644 --- a/modules/backend/behaviors/RelationController.php +++ b/modules/backend/behaviors/RelationController.php @@ -17,6 +17,8 @@ use October\Rain\Database\Model; */ class RelationController extends ControllerBehavior { + use \Backend\Traits\FormModelSaver; + /** * @var const Postback parameter for the active relationship field. */ @@ -272,89 +274,6 @@ class RelationController extends ControllerBehavior } } - /** - * Determine the default buttons based on the model relationship type. - * @return string - */ - protected function evalToolbarButtons() - { - if ($buttons = $this->getConfig('view[toolbarButtons]')) { - return is_array($buttons) - ? $buttons - : array_map('trim', explode('|', $buttons)); - } - - switch ($this->relationType) { - case 'hasMany': - case 'belongsToMany': - return ['create', 'add', 'delete', 'remove']; - - case 'hasOne': - case 'belongsTo': - return ['create', 'update', 'link', 'delete', 'unlink']; - } - } - - /** - * Determine the view mode based on the model relationship type. - * @return string - */ - protected function evalViewMode() - { - if ($this->forceViewMode) { - return $this->forceViewMode; - } - - switch ($this->relationType) { - case 'hasMany': - case 'belongsToMany': - return 'multi'; - - case 'hasOne': - case 'belongsTo': - return 'single'; - } - } - - /** - * Determine the management mode based on the relation type and settings. - * @return string - */ - protected function evalManageMode() - { - if ($mode = post(self::PARAM_MODE)) { - return $mode; - } - - if ($this->forceManageMode) { - return $this->forceManageMode; - } - - switch ($this->eventTarget) { - case 'button-create': - case 'button-update': - return 'form'; - - case 'button-link': - return 'list'; - } - - switch ($this->relationType) { - case 'belongsTo': - return 'list'; - - case 'belongsToMany': - if (isset($this->config->pivot)) return 'pivot'; - elseif ($this->eventTarget == 'list') return 'form'; - else return 'list'; - - case 'hasOne': - case 'hasMany': - if ($this->eventTarget == 'button-add') return 'list'; - else return 'form'; - } - } - /** * Renders the relationship manager. * @param string $field The relationship field. @@ -889,8 +808,14 @@ class RelationController extends ControllerBehavior { $this->beforeAjax(); + $foreignKeyName = $this->relationModel->getQualifiedKeyName(); + $hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first(); $saveData = $this->pivotWidget->getSaveData(); - $this->relationObject->updateExistingPivot($this->manageId, $saveData, true); + + $modelsToSave = $this->prepareModelsToSave($hydratedModel, $saveData); + foreach ($modelsToSave as $modelToSave) { + $modelToSave->save(); + } return ['#'.$this->relationGetId('view') => $this->relationRenderView()]; } @@ -960,7 +885,7 @@ class RelationController extends ControllerBehavior * Multiple (has many, belongs to many) */ if ($this->viewMode == 'multi') { - $config = $this->makeConfig($this->config->list); + $config = $this->makeConfigForMode('view', 'list'); $config->model = $this->relationModel; $config->alias = $this->alias . 'ViewList'; $config->showSorting = $this->getConfig('view[showSorting]', true); @@ -1027,7 +952,7 @@ class RelationController extends ControllerBehavior $this->controller->relationExtendQuery($query, $this->field); $this->viewModel = $query->getResults() ?: $this->relationModel; - $config = $this->makeConfig($this->config->form); + $config = $this->makeConfigForMode('view', 'form'); $config->model = $this->viewModel; $config->arrayName = class_basename($this->relationModel); $config->context = 'relation'; @@ -1046,13 +971,14 @@ class RelationController extends ControllerBehavior /* * Pivot */ - if ($this->manageMode == 'pivot' && isset($this->config->list)) { - $config = $this->makeConfig($this->config->list); + if ($this->manageMode == 'pivot') { + $config = $this->makeConfigForMode('manage', 'list'); $config->model = $this->relationModel; $config->alias = $this->alias . 'ManagePivotList'; $config->showSetup = false; - $config->defaultSort = $this->getConfig('pivot[defaultSort]'); - $config->recordsPerPage = $this->getConfig('pivot[recordsPerPage]'); + $config->showSorting = $this->getConfig('manage[showSorting]', false); + $config->defaultSort = $this->getConfig('manage[defaultSort]'); + $config->recordsPerPage = $this->getConfig('manage[recordsPerPage]'); $config->recordOnClick = sprintf( "$.oc.relationBehavior.clickManagePivotListRecord(:id, '%s', '%s')", $this->field, @@ -1078,8 +1004,8 @@ class RelationController extends ControllerBehavior /* * List */ - elseif ($this->manageMode == 'list' && isset($this->config->list)) { - $config = $this->makeConfig($this->config->list); + elseif ($this->manageMode == 'list') { + $config = $this->makeConfigForMode('manage', 'list'); $config->model = $this->relationModel; $config->alias = $this->alias . 'ManageList'; $config->showSetup = false; @@ -1116,7 +1042,7 @@ class RelationController extends ControllerBehavior /* * Form */ - elseif ($this->manageMode == 'form' && isset($this->config->form)) { + elseif ($this->manageMode == 'form') { /* * Determine supplied form context @@ -1131,7 +1057,7 @@ class RelationController extends ControllerBehavior } } - $config = $this->makeConfig($this->config->form); + $config = $this->makeConfigForMode('manage', 'form'); $config->model = $this->relationModel; $config->arrayName = class_basename($this->relationModel); $config->context = $context ?: 'relation'; @@ -1179,7 +1105,7 @@ class RelationController extends ControllerBehavior protected function makePivotWidget() { - $config = $this->makeConfig($this->config->pivot); + $config = $this->makeConfigForMode('pivot', 'form'); $config->model = $this->relationModel; $config->arrayName = class_basename($this->relationModel); $config->context = 'relation'; @@ -1189,10 +1115,11 @@ class RelationController extends ControllerBehavior * Existing record */ if ($this->manageId) { - $config->model = $this->relationModel->find($this->manageId); - $config->data = $this->relationObject->newPivotStatementForId($this->manageId)->first(); + $foreignKeyName = $this->relationModel->getQualifiedKeyName(); + $hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first(); - if (!$config->model || !$config->data) { + $config->model = $hydratedModel; + if (!$config->model) { throw new ApplicationException(Lang::get('backend::lang.model.not_found', [ 'class' => get_class($config->model), 'id' => $this->manageId ])); @@ -1222,4 +1149,134 @@ class RelationController extends ControllerBehavior return $this->sessionKey = FormHelper::getSessionKey(); } + + // + // Helpers + // + + + /** + * Determine the default buttons based on the model relationship type. + * @return string + */ + protected function evalToolbarButtons() + { + if ($buttons = $this->getConfig('view[toolbarButtons]')) { + return is_array($buttons) + ? $buttons + : array_map('trim', explode('|', $buttons)); + } + + switch ($this->relationType) { + case 'hasMany': + case 'belongsToMany': + return ['create', 'add', 'delete', 'remove']; + + case 'hasOne': + case 'belongsTo': + return ['create', 'update', 'link', 'delete', 'unlink']; + } + } + + /** + * Determine the view mode based on the model relationship type. + * @return string + */ + protected function evalViewMode() + { + if ($this->forceViewMode) { + return $this->forceViewMode; + } + + switch ($this->relationType) { + case 'hasMany': + case 'belongsToMany': + return 'multi'; + + case 'hasOne': + case 'belongsTo': + return 'single'; + } + } + + /** + * Determine the management mode based on the relation type and settings. + * @return string + */ + protected function evalManageMode() + { + if ($mode = post(self::PARAM_MODE)) { + return $mode; + } + + if ($this->forceManageMode) { + return $this->forceManageMode; + } + + switch ($this->eventTarget) { + case 'button-create': + case 'button-update': + return 'form'; + + case 'button-link': + return 'list'; + } + + switch ($this->relationType) { + case 'belongsTo': + return 'list'; + + case 'belongsToMany': + if (isset($this->config->pivot)) return 'pivot'; + elseif ($this->eventTarget == 'list') return 'form'; + else return 'list'; + + case 'hasOne': + case 'hasMany': + if ($this->eventTarget == 'button-add') return 'list'; + else return 'form'; + } + } + + /** + * Returns the configuration for a mode (view, manage, pivot) for an + * expected type (list, form). Uses fallback configuration. + */ + protected function makeConfigForMode($mode = 'view', $type = 'list') + { + $config = null; + + /* + * Look for $this->config->view['list'] + */ + if ( + isset($this->config->{$mode}) && + array_key_exists($type, $this->config->{$mode}) + ) { + $config = $this->config->{$mode}[$type]; + } + /* + * Look for $this->config->list + */ + elseif (isset($this->config->{$type})) { + $config = $this->config->{$type}; + } + + /* + * Apply substitutes: + * + * - view.list => manage.list + */ + if (!$config) { + + if ($mode == 'manage' && $type == 'list') { + return $this->makeConfigForMode('view', $type); + } + + throw new ApplicationException('Missing configuration for '.$mode.'.'.$type.' in RelationController definition '.$this->field); + } + + return $this->makeConfig($config); + } + } diff --git a/modules/backend/behaviors/relationcontroller/partials/_manage_form.htm b/modules/backend/behaviors/relationcontroller/partials/_manage_form.htm index a58ca69c4..1ec08321d 100644 --- a/modules/backend/behaviors/relationcontroller/partials/_manage_form.htm +++ b/modules/backend/behaviors/relationcontroller/partials/_manage_form.htm @@ -5,6 +5,7 @@ +