Vue Datatables (#2597)

* Adding Vue components for Charts and Multi Select

* List Views


* Improve UI of datatable

* Refactor Vue Datatable for reusability
This commit is contained in:
David Bomba 2019-01-13 12:42:03 +02:00 committed by GitHub
parent 0faf91dd5d
commit 9204510193
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 42713 additions and 1381 deletions

View File

@ -109,7 +109,7 @@ class ClientDatatable extends EntityDatatable
* @param $data Std Class of client datatable rows
* @return object Rendered action column items
*/
private function buildActionColumn($data) : object
private function buildActionColumn($data)
{
//if(auth()->user()->is_admin())
@ -157,4 +157,70 @@ class ClientDatatable extends EntityDatatable
}
public function buildOptions()
{
return collect([
'per_page' => 20,
'sort_order' => [
[
'field' => 'name',
'sortField' => 'name',
'direction' => 'asc',
]
],
'fields' => [
[
'name' => '__checkbox', // <----
'title' => '',
'titleClass' => 'center aligned',
'dataClass' => 'center aligned'
],
[
'name' => 'name',
'title' => trans('texts.name'),
'sortField' => 'name',
'visible' => false,
'dataClass' => 'center aligned'
],
[
'name' => 'contact',
'title' => trans('texts.contact'),
'sortField' => 'contact',
'visible' => false,
'dataClass' => 'center aligned'
],
[
'name' => 'email',
'title' => trans('texts.email'),
'sortField' => 'email',
'dataClass' => 'center aligned'
],
[
'name' => 'client_created_at',
'title' => trans('texts.date_created'),
'sortField' => 'client_created_at',
'dataClass' => 'center aligned'
],
[
'name' => 'last_login',
'title' => trans('texts.last_login'),
'sortField' => 'last_login',
'dataClass' => 'center aligned'
],
[
'name' => 'balance',
'title' => trans('texts.balance'),
'sortField' => 'balance',
'dataClass' => 'center aligned'
],
[
'name' => '__component:client-actions',
'title' => '',
'titleClass' => 'center aligned',
'dataClass' => 'center aligned'
]
]
]);
}
}

View File

@ -20,6 +20,13 @@ class ClientFilters extends QueryFilters
return $this->builder->where('balance', $parts->operator, $parts->value);
}
public function between_balance($balance)
{
$parts = explode(":", $balance);
return $this->builder->whereBetween('balance', [$parts[0], $parts[1]]);
}
/**
* Filter by popularity.

View File

@ -41,7 +41,11 @@ class ClientController extends Controller
if(request('page'))
return $this->clientDatatable->query(request(), $this->getCurrentCompanyId());
return view('client.vue_list');
$data = [
'datatable' => $this->clientDatatable->buildOptions()
];
return view('client.vue_list', $data);
/*
if (request()->ajax()) {
@ -212,4 +216,9 @@ class ClientController extends Controller
//
}
public function builk()
{
}
}

View File

@ -31,9 +31,7 @@
"predis/predis": "^1.1",
"spatie/laravel-html": "^2.19",
"webpatser/laravel-countries": "dev-master#75992ad",
"wildbit/postmark-php": "^2.6",
"yajra/laravel-datatables": "^1.0",
"yajra/laravel-datatables-html": "^3.0"
"wildbit/postmark-php": "^2.6"
},
"require-dev": {
"beyondcode/laravel-dump-server": "^1.0",

956
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -159,8 +159,6 @@ return [
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
Yajra\DataTables\DataTablesServiceProvider::class,
Yajra\DataTables\HtmlServiceProvider::class,
/*
* Dependency Service Providers
*/

View File

