mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-09 20:52:56 +01:00
User Settings (#2601)
* Datamapping JSON Settings * JSON Mapping * User Setting Defaults * Testing Json Mapper * Implemented User Settings - hydrated from JSON format
This commit is contained in:
parent
9204510193
commit
feafbd9826
40
app/DataMapper/DefaultSettings.php
Normal file
40
app/DataMapper/DefaultSettings.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\DataMapper;
|
||||
|
||||
use App\Models\Client;
|
||||
|
||||
class DefaultSettings
|
||||
{
|
||||
|
||||
public static $per_page = 20;
|
||||
|
||||
public static function userSettings() : \stdClass
|
||||
{
|
||||
return (object)[
|
||||
class_basename(Client::class) => self::clientSettings(),
|
||||
];
|
||||
}
|
||||
|
||||
private static function clientSettings() : \stdClass
|
||||
{
|
||||
|
||||
return (object)[
|
||||
'datatable' => (object) [
|
||||
'per_page' => self::$per_page,
|
||||
'column_visibility' => (object)[
|
||||
'__checkbox' => true,
|
||||
'name' => true,
|
||||
'contact' => true,
|
||||
'email' => true,
|
||||
'client_created_at' => true,
|
||||
'last_login' => true,
|
||||
'balance' => true,
|
||||
'__component:client-actions' => true
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,7 @@ use App\Models\Client;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\Traits\UserSessionAttributes;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@ -157,8 +158,11 @@ class ClientDatatable extends EntityDatatable
|
||||
|
||||
}
|
||||
|
||||
public function buildOptions()
|
||||
public function buildOptions() : Collection
|
||||
{
|
||||
|
||||
$visible = auth()->user()->getColumnVisibility(Client::class);
|
||||
|
||||
return collect([
|
||||
'per_page' => 20,
|
||||
'sort_order' => [
|
||||
@ -170,53 +174,59 @@ class ClientDatatable extends EntityDatatable
|
||||
],
|
||||
'fields' => [
|
||||
[
|
||||
'name' => '__checkbox', // <----
|
||||
'name' => '__checkbox',
|
||||
'title' => '',
|
||||
'titleClass' => 'center aligned',
|
||||
'visible' => $visible->__checkbox,
|
||||
'dataClass' => 'center aligned'
|
||||
],
|
||||
[
|
||||
'name' => 'name',
|
||||
'title' => trans('texts.name'),
|
||||
'sortField' => 'name',
|
||||
'visible' => false,
|
||||
'visible' => $visible->name,
|
||||
'dataClass' => 'center aligned'
|
||||
],
|
||||
[
|
||||
'name' => 'contact',
|
||||
'title' => trans('texts.contact'),
|
||||
'sortField' => 'contact',
|
||||
'visible' => false,
|
||||
'visible' => $visible->contact,
|
||||
'dataClass' => 'center aligned'
|
||||
],
|
||||
[
|
||||
'name' => 'email',
|
||||
'title' => trans('texts.email'),
|
||||
'sortField' => 'email',
|
||||
'visible' => $visible->email,
|
||||
'dataClass' => 'center aligned'
|
||||
],
|
||||
[
|
||||
'name' => 'client_created_at',
|
||||
'title' => trans('texts.date_created'),
|
||||
'sortField' => 'client_created_at',
|
||||
'visible' => $visible->client_created_at,
|
||||
'dataClass' => 'center aligned'
|
||||
],
|
||||
[
|
||||
'name' => 'last_login',
|
||||
'title' => trans('texts.last_login'),
|
||||
'sortField' => 'last_login',
|
||||
'visible' => $visible->last_login,
|
||||
'dataClass' => 'center aligned'
|
||||
],
|
||||
[
|
||||
'name' => 'balance',
|
||||
'title' => trans('texts.balance'),
|
||||
'sortField' => 'balance',
|
||||
'visible' => $visible->balance,
|
||||
'dataClass' => 'center aligned'
|
||||
],
|
||||
[
|
||||
'name' => '__component:client-actions',
|
||||
'title' => '',
|
||||
'titleClass' => 'center aligned',
|
||||
'visible' => true,
|
||||
'dataClass' => 'center aligned'
|
||||
]
|
||||
]
|
||||
|
@ -6,6 +6,16 @@ class CompanyUser extends BaseModel
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'settings' => 'collection',
|
||||
];
|
||||
|
||||
public function account()
|
||||
{
|
||||
return $this->hasOne(Account::class);
|
||||
@ -13,11 +23,11 @@ class CompanyUser extends BaseModel
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->hasOne(User::class)->withPivot('permissions');
|
||||
return $this->hasOne(User::class)->withPivot('permissions', 'settings');
|
||||
}
|
||||
|
||||
public function company()
|
||||
{
|
||||
return $this->hasOne(Company::class)->withPivot('permissions');
|
||||
return $this->hasOne(Company::class)->withPivot('permissions', 'settings');
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Models\Traits\UserTrait;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Utils\Traits\UserSessionAttributes;
|
||||
use App\Utils\Traits\UserSettings;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@ -18,7 +19,8 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
use PresentableTrait;
|
||||
use MakesHash;
|
||||
use UserSessionAttributes;
|
||||
|
||||
use UserSettings;
|
||||
|
||||
protected $guard = 'user';
|
||||
|
||||
protected $dates = ['deleted_at'];
|
||||
@ -55,16 +57,31 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'slack_webhook_url',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns all companies a user has access to.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function companies()
|
||||
{
|
||||
return $this->belongsToMany(Company::class)->withPivot('permissions');
|
||||
return $this->belongsToMany(Company::class)->withPivot('permissions','settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current company
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function company()
|
||||
{
|
||||
return $this->companies()->where('company_id', $this->getCurrentCompanyId())->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a object of user permissions
|
||||
*
|
||||
* @return stdClass
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
|
||||
@ -76,26 +93,63 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
return $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a object of User Settings
|
||||
*
|
||||
* @return stdClass
|
||||
*/
|
||||
public function settings()
|
||||
{
|
||||
return json_decode($this->company()->pivot->settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean of the administrator status of the user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_admin()
|
||||
{
|
||||
return $this->company()->pivot->is_admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all user created contacts
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function contacts()
|
||||
{
|
||||
return $this->hasMany(Contact::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean value if the user owns the current Entity
|
||||
*
|
||||
* @param string Entity
|
||||
* @return bool
|
||||
*/
|
||||
public function owns($entity) : bool
|
||||
{
|
||||
return ! empty($entity->user_id) && $entity->user_id == $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens a stdClass representation of the User Permissions
|
||||
* into a Collection
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function permissionsFlat()
|
||||
{
|
||||
return collect($this->permissions())->flatten();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a array of permission for the mobile application
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function permissionsMap()
|
||||
{
|
||||
|
||||
@ -104,4 +158,5 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
|
||||
return array_combine($keys, $values);
|
||||
}
|
||||
|
||||
}
|
||||
|
10
composer.lock
generated
10
composer.lock
generated
@ -1761,16 +1761,16 @@
|
||||
},
|
||||
{
|
||||
"name": "opis/closure",
|
||||
"version": "3.1.3",
|
||||
"version": "3.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opis/closure.git",
|
||||
"reference": "5e9095ce871a425ab87a854b285b7766de38a7d9"
|
||||
"reference": "41f5da65d75cf473e5ee582df8fc7f2c733ce9d6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/opis/closure/zipball/5e9095ce871a425ab87a854b285b7766de38a7d9",
|
||||
"reference": "5e9095ce871a425ab87a854b285b7766de38a7d9",
|
||||
"url": "https://api.github.com/repos/opis/closure/zipball/41f5da65d75cf473e5ee582df8fc7f2c733ce9d6",
|
||||
"reference": "41f5da65d75cf473e5ee582df8fc7f2c733ce9d6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -1818,7 +1818,7 @@
|
||||
"serialization",
|
||||
"serialize"
|
||||
],
|
||||
"time": "2019-01-06T22:07:38+00:00"
|
||||
"time": "2019-01-14T14:45:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/random_compat",
|
||||
|
@ -227,7 +227,6 @@ return [
|
||||
'URL' => Illuminate\Support\Facades\URL::class,
|
||||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
'Datatables' => Yajra\Datatables\Facades\Datatables::class,
|
||||
|
||||
/*
|
||||
* Dependency Facades
|
||||
|
@ -151,6 +151,12 @@ class CreateUsersTable extends Migration
|
||||
$table->string('custom_label2')->nullable();
|
||||
$table->string('custom_value2')->nullable();
|
||||
|
||||
$table->string('custom_label3')->nullable();
|
||||
$table->string('custom_value3')->nullable();
|
||||
|
||||
$table->string('custom_label4')->nullable();
|
||||
$table->string('custom_value4')->nullable();
|
||||
|
||||
$table->string('custom_client_label1')->nullable();
|
||||
$table->string('custom_client_label2')->nullable();
|
||||
|
||||
@ -191,6 +197,7 @@ class CreateUsersTable extends Migration
|
||||
$table->unsignedInteger('account_id');
|
||||
$table->unsignedInteger('user_id')->index();
|
||||
$table->text('permissions');
|
||||
$table->text('settings');
|
||||
$table->boolean('is_owner')->default(false);
|
||||
$table->boolean('is_admin');
|
||||
$table->boolean('is_locked')->default(false); // locks user out of account
|
||||
|
@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\DataMapper\DefaultSettings;
|
||||
use App\Models\Account;
|
||||
use App\Models\Client;
|
||||
use App\Models\ClientContact;
|
||||
@ -47,11 +48,14 @@ class UsersTableSeeder extends Seeder
|
||||
'create_client'
|
||||
]);
|
||||
|
||||
$userSettings = DefaultSettings::userSettings();
|
||||
|
||||
$user->companies()->attach($company->id, [
|
||||
'account_id' => $account->id,
|
||||
'is_owner' => 1,
|
||||
'is_admin' => 1,
|
||||
'permissions' => $userPermissions->toJson(),
|
||||
'settings' => json_encode($userSettings),
|
||||
'is_locked' => 0,
|
||||
]);
|
||||
|
||||
|
913
public/js/client_create.js
vendored
913
public/js/client_create.js
vendored
File diff suppressed because it is too large
Load Diff
913
public/js/client_create.min.js
vendored
913
public/js/client_create.min.js
vendored
File diff suppressed because it is too large
Load Diff
913
public/js/client_edit.js
vendored
913
public/js/client_edit.js
vendored
File diff suppressed because it is too large
Load Diff
913
public/js/client_edit.min.js
vendored
913
public/js/client_edit.min.js
vendored
File diff suppressed because it is too large
Load Diff
3
public/js/client_list.js
vendored
3
public/js/client_list.js
vendored
@ -4326,7 +4326,7 @@ exports = module.exports = __webpack_require__("./node_modules/css-loader/lib/cs
|
||||
|
||||
|
||||
// module
|
||||
exports.push([module.i, "\n.pagination {\n margin: 0;\n float: right;\n}\n.pagination a.page {\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 10px;\n margin-right: 2px;\n}\n.pagination a.page.active {\n color: white;\n background-color: #337ab7;\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 10px;\n margin-right: 2px;\n}\n.pagination a.btn-nav {\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 7px;\n margin-right: 2px;\n}\n.pagination a.btn-nav.disabled {\n color: lightgray;\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 7px;\n margin-right: 2px;\n cursor: not-allowed;\n}\n.pagination-info {\n float: left;\n}\nth {\n background: #777777;\n color: #fff;\n}\n.sortable th i:hover {\n color: #fff;\n}\n\n", ""]);
|
||||
exports.push([module.i, "\n.pagination {\n margin: 0;\n float: right;\n}\n.pagination a.page {\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 10px;\n margin-right: 2px;\n}\n.pagination a.page.active {\n color: white;\n background-color: #337ab7;\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 10px;\n margin-right: 2px;\n}\n.pagination a.btn-nav {\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 7px;\n margin-right: 2px;\n}\n.pagination a.btn-nav.disabled {\n color: lightgray;\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 7px;\n margin-right: 2px;\n cursor: not-allowed;\n}\n.pagination-info {\n float: left;\n}\nth {\n background: #777777;\n color: #fff;\n}\n\n", ""]);
|
||||
|
||||
// exports
|
||||
|
||||
@ -22540,7 +22540,6 @@ exports.clearImmediate = (typeof self !== "undefined" && self.clearImmediate) ||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = {
|
||||
mounted: function () {
|
||||
console.dir('loaded');
|
||||
}
|
||||
};
|
||||
|
||||
|
3
public/js/client_list.min.js
vendored
3
public/js/client_list.min.js
vendored
@ -4326,7 +4326,7 @@ exports = module.exports = __webpack_require__("./node_modules/css-loader/lib/cs
|
||||
|
||||
|
||||
// module
|
||||
exports.push([module.i, "\n.pagination {\n margin: 0;\n float: right;\n}\n.pagination a.page {\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 10px;\n margin-right: 2px;\n}\n.pagination a.page.active {\n color: white;\n background-color: #337ab7;\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 10px;\n margin-right: 2px;\n}\n.pagination a.btn-nav {\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 7px;\n margin-right: 2px;\n}\n.pagination a.btn-nav.disabled {\n color: lightgray;\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 7px;\n margin-right: 2px;\n cursor: not-allowed;\n}\n.pagination-info {\n float: left;\n}\nth {\n background: #777777;\n color: #fff;\n}\n.sortable th i:hover {\n color: #fff;\n}\n\n", ""]);
|
||||
exports.push([module.i, "\n.pagination {\n margin: 0;\n float: right;\n}\n.pagination a.page {\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 10px;\n margin-right: 2px;\n}\n.pagination a.page.active {\n color: white;\n background-color: #337ab7;\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 10px;\n margin-right: 2px;\n}\n.pagination a.btn-nav {\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 7px;\n margin-right: 2px;\n}\n.pagination a.btn-nav.disabled {\n color: lightgray;\n border: 1px solid lightgray;\n border-radius: 3px;\n padding: 5px 7px;\n margin-right: 2px;\n cursor: not-allowed;\n}\n.pagination-info {\n float: left;\n}\nth {\n background: #777777;\n color: #fff;\n}\n\n", ""]);
|
||||
|
||||
// exports
|
||||
|
||||
@ -22540,7 +22540,6 @@ exports.clearImmediate = (typeof self !== "undefined" && self.clearImmediate) ||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = {
|
||||
mounted: function () {
|
||||
console.dir('loaded');
|
||||
}
|
||||
};
|
||||
|
||||
|
1951
public/js/coreui.js
vendored
1951
public/js/coreui.js
vendored
File diff suppressed because it is too large
Load Diff
1951
public/js/coreui.min.js
vendored
1951
public/js/coreui.min.js
vendored
File diff suppressed because it is too large
Load Diff
913
public/js/localization.js
vendored
913
public/js/localization.js
vendored
File diff suppressed because it is too large
Load Diff
913
public/js/localization.min.js
vendored
913
public/js/localization.min.js
vendored
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,7 @@ import Vue from 'vue'
|
||||
export default {
|
||||
|
||||
mounted() {
|
||||
console.dir('loaded');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,8 +135,5 @@ export default {
|
||||
background: #777777;
|
||||
color: #fff;
|
||||
}
|
||||
.sortable th i:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
</style>
|
36
tests/Unit/DefaultSettingsTest.php
Normal file
36
tests/Unit/DefaultSettingsTest.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\DataMapper\DefaultSettings;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @covers App\DataMapper\DefaultSettings
|
||||
*/
|
||||
class DefaultTest extends TestCase
|
||||
{
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
|
||||
parent::setUp();
|
||||
|
||||
}
|
||||
|
||||
public function testDefaultUserSettings()
|
||||
{
|
||||
$user_settings = DefaultSettings::userSettings();
|
||||
|
||||
$this->assertEquals($user_settings->Client->datatable->per_page, 20);
|
||||
}
|
||||
|
||||
public function testIsObject()
|
||||
{
|
||||
$user_settings = DefaultSettings::userSettings();
|
||||
|
||||
$this->assertInternalType('object',$user_settings->Client->datatable->column_visibility);
|
||||
|
||||
}
|
||||
}
|
17
tests/Unit/EvaluateStringTest.php
Normal file
17
tests/Unit/EvaluateStringTest.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\DataMapper\DefaultSettings;
|
||||
use App\Models\Client;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EvaluateStringTest extends TestCase
|
||||
{
|
||||
|
||||
public function testClassNameResolution()
|
||||
{
|
||||
$this->assertEquals(class_basename(Client::class), 'Client');
|
||||
}
|
||||
|
||||
}
|
58
tests/Unit/NestedCollectionTest.php
Normal file
58
tests/Unit/NestedCollectionTest.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @covers App\Utils\NumberHelper
|
||||
*/
|
||||
class NestedCollectionTest extends TestCase
|
||||
{
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$this->map = (object)[
|
||||
'client' => (object)[
|
||||
'datatable' => (object)[
|
||||
'per_page' => 20,
|
||||
'column_visibility' => (object)[
|
||||
'__checkbox' => true,
|
||||
'name' => true,
|
||||
'contact' => true,
|
||||
'email' => true,
|
||||
'client_created_at' => true,
|
||||
'last_login' => true,
|
||||
'balance' => true,
|
||||
'__component:client-actions' => true,
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testPerPageAttribute()
|
||||
{
|
||||
$this->assertEquals($this->map->client->datatable->per_page, 20);
|
||||
}
|
||||
|
||||
public function testNameAttributeVisibility()
|
||||
{
|
||||
$this->assertEquals($this->map->client->datatable->column_visibility->name, true);
|
||||
}
|
||||
|
||||
public function testStringAsEntityProperty()
|
||||
{
|
||||
$entity = 'client';
|
||||
|
||||
$this->assertEquals($this->map->{$entity}->datatable->column_visibility->name, true);
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user