diff --git a/resources/views/livewire/datatables/label.blade.php b/resources/views/livewire/datatables/label.blade.php
index 7bed4d3b..2310faad 100644
--- a/resources/views/livewire/datatables/label.blade.php
+++ b/resources/views/livewire/datatables/label.blade.php
@@ -1,3 +1,4 @@
-
+
+
{!! $column['content'] ?? '' !!}
diff --git a/src/Action.php b/src/Action.php
new file mode 100644
index 00000000..91d1ff06
--- /dev/null
+++ b/src/Action.php
@@ -0,0 +1,89 @@
+$method, $args);
+ }
+ }
+
+ public static function value($value)
+ {
+ $action = new static;
+ $action->value = $value;
+
+ return $action;
+ }
+
+ public function label($label)
+ {
+ $this->label = $label;
+
+ return $this;
+ }
+
+ public function group($group)
+ {
+ $this->group = $group;
+
+ return $this;
+ }
+
+ public static function groupBy($group, $actions)
+ {
+ if ($actions instanceof \Closure) {
+ return collect($actions())->each(function ($item) use ($group) {
+ $item->group = $group;
+ });
+ }
+ }
+
+ public function export($fileName)
+ {
+ $this->fileName = $fileName;
+ $this->isExport();
+
+ return $this;
+ }
+
+ public function isExport($isExport = true)
+ {
+ $this->isExport = $isExport;
+
+ return $this;
+ }
+
+ public function styles($styles)
+ {
+ $this->styles = $styles;
+
+ return $this;
+ }
+
+ public function widths($widths)
+ {
+ $this->widths = $widths;
+
+ return $this;
+ }
+
+ public function callback($callable)
+ {
+ $this->callable = $callable;
+
+ return $this;
+ }
+}
diff --git a/src/Column.php b/src/Column.php
index 1972f94a..5208b0f6 100644
--- a/src/Column.php
+++ b/src/Column.php
@@ -2,6 +2,7 @@
namespace Mediconesystems\LivewireDatatables;
+use Closure;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Mediconesystems\LivewireDatatables\Http\Livewire\LivewireDatatable;
@@ -11,17 +12,18 @@ class Column
public $type = 'string';
public $index = 0;
public $label;
+ public $tooltip;
public $name;
public $select;
public $joins;
public $base;
public $raw;
public $searchable;
+ public $sortable;
public $filterOn;
public $filterable;
public $hideable;
public $sort;
- public $unsortable;
public $defaultSort;
public $callback;
public $hidden;
@@ -30,13 +32,25 @@ class Column
public $params = [];
public $additionalSelects = [];
public $filterView;
- public $align = 'left';
+ public $headerAlign = 'left';
+ public $contentAlign = 'left';
public $preventExport;
public $width;
public $minWidth;
public $maxWidth;
public $exportCallback;
+ /**
+ * @var bool should the sum of all summarizable cells in this column be
+ * displayed as a summary at the bottom of the table?
+ */
+ public $summary = false;
+
+ /**
+ * @var bool allow the content of the column to wrap into multiple lines.
+ */
+ public $wrappable = true;
+
/**
* @var string (optional) you can group your columns to let the user toggle the visibility of a group at once.
*/
@@ -45,6 +59,11 @@ class Column
/** @var array list all column types that are not sortable by SQL here */
public const UNSORTABLE_TYPES = ['label', 'checkbox'];
+ public function __construct()
+ {
+ $this->sortable = config('livewire-datatables.default_sortable', true);
+ }
+
public static function name($name)
{
$column = new static;
@@ -85,11 +104,25 @@ public static function raw($raw)
return $column;
}
- public static function callback($columns, $callback, $params = [])
- {
+ /**
+ * Make a callback function.
+ *
+ * @param $columns Array|string The (comma separated) columns that should be retrieved from the database.
+ * Is being translated directly into the `.sql`.
+ * @param $callback Closure|string A callback that defines how the retrieved columns are processed.
+ * @param $params Array Optional additional parameters that are passed to the given Closure.
+ * @param $callbackName string Optional string that defines the 'name' of the column.
+ * Leave empty to let livewire autogenerate a distinct value.
+ */
+ public static function callback(
+ array|string $columns,
+ Closure|string $callback,
+ array $params = [],
+ ?string $callbackName = null
+ ) {
$column = new static;
- $column->name = 'callback_' . crc32(json_encode(func_get_args()));
+ $column->name = 'callback_' . ($callbackName ?? crc32(json_encode(func_get_args())));
$column->callback = $callback;
$column->additionalSelects = is_array($columns) ? $columns : array_map('trim', explode(',', $columns));
$column->params = $params;
@@ -129,6 +162,34 @@ public function label($label)
return $this;
}
+ public function wrap()
+ {
+ $this->wrappable = true;
+
+ return $this;
+ }
+
+ public function unwrap()
+ {
+ $this->wrappable = false;
+
+ return $this;
+ }
+
+ public function enableSummary()
+ {
+ $this->summary = true;
+
+ return $this;
+ }
+
+ public function disableSummary()
+ {
+ $this->summary = false;
+
+ return $this;
+ }
+
public function setIndex($index)
{
$this->index = $index;
@@ -136,6 +197,16 @@ public function setIndex($index)
return $this;
}
+ public function tooltip($text, $label = null)
+ {
+ $this->tooltip = [
+ 'text' => $text,
+ 'label' => $label,
+ ];
+
+ return $this;
+ }
+
public function sortBy($column)
{
$this->sort = $column;
@@ -150,16 +221,23 @@ public function defaultSort($direction = true)
return $this;
}
- public function hideable()
+ public function hideable($hideable = true)
+ {
+ $this->hideable = $hideable;
+
+ return $this;
+ }
+
+ public function sortable()
{
- $this->hideable = true;
+ $this->sortable = true;
return $this;
}
public function unsortable()
{
- $this->unsortable = true;
+ $this->sortable = false;
return $this;
}
@@ -272,10 +350,10 @@ public function round($precision = 0)
return $this;
}
- public function view($view)
+ public function view($view, $data = [])
{
- $this->callback = function ($value, $row) use ($view) {
- return view($view, ['value' => $value, 'row' => $row]);
+ $this->callback = function ($value, $row) use ($view, $data) {
+ return view($view, ['value' => $value, 'row' => $row, ...$data]);
};
$this->exportCallback = function ($value) {
@@ -322,14 +400,44 @@ public function hide()
public function alignRight()
{
- $this->align = 'right';
+ $this->headerAlign = 'right';
+ $this->contentAlign = 'right';
return $this;
}
public function alignCenter()
{
- $this->align = 'center';
+ $this->headerAlign = 'center';
+ $this->contentAlign = 'center';
+
+ return $this;
+ }
+
+ public function headerAlignRight()
+ {
+ $this->headerAlign = 'right';
+
+ return $this;
+ }
+
+ public function contentAlignRight()
+ {
+ $this->contentAlign = 'right';
+
+ return $this;
+ }
+
+ public function headerAlignCenter()
+ {
+ $this->headerAlign = 'center';
+
+ return $this;
+ }
+
+ public function contentAlignCenter()
+ {
+ $this->contentAlign = 'center';
return $this;
}
@@ -353,7 +461,7 @@ public function aggregate()
public function isBaseColumn()
{
- return ! Str::contains($this->name, '.') && ! $this->raw;
+ return ! Str::startsWith($this->name, 'callback_') && ! Str::contains($this->name, '.') && ! $this->raw;
}
public function field()
diff --git a/src/ColumnSet.php.old.ctp b/src/ColumnSet.php.old.ctp
deleted file mode 100644
index f63b5868..00000000
--- a/src/ColumnSet.php.old.ctp
+++ /dev/null
@@ -1,228 +0,0 @@
-columns = $columns;
- }
-
- public static function build($input)
- {
- return is_array($input)
- ? self::fromArray($input)
- : self::fromModelInstance($input);
- }
-
- public static function fromModelInstance($model)
- {
- return new static(
- collect($model->getAttributes())->keys()->reject(function ($name) use ($model) {
- return in_array($name, $model->getHidden());
- })->map(function ($attribute) use ($model) {
- return Column::name($attribute);
- })
- );
- }
-
- public static function fromArray($columns)
- {
- return new static(collect($columns));
- }
-
- public function include($include)
- {
- if (!$include) {
- return $this;
- }
-
- $include = collect(is_array($include) ? $include : array_map('trim', explode(',', $include)));
- $this->columns = $include->map(function ($column) {
- return Str::contains($column, '|')
- ? Column::name(Str::before($column, '|'))->label(Str::after($column, '|'))
- : Column::name($column);
- });
-
- return $this;
- }
-
- public function exclude($exclude)
- {
- if (!$exclude) {
- return $this;
- }
-
- $exclude = is_array($exclude) ? $exclude : array_map('trim', explode(',', $exclude));
-
- $this->columns = $this->columns->reject(function ($column) use ($exclude) {
- return in_array(Str::after($column->name, '.'), $exclude);
- });
-
- return $this;
- }
-
- public function hide($hidden)
- {
- if (!$hidden) {
- return $this;
- }
- $hidden = is_array($hidden) ? $hidden : array_map('trim', explode(',', $hidden));
- $this->columns->each(function ($column) use ($hidden) {
- $column->hidden = in_array(Str::after($column->name, '.'), $hidden);
- });
-
- return $this;
- }
-
- public function formatDates($dates)
- {
- $dates = is_array($dates) ? $dates : array_map('trim', explode(',', $dates));
-
- $this->columns = $this->columns->map(function ($column) use ($dates) {
- foreach ($dates as $date) {
- if ($column->name === Str::before($date, '|')) {
- $format = Str::of($date)->contains('|') ? Str::after($date, '|') : null;
-
- return DateColumn::name($column->name)->format($format);
- }
- }
- return $column;
- });
-
- return $this;
- }
-
- public function formatTimes($times)
- {
- $times = is_array($times) ? $times : array_map('trim', explode(',', $times));
-
- $this->columns = $this->columns->map(function ($column) use ($times) {
- foreach ($times as $time) {
- if (Str::after($column->name, '.') === Str::before($time, '|')) {
- $format = Str::of($time)->contains('|') ? Str::after($time, '|') : null;
- return TimeColumn::name($column->name)->format($format);
- }
- }
- return $column;
- });
-
- return $this;
- }
-
- public function search($searchable)
- {
- if (!$searchable) {
- return $this;
- }
-
- $searchable = is_array($searchable) ? $searchable : array_map('trim', explode(',', $searchable));
- $this->columns->each(function ($column) use ($searchable) {
- $column->searchable = in_array($column->name, $searchable);
- });
-
- return $this;
- }
-
- public function sort($sort)
- {
- if ($sort && $column = $this->columns->first(function ($column) use ($sort) {
- return Str::after($column->name, '.') === Str::before($sort, '|');
- })) {
- $column->defaultSort(Str::of($sort)->contains('|') ? Str::after($sort, '|') : null);
- }
- return $this;
- }
-
- public function columns()
- {
- return collect($this->columns);
- }
-
- public function columnsArray()
- {
- return $this->columns()->map->toArray()->toArray();
- }
-
- public function processForBuilder($builder)
- {
- $this->columns = $this->columns->map(function ($column) use ($builder) {
-
- foreach (array_merge([$column->base ?? $column->name], $column->additionalSelects) as $name) {
-
- if (!Str::contains($name, '.')) {
- if (!Str::startsWith($name, 'callback_')) {
- $selects[] = $builder->getModel()->getTable() . '.' . $name;
- if ($column->isEditable()) {
- $selects[] = $builder->getModel()->getTable() . '.' . $builder->getModel()->getKeyName() . ' AS ' . $builder->getModel()->getTable() . '.' . $builder->getModel()->getKeyName();
- }
- }
- }
-
- $parent = $builder;
- foreach (explode('.', Str::beforeLast($name, '.')) as $join) {
-
- if (method_exists($parent->getModel(), $join)) {
- $relation = $parent->getRelation($join);
- // dump($parent, $join, $relation);
- if ($relation instanceof HasOne || $relation instanceof BelongsTo) {
- $column->joins[] = [
- $relation->getRelated()->getTable(),
- $relation instanceof HasOne ? $relation->getQualifiedForeignKeyName() : $relation->getQualifiedOwnerKeyName(),
- $relation instanceof HasOne ? $relation->getQualifiedParentKeyName() : $relation->getQualifiedForeignKeyName()
- ];
-
- $parent = $relation;
-
- $selects = [$parent->getRelated()->getTable() . '.' . Str::afterLast($name, '.') . ($name === $column->name
- ? ' AS ' . $name
- : '')];
- }
-
- if ($relation instanceof HasMany || $relation instanceof BelongsToMany) {
- $name = explode('.', $name);
- $column->aggregates[] = [$name[0], $column->aggregate(), $name[1]];
- }
- }
- }
- }
-
- if (count($selects) > 1) {
- if ($column->callback && !$column->isEditable()) {
-
- $column->additionalSelects = [];
- $column->select = DB::raw('CONCAT_WS("' . static::SEPARATOR . '" ,' .
- collect($selects)->map(function ($select) {
- return "COALESCE($select, '')";
- })->join(', ') . ')' . ' AS `' . $column->name . '`');
- } else {
- $column->select = array_shift($selects);
- $column->additionalSelects = $selects;
- }
- } else if (count($selects)) {
- foreach ($selects as $select) {
- $column->select = $select . ($column->callback ? ' AS ' . $column->name : '');
- }
- }
-
- return $column;
- });
-
- // dd($this->columns);
- return $this;
- }
-}
diff --git a/src/Commands/DatatableMakeCommand.php b/src/Commands/DatatableMakeCommand.php
new file mode 100644
index 00000000..2e6c6bdf
--- /dev/null
+++ b/src/Commands/DatatableMakeCommand.php
@@ -0,0 +1,8 @@
+collection->first());
}
+
+ public function setFileName($fileName)
+ {
+ $this->fileName = $fileName;
+
+ return $this;
+ }
+
+ public function getFileName(): string
+ {
+ return $this->fileName;
+ }
+
+ public function setColumnWidths($columnWidths)
+ {
+ $this->columnWidths = $columnWidths;
+
+ return $this;
+ }
+
+ public function getColumnWidths(): array
+ {
+ return $this->columnWidths;
+ }
+
+ public function columnWidths(): array
+ {
+ return $this->getColumnWidths();
+ }
+
+ public function setStyles($styles)
+ {
+ $this->styles = $styles;
+
+ return $this;
+ }
+
+ public function getStyles(): array
+ {
+ return $this->styles;
+ }
+
+ public function styles(Worksheet $sheet)
+ {
+ return $this->getStyles();
+ }
+
+ public function download()
+ {
+ return Excel::download($this, $this->getFileName());
+ }
}
diff --git a/src/Http/Livewire/LivewireDatatable.php b/src/Http/Livewire/LivewireDatatable.php
index 15a91e45..1366cc7f 100644
--- a/src/Http/Livewire/LivewireDatatable.php
+++ b/src/Http/Livewire/LivewireDatatable.php
@@ -3,12 +3,9 @@
namespace Mediconesystems\LivewireDatatables\Http\Livewire;
use Exception;
-use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
-use Illuminate\Database\Eloquent\Relations\HasOne;
-use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
@@ -16,7 +13,6 @@
use Illuminate\View\View;
use Livewire\Component;
use Livewire\WithPagination;
-use Maatwebsite\Excel\Facades\Excel;
use Mediconesystems\LivewireDatatables\Column;
use Mediconesystems\LivewireDatatables\ColumnSet;
use Mediconesystems\LivewireDatatables\Exports\DatatableExport;
@@ -40,6 +36,7 @@ class LivewireDatatable extends Component
public $activeBooleanFilters = [];
public $activeTextFilters = [];
public $activeNumberFilters = [];
+ public $defaultFilters = [];
public $hideHeader;
public $hidePagination;
public $perPage;
@@ -50,11 +47,11 @@ class LivewireDatatable extends Component
public $times;
public $searchable;
public $exportable;
- public $export_name;
public $hideable;
public $params;
public $selected = [];
public $beforeTableSlot;
+ public $buttonsSlot;
public $afterTableSlot;
public $complex;
public $complexQuery;
@@ -68,8 +65,14 @@ class LivewireDatatable extends Component
public $persistSort = true;
public $persistPerPage = true;
public $persistFilters = true;
+ public $visibleSelected = [];
public $row = 1;
+ public $tablePrefix = '';
+
+ public $actions;
+ public $massActionOption;
+
/**
* @var array List your groups and the corresponding label (or translation) here.
* The label can be a i18n placeholder like 'app.my_string' and it will be automatically translated via __().
@@ -110,6 +113,31 @@ class LivewireDatatable extends Component
'does not include' => '<>',
];
+ protected $viewColumns = [
+ 'index',
+ 'hidden',
+ 'label',
+ 'tooltip',
+ 'group',
+ 'summary',
+ 'content',
+ 'headerAlign',
+ 'contentAlign',
+ 'type',
+ 'filterable',
+ 'hideable',
+ 'sortable',
+ 'complex',
+ 'filterView',
+ 'name',
+ 'params',
+ 'wrappable',
+ 'width',
+ 'minWidth',
+ 'maxWidth',
+ 'preventExport',
+ ];
+
/**
* This events allows to control the options of the datatable from foreign livewire components
* by using $emit.
@@ -124,9 +152,7 @@ public function applyToTable($options)
if (isset($options['hiddenColumns']) && is_array($options['hiddenColumns'])) {
// first display all columns,
- foreach ($this->columns as $key => $column) {
- $this->columns[$key]['hidden'] = false;
- }
+ $this->resetHiddenColumns();
// then hide all columns that should be hidden:
foreach ($options['hiddenColumns'] as $columnToHide) {
@@ -149,6 +175,7 @@ public function applyToTable($options)
'activeNumberFilters',
'hide',
'selected',
+ 'pinnedRecords',
] as $property) {
if (isset($options[$property])) {
$this->$property = $options[$property];
@@ -164,6 +191,7 @@ public function applyToTable($options)
public function resetTable()
{
$this->perPage = config('livewire-datatables.default_per_page', 10);
+ $this->sort = $this->defaultSort();
$this->search = null;
$this->setPage(1);
$this->activeSelectFilters = [];
@@ -173,16 +201,29 @@ public function resetTable()
$this->activeBooleanFilters = [];
$this->activeNumberFilters = [];
$this->hide = null;
+ $this->resetHiddenColumns();
$this->selected = [];
}
+ /**
+ * Display all columns, also those that are currently hidden.
+ * Should get called when resetting the table.
+ */
+ public function resetHiddenColumns()
+ {
+ foreach ($this->columns as $key => $column) {
+ $this->columns[$key]['hidden'] = false;
+ }
+ }
+
public function updatedSearch()
{
+ $this->visibleSelected = ($this->search) ? array_intersect($this->getQuery()->get()->pluck('checkbox_attribute')->toArray(), $this->selected) : $this->selected;
$this->setPage(1);
}
public function mount(
- $model = null,
+ $model = false,
$include = [],
$exclude = [],
$hide = [],
@@ -194,9 +235,9 @@ public function mount(
$hidePagination = null,
$perPage = null,
$exportable = false,
- $export_name = null,
$hideable = false,
$beforeTableSlot = false,
+ $buttonsSlot = false,
$afterTableSlot = false,
$params = []
) {
@@ -214,6 +255,7 @@ public function mount(
'exportable',
'hideable',
'beforeTableSlot',
+ 'buttonsSlot',
'afterTableSlot',
] as $property) {
$this->$property = $this->$property ?? $$property;
@@ -222,13 +264,19 @@ public function mount(
$this->params = $params;
$this->columns = $this->getViewColumns();
-
+ $this->actions = $this->getMassActions();
$this->initialiseSearch();
$this->initialiseSort();
$this->initialiseHiddenColumns();
+ $this->initialiseDefaultFilters();
$this->initialiseFilters();
$this->initialisePerPage();
$this->initialiseColumnGroups();
+ $this->model = $this->model ?: get_class($this->builder()->getModel());
+
+ if (isset($this->pinnedRecords)) {
+ $this->initialisePinnedRecords();
+ }
}
// save settings
@@ -249,26 +297,9 @@ public function columns()
public function getViewColumns()
{
return collect($this->freshColumns)->map(function ($column) {
- return collect($column)->only([
- 'index',
- 'hidden',
- 'label',
- 'group',
- 'content',
- 'align',
- 'type',
- 'filterable',
- 'hideable',
- 'complex',
- 'filterView',
- 'name',
- 'params',
- 'width',
- 'minWidth',
- 'maxWidth',
- 'unsortable',
- 'preventExport',
- ])->toArray();
+ return collect($column)
+ ->only($this->viewColumns)
+ ->toArray();
})->toArray();
}
@@ -313,45 +344,38 @@ public function getProcessedColumnsProperty()
->sort($this->sort);
}
- public function resolveColumnName($column)
- {
- return $column->isBaseColumn()
- ? $this->query->getModel()->getTable() . '.' . ($column->base ?? Str::before($column->name, ':'))
- : $column->select ?? $this->resolveRelationColumn($column->base ?? $column->name, $column->aggregate);
- }
-
- public function resolveCheckboxColumnName($column)
- {
- $column = is_object($column)
- ? $column->toArray()
- : $column;
-
- return Str::contains($column['base'], '.')
- ? $this->resolveRelationColumn($column['base'], $column['aggregate'])
- : $this->query->getModel()->getTable() . '.' . $column['base'];
- }
-
public function resolveAdditionalSelects($column)
{
$selects = collect($column->additionalSelects)->map(function ($select) use ($column) {
return Str::contains($select, '.')
- ? $this->resolveRelationColumn($select, Str::contains($select, ':') ? Str::after($select, ':') : null, $column->name)
+ ? $this->resolveColumnName($column, $select)
: $this->query->getModel()->getTable() . '.' . $select;
});
- return $selects->count() > 1
- ? new Expression("CONCAT_WS('" . static::SEPARATOR . "' ," .
+ if (DB::connection() instanceof \Illuminate\Database\SQLiteConnection) {
+ // SQLite dialect.
+ return $selects->count() > 1
+ ? new Expression('(' .
collect($selects)->map(function ($select) {
- return "COALESCE($select, '')";
+ return 'COALESCE(' . $this->tablePrefix . $select . ', \'\')';
+ })->join(" || '" . static::SEPARATOR . "' || ") . ')')
+ : $selects->first();
+ } else {
+ // Default to MySql dialect.
+ return $selects->count() > 1
+ ? new Expression("CONCAT_WS('" . static::SEPARATOR . "' ," .
+ collect($selects)->map(function ($select) {
+ return 'COALESCE(' . $this->tablePrefix . $select . ', \'\')';
})->join(', ') . ')')
- : $selects->first();
+ : $selects->first();
+ }
}
public function resolveEditableColumnName($column)
{
return [
$column->select,
- $this->query->getModel()->getTable() . '.' . $this->query->getModel()->getKeyName(),
+ $this->query->getModel()->getTable() . '.' . $this->query->getModel()->getKeyName() . ' AS ' . $column->name . '_edit_id',
];
}
@@ -365,12 +389,6 @@ public function getSelectStatements($withAlias = false, $export = false)
return $column;
}
- if ($column->isType('checkbox')) {
- $column->select = $this->resolveCheckboxColumnName($column);
-
- return $column;
- }
-
if (Str::startsWith($column->name, 'callback_')) {
$column->select = $this->resolveAdditionalSelects($column);
@@ -412,94 +430,41 @@ public function getSelectStatements($withAlias = false, $export = false)
});
}
- protected function resolveRelationColumn($name, $aggregate = null, $alias = null)
+ protected function resolveColumnName($column, $additional = null)
{
- $parts = explode('.', Str::before($name, ':'));
- $columnName = array_pop($parts);
- $relation = implode('.', $parts);
-
- return method_exists($this->query->getModel(), $parts[0])
- ? $this->joinRelation($relation, $columnName, $aggregate, $alias ?? $name)
- : $name;
- }
-
- protected function joinRelation($relation, $relationColumn, $aggregate = null, $alias = null)
- {
- $table = '';
- $model = '';
- $lastQuery = $this->query;
- foreach (explode('.', $relation) as $eachRelation) {
- $model = $lastQuery->getRelation($eachRelation);
-
- switch (true) {
- case $model instanceof HasOne:
- $table = $model->getRelated()->getTable();
- $foreign = $model->getQualifiedForeignKeyName();
- $other = $model->getQualifiedParentKeyName();
- break;
+ if ($column->isBaseColumn()) {
+ return $this->query->getModel()->getTable() . '.' . ($column->base ?? Str::before($column->name, ':'));
+ }
- case $model instanceof HasMany:
- $this->query->customWithAggregate($relation, $aggregate ?? 'count', $relationColumn, $alias);
- $table = null;
- break;
+ $relations = explode('.', Str::before(($additional ?: $column->name), ':'));
+ $aggregate = Str::after(($additional ?: $column->name), ':');
- case $model instanceof BelongsTo:
- $table = $model->getRelated()->getTable();
- $foreign = $model->getQualifiedForeignKeyName();
- $other = $model->getQualifiedOwnerKeyName();
- break;
+ if (! method_exists($this->query->getModel(), $relations[0])) {
+ return $additional ?: $column->name;
+ }
- case $model instanceof BelongsToMany:
- $this->query->customWithAggregate($relation, $aggregate ?? 'count', $relationColumn, $alias);
- $table = null;
- break;
+ $columnName = array_pop($relations);
+ $aggregateName = implode('.', $relations);
- case $model instanceof HasOneThrough:
- $pivot = explode('.', $model->getQualifiedParentKeyName())[0];
- $pivotPK = $model->getQualifiedFirstKeyName();
- $pivotFK = $model->getQualifiedLocalKeyName();
- $this->performJoin($pivot, $pivotPK, $pivotFK);
+ $relatedQuery = $this->query;
- $related = $model->getRelated();
- $table = $related->getTable();
- $tablePK = $related->getForeignKey();
- $foreign = $pivot . '.' . $tablePK;
- $other = $related->getQualifiedKeyName();
+ while (count($relations) > 0) {
+ $relation = array_shift($relations);
- break;
+ if ($relatedQuery->getRelation($relation) instanceof HasMany || $relatedQuery->getRelation($relation) instanceof HasManyThrough || $relatedQuery->getRelation($relation) instanceof BelongsToMany) {
+ $this->query->customWithAggregate($aggregateName, $column->aggregate ?? 'count', $columnName, $column->name);
- default:
- $this->query->customWithAggregate($relation, $aggregate ?? 'count', $relationColumn, $alias);
+ return null;
}
- if ($table) {
- $this->performJoin($table, $foreign, $other);
- }
- $lastQuery = $model->getQuery();
- }
- if ($model instanceof HasOne || $model instanceof BelongsTo || $model instanceof HasOneThrough) {
- return $table . '.' . $relationColumn;
- }
+ $useThrough = collect($this->query->getQuery()->joins)
+ ->pluck('table')
+ ->contains($relatedQuery->getRelation($relation)->getRelated()->getTable());
- if ($model instanceof HasMany) {
- return;
+ $relatedQuery = $this->query->joinRelation($relation, null, 'left', $useThrough, $relatedQuery);
}
- if ($model instanceof BelongsToMany) {
- return;
- }
- }
-
- protected function performJoin($table, $foreign, $other, $type = 'left')
- {
- $joins = [];
- foreach ((array) $this->query->getQuery()->joins as $key => $join) {
- $joins[] = $join->table;
- }
-
- if (! in_array($table, $joins)) {
- $this->query->join($table, $foreign, '=', $other, $type);
- }
+ return $relatedQuery->getQuery()->from . '.' . $columnName;
}
public function getFreshColumnsProperty()
@@ -600,8 +565,8 @@ public function initialiseSort()
return in_array($column['type'], Column::UNSORTABLE_TYPES) || $column['hidden'];
})->keys()->first();
- $this->getSessionStoredSort();
$this->direction = $this->defaultSort() && $this->defaultSort()['direction'] === 'asc';
+ $this->getSessionStoredSort();
}
public function initialiseHiddenColumns()
@@ -637,6 +602,51 @@ public function initialiseColumnGroups()
}, $this->columns);
}
+ public function initialiseDefaultFilters()
+ {
+ if (! $this->defaultFilters || ! is_array($this->defaultFilters) || count($this->defaultFilters) === 0) {
+ return;
+ }
+
+ $columns = collect($this->columns);
+
+ foreach ($this->defaultFilters as $columnName => $value) {
+ $columnIndex = $columns->search(function ($column) use ($columnName) {
+ return $column['name'] === $columnName;
+ });
+
+ if ($columnIndex === false) {
+ continue;
+ }
+
+ $column = $columns[$columnIndex];
+
+ if ($column['type'] === 'string') {
+ $this->activeTextFilters[$columnIndex] = $value;
+ }
+
+ if ($column['type'] === 'boolean') {
+ $this->activeBooleanFilters[$columnIndex] = $value;
+ }
+
+ if ($column['type'] === 'select') {
+ $this->activeSelectFilters[$columnIndex] = $value;
+ }
+
+ if ($column['type'] === 'date') {
+ $this->activeDateFilters[$columnIndex] = $value;
+ }
+
+ if ($column['type'] === 'time') {
+ $this->activeTimeFilters[$columnIndex] = $value;
+ }
+
+ if ($column['type'] === 'number') {
+ $this->activeNumberFilters[$columnIndex] = $value;
+ }
+ }
+ }
+
public function initialiseFilters()
{
if (! $this->persistFilters) {
@@ -645,12 +655,29 @@ public function initialiseFilters()
$filters = session()->get($this->sessionStorageKey() . '_filter');
- $this->activeBooleanFilters = $filters['boolean'] ?? [];
- $this->activeSelectFilters = $filters['select'] ?? [];
- $this->activeTextFilters = $filters['text'] ?? [];
- $this->activeDateFilters = $filters['date'] ?? [];
- $this->activeTimeFilters = $filters['time'] ?? [];
- $this->activeNumberFilters = $filters['number'] ?? [];
+ if (! empty($filters['text'])) {
+ $this->activeTextFilters = $filters['text'];
+ }
+
+ if (! empty($filters['boolean'])) {
+ $this->activeBooleanFilters = $filters['boolean'];
+ }
+
+ if (! empty($filters['select'])) {
+ $this->activeSelectFilters = $filters['select'];
+ }
+
+ if (! empty($filters['date'])) {
+ $this->activeDateFilters = $filters['date'];
+ }
+
+ if (! empty($filters['time'])) {
+ $this->activeTimeFilters = $filters['time'];
+ }
+
+ if (! empty($filters['number'])) {
+ $this->activeNumberFilters = $filters['number'];
+ }
if (isset($filters['search'])) {
$this->search = $filters['search'];
@@ -669,10 +696,9 @@ public function defaultSort()
] : null;
}
- public function getSortString()
+ public function getSortString($dbtable)
{
$column = $this->freshColumns[$this->sort];
- $dbTable = DB::connection()->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME);
switch (true) {
case $column['sort']:
@@ -691,11 +717,49 @@ public function getSortString()
return Str::before($column['select'], ' AS ');
break;
- default:
- return $dbTable == 'pgsql' || $dbTable == 'sqlsrv'
- ? new Expression('"' . $column['name'] . '"')
- : new Expression('`' . $column['name'] . '`');
- break;
+ default:
+
+ switch ($dbtable) {
+ case 'mysql':
+ return new Expression('`' . $column['name'] . '`');
+ break;
+ case 'pgsql':
+ return new Expression('"' . $column['name'] . '"');
+ break;
+ case 'sqlsrv':
+ return new Expression("'" . $column['name'] . "'");
+ break;
+ default:
+ return new Expression("'" . $column['name'] . "'");
+ }
+ }
+ }
+
+ /**
+ * @return bool has the user defined at least one column to display a summary row?
+ */
+ public function hasSummaryRow()
+ {
+ foreach ($this->columns as $column) {
+ if ($column['summary']) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Attempt so summarize each data cell of the given column.
+ * In case we have a string or any other value that is not summarizable,
+ * we return a empty string.
+ */
+ public function summarize($column)
+ {
+ try {
+ return $this->results->sum($column);
+ } catch (\TypeError $e) {
+ return '';
}
}
@@ -726,11 +790,14 @@ public function sort($index, $direction = null)
if ($direction === null) { // toggle direction
$this->direction = ! $this->direction;
} else {
- $this->direction = $direction === 'desc' ? false : true;
+ $this->direction = $direction === 'asc' ? true : false;
}
} else {
$this->sort = (int) $index;
}
+ if ($direction !== null) {
+ $this->direction = $direction === 'asc' ? true : false;
+ }
$this->setPage(1);
session()->put([
@@ -816,6 +883,7 @@ public function isGroupHidden($group)
public function doBooleanFilter($index, $value)
{
$this->activeBooleanFilters[$index] = $value;
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -823,6 +891,7 @@ public function doBooleanFilter($index, $value)
public function doSelectFilter($index, $value)
{
$this->activeSelectFilters[$index][] = $value;
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -832,7 +901,7 @@ public function doTextFilter($index, $value)
foreach (explode(' ', $value) as $val) {
$this->activeTextFilters[$index][] = $val;
}
-
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -840,6 +909,7 @@ public function doTextFilter($index, $value)
public function doDateFilterStart($index, $start)
{
$this->activeDateFilters[$index]['start'] = $start;
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -847,6 +917,7 @@ public function doDateFilterStart($index, $start)
public function doDateFilterEnd($index, $end)
{
$this->activeDateFilters[$index]['end'] = $end;
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -854,6 +925,7 @@ public function doDateFilterEnd($index, $end)
public function doTimeFilterStart($index, $start)
{
$this->activeTimeFilters[$index]['start'] = $start;
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -861,6 +933,7 @@ public function doTimeFilterStart($index, $start)
public function doTimeFilterEnd($index, $end)
{
$this->activeTimeFilters[$index]['end'] = $end;
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -869,6 +942,7 @@ public function doNumberFilterStart($index, $start)
{
$this->activeNumberFilters[$index]['start'] = ($start != '') ? (int) $start : null;
$this->clearEmptyNumberFilter($index);
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -877,6 +951,7 @@ public function doNumberFilterEnd($index, $end)
{
$this->activeNumberFilters[$index]['end'] = ($end != '') ? (int) $end : null;
$this->clearEmptyNumberFilter($index);
+ $this->setVisibleSelected();
$this->setPage(1);
$this->setSessionStoredFilters();
}
@@ -893,6 +968,7 @@ public function clearEmptyNumberFilter($index)
public function removeSelectFilter($column, $key = null)
{
unset($this->activeSelectFilters[$column][$key]);
+ $this->visibleSelected = $this->selected;
if (count($this->activeSelectFilters[$column]) < 1) {
unset($this->activeSelectFilters[$column]);
}
@@ -910,6 +986,7 @@ public function clearAllFilters()
$this->activeNumberFilters = [];
$this->complexQuery = null;
$this->userFilter = null;
+ $this->visibleSelected = $this->selected;
$this->setPage(1);
$this->setSessionStoredFilters();
@@ -919,6 +996,7 @@ public function clearAllFilters()
public function removeBooleanFilter($column)
{
unset($this->activeBooleanFilters[$column]);
+ $this->visibleSelected = $this->selected;
$this->setSessionStoredFilters();
}
@@ -932,12 +1010,14 @@ public function removeTextFilter($column, $key = null)
} else {
unset($this->activeTextFilters[$column]);
}
+ $this->visibleSelected = $this->selected;
$this->setSessionStoredFilters();
}
public function removeNumberFilter($column)
{
unset($this->activeNumberFilters[$column]);
+ $this->visibleSelected = $this->selected;
$this->setSessionStoredFilters();
}
@@ -1112,6 +1192,8 @@ public function buildDatabaseQuery($export = false)
{
$this->query = $this->builder();
+ $this->tablePrefix = $this->query->getConnection()->getTablePrefix() ?? '';
+
$this->query->addSelect(
$this->getSelectStatements(true, $export)
->filter()
@@ -1129,11 +1211,16 @@ public function buildDatabaseQuery($export = false)
->addTimeRangeFilter()
->addComplexQuery()
->addSort();
+
+ if (isset($this->pinnedRecors)) {
+ $this->applyPinnedRecords();
+ }
}
public function complexQuery($rules)
{
$this->complexQuery = $rules;
+ $this->setPage(1);
}
public function addComplexQuery()
@@ -1146,8 +1233,6 @@ public function addComplexQuery()
$this->processNested($this->complexQuery, $query);
});
- $this->setPage(1);
-
return $this;
}
@@ -1191,9 +1276,15 @@ public function processNested($rules = null, $query = null, $logic = 'and')
$query->whereNotNull($column);
} elseif ($this->columns[$rule['content']['column']]['type'] === 'boolean') {
if ($rule['content']['value'] === 'true') {
- $query->whereNotNull(Str::contains($column, '(') ? DB::raw($column) : $column);
+ $query->where(function ($query) use ($column) {
+ $query->whereNotNull(Str::contains($column, '(') ? DB::raw($column) : $column)
+ ->where($column, '<>', 0);
+ });
} else {
- $query->whereNull(Str::contains($column, '(') ? DB::raw($column) : $column);
+ $query->where(function ($query) use ($column) {
+ $query->whereNull(Str::contains($column, '(') ? DB::raw($column) : $column)
+ ->orWhere(Str::contains($column, '(') ? DB::raw($column) : $column, 0);
+ });
}
} else {
$col = (isset($this->freshColumns[$rule['content']['column']]['round']) && $this->freshColumns[$rule['content']['column']]['round'] !== null)
@@ -1234,10 +1325,10 @@ public function addGlobalSearch()
foreach ($this->getColumnFilterStatement($i) as $column) {
$query->when(is_array($column), function ($query) use ($search, $column) {
foreach ($column as $col) {
- $query->orWhereRaw('LOWER(' . $col . ') like ?', '%' . mb_strtolower($search) . '%');
+ $query->orWhereRaw('LOWER(' . (Str::contains(mb_strtolower($column), 'concat') ? '' : $this->tablePrefix) . $col . ') like ?', '%' . mb_strtolower($search) . '%');
}
}, function ($query) use ($search, $column) {
- $query->orWhereRaw('LOWER(' . $column . ') like ?', '%' . mb_strtolower($search) . '%');
+ $query->orWhereRaw('LOWER(' . (Str::contains(mb_strtolower($column), 'concat') ? '' : $this->tablePrefix) . $column . ') like ?', '%' . mb_strtolower($search) . '%');
});
}
});
@@ -1280,14 +1371,14 @@ public function addSelectFilters()
if ($this->freshColumns[$index]['type'] === 'json') {
$query->where(function ($query) use ($value, $index) {
foreach ($this->getColumnFilterStatement($index) as $column) {
- $query->whereRaw('LOWER(' . $column . ') like ?', [mb_strtolower("%$value%")]);
+ $query->whereRaw('LOWER(' . $this->tablePrefix . $column . ') like ?', [mb_strtolower("%$value%")]);
}
});
} else {
$query->orWhere(function ($query) use ($value, $index) {
foreach ($this->getColumnFilterStatement($index) as $column) {
if (Str::contains(mb_strtolower($column), 'concat')) {
- $query->orWhereRaw('LOWER(' . $column . ') like ?', [mb_strtolower("%$value%")]);
+ $query->orWhereRaw('LOWER(' . $this->tablePrefix . $column . ') like ?', [mb_strtolower("%$value%")]);
} else {
$query->orWhereRaw($column . ' = ?', $value);
}
@@ -1355,7 +1446,7 @@ public function addTextFilters()
$query->orWhere(function ($query) use ($index, $value) {
foreach ($this->getColumnFilterStatement($index) as $column) {
$column = is_array($column) ? $column[0] : $column;
- $query->orWhereRaw('LOWER(' . $column . ') like ?', [mb_strtolower("%$value%")]);
+ $query->orWhereRaw('LOWER(' . $this->tablePrefix . $column . ') like ?', [mb_strtolower("%$value%")]);
}
});
}
@@ -1408,8 +1499,8 @@ public function addDateRangeFilter()
break;
}
$query->whereBetween($this->getColumnFilterStatement($index)[0], [
- isset($filter['start']) && $filter['start'] != '' ? $filter['start'] : '0000-00-00',
- isset($filter['end']) && $filter['end'] != '' ? $filter['end'] : '9999-12-31',
+ isset($filter['start']) && $filter['start'] != '' ? $filter['start'] : config('livewire-datatables.default_time_start', '0000-00-00'),
+ isset($filter['end']) && $filter['end'] != '' ? $filter['end'] : config('livewire-datatables.default_time_end', '9999-12-31'),
]);
}
});
@@ -1451,7 +1542,10 @@ public function addTimeRangeFilter()
public function addSort()
{
if (isset($this->sort) && isset($this->freshColumns[$this->sort]) && $this->freshColumns[$this->sort]['name']) {
- $this->query->orderBy(DB::raw($this->getSortString()), $this->direction ? 'asc' : 'desc');
+ if (isset($this->pinnedRecords) && $this->pinnedRecords) {
+ $this->query->orderBy(DB::raw('FIELD(id,' . implode(',', $this->pinnedRecords) . ')'), 'DESC');
+ }
+ $this->query->orderBy(DB::raw($this->getSortString($this->query->getConnection()->getPDO()->getAttribute(\PDO::ATTR_DRIVER_NAME))), $this->direction ? 'asc' : 'desc');
}
return $this;
@@ -1484,6 +1578,9 @@ public function mapCallbacks($paginatedCollection, $export = false)
{
$paginatedCollection->collect()->map(function ($row, $i) use ($export) {
foreach ($row as $name => $value) {
+ if ($this->search && ! config('livewire-datatables.suppress_search_highlights') && $this->searchableColumns()->firstWhere('name', $name)) {
+ $row->$name = $this->highlight($row->$name, $this->search);
+ }
if ($export && isset($this->export_callbacks[$name])) {
$values = Str::contains($value, static::SEPARATOR) ? explode(static::SEPARATOR, $value) : [$value, $row];
$row->$name = $this->export_callbacks[$name](...$values);
@@ -1492,10 +1589,8 @@ public function mapCallbacks($paginatedCollection, $export = false)
'value' => $value,
'key' => $this->builder()->getModel()->getQualifiedKeyName(),
'column' => Str::after($name, '.'),
- 'rowId' => $row->{$this->builder()->getModel()->getTable() . '.' . $this->builder()->getModel()->getKeyName()} ?? $row->{$this->builder()->getModel()->getKeyName()},
+ 'rowId' => $row->{$name . '_edit_id'},
]);
- } elseif ($export && isset($this->export_callbacks[$name])) {
- $row->$name = $this->export_callbacks[$name]($value, $row);
} elseif (isset($this->callbacks[$name]) && is_string($this->callbacks[$name])) {
$row->$name = $this->{$this->callbacks[$name]}($value, $row);
} elseif (Str::startsWith($name, 'callback_')) {
@@ -1503,10 +1598,6 @@ public function mapCallbacks($paginatedCollection, $export = false)
} elseif (isset($this->callbacks[$name]) && is_callable($this->callbacks[$name])) {
$row->$name = $this->callbacks[$name]($value, $row);
}
-
- if ($this->search && ! config('livewire-datatables.suppress_search_highlights') && $this->searchableColumns()->firstWhere('name', $name)) {
- $row->$name = $this->highlight($row->$name, $this->search);
- }
}
return $row;
@@ -1582,24 +1673,30 @@ public function render()
return view('datatables::datatable')->layoutData(['title' => $this->title]);
}
- public function export()
+ public function export(string $filename = 'DatatableExport.xlsx')
{
$this->forgetComputed();
- $results = $this->mapCallbacks(
+ $export = new DatatableExport($this->getExportResultsSet());
+ $export->setFilename($filename);
+
+ return $export->download();
+ }
+
+ public function getExportResultsSet()
+ {
+ return $this->mapCallbacks(
$this->getQuery()->when(count($this->selected), function ($query) {
return $query->havingRaw('checkbox_attribute IN (' . implode(',', $this->selected) . ')');
})->get(),
true
)->map(function ($item) {
- return collect($this->columns)->reject(function ($value, $key) {
+ return collect($this->columns)->reject(function ($value) {
return $value['preventExport'] == true || $value['hidden'] == true;
- })->mapWithKeys(function ($value, $key) use ($item) {
+ })->mapWithKeys(function ($value) use ($item) {
return [$value['label'] ?? $value['name'] => $item->{$value['name']}];
})->all();
});
-
- return Excel::download(new DatatableExport($results), $this->export_name ? $this->export_name . '.xlsx' : 'DatatableExport.xlsx');
}
public function getQuery($export = false)
@@ -1611,8 +1708,6 @@ public function getQuery($export = false)
public function checkboxQuery()
{
- $this->resolveCheckboxColumnName(collect($this->freshColumns)->firstWhere('type', 'checkbox'));
-
return $this->query->reorder()->get()->map(function ($row) {
return (string) $row->checkbox_attribute;
});
@@ -1620,14 +1715,38 @@ public function checkboxQuery()
public function toggleSelectAll()
{
- if (count($this->selected) === $this->getQuery()->count()) {
- $this->selected = [];
+ $visible_checkboxes = $this->getQuery()->get()->pluck('checkbox_attribute')->toArray();
+ $visible_checkboxes = array_map('strval', $visible_checkboxes);
+ if ($this->searchOrFilterActive()) {
+ if (count($this->visibleSelected) === count($visible_checkboxes)) {
+ $this->selected = array_values(array_diff($this->selected, $visible_checkboxes));
+ $this->visibleSelected = [];
+ } else {
+ $this->selected = array_unique(array_merge($this->selected, $visible_checkboxes));
+ sort($this->selected);
+ $this->visibleSelected = $visible_checkboxes;
+ }
} else {
- $this->selected = $this->checkboxQuery()->values()->toArray();
+ if (count($this->selected) === $this->getQuery()->getCountForPagination()) {
+ $this->selected = [];
+ } else {
+ $this->selected = $this->checkboxQuery()->values()->toArray();
+ }
+ $this->visibleSelected = $this->selected;
}
+
$this->forgetComputed();
}
+ public function updatedSelected()
+ {
+ if ($this->searchOrFilterActive()) {
+ $this->setVisibleSelected();
+ } else {
+ $this->visibleSelected = $this->selected;
+ }
+ }
+
public function rowIsSelected($row)
{
return isset($row->checkbox_attribute) && in_array($row->checkbox_attribute, $this->selected);
@@ -1648,6 +1767,11 @@ public function getSavedQueries()
// Override this method with your own method for getting saved queries
}
+ public function buildActions()
+ {
+ // Override this method with your own method for creating mass actions
+ }
+
public function rowClasses($row, $loop)
{
// Override this method with your own method for adding classes to a row
@@ -1667,4 +1791,81 @@ public function cellClasses($row, $column)
// Override this method with your own method for adding classes to a cell
return config('livewire-datatables.default_classes.cell', 'text-sm text-gray-900');
}
+
+ public function getMassActions()
+ {
+ return collect($this->massActions)->map(function ($action) {
+ return collect($action)->only(['group', 'value', 'label'])->toArray();
+ })->toArray();
+ }
+
+ public function getMassActionsProperty()
+ {
+ $actions = collect($this->buildActions())->flatten();
+
+ $duplicates = $actions->pluck('value')->duplicates();
+
+ if ($duplicates->count()) {
+ throw new Exception('Duplicate Mass Action(s): ' . implode(', ', $duplicates->toArray()));
+ }
+
+ return $actions->toArray();
+ }
+
+ public function getMassActionsOptionsProperty()
+ {
+ return collect($this->actions)->groupBy(function ($item) {
+ return $item['group'];
+ }, true);
+ }
+
+ public function massActionOptionHandler()
+ {
+ if (! $this->massActionOption) {
+ return;
+ }
+
+ $option = $this->massActionOption;
+
+ $action = collect($this->massActions)->filter(function ($item) use ($option) {
+ return $item->value === $option;
+ })->shift();
+
+ $collection = collect($action);
+
+ if ($collection->get('isExport')) {
+ $datatableExport = new DatatableExport($this->getExportResultsSet());
+
+ $datatableExport->setFileName($collection->get('fileName'));
+
+ $datatableExport->setStyles($collection->get('styles'));
+
+ $datatableExport->setColumnWidths($collection->get('widths'));
+
+ return $datatableExport->download();
+ }
+
+ if (! count($this->selected)) {
+ $this->massActionOption = null;
+
+ return;
+ }
+
+ if ($collection->has('callable') && is_callable($action->callable)) {
+ $action->callable($option, $this->selected);
+ }
+
+ $this->massActionOption = null;
+ }
+
+ private function searchOrFilterActive()
+ {
+ return ! empty($this->search) || $this->getActiveFiltersProperty();
+ }
+
+ private function setVisibleSelected()
+ {
+ $this->visibleSelected = array_intersect($this->getQuery()->get()->pluck('checkbox_attribute')->toArray(), $this->selected);
+ $this->visibleSelected = array_map('strval', $this->visibleSelected);
+ }
}
diff --git a/src/LivewireDatatablesServiceProvider.php b/src/LivewireDatatablesServiceProvider.php
index 8fcaa3f2..f63dde05 100644
--- a/src/LivewireDatatablesServiceProvider.php
+++ b/src/LivewireDatatablesServiceProvider.php
@@ -11,6 +11,7 @@
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Livewire\Livewire;
+use Mediconesystems\LivewireDatatables\Commands\DatatableMakeCommand;
use Mediconesystems\LivewireDatatables\Commands\MakeDatatableCommand;
use Mediconesystems\LivewireDatatables\Http\Controllers\FileExportController;
use Mediconesystems\LivewireDatatables\Http\Livewire\ComplexQuery;
@@ -48,7 +49,7 @@ public function boot()
__DIR__ . '/../resources/views/icons' => resource_path('views/livewire/datatables/icons'),
], 'views');
- $this->commands([MakeDatatableCommand::class]);
+ $this->commands([MakeDatatableCommand::class, DatatableMakeCommand::class]);
}
Route::get('/datatables/{filename}', [FileExportController::class, 'handle'])
@@ -94,7 +95,7 @@ public function loadEloquentBuilderMacros()
$table = $relation->getRelated()->newQuery()->getQuery()->from === $this->getQuery()->from
? $relation->getRelationCountHashWithoutIncrementing()
- : $relation->getRelated()->getTable();
+ : ($this->query->getConnection()->getTablePrefix() ?? '') . $relation->getRelated()->getTable();
$query = $relation->getRelationExistenceAggregatesQuery(
$relation->getRelated()->newQuery(),
@@ -124,7 +125,7 @@ public function loadEloquentBuilderMacros()
$table = $relation->getRelated()->newQuery()->getQuery()->from === $this->getQuery()->from
? $relation->getRelationCountHashWithoutIncrementing()
- : $relation->getRelated()->getTable();
+ : ($this->query->getConnection()->getTablePrefix() ?? '') . $relation->getRelated()->getTable();
$hasQuery = $relation->getRelationExistenceAggregatesQuery(
$relation->getRelated()->newQueryWithoutRelationships(),
@@ -149,6 +150,7 @@ public function loadRelationMacros()
}
$expression = $aggregate === 'group_concat'
+
? $distinct_aggregate
: new Expression('COALESCE(' . $aggregate . "({$column}),0)");
diff --git a/src/NumberColumn.php b/src/NumberColumn.php
index e37dd442..7dbeb558 100644
--- a/src/NumberColumn.php
+++ b/src/NumberColumn.php
@@ -5,10 +5,11 @@
class NumberColumn extends Column
{
public $type = 'number';
- public $align = 'right';
+ public $headerAlign = 'right';
+ public $contentAlign = 'right';
public $round;
- public function round($places = 0)
+ public function round($places = 0): self
{
$this->round = $places;
@@ -18,4 +19,13 @@ public function round($places = 0)
return $this;
}
+
+ public function format(int $places = 0): self
+ {
+ $this->callback = function ($value) use ($places) {
+ return number_format($value, $places, '.', ',');
+ };
+
+ return $this;
+ }
}
diff --git a/src/Traits/CanPinRecords.php b/src/Traits/CanPinRecords.php
new file mode 100644
index 00000000..c695bbe7
--- /dev/null
+++ b/src/Traits/CanPinRecords.php
@@ -0,0 +1,82 @@
+label(__('Pin selected Records'))
+ ->callback(function ($mode, $items) {
+ $this->pinnedRecords = array_merge($this->pinnedRecords, $items);
+ $this->selected = $this->pinnedRecords;
+
+ session()->put($this->sessionKey(), $this->pinnedRecords);
+ }),
+
+ Action::value('unpin')
+ ->label(__('Unpin selected Records'))
+ ->callback(function ($mode, $items) {
+ $this->pinnedRecords = array_diff($this->pinnedRecords, $items);
+ $this->selected = $this->pinnedRecords;
+
+ session()->put($this->sessionKey(), $this->pinnedRecords);
+ }),
+ ]);
+ }
+
+ public function resetTable()
+ {
+ parent::resetTable();
+ $this->pinnedRecords = [];
+ }
+
+ protected function initialisePinnedRecords()
+ {
+ if (session()->has($this->sessionKey())) {
+ $this->pinnedRecords = session()->get($this->sessionKey());
+ }
+
+ $this->selected = $this->pinnedRecords;
+ }
+
+ /**
+ * This function should be called after every filter method to ensure pinned records appear
+ * in every possible filter combination.
+ * Ensures to have at least _one other_ where applied to the current query build
+ * to apply this orWhere() on top of that.
+ */
+ protected function applyPinnedRecords(): self
+ {
+ if (isset($this->pinnedRecords) && $this->pinnedRecords && $this->query->getQuery()->wheres) {
+ $this->query->orWhereIn('id', $this->pinnedRecords);
+ }
+
+ return $this;
+ }
+
+ private function sessionKey(): string
+ {
+ return $this->sessionStorageKey() . $this->sessionKeyPostfix;
+ }
+}
diff --git a/src/Traits/WithCallbacks.php b/src/Traits/WithCallbacks.php
index 5a72d64a..540fa8f9 100644
--- a/src/Traits/WithCallbacks.php
+++ b/src/Traits/WithCallbacks.php
@@ -13,6 +13,6 @@ public function edited($value, $key, $column, $rowId)
->where(Str::after($key, '.'), $rowId)
->update([$column => $value]);
- $this->emit('fieldEdited', $rowId);
+ $this->emit('fieldEdited', $rowId, $column);
}
}
diff --git a/tests/ColumnTest.php b/tests/ColumnTest.php
index c779ac4c..20b772b6 100644
--- a/tests/ColumnTest.php
+++ b/tests/ColumnTest.php
@@ -81,6 +81,7 @@ public function it_returns_an_array_from_column()
'sort' => null,
'defaultSort' => null,
'searchable' => null,
+ 'sortable' => null,
'params' => [],
'additionalSelects' => [],
'scope' => null,
@@ -89,10 +90,9 @@ public function it_returns_an_array_from_column()
'select' => null,
'joins' => null,
'aggregate' => 'group_concat',
- 'align' => 'left',
+ 'headerAlign' => 'left',
'preventExport' => null,
'width' => null,
- 'unsortable' => null,
'exportCallback' => function () {
},
'filterOn' => null,
@@ -126,6 +126,7 @@ public function it_returns_an_array_from_raw()
'sort' => 'SELECT column FROM table',
'defaultSort' => 'asc',
'searchable' => null,
+ 'sortable' => null,
'params' => [],
'additionalSelects' => [],
'scope' => null,
@@ -133,10 +134,9 @@ public function it_returns_an_array_from_raw()
'filterView' => null,
'select' => DB::raw('SELECT column FROM table'),
'joins' => null,
- 'align' => 'left',
+ 'headerAlign' => 'left',
'preventExport' => null,
'width' => null,
- 'unsortable' => null,
'exportCallback' => null,
'filterOn' => null,
'group' => null,
@@ -167,6 +167,7 @@ public function it_returns_width_property_from_column()
'sort' => null,
'defaultSort' => null,
'searchable' => null,
+ 'sortable' => null,
'params' => [],
'additionalSelects' => [],
'scope' => null,
@@ -175,10 +176,9 @@ public function it_returns_width_property_from_column()
'select' => null,
'joins' => null,
'aggregate' => 'group_concat',
- 'align' => 'left',
+ 'headerAlign' => 'left',
'preventExport' => null,
'width' => '1em',
- 'unsortable' => null,
'exportCallback' => null,
'filterOn' => null,
'group' => null,
@@ -216,7 +216,7 @@ public function check_invalid_width_unit_not_returning_value()
'select' => null,
'joins' => null,
'aggregate' => 'group_concat',
- 'align' => 'left',
+ 'headerAlign' => 'left',
'preventExport' => null,
'width' => null,
], $subject);
@@ -249,7 +249,7 @@ public function check_adding_px_to_numeric_width_input()
'select' => null,
'joins' => null,
'aggregate' => 'group_concat',
- 'align' => 'left',
+ 'headerAlign' => 'left',
'preventExport' => null,
'width' => '5px',
], $subject);
diff --git a/tests/LivewireDatatableQueryBuilderTest.php b/tests/LivewireDatatableQueryBuilderTest.php
index 6d839891..725b5c6f 100644
--- a/tests/LivewireDatatableQueryBuilderTest.php
+++ b/tests/LivewireDatatableQueryBuilderTest.php
@@ -123,17 +123,17 @@ public function it_creates_a_query_builder_for_belongs_to_relation_columns()
$subject = new LivewireDatatable(1);
$subject->mount(DummyHasManyModel::class, ['id', 'dummy_model.name']);
- $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_has_many_models"."dummy_model_id" = "dummy_models"."id" order by `id` desc', $subject->getQuery()->toSql());
+ $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_models"."id" = "dummy_has_many_models"."dummy_model_id" order by `id` desc', $subject->getQuery()->toSql());
$subject->sort(1);
$subject->forgetComputed();
- $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_has_many_models"."dummy_model_id" = "dummy_models"."id" order by dummy_models.name desc', $subject->getQuery()->toSql());
+ $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_models"."id" = "dummy_has_many_models"."dummy_model_id" order by dummy_models.name desc', $subject->getQuery()->toSql());
$subject->sort(1);
$subject->forgetComputed();
- $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_has_many_models"."dummy_model_id" = "dummy_models"."id" order by dummy_models.name asc', $subject->getQuery()->toSql());
+ $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_models"."id" = "dummy_has_many_models"."dummy_model_id" order by dummy_models.name asc', $subject->getQuery()->toSql());
}
/** @test */
@@ -148,7 +148,7 @@ public function it_creates_a_where_query_for_belongs_to_relation_columns()
// $subject->doNumberFilterEnd(1, 456);
$subject->forgetComputed();
- $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_has_many_models"."dummy_model_id" = "dummy_models"."id" where (dummy_models.name >= ?) order by dummy_has_many_models.id desc', $subject->getQuery()->toSql());
+ $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_models"."id" = "dummy_has_many_models"."dummy_model_id" where (dummy_models.name >= ?) order by dummy_has_many_models.id desc', $subject->getQuery()->toSql());
$this->assertEquals([123], $subject->getQuery()->getBindings());
@@ -163,7 +163,7 @@ public function it_creates_a_where_query_for_belongs_to_relation_columns()
$subject->doNumberFilterEnd(1, null);
$subject->forgetComputed();
- $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_has_many_models"."dummy_model_id" = "dummy_models"."id" order by dummy_has_many_models.id desc', $subject->getQuery()->toSql());
+ $this->assertEquals('select "dummy_has_many_models"."id" as "id", "dummy_models"."name" as "dummy_model.name" from "dummy_has_many_models" left join "dummy_models" on "dummy_models"."id" = "dummy_has_many_models"."dummy_model_id" order by dummy_has_many_models.id desc', $subject->getQuery()->toSql());
$this->assertEquals([], $subject->getQuery()->getBindings());
}
diff --git a/tests/MakeDatatableCommandTest.php b/tests/MakeDatatableCommandTest.php
index 578aa243..4414f268 100644
--- a/tests/MakeDatatableCommandTest.php
+++ b/tests/MakeDatatableCommandTest.php
@@ -12,7 +12,7 @@ class MakeDatatableCommandTest extends TestCase
/** @test */
public function component_is_created_by_make_command()
{
- Artisan::call('livewire:datatable', ['name' => 'foo']);
+ Artisan::call('make:livewire-datatable', ['name' => 'foo']);
$this->assertTrue(File::exists($this->livewireClassesPath('Foo.php')));
}
@@ -20,7 +20,7 @@ public function component_is_created_by_make_command()
/** @test */
public function dot_nested_component_is_created_by_make_command()
{
- Artisan::call('livewire:datatable', ['name' => 'foo.bar']);
+ Artisan::call('make:livewire-datatable', ['name' => 'foo.bar']);
$this->assertTrue(File::exists($this->livewireClassesPath('Foo/Bar.php')));
}
@@ -28,7 +28,7 @@ public function dot_nested_component_is_created_by_make_command()
/** @test */
public function forward_slash_nested_component_is_created_by_make_command()
{
- Artisan::call('livewire:datatable', ['name' => 'foo/bar']);
+ Artisan::call('make:livewire-datatable', ['name' => 'foo/bar']);
$this->assertTrue(File::exists($this->livewireClassesPath('Foo/Bar.php')));
}
@@ -36,7 +36,7 @@ public function forward_slash_nested_component_is_created_by_make_command()
/** @test */
public function multiword_component_is_created_by_make_command()
{
- Artisan::call('livewire:datatable', ['name' => 'foo-bar']);
+ Artisan::call('make:livewire-datatable', ['name' => 'foo-bar']);
$this->assertTrue(File::exists($this->livewireClassesPath('FooBar.php')));
}
@@ -44,7 +44,7 @@ public function multiword_component_is_created_by_make_command()
/** @test */
public function pascal_case_component_is_automatically_converted_by_make_command()
{
- Artisan::call('livewire:datatable', ['name' => 'FooBar.FooBar']);
+ Artisan::call('make:livewire-datatable', ['name' => 'FooBar.FooBar']);
$this->assertTrue(File::exists($this->livewireClassesPath('FooBar/FooBar.php')));
}
@@ -52,7 +52,7 @@ public function pascal_case_component_is_automatically_converted_by_make_command
/** @test */
public function snake_case_component_is_automatically_converted_by_make_command()
{
- Artisan::call('livewire:datatable', ['name' => 'text_replace']);
+ Artisan::call('make:livewire-datatable', ['name' => 'text_replace']);
$this->assertTrue(File::exists($this->livewireClassesPath('TextReplace.php')));
}
@@ -60,7 +60,7 @@ public function snake_case_component_is_automatically_converted_by_make_command(
/** @test */
public function snake_case_component_is_automatically_converted_by_make_command_on_nested_component()
{
- Artisan::call('livewire:datatable', ['name' => 'TextManager.text_replace']);
+ Artisan::call('make:livewire-datatable', ['name' => 'TextManager.text_replace']);
$this->assertTrue(File::exists($this->livewireClassesPath('TextManager/TextReplace.php')));
}
@@ -79,7 +79,7 @@ public function new_component_model_name_matches_option()
/** @test */
public function a_component_is_not_created_with_a_reserved_class_name()
{
- Artisan::call('livewire:datatable', ['name' => 'component']);
+ Artisan::call('make:livewire-datatable', ['name' => 'component']);
$this->assertFalse(File::exists($this->livewireClassesPath('Component.php')));
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 7f834513..9680e5d3 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -8,6 +8,7 @@
use Maatwebsite\Excel\ExcelServiceProvider;
use Mediconesystems\LivewireDatatables\LivewireDatatablesServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;
+use Reedware\LaravelRelationJoins\LaravelRelationJoinServiceProvider;
class TestCase extends Orchestra
{
@@ -26,6 +27,7 @@ protected function getPackageProviders($app)
LivewireServiceProvider::class,
LivewireDatatablesServiceProvider::class,
ExcelServiceProvider::class,
+ LaravelRelationJoinServiceProvider::class,
];
}