@ -1,70 +0,0 @@
<?php
return [
/*
* Namespaces used by the generator.
*/
'namespace' => [
/*
* Base namespace/directory to create the new file.
* This is appended on default Laravel namespace.
* Usage: php artisan datatables:make User
* Output: App\DataTables\UserDataTable
* With Model: App\User (default model)
* Export filename: users_timestamp
*/
'base' => 'DataTables',
/*
* Base namespace/directory where your model's are located.
* This is appended on default Laravel namespace.
* Usage: php artisan datatables:make Post --model
* Output: App\DataTables\PostDataTable
* With Model: App\Post
* Export filename: posts_timestamp
*/
'model' => '',
],
/*
* Set Custom stub folder
*/
//'stub' => '/resources/custom_stub',
/*
* PDF generator to be used when converting the table to pdf.
* Available generators: excel, snappy
* Snappy package: barryvdh/laravel-snappy
* Excel package: maatwebsite/excel
*/
'pdf_generator' => 'snappy',
/*
* Snappy PDF options.
*/
'snappy' => [
'options' => [
'no-outline' => true,
'margin-left' => '0',
'margin-right' => '0',
'margin-top' => '10mm',
'margin-bottom' => '10mm',
],
'orientation' => 'landscape',
],
/*
* Default html builder parameters.
*/
'parameters' => [
'dom' => 'Bfrtip',
'order' => [[0, 'desc']],
'buttons' => [
'create',
'export',
'print',
'reset',
'reload',
],
],
];

View File

@ -1,13 +0,0 @@
<?php
return [
/*
* Request key name to parse includes on fractal.
*/
'includes' => 'include',
/*
* Default fractal serializer.
*/
'serializer' => League\Fractal\Serializer\DataArraySerializer::class,
];

View File

@ -1,16 +0,0 @@
<?php
return [
/*
* Default table attributes when generating the table.
*/
'table' => [
'class' => 'table table-hover',
'id' => 'ninja',
],
/*
* Default condition to determine if a parameter is a callback or not
* Callbacks needs to start by those terms or they will be casted to string
*/
'callback' => ['$', '$.', 'function'],
];

View File

@ -1,116 +0,0 @@
<?php
return [
/*
* DataTables search options.
*/
'search' => [
/*
* Smart search will enclose search keyword with wildcard string "%keyword%".
* SQL: column LIKE "%keyword%"
*/
'smart' => true,
/*
* Multi-term search will explode search keyword using spaces resulting into multiple term search.
*/
'multi_term' => true,
/*
* Case insensitive will search the keyword in lower case format.
* SQL: LOWER(column) LIKE LOWER(keyword)
*/
'case_insensitive' => true,
/*
* Wild card will add "%" in between every characters of the keyword.
* SQL: column LIKE "%k%e%y%w%o%r%d%"
*/
'use_wildcards' => false,
],
/*
* DataTables internal index id response column name.
*/
'index_column' => 'DT_Row_Index',
/*
* List of available builders for DataTables.
* This is where you can register your custom dataTables builder.
*/
'engines' => [
'eloquent' => \Yajra\DataTables\EloquentDataTable::class,
'query' => \Yajra\DataTables\QueryDataTable::class,
'collection' => \Yajra\DataTables\CollectionDataTable::class,
'resource' => \Yajra\DataTables\ApiResourceDataTable::class,
],
/*
* DataTables accepted builder to engine mapping.
* This is where you can override which engine a builder should use
* Note, only change this if you know what you are doing!
*/
'builders' => [
//Illuminate\Database\Eloquent\Relations\Relation::class => 'eloquent',
//Illuminate\Database\Eloquent\Builder::class => 'eloquent',
//Illuminate\Database\Query\Builder::class => 'query',
//Illuminate\Support\Collection::class => 'collection',
],
/*
* Nulls last sql pattern for Posgresql & Oracle.
* For MySQL, use '-%s %s'
*/
'nulls_last_sql' => '%s %s NULLS LAST',
/*
* User friendly message to be displayed on user if error occurs.
* Possible values:
* null - The exception message will be used on error response.
* 'throw' - Throws a \Yajra\DataTables\Exceptions\Exception. Use your custom error handler if needed.
* 'custom message' - Any friendly message to be displayed to the user. You can also use translation key.
*/
'error' => env('DATATABLES_ERROR', null),
/*
* Default columns definition of dataTable utility functions.
*/
'columns' => [
/*
* List of columns hidden/removed on json response.
*/
'excess' => ['rn', 'row_num'],
/*
* List of columns to be escaped. If set to *, all columns are escape.
* Note: You can set the value to empty array to disable XSS protection.
*/
'escape' => '*',
/*
* List of columns that are allowed to display html content.
* Note: Adding columns to list will make us available to XSS attacks.
*/
'raw' => ['action'],
/*
* List of columns are are forbidden from being searched/sorted.
*/
'blacklist' => ['password', 'remember_token'],
/*
* List of columns that are only allowed fo search/sort.
* If set to *, all columns are allowed.
*/
'whitelist' => '*',
],
/*
* JsonResponse header and options config.
*/
'json' => [
'header' => [],
'options' => 0,
],
];

