diff --git a/CHANGELOG.md b/CHANGELOG.md index 624e03bf0..35981416b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +* **Build 236** (2015-03-28) + - Default context of `manage` and `pivot` forms is now *create* and *update* respectively, instead of the old value *relation*. Use the `context` option to set it back to the old value (see Backend > Relations docs). + * **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). diff --git a/README.md b/README.md index 304e6c7c0..5eb0cbe28 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ October's mission is to show the world that web development is not rocket scienc ### Learning October -The best place to learn October is by [reading the documentation](http://octobercms.com/docs). +The best place to learn October is by [reading the documentation](http://octobercms.com/docs) or [the resources page](http://octobercms.com/resources). ### Installing October @@ -50,4 +50,4 @@ The OctoberCMS platform is open-sourced software licensed under the [MIT license ### Contributing -Before sending a Pull Request, be sure to review the [Contributing Guidelines](CONTRIBUTING.md) first. \ No newline at end of file +Before sending a Pull Request, be sure to review the [Contributing Guidelines](CONTRIBUTING.md) first. diff --git a/modules/backend/assets/css/controls.css b/modules/backend/assets/css/controls.css index f5343b3fd..f3889c72b 100644 --- a/modules/backend/assets/css/controls.css +++ b/modules/backend/assets/css/controls.css @@ -791,8 +791,7 @@ ul.status-list li span.status.info{background:#5bc0de} .control-breadcrumb li a:hover{color:#ecf0f1} .control-breadcrumb li:after{font-size:14px;line-height:14px;display:inline-block;margin-left:6px;margin-right:2px;vertical-align:baseline;color:#9da3a7;font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;*margin-right:.3em;content:"\f105"} .control-breadcrumb li:last-child:after{content:''} -.control-breadcrumb + .content-tabs,.control-breadcrumb + .padded-container{margin-top:-20px} -.control-breadcrumb.breadcrumb-flush{margin-bottom:0} +body.breadcrumb-flush .control-breadcrumb,.control-breadcrumb.breadcrumb-flush{margin-bottom:0} body.slim-container .control-breadcrumb{margin-left:0;margin-right:0} body.compact-container .control-breadcrumb{margin-top:0;margin-left:0;margin-right:0} div.control-popover{position:absolute;background-clip:content-box;left:0;top:0;z-index:570;visibility:hidden} @@ -1180,4 +1179,4 @@ div.control-scrollpad > .scrollpad-scrollbar:hover{opacity:0.7;-webkit-transitio div.control-scrollpad > .scrollpad-scrollbar[data-visible]{opacity:0.7} div.control-scrollpad > .scrollpad-scrollbar[data-hidden]{display:none} div.control-scrollpad[data-direction=horizontal] > .scrollpad-scrollbar{top:auto;left:0;width:auto;height:11px} -div.control-scrollpad[data-direction=horizontal] > .scrollpad-scrollbar .drag-handle{right:auto;top:2px;height:7px;min-height:0;min-width:10px;width:auto} \ No newline at end of file +div.control-scrollpad[data-direction=horizontal] > .scrollpad-scrollbar .drag-handle{right:auto;top:2px;height:7px;min-height:0;min-width:10px;width:auto} diff --git a/modules/backend/assets/js/october-min.js b/modules/backend/assets/js/october-min.js index ecdcfb29b..adfe0a18a 100644 --- a/modules/backend/assets/js/october-min.js +++ b/modules/backend/assets/js/october-min.js @@ -261,17 +261,18 @@ $el.on('oc.triggerOn.update',function(e){e.stopPropagation() self.onConditionChanged()}) self.onConditionChanged()} TriggerOn.prototype.onConditionChanged=function(){if(this.triggerCondition=='checked'){this.updateTarget($(this.options.trigger+':checked',this.triggerParent).length>0)} -else if(this.triggerCondition=='value'){var trigger=$(this.options.trigger+':checked',this.triggerParent);if(trigger.length){this.updateTarget(trigger.val()==this.triggerConditionValue)}else{this.updateTarget($(this.options.trigger,this.triggerParent).val()==this.triggerConditionValue)}}} +else if(this.triggerCondition=='value'){var trigger=$(this.options.trigger+':checked',this.triggerParent);if(trigger.length){this.updateTarget(trigger.val()==this.triggerConditionValue)} +else{this.updateTarget($(this.options.trigger,this.triggerParent).val()==this.triggerConditionValue)}}} TriggerOn.prototype.updateTarget=function(status){if(this.options.triggerAction=='show') -this.$el.toggleClass('hide',!status).trigger('hide',[!status]) +this.$el.toggleClass('hide',!status).trigger('hide.oc.triggerapi',[!status]) else if(this.options.triggerAction=='hide') -this.$el.toggleClass('hide',status).trigger('hide',[status]) +this.$el.toggleClass('hide',status).trigger('hide.oc.triggerapi',[status]) else if(this.options.triggerAction=='enable') -this.$el.prop('disabled',!status).trigger('disable',[!status]).toggleClass('control-disabled',!status) +this.$el.prop('disabled',!status).trigger('disable.oc.triggerapi',[!status]).toggleClass('control-disabled',!status) else if(this.options.triggerAction=='disable') -this.$el.prop('disabled',status).trigger('disable',[status]).toggleClass('control-disabled',status) +this.$el.prop('disabled',status).trigger('disable.oc.triggerapi',[status]).toggleClass('control-disabled',status) else if(this.options.triggerAction=='empty'&&status) -this.$el.trigger('empty').val('') +this.$el.trigger('empty.oc.triggerapi').val('') if(this.options.triggerAction=='show'||this.options.triggerAction=='hide') this.fixButtonClasses() $(window).trigger('resize')} @@ -583,7 +584,9 @@ this.$el.on('modified.oc.tab',function(ev){ev.preventDefault() self.modifyTab($(ev.target).closest('ul.nav-tabs > li, div.tab-content > div'))}) this.$el.on('unmodified.oc.tab',function(ev){ev.preventDefault() self.unmodifyTab($(ev.target).closest('ul.nav-tabs > li, div.tab-content > div'))}) -this.$tabsContainer.on('shown.bs.tab','li',function(){$(window).trigger('oc.updateUi')}) +this.$tabsContainer.on('shown.bs.tab','li',function(){$(window).trigger('oc.updateUi') +var tabUrl=$('> a',this).data('tabUrl') +if(tabUrl){window.history.replaceState({},'Tab link reference',tabUrl)}}) if(this.options.slidable){this.$pagesContainer.touchwipe({wipeRight:function(){self.prev();},wipeLeft:function(){self.next();},preventDefaultEvents:false,min_move_x:60});} this.$tabsContainer.toolbar({scrollClassContainer:this.$el}) this.updateClasses()} diff --git a/modules/backend/assets/js/october.tab.js b/modules/backend/assets/js/october.tab.js index 7154eb71d..a4d5bf344 100644 --- a/modules/backend/assets/js/october.tab.js +++ b/modules/backend/assets/js/october.tab.js @@ -107,8 +107,13 @@ }) this.$tabsContainer.on('shown.bs.tab', 'li', function(){ - // self.$tabsContainer.dragScroll('fixScrollClasses') - $(window).trigger('oc.updateUi') + // self.$tabsContainer.dragScroll('fixScrollClasses') + $(window).trigger('oc.updateUi') + + var tabUrl = $('> a', this).data('tabUrl') + if (tabUrl) { + window.history.replaceState({}, 'Tab link reference', tabUrl) + } }) if (this.options.slidable) { diff --git a/modules/backend/assets/js/october.triggerapi.js b/modules/backend/assets/js/october.triggerapi.js index c0be46ff8..f9af7a5a9 100644 --- a/modules/backend/assets/js/october.triggerapi.js +++ b/modules/backend/assets/js/october.triggerapi.js @@ -81,7 +81,8 @@ var trigger = $(this.options.trigger + ':checked', this.triggerParent); if (trigger.length) { this.updateTarget(trigger.val() == this.triggerConditionValue) - } else { + } + else { this.updateTarget($(this.options.trigger, this.triggerParent).val() == this.triggerConditionValue) } } @@ -89,15 +90,15 @@ TriggerOn.prototype.updateTarget = function(status) { if (this.options.triggerAction == 'show') - this.$el.toggleClass('hide', !status).trigger('hide', [!status]) + this.$el.toggleClass('hide', !status).trigger('hide.oc.triggerapi', [!status]) else if (this.options.triggerAction == 'hide') - this.$el.toggleClass('hide', status).trigger('hide', [status]) + this.$el.toggleClass('hide', status).trigger('hide.oc.triggerapi', [status]) else if (this.options.triggerAction == 'enable') - this.$el.prop('disabled', !status).trigger('disable', [!status]).toggleClass('control-disabled', !status) + this.$el.prop('disabled', !status).trigger('disable.oc.triggerapi', [!status]).toggleClass('control-disabled', !status) else if (this.options.triggerAction == 'disable') - this.$el.prop('disabled', status).trigger('disable', [status]).toggleClass('control-disabled', status) + this.$el.prop('disabled', status).trigger('disable.oc.triggerapi', [status]).toggleClass('control-disabled', status) else if (this.options.triggerAction == 'empty' && status) - this.$el.trigger('empty').val('') + this.$el.trigger('empty.oc.triggerapi').val('') if (this.options.triggerAction == 'show' || this.options.triggerAction == 'hide') this.fixButtonClasses() diff --git a/modules/backend/assets/less/controls/breadcrumb.less b/modules/backend/assets/less/controls/breadcrumb.less index c52445643..348005d2c 100644 --- a/modules/backend/assets/less/controls/breadcrumb.less +++ b/modules/backend/assets/less/controls/breadcrumb.less @@ -39,15 +39,12 @@ content:''; } } +} - + .content-tabs, + .padded-container { - margin-top: -20px; - } - - // Breadcrumb to sit flush to the element below - &.breadcrumb-flush { - margin-bottom: 0; - } +// Breadcrumb to sit flush to the element below +body.breadcrumb-flush .control-breadcrumb, +.control-breadcrumb.breadcrumb-flush { + margin-bottom: 0; } body.slim-container { diff --git a/modules/backend/assets/less/october.less b/modules/backend/assets/less/october.less index 5ead03bf1..ca2803d1c 100644 --- a/modules/backend/assets/less/october.less +++ b/modules/backend/assets/less/october.less @@ -1,3 +1,15 @@ +// +// Z-Index frequencies: +// +// 0-200 - Base layer (content) +// 200-400 - Base menus / dropdowns +// 400-600 - Secondary layer (full screen) +// 600-800 - Secondary menus / dropdowns +// 800-1000 - Tertiary layer (popups) +// 1000-1200 - Tertiary menus / dropdowns +// 1200-9000 - Reserved for frequency manager +// + // // Combines layout and vendor styles // diff --git a/modules/backend/behaviors/RelationController.php b/modules/backend/behaviors/RelationController.php index ac10a4d98..0c5d97140 100644 --- a/modules/backend/behaviors/RelationController.php +++ b/modules/backend/behaviors/RelationController.php @@ -154,6 +154,11 @@ class RelationController extends ControllerBehavior */ protected $manageId; + /** + * @var int Foeign id of a selected pivot record. + */ + protected $foreignId; + /** * @var string Active session key, used for deferred bindings. */ @@ -187,6 +192,69 @@ class RelationController extends ControllerBehavior $this->config = $this->originalConfig = $this->makeConfig($controller->relationConfig, $this->requiredConfig); } + /** + * Validates the supplied field and initializes the relation manager. + * @param string $field The relationship field. + * @return string The active field name. + */ + protected function validateField($field = null) + { + $field = $field ?: post(self::PARAM_FIELD); + + if ($field && $field != $this->field) { + $this->initRelation($this->model, $field); + } + + if (!$field && !$this->field) { + throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field'))); + } + + return $field ?: $this->field; + } + + /** + * Prepares the view data. + * @return void + */ + public function prepareVars() + { + $this->vars['relationManageId'] = $this->manageId; + $this->vars['relationLabel'] = $this->config->label ?: $this->field; + $this->vars['relationField'] = $this->field; + $this->vars['relationType'] = $this->relationType; + $this->vars['relationSearchWidget'] = $this->searchWidget; + $this->vars['relationToolbarWidget'] = $this->toolbarWidget; + $this->vars['relationManageMode'] = $this->manageMode; + $this->vars['relationManageWidget'] = $this->manageWidget; + $this->vars['relationToolbarButtons'] = $this->toolbarButtons; + $this->vars['relationViewMode'] = $this->viewMode; + $this->vars['relationViewWidget'] = $this->viewWidget; + $this->vars['relationViewModel'] = $this->viewModel; + $this->vars['relationPivotWidget'] = $this->pivotWidget; + $this->vars['relationSessionKey'] = $this->relationGetSessionKey(); + } + + /** + * The controller action is responsible for supplying the parent model + * so it's action must be fired. Additionally, each AJAX request must + * supply the relation's field name (_relation_field). + */ + protected function beforeAjax() + { + if ($this->initialized) { + return; + } + + $this->controller->pageAction(); + $this->validateField(); + $this->prepareVars(); + $this->initialized = true; + } + + // + // Interface + // + /** * Prepare the widgets used by this behavior * @param Model $model @@ -242,6 +310,7 @@ class RelationController extends ControllerBehavior $this->viewMode = $this->evalViewMode(); $this->manageMode = $this->evalManageMode(); $this->manageId = post('manage_id'); + $this->foreignId = post('foreign_id'); /* * Toolbar widget @@ -354,65 +423,6 @@ class RelationController extends ControllerBehavior return $this->relationRender($field, ['section' => 'view']); } - /** - * Validates the supplied field and initializes the relation manager. - * @param string $field The relationship field. - * @return string The active field name. - */ - protected function validateField($field = null) - { - $field = $field ?: post(self::PARAM_FIELD); - - if ($field && $field != $this->field) { - $this->initRelation($this->model, $field); - } - - if (!$field && !$this->field) { - throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field'))); - } - - return $field ?: $this->field; - } - - /** - * Prepares the view data. - * @return void - */ - public function prepareVars() - { - $this->vars['relationManageId'] = $this->manageId; - $this->vars['relationLabel'] = $this->config->label ?: $this->field; - $this->vars['relationField'] = $this->field; - $this->vars['relationType'] = $this->relationType; - $this->vars['relationSearchWidget'] = $this->searchWidget; - $this->vars['relationToolbarWidget'] = $this->toolbarWidget; - $this->vars['relationManageMode'] = $this->manageMode; - $this->vars['relationManageWidget'] = $this->manageWidget; - $this->vars['relationToolbarButtons'] = $this->toolbarButtons; - $this->vars['relationViewMode'] = $this->viewMode; - $this->vars['relationViewWidget'] = $this->viewWidget; - $this->vars['relationViewModel'] = $this->viewModel; - $this->vars['relationPivotWidget'] = $this->pivotWidget; - $this->vars['relationSessionKey'] = $this->relationGetSessionKey(); - } - - /** - * The controller action is responsible for supplying the parent model - * so it's action must be fired. Additionally, each AJAX request must - * supply the relation's field name (_relation_field). - */ - protected function beforeAjax() - { - if ($this->initialized) { - return; - } - - $this->controller->pageAction(); - $this->validateField(); - $this->prepareVars(); - $this->initialized = true; - } - /** * Controller accessor for making partials within this behavior. * @param string $partial @@ -449,39 +459,313 @@ class RelationController extends ControllerBehavior } /** - * Returns the existing record IDs for the relation. + * Returns the active session key. */ - protected function findExistingRelationIds($checkIds = null) + public function relationGetSessionKey($force = false) { - $foreignKeyName = $this->relationModel->getQualifiedKeyName(); - - $results = $this->relationObject - ->getBaseQuery() - ->select($foreignKeyName); - - if ($checkIds !== null && is_array($checkIds) && count($checkIds)) { - $results = $results->whereIn($foreignKeyName, $checkIds); + if ($this->sessionKey && !$force) { + return $this->sessionKey; } - return $results->lists($foreignKeyName); + if (post('_relation_session_key')) { + return $this->sessionKey = post('_relation_session_key'); + } + + if (post('_session_key')) { + return $this->sessionKey = post('_session_key'); + } + + return $this->sessionKey = FormHelper::getSessionKey(); } // - // Overrides + // Widgets // - /** - * Controller override: Extend the query used for populating the list - * after the default query is processed. - * @param October\Rain\Database\Builder $query - * @param string $field - */ - public function relationExtendQuery($query, $field) + protected function makeSearchWidget() { + $config = $this->makeConfig(); + $config->alias = $this->alias . 'ManageSearch'; + $config->growable = false; + $config->prompt = 'backend::lang.list.search_prompt'; + $widget = $this->makeWidget('Backend\Widgets\Search', $config); + $widget->cssClasses[] = 'recordfinder-search'; + return $widget; } - public function relationExtendRefreshResults($field) + protected function makeToolbarWidget() { + $defaultConfig = []; + + /* + * Add buttons to toolbar + */ + $defaultButtons = null; + + if (!$this->readOnly) { + $defaultButtons = '~/modules/backend/behaviors/relationcontroller/partials/_toolbar.htm'; + } + + $defaultConfig['buttons'] = $this->getConfig('view[toolbarPartial]', $defaultButtons); + + /* + * Make config + */ + $toolbarConfig = $this->makeConfig($this->getConfig('toolbar', $defaultConfig)); + $toolbarConfig->alias = $this->alias . 'Toolbar'; + + /* + * Add search to toolbar + */ + $useSearch = $this->viewMode == 'multi' && $this->getConfig('view[showSearch]'); + + if ($useSearch) { + $toolbarConfig->search = [ + 'prompt' => 'backend::lang.list.search_prompt' + ]; + } + + /* + * No buttons, no search should mean no toolbar + */ + if (empty($toolbarConfig->search) && empty($toolbarConfig->buttons)) + return; + + $toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig); + $toolbarWidget->cssClasses[] = 'list-header'; + + return $toolbarWidget; + } + + protected function makeViewWidget() + { + /* + * Multiple (has many, belongs to many) + */ + if ($this->viewMode == 'multi') { + $config = $this->makeConfigForMode('view', 'list'); + $config->model = $this->relationModel; + $config->alias = $this->alias . 'ViewList'; + $config->showSorting = $this->getConfig('view[showSorting]', true); + $config->defaultSort = $this->getConfig('view[defaultSort]'); + $config->recordsPerPage = $this->getConfig('view[recordsPerPage]'); + $config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly); + $config->recordUrl = $this->getConfig('view[recordUrl]', null); + + $defaultOnClick = sprintf( + "$.oc.relationBehavior.clickViewListRecord(':id', '%s', '%s')", + $this->field, + $this->relationGetSessionKey() + ); + + if ($config->recordUrl) { + $defaultOnClick = null; + } + + $config->recordOnClick = $this->getConfig('view[recordOnClick]', $defaultOnClick); + + if ($emptyMessage = $this->getConfig('emptyMessage')) { + $config->noRecordsMessage = $emptyMessage; + } + + /* + * Constrain the query by the relationship and deferred items + */ + $widget = $this->makeWidget('Backend\Widgets\Lists', $config); + $widget->bindEvent('list.extendQuery', function ($query) { + $this->controller->relationExtendQuery($query, $this->field); + + $this->relationObject->setQuery($query); + if ($sessionKey = $this->relationGetSessionKey()) { + $this->relationObject->withDeferred($sessionKey); + } + elseif ($this->model->exists) { + $this->relationObject->addConstraints(); + } + + /* + * Allows pivot data to enter the fray + */ + if ($this->relationType == 'belongsToMany') { + $this->relationObject->setQuery($query->getQuery()); + return $this->relationObject; + } + }); + + /* + * Constrain the list by the search widget, if available + */ + if ($this->toolbarWidget && $this->getConfig('view[showSearch]')) { + if ($searchWidget = $this->toolbarWidget->getSearchWidget()) { + $searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) { + $widget->setSearchTerm($searchWidget->getActiveTerm()); + return $widget->onRefresh(); + }); + + $searchWidget->setActiveTerm(null); + } + } + } + /* + * Single (belongs to, has one) + */ + elseif ($this->viewMode == 'single') { + $query = $this->relationObject; + $this->controller->relationExtendQuery($query, $this->field); + $this->viewModel = $query->getResults() ?: $this->relationModel; + + $config = $this->makeConfigForMode('view', 'form'); + $config->model = $this->viewModel; + $config->arrayName = class_basename($this->relationModel); + $config->context = 'relation'; + $config->alias = $this->alias . 'ViewForm'; + + $widget = $this->makeWidget('Backend\Widgets\Form', $config); + $widget->previewMode = true; + } + + return $widget; + } + + protected function makeManageWidget() + { + $widget = null; + + /* + * List / Pivot + */ + if ($this->manageMode == 'list' || $this->manageMode == 'pivot') { + $isPivot = $this->manageMode == 'pivot'; + + $config = $this->makeConfigForMode('manage', 'list'); + $config->model = $this->relationModel; + $config->alias = $this->alias . 'ManageList'; + $config->showSetup = false; + $config->showCheckboxes = $this->getConfig('manage[showCheckboxes]', !$isPivot); + $config->showSorting = $this->getConfig('manage[showSorting]', !$isPivot); + $config->defaultSort = $this->getConfig('manage[defaultSort]'); + $config->recordsPerPage = $this->getConfig('manage[recordsPerPage]'); + + if ($this->viewMode == 'single') { + $config->showCheckboxes = false; + $config->recordOnClick = sprintf( + "$.oc.relationBehavior.clickManageListRecord(:id, '%s', '%s')", + $this->field, + $this->relationGetSessionKey() + ); + } + elseif ($config->showCheckboxes) { + $config->recordOnClick = "$.oc.relationBehavior.toggleListCheckbox(this)"; + } + elseif ($isPivot) { + $config->recordOnClick = sprintf( + "$.oc.relationBehavior.clickManagePivotListRecord(:id, '%s', '%s')", + $this->field, + $this->relationGetSessionKey() + ); + } + + $widget = $this->makeWidget('Backend\Widgets\Lists', $config); + + /* + * Link the Search Widget to the List Widget + */ + if ($this->getConfig('manage[showSearch]')) { + $this->searchWidget = $this->makeSearchWidget(); + $this->searchWidget->bindToController(); + $this->searchWidget->bindEvent('search.submit', function () use ($widget) { + $widget->setSearchTerm($this->searchWidget->getActiveTerm()); + return $widget->onRefresh(); + }); + + $this->searchWidget->setActiveTerm(null); + } + } + /* + * Form + */ + elseif ($this->manageMode == 'form') { + + $config = $this->makeConfigForMode('manage', 'form'); + $config->model = $this->relationModel; + $config->arrayName = class_basename($this->relationModel); + $config->context = $this->evalFormContext('manage', !!$this->manageId); + $config->alias = $this->alias . 'ManageForm'; + + /* + * Existing record + */ + if ($this->manageId) { + $config->model = $config->model->find($this->manageId); + if (!$config->model) { + throw new ApplicationException(Lang::get('backend::lang.model.not_found', [ + 'class' => get_class($config->model), 'id' => $this->manageId + ])); + } + } + + $widget = $this->makeWidget('Backend\Widgets\Form', $config); + } + + if (!$widget) { + return null; + } + + /* + * Exclude existing relationships + */ + if ($this->manageMode == 'pivot' || $this->manageMode == 'list') { + $widget->bindEvent('list.extendQuery', function ($query) { + $this->controller->relationExtendQuery($query, $this->field); + + /* + * Where not in the current list of related records + */ + $existingIds = $this->findExistingRelationIds(); + if (count($existingIds)) { + $query->whereNotIn($this->relationModel->getQualifiedKeyName(), $existingIds); + } + + }); + } + + return $widget; + } + + protected function makePivotWidget() + { + $config = $this->makeConfigForMode('pivot', 'form'); + $config->model = $this->relationModel; + $config->arrayName = class_basename($this->relationModel); + $config->context = $this->evalFormContext('pivot', !!$this->manageId); + $config->alias = $this->alias . 'ManagePivotForm'; + + /* + * Existing record + */ + if ($this->manageId) { + $foreignKeyName = $this->relationModel->getQualifiedKeyName(); + $hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first(); + + $config->model = $hydratedModel; + if (!$config->model) { + throw new ApplicationException(Lang::get('backend::lang.model.not_found', [ + 'class' => get_class($config->model), 'id' => $this->manageId + ])); + } + } + else { + if ($this->foreignId && ($foreignModel = $this->relationModel->find($this->foreignId))) { + $foreignModel->exists = false; + $config->model = $foreignModel; + } + + $pivotModel = $this->relationObject->newPivot(); + $config->model->setRelation('pivot', $pivotModel); + } + + $widget = $this->makeWidget('Backend\Widgets\Form', $config); + return $widget; } // @@ -775,11 +1059,19 @@ class RelationController extends ControllerBehavior return $this->relationRefresh(); } + /** + * Add multiple items using a single pivot form. + */ + public function onRelationManageAddPivot() + { + return $this->onRelationManagePivotForm(); + } + public function onRelationManagePivotForm() { $this->beforeAjax(); - $this->vars['foreignId'] = post('foreign_id'); + $this->vars['foreignId'] = $this->foreignId ?: post('checked'); return $this->relationMakePartial('pivot_form'); } @@ -787,18 +1079,24 @@ class RelationController extends ControllerBehavior { $this->beforeAjax(); - $foreignId = post('foreign_id'); - $foreignModel = $this->relationModel->find($foreignId); - $saveData = $this->pivotWidget->getSaveData(); + /* + * Add the checked IDs to the pivot table + */ + $foreignIds = (array) $this->foreignId; + $this->relationObject->sync($foreignIds, false); /* - * Check for existing relation + * Save data to models */ $foreignKeyName = $this->relationModel->getQualifiedKeyName(); - $existing = $this->relationObject->where($foreignKeyName, $foreignId)->count(); + $hyrdatedModels = $this->relationObject->whereIn($foreignKeyName, $foreignIds)->get(); + $saveData = $this->pivotWidget->getSaveData(); - if (!$existing) { - $this->relationObject->add($foreignModel, null, $saveData); + foreach ($hyrdatedModels as $hydratedModel) { + $modelsToSave = $this->prepareModelsToSave($hydratedModel, $saveData); + foreach ($modelsToSave as $modelToSave) { + $modelToSave->save(); + } } return ['#'.$this->relationGetId('view') => $this->relationRenderView()]; @@ -821,339 +1119,44 @@ class RelationController extends ControllerBehavior } // - // Widgets + // Overrides // - protected function makeSearchWidget() - { - $config = $this->makeConfig(); - $config->alias = $this->alias . 'ManageSearch'; - $config->growable = false; - $config->prompt = 'backend::lang.list.search_prompt'; - $widget = $this->makeWidget('Backend\Widgets\Search', $config); - $widget->cssClasses[] = 'recordfinder-search'; - return $widget; - } - - protected function makeToolbarWidget() - { - $defaultConfig = []; - - /* - * Add buttons to toolbar - */ - $defaultButtons = null; - - if (!$this->readOnly) { - $defaultButtons = '~/modules/backend/behaviors/relationcontroller/partials/_toolbar.htm'; - } - - $defaultConfig['buttons'] = $this->getConfig('view[toolbarPartial]', $defaultButtons); - - /* - * Make config - */ - $toolbarConfig = $this->makeConfig($this->getConfig('toolbar', $defaultConfig)); - $toolbarConfig->alias = $this->alias . 'Toolbar'; - - /* - * Add search to toolbar - */ - $useSearch = $this->viewMode == 'multi' && $this->getConfig('view[showSearch]'); - - if ($useSearch) { - $toolbarConfig->search = [ - 'prompt' => 'backend::lang.list.search_prompt' - ]; - } - - /* - * No buttons, no search should mean no toolbar - */ - if (empty($toolbarConfig->search) && empty($toolbarConfig->buttons)) - return; - - $toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig); - $toolbarWidget->cssClasses[] = 'list-header'; - - return $toolbarWidget; - } - - protected function makeViewWidget() - { - /* - * Multiple (has many, belongs to many) - */ - if ($this->viewMode == 'multi') { - $config = $this->makeConfigForMode('view', 'list'); - $config->model = $this->relationModel; - $config->alias = $this->alias . 'ViewList'; - $config->showSorting = $this->getConfig('view[showSorting]', true); - $config->defaultSort = $this->getConfig('view[defaultSort]'); - $config->recordsPerPage = $this->getConfig('view[recordsPerPage]'); - $config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly); - - $defaultOnClick = sprintf( - "$.oc.relationBehavior.clickViewListRecord(':id', '%s', '%s')", - $this->field, - $this->relationGetSessionKey() - ); - - $config->recordOnClick = $this->getConfig('view[recordOnClick]', $defaultOnClick); - $config->recordUrl = $this->getConfig('view[recordUrl]', null); - - if ($emptyMessage = $this->getConfig('emptyMessage')) { - $config->noRecordsMessage = $emptyMessage; - } - - /* - * Constrain the query by the relationship and deferred items - */ - $widget = $this->makeWidget('Backend\Widgets\Lists', $config); - $widget->bindEvent('list.extendQuery', function ($query) { - $this->controller->relationExtendQuery($query, $this->field); - - $this->relationObject->setQuery($query); - if ($sessionKey = $this->relationGetSessionKey()) { - $this->relationObject->withDeferred($sessionKey); - } - elseif ($this->model->exists) { - $this->relationObject->addConstraints(); - } - - /* - * Allows pivot data to enter the fray - */ - if ($this->relationType == 'belongsToMany') { - $this->relationObject->setQuery($query->getQuery()); - return $this->relationObject; - } - }); - - /* - * Constrain the list by the search widget, if available - */ - if ($this->toolbarWidget && $this->getConfig('view[showSearch]')) { - if ($searchWidget = $this->toolbarWidget->getSearchWidget()) { - $searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) { - $widget->setSearchTerm($searchWidget->getActiveTerm()); - return $widget->onRefresh(); - }); - - $searchWidget->setActiveTerm(null); - } - } - } - /* - * Single (belongs to, has one) - */ - elseif ($this->viewMode == 'single') { - $query = $this->relationObject; - $this->controller->relationExtendQuery($query, $this->field); - $this->viewModel = $query->getResults() ?: $this->relationModel; - - $config = $this->makeConfigForMode('view', 'form'); - $config->model = $this->viewModel; - $config->arrayName = class_basename($this->relationModel); - $config->context = 'relation'; - $config->alias = $this->alias . 'ViewForm'; - - $widget = $this->makeWidget('Backend\Widgets\Form', $config); - $widget->previewMode = true; - } - - return $widget; - } - - protected function makeManageWidget() - { - $widget = null; - /* - * Pivot - */ - if ($this->manageMode == 'pivot') { - $config = $this->makeConfigForMode('manage', 'list'); - $config->model = $this->relationModel; - $config->alias = $this->alias . 'ManagePivotList'; - $config->showSetup = false; - $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, - $this->relationGetSessionKey() - ); - - $widget = $this->makeWidget('Backend\Widgets\Lists', $config); - - /* - * Link the Search Widget to the List Widget - */ - if ($this->getConfig('pivot[showSearch]')) { - $this->searchWidget = $this->makeSearchWidget(); - $this->searchWidget->bindToController(); - $this->searchWidget->bindEvent('search.submit', function () use ($widget) { - $widget->setSearchTerm($this->searchWidget->getActiveTerm()); - return $widget->onRefresh(); - }); - - $this->searchWidget->setActiveTerm(null); - } - } - /* - * List - */ - elseif ($this->manageMode == 'list') { - $config = $this->makeConfigForMode('manage', 'list'); - $config->model = $this->relationModel; - $config->alias = $this->alias . 'ManageList'; - $config->showSetup = false; - $config->showCheckboxes = true; - $config->showSorting = $this->getConfig('manage[showSorting]', true); - $config->defaultSort = $this->getConfig('manage[defaultSort]'); - $config->recordsPerPage = $this->getConfig('manage[recordsPerPage]'); - - if ($this->viewMode == 'single') { - $config->showCheckboxes = false; - $config->recordOnClick = sprintf( - "$.oc.relationBehavior.clickManageListRecord(:id, '%s', '%s')", - $this->field, - $this->relationGetSessionKey() - ); - } - - $widget = $this->makeWidget('Backend\Widgets\Lists', $config); - - /* - * Link the Search Widget to the List Widget - */ - if ($this->getConfig('manage[showSearch]')) { - $this->searchWidget = $this->makeSearchWidget(); - $this->searchWidget->bindToController(); - $this->searchWidget->bindEvent('search.submit', function () use ($widget) { - $widget->setSearchTerm($this->searchWidget->getActiveTerm()); - return $widget->onRefresh(); - }); - - $this->searchWidget->setActiveTerm(null); - } - } - /* - * Form - */ - elseif ($this->manageMode == 'form') { - - /* - * Determine supplied form context - */ - $manageConfig = isset($this->config->manage) ? $this->config->manage : []; - - if ($context = array_get($manageConfig, 'context')) { - if (is_array($context)) { - $context = $this->manageId - ? array_get($context, 'update') - : array_get($context, 'create'); - } - } - - $config = $this->makeConfigForMode('manage', 'form'); - $config->model = $this->relationModel; - $config->arrayName = class_basename($this->relationModel); - $config->context = $context ?: 'relation'; - $config->alias = $this->alias . 'ManageForm'; - - /* - * Existing record - */ - if ($this->manageId) { - $config->model = $config->model->find($this->manageId); - if (!$config->model) { - throw new ApplicationException(Lang::get('backend::lang.model.not_found', [ - 'class' => get_class($config->model), 'id' => $this->manageId - ])); - } - } - - $widget = $this->makeWidget('Backend\Widgets\Form', $config); - } - - if (!$widget) { - return null; - } - - /* - * Exclude existing relationships - */ - if ($this->manageMode == 'pivot' || $this->manageMode == 'list') { - $widget->bindEvent('list.extendQuery', function ($query) { - $this->controller->relationExtendQuery($query, $this->field); - - /* - * Where not in the current list of related records - */ - $existingIds = $this->findExistingRelationIds(); - if (count($existingIds)) { - $query->whereNotIn($this->relationModel->getQualifiedKeyName(), $existingIds); - } - - }); - } - - return $widget; - } - - protected function makePivotWidget() - { - $config = $this->makeConfigForMode('pivot', 'form'); - $config->model = $this->relationModel; - $config->arrayName = class_basename($this->relationModel); - $config->context = 'relation'; - $config->alias = $this->alias . 'ManagePivotForm'; - - /* - * Existing record - */ - if ($this->manageId) { - $foreignKeyName = $this->relationModel->getQualifiedKeyName(); - $hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first(); - - $config->model = $hydratedModel; - if (!$config->model) { - throw new ApplicationException(Lang::get('backend::lang.model.not_found', [ - 'class' => get_class($config->model), 'id' => $this->manageId - ])); - } - } - - $widget = $this->makeWidget('Backend\Widgets\Form', $config); - return $widget; - } - /** - * Returns the active session key. + * Controller override: Extend the query used for populating the list + * after the default query is processed. + * @param October\Rain\Database\Builder $query + * @param string $field */ - public function relationGetSessionKey($force = false) + public function relationExtendQuery($query, $field) { - if ($this->sessionKey && !$force) { - return $this->sessionKey; - } + } - if (post('_relation_session_key')) { - return $this->sessionKey = post('_relation_session_key'); - } - - if (post('_session_key')) { - return $this->sessionKey = post('_session_key'); - } - - return $this->sessionKey = FormHelper::getSessionKey(); + public function relationExtendRefreshResults($field) + { } // // Helpers // + /** + * Returns the existing record IDs for the relation. + */ + protected function findExistingRelationIds($checkIds = null) + { + $foreignKeyName = $this->relationModel->getQualifiedKeyName(); + + $results = $this->relationObject + ->getBaseQuery() + ->select($foreignKeyName); + + if ($checkIds !== null && is_array($checkIds) && count($checkIds)) { + $results = $results->whereIn($foreignKeyName, $checkIds); + } + + return $results->lists($foreignKeyName); + } /** * Determine the default buttons based on the model relationship type. @@ -1238,6 +1241,28 @@ class RelationController extends ControllerBehavior } } + /** + * Determine supplied form context. + */ + protected function evalFormContext($mode = 'manage', $exists = false) + { + $config = isset($this->config->{$mode}) ? $this->config->{$mode} : []; + + if ($context = array_get($config, 'context')) { + if (is_array($context)) { + $context = $exists + ? array_get($context, 'update') + : array_get($context, 'create'); + } + } + + if (!$context) { + $context = $exists ? 'update' : 'create'; + } + + return $context; + } + /** * Returns the configuration for a mode (view, manage, pivot) for an * expected type (list, form). Uses fallback configuration. diff --git a/modules/backend/behaviors/relationcontroller/assets/js/october.relation.js b/modules/backend/behaviors/relationcontroller/assets/js/october.relation.js index b541f593d..581feb4ff 100644 --- a/modules/backend/behaviors/relationcontroller/assets/js/october.relation.js +++ b/modules/backend/behaviors/relationcontroller/assets/js/october.relation.js @@ -5,18 +5,8 @@ var RelationBehavior = function() { - this.clickManageListRecord = function(recordId, relationField, sessionKey) { - var oldPopup = $('#relationManagePopup') - - $.request('onRelationClickManageList', { - data: { - 'record_id': recordId, - '_relation_field': relationField, - '_session_key': sessionKey - } - }) - - oldPopup.popup('hide') + this.toggleListCheckbox = function(el) { + $(el).closest('.control-list').listWidget('toggleChecked', [el]) } this.clickViewListRecord = function(recordId, relationField, sessionKey) { @@ -33,6 +23,20 @@ }) } + this.clickManageListRecord = function(recordId, relationField, sessionKey) { + var oldPopup = $('#relationManagePopup') + + $.request('onRelationClickManageList', { + data: { + 'record_id': recordId, + '_relation_field': relationField, + '_session_key': sessionKey + } + }) + + oldPopup.popup('hide') + } + this.clickManagePivotListRecord = function(foreignId, relationField, sessionKey) { var oldPopup = $('#relationManagePivotPopup'), newPopup = $('') diff --git a/modules/backend/behaviors/relationcontroller/partials/_manage_pivot.htm b/modules/backend/behaviors/relationcontroller/partials/_manage_pivot.htm index 02f0ca616..6572dc1be 100644 --- a/modules/backend/behaviors/relationcontroller/partials/_manage_pivot.htm +++ b/modules/backend/behaviors/relationcontroller/partials/_manage_pivot.htm @@ -17,6 +17,18 @@