1
0
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:
David Bomba 2019-01-16 09:00:25 +11:00 committed by GitHub
parent 9204510193
commit feafbd9826
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 6831 additions and 2824 deletions

View 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
]
]
];
}
}

View File

@ -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'
]
]

View File

@ -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');
}
}

View File

@ -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
View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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,
]);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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');
}
};

View File

@ -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

File diff suppressed because it is too large Load Diff

1951
public/js/coreui.min.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ import Vue from 'vue'
export default {
mounted() {
console.dir('loaded');
}
}

View File

@ -135,8 +135,5 @@ export default {
background: #777777;
color: #fff;
}
.sortable th i:hover {
color: #fff;
}
</style>

View 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);
}
}

View 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');
}
}

View 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);
}
}