View File

@ -20,7 +20,7 @@
"axios": "^0.18",
"babel-preset-stage-2": "^6.24.1",
"bootstrap": "^4.0.0",
"chart.js": "^2.7.2",
"chart.js": "^2.7.3",
"cross-env": "^5.1",
"flag-icon-css": "3.2.0",
"font-awesome": "^4.7",
@ -45,8 +45,10 @@
"socket.io-client": "^2.1.1",
"ts-loader": "3.5.0",
"typescript": "^3.1.6",
"vue-chartjs": "^3.4.0",
"vue-events": "^3.1.0",
"vue-i18n": "^8.3.0",
"vue-multiselect": "^2.1.3",
"vue-select": "^2.5.1",
"vue-toastr": "^2.0.16"
}

View File

@ -34374,6 +34374,8 @@ Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_lodash___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_toastr__ = __webpack_require__("./node_modules/vue-toastr/dist/vue-toastr.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_vue_toastr__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2_vue__ = __webpack_require__("./node_modules/vue/dist/vue.common.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2_vue___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_vue__);
// lodash handles our translations
@ -34383,11 +34385,13 @@ Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// Import toastr scss file: need webpack sass-loader
__webpack_require__("./node_modules/vue-toastr/src/vue-toastr.scss");
// Register vue component
Vue.component('vue-toastr', __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default.a);
__WEBPACK_IMPORTED_MODULE_2_vue___default.a.component('vue-toastr', __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default.a);
// Global translation helper
Vue.prototype.trans = function (string) {
__WEBPACK_IMPORTED_MODULE_2_vue___default.a.prototype.trans = function (string) {
return __WEBPACK_IMPORTED_MODULE_0_lodash__["get"](i18n, string);
};
@ -34397,7 +34401,7 @@ window.Vue = __webpack_require__("./node_modules/vue/dist/vue.common.js");
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/* Development only*/
Vue.config.devtools = true;
__WEBPACK_IMPORTED_MODULE_2_vue___default.a.config.devtools = true;
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',

View File

@ -34374,6 +34374,8 @@ Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0_lodash___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0_lodash__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_toastr__ = __webpack_require__("./node_modules/vue-toastr/dist/vue-toastr.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_1_vue_toastr__);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2_vue__ = __webpack_require__("./node_modules/vue/dist/vue.common.js");
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_2_vue___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_2_vue__);
// lodash handles our translations
@ -34383,11 +34385,13 @@ Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// Import toastr scss file: need webpack sass-loader
__webpack_require__("./node_modules/vue-toastr/src/vue-toastr.scss");
// Register vue component
Vue.component('vue-toastr', __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default.a);
__WEBPACK_IMPORTED_MODULE_2_vue___default.a.component('vue-toastr', __WEBPACK_IMPORTED_MODULE_1_vue_toastr___default.a);
// Global translation helper
Vue.prototype.trans = function (string) {
__WEBPACK_IMPORTED_MODULE_2_vue___default.a.prototype.trans = function (string) {
return __WEBPACK_IMPORTED_MODULE_0_lodash__["get"](i18n, string);
};
@ -34397,7 +34401,7 @@ window.Vue = __webpack_require__("./node_modules/vue/dist/vue.common.js");
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/* Development only*/
Vue.config.devtools = true;
__WEBPACK_IMPORTED_MODULE_2_vue___default.a.config.devtools = true;
window.axios.defaults.headers.common = {
'X-Requested-With': 'XMLHttpRequest',

21282
public/js/client_list.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,8 @@ import Toastr from 'vue-toastr';
// Import toastr scss file: need webpack sass-loader
require('vue-toastr/src/vue-toastr.scss');
import Vue from 'vue';
// Register vue component
Vue.component('vue-toastr',Toastr);

View File

@ -1,13 +1,22 @@
//import * as Vue from 'vue';
require('../bootstrap');
/* Must be declare in every child view*/
declare var i18n;
import Vue from 'vue';
import axios from 'axios';
Vue.component('client-list', require('../components/client/ClientList.vue'));
Vue.component('client-actions', require('../components/client/ClientActions.vue'));
Vue.component('vuetable', require('vuetable-2/src/components/Vuetable'));
Vue.component('vuetable-pagination', require('vuetable-2/src/components/VuetablePagination'));
Vue.component('vuetable-pagination-bootstrap', require('../components/util/VuetablePaginationBootstrap'));
Vue.component('vuetable-filter-bar', require('../components/util/VuetableFilterBar'));
Vue.component('vuetable-query-filter', require('../components/client/ClientFilters.vue'));
Vue.component('vuetable-multi-select', require('../components/util/VuetableMultiSelect.vue'));
Vue.component('list-actions', require('../components/util/VueListActions.vue'));
window.onload = function () {

View File

@ -0,0 +1,23 @@
<template>
<div>
<vuetable-filter-bar></vuetable-filter-bar>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default {
mounted() {
console.dir('loaded');
}
}
</script>
<style type="text/css">
</style>

View File

@ -1,12 +1,11 @@
<template>
<div>
<vuetable-filter-bar></vuetable-filter-bar>
<vuetable ref="vuetable"
api-url="/clients"
:fields="fields"
:per-page="20"
:per-page="perPage"
:sort-order="sortOrder"
:append-params="moreParams"
:css="css.table"
@ -39,76 +38,27 @@ import VuetableCss from '../util/VuetableCss'
Vue.use(VueEvents)
export default {
components: {
Vuetable,
VuetablePagination,
VuetablePaginationInfo
},
data () {
data: function () {
return {
css: VuetableCss,
sortOrder: [
{
field: 'name',
sortField: 'name',
direction: 'asc'
}
],
perPage: this.datatable.per_page,
sortOrder: this.datatable.sort_order,
moreParams: {},
fields: [
{
name: '__checkbox', // <----
title: '',
titleClass: 'center aligned',
dataClass: 'center aligned'
},
{
name: 'name',
sortField: 'name',
dataClass: 'center aligned'
},
{
name: 'contact',
sortField: 'contact',
dataClass: 'center aligned'
},
{
name: 'email',
sortField: 'email',
dataClass: 'center aligned'
},
{
name: 'client_created_at',
title: 'Date created',
sortField: 'client_created_at',
dataClass: 'center aligned'
},
{
name: 'last_login',
title: 'Last login',
sortField: 'last_login',
dataClass: 'center aligned'
},
{
name: 'balance',
sortField: 'balance',
dataClass: 'center aligned'
},
{
name: '__component:client-actions', // <----
title: '',
titleClass: 'center aligned',
dataClass: 'center aligned'
}
]
fields: this.datatable.fields
}
},
//props: ['list'],
props: ['datatable'],
mounted() {
this.$events.$on('filter-set', eventData => this.onFilterSet(eventData))
this.$events.$on('filter-reset', e => this.onFilterReset())
console.dir(this.datatable)
},
beforeMount: function () {
@ -144,7 +94,8 @@ export default {
}
</script>
<style>
<style type="text/css">
.pagination {
margin: 0;
float: right;
@ -180,4 +131,12 @@ export default {
.pagination-info {
float: left;
}
th {
background: #777777;
color: #fff;
}
.sortable th i:hover {
color: #fff;
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<div class="d-flex justify-content-start">
<div class="p-2">
<div class="btn-group">
<button type="button" class="btn btn-primary btn-lg">Archive</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(81px, 38px, 0px);">
<a class="dropdown-item" href="#">Archive</a>
<a class="dropdown-item" href="#">Delete</a>
</div>
</div>
</div>
<div class="mr-auto p-2">
<vuetable-multi-select></vuetable-multi-select>
</div>
<div class="ml-auto p-2">
<vuetable-query-filter></vuetable-query-filter>
</div>
<div class="p-2">
<button class="btn btn-primary btn-lg ">{{ trans('texts.new_client') }}</button>
</div>
</div>
</template>
<script lang="ts">
</script>
<style>
</style>

View File

@ -1,6 +1,6 @@
export default {
table: {
tableClass: 'table table-bordered table-hover',
tableClass: 'table table-striped table-hover',
loadingClass: 'loading',
ascendingIcon: 'fa fa-angle-double-up',
descendingIcon: 'fa fa-angle-double-down',

View File

@ -1,15 +1,11 @@
<template>
<div class="container">
<div class="row">
<div class="col-md-9" style="padding:10px;">
<div class="input-group">
<input type="text" v-model="filterText" class="form-control" @keyup.enter="doFilter" placeholder="search">
<button class="btn btn-primary" @click="doFilter">Go</button>
<button class="btn btn-light" @click="resetFilter">Reset</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
@ -38,4 +34,7 @@
.form-inline > * {
margin:5px 10px;
}
.form-control {
min-height: 40px;
}
</style>

View File

@ -0,0 +1,42 @@
<!-- Vue component -->
<template>
<div style="width:300px;">
<multiselect v-model="value"
:options="options"
:multiple="true"
:placeholder="trans('texts.status')"
@input="onChange"
></multiselect>
</div>
</template>
<script lang="ts">
import Multiselect from 'vue-multiselect'
export default {
// OR register locally
components: { Multiselect },
data () {
return {
value: 'active',
options: ['active', 'archived', 'deleted']
}
},
methods: {
onChange (value) {
console.dir(this.value)
this.value = value
if (value.indexOf('Reset me!') !== -1) this.value = []
},
onSelect (option) {
if (option === 'Disable me!') this.isDisabled = true
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style>
</style>

View File

@ -1,4 +1,4 @@
@extends('layouts.master', ['header' => $header])
@extends('layouts.master', ['header' => $header])
@section('body')
<main class="main" id="client_create">
@ -9,7 +9,7 @@
<form @submit.prevent="onSubmit" @keydown="form.errors.clear($event.target.name)">
<div class="container-fluid">
<vue-toastr ref="toastr"></vue-toastr>
<vue-toastr ref="toastr"></vue-toastr>
<div class="row">
<!-- Client Details and Address Column -->

View File

@ -10,23 +10,18 @@
<!-- Breadcrumb-->
{{ Breadcrumbs::render('clients') }}
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<button class="btn btn-primary btn-lg pull-right">{{ trans('texts.new_client') }}</button>
</div>
<div class="container-fluid" id="client_list">
<list-actions></list-actions>
<div style="background: #fff;">
<client-list :datatable="{{ $datatable }}"></client-list>
</div>
<div id="client_list" style="padding-top:20px;">
<div class="animated fadeIn">
<div class="col-md-12 card">
<client-list></client-list>
</div>
</div>
</div>
</div>
</main>
<script defer src=" {{ mix('/js/client_list.min.js') }}"></script>

View File

@ -42,11 +42,17 @@ Route::group(['middleware' => ['auth:user', 'db']], function () {
Route::resource('dashboard', 'DashboardController'); // name = (dashboard. index / create / show / update / destroy / edit
Route::get('logout', 'Auth\LoginController@logout')->name('user.logout');
Route::resource('invoices', 'InvoiceController'); // name = (invoices. index / create / show / update / destroy / edit
Route::post('invoices/bulk', 'InvoiceController@bulk')->name('invoices.bulk');
Route::resource('clients', 'ClientController'); // name = (clients. index / create / show / update / destroy / edit
Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk');
Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit
Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk');
Route::resource('payments', 'PaymentController'); // name = (payments. index / create / show / update / destroy / edit
Route::post('payments/bulk', 'PaymentController@bulk')->name('payments.bulk');
Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit
Route::post('credits/bulk', 'CreditController@bulk')->name('credits.bulk');
Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit
Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk');
Route::resource('user', 'UserProfileController'); // name = (clients. index / create / show / update / destroy / edit
Route::get('settings', 'SettingsController@index')->name('user.settings');