diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e055f65..13a85cc29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +* **Build 13x** (2014-08-xx) + - List widget has been refactored to improve efficiency. + - Added new list column type `nameFrom` (take name from X attribute) as an alternative to `select`. + * **Build 137** (2014-08-14) - Lists now support Filters (see Backend > Lists docs). - Numerous hard coded phrases converted to localized strings. diff --git a/modules/backend/classes/ListColumn.php b/modules/backend/classes/ListColumn.php index ea9d14346..7cc7ff096 100644 --- a/modules/backend/classes/ListColumn.php +++ b/modules/backend/classes/ListColumn.php @@ -41,7 +41,14 @@ class ListColumn public $sortable = true; /** - * @var string Custom SQL for selecting this record value. + * @var string Model attribute to use for the display value, this will + * override any $sqlSelect definition. + */ + public $nameFrom; + + /** + * @var string Custom SQL for selecting this record display value, + * the @ symbol is replaced with the table name. */ public $sqlSelect; @@ -103,6 +110,7 @@ class ListColumn if (isset($config['searchable'])) $this->searchable = $config['searchable']; if (isset($config['sortable'])) $this->sortable = $config['sortable']; if (isset($config['invisible'])) $this->invisible = $config['invisible']; + if (isset($config['nameFrom'])) $this->nameFrom = $config['nameFrom']; if (isset($config['select'])) $this->sqlSelect = $config['select']; if (isset($config['relation'])) $this->relation = $config['relation']; if (isset($config['format'])) $this->format = $config['format']; diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php index e6483cc11..9514aafb3 100644 --- a/modules/backend/lang/en/lang.php +++ b/modules/backend/lang/en/lang.php @@ -47,6 +47,7 @@ return [ 'menu_label' => 'Dashboard', 'widget_label' => 'Widget', 'widget_width' => 'Width', + 'full_width' => 'full width', 'add_widget' => 'Add widget', 'widget_inspector_title' => 'Widget configuration', 'widget_inspector_description' => 'Configure the report widget', @@ -119,15 +120,13 @@ return [ 'setup_title' => 'List Setup', 'setup_help' => 'Use checkboxes to select columns you want to see in the list. You can change position of columns by dragging them up or down.', 'records_per_page' => 'Records per page', - 'records_per_page_help' => 'Select the number of records per page to display. Please note that high number of records on a single page can reduce performance.', - 'apply_changes' => 'Apply changes', - 'cancel' => 'Cancel' + 'records_per_page_help' => 'Select the number of records per page to display. Please note that high number of records on a single page can reduce performance.' ], 'fileupload' => [ - 'attachment' => 'Attachment', - 'help' => 'Add a title and description for this attachment.', - 'title_label' => 'Title', - 'description_label' => 'Description' + 'attachment' => 'Attachment', + 'help' => 'Add a title and description for this attachment.', + 'title_label' => 'Title', + 'description_label' => 'Description' ], 'form' => [ 'create_title' => "New :name", @@ -163,6 +162,7 @@ return [ 'select' => 'Select', 'select_all' => 'all', 'select_none' => 'none', + 'select_placeholder' => 'please select', 'insert_row' => 'Insert Row', 'delete_row' => 'Delete Row' ], diff --git a/modules/backend/lang/it/lang.php b/modules/backend/lang/it/lang.php index b93286705..c339aae68 100644 --- a/modules/backend/lang/it/lang.php +++ b/modules/backend/lang/it/lang.php @@ -113,9 +113,7 @@ return [ 'setup_title' => 'Configura elenco', 'setup_help' => 'Utilizza le checkbox per selezionare le colonne che vuoi visualizzare nell\'elenco. Puoi cambiare la posizione delle colonne trascinandole verso l\'alto o il basso.', 'records_per_page' => 'Record per pagina', - 'records_per_page_help' => 'Seleziona il numero di record da visualizzare su ogni pagina. Ricorda che un numero elevato di record in una singola pagina può ridurre le prestazioni.', - 'apply_changes' => 'Applica modifiche', - 'cancel' => 'Annulla' + 'records_per_page_help' => 'Seleziona il numero di record da visualizzare su ogni pagina. Ricorda che un numero elevato di record in una singola pagina può ridurre le prestazioni.' ], 'form' => [ 'create_title' => "Nuovo :name", diff --git a/modules/backend/widgets/Lists.php b/modules/backend/widgets/Lists.php index a0cce838b..8c8216b73 100644 --- a/modules/backend/widgets/Lists.php +++ b/modules/backend/widgets/Lists.php @@ -265,9 +265,10 @@ class Lists extends WidgetBase protected function prepareModel() { $query = $this->model->newQuery(); - $selects = [$this->model->getTable().'.*']; - $tables = ['base'=>$this->model->getTable()]; + $primaryTable = $this->model->getTable(); + $selects = [$primaryTable.'.*']; $joins = []; + $withs = []; /* * Extensibility @@ -276,68 +277,129 @@ class Lists extends WidgetBase $this->fireEvent('list.extendQueryBefore', [$query]); /* - * Related custom selects, must come first + * Prepare searchable column names */ - foreach ($this->getVisibleListColumns() as $column) { - if (!isset($column->relation) || !isset($column->sqlSelect)) - continue; + $primarySearchable = []; + $relationSearchable = []; - if (!$this->model->hasRelation($column->relation)) - throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', ['class'=>get_class($this->model), 'relation'=>$column->relation])); + $columnsToSearch = []; + if (!empty($this->searchTerm) && ($searchableColumns = $this->getSearchableColumns())) { + foreach ($searchableColumns as $column) { + /* + * Related + */ + if ($this->isColumnRelated($column)) { + $table = $this->model->makeRelation($column->relation)->getTable(); + $columnName = isset($column->sqlSelect) + ? DbDongle::raw($this->parseTableName($column->sqlSelect, $table)) + : $table . '.' . $column->nameFrom; - $alias = Db::getQueryGrammar()->wrap($column->columnName); - $table = $this->model->makeRelation($column->relation)->getTable(); - $relationType = $this->model->getRelationType($column->relation); - $sqlSelect = $this->parseTableName($column->sqlSelect, $table); + $relationSearchable[$column->relation][] = $columnName; + } + /* + * Primary + */ + else { + $columnName = isset($column->sqlSelect) + ? DbDongle::raw($this->parseTableName($column->sqlSelect, $primaryTable)) + : $primaryTable . '.' . $column->columnName; - if (in_array($relationType, ['hasMany', 'belongsToMany', 'morphToMany', 'morphedByMany', 'morphMany', 'attachMany', 'hasManyThrough'])) - $selects[] = DbDongle::raw("group_concat(" . $sqlSelect . " separator ', ') as ". $alias); - else - $selects[] = DbDongle::raw($sqlSelect . ' as '. $alias); - - $joins[] = $column->relation; - $tables[$column->relation] = $table; + $primarySearchable[] = $columnName; + } + } } - if ($joins) - $query->joinWith(array_unique($joins), false); + /* + * Prepare related eager loads (withs) and custom selects (joins) + */ + foreach ($this->getVisibleListColumns() as $column) { + + if (!$this->isColumnRelated($column) || (!isset($column->sqlSelect) && !isset($column->nameFrom))) + continue; + + if (isset($column->nameFrom)) + $withs[] = $column->relation; + + $joins[] = $column->relation; + } + + /* + * Include any relation constraints + */ + if ($joins) { + foreach (array_unique($joins) as $join) { + /* + * Apply a supplied search term for relation columns and + * constrain the query only if there is something to search for + */ + $columnsToSearch = array_get($relationSearchable, $join, []); + + if (count($columnsToSearch) > 0) { + $query->whereHas($join, function($_query) use ($columnsToSearch) { + $_query->searchWhere($this->searchTerm, $columnsToSearch); + }); + } + } + } + + /* + * Add eager loads to the query + */ + if ($withs) { + $query->with(array_unique($withs)); + } /* * Custom select queries */ foreach ($this->getVisibleListColumns() as $column) { - if (!isset($column->sqlSelect) || isset($column->relation)) + if (!isset($column->sqlSelect)) continue; $alias = Db::getQueryGrammar()->wrap($column->columnName); - $sqlSelect = $this->parseTableName($column->sqlSelect, $tables['base']); - $selects[] = DbDongle::raw($sqlSelect . ' as '. $alias); + + /* + * Relation column + */ + if (isset($column->relation)) { + $table = $this->model->makeRelation($column->relation)->getTable(); + $relationType = $this->model->getRelationType($column->relation); + $sqlSelect = $this->parseTableName($column->sqlSelect, $table); + + /* + * Manipulate a count query for the sub query + */ + $relationObj = $this->model->{$column->relation}(); + $countQuery = $relationObj->getRelationCountQuery($relationObj->getRelated()->newQuery(), $query); + + $joinSql = $this->isColumnRelated($column, true) + ? DbDongle::raw("group_concat(" . $sqlSelect . " separator ', ')") + : DbDongle::raw($sqlSelect); + + $joinSql = $countQuery->select($joinSql)->toSql(); + + $selects[] = Db::raw("(".$joinSql.") as ".$alias); + } + /* + * Primary column + */ + else { + $sqlSelect = $this->parseTableName($column->sqlSelect, $primaryTable); + $selects[] = DbDongle::raw($sqlSelect . ' as '. $alias); + } } /* - * Handle a supplied search term + * Apply a supplied search term for primary columns */ - if (!empty($this->searchTerm) && ($searchableColumns = $this->getSearchableColumns())) { - $query->orWhere(function($innerQuery) use ($searchableColumns, $tables) { - $columnsToSearch = []; - foreach ($searchableColumns as $column) { - - if (isset($column->sqlSelect)) { - $table = (isset($column->relation)) ? $tables[$column->relation] : 'base'; - $columnName = DbDongle::raw($this->parseTableName($column->sqlSelect, $table)); - } - else - $columnName = $tables['base'] . '.' . $column->columnName; - - $columnsToSearch[] = $columnName; - } - - $innerQuery->searchWhere($this->searchTerm, $columnsToSearch); + if (count($primarySearchable) > 0) { + $query->orWhere(function($innerQuery) use ($primarySearchable) { + $innerQuery->searchWhere($this->searchTerm, $primarySearchable); }); } /* - * Handle sorting + * Apply sorting */ if ($sortColumn = $this->getSortColumn()) { $query->orderBy($sortColumn, $this->sortDirection); @@ -356,9 +418,7 @@ class Lists extends WidgetBase Event::fire('backend.list.extendQuery', [$this, $query]); $this->fireEvent('list.extendQuery', [$query]); - // Grouping due to the joinWith() call $query->select($selects); - $query->groupBy($this->model->getQualifiedKeyName()); return $query; } @@ -555,16 +615,33 @@ class Lists extends WidgetBase */ public function getColumnValue($record, $column) { + + $columnName = $column->columnName; + /* - * If the column is a relation, it will be a custom select, + * Handle taking name from model attribute. + */ + if ($column->nameFrom) { + if (!array_key_exists($columnName, $record->getRelations())) + $value = null; + elseif ($this->isColumnRelated($column, true)) + $value = implode(', ', $record->{$columnName}->lists($column->nameFrom)); + elseif ($this->isColumnRelated($column)) + $value = $record->{$columnName}->{$column->nameFrom}; + else + $value = $record->{$column->nameFrom}; + } + /* + * Otherwise, if the column is a relation, it will be a custom select, * so prevent the Model from attempting to load the relation * if the value is NULL. */ - $columnName = $column->columnName; - if ($record->hasRelation($columnName) && array_key_exists($columnName, $record->attributes)) - $value = $record->attributes[$columnName]; - else - $value = $record->{$columnName}; + else { + if ($record->hasRelation($columnName) && array_key_exists($columnName, $record->attributes)) + $value = $record->attributes[$columnName]; + else + $value = $record->{$columnName}; + } if (method_exists($this, 'eval'. studly_case($column->type) .'TypeValue')) $value = $this->{'eval'. studly_case($column->type) .'TypeValue'}($record, $column, $value); @@ -962,4 +1039,38 @@ class Lists extends WidgetBase return $this->onRefresh(); } + // + // Helpers + // + + /** + * Check if column refers to a relation of the model + * @param ListColumn $column List column object + * @param boolean $multi If set, returns true only if the relation is a "multiple relation type" + * @return boolean + */ + protected function isColumnRelated($column, $multi = false) + { + if (!isset($column->relation)) + return false; + + if (!$this->model->hasRelation($column->relation)) + throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', ['class'=>get_class($this->model), 'relation'=>$column->relation])); + + if (!$multi) + return true; + + $relationType = $this->model->getRelationType($column->relation); + + return in_array($relationType, [ + 'hasMany', + 'belongsToMany', + 'morphToMany', + 'morphedByMany', + 'morphMany', + 'attachMany', + 'hasManyThrough' + ]); + } + } \ No newline at end of file diff --git a/modules/backend/widgets/ReportContainer.php b/modules/backend/widgets/ReportContainer.php index 623649777..161048c88 100644 --- a/modules/backend/widgets/ReportContainer.php +++ b/modules/backend/widgets/ReportContainer.php @@ -117,7 +117,7 @@ class ReportContainer extends WidgetBase { $sizes = []; for ($i = 1; $i <= 10; $i++) - $sizes[$i] = $i < 10 ? $i : $i.' (full width)'; + $sizes[$i] = $i < 10 ? $i : $i.' (' . Lang::get('backend::lang.dashboard.full_width') . ')'; $this->vars['sizes'] = $sizes; $this->vars['widgets'] = WidgetManager::instance()->listReportWidgets(); diff --git a/modules/backend/widgets/lists/partials/_setup_form.htm b/modules/backend/widgets/lists/partials/_setup_form.htm index 0ae7b83eb..3d51f8d21 100644 --- a/modules/backend/widgets/lists/partials/_setup_form.htm +++ b/modules/backend/widgets/lists/partials/_setup_form.htm @@ -54,13 +54,13 @@ data-request="getEventHandler('onApplySetup') ?>" data-dismiss="popup" data-stripe-load-indicator> - + \ No newline at end of file diff --git a/modules/backend/widgets/reportcontainer/partials/_new_widget_popup.htm b/modules/backend/widgets/reportcontainer/partials/_new_widget_popup.htm index 6121d674d..e27c7410b 100644 --- a/modules/backend/widgets/reportcontainer/partials/_new_widget_popup.htm +++ b/modules/backend/widgets/reportcontainer/partials/_new_widget_popup.htm @@ -10,7 +10,7 @@