Samuel Georges 57b10704ce Support for overriding list views
For consistency, entry partial for controller behaviors should be called "container"
Added addViewPath() method to ViewMaker
Remove "fa" from icon docs (not necessary)
Fixes #2439
2016-10-29 14:10:21 +11:00

1463 lines
40 KiB
PHP

<?php namespace Backend\Widgets;
use Db;
use Html;
use App;
use Lang;
use Input;
use Event;
use Backend;
use DbDongle;
use Carbon\Carbon;
use October\Rain\Html\Helper as HtmlHelper;
use October\Rain\Router\Helper as RouterHelper;
use System\Helpers\DateTime as DateTimeHelper;
use System\Classes\PluginManager;
use Backend\Classes\ListColumn;
use Backend\Classes\WidgetBase;
use ApplicationException;
use October\Rain\Database\Model;
use DateTime;
/**
* List Widget
* Used for building back end lists, renders a list of model objects
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*/
class Lists extends WidgetBase
{
//
// Configurable properties
//
/**
* @var array List column configuration.
*/
public $columns;
/**
* @var Model List model object.
*/
public $model;
/**
* @var string Link for each record row. Replace :id with the record id.
*/
public $recordUrl;
/**
* @var string Click event for each record row. Replace :id with the record id.
*/
public $recordOnClick;
/**
* @var string Message to display when there are no records in the list.
*/
public $noRecordsMessage = 'backend::lang.list.no_records';
/**
* @var int Maximum rows to display for each page.
*/
public $recordsPerPage;
/**
* @var bool Shows the sorting options for each column.
*/
public $showSorting = true;
/**
* @var mixed A default sort column to look for.
*/
public $defaultSort;
/**
* @var bool Display a checkbox next to each record row.
*/
public $showCheckboxes = false;
/**
* @var bool Display the list set up used for column visibility and ordering.
*/
public $showSetup = false;
/**
* @var bool Display parent/child relationships in the list.
*/
public $showTree = false;
/**
* @var bool Expand the tree nodes by default.
*/
public $treeExpanded = false;
/**
* @var bool|string Display pagination when limiting records per page.
*/
public $showPagination = 'auto';
/**
* @var string Specify a custom view path to override partials used by the list.
*/
public $customViewPath;
//
// Object properties
//
/**
* {@inheritDoc}
*/
protected $defaultAlias = 'list';
/**
* @var array Collection of all list columns used in this list.
* @see Backend\Classes\ListColumn
*/
protected $allColumns;
/**
* @var array Override default columns with supplied key names.
*/
protected $columnOverride;
/**
* @var array Columns to display and their order.
*/
protected $visibleColumns;
/**
* @var array Model data collection.
*/
protected $records;
/**
* @var int Current page number.
*/
protected $currentPageNumber;
/**
* @var string Filter the records by a search term.
*/
protected $searchTerm;
/**
* @var string If searching the records, specifies a policy to use.
* - all: result must contain all words
* - any: result can contain any word
* - exact: result must contain the exact phrase
*/
protected $searchMode;
/**
* @var string Use a custom scope method for performing searches.
*/
protected $searchScope;
/**
* @var array Collection of functions to apply to each list query.
*/
protected $filterCallbacks = [];
/**
* @var array All sortable columns.
*/
protected $sortableColumns;
/**
* @var string Sets the list sorting column.
*/
protected $sortColumn;
/**
* @var string Sets the list sorting direction (asc, desc)
*/
protected $sortDirection;
/**
* @var array List of CSS classes to apply to the list container element
*/
public $cssClasses = [];
/**
* Initialize the widget, called by the constructor and free from its parameters.
*/
public function init()
{
$this->fillFromConfig([
'columns',
'model',
'recordUrl',
'recordOnClick',
'noRecordsMessage',
'recordsPerPage',
'showSorting',
'defaultSort',
'showCheckboxes',
'showSetup',
'showTree',
'treeExpanded',
'showPagination',
'customViewPath',
]);
/*
* Configure the list widget
*/
$this->recordsPerPage = $this->getSession('per_page', $this->recordsPerPage);
if ($this->showPagination == 'auto') {
$this->showPagination = $this->recordsPerPage && $this->recordsPerPage > 0;
}
if ($this->customViewPath) {
$this->addViewPath($this->customViewPath);
}
$this->validateModel();
$this->validateTree();
}
/**
* {@inheritDoc}
*/
protected function loadAssets()
{
$this->addJs('js/october.list.js', 'core');
}
/**
* Renders the widget.
*/
public function render()
{
$this->prepareVars();
return $this->makePartial('list-container');
}
/**
* Prepares the list data
*/
public function prepareVars()
{
$this->vars['cssClasses'] = implode(' ', $this->cssClasses);
$this->vars['columns'] = $this->getVisibleColumns();
$this->vars['columnTotal'] = $this->getTotalColumns();
$this->vars['records'] = $this->getRecords();
$this->vars['noRecordsMessage'] = trans($this->noRecordsMessage);
$this->vars['showCheckboxes'] = $this->showCheckboxes;
$this->vars['showSetup'] = $this->showSetup;
$this->vars['showPagination'] = $this->showPagination;
$this->vars['showSorting'] = $this->showSorting;
$this->vars['sortColumn'] = $this->getSortColumn();
$this->vars['sortDirection'] = $this->sortDirection;
$this->vars['showTree'] = $this->showTree;
$this->vars['treeLevel'] = 0;
if ($this->showPagination) {
$this->vars['recordTotal'] = $this->records->total();
$this->vars['pageCurrent'] = $this->records->currentPage();
$this->vars['pageLast'] = $this->records->lastPage();
$this->vars['pageFrom'] = $this->records->firstItem();
$this->vars['pageTo'] = $this->records->lastItem();
}
else {
$this->vars['recordTotal'] = $this->records->count();
$this->vars['pageCurrent'] = 1;
}
}
/**
* Event handler for refreshing the list.
*/
public function onRefresh()
{
$this->prepareVars();
return ['#'.$this->getId() => $this->makePartial('list')];
}
/**
* Event handler for switching the page number.
*/
public function onPaginate()
{
$this->currentPageNumber = post('page');
return $this->onRefresh();
}
/**
* Validate the supplied form model.
* @return void
*/
protected function validateModel()
{
if (!$this->model) {
throw new ApplicationException(Lang::get(
'backend::lang.list.missing_model',
['class'=>get_class($this->controller)]
));
}
if (!$this->model instanceof Model) {
throw new ApplicationException(Lang::get(
'backend::lang.model.invalid_class',
['model'=>get_class($this->model), 'class'=>get_class($this->controller)]
));
}
return $this->model;
}
/**
* Replaces the @ symbol with a table name in a model
* @param string $sql
* @param string $table
* @return string
*/
protected function parseTableName($sql, $table)
{
return str_replace('@', $table.'.', $sql);
}
/**
* Applies any filters to the model.
*/
public function prepareModel()
{
$query = $this->model->newQuery();
$primaryTable = $this->model->getTable();
$selects = [$primaryTable.'.*'];
$joins = [];
$withs = [];
/*
* Extensibility
*/
Event::fire('backend.list.extendQueryBefore', [$this, $query]);
$this->fireEvent('list.extendQueryBefore', [$query]);
/*
* Prepare searchable column names
*/
$primarySearchable = [];
$relationSearchable = [];
$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->valueFrom;
$relationSearchable[$column->relation][] = $columnName;
}
/*
* Primary
*/
else {
$columnName = isset($column->sqlSelect)
? DbDongle::raw($this->parseTableName($column->sqlSelect, $primaryTable))
: DbDongle::cast(Db::getTablePrefix() . $primaryTable . '.' . $column->columnName, 'TEXT');
$primarySearchable[] = $columnName;
}
}
}
/*
* Prepare related eager loads (withs) and custom selects (joins)
*/
foreach ($this->getVisibleColumns() as $column) {
if (!$this->isColumnRelated($column) || (!isset($column->sqlSelect) && !isset($column->valueFrom))) {
continue;
}
if (isset($column->valueFrom)) {
$withs[] = $column->relation;
}
$joins[] = $column->relation;
}
/*
* Add eager loads to the query
*/
if ($withs) {
$query->with(array_unique($withs));
}
/*
* Apply search term
*/
$query->where(function ($innerQuery) use ($primarySearchable, $relationSearchable, $joins) {
/*
* Search primary columns
*/
if (count($primarySearchable) > 0) {
$this->applySearchToQuery($innerQuery, $primarySearchable, 'or');
}
/*
* Search relation columns
*/
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) {
$innerQuery->orWhereHas($join, function ($_query) use ($columnsToSearch) {
$this->applySearchToQuery($_query, $columnsToSearch);
});
}
}
}
});
/*
* Custom select queries
*/
foreach ($this->getVisibleColumns() as $column) {
if (!isset($column->sqlSelect)) {
continue;
}
$alias = $query->getQuery()->getGrammar()->wrap($column->columnName);
/*
* Relation column
*/
if (isset($column->relation)) {
// @todo Find a way...
$relationType = $this->model->getRelationType($column->relation);
if ($relationType == 'morphTo') {
throw new ApplicationException('The relationship morphTo is not supported for list columns.');
}
$table = $this->model->makeRelation($column->relation)->getTable();
$sqlSelect = $this->parseTableName($column->sqlSelect, $table);
/*
* Manipulate a count query for the sub query
*/
$relationObj = $this->model->{$column->relation}();
$countQuery = $relationObj->getRelationCountQuery($relationObj->getRelated()->newQueryWithoutScopes(), $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);
}
}
/*
* Apply sorting
*/
if ($sortColumn = $this->getSortColumn()) {
if (($column = array_get($this->allColumns, $sortColumn)) && $column->valueFrom) {
$sortColumn = $this->isColumnPivot($column)
? 'pivot_' . $column->valueFrom
: $column->valueFrom;
}
$query->orderBy($sortColumn, $this->sortDirection);
}
/*
* Apply filters
*/
foreach ($this->filterCallbacks as $callback) {
$callback($query);
}
/*
* Add custom selects
*/
$query->select($selects);
/*
* Extensibility
*/
if (
($event = $this->fireEvent('list.extendQuery', [$query], true)) ||
($event = Event::fire('backend.list.extendQuery', [$this, $query], true))
) {
return $event;
}
return $query;
}
/**
* Returns all the records from the supplied model, after filtering.
* @return Collection
*/
protected function getRecords()
{
$model = $this->prepareModel();
if ($this->showTree) {
$records = $model->getNested();
}
elseif ($this->showPagination) {
$records = $model->paginate($this->recordsPerPage, $this->currentPageNumber);
}
else {
$records = $model->get();
}
return $this->records = $records;
}
/**
* Returns the record URL address for a list row.
* @param Model $record
* @return string
*/
public function getRecordUrl($record)
{
if (isset($this->recordOnClick)) {
return 'javascript:;';
}
if (!isset($this->recordUrl)) {
return null;
}
$data = $record->toArray();
$data += [$record->getKeyName() => $record->getKey()];
$columns = array_keys($data);
$url = RouterHelper::parseValues($data, $columns, $this->recordUrl);
return Backend::url($url);
}
/**
* Returns the onclick event for a list row.
* @param Model $record
* @return string
*/
public function getRecordOnClick($record)
{
if (!isset($this->recordOnClick)) {
return null;
}
$columns = array_keys($record->getAttributes());
$recordOnClick = RouterHelper::parseValues($record, $columns, $this->recordOnClick);
return Html::attributes(['onclick' => $recordOnClick]);
}
/**
* Get all the registered columns for the instance.
* @return array
*/
public function getColumns()
{
return $this->allColumns ?: $this->defineListColumns();
}
/**
* Get a specified column object
* @param string $column
* @return mixed
*/
public function getColumn($column)
{
return $this->allColumns[$column];
}
/**
* Returns the list columns that are visible by list settings or default
*/
public function getVisibleColumns()
{
$definitions = $this->defineListColumns();
$columns = [];
/*
* Supplied column list
*/
if ($this->columnOverride === null) {
$this->columnOverride = $this->getSession('visible', null);
}
if ($this->columnOverride && is_array($this->columnOverride)) {
$invalidColumns = array_diff($this->columnOverride, array_keys($definitions));
if (!count($definitions)) {
throw new ApplicationException(Lang::get(
'backend::lang.list.missing_column',
['columns'=>implode(',', $invalidColumns)]
));
}
$availableColumns = array_intersect($this->columnOverride, array_keys($definitions));
foreach ($availableColumns as $columnName) {
$definitions[$columnName]->invisible = false;
$columns[$columnName] = $definitions[$columnName];
}
}
/*
* Use default column list
*/
else {
foreach ($definitions as $columnName => $column) {
if ($column->invisible) {
continue;
}
$columns[$columnName] = $definitions[$columnName];
}
}
return $this->visibleColumns = $columns;
}
/**
* Builds an array of list columns with keys as the column name and values as a ListColumn object.
*/
protected function defineListColumns()
{
if (!isset($this->columns) || !is_array($this->columns) || !count($this->columns)) {
$class = get_class($this->model instanceof Model ? $this->model : $this->controller);
throw new ApplicationException(Lang::get('backend::lang.list.missing_columns', compact('class')));
}
$this->addColumns($this->columns);
/*
* Extensibility
*/
Event::fire('backend.list.extendColumns', [$this]);
$this->fireEvent('list.extendColumns');
/*
* Use a supplied column order
*/
if ($columnOrder = $this->getSession('order', null)) {
$orderedDefinitions = [];
foreach ($columnOrder as $column) {
if (isset($this->allColumns[$column])) {
$orderedDefinitions[$column] = $this->allColumns[$column];
}
}
$this->allColumns = array_merge($orderedDefinitions, $this->allColumns);
}
return $this->allColumns;
}
/**
* Programatically add columns, used internally and for extensibility.
* @param array $columns Column definitions
*/
public function addColumns(array $columns)
{
/*
* Build a final collection of list column objects
*/
foreach ($columns as $columnName => $config) {
$this->allColumns[$columnName] = $this->makeListColumn($columnName, $config);
}
}
/**
* Programatically remove a column, used for extensibility.
* @param string $column Column name
*/
public function removeColumn($columnName)
{
if (isset($this->allColumns[$columnName])) {
unset($this->allColumns[$columnName]);
}
}
/**
* Creates a list column object from it's name and configuration.
*/
protected function makeListColumn($name, $config)
{
if (is_string($config)) {
$label = $config;
}
elseif (isset($config['label'])) {
$label = $config['label'];
}
else {
$label = studly_case($name);
}
if (starts_with($name, 'pivot[') && strpos($name, ']') !== false) {
$_name = HtmlHelper::nameToArray($name);
$config['relation'] = array_shift($_name);
$config['valueFrom'] = array_shift($_name);
$config['searchable'] = false;
}
elseif (strpos($name, '[') !== false && strpos($name, ']') !== false) {
$config['valueFrom'] = $name;
$config['sortable'] = false;
$config['searchable'] = false;
}
$columnType = isset($config['type']) ? $config['type'] : null;
$column = new ListColumn($name, $label);
$column->displayAs($columnType, $config);
return $column;
}
/**
* Calculates the total columns used in the list, including checkboxes
* and other additions.
*/
protected function getTotalColumns()
{
$columns = $this->visibleColumns ?: $this->getVisibleColumns();
$total = count($columns);
if ($this->showCheckboxes) {
$total++;
}
if ($this->showSetup) {
$total++;
}
return $total;
}
/**
* Looks up the column header
*/
public function getHeaderValue($column)
{
$value = Lang::get($column->label);
/*
* Extensibility
*/
if ($response = Event::fire('backend.list.overrideHeaderValue', [$this, $column, $value], true)) {
$value = $response;
}
if ($response = $this->fireEvent('list.overrideHeaderValue', [$column, $value], true)) {
$value = $response;
}
return $value;
}
/**
* Returns a raw column value
* @return string
*/
public function getColumnValueRaw($record, $column)
{
$columnName = $column->columnName;
/*
* Handle taking value from model relation.
*/
if ($column->valueFrom && $column->relation) {
$columnName = $column->relation;
if (!array_key_exists($columnName, $record->getRelations())) {
$value = null;
}
elseif ($this->isColumnRelated($column, true)) {
$value = $record->{$columnName}->lists($column->valueFrom);
}
elseif ($this->isColumnRelated($column) || $this->isColumnPivot($column)) {
$value = $record->{$columnName} ? $record->{$columnName}->{$column->valueFrom} : null;
}
else {
$value = null;
}
}
/*
* Handle taking value from model attribute.
*/
elseif ($column->valueFrom) {
$keyParts = HtmlHelper::nameToArray($column->valueFrom);
$value = $record;
foreach ($keyParts as $key) {
$value = $value->{$key};
}
}
/*
* 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.
*/
else {
if ($record->hasRelation($columnName) && array_key_exists($columnName, $record->attributes)) {
$value = $record->attributes[$columnName];
}
else {
$value = $record->{$columnName};
}
}
return $value;
}
/**
* Returns a column value, with filters applied
* @return string
*/
public function getColumnValue($record, $column)
{
$value = $this->getColumnValueRaw($record, $column);
if (method_exists($this, 'eval'. studly_case($column->type) .'TypeValue')) {
$value = $this->{'eval'. studly_case($column->type) .'TypeValue'}($record, $column, $value);
}
else {
$value = $this->evalCustomListType($column->type, $record, $column, $value);
}
/*
* Apply default value.
*/
if ($value === '' || $value === null) {
$value = $column->defaults;
}
/*
* Extensibility
*/
if (($response = Event::fire('backend.list.overrideColumnValue', [$this, $record, $column, $value], true)) !== null) {
$value = $response;
}
if (($response = $this->fireEvent('list.overrideColumnValue', [$record, $column, $value], true)) !== null) {
$value = $response;
}
return $value;
}
/**
* Adds a custom CSS class string to a record row
* @param Model $record Populated model
* @return string
*/
public function getRowClass($record)
{
$value = '';
/*
* Extensibility
*/
if ($response = Event::fire('backend.list.injectRowClass', [$this, $record], true)) {
$value = $response;
}
if ($response = $this->fireEvent('list.injectRowClass', [$record], true)) {
$value = $response;
}
return $value;
}
//
// Value processing
//
/**
* Process a custom list types registered by plugins.
*/
protected function evalCustomListType($type, $record, $column, $value)
{
$plugins = PluginManager::instance()->getRegistrationMethodValues('registerListColumnTypes');
foreach ($plugins as $availableTypes) {
if (!isset($availableTypes[$type])) {
continue;
}
$callback = $availableTypes[$type];
if (is_callable($callback)) {
return call_user_func_array($callback, [$value, $column, $record]);
}
}
throw new ApplicationException(sprintf('List column type "%s" could not be found.', $type));
}
/**
* Process as text, escape the value
*/
protected function evalTextTypeValue($record, $column, $value)
{
if (is_array($value) && count($value) == count($value, COUNT_RECURSIVE)) {
$value = implode(', ', $value);
}
return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
}
/**
* Process as number, proxy to text
*/
protected function evalNumberTypeValue($record, $column, $value)
{
return $this->evalTextTypeValue($record, $column, $value);
}
/**
* Common mistake, relation is not a valid list column.
* @deprecated Remove if year >= 2018
*/
protected function evalRelationTypeValue($record, $column, $value)
{
traceLog(sprintf('Warning: List column type "relation" for class "%s" is not valid.', get_class($record)));
return $this->evalTextTypeValue($record, $column, $value);
}
/**
* Process as partial reference
*/
protected function evalPartialTypeValue($record, $column, $value)
{
return $this->controller->makePartial($column->path ?: $column->columnName, [
'listColumn' => $column,
'listRecord' => $record,
'listValue' => $value,
'column' => $column,
'record' => $record,
'value' => $value
]);
}
/**
* Process as boolean switch
*/
protected function evalSwitchTypeValue($record, $column, $value)
{
$contents = '';
if ($value) {
$contents = Lang::get('backend::lang.list.column_switch_true');
}
else {
$contents = Lang::get('backend::lang.list.column_switch_false');
}
return $contents;
}
/**
* Process as a datetime value
*/
protected function evalDatetimeTypeValue($record, $column, $value)
{
if ($value === null) {
return null;
}
$dateTime = $this->validateDateTimeValue($value, $column);
if ($column->format !== null) {
$value = $dateTime->format($column->format);
}
else {
$value = $dateTime->toDayDateTimeString();
}
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'format' => $column->format,
'formatAlias' => 'dateTimeLongMin'
]);
}
/**
* Process as a time value
*/
protected function evalTimeTypeValue($record, $column, $value)
{
if ($value === null) {
return null;
}
$dateTime = $this->validateDateTimeValue($value, $column);
$format = $column->format !== null ? $column->format : 'g:i A';
$value = $dateTime->format($format);
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'format' => $column->format,
'formatAlias' => 'time'
]);
}
/**
* Process as a date value
*/
protected function evalDateTypeValue($record, $column, $value)
{
if ($value === null) {
return null;
}
$dateTime = $this->validateDateTimeValue($value, $column);
if ($column->format !== null) {
$value = $dateTime->format($column->format);
}
else {
$value = $dateTime->toFormattedDateString();
}
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'format' => $column->format,
'formatAlias' => 'dateLongMin'
]);
}
/**
* Process as diff for humans (1 min ago)
*/
protected function evalTimesinceTypeValue($record, $column, $value)
{
if ($value === null) {
return null;
}
$dateTime = $this->validateDateTimeValue($value, $column);
$value = DateTimeHelper::timeSince($dateTime);
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'timeSince' => true
]);
}
/**
* Process as time as current tense (Today at 0:00)
*/
protected function evalTimetenseTypeValue($record, $column, $value)
{
if ($value === null) {
return null;
}
$dateTime = $this->validateDateTimeValue($value, $column);
$value = DateTimeHelper::timeTense($dateTime);
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'timeTense' => true
]);
}
/**
* Validates a column type as a date
*/
protected function validateDateTimeValue($value, $column)
{
$value = DateTimeHelper::makeCarbon($value, false);
if (!$value instanceof Carbon) {
throw new ApplicationException(Lang::get(
'backend::lang.list.invalid_column_datetime',
['column' => $column->columnName]
));
}
return $value;
}
//
// Filtering
//
public function addFilter(callable $filter)
{
$this->filterCallbacks[] = $filter;
}
//
// Searching
//
/**
* Applies a search term to the list results, searching will disable tree
* view if a value is supplied.
* @param string $term
*/
public function setSearchTerm($term)
{
if (!empty($term)) {
$this->showTree = false;
}
$this->searchTerm = $term;
}
/**
* Applies a search options to the list search.
* @param array $options
*/
public function setSearchOptions($options = [])
{
extract(array_merge([
'mode' => null,
'scope' => null
], $options));
$this->searchMode = $mode;
$this->searchScope = $scope;
}
/**
* Returns a collection of columns which can be searched.
* @return array
*/
protected function getSearchableColumns()
{
$columns = $this->getColumns();
$searchable = [];
foreach ($columns as $column) {
if (!$column->searchable) {
continue;
}
$searchable[] = $column;
}
return $searchable;
}
/**
* Applies the search constraint to a query.
*/
protected function applySearchToQuery($query, $columns, $boolean = 'and')
{
$term = $this->searchTerm;
if ($scopeMethod = $this->searchScope) {
$searchMethod = $boolean == 'and' ? 'where' : 'orWhere';
$query->$searchMethod(function($q) use ($term, $scopeMethod) {
$q->$scopeMethod($term);
});
}
else {
$searchMethod = $boolean == 'and' ? 'searchWhere' : 'orSearchWhere';
$query->$searchMethod($term, $columns, $this->searchMode);
}
}
//
// Sorting
//
/**
* Event handler for sorting the list.
*/
public function onSort()
{
if ($column = post('sortColumn')) {
/*
* Toggle the sort direction and set the sorting column
*/
$sortOptions = ['column' => $this->getSortColumn(), 'direction' => $this->sortDirection];
if ($column != $sortOptions['column'] || $sortOptions['direction'] == 'asc') {
$this->sortDirection = $sortOptions['direction'] = 'desc';
}
else {
$this->sortDirection = $sortOptions['direction'] = 'asc';
}
$this->sortColumn = $sortOptions['column'] = $column;
$this->putSession('sort', $sortOptions);
/*
* Persist the page number
*/
$this->currentPageNumber = post('page');
return $this->onRefresh();
}
}
/**
* Returns the current sorting column, saved in a session or cached.
*/
protected function getSortColumn()
{
if (!$this->isSortable()) {
return false;
}
if ($this->sortColumn !== null) {
return $this->sortColumn;
}
/*
* User preference
*/
if ($this->showSorting && ($sortOptions = $this->getSession('sort'))) {
$this->sortColumn = $sortOptions['column'];
$this->sortDirection = $sortOptions['direction'];
}
else {
/*
* Supplied default
*/
if (is_string($this->defaultSort)) {
$this->sortColumn = $this->defaultSort;
$this->sortDirection = 'desc';
}
elseif (is_array($this->defaultSort) && isset($this->defaultSort['column'])) {
$this->sortColumn = $this->defaultSort['column'];
$this->sortDirection = (isset($this->defaultSort['direction'])) ?
$this->defaultSort['direction'] :
'desc';
}
}
/*
* First available column
*/
if ($this->sortColumn === null || !$this->isSortable($this->sortColumn)) {
$columns = $this->visibleColumns ?: $this->getVisibleColumns();
$columns = array_filter($columns, function($column){ return $column->sortable; });
$this->sortColumn = key($columns);
$this->sortDirection = 'desc';
}
return $this->sortColumn;
}
/**
* Returns true if the column can be sorted.
*/
protected function isSortable($column = null)
{
if ($column === null) {
return (count($this->getSortableColumns()) > 0);
}
else {
return array_key_exists($column, $this->getSortableColumns());
}
}
/**
* Returns a collection of columns which are sortable.
*/
protected function getSortableColumns()
{
if ($this->sortableColumns !== null) {
return $this->sortableColumns;
}
$columns = $this->getColumns();
$sortable = array_filter($columns, function($column){
return $column->sortable;
});
return $this->sortableColumns = $sortable;
}
//
// List Setup
//
/**
* Event handler to display the list set up.
*/
public function onLoadSetup()
{
$this->vars['columns'] = $this->getSetupListColumns();
$this->vars['perPageOptions'] = $this->getSetupPerPageOptions();
$this->vars['recordsPerPage'] = $this->recordsPerPage;
return $this->makePartial('setup_form');
}
/**
* Event handler to apply the list set up.
*/
public function onApplySetup()
{
if (($visibleColumns = post('visible_columns')) && is_array($visibleColumns)) {
$this->columnOverride = array_keys($visibleColumns);
$this->putSession('visible', $this->columnOverride);
}
$this->recordsPerPage = post('records_per_page', $this->recordsPerPage);
$this->putSession('order', post('column_order'));
$this->putSession('per_page', $this->recordsPerPage);
return $this->onRefresh();
}
/**
* Returns an array of allowable records per page.
*/
protected function getSetupPerPageOptions()
{
$perPageOptions = [20, 40, 80, 100, 120];
if (!in_array($this->recordsPerPage, $perPageOptions)) {
$perPageOptions[] = $this->recordsPerPage;
}
sort($perPageOptions);
return $perPageOptions;
}
/**
* Returns all the list columns used for list set up.
*/
protected function getSetupListColumns()
{
/*
* Force all columns invisible
*/
$columns = $this->defineListColumns();
foreach ($columns as $column) {
$column->invisible = true;
}
return array_merge($columns, $this->getVisibleColumns());
}
//
// Tree
//
/**
* Validates the model and settings if showTree is used
* @return void
*/
public function validateTree()
{
if (!$this->showTree) {
return;
}
$this->showSorting = $this->showPagination = false;
if (!$this->model->methodExists('getChildren')) {
throw new ApplicationException(
'To display list as a tree, the specified model must have a method "getChildren"'
);
}
if (!$this->model->methodExists('getChildCount')) {
throw new ApplicationException(
'To display list as a tree, the specified model must have a method "getChildCount"'
);
}
}
/**
* Checks if a node (model) is expanded in the session.
* @param Model $node
* @return boolean
*/
public function isTreeNodeExpanded($node)
{
return $this->getSession('tree_node_status_' . $node->getKey(), $this->treeExpanded);
}
/**
* Sets a node (model) to an expanded or collapsed state, stored in the
* session, then renders the list again.
* @return string List HTML contents.
*/
public function onToggleTreeNode()
{
$this->putSession('tree_node_status_' . post('node_id'), post('status') ? 0 : 1);
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) || $this->isColumnPivot($column)) {
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'
]);
}
/**
* Checks if a column refers to a pivot model specifically.
* @param ListColumn $column List column object
* @return boolean
*/
protected function isColumnPivot($column)
{
if (!isset($column->relation) || $column->relation != 'pivot') {
return false;
}
return true;
}
}