├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── laravel-table.php ├── resources └── views │ ├── bootstrap-4 │ ├── bulk-action.blade.php │ ├── column-action.blade.php │ ├── filter.blade.php │ ├── head-action.blade.php │ ├── row-action.blade.php │ └── table.blade.php │ └── bootstrap-5 │ ├── bulk-action.blade.php │ ├── column-action.blade.php │ ├── filter.blade.php │ ├── head-action.blade.php │ ├── row-action.blade.php │ └── table.blade.php └── src ├── Abstracts ├── AbstractBulkAction.php ├── AbstractColumnAction.php ├── AbstractFilter.php ├── AbstractFormatter.php ├── AbstractHeadAction.php ├── AbstractRowAction.php └── AbstractTableConfiguration.php ├── BulkActions ├── ActivateBulkAction.php ├── CancelEmailVerificationBulkAction.php ├── DeactivateBulkAction.php ├── DestroyBulkAction.php └── VerifyEmailBulkAction.php ├── Column.php ├── ColumnActions ├── ToggleBooleanColumnAction.php └── ToggleEmailVerifiedColumnAction.php ├── Console └── Commands │ ├── MakeBulkAction.php │ ├── MakeColumnAction.php │ ├── MakeFilter.php │ ├── MakeFormatter.php │ ├── MakeHeadAction.php │ ├── MakeRowAction.php │ ├── MakeTable.php │ └── stubs │ ├── bulk.action.stub │ ├── column.action.stub │ ├── filter.stub │ ├── formatter.stub │ ├── head.action.stub │ ├── row.action.stub │ ├── table.model.stub │ └── table.stub ├── Exceptions ├── InvalidColumnSortDirection.php ├── InvalidTableConfiguration.php ├── NoColumnsDeclared.php └── UnrecognizedActionType.php ├── Filters ├── BooleanFilter.php ├── NullFilter.php ├── RelationshipFilter.php └── ValueFilter.php ├── Formatters ├── BooleanFormatter.php ├── DateFormatter.php └── StrLimitFormatter.php ├── HeadActions ├── AddHeadAction.php ├── CreateHeadAction.php └── RedirectHeadAction.php ├── LaravelTableServiceProvider.php ├── Livewire └── Table.php ├── Result.php ├── RowActions ├── DestroyRowAction.php ├── EditRowAction.php ├── RedirectRowAction.php └── ShowRowAction.php └── Table.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this package will be documented in this file. 4 | 5 | ## [5.3.1](https://github.com/Okipa/laravel-table/compare/5.3.0...5.3.1) 6 | 7 | 2023-04-02 8 | 9 | * Fixed searching SQL request for PostgreSQL by casting every values into text (fixes #129) 10 | 11 | ## [5.3.0](https://github.com/Okipa/laravel-table/compare/5.2.2...5.3.0) 12 | 13 | 2023-01-21 14 | 15 | * Added PHP 8.2 support and locked PHP versions to 8.1 and 8.2 16 | * Dropped Laravel 8 support 17 | * Added Laravel 10 support 18 | 19 | ## [5.2.2](https://github.com/Okipa/laravel-table/compare/5.2.1...5.2.2) 20 | 21 | 2023-01-01 22 | 23 | * Fixed filter reset button missing margin-top class for Bootstrap 4 & 5 templates 24 | 25 | ## [5.2.1](https://github.com/Okipa/laravel-table/compare/5.2.0...5.2.1) 26 | 27 | 2022-11-14 28 | 29 | * Added the ability to redefine the entire table positioning when reordering instead of just reversing some possibly incorrect positions 30 | 31 | ## [5.2.0](https://github.com/Okipa/laravel-table/compare/5.1.2...5.2.0) 32 | 33 | 2022-10-28 34 | 35 | * Added a new built-in `RedirectRowAction`, that is now used to render the pre-configured `ShowRowAction` 36 | * Added an optional second argument `bool $openInNewWindow = false` to the `ShowRowAction` 37 | * Added a new pre-configured `AddHeadAction`, that is using the built-in `RedirectHeadAction` 38 | * Added a new `Add` translation for it that you'll have to add to [your own translations](/README.md#translations) 39 | * Added a new `config('laravel-table.icon.add')` config for it with the `` default value that you'll also have to add to [your published configuration file](/README.md#configuration) 40 | 41 | ## [5.1.2](https://github.com/Okipa/laravel-table/compare/5.1.1...5.1.2) 42 | 43 | 2022-10-27 44 | 45 | * Improved CI by @szepeviktor in https://github.com/Okipa/laravel-table/pull/110 46 | * Improved PHPStan config by @szepeviktor (first contribution) in https://github.com/Okipa/laravel-table/pull/109 47 | 48 | ## [5.1.1](https://github.com/Okipa/laravel-table/compare/5.1.0...5.1.1) 49 | 50 | 2022-10-26 51 | 52 | * Fixed [reordering feature](/README.md#allow-columns-to-be-reordered-from-drag-and-drop-action) that was not compatible with packages like [spatie/eloquent-sortable](https://github.com/spatie/eloquent-sortable) (which authorize several model entries to have the same position when the sorting does group models from a query => [example](https://github.com/spatie/eloquent-sortable#grouping)) 53 | 54 | ## [5.1.0](https://github.com/Okipa/laravel-table/compare/5.0.2...5.1.0) 55 | 56 | 2022-10-25 57 | 58 | * Added ability to chain a `->when(bool $condition)` method to an instantiated head action, in order to enable it conditionally 59 | * Added a new built-in `RedirectHeadAction`, that is now used to render the pre-configured `CreateHeadAction` 60 | * Added an optional second argument `bool $openInNewWindow = false` to the `CreateHeadAction` 61 | * Added a new [JavaScript snippet](/README.md#set-up-a-few-lines-of-javascript) to handle head action link opening in tab: you'll have to add it if you want to benefit from this new ability 62 | 63 | ## [5.0.2](https://github.com/Okipa/laravel-table/compare/5.0.1...5.0.2) 64 | 65 | 2022-10-07 66 | 67 | * Fixed wrong `form-select` class uses for Bootstrap 4 template selects: replaced them by `custom-select` 68 | * Fixed Column action still displays original column value with ->when(false) : #103 69 | 70 | ## [5.0.1](https://github.com/Okipa/laravel-table/compare/5.0.0...5.0.1) 71 | 72 | 2022-09-26 73 | 74 | * Fixed `make:table --model=` command 75 | 76 | ## [5.0.0](https://github.com/Okipa/laravel-table/compare/4.0.4...5.0.0) 77 | 78 | 2022-09-21 79 | 80 | * Added support for Laravel 9 81 | * Dropped support for Laravel 7 and earlier versions 82 | * Added support for PHP 8.1 83 | * Dropped support for PHP 8.0 and earlier versions 84 | * Added `livewire/livewire` dependency as the package is now based on it 85 | * Removed `okipa/laravel-html-helper` dependency 86 | * Replaced `phpcs/phpcbf` by `laravel/pint` 87 | * Added several features : 88 | * New SPA behaviour 89 | * New filters ability 90 | * Head actions (replacing routes declarations) 91 | * New bulk actions ability 92 | * Row actions (replacing routes declarations) 93 | * New ability to search from closure 94 | * New ability to sort from closure 95 | * New column actions ability 96 | * New ability to use built-in column formatters and to create custom ones 97 | * New drag-and-drop reordering ability 98 | 99 | :point_right: [See the upgrade guide](/docs/upgrade-guides/from-v4-to-v5.md) 100 | 101 | ## [4.0.7](https://github.com/Okipa/laravel-table/compare/4.0.6...4.0.7) 102 | 103 | 2022-05-17 104 | 105 | * Fixed search wrong behaviour when rows number is defined to `null` 106 | 107 | ## [4.0.6](https://github.com/Okipa/laravel-table/compare/4.0.5...4.0.6) 108 | 109 | 2022-03-10 110 | 111 | * Added ability to set a timezone when formatting a date/time column with the `dateTimeFormat` method: 112 | * Updated column `dateTimeFormat` method signature to `dateTimeFormat(string $dateTimeFormat, string $timezone = null): \Okipa\LaravelTable\Column` 113 | * If no timezone is set, the default one, defined in `config('app.timezone')` is used 114 | 115 | ## [4.0.5](https://github.com/Okipa/laravel-table/compare/4.0.4...4.0.5) 116 | 117 | 2022-03-01 118 | 119 | * Fixed error when appending array data to table with the `appendData` method 120 | 121 | ## [4.0.4](https://github.com/Okipa/laravel-table/compare/4.0.3...4.0.4) 122 | 123 | 2021-08-17 124 | 125 | * Added `$type` property to `src/Console/Commands/MakeTable` in order to display correct messages: 126 | * `Table already exists!` 127 | * `Table created successfully.` 128 | 129 | ## [4.0.3](https://github.com/Okipa/laravel-table/compare/4.0.2...4.0.3) 130 | 131 | 2021-04-11 132 | 133 | * Updated table generation stub when called with `--model` option 134 | 135 | ## [4.0.2](https://github.com/Okipa/laravel-table/compare/4.0.1...4.0.2) 136 | 137 | 2021-01-10 138 | 139 | * Fixed #66 : Bugfix to allow the `->rowsConditionalClasses()` method to be called several times on a table. In addition, the second `$rowClasses` argument of this method now accepts array or closure (which let you manipulate a `\Illuminate\Database\Eloquent\Model $model` attribute) 140 | * Fixed #68 : Removed useless treatment when data is appended to paginator with `->appendData()` method, which led to `+` character addition when values were containing spaces 141 | 142 | ## [4.0.1](https://github.com/Okipa/laravel-table/compare/4.0.0...4.0.1) 143 | 144 | 2020-09-14 145 | 146 | * Fixed wrong `okipa/laravel-html-helper` version in composer.json 147 | 148 | ## [4.0.0](https://github.com/Okipa/laravel-table/compare/3.1.3...4.0.0) 149 | 150 | 2020-09-14 151 | 152 | * Added PHP 8 support 153 | * Removed Scrutinizer analysis 154 | * Updated PHPCS checker and fixer norm to PSR-12 155 | * Upgraded https://github.com/Okipa/laravel-html-helper to v2 156 | 157 | :point_right: [See the upgrade guide](/docs/upgrade-guides/from-v3-to-v4.md) 158 | 159 | ## [3.1.3](https://github.com/Okipa/laravel-table/compare/3.1.2...3.1.3) 160 | 161 | 2020-09-10 162 | 163 | * Fixed wrong sensitive case searching which was involuntarily executed for JSON database fields instead of insensitive case searching as normal 164 | 165 | ## [3.1.2](https://github.com/Okipa/laravel-table/compare/3.1.1...3.1.2) 166 | 167 | 2020-08-24 168 | 169 | * Fixed doc js snippet given in [destroyConfirmationHtmlAttributes](./README.md#table-destroyConfirmationHtmlAttributes) 170 | 171 | ## [3.1.1](https://github.com/Okipa/laravel-table/compare/3.1.0...3.1.1) 172 | 173 | 2020-08-24 174 | 175 | * Fixed column cell value not escaped when using `$column->value()` method (https://github.com/Okipa/laravel-table/issues/54) 176 | 177 | ## [3.1.0](https://github.com/Okipa/laravel-table/compare/3.0.1...3.1.0) 178 | 179 | 2020-08-24 180 | 181 | * Reverted the previous change (3.0.1) as the `button` method without arguments has no visual effect: added instructions in V2 to v3 the upgrade-guide to take care of this new behaviour 182 | * Fixed an issue where create action button was not displayed when searching and rows number definition were disabled 183 | * Fixed a v3 regression where `rows number definition` was wrongly named `rows number selection` at different places (config, templates, methods, ...): this is an unfortunately breaking change if you published config or templates but I take advantage of the early release of the V3 and from the fact that Laravel 8 is not released to do it 184 | * Show and edit actions are now triggered by a simple link rather than a form, which was useless as these routes are called with a `GET` http request 185 | * Minor default templates changes in order to give laravel-table a prettier look 186 | * Minor default config value changes in order to give laravel-table a prettier look 187 | 188 | ## [3.0.1](https://github.com/Okipa/laravel-table/compare/3.0.1...3.0.1) 189 | 190 | 2020-08-24 191 | 192 | * Fixed Column `button` method behaviour which is supposed to allow usage without arguments 193 | 194 | ## [3.0.0](https://github.com/Okipa/laravel-table/compare/2.0.0...3.0.0) 195 | 196 | 2020-08-09 197 | 198 | * Added Laravel 8 support 199 | * Dropped Laravel 6 support 200 | * Added template customization methods for `Table` instances : 201 | * `rowsSearchingTemplate` 202 | * `rowsNumberDefinitionTemplate` 203 | * `createActionTemplate` 204 | * `columnTitlesTemplate` 205 | * `navigationStatusTemplate` 206 | * `paginationTemplate` 207 | * Updated templates 208 | 209 | :point_right: [See the upgrade guide](/docs/upgrade-guides/from-v2-to-v3.md) 210 | 211 | ## [2.0.0](https://github.com/Okipa/laravel-table/compare/1.5.0...2.0.0) 212 | 213 | 2020-04-30 214 | 215 | * Dropped support for PHP versions under 7.4 216 | * Dropped support for Laravel versions under 6.0 217 | * Restructured configuration file 218 | * Removed translation files 219 | * Updated templates 220 | * Removed the deprecated methods 221 | * Refactored the whole architecture to facilitate code comprehension and maintenance 222 | * New architecture and usage 223 | 224 | :point_right: [See the upgrade guide](/docs/upgrade-guides/from-v1-to-v2.md) 225 | 226 | ## [1.5.0](https://github.com/Okipa/laravel-table/compare/1.4.0...1.5.0) 227 | 228 | 2020-04-26 229 | 230 | * Deprecated the `->icon()` method 231 | * Added the `->prepend()` method to the table columns (which will replace the previous `->icon()` one) to prepend HTML to a column value 232 | * Added the `->append()` method to the table columns to append HTML to a column value 233 | 234 | ## [1.4.0](https://github.com/Okipa/laravel-table/compare/1.3.0...1.4.0) 235 | 236 | 2020-04-26 237 | 238 | * Added more granularity in the template customization possibilities : the `show`, `edit` and `destroy` actions are now defined in their own component. This way, it becomes easier to customize tiny parts of the table without touching to the others 239 | * Added `config('laravel-table.template.show')`, `config('laravel-table.template.edit')` and `config('laravel-table.template.destroy')` configs to set each new default component path 240 | * Added `->showTemplate()`, `->editTemplate()` and `->destroyTemplate()` to give the ability to customize these templates on the fly 241 | * Added fallback path for each template if the config value is not defined, in order to prevent any update breaking change 242 | 243 | ## [1.3.0](https://github.com/Okipa/laravel-table/compare/1.2.7...1.3.0) 244 | 245 | 2020-04-25 246 | 247 | * Tests have been migrated from Travis to Github actions 248 | * Added PHP7.4 support 249 | * Added Laravel 7 support 250 | * Dropped Laravel support before 5.8 version 251 | * Dropped PHP support before 7.2 version 252 | * Reworked the documentation 253 | 254 | ## [1.2.7](https://github.com/Okipa/laravel-table/compare/1.2.6...1.2.7) 255 | 256 | 2020-04-03 257 | 258 | * Fixed missing column when the `show` action is the only one defined 259 | 260 | ## [1.2.6](https://github.com/Okipa/laravel-table/compare/1.2.5...1.2.6) 261 | 262 | 2020-01-05 263 | 264 | * Replaced hard-coded `info` action icon by config value 265 | 266 | ## [1.2.5](https://github.com/Okipa/laravel-table/compare/1.2.4...1.2.5) 267 | 268 | 2019-10-15 269 | 270 | * Fixed the translations publication and overriding as specified on the Laravel documentation: https://laravel.com/docs/packages#translations 271 | * Changed the command to publish the translations to: `php artisan vendor:publish --tag=laravel-table:translations` 272 | * Changed the command to publish the configuration to: `php artisan vendor:publish --tag=laravel-table:config` 273 | * Changed the command to publish the views to: `php artisan vendor:publish --tag=laravel-table:views` 274 | * Improved testing with Travis CI (added some tests with `--prefer-lowest` composer tag to check the package compatibility with the lowest dependencies versions) 275 | 276 | ## [1.2.4](https://github.com/Okipa/laravel-table/compare/1.2.3...1.2.4) 277 | 278 | 2019-10-09 279 | 280 | * Transferred PhpUnit builds tasks from Scrutinizer to Travis CI 281 | * Transferred code coverage storage from Scrutinizer to Coveralls 282 | * Re-authorized PHP7.1 as minimal version 283 | 284 | ## [1.2.3](https://github.com/Okipa/laravel-table/compare/1.2.2...1.2.3) 285 | 286 | 2019-09-13 287 | 288 | * The model is now directly passed to the route during the table `show`, `edit` and `destroy` routes generation instead of its id 289 | ```php 290 | // Assuming your declared your edit route like this: 291 | (new Table())->model(User::class)->routes([ 292 | // ... 293 | 'edit' => ['name'=> 'user.edit', 'params' => ['foo' => 'bar']], 294 | //... 295 | ]); 296 | // The route will be generated like this during the table instantiation: 297 | route('user.edit', [$user, 'foo' => 'bar']); 298 | // Instead of this way: 299 | route('user.edit', [$user->id, 'foo' => 'bar']); 300 | ``` 301 | 302 | ## [1.2.2](https://github.com/Okipa/laravel-table/compare/1.2.1...1.2.2) 303 | 304 | 2019-09-13 305 | 306 | * Fixed params order when generating the table routes. The table model id was not positioned at first when declaring other parameters 307 | ```php 308 | // With a route declared like this: 309 | Route::get('user/edit/{user}/{foo}', 'UsersController@edit')->name('user.edit'); 310 | // And a table routes declaration like this: 311 | (new Table())->model(User::class)->routes([ 312 | // ... 313 | 'edit' => ['name'=> 'user.edit', 'params' => ['bar']], 314 | //... 315 | ]); 316 | // The route is now correctly generated and gives: /user/edit/1/bar instead of /user/edit/bar/1 317 | ``` 318 | 319 | ## [1.2.1](https://github.com/Okipa/laravel-table/compare/1.2.0...1.2.1) 320 | 321 | 2019-09-13 322 | 323 | * Fixed the `show`, `edit` and `destroy` route generation, since Laravel 6 does handle differently the key given in the `route()` helper: 324 | ```php 325 | // Assuming your declared your edit route like this: 326 | (new Table())->model(User::class)->routes([ 327 | // ... 328 | 'edit' => ['name'=> 'user.edit', 'params' => ['foo' => 'bar']], 329 | //... 330 | ]); 331 | // The route will be generated like this during the table instantiation: 332 | route('user.edit', [$user->id, 'foo' => 'bar']); 333 | // Instead of this way 334 | route('user.edit', ['id' => $user->id, 'foo' => 'bar']); 335 | ``` 336 | 337 | ## [1.2.0](https://github.com/Okipa/laravel-table/compare/1.1.0...1.2.0) 338 | 339 | 2019-09-04 340 | 341 | * Added compatibility for Laravel 6 342 | 343 | ## [1.1.0](https://github.com/Okipa/laravel-table/compare/1.0.13...1.1.0) 344 | 345 | 2019-08-02 346 | 347 | * Added the possibility to add an identifier to a table with `->identifier('your identifier')`. This identifier will be used for several things: 348 | * It will be added as an id (formatted as a slug string) to the table itself 349 | * It will be used to automatically customize the following interaction fields sent to the table, in order to be able to interact with a specific table if you have several of them on a single view: `rows`, `search`, `sort_by`, `sort_dir` 350 | * :warning: if you have published the views, you will have to re-publish them 351 | 352 | ## [1.0.13](https://github.com/Okipa/laravel-table/compare/1.0.12...1.0.13) 353 | 354 | 2019-05-14 355 | 356 | * Fixed a use case when no sortable columns are defined and an empty `orderBy` is called in the SQL request, causing an exception with MySQL 357 | 358 | ## [1.0.12](https://github.com/Okipa/laravel-table/compare/1.0.11...1.0.12) 359 | 360 | 2019-05-09 361 | 362 | * Locked project compatibility to Laravel 5.5+ and PHP7.2+ to avoid issues 363 | * Improved code phpdoc for better maintainability 364 | 365 | ## [1.0.11](https://github.com/Okipa/laravel-table/compare/1.0.10...1.0.11) 366 | 367 | 2019-05-06 368 | 369 | * Added `show` to the list of available routes. - _[@Dranthos](https://github.com/Dranthos)_ 370 | * Added Spanish translation. - _[@Dranthos](https://github.com/Dranthos)_ 371 | * Wrapped sortable columns titles in order to avoid line jump between the sort icon and the column title (Issue #14) 372 | * Improved rows number selection and search bar template to fix wrong display the rows number is disabled for example (Issue #15) 373 | * Added possibility to show all the models contained in database with `->rowsNumber(null)` chained on the `Table` instance (Issue #16) 374 | 375 | ## [1.0.10](https://github.com/Okipa/laravel-table/compare/1.0.9...1.0.10) 376 | 377 | 2019-02-21 378 | 379 | * Fixed a templating problem when disabling a line (one `td` html tag was missing) 380 | 381 | ## [1.0.9](https://github.com/Okipa/laravel-table/compare/1.0.8...1.0.9) 382 | 383 | 2019-02-21 384 | 385 | * Updated design in order to respect the bootstrap basics 386 | * Updated config architecture to improve the logic 387 | * The `edit` and `destroy` buttons are now hidden when a line is disabled 388 | * Improved compatibility with `postgres` for the searching action, using `ILIKE` instead of `LIKE` operator for case-insensitive searching 389 | 390 | ## [1.0.8](https://github.com/Okipa/laravel-table/compare/1.0.7...1.0.8) 391 | 392 | 2019-02-21 393 | 394 | * Updated the result displaying in one and only `td` html tag: the title is displayed on the left and the result html on the right 395 | * Also fixed the result classes rendering location, which is now on the `tr` html tag and no more on the `td` html tags 396 | 397 | ## [1.0.7](https://github.com/Okipa/laravel-table/compare/1.0.6...1.0.7) 398 | 399 | 2019-02-19 400 | 401 | * **Possible breaking change:** reverted last tag features => removed the capacity to add some result outputs with the `->result()` method the Column objects 402 | * **Possible breaking change:** reverted last tag features => removed the capacity to override default classes (config) for the results cells with the Table `->resultClasses()` method 403 | * Added the capacity to append some results objects to the table with the `->result()` method with the following methods: 404 | * `->title()`: Set the result row title 405 | * `->html()`: Display a HTML output for the result row. The closure let you manipulate the following attributes: `\Illuminate\Support\Collection $paginatedRows` 406 | * `->classes()`: Override the default results classes and apply the given classes only on this result row. The default result classes are managed by the `config('laravel-table.classes.results')` value 407 | * Added the capacity to manage a custom results template path in the config and with the `->resultsTemplatePath()` method 408 | 409 | ## [1.0.6](https://github.com/Okipa/laravel-table/compare/1.0.5...1.0.6) 410 | 411 | 2019-02-19 412 | 413 | * Added the capacity to add some result outputs with the `->result()` method the Column objects 414 | * Added the capacity to override default classes (config) for the results cells with the Table `->resultClasses()` method 415 | * Improved accessibility by adding `scope` attributes to correct td html tags 416 | 417 | ## [1.0.5](https://github.com/Okipa/laravel-table/compare/1.0.4...1.0.5) 418 | 419 | 2019-02-18 420 | 421 | * Updated `thead` and `tfoot` components in order to improve the responsive behavior 422 | 423 | ## [1.0.4](https://github.com/Okipa/laravel-table/compare/1.0.3...1.0.4) 424 | 425 | 2019-02-17 426 | 427 | * `->appends()` does now also add appended attributes to search canceling and sorting actions 428 | 429 | ## [1.0.3](https://github.com/Okipa/laravel-table/compare/1.0.2...1.0.3) 430 | 431 | 2019-02-15 432 | 433 | * `->appends()` method does now add appended key values to rows number selection form and to searching form as hidden fields 434 | 435 | ## [1.0.2](https://github.com/Okipa/laravel-table/compare/1.0.1...1.0.2) 436 | 437 | 2019-02-15 438 | 439 | * Fixed searching queries process on regular table columns fields when aliased tables are declared in the `->query()` table method 440 | 441 | ## [1.0.1](https://github.com/Okipa/laravel-table/releases/tag/1.0.1) 442 | 443 | 2019-02-13 444 | 445 | * Merged pull request https://github.com/Okipa/laravel-table/pull/2: wrapped searching queries into a `->where()` clause to avoid wrong behaviours when searching values. Thanks to https://github.com/costeirs for the PR 446 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Arthur LORENT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "okipa/laravel-table", 3 | "description": "Generate tables from Eloquent models.", 4 | "type": "library", 5 | "keywords": [ 6 | "Okipa", 7 | "package", 8 | "laravel", 9 | "php", 10 | "livewire", 11 | "table", 12 | "list", 13 | "laravel-table", 14 | "generate", 15 | "generator", 16 | "generation", 17 | "html" 18 | ], 19 | "homepage": "https://github.com/okipa/laravel-table", 20 | "license": "MIT", 21 | "authors": [ 22 | { 23 | "name": "Arthur LORENT", 24 | "email": "arthur.lorent@gmail.com", 25 | "role": "Developer" 26 | } 27 | ], 28 | "require": { 29 | "php": "8.1.*|8.2.*", 30 | "illuminate/contracts": "^9.0|^10.0", 31 | "livewire/livewire": "^2.0" 32 | }, 33 | "require-dev": { 34 | "ext-pdo": "*", 35 | "brianium/paratest": "^6.4", 36 | "laravel/pint": "^1.1", 37 | "nunomaduro/collision": "^6.0", 38 | "nunomaduro/larastan": "^2.0", 39 | "orchestra/testbench": "^7.0|^8.0", 40 | "phpmd/phpmd": "^2.11" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Okipa\\LaravelTable\\": "src/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Tests\\": "tests/", 50 | "Tests\\Database\\Factories\\": "tests/database/factories/" 51 | } 52 | }, 53 | "scripts": { 54 | "pint": ["vendor/bin/pint"], 55 | "phpmd": "vendor/bin/phpmd config,src,tests text phpmd.xml", 56 | "phpstan": "vendor/bin/phpstan analyse --memory-limit=2G", 57 | "phpunit" : "vendor/bin/phpunit", 58 | "test": ["@pint", "@phpmd", "@phpstan", "@phpunit"] 59 | }, 60 | "extra": { 61 | "laravel": { 62 | "providers": [ 63 | "Okipa\\LaravelTable\\LaravelTableServiceProvider" 64 | ] 65 | } 66 | }, 67 | "config": { 68 | "sort-packages": true 69 | }, 70 | "minimum-stability": "dev", 71 | "prefer-stable": true 72 | } 73 | -------------------------------------------------------------------------------- /config/laravel-table.php: -------------------------------------------------------------------------------- 1 | 'bootstrap-5', 13 | 14 | /** Set all the displayed action icons. */ 15 | 'icon' => [ 16 | 'filter' => '', 17 | 'rows_number' => '', 18 | 'sort' => '', 19 | 'sort_asc' => '', 20 | 'sort_desc' => '', 21 | 'search' => '', 22 | 'validate' => '', 23 | 'info' => '', 24 | 'reset' => '', 25 | 'drag_drop' => '', 26 | 'add' => '', 27 | 'create' => '', 28 | 'show' => '', 29 | 'edit' => '', 30 | 'destroy' => '', 31 | 'active' => '', 32 | 'inactive' => '', 33 | 'email_verified' => '', 34 | 'email_unverified' => '', 35 | 'toggle_on' => '', 36 | 'toggle_off' => '', 37 | ], 38 | 39 | /** The default table select HTML components attributes. */ 40 | 'html_select_components_attributes' => [], 41 | 42 | /** Whether the select allowing to choose the number of rows per page should be displayed by default. */ 43 | 'enable_number_of_rows_per_page_choice' => true, 44 | 45 | /** The default number-of-rows-per-page-select options. */ 46 | 'number_of_rows_per_page_default_options' => [10, 25, 50, 75, 100], 47 | 48 | ]; 49 | -------------------------------------------------------------------------------- /resources/views/bootstrap-4/bulk-action.blade.php: -------------------------------------------------------------------------------- 1 |
  • 2 | 8 |
  • 9 | -------------------------------------------------------------------------------- /resources/views/bootstrap-4/column-action.blade.php: -------------------------------------------------------------------------------- 1 | 7 | {!! $icon !!}{{ $label ? ' ' . $label : null }} 8 | 9 | -------------------------------------------------------------------------------- /resources/views/bootstrap-4/filter.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 8 |
    9 | -------------------------------------------------------------------------------- /resources/views/bootstrap-4/head-action.blade.php: -------------------------------------------------------------------------------- 1 | 5 | {!! $icon !!} {{ $title }} 6 | 7 | -------------------------------------------------------------------------------- /resources/views/bootstrap-4/row-action.blade.php: -------------------------------------------------------------------------------- 1 | 7 | {!! $icon !!} 8 | 9 | -------------------------------------------------------------------------------- /resources/views/bootstrap-4/table.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | @if($initialized) 3 | @if($orderColumn) 4 | 7 | @endif 8 |
    9 | 10 | {{-- Table header--}} 11 | 12 | {{-- Filters --}} 13 | @if($filtersArray) 14 | 15 | 39 | 40 | @endif 41 | {{-- Search/Number of rows per page/Head action --}} 42 | 43 | 121 | 122 | {{-- Column headings --}} 123 | 124 | {{-- Bulk actions --}} 125 | @if($tableBulkActionsArray) 126 | 146 | @endif 147 | {{-- Sorting/Column titles --}} 148 | @foreach($columns as $column) 149 | 176 | @endforeach 177 | {{-- Row actions --}} 178 | @if($tableRowActionsArray) 179 | 182 | @endif 183 | 184 | 185 | {{-- Table body--}} 186 | 187 | {{-- Rows --}} 188 | @forelse($rows as $model) 189 | laravel_table_unique_identifier, []), ['border-bottom']))> 190 | {{-- Row bulk action selector --}} 191 | @if($tableBulkActionsArray) 192 | 195 | @endif 196 | {{-- Row columns values --}} 197 | @foreach($columns as $column) 198 | @if($loop->first) 199 | 202 | @else 203 | 206 | @endif 207 | @endforeach 208 | {{-- Row actions --}} 209 | @if($tableRowActionsArray) 210 | 219 | @endif 220 | 221 | @empty 222 | 223 | 229 | 230 | @endforelse 231 | 232 | {{-- Table footer--}} 233 | 234 | {{-- Results --}} 235 | @foreach($results as $result) 236 | 237 | 243 | 244 | @endforeach 245 | 246 | 256 | 257 | 258 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 16 |
    17 |
    18 | {!! config('laravel-table.icon.filter') !!} 19 |
    20 | @foreach($filtersArray as $filterArray) 21 | @unless($resetFilters) 22 |
    23 | @endif 24 | {!! Okipa\LaravelTable\Abstracts\AbstractFilter::make($filterArray)->render() !!} 25 | @unless($resetFilters) 26 |
    27 | @endif 28 | @endforeach 29 | @if(collect($this->selectedFilters)->filter(fn(mixed $filter) => isset($filter) && $filter !== '' && $filter !== [])->isNotEmpty()) 30 | 34 | {!! config('laravel-table.icon.reset') !!} 35 | 36 | @endif 37 |
    38 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 44 |
    45 | {{-- Search --}} 46 |
    47 | @if($searchableLabels) 48 |
    49 |
    50 |
    51 |
    52 | 53 | {!! config('laravel-table.icon.search') !!} 54 | 55 |
    56 | 61 |
    62 | 63 | 68 | 69 |
    70 | @if($searchBy) 71 | 80 | @endif 81 |
    82 |
    83 |
    84 | @endif 85 |
    86 |
    87 | {{-- Number of rows per page --}} 88 | @if($numberOfRowsPerPageChoiceEnabled) 89 |
    $headActionArray, 'pl-xl-3' => ! $headActionArray, 'py-1'])> 90 |
    91 |
    92 | 93 | {!! config('laravel-table.icon.rows_number') !!} 94 | 95 |
    96 | 109 |
    110 |
    111 | @endif 112 | {{-- Head action --}} 113 | @if($headActionArray) 114 |
    115 | {{ Okipa\LaravelTable\Abstracts\AbstractHeadAction::make($headActionArray)->render() }} 116 |
    117 | @endif 118 |
    119 |
    120 |
    127 |
    128 | {{-- Bulk actions select all --}} 129 | 130 | {{-- Bulk actions dropdown --}} 131 | 144 |
    145 |
    150 | @if($column->isSortable($orderColumn)) 151 | @if($sortBy === $column->getAttribute()) 152 | 157 | {!! $sortDir === 'asc' 158 | ? config('laravel-table.icon.sort_desc') 159 | : config('laravel-table.icon.sort_asc') !!} 160 | {{ $column->getTitle() }} 161 | 162 | @else 163 | 168 | {!! config('laravel-table.icon.sort') !!} 169 | {{ $column->getTitle() }} 170 | 171 | @endif 172 | @else 173 | {{ $column->getTitle() }} 174 | @endif 175 | 180 | {{ __('Actions') }} 181 |
    193 | 194 | 200 | {!! $orderColumn ? '' . config('laravel-table.icon.drag_drop') . '' : null !!}{{ $column->getValue($model, $tableColumnActionsArray) }} 201 | 204 | {{ $column->getValue($model, $tableColumnActionsArray) }} 205 | 211 |
    212 | @if($rowActionsArray = Okipa\LaravelTable\Abstracts\AbstractRowAction::retrieve($tableRowActionsArray, $model->getKey())) 213 | @foreach($rowActionsArray as $rowActionArray) 214 | {{ Okipa\LaravelTable\Abstracts\AbstractRowAction::make($rowActionArray)->render($model) }} 215 | @endforeach 216 | @endif 217 |
    218 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 224 | 225 | {!! config('laravel-table.icon.info') !!} 226 | 227 | {{ __('No results were found.') }} 228 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 238 |
    239 |
    {{ $result->getTitle() }}
    240 |
    {{ $result->getValue() }}
    241 |
    242 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 247 |
    248 |
    249 |
    {!! $navigationStatus !!}
    250 |
    251 |
    252 | {!! $rows->links() !!} 253 |
    254 |
    255 |
    259 |
    260 | @else 261 |
    262 |
    263 | {{ __('Loading in progress...') }} 264 |
    265 | {{ __('Loading in progress...') }} 266 |
    267 | @endif 268 |
    269 | -------------------------------------------------------------------------------- /resources/views/bootstrap-5/bulk-action.blade.php: -------------------------------------------------------------------------------- 1 |
  • 2 | 8 |
  • 9 | -------------------------------------------------------------------------------- /resources/views/bootstrap-5/column-action.blade.php: -------------------------------------------------------------------------------- 1 | 7 | {!! $icon !!}{{ $label ? ' ' . $label : null }} 8 | 9 | -------------------------------------------------------------------------------- /resources/views/bootstrap-5/filter.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 8 |
    9 | -------------------------------------------------------------------------------- /resources/views/bootstrap-5/head-action.blade.php: -------------------------------------------------------------------------------- 1 | 5 | {!! $icon !!} {{ $title }} 6 | 7 | -------------------------------------------------------------------------------- /resources/views/bootstrap-5/row-action.blade.php: -------------------------------------------------------------------------------- 1 | 7 | {!! $icon !!} 8 | 9 | -------------------------------------------------------------------------------- /resources/views/bootstrap-5/table.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | @if($initialized) 3 | @if($orderColumn) 4 | 7 | @endif 8 |
    9 | 10 | {{-- Table header--}} 11 | 12 | {{-- Filters --}} 13 | @if($filtersArray) 14 | 15 | 39 | 40 | @endif 41 | {{-- Search/Number of rows per page/Head action --}} 42 | 43 | 113 | 114 | {{-- Column headings --}} 115 | 116 | {{-- Bulk actions --}} 117 | @if($tableBulkActionsArray) 118 | 138 | @endif 139 | {{-- Sorting/Column titles --}} 140 | @foreach($columns as $column) 141 | 168 | @endforeach 169 | {{-- Row actions --}} 170 | @if($tableRowActionsArray) 171 | 174 | @endif 175 | 176 | 177 | {{-- Table body--}} 178 | 179 | {{-- Rows --}} 180 | @forelse($rows as $model) 181 | laravel_table_unique_identifier, []), ['border-bottom']))> 182 | {{-- Row bulk action selector --}} 183 | @if($tableBulkActionsArray) 184 | 187 | @endif 188 | {{-- Row columns values --}} 189 | @foreach($columns as $column) 190 | @if($loop->first) 191 | 194 | @else 195 | 198 | @endif 199 | @endforeach 200 | {{-- Row actions --}} 201 | @if($tableRowActionsArray) 202 | 211 | @endif 212 | 213 | @empty 214 | 215 | 221 | 222 | @endforelse 223 | 224 | {{-- Table footer--}} 225 | 226 | {{-- Results --}} 227 | @foreach($results as $result) 228 | 229 | 235 | 236 | @endforeach 237 | 238 | 248 | 249 | 250 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 16 |
    17 |
    18 | {!! config('laravel-table.icon.filter') !!} 19 |
    20 | @foreach($filtersArray as $filterArray) 21 | @unless($resetFilters) 22 |
    23 | @endif 24 | {!! Okipa\LaravelTable\Abstracts\AbstractFilter::make($filterArray)->render() !!} 25 | @unless($resetFilters) 26 |
    27 | @endif 28 | @endforeach 29 | @if(collect($this->selectedFilters)->filter(fn(mixed $filter) => isset($filter) && $filter !== '' && $filter !== [])->isNotEmpty()) 30 | 34 | {!! config('laravel-table.icon.reset') !!} 35 | 36 | @endif 37 |
    38 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 44 |
    45 | {{-- Search --}} 46 |
    47 | @if($searchableLabels) 48 |
    49 |
    50 |
    51 | 52 | {!! config('laravel-table.icon.search') !!} 53 | 54 | 59 | 60 | 65 | 66 | @if($searchBy) 67 | 68 | 71 | {!! config('laravel-table.icon.reset') !!} 72 | 73 | 74 | @endif 75 |
    76 |
    77 |
    78 | @endif 79 |
    80 |
    81 | {{-- Number of rows per page --}} 82 | @if($numberOfRowsPerPageChoiceEnabled) 83 |
    $headActionArray, 'ps-xl-3' => ! $headActionArray, 'py-1'])> 84 |
    85 | 86 | {!! config('laravel-table.icon.rows_number') !!} 87 | 88 | 101 |
    102 |
    103 | @endif 104 | {{-- Head action --}} 105 | @if($headActionArray) 106 |
    107 | {{ Okipa\LaravelTable\Abstracts\AbstractHeadAction::make($headActionArray)->render() }} 108 |
    109 | @endif 110 |
    111 |
    112 |
    119 |
    120 | {{-- Bulk actions select all --}} 121 | 122 | {{-- Bulk actions dropdown --}} 123 | 136 |
    137 |
    142 | @if($column->isSortable($orderColumn)) 143 | @if($sortBy === $column->getAttribute()) 144 | 149 | {!! $sortDir === 'asc' 150 | ? config('laravel-table.icon.sort_desc') 151 | : config('laravel-table.icon.sort_asc') !!} 152 | {{ $column->getTitle() }} 153 | 154 | @else 155 | 160 | {!! config('laravel-table.icon.sort') !!} 161 | {{ $column->getTitle() }} 162 | 163 | @endif 164 | @else 165 | {{ $column->getTitle() }} 166 | @endif 167 | 172 | {{ __('Actions') }} 173 |
    185 | 186 | 192 | {!! $orderColumn ? '' . config('laravel-table.icon.drag_drop') . '' : null !!}{{ $column->getValue($model, $tableColumnActionsArray) }} 193 | 196 | {{ $column->getValue($model, $tableColumnActionsArray) }} 197 | 203 |
    204 | @if($rowActionsArray = Okipa\LaravelTable\Abstracts\AbstractRowAction::retrieve($tableRowActionsArray, $model->getKey())) 205 | @foreach($rowActionsArray as $rowActionArray) 206 | {{ Okipa\LaravelTable\Abstracts\AbstractRowAction::make($rowActionArray)->render($model) }} 207 | @endforeach 208 | @endif 209 |
    210 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 216 | 217 | {!! config('laravel-table.icon.info') !!} 218 | 219 | {{ __('No results were found.') }} 220 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 230 |
    231 |
    {{ $result->getTitle() }}
    232 |
    {{ $result->getValue() }}
    233 |
    234 |
    1 ? ' colspan="' . $columnsCount . '"' : null !!}> 239 |
    240 |
    241 |
    {!! $navigationStatus !!}
    242 |
    243 |
    244 | {!! $rows->links() !!} 245 |
    246 |
    247 |
    251 |
    252 | @else 253 |
    254 |
    255 | {{ __('Loading in progress...') }} 256 |
    257 | {{ __('Loading in progress...') }} 258 |
    259 | @endif 260 |
    261 | -------------------------------------------------------------------------------- /src/Abstracts/AbstractBulkAction.php: -------------------------------------------------------------------------------- 1 | bulkActionClass = $this::class; 32 | $this->modelClass = $model::class; 33 | $this->identifier = $this->identifier(); 34 | } 35 | 36 | abstract protected function identifier(): string; 37 | 38 | abstract protected function label(array $allowedModelKeys): string; 39 | 40 | abstract protected function defaultConfirmationQuestion( 41 | array $allowedModelKeys, 42 | array $disallowedModelKeys 43 | ): null|string; 44 | 45 | abstract protected function defaultFeedbackMessage( 46 | array $allowedModelKeys, 47 | array $disallowedModelKeys 48 | ): null|string; 49 | 50 | /** @return mixed|void */ 51 | abstract public function action(Collection $models, Component $livewire); 52 | 53 | public static function retrieve(array $bulkActions, string $identifier): array 54 | { 55 | return Arr::first($bulkActions, static fn (array $bulkAction) => $bulkAction['identifier'] === $identifier); 56 | } 57 | 58 | public static function make(array $bulkActionArray): self 59 | { 60 | $bulkActionInstance = app($bulkActionArray['bulkActionClass'], $bulkActionArray); 61 | $bulkActionInstance->bulkActionClass = $bulkActionArray['bulkActionClass']; 62 | $bulkActionInstance->modelClass = $bulkActionArray['modelClass']; 63 | $bulkActionInstance->identifier = $bulkActionArray['identifier']; 64 | $bulkActionInstance->allowedModelKeys = $bulkActionArray['allowedModelKeys']; 65 | $bulkActionInstance->disallowedModelKeys = $bulkActionArray['disallowedModelKeys']; 66 | $bulkActionInstance->confirmationQuestion = $bulkActionArray['confirmationQuestion']; 67 | $bulkActionInstance->feedbackMessage = $bulkActionArray['feedbackMessage']; 68 | 69 | return $bulkActionInstance; 70 | } 71 | 72 | public function render(): View 73 | { 74 | return view('laravel-table::' . config('laravel-table.ui') . '.bulk-action', [ 75 | 'bulkAction' => $this, 76 | 'label' => $this->label($this->allowedModelKeys), 77 | ]); 78 | } 79 | 80 | public function when(bool $condition): self 81 | { 82 | $this->isAllowed = $condition; 83 | 84 | return $this; 85 | } 86 | 87 | public function isAllowed(): bool 88 | { 89 | return $this->isAllowed; 90 | } 91 | 92 | public function confirmationQuestion(string|false $confirmationQuestion): self 93 | { 94 | $this->confirmationQuestion = $confirmationQuestion; 95 | 96 | return $this; 97 | } 98 | 99 | public function getConfirmationQuestion(): null|string 100 | { 101 | return $this->confirmationQuestion ?? $this->defaultConfirmationQuestion( 102 | $this->allowedModelKeys, 103 | $this->disallowedModelKeys 104 | ); 105 | } 106 | 107 | public function feedbackMessage(string|false $feedbackMessage): self 108 | { 109 | $this->feedbackMessage = $feedbackMessage; 110 | 111 | return $this; 112 | } 113 | 114 | public function getFeedbackMessage(): null|string 115 | { 116 | return $this->feedbackMessage ?? $this->defaultFeedbackMessage( 117 | $this->allowedModelKeys, 118 | $this->disallowedModelKeys 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Abstracts/AbstractColumnAction.php: -------------------------------------------------------------------------------- 1 | columnActionClass = $this::class; 29 | $this->modelClass = $model::class; 30 | $this->modelKey = $model->getKey(); 31 | $this->attribute = $attribute; 32 | } 33 | 34 | abstract protected function class(Model $model, string $attribute): array; 35 | 36 | abstract protected function title(Model $model, string $attribute): string; 37 | 38 | abstract protected function icon(Model $model, string $attribute): string; 39 | 40 | abstract protected function label(Model $model, string $attribute): null|string; 41 | 42 | abstract protected function defaultConfirmationQuestion(Model $model, string $attribute): null|string; 43 | 44 | abstract protected function defaultFeedbackMessage(Model $model, string $attribute): null|string; 45 | 46 | /** @return mixed|void */ 47 | abstract public function action(Model $model, string $attribute, Component $livewire); 48 | 49 | public static function retrieve(array $columnActions, null|string $modelKey, string $attribute): null|array 50 | { 51 | if (! $modelKey) { 52 | return null; 53 | } 54 | 55 | return Arr::first($columnActions, static fn (array $columnAction) => $columnAction['modelKey'] === $modelKey 56 | && $columnAction['attribute'] === $attribute); 57 | } 58 | 59 | public static function make(array $columnActionArray): self 60 | { 61 | $columnActionInstance = app($columnActionArray['columnActionClass'], $columnActionArray); 62 | $columnActionInstance->columnActionClass = $columnActionArray['columnActionClass']; 63 | $columnActionInstance->modelClass = $columnActionArray['modelClass']; 64 | $columnActionInstance->modelKey = $columnActionArray['modelKey']; 65 | $columnActionInstance->attribute = $columnActionArray['attribute']; 66 | $columnActionInstance->isAllowed = $columnActionArray['isAllowed']; 67 | $columnActionInstance->confirmationQuestion = $columnActionArray['confirmationQuestion']; 68 | $columnActionInstance->feedbackMessage = $columnActionArray['feedbackMessage']; 69 | 70 | return $columnActionInstance; 71 | } 72 | 73 | public function render(Model $model, string $attribute): View 74 | { 75 | return view('laravel-table::' . config('laravel-table.ui') . '.column-action', [ 76 | 'columnAction' => $this, 77 | 'class' => $this->class($model, $attribute), 78 | 'title' => $this->title($model, $attribute), 79 | 'label' => $this->label($model, $attribute), 80 | 'icon' => $this->icon($model, $attribute), 81 | 'shouldBeConfirmed' => (bool) $this->getConfirmationQuestion($model, $attribute), 82 | ]); 83 | } 84 | 85 | public function when(bool $condition): self 86 | { 87 | $this->isAllowed = $condition; 88 | 89 | return $this; 90 | } 91 | 92 | public function isAllowed(): bool 93 | { 94 | return $this->isAllowed; 95 | } 96 | 97 | public function confirmationQuestion(string|false $confirmationQuestion): self 98 | { 99 | $this->confirmationQuestion = $confirmationQuestion; 100 | 101 | return $this; 102 | } 103 | 104 | public function getConfirmationQuestion(Model $model, string $attribute): null|string 105 | { 106 | return $this->confirmationQuestion ?? $this->defaultConfirmationQuestion($model, $attribute); 107 | } 108 | 109 | public function feedbackMessage(string|false $feedbackMessage): self 110 | { 111 | $this->feedbackMessage = $feedbackMessage; 112 | 113 | return $this; 114 | } 115 | 116 | public function getFeedbackMessage(Model $model, string $attribute): null|string 117 | { 118 | return $this->feedbackMessage ?? $this->defaultFeedbackMessage($model, $attribute); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Abstracts/AbstractFilter.php: -------------------------------------------------------------------------------- 1 | filterClass = $this::class; 30 | $this->identifier = $this->identifier(); 31 | $this->modelKeyName = $modelKeyName; 32 | } 33 | 34 | abstract protected function identifier(): string; 35 | 36 | protected function class(): array 37 | { 38 | return []; 39 | } 40 | 41 | protected function attributes(): array 42 | { 43 | return [ 44 | 'multiple' => $this->multiple(), 45 | 'placeholder' => $this->label(), 46 | 'aria-label' => $this->label(), 47 | ...config('laravel-table.html_select_components_attributes'), 48 | ]; 49 | } 50 | 51 | abstract protected function label(): string; 52 | 53 | abstract protected function options(): array; 54 | 55 | abstract protected function multiple(): bool; 56 | 57 | abstract public function filter(Builder $query, mixed $selected): void; 58 | 59 | public static function retrieve(array $filtersArray, string $identifier): array 60 | { 61 | return collect($filtersArray)->firstOrFail('identifier', $identifier); 62 | } 63 | 64 | public static function make(array $filterArray): self 65 | { 66 | /** @var \Okipa\LaravelTable\Abstracts\AbstractFilter $filterInstance */ 67 | $filterInstance = app($filterArray['filterClass'], $filterArray); 68 | $filterInstance->filterClass = $filterArray['filterClass']; 69 | $filterInstance->identifier = $filterArray['identifier']; 70 | $filterInstance->modelKeyName = $filterArray['modelKeyName']; 71 | 72 | return $filterInstance; 73 | } 74 | 75 | public function render(): View 76 | { 77 | return view('laravel-table::' . config('laravel-table.ui') . '.filter', [ 78 | 'filter' => $this, 79 | 'class' => $this->class(), 80 | 'attributes' => (new ComponentAttributeBag($this->attributes())), 81 | 'label' => $this->label(), 82 | 'options' => $this->options(), 83 | 'multiple' => $this->multiple(), 84 | ]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Abstracts/AbstractFormatter.php: -------------------------------------------------------------------------------- 1 | rowActionClass = $this::class; 17 | } 18 | 19 | abstract protected function class(): array; 20 | 21 | abstract protected function icon(): string; 22 | 23 | abstract protected function title(): string; 24 | 25 | public function when(bool $condition): self 26 | { 27 | $this->isAllowed = $condition; 28 | 29 | return $this; 30 | } 31 | 32 | public function isAllowed(): bool 33 | { 34 | return $this->isAllowed; 35 | } 36 | 37 | /** @return mixed|void */ 38 | abstract public function action(Component $livewire); 39 | 40 | public static function make(array $rowActionArray): self 41 | { 42 | $headActionInstance = app($rowActionArray['rowActionClass'], $rowActionArray); 43 | $headActionInstance->rowActionClass = $rowActionArray['rowActionClass']; 44 | 45 | return $headActionInstance; 46 | } 47 | 48 | public function render(): View 49 | { 50 | return view('laravel-table::' . config('laravel-table.ui') . '.head-action', [ 51 | 'class' => $this->class(), 52 | 'title' => $this->title(), 53 | 'icon' => $this->icon(), 54 | ]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Abstracts/AbstractRowAction.php: -------------------------------------------------------------------------------- 1 | rowActionClass = $this::class; 35 | $this->modelClass = $model::class; 36 | $this->modelKey = $model->getKey(); 37 | $this->identifier = $this->identifier(); 38 | } 39 | 40 | abstract protected function identifier(): string; 41 | 42 | abstract protected function class(Model $model): array; 43 | 44 | abstract protected function icon(Model $model): string; 45 | 46 | abstract protected function title(Model $model): string; 47 | 48 | abstract protected function defaultConfirmationQuestion(Model $model): null|string; 49 | 50 | abstract protected function defaultFeedbackMessage(Model $model): null|string; 51 | 52 | /** @return mixed|void */ 53 | abstract public function action(Model $model, Component $livewire); 54 | 55 | public static function retrieve(array $rowActions, null|string $modelKey): array 56 | { 57 | if (! $modelKey) { 58 | return []; 59 | } 60 | 61 | return Arr::where($rowActions, static fn (array $rowAction) => $rowAction['modelKey'] === $modelKey); 62 | } 63 | 64 | public static function make(array $rowActionArray): self 65 | { 66 | $rowActionInstance = app($rowActionArray['rowActionClass'], $rowActionArray); 67 | $rowActionInstance->rowActionClass = $rowActionArray['rowActionClass']; 68 | $rowActionInstance->modelClass = $rowActionArray['modelClass']; 69 | $rowActionInstance->modelKey = $rowActionArray['modelKey']; 70 | $rowActionInstance->identifier = $rowActionArray['identifier']; 71 | $rowActionInstance->confirmationQuestion = $rowActionArray['confirmationQuestion']; 72 | $rowActionInstance->feedbackMessage = $rowActionArray['feedbackMessage']; 73 | 74 | return $rowActionInstance; 75 | } 76 | 77 | public function render(Model $model): View 78 | { 79 | return view('laravel-table::' . config('laravel-table.ui') . '.row-action', [ 80 | 'rowAction' => $this, 81 | 'class' => $this->class($model), 82 | 'title' => $this->title($model), 83 | 'icon' => $this->icon($model), 84 | 'shouldBeConfirmed' => (bool) $this->getConfirmationQuestion($model), 85 | ]); 86 | } 87 | 88 | public function when(bool $condition): self 89 | { 90 | $this->isAllowed = $condition; 91 | 92 | return $this; 93 | } 94 | 95 | public function isAllowed(): bool 96 | { 97 | return $this->isAllowed; 98 | } 99 | 100 | public function confirmationQuestion(string|false $confirmationQuestion): self 101 | { 102 | $this->confirmationQuestion = $confirmationQuestion; 103 | 104 | return $this; 105 | } 106 | 107 | public function getConfirmationQuestion(Model $model): null|string 108 | { 109 | return $this->confirmationQuestion ?? $this->defaultConfirmationQuestion($model); 110 | } 111 | 112 | public function feedbackMessage(string|false $feedbackMessage): self 113 | { 114 | $this->feedbackMessage = $feedbackMessage; 115 | 116 | return $this; 117 | } 118 | 119 | public function getFeedbackMessage(Model $model): null|string 120 | { 121 | return $this->feedbackMessage ?? $this->defaultFeedbackMessage($model); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Abstracts/AbstractTableConfiguration.php: -------------------------------------------------------------------------------- 1 | table(); 12 | $table->columns($this->columns()); 13 | $table->results($this->results()); 14 | 15 | return $table; 16 | } 17 | 18 | abstract protected function table(): Table; 19 | 20 | abstract protected function columns(): array; 21 | 22 | protected function results(): array 23 | { 24 | return []; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/BulkActions/ActivateBulkAction.php: -------------------------------------------------------------------------------- 1 | 1 31 | ? __('Are you sure you want to execute the action :action on the :count selected lines?', [ 32 | 'action' => __('Activate'), 33 | 'count' => count($allowedModelKeys), 34 | ]) 35 | : __('Are you sure you want to execute the action :action on the line #:primary?', [ 36 | 'action' => __('Activate'), 37 | 'primary' => Arr::first($allowedModelKeys), 38 | ]); 39 | $disallowedLinesCount = count($disallowedModelKeys); 40 | if ($disallowedLinesCount) { 41 | $disallowedLinesSentence = ' '; 42 | $disallowedLinesSentence .= $disallowedLinesCount > 1 43 | ? __(':count selected lines do not allow the action :action and will not be affected.', [ 44 | 'count' => $disallowedLinesCount, 45 | 'action' => __('Activate'), 46 | ]) 47 | : __('The line #:primary does not allow the action :action and will not be affected.', [ 48 | 'primary' => Arr::first($disallowedModelKeys), 49 | 'action' => __('Activate'), 50 | ]); 51 | } 52 | 53 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 54 | } 55 | 56 | protected function defaultFeedbackMessage(array $allowedModelKeys, array $disallowedModelKeys): string|null 57 | { 58 | $allowedLinesCount = count($allowedModelKeys); 59 | $allowedLinesSentence = $allowedLinesCount > 1 60 | ? __('The action :action has been executed on the :count selected lines.', [ 61 | 'action' => __('Activate'), 62 | 'count' => count($allowedModelKeys), 63 | ]) 64 | : __('The action :action has been executed on the line #:primary.', [ 65 | 'action' => __('Activate'), 66 | 'primary' => Arr::first($allowedModelKeys), 67 | ]); 68 | $disallowedLinesCount = count($disallowedModelKeys); 69 | if ($disallowedLinesCount) { 70 | $disallowedLinesSentence = ' '; 71 | $disallowedLinesSentence .= $disallowedLinesCount > 1 72 | ? __(':count selected lines do not allow the action :action and were not affected.', [ 73 | 'count' => $disallowedLinesCount, 74 | 'action' => __('Activate'), 75 | ]) 76 | : __('The line #:primary does not allow the action :action and was not affected.', [ 77 | 'primary' => Arr::first($disallowedModelKeys), 78 | 'action' => __('Activate'), 79 | ]); 80 | } 81 | 82 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 83 | } 84 | 85 | public function action(Collection $models, Component $livewire): void 86 | { 87 | foreach ($models as $model) { 88 | // Update attribute even if it not in model `$fillable` 89 | $model->forceFill([$this->attribute => true])->save(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/BulkActions/CancelEmailVerificationBulkAction.php: -------------------------------------------------------------------------------- 1 | 1 31 | ? __('Are you sure you want to execute the action :action on the :count selected lines?', [ 32 | 'action' => __('Unverify Email'), 33 | 'count' => count($allowedModelKeys), 34 | ]) 35 | : __('Are you sure you want to execute the action :action on the line #:primary?', [ 36 | 'action' => __('Unverify Email'), 37 | 'primary' => Arr::first($allowedModelKeys), 38 | ]); 39 | $disallowedLinesCount = count($disallowedModelKeys); 40 | if ($disallowedLinesCount) { 41 | $disallowedLinesSentence = ' '; 42 | $disallowedLinesSentence .= $disallowedLinesCount > 1 43 | ? __(':count selected lines do not allow the action :action and will not be affected.', [ 44 | 'count' => $disallowedLinesCount, 45 | 'action' => __('Unverify Email'), 46 | ]) 47 | : __('The line #:primary does not allow the action :action and will not be affected.', [ 48 | 'primary' => Arr::first($disallowedModelKeys), 49 | 'action' => __('Unverify Email'), 50 | ]); 51 | } 52 | 53 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 54 | } 55 | 56 | protected function defaultFeedbackMessage(array $allowedModelKeys, array $disallowedModelKeys): string|null 57 | { 58 | $allowedLinesCount = count($allowedModelKeys); 59 | $allowedLinesSentence = $allowedLinesCount > 1 60 | ? __('The action :action has been executed on the :count selected lines.', [ 61 | 'action' => __('Unverify Email'), 62 | 'count' => count($allowedModelKeys), 63 | ]) 64 | : __('The action :action has been executed on the line #:primary.', [ 65 | 'action' => __('Unverify Email'), 66 | 'primary' => Arr::first($allowedModelKeys), 67 | ]); 68 | $disallowedLinesCount = count($disallowedModelKeys); 69 | if ($disallowedLinesCount) { 70 | $disallowedLinesSentence = ' '; 71 | $disallowedLinesSentence .= $disallowedLinesCount > 1 72 | ? __(':count selected lines do not allow the action :action and were not affected.', [ 73 | 'count' => $disallowedLinesCount, 74 | 'action' => __('Unverify Email'), 75 | ]) 76 | : __('The line #:primary does not allow the action :action and was not affected.', [ 77 | 'primary' => Arr::first($disallowedModelKeys), 78 | 'action' => __('Unverify Email'), 79 | ]); 80 | } 81 | 82 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 83 | } 84 | 85 | public function action(Collection $models, Component $livewire): void 86 | { 87 | foreach ($models as $model) { 88 | // Update attribute even if it not in model `$fillable` 89 | $model->forceFill([$this->attribute => null])->save(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/BulkActions/DeactivateBulkAction.php: -------------------------------------------------------------------------------- 1 | 1 31 | ? __('Are you sure you want to execute the action :action on the :count selected lines?', [ 32 | 'action' => __('Deactivate'), 33 | 'count' => count($allowedModelKeys), 34 | ]) 35 | : __('Are you sure you want to execute the action :action on the line #:primary?', [ 36 | 'action' => __('Deactivate'), 37 | 'primary' => Arr::first($allowedModelKeys), 38 | ]); 39 | $disallowedLinesCount = count($disallowedModelKeys); 40 | if ($disallowedLinesCount) { 41 | $disallowedLinesSentence = ' '; 42 | $disallowedLinesSentence .= $disallowedLinesCount > 1 43 | ? __(':count selected lines do not allow the action :action and will not be affected.', [ 44 | 'count' => $disallowedLinesCount, 45 | 'action' => __('Deactivate'), 46 | ]) 47 | : __('The line #:primary does not allow the action :action and will not be affected.', [ 48 | 'primary' => Arr::first($disallowedModelKeys), 49 | 'action' => __('Deactivate'), 50 | ]); 51 | } 52 | 53 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 54 | } 55 | 56 | protected function defaultFeedbackMessage(array $allowedModelKeys, array $disallowedModelKeys): string|null 57 | { 58 | $allowedLinesCount = count($allowedModelKeys); 59 | $allowedLinesSentence = $allowedLinesCount > 1 60 | ? __('The action :action has been executed on the :count selected lines.', [ 61 | 'action' => __('Deactivate'), 62 | 'count' => count($allowedModelKeys), 63 | ]) 64 | : __('The action :action has been executed on the line #:primary.', [ 65 | 'action' => __('Deactivate'), 66 | 'primary' => Arr::first($allowedModelKeys), 67 | ]); 68 | $disallowedLinesCount = count($disallowedModelKeys); 69 | if ($disallowedLinesCount) { 70 | $disallowedLinesSentence = ' '; 71 | $disallowedLinesSentence .= $disallowedLinesCount > 1 72 | ? __(':count selected lines do not allow the action :action and were not affected.', [ 73 | 'count' => $disallowedLinesCount, 74 | 'action' => __('Deactivate'), 75 | ]) 76 | : __('The line #:primary does not allow the action :action and was not affected.', [ 77 | 'primary' => Arr::first($disallowedModelKeys), 78 | 'action' => __('Deactivate'), 79 | ]); 80 | } 81 | 82 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 83 | } 84 | 85 | public function action(Collection $models, Component $livewire): void 86 | { 87 | foreach ($models as $model) { 88 | // Update attribute even if it not in model `$fillable` 89 | $model->forceFill([$this->attribute => false])->save(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/BulkActions/DestroyBulkAction.php: -------------------------------------------------------------------------------- 1 | 1 26 | ? __('Are you sure you want to execute the action :action on the :count selected lines?', [ 27 | 'action' => __('Destroy'), 28 | 'count' => count($allowedModelKeys), 29 | ]) 30 | : __('Are you sure you want to execute the action :action on the line #:primary?', [ 31 | 'action' => __('Destroy'), 32 | 'primary' => Arr::first($allowedModelKeys), 33 | ]); 34 | $disallowedLinesCount = count($disallowedModelKeys); 35 | if ($disallowedLinesCount) { 36 | $disallowedLinesSentence = ' '; 37 | $disallowedLinesSentence .= $disallowedLinesCount > 1 38 | ? __(':count selected lines do not allow the action :action and will not be affected.', [ 39 | 'count' => $disallowedLinesCount, 40 | 'action' => __('Destroy'), 41 | ]) 42 | : __('The line #:primary does not allow the action :action and will not be affected.', [ 43 | 'primary' => Arr::first($disallowedModelKeys), 44 | 'action' => __('Destroy'), 45 | ]); 46 | } 47 | 48 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 49 | } 50 | 51 | protected function defaultFeedbackMessage(array $allowedModelKeys, array $disallowedModelKeys): string|null 52 | { 53 | $allowedLinesCount = count($allowedModelKeys); 54 | $allowedLinesSentence = $allowedLinesCount > 1 55 | ? __('The action :action has been executed on the :count selected lines.', [ 56 | 'action' => __('Destroy'), 57 | 'count' => count($allowedModelKeys), 58 | ]) 59 | : __('The action :action has been executed on the line #:primary.', [ 60 | 'action' => __('Destroy'), 61 | 'primary' => Arr::first($allowedModelKeys), 62 | ]); 63 | $disallowedLinesCount = count($disallowedModelKeys); 64 | if ($disallowedLinesCount) { 65 | $disallowedLinesSentence = ' '; 66 | $disallowedLinesSentence .= $disallowedLinesCount > 1 67 | ? __(':count selected lines do not allow the action :action and were not affected.', [ 68 | 'count' => $disallowedLinesCount, 69 | 'action' => __('Destroy'), 70 | ]) 71 | : __('The line #:primary does not allow the action :action and was not affected.', [ 72 | 'primary' => Arr::first($disallowedModelKeys), 73 | 'action' => __('Destroy'), 74 | ]); 75 | } 76 | 77 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 78 | } 79 | 80 | public function action(Collection $models, Component $livewire): void 81 | { 82 | $models->each->delete(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/BulkActions/VerifyEmailBulkAction.php: -------------------------------------------------------------------------------- 1 | 1 32 | ? __('Are you sure you want to execute the action :action on the :count selected lines?', [ 33 | 'action' => __('Verify Email'), 34 | 'count' => count($allowedModelKeys), 35 | ]) 36 | : __('Are you sure you want to execute the action :action on the line #:primary?', [ 37 | 'action' => __('Verify Email'), 38 | 'primary' => Arr::first($allowedModelKeys), 39 | ]); 40 | $disallowedLinesCount = count($disallowedModelKeys); 41 | if ($disallowedLinesCount) { 42 | $disallowedLinesSentence = ' '; 43 | $disallowedLinesSentence .= $disallowedLinesCount > 1 44 | ? __(':count selected lines do not allow the action :action and will not be affected.', [ 45 | 'count' => $disallowedLinesCount, 46 | 'action' => __('Verify Email'), 47 | ]) 48 | : __('The line #:primary does not allow the action :action and will not be affected.', [ 49 | 'primary' => Arr::first($disallowedModelKeys), 50 | 'action' => __('Verify Email'), 51 | ]); 52 | } 53 | 54 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 55 | } 56 | 57 | protected function defaultFeedbackMessage(array $allowedModelKeys, array $disallowedModelKeys): string|null 58 | { 59 | $allowedLinesCount = count($allowedModelKeys); 60 | $allowedLinesSentence = $allowedLinesCount > 1 61 | ? __('The action :action has been executed on the :count selected lines.', [ 62 | 'action' => __('Verify Email'), 63 | 'count' => count($allowedModelKeys), 64 | ]) 65 | : __('The action :action has been executed on the line #:primary.', [ 66 | 'action' => __('Verify Email'), 67 | 'primary' => Arr::first($allowedModelKeys), 68 | ]); 69 | $disallowedLinesCount = count($disallowedModelKeys); 70 | if ($disallowedLinesCount) { 71 | $disallowedLinesSentence = ' '; 72 | $disallowedLinesSentence .= $disallowedLinesCount > 1 73 | ? __(':count selected lines do not allow the action :action and were not affected.', [ 74 | 'count' => $disallowedLinesCount, 75 | 'action' => __('Verify Email'), 76 | ]) 77 | : __('The line #:primary does not allow the action :action and was not affected.', [ 78 | 'primary' => Arr::first($disallowedModelKeys), 79 | 'action' => __('Verify Email'), 80 | ]); 81 | } 82 | 83 | return $allowedLinesSentence . ($disallowedLinesSentence ?? ''); 84 | } 85 | 86 | public function action(Collection $models, Component $livewire): void 87 | { 88 | foreach ($models as $model) { 89 | // Update attribute even if it not in model `$fillable` 90 | $model->forceFill([$this->attribute => Date::now()])->save(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Column.php: -------------------------------------------------------------------------------- 1 | title = __('validation.attributes.' . $this->attribute); 37 | } 38 | 39 | public static function make(string $attribute = null): self 40 | { 41 | return new self($attribute); 42 | } 43 | 44 | public function title(string $title): self 45 | { 46 | $this->title = $title; 47 | 48 | return $this; 49 | } 50 | 51 | public function getTitle(): string|null 52 | { 53 | return $this->title; 54 | } 55 | 56 | public function sortable(Closure $sortableClosure = null): self 57 | { 58 | $this->sortable = true; 59 | $this->sortableClosure = $sortableClosure; 60 | 61 | return $this; 62 | } 63 | 64 | /** @throws \Okipa\LaravelTable\Exceptions\InvalidColumnSortDirection */ 65 | public function sortByDefault(string $sortDir = 'asc'): self 66 | { 67 | if (! in_array($sortDir, ['asc', 'desc'], true)) { 68 | throw new InvalidColumnSortDirection($sortDir); 69 | } 70 | $this->sortByDefault = true; 71 | $this->sortDirByDefault = $sortDir; 72 | 73 | return $this; 74 | } 75 | 76 | public function isSortable(Column|null $orderColumn): bool 77 | { 78 | if ($orderColumn) { 79 | return $this->getAttribute() === $orderColumn->getAttribute(); 80 | } 81 | 82 | return $this->sortable; 83 | } 84 | 85 | public function getAttribute(): string 86 | { 87 | return $this->attribute; 88 | } 89 | 90 | public function getSortableClosure(): Closure|null 91 | { 92 | return $this->sortableClosure; 93 | } 94 | 95 | public function isSortedByDefault(): bool 96 | { 97 | return $this->sortByDefault; 98 | } 99 | 100 | public function getSortDirByDefault(): string 101 | { 102 | return $this->sortDirByDefault; 103 | } 104 | 105 | public function searchable(Closure $searchableClosure = null): self 106 | { 107 | $this->searchable = true; 108 | $this->searchableClosure = $searchableClosure; 109 | 110 | return $this; 111 | } 112 | 113 | public function isSearchable(): bool 114 | { 115 | return $this->searchable; 116 | } 117 | 118 | public function getSearchableClosure(): Closure|null 119 | { 120 | return $this->searchableClosure; 121 | } 122 | 123 | public function format(Closure|AbstractFormatter $formatter, bool $escapeHtml = false): self 124 | { 125 | $this->formatter = $formatter; 126 | $this->escapeHtml = $escapeHtml; 127 | 128 | return $this; 129 | } 130 | 131 | public function action(Closure $columnActionClosure): self 132 | { 133 | $this->columnActionClosure = $columnActionClosure; 134 | 135 | return $this; 136 | } 137 | 138 | public function getAction(): Closure|null 139 | { 140 | return $this->columnActionClosure; 141 | } 142 | 143 | public function getValue(Model $model, array $tableColumnActionsArray): HtmlString|string|null 144 | { 145 | $columnActionArray = AbstractColumnAction::retrieve( 146 | $tableColumnActionsArray, 147 | $model->getKey(), 148 | $this->getAttribute() 149 | ); 150 | if ($columnActionArray) { 151 | $columnActionInstance = AbstractColumnAction::make($columnActionArray); 152 | 153 | return $columnActionInstance->isAllowed() 154 | ? new HtmlString(AbstractColumnAction::make($columnActionArray) 155 | ->render($model, $this->attribute) 156 | ->render()) 157 | : null; 158 | } 159 | if ($this->formatter instanceof Closure) { 160 | return $this->manageHtmlEscaping(($this->formatter)($model)); 161 | } 162 | if ($this->formatter instanceof AbstractFormatter) { 163 | return $this->manageHtmlEscaping($this->formatter->format($model, $this->attribute)); 164 | } 165 | 166 | return $this->manageHtmlEscaping($model->{$this->attribute}); 167 | } 168 | 169 | protected function manageHtmlEscaping(mixed $value): HtmlString|string 170 | { 171 | return $this->escapeHtml ? $value : new HtmlString($value); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/ColumnActions/ToggleBooleanColumnAction.php: -------------------------------------------------------------------------------- 1 | $model->{$attribute}, 15 | 'link-danger' => ! $model->{$attribute}, 16 | ]; 17 | } 18 | 19 | protected function title(Model $model, string $attribute): string 20 | { 21 | return $model->{$attribute} ? __('Toggle Off') : __('Toggle On'); 22 | } 23 | 24 | protected function icon(Model $model, string $attribute): string 25 | { 26 | return $model->{$attribute} 27 | ? config('laravel-table.icon.toggle_on') 28 | : config('laravel-table.icon.toggle_off'); 29 | } 30 | 31 | protected function label(Model $model, string $attribute): string|null 32 | { 33 | return null; 34 | } 35 | 36 | protected function defaultConfirmationQuestion(Model $model, string $attribute): string|null 37 | { 38 | return null; 39 | } 40 | 41 | protected function defaultFeedbackMessage(Model $model, string $attribute): string|null 42 | { 43 | return __('The action :action has been executed on the field :attribute from the line #:primary.', [ 44 | 'action' => $model->{$attribute} ? __('Toggle Off') : __('Toggle On'), 45 | 'attribute' => __('validation.attributes.' . $attribute), 46 | 'primary' => $model->getKey(), 47 | ]); 48 | } 49 | 50 | public function action(Model $model, string $attribute, Component $livewire): void 51 | { 52 | // Update attribute even if it not in model `$fillable` 53 | $model->forceFill([$attribute => ! $model->{$attribute}])->save(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ColumnActions/ToggleEmailVerifiedColumnAction.php: -------------------------------------------------------------------------------- 1 | $model->{$attribute}, 16 | 'link-danger' => ! $model->{$attribute}, 17 | ]; 18 | } 19 | 20 | protected function icon(Model $model, string $attribute): string 21 | { 22 | return $model->{$attribute} 23 | ? config('laravel-table.icon.email_verified') 24 | : config('laravel-table.icon.email_unverified'); 25 | } 26 | 27 | protected function title(Model $model, string $attribute): string 28 | { 29 | return $model->{$attribute} ? __('Unverify Email') : __('Verify Email'); 30 | } 31 | 32 | protected function label(Model $model, string $attribute): string|null 33 | { 34 | return null; 35 | } 36 | 37 | protected function defaultConfirmationQuestion(Model $model, string $attribute): string|null 38 | { 39 | return __('Are you sure you want to execute the action :action on the field ' 40 | . ':attribute from the line #:primary?', [ 41 | 'action' => $model->{$attribute} ? __('Unverify Email') : __('Verify Email'), 42 | 'attribute' => __('validation.attributes.' . $attribute), 43 | 'primary' => $model->getKey(), 44 | ]); 45 | } 46 | 47 | protected function defaultFeedbackMessage(Model $model, string $attribute): string|null 48 | { 49 | return __('The action :action has been executed on the field :attribute from the line #:primary.', [ 50 | 'action' => $model->{$attribute} ? __('Unverify Email') : __('Verify Email'), 51 | 'attribute' => __('validation.attributes.' . $attribute), 52 | 'primary' => $model->getKey(), 53 | ]); 54 | } 55 | 56 | public function action(Model $model, string $attribute, Component $livewire): void 57 | { 58 | // Update attribute even if it not in model `$fillable` 59 | $model->forceFill([$attribute => $model->{$attribute} ? null : Date::now()])->save(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Console/Commands/MakeBulkAction.php: -------------------------------------------------------------------------------- 1 | option('model')) { 24 | $stub = '/stubs/table.model.stub'; 25 | } 26 | $stub ??= '/stubs/table.stub'; 27 | 28 | return __DIR__ . $stub; 29 | } 30 | 31 | /** @param string $rootNamespace */ 32 | protected function getDefaultNamespace($rootNamespace): string 33 | { 34 | return $rootNamespace . '\Tables'; 35 | } 36 | 37 | /** @throws \Illuminate\Contracts\Filesystem\FileNotFoundException */ 38 | protected function buildClass($name): string 39 | { 40 | $replace = []; 41 | if ($this->option('model')) { 42 | $replace = $this->buildModelReplacements($replace); 43 | } 44 | 45 | return str_replace( 46 | array_keys($replace), 47 | array_values($replace), 48 | parent::buildClass($name) 49 | ); 50 | } 51 | 52 | protected function buildModelReplacements(array $replace): array 53 | { 54 | $modelClass = $this->parseModel($this->option('model')); 55 | 56 | return array_merge($replace, [ 57 | 'DummyFullModelClass' => $modelClass, 58 | 'DummyModelClass' => class_basename($modelClass), 59 | '$dummyModel' => '$' . Str::camel(class_basename($modelClass)), 60 | 'dummyRoute' => Str::camel(class_basename($modelClass)), 61 | ]); 62 | } 63 | 64 | protected function parseModel(string $model): string 65 | { 66 | $result = preg_match('([^A-Za-z0-9_/\\\\])', $model); 67 | if ($result) { 68 | throw new InvalidArgumentException('Model name contains invalid characters.'); 69 | } 70 | $model = trim(str_replace('/', '\\', $model), '\\'); 71 | $rootNamespace = $this->laravel->getNamespace(); 72 | if (! Str::startsWith($model, $rootNamespace)) { 73 | $model = $rootNamespace . $model; 74 | } 75 | 76 | return $model; 77 | } 78 | 79 | protected function getOptions(): array 80 | { 81 | return [ 82 | ['model', 'm', InputOption::VALUE_OPTIONAL, 'Generate a table configuration for the given model.'], 83 | ]; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Console/Commands/stubs/bulk.action.stub: -------------------------------------------------------------------------------- 1 | model(DummyModelClass::class) 18 | ->rowActions(fn(DummyModelClass $dummyModel) => [ 19 | new EditRowAction(route('dummyRoute.edit', $dummyModel)), 20 | new DestroyRowAction(), 21 | ]); 22 | } 23 | 24 | protected function columns(): array 25 | { 26 | return [ 27 | Column::make('id')->sortable(), 28 | Column::make('created_at')->format(new DateFormatter('d/m/Y H:i'))->sortable(), 29 | Column::make('updated_at')->format(new DateFormatter('d/m/Y H:i'))->sortable()->sortByDefault('desc'), 30 | ]; 31 | } 32 | 33 | protected function results(): array 34 | { 35 | return [ 36 | // The table results configuration. 37 | // As results are optional on tables, you may delete this method if you do not use it. 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Console/Commands/stubs/table.stub: -------------------------------------------------------------------------------- 1 | attribute; 18 | } 19 | 20 | protected function label(): string 21 | { 22 | return $this->label; 23 | } 24 | 25 | protected function multiple(): bool 26 | { 27 | return false; 28 | } 29 | 30 | protected function options(): array 31 | { 32 | return [ 33 | true => __('Yes'), 34 | false => __('No'), 35 | ]; 36 | } 37 | 38 | public function filter(Builder $query, mixed $selected): void 39 | { 40 | $query->where($this->attribute, $selected); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Filters/NullFilter.php: -------------------------------------------------------------------------------- 1 | attribute; 18 | } 19 | 20 | protected function label(): string 21 | { 22 | return $this->label; 23 | } 24 | 25 | protected function multiple(): bool 26 | { 27 | return false; 28 | } 29 | 30 | protected function options(): array 31 | { 32 | return [ 33 | true => __('Yes'), 34 | false => __('No'), 35 | ]; 36 | } 37 | 38 | public function filter(Builder $query, mixed $selected): void 39 | { 40 | $selected ? $query->whereNotNull($this->attribute) : $query->whereNull($this->attribute); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Filters/RelationshipFilter.php: -------------------------------------------------------------------------------- 1 | relationship; 23 | } 24 | 25 | protected function label(): string 26 | { 27 | return $this->label; 28 | } 29 | 30 | protected function multiple(): bool 31 | { 32 | return $this->multiple; 33 | } 34 | 35 | protected function options(): array 36 | { 37 | return $this->options; 38 | } 39 | 40 | public function filter(Builder $query, mixed $selected): void 41 | { 42 | $query->whereHas( 43 | $this->relationship, 44 | fn (Builder $category) => $category->whereIn($this->modelKeyName, Arr::wrap($selected)) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Filters/ValueFilter.php: -------------------------------------------------------------------------------- 1 | attribute; 23 | } 24 | 25 | protected function label(): string 26 | { 27 | return $this->label; 28 | } 29 | 30 | protected function multiple(): bool 31 | { 32 | return $this->multiple; 33 | } 34 | 35 | protected function options(): array 36 | { 37 | return $this->options; 38 | } 39 | 40 | public function filter(Builder $query, mixed $selected): void 41 | { 42 | $query->whereIn($this->attribute, Arr::wrap($selected)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Formatters/BooleanFormatter.php: -------------------------------------------------------------------------------- 1 | {$attribute}; 13 | if (! isset($boolean)) { 14 | return null; 15 | } 16 | 17 | return $boolean 18 | ? '' . config('laravel-table.icon.active') . '' 19 | : '' . config('laravel-table.icon.inactive') . ''; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Formatters/DateFormatter.php: -------------------------------------------------------------------------------- 1 | {$attribute}; 18 | if (! $date) { 19 | return null; 20 | } 21 | 22 | return $this->timezone 23 | ? $date->timezone($this->timezone)->format($this->format) 24 | : $date->format($this->format); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Formatters/StrLimitFormatter.php: -------------------------------------------------------------------------------- 1 | {$attribute}; 19 | if (! $string) { 20 | return null; 21 | } 22 | $truncatedString = Str::limit($model->{$attribute}, $this->limit, $this->end); 23 | 24 | return <<{$truncatedString} 26 | HTML; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/HeadActions/AddHeadAction.php: -------------------------------------------------------------------------------- 1 | redirectHeadAction = new RedirectHeadAction( 15 | url: $createUrl, 16 | label: __('Add'), 17 | icon: config('laravel-table.icon.add'), 18 | openInNewWindow: $openInNewWindow 19 | ); 20 | } 21 | 22 | protected function class(): array 23 | { 24 | return $this->redirectHeadAction->class(); 25 | } 26 | 27 | protected function title(): string 28 | { 29 | return $this->redirectHeadAction->title(); 30 | } 31 | 32 | protected function icon(): string 33 | { 34 | return $this->redirectHeadAction->icon(); 35 | } 36 | 37 | public function action(Component $livewire): void 38 | { 39 | $this->redirectHeadAction->action($livewire); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/HeadActions/CreateHeadAction.php: -------------------------------------------------------------------------------- 1 | redirectHeadAction = new RedirectHeadAction( 15 | url: $createUrl, 16 | label: __('Create'), 17 | icon: config('laravel-table.icon.create'), 18 | openInNewWindow: $openInNewWindow 19 | ); 20 | } 21 | 22 | protected function class(): array 23 | { 24 | return $this->redirectHeadAction->class(); 25 | } 26 | 27 | protected function title(): string 28 | { 29 | return $this->redirectHeadAction->title(); 30 | } 31 | 32 | protected function icon(): string 33 | { 34 | return $this->redirectHeadAction->icon(); 35 | } 36 | 37 | public function action(Component $livewire): void 38 | { 39 | $this->redirectHeadAction->action($livewire); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/HeadActions/RedirectHeadAction.php: -------------------------------------------------------------------------------- 1 | class; 23 | } 24 | 25 | protected function title(): string 26 | { 27 | return __($this->label); 28 | } 29 | 30 | protected function icon(): string 31 | { 32 | return $this->icon; 33 | } 34 | 35 | public function action(Component $livewire): void 36 | { 37 | $this->openInNewWindow 38 | ? $livewire->emit('laraveltable:link:open:newtab', $this->url) 39 | : redirect()->to($this->url); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/LaravelTableServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__ . '/../resources/views', 'laravel-table'); 20 | $this->publishes([ 21 | __DIR__ . '/../config/laravel-table.php' => config_path('laravel-table.php'), 22 | ], 'laravel-table:config'); 23 | $this->publishes([ 24 | __DIR__ . '/../resources/views' => resource_path('views/vendor/laravel-table'), 25 | ], 'laravel-table:views'); 26 | $this->registerLivewireComponents(); 27 | } 28 | 29 | protected function registerLivewireComponents(): void 30 | { 31 | Livewire::component('table', \Okipa\LaravelTable\Livewire\Table::class); 32 | } 33 | 34 | public function register(): void 35 | { 36 | $this->mergeConfigFrom(__DIR__ . '/../config/laravel-table.php', 'laravel-table'); 37 | $this->commands([ 38 | MakeTable::class, 39 | MakeFilter::class, 40 | MakeHeadAction::class, 41 | MakeBulkAction::class, 42 | MakeRowAction::class, 43 | MakeColumnAction::class, 44 | MakeFormatter::class, 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Livewire/Table.php: -------------------------------------------------------------------------------- 1 | 'cancelWireIgnoreOnFilters', 70 | 'laraveltable:action:confirmed' => 'actionConfirmed', 71 | 'laraveltable:refresh' => 'refresh', 72 | ]; 73 | 74 | public function init(): void 75 | { 76 | $this->initPaginationTheme(); 77 | $this->initialized = true; 78 | } 79 | 80 | protected function initPaginationTheme(): void 81 | { 82 | $this->paginationTheme = Str::contains(config('laravel-table.ui'), 'bootstrap') 83 | ? 'bootstrap' 84 | : 'tailwind'; 85 | } 86 | 87 | /** 88 | * @throws \Okipa\LaravelTable\Exceptions\InvalidTableConfiguration 89 | * @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared 90 | * @throws JsonException 91 | */ 92 | public function render(): View 93 | { 94 | $config = $this->buildConfig(); 95 | 96 | return view('laravel-table::' . config('laravel-table.ui') . '.table', $this->buildTable($config)); 97 | } 98 | 99 | /** @throws \Okipa\LaravelTable\Exceptions\InvalidTableConfiguration */ 100 | protected function buildConfig(): AbstractTableConfiguration 101 | { 102 | 103 | $config = app($this->config); 104 | if (! $config instanceof AbstractTableConfiguration) { 105 | throw new InvalidTableConfiguration($this->config); 106 | } 107 | foreach ($this->configParams as $key => $value) { 108 | $config->{$key} = $value; 109 | } 110 | 111 | return $config; 112 | } 113 | 114 | /** 115 | * @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared 116 | * @throws JsonException 117 | */ 118 | protected function buildTable(AbstractTableConfiguration $config): array 119 | { 120 | $table = $config->setup(); 121 | // Events triggering on load 122 | $table->triggerEventsEmissionOnLoad($this); 123 | // Prepend reorder column 124 | $table->prependReorderColumn(); 125 | // Search 126 | $this->searchableLabels = $table->getSearchableLabels(); 127 | // Sort 128 | $columnSortedByDefault = $table->getColumnSortedByDefault(); 129 | $this->sortBy = $table->getOrderColumn()?->getAttribute() 130 | ?: ($this->sortBy ?? $columnSortedByDefault?->getAttribute()); 131 | $this->sortDir ??= $columnSortedByDefault?->getSortDirByDefault(); 132 | $sortableClosure = $this->sortBy && ! $table->getOrderColumn() 133 | ? $table->getColumn($this->sortBy)->getSortableClosure() 134 | : null; 135 | // Paginate 136 | $numberOfRowsPerPageOptions = $table->getNumberOfRowsPerPageOptions(); 137 | $this->numberOfRowsPerPage ??= Arr::first($numberOfRowsPerPageOptions); 138 | // Filters 139 | $filtersArray = $table->generateFiltersArray(); 140 | $filterClosures = $table->getFilterClosures($filtersArray, $this->selectedFilters); 141 | // Query preparation 142 | $query = $table->prepareQuery( 143 | $filterClosures, 144 | $this->searchBy, 145 | $sortableClosure ?: $this->sortBy, 146 | $this->sortDir, 147 | ); 148 | // Rows generation 149 | $table->paginateRows($query, $this->numberOfRowsPerPage); 150 | // Results computing 151 | $table->computeResults($table->getRows()->getCollection()); 152 | // Head action 153 | $this->headActionArray = $table->getHeadActionArray(); 154 | // Bulk actions 155 | if (in_array($this->selectedModelKeys, [['selectAll'], ['unselectAll']], true)) { 156 | $this->selectedModelKeys = $this->selectedModelKeys === ['selectAll'] 157 | ? $table->getRows()->map(fn (Model $model) => (string) $model->getKey())->toArray() 158 | : []; 159 | } 160 | $this->tableBulkActionsArray = $table->generateBulkActionsArray($this->selectedModelKeys); 161 | // Row actions 162 | $this->tableRowActionsArray = $table->generateRowActionsArray(); 163 | // Column actions 164 | $this->tableColumnActionsArray = $table->generateColumnActionsArray(); 165 | // Reorder config 166 | $this->reorderConfig = $table->getReorderConfig($this->sortDir); 167 | 168 | return [ 169 | 'columns' => $table->getColumns(), 170 | 'columnsCount' => ($this->tableBulkActionsArray ? 1 : 0) 171 | + $table->getColumns()->count() 172 | + ($this->tableRowActionsArray ? 1 : 0), 173 | 'rows' => $table->getRows(), 174 | 'orderColumn' => $table->getOrderColumn(), 175 | 'filtersArray' => $filtersArray, 176 | 'numberOfRowsPerPageChoiceEnabled' => $table->isNumberOfRowsPerPageChoiceEnabled(), 177 | 'numberOfRowsPerPageOptions' => $numberOfRowsPerPageOptions, 178 | 'tableRowClass' => $table->getRowClass(), 179 | 'results' => $table->getResults(), 180 | 'navigationStatus' => $table->getNavigationStatus(), 181 | ]; 182 | } 183 | 184 | public function reorder(array $newPositions): void 185 | { 186 | [ 187 | 'modelClass' => $modelClass, 188 | 'reorderAttribute' => $reorderAttribute, 189 | 'sortDir' => $sortDir, 190 | 'beforeReorderAllModelKeysWithPosition' => $beforeReorderAllModelKeysWithPositionRawArray, 191 | ] = $this->reorderConfig; 192 | $beforeReorderAllModelKeysWithPositionCollection = collect($beforeReorderAllModelKeysWithPositionRawArray) 193 | ->sortBy(callback: 'position', descending: $sortDir === 'desc'); 194 | $afterReorderModelKeysWithPositionCollection = collect($newPositions) 195 | ->sortBy('order') 196 | ->map(fn (array $newPosition) => [ 197 | 'modelKey' => $newPosition['value'], 198 | 'position' => $beforeReorderAllModelKeysWithPositionCollection->firstWhere( 199 | 'modelKey', 200 | $newPosition['value'] 201 | )['position'], 202 | ]); 203 | $beforeReorderModelKeysWithPositionCollection = $afterReorderModelKeysWithPositionCollection 204 | ->map(fn (array $afterReorderModelKeyWithPosition) => $beforeReorderAllModelKeysWithPositionCollection 205 | ->firstWhere('modelKey', $afterReorderModelKeyWithPosition['modelKey'])) 206 | ->sortBy(callback: 'position', descending: $sortDir === 'desc'); 207 | $beforeReorderModelKeysWithIndexCollection = $beforeReorderModelKeysWithPositionCollection->pluck('modelKey'); 208 | $afterReorderModelKeysWithIndexCollection = $afterReorderModelKeysWithPositionCollection->pluck('modelKey'); 209 | $reorderedPositions = collect(); 210 | foreach ($beforeReorderAllModelKeysWithPositionCollection as $beforeReorderModelKeysWithPosition) { 211 | $modelKey = $beforeReorderModelKeysWithPosition['modelKey']; 212 | $indexAfterReordering = $afterReorderModelKeysWithIndexCollection->search($modelKey); 213 | if ($indexAfterReordering === false) { 214 | $currentPosition = $beforeReorderAllModelKeysWithPositionCollection->firstWhere( 215 | 'modelKey', 216 | $modelKey 217 | )['position']; 218 | $reorderedPositions->push(['modelKey' => $modelKey, 'position' => $currentPosition]); 219 | 220 | continue; 221 | } 222 | $modelKeyCurrentOneWillReplace = $beforeReorderModelKeysWithIndexCollection->get($indexAfterReordering); 223 | $newPosition = $beforeReorderAllModelKeysWithPositionCollection->firstWhere( 224 | 'modelKey', 225 | $modelKeyCurrentOneWillReplace 226 | )['position']; 227 | $reorderedPositions->push(['modelKey' => $modelKey, 'position' => $newPosition]); 228 | } 229 | $startOrder = 1; 230 | foreach ($reorderedPositions->sortBy('position') as $reorderedPosition) { 231 | app($modelClass)->find($reorderedPosition['modelKey'])->update([$reorderAttribute => $startOrder++]); 232 | } 233 | $this->emit('laraveltable:action:feedback', __('The list has been reordered.')); 234 | } 235 | 236 | public function changeNumberOfRowsPerPage(int $numberOfRowsPerPage): void 237 | { 238 | $this->numberOfRowsPerPage = $numberOfRowsPerPage; 239 | } 240 | 241 | public function sortBy(string $columnKey): void 242 | { 243 | $this->sortDir = $this->sortBy !== $columnKey || $this->sortDir === 'desc' 244 | ? 'asc' 245 | : 'desc'; 246 | $this->sortBy = $columnKey; 247 | } 248 | 249 | public function headAction(): mixed 250 | { 251 | if (! $this->headActionArray) { 252 | return null; 253 | } 254 | $headActionInstance = AbstractHeadAction::make($this->headActionArray); 255 | if (! $headActionInstance->isAllowed()) { 256 | return null; 257 | } 258 | 259 | return $headActionInstance->action($this); 260 | } 261 | 262 | public function updatedSelectAll(): void 263 | { 264 | $this->selectedModelKeys = $this->selectAll ? ['selectAll'] : ['unselectAll']; 265 | } 266 | 267 | public function resetFilters(): void 268 | { 269 | $this->selectedFilters = []; 270 | $this->resetFilters = true; 271 | $this->emitSelf('laraveltable:filters:wire:ignore:cancel'); 272 | } 273 | 274 | public function cancelWireIgnoreOnFilters(): void 275 | { 276 | $this->resetFilters = false; 277 | } 278 | 279 | /** @throws \Okipa\LaravelTable\Exceptions\UnrecognizedActionType */ 280 | public function actionConfirmed(string $actionType, string $identifier, null|string $modelKey): mixed 281 | { 282 | return match ($actionType) { 283 | 'bulkAction' => $this->bulkAction($identifier, false), 284 | 'rowAction' => $this->rowAction($identifier, $modelKey, false), 285 | 'columnAction' => $this->columnAction($identifier, $modelKey, false), 286 | default => throw new UnrecognizedActionType($actionType), 287 | }; 288 | } 289 | 290 | public function bulkAction(string $identifier, bool $requiresConfirmation): mixed 291 | { 292 | $bulkActionArray = AbstractBulkAction::retrieve($this->tableBulkActionsArray, $identifier); 293 | $bulkActionInstance = AbstractBulkAction::make($bulkActionArray); 294 | if (! $bulkActionInstance->allowedModelKeys) { 295 | return null; 296 | } 297 | if ($requiresConfirmation) { 298 | return $this->emit( 299 | 'laraveltable:action:confirm', 300 | 'bulkAction', 301 | $identifier, 302 | null, 303 | $bulkActionInstance->getConfirmationQuestion() 304 | ); 305 | } 306 | $feedbackMessage = $bulkActionInstance->getFeedbackMessage(); 307 | if ($feedbackMessage) { 308 | $this->emit('laraveltable:action:feedback', $feedbackMessage); 309 | } 310 | $models = app($bulkActionInstance->modelClass)->findMany($bulkActionInstance->allowedModelKeys); 311 | 312 | return $bulkActionInstance->action($models, $this); 313 | } 314 | 315 | public function rowAction(string $identifier, string $modelKey, bool $requiresConfirmation): mixed 316 | { 317 | $rowActionsArray = AbstractRowAction::retrieve($this->tableRowActionsArray, $modelKey); 318 | if (! $rowActionsArray) { 319 | return null; 320 | } 321 | $rowActionArray = collect($rowActionsArray)->where('identifier', $identifier)->first(); 322 | $rowActionInstance = AbstractRowAction::make($rowActionArray); 323 | if (! $rowActionInstance->isAllowed()) { 324 | return null; 325 | } 326 | $model = app($rowActionArray['modelClass'])->findOrFail($modelKey); 327 | if ($requiresConfirmation) { 328 | return $this->emit( 329 | 'laraveltable:action:confirm', 330 | 'rowAction', 331 | $identifier, 332 | $modelKey, 333 | $rowActionInstance->getConfirmationQuestion($model) 334 | ); 335 | } 336 | $feedbackMessage = $rowActionInstance->getFeedbackMessage($model); 337 | if ($feedbackMessage) { 338 | $this->emit('laraveltable:action:feedback', $feedbackMessage); 339 | } 340 | 341 | return $rowActionInstance->action($model, $this); 342 | } 343 | 344 | public function columnAction(string $identifier, string $modelKey, bool $requiresConfirmation): mixed 345 | { 346 | $columnActionArray = AbstractColumnAction::retrieve($this->tableColumnActionsArray, $modelKey, $identifier); 347 | if (! $columnActionArray) { 348 | return null; 349 | } 350 | $columnActionInstance = AbstractColumnAction::make($columnActionArray); 351 | if (! $columnActionInstance->isAllowed()) { 352 | return null; 353 | } 354 | $model = app($columnActionArray['modelClass'])->findOrFail($modelKey); 355 | if ($requiresConfirmation) { 356 | return $this->emit( 357 | 'laraveltable:action:confirm', 358 | 'columnAction', 359 | $identifier, 360 | $modelKey, 361 | $columnActionInstance->getConfirmationQuestion($model, $identifier) 362 | ); 363 | } 364 | $feedbackMessage = $columnActionInstance->getFeedbackMessage($model, $identifier); 365 | if ($feedbackMessage) { 366 | $this->emit('laraveltable:action:feedback', $feedbackMessage); 367 | } 368 | 369 | return $columnActionInstance->action($model, $identifier, $this); 370 | } 371 | 372 | public function refresh(array $configParams = [], array $targetedConfigs = []): void 373 | { 374 | if ($targetedConfigs && ! in_array($this->config, $targetedConfigs, true)) { 375 | return; 376 | } 377 | $this->configParams = [...$this->configParams, ...$configParams]; 378 | $this->emitSelf('$refresh'); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | title = $title; 33 | 34 | return $this; 35 | } 36 | 37 | public function format(Closure $formatClosure, bool $escapeHtml = false): self 38 | { 39 | $this->formatClosure = $formatClosure; 40 | $this->escapeHtml = $escapeHtml; 41 | 42 | return $this; 43 | } 44 | 45 | public function getTitle(): string 46 | { 47 | return $this->title; 48 | } 49 | 50 | public function compute(Builder $totalRowsQuery, Collection $displayedRows): self 51 | { 52 | $this->value = ($this->formatClosure)($totalRowsQuery, $displayedRows); 53 | 54 | return $this; 55 | } 56 | 57 | public function getValue(): string 58 | { 59 | return $this->manageHtmlEscaping($this->value); 60 | } 61 | 62 | protected function manageHtmlEscaping(mixed $value): HtmlString|string 63 | { 64 | return $this->escapeHtml ? $value : new HtmlString($value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/RowActions/DestroyRowAction.php: -------------------------------------------------------------------------------- 1 | __('Destroy'), 35 | 'primary' => $model->getKey(), 36 | ]); 37 | } 38 | 39 | protected function defaultFeedbackMessage(Model $model): null|string 40 | { 41 | return __('The action :action has been executed on the line #:primary.', [ 42 | 'action' => __('Destroy'), 43 | 'primary' => $model->getKey(), 44 | ]); 45 | } 46 | 47 | public function action(Model $model, Component $livewire): void 48 | { 49 | $model->delete(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/RowActions/EditRowAction.php: -------------------------------------------------------------------------------- 1 | to($this->editUrl); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/RowActions/RedirectRowAction.php: -------------------------------------------------------------------------------- 1 | title); 27 | } 28 | 29 | protected function class(Model $model): array 30 | { 31 | return $this->class; 32 | } 33 | 34 | protected function icon(Model $model): string 35 | { 36 | return $this->icon; 37 | } 38 | 39 | protected function title(Model $model): string 40 | { 41 | return $this->title; 42 | } 43 | 44 | protected function defaultConfirmationQuestion(Model $model): string|null 45 | { 46 | return $this->defaultConfirmationQuestion; 47 | } 48 | 49 | protected function defaultFeedbackMessage(Model $model): string|null 50 | { 51 | return $this->defaultFeedbackMessage; 52 | } 53 | 54 | public function action(Model $model, Component $livewire): void 55 | { 56 | $this->openInNewWindow 57 | ? $livewire->emit('laraveltable:link:open:newtab', $this->url) 58 | : redirect()->to($this->url); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/RowActions/ShowRowAction.php: -------------------------------------------------------------------------------- 1 | redirectRowAction = new RedirectRowAction( 16 | url: $showUrl, 17 | title: __('Show'), 18 | icon: config('laravel-table.icon.show'), 19 | openInNewWindow: $openInNewWindow 20 | ); 21 | } 22 | 23 | protected function identifier(): string 24 | { 25 | return $this->redirectRowAction->identifier(); 26 | } 27 | 28 | protected function class(Model $model): array 29 | { 30 | return $this->redirectRowAction->class($model); 31 | } 32 | 33 | protected function icon(Model $model): string 34 | { 35 | return $this->redirectRowAction->icon($model); 36 | } 37 | 38 | protected function title(Model $model): string 39 | { 40 | return $this->redirectRowAction->title($model); 41 | } 42 | 43 | protected function defaultConfirmationQuestion(Model $model): null|string 44 | { 45 | return $this->redirectRowAction->defaultConfirmationQuestion($model); 46 | } 47 | 48 | protected function defaultFeedbackMessage(Model $model): null|string 49 | { 50 | return $this->redirectRowAction->defaultFeedbackMessage($model); 51 | } 52 | 53 | public function action(Model $model, Component $livewire): void 54 | { 55 | $this->redirectRowAction->action($model, $livewire); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Table.php: -------------------------------------------------------------------------------- 1 | numberOfRowsPerPageChoiceEnabled = config('laravel-table.enable_number_of_rows_per_page_choice'); 51 | $this->numberOfRowsPerPageOptions = config('laravel-table.number_of_rows_per_page_default_options'); 52 | $this->columns = collect(); 53 | $this->results = collect(); 54 | } 55 | 56 | public function model(string $modelClass): self 57 | { 58 | $this->model = app($modelClass); 59 | 60 | return $this; 61 | } 62 | 63 | public function getModel(): Model 64 | { 65 | return $this->model; 66 | } 67 | 68 | public function emitEventsOnLoad(array $eventsToEmitOnLoad): self 69 | { 70 | $this->eventsToEmitOnLoad = $eventsToEmitOnLoad; 71 | 72 | return $this; 73 | } 74 | 75 | /** @throws \Okipa\LaravelTable\Exceptions\InvalidColumnSortDirection */ 76 | public function reorderable(string $attribute, string $title = null, string $sortDirByDefault = 'asc'): self 77 | { 78 | $orderColumn = Column::make($attribute)->sortable()->sortByDefault($sortDirByDefault); 79 | if ($title) { 80 | $orderColumn->title($title); 81 | } 82 | $this->orderColumn = $orderColumn; 83 | 84 | return $this; 85 | } 86 | 87 | public static function make(): self 88 | { 89 | return new self(); 90 | } 91 | 92 | /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */ 93 | public function prependReorderColumn(): void 94 | { 95 | $orderColumn = $this->getOrderColumn(); 96 | if ($orderColumn) { 97 | $this->getColumns()->prepend($orderColumn); 98 | } 99 | } 100 | 101 | public function getOrderColumn(): null|Column 102 | { 103 | return $this->orderColumn; 104 | } 105 | 106 | /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */ 107 | public function getColumns(): Collection 108 | { 109 | if ($this->columns->isEmpty()) { 110 | throw new NoColumnsDeclared($this->model); 111 | } 112 | 113 | return $this->columns; 114 | } 115 | 116 | public function getReorderConfig(null|string $sortDir): array 117 | { 118 | if (! $this->getOrderColumn()) { 119 | return []; 120 | } 121 | $query = $this->model->query(); 122 | // Query 123 | if ($this->queryClosure) { 124 | $query->where(fn ($subQueryQuery) => ($this->queryClosure)($query)); 125 | } 126 | // Sort 127 | $query->orderBy($this->getOrderColumn()->getAttribute(), $sortDir); 128 | 129 | return [ 130 | 'modelClass' => $this->model::class, 131 | 'reorderAttribute' => $this->getOrderColumn()->getAttribute(), 132 | 'sortDir' => $sortDir, 133 | 'beforeReorderAllModelKeysWithPosition' => $query 134 | ->get() 135 | ->map(fn (Model $model) => [ 136 | 'modelKey' => (string) $model->getKey(), 137 | 'position' => $model->getAttribute($this->getOrderColumn()->getAttribute()), 138 | ]) 139 | ->toArray(), 140 | ]; 141 | } 142 | 143 | public function query(Closure $queryClosure): self 144 | { 145 | $this->queryClosure = $queryClosure; 146 | 147 | return $this; 148 | } 149 | 150 | /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */ 151 | public function prepareQuery( 152 | array $filterClosures, 153 | null|string $searchBy, 154 | string|Closure|null $sortBy, 155 | null|string $sortDir 156 | ): Builder { 157 | $query = $this->model->query(); 158 | // Query 159 | if ($this->queryClosure) { 160 | $query->where(fn (Builder $subQueryQuery) => ($this->queryClosure)($query)); 161 | } 162 | // Filters 163 | if ($filterClosures) { 164 | $query->where(function (Builder $subFiltersQuery) use ($filterClosures) { 165 | foreach ($filterClosures as $filterClosure) { 166 | $filterClosure($subFiltersQuery); 167 | } 168 | }); 169 | } 170 | // Search 171 | if ($searchBy) { 172 | $query->where(function (Builder $subSearchQuery) use ($searchBy) { 173 | $this->getSearchableColumns() 174 | ->each(function (Column $searchableColumn) use ($subSearchQuery, $searchBy) { 175 | $searchableClosure = $searchableColumn->getSearchableClosure(); 176 | $searchableClosure 177 | ? $subSearchQuery->orWhere(fn (Builder $orWhereQuery) => ($searchableClosure)( 178 | $orWhereQuery, 179 | $searchBy 180 | )) 181 | : $subSearchQuery->orWhereRaw( 182 | $this->getSearchSqlStatement($searchableColumn->getAttribute()), 183 | ['%' . Str::of($searchBy)->trim()->lower() . '%'] 184 | ); 185 | }); 186 | }); 187 | } 188 | // Sort 189 | if ($sortBy && $sortDir) { 190 | $sortBy instanceof Closure 191 | ? $sortBy($query, $sortDir) 192 | : $query->orderBy($sortBy, $sortDir); 193 | } 194 | 195 | return $query; 196 | } 197 | 198 | /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */ 199 | protected function getSearchableColumns(): Collection 200 | { 201 | return $this->getColumns()->filter(fn (Column $column) => $column->isSearchable()); 202 | } 203 | 204 | protected function getSearchSqlStatement(string $attribute): string 205 | { 206 | $connection = config('database.default'); 207 | $driver = config('database.connections.' . $connection . '.driver'); 208 | 209 | return $this->getSqlLowerFunction($driver, $attribute) . ' ' 210 | . $this->getSqlCaseInsensitiveSearchingLikeOperator($driver) . ' ?'; 211 | } 212 | 213 | protected function getSqlLowerFunction(string $driver, string $attribute): string 214 | { 215 | return $driver === 'pgsql' ? 'LOWER(CAST(' . $attribute . ' AS TEXT))' : 'LOWER(' . $attribute . ')'; 216 | } 217 | 218 | protected function getSqlCaseInsensitiveSearchingLikeOperator(string $driver): string 219 | { 220 | return $driver === 'pgsql' ? 'ILIKE' : 'LIKE'; 221 | } 222 | 223 | public function getRows(): LengthAwarePaginator 224 | { 225 | return $this->rows; 226 | } 227 | 228 | public function triggerEventsEmissionOnLoad(Livewire\Table $table): void 229 | { 230 | foreach ($this->eventsToEmitOnLoad as $event => $params) { 231 | $eventName = is_string($event) ? $event : $params; 232 | $eventParams = is_array($params) ? $params : []; 233 | $table->emit($eventName, $eventParams); 234 | } 235 | } 236 | 237 | public function enableNumberOfRowsPerPageChoice(bool $numberOfRowsPerPageChoiceEnabled): self 238 | { 239 | $this->numberOfRowsPerPageChoiceEnabled = $numberOfRowsPerPageChoiceEnabled; 240 | 241 | return $this; 242 | } 243 | 244 | public function isNumberOfRowsPerPageChoiceEnabled(): bool 245 | { 246 | return $this->numberOfRowsPerPageChoiceEnabled; 247 | } 248 | 249 | public function numberOfRowsPerPageOptions(array $numberOfRowsPerPageOptions): self 250 | { 251 | $this->numberOfRowsPerPageOptions = $numberOfRowsPerPageOptions; 252 | 253 | return $this; 254 | } 255 | 256 | public function getNumberOfRowsPerPageOptions(): array 257 | { 258 | return $this->numberOfRowsPerPageOptions; 259 | } 260 | 261 | public function filters(array $filters): self 262 | { 263 | $this->filters = $filters; 264 | 265 | return $this; 266 | } 267 | 268 | public function headAction(AbstractHeadAction $headAction): self 269 | { 270 | $this->headAction = $headAction; 271 | 272 | return $this; 273 | } 274 | 275 | public function rowActions(Closure $rowActionsClosure): self 276 | { 277 | $this->rowActionsClosure = $rowActionsClosure; 278 | 279 | return $this; 280 | } 281 | 282 | public function bulkActions(Closure $bulkActionsClosure): self 283 | { 284 | $this->bulkActionsClosure = $bulkActionsClosure; 285 | 286 | return $this; 287 | } 288 | 289 | public function rowClass(Closure $rowClassesClosure): self 290 | { 291 | $this->rowClassesClosure = $rowClassesClosure; 292 | 293 | return $this; 294 | } 295 | 296 | public function columns(array $columns): void 297 | { 298 | $this->columns = collect($columns); 299 | } 300 | 301 | public function results(array $results): void 302 | { 303 | $this->results = collect($results); 304 | } 305 | 306 | /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */ 307 | public function getColumnSortedByDefault(): null|Column 308 | { 309 | $sortableColumns = $this->getColumns() 310 | ->filter(fn (Column $column) => $column->isSortable($this->getOrderColumn())); 311 | if ($sortableColumns->isEmpty()) { 312 | return null; 313 | } 314 | $columnSortedByDefault = $sortableColumns->filter(fn (Column $column) => $column->isSortedByDefault())->first(); 315 | if (! $columnSortedByDefault) { 316 | return $sortableColumns->first(); 317 | } 318 | 319 | return $columnSortedByDefault; 320 | } 321 | 322 | /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */ 323 | public function getColumn(string $attribute): Column 324 | { 325 | return $this->getColumns()->filter(fn (Column $column) => $column->getAttribute() === $attribute)->first(); 326 | } 327 | 328 | public function paginateRows(Builder $query, int $numberOfRowsPerPage): void 329 | { 330 | $this->rows = $query->paginate($numberOfRowsPerPage); 331 | $this->rows->transform(function (Model $model) { 332 | $model->laravel_table_unique_identifier = Str::uuid()->getInteger()->toString(); 333 | 334 | return $model; 335 | }); 336 | } 337 | 338 | public function computeResults(Collection $displayedRows): void 339 | { 340 | $this->results = $this->results->map(fn (Result $result) => $result->compute( 341 | $this->model->query()->toBase(), 342 | $displayedRows, 343 | )); 344 | } 345 | 346 | public function generateFiltersArray(): array 347 | { 348 | return collect($this->filters)->map(function (AbstractFilter $filter) { 349 | $filter->setup($this->model->getKeyName()); 350 | 351 | return json_decode(json_encode( 352 | $filter, 353 | JSON_THROW_ON_ERROR 354 | ), true, 512, JSON_THROW_ON_ERROR); 355 | })->toArray(); 356 | } 357 | 358 | public function getFilterClosures(array $filtersArray, array $selectedFilters): array 359 | { 360 | $filterClosures = []; 361 | foreach ($selectedFilters as $identifier => $value) { 362 | if ($value === '' || $value === []) { 363 | continue; 364 | } 365 | $filterArray = AbstractFilter::retrieve($filtersArray, $identifier); 366 | $filterInstance = AbstractFilter::make($filterArray); 367 | $filterClosures[$identifier] = static fn (Builder $query) => $filterInstance->filter($query, $value); 368 | } 369 | 370 | return $filterClosures; 371 | } 372 | 373 | public function getHeadActionArray(): array 374 | { 375 | if (! $this->headAction) { 376 | return []; 377 | } 378 | $this->headAction->setup(); 379 | if (! $this->headAction->isAllowed()) { 380 | return []; 381 | } 382 | 383 | return (array) $this->headAction; 384 | } 385 | 386 | /** @throws JsonException */ 387 | public function getRowClass(): array 388 | { 389 | $tableRowClass = []; 390 | if (! $this->rowClassesClosure) { 391 | return $tableRowClass; 392 | } 393 | foreach ($this->rows->getCollection() as $model) { 394 | $tableRowClass[$model->laravel_table_unique_identifier] = ($this->rowClassesClosure)($model); 395 | } 396 | 397 | return json_decode(json_encode( 398 | $tableRowClass, 399 | JSON_THROW_ON_ERROR 400 | ), true, 512, JSON_THROW_ON_ERROR); 401 | } 402 | 403 | /** @throws JsonException */ 404 | public function generateBulkActionsArray(array $selectedModelKeys): array 405 | { 406 | $tableBulkActionsArray = []; 407 | $tableRawBulkActionsArray = []; 408 | if (! $this->bulkActionsClosure) { 409 | return $tableBulkActionsArray; 410 | } 411 | $bulkActionsModelKeys = []; 412 | foreach ($this->rows as $index => $model) { 413 | $modelBulkActions = collect(($this->bulkActionsClosure)($model)); 414 | foreach ($modelBulkActions as $modelBulkAction) { 415 | $modelBulkAction->setup($model); 416 | if (! $index) { 417 | $tableRawBulkActionsArray[] = json_decode(json_encode( 418 | $modelBulkAction, 419 | JSON_THROW_ON_ERROR 420 | ), true, 512, JSON_THROW_ON_ERROR); 421 | } 422 | if (! in_array((string) $model->getKey(), $selectedModelKeys, true)) { 423 | continue; 424 | } 425 | $modelBulkAction->isAllowed() 426 | ? $bulkActionsModelKeys[$modelBulkAction->identifier]['allowed'][] = $model->getKey() 427 | : $bulkActionsModelKeys[$modelBulkAction->identifier]['disallowed'][] = $model->getKey(); 428 | } 429 | } 430 | foreach ($tableRawBulkActionsArray as $tableBulkActionArray) { 431 | $identifier = $tableBulkActionArray['identifier']; 432 | $tableBulkActionArray['allowedModelKeys'] = $bulkActionsModelKeys[$identifier]['allowed'] ?? []; 433 | $tableBulkActionArray['disallowedModelKeys'] = $bulkActionsModelKeys[$identifier]['disallowed'] ?? []; 434 | $tableBulkActionsArray[] = $tableBulkActionArray; 435 | } 436 | 437 | return $tableBulkActionsArray; 438 | } 439 | 440 | public function generateRowActionsArray(): array 441 | { 442 | $tableRowActionsArray = []; 443 | if (! $this->rowActionsClosure) { 444 | return $tableRowActionsArray; 445 | } 446 | foreach ($this->rows->getCollection() as $model) { 447 | $rowActions = collect(($this->rowActionsClosure)($model)) 448 | ->filter(fn (AbstractRowAction $rowAction) => $rowAction->isAllowed()); 449 | $rowActionsArray = $rowActions->map(static function (AbstractRowAction $rowAction) use ($model) { 450 | $rowAction->setup($model); 451 | 452 | return json_decode(json_encode( 453 | $rowAction, 454 | JSON_THROW_ON_ERROR 455 | ), true, 512, JSON_THROW_ON_ERROR); 456 | })->toArray(); 457 | $tableRowActionsArray = [...$tableRowActionsArray, ...$rowActionsArray]; 458 | } 459 | 460 | return $tableRowActionsArray; 461 | } 462 | 463 | /** 464 | * @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared 465 | * @throws JsonException 466 | */ 467 | public function generateColumnActionsArray(): array 468 | { 469 | $tableColumnActionsArray = []; 470 | foreach ($this->rows->getCollection() as $model) { 471 | $columnActions = $this->getColumns() 472 | ->mapWithKeys(fn (Column $column) => [ 473 | $column->getAttribute() => $column->getAction() 474 | ? $column->getAction()($model) 475 | : null, 476 | ]) 477 | ->filter(); 478 | foreach ($columnActions as $attribute => $columnAction) { 479 | $columnAction->setup($model, $attribute); 480 | $tableColumnActionsArray[] = json_decode(json_encode( 481 | $columnAction, 482 | JSON_THROW_ON_ERROR 483 | ), true, 512, JSON_THROW_ON_ERROR); 484 | } 485 | } 486 | 487 | return $tableColumnActionsArray; 488 | } 489 | 490 | /** @throws \Okipa\LaravelTable\Exceptions\NoColumnsDeclared */ 491 | public function getSearchableLabels(): string 492 | { 493 | return $this->getSearchableColumns() 494 | ->map(fn (Column $searchableColumn) => ['title' => $searchableColumn->getTitle()]) 495 | ->implode('title', ', '); 496 | } 497 | 498 | public function getResults(): Collection 499 | { 500 | return $this->results; 501 | } 502 | 503 | public function getNavigationStatus(): string 504 | { 505 | return __('Showing results :start to :stop on :total', [ 506 | 'start' => $this->rows->isNotEmpty() 507 | ? ($this->rows->perPage() * ($this->rows->currentPage() - 1)) + 1 508 | : 0, 509 | 'stop' => $this->rows->count() + (($this->rows->currentPage() - 1) * $this->rows->perPage()), 510 | 'total' => $this->rows->total(), 511 | ]); 512 | } 513 | } 514 | --------------------------------------------------------------------